okstra 0.172.0 → 0.174.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/README.md +8 -6
- package/docs/architecture/storage-model.md +24 -3
- package/docs/architecture.md +21 -35
- package/docs/cli.md +39 -7
- package/docs/container.md +1 -1
- package/docs/contributor-change-matrix.md +1 -1
- package/docs/performance-improvement-plan-v2.md +6 -5
- package/docs/project-structure-overview.md +33 -25
- package/docs/task-process/README.md +6 -4
- package/docs/task-process/error-analysis.md +2 -2
- package/docs/task-process/final-verification.md +2 -2
- package/docs/task-process/implementation-option-selection.md +70 -0
- package/docs/task-process/implementation-planning.md +24 -16
- package/docs/task-process/requirements-discovery.md +2 -2
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +1 -1
- package/runtime/agents/workers/report-writer-worker.md +30 -6
- package/runtime/bin/lib/okstra/cli.sh +5 -1
- package/runtime/bin/lib/okstra/globals.sh +2 -1
- package/runtime/bin/lib/okstra/usage.sh +3 -0
- package/runtime/bin/okstra-provider-exec.py +29 -12
- package/runtime/bin/okstra-trace-cleanup.sh +58 -129
- package/runtime/bin/okstra.sh +2 -0
- package/runtime/prompts/duties/direction-selection-worker.md +44 -0
- package/runtime/prompts/duties/planning-worker.md +12 -4
- package/runtime/prompts/lead/adapters/cmux.md +2 -0
- package/runtime/prompts/lead/context-loader.md +1 -1
- package/runtime/prompts/lead/convergence.md +5 -5
- package/runtime/prompts/lead/okstra-lead-contract.md +7 -6
- package/runtime/prompts/lead/plan-body-verification.md +23 -6
- package/runtime/prompts/lead/report-writer.md +33 -11
- package/runtime/prompts/profiles/_common-contract.md +3 -3
- package/runtime/prompts/profiles/_implementation-deliverable.md +2 -2
- package/runtime/prompts/profiles/_implementation-executor.md +2 -0
- package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
- package/runtime/prompts/profiles/error-analysis.md +4 -4
- package/runtime/prompts/profiles/final-verification.md +3 -3
- package/runtime/prompts/profiles/forbidden-actions.json +7 -0
- package/runtime/prompts/profiles/implementation-option-selection.md +35 -0
- package/runtime/prompts/profiles/implementation-planning.md +61 -46
- package/runtime/prompts/profiles/implementation.md +4 -2
- package/runtime/prompts/profiles/improvement-discovery.md +1 -1
- package/runtime/prompts/profiles/release-handoff.md +1 -1
- package/runtime/prompts/profiles/requirements-discovery.md +3 -3
- package/runtime/prompts/wizard/prompts.ko.json +9 -1
- package/runtime/python/okstra_ctl/adapters/dispatch/__init__.py +1 -6
- package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +4 -4
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +5 -0
- package/runtime/python/okstra_ctl/agent_invocation.py +1 -0
- package/runtime/python/okstra_ctl/analysis_packet.py +6 -0
- package/runtime/python/okstra_ctl/conformance.py +68 -0
- package/runtime/python/okstra_ctl/dispatch_core.py +89 -39
- package/runtime/python/okstra_ctl/dispatch_state.py +142 -14
- package/runtime/python/okstra_ctl/doctor.py +2 -2
- package/runtime/python/okstra_ctl/domain/worker_exec.py +5 -0
- package/runtime/python/okstra_ctl/exact_coverage.py +128 -0
- package/runtime/python/okstra_ctl/final_report_schema.py +5 -4
- package/runtime/python/okstra_ctl/fix_cycles.py +3 -1
- package/runtime/python/okstra_ctl/implementation_direction.py +836 -0
- package/runtime/python/okstra_ctl/implementation_options.py +479 -0
- package/runtime/python/okstra_ctl/pane_reclaim.py +13 -22
- package/runtime/python/okstra_ctl/plan_items.py +51 -3
- package/runtime/python/okstra_ctl/render.py +1 -0
- package/runtime/python/okstra_ctl/render_final_report.py +16 -19
- package/runtime/python/okstra_ctl/report_contract.py +45 -14
- package/runtime/python/okstra_ctl/report_finalize.py +68 -9
- package/runtime/python/okstra_ctl/report_html/render.py +4 -2
- package/runtime/python/okstra_ctl/report_html/router.py +4 -0
- package/runtime/python/okstra_ctl/report_html/view_models/implementation_option_selection.py +32 -0
- package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +25 -10
- package/runtime/python/okstra_ctl/report_views.py +148 -12
- package/runtime/python/okstra_ctl/run.py +393 -4
- package/runtime/python/okstra_ctl/schema_excerpt.py +1 -1
- package/runtime/python/okstra_ctl/scope_provenance.py +16 -10
- package/runtime/python/okstra_ctl/session.py +69 -12
- package/runtime/python/okstra_ctl/team.py +51 -25
- package/runtime/python/okstra_ctl/tmux.py +19 -149
- package/runtime/python/okstra_ctl/user_response.py +75 -0
- package/runtime/python/okstra_ctl/wizard.py +144 -0
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +2 -0
- package/runtime/python/okstra_ctl/worker_request.py +2 -0
- package/runtime/python/okstra_ctl/workflow.py +29 -7
- package/runtime/python/okstra_ctl/worktree.py +69 -3
- package/runtime/python/okstra_token_usage/cli.py +1 -1
- package/runtime/python/okstra_token_usage/collect.py +66 -6
- package/runtime/schemas/final-report-v2.0.schema.json +1428 -137
- package/runtime/skills/okstra-setup/references/project-config.md +11 -0
- package/runtime/templates/reports/final-report-v2.template.md +4 -0
- package/runtime/templates/reports/final-verification-input.template.md +1 -1
- package/runtime/templates/reports/html/base.template.html +3 -2
- package/runtime/templates/reports/html/i18n/en.json +21 -1
- package/runtime/templates/reports/html/i18n/ko.json +21 -1
- package/runtime/templates/reports/html/macros/forms.html +21 -2
- package/runtime/templates/reports/html/tasks/implementation-option-selection.template.html +49 -0
- package/runtime/templates/reports/html/tasks/implementation-planning.template.html +36 -2
- package/runtime/templates/reports/i18n/en.json +13 -0
- package/runtime/templates/reports/implementation-input.template.md +4 -2
- package/runtime/templates/reports/implementation-planning-input.template.md +18 -4
- package/runtime/templates/reports/improvement-discovery-input.template.md +1 -1
- package/runtime/templates/reports/md/tasks/implementation-option-selection.template.md +13 -0
- package/runtime/templates/reports/md/tasks/implementation-planning.template.md +17 -0
- package/runtime/templates/reports/report.js +111 -4
- package/runtime/templates/reports/settings.template.json +0 -24
- package/runtime/templates/reports/task-brief.template.md +9 -3
- package/runtime/templates/reports/user-response.template.md +25 -4
- package/runtime/templates/worker-prompt-preamble.md +8 -0
- package/runtime/validators/lib/fixtures.sh +49 -17
- package/runtime/validators/validate-implementation-plan-stages.py +169 -4
- package/runtime/validators/validate-report-views.py +2 -2
- package/runtime/validators/validate-run.py +149 -498
- package/runtime/validators/validate_improvement_report.py +5 -1
- package/runtime/validators/validate_session_conformance.py +1 -1
- package/src/cli-registry.mjs +8 -1
- package/src/commands/execute/codex-run.mjs +1 -0
- package/src/commands/execute/render-bundle.mjs +1 -0
- package/src/commands/execute/team.mjs +3 -3
- package/src/commands/execute/worktree-status.mjs +109 -0
- package/src/commands/lifecycle/install.mjs +0 -2
- package/src/commands/report/finalize.mjs +13 -6
- package/runtime/bin/okstra-subagent-reclaim.sh +0 -26
- package/runtime/schemas/final-report-v1.0.schema.json +0 -6366
- package/runtime/templates/reports/final-report.template.md +0 -1258
|
@@ -26,6 +26,7 @@ import tempfile
|
|
|
26
26
|
from dataclasses import dataclass, field
|
|
27
27
|
from datetime import datetime, timezone
|
|
28
28
|
from pathlib import Path
|
|
29
|
+
from typing import Sequence
|
|
29
30
|
|
|
30
31
|
from okstra_project import project_json_path, upsert_project_json
|
|
31
32
|
from okstra_project.state import slugify
|
|
@@ -48,6 +49,15 @@ from .clarification_items import (
|
|
|
48
49
|
)
|
|
49
50
|
from .error_report import prior_run_error_digest
|
|
50
51
|
from .incremental_scope import ReverifyScopeError, parse_user_reverify_scope
|
|
52
|
+
from .implementation_direction import (
|
|
53
|
+
DirectionSelectionError,
|
|
54
|
+
SelectedDirection,
|
|
55
|
+
lexical_absolute_path,
|
|
56
|
+
resolve_selected_direction,
|
|
57
|
+
validate_selected_direction_plan,
|
|
58
|
+
validate_task_artifact_path,
|
|
59
|
+
write_selected_direction_snapshot,
|
|
60
|
+
)
|
|
51
61
|
from .qa_commands import format_errors as _format_qa_errors, validate_qa_commands
|
|
52
62
|
from .material import (
|
|
53
63
|
build_analysis_material,
|
|
@@ -115,7 +125,11 @@ from .render import (
|
|
|
115
125
|
)
|
|
116
126
|
from okstra_project.dirs import okstra_home
|
|
117
127
|
|
|
118
|
-
from .dispatch_state import
|
|
128
|
+
from .dispatch_state import (
|
|
129
|
+
BACKEND_CMUX_PANE,
|
|
130
|
+
detect_terminal_backend,
|
|
131
|
+
generate_claude_session_id,
|
|
132
|
+
)
|
|
119
133
|
from .run_context import (
|
|
120
134
|
compute_and_write_run_context,
|
|
121
135
|
refresh_run_context_snapshot,
|
|
@@ -129,7 +143,6 @@ from .seeding import (
|
|
|
129
143
|
verify_installation,
|
|
130
144
|
)
|
|
131
145
|
from .session import (
|
|
132
|
-
generate_claude_session_id,
|
|
133
146
|
resolve_inproc_lead_session_id,
|
|
134
147
|
write_claude_resume_command_file,
|
|
135
148
|
)
|
|
@@ -154,6 +167,7 @@ from .brief_frontmatter import (
|
|
|
154
167
|
has_reporter_confirmation_contract,
|
|
155
168
|
read_brief_frontmatter,
|
|
156
169
|
)
|
|
170
|
+
from .scope_provenance import brief_end_state_id_sequence
|
|
157
171
|
|
|
158
172
|
# Frontmatter approval-flag matcher.
|
|
159
173
|
#
|
|
@@ -183,6 +197,10 @@ IMPLEMENTATION_OPTION_FRONTMATTER_PATTERN = re.compile(
|
|
|
183
197
|
r"^implementation-option:[ \t]*(.*)$",
|
|
184
198
|
re.MULTILINE,
|
|
185
199
|
)
|
|
200
|
+
SELECTED_DIRECTION_FRONTMATTER_PATTERN = re.compile(
|
|
201
|
+
r"^selected-direction-ref:[ \t]*(.*)$",
|
|
202
|
+
re.MULTILINE,
|
|
203
|
+
)
|
|
186
204
|
|
|
187
205
|
# validators/validate-run.py:_FRONTMATTER_BLOCK_RE 의 미러 — 선행 BOM/빈 줄을
|
|
188
206
|
# 허용해 두 모듈이 같은 리포트의 frontmatter 게이트를 동일하게 판정하게 한다.
|
|
@@ -281,6 +299,43 @@ def _validate_data_json_approval_consistency(
|
|
|
281
299
|
)
|
|
282
300
|
|
|
283
301
|
|
|
302
|
+
def _validate_approved_plan_conformance(path: Path) -> None:
|
|
303
|
+
"""승인 계획의 stage conformance 선언 형식을 승인 경계에서 판정한다.
|
|
304
|
+
|
|
305
|
+
같은 판정이 지금까지는 구현 런의 마지막 `validate-run` 에서만 나왔다. 그때는
|
|
306
|
+
워커 배치와 수렴이 이미 끝난 뒤라 런 하나를 통째로 버려야 했다 — 실측된
|
|
307
|
+
실패에서 승인 계획의 stage 2~7 이 기계 형식이 아니라 산문이었다.
|
|
308
|
+
|
|
309
|
+
선언이 없는 stage 는 건드리지 않는다(`conformance.malformed_conformance_stages`
|
|
310
|
+
참조): XOR 부재는 계획 단계의 check S11 이 이미 막고, 구현 진입 게이트도
|
|
311
|
+
validate-run 이 계속 요구한다. 여기서 다시 하면 같은 규칙이 두 곳이 된다.
|
|
312
|
+
"""
|
|
313
|
+
from .conformance import malformed_conformance_stages
|
|
314
|
+
|
|
315
|
+
loaded = _load_final_report_data_if_present(path)
|
|
316
|
+
if loaded is None:
|
|
317
|
+
return
|
|
318
|
+
data_path, data = loaded
|
|
319
|
+
bad = malformed_conformance_stages(data)
|
|
320
|
+
if not bad:
|
|
321
|
+
return
|
|
322
|
+
# 한 stage 씩 고치고 다시 막히는 왕복을 피하려면 전부 나열해야 한다 —
|
|
323
|
+
# 실패한 런에서 검증기는 stage 2 만 지목했지만 실제로는 2~7 전부였다.
|
|
324
|
+
stages = ", ".join(str(number) for number in bad)
|
|
325
|
+
raise PrepareError(
|
|
326
|
+
f"approved plan data.json has malformed conformanceTests for stage(s) "
|
|
327
|
+
f"{stages}: {data_path}\n"
|
|
328
|
+
" each declaring stage must read "
|
|
329
|
+
"`<task_root>/qa/scripts/stage-<N>.<ext> "
|
|
330
|
+
"(requires=[db|io|http|external,...])` after the "
|
|
331
|
+
"`Conformance tests: stage-<N> — ` prefix "
|
|
332
|
+
"(prompts/profiles/implementation-planning.md), or carry "
|
|
333
|
+
"`Conformance exemption: <reason>` instead.\n"
|
|
334
|
+
" re-run implementation-planning so the declaration is regenerated in "
|
|
335
|
+
"that form, then approve it."
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
|
|
284
339
|
def _set_data_json_approved_true_if_present(path: Path) -> bool:
|
|
285
340
|
loaded = _load_final_report_data_if_present(path)
|
|
286
341
|
if loaded is None:
|
|
@@ -393,6 +448,8 @@ class PrepareInputs:
|
|
|
393
448
|
# 별개 채널이다.
|
|
394
449
|
stages: str = ""
|
|
395
450
|
clarification_response_path: str = "" # absolute or empty
|
|
451
|
+
# implementation-planning 신규 실행 전용: 검증된 방향 선택 최종 보고서.
|
|
452
|
+
selected_direction_path: str = ""
|
|
396
453
|
# implementation-planning 전용: 사용자가 고른 이번 재실행의 재검증 범위.
|
|
397
454
|
# "" / "auto" = 리드의 `okstra incremental-scope` 판정에 맡김, "full" =
|
|
398
455
|
# 전체 재검증 강제, "<stage csv>" = 그 stage 들을 impacted 로 지정.
|
|
@@ -457,6 +514,7 @@ def _validate_approved_plan(path: str) -> None:
|
|
|
457
514
|
)
|
|
458
515
|
_reject_blocking_plan_body_gate(p, body, action="approved plan validation")
|
|
459
516
|
_validate_data_json_approval_consistency(p, markdown_approved=True)
|
|
517
|
+
_validate_approved_plan_conformance(p)
|
|
460
518
|
# frontmatter approved == true 상태. §1 Clarification Items 의
|
|
461
519
|
# Blocks=approval 행이 아직 open/answered 면 승인을 무효화한다.
|
|
462
520
|
scan = scan_approval_gate(p)
|
|
@@ -781,6 +839,7 @@ def _canonical_argv(inp: PrepareInputs, ctx: dict) -> list[str]:
|
|
|
781
839
|
("--approved-plan", inp.approved_plan_path),
|
|
782
840
|
("--implementation-option", inp.implementation_option),
|
|
783
841
|
("--clarification-response", inp.clarification_response_path),
|
|
842
|
+
("--selected-direction", inp.selected_direction_path),
|
|
784
843
|
("--workers", workers),
|
|
785
844
|
("--lead-provider", inp.lead_provider or ctx.get("LEAD_PROVIDER", "")),
|
|
786
845
|
("--lead-model", inp.lead_model or ctx.get("LEAD_MODEL", "")),
|
|
@@ -992,6 +1051,13 @@ def _validate_prepare_inputs(project_root: Path, inp: PrepareInputs) -> list:
|
|
|
992
1051
|
pass
|
|
993
1052
|
elif not inp.brief_path.is_file():
|
|
994
1053
|
raise PrepareError(f"task brief not found: {inp.brief_path}")
|
|
1054
|
+
elif inp.task_type == "implementation-option-selection" and not brief_end_state_id_sequence(
|
|
1055
|
+
inp.brief_path
|
|
1056
|
+
):
|
|
1057
|
+
raise PrepareError(
|
|
1058
|
+
"regenerate the brief with stable end-state IDs before starting "
|
|
1059
|
+
"implementation-option-selection"
|
|
1060
|
+
)
|
|
995
1061
|
ctx_stage_map: list = []
|
|
996
1062
|
# implementation 과 final-verification 은 둘 다 승인된 plan 의 Stage Map 을
|
|
997
1063
|
# 입력으로 받는다(전자는 실행 scope, 후자는 검증 scope).
|
|
@@ -1041,10 +1107,123 @@ def _validate_prepare_inputs(project_root: Path, inp: PrepareInputs) -> list:
|
|
|
1041
1107
|
raise PrepareError(
|
|
1042
1108
|
f"clarification response file not found: {inp.clarification_response_path}"
|
|
1043
1109
|
)
|
|
1110
|
+
_validate_planning_entry_inputs(project_root, inp)
|
|
1044
1111
|
_validate_reverify_scope(inp)
|
|
1045
1112
|
return ctx_stage_map
|
|
1046
1113
|
|
|
1047
1114
|
|
|
1115
|
+
_PLANNING_REPORT_RE = re.compile(
|
|
1116
|
+
r"^final-report-implementation-planning-\d{3,}\.md$"
|
|
1117
|
+
)
|
|
1118
|
+
|
|
1119
|
+
|
|
1120
|
+
def _resolve_planning_input_path(
|
|
1121
|
+
path_value: str, project_root: Path
|
|
1122
|
+
) -> Path | None:
|
|
1123
|
+
"""Resolve cwd-first planning inputs without following symbolic links."""
|
|
1124
|
+
raw_path = Path(path_value).expanduser()
|
|
1125
|
+
cwd_candidate = lexical_absolute_path(raw_path)
|
|
1126
|
+
if cwd_candidate.is_file():
|
|
1127
|
+
return cwd_candidate
|
|
1128
|
+
if not raw_path.is_absolute():
|
|
1129
|
+
project_candidate = lexical_absolute_path(project_root / raw_path)
|
|
1130
|
+
if project_candidate.is_file():
|
|
1131
|
+
return project_candidate
|
|
1132
|
+
return None
|
|
1133
|
+
|
|
1134
|
+
|
|
1135
|
+
def _is_existing_planning_report(
|
|
1136
|
+
path_value: str, project_root: Path, inp: PrepareInputs
|
|
1137
|
+
) -> bool:
|
|
1138
|
+
from .paths import task_dir
|
|
1139
|
+
|
|
1140
|
+
try:
|
|
1141
|
+
report = lexical_absolute_path(Path(path_value))
|
|
1142
|
+
task_root = lexical_absolute_path(
|
|
1143
|
+
task_dir(project_root, inp.task_group, inp.task_id)
|
|
1144
|
+
)
|
|
1145
|
+
relative = report.relative_to(task_root)
|
|
1146
|
+
except ValueError:
|
|
1147
|
+
return False
|
|
1148
|
+
has_planning_layout = (
|
|
1149
|
+
_PLANNING_REPORT_RE.fullmatch(report.name) is not None
|
|
1150
|
+
and relative.parts[:3] == (
|
|
1151
|
+
"runs",
|
|
1152
|
+
"implementation-planning",
|
|
1153
|
+
"reports",
|
|
1154
|
+
)
|
|
1155
|
+
and len(relative.parts) == 4
|
|
1156
|
+
)
|
|
1157
|
+
if not has_planning_layout:
|
|
1158
|
+
return False
|
|
1159
|
+
try:
|
|
1160
|
+
validate_task_artifact_path(report, task_root, "planning report")
|
|
1161
|
+
except DirectionSelectionError:
|
|
1162
|
+
return False
|
|
1163
|
+
return True
|
|
1164
|
+
|
|
1165
|
+
|
|
1166
|
+
def _validate_planning_entry_inputs(
|
|
1167
|
+
project_root: Path, inp: PrepareInputs
|
|
1168
|
+
) -> None:
|
|
1169
|
+
if inp.task_type != "implementation-planning":
|
|
1170
|
+
if inp.selected_direction_path:
|
|
1171
|
+
raise PrepareError(
|
|
1172
|
+
"--selected-direction is only meaningful with --task-type "
|
|
1173
|
+
f"implementation-planning; got {inp.task_type}"
|
|
1174
|
+
)
|
|
1175
|
+
return
|
|
1176
|
+
if inp.selected_direction_path and inp.clarification_response_path:
|
|
1177
|
+
raise PrepareError(
|
|
1178
|
+
"implementation-planning accepts either --selected-direction for a "
|
|
1179
|
+
"new plan or --clarification-response for a rerun, not both"
|
|
1180
|
+
)
|
|
1181
|
+
if inp.selected_direction_path:
|
|
1182
|
+
return
|
|
1183
|
+
if inp.clarification_response_path:
|
|
1184
|
+
if not _is_existing_planning_report(
|
|
1185
|
+
inp.clarification_response_path, project_root, inp
|
|
1186
|
+
):
|
|
1187
|
+
raise PrepareError(
|
|
1188
|
+
"implementation-planning rerun --clarification-response must point "
|
|
1189
|
+
"to an existing implementation-planning report for the same task"
|
|
1190
|
+
)
|
|
1191
|
+
return
|
|
1192
|
+
raise PrepareError(
|
|
1193
|
+
"implementation-planning requires --selected-direction for a new plan or "
|
|
1194
|
+
"--clarification-response to an existing implementation-planning report"
|
|
1195
|
+
)
|
|
1196
|
+
|
|
1197
|
+
|
|
1198
|
+
def _resolve_planning_direction(inp: PrepareInputs) -> SelectedDirection | None:
|
|
1199
|
+
if inp.task_type != "implementation-planning" or not inp.selected_direction_path:
|
|
1200
|
+
return None
|
|
1201
|
+
from .paths import task_dir
|
|
1202
|
+
|
|
1203
|
+
try:
|
|
1204
|
+
report = lexical_absolute_path(Path(inp.selected_direction_path))
|
|
1205
|
+
expected_task_root = lexical_absolute_path(
|
|
1206
|
+
task_dir(Path(inp.project_root), inp.task_group, inp.task_id)
|
|
1207
|
+
)
|
|
1208
|
+
report.relative_to(expected_task_root)
|
|
1209
|
+
except ValueError as exc:
|
|
1210
|
+
raise PrepareError(
|
|
1211
|
+
"selected direction report must belong to the same task"
|
|
1212
|
+
) from exc
|
|
1213
|
+
try:
|
|
1214
|
+
validate_task_artifact_path(
|
|
1215
|
+
report, expected_task_root, "selected direction report"
|
|
1216
|
+
)
|
|
1217
|
+
return resolve_selected_direction(
|
|
1218
|
+
report,
|
|
1219
|
+
expected_task_key=(
|
|
1220
|
+
f"{inp.project_id}:{inp.task_group}:{inp.task_id}"
|
|
1221
|
+
),
|
|
1222
|
+
)
|
|
1223
|
+
except DirectionSelectionError as exc:
|
|
1224
|
+
raise PrepareError(str(exc)) from exc
|
|
1225
|
+
|
|
1226
|
+
|
|
1048
1227
|
def _validate_reverify_scope(inp: PrepareInputs) -> None:
|
|
1049
1228
|
"""A pinned re-verification scope is only actionable on a planning re-run.
|
|
1050
1229
|
|
|
@@ -1070,8 +1249,165 @@ def _validate_reverify_scope(inp: PrepareInputs) -> None:
|
|
|
1070
1249
|
raise PrepareError(str(exc)) from exc
|
|
1071
1250
|
|
|
1072
1251
|
|
|
1252
|
+
def _implementation_plan_contract(
|
|
1253
|
+
inp: PrepareInputs,
|
|
1254
|
+
) -> tuple[str, Path, dict | None]:
|
|
1255
|
+
"""Classify an implementation plan from its sibling data.json contract."""
|
|
1256
|
+
plan_path = Path(inp.approved_plan_path)
|
|
1257
|
+
body = plan_path.read_text(encoding="utf-8", errors="replace")
|
|
1258
|
+
frontmatter = _extract_frontmatter_block(body) or ""
|
|
1259
|
+
markdown_ref_match = SELECTED_DIRECTION_FRONTMATTER_PATTERN.search(frontmatter)
|
|
1260
|
+
markdown_ref: str | None = None
|
|
1261
|
+
if markdown_ref_match is not None:
|
|
1262
|
+
markdown_ref = markdown_ref_match.group(1).strip()
|
|
1263
|
+
if markdown_ref.startswith('"'):
|
|
1264
|
+
try:
|
|
1265
|
+
markdown_ref = json.loads(markdown_ref)
|
|
1266
|
+
except json.JSONDecodeError as exc:
|
|
1267
|
+
raise PrepareError(
|
|
1268
|
+
"selected-direction plan markdown `selected-direction-ref` is "
|
|
1269
|
+
"not a valid quoted string"
|
|
1270
|
+
) from exc
|
|
1271
|
+
elif len(markdown_ref) >= 2 and markdown_ref[0] == markdown_ref[-1] == "'":
|
|
1272
|
+
markdown_ref = markdown_ref[1:-1].replace("''", "'")
|
|
1273
|
+
if not isinstance(markdown_ref, str) or not markdown_ref.strip():
|
|
1274
|
+
raise PrepareError(
|
|
1275
|
+
"selected-direction plan markdown `selected-direction-ref` must "
|
|
1276
|
+
"be a non-empty string"
|
|
1277
|
+
)
|
|
1278
|
+
loaded = _load_final_report_data_if_present(plan_path)
|
|
1279
|
+
if loaded is None:
|
|
1280
|
+
if markdown_ref_match is not None:
|
|
1281
|
+
raise PrepareError(
|
|
1282
|
+
"selected-direction plan requires its sibling data.json before "
|
|
1283
|
+
f"implementation entry: {_final_report_data_path(plan_path)}"
|
|
1284
|
+
)
|
|
1285
|
+
return "legacy", plan_path, None
|
|
1286
|
+
_, data = loaded
|
|
1287
|
+
planning = data.get("implementationPlanning")
|
|
1288
|
+
if not isinstance(planning, dict):
|
|
1289
|
+
raise PrepareError(
|
|
1290
|
+
"approved plan sibling data.json must contain an "
|
|
1291
|
+
"implementationPlanning object"
|
|
1292
|
+
)
|
|
1293
|
+
contract = planning.get("planningContract")
|
|
1294
|
+
selected_payload = any(
|
|
1295
|
+
field in planning
|
|
1296
|
+
for field in (
|
|
1297
|
+
"selectedDirectionRef",
|
|
1298
|
+
"directionRealization",
|
|
1299
|
+
"directionInvalidation",
|
|
1300
|
+
"outcome",
|
|
1301
|
+
)
|
|
1302
|
+
)
|
|
1303
|
+
if contract is None:
|
|
1304
|
+
if markdown_ref_match is not None or selected_payload:
|
|
1305
|
+
raise PrepareError(
|
|
1306
|
+
"selected-direction markers require "
|
|
1307
|
+
"implementationPlanning.planningContract=`selected-direction`; "
|
|
1308
|
+
"the markdown `selected-direction-ref` and data contract disagree"
|
|
1309
|
+
)
|
|
1310
|
+
return "legacy", plan_path, data
|
|
1311
|
+
if contract != "selected-direction":
|
|
1312
|
+
raise PrepareError(
|
|
1313
|
+
"approved plan sibling data.json has unsupported planningContract "
|
|
1314
|
+
f"{contract!r}"
|
|
1315
|
+
)
|
|
1316
|
+
if markdown_ref_match is None:
|
|
1317
|
+
raise PrepareError(
|
|
1318
|
+
"selected-direction plan markdown frontmatter must contain "
|
|
1319
|
+
"`selected-direction-ref:`"
|
|
1320
|
+
)
|
|
1321
|
+
selected_ref = planning.get("selectedDirectionRef")
|
|
1322
|
+
snapshot_ref = (
|
|
1323
|
+
selected_ref.get("snapshotPath")
|
|
1324
|
+
if isinstance(selected_ref, dict)
|
|
1325
|
+
else None
|
|
1326
|
+
)
|
|
1327
|
+
if not isinstance(markdown_ref, str) or markdown_ref != snapshot_ref:
|
|
1328
|
+
raise PrepareError(
|
|
1329
|
+
"selected-direction plan markdown `selected-direction-ref` must "
|
|
1330
|
+
"exactly match implementationPlanning.selectedDirectionRef.snapshotPath"
|
|
1331
|
+
)
|
|
1332
|
+
if IMPLEMENTATION_OPTION_FRONTMATTER_PATTERN.search(frontmatter):
|
|
1333
|
+
raise PrepareError(
|
|
1334
|
+
"selected-direction plan must not contain an `implementation-option:` "
|
|
1335
|
+
"frontmatter field"
|
|
1336
|
+
)
|
|
1337
|
+
return "selected-direction", plan_path, data
|
|
1338
|
+
|
|
1339
|
+
|
|
1340
|
+
def _validate_selected_implementation_plan(
|
|
1341
|
+
inp: PrepareInputs,
|
|
1342
|
+
plan_path: Path,
|
|
1343
|
+
data: dict,
|
|
1344
|
+
) -> None:
|
|
1345
|
+
"""Apply the Task 8 selected-direction semantics at implementation entry."""
|
|
1346
|
+
planning = data.get("implementationPlanning") or {}
|
|
1347
|
+
if planning.get("outcome") != "plan-ready":
|
|
1348
|
+
raise PrepareError(
|
|
1349
|
+
"selected-direction plan is not implementation-ready: "
|
|
1350
|
+
f"outcome={planning.get('outcome')!r}; direction-invalidated plans "
|
|
1351
|
+
"must re-enter implementation-option-selection"
|
|
1352
|
+
)
|
|
1353
|
+
summary = planning.get("coverageSummary") or {}
|
|
1354
|
+
if summary.get("coverageVerdict") != "exact":
|
|
1355
|
+
raise PrepareError(
|
|
1356
|
+
"selected-direction plan requires exact coverage before implementation"
|
|
1357
|
+
)
|
|
1358
|
+
|
|
1359
|
+
from .paths import task_dir
|
|
1360
|
+
|
|
1361
|
+
expected_task_root = lexical_absolute_path(
|
|
1362
|
+
task_dir(Path(inp.project_root), inp.task_group, inp.task_id)
|
|
1363
|
+
)
|
|
1364
|
+
try:
|
|
1365
|
+
validate_task_artifact_path(plan_path, expected_task_root, "approved plan")
|
|
1366
|
+
data_path = validate_task_artifact_path(
|
|
1367
|
+
_final_report_data_path(plan_path),
|
|
1368
|
+
expected_task_root,
|
|
1369
|
+
"approved plan data.json",
|
|
1370
|
+
)
|
|
1371
|
+
if data_path != _final_report_data_path(plan_path):
|
|
1372
|
+
raise DirectionSelectionError("approved plan data.json must be a sibling")
|
|
1373
|
+
snapshot_path = validate_task_artifact_path(
|
|
1374
|
+
expected_task_root / "instruction-set" / "selected-direction.json",
|
|
1375
|
+
expected_task_root,
|
|
1376
|
+
"selected direction snapshot",
|
|
1377
|
+
)
|
|
1378
|
+
brief_path = validate_task_artifact_path(
|
|
1379
|
+
expected_task_root / "instruction-set" / "task-brief.md",
|
|
1380
|
+
expected_task_root,
|
|
1381
|
+
"selected direction task brief",
|
|
1382
|
+
)
|
|
1383
|
+
except DirectionSelectionError as exc:
|
|
1384
|
+
raise PrepareError(f"selected-direction plan input is invalid: {exc}") from exc
|
|
1385
|
+
|
|
1386
|
+
expected_task_key = f"{inp.project_id}:{inp.task_group}:{inp.task_id}"
|
|
1387
|
+
actual_task_key = (data.get("header") or {}).get("taskKey")
|
|
1388
|
+
if actual_task_key != expected_task_key:
|
|
1389
|
+
raise PrepareError(
|
|
1390
|
+
"selected-direction plan header.taskKey must match the implementation "
|
|
1391
|
+
f"task: expected {expected_task_key!r}, got {actual_task_key!r}"
|
|
1392
|
+
)
|
|
1393
|
+
failures = validate_selected_direction_plan(data, brief_path, snapshot_path)
|
|
1394
|
+
if failures:
|
|
1395
|
+
raise PrepareError(
|
|
1396
|
+
"selected-direction plan failed implementation entry validation:\n - "
|
|
1397
|
+
+ "\n - ".join(failures)
|
|
1398
|
+
)
|
|
1399
|
+
|
|
1400
|
+
|
|
1073
1401
|
def _prepare_implementation_approved_plan(inp: PrepareInputs) -> list:
|
|
1074
1402
|
"""Apply approved-plan inputs only after canonical brief preflight succeeds."""
|
|
1403
|
+
contract, plan_path, data = _implementation_plan_contract(inp)
|
|
1404
|
+
if contract == "selected-direction":
|
|
1405
|
+
assert data is not None
|
|
1406
|
+
_validate_selected_implementation_plan(inp, plan_path, data)
|
|
1407
|
+
if inp.implementation_option:
|
|
1408
|
+
raise PrepareError(
|
|
1409
|
+
"--implementation-option is not accepted for a selected-direction plan"
|
|
1410
|
+
)
|
|
1075
1411
|
if inp.approve_plan_ack or inp.implementation_option:
|
|
1076
1412
|
with worktree_provision_mutex(
|
|
1077
1413
|
okstra_home(), inp.project_id,
|
|
@@ -1083,7 +1419,11 @@ def _prepare_implementation_approved_plan(inp: PrepareInputs) -> list:
|
|
|
1083
1419
|
_apply_cli_implementation_option(
|
|
1084
1420
|
inp.approved_plan_path, inp.implementation_option
|
|
1085
1421
|
)
|
|
1422
|
+
contract, plan_path, data = _implementation_plan_contract(inp)
|
|
1086
1423
|
_validate_approved_plan(inp.approved_plan_path)
|
|
1424
|
+
if contract == "selected-direction":
|
|
1425
|
+
assert data is not None
|
|
1426
|
+
_validate_selected_implementation_plan(inp, plan_path, data)
|
|
1087
1427
|
_validate_stage_structure(inp.approved_plan_path)
|
|
1088
1428
|
return _parse_stage_map_into_ctx(inp.approved_plan_path)
|
|
1089
1429
|
|
|
@@ -1348,9 +1688,23 @@ def _resolve_roster(inp: PrepareInputs, profile_file: Path) -> tuple[list[str],
|
|
|
1348
1688
|
validate_workers_against_profile(workers, profile_workers, optional_workers)
|
|
1349
1689
|
if not workers:
|
|
1350
1690
|
raise PrepareError(f"no workers resolved for profile: {inp.task_type}")
|
|
1691
|
+
_validate_option_selection_roster(inp.task_type, workers)
|
|
1351
1692
|
return workers, ",".join(workers)
|
|
1352
1693
|
|
|
1353
1694
|
|
|
1695
|
+
def _validate_option_selection_roster(
|
|
1696
|
+
task_type: str, workers: Sequence[str]
|
|
1697
|
+
) -> None:
|
|
1698
|
+
"""Require independent analysis from three workers before option selection."""
|
|
1699
|
+
if task_type != "implementation-option-selection":
|
|
1700
|
+
return
|
|
1701
|
+
analyser_count = sum(worker != "report-writer" for worker in workers)
|
|
1702
|
+
if analyser_count < 3:
|
|
1703
|
+
raise PrepareError(
|
|
1704
|
+
"implementation-option-selection requires at least 3 analyser workers"
|
|
1705
|
+
)
|
|
1706
|
+
|
|
1707
|
+
|
|
1354
1708
|
def _resolve_pr_template(inp: PrepareInputs) -> tuple[str, str]:
|
|
1355
1709
|
"""release-handoff 전용 PR 본문 템플릿 경로 + 출처를 해소한다 (그 외엔 빈 값)."""
|
|
1356
1710
|
if inp.task_type != "release-handoff":
|
|
@@ -1987,11 +2341,17 @@ def _write_instruction_set_sources(
|
|
|
1987
2341
|
profile_content: str,
|
|
1988
2342
|
review_material: str,
|
|
1989
2343
|
host_rules_file: Path | None,
|
|
2344
|
+
selected_direction: SelectedDirection | None,
|
|
1990
2345
|
) -> Path:
|
|
1991
2346
|
"""instruction-set 디렉터리에 profile/material/brief/clarification/directive 와
|
|
1992
2347
|
reference-expectations 를 기록하고 디렉터리 경로를 돌려준다."""
|
|
1993
2348
|
instruction_set = Path(ctx["INSTRUCTION_SET_PATH"])
|
|
1994
2349
|
instruction_set.mkdir(parents=True, exist_ok=True)
|
|
2350
|
+
if selected_direction is not None:
|
|
2351
|
+
write_selected_direction_snapshot(
|
|
2352
|
+
selected_direction,
|
|
2353
|
+
instruction_set / "selected-direction.json",
|
|
2354
|
+
)
|
|
1995
2355
|
_write_analysis_evidence_artifact(ctx, instruction_set)
|
|
1996
2356
|
_write_verification_target_artifact(inp, ctx, instruction_set)
|
|
1997
2357
|
_write_prior_run_error_digest(ctx, instruction_set)
|
|
@@ -2252,6 +2612,7 @@ def _persist_run_inputs(
|
|
|
2252
2612
|
"relatedTasks": inp.related_tasks_raw,
|
|
2253
2613
|
"approvedPlanPath": approved_plan_path,
|
|
2254
2614
|
"clarificationResponsePath": inp.clarification_response_path,
|
|
2615
|
+
"selectedDirectionPath": inp.selected_direction_path,
|
|
2255
2616
|
"analysisTarget": json.loads(ctx.get("ANALYSIS_TARGET_JSON", "{}")).get(
|
|
2256
2617
|
"requestedValue", ""
|
|
2257
2618
|
),
|
|
@@ -2796,6 +3157,7 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
2796
3157
|
inp.brief_path,
|
|
2797
3158
|
assets.brief_validator,
|
|
2798
3159
|
)
|
|
3160
|
+
selected_direction = _resolve_planning_direction(inp)
|
|
2799
3161
|
if inp.task_type == "implementation":
|
|
2800
3162
|
ctx_stage_map = _prepare_implementation_approved_plan(inp)
|
|
2801
3163
|
|
|
@@ -2929,6 +3291,10 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
2929
3291
|
relative_to_project_root(Path(inp.clarification_response_path), project_root)
|
|
2930
3292
|
if inp.clarification_response_path else ""
|
|
2931
3293
|
)
|
|
3294
|
+
selected_direction_relative = (
|
|
3295
|
+
relative_to_project_root(Path(inp.selected_direction_path), project_root)
|
|
3296
|
+
if inp.selected_direction_path else ""
|
|
3297
|
+
)
|
|
2932
3298
|
|
|
2933
3299
|
forbidden_by_phase = load_phase_forbidden(workspace_root)
|
|
2934
3300
|
|
|
@@ -2946,6 +3312,8 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
2946
3312
|
"CLAUDE_SESSION_ID": claude_session_id,
|
|
2947
3313
|
"CLARIFICATION_RESPONSE_PATH": inp.clarification_response_path,
|
|
2948
3314
|
"CLARIFICATION_RESPONSE_RELATIVE_PATH": clarification_relative,
|
|
3315
|
+
"SELECTED_DIRECTION_PATH": inp.selected_direction_path,
|
|
3316
|
+
"SELECTED_DIRECTION_RELATIVE_PATH": selected_direction_relative,
|
|
2949
3317
|
**_reverify_scope_ctx(inp.reverify_scope),
|
|
2950
3318
|
"BRIEF_FILE_PATH": str(inp.brief_path),
|
|
2951
3319
|
"BRIEF_RELATIVE_PATH": brief_relative,
|
|
@@ -2978,7 +3346,12 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
2978
3346
|
|
|
2979
3347
|
# ---- write instruction-set scaffolding ----
|
|
2980
3348
|
instruction_set = _write_instruction_set_sources(
|
|
2981
|
-
inp,
|
|
3349
|
+
inp,
|
|
3350
|
+
ctx,
|
|
3351
|
+
profile_content,
|
|
3352
|
+
review_material,
|
|
3353
|
+
assets.host_rules_file,
|
|
3354
|
+
selected_direction,
|
|
2982
3355
|
)
|
|
2983
3356
|
# ---- run-inputs persistence ----
|
|
2984
3357
|
_persist_run_inputs(inp, ctx, models, selected_reviewers, brief_relative)
|
|
@@ -3134,6 +3507,7 @@ def main(argv: list[str]) -> int:
|
|
|
3134
3507
|
),
|
|
3135
3508
|
)
|
|
3136
3509
|
p.add_argument("--clarification-response", default="", dest="clarification_response_path")
|
|
3510
|
+
p.add_argument("--selected-direction", default="", dest="selected_direction_path")
|
|
3137
3511
|
p.add_argument(
|
|
3138
3512
|
"--reverify-scope",
|
|
3139
3513
|
default="",
|
|
@@ -3212,7 +3586,9 @@ def main(argv: list[str]) -> int:
|
|
|
3212
3586
|
return 1
|
|
3213
3587
|
clarification_abs = ""
|
|
3214
3588
|
if args.clarification_response_path:
|
|
3215
|
-
cr =
|
|
3589
|
+
cr = _resolve_planning_input_path(
|
|
3590
|
+
args.clarification_response_path, project_root
|
|
3591
|
+
)
|
|
3216
3592
|
if cr is None:
|
|
3217
3593
|
print(
|
|
3218
3594
|
f"clarification response file not found: {args.clarification_response_path}",
|
|
@@ -3220,6 +3596,18 @@ def main(argv: list[str]) -> int:
|
|
|
3220
3596
|
)
|
|
3221
3597
|
return 1
|
|
3222
3598
|
clarification_abs = str(cr)
|
|
3599
|
+
selected_direction_abs = ""
|
|
3600
|
+
if args.selected_direction_path:
|
|
3601
|
+
selected = _resolve_planning_input_path(
|
|
3602
|
+
args.selected_direction_path, project_root
|
|
3603
|
+
)
|
|
3604
|
+
if selected is None:
|
|
3605
|
+
print(
|
|
3606
|
+
f"selected direction report not found: {args.selected_direction_path}",
|
|
3607
|
+
file=__import__("sys").stderr,
|
|
3608
|
+
)
|
|
3609
|
+
return 1
|
|
3610
|
+
selected_direction_abs = str(selected)
|
|
3223
3611
|
|
|
3224
3612
|
inputs = PrepareInputs(
|
|
3225
3613
|
workspace_root=Path(args.workspace_root).resolve(),
|
|
@@ -3254,6 +3642,7 @@ def main(argv: list[str]) -> int:
|
|
|
3254
3642
|
stage=args.stage,
|
|
3255
3643
|
stages=args.stages,
|
|
3256
3644
|
clarification_response_path=clarification_abs,
|
|
3645
|
+
selected_direction_path=selected_direction_abs,
|
|
3257
3646
|
reverify_scope=args.reverify_scope,
|
|
3258
3647
|
pr_template_path=args.pr_template_path,
|
|
3259
3648
|
render_only=args.render_only,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Build a task-type-scoped excerpt of the final-report schema.
|
|
2
2
|
|
|
3
|
-
The full schema (``schemas/final-report-
|
|
3
|
+
The full schema (``schemas/final-report-v2.0.schema.json``) carries the
|
|
4
4
|
deliverable property blocks for ALL task-types (``errorAnalysis``, the three
|
|
5
5
|
read-only analysis blocks, ``implementationPlanning``, ``releaseHandoff``,
|
|
6
6
|
``implementation``, and ``finalVerification``) plus a
|
|
@@ -21,7 +21,7 @@ CONTRACT_RULES = frozenset(
|
|
|
21
21
|
)
|
|
22
22
|
|
|
23
23
|
# Mirrors the report schema's requirement id pattern
|
|
24
|
-
# (schemas/final-report-
|
|
24
|
+
# (schemas/final-report-v2.0.schema.json: `^R-\d{3,}$`).
|
|
25
25
|
_REQ_ID_RE = re.compile(r"^R-\d{3,}$")
|
|
26
26
|
_BRIEF_RE = re.compile(r"^brief:\s*(?P<heading>.+?)\s*$")
|
|
27
27
|
_DERIVED_RE = re.compile(r"^derived:\s*(?P<parent>\S+)\s*[—-]\s*(?P<reason>.+?)\s*$")
|
|
@@ -89,8 +89,8 @@ def brief_headings(brief_path: Path) -> set[str]:
|
|
|
89
89
|
return headings
|
|
90
90
|
|
|
91
91
|
|
|
92
|
-
def
|
|
93
|
-
"""End-state ids the brief declares,
|
|
92
|
+
def brief_end_state_id_sequence(brief_path: Path) -> tuple[str, ...]:
|
|
93
|
+
"""End-state ids the brief declares, preserving section and item order.
|
|
94
94
|
|
|
95
95
|
The reading rules mirror validators/validate-brief.py exactly, because that
|
|
96
96
|
file checks the very same lines for their format and the two readers must
|
|
@@ -106,16 +106,17 @@ def brief_end_state_ids(brief_path: Path) -> set[str]:
|
|
|
106
106
|
accepted — is the worse failure: the brief passes, the id is absent from the
|
|
107
107
|
declared set, and no phase is ever asked to account for it.
|
|
108
108
|
|
|
109
|
-
An empty
|
|
110
|
-
the downstream conditional gate keys on: a legacy brief keeps the
|
|
111
|
-
path instead of being wedged by a contract it was never written
|
|
109
|
+
An empty sequence means the brief predates the end-state sections, which is
|
|
110
|
+
what the downstream conditional gate keys on: a legacy brief keeps the
|
|
111
|
+
legacy path instead of being wedged by a contract it was never written
|
|
112
|
+
against.
|
|
112
113
|
"""
|
|
113
114
|
try:
|
|
114
115
|
text = _HTML_COMMENT_RE.sub("", Path(brief_path).read_text(encoding="utf-8"))
|
|
115
116
|
except OSError:
|
|
116
|
-
return
|
|
117
|
+
return ()
|
|
117
118
|
|
|
118
|
-
ids:
|
|
119
|
+
ids: list[str] = []
|
|
119
120
|
section = ""
|
|
120
121
|
for line in text.splitlines():
|
|
121
122
|
if m := _SECTION_HEADING_RE.match(line):
|
|
@@ -125,8 +126,13 @@ def brief_end_state_ids(brief_path: Path) -> set[str]:
|
|
|
125
126
|
if not section:
|
|
126
127
|
continue
|
|
127
128
|
if m := _END_STATE_BULLET_RE.match(line.strip()):
|
|
128
|
-
ids.
|
|
129
|
-
return ids
|
|
129
|
+
ids.append(m.group("id"))
|
|
130
|
+
return tuple(ids)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def brief_end_state_ids(brief_path: Path) -> set[str]:
|
|
134
|
+
"""End-state ids the brief declares, across all three sections."""
|
|
135
|
+
return set(brief_end_state_id_sequence(brief_path))
|
|
130
136
|
|
|
131
137
|
|
|
132
138
|
@dataclass(frozen=True)
|