okstra 0.61.0 → 0.63.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.61.0",
3
+ "version": "0.63.0",
4
4
  "description": "Multi-agent cross-verification orchestrator runtime + Claude Code skills.",
5
5
  "license": "MIT",
6
6
  "author": "devonshin",
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.61.0",
3
- "builtAt": "2026-06-09T07:19:02.278Z",
2
+ "package": "0.63.0",
3
+ "builtAt": "2026-06-09T09:05:23.698Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -90,8 +90,12 @@ You are an in-process Claude subagent — Lead's `Agent()` call blocks until you
90
90
 
91
91
  After your `Write` to the assigned worker-results file (path provided by Lead as `**Result Path:**` — the canonical anchor header defined in `okstra-team-contract` "Worker Prompt Composition" — or derived under `runs/<task-type>/worker-results/claude-worker-<task-type>-<seq>.md`) succeeds:
92
92
 
93
- 1. Return your final assistant message **immediately**, in this format:
94
- `Worker results written to <abs path>. Sections 1–5 complete. Findings: <n>.`
93
+ 1. Return your final assistant message **immediately**. Begin every return with your model identity, copied verbatim from the `**Model:** Claude worker, <modelExecutionValue>` line in your dispatch prompt (per Worker Preamble → "Return message to the lead"), then the status line — for an analysis dispatch:
94
+ ```
95
+ **Model:** Claude worker, <modelExecutionValue>
96
+ Worker results written to <abs path>. Sections 1–5 complete. Findings: <n>.
97
+ ```
98
+ The `**Model:**` line precedes whatever you return — analysis status above, or a convergence reverify verdict summary.
95
99
  2. Do NOT perform additional `Read`, `Grep`, `Glob`, MCP, or self-review tool calls after the file is written.
96
100
  3. Do NOT rewrite the worker-results file with `Write` more than once. If a correction is genuinely required, perform a single `Edit` and then return immediately.
97
101
  4. The only exception is recording a `tool-failure` in the errors sidecar when a post-Write failure is itself the failure being reported — return immediately after that single sidecar append.
@@ -106,7 +106,11 @@ The wrapper exists because Claude Code's Bash permission matcher rejects simple-
106
106
  2. Record a `cli-failure` event directly to the run-level error log via the exact `okstra-error-log.py append-observed` template in §"Error reporting" — substitute `--exit-code 0`, `--duration-ms <observed-ms>`, `--message "okstra-codex-exec.sh exited 0 but no result file at <abs-path>"`, and `--stderr-excerpt-file <temp-tail-path>`.
107
107
  3. Return `CODEX_RESULT_MISSING: codex exited 0 but result file absent at <abs-path>` instead of the raw stdout. The lead is responsible for deciding redispatch per `okstra-team-contract` "Lead Redispatch Policy on Result-Missing".
108
108
 
109
- d. **Normal return.** Otherwise (`exit_code == 0` AND result file exists), concatenate the wrapper's accumulated stdout from `BashOutput` and return it as-is without modification.
109
+ d. **Normal return.** Otherwise (`exit_code == 0` AND result file exists), return the wrapper's accumulated stdout from `BashOutput`, prefixed by exactly one model-identity line copied verbatim from the `**Model:** Codex worker, <execution-value>` line in the lead prompt (per Worker Preamble → "Return message to the lead"):
110
+ ```
111
+ **Model:** Codex worker, <assigned-model-execution-value>
112
+ ```
113
+ Emit that line first, then the stdout unmodified. The model line is the ONLY addition permitted — do not otherwise summarize or alter the CLI output. This applies to convergence reverify dispatches too.
110
114
 
111
115
  9. When `Task Type` is `improvement-discovery`, the lead's Phase 1.5 reflect-back log at `<RUN_DIR>/state/phase-1.5-grilling.md` is the authoritative scope and lens definition. Read its `Resolved scope` and `Resolved lenses` blocks and do NOT re-interpret the brief's raw `scan-scope` / `priority-lenses` fields. Findings that violate the resolved lens whitelist or scope are rejected by `validators/validate-improvement-report.py`.
112
116
 
@@ -228,7 +232,7 @@ pre-flight terminal status, not a runtime CLI error.
228
232
 
229
233
  - Ignore stderr warnings from MCP integration.
230
234
  - Return error messages as-is on failure.
231
- - Do not summarize or modify Codex results.
235
+ - Do not summarize or modify Codex results beyond prepending the single `**Model:**` line on a normal return (step 8d).
232
236
  - Sections 1–5 of the worker output are the common core shared with the Claude and Gemini workers — the dispatched prompt asks identical questions for all three roles, and the Codex CLI must answer all of them, not only implementation-feasibility findings. Your specialization (implementation realism, code-path implications, edge cases, technical trade-offs) belongs only in optional Section 6 as additive depth. A Codex result whose Findings section is populated solely with implementation-feasibility items is in breach of contract; see `skills/okstra-team-contract/SKILL.md` "Worker Output Contract".
233
237
 
234
238
  ## Stage evidence emission (BLOCKING, implementation task only)
@@ -106,7 +106,11 @@ The wrapper exists because Claude Code's Bash permission matcher rejects simple-
106
106
  2. Record a `cli-failure` event directly to the run-level error log via the exact `okstra-error-log.py append-observed` template in §"Error reporting" — substitute `--exit-code 0`, `--duration-ms <observed-ms>`, `--message "okstra-gemini-exec.sh exited 0 but no result file at <abs-path>"`, and `--stderr-excerpt-file <temp-tail-path>`.
107
107
  3. Return `GEMINI_RESULT_MISSING: gemini exited 0 but result file absent at <abs-path>` instead of the raw stdout. The lead is responsible for deciding redispatch per `okstra-team-contract` "Lead Redispatch Policy on Result-Missing".
108
108
 
109
- d. **Normal return.** Otherwise (`exit_code == 0` AND result file exists), concatenate the wrapper's accumulated stdout from `BashOutput` and return it as-is without modification.
109
+ d. **Normal return.** Otherwise (`exit_code == 0` AND result file exists), return the wrapper's accumulated stdout from `BashOutput`, prefixed by exactly one model-identity line copied verbatim from the `**Model:** Gemini worker, <execution-value>` line in the lead prompt (per Worker Preamble → "Return message to the lead"):
110
+ ```
111
+ **Model:** Gemini worker, <assigned-model-execution-value>
112
+ ```
113
+ Emit that line first, then the stdout unmodified. The model line is the ONLY addition permitted — do not otherwise summarize or alter the CLI output. This applies to convergence reverify dispatches too.
110
114
 
111
115
  9. When `Task Type` is `improvement-discovery`, the lead's Phase 1.5 reflect-back log at `<RUN_DIR>/state/phase-1.5-grilling.md` is the authoritative scope and lens definition. Read its `Resolved scope` and `Resolved lenses` blocks and do NOT re-interpret the brief's raw `scan-scope` / `priority-lenses` fields. Findings that violate the resolved lens whitelist or scope are rejected by `validators/validate-improvement-report.py`.
112
116
 
@@ -228,7 +232,7 @@ pre-flight terminal status, not a runtime CLI error.
228
232
 
229
233
  - Always specify the assigned `-m` value for the current run.
230
234
  - Return error messages as-is on failure.
231
- - Do not summarize or modify Gemini results.
235
+ - Do not summarize or modify Gemini results beyond prepending the single `**Model:**` line on a normal return (step 8d).
232
236
  - Sections 1–5 of the worker output are the common core shared with the Claude and Codex workers — the dispatched prompt asks identical questions for all three roles, and the Gemini CLI must answer all of them, not only requirement-interpretation findings. Your specialization (requirement interpretation, consistency, safety, documentation quality, alternative viewpoints) belongs only in optional Section 6 as additive depth. A Gemini result whose Findings section is populated solely with requirement-interpretation items is in breach of contract; see `skills/okstra-team-contract/SKILL.md` "Worker Output Contract".
233
237
 
234
238
  ## Stage evidence emission (BLOCKING, implementation task only)
@@ -103,7 +103,12 @@ Rules (the schema enforces most of these — they are listed here so you know *w
103
103
  - For `implementation-planning`, populate `implementationPlanning.requirementCoverage` with one row per concrete requirement from the brief / packet, using IDs `R-001`, `R-002`, ... in source order. `coveredBy` MUST name the specific Option Candidate plus Stage/Step that satisfies the requirement. Use `status: "covered"` only when the report's plan actually covers it; otherwise use `gap` or `blocked C-NNN` and ensure the corresponding `Clarification Items` row blocks approval. Do not collapse this into `ticketCoverage`; ticket coverage is not requirement coverage.
104
104
  - When the `Task Type` is `improvement-discovery`, populate `## 5.9 Improvement Candidates` with the 10-column schema enforced by `validators/validate-improvement-report.py`. Source the row IDs (`I-NNN`), lens whitelist, and Source workers patterns from `scripts/okstra_ctl/improvement_lenses.py` — do NOT introduce new lens names or worker prefixes. `improvement-discovery` is NOT in the data.json schema enum, so author its markdown directly (not via `okstra-render-final-report.py`). Immediately after writing the markdown, run (`Bash`): `python3 scripts/okstra-inject-report-index.py <markdown path> --report-language <en|ko>`. That adds the top-of-report Index plus `I-NNN` / `C-NNN` scroll anchors; the run validator fails the report when the Index anchor is absent.
105
105
 
106
- Write the data.json with your `Write` tool against the absolute `Result Path`. Then invoke the renderer (`Bash`): `python3 scripts/okstra-render-final-report.py <data.json path>`. Confirm both files exist and respond with a short status line: `data.json written to <abs path>; markdown rendered to <abs path>. Sections populated: <count>.`
106
+ Write the data.json with your `Write` tool against the absolute `Result Path`. Then invoke the renderer (`Bash`): `python3 scripts/okstra-render-final-report.py <data.json path>`. Confirm both files exist and respond with a short status line prefixed by your model identity, copied verbatim from the `**Model:** Report writer worker, <modelExecutionValue>` line in your dispatch prompt (per Worker Preamble → "Return message to the lead"):
107
+
108
+ ```
109
+ **Model:** Report writer worker, <modelExecutionValue>
110
+ data.json written to <abs path>; markdown rendered to <abs path>. Sections populated: <count>.
111
+ ```
107
112
 
108
113
  <!-- Worker Result File contract lives above, right after the Authority
109
114
  section. The legacy "after Authoring Contract" placement was kept
@@ -393,11 +393,62 @@ PY
393
393
  exit 1
394
394
  fi
395
395
 
396
+ # 직전 run 의 워커/모델/directive/related-tasks 를 그대로 이어 실행한다 — resume 가
397
+ # 이 설정들을 안 넘기면 매 단계 재입력이 아니라 Python phase 기본값으로 조용히
398
+ # 되돌아가 버린다. run-inputs 의 위치/구조 지식은 okstra_ctl.run_context 가 SSOT.
399
+ local resume_extra_args=()
400
+ while IFS= read -r -d '' _arg; do
401
+ resume_extra_args+=("$_arg")
402
+ done < <(PYTHONPATH="$WORKSPACE_ROOT/scripts:${PYTHONPATH-}" python3 - \
403
+ "$PROJECT_ROOT" "$TASK_GROUP" "$TASK_ID" "$resolved_type" <<'PY'
404
+ import sys
405
+ from okstra_ctl.run_context import latest_run_inputs
406
+ from okstra_ctl.ids import slugify_task_segment
407
+
408
+ project_root, task_group, task_id, task_type = sys.argv[1:5]
409
+ inputs = latest_run_inputs(
410
+ project_root, task_group, task_id,
411
+ phase_segment=slugify_task_segment(task_type))
412
+
413
+
414
+ def emit(flag, value):
415
+ if value:
416
+ sys.stdout.write(flag + "\0" + value + "\0")
417
+
418
+
419
+ def model(value):
420
+ # 직전 run 의 모델 display 값. "default"/빈 값은 phase 기본값을 뜻하므로 넘기지
421
+ # 않는다(명시 플래그로 고정하면 안 됨).
422
+ return value if isinstance(value, str) and value not in ("", "default") else ""
423
+
424
+
425
+ workers = inputs.get("workers")
426
+ if isinstance(workers, list):
427
+ workers = ",".join(w for w in workers if isinstance(w, str))
428
+ elif not isinstance(workers, str):
429
+ workers = ""
430
+ emit("--workers", workers)
431
+ emit("--lead-model", model(inputs.get("leadModel")))
432
+ emit("--claude-model", model(inputs.get("claudeModel")))
433
+ emit("--codex-model", model(inputs.get("codexModel")))
434
+ emit("--gemini-model", model(inputs.get("geminiModel")))
435
+ emit("--report-writer-model", model(inputs.get("reportWriterModel")))
436
+ directive = inputs.get("directive")
437
+ emit("--directive", directive if isinstance(directive, str) else "")
438
+ related = inputs.get("relatedTasks")
439
+ emit("--related-tasks", related if isinstance(related, str) else "")
440
+ PY
441
+ )
442
+
396
443
  if [[ "${OKSTRA_RESUME_CLARIFICATION_DRY_RUN:-false}" == "true" ]]; then
397
444
  printf 'resume-clarification (dry-run):\n' >&2
398
445
  printf ' report: %s\n' "$report_path" >&2
399
- printf ' re-exec: bash %s --task-type %s --project-id %s --task-group %s --task-id %s --task-brief %s --clarification-response %s\n' \
446
+ printf ' re-exec: bash %s --task-type %s --project-id %s --task-group %s --task-id %s --task-brief %s --clarification-response %s' \
400
447
  "$0" "$resolved_type" "$PROJECT_ID" "$TASK_GROUP" "$TASK_ID" "$resume_brief_rel" "$report_path" >&2
448
+ if (( ${#resume_extra_args[@]} > 0 )); then
449
+ printf ' %s' "${resume_extra_args[@]}" >&2
450
+ fi
451
+ printf '\n' >&2
401
452
  return 0
402
453
  fi
403
454
 
@@ -415,5 +466,6 @@ PY
415
466
  --task-group "$TASK_GROUP" \
416
467
  --task-id "$TASK_ID" \
417
468
  --task-brief "$resume_brief_rel" \
418
- --clarification-response "$report_path"
469
+ --clarification-response "$report_path" \
470
+ ${resume_extra_args[@]+"${resume_extra_args[@]}"}
419
471
  }
@@ -251,6 +251,14 @@
251
251
  "provider": "{provider} critic"
252
252
  }
253
253
  },
254
+ "reuse_previous": {
255
+ "label": "이 task·phase 의 직전 run 설정이 남아 있습니다. 그대로 재사용할까요?\n· 예 — 직전 run 의 워커 구성·역할별 모델·directive·관련 task 를 그대로 가져오고, 가장 최근 final-report 를 clarification 입력으로 자동 선택해 바로 확인 단계로 넘어갑니다.\n· 아니오 — 모델/워커/directive 등을 단계별로 다시 입력합니다.",
256
+ "echo_template": "reuse-previous: {value}",
257
+ "options": {
258
+ "yes": "예 — 직전 설정 그대로 진행",
259
+ "no": "아니오 — 단계별로 다시 입력"
260
+ }
261
+ },
254
262
  "defaults_or_custom": {
255
263
  "label": "역할별로 어떤 모델을 쓸지 정하는 단계입니다 (참여 워커 구성을 바꾸는 게 아닙니다).\n· 기본값으로 진행 — lead·실행자/워커·report-writer 를 모두 추천 모델로 두고 바로 진행합니다.\n· 커스터마이즈 — 역할별 모델을 직접 고르고, 추가 directive·관련 task 도 지정합니다.",
256
264
  "echo_template": "customize: {value}",
@@ -23,7 +23,51 @@ from datetime import datetime, timezone
23
23
  from pathlib import Path
24
24
  from typing import Iterator, Optional
25
25
 
26
- from .paths import compute_run_paths
26
+ from .paths import compute_run_paths, task_runs_dir
27
+
28
+
29
+ def latest_run_inputs(
30
+ project_root: object,
31
+ task_group: str,
32
+ task_id: str,
33
+ *,
34
+ phase_segment: str = "",
35
+ ) -> dict:
36
+ """가장 최근 ``run-inputs-*.json`` 의 ``inputs`` dict 를 반환 (부재 시 {}).
37
+
38
+ ``phase_segment`` 가 주어지면 그 phase 의 ``runs/<seg>/manifests/`` 만 스캔한다
39
+ (resume 는 같은 phase 를 재실행하므로 그 phase 의 직전 입력이 기준). 비어 있으면
40
+ task 의 모든 phase 를 통틀어 가장 최근(mtime) 파일을 고른다.
41
+
42
+ 페이로드 구조는 ``{"schemaVersion", "writtenAt", "inputs": {...}}`` 이며 실제
43
+ 입력은 top-level 이 아니라 ``inputs`` 아래에 있다. run-inputs 의 위치/구조 지식은
44
+ 이 함수 한 곳에만 둔다(wizard·okstra.sh resume 가 공유).
45
+ """
46
+ if not (project_root and task_group and task_id):
47
+ return {}
48
+ runs_base = task_runs_dir(Path(project_root), task_group, task_id)
49
+ if not runs_base.is_dir():
50
+ return {}
51
+ if phase_segment:
52
+ glob_iter = (runs_base / phase_segment / "manifests").glob(
53
+ "run-inputs-*.json")
54
+ else:
55
+ glob_iter = runs_base.glob("*/manifests/run-inputs-*.json")
56
+ candidates: list[tuple[float, Path]] = []
57
+ for inp in glob_iter:
58
+ try:
59
+ candidates.append((inp.stat().st_mtime, inp))
60
+ except OSError:
61
+ continue
62
+ if not candidates:
63
+ return {}
64
+ candidates.sort(key=lambda x: -x[0])
65
+ try:
66
+ data = json.loads(candidates[0][1].read_text(encoding="utf-8"))
67
+ except (OSError, json.JSONDecodeError):
68
+ return {}
69
+ inputs = data.get("inputs")
70
+ return inputs if isinstance(inputs, dict) else {}
27
71
 
28
72
 
29
73
  def _now_iso() -> str:
@@ -57,6 +57,7 @@ from okstra_ctl.worktree import (
57
57
  preview_worktree_decision,
58
58
  )
59
59
  from okstra_ctl.paths import task_runs_dir
60
+ from okstra_ctl.run_context import latest_run_inputs
60
61
  from okstra_project.dirs import project_json_path
61
62
  from okstra_project.state import (
62
63
  StateError,
@@ -231,6 +232,7 @@ S_STAGE_PICK = "stage_pick"
231
232
  S_EXECUTOR = "executor"
232
233
  S_CRITIC_PICK = "critic_pick"
233
234
  S_CRITIC_TEXT = "critic_text"
235
+ S_REUSE_PREVIOUS = "reuse_previous"
234
236
  S_DEFAULTS_OR_CUSTOM = "defaults_or_custom"
235
237
  S_WORKERS_OVERRIDE = "workers_override"
236
238
  S_LEAD_MODEL = "lead_model"
@@ -321,6 +323,9 @@ class WizardState:
321
323
  critic: str = ""
322
324
  critic_pending_text: bool = False
323
325
 
326
+ # resume: 직전 run-inputs 재사용 여부 (None=미응답, True=재사용, False=재입력)
327
+ reuse_previous: Optional[bool] = None
328
+
324
329
  # customize
325
330
  use_defaults: Optional[bool] = None
326
331
  workers_override: str = ""
@@ -1393,31 +1398,18 @@ def _suggest_latest_final_report(state: WizardState) -> str:
1393
1398
  return str(best)
1394
1399
 
1395
1400
 
1401
+ def _latest_run_inputs(
1402
+ state: WizardState, *, phase_segment: str = ""
1403
+ ) -> dict:
1404
+ """직전 run 의 입력 스냅샷. 위치/구조 지식은 run_context.latest_run_inputs 가 SSOT."""
1405
+ return latest_run_inputs(
1406
+ state.project_root, state.task_group, state.task_id,
1407
+ phase_segment=phase_segment)
1408
+
1409
+
1396
1410
  def _suggest_last_directive(state: WizardState) -> str:
1397
1411
  """같은 task 의 가장 최근 run-inputs-*.json 에서 directive 값을 자동 추출."""
1398
- if not state.task_group or not state.task_id or not state.project_root:
1399
- return ""
1400
- runs_base = task_runs_dir(state.project_root, state.task_group, state.task_id)
1401
- if not runs_base.is_dir():
1402
- return ""
1403
- candidates: list[tuple[float, Path]] = []
1404
- for phase_dir in runs_base.iterdir():
1405
- if not phase_dir.is_dir():
1406
- continue
1407
- for run_dir in phase_dir.iterdir():
1408
- for inp in run_dir.glob("run-inputs-*.json"):
1409
- try:
1410
- candidates.append((inp.stat().st_mtime, inp))
1411
- except OSError:
1412
- continue
1413
- if not candidates:
1414
- return ""
1415
- candidates.sort(key=lambda x: -x[0])
1416
- try:
1417
- data = json.loads(candidates[0][1].read_text(encoding="utf-8"))
1418
- except (OSError, json.JSONDecodeError):
1419
- return ""
1420
- val = data.get("directive") or ""
1412
+ val = _latest_run_inputs(state).get("directive") or ""
1421
1413
  return val if isinstance(val, str) else ""
1422
1414
 
1423
1415
 
@@ -1697,6 +1689,81 @@ def _submit_executor(state: WizardState, value: str) -> Optional[str]:
1697
1689
  return f"executor: {value}"
1698
1690
 
1699
1691
 
1692
+ # resume 재사용을 제안하는 phase. okstra.sh --resume-clarification 과 동일하게
1693
+ # §1 clarification 을 갖는 비-implementation phase 로 한정한다(implementation 은
1694
+ # approved-plan/stage 가 매 run 고유라 재사용 대상이 아니다).
1695
+ _RESUME_REUSE_PHASES = (
1696
+ "requirements-discovery", "error-analysis", "implementation-planning")
1697
+
1698
+ # YES 선택 시 prefill 로 충족되어 다시 묻지 않는 설정 단계들.
1699
+ _REUSE_FILLED_STEPS = (
1700
+ S_DEFAULTS_OR_CUSTOM, S_WORKERS_OVERRIDE, S_LEAD_MODEL, S_EXECUTOR_MODEL,
1701
+ S_CLAUDE_MODEL, S_CODEX_MODEL, S_GEMINI_MODEL, S_REPORT_WRITER_MODEL,
1702
+ S_DIRECTIVE_PICK, S_DIRECTIVE, S_RELATED_TASKS_PICK, S_RELATED_TASKS,
1703
+ S_CLARIFICATION_PICK, S_CLARIFICATION)
1704
+
1705
+
1706
+ def _has_prior_run_inputs(state: WizardState) -> bool:
1707
+ seg = slugify_task_segment(state.task_type) if state.task_type else ""
1708
+ return bool(_latest_run_inputs(state, phase_segment=seg))
1709
+
1710
+
1711
+ def _safe_model(provider: str, raw: object) -> str:
1712
+ """run-inputs 의 모델 display 값을 wizard 필드로 환원. 알 수 없는 값은
1713
+ 빈 문자열(phase 기본값)로 안전하게 떨어뜨린다."""
1714
+ if not isinstance(raw, str):
1715
+ return ""
1716
+ try:
1717
+ return _validate_model(provider, raw)
1718
+ except WizardError:
1719
+ return ""
1720
+
1721
+
1722
+ def _build_reuse_previous(state: WizardState) -> Prompt:
1723
+ t = _p(state.workspace_root, "reuse_previous")
1724
+ return Prompt(
1725
+ step=S_REUSE_PREVIOUS, kind="pick",
1726
+ label=t["label"],
1727
+ options=[_opt(k, v) for k, v in t["options"].items()],
1728
+ echo_template=t["echo_template"],
1729
+ )
1730
+
1731
+
1732
+ def _submit_reuse_previous(state: WizardState, value: str) -> Optional[str]:
1733
+ if value not in ("yes", "no"):
1734
+ raise WizardError(f"expected 'yes' or 'no', got: {value!r}")
1735
+ if value == "no":
1736
+ state.reuse_previous = False
1737
+ return "reuse-previous: no (step-by-step re-entry)"
1738
+ state.reuse_previous = True
1739
+ seg = slugify_task_segment(state.task_type)
1740
+ inputs = _latest_run_inputs(state, phase_segment=seg)
1741
+ state.use_defaults = False
1742
+ workers = inputs.get("workers")
1743
+ state.workers_override = (
1744
+ ",".join(w for w in workers if isinstance(w, str))
1745
+ if isinstance(workers, list) else "")
1746
+ state.lead_model = _safe_model("claude", inputs.get("leadModel"))
1747
+ state.claude_model = _safe_model("claude", inputs.get("claudeModel"))
1748
+ state.codex_model = _safe_model("codex", inputs.get("codexModel"))
1749
+ state.gemini_model = _safe_model("gemini", inputs.get("geminiModel"))
1750
+ state.report_writer_model = _safe_model(
1751
+ "claude", inputs.get("reportWriterModel"))
1752
+ directive = inputs.get("directive")
1753
+ state.directive = directive if isinstance(directive, str) else ""
1754
+ related = inputs.get("relatedTasks")
1755
+ state.related_tasks_raw = related if isinstance(related, str) else ""
1756
+ # clarification 은 직전 run 의 입력이 아니라 "직전 run 의 산출물(가장 최근
1757
+ # final-report)" 을 재실행 입력으로 자동 선택한다 — resume-clarification 의 본질.
1758
+ state.clarification_response_path = _suggest_latest_final_report(state)
1759
+ for sid in _REUSE_FILLED_STEPS:
1760
+ if sid not in state.answered:
1761
+ state.answered.append(sid)
1762
+ return (f"reuse-previous: yes "
1763
+ f"(workers={state.workers_override or 'profile-default'}, "
1764
+ f"lead-model={state.lead_model or 'default'})")
1765
+
1766
+
1700
1767
  def _build_defaults_or_custom(state: WizardState) -> Prompt:
1701
1768
  t = _p(state.workspace_root, "defaults_or_custom")
1702
1769
  return Prompt(
@@ -2158,6 +2225,18 @@ STEPS: list[Step] = [
2158
2225
  applies=lambda s: (s.critic_pending_text and S_CRITIC_TEXT not in s.answered),
2159
2226
  build=_build_critic_text, submit=_submit_critic_text,
2160
2227
  owns=("critic", "critic_pending_text")),
2228
+ # resume: identity 가 모두 정해진 뒤(직전 run 과 같은 task/phase 가 확정),
2229
+ # 모델/워커/directive/related/clarification 설정 단계로 들어가기 직전에
2230
+ # "직전 run 설정 그대로 재사용?" 를 한 번 묻는다. YES 면 그 단계들을 prefill 로
2231
+ # 충족시켜 곧장 confirm 으로 직행하고, NO 면 현행대로 단계별 입력을 받는다.
2232
+ Step(S_REUSE_PREVIOUS,
2233
+ applies=lambda s: (_identity_ready(s)
2234
+ and s.use_defaults is None
2235
+ and s.reuse_previous is None
2236
+ and s.task_type in _RESUME_REUSE_PHASES
2237
+ and _has_prior_run_inputs(s)),
2238
+ build=_build_reuse_previous, submit=_submit_reuse_previous,
2239
+ owns=("reuse_previous",)),
2161
2240
  Step(S_DEFAULTS_OR_CUSTOM,
2162
2241
  applies=lambda s: (_identity_ready(s)
2163
2242
  and s.use_defaults is None),
@@ -2365,6 +2444,7 @@ _FIELD_DEFAULTS: dict[str, Any] = {
2365
2444
  "approved_plan_pending_text": False,
2366
2445
  "selected_stage": "auto",
2367
2446
  "executor": "", "critic": "", "critic_pending_text": False,
2447
+ "reuse_previous": None,
2368
2448
  "use_defaults": None, "workers_override": "",
2369
2449
  "lead_model": "", "claude_model": "", "codex_model": "",
2370
2450
  "gemini_model": "", "report_writer_model": "", "directive": "",
@@ -235,12 +235,17 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
235
235
  {% for stage in implementationPlanning.stages %}
236
236
  ## 5.5.{{ stage.stage }} Stage {{ stage.stage }}: {{ stage.title }}
237
237
 
238
- Slice value: {{ stage.sliceValue }}
239
- Acceptance: {{ stage.acceptance }}
240
- {% if stage.conformanceTests %}Conformance tests: stage-{{ stage.stage }} — {{ stage.conformanceTests }}
241
- {% else %}Conformance exemption: {{ stage.conformanceExemption }}
242
- {% endif %}{% if stage.tddExemption %}TDD exemption: {{ stage.tddExemption }}
238
+ - **Slice value:** {{ stage.sliceValue }}
239
+ - **Acceptance:** {{ stage.acceptance }}
240
+ {% if stage.conformanceTests %}
241
+ - **Conformance tests:** stage-{{ stage.stage }} {{ stage.conformanceTests }}
242
+ {% else %}
243
+ - **Conformance exemption:** {{ stage.conformanceExemption }}
243
244
  {% endif %}
245
+ {% if stage.tddExemption %}
246
+ - **TDD exemption:** {{ stage.tddExemption }}
247
+ {% endif %}
248
+
244
249
  ### Carry-In
245
250
 
246
251
  {{ stage.carryIn }}
@@ -68,7 +68,7 @@
68
68
  "columnRelatedIds": "Related item IDs"
69
69
  },
70
70
  "finalVerdict": {
71
- "intro": "Final conclusion and recommended direction. `Direction` values: `continue-investigation / begin-implementation / approve / reject / hold`. When `task-type` is `final-verification`, `Verdict Token` is one of `accepted / conditional-accept / blocked` and serves as the `release-handoff` entry gate. For all other task-types: `not-applicable`."
71
+ "intro": "This run's final conclusion and next action. **`Direction`** is the recommended next action — one of `continue-investigation`, `begin-implementation`, `approve`, `reject`, or `hold` and is present for every task-type. **`Verdict Token`** is meaningful only for the `final-verification` task-type, where it is one of `accepted`, `conditional-accept`, or `blocked` and serves as the `release-handoff` entry gate. For every other task-type, `Verdict Token` is always `not-applicable`."
72
72
  },
73
73
  "evidence": {
74
74
  "sourceItemsColumnNote": "The `Source items` column is described in §6.1."
@@ -68,7 +68,7 @@
68
68
  "columnRelatedIds": "관련 항목 IDs"
69
69
  },
70
70
  "finalVerdict": {
71
- "intro": "최종 결론과 권장 방향입니다. `Direction` 값: `continue-investigation / begin-implementation / approve / reject / hold`. `task-type` `final-verification` `Verdict Token` `accepted / conditional-accept / blocked` 중 하나이며 `release-handoff` 진입 게이트로 쓰입니다. 그 외 task-type 에서는 `not-applicable`."
71
+ "intro": "이 run 의 최종 결론과 다음 행동입니다. **`Direction`** 권장 다음 행동으로 `continue-investigation`(조사 계속) · `begin-implementation`(구현 시작) · `approve`(승인) · `reject`(반려) · `hold`(보류) 중 하나이며, 모든 task-type 존재합니다. **`Verdict Token`** 은 `final-verification` task-type 에서만 의미를 가집니다 `accepted` · `conditional-accept` · `blocked` 중 하나로 `release-handoff` 진입 게이트로 쓰입니다. 그 외 task-type 에서 `Verdict Token` 은 항상 `not-applicable`(해당 없음) 입니다."
72
72
  },
73
73
  "evidence": {
74
74
  "sourceItemsColumnNote": "`Source items` 열 설명은 §6.1 과 동일합니다."
@@ -114,6 +114,16 @@ For task types `requirements-discovery`, `error-analysis`, `implementation-plann
114
114
 
115
115
  See `skills/okstra-team-contract/SKILL.md` "Worker Output Contract" for the full frontmatter schema and section ordering rules. This preamble is consistent with that contract; the team-contract document is authoritative if the two ever diverge.
116
116
 
117
+ ## Return message to the lead (all workers)
118
+
119
+ The short message you return inline to the lead — the text shown to the user as your agent box — MUST begin with one line identifying the model you ran under, copied verbatim from the `**Model:** <Role>, <modelExecutionValue>` line in your dispatch prompt:
120
+
121
+ ```
122
+ **Model:** <Role>, <modelExecutionValue>
123
+ ```
124
+
125
+ Emit that line first, then your normal status / summary text on the following lines. This keeps the acting model visible in every worker box — across every provider (Claude / Codex / Gemini / report-writer) and every dispatch type (initial analysis, convergence reverify, report authoring). Copy the value exactly; never guess, abbreviate, or substitute a provider default. If your prompt carries no `**Model:**` line, say so in the return message rather than inventing one. Each worker spec applies this rule at its own concrete return point — see `agents/workers/claude-worker.md` "Stop Condition", `agents/workers/_cli-wrapper-template.md` step 8d, and `agents/workers/report-writer-worker.md` "Authoring Contract".
126
+
117
127
  ## Writing style (all prose output — analysis workers + report-writer)
118
128
 
119
129
  When you write in Korean (`Report Language: ko` / `meta.reportLanguage = "ko"`), write so a Korean developer understands it on first read. Translate the **meaning**, never the dictionary word.
@@ -162,11 +162,16 @@ def _check_each_stage_section(text: str, stages: List[StageMeta]) -> List[Valida
162
162
  return errs
163
163
 
164
164
 
165
- SLICE_VALUE = re.compile(r"^\s*Slice value\s*:\s*(.+?)\s*$", re.M)
166
- ACCEPTANCE = re.compile(r"^\s*Acceptance\s*:\s*(.+?)\s*$", re.M)
167
- TDD_EXEMPTION = re.compile(r"^\s*TDD exemption\s*:\s*\S", re.M)
168
- CONFORMANCE_TESTS = re.compile(r"^\s*Conformance tests\s*:\s*\S", re.M)
169
- CONFORMANCE_EXEMPTION = re.compile(r"^\s*Conformance exemption\s*:\s*\S", re.M)
165
+ # Each label line may be plain (`Slice value: …`) or rendered as a bold-label
166
+ # bullet (`- **Slice value:** …`). The optional `(?:[-*]\s+)?` bullet marker and
167
+ # the two optional `\*\*` groups accept both forms; the bold wraps the colon, so
168
+ # the closing `**` sits AFTER the colon.
169
+ _LABEL_PREFIX = r"^\s*(?:[-*]\s+)?(?:\*\*)?"
170
+ SLICE_VALUE = re.compile(_LABEL_PREFIX + r"Slice value\s*:\s*(?:\*\*)?\s*(.+?)\s*$", re.M)
171
+ ACCEPTANCE = re.compile(_LABEL_PREFIX + r"Acceptance\s*:\s*(?:\*\*)?\s*(.+?)\s*$", re.M)
172
+ TDD_EXEMPTION = re.compile(_LABEL_PREFIX + r"TDD exemption\s*:\s*(?:\*\*)?\s*\S", re.M)
173
+ CONFORMANCE_TESTS = re.compile(_LABEL_PREFIX + r"Conformance tests\s*:\s*(?:\*\*)?\s*\S", re.M)
174
+ CONFORMANCE_EXEMPTION = re.compile(_LABEL_PREFIX + r"Conformance exemption\s*:\s*(?:\*\*)?\s*\S", re.M)
170
175
 
171
176
 
172
177
  def _check_slice_tdd(text: str, stages: List[StageMeta]) -> List[ValidationError]: