okstra 0.225.0 → 0.226.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/cli-registry.mjs +6 -0
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/execute/render-bundle.mjs +5 -0
- package/dist/commands/execute/render-bundle.mjs.map +1 -1
- package/dist/commands/pr/pr.mjs +42 -13
- package/dist/commands/pr/pr.mjs.map +1 -1
- package/docs/cli.md +19 -3
- package/docs/project-structure-overview.md +2 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/bin/lib/okstra/cli.sh +5 -1
- package/runtime/bin/lib/okstra/globals.sh +1 -0
- package/runtime/bin/lib/okstra/usage.sh +3 -0
- package/runtime/bin/okstra.sh +2 -0
- package/runtime/prompts/duties/code-reviewer.json +7 -4
- package/runtime/prompts/duties/technical-verification-worker.json +2 -1
- package/runtime/prompts/lead/convergence.md +4 -1
- package/runtime/prompts/lead/phase-routing.md +7 -3
- package/runtime/prompts/lead/team-contract.md +1 -1
- package/runtime/prompts/wizard/prompts.ko.json +15 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +5 -0
- package/runtime/python/okstra_ctl/analysis_packet.py +108 -2
- package/runtime/python/okstra_ctl/approved_plan_stage_extract.py +206 -0
- package/runtime/python/okstra_ctl/brief_sources.py +200 -0
- package/runtime/python/okstra_ctl/business_flow/store.py +15 -2
- package/runtime/python/okstra_ctl/cmux.py +22 -5
- package/runtime/python/okstra_ctl/code_review_coverage.py +26 -2
- package/runtime/python/okstra_ctl/convergence_critic_prompt.py +4 -1
- package/runtime/python/okstra_ctl/design_surfaces.py +19 -7
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +20 -18
- package/runtime/python/okstra_ctl/implementation_direction.py +10 -0
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +41 -0
- package/runtime/python/okstra_ctl/manager_split.py +185 -27
- package/runtime/python/okstra_ctl/next_phase.py +3 -1
- package/runtime/python/okstra_ctl/phases/final_verification/prior_blockers.py +128 -0
- package/runtime/python/okstra_ctl/phases/final_verification/profile.md +8 -5
- package/runtime/python/okstra_ctl/phases/final_verification/report_assets/final-verification.template.html +1 -1
- package/runtime/python/okstra_ctl/phases/final_verification/validation.py +84 -0
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-executor.md +1 -1
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/authoring.py +13 -5
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/boundary.json +1 -1
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/entry.py +67 -0
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/profile.md +4 -2
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/report_assets/implementation-option-selection.template.html +2 -1
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/validation.py +127 -18
- package/runtime/python/okstra_ctl/phases/implementation_planning/profile.md +2 -1
- package/runtime/python/okstra_ctl/phases/technical_verification/profile.md +2 -2
- package/runtime/python/okstra_ctl/report_assembly.py +23 -2
- package/runtime/python/okstra_ctl/report_views.py +2 -1
- package/runtime/python/okstra_ctl/run.py +46 -1
- package/runtime/python/okstra_ctl/run_context.py +13 -8
- package/runtime/python/okstra_ctl/wizard/confirmation.py +6 -0
- package/runtime/python/okstra_ctl/wizard/ids.py +6 -0
- package/runtime/python/okstra_ctl/wizard/registry.py +21 -0
- package/runtime/python/okstra_ctl/wizard/render.py +4 -0
- package/runtime/python/okstra_ctl/wizard/sources.py +33 -11
- package/runtime/python/okstra_ctl/wizard/state.py +4 -0
- package/runtime/python/okstra_ctl/wizard/steps_options.py +74 -9
- package/runtime/python/okstra_ctl/worker_prompt_body.py +21 -1
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +11 -4
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +3 -0
- package/runtime/schemas/final-report-v2.0.schema.json +74 -0
- package/runtime/schemas/final-report-v3.0.schema.json +99 -2
- package/runtime/skills/okstra-brief-gen/SKILL.md +26 -0
- package/runtime/skills/okstra-code-review/SKILL.md +2 -0
- package/runtime/skills/okstra-code-review/references/census-rules.md +16 -2
- package/runtime/skills/okstra-code-review/references/review-calibration.md +7 -1
- package/runtime/skills/okstra-manager/SKILL.md +15 -2
- package/runtime/skills/okstra-pr-gen/SKILL.md +15 -1
- package/runtime/templates/reports/brief.template.md +10 -1
- package/runtime/templates/reports/html/i18n/en.json +8 -0
- package/runtime/templates/reports/html/i18n/ko.json +8 -0
- package/runtime/templates/worker-prompt-preamble.md +1 -1
- package/runtime/validators/validate-brief.py +39 -1
- package/runtime/validators/validate-run.py +5 -0
|
@@ -55,7 +55,7 @@ from .phases.implementation_planning.entry import (
|
|
|
55
55
|
_validate_reverify_scope,
|
|
56
56
|
)
|
|
57
57
|
from . import fix_cycles
|
|
58
|
-
from .analysis_packet import build_analysis_packet
|
|
58
|
+
from .analysis_packet import build_analysis_packet, cited_documents_block
|
|
59
59
|
from .mcp_servers import NO_MCP_SERVERS_LINE, prepare_mcp
|
|
60
60
|
from .coverage_census import (
|
|
61
61
|
CensusError,
|
|
@@ -236,6 +236,7 @@ from .locks import worktree_provision_mutex
|
|
|
236
236
|
from . import stage_targets as _stage_targets
|
|
237
237
|
from .phases.catalog import PhaseAssetError, profile_markdown, resolve_include
|
|
238
238
|
from .phases.final_verification.target import acquire_final_verification_target
|
|
239
|
+
from .phases.final_verification.prior_blockers import prior_blockers_packet_block
|
|
239
240
|
from .phases.final_verification.entry import (
|
|
240
241
|
apply_verification_target,
|
|
241
242
|
reject_prepare_flags,
|
|
@@ -534,6 +535,7 @@ def _set_data_json_approved_true_if_present(path: Path) -> bool:
|
|
|
534
535
|
# 가 whole-task 통합 실패를 동일 타입으로 받는다. run.py 는 재노출만 한다.
|
|
535
536
|
PrepareError = _stage_targets.PrepareError
|
|
536
537
|
from .phases.implementation_option_selection.entry import (
|
|
538
|
+
selection_mode_packet_block,
|
|
537
539
|
validate_brief_requirements,
|
|
538
540
|
)
|
|
539
541
|
|
|
@@ -607,6 +609,9 @@ class PrepareInputs:
|
|
|
607
609
|
clarification_response_path: str = "" # absolute or empty
|
|
608
610
|
# implementation-planning 신규 실행 전용: 검증된 방향 선택 최종 보고서.
|
|
609
611
|
selected_direction_path: str = ""
|
|
612
|
+
# implementation-option-selection 전용: 리드가 미리 정한 방향과 그 근거.
|
|
613
|
+
# 값이 있으면 이 run 은 `preselected-validation`, 없으면 `candidate-comparison`.
|
|
614
|
+
preselected_direction: str = ""
|
|
610
615
|
# implementation-planning 전용: 사용자가 고른 이번 재실행의 재검증 범위.
|
|
611
616
|
# "" / "auto" = 리드의 `okstra incremental-scope` 판정에 맡김, "full" =
|
|
612
617
|
# 전체 재검증 강제, "<stage csv>" = 그 stage 들을 impacted 로 지정.
|
|
@@ -1136,6 +1141,7 @@ def _canonical_argv(inp: PrepareInputs, ctx: dict) -> list[str]:
|
|
|
1136
1141
|
("--implementation-option", inp.implementation_option),
|
|
1137
1142
|
("--clarification-response", inp.clarification_response_path),
|
|
1138
1143
|
("--selected-direction", inp.selected_direction_path),
|
|
1144
|
+
("--preselected-direction", inp.preselected_direction),
|
|
1139
1145
|
(
|
|
1140
1146
|
"--lead-provider",
|
|
1141
1147
|
inp.lead_provider or lead_fallbacks.get("LEAD_PROVIDER", ""),
|
|
@@ -1416,6 +1422,11 @@ def _validate_prepare_inputs(project_root: Path, inp: PrepareInputs) -> list:
|
|
|
1416
1422
|
raise PrepareError(f"task brief not found: {inp.brief_path}")
|
|
1417
1423
|
elif inp.task_type == "implementation-option-selection":
|
|
1418
1424
|
validate_brief_requirements(inp.brief_path)
|
|
1425
|
+
if inp.preselected_direction and inp.task_type != "implementation-option-selection":
|
|
1426
|
+
raise PrepareError(
|
|
1427
|
+
"--preselected-direction is only meaningful with --task-type "
|
|
1428
|
+
f"implementation-option-selection; got {inp.task_type}"
|
|
1429
|
+
)
|
|
1419
1430
|
ctx_stage_map: list = []
|
|
1420
1431
|
# implementation 과 final-verification 은 둘 다 승인된 plan 의 Stage Map 을
|
|
1421
1432
|
# 입력으로 받는다(전자는 실행 scope, 후자는 검증 scope).
|
|
@@ -3068,6 +3079,22 @@ def _write_instruction_set_sources(
|
|
|
3068
3079
|
if inp.clarification_response_path else ""
|
|
3069
3080
|
),
|
|
3070
3081
|
verification_target_path=ctx.get("VERIFICATION_TARGET_RELATIVE_PATH", ""),
|
|
3082
|
+
selection_mode_text=(
|
|
3083
|
+
selection_mode_packet_block(
|
|
3084
|
+
inp.preselected_direction,
|
|
3085
|
+
Path(inp.clarification_response_path) if inp.clarification_response_path else None,
|
|
3086
|
+
Path(ctx["PROJECT_ROOT"]),
|
|
3087
|
+
)
|
|
3088
|
+
if inp.task_type == "implementation-option-selection" else ""
|
|
3089
|
+
),
|
|
3090
|
+
cited_documents_text=cited_documents_block(
|
|
3091
|
+
(instruction_set / "task-brief.md").read_text(encoding="utf-8"),
|
|
3092
|
+
Path(ctx["PROJECT_ROOT"]),
|
|
3093
|
+
),
|
|
3094
|
+
prior_verification_text=(
|
|
3095
|
+
prior_blockers_packet_block(Path(ctx["FINAL_REPORT_PATH"]))
|
|
3096
|
+
if inp.task_type == "final-verification" else ""
|
|
3097
|
+
),
|
|
3071
3098
|
)
|
|
3072
3099
|
if inp.task_type == "technical-verification":
|
|
3073
3100
|
packet += materialize_technical_input_packet(
|
|
@@ -3296,6 +3323,7 @@ def _persist_run_inputs(
|
|
|
3296
3323
|
"approvedPlanPath": approved_plan_path,
|
|
3297
3324
|
"clarificationResponsePath": inp.clarification_response_path,
|
|
3298
3325
|
"selectedDirectionPath": inp.selected_direction_path,
|
|
3326
|
+
"preselectedDirection": inp.preselected_direction,
|
|
3299
3327
|
"analysisTarget": json.loads(ctx.get("ANALYSIS_TARGET_JSON", "{}")).get(
|
|
3300
3328
|
"requestedValue", ""
|
|
3301
3329
|
),
|
|
@@ -4060,6 +4088,11 @@ def _prepare_user_authorization(inp: PrepareInputs) -> dict:
|
|
|
4060
4088
|
for key, value in expected.items():
|
|
4061
4089
|
if record["scope"].get(key) != value:
|
|
4062
4090
|
raise PrepareError(f"user authorization scope does not match --{key}; confirm the changed scope")
|
|
4091
|
+
# A record confirmed before this key existed scoped a run without a preselected direction.
|
|
4092
|
+
if record["scope"].get("preselected-direction", "") != inp.preselected_direction:
|
|
4093
|
+
raise PrepareError(
|
|
4094
|
+
"user authorization scope does not match --preselected-direction; confirm the changed scope"
|
|
4095
|
+
)
|
|
4063
4096
|
for key, path in (
|
|
4064
4097
|
("task-brief", "" if inp.task_type in ("release-handoff", "discussion") else inp.brief_path),
|
|
4065
4098
|
("approved-plan", inp.approved_plan_path),
|
|
@@ -4537,6 +4570,7 @@ Usage:
|
|
|
4537
4570
|
[--related-tasks <list>] [--base-ref <ref>] \
|
|
4538
4571
|
[--clarification-response <path>] [--work-category <cat>] \
|
|
4539
4572
|
[--selected-direction <selection-final-report.md>] \
|
|
4573
|
+
[--preselected-direction <direction and citation>] \
|
|
4540
4574
|
[--stage <auto|N>] [--stages <csv>] [--pr-template-path <path>] \
|
|
4541
4575
|
[--fix-cycle <yes|no>] [--discussion-start <link|text|task-key>]
|
|
4542
4576
|
|
|
@@ -4671,6 +4705,16 @@ def build_prepare_argument_parser():
|
|
|
4671
4705
|
)
|
|
4672
4706
|
p.add_argument("--clarification-response", default="", dest="clarification_response_path")
|
|
4673
4707
|
p.add_argument("--selected-direction", default="", dest="selected_direction_path")
|
|
4708
|
+
p.add_argument(
|
|
4709
|
+
"--preselected-direction",
|
|
4710
|
+
default="",
|
|
4711
|
+
dest="preselected_direction",
|
|
4712
|
+
help=(
|
|
4713
|
+
"implementation-option-selection only: the one direction this run "
|
|
4714
|
+
"validates, with its citation. Sets the run to preselected-validation; "
|
|
4715
|
+
"omitted means candidate-comparison."
|
|
4716
|
+
),
|
|
4717
|
+
)
|
|
4674
4718
|
p.add_argument(
|
|
4675
4719
|
"--reverify-scope",
|
|
4676
4720
|
default="",
|
|
@@ -4907,6 +4951,7 @@ def main(argv: list[str]) -> int:
|
|
|
4907
4951
|
stages=args.stages,
|
|
4908
4952
|
clarification_response_path=clarification_abs,
|
|
4909
4953
|
selected_direction_path=selected_direction_abs,
|
|
4954
|
+
preselected_direction=args.preselected_direction.strip(),
|
|
4910
4955
|
reverify_scope=args.reverify_scope,
|
|
4911
4956
|
pr_template_path=args.pr_template_path,
|
|
4912
4957
|
render_only=args.render_only,
|
|
@@ -36,12 +36,15 @@ def latest_run_inputs(
|
|
|
36
36
|
task_id: str,
|
|
37
37
|
*,
|
|
38
38
|
phase_segment: str = "",
|
|
39
|
+
stage: Optional[int] = None,
|
|
39
40
|
) -> dict:
|
|
40
41
|
"""가장 최근 ``run-inputs-*.json`` 의 ``inputs`` dict 를 반환 (부재 시 {}).
|
|
41
42
|
|
|
42
|
-
``phase_segment`` 가 주어지면 그 phase 의 ``runs/<seg>/manifests/``
|
|
43
|
-
(resume 는 같은 phase 를 재실행하므로
|
|
44
|
-
task 의 모든 phase 를 통틀어 가장
|
|
43
|
+
``phase_segment`` 가 주어지면 그 phase 의 ``runs/<seg>/manifests/`` 와
|
|
44
|
+
``runs/<seg>/stage-*/manifests/`` 만 스캔한다 (resume 는 같은 phase 를 재실행하므로
|
|
45
|
+
그 phase 의 직전 입력이 기준). 비어 있으면 task 의 모든 phase 를 통틀어 가장
|
|
46
|
+
최근(mtime) 파일을 고른다. ``stage`` 는 ``phase_segment`` 와 함께 써서
|
|
47
|
+
``runs/<seg>/stage-<N>/manifests/`` 하나로 좁힌다.
|
|
45
48
|
|
|
46
49
|
페이로드 구조는 ``{"schemaVersion", "writtenAt", "inputs": {...}}`` 이며 실제
|
|
47
50
|
입력은 top-level 이 아니라 ``inputs`` 아래에 있다. run-inputs 의 위치/구조 지식은
|
|
@@ -52,13 +55,15 @@ def latest_run_inputs(
|
|
|
52
55
|
runs_base = task_runs_dir(Path(project_root), task_group, task_id)
|
|
53
56
|
if not runs_base.is_dir():
|
|
54
57
|
return {}
|
|
55
|
-
if phase_segment:
|
|
56
|
-
|
|
57
|
-
"run-inputs-*.json")
|
|
58
|
+
if phase_segment and stage is not None:
|
|
59
|
+
patterns = [f"{phase_segment}/stage-{int(stage)}/manifests/run-inputs-*.json"]
|
|
58
60
|
else:
|
|
59
|
-
|
|
61
|
+
# implementation / final-verification run 은 stage-<N>/ 한 단계 아래에 있다.
|
|
62
|
+
scope = phase_segment or "*"
|
|
63
|
+
patterns = [f"{scope}/manifests/run-inputs-*.json",
|
|
64
|
+
f"{scope}/stage-*/manifests/run-inputs-*.json"]
|
|
60
65
|
candidates: list[tuple[float, Path]] = []
|
|
61
|
-
for inp in
|
|
66
|
+
for inp in (path for pattern in patterns for path in runs_base.glob(pattern)):
|
|
62
67
|
try:
|
|
63
68
|
candidates.append((inp.stat().st_mtime, inp))
|
|
64
69
|
except OSError:
|
|
@@ -7,6 +7,7 @@ from typing import Optional
|
|
|
7
7
|
from okstra_ctl.registry.host_registry import default_host_registry
|
|
8
8
|
from okstra_ctl.clarification_items import sidecar_answers, user_response_sidecars
|
|
9
9
|
from okstra_ctl.incremental_scope import CARRY_ALL_SCOPE
|
|
10
|
+
from okstra_ctl.phases.implementation_option_selection.entry import selection_mode
|
|
10
11
|
from okstra_ctl.wizard_stage_intent import wizard_stage_confirmation_label, resolve_wizard_stage_intent
|
|
11
12
|
from okstra_ctl.worktree import (
|
|
12
13
|
compute_worktree_path,
|
|
@@ -325,6 +326,11 @@ def confirmation_block(state: WizardState) -> str:
|
|
|
325
326
|
lines.append(reverify_line)
|
|
326
327
|
if state.selected_direction_path:
|
|
327
328
|
lines.append(f" selected-direction: {state.selected_direction_path}")
|
|
329
|
+
if state.task_type == "implementation-option-selection":
|
|
330
|
+
mode = selection_mode(state.preselected_direction)
|
|
331
|
+
lines.append(f" selection-mode: {mode}")
|
|
332
|
+
if state.preselected_direction:
|
|
333
|
+
lines.append(f" preselected-direction: {state.preselected_direction}")
|
|
328
334
|
if state.task_type == "release-handoff" and state.handoff_stages:
|
|
329
335
|
scope = _msg(state.workspace_root, "confirmation",
|
|
330
336
|
"handoff_scope_stages", stages=state.handoff_stages)
|
|
@@ -220,6 +220,12 @@ S_CLARIFICATION_PICK = "clarification_pick"
|
|
|
220
220
|
S_CLARIFICATION = "clarification"
|
|
221
221
|
|
|
222
222
|
|
|
223
|
+
S_SELECTION_MODE_PICK = "selection_mode_pick"
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
S_PRESELECTED_DIRECTION = "preselected_direction"
|
|
227
|
+
|
|
228
|
+
|
|
223
229
|
S_REVERIFY_SCOPE_PICK = "reverify_scope_pick"
|
|
224
230
|
|
|
225
231
|
|
|
@@ -56,6 +56,7 @@ from .ids import (
|
|
|
56
56
|
S_HANDOFF_STAGE_PICK,
|
|
57
57
|
S_PROJECT_EVIDENCE,
|
|
58
58
|
S_PROJECT_EVIDENCE_PICK,
|
|
59
|
+
S_PRESELECTED_DIRECTION,
|
|
59
60
|
S_PR_TEMPLATE,
|
|
60
61
|
S_PR_TEMPLATE_PICK,
|
|
61
62
|
S_REASONING_EFFORT,
|
|
@@ -67,6 +68,7 @@ from .ids import (
|
|
|
67
68
|
S_REVERIFY_SCOPE_PICK,
|
|
68
69
|
S_REVERIFY_SCOPE_STAGES,
|
|
69
70
|
S_SELECTED_DIRECTION_PICK,
|
|
71
|
+
S_SELECTION_MODE_PICK,
|
|
70
72
|
S_STAGE_PICK,
|
|
71
73
|
S_TASK_GROUP,
|
|
72
74
|
S_TASK_GROUP_TEXT,
|
|
@@ -185,6 +187,8 @@ from .steps_options import (
|
|
|
185
187
|
_build_pr_template_scope,
|
|
186
188
|
_build_related_tasks,
|
|
187
189
|
_build_related_tasks_pick,
|
|
190
|
+
_build_preselected_direction,
|
|
191
|
+
_build_selection_mode_pick,
|
|
188
192
|
_submit_clarification,
|
|
189
193
|
_submit_clarification_pick,
|
|
190
194
|
_submit_directive,
|
|
@@ -194,6 +198,8 @@ from .steps_options import (
|
|
|
194
198
|
_submit_pr_template_scope,
|
|
195
199
|
_submit_related_tasks,
|
|
196
200
|
_submit_related_tasks_pick,
|
|
201
|
+
_submit_preselected_direction,
|
|
202
|
+
_submit_selection_mode_pick,
|
|
197
203
|
)
|
|
198
204
|
from .steps_roles import _build_reuse_previous, _submit_reuse_previous
|
|
199
205
|
from .confirmation import _build_confirm, _submit_confirm
|
|
@@ -556,6 +562,19 @@ STEPS: list[Step] = [
|
|
|
556
562
|
and S_CLARIFICATION not in s.answered),
|
|
557
563
|
build=_build_clarification, submit=_submit_clarification,
|
|
558
564
|
owns=("clarification_response_path", "clarification_pending_text")),
|
|
565
|
+
# clarification 뒤 — 이어받는 리포트의 다음 단계 지시를 라벨에 보여 준다.
|
|
566
|
+
Step(S_SELECTION_MODE_PICK,
|
|
567
|
+
applies=lambda s: (s.task_type == "implementation-option-selection"
|
|
568
|
+
and _identity_ready(s)
|
|
569
|
+
and not s.clarification_pending_text
|
|
570
|
+
and S_SELECTION_MODE_PICK not in s.answered),
|
|
571
|
+
build=_build_selection_mode_pick, submit=_submit_selection_mode_pick,
|
|
572
|
+
owns=("preselected_direction", "preselected_direction_pending_text")),
|
|
573
|
+
Step(S_PRESELECTED_DIRECTION,
|
|
574
|
+
applies=lambda s: (s.preselected_direction_pending_text
|
|
575
|
+
and S_PRESELECTED_DIRECTION not in s.answered),
|
|
576
|
+
build=_build_preselected_direction, submit=_submit_preselected_direction,
|
|
577
|
+
owns=("preselected_direction", "preselected_direction_pending_text")),
|
|
559
578
|
# clarification 뒤에 온다. `_planning_rerun_selected` 는 clarification 답을 읽어
|
|
560
579
|
# "이 런은 기존 계획서의 재실행" 인지 판정하는데, 이 단계가 clarification 앞에
|
|
561
580
|
# 있던 동안 그 값은 언제나 비어 있었다 — 가드가 참이 될 수 없는 자리에 놓여
|
|
@@ -663,6 +682,8 @@ def _ready_for_confirm(s: WizardState) -> bool:
|
|
|
663
682
|
S_RELATED_TASKS,
|
|
664
683
|
S_CLARIFICATION_PICK,
|
|
665
684
|
S_CLARIFICATION,
|
|
685
|
+
S_SELECTION_MODE_PICK,
|
|
686
|
+
S_PRESELECTED_DIRECTION,
|
|
666
687
|
S_PR_TEMPLATE_PICK,
|
|
667
688
|
S_PR_TEMPLATE,
|
|
668
689
|
S_PR_TEMPLATE_SCOPE,
|
|
@@ -112,6 +112,10 @@ def render_args(state: WizardState) -> dict[str, Any]:
|
|
|
112
112
|
"related-tasks": state.related_tasks_raw,
|
|
113
113
|
"clarification-response": state.clarification_response_path,
|
|
114
114
|
"selected-direction": state.selected_direction_path,
|
|
115
|
+
"preselected-direction": (
|
|
116
|
+
state.preselected_direction
|
|
117
|
+
if state.task_type == "implementation-option-selection" else ""
|
|
118
|
+
),
|
|
115
119
|
"reverify-scope": (
|
|
116
120
|
state.reverify_scope
|
|
117
121
|
if state.task_type == "implementation-planning" else ""
|
|
@@ -25,6 +25,7 @@ from okstra_ctl.final_report_paths import final_report_data_path
|
|
|
25
25
|
from okstra_ctl.plan_run_root import list_implementation_planning_reports
|
|
26
26
|
from okstra_ctl.stage_map import StageMapError, parse_stage_map_file
|
|
27
27
|
from okstra_ctl.workflow import PHASE_SEQUENCE
|
|
28
|
+
from okstra_ctl.wizard_stage_intent import WHOLE_TASK_STAGE
|
|
28
29
|
from okstra_ctl import worktree_registry
|
|
29
30
|
from okstra_ctl.worktree import is_git_work_tree, main_worktree_path
|
|
30
31
|
from okstra_ctl.paths import RunRef, task_runs_dir
|
|
@@ -362,14 +363,32 @@ def _contained_final_report_data_path(report: Path, task_root: Path) -> Path:
|
|
|
362
363
|
)
|
|
363
364
|
|
|
364
365
|
|
|
366
|
+
def _stage_scoped_report_patterns(
|
|
367
|
+
segment: str, selected_stage: str
|
|
368
|
+
) -> tuple[str, ...]:
|
|
369
|
+
"""`segment` 아래에서 이 run 의 stage 범위에 속한 보고서 glob.
|
|
370
|
+
|
|
371
|
+
숫자 stage 는 `stage-<N>/` 만, 전체 작업(flat run 디렉터리)은 flat 만 본다 —
|
|
372
|
+
다른 범위의 답변은 이 run 의 clarification 이 아니다. stage 를 고르지 않는
|
|
373
|
+
run(release-handoff 등)은 둘 다 본다.
|
|
374
|
+
"""
|
|
375
|
+
report = "reports/final-report-*.data.json"
|
|
376
|
+
if selected_stage.isdigit():
|
|
377
|
+
return (f"{segment}/stage-{selected_stage}/{report}",)
|
|
378
|
+
if selected_stage == WHOLE_TASK_STAGE:
|
|
379
|
+
return (f"{segment}/{report}",)
|
|
380
|
+
return (f"{segment}/{report}", f"{segment}/stage-*/{report}")
|
|
381
|
+
|
|
382
|
+
|
|
365
383
|
def _newest_contained_final_report(
|
|
366
384
|
runs_base: Path,
|
|
367
385
|
task_root: Path,
|
|
368
|
-
|
|
386
|
+
glob_patterns: tuple[str, ...],
|
|
369
387
|
project_root: Path,
|
|
370
388
|
) -> Path | None:
|
|
371
389
|
candidates: list[Path] = []
|
|
372
|
-
for
|
|
390
|
+
reports = (path for pattern in glob_patterns for path in runs_base.glob(pattern))
|
|
391
|
+
for report in reports:
|
|
373
392
|
if not report.is_file():
|
|
374
393
|
continue
|
|
375
394
|
try:
|
|
@@ -513,7 +532,7 @@ def _technical_evidence_for_comparison(
|
|
|
513
532
|
project_root = Path(state.project_root)
|
|
514
533
|
report = _newest_contained_final_report(
|
|
515
534
|
task_root / "runs", task_root,
|
|
516
|
-
"technical-verification/reports/final-report-*.data.json", project_root,
|
|
535
|
+
("technical-verification/reports/final-report-*.data.json",), project_root,
|
|
517
536
|
)
|
|
518
537
|
if report is None:
|
|
519
538
|
return source
|
|
@@ -531,10 +550,11 @@ def _suggest_latest_final_report(state: WizardState) -> str:
|
|
|
531
550
|
"""clarification carry-in 으로 추천할 직전 final-report 의 relpath.
|
|
532
551
|
|
|
533
552
|
resume-clarification 은 "같은 phase 를 답변과 함께 재실행" 이므로 재실행
|
|
534
|
-
task-type **자신의** 보고서(``runs/<task-type
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
553
|
+
task-type **자신의** 보고서(``runs/<task-type>[/stage-<N>]/reports/``)를
|
|
554
|
+
우선한다(stage 범위는 ``_stage_scoped_report_patterns``). 그 phase 에 보고서가
|
|
555
|
+
없을 때만(예: 다음 phase 로 진행) 전체 phase 의 mtime 최신으로 폴백한다 —
|
|
556
|
+
과거엔 무조건 전체 mtime 최신이라 더 최근에 재렌더된 다른 phase 보고서를 잘못
|
|
557
|
+
집을 수 있었다. 못 찾으면 빈 문자열.
|
|
538
558
|
"""
|
|
539
559
|
if not state.task_group or not state.task_id or not state.project_root:
|
|
540
560
|
return ""
|
|
@@ -560,7 +580,7 @@ def _suggest_latest_final_report(state: WizardState) -> str:
|
|
|
560
580
|
best = _newest_contained_final_report(
|
|
561
581
|
runs_base,
|
|
562
582
|
task_root,
|
|
563
|
-
|
|
583
|
+
_stage_scoped_report_patterns(seg, state.selected_stage),
|
|
564
584
|
Path(state.project_root),
|
|
565
585
|
)
|
|
566
586
|
# implementation-planning 의 `--clarification-response` 는 같은 phase 의
|
|
@@ -572,7 +592,9 @@ def _suggest_latest_final_report(state: WizardState) -> str:
|
|
|
572
592
|
best = _newest_contained_final_report(
|
|
573
593
|
runs_base,
|
|
574
594
|
task_root,
|
|
575
|
-
|
|
595
|
+
# 다른 phase 의 flat 보고서(계획서 등)는 stage 범위와 무관하게 후보다.
|
|
596
|
+
("*/reports/final-report-*.data.json",
|
|
597
|
+
*_stage_scoped_report_patterns("*", state.selected_stage)),
|
|
576
598
|
Path(state.project_root),
|
|
577
599
|
)
|
|
578
600
|
best = _technical_evidence_for_comparison(state, best, task_root)
|
|
@@ -592,12 +614,12 @@ def _suggest_latest_final_report(state: WizardState) -> str:
|
|
|
592
614
|
|
|
593
615
|
|
|
594
616
|
def _latest_run_inputs(
|
|
595
|
-
state: WizardState, *, phase_segment: str = ""
|
|
617
|
+
state: WizardState, *, phase_segment: str = "", stage: Optional[int] = None
|
|
596
618
|
) -> dict:
|
|
597
619
|
"""직전 run 의 입력 스냅샷. 위치/구조 지식은 run_context.latest_run_inputs 가 SSOT."""
|
|
598
620
|
return latest_run_inputs(
|
|
599
621
|
state.project_root, state.task_group, state.task_id,
|
|
600
|
-
phase_segment=phase_segment)
|
|
622
|
+
phase_segment=phase_segment, stage=stage)
|
|
601
623
|
|
|
602
624
|
|
|
603
625
|
def _clarification_pick_required(state: WizardState) -> bool:
|
|
@@ -120,6 +120,9 @@ class WizardState:
|
|
|
120
120
|
clarification_pending_text: bool = False
|
|
121
121
|
last_final_report_cached: str = ""
|
|
122
122
|
selected_direction_path: str = ""
|
|
123
|
+
# implementation-option-selection: 리드가 미리 정한 방향과 근거. 비면 candidate-comparison.
|
|
124
|
+
preselected_direction: str = ""
|
|
125
|
+
preselected_direction_pending_text: bool = False
|
|
123
126
|
# "" | "auto" | "full" | "<stage csv>" — 사용자가 고른 이번 재실행의 재검증
|
|
124
127
|
# 범위. implementation-planning 재실행에서 좁힐 여지가 있을 때만 채워진다.
|
|
125
128
|
reverify_scope: str = ""
|
|
@@ -625,6 +628,7 @@ _FIELD_DEFAULTS: dict[str, Any] = {
|
|
|
625
628
|
"related_tasks_raw": "", "related_tasks_pending_text": False,
|
|
626
629
|
"clarification_response_path": "", "clarification_pending_text": False,
|
|
627
630
|
"selected_direction_path": "",
|
|
631
|
+
"preselected_direction": "", "preselected_direction_pending_text": False,
|
|
628
632
|
"reverify_scope": "", "reverify_scope_pending_text": False,
|
|
629
633
|
"pr_template_path": "", "pr_template_pending_text": False,
|
|
630
634
|
"pr_template_scope": "",
|
|
@@ -8,10 +8,17 @@ from typing import Callable, Optional
|
|
|
8
8
|
|
|
9
9
|
from okstra_ctl.clarification_items import user_response_sidecars
|
|
10
10
|
from okstra_ctl.domain.worker_exec import REASONING_EFFORTS
|
|
11
|
+
from okstra_ctl.ids import slugify_task_segment
|
|
11
12
|
from okstra_ctl.manager_paths import MANAGER_CONTEXT_DIRECTIVE_PREFIX
|
|
12
13
|
from okstra_ctl.json_boundary import JsonBoundaryError, load_owned_object, write_owned_object_atomic
|
|
13
14
|
from okstra_ctl.report_language import is_language_tag
|
|
15
|
+
from okstra_ctl.phases.implementation_option_selection.entry import (
|
|
16
|
+
CANDIDATE_COMPARISON,
|
|
17
|
+
PRESELECTED_VALIDATION,
|
|
18
|
+
upstream_next_steps,
|
|
19
|
+
)
|
|
14
20
|
from okstra_ctl.phases.implementation_planning.entry import is_existing_planning_report
|
|
21
|
+
from okstra_ctl.prepare_error import PrepareError
|
|
15
22
|
from okstra_ctl.pr_template import PrTemplateError, resolve_pr_template_path
|
|
16
23
|
from okstra_project.dirs import project_json_path
|
|
17
24
|
from okstra_project.state import StateError, list_project_tasks
|
|
@@ -23,6 +30,7 @@ from .ids import (
|
|
|
23
30
|
S_CLARIFICATION_PICK,
|
|
24
31
|
S_DIRECTIVE,
|
|
25
32
|
S_DIRECTIVE_PICK,
|
|
33
|
+
S_PRESELECTED_DIRECTION,
|
|
26
34
|
S_PR_TEMPLATE,
|
|
27
35
|
S_PR_TEMPLATE_PICK,
|
|
28
36
|
S_PR_TEMPLATE_SCOPE,
|
|
@@ -30,6 +38,7 @@ from .ids import (
|
|
|
30
38
|
S_RELATED_TASKS,
|
|
31
39
|
S_RELATED_TASKS_PICK,
|
|
32
40
|
S_REPORT_LANGUAGE,
|
|
41
|
+
S_SELECTION_MODE_PICK,
|
|
33
42
|
_LATEST_REPORT_TOKEN,
|
|
34
43
|
_PROJECT_DEFAULT_TOKEN,
|
|
35
44
|
_REUSE_LAST_TOKEN,
|
|
@@ -97,7 +106,13 @@ def _clarification_sidecar_note(state: WizardState, suggestion: str) -> str:
|
|
|
97
106
|
|
|
98
107
|
def _suggest_last_directive(state: WizardState) -> str:
|
|
99
108
|
"""같은 task 의 가장 최근 run-inputs-*.json 에서 directive 값을 자동 추출."""
|
|
100
|
-
|
|
109
|
+
inputs: dict = {}
|
|
110
|
+
if state.task_type and state.selected_stage.isdigit():
|
|
111
|
+
# stage run 의 directive 는 그 stage 범위로 쓰였다 — 형제 stage 것은 남의 것이다.
|
|
112
|
+
inputs = _latest_run_inputs(
|
|
113
|
+
state, phase_segment=slugify_task_segment(state.task_type),
|
|
114
|
+
stage=int(state.selected_stage))
|
|
115
|
+
val = (inputs or _latest_run_inputs(state)).get("directive") or ""
|
|
101
116
|
return val if isinstance(val, str) else ""
|
|
102
117
|
|
|
103
118
|
|
|
@@ -129,9 +144,9 @@ class _OptionalCachedPickSpec:
|
|
|
129
144
|
# 추천 옵션 라벨·echo 뒤에 덧붙일 동적 안내(예: 함께 첨부될 사이드카 개수).
|
|
130
145
|
# (state, 추천 relpath) -> note. 미설정 picker 는 영향 없음.
|
|
131
146
|
recommend_note: Optional[Callable[["WizardState", str], str]] = None
|
|
132
|
-
# Whether the
|
|
133
|
-
#
|
|
134
|
-
|
|
147
|
+
# Whether the screen is always shown. No recorded value tells the wizard that a
|
|
148
|
+
# run needs no directive, and a previous one was written for another run.
|
|
149
|
+
always_ask: bool = False
|
|
135
150
|
|
|
136
151
|
|
|
137
152
|
def _build_optional_cached_pick(state: WizardState, spec: _OptionalCachedPickSpec) -> Prompt:
|
|
@@ -149,12 +164,12 @@ def _build_optional_cached_pick(state: WizardState, spec: _OptionalCachedPickSpe
|
|
|
149
164
|
setattr(state, spec.cache_attr, suggestion)
|
|
150
165
|
options.append(_opt(PICK_SKIP, t["options"][PICK_SKIP]))
|
|
151
166
|
options.append(_opt(PICK_TYPE_CUSTOM, t["options"][PICK_TYPE_CUSTOM]))
|
|
152
|
-
if
|
|
167
|
+
if spec.always_ask:
|
|
168
|
+
auto_value = ""
|
|
169
|
+
elif not suggestion:
|
|
153
170
|
auto_value = PICK_SKIP
|
|
154
|
-
elif spec.auto_take_suggestion:
|
|
155
|
-
auto_value = spec.recommend_token
|
|
156
171
|
else:
|
|
157
|
-
auto_value =
|
|
172
|
+
auto_value = spec.recommend_token
|
|
158
173
|
return Prompt(
|
|
159
174
|
step=spec.step, kind="pick",
|
|
160
175
|
label=t["label"], options=options,
|
|
@@ -218,7 +233,7 @@ _DIRECTIVE_PICK_SPEC = _OptionalCachedPickSpec(
|
|
|
218
233
|
cache_attr="last_directive_cached", target_attr="directive",
|
|
219
234
|
pending_attr="directive_pending_text",
|
|
220
235
|
recommend_note=_directive_context_note,
|
|
221
|
-
|
|
236
|
+
always_ask=True,
|
|
222
237
|
)
|
|
223
238
|
|
|
224
239
|
|
|
@@ -393,6 +408,56 @@ def _submit_clarification(state: WizardState, value: str) -> Optional[str]:
|
|
|
393
408
|
return f"clarification: {p}"
|
|
394
409
|
|
|
395
410
|
|
|
411
|
+
def _build_selection_mode_pick(state: WizardState) -> Prompt:
|
|
412
|
+
"""The mode is the lead's decision; the upstream report's routing is shown beside it."""
|
|
413
|
+
t = _p(state.workspace_root, "selection_mode_pick")
|
|
414
|
+
label = t["label"]
|
|
415
|
+
if state.clarification_response_path:
|
|
416
|
+
try:
|
|
417
|
+
steps = upstream_next_steps(Path(state.clarification_response_path))
|
|
418
|
+
except PrepareError as exc:
|
|
419
|
+
raise WizardError(str(exc)) from exc
|
|
420
|
+
if steps:
|
|
421
|
+
label += t["labels"]["upstream"].format(steps=" / ".join(steps))
|
|
422
|
+
return Prompt(
|
|
423
|
+
step=S_SELECTION_MODE_PICK, kind="pick", label=label,
|
|
424
|
+
options=[_opt(value, text) for value, text in t["options"].items()],
|
|
425
|
+
echo_template=t["echo_template"],
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _submit_selection_mode_pick(state: WizardState, value: str) -> Optional[str]:
|
|
430
|
+
if value == CANDIDATE_COMPARISON:
|
|
431
|
+
state.preselected_direction = ""
|
|
432
|
+
state.preselected_direction_pending_text = False
|
|
433
|
+
return f"selection-mode: {CANDIDATE_COMPARISON}"
|
|
434
|
+
if value == PRESELECTED_VALIDATION:
|
|
435
|
+
state.preselected_direction_pending_text = True
|
|
436
|
+
return None
|
|
437
|
+
raise WizardError(
|
|
438
|
+
f"expected {CANDIDATE_COMPARISON!r} or {PRESELECTED_VALIDATION!r}, got: {value!r}"
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _build_preselected_direction(state: WizardState) -> Prompt:
|
|
443
|
+
t = _p(state.workspace_root, "preselected_direction")
|
|
444
|
+
return Prompt(
|
|
445
|
+
step=S_PRESELECTED_DIRECTION, kind="text",
|
|
446
|
+
label=t["label"], echo_template=t["echo_template"],
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _submit_preselected_direction(state: WizardState, value: str) -> Optional[str]:
|
|
451
|
+
direction = (value or "").strip()
|
|
452
|
+
if not direction:
|
|
453
|
+
raise WizardError(
|
|
454
|
+
f"{PRESELECTED_VALIDATION} needs the direction and the citation that fixed it"
|
|
455
|
+
)
|
|
456
|
+
state.preselected_direction = direction
|
|
457
|
+
state.preselected_direction_pending_text = False
|
|
458
|
+
return f"preselected-direction: {direction}"
|
|
459
|
+
|
|
460
|
+
|
|
396
461
|
def _build_pr_template(state: WizardState) -> Prompt:
|
|
397
462
|
t = _p(state.workspace_root, "pr_template")
|
|
398
463
|
return Prompt(
|
|
@@ -24,6 +24,7 @@ def analysis_prompt_body(
|
|
|
24
24
|
plan: PromptPlan,
|
|
25
25
|
approved_plan_path: str = "",
|
|
26
26
|
selected_direction_path: str = "",
|
|
27
|
+
stage_extract_path: str = "",
|
|
27
28
|
) -> list[str]:
|
|
28
29
|
"""Render the provider-neutral body for an initial analysis audience."""
|
|
29
30
|
label = analysis_worker_label(worker_id)
|
|
@@ -43,7 +44,12 @@ def analysis_prompt_body(
|
|
|
43
44
|
"",
|
|
44
45
|
"## Inputs",
|
|
45
46
|
*analysis_input_lines(
|
|
46
|
-
manifest,
|
|
47
|
+
manifest,
|
|
48
|
+
active_context,
|
|
49
|
+
plan,
|
|
50
|
+
approved_plan_path,
|
|
51
|
+
selected_direction_path,
|
|
52
|
+
stage_extract_path,
|
|
47
53
|
),
|
|
48
54
|
"",
|
|
49
55
|
mcp_pointer_line(),
|
|
@@ -60,7 +66,21 @@ def analysis_input_lines(
|
|
|
60
66
|
plan: PromptPlan,
|
|
61
67
|
approved_plan_path: str = "",
|
|
62
68
|
selected_direction_path: str = "",
|
|
69
|
+
stage_extract_path: str = "",
|
|
63
70
|
) -> list[str]:
|
|
71
|
+
if plan.audience == "implementation-executor":
|
|
72
|
+
# The packet already carries the brief extract, the reference
|
|
73
|
+
# expectations, the profile prelude and the directive; the task brief,
|
|
74
|
+
# analysis material, profile and reference files repeat them in full.
|
|
75
|
+
# Measured 2026-10-30 (dev-11562): those four files were 1,929 lines
|
|
76
|
+
# beside a 4,865-line packet. The clarification file stays: the
|
|
77
|
+
# packet keeps only its extract, and a lazy-delivery prompt has no
|
|
78
|
+
# inlined authority block.
|
|
79
|
+
return existing_input_lines((
|
|
80
|
+
("Primary analysis packet", instruction_path(manifest, active_context, "analysisPacketPath")),
|
|
81
|
+
("Approved plan stage extract", stage_extract_path),
|
|
82
|
+
("Clarification response", instruction_path(manifest, active_context, "clarificationResponsePath")),
|
|
83
|
+
))
|
|
64
84
|
if plan.packet_only:
|
|
65
85
|
packet_path = instruction_path(manifest, active_context, "analysisPacketPath")
|
|
66
86
|
# 승인 계획은 `## Inputs` 에 실어야 한다. 워커의 읽기 범위는 이 절과
|
|
@@ -27,6 +27,8 @@ class WorkerPromptHeaderError(Exception):
|
|
|
27
27
|
# skill catalogs, and similar non-okstra artifacts. Restating the scope inline
|
|
28
28
|
# puts the rule in front of the worker before its first tool call, which is the
|
|
29
29
|
# only point at which it can still win over those host instructions.
|
|
30
|
+
# 패킷의 페이지 읽기 규칙도 같은 이유로 여기 있다 — 패킷 안 목차는 파일을 연 뒤에야
|
|
31
|
+
# 보이는데, 통째 `cat` 은 그 전에 잘린다(실측 609KB 패킷, codex).
|
|
30
32
|
READ_SCOPE_HEADER = (
|
|
31
33
|
"**Read scope:** Read only the paths this prompt enumerates "
|
|
32
34
|
"(`[Required reading]`, `## Inputs`, verification-target paths) plus "
|
|
@@ -39,7 +41,11 @@ READ_SCOPE_HEADER = (
|
|
|
39
41
|
"NOT apply inside an okstra worker run: do not auto-read `graphify-out/`, "
|
|
40
42
|
"`SKILL.md`, or other artifacts outside `<PROJECT_ROOT>/.okstra/`. If an "
|
|
41
43
|
"un-enumerated file seems essential, record it under *Missing Information "
|
|
42
|
-
"or Assumptions* instead of reading it."
|
|
44
|
+
"or Assumptions* instead of reading it. Read the primary analysis packet "
|
|
45
|
+
"in pages of at most 200 lines (`sed -n '1,200p'`, then `'201,400p'`, …), "
|
|
46
|
+
"never with one whole-file `cat` or view: worker CLIs cut long output "
|
|
47
|
+
"without saying so, and the packet's `## Section Index` gives each "
|
|
48
|
+
"section's line range."
|
|
43
49
|
)
|
|
44
50
|
EVIDENCE_LEDGER_HEADER = "**Evidence ledger:** required-v1"
|
|
45
51
|
|
|
@@ -63,12 +69,13 @@ EVIDENCE_CITATION_HEADER = (
|
|
|
63
69
|
# 구현 워커는 명령 행을 쓰지 못한다.
|
|
64
70
|
EVIDENCE_COMMANDS_HEADER = (
|
|
65
71
|
"**Evidence commands:** Append one `- Evidence command: "
|
|
66
|
-
"{\"command\":\"<exact command>\",\"cwd\":\"<
|
|
72
|
+
"{\"command\":\"<exact command>\",\"cwd\":\"<directory the command ran in>\","
|
|
67
73
|
"\"exitCode\":0,\"outputSummary\":\"<one-line result>\"}` row to the audit "
|
|
68
74
|
"sidecar for each command that produced or verified a conclusion. Do not "
|
|
69
75
|
"record exploratory `rg`, `ls`, or file-opening commands. Write the command "
|
|
70
|
-
"as you ran it.
|
|
71
|
-
"
|
|
76
|
+
"as you ran it. When it starts with `cd <dir> &&`, `cwd` is that `<dir>`, "
|
|
77
|
+
"not the shell's starting directory. A value that must not be written down "
|
|
78
|
+
"is yours to omit or pass as a `$VAR` reference."
|
|
72
79
|
)
|
|
73
80
|
|
|
74
81
|
# `agy`'s write tool validates the target against the Gemini artifact store
|
|
@@ -34,6 +34,9 @@ ERRORS_PATH_HEADERS = (
|
|
|
34
34
|
# itself: the lead's launch prompt carries them, and no worker reads that.
|
|
35
35
|
APPROVED_PLAN_HEADER = "**Approved plan:**"
|
|
36
36
|
IMPLEMENTATION_STAGE_HEADER = "**Stage for this implementation run:**"
|
|
37
|
+
# Executor-only and optional: a prompt published before it, or a run whose plan
|
|
38
|
+
# is a Markdown report, has no extract and the executor reads the plan itself.
|
|
39
|
+
APPROVED_PLAN_STAGE_EXTRACT_HEADER = "**Approved plan stage extract:**"
|
|
37
40
|
# A stage's verifiers share one worktree and run as one batch. The Tier 3 script
|
|
38
41
|
# (builds, fixed ports) and the self-mock mutation probe (edits sources in place)
|
|
39
42
|
# must each run once, by the verifier named here.
|