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.
Files changed (37) hide show
  1. package/dist/cli-registry.mjs +18 -0
  2. package/dist/cli-registry.mjs.map +1 -1
  3. package/dist/lib/paths.mjs +1 -1
  4. package/dist/lib/paths.mjs.map +1 -1
  5. package/docs/cli.md +4 -1
  6. package/package.json +1 -1
  7. package/runtime/BUILD.json +2 -2
  8. package/runtime/prompts/lead/okstra-lead-contract.md +7 -4
  9. package/runtime/python/okstra_ctl/blocking_checks.py +8 -0
  10. package/runtime/python/okstra_ctl/business_flow/engine.py +10 -2
  11. package/runtime/python/okstra_ctl/convergence_provenance.py +8 -2
  12. package/runtime/python/okstra_ctl/correction_cli.py +59 -0
  13. package/runtime/python/okstra_ctl/correction_round.py +168 -0
  14. package/runtime/python/okstra_ctl/dispatch_core.py +18 -0
  15. package/runtime/python/okstra_ctl/lead_progress.py +56 -11
  16. package/runtime/python/okstra_ctl/manager_cli.py +2 -0
  17. package/runtime/python/okstra_ctl/manager_launch.py +23 -2
  18. package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-deliverable.md +29 -6
  19. package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-verifier.md +2 -0
  20. package/runtime/python/okstra_ctl/phases/implementation/profile.md +2 -1
  21. package/runtime/python/okstra_ctl/phases/implementation/report_assets/implementation.template.html +6 -0
  22. package/runtime/python/okstra_ctl/phases/implementation/spec.md +2 -1
  23. package/runtime/python/okstra_ctl/phases/implementation/validation.py +62 -1
  24. package/runtime/python/okstra_ctl/phases/release_handoff/boundary.json +2 -2
  25. package/runtime/python/okstra_ctl/phases/release_handoff/profile.md +2 -2
  26. package/runtime/python/okstra_ctl/phases/release_handoff/spec.md +1 -1
  27. package/runtime/python/okstra_ctl/report_assembly.py +5 -5
  28. package/runtime/python/okstra_ctl/report_inputs.py +10 -1
  29. package/runtime/python/okstra_ctl/stage_done.py +183 -0
  30. package/runtime/python/okstra_ctl/stage_fail.py +114 -0
  31. package/runtime/python/okstra_ctl/stage_reopen.py +12 -8
  32. package/runtime/python/okstra_ctl/wizard/engine.py +1 -2
  33. package/runtime/schemas/final-report-v3.0.schema.json +78 -0
  34. package/runtime/skills/okstra-manager/SKILL.md +2 -2
  35. package/runtime/templates/reports/html/i18n/en.json +6 -0
  36. package/runtime/templates/reports/html/i18n/ko.json +6 -0
  37. 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 reopen_stage(
35
- project_root: Path, task_key: str, stage: int, reason: str, evidence: str = "",
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)