okstra 0.92.0 → 0.92.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.92.0",
3
+ "version": "0.92.1",
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.92.0",
3
- "builtAt": "2026-06-18T07:29:46.716Z",
2
+ "package": "0.92.1",
3
+ "builtAt": "2026-06-18T08:35:38.504Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -358,7 +358,7 @@
358
358
  }
359
359
  },
360
360
  "defaults_or_custom": {
361
- "label": "역할별 모델·실행 옵션 단계입니다 (참여 워커 구성을 바꾸는 게 아닙니다).\n이번 run 워커: {workers}\n· 기본값으로 진행 — 모든 역할을 추천 모델로 둡니다. 추천 기본값: lead·report-writer=opus, claude=opus, codex=gpt-5.5, antigravity=auto (실제 값은 runtime 기본값으로 해소). directive·관련 task 없이 바로 진행.\n· 커스터마이즈 — 역할별 모델을 직접 고르고, 추가 directive·관련 task 도 지정합니다.",
361
+ "label": "역할별 모델 선택 단계입니다 (참여 워커 구성을 바꾸는 게 아닙니다).\n이번 run 에서 모델을 고를 역할:\n{role_models}\n· 기본값으로 진행 — 추천 모델을 그대로 쓰고, directive·관련 task 없이 바로 넘어갑니다.\n· 커스터마이즈 — 역할별 모델을 직접 고르고, 추가 directive·관련 task 도 지정합니다.\n(추천 값은 runtime 기본값으로 해소됩니다. 분석에 참여하지 않는 역할은 위 목록에 나오지 않습니다 — 예: antigravity 는 implementation 의 executor 로 선택했을 때만 표시됩니다.)",
362
362
  "echo_template": "customize: {value}",
363
363
  "options": {
364
364
  "defaults": "기본값으로 진행 (역할별 추천 모델 그대로)",
@@ -1315,33 +1315,42 @@ class _ModelBindings:
1315
1315
  executor_model_meta: ModelAssignment
1316
1316
 
1317
1317
 
1318
+ def recommended_role_models() -> dict[str, str]:
1319
+ """역할 → 추천 모델 display 값 (env override 반영). prepare 의 모델 해소와
1320
+ wizard 의 안내 표기가 공유하는 단일 기준점."""
1321
+ lead_default = _default("OKSTRA_DEFAULT_LEAD_MODEL", "opus")
1322
+ return {
1323
+ "lead": lead_default,
1324
+ "claude": _default("OKSTRA_DEFAULT_CLAUDE_MODEL", "opus"),
1325
+ "codex": _default("OKSTRA_DEFAULT_CODEX_MODEL", "gpt-5.5"),
1326
+ "antigravity": _default("OKSTRA_DEFAULT_ANTIGRAVITY_MODEL", "auto"),
1327
+ "report-writer": _default("OKSTRA_DEFAULT_REPORT_WRITER_MODEL", lead_default),
1328
+ }
1329
+
1330
+
1318
1331
  def _resolve_worker_models(inp: PrepareInputs) -> dict:
1319
1332
  """lead/claude/codex/antigravity/report-writer 모델을 alias → (display, execution) 로 해소."""
1320
- lead_default = _default("OKSTRA_DEFAULT_LEAD_MODEL", "opus")
1321
- claude_default = _default("OKSTRA_DEFAULT_CLAUDE_MODEL", "opus")
1322
- codex_default = _default("OKSTRA_DEFAULT_CODEX_MODEL", "gpt-5.5")
1323
- antigravity_default = _default("OKSTRA_DEFAULT_ANTIGRAVITY_MODEL", "auto")
1324
- report_writer_default = _default("OKSTRA_DEFAULT_REPORT_WRITER_MODEL", lead_default)
1333
+ rec = recommended_role_models()
1325
1334
  return {
1326
1335
  "lead": resolve_model_metadata(
1327
1336
  provider="claude", raw_value=inp.lead_model,
1328
- default_display=lead_default, default_execution=lead_default,
1337
+ default_display=rec["lead"], default_execution=rec["lead"],
1329
1338
  ),
1330
1339
  "cw": resolve_model_metadata(
1331
1340
  provider="claude", raw_value=inp.claude_model,
1332
- default_display=claude_default, default_execution=claude_default,
1341
+ default_display=rec["claude"], default_execution=rec["claude"],
1333
1342
  ),
1334
1343
  "co": resolve_model_metadata(
1335
1344
  provider="codex", raw_value=inp.codex_model,
1336
- default_display=codex_default, default_execution=codex_default,
1345
+ default_display=rec["codex"], default_execution=rec["codex"],
1337
1346
  ),
1338
1347
  "ge": resolve_model_metadata(
1339
1348
  provider="antigravity", raw_value=inp.antigravity_model,
1340
- default_display=antigravity_default, default_execution=antigravity_default,
1349
+ default_display=rec["antigravity"], default_execution=rec["antigravity"],
1341
1350
  ),
1342
1351
  "rw": resolve_model_metadata(
1343
1352
  provider="claude", raw_value=inp.report_writer_model,
1344
- default_display=report_writer_default, default_execution=report_writer_default,
1353
+ default_display=rec["report-writer"], default_execution=rec["report-writer"],
1345
1354
  ),
1346
1355
  }
1347
1356
 
@@ -44,6 +44,7 @@ from okstra_ctl.run import (
44
44
  _reject_blocking_plan_body_gate,
45
45
  _set_data_json_approved_true_if_present,
46
46
  _validate_data_json_approval_consistency,
47
+ recommended_role_models,
47
48
  )
48
49
  from okstra_ctl.user_response import (
49
50
  UserResponseApprovalRecord,
@@ -93,6 +94,15 @@ TASK_TYPE_VALUES = [tt for tt, _ in TASK_TYPES]
93
94
 
94
95
  EXECUTORS = ["claude", "codex", "antigravity"]
95
96
 
97
+ # 역할 한 줄 설명 — defaults_or_custom 안내에서 각 역할이 무슨 일을 하는지 보여준다.
98
+ ROLE_BLURBS = {
99
+ "lead": "Claude lead · 워커 dispatch·교차검증 수렴·최종 합성",
100
+ "claude": "독립 분석 워커 (Claude)",
101
+ "codex": "독립 분석 워커 (Codex)",
102
+ "antigravity": "독립 분석 워커 (Antigravity)",
103
+ "report-writer": "최종 리포트 작성 (분석은 하지 않음)",
104
+ }
105
+
96
106
  # Task types that consume an approved plan and need a stage-scope pick:
97
107
  # implementation executes a stage, final-verification verifies a stage
98
108
  # (or the whole task via `auto`). Both gate the approved-plan + stage steps.
@@ -2357,11 +2367,34 @@ def _submit_reuse_previous(state: WizardState, value: str) -> Optional[str]:
2357
2367
  f"lead-model={state.lead_model or 'default'})")
2358
2368
 
2359
2369
 
2370
+ def _role_model_lines(state: WizardState) -> str:
2371
+ """이번 run 에서 실제로 모델을 고르게 되는 역할만, 추천 모델과 함께 나열한다.
2372
+ 뒤따르는 *_model 단계의 등장 조건과 1:1 로 맞춰 안내와 실제 화면이 어긋나지
2373
+ 않게 한다 (그래서 분석에 참여하지 않는 antigravity 는 executor 일 때만 나온다)."""
2374
+ rec = recommended_role_models()
2375
+ roster = _resolved_roster(state)
2376
+ impl = state.task_type == "implementation"
2377
+
2378
+ def line(role_label: str, blurb: str, model: str) -> str:
2379
+ return f" · {role_label} ({blurb}) — 추천 {model}"
2380
+
2381
+ lines = [line("lead", ROLE_BLURBS["lead"], rec["lead"])]
2382
+ if impl:
2383
+ ex = state.executor or "claude"
2384
+ lines.append(line(f"executor={ex}", "코드 구현 실행자", rec.get(ex, "auto")))
2385
+ else:
2386
+ for w in ("claude", "codex", "antigravity"):
2387
+ if w in roster:
2388
+ lines.append(line(w, ROLE_BLURBS[w], rec[w]))
2389
+ if impl or "report-writer" in roster:
2390
+ lines.append(line("report-writer", ROLE_BLURBS["report-writer"],
2391
+ rec["report-writer"]))
2392
+ return "\n".join(lines)
2393
+
2394
+
2360
2395
  def _build_defaults_or_custom(state: WizardState) -> Prompt:
2361
- roster = (state.workers_override
2362
- or ",".join(state.profile_workers)
2363
- or "(profile default)")
2364
- t = _p(state.workspace_root, "defaults_or_custom", workers=roster)
2396
+ t = _p(state.workspace_root, "defaults_or_custom",
2397
+ role_models=_role_model_lines(state))
2365
2398
  return Prompt(
2366
2399
  step=S_DEFAULTS_OR_CUSTOM, kind="pick",
2367
2400
  label=t["label"],
@@ -3306,11 +3339,16 @@ def _remaining_screens(state: WizardState) -> int:
3306
3339
  return screens
3307
3340
 
3308
3341
 
3309
- def _screen_progress(state: WizardState) -> dict[str, int]:
3342
+ def _screen_progress(state: WizardState) -> dict[str, Any]:
3310
3343
  passed = _passed_screens(state)
3311
3344
  total = passed + _remaining_screens(state)
3312
3345
  index = passed + 1
3313
- return {"index": index, "total": total, "remaining": max(0, total - index)}
3346
+ remaining = max(0, total - index)
3347
+ suffix = "마지막 단계" if remaining == 0 else f"앞으로 {remaining} 스텝 남음"
3348
+ return {
3349
+ "index": index, "total": total, "remaining": remaining,
3350
+ "label": f"Step {index}/{total} · {suffix}",
3351
+ }
3314
3352
 
3315
3353
 
3316
3354
  def prompt_payload(state: WizardState, prompt: Prompt) -> dict[str, Any]:
@@ -32,7 +32,7 @@ Every wizard call returns JSON. The two shapes you'll see:
32
32
  "progress": { "index": 5, "total": 11, "remaining": 6 } } }
33
33
  ```
34
34
 
35
- Every non-terminal `next` carries a `progress` object (`done` / `aborted` omit it). `index` is the 1-based number of the **current screen** (pick_group counts as one screen), `total` is the wizard's estimate of the full screen count, and `remaining` = `total index`. The total is a forward estimate from the current answers, so it may grow by a few when the user opens a branch (e.g. choosing *Customize* adds the model screen). **Always suffix the rendered prompt with the progress marker** — see Step 3.
35
+ Every non-terminal `next` carries a `progress` object (`done` / `aborted` omit it). It holds `index` (1-based number of the **current screen**; pick_group counts as one screen), `total` (the wizard's forward estimate of the full screen count may grow by a few when the user opens a branch, e.g. choosing *Customize* adds the model screen), `remaining` (`total index`), and **`label`** the ready-to-render Korean marker the wizard already composed (e.g. `Step 10/11 · 앞으로 1 스텝 남음`, or `Step 11/11 · 마지막 단계` on the final screen). **Always suffix the rendered prompt with `progress.label` verbatim** — see Step 3. Do not recompute the marker or decide "마지막 단계" yourself; only the wizard knows whether more screens follow.
36
36
 
37
37
  ```json
38
38
  { "ok": false, "error": "approved plan has no APPROVED marker: ...",
@@ -96,7 +96,7 @@ Output: the same `{ok, next}` JSON described above. The first `next` is always `
96
96
 
97
97
  Repeat until `next.kind == "done"` (or `"aborted"` — terminal cancel, see "How the wizard talks to you"):
98
98
 
99
- 1. **Render** the prompt according to `kind` (and `multi` for pick). **Always append the progress marker to the rendered question label** (the `AskUserQuestion` question text, or the `text`-prompt message): suffix `label` with ` (Step <index>/<total> · 남은 <remaining>단계)`, using `next.progress`. When `remaining == 0` (the final `confirm` screen), use `(Step <index>/<total> · 마지막 단계)` instead. Example: `모델을 선택하세요 (Step 8/11 · 남은 3단계)`. Re-prompts after `ok: false` reuse `current.progress` the same way. The progress marker is presentation-only — never send it back to the wizard as part of an answer.
99
+ 1. **Render** the prompt according to `kind` (and `multi` for pick). **Always append the progress marker to the rendered question label** (the `AskUserQuestion` question text, or the `text`-prompt message): suffix it with ` (<next.progress.label>)` render `progress.label` exactly as the wizard sent it, never recompute it. Example: label `Step 8/11 · 앞으로 3 스텝 남음` `모델을 선택하세요 (Step 8/11 · 앞으로 3 스텝 남음)`. Re-prompts after `ok: false` reuse `current.progress.label` the same way. The progress marker is presentation-only — never send it back to the wizard as part of an answer.
100
100
  - `pick` + `multi: false` → `AskUserQuestion` with `multiSelect: false`, `label`, and `options`. The user's chosen option's `value` is the answer string.
101
101
  - `pick` + `multi: true` → `AskUserQuestion` with `multiSelect: true`, `label`, and `options`. Join the selected `value`s with `,` into a single literal CSV string (e.g. `"claude,codex,antigravity"`) and submit it as a single `--answer "claude,codex,antigravity"`. Empty selection submits `--answer ""` and the wizard re-prompts.
102
102
  - `pick_group` → one `AskUserQuestion` with one question per `questions[]` entry (tab). Map each tab's selected `value` back by `questions[].step`, assemble a JSON object, and submit it as a single literal `--answer '<json>'`.