okstra 0.52.0 → 0.53.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.kr.md +1 -1
- package/README.md +1 -1
- package/docs/kr/architecture.md +1 -0
- package/docs/kr/cli.md +2 -1
- package/docs/superpowers/plans/2026-06-06-final-verification-whole-task-gate.md +993 -0
- package/docs/superpowers/plans/2026-06-06-stage-parallel-and-pending-fixes.md +93 -0
- package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p1.md +447 -0
- package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p2.md +289 -0
- package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p3.md +774 -0
- package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p4.md +303 -0
- package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p5-multidep-base.md +387 -0
- package/docs/superpowers/specs/2026-06-06-final-verification-whole-task-gate-design.md +126 -0
- package/docs/superpowers/specs/2026-06-06-stage-worktree-isolation-design.md +180 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/report-writer-worker.md +1 -0
- package/runtime/bin/lib/okstra/cli.sh +5 -1
- package/runtime/bin/okstra.sh +1 -0
- package/runtime/prompts/launch.template.md +1 -0
- package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
- package/runtime/prompts/profiles/_implementation-executor.md +16 -9
- package/runtime/prompts/profiles/_implementation-verifier.md +4 -1
- package/runtime/prompts/profiles/final-verification.md +7 -7
- package/runtime/prompts/profiles/implementation-planning.md +8 -4
- package/runtime/prompts/wizard/prompts.ko.json +3 -2
- package/runtime/python/okstra_ctl/analysis_packet.py +14 -2
- package/runtime/python/okstra_ctl/render.py +3 -0
- package/runtime/python/okstra_ctl/run.py +541 -41
- package/runtime/python/okstra_ctl/wizard.py +25 -7
- package/runtime/python/okstra_ctl/worktree.py +126 -9
- package/runtime/python/okstra_ctl/worktree_registry.py +88 -17
- package/runtime/schemas/final-report-v1.0.schema.json +36 -0
- package/runtime/skills/okstra-convergence/SKILL.md +14 -3
- package/runtime/skills/okstra-run/SKILL.md +1 -1
- package/runtime/templates/reports/final-report.template.md +12 -0
- package/runtime/templates/reports/final-verification-input.template.md +8 -5
- package/runtime/templates/reports/i18n/en.json +3 -1
- package/runtime/templates/reports/i18n/ko.json +3 -1
- package/runtime/validators/validate-run.py +143 -1
- package/runtime/validators/validate-workflow.sh +6 -1
|
@@ -36,6 +36,8 @@ from okstra_ctl.pr_template import PrTemplateError, resolve_pr_template_path
|
|
|
36
36
|
from okstra_ctl.run import (
|
|
37
37
|
APPROVED_FRONTMATTER_PATTERN,
|
|
38
38
|
_extract_frontmatter_block,
|
|
39
|
+
_reject_blocking_plan_body_gate,
|
|
40
|
+
_validate_data_json_approval_consistency,
|
|
39
41
|
)
|
|
40
42
|
from okstra_ctl.workers import (
|
|
41
43
|
ALLOWED_WORKERS,
|
|
@@ -77,6 +79,11 @@ TASK_TYPE_VALUES = [tt for tt, _ in TASK_TYPES]
|
|
|
77
79
|
|
|
78
80
|
EXECUTORS = ["claude", "codex", "gemini"]
|
|
79
81
|
|
|
82
|
+
# Task types that consume an approved plan and need a stage-scope pick:
|
|
83
|
+
# implementation executes a stage, final-verification verifies a stage
|
|
84
|
+
# (or the whole task via `auto`). Both gate the approved-plan + stage steps.
|
|
85
|
+
_STAGE_SCOPED_TASK_TYPES = ("implementation", "final-verification")
|
|
86
|
+
|
|
80
87
|
CANONICAL_BASE_REFS = ["main", "dev", "staging", "preprod", "prod"]
|
|
81
88
|
BASE_REF_FREE_INPUT_TOKEN = "__free_input__"
|
|
82
89
|
|
|
@@ -410,6 +417,8 @@ def _validate_approved_plan(path_str: str, project_root: Path) -> Path:
|
|
|
410
417
|
" edit the report and change the line to `approved: true`, or re-run "
|
|
411
418
|
"okstra with `--approve` to flip it from the CLI."
|
|
412
419
|
)
|
|
420
|
+
_reject_blocking_plan_body_gate(p, body, action="approved plan validation")
|
|
421
|
+
_validate_data_json_approval_consistency(p, markdown_approved=True)
|
|
413
422
|
# frontmatter approved == true 라도 §1 의 Blocks=approval 행이 미해결이면
|
|
414
423
|
# 승인이 무효 — prepare_task_bundle 의 _validate_approved_plan 과 동일 규약.
|
|
415
424
|
blockers = unresolved_approval_blockers(body)
|
|
@@ -1220,7 +1229,12 @@ def _build_stage_pick(state: WizardState) -> Prompt:
|
|
|
1220
1229
|
stages, _errs = mod._parse_stage_map(plan_text)
|
|
1221
1230
|
finally:
|
|
1222
1231
|
_sys.modules.pop("_ip_stage_v_wizard", None)
|
|
1223
|
-
|
|
1232
|
+
auto_label = (
|
|
1233
|
+
t["options"].get("auto_final_verification", t["options"]["auto"])
|
|
1234
|
+
if state.task_type == "final-verification"
|
|
1235
|
+
else t["options"]["auto"]
|
|
1236
|
+
)
|
|
1237
|
+
options = [_opt("auto", auto_label)]
|
|
1224
1238
|
for s in stages:
|
|
1225
1239
|
depends = ",".join(map(str, s.depends_on)) or "(none)"
|
|
1226
1240
|
options.append(_opt(
|
|
@@ -1969,7 +1983,7 @@ STEPS: list[Step] = [
|
|
|
1969
1983
|
build=_build_base_ref_text, submit=_submit_base_ref_text,
|
|
1970
1984
|
owns=("base_ref", "base_ref_pending_text")),
|
|
1971
1985
|
Step(S_APPROVED_PLAN_PICK,
|
|
1972
|
-
applies=lambda s: (s.task_type
|
|
1986
|
+
applies=lambda s: (s.task_type in _STAGE_SCOPED_TASK_TYPES
|
|
1973
1987
|
and not s.approved_plan_path
|
|
1974
1988
|
and not s.approved_plan_pending_text
|
|
1975
1989
|
and S_APPROVED_PLAN_PICK not in s.answered
|
|
@@ -1981,7 +1995,7 @@ STEPS: list[Step] = [
|
|
|
1981
1995
|
build=_build_approved_plan_pick, submit=_submit_approved_plan_pick,
|
|
1982
1996
|
owns=("approved_plan_path", "approved_plan_pending_text")),
|
|
1983
1997
|
Step(S_APPROVED_PLAN,
|
|
1984
|
-
applies=lambda s: (s.task_type
|
|
1998
|
+
applies=lambda s: (s.task_type in _STAGE_SCOPED_TASK_TYPES
|
|
1985
1999
|
and not s.approved_plan_path
|
|
1986
2000
|
and bool(s.brief_path)
|
|
1987
2001
|
and (s.reuse_worktree is True
|
|
@@ -1992,7 +2006,7 @@ STEPS: list[Step] = [
|
|
|
1992
2006
|
build=_build_approved_plan, submit=_submit_approved_plan,
|
|
1993
2007
|
owns=("approved_plan_path", "approved_plan_pending_text")),
|
|
1994
2008
|
Step(S_STAGE_PICK,
|
|
1995
|
-
applies=lambda s: (s.task_type
|
|
2009
|
+
applies=lambda s: (s.task_type in _STAGE_SCOPED_TASK_TYPES
|
|
1996
2010
|
and bool(s.approved_plan_path)
|
|
1997
2011
|
and S_STAGE_PICK not in s.answered),
|
|
1998
2012
|
build=_build_stage_pick, submit=_submit_stage_pick,
|
|
@@ -2155,8 +2169,11 @@ def _identity_ready(s: WizardState) -> bool:
|
|
|
2155
2169
|
return False
|
|
2156
2170
|
if s.base_ref_pending_text:
|
|
2157
2171
|
return False
|
|
2172
|
+
if s.task_type in _STAGE_SCOPED_TASK_TYPES:
|
|
2173
|
+
if not s.approved_plan_path:
|
|
2174
|
+
return False
|
|
2158
2175
|
if s.task_type == "implementation":
|
|
2159
|
-
if not s.
|
|
2176
|
+
if not s.executor:
|
|
2160
2177
|
return False
|
|
2161
2178
|
return True
|
|
2162
2179
|
|
|
@@ -2350,7 +2367,7 @@ def render_args(state: WizardState) -> dict[str, str]:
|
|
|
2350
2367
|
"executor": state.executor,
|
|
2351
2368
|
"critic": state.critic,
|
|
2352
2369
|
"approved-plan": state.approved_plan_path,
|
|
2353
|
-
"stage": (state.selected_stage or "auto") if state.task_type
|
|
2370
|
+
"stage": (state.selected_stage or "auto") if state.task_type in _STAGE_SCOPED_TASK_TYPES else "",
|
|
2354
2371
|
"base-ref": base_ref,
|
|
2355
2372
|
"workers": workers,
|
|
2356
2373
|
"directive": state.directive,
|
|
@@ -2395,8 +2412,9 @@ def confirmation_block(state: WizardState) -> str:
|
|
|
2395
2412
|
lines.append(f" directive : {state.directive or '(none)'}")
|
|
2396
2413
|
if state.task_type in ("requirements-discovery", "error-analysis", "implementation-planning", "final-verification"):
|
|
2397
2414
|
lines.append(f" critic : {state.critic or '(off)'}")
|
|
2398
|
-
if state.task_type
|
|
2415
|
+
if state.task_type in _STAGE_SCOPED_TASK_TYPES:
|
|
2399
2416
|
lines.append(f" approved-plan : {state.approved_plan_path}")
|
|
2417
|
+
lines.append(f" stage : {state.selected_stage or 'auto'}")
|
|
2400
2418
|
if state.clarification_response_path:
|
|
2401
2419
|
lines.append(f" clarification : {state.clarification_response_path}")
|
|
2402
2420
|
if state.task_type == "release-handoff" and state.pr_template_path:
|
|
@@ -491,30 +491,37 @@ def compute_worktree_path(
|
|
|
491
491
|
project_id: str,
|
|
492
492
|
task_group_segment: str,
|
|
493
493
|
task_id_segment: str,
|
|
494
|
+
stage_number: Optional[int] = None,
|
|
494
495
|
) -> Path:
|
|
495
|
-
"""Pure path computation. One worktree dir per task-key
|
|
496
|
-
|
|
497
|
-
Uses `OKSTRA_HOME` when set (test hook), else
|
|
498
|
-
|
|
499
|
-
shares this dir.
|
|
500
|
-
"""
|
|
496
|
+
"""Pure path computation. One worktree dir per task-key, or per
|
|
497
|
+
`<task-key>/stage-<N>` when stage_number is given (implementation
|
|
498
|
+
stage isolation). Uses `OKSTRA_HOME` when set (test hook), else
|
|
499
|
+
`~/.okstra`."""
|
|
501
500
|
okstra_home = os.environ.get("OKSTRA_HOME", "").strip()
|
|
502
501
|
base = Path(okstra_home) if okstra_home else (Path.home() / ".okstra")
|
|
503
|
-
|
|
502
|
+
path = (
|
|
504
503
|
base / "worktrees"
|
|
505
504
|
/ _safe_segment(project_id)
|
|
506
505
|
/ _safe_segment(task_group_segment)
|
|
507
506
|
/ _safe_segment(task_id_segment)
|
|
508
507
|
)
|
|
508
|
+
if stage_number is not None:
|
|
509
|
+
path = path / f"stage-{stage_number}"
|
|
510
|
+
return path
|
|
509
511
|
|
|
510
512
|
|
|
511
513
|
def compute_branch_name(
|
|
512
514
|
*,
|
|
513
515
|
work_category: str,
|
|
514
516
|
task_id_segment: str,
|
|
517
|
+
stage_number: Optional[int] = None,
|
|
515
518
|
) -> str:
|
|
516
|
-
"""One branch per task-key
|
|
517
|
-
|
|
519
|
+
"""One branch per task-key, or `<prefix>-<task-id>-s<N>` for an
|
|
520
|
+
implementation stage worktree."""
|
|
521
|
+
name = f"{_work_category_prefix(work_category)}-{_safe_segment(task_id_segment)}"
|
|
522
|
+
if stage_number is not None:
|
|
523
|
+
name = f"{name}-s{stage_number}"
|
|
524
|
+
return name
|
|
518
525
|
|
|
519
526
|
|
|
520
527
|
def provision_task_worktree(
|
|
@@ -693,3 +700,113 @@ def provision_task_worktree(
|
|
|
693
700
|
f"(base {base_label}; phase {task_type}){linked_suffix}"
|
|
694
701
|
),
|
|
695
702
|
)
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def provision_stage_worktree(
|
|
706
|
+
*,
|
|
707
|
+
project_root: Path,
|
|
708
|
+
project_id: str,
|
|
709
|
+
task_group_segment: str,
|
|
710
|
+
task_id_segment: str,
|
|
711
|
+
work_category: str,
|
|
712
|
+
stage_number: int,
|
|
713
|
+
base_commit: str,
|
|
714
|
+
) -> WorktreeProvision:
|
|
715
|
+
"""Materialise an isolated worktree for one implementation stage.
|
|
716
|
+
|
|
717
|
+
Unlike `provision_task_worktree` (one worktree per task-key shared
|
|
718
|
+
across phases), this provisions a per-stage worktree branched from
|
|
719
|
+
`base_commit` at `<task-key>/stage-<N>/` on branch `<prefix>-<task>-s<N>`.
|
|
720
|
+
The stage-key (`<task-key>#stage-<N>`) is reserved atomically through
|
|
721
|
+
`worktree_registry`; re-entry of the same stage-key returns the
|
|
722
|
+
existing entry. Branch / on-disk conflicts roll back the worktree
|
|
723
|
+
before re-raising so a retry is not blocked.
|
|
724
|
+
"""
|
|
725
|
+
if not base_commit:
|
|
726
|
+
raise RuntimeError("provision_stage_worktree requires a base_commit")
|
|
727
|
+
|
|
728
|
+
safe_project = _safe_segment(project_id)
|
|
729
|
+
safe_group = _safe_segment(task_group_segment)
|
|
730
|
+
safe_task = _safe_segment(task_id_segment)
|
|
731
|
+
worktree_path = compute_worktree_path(
|
|
732
|
+
project_id=safe_project, task_group_segment=safe_group,
|
|
733
|
+
task_id_segment=safe_task, stage_number=stage_number,
|
|
734
|
+
)
|
|
735
|
+
branch = compute_branch_name(
|
|
736
|
+
work_category=work_category, task_id_segment=safe_task,
|
|
737
|
+
stage_number=stage_number,
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
existing = worktree_registry.lookup(
|
|
741
|
+
safe_project, safe_group, safe_task, stage_number=stage_number)
|
|
742
|
+
if existing is not None and existing.status == "active":
|
|
743
|
+
return WorktreeProvision(
|
|
744
|
+
status="reused", path=existing.worktree_path,
|
|
745
|
+
branch=existing.branch, base_ref=existing.base_ref,
|
|
746
|
+
note=(
|
|
747
|
+
f"stage {stage_number} worktree reused at "
|
|
748
|
+
f"{existing.worktree_path} on branch {existing.branch} "
|
|
749
|
+
f"(base {existing.base_ref[:12]})"
|
|
750
|
+
),
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
if worktree_path.exists():
|
|
754
|
+
raise RuntimeError(
|
|
755
|
+
f"stage worktree path already exists but is not in the registry: "
|
|
756
|
+
f"{worktree_path}. Remove it before retrying."
|
|
757
|
+
)
|
|
758
|
+
if _branch_exists(project_root, branch):
|
|
759
|
+
raise RuntimeError(
|
|
760
|
+
f"stage worktree branch already exists: {branch}. "
|
|
761
|
+
"Delete it (`git branch -D <branch>`) before retrying."
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
main_root = main_worktree_path(project_root)
|
|
765
|
+
resolved_sha = _resolve_commit_sha(main_root, base_commit)
|
|
766
|
+
if not resolved_sha:
|
|
767
|
+
raise RuntimeError(
|
|
768
|
+
f"could not resolve base_commit `{base_commit}` in main worktree "
|
|
769
|
+
f"({main_root}); ensure the commit exists locally"
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
worktree_path.parent.mkdir(parents=True, exist_ok=True)
|
|
773
|
+
res = _git(
|
|
774
|
+
main_root,
|
|
775
|
+
"worktree", "add", "-b", branch, str(worktree_path), resolved_sha,
|
|
776
|
+
)
|
|
777
|
+
if res.returncode != 0:
|
|
778
|
+
raise RuntimeError(
|
|
779
|
+
f"`git worktree add` failed (exit={res.returncode}): "
|
|
780
|
+
f"{(res.stderr or res.stdout).strip()}"
|
|
781
|
+
)
|
|
782
|
+
|
|
783
|
+
_link_sync_dirs(main_root, worktree_path)
|
|
784
|
+
_link_sync_files(main_root, worktree_path)
|
|
785
|
+
_copy_snapshot_files(main_root, worktree_path)
|
|
786
|
+
|
|
787
|
+
try:
|
|
788
|
+
worktree_registry.reserve(
|
|
789
|
+
project_id=safe_project,
|
|
790
|
+
task_group=safe_group,
|
|
791
|
+
task_id=safe_task,
|
|
792
|
+
worktree_path=str(worktree_path),
|
|
793
|
+
branch=branch,
|
|
794
|
+
base_ref=resolved_sha,
|
|
795
|
+
phase="implementation",
|
|
796
|
+
stage_number=stage_number,
|
|
797
|
+
)
|
|
798
|
+
except RuntimeError:
|
|
799
|
+
_git(main_root, "worktree", "remove", "--force", str(worktree_path))
|
|
800
|
+
_git(main_root, "branch", "-D", branch)
|
|
801
|
+
raise
|
|
802
|
+
|
|
803
|
+
_seed_worktree_settings_symlink(worktree_path)
|
|
804
|
+
|
|
805
|
+
return WorktreeProvision(
|
|
806
|
+
status="created", path=str(worktree_path),
|
|
807
|
+
branch=branch, base_ref=resolved_sha,
|
|
808
|
+
note=(
|
|
809
|
+
f"stage {stage_number} worktree created at {worktree_path} "
|
|
810
|
+
f"on branch {branch} (base {resolved_sha[:12]})"
|
|
811
|
+
),
|
|
812
|
+
)
|
|
@@ -43,15 +43,17 @@ def _okstra_worktrees_dir() -> Path:
|
|
|
43
43
|
return base / "worktrees"
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
def task_key(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
`<
|
|
52
|
-
|
|
53
|
-
""
|
|
54
|
-
|
|
46
|
+
def task_key(
|
|
47
|
+
project_id: str, task_group: str, task_id: str,
|
|
48
|
+
stage_number: Optional[int] = None,
|
|
49
|
+
) -> str:
|
|
50
|
+
"""Canonical task-key. With stage_number, returns the stage-scoped
|
|
51
|
+
key `<proj>/<group>/<task>#stage-<N>` used to reserve a per-stage
|
|
52
|
+
worktree independently of the task-key entry."""
|
|
53
|
+
base = f"{project_id}/{task_group}/{task_id}"
|
|
54
|
+
if stage_number is not None:
|
|
55
|
+
return f"{base}#stage-{stage_number}"
|
|
56
|
+
return base
|
|
55
57
|
|
|
56
58
|
|
|
57
59
|
@dataclass
|
|
@@ -66,6 +68,8 @@ class WorktreeEntry:
|
|
|
66
68
|
created_at: str
|
|
67
69
|
last_phase: str = ""
|
|
68
70
|
status: str = "active" # "active" | "released"
|
|
71
|
+
stage: Optional[int] = None
|
|
72
|
+
implementation_base_commit: str = ""
|
|
69
73
|
|
|
70
74
|
|
|
71
75
|
@contextlib.contextmanager
|
|
@@ -112,13 +116,11 @@ def _save(data: dict) -> None:
|
|
|
112
116
|
os.replace(tmp, p)
|
|
113
117
|
|
|
114
118
|
|
|
115
|
-
def lookup(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
"""
|
|
121
|
-
key = task_key(project_id, task_group, task_id)
|
|
119
|
+
def lookup(
|
|
120
|
+
project_id: str, task_group: str, task_id: str,
|
|
121
|
+
stage_number: Optional[int] = None,
|
|
122
|
+
) -> Optional[WorktreeEntry]:
|
|
123
|
+
key = task_key(project_id, task_group, task_id, stage_number)
|
|
122
124
|
with _registry_lock():
|
|
123
125
|
data = _load()
|
|
124
126
|
row = data["tasks"].get(key)
|
|
@@ -136,13 +138,14 @@ def reserve(
|
|
|
136
138
|
branch: str,
|
|
137
139
|
base_ref: str,
|
|
138
140
|
phase: str = "",
|
|
141
|
+
stage_number: Optional[int] = None,
|
|
139
142
|
) -> WorktreeEntry:
|
|
140
143
|
"""Atomically insert a new entry. Raises RuntimeError if the
|
|
141
144
|
task-key already exists or the branch is already owned by a
|
|
142
145
|
different task-key. Callers should `lookup()` first when re-entry
|
|
143
146
|
is expected.
|
|
144
147
|
"""
|
|
145
|
-
key = task_key(project_id, task_group, task_id)
|
|
148
|
+
key = task_key(project_id, task_group, task_id, stage_number)
|
|
146
149
|
now = time.strftime("%Y-%m-%dT%H:%M:%S%z") or time.strftime("%Y-%m-%dT%H:%M:%S")
|
|
147
150
|
with _registry_lock():
|
|
148
151
|
data = _load()
|
|
@@ -170,6 +173,7 @@ def reserve(
|
|
|
170
173
|
"created_at": now,
|
|
171
174
|
"last_phase": phase,
|
|
172
175
|
"status": "active",
|
|
176
|
+
"stage": stage_number,
|
|
173
177
|
}
|
|
174
178
|
data["tasks"][key] = row
|
|
175
179
|
data["branches"][branch] = key
|
|
@@ -191,6 +195,73 @@ def touch_phase(project_id: str, task_group: str, task_id: str, phase: str) -> N
|
|
|
191
195
|
_save(data)
|
|
192
196
|
|
|
193
197
|
|
|
198
|
+
def set_implementation_base(
|
|
199
|
+
project_id: str, task_group: str, task_id: str, commit: str,
|
|
200
|
+
) -> str:
|
|
201
|
+
"""Fix the shared base commit for this task's implementation stages,
|
|
202
|
+
once. Idempotent: if already set, the existing value is returned and
|
|
203
|
+
`commit` is ignored (so two concurrent first-stage runs converge).
|
|
204
|
+
Raises RuntimeError when the task-key entry does not exist."""
|
|
205
|
+
key = task_key(project_id, task_group, task_id)
|
|
206
|
+
with _registry_lock():
|
|
207
|
+
data = _load()
|
|
208
|
+
row = data["tasks"].get(key)
|
|
209
|
+
if row is None:
|
|
210
|
+
raise RuntimeError(
|
|
211
|
+
f"no task-key entry to anchor implementation base: {key}"
|
|
212
|
+
)
|
|
213
|
+
already = row.get("implementation_base_commit")
|
|
214
|
+
if already:
|
|
215
|
+
return already
|
|
216
|
+
row["implementation_base_commit"] = commit
|
|
217
|
+
_save(data)
|
|
218
|
+
return commit
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def get_implementation_base(
|
|
222
|
+
project_id: str, task_group: str, task_id: str,
|
|
223
|
+
) -> Optional[str]:
|
|
224
|
+
"""Return the fixed implementation base commit, or None when unset /
|
|
225
|
+
task-key missing."""
|
|
226
|
+
key = task_key(project_id, task_group, task_id)
|
|
227
|
+
with _registry_lock():
|
|
228
|
+
data = _load()
|
|
229
|
+
row = data["tasks"].get(key)
|
|
230
|
+
if row is None:
|
|
231
|
+
return None
|
|
232
|
+
return row.get("implementation_base_commit") or None
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def get_stage_row(
|
|
236
|
+
project_id: str, task_group: str, task_id: str, stage: int,
|
|
237
|
+
) -> Optional[dict]:
|
|
238
|
+
"""Return the stage-key registry row (worktree_path / base_ref / branch)
|
|
239
|
+
for `<task-key>#stage-<stage>`, or None when no such reservation exists."""
|
|
240
|
+
key = task_key(project_id, task_group, task_id, stage)
|
|
241
|
+
with _registry_lock():
|
|
242
|
+
data = _load()
|
|
243
|
+
return data["tasks"].get(key)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def list_active_stage_numbers(
|
|
247
|
+
project_id: str, task_group: str, task_id: str,
|
|
248
|
+
) -> set:
|
|
249
|
+
"""Return the set of stage numbers with an active stage-key reservation
|
|
250
|
+
for this task. Used by the stage resolver to exclude stages a concurrent
|
|
251
|
+
run already holds (the occupancy SSOT). Excludes the task-key entry
|
|
252
|
+
(stage is None) and released entries."""
|
|
253
|
+
prefix = task_key(project_id, task_group, task_id) + "#stage-"
|
|
254
|
+
with _registry_lock():
|
|
255
|
+
data = _load()
|
|
256
|
+
out = set()
|
|
257
|
+
for key, row in data["tasks"].items():
|
|
258
|
+
if (key.startswith(prefix)
|
|
259
|
+
and row.get("status") == "active"
|
|
260
|
+
and row.get("stage") is not None):
|
|
261
|
+
out.add(row["stage"])
|
|
262
|
+
return out
|
|
263
|
+
|
|
264
|
+
|
|
194
265
|
def release(project_id: str, task_group: str, task_id: str) -> Optional[WorktreeEntry]:
|
|
195
266
|
"""Mark the entry as `released` (worktree dir intact — preservation
|
|
196
267
|
is the project's policy). The branch index is freed so future
|
|
@@ -117,6 +117,8 @@
|
|
|
117
117
|
}
|
|
118
118
|
},
|
|
119
119
|
|
|
120
|
+
"verificationScope": { "enum": ["whole-task", "single-stage"] },
|
|
121
|
+
|
|
120
122
|
"clarificationCarryIn": {
|
|
121
123
|
"type": "object",
|
|
122
124
|
"description": "RENDER_IF non-empty. Carry-in clarifications from the previous run.",
|
|
@@ -337,6 +339,7 @@
|
|
|
337
339
|
"dependencyMigrationRisk",
|
|
338
340
|
"validationChecklist",
|
|
339
341
|
"rollbackStrategy",
|
|
342
|
+
"requirementCoverage",
|
|
340
343
|
"planBodyVerification"
|
|
341
344
|
],
|
|
342
345
|
"additionalProperties": false,
|
|
@@ -371,6 +374,11 @@
|
|
|
371
374
|
"minItems": 1,
|
|
372
375
|
"items": { "$ref": "#/$defs/RollbackRow" }
|
|
373
376
|
},
|
|
377
|
+
"requirementCoverage": {
|
|
378
|
+
"type": "array",
|
|
379
|
+
"minItems": 1,
|
|
380
|
+
"items": { "$ref": "#/$defs/ImplementationRequirementCoverageRow" }
|
|
381
|
+
},
|
|
374
382
|
"planBodyVerification": { "$ref": "#/$defs/PlanBodyVerification" }
|
|
375
383
|
}
|
|
376
384
|
},
|
|
@@ -565,6 +573,18 @@
|
|
|
565
573
|
"gitDiffStat": { "type": "string" }
|
|
566
574
|
}
|
|
567
575
|
},
|
|
576
|
+
"stageReports": {
|
|
577
|
+
"type": "array",
|
|
578
|
+
"items": {
|
|
579
|
+
"type": "object",
|
|
580
|
+
"additionalProperties": false,
|
|
581
|
+
"required": ["stage", "reportPath"],
|
|
582
|
+
"properties": {
|
|
583
|
+
"stage": { "type": "integer" },
|
|
584
|
+
"reportPath": { "type": "string" }
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
},
|
|
568
588
|
"acceptanceBlockers": {
|
|
569
589
|
"type": "array",
|
|
570
590
|
"items": { "$ref": "#/$defs/AcceptanceBlockerRow" }
|
|
@@ -1294,6 +1314,22 @@
|
|
|
1294
1314
|
}
|
|
1295
1315
|
},
|
|
1296
1316
|
|
|
1317
|
+
"ImplementationRequirementCoverageRow": {
|
|
1318
|
+
"type": "object",
|
|
1319
|
+
"required": ["id", "source", "requirement", "coveredBy", "status"],
|
|
1320
|
+
"additionalProperties": false,
|
|
1321
|
+
"properties": {
|
|
1322
|
+
"id": { "type": "string", "pattern": "^R-\\d{3,}$" },
|
|
1323
|
+
"source": { "type": "string", "minLength": 1 },
|
|
1324
|
+
"requirement": { "type": "string", "minLength": 1 },
|
|
1325
|
+
"coveredBy": { "type": "string", "minLength": 1 },
|
|
1326
|
+
"status": {
|
|
1327
|
+
"type": "string",
|
|
1328
|
+
"pattern": "^(covered|gap|blocked C-\\d{3,})$"
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
},
|
|
1332
|
+
|
|
1297
1333
|
"ReadonlyCommandRow": {
|
|
1298
1334
|
"type": "object",
|
|
1299
1335
|
"required": ["number", "tier", "command", "status", "exitCode", "outputTail"],
|
|
@@ -639,6 +639,7 @@ From the report-writer's draft of `## 5.5 Implementation Plan Deliverables`, lea
|
|
|
639
639
|
| `P-Dep-<N>` | `4.5.5 Dependency / Migration Risk` | one dependency row |
|
|
640
640
|
| `P-Val-<N>` | `4.5.6 Validation Checklist` | one checklist item |
|
|
641
641
|
| `P-Rb-<N>` | `4.5.7 Rollback Strategy` | one rollback path |
|
|
642
|
+
| `P-Req-<N>` | `4.5.8 Requirement Coverage` | one requirement coverage row |
|
|
642
643
|
|
|
643
644
|
`4.5.2 Trade-off Matrix` and `4.5.3 Recommended Option` are NOT extracted as standalone plan items — the trade-off matrix is evaluated implicitly through each option's `P-Opt-*` verification, and the recommended option is one of those `P-Opt-*` rows.
|
|
644
645
|
|
|
@@ -655,6 +656,7 @@ The verdict tokens `AGREE` / `DISAGREE` / `SUPPLEMENT` are reused, but their mea
|
|
|
655
656
|
- `c` — validation signal is not observable
|
|
656
657
|
- `d` — rollback violates commit / dependency order
|
|
657
658
|
- `e` — item contradicts the trade-off matrix
|
|
659
|
+
- `f` — requirement coverage row cites no concrete option / stage / step, cites a non-existent option / stage / step, or marks a requirement `covered` while the cited plan item does not satisfy the row's stated requirement
|
|
658
660
|
- **SUPPLEMENT**: the item is sound but is missing a dependency / edge case / precondition.
|
|
659
661
|
|
|
660
662
|
Worker non-result handling (`timeout`, `error`, no result file, wrapper `cli-failure`) is identical to finding convergence: do NOT aggregate as DISAGREE, record `contract-violation`, and apply the round-level abort rule below.
|
|
@@ -663,6 +665,8 @@ Worker non-result handling (`timeout`, `error`, no result file, wrapper `cli-fai
|
|
|
663
665
|
|
|
664
666
|
Plan-body verification only supports **lightweight mode** (defined in §"Verification Mode" above). `full-reanalysis` is not meaningful here because the "original source materials" for a plan item are the worker's own analysis plus the lead-mediated synthesis — there is no independent ground truth to re-read. The manifest's top-level `verificationMode` is ignored for this round; lightweight is always used.
|
|
665
667
|
|
|
668
|
+
Exception for `P-Req-*`: verifiers still MUST NOT re-open the original task brief for this round, but they MUST compare the requirement text embedded in the `Requirement Coverage` row with the cited Option / Stage / Step in the draft plan. A row is not sound merely because it says `covered`; the cited plan item must actually satisfy the stated requirement.
|
|
669
|
+
|
|
666
670
|
### Adversarial plan-body posture
|
|
667
671
|
|
|
668
672
|
When `config.adversarial == true` (the default for `implementation-planning`; see the top-level §"Configuration" table), the plan-body round runs with an **adversarial posture**. The classification rules and gate arithmetic in §"Round protocol" are UNCHANGED — `majority-disagree` (a *majority* of analysers DISAGREE) remains the only classification that blocks the Approval marker, and `dissent-isolated` still passes the gate. Adversarial mode changes only *how each verifier evaluates an item*:
|
|
@@ -670,6 +674,7 @@ When `config.adversarial == true` (the default for `implementation-planning`; se
|
|
|
670
674
|
- The burden of proof sits on the plan: an item earns `AGREE` only if the verifier actively tried to break it and could not.
|
|
671
675
|
- The verifier MUST open the file paths / symbols / commands the item cites and confirm they exist and are executable as written. This is the one allowed widening of the lightweight "judge from internal consistency and stated commands / paths" rule — confirming the existence of cited paths is not "re-analyzing the original requirements".
|
|
672
676
|
- If a cited path / command / validation signal cannot be confirmed, the verifier responds `DISAGREE(<kind>)` with the applicable breakage kind (a–e); uncertainty resolves toward DISAGREE, not AGREE.
|
|
677
|
+
- For `P-Req-*` items, a single `DISAGREE(f)` is approval-blocking even in a two-worker roster. Requirement coverage is a hard gate: one verified contradiction between a requirement row and its cited plan item creates a `majority-disagree` classification for gate purposes and MUST become a `Blocks=approval` clarification row.
|
|
673
678
|
|
|
674
679
|
Plan-body verification stays **lightweight** even under this posture — the `verificationMode = "full-reanalysis"` forcing in §"Adversarial Verification Mode" applies to finding convergence only (see §"Mode constraint"); the adversarial posture here only changes verifier behaviour, not the mode. This raises verification *quality* (active refutation, plan-side burden) without changing the gate *threshold* — a single dissent still does not block approval; a majority is required (deliberate design decision).
|
|
675
680
|
|
|
@@ -682,7 +687,7 @@ Plan-body verification stays **lightweight** even under this posture — the `ve
|
|
|
682
687
|
- `full-consensus` — all participating analysers `AGREE` (SUPPLEMENT counts as agree on the item itself).
|
|
683
688
|
- `partial-consensus` — majority `AGREE`, dissenting `DISAGREE` recorded.
|
|
684
689
|
- `dissent-isolated` — only one worker `DISAGREE`s, others `AGREE` — treat as `partial-consensus` for gate purposes; record dissent. (Distinct from finding-convergence `worker-unique`, which means the *opposite*: only one worker AGREEs. Plan-body classifications use this dedicated label to avoid the collision.)
|
|
685
|
-
- `majority-disagree` — majority of analysers `DISAGREE` on this item
|
|
690
|
+
- `majority-disagree` — majority of analysers `DISAGREE` on this item, OR any analyser emits `DISAGREE(f)` for a `P-Req-*` item. This classification **blocks the Approval marker**.
|
|
686
691
|
- `contested` only meaningful when `maxRounds > 1`; at default `maxRounds=1`, fold any unresolved item into `partial-consensus`.
|
|
687
692
|
5. Gate result resolution:
|
|
688
693
|
- any `majority-disagree` item present AND `gating=true` → `blocked-by-disagreement`
|
|
@@ -737,7 +742,7 @@ Plan-body verification stays **lightweight** even under this posture — the `ve
|
|
|
737
742
|
|
|
738
743
|
`planItems[].classification` enum: `full-consensus | partial-consensus | dissent-isolated | majority-disagree | contested`. `contested` only appears when `maxRounds > 1`; at default `maxRounds=1` any otherwise-unresolved item folds into `partial-consensus` per the round protocol above.
|
|
739
744
|
|
|
740
|
-
`planItems[].votes.<worker>` is the verbatim verdict token emitted by the worker — `AGREE | DISAGREE(<a|b|c|d|e>) | SUPPLEMENT` — or `verification-error` for terminal non-result dispatches. The `DISAGREE` token retains its `<kind>` suffix so the breakage class is recoverable from the state file alone.
|
|
745
|
+
`planItems[].votes.<worker>` is the verbatim verdict token emitted by the worker — `AGREE | DISAGREE(<a|b|c|d|e|f>) | SUPPLEMENT` — or `verification-error` for terminal non-result dispatches. The `DISAGREE` token retains its `<kind>` suffix so the breakage class is recoverable from the state file alone.
|
|
741
746
|
|
|
742
747
|
### Plan-body reverify prompt
|
|
743
748
|
|
|
@@ -759,7 +764,8 @@ verdict:
|
|
|
759
764
|
(b) command is not executable or is ambiguous,
|
|
760
765
|
(c) validation signal is not observable,
|
|
761
766
|
(d) rollback violates commit / dependency order,
|
|
762
|
-
(e) item contradicts the trade-off matrix
|
|
767
|
+
(e) item contradicts the trade-off matrix,
|
|
768
|
+
(f) requirement coverage row does not actually map the stated requirement to a concrete satisfying option / stage / step.
|
|
763
769
|
- **SUPPLEMENT**: The item is sound but a dependency / edge case / precondition
|
|
764
770
|
is missing.
|
|
765
771
|
|
|
@@ -767,6 +773,11 @@ Do NOT re-analyze the original requirements. Judge solely from plan internal
|
|
|
767
773
|
consistency and stated commands / paths. Do NOT inspect the original task brief
|
|
768
774
|
or worker analyses for this round.
|
|
769
775
|
|
|
776
|
+
For `P-Req-*` items, compare only the requirement text embedded in the row
|
|
777
|
+
against the cited plan item(s). Do not open the original brief, but do reject
|
|
778
|
+
coverage rows that cite no concrete option/stage/step or cite a plan item that
|
|
779
|
+
does not satisfy the row's own requirement.
|
|
780
|
+
|
|
770
781
|
## Plan items to verify
|
|
771
782
|
|
|
772
783
|
### P-Step-3 [TICKETID: <id>]: <one-line summary>
|
|
@@ -122,7 +122,7 @@ That is the entire interactive flow. The wizard handles:
|
|
|
122
122
|
- task-type pick (with `nextRecommendedPhase` surfaced as recommended for existing tasks),
|
|
123
123
|
- brief path (with `유지 / 변경` for existing tasks),
|
|
124
124
|
- base-ref pick + git rev-parse validation (skipped when reusing an active worktree),
|
|
125
|
-
- `implementation`-only sub-flow: approved-plan path (frontmatter `approved: true` check) + executor pick,
|
|
125
|
+
- `implementation`-only sub-flow: approved-plan path (frontmatter `approved: true` check) + stage pick (`auto` = 의존성 충족된 가장 빠른 미완료 stage, 또는 특정 stage 번호) + executor pick,
|
|
126
126
|
- `Use defaults / Customize` branch with profile-aware worker/model questions,
|
|
127
127
|
- `release-handoff` PR template override + persist scope,
|
|
128
128
|
- final `Proceed / Edit` confirmation; on `Edit` the wizard asks which step to rewind to and clears every later answer.
|
|
@@ -211,6 +211,14 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
211
211
|
| {{ row.id }} | {{ row.step }} | `{{ row.action }}` | {{ row.triggerSignal }} | `{{ row.verificationMethod }}` |
|
|
212
212
|
{% endfor %}
|
|
213
213
|
|
|
214
|
+
### 5.5.8 Requirement Coverage
|
|
215
|
+
|
|
216
|
+
| ID | Source | Requirement | Covered by option / stage / step | Status |
|
|
217
|
+
|----|--------|-------------|-----------------------------------|--------|
|
|
218
|
+
{% for row in implementationPlanning.requirementCoverage -%}
|
|
219
|
+
| {{ row.id }} | `{{ row.source }}` | {{ row.requirement }} | {{ row.coveredBy }} | `{{ row.status }}` |
|
|
220
|
+
{% endfor %}
|
|
221
|
+
|
|
214
222
|
### 5.5.9 Plan Body Verification{% if t("sectionAside.planBodyVerification") != "Plan Body Verification" %} ({{ t("sectionAside.planBodyVerification") }}){% endif %}
|
|
215
223
|
|
|
216
224
|
{{ t("sectionIntro.planBodyVerification") }}
|
|
@@ -416,6 +424,10 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
416
424
|
```
|
|
417
425
|
{{ finalVerification.sourceImplementationReport.gitDiffStat }}
|
|
418
426
|
```
|
|
427
|
+
{% if verificationScope in ['whole-task', 'single-stage'] %}- {{ t("finalVerification.verificationScope") }}: `{{ verificationScope }}`
|
|
428
|
+
{% endif %}{% if finalVerification.stageReports %}- {{ t("finalVerification.stageReportsLabel") }}:
|
|
429
|
+
{% for row in finalVerification.stageReports %} - stage {{ row.stage }}: `{{ row.reportPath }}`
|
|
430
|
+
{% endfor %}{% endif %}
|
|
419
431
|
|
|
420
432
|
### 5.8.2 Acceptance Blockers
|
|
421
433
|
|
|
@@ -29,15 +29,18 @@ taskType: "{{FM_TASK_TYPE}}"
|
|
|
29
29
|
- What was supposed to be delivered?
|
|
30
30
|
- What is the intended acceptance decision?
|
|
31
31
|
|
|
32
|
+
## 검증 모드
|
|
33
|
+
|
|
34
|
+
- 기본은 **전체-task** 검증입니다(`--stage auto`): 모든 Stage Map stage 가 구현·머지된 뒤 한 번 실행합니다.
|
|
35
|
+
- 특정 stage 만 격리 검증하려면 `--stage N` 으로 **단독-stage** 모드를 씁니다(release-handoff 진입 불가, 부분 검증).
|
|
36
|
+
- worktree / base / head 는 okstra 가 registry 와 `consumers.jsonl` 에서 자동 해소하므로 이 입력서에 수동 기입하지 않습니다.
|
|
37
|
+
|
|
32
38
|
## Source Implementation Report
|
|
33
39
|
|
|
34
40
|
- Path (project-relative) to the originating `implementation` final-report:
|
|
35
|
-
- Worktree / checkout path that final-verification must inspect:
|
|
36
|
-
- Implementation base ref (`<base>` for `git diff --stat <base>..HEAD`):
|
|
37
|
-
- Implementation head SHA expected at verification start:
|
|
38
41
|
- Quoted `Commit list` / `Diff summary` excerpt from the implementation report:
|
|
39
42
|
|
|
40
|
-
>
|
|
43
|
+
> 보고서 경로가 비거나 누락된 보고서를 가리키면 final-verification 은 status `blocked` 으로 끝내고 `implementation` 또는 `implementation-planning` 으로 라우팅합니다. 검증 대상(worktree/base/head)은 okstra 가 자동 해소하므로 수동 기입이 어긋나 막히는 일은 없습니다.
|
|
41
44
|
|
|
42
45
|
## Requirement Coverage Source
|
|
43
46
|
|
|
@@ -87,7 +90,7 @@ taskType: "{{FM_TASK_TYPE}}"
|
|
|
87
90
|
|
|
88
91
|
## Questions for Analysers
|
|
89
92
|
|
|
90
|
-
1.
|
|
93
|
+
1. Did your analysis run against the injected `VERIFICATION_TARGET` (base / head SHA / worktree), and does the diff at that target fully cover the stage(s) under verification? (A head you cannot confirm against the injected target is a `tool-failure`, not a silent proceed.)
|
|
91
94
|
2. For each requirement / acceptance criterion, what exact artifact (commit SHA, test output, log line, config value) proves coverage?
|
|
92
95
|
3. Are there any acceptance blockers?
|
|
93
96
|
4. What residual risks remain?
|
|
@@ -134,6 +134,8 @@
|
|
|
134
134
|
},
|
|
135
135
|
"finalVerification": {
|
|
136
136
|
"validationEvidenceAside": "requirements coverage",
|
|
137
|
-
"columnRequirement": "Requirement (plan/brief citation)"
|
|
137
|
+
"columnRequirement": "Requirement (plan/brief citation)",
|
|
138
|
+
"verificationScope": "Verification scope",
|
|
139
|
+
"stageReportsLabel": "Source implementation reports (per stage)"
|
|
138
140
|
}
|
|
139
141
|
}
|
|
@@ -134,6 +134,8 @@
|
|
|
134
134
|
},
|
|
135
135
|
"finalVerification": {
|
|
136
136
|
"validationEvidenceAside": "요구사항 커버리지",
|
|
137
|
-
"columnRequirement": "Requirement (plan/brief 인용)"
|
|
137
|
+
"columnRequirement": "Requirement (plan/brief 인용)",
|
|
138
|
+
"verificationScope": "검증 범위",
|
|
139
|
+
"stageReportsLabel": "stage 별 구현 리포트"
|
|
138
140
|
}
|
|
139
141
|
}
|