okstra 0.32.1 → 0.34.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.
@@ -880,7 +880,7 @@ Phase 7 step 1.5 가 final-report MD 한 본을 입력으로 두 view 를 결정
880
880
  ### Live-log mirror (codex / gemini wrapper)
881
881
 
882
882
  - `scripts/okstra-codex-exec.sh`, `scripts/okstra-gemini-exec.sh` 는 dispatch 마다 prompt path 옆에 `<prompt>.log` sidecar 를 만들고 stdout 을 거기로 mirror 합니다 (`tee`, `PIPESTATUS[0]` 로 종료코드 보존). stderr 은 같은 파일에 append (subagent stderr 캡처 contract 보존), 매 dispatch 시 truncate. 호출 subagent 의 `BashOutput` 폴링은 60s 간격이라 long-running run (analysis 의 large-codebase scan, implementation 의 cargo / pytest) 동안 사용자가 stalled state 를 탐지할 수 없는 문제를 해소합니다.
883
- - `$TMUX` 가 셋팅된 lead 환경이면 wrapper 가 sibling pane 을 자동 분할해 `tail -F <log-path>` 를 띄웁니다. pane title 은 `<cli>-<role>-trace` (e.g. `codex-worker-trace`, `gemini-worker-trace`); role 은 wrapper 의 5번째 optional positional 인자이며, 누락 시 기본값 `worker` 로 떨어집니다. caller 가 다른 라벨(예: `executor`)을 원하면 5번째 인자로 명시해야 합니다. focus 는 caller pane 으로 복귀하고, CLI 종료 후 pane 은 유지돼 스크롤백 가능. `$TMUX` 미설정, split 실패, 구버전 tmux 등 모든 경로는 silent degrade.
883
+ - `$TMUX` 가 셋팅된 lead 환경이면 wrapper 가 sibling pane 을 자동 분할해 `tail -F <log-path>` 를 띄웁니다. trace pane title 은 `<cli>-<role>-<pid>-trace` (e.g. `codex-worker-93421-trace`, `gemini-executor-93422-trace`); 동일 시점에 caller (worker) pane title 도 `<cli>-<role>-<pid>` 로 셋팅돼 worker ↔ trace 쌍을 한 눈에 매칭할 수 있습니다. `<pid>` 는 wrapper 자기 자신의 PID 라서 동일 role 의 worker 가 둘 이상 동시에 spawn 돼도 서로 구분됩니다. role 은 wrapper 의 5번째 optional positional 인자이며, 누락 시 기본값 `worker` 로 떨어집니다. caller 가 다른 라벨(예: `executor`)을 원하면 5번째 인자로 명시해야 합니다. wrapper 진입 직전의 caller pane title 은 capture 해두고 EXIT trap 에서 복원하므로, dispatch 사이의 stale title 이 남지 않습니다. focus 는 caller pane 으로 복귀하고, CLI 종료 후 pane 은 유지돼 스크롤백 가능. `$TMUX` 미설정, split 실패, 구버전 tmux 등 모든 경로는 silent degrade.
884
884
  - **Claude `/exit` 시 자동 정리**: trace pane 의 `tail -F` 는 tmux 셸의 자식이라 Claude 가 종료돼도 살아남는 문제를 막기 위해, wrapper 는 spawn 한 pane id 를 caller `$TMUX_PANE` 으로 키된 registry (`${TMPDIR:-/tmp}/okstra-trace-panes/<caller-pane>.list`) 에 append 합니다. `templates/reports/settings.template.json` 의 `hooks.SessionEnd` 가 `$HOME/.okstra/bin/okstra-trace-cleanup.sh` 를 호출해 자신의 caller pane registry 만 읽어 `tmux kill-pane` 합니다. caller pane 단위로 scope 가 잡혀 있어 같은 tmux 세션에 Claude 인스턴스가 여러 개 떠 있어도 서로의 trace pane 을 죽이지 않습니다. tmux 가 없거나 stale pane id 인 경우 silent degrade.
885
885
  - **Phase 종료 시 사용자 확인**: 매 phase 의 마지막 단계로 lead 가 `okstra-trace-cleanup.sh --list` 로 등록된 pane 목록을 출력한 뒤 사용자에게 "모두 닫기 / 그대로 두기" 양자택일을 묻고 응답대로 처리합니다 (`prompts/profiles/_common-contract.md` 의 *Phase wrap-up* 항목). `$TMUX_PANE` 미설정 환경에서는 단계 자체가 silent-skip. `--list` 모드는 pane 을 죽이지 않고 `<pane_id>\t<pane_title>` 만 출력하므로 사용자가 무엇이 닫힐지 시각적으로 확인할 수 있습니다.
886
886
  - 디스크 누적은 `okstra-logs` skill 이 read-only 로 인벤토리 + cleanup 명령을 제안합니다 (실행은 사용자 copy-paste).
package/docs/kr/cli.md CHANGED
@@ -546,5 +546,5 @@ chmod +x ~/.local/bin/okstra-ctl
546
546
 
547
547
  ### Live-log sidecar
548
548
 
549
- codex / gemini wrapper 는 매 dispatch 마다 `runs/<task-type>/prompts/<worker>-prompt-<phase>-<seq>.log` sidecar 를 만들고 stdout / stderr 를 mirror 합니다. tmux 안에서 lead 를 띄우면 wrapper 가 자동으로 `tail -F` pane 을 분할합니다 (title: `<cli>-<role>-trace`). 분할된 trace pane 은 caller `$TMUX_PANE` 으로 키된 registry 에 등록돼, Claude `/exit` 시 `SessionEnd` 훅이 `okstra-trace-cleanup.sh` 로 자동 정리합니다. 사용량 인벤토리와 `find … -delete` cleanup 명령은 `okstra-logs` skill 이 read-only 로 제안합니다. 자세한 와이어링은 [`docs/kr/architecture.md`](architecture.md) 의 *Live-log mirror* 절 참고.
549
+ codex / gemini wrapper 는 매 dispatch 마다 `runs/<task-type>/prompts/<worker>-prompt-<phase>-<seq>.log` sidecar 를 만들고 stdout / stderr 를 mirror 합니다. tmux 안에서 lead 를 띄우면 wrapper 가 자동으로 `tail -F` pane 을 분할합니다 (trace pane title: `<cli>-<role>-<pid>-trace`, caller (worker) pane title: `<cli>-<role>-<pid>` — wrapper PID 가 동일 role 의 동시 dispatch 를 구분합니다). 분할된 trace pane 은 caller `$TMUX_PANE` 으로 키된 registry 에 등록돼, Claude `/exit` 시 `SessionEnd` 훅이 `okstra-trace-cleanup.sh` 로 자동 정리합니다. 사용량 인벤토리와 `find … -delete` cleanup 명령은 `okstra-logs` skill 이 read-only 로 제안합니다. 자세한 와이어링은 [`docs/kr/architecture.md`](architecture.md) 의 *Live-log mirror* 절 참고.
550
550
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.32.1",
3
+ "version": "0.34.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.32.1",
3
- "builtAt": "2026-05-19T14:15:13.766Z",
2
+ "package": "0.34.0",
3
+ "builtAt": "2026-05-19T16:31:42.883Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -82,6 +82,27 @@ User-utterance interpretation rule:
82
82
  - If the current phase's outputs are already complete and the user clearly wants to advance, reply with the phase-transition checklist above and the exact next-run command. Wait for explicit user confirmation before any action that belongs to the next phase.
83
83
  - If `nextRecommendedPhase` is `implementation-planning`, the next run produces a **plan**, not code. The next run after that is `implementation`.
84
84
 
85
+ ## Progress reporting (BLOCKING)
86
+
87
+ A single okstra run frequently spans 30–120 minutes of wall-clock time with multi-minute silent windows while workers run. Without explicit progress signals the user cannot distinguish "still working" from "hung", so Lead MUST emit a single short progress line at each of the checkpoints below — as plain user-facing text in a separate brief message (not buried inside a tool call). One line per checkpoint, format: `PROGRESS: <phase-id> <verb-phrase>`.
88
+
89
+ Required checkpoints:
90
+
91
+ - `PROGRESS: phase-1-intake reading task bundle` — at the start of Phase 1, before issuing parallel Read calls.
92
+ - `PROGRESS: phase-1-intake complete` — after all intake reads return.
93
+ - `PROGRESS: phase-2-prompts preparing <N> worker prompts` — at the start of Phase 2, before any `Write` to the assigned prompt paths.
94
+ - `PROGRESS: phase-3-team-create attempting TeamCreate` — immediately before the `TeamCreate` call.
95
+ - `PROGRESS: phase-4-dispatch worker=<role> model=<model>` — once per worker, immediately before the `Agent` / wrapper call.
96
+ - `PROGRESS: phase-5-collect worker=<role> status=<terminal-status>` — once per worker, immediately after the result file is verified.
97
+ - `PROGRESS: phase-5.5-convergence round=<N> queue=<count>` — at the start of each convergence round (Phase 5.5).
98
+ - `PROGRESS: phase-6-synthesis dispatching report-writer-worker` — at the start of Phase 6.
99
+ - `PROGRESS: phase-7-persist updating manifests` — at the start of Phase 7.
100
+ - `PROGRESS: complete final-report=<relative-path>` — final summary line, after all persistence.
101
+
102
+ These lines are the only structured signal the user has during a long run. Do NOT replace them with prose ("Now I'm starting Phase 2..."), do NOT skip a checkpoint because "the previous message already said that", and do NOT batch multiple checkpoints into one. Each line stands alone so the user (or any operator scraping stdout) can timestamp it externally.
103
+
104
+ `okstra-run` (in-session) surfaces these lines to the user directly; the bash-spawned path leaves them in the session jsonl for post-hoc retrieval. Neither path requires any additional formatting from Lead — emit the literal `PROGRESS:` prefix and the rest of the line as plain text.
105
+
85
106
  ## Default model assignments
86
107
 
87
108
  Unless the task bundle overrides:
@@ -316,6 +337,7 @@ After persistence, reply briefly in Korean with: completion status, final report
316
337
  | Letting `convergence.maxRounds` default to 2 for `requirements-discovery` | Resolve effective default to `1` for discovery and record in convergence state artifact |
317
338
  | Issuing serial Read calls in Phase 1 | The intake files are independent — issue all Read calls in a single message (parallel) |
318
339
  | Flagging the claude-worker dispatch prompt as "incomplete" because it lacks `[Required reading]` / `[Error reporting]` blocks | Intentional asymmetry — see [okstra-team-contract](./skills/okstra-team-contract/SKILL.md) "Asymmetry between claude-worker and codex/gemini-worker prompts" |
340
+ | Waiting silently while the dispatched `claude-worker` Agent call returns nothing for many minutes (the dev-9495 pattern: two 28+25-minute hangs before lead manually `tmux kill-pane`d) | The claude-worker MUST append a `- PROGRESS: <stage> <ISO-UTC>` line to its audit sidecar (`runs/<task-type>/worker-results/claude-worker-audit-<task-type>-<seq>.md`) at least every 5 minutes (see `agents/workers/claude-worker.md` "Heartbeat" rule). If the sidecar is absent or its mtime is >5 minutes stale, treat the dispatch as `timeout` and redispatch once with a byte-identical prompt; after a second silent hang, record terminal status `timeout` with the missing-sidecar reason in team-state. Lead cannot poll mid-Agent-call but MUST inspect the audit sidecar immediately when the Agent call finally returns — a missing sidecar after `completed` is itself a contract violation per the heartbeat rule |
319
341
  | Re-sending confirmed findings (`full-consensus`/`partial-consensus`/`worker-unique`) to a worker in Round 2 | Queue pruning rule — see [okstra-convergence](./skills/okstra-convergence/SKILL.md) "Round 1-N: Re-verification Loop (queue-pruned)" |
320
342
  | Aggregating a `timeout`/`error` reverify dispatch as `DISAGREE` | Worker failure handling — record as `verification-error` and add to `skippedWorkers[]`. See [okstra-convergence](./skills/okstra-convergence/SKILL.md) "Worker failure handling in reverify" |
321
343
  | Skipping `--substitute-data` in the Phase 7 collector run | Always pass the flag — see [okstra-report-writer](./skills/okstra-report-writer/SKILL.md) "Phase 7 token-usage collector" |
@@ -59,6 +59,7 @@ Before producing any output, you MUST read every input file enumerated in the `[
59
59
  - Use a single `Read` call per file with no `offset` and no `limit`. If a file is genuinely too large for one read, page through it with explicit `offset` / `limit` calls that together cover the entire file, and record the page boundaries in your Findings.
60
60
  - For the carry-in clarification response, walk every row of `## 5. Clarification Items` (`C-001`, `C-002`, ...) in full, including rows whose `User input` cell is blank — a blank `User input` with `Status=open` is itself a signal you must surface, not skip. Skimming these rows is the most common failure mode here; the fact that the file you will eventually contribute to has a structurally similar section 5 is NOT a license to skim.
61
61
  - Before listing any Findings, write a Reading Confirmation block to your **audit sidecar** at `runs/<task-type>/worker-results/claude-worker-audit-<task-type>-<seq>.md` (sibling to your main worker-results file — substitute `claude-worker-<task-type>-<seq>.md` → `claude-worker-audit-<task-type>-<seq>.md`). The sidecar's body begins with `# Claude Worker Audit — <task-key>` followed by one short line per input file confirming end-to-end reading (e.g. `- Read task-brief.md end-to-end (147 lines).`). Do NOT include a `## 0. Reading Confirmation` heading in the main worker-results file — the validator now fails worker-results that contain one. If you cannot truthfully confirm a file end-to-end, record a `tool-failure` in the errors sidecar instead of fabricating Findings.
62
+ - **Heartbeat — write the audit sidecar EARLY and APPEND per stage (BLOCKING).** Because this worker runs as an in-process Agent or a fresh-session tmux pane, the lead has no `BashOutput`-style liveness signal while waiting for your return. The audit sidecar is the only signal that survives a silent hang. Write the sidecar immediately after extracting `Project Root` and the assigned paths — BEFORE the per-file end-to-end reads — with just the heading line (`# Claude Worker Audit — <task-key>`) and one `- PROGRESS: started <ISO-8601-UTC>` line. Then APPEND one short progress line per stage as you advance: `read-<filename>`, `analysis-start`, `findings-draft-start`, `findings-draft-complete`, `write-result-start`. Each line: `- PROGRESS: <stage> <ISO-8601-UTC>`. The append cadence MUST NOT exceed 5 minutes — if a single analysis stage is taking longer, emit a `- PROGRESS: in-stage:<stage> <ISO-8601-UTC>` heartbeat. A 5-minute stale sidecar mtime is the canonical "this worker has hung" signal for the operator (the lead is blocked on the Agent call and cannot detect this itself, but a human watching via `tail -F <audit-sidecar>` from another terminal can). Sidecar write/append uses `Write` (for the initial creation) and `Edit` / heredoc `>>` for the per-stage append — heredoc append is the lighter option once the file exists.
62
63
  - Do not skip a file because its name suggests its content is already familiar from a prior run. Each file is canonical for the current run only.
63
64
 
64
65
  ## Worker Output Structure
@@ -30,7 +30,7 @@ You are a Codex worker agent. Your job is to execute the OpenAI Codex CLI and re
30
30
  $HOME/.okstra/bin/okstra-codex-exec.sh "<absolute-project-root>" "<assigned-model-execution-value>" "<absolute-prompt-history-path>" [<absolute-worktree-path>] [<role>]
31
31
  ```
32
32
 
33
- The fifth argument `<role>` is the trace-pane label suffix (`codex-<role>-trace`); pass the literal string `worker` for every dispatch from this subagent. The wrapper defaults to `worker` when the argument is omitted, but pass it explicitly so the dispatch is self-describing.
33
+ The fifth argument `<role>` is folded into both the caller (worker) pane title `codex-<role>-<pid>` and the sibling trace-pane title `codex-<role>-<pid>-trace` (`<pid>` = the wrapper's PID, present so concurrent dispatches of the same role can be told apart). Pass the literal string `worker` for every dispatch from this subagent. The wrapper defaults to `worker` when the argument is omitted, but pass it explicitly so the dispatch is self-describing.
34
34
 
35
35
  The fourth argument is **mandatory for implementation phase** and optional otherwise. It must be the literal `EXECUTOR_WORKTREE_PATH` recorded in the run context; the wrapper forwards it to codex as `--add-dir`, which grants the codex sandbox write access to the worktree (where all implementation-phase mutations occur). Without it, codex's `workspace-write` sandbox is anchored only at `<project-root>` and rejects every Edit/Write that targets the worktree (EPERM), which is the failure pattern that originally motivated this argument.
36
36
 
@@ -30,7 +30,7 @@ You are a Gemini worker agent. Your job is to execute the Google Gemini CLI and
30
30
  $HOME/.okstra/bin/okstra-gemini-exec.sh "<absolute-project-root>" "<assigned-model-execution-value>" "<absolute-prompt-history-path>" [<absolute-worktree-path>] [<role>]
31
31
  ```
32
32
 
33
- The fifth argument `<role>` is the trace-pane label suffix (`gemini-<role>-trace`); pass the literal string `worker` for every dispatch from this subagent. The wrapper defaults to `worker` when the argument is omitted, but pass it explicitly so the dispatch is self-describing.
33
+ The fifth argument `<role>` is folded into both the caller (worker) pane title `gemini-<role>-<pid>` and the sibling trace-pane title `gemini-<role>-<pid>-trace` (`<pid>` = the wrapper's PID, present so concurrent dispatches of the same role can be told apart). Pass the literal string `worker` for every dispatch from this subagent. The wrapper defaults to `worker` when the argument is omitted, but pass it explicitly so the dispatch is self-describing.
34
34
 
35
35
  The fourth argument is **mandatory for implementation phase** and optional otherwise. It must be the literal `EXECUTOR_WORKTREE_PATH` recorded in the run context; the wrapper appends it to gemini's `--include-directories` list so the model can both read and operate on the worktree alongside project-root.
36
36
 
@@ -13,10 +13,21 @@
13
13
  # Bash($HOME/.okstra/bin/okstra-codex-exec.sh:*)
14
14
  #
15
15
  # Usage:
16
- # okstra-codex-exec.sh <project-root> <model-execution-value> <prompt-path> [worktree-path] [role]
16
+ # okstra-codex-exec.sh <project-root> <model-execution-value> <prompt-path> [worktree-path] [role] [idle-timeout-seconds]
17
17
  #
18
18
  # project-root / model-execution-value / prompt-path are required.
19
19
  #
20
+ # idle-timeout-seconds is optional (default 600 = 10 minutes). When > 0, an
21
+ # in-process watchdog polls the live-log mtime; if no stdout/stderr write
22
+ # occurs for that many seconds, the underlying `codex exec` is SIGTERM'd
23
+ # (then SIGKILL'd after a 5-second grace), the status sidecar gets a
24
+ # `{timeout: true, idle_seconds, idle_at_ts, terminated_by: "idle-watchdog"}`
25
+ # marker, and the wrapper exits non-zero. Pass `0` to disable. Default
26
+ # exists because silent worker hangs are the dominant lead-time waste —
27
+ # observed 28+25 minutes on hung claude-worker dispatches before manual
28
+ # kill; a 10-minute cap costs ≤10m per hang while leaving long but live
29
+ # runs untouched.
30
+ #
20
31
  # worktree-path is optional and used for okstra implementation phase, where the
21
32
  # executor must mutate files inside a git worktree that lives outside
22
33
  # project-root. When supplied (non-empty), it is forwarded to codex as
@@ -56,8 +67,8 @@
56
67
  # The wrapper exits non-zero on any preflight failure.
57
68
  set -euo pipefail
58
69
 
59
- if [[ $# -lt 3 || $# -gt 5 ]]; then
60
- printf 'usage: %s <project-root> <model-execution-value> <prompt-path> [worktree-path] [role]\n' "$(basename "$0")" >&2
70
+ if [[ $# -lt 3 || $# -gt 6 ]]; then
71
+ printf 'usage: %s <project-root> <model-execution-value> <prompt-path> [worktree-path] [role] [idle-timeout-seconds]\n' "$(basename "$0")" >&2
61
72
  exit 64
62
73
  fi
63
74
 
@@ -66,6 +77,12 @@ model="$2"
66
77
  prompt_path="$3"
67
78
  worktree_path="${4-}"
68
79
  role="${5:-worker}"
80
+ idle_timeout_secs="${6:-600}"
81
+
82
+ if ! [[ "$idle_timeout_secs" =~ ^[0-9]+$ ]]; then
83
+ printf 'okstra-codex-exec: idle-timeout-seconds must be a non-negative integer: %q\n' "$idle_timeout_secs" >&2
84
+ exit 69
85
+ fi
69
86
 
70
87
  if [[ -z "$project_root" || ! -d "$project_root" ]]; then
71
88
  printf 'okstra-codex-exec: project-root is missing or not a directory: %q\n' "$project_root" >&2
@@ -170,6 +187,21 @@ python3 "$script_dir/okstra-wrapper-status.py" \
170
187
  init "$status_path" "$(basename "$0")" "$role" "$$" "$started_ts" "$log_path" \
171
188
  >>"$log_path" 2>&1 || true
172
189
 
190
+ # Pane titles: worker (caller) pane gets `codex-<role>-<pid>`; the sibling
191
+ # trace pane appends `-trace`. The wrapper PID disambiguates concurrent
192
+ # dispatches of the same role (e.g. two `codex-worker` panes spawned in
193
+ # parallel) so the operator can match worker ↔ trace at a glance.
194
+ pane_label="codex-${role}-$$"
195
+ trace_label="${pane_label}-trace"
196
+
197
+ # Capture the caller pane's current title so the EXIT trap can restore it
198
+ # once the wrapper returns. Empty when not in tmux or capture fails — the
199
+ # restore step degrades to a no-op in that case.
200
+ original_caller_title=""
201
+ if [[ -n "${TMUX_PANE:-}" ]]; then
202
+ original_caller_title=$(tmux display-message -p -t "$TMUX_PANE" '#{pane_title}' 2>/dev/null || true)
203
+ fi
204
+
173
205
  _okstra_status_finish() {
174
206
  local exit_code=$?
175
207
  local ended_ts
@@ -178,17 +210,29 @@ _okstra_status_finish() {
178
210
  python3 "$script_dir/okstra-wrapper-status.py" \
179
211
  finish "$status_path" "$exit_code" "$ended_ts" "$duration_ms" \
180
212
  >>"$log_path" 2>&1 || true
213
+ if [[ -n "${TMUX_PANE:-}" && -n "$original_caller_title" ]]; then
214
+ tmux select-pane -t "$TMUX_PANE" -T "$original_caller_title" 2>/dev/null || true
215
+ fi
181
216
  }
182
217
  trap _okstra_status_finish EXIT
183
218
 
219
+ # Label the caller (worker) pane now that the restore trap is armed. Any
220
+ # failure after this point still rewinds the title to its prior value.
221
+ if [[ -n "${TMUX_PANE:-}" ]]; then
222
+ tmux select-pane -t "$TMUX_PANE" -T "$pane_label" 2>/dev/null || true
223
+ fi
224
+
184
225
  # When a tmux session is reachable, split a sibling pane that tails the live
185
226
  # log so the operator can watch codex's progress in real time without waiting
186
227
  # for the wrapper to exit. This fires in every phase the wrapper is invoked
187
228
  # from (analysis, error-analysis, implementation-planning, implementation,
188
229
  # …) — long-running codex dispatches are not implementation-specific. The
189
- # new pane carries the title `codex-<role>-trace` `role` is the optional
190
- # 5th positional arg (defaults to `worker`); callers that dispatch a
191
- # different role (e.g. `executor`) must pass it explicitly. The pane uses
230
+ # new pane carries the title `codex-<role>-<pid>-trace` (matching the
231
+ # caller pane's `codex-<role>-<pid>` label so worker trace pairs are
232
+ # greppable); `role` is the optional 5th positional arg (defaults to
233
+ # `worker`); callers that dispatch a different role (e.g. `executor`) must
234
+ # pass it explicitly. The `<pid>` suffix is the wrapper's PID and
235
+ # disambiguates concurrent dispatches of the same role. The pane uses
192
236
  # `tail -F`
193
237
  # (follow-by-name) so it survives any truncation a re-dispatch performs on
194
238
  # the same log path. Failures are tolerated silently: missing $TMUX, a tmux
@@ -201,7 +245,7 @@ if [[ -n "${TMUX:-}" ]]; then
201
245
  -c "$(dirname "$log_path")" \
202
246
  "tail -F $(printf '%q' "$log_path")" 2>/dev/null || true)
203
247
  if [[ -n "$trace_pane" ]]; then
204
- tmux select-pane -t "$trace_pane" -T "codex-${role}-trace" 2>/dev/null || true
248
+ tmux select-pane -t "$trace_pane" -T "$trace_label" 2>/dev/null || true
205
249
  tmux last-pane 2>/dev/null || true
206
250
  # Register the spawned pane so the `SessionEnd` hook (see
207
251
  # `okstra-trace-cleanup.sh`) can kill it when the caller's Claude
@@ -220,14 +264,64 @@ fi
220
264
  # stdin redirect, stderr capture, and pipeline mirroring are intentionally
221
265
  # inside the wrapper — this is the entire reason this script exists.
222
266
  #
223
- # stdout: tee'd to both the log file (for `tail -f`) AND the wrapper's own
267
+ # stdout: tee'd to both the live log (for `tail -f`) AND the wrapper's own
224
268
  # stdout (so the subagent's `BashOutput` still captures the final
225
- # text verbatim for Phase 5 synthesis).
226
- # stderr: appended to the log file only mirrors the prior `2>/dev/null`
269
+ # text verbatim for Phase 5 synthesis). Implemented via process
270
+ # substitution so codex itself stays a single addressable PID we
271
+ # can SIGTERM from the watchdog.
272
+ # stderr: appended to the live log only — mirrors the prior `2>/dev/null`
227
273
  # contract of keeping the wrapper's stderr stream clean.
228
- # exit: `PIPESTATUS[0]` preserves codex's own exit code (tee always 0).
229
- {
230
- codex exec -C "$project_root" ${extra_args[@]+"${extra_args[@]}"} --model "$model" --sandbox workspace-write - \
231
- < "$prompt_path" 2>> "$log_path"
232
- } | tee -a "$log_path"
233
- exit "${PIPESTATUS[0]}"
274
+ # exit: codex's own exit code is preserved by `wait`.
275
+ codex exec -C "$project_root" ${extra_args[@]+"${extra_args[@]}"} --model "$model" --sandbox workspace-write - \
276
+ < "$prompt_path" \
277
+ 2>> "$log_path" \
278
+ > >(tee -a "$log_path") &
279
+ codex_pid=$!
280
+
281
+ # Idle watchdog: poll the live log's mtime; if no write (stdout or stderr)
282
+ # arrives for $idle_timeout_secs, SIGTERM codex, give it a 5-second grace,
283
+ # then SIGKILL. Record the termination cause in the status sidecar so the
284
+ # caller (lead) can distinguish "ran to completion with non-zero exit" from
285
+ # "killed because it went silent". Set 0 to disable entirely.
286
+ watchdog_pid=""
287
+ if (( idle_timeout_secs > 0 )); then
288
+ poll_interval=$(( idle_timeout_secs / 20 ))
289
+ (( poll_interval < 5 )) && poll_interval=5
290
+ (( poll_interval > 30 )) && poll_interval=30
291
+ (
292
+ while kill -0 "$codex_pid" 2>/dev/null; do
293
+ sleep "$poll_interval"
294
+ kill -0 "$codex_pid" 2>/dev/null || exit 0
295
+ last_mtime=$(stat -f %m "$log_path" 2>/dev/null || stat -c %Y "$log_path" 2>/dev/null || printf '0')
296
+ now=$(date +%s)
297
+ idle=$(( now - last_mtime ))
298
+ if (( idle >= idle_timeout_secs )); then
299
+ printf '\n[okstra wrapper] idle-watchdog: %ds without stdout — terminating codex (pid=%d)\n' \
300
+ "$idle" "$codex_pid" >> "$log_path" 2>&1 || true
301
+ python3 "$script_dir/okstra-wrapper-status.py" \
302
+ timeout "$status_path" "$now" "$idle" >>"$log_path" 2>&1 || true
303
+ kill -TERM "$codex_pid" 2>/dev/null || true
304
+ sleep 5
305
+ kill -KILL "$codex_pid" 2>/dev/null || true
306
+ exit 0
307
+ fi
308
+ done
309
+ ) &
310
+ watchdog_pid=$!
311
+ fi
312
+
313
+ set +e
314
+ wait "$codex_pid"
315
+ codex_exit=$?
316
+ set -e
317
+
318
+ if [[ -n "$watchdog_pid" ]]; then
319
+ kill "$watchdog_pid" 2>/dev/null || true
320
+ wait "$watchdog_pid" 2>/dev/null || true
321
+ fi
322
+
323
+ # Drain the process-substitution tee so the final lines reach the live log
324
+ # and the caller's stdout before exit.
325
+ wait 2>/dev/null || true
326
+
327
+ exit "$codex_exit"
@@ -13,10 +13,19 @@
13
13
  # Bash($HOME/.okstra/bin/okstra-gemini-exec.sh:*)
14
14
  #
15
15
  # Usage:
16
- # okstra-gemini-exec.sh <project-root> <model-execution-value> <prompt-path> [worktree-path] [role]
16
+ # okstra-gemini-exec.sh <project-root> <model-execution-value> <prompt-path> [worktree-path] [role] [idle-timeout-seconds]
17
17
  #
18
18
  # project-root / model-execution-value / prompt-path are required.
19
19
  #
20
+ # idle-timeout-seconds is optional (default 600 = 10 minutes). When > 0, an
21
+ # in-process watchdog polls the live-log mtime; if no stdout/stderr write
22
+ # occurs for that many seconds, the underlying `gemini` is SIGTERM'd (then
23
+ # SIGKILL'd after a 5-second grace), the status sidecar gets a
24
+ # `{timeout: true, idle_seconds, idle_at_ts, terminated_by: "idle-watchdog"}`
25
+ # marker, and the wrapper exits non-zero. Pass `0` to disable. Kept in
26
+ # lock-step with `okstra-codex-exec.sh` — see that wrapper for the full
27
+ # design rationale.
28
+ #
20
29
  # worktree-path is optional and used for okstra implementation phase, where the
21
30
  # executor must mutate files inside a git worktree that lives outside
22
31
  # project-root. When supplied (non-empty), it is appended to gemini's
@@ -36,8 +45,8 @@
36
45
  # The wrapper exits non-zero on any preflight failure.
37
46
  set -euo pipefail
38
47
 
39
- if [[ $# -lt 3 || $# -gt 5 ]]; then
40
- printf 'usage: %s <project-root> <model-execution-value> <prompt-path> [worktree-path] [role]\n' "$(basename "$0")" >&2
48
+ if [[ $# -lt 3 || $# -gt 6 ]]; then
49
+ printf 'usage: %s <project-root> <model-execution-value> <prompt-path> [worktree-path] [role] [idle-timeout-seconds]\n' "$(basename "$0")" >&2
41
50
  exit 64
42
51
  fi
43
52
 
@@ -46,6 +55,12 @@ model="$2"
46
55
  prompt_path="$3"
47
56
  worktree_path="${4-}"
48
57
  role="${5:-worker}"
58
+ idle_timeout_secs="${6:-600}"
59
+
60
+ if ! [[ "$idle_timeout_secs" =~ ^[0-9]+$ ]]; then
61
+ printf 'okstra-gemini-exec: idle-timeout-seconds must be a non-negative integer: %q\n' "$idle_timeout_secs" >&2
62
+ exit 69
63
+ fi
49
64
 
50
65
  if [[ -z "$project_root" || ! -d "$project_root" ]]; then
51
66
  printf 'okstra-gemini-exec: project-root is missing or not a directory: %q\n' "$project_root" >&2
@@ -121,6 +136,21 @@ python3 "$script_dir/okstra-wrapper-status.py" \
121
136
  init "$status_path" "$(basename "$0")" "$role" "$$" "$started_ts" "$log_path" \
122
137
  >>"$log_path" 2>&1 || true
123
138
 
139
+ # Pane titles: worker (caller) pane gets `gemini-<role>-<pid>`; the sibling
140
+ # trace pane appends `-trace`. The wrapper PID disambiguates concurrent
141
+ # dispatches of the same role (e.g. two `gemini-worker` panes spawned in
142
+ # parallel) so the operator can match worker ↔ trace at a glance.
143
+ pane_label="gemini-${role}-$$"
144
+ trace_label="${pane_label}-trace"
145
+
146
+ # Capture the caller pane's current title so the EXIT trap can restore it
147
+ # once the wrapper returns. Empty when not in tmux or capture fails — the
148
+ # restore step degrades to a no-op in that case.
149
+ original_caller_title=""
150
+ if [[ -n "${TMUX_PANE:-}" ]]; then
151
+ original_caller_title=$(tmux display-message -p -t "$TMUX_PANE" '#{pane_title}' 2>/dev/null || true)
152
+ fi
153
+
124
154
  _okstra_status_finish() {
125
155
  local exit_code=$?
126
156
  local ended_ts
@@ -129,23 +159,34 @@ _okstra_status_finish() {
129
159
  python3 "$script_dir/okstra-wrapper-status.py" \
130
160
  finish "$status_path" "$exit_code" "$ended_ts" "$duration_ms" \
131
161
  >>"$log_path" 2>&1 || true
162
+ if [[ -n "${TMUX_PANE:-}" && -n "$original_caller_title" ]]; then
163
+ tmux select-pane -t "$TMUX_PANE" -T "$original_caller_title" 2>/dev/null || true
164
+ fi
132
165
  }
133
166
  trap _okstra_status_finish EXIT
134
167
 
168
+ # Label the caller (worker) pane now that the restore trap is armed. Any
169
+ # failure after this point still rewinds the title to its prior value.
170
+ if [[ -n "${TMUX_PANE:-}" ]]; then
171
+ tmux select-pane -t "$TMUX_PANE" -T "$pane_label" 2>/dev/null || true
172
+ fi
173
+
135
174
  # When a tmux session is reachable, split a sibling pane tailing the log so
136
175
  # the operator can watch progress live. This fires in every phase the
137
176
  # wrapper is invoked from — long-running gemini dispatches are not
138
- # implementation-specific. Title `gemini-<role>-trace` `role` is the
139
- # optional 5th positional arg (defaults to `worker`); callers that
140
- # dispatch a different role must pass it explicitly. See the codex
141
- # wrapper for the full design rationale and the silent-degrade failure
142
- # model.
177
+ # implementation-specific. Title `gemini-<role>-<pid>-trace` (matching the
178
+ # caller pane's `gemini-<role>-<pid>` label so worker trace pairs are
179
+ # greppable). `role` is the optional 5th positional arg (defaults to
180
+ # `worker`); callers that dispatch a different role must pass it
181
+ # explicitly. The `<pid>` suffix is the wrapper's PID and disambiguates
182
+ # concurrent dispatches of the same role. See the codex wrapper for the
183
+ # full design rationale and the silent-degrade failure model.
143
184
  if [[ -n "${TMUX:-}" ]]; then
144
185
  trace_pane=$(tmux split-window -h -P -F '#{pane_id}' \
145
186
  -c "$(dirname "$log_path")" \
146
187
  "tail -F $(printf '%q' "$log_path")" 2>/dev/null || true)
147
188
  if [[ -n "$trace_pane" ]]; then
148
- tmux select-pane -t "$trace_pane" -T "gemini-${role}-trace" 2>/dev/null || true
189
+ tmux select-pane -t "$trace_pane" -T "$trace_label" 2>/dev/null || true
149
190
  tmux last-pane 2>/dev/null || true
150
191
  # See `okstra-codex-exec.sh` for the registry rationale — kept in lock-step.
151
192
  if [[ -n "${TMUX_PANE:-}" ]]; then
@@ -163,14 +204,58 @@ fi
163
204
  # `--include-directories` plus the Project Root referenced in the prompt
164
205
  # body itself.
165
206
  #
166
- # stdout: tee'd to both the log file (for `tail -f`) AND the wrapper's own
207
+ # stdout: tee'd to both the live log (for `tail -f`) AND the wrapper's own
167
208
  # stdout (so the subagent's `BashOutput` still captures the final
168
- # text verbatim for Phase 5 synthesis).
169
- # stderr: appended to the log file only mirrors the prior `2>/dev/null`
209
+ # text verbatim for Phase 5 synthesis). Implemented via process
210
+ # substitution so gemini itself stays a single addressable PID we
211
+ # can SIGTERM from the watchdog.
212
+ # stderr: appended to the live log only — mirrors the prior `2>/dev/null`
170
213
  # contract of keeping the wrapper's stderr stream clean.
171
- # exit: `PIPESTATUS[0]` preserves gemini's own exit code (tee always 0).
172
- {
173
- gemini -p - -m "$model" -o text --include-directories "$include_dirs" \
174
- < "$prompt_path" 2>> "$log_path"
175
- } | tee -a "$log_path"
176
- exit "${PIPESTATUS[0]}"
214
+ # exit: gemini's own exit code is preserved by `wait`.
215
+ gemini -p - -m "$model" -o text --include-directories "$include_dirs" \
216
+ < "$prompt_path" \
217
+ 2>> "$log_path" \
218
+ > >(tee -a "$log_path") &
219
+ gemini_pid=$!
220
+
221
+ # Idle watchdog — see `okstra-codex-exec.sh` for the full rationale.
222
+ watchdog_pid=""
223
+ if (( idle_timeout_secs > 0 )); then
224
+ poll_interval=$(( idle_timeout_secs / 20 ))
225
+ (( poll_interval < 5 )) && poll_interval=5
226
+ (( poll_interval > 30 )) && poll_interval=30
227
+ (
228
+ while kill -0 "$gemini_pid" 2>/dev/null; do
229
+ sleep "$poll_interval"
230
+ kill -0 "$gemini_pid" 2>/dev/null || exit 0
231
+ last_mtime=$(stat -f %m "$log_path" 2>/dev/null || stat -c %Y "$log_path" 2>/dev/null || printf '0')
232
+ now=$(date +%s)
233
+ idle=$(( now - last_mtime ))
234
+ if (( idle >= idle_timeout_secs )); then
235
+ printf '\n[okstra wrapper] idle-watchdog: %ds without stdout — terminating gemini (pid=%d)\n' \
236
+ "$idle" "$gemini_pid" >> "$log_path" 2>&1 || true
237
+ python3 "$script_dir/okstra-wrapper-status.py" \
238
+ timeout "$status_path" "$now" "$idle" >>"$log_path" 2>&1 || true
239
+ kill -TERM "$gemini_pid" 2>/dev/null || true
240
+ sleep 5
241
+ kill -KILL "$gemini_pid" 2>/dev/null || true
242
+ exit 0
243
+ fi
244
+ done
245
+ ) &
246
+ watchdog_pid=$!
247
+ fi
248
+
249
+ set +e
250
+ wait "$gemini_pid"
251
+ gemini_exit=$?
252
+ set -e
253
+
254
+ if [[ -n "$watchdog_pid" ]]; then
255
+ kill "$watchdog_pid" 2>/dev/null || true
256
+ wait "$watchdog_pid" 2>/dev/null || true
257
+ fi
258
+
259
+ wait 2>/dev/null || true
260
+
261
+ exit "$gemini_exit"
@@ -3,6 +3,10 @@
3
3
  You are `Claude lead` for project `{{PROJECT_ID}}`.
4
4
  Invoke the `okstra` skill now. Read the manifests below for all task metadata, paths, model assignments, and worker roster.
5
5
 
6
+ ## Progress reporting (BLOCKING)
7
+
8
+ Emit one `PROGRESS: <phase-id> <verb-phrase>` line as plain user-facing text at every checkpoint enumerated in `agents/SKILL.md` "Progress reporting (BLOCKING)" — phase-1-intake start/complete, phase-2-prompts, phase-3-team-create, phase-4-dispatch (per worker), phase-5-collect (per worker), phase-5.5-convergence (per round), phase-6-synthesis, phase-7-persist, and final `complete`. One line per checkpoint, never batched, never replaced with prose. This is the only signal the user has during multi-minute silent windows.
9
+
6
10
  ## Current Phase Boundary
7
11
 
8
12
  - Current lifecycle phase: `{{WORKFLOW_CURRENT_PHASE}}`
@@ -26,7 +26,7 @@ profile document.
26
26
  - Anti-escalation rule (shared):
27
27
  - treating "다음 단계 진행해" or equivalent user phrases as authorisation to start a *different* lifecycle phase is forbidden. The next phase begins only in a separate okstra run launched with the new `--task-type`. Per-profile documents may further restrict this within their own scope.
28
28
  - Phase wrap-up — worker trace pane disposition (shared, MUST be the *last* step before returning control to the user):
29
- - Codex / Gemini worker wrappers spawn `tail -F` trace panes in the lead's tmux session (`codex-<role>-trace`, `gemini-<role>-trace`). They survive every worker invocation by design so the operator can scroll back through the final output, but accumulate across phases and clutter the screen.
29
+ - Codex / Gemini worker wrappers spawn `tail -F` trace panes in the lead's tmux session, titled `<cli>-<role>-<pid>-trace` (e.g. `codex-worker-93421-trace`, `gemini-executor-93422-trace`) — the matching caller (worker) pane is titled `<cli>-<role>-<pid>` so worker ↔ trace pairs can be matched by the shared `<pid>` suffix. They survive every worker invocation by design so the operator can scroll back through the final output, but accumulate across phases and clutter the screen.
30
30
  - When `$TMUX_PANE` is set, 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` exactly once. The output is a tab-separated `<pane_id>\t<pane_title>` list of every trace pane registered for this Claude session.
31
31
  - If the list is empty, skip the question — there is nothing to ask about.
32
32
  - Otherwise the lead MUST present the user with a strict binary choice **before** declaring the phase complete. Use one prompt of this shape (Korean preferred, English acceptable if the rest of the run is in English):