okstra 0.219.1 → 0.220.1
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 +18 -0
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/lib/paths.mjs +1 -1
- package/dist/lib/paths.mjs.map +1 -1
- package/docs/cli.md +4 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/lead/okstra-lead-contract.md +7 -4
- package/runtime/python/okstra_ctl/blocking_checks.py +8 -0
- package/runtime/python/okstra_ctl/business_flow/engine.py +10 -2
- package/runtime/python/okstra_ctl/convergence_provenance.py +8 -2
- package/runtime/python/okstra_ctl/correction_cli.py +59 -0
- package/runtime/python/okstra_ctl/correction_round.py +168 -0
- package/runtime/python/okstra_ctl/dispatch_core.py +18 -0
- package/runtime/python/okstra_ctl/lead_progress.py +56 -11
- package/runtime/python/okstra_ctl/manager_cli.py +2 -0
- package/runtime/python/okstra_ctl/manager_launch.py +23 -2
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-deliverable.md +29 -6
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-verifier.md +2 -0
- package/runtime/python/okstra_ctl/phases/implementation/profile.md +2 -1
- package/runtime/python/okstra_ctl/phases/implementation/report_assets/implementation.template.html +6 -0
- package/runtime/python/okstra_ctl/phases/implementation/spec.md +2 -1
- package/runtime/python/okstra_ctl/phases/implementation/validation.py +62 -1
- package/runtime/python/okstra_ctl/phases/release_handoff/boundary.json +2 -2
- package/runtime/python/okstra_ctl/phases/release_handoff/profile.md +2 -2
- package/runtime/python/okstra_ctl/phases/release_handoff/spec.md +1 -1
- package/runtime/python/okstra_ctl/report_assembly.py +5 -5
- package/runtime/python/okstra_ctl/report_inputs.py +10 -1
- package/runtime/python/okstra_ctl/stage_done.py +183 -0
- package/runtime/python/okstra_ctl/stage_fail.py +114 -0
- package/runtime/python/okstra_ctl/stage_reopen.py +12 -8
- package/runtime/python/okstra_ctl/wizard/engine.py +1 -2
- package/runtime/schemas/final-report-v3.0.schema.json +78 -0
- package/runtime/skills/okstra-manager/SKILL.md +2 -2
- package/runtime/templates/reports/html/i18n/en.json +6 -0
- package/runtime/templates/reports/html/i18n/ko.json +6 -0
- package/runtime/validators/validate-run.py +2 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""`okstra stage-done` — settle a passing implementation stage: carry sidecar, then `done` row.
|
|
2
|
+
|
|
3
|
+
Both writes were lead hand-work. The `done` row needed a python call to `append_consumer`,
|
|
4
|
+
which Claude Code auto mode blocks, and the carry path in `_implementation-deliverable.md`
|
|
5
|
+
(`runs/<impl-task-key>/carry/stage-<N>.json`) read as the staged run directory: measured
|
|
6
|
+
2026-10-17 (figma-plugin dev-11377 run 002), the lead wrote
|
|
7
|
+
`runs/implementation/stage-1/carry/stage-1.json`, validate-run looked for the flat
|
|
8
|
+
`runs/implementation/carry/stage-1.json`, and the `done` row kept the wrong `carry_path`.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from okstra_project import ResolverError, StateError, resolve_project_root
|
|
19
|
+
|
|
20
|
+
from .consumers import (
|
|
21
|
+
FAILED_CARRY_STATUSES,
|
|
22
|
+
append_consumer,
|
|
23
|
+
carry_head_commit,
|
|
24
|
+
read_consumers,
|
|
25
|
+
)
|
|
26
|
+
from .json_boundary import JsonBoundaryError, load_owned_object, write_owned_object_atomic
|
|
27
|
+
from .paths import RunRef
|
|
28
|
+
from .stage_reopen import resolve_planned_stage
|
|
29
|
+
|
|
30
|
+
_REPORT_LABEL_RE = re.compile(r"^final-report-(?P<label>[a-z-]+-\d+)\.")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _read_carry(carry_file: Path, stage: int) -> dict:
|
|
34
|
+
try:
|
|
35
|
+
carry = load_owned_object(carry_file, artifact="implementation stage carry")
|
|
36
|
+
except (JsonBoundaryError, OSError) as exc:
|
|
37
|
+
raise StateError(
|
|
38
|
+
f"--carry must name the executor's `### Stage Carry Evidence` JSON saved under "
|
|
39
|
+
f"this run's .okstra tree: {exc}", stage="carry") from exc
|
|
40
|
+
recorded = carry.get("stage", carry.get("stageNumber"))
|
|
41
|
+
if isinstance(recorded, int) and recorded != stage:
|
|
42
|
+
raise StateError(f"--carry records stage {recorded}, not {stage}", stage="carry")
|
|
43
|
+
if str(carry.get("status") or "").lower() in FAILED_CARRY_STATUSES:
|
|
44
|
+
raise StateError(
|
|
45
|
+
f"--carry records status {carry['status']!r}; a failed stage is recorded with "
|
|
46
|
+
"`okstra stage-fail`", stage="carry")
|
|
47
|
+
if not carry_head_commit(carry):
|
|
48
|
+
raise StateError("--carry names no head commit (stageCommitRange.head)", stage="carry")
|
|
49
|
+
return carry
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _supersede(target: Path, rows: list[dict], stage: int) -> Path:
|
|
53
|
+
"""Move the sidecar of the run that first settled a reopened stage aside, unchanged."""
|
|
54
|
+
settled = [r for r in rows if r.get("stage") == stage and r.get("status") == "done"]
|
|
55
|
+
if not settled:
|
|
56
|
+
raise StateError(
|
|
57
|
+
f"{target} exists but stage {stage} was never settled — one stage holds one "
|
|
58
|
+
"sidecar; inspect it before replacing it by hand", stage="carry")
|
|
59
|
+
match = _REPORT_LABEL_RE.match(Path(str(settled[-1].get("report_path") or "")).name)
|
|
60
|
+
if not match:
|
|
61
|
+
raise StateError(
|
|
62
|
+
f"cannot name the superseded sidecar: the earlier done row's report_path "
|
|
63
|
+
f"{settled[-1].get('report_path')!r} is not a final-report path", stage="carry")
|
|
64
|
+
moved = target.parent / "superseded" / f"stage-{stage}-from-{match['label']}.json"
|
|
65
|
+
if moved.exists():
|
|
66
|
+
raise StateError(f"{moved} already exists", stage="carry")
|
|
67
|
+
moved.parent.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
target.rename(moved)
|
|
69
|
+
return moved
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def done_stage(
|
|
73
|
+
project_root: Path, task_key: str, stage: int, carry_file: Path, report: Path,
|
|
74
|
+
) -> dict:
|
|
75
|
+
"""Write the stage's carry sidecar at its canonical path and append the `done` row."""
|
|
76
|
+
identity, plan_run_root = resolve_planned_stage(project_root, task_key, stage)
|
|
77
|
+
carry = _read_carry(carry_file, stage)
|
|
78
|
+
head = carry_head_commit(carry)
|
|
79
|
+
report = report if report.is_absolute() else project_root / report
|
|
80
|
+
try:
|
|
81
|
+
report_rel = report.relative_to(RunRef.from_report_path(report).run_dir).as_posix()
|
|
82
|
+
except ValueError as exc:
|
|
83
|
+
raise StateError(f"--report is not a final-report path: {report}", stage="report") from exc
|
|
84
|
+
|
|
85
|
+
rows = read_consumers(plan_run_root)
|
|
86
|
+
last = [r for r in rows if r.get("stage") == stage and r.get("status") in ("done", "failed")]
|
|
87
|
+
if last and last[-1].get("status") == "done" and last[-1].get("head_commit") != head:
|
|
88
|
+
raise StateError(
|
|
89
|
+
f"stage {stage} is already done at {last[-1].get('head_commit')}; withdraw it with "
|
|
90
|
+
"`okstra stage-reopen` before settling another head", stage="consumers")
|
|
91
|
+
|
|
92
|
+
task_root = Path(identity["taskRoot"])
|
|
93
|
+
target = RunRef.from_task_root(task_root, "implementation").carry(stage)
|
|
94
|
+
superseded = None
|
|
95
|
+
if target.exists():
|
|
96
|
+
existing = load_owned_object(target, artifact="implementation stage carry")
|
|
97
|
+
if carry_head_commit(existing) != head:
|
|
98
|
+
superseded = _supersede(target, rows, stage)
|
|
99
|
+
if not target.exists():
|
|
100
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
101
|
+
write_owned_object_atomic(target, carry, artifact="implementation stage carry")
|
|
102
|
+
carry_rel = target.relative_to(task_root).as_posix()
|
|
103
|
+
append_consumer(
|
|
104
|
+
plan_run_root,
|
|
105
|
+
impl_task_key=identity["taskKey"],
|
|
106
|
+
stage=stage,
|
|
107
|
+
status="done",
|
|
108
|
+
head_commit=head,
|
|
109
|
+
carry_path=carry_rel,
|
|
110
|
+
report_path=report_rel,
|
|
111
|
+
completed_at=datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
112
|
+
)
|
|
113
|
+
return {
|
|
114
|
+
"taskKey": identity["taskKey"],
|
|
115
|
+
"stage": stage,
|
|
116
|
+
"headCommit": head,
|
|
117
|
+
"carryPath": str(target),
|
|
118
|
+
"supersededCarryPath": str(superseded) if superseded else "",
|
|
119
|
+
"reportPath": report_rel,
|
|
120
|
+
"consumersPath": str(plan_run_root / "consumers.jsonl"),
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
_CLI_EPILOG = r"""Usage:
|
|
125
|
+
okstra stage-done <task-key> --stage <N> --carry <carry.json> --report <final-report path>
|
|
126
|
+
|
|
127
|
+
Settles an implementation stage whose synthesised verdict is not FAIL. Writes the
|
|
128
|
+
executor's `### Stage Carry Evidence` JSON (saved by the lead under the run's
|
|
129
|
+
.okstra tree, with the post-fix head after a lead minor fix) to the stage's
|
|
130
|
+
canonical sidecar `runs/implementation/carry/stage-<N>.json`, then appends the
|
|
131
|
+
`done` row with that carry path, the report path relative to its run directory,
|
|
132
|
+
the carry's head and the completion time. A reopened stage's previous sidecar
|
|
133
|
+
is moved to `carry/superseded/stage-<N>-from-<task-type>-<seq>.json` first.
|
|
134
|
+
Refuses a carry for another stage, a failed carry, and a stage already done at
|
|
135
|
+
another head. Re-running with the same carry writes nothing new.
|
|
136
|
+
|
|
137
|
+
Output: JSON { ok, taskKey, stage, headCommit, carryPath, supersededCarryPath,
|
|
138
|
+
reportPath, consumersPath }. Exit 1 on a refusal, 2 when
|
|
139
|
+
PROJECT_ROOT cannot be resolved.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def main(argv: list[str] | None = None) -> int:
|
|
144
|
+
parser = argparse.ArgumentParser(
|
|
145
|
+
description="Settle a passing implementation stage (carry sidecar + done row).",
|
|
146
|
+
epilog=_CLI_EPILOG,
|
|
147
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
148
|
+
prog="okstra stage-done")
|
|
149
|
+
parser.add_argument("task_key", metavar="task-key",
|
|
150
|
+
help="project-id:task-group:task-id")
|
|
151
|
+
parser.add_argument("--stage", type=int, required=True,
|
|
152
|
+
help="the Stage Map number this run implemented")
|
|
153
|
+
parser.add_argument("--carry", type=Path, required=True,
|
|
154
|
+
help="the Stage Carry Evidence JSON saved under the run's .okstra tree")
|
|
155
|
+
parser.add_argument("--report", type=Path, required=True,
|
|
156
|
+
help="this run's final-report path")
|
|
157
|
+
parser.add_argument("--project-root", "--project", default="",
|
|
158
|
+
help="use this directory as PROJECT_ROOT")
|
|
159
|
+
parser.add_argument("--cwd", default=".",
|
|
160
|
+
help="resolve PROJECT_ROOT starting from here")
|
|
161
|
+
args = parser.parse_args(argv)
|
|
162
|
+
|
|
163
|
+
def emit(payload: dict) -> None:
|
|
164
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
resolved = resolve_project_root(explicit_root=args.project_root, cwd=args.cwd)
|
|
168
|
+
except ResolverError as exc:
|
|
169
|
+
emit({"ok": False, "stage": "resolve", "reason": str(exc)})
|
|
170
|
+
return 2
|
|
171
|
+
try:
|
|
172
|
+
settled = done_stage(Path(resolved), args.task_key, args.stage,
|
|
173
|
+
args.carry, args.report)
|
|
174
|
+
except StateError as exc:
|
|
175
|
+
emit({"ok": False, "stage": getattr(exc, "stage", None) or "stage-done",
|
|
176
|
+
"reason": str(exc)})
|
|
177
|
+
return 1
|
|
178
|
+
emit({"ok": True, **settled})
|
|
179
|
+
return 0
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
if __name__ == "__main__":
|
|
183
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""`okstra stage-fail` — FAIL 로 끝난 implementation run 의 stage 에 `failed` 행을 남긴다.
|
|
2
|
+
|
|
3
|
+
종합 verdict 가 FAIL 이면 리드는 carry sidecar 대신 `failed` 행을 써야 한다
|
|
4
|
+
(`_implementation-deliverable.md` §"Lead post-stage persistence"). 그 행을 쓰는 명령이
|
|
5
|
+
없어 리드가 `append_consumer` 를 python 으로 직접 불렀고, Claude Code auto-mode 가 그
|
|
6
|
+
호출을 막아 사용자가 손으로 실행해야 했다 — 실측(2026-10-17, figma-plugin dev-11377).
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from okstra_project import ResolverError, StateError, resolve_project_root
|
|
15
|
+
|
|
16
|
+
from .consumers import append_consumer, last_lifecycle_status_by_stage, read_consumers
|
|
17
|
+
from .stage_reopen import resolve_planned_stage
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def fail_stage(
|
|
21
|
+
project_root: Path, task_key: str, stage: int, head_commit: str, report_path: str,
|
|
22
|
+
reason: str,
|
|
23
|
+
) -> dict:
|
|
24
|
+
"""아직 `done` 이 아닌 stage 에 `failed` 행을 붙인다."""
|
|
25
|
+
for flag, value in (("--head-commit", head_commit), ("--report", report_path),
|
|
26
|
+
("--reason", reason)):
|
|
27
|
+
if not value.strip():
|
|
28
|
+
raise StateError(f"{flag} is required", stage=flag.lstrip("-"))
|
|
29
|
+
identity, plan_run_root = resolve_planned_stage(project_root, task_key, stage)
|
|
30
|
+
recorded = last_lifecycle_status_by_stage(read_consumers(plan_run_root)).get(stage)
|
|
31
|
+
if recorded == "done":
|
|
32
|
+
raise StateError(
|
|
33
|
+
f"stage {stage} is recorded 'done' — withdraw a settled stage with "
|
|
34
|
+
"`okstra stage-reopen`, which records the done head it withdraws",
|
|
35
|
+
stage="consumers",
|
|
36
|
+
)
|
|
37
|
+
append_consumer(
|
|
38
|
+
plan_run_root,
|
|
39
|
+
impl_task_key=identity["taskKey"],
|
|
40
|
+
stage=stage,
|
|
41
|
+
status="failed",
|
|
42
|
+
head_commit=head_commit.strip(),
|
|
43
|
+
report_path=report_path.strip(),
|
|
44
|
+
reason=reason.strip(),
|
|
45
|
+
)
|
|
46
|
+
return {
|
|
47
|
+
"taskKey": identity["taskKey"],
|
|
48
|
+
"stage": stage,
|
|
49
|
+
"headCommit": head_commit.strip(),
|
|
50
|
+
"reportPath": report_path.strip(),
|
|
51
|
+
"reason": reason.strip(),
|
|
52
|
+
"consumersPath": str(plan_run_root / "consumers.jsonl"),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
_CLI_EPILOG = r"""Usage:
|
|
57
|
+
okstra stage-fail <task-key> --stage <N> --head-commit <sha> --report <path> --reason "<why>"
|
|
58
|
+
|
|
59
|
+
Appends the `failed` row an implementation run writes when its synthesised verdict
|
|
60
|
+
is FAIL, or when it ends with no verifier result: dependents stay blocked, the
|
|
61
|
+
stage's worktree occupancy is released, and `/okstra-run` offers the stage for a
|
|
62
|
+
fix run that reuses its worktree and branch. --report is this run's final-report
|
|
63
|
+
path as the `done` row would record it. Refuses a stage that is not in the latest
|
|
64
|
+
Stage Map, is cancelled, or is already `done` (use `okstra stage-reopen`).
|
|
65
|
+
Appending the same row twice writes it once.
|
|
66
|
+
|
|
67
|
+
Output: JSON { ok, taskKey, stage, headCommit, reportPath, reason, consumersPath }.
|
|
68
|
+
Exit 1 on a refusal, 2 when PROJECT_ROOT cannot be resolved.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def main(argv: list[str] | None = None) -> int:
|
|
73
|
+
parser = argparse.ArgumentParser(
|
|
74
|
+
description="Record a FAIL-ended implementation stage as failed.",
|
|
75
|
+
epilog=_CLI_EPILOG,
|
|
76
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
77
|
+
prog="okstra stage-fail")
|
|
78
|
+
parser.add_argument("task_key", metavar="task-key",
|
|
79
|
+
help="project-id:task-group:task-id")
|
|
80
|
+
parser.add_argument("--stage", type=int, required=True,
|
|
81
|
+
help="the Stage Map number this run implemented")
|
|
82
|
+
parser.add_argument("--head-commit", required=True,
|
|
83
|
+
help="the stage worktree HEAD at the end of this run")
|
|
84
|
+
parser.add_argument("--report", required=True,
|
|
85
|
+
help="this run's final-report path, as a done row records it")
|
|
86
|
+
parser.add_argument("--reason", required=True,
|
|
87
|
+
help="why the stage failed; recorded on the row")
|
|
88
|
+
parser.add_argument("--project-root", "--project", default="",
|
|
89
|
+
help="use this directory as PROJECT_ROOT")
|
|
90
|
+
parser.add_argument("--cwd", default=".",
|
|
91
|
+
help="resolve PROJECT_ROOT starting from here")
|
|
92
|
+
args = parser.parse_args(argv)
|
|
93
|
+
|
|
94
|
+
def emit(payload: dict) -> None:
|
|
95
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
resolved = resolve_project_root(explicit_root=args.project_root, cwd=args.cwd)
|
|
99
|
+
except ResolverError as exc:
|
|
100
|
+
emit({"ok": False, "stage": "resolve", "reason": str(exc)})
|
|
101
|
+
return 2
|
|
102
|
+
try:
|
|
103
|
+
failed = fail_stage(Path(resolved), args.task_key, args.stage,
|
|
104
|
+
args.head_commit, args.report, args.reason)
|
|
105
|
+
except StateError as exc:
|
|
106
|
+
emit({"ok": False, "stage": getattr(exc, "stage", None) or "stage-fail",
|
|
107
|
+
"reason": str(exc)})
|
|
108
|
+
return 1
|
|
109
|
+
emit({"ok": True, **failed})
|
|
110
|
+
return 0
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
if __name__ == "__main__":
|
|
114
|
+
raise SystemExit(main())
|
|
@@ -31,16 +31,10 @@ from .paths import RunRef
|
|
|
31
31
|
from .stage_map import StageMapError, load_latest_plan_stage_map
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
def
|
|
35
|
-
|
|
36
|
-
) -> dict:
|
|
37
|
-
"""마지막 행이 `done` 인 stage 에 `failed` 행을 붙이고 그 근거를 돌려준다."""
|
|
38
|
-
if not reason.strip():
|
|
39
|
-
raise StateError("--reason is required: say why the stage is reopened",
|
|
40
|
-
stage="reason")
|
|
34
|
+
def resolve_planned_stage(project_root: Path, task_key: str, stage: int) -> tuple[dict, Path]:
|
|
35
|
+
"""task identity 와 plan run root 를 돌려준다. 최신 Stage Map 에 없거나 취소된 stage 는 거절한다."""
|
|
41
36
|
identity = resolve_task_identity(project_root, task_key)
|
|
42
37
|
task_root = Path(identity["taskRoot"])
|
|
43
|
-
plan_run_root = RunRef.from_task_root(task_root, "implementation-planning").run_dir
|
|
44
38
|
try:
|
|
45
39
|
stage_map = load_latest_plan_stage_map(task_root)
|
|
46
40
|
except StageMapError as exc:
|
|
@@ -50,7 +44,17 @@ def reopen_stage(
|
|
|
50
44
|
raise StateError(f"stage {stage} is not in this task's Stage Map", stage="stage-map")
|
|
51
45
|
if any(row.get("stage_number") == stage and row.get("cancelled") for row in rows):
|
|
52
46
|
raise StateError(f"stage {stage} is cancelled in the latest plan", stage="stage-map")
|
|
47
|
+
return identity, RunRef.from_task_root(task_root, "implementation-planning").run_dir
|
|
48
|
+
|
|
53
49
|
|
|
50
|
+
def reopen_stage(
|
|
51
|
+
project_root: Path, task_key: str, stage: int, reason: str, evidence: str = "",
|
|
52
|
+
) -> dict:
|
|
53
|
+
"""마지막 행이 `done` 인 stage 에 `failed` 행을 붙이고 그 근거를 돌려준다."""
|
|
54
|
+
if not reason.strip():
|
|
55
|
+
raise StateError("--reason is required: say why the stage is reopened",
|
|
56
|
+
stage="reason")
|
|
57
|
+
identity, plan_run_root = resolve_planned_stage(project_root, task_key, stage)
|
|
54
58
|
consumers = read_consumers(plan_run_root)
|
|
55
59
|
recorded = last_lifecycle_status_by_stage(consumers).get(stage)
|
|
56
60
|
if recorded != "done":
|
|
@@ -96,7 +96,7 @@ def _build_group_prompt(state: WizardState, group_id: str) -> Prompt:
|
|
|
96
96
|
|
|
97
97
|
|
|
98
98
|
def _is_lone_choice_pick(prompt: Prompt) -> bool:
|
|
99
|
-
"""고를 것이 한 줄뿐인 단일
|
|
99
|
+
"""고를 것이 한 줄뿐인 픽인가 — 단일 선택이든 체크박스든.
|
|
100
100
|
|
|
101
101
|
호스트 선택기는 옵션 2개 미만을 받지 않아(`nativeLimits` `minOptions`) 이런
|
|
102
102
|
화면은 번호 1개짜리 목록으로 내려간다 — 사용자는 고를 것이 없는 목록에서
|
|
@@ -106,7 +106,6 @@ def _is_lone_choice_pick(prompt: Prompt) -> bool:
|
|
|
106
106
|
"""
|
|
107
107
|
return (
|
|
108
108
|
prompt.kind == "pick"
|
|
109
|
-
and not prompt.multi
|
|
110
109
|
and len(prompt.options) == 1
|
|
111
110
|
and prompt.options[0].value != _ABORT_OPTION
|
|
112
111
|
)
|
|
@@ -1355,6 +1355,14 @@
|
|
|
1355
1355
|
"$ref": "#/$defs/LeadFixRow"
|
|
1356
1356
|
}
|
|
1357
1357
|
},
|
|
1358
|
+
"correctionRounds": {
|
|
1359
|
+
"type": "array",
|
|
1360
|
+
"description": "Correction rounds run in this run after a verifier FAIL (`_implementation-deliverable.md` \"Correction round\"): each sends the executor a bounded fix and the verifiers a re-verification. `verifierResults` holds the last round's verdicts; earlier FAIL verdicts stay here. Optional so reports of tasks already in flight keep validating.",
|
|
1361
|
+
"maxItems": 2,
|
|
1362
|
+
"items": {
|
|
1363
|
+
"$ref": "#/$defs/CorrectionRoundRow"
|
|
1364
|
+
}
|
|
1365
|
+
},
|
|
1358
1366
|
"rollbackVerification": {
|
|
1359
1367
|
"type": "array",
|
|
1360
1368
|
"minItems": 1,
|
|
@@ -10159,6 +10167,76 @@
|
|
|
10159
10167
|
}
|
|
10160
10168
|
}
|
|
10161
10169
|
},
|
|
10170
|
+
"CorrectionRoundRow": {
|
|
10171
|
+
"type": "object",
|
|
10172
|
+
"description": "One correction round: the findings it fixed, the executor's commit range, and each verifier's re-verification verdict.",
|
|
10173
|
+
"required": [
|
|
10174
|
+
"round",
|
|
10175
|
+
"findingIds",
|
|
10176
|
+
"baseCommit",
|
|
10177
|
+
"commit",
|
|
10178
|
+
"verifierVerdicts",
|
|
10179
|
+
"result"
|
|
10180
|
+
],
|
|
10181
|
+
"additionalProperties": false,
|
|
10182
|
+
"properties": {
|
|
10183
|
+
"round": {
|
|
10184
|
+
"type": "integer",
|
|
10185
|
+
"minimum": 1,
|
|
10186
|
+
"maximum": 2
|
|
10187
|
+
},
|
|
10188
|
+
"findingIds": {
|
|
10189
|
+
"type": "array",
|
|
10190
|
+
"minItems": 1,
|
|
10191
|
+
"items": {
|
|
10192
|
+
"type": "string",
|
|
10193
|
+
"minLength": 1
|
|
10194
|
+
}
|
|
10195
|
+
},
|
|
10196
|
+
"baseCommit": {
|
|
10197
|
+
"description": "Stage HEAD before the round.",
|
|
10198
|
+
"type": "string",
|
|
10199
|
+
"pattern": "^[0-9a-f]{7,40}$"
|
|
10200
|
+
},
|
|
10201
|
+
"commit": {
|
|
10202
|
+
"description": "Stage HEAD after the executor's fix.",
|
|
10203
|
+
"type": "string",
|
|
10204
|
+
"pattern": "^[0-9a-f]{7,40}$"
|
|
10205
|
+
},
|
|
10206
|
+
"verifierVerdicts": {
|
|
10207
|
+
"type": "array",
|
|
10208
|
+
"minItems": 1,
|
|
10209
|
+
"items": {
|
|
10210
|
+
"type": "object",
|
|
10211
|
+
"required": [
|
|
10212
|
+
"verifier",
|
|
10213
|
+
"verdict"
|
|
10214
|
+
],
|
|
10215
|
+
"additionalProperties": false,
|
|
10216
|
+
"properties": {
|
|
10217
|
+
"verifier": {
|
|
10218
|
+
"type": "string",
|
|
10219
|
+
"minLength": 1
|
|
10220
|
+
},
|
|
10221
|
+
"verdict": {
|
|
10222
|
+
"enum": [
|
|
10223
|
+
"PASS",
|
|
10224
|
+
"CONCERNS",
|
|
10225
|
+
"FAIL"
|
|
10226
|
+
]
|
|
10227
|
+
}
|
|
10228
|
+
}
|
|
10229
|
+
}
|
|
10230
|
+
},
|
|
10231
|
+
"result": {
|
|
10232
|
+
"description": "`pass` when no re-verification verdict is FAIL.",
|
|
10233
|
+
"enum": [
|
|
10234
|
+
"pass",
|
|
10235
|
+
"fail"
|
|
10236
|
+
]
|
|
10237
|
+
}
|
|
10238
|
+
}
|
|
10239
|
+
},
|
|
10162
10240
|
"LeadFixRow": {
|
|
10163
10241
|
"type": "object",
|
|
10164
10242
|
"description": "One lead minor fix: the verifier findings it answers, the commit that carries it, and the re-run checks that decide whether the run may settle the stage. `changedLines` has no schema maximum on purpose: an over-limit fix must still be recordable, and `_validate_lead_fixes` reports it.",
|
|
@@ -29,7 +29,7 @@ okstra manager task status --manager-id <manager-id> --task-group <task-group> -
|
|
|
29
29
|
okstra manager task sync --manager-id <manager-id> --task-group <task-group> --task-id <task-id>
|
|
30
30
|
okstra manager task assign --manager-id <manager-id> --task-group <task-group> --task-id <task-id> --project-id <project-id> [--child-task-id <child-task-id>] [--role <role>] [--tag <tag>] [--assignment <text>]
|
|
31
31
|
okstra manager task note --manager-id <manager-id> --task-group <task-group> --task-id <task-id> --scope <shared|project> [--project-id <project-id>] --body <text>
|
|
32
|
-
okstra manager task run --manager-id <manager-id> --project-id <project-id> --task-group <task-group> --task-id <task-id> [--child-task-id <child-task-id>]
|
|
32
|
+
okstra manager task run --manager-id <manager-id> --project-id <project-id> --task-group <task-group> --task-id <task-id> [--child-task-id <child-task-id>] [--allow-completed]
|
|
33
33
|
okstra manager task split --manager-id <manager-id> --task-group <task-group> --task-id <task-id> --plan <split-plan.json> [--overwrite]
|
|
34
34
|
okstra manager list managers
|
|
35
35
|
okstra manager list projects --manager-id <manager-id>
|
|
@@ -104,7 +104,7 @@ When the user wants to see a manager at a glance, run `okstra manager task sync
|
|
|
104
104
|
For launching child work:
|
|
105
105
|
|
|
106
106
|
1. Run `okstra manager task sync ...` if the manager snapshot must be refreshed first.
|
|
107
|
-
2. Run `okstra manager task run ...`.
|
|
107
|
+
2. Run `okstra manager task run ...`. It re-reads the child's state from its project, not from the snapshot. When the child is already done there (terminal next phase, or work status `done`), it refuses with the last phase and report path; tell the user that and do not add `--allow-completed` unless the user asks to run the child again.
|
|
108
108
|
3. Read the returned fixed fields `Backend`, `Worker dispatch backend`, `Project root`, `Context path`, `Run command`, every numbered `Run arg N`, and `Shell command`.
|
|
109
109
|
4. Give the user the `Shell command` line verbatim and tell them to run it in a new terminal. It starts the installed launcher in the child project: the launcher fills the task type and brief from the child task manifest or asks for them, prepares the run with the manager context directive, and starts the child lead as a separate host process (`Backend` `spawn-process`).
|
|
110
110
|
|
|
@@ -678,6 +678,12 @@
|
|
|
678
678
|
"lead-fix-changed-lines": "Changed lines",
|
|
679
679
|
"lead-fix-result-pass": "checks pass",
|
|
680
680
|
"lead-fix-result-fail": "checks still fail",
|
|
681
|
+
"correction-rounds": "Corrected within this run",
|
|
682
|
+
"correction-rounds-intro": "Verifiers rejected the change. Instead of starting a fix run, the executor fixed the confirmed findings in this run and the verifiers checked the fix again.",
|
|
683
|
+
"correction-round": "Round",
|
|
684
|
+
"correction-range": "Fix range",
|
|
685
|
+
"correction-result-pass": "re-verification passed",
|
|
686
|
+
"correction-result-fail": "re-verification failed",
|
|
681
687
|
"entry-count": "{count} entries",
|
|
682
688
|
"manual-user-test": "Manual user test",
|
|
683
689
|
"environment": "Environment:",
|
|
@@ -678,6 +678,12 @@
|
|
|
678
678
|
"lead-fix-changed-lines": "바뀐 줄 수",
|
|
679
679
|
"lead-fix-result-pass": "검사 통과",
|
|
680
680
|
"lead-fix-result-fail": "검사 여전히 실패",
|
|
681
|
+
"correction-rounds": "이 run 안에서 고친 것",
|
|
682
|
+
"correction-rounds-intro": "검증자가 변경을 거부했습니다. fix run 을 새로 여는 대신 이 run 안에서 실행자가 확인된 finding 을 고치고, 검증자가 그 수정을 다시 검증했습니다.",
|
|
683
|
+
"correction-round": "라운드",
|
|
684
|
+
"correction-range": "수정 범위",
|
|
685
|
+
"correction-result-pass": "재검증 통과",
|
|
686
|
+
"correction-result-fail": "재검증 실패",
|
|
681
687
|
"entry-count": "{count}개 항목",
|
|
682
688
|
"manual-user-test": "수동 사용자 테스트",
|
|
683
689
|
"environment": "환경:",
|
|
@@ -40,6 +40,7 @@ from okstra_ctl.phases.implementation.validation import (
|
|
|
40
40
|
_validate_verifier_reran_independently,
|
|
41
41
|
_validate_verifier_discrepancy_is_not_passed,
|
|
42
42
|
_validate_verifier_fail_blocks_verdict,
|
|
43
|
+
_validate_correction_rounds,
|
|
43
44
|
_validate_lead_fixes,
|
|
44
45
|
_validate_verifier_command_log_is_read_only as validate_verifier_command_log_is_read_only,
|
|
45
46
|
_validate_verifier_discrepancy_names_checklist_phase as validate_verifier_discrepancy_names_checklist_phase,
|
|
@@ -3187,6 +3188,7 @@ def validate_final_report_data(
|
|
|
3187
3188
|
_validate_stage_carry_sidecar_exists(data, report_path, failures)
|
|
3188
3189
|
_validate_lead_fixes(data, failures)
|
|
3189
3190
|
_validate_lead_fix_carry_head(data, report_path, failures)
|
|
3191
|
+
_validate_correction_rounds(data, manifest, failures)
|
|
3190
3192
|
_validate_lead_authored_report(data, report_path, failures)
|
|
3191
3193
|
if task_type == "error-analysis":
|
|
3192
3194
|
_validate_error_analysis_consistency(data, failures)
|