okstra 0.87.0 → 0.87.2

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.87.0",
3
+ "version": "0.87.2",
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.87.0",
3
- "builtAt": "2026-06-16T18:50:52.236Z",
2
+ "package": "0.87.2",
3
+ "builtAt": "2026-06-16T19:58:20.906Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -206,12 +206,13 @@ These phases are governed by [team-contract](./team-contract.md). It is the cano
206
206
 
207
207
  `TeamCreate` MUST be the first Agent-related tool call after Phase 2 prompt preparation. Do not call `Agent(... team_name: ...)` for any worker until this phase has executed — the Agent tool rejects `team_name` for non-existent teams with `"team을 먼저 생성하거나 team_name 없이 호출해야 합니다"` / `"team must be created first or call without team_name"`, and silently stripping `team_name` to retry is NOT a valid recovery (it loses the Teams split-pane behavior and is indistinguishable from never having attempted Teams mode).
208
208
 
209
+ 0. **Surface the Teams tools first (deferred-tool environments) — BLOCKING.** `TeamCreate` / `TeamDelete` / `SendMessage` / `TaskList` are commonly **deferred tools**: present by name but not callable until their schemas are loaded via ToolSearch. Before step 1, load them with the explicit selector form `ToolSearch("select:TeamCreate,TeamDelete,SendMessage,TaskList")` — NOT a keyword query. Keyword-search ranking is non-deterministic while MCP servers are still (re)connecting (this is why the failure surfaces on `implementation` entry, after a context compaction re-injects the deferred-tool list mid-reconnect, far more than on earlier phases), so a keyword miss is NOT evidence the environment lacks Agent Teams. If the `select:` result comes back without `TeamCreate`, retry the same `select:` call ONCE. A non-empty result means the tool exists — proceed to step 1 and let the actual `TeamCreate` return value decide ok/error. Only when the explicit `select:` for `TeamCreate` is empty on BOTH attempts may you treat the environment as lacking Agent Teams support and go to step 5.
209
210
  1. Call `TeamCreate(team_name: ..., description: "Lead-plus-worker okstra run for <task-key>")`. The team name comes verbatim from the launch prompt's Team Creation Gate block — `okstra-<task-key>`, and implementation stage runs append `-s<N>` (stage isolation: a leftover team from another stage of the same task must not collide).
210
211
  2. Record the `TeamCreate` outcome in team-state under `teamCreate: { attempted: true, status: "ok"|"error", error?: <message> }` AND the exact name as `teamName` before any dispatch. This is the audit trail that justifies a later no-`team_name` fallback, and `teamName` is the SSOT every later consumer (teardown, reconcile, token collector) reads.
211
212
  2-1. If `TeamCreate` fails with "team already exists" (stale leftover from an earlier attempt): call `TeamList`; if the team is listed in this session, `TeamDelete` it and retry step 1 once. If it is NOT listed, do NOT remove `~/.claude/teams/...` / `~/.claude/tasks/...` with shell commands on your own initiative — that is destructive harness-internal state and `rm -rf` is commonly denied by user permission rules. Ask the user via AskUserQuestion (recommended option: quarantine); on approval, move both dirs into `~/.okstra/trash/<UTC-timestamp>/` with `mv` (reversible), then retry step 1 once.
212
213
  3. Verify `team-state.lead.sessionId` is populated. The `okstra.sh` exec path fills it automatically (`generate_claude_session_id` → `claude --session-id ...`). The render-only / in-session takeover path (`okstra-run` skill) auto-detects the live session's jsonl via `resolve_inproc_lead_session_id`, but the detector is best-effort and may return empty if `~/.claude/projects/<encoded-cwd>/` is unreadable or has no jsonl yet. If `lead.sessionId` is empty at this point, write the running session's id into team-state before proceeding — Phase 7 token-usage collection depends on it and will fail with `lead jsonl not found (sessionId=)` otherwise.
213
214
  4. If `TeamCreate` succeeds, proceed to Phase 4 (dispatch with `team_name`).
214
- 5. If `TeamCreate` fails (tool unavailable, permission denied, environment lacks Agent Teams support), proceed to Phase 5 fallback (dispatch with `run_in_background: true` and no `team_name`).
215
+ 5. If `TeamCreate` fails permission denied, OR the step-0 explicit `select:` returned empty for `TeamCreate` on both attempts (environment genuinely lacks Agent Teams support) proceed to Phase 5 fallback (dispatch with `run_in_background: true` and no `team_name`). A keyword-`ToolSearch` miss, a "deferred tool not yet loaded" state, or any signal short of an attempted `TeamCreate` call / empty step-0 `select:` is NOT a failure: surface the tool (step 0) and call it before deciding. Recording `teamCreate.status: "error"` without either an attempted `TeamCreate` or a doubly-empty step-0 `select:` is a contract violation (it silently degrades the run to background dispatch and loses the Teams split-pane behavior).
215
216
 
216
217
  Use agent and subagent names that map cleanly to the selected worker roles. Do not create ambiguous role names that differ from `Claude worker`, `Codex worker`, `Antigravity worker`, or `Report writer worker`.
217
218
 
@@ -32,10 +32,11 @@ profile document.
32
32
  - Run-start pane recording (shared — runs ONCE at run start, before the FIRST worker dispatch):
33
33
  - The codex/antigravity wrappers now self-anchor their trace pane by walking their own ancestor PIDs against tmux `pane_pid`s (see `lib/okstra/tmux-pane.sh`), so they no longer depend on this file. The lead still records its own pane id here for the cleanup steps below (which-pane-to-never-kill) and as the "am I in tmux" gate. A bare `tmux display-message -p '#{pane_id}'` is NOT reliable for this — Claude Code's Bash tool strips `$TMUX`/`$TMUX_PANE`, so that command returns the most-recently-active *client's* pane (often a different session, or a foreign pane when the lead is launched outside tmux entirely). The lead therefore records via the same ancestry resolver.
34
34
  - The lead MUST run once, at run start: `mkdir -p "<RUN_DIR>/state" && { . "$HOME/.okstra/bin/lib/okstra/tmux-pane.sh" 2>/dev/null && okstra_resolve_caller_pane; } > "<RUN_DIR>/state/lead-pane.id" 2>/dev/null || true` (substitute the run's absolute `RUN_DIR`). When the lead is not inside a tmux pane (e.g. Claude launched from the GUI app) no ancestor matches a pane, the file is empty, and every pane step below silently no-ops — that empty/absent file is the single signal that the lead is not in tmux.
35
+ - This recording is **silent internal setup**: the lead MUST emit NOTHING to the user about it — no pane id, no `%NNN`, no `lead-pane.id` path, no announcement that the phase-start-reset / wrap-up-disposition steps "activate". Just record the file and proceed.
35
36
  - Phase-start pane reset (shared — runs BEFORE dispatching each new worker batch):
36
37
  - okstra creates two kinds of tmux pane per run: (a) **worker-agent panes** the harness gives to dispatched subagents (titled `claude-worker` / `codex-worker` / `antigravity-worker` / `report-writer-worker`), and (b) **trace panes** the codex/antigravity wrappers spawn (`<cli>-<role>-<pid>-tail`). Both accumulate across internal phases because each new phase dispatches a fresh worker batch and the prior panes are never reclaimed.
37
38
  - When `<RUN_DIR>/state/lead-pane.id` is non-empty (the lead is in tmux), the lead MUST run `$HOME/.okstra/bin/okstra-trace-cleanup.sh --run-dir "<RUN_DIR>"` **immediately before** dispatching the next phase's workers — i.e. just before emitting each `PROGRESS: phase-5.5-convergence round=<N>` marker and just before `PROGRESS: phase-6-synthesis dispatching report-writer-worker`. This closes every prior-phase okstra pane (worker-agent + trace) for this run, while NEVER killing the lead's own pane.
38
- - This is **automatic and silent** — NO user prompt. Report it in one short line (e.g. `이전 phase okstra pane 3개 정리`) and proceed. It is silent-skipped when the lead is not in tmux; the lead MUST NOT fabricate a synthetic pane list in that case.
39
+ - This is **automatic and silent** — NO user prompt. Report it in one short, plain-language line (e.g. `이전 phase okstra pane 3개 정리`) and proceed — never expose internal identifiers (`%NNN`, `lead-pane.id`, step names like "phase-start pane reset"). It is silent-skipped when the lead is not in tmux; the lead MUST NOT fabricate a synthetic pane list in that case.
39
40
  - Phase wrap-up — okstra pane disposition (shared, runs AFTER Phase 7 persistence/token collection and BEFORE teammate teardown):
40
41
  - At run end the only residual okstra panes are the LAST phase's (e.g. the `report-writer-worker` agent pane and any codex/antigravity trace pane). `okstra-trace-cleanup.sh --list --run-dir "<RUN_DIR>"` returns one tab-separated `<pane_id>\t<pane_title>` line per residual okstra pane (worker-agent + trace) for this run.
41
42
  - When `<RUN_DIR>/state/lead-pane.id` is non-empty, after the final-report file has been written and the routing recommendation has been issued, the lead MUST run `$HOME/.okstra/bin/okstra-trace-cleanup.sh --list --run-dir "<RUN_DIR>"` exactly once. The output lists every residual okstra pane (worker-agent + trace) for this run, never the lead's own pane.
@@ -7,7 +7,7 @@
7
7
  - codex
8
8
  - report-writer
9
9
  - Optional workers (opt-in via `--workers`):
10
- - antigravity — when added to the roster it joins the verifier set; when omitted only the Claude+Codex verifiers participate (`--executor antigravity` is therefore not selectable without explicitly listing `antigravity` in `--workers`)
10
+ - antigravity — when added to the roster it joins the verifier set; when omitted only the Claude+Codex verifiers participate. `--executor antigravity` requires `antigravity` in the roster: the direct CLI demands it explicitly in `--workers`, while the wizard adds it automatically when you pick antigravity as the executor (the executor pick *is* the explicit listing).
11
11
  - **Executor binding (resolved at run-prep time, fixed for this run):**
12
12
  - Executor display name: `{{EXECUTOR_DISPLAY_NAME}}`
13
13
  - Executor provider: `{{EXECUTOR_PROVIDER}}` (one of: `claude` | `codex` | `antigravity`; chosen via `--executor` or `OKSTRA_DEFAULT_EXECUTOR`, default `claude`)
@@ -219,7 +219,7 @@
219
219
  "approve_plan_confirm": {
220
220
  "label": "이 플랜으로 구현을 진행할까요?\n {path}\n· 예 — 이 plan 대로 구현을 실행합니다. 플랜이 아직 승인 전이면 지금 data.json(정본) + 리포트를 함께 approved 로 처리한 뒤 진행합니다. (markdown 만 손으로 고치면 일관성 검증에서 거부되므로 이 경로로 승인하세요.)\n· 아니오 — 진행하지 않습니다.",
221
221
  "label_final_verification": "이 plan 을 검증 기준으로 삼아 진행할까요?\n {path}\n· 예 — 선택한 stage 의 구현 결과를 위 plan 기준으로 검증합니다. (plan 이 아직 승인 전이면 data.json(정본) + 리포트를 함께 approved 로 처리한 뒤 검증합니다.)\n· 아니오 — 진행하지 않습니다.",
222
- "html_approval_note": "\nHTML 승인 기록 발견: {sidecar}\n 선택 옵션: {option}\n· 예(기록 적용) — 승인과 함께 위 옵션을 frontmatter 에 기록합니다.",
222
+ "html_approval_note": "\n보고서에서 내보낸 승인 기록을 발견했습니다: {sidecar}\n 선택한 구현 옵션: {option}\n· 예(기록 적용) — 승인과 함께 위 옵션을 frontmatter 에 기록합니다.",
223
223
  "html_approval_note_default_option": "(추천 옵션 그대로)",
224
224
  "echo_template": "approve-plan: {value}",
225
225
  "options": {
@@ -231,8 +231,8 @@
231
231
  "no": "아니오 — 진행하지 않음"
232
232
  },
233
233
  "options_html_approval": {
234
- "yes_apply": "예 — HTML 기록대로 승인 + 옵션 적용 (추천)",
235
- "yes": "예 — 승인만 (HTML 기록 무시)",
234
+ "yes_apply": "예 — 내보낸 기록대로 승인 + 옵션 적용 (추천)",
235
+ "yes": "예 — 승인만 (내보낸 기록 무시)",
236
236
  "no": "아니오 — 진행하지 않음"
237
237
  },
238
238
  "echo_variants": {
@@ -245,7 +245,7 @@
245
245
  "errors": {
246
246
  "declined": "진행을 선택하지 않으면 다음 단계로 넘어갈 수 없습니다. 진행(예)하거나 위저드를 종료하세요.",
247
247
  "still_unapproved": "approve-plan: 승인 처리 후에도 승인 상태가 아닙니다 (data.json/markdown 불일치): {path}",
248
- "unknown_option": "HTML 승인 기록의 옵션 `{option}` 이 plan 의 Option Candidates 에 없습니다. 유효 후보: {candidates}. 보고서 HTML 에서 다시 Export 하거나 '예 — 승인만' 을 선택하세요."
248
+ "unknown_option": "보고서에서 내보낸 승인 기록의 옵션 `{option}` 이 plan 의 Option Candidates 에 없습니다. 유효 후보: {candidates}. 보고서에서 다시 내보내거나 '예 — 승인만' 을 선택하세요."
249
249
  }
250
250
  },
251
251
  "stage_pick": {
@@ -407,7 +407,8 @@ def _emit_table(lines: list[str], start: int, section_path: list[str]) -> tuple[
407
407
  f"<td{_col_class(idx, narrow_cols)}>{_inline(cell)}</td>"
408
408
  )
409
409
  body_rows.append(
410
- f'<tr data-response-id="{html.escape(meta.row_id)}" '
410
+ f'<tr id="{html.escape(meta.row_id.lower())}" '
411
+ f'data-response-id="{html.escape(meta.row_id)}" '
411
412
  f'data-kind="{html.escape(meta.kind)}" '
412
413
  f'data-status="{html.escape(meta.status)}">'
413
414
  + "".join(cells_html)
@@ -549,21 +550,14 @@ def _form_control(
549
550
  "</select>"
550
551
  )
551
552
  other_html = (
552
- f' <input type="text" data-other-for="{rid}" '
553
- f'placeholder="기타 응답"{disabled} hidden value="{safe_value}">'
553
+ f' <textarea data-other-for="{rid}" '
554
+ f'placeholder="기타 응답" rows="2"{disabled} hidden>'
555
+ f"{safe_value}</textarea>"
554
556
  )
555
557
  return select_html + other_html
556
558
 
557
- # material/data-point 류는 짧은 단일값. 텍스트 인풋으로.
558
- if kind_lc in ("material", "data-point"):
559
- return (
560
- f'<input type="text" name="{rid}" data-response-id="{rid}" '
561
- f'data-kind="{html.escape(kind)}" '
562
- f'placeholder="{html.escape(placeholder)}"{disabled} '
563
- f'value="{safe_value}">'
564
- )
565
-
566
- # 그 외 (decision-without-enum 포함) — 자유 본문은 textarea 유지.
559
+ # 나머지 kind(material/data-point/decision-without-enum)는 모두 자유 입력.
560
+ # 넘는 응답을 편하게 적도록 고정 높이 input 대신 textarea 로 렌더한다.
567
561
  return (
568
562
  f'<textarea name="{rid}" data-response-id="{rid}" '
569
563
  f'data-kind="{html.escape(kind)}" rows="2" '
@@ -846,7 +846,13 @@ def _resolved_roster(state: WizardState) -> list[str]:
846
846
  """Effective worker list AFTER override. Implementation: profile default
847
847
  (caller never asks for override). Others: override or profile default."""
848
848
  if state.task_type == "implementation":
849
- return list(state.profile_workers)
849
+ roster = list(state.profile_workers)
850
+ # implementation 은 roster override 단계를 건너뛰므로, executor 로 명시
851
+ # 선택한 워커(예: antigravity)가 프로필 기본 roster 에 없으면 여기서
852
+ # 합류시켜야 render-bundle 의 executor∈roster 가드를 통과한다.
853
+ if state.executor and state.executor not in roster:
854
+ roster.append(state.executor)
855
+ return roster
850
856
  if state.workers_override.strip():
851
857
  return normalize_workers(state.workers_override)
852
858
  return list(state.profile_workers)
@@ -121,7 +121,7 @@ That is the entire interactive flow. The wizard handles:
121
121
  - task-type pick (추천 3개 — `nextRecommendedPhase` recommended / 현재 phase 재실행 / 라이프사이클 다음 단계 — + 직접 입력; 직접 입력은 후속 `text` 단계에서 전체 task-type 화이트리스트로 검증),
122
122
  - brief path — **entry task-type(requirements-discovery / error-analysis / improvement-discovery)에서만 질문** (same-group `.okstra/briefs/<task-group>/**/*.md` candidates first, direct input last; `유지 / 변경` for existing entry tasks). downstream task-type 은 manifest 의 brief 를 자동 carry-in 하고, 등록 brief 가 없으면 `brief_carry` 3-옵션(entry 전환 추천 / 직접 입력 / 중단)이 뜬다. `release-handoff` 는 brief 단계가 아예 없다 — prepare 가 검증 보고서 인용 input 문서를 생성한다,
123
123
  - base-ref pick + git rev-parse validation (skipped when reusing an active worktree),
124
- - `implementation`-only sub-flow: approved-plan path (frontmatter `approved: true` check) + stage pick (`auto` = 의존성 충족된 가장 빠른 미완료 stage, 또는 특정 stage 번호) + executor pick. approved-plan 선택 시 그 run 의 sibling `user-responses/` 에서 plan 과 source-report·seq 가 일치하는 HTML `## APPROVAL` sidecar 를 감지하면 approve-confirm 단계가 3-옵션(`yes_apply` HTML 기록대로 승인+옵션 적용 추천 / `yes` 승인만 / `no` 중단)으로 확장된다 — `yes_apply` 는 옵션을 plan 의 `optionCandidates` 에 대해 검증한 뒤 기존 승인·옵션 경로로 적용한다,
124
+ - `implementation`-only sub-flow: approved-plan path (frontmatter `approved: true` check) + stage pick (`auto` = 의존성 충족된 가장 빠른 미완료 stage, 또는 특정 stage 번호) + executor pick. approved-plan 선택 시 그 run 의 sibling `user-responses/` 에서 plan 과 source-report·seq 가 일치하는, 보고서에서 내보낸 `## APPROVAL` sidecar 를 감지하면 approve-confirm 단계가 3-옵션(`yes_apply` 내보낸 기록대로 승인+옵션 적용 추천 / `yes` 승인만 / `no` 중단)으로 확장된다 — `yes_apply` 는 옵션을 plan 의 `optionCandidates` 에 대해 검증한 뒤 기존 승인·옵션 경로로 적용한다,
125
125
  - `release-handoff`-only sub-flow: approved plan 자동 해소 후 `handoff_stage_pick` 멀티선택 — eligible stage 묶음(stage-group) 또는 전체 task(accepted whole-task 검증 보고서 존재 시) 선택; 결과는 render-args 의 `stages` 키(csv, whole-task 면 빈 값)로 나간다,
126
126
  - `Use defaults / Customize` branch with profile-aware worker/model questions,
127
127
  - `release-handoff` PR template override + persist scope,
@@ -3,10 +3,9 @@
3
3
  * Responsibilities:
4
4
  * 1. Collect entry values for every <tr data-response-id> whose
5
5
  * Status is open/answered (disabled rows skipped automatically).
6
- * Widgets: <select> (enum decision), <input data-other-for> (기타
7
- * input revealed when select == "__other__"), <input
8
- * data-response-id> (material/data-point single-line), <textarea>
9
- * (everything else / fallback).
6
+ * Widgets: <select> (enum decision), <textarea data-other-for>
7
+ * (기타 input revealed when select == "__other__"), <textarea
8
+ * data-response-id> (everything else / fallback).
10
9
  * 2. Serialise the entries into markdown whose bytes are IDENTICAL
11
10
  * to scripts/okstra_ctl/report_views.py serialize_user_response.
12
11
  * 3. Write the result to <pre id="user-response-output">, offer a
@@ -56,7 +55,7 @@
56
55
  if (picked === "") return "";
57
56
  if (picked === "__other__") {
58
57
  var rid = sel.getAttribute("data-response-id") || "";
59
- var other = row.querySelector('input[data-other-for="' + rid + '"]');
58
+ var other = row.querySelector('textarea[data-other-for="' + rid + '"]');
60
59
  return other ? trimMultiline(other.value) : "";
61
60
  }
62
61
  var opt = sel.options[sel.selectedIndex];
@@ -69,11 +68,6 @@
69
68
  if (ta.disabled) return "";
70
69
  return trimMultiline(ta.value);
71
70
  }
72
- var inp = row.querySelector("input[data-response-id]");
73
- if (inp) {
74
- if (inp.disabled) return "";
75
- return trimMultiline(inp.value);
76
- }
77
71
  return "";
78
72
  }
79
73
 
@@ -119,7 +113,7 @@
119
113
  for (var i = 0; i < selects.length; i++) {
120
114
  (function (sel) {
121
115
  var rid = sel.getAttribute("data-response-id") || "";
122
- var other = document.querySelector('input[data-other-for="' + rid + '"]');
116
+ var other = document.querySelector('textarea[data-other-for="' + rid + '"]');
123
117
  if (!other) return;
124
118
  var update = function () {
125
119
  other.hidden = sel.value !== "__other__";