okstra 0.223.2 → 0.224.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/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/coding-preflight/clean-code.md +8 -1
- package/runtime/python/okstra_ctl/incremental_carry.py +10 -16
- package/runtime/python/okstra_ctl/phases/final_verification/profile.md +1 -1
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-diff-review.md +1 -1
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-verifier.md +2 -2
- package/runtime/python/okstra_ctl/phases/implementation_planning/authoring.py +98 -33
- package/runtime/python/okstra_ctl/phases/implementation_planning/instructions/plan-body-verification.md +22 -2
- package/runtime/python/okstra_ctl/phases/implementation_planning/plan_body.py +33 -0
- package/runtime/python/okstra_ctl/plan_item_evidence.py +237 -0
- package/runtime/python/okstra_ctl/plan_items.py +12 -9
- package/runtime/python/okstra_ctl/plan_review_context.py +82 -0
- package/runtime/python/okstra_ctl/verdict_blocks.py +17 -1
- package/runtime/python/okstra_ctl/wizard/engine.py +26 -5
- package/runtime/python/okstra_ctl/wizard/registry.py +2 -0
- package/runtime/python/okstra_ctl/wizard/state.py +5 -0
- package/runtime/python/okstra_ctl/wizard/steps_identity.py +99 -16
- package/runtime/python/okstra_ctl/wizard/steps_options.py +11 -0
- package/runtime/python/okstra_ctl/wizard/steps_plan.py +14 -0
- package/runtime/schemas/final-report-v2.0.schema.json +3 -1
- package/runtime/schemas/final-report-v3.0.schema.json +3 -1
|
@@ -6,12 +6,15 @@ from pathlib import Path
|
|
|
6
6
|
from collections.abc import Mapping
|
|
7
7
|
from typing import Any, Optional
|
|
8
8
|
|
|
9
|
-
from okstra_ctl import next_phase
|
|
9
|
+
from okstra_ctl import next_phase, worktree_registry
|
|
10
|
+
from okstra_ctl.consumers import latest_done_by_stage, read_consumers
|
|
10
11
|
from okstra_ctl.ids import slugify_task_segment
|
|
12
|
+
from okstra_ctl.paths import RunRef
|
|
11
13
|
from okstra_ctl.group_context import (
|
|
12
14
|
QUEUE_DONE,
|
|
13
15
|
QUEUE_IN_PROGRESS,
|
|
14
16
|
QUEUE_NOT_STARTED,
|
|
17
|
+
group_briefs,
|
|
15
18
|
group_context_file,
|
|
16
19
|
group_queue,
|
|
17
20
|
is_group_context_file,
|
|
@@ -79,15 +82,18 @@ def _validate_base_ref(ref: str, project_root: Path) -> str:
|
|
|
79
82
|
ref = (ref or "").strip()
|
|
80
83
|
if not ref:
|
|
81
84
|
raise WizardError("base ref is empty")
|
|
82
|
-
|
|
85
|
+
if not _ref_resolves(_git_main_worktree(project_root), ref):
|
|
86
|
+
raise WizardError(f"base ref not found in repository: {ref}")
|
|
87
|
+
return ref
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _ref_resolves(main_wt: Path, ref: str) -> bool:
|
|
83
91
|
rc = subprocess.call(
|
|
84
92
|
["git", "-C", str(main_wt), "rev-parse",
|
|
85
93
|
"--verify", "--quiet", f"{ref}^{{commit}}"],
|
|
86
94
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
87
95
|
)
|
|
88
|
-
|
|
89
|
-
raise WizardError(f"base ref not found in repository: {ref}")
|
|
90
|
-
return ref
|
|
96
|
+
return rc == 0
|
|
91
97
|
|
|
92
98
|
|
|
93
99
|
# --- builders ---
|
|
@@ -605,8 +611,12 @@ def _build_task_type(state: WizardState) -> Prompt:
|
|
|
605
611
|
brief_phase = _brief_recommended_task_type(state) if fresh_task else ""
|
|
606
612
|
recommended = (revision_requested or state.task_type
|
|
607
613
|
or _recommended_task_type(state, manifest))
|
|
614
|
+
# Only a recommendation okstra recorded is taken without asking: the pointer,
|
|
615
|
+
# a revision request, or the brief's own entry phase.
|
|
616
|
+
auto_value = recommended
|
|
608
617
|
if not recommended and not workflow:
|
|
609
618
|
recommended = state.requested_task_type or brief_phase or TASK_TYPE_VALUES[0]
|
|
619
|
+
auto_value = recommended if recommended in (state.requested_task_type, brief_phase) else ""
|
|
610
620
|
if brief_phase and recommended == brief_phase:
|
|
611
621
|
recommended_suffix = t["options"].get("_BRIEF_SUFFIX", recommended_suffix)
|
|
612
622
|
pointer = next_phase.promote(workflow.get("nextRecommendedPhase"))
|
|
@@ -638,7 +648,8 @@ def _build_task_type(state: WizardState) -> Prompt:
|
|
|
638
648
|
options.append(_opt(PICK_TYPE_CUSTOM, t["options"][PICK_TYPE_CUSTOM]))
|
|
639
649
|
return Prompt(step=S_TASK_TYPE, kind="pick",
|
|
640
650
|
label=t["label"], options=options,
|
|
641
|
-
echo_template=t["echo_template"]
|
|
651
|
+
echo_template=t["echo_template"],
|
|
652
|
+
auto_value=auto_value)
|
|
642
653
|
|
|
643
654
|
|
|
644
655
|
def _carry_in_existing_brief(state: WizardState) -> str:
|
|
@@ -835,28 +846,100 @@ def _build_brief_carry(state: WizardState) -> Prompt:
|
|
|
835
846
|
)
|
|
836
847
|
|
|
837
848
|
|
|
838
|
-
def
|
|
839
|
-
"""The main worktree's branch when
|
|
849
|
+
def _checked_out_branch(main_wt: Path) -> str:
|
|
850
|
+
"""The main worktree's branch, or '' when HEAD is detached.
|
|
840
851
|
|
|
841
852
|
Projects differ in their integration branch (main vs preprod), and nothing in
|
|
842
853
|
okstra records it; the branch the user keeps checked out is the observed hint.
|
|
843
854
|
"""
|
|
844
|
-
try:
|
|
845
|
-
main_wt = _git_main_worktree(project_root)
|
|
846
|
-
except WizardError:
|
|
847
|
-
return ""
|
|
848
855
|
result = subprocess.run(
|
|
849
856
|
["git", "-C", str(main_wt), "symbolic-ref", "--quiet", "--short", "HEAD"],
|
|
850
857
|
capture_output=True, text=True,
|
|
851
858
|
)
|
|
852
|
-
|
|
853
|
-
|
|
859
|
+
return result.stdout.strip() if result.returncode == 0 else ""
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
def _recorded_stage_branches(state: WizardState, task_id: str) -> list[str]:
|
|
863
|
+
"""A task's stage branches, top of the stack first: PR heads, then done stages.
|
|
864
|
+
|
|
865
|
+
release-handoff `record-pr` writes a `pr` row per stage PR; a task that never
|
|
866
|
+
reached handoff only has `done` rows, whose branch lives in the worktree
|
|
867
|
+
registry's stage row (kept after the stage is released).
|
|
868
|
+
"""
|
|
869
|
+
key = f"{state.project_id}:{state.task_group}:{task_id}"
|
|
870
|
+
task_root = find_task_root(Path(state.project_root), key)
|
|
871
|
+
if task_root is None:
|
|
872
|
+
return []
|
|
873
|
+
rows = read_consumers(RunRef.from_task_root(task_root, "implementation-planning").run_dir)
|
|
874
|
+
prs = [row for row in rows if row.get("status") == "pr" and row.get("branch")]
|
|
875
|
+
prs.sort(key=lambda row: max(row.get("stages") or [0]), reverse=True)
|
|
876
|
+
branches = [str(row["branch"]) for row in prs]
|
|
877
|
+
for stage in sorted(latest_done_by_stage(rows), reverse=True):
|
|
878
|
+
stage_row = worktree_registry.get_stage_row(
|
|
879
|
+
state.project_id, state.task_group, task_id, stage) or {}
|
|
880
|
+
if stage_row.get("branch"):
|
|
881
|
+
branches.append(str(stage_row["branch"]))
|
|
882
|
+
return branches
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
def _predecessor_base_ref(state: WizardState, main_wt: Path) -> tuple[str, bool]:
|
|
886
|
+
"""(branch, succession) from the nearest earlier task in the group's start order.
|
|
887
|
+
|
|
888
|
+
A follow-up task in a group usually builds on the previous task's unmerged
|
|
889
|
+
stage branches. `succession` is True when okstra records that this task
|
|
890
|
+
follows that one: it is the next row in the start order (what the
|
|
891
|
+
predecessor's closeout named as `nextInGroup`), or this task's brief waits
|
|
892
|
+
for that ticket in its Related Task Graph.
|
|
893
|
+
"""
|
|
894
|
+
if not (state.project_root and state.task_group and state.task_id):
|
|
895
|
+
return "", False
|
|
896
|
+
project_root = Path(state.project_root)
|
|
897
|
+
queue = _group_start_order(project_root, state.task_group)
|
|
898
|
+
brief = _project_relative_path(Path(state.brief_path), project_root) if state.brief_path else ""
|
|
899
|
+
current = next(
|
|
900
|
+
(index for index, row in enumerate(queue)
|
|
901
|
+
if row.task_id == state.task_id or (brief and row.brief == brief)),
|
|
902
|
+
None,
|
|
903
|
+
)
|
|
904
|
+
if current is None:
|
|
905
|
+
return "", False
|
|
906
|
+
waits: set[str] = set()
|
|
907
|
+
for item in group_briefs(project_root, state.task_group):
|
|
908
|
+
waits.update(item["waits_for"].get(queue[current].ticket_id, set()))
|
|
909
|
+
for index in range(current - 1, -1, -1):
|
|
910
|
+
row = queue[index]
|
|
911
|
+
branch = next(
|
|
912
|
+
(b for b in _recorded_stage_branches(state, row.task_id) if _ref_resolves(main_wt, b)),
|
|
913
|
+
"",
|
|
914
|
+
)
|
|
915
|
+
if branch:
|
|
916
|
+
return branch, index == current - 1 or row.ticket_id in waits
|
|
917
|
+
return "", False
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
def _base_ref_candidates(state: WizardState) -> tuple[list[str], str]:
|
|
921
|
+
"""(refs in display order, recommended ref or '')."""
|
|
922
|
+
try:
|
|
923
|
+
main_wt = _git_main_worktree(Path(state.project_root))
|
|
924
|
+
except WizardError:
|
|
925
|
+
return list(CANONICAL_BASE_REFS), ""
|
|
926
|
+
predecessor, succession = _predecessor_base_ref(state, main_wt)
|
|
927
|
+
checked_out = _checked_out_branch(main_wt)
|
|
928
|
+
derived = [predecessor] if predecessor else []
|
|
929
|
+
if (checked_out and checked_out not in CANONICAL_BASE_REFS
|
|
930
|
+
and checked_out not in derived and _ref_resolves(main_wt, checked_out)):
|
|
931
|
+
derived.append(checked_out)
|
|
932
|
+
if succession:
|
|
933
|
+
recommended = predecessor
|
|
934
|
+
else:
|
|
935
|
+
recommended = checked_out if checked_out in CANONICAL_BASE_REFS else ""
|
|
936
|
+
refs = derived + [r for r in CANONICAL_BASE_REFS if r not in derived]
|
|
937
|
+
return sorted(refs, key=lambda r: r != recommended), recommended
|
|
854
938
|
|
|
855
939
|
|
|
856
940
|
def _build_base_ref_pick(state: WizardState) -> Prompt:
|
|
857
941
|
t = _p(state.workspace_root, "base_ref_pick")
|
|
858
|
-
recommended =
|
|
859
|
-
refs = sorted(CANONICAL_BASE_REFS, key=lambda r: r != recommended)
|
|
942
|
+
refs, recommended = _base_ref_candidates(state)
|
|
860
943
|
options = [_opt(r, recommended=r == recommended) for r in refs]
|
|
861
944
|
for value, label in _static_options(t):
|
|
862
945
|
options.append(_opt(value=value, label=label))
|
|
@@ -120,6 +120,9 @@ class _OptionalCachedPickSpec:
|
|
|
120
120
|
# 추천 옵션 라벨·echo 뒤에 덧붙일 동적 안내(예: 함께 첨부될 사이드카 개수).
|
|
121
121
|
# (state, 추천 relpath) -> note. 미설정 picker 는 영향 없음.
|
|
122
122
|
recommend_note: Optional[Callable[["WizardState", str], str]] = None
|
|
123
|
+
# Whether the wizard takes the suggestion without asking. A previous directive
|
|
124
|
+
# is an instruction written for another run, so reusing it stays a question.
|
|
125
|
+
auto_take_suggestion: bool = True
|
|
123
126
|
|
|
124
127
|
|
|
125
128
|
def _build_optional_cached_pick(state: WizardState, spec: _OptionalCachedPickSpec) -> Prompt:
|
|
@@ -137,10 +140,17 @@ def _build_optional_cached_pick(state: WizardState, spec: _OptionalCachedPickSpe
|
|
|
137
140
|
setattr(state, spec.cache_attr, suggestion)
|
|
138
141
|
options.append(_opt(PICK_SKIP, t["options"][PICK_SKIP]))
|
|
139
142
|
options.append(_opt(PICK_TYPE_CUSTOM, t["options"][PICK_TYPE_CUSTOM]))
|
|
143
|
+
if not suggestion:
|
|
144
|
+
auto_value = PICK_SKIP
|
|
145
|
+
elif spec.auto_take_suggestion:
|
|
146
|
+
auto_value = spec.recommend_token
|
|
147
|
+
else:
|
|
148
|
+
auto_value = ""
|
|
140
149
|
return Prompt(
|
|
141
150
|
step=spec.step, kind="pick",
|
|
142
151
|
label=t["label"], options=options,
|
|
143
152
|
echo_template=t["echo_template"],
|
|
153
|
+
auto_value=auto_value,
|
|
144
154
|
)
|
|
145
155
|
|
|
146
156
|
|
|
@@ -199,6 +209,7 @@ _DIRECTIVE_PICK_SPEC = _OptionalCachedPickSpec(
|
|
|
199
209
|
cache_attr="last_directive_cached", target_attr="directive",
|
|
200
210
|
pending_attr="directive_pending_text",
|
|
201
211
|
recommend_note=_directive_context_note,
|
|
212
|
+
auto_take_suggestion=False,
|
|
202
213
|
)
|
|
203
214
|
|
|
204
215
|
|
|
@@ -276,6 +276,7 @@ def _build_approved_plan_pick(state: WizardState) -> Prompt:
|
|
|
276
276
|
step=S_APPROVED_PLAN_PICK, kind="pick",
|
|
277
277
|
label=label, options=options,
|
|
278
278
|
echo_template=t["echo_template"],
|
|
279
|
+
auto_value=PICK_USE_DEFAULT if default is not None else "",
|
|
279
280
|
)
|
|
280
281
|
|
|
281
282
|
|
|
@@ -341,9 +342,22 @@ def _build_approve_plan_confirm(state: WizardState) -> Prompt:
|
|
|
341
342
|
for k, v in options_map.items()
|
|
342
343
|
],
|
|
343
344
|
echo_template=t["echo_template"],
|
|
345
|
+
auto_value="yes" if _already_approved(state) else "",
|
|
344
346
|
)
|
|
345
347
|
|
|
346
348
|
|
|
349
|
+
def _already_approved(state: WizardState) -> bool:
|
|
350
|
+
"""An approved plan with no exported approval sidecar leaves nothing to decide."""
|
|
351
|
+
if state.html_approval_sidecar or not state.approve_plan_candidate:
|
|
352
|
+
return False
|
|
353
|
+
try:
|
|
354
|
+
_, fully_approved = _classify_approved_plan(
|
|
355
|
+
state.approve_plan_candidate, Path(state.project_root))
|
|
356
|
+
except (PrepareError, WizardError):
|
|
357
|
+
return False
|
|
358
|
+
return fully_approved
|
|
359
|
+
|
|
360
|
+
|
|
347
361
|
def _submit_approve_plan_confirm(state: WizardState, value: str) -> Optional[str]:
|
|
348
362
|
allowed = ("yes_apply", "yes", "no") if state.html_approval_sidecar else ("yes", "no")
|
|
349
363
|
if value not in allowed:
|
|
@@ -9557,7 +9557,8 @@
|
|
|
9557
9557
|
"block": {"type": "string", "enum": ["execution", "record"]},
|
|
9558
9558
|
"stageScope": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "integer", "minimum": 1}},
|
|
9559
9559
|
"contentHash": {"type": "string", "pattern": "^[a-f0-9]{64}$", "description": "SHA-256 of subject+payload+block+stageScope for the current extract."},
|
|
9560
|
-
"verifiedContentHash": {"type": "string", "pattern": "^[a-f0-9]{64}$", "description": "
|
|
9560
|
+
"verifiedContentHash": {"type": "string", "pattern": "^[a-f0-9]{64}$", "description": "Body hash at the last apply-verdicts; insufficient on its own for runtime verdict reuse."},
|
|
9561
|
+
"verifiedReviewHash": {"type": "string", "pattern": "^[a-f0-9]{64}$", "description": "Runtime fingerprint of the judged body, declared evidence closure, contract, roster and detailed votes."},
|
|
9561
9562
|
"id": {
|
|
9562
9563
|
"type": "string",
|
|
9563
9564
|
"minLength": 1
|
|
@@ -9597,6 +9598,7 @@
|
|
|
9597
9598
|
"additionalProperties": false,
|
|
9598
9599
|
"properties": {
|
|
9599
9600
|
"claimKind": {"description": "Set only on DISAGREE. `fact` claims are settled by reproduction, `judgement` by quorum.", "enum": ["fact", "judgement", null]},
|
|
9601
|
+
"dependencies": {"type": "object", "required": ["complete", "itemIds", "paths"], "additionalProperties": false, "properties": {"complete": {"type": "boolean"}, "itemIds": {"type": "array", "items": {"type": "string", "minLength": 1}}, "paths": {"type": "array", "items": {"type": "string", "minLength": 1}}}},
|
|
9600
9602
|
"reproduction": {"description": "Set only on a `fact` claim. The probe okstra runs; see okstra_ctl/claim_reproduction.py.", "type": ["object", "null"]},
|
|
9601
9603
|
"reproductionResult": {"description": "Written by okstra, not by the worker.", "enum": ["reproduced", "not-reproduced", "not-runnable", null]},
|
|
9602
9604
|
"worker": {
|
|
@@ -9849,7 +9849,8 @@
|
|
|
9849
9849
|
"block": {"type": "string", "enum": ["execution", "record"]},
|
|
9850
9850
|
"stageScope": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "integer", "minimum": 1}},
|
|
9851
9851
|
"contentHash": {"type": "string", "pattern": "^[a-f0-9]{64}$", "description": "SHA-256 of subject+payload+block+stageScope for the current extract."},
|
|
9852
|
-
"verifiedContentHash": {"type": "string", "pattern": "^[a-f0-9]{64}$", "description": "
|
|
9852
|
+
"verifiedContentHash": {"type": "string", "pattern": "^[a-f0-9]{64}$", "description": "Body hash at the last apply-verdicts; insufficient on its own for runtime verdict reuse."},
|
|
9853
|
+
"verifiedReviewHash": {"type": "string", "pattern": "^[a-f0-9]{64}$", "description": "Runtime fingerprint of the judged body, declared evidence closure, contract, roster and detailed votes."},
|
|
9853
9854
|
"id": {
|
|
9854
9855
|
"type": "string",
|
|
9855
9856
|
"minLength": 1
|
|
@@ -9896,6 +9897,7 @@
|
|
|
9896
9897
|
"additionalProperties": false,
|
|
9897
9898
|
"properties": {
|
|
9898
9899
|
"claimKind": {"description": "Set only on DISAGREE. `fact` claims are settled by reproduction, `judgement` by quorum.", "enum": ["fact", "judgement", null]},
|
|
9900
|
+
"dependencies": {"type": "object", "required": ["complete", "itemIds", "paths"], "additionalProperties": false, "properties": {"complete": {"type": "boolean"}, "itemIds": {"type": "array", "items": {"type": "string", "minLength": 1}}, "paths": {"type": "array", "items": {"type": "string", "minLength": 1}}}},
|
|
9899
9901
|
"reproduction": {"description": "Set only on a `fact` claim. The probe okstra runs; see okstra_ctl/claim_reproduction.py.", "type": ["object", "null"]},
|
|
9900
9902
|
"reproductionResult": {"description": "Written by okstra, not by the worker.", "enum": ["reproduced", "not-reproduced", "not-runnable", null]},
|
|
9901
9903
|
"worker": {
|