okstra 0.18.3 → 0.19.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.
@@ -105,10 +105,20 @@ okstra 의 prepare 책임은 단일 python 진입점 [`okstra_ctl.run.prepare_ta
105
105
 
106
106
  ### Python module 진입점 (single authority)
107
107
 
108
- - [`okstra_ctl.run`](scripts/okstra_ctl/run.py) `prepare_task_bundle()` orchestrator + argparse CLI (`python3 -m okstra_ctl.run --workspace-root ... --project-root ... ...`).
108
+ > **호출 규약.** 아래 `python3 -m okstra_ctl.*` / `python3 -m okstra_project.*` 형태는 **모듈 식별자**일 뿐이며, 시스템 site-packages에 설치되지 않습니다. 직접 셸에서 호출하려면 먼저 `PYTHONPATH` 를 `~/.okstra/lib/python` 으로 export 해야 합니다:
109
+ >
110
+ > ```bash
111
+ > eval "$(okstra paths --shell)" # OKSTRA_PYTHONPATH 등을 export
112
+ > export PYTHONPATH="$OKSTRA_PYTHONPATH"
113
+ > python3 -m okstra_ctl.run --help # 이제 동작
114
+ > ```
115
+ >
116
+ > 위 두 줄을 생략하면 `ModuleNotFoundError: No module named 'okstra_ctl'` 로 즉시 실패합니다 (실제 implementation phase 워커가 docs 만 보고 직접 호출하다가 자주 겪는 패턴). 일반 사용자/워커는 모듈을 직접 부르지 말고 `scripts/okstra.sh` 또는 `/okstra-run` 진입점을 사용하세요 — 그 wrapper 들이 PYTHONPATH 세팅을 자동으로 해 줍니다.
117
+
118
+ - [`okstra_ctl.run`](scripts/okstra_ctl/run.py) — `prepare_task_bundle()` orchestrator + argparse CLI (`python3 -m okstra_ctl.run --workspace-root ... --project-root ... ...`, **PYTHONPATH 세팅 필요 — 위 호출 규약 참조**).
109
119
  - [`okstra_ctl.paths`](scripts/okstra_ctl/paths.py) — `compute_run_paths()` pure path/seq 계산.
110
120
  - [`okstra_ctl.run_context`](scripts/okstra_ctl/run_context.py) — `compute_and_write_run_context()`, `write_run_inputs()`, per-task mutex.
111
- - [`okstra_ctl.render`](scripts/okstra_ctl/render.py) — task-manifest / run-manifest / timeline / task-index / team-state / launch.template / reference-expectations / discovery 9개 render 함수 + `python3 -m okstra_ctl.render <subcommand>` dispatcher.
121
+ - [`okstra_ctl.render`](scripts/okstra_ctl/render.py) — task-manifest / run-manifest / timeline / task-index / team-state / launch.template / reference-expectations / discovery 9개 render 함수 + `python3 -m okstra_ctl.render <subcommand>` dispatcher (**PYTHONPATH 세팅 필요 — 위 호출 규약 참조**).
112
122
  - [`okstra_ctl.workers`](scripts/okstra_ctl/workers.py) · [`okstra_ctl.models`](scripts/okstra_ctl/models.py) — worker / model 해소.
113
123
  - [`okstra_ctl.workflow`](scripts/okstra_ctl/workflow.py) — phase rules (PHASE_ALLOWED_OUTPUTS / PHASE_FORBIDDEN_ACTIONS).
114
124
  - [`okstra_ctl.material`](scripts/okstra_ctl/material.py) — `analysis-material.md` 본문 + related-tasks 빌더.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.18.3",
3
+ "version": "0.19.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.18.3",
3
- "builtAt": "2026-05-13T13:59:58.844Z",
2
+ "package": "0.19.1",
3
+ "builtAt": "2026-05-13T15:21:25.918Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -77,16 +77,16 @@ The wrapper exists because Claude Code's Bash permission matcher rejects simple-
77
77
  ```
78
78
  Call `Bash` with `run_in_background: true`. Capture the returned `bash_id` (a.k.a. `shell_id`). Pass the positional arguments verbatim — do NOT use environment variables, `cd`, `&&` chains, or pipes from `cat`. Substitute the literal extracted Project Root, model execution value, prompt-history path, and worktree path. The fourth argument is **mandatory for implementation phase** (extract from `EXECUTOR_WORKTREE_PATH` in the lead prompt's run context or the `**Worktree:**` / `cwd for every mutating command:` line) and **may be omitted only for non-implementation analysis phases** that do not mutate the worktree. Omitting it during implementation will cause every Edit/Write to fail with EPERM. The wrapper handles `-C`, `--add-dir`, `--model`, `--sandbox workspace-write`, the stdin redirect from the prompt file, and stderr suppression internally. Calling `codex exec` directly (without the wrapper) is an error in this skill: the redirect tokens disqualify the prefix match against `Bash(codex exec:*)` and produce a permission prompt every dispatch.
79
79
 
80
- **Poll loop (60-second cadence, 30-minute hard cap):**
81
- - Record `start_ts` at dispatch time.
80
+ **Poll loop (BashOutput-only, 30-minute hard cap):**
81
+ - Record `start_ts` at dispatch time via a single `Bash` call: `date +%s` (output captured).
82
82
  - Repeat:
83
- 1. Foreground `Bash` call: `sleep 60` (or shorter on the first iteration if you expect a fast finish).
84
- 2. Call `BashOutput(bash_id: <shell_id>)`. Inspect `status`.
85
- 3. If `status == "completed"`: break out of the loop and proceed to step 8.
86
- 4. If `now - start_ts > 1800` seconds: call `KillShell(shell_id: <shell_id>)`, then record a `cli-failure` event with `--error-type cli-failure`, `--exit-code 124`, `--duration-ms 1800000`, `--message "okstra-codex-exec.sh exceeded 30m polling cap"`, and return `CODEX_CLI_TIMEOUT: codex exec exceeded 30-minute polling cap`.
87
- 5. Otherwise continue polling.
83
+ 1. Call `BashOutput(bash_id: <shell_id>)`. Inspect `status`. The harness's `BashOutput` primitive already waits internally for new output before returning; back-to-back calls are the canonical wait mechanism for a background shell.
84
+ 2. If `status == "completed"`: break out of the loop and proceed to step 8.
85
+ 3. If wall-clock elapsed (`current_ts - start_ts`) exceeds `1800` seconds: call `KillShell(shell_id: <shell_id>)`, then record a `cli-failure` event with `--error-type cli-failure`, `--exit-code 124`, `--duration-ms 1800000`, `--message "okstra-codex-exec.sh exceeded 30m polling cap"`, and return `CODEX_CLI_TIMEOUT: codex exec exceeded 30-minute polling cap`.
86
+ 4. Otherwise continue polling. Read `current_ts` cheaply via another `Bash` call (`date +%s`) at most once per poll iteration.
88
87
  - Do NOT abort the loop on transient `running` status. Only `completed` or the 30-minute cap end it.
89
88
  - Do NOT issue parallel `BashOutput` calls or speculate about progress between polls.
89
+ - **No standalone `sleep` between polls.** The harness blocks `sleep` calls of 5 seconds or longer as a circumvention vector and explicitly forbids chaining shorter sleeps to work around it. `BashOutput` itself is the wait primitive — calling it again immediately after a `running` status is correct. If you find yourself wanting to "slow down" the loop, that desire is a leftover from the retired 60-second-cadence rule and should be ignored.
90
90
 
91
91
  8. Concatenate the wrapper's accumulated stdout from `BashOutput` and return it as-is without modification. If the final `BashOutput` reports a non-zero `exit_code`, follow the **CLI failure** rule in §"Error reporting" before returning.
92
92
 
@@ -95,7 +95,7 @@ The wrapper exists because Claude Code's Bash permission matcher rejects simple-
95
95
  This wrapper is a thin Bash-execution shell over the Codex CLI (via `okstra-codex-exec.sh`). The CLI process itself is the analysis engine; this subagent's only job is to dispatch it and forward output. Therefore:
96
96
 
97
97
  - Return immediately after the polling loop exits with `completed` (or after recording any required `cli-failure` event for a non-zero exit / 30-minute cap / rate-limit).
98
- - The only tool calls permitted during the polling loop are `Bash` (for `sleep`), `BashOutput`, and — on the timeout path only — `KillShell`. Do NOT perform additional `Read`, `Grep`, `Glob` calls between polls; do NOT inspect intermediate wrapper output mid-run.
98
+ - The only tool calls permitted during the polling loop are `BashOutput`, a single `Bash` call per iteration for `date +%s` (timeout bookkeeping only — no `sleep`), and — on the timeout path only — `KillShell`. Do NOT perform additional `Read`, `Grep`, `Glob` calls between polls; do NOT inspect intermediate wrapper output mid-run.
99
99
  - Outside the polling loop, no `Read`, `Grep`, or `Glob` beyond what is strictly required by steps 1–7 (prompt persistence, Project Root extraction, model resolution).
100
100
  - Do NOT re-invoke `okstra-codex-exec.sh` to "double-check" or "rerun for safety" — convergence (Phase 5.5) handles cross-worker reconciliation. A single CLI dispatch per dispatched-prompt is the contract.
101
101
 
@@ -77,16 +77,16 @@ The wrapper exists because Claude Code's Bash permission matcher rejects simple-
77
77
  ```
78
78
  Call `Bash` with `run_in_background: true`. Capture the returned `bash_id` (a.k.a. `shell_id`). Pass the positional arguments verbatim — do NOT use environment variables, `cd`, `&&` chains, or pipes from `cat`. Substitute the literal extracted Project Root, model execution value, prompt-history path, and worktree path. The fourth argument is **mandatory for implementation phase** (extract from `EXECUTOR_WORKTREE_PATH` in the lead prompt's run context or the `**Worktree:**` / `cwd for every mutating command:` line) and **may be omitted only for non-implementation analysis phases** that do not mutate the worktree. The wrapper handles `-p -`, `-m`, `-o text`, `--include-directories`, the stdin redirect from the prompt file, and stderr suppression internally. Calling `gemini` directly (without the wrapper) is an error in this skill: the redirect tokens disqualify the prefix match against `Bash(gemini:*)` and produce a permission prompt every dispatch.
79
79
 
80
- **Poll loop (60-second cadence, 30-minute hard cap):**
81
- - Record `start_ts` at dispatch time.
80
+ **Poll loop (BashOutput-only, 30-minute hard cap):**
81
+ - Record `start_ts` at dispatch time via a single `Bash` call: `date +%s` (output captured).
82
82
  - Repeat:
83
- 1. Foreground `Bash` call: `sleep 60` (or shorter on the first iteration if you expect a fast finish).
84
- 2. Call `BashOutput(bash_id: <shell_id>)`. Inspect `status`.
85
- 3. If `status == "completed"`: break out of the loop and proceed to step 8.
86
- 4. If `now - start_ts > 1800` seconds: call `KillShell(shell_id: <shell_id>)`, then record a `cli-failure` event with `--error-type cli-failure`, `--exit-code 124`, `--duration-ms 1800000`, `--message "okstra-gemini-exec.sh exceeded 30m polling cap"`, and return `GEMINI_CLI_TIMEOUT: gemini exec exceeded 30-minute polling cap`.
87
- 5. Otherwise continue polling.
83
+ 1. Call `BashOutput(bash_id: <shell_id>)`. Inspect `status`. The harness's `BashOutput` primitive already waits internally for new output before returning; back-to-back calls are the canonical wait mechanism for a background shell.
84
+ 2. If `status == "completed"`: break out of the loop and proceed to step 8.
85
+ 3. If wall-clock elapsed (`current_ts - start_ts`) exceeds `1800` seconds: call `KillShell(shell_id: <shell_id>)`, then record a `cli-failure` event with `--error-type cli-failure`, `--exit-code 124`, `--duration-ms 1800000`, `--message "okstra-gemini-exec.sh exceeded 30m polling cap"`, and return `GEMINI_CLI_TIMEOUT: gemini exec exceeded 30-minute polling cap`.
86
+ 4. Otherwise continue polling. Read `current_ts` cheaply via another `Bash` call (`date +%s`) at most once per poll iteration.
88
87
  - Do NOT abort the loop on transient `running` status. Only `completed` or the 30-minute cap end it.
89
88
  - Do NOT issue parallel `BashOutput` calls or speculate about progress between polls.
89
+ - **No standalone `sleep` between polls.** The harness blocks `sleep` calls of 5 seconds or longer as a circumvention vector and explicitly forbids chaining shorter sleeps to work around it. `BashOutput` itself is the wait primitive — calling it again immediately after a `running` status is correct. If you find yourself wanting to "slow down" the loop, that desire is a leftover from the retired 60-second-cadence rule and should be ignored.
90
90
 
91
91
  8. Concatenate the wrapper's accumulated stdout from `BashOutput` and return it as-is without modification. If the final `BashOutput` reports a non-zero `exit_code`, follow the **CLI failure** rule in §"Error reporting" before returning.
92
92
 
@@ -95,7 +95,7 @@ The wrapper exists because Claude Code's Bash permission matcher rejects simple-
95
95
  This wrapper is a thin Bash-execution shell over the Gemini CLI (via `okstra-gemini-exec.sh`). The CLI process itself is the analysis engine; this subagent's only job is to dispatch it and forward output. Therefore:
96
96
 
97
97
  - Return immediately after the polling loop exits with `completed` (or after recording any required `cli-failure` event for a non-zero exit / 30-minute cap / rate-limit).
98
- - The only tool calls permitted during the polling loop are `Bash` (for `sleep`), `BashOutput`, and — on the timeout path only — `KillShell`. Do NOT perform additional `Read`, `Grep`, `Glob` calls between polls; do NOT inspect intermediate wrapper output mid-run.
98
+ - The only tool calls permitted during the polling loop are `BashOutput`, a single `Bash` call per iteration for `date +%s` (timeout bookkeeping only — no `sleep`), and — on the timeout path only — `KillShell`. Do NOT perform additional `Read`, `Grep`, `Glob` calls between polls; do NOT inspect intermediate wrapper output mid-run.
99
99
  - Outside the polling loop, no `Read`, `Grep`, or `Glob` beyond what is strictly required by steps 1–7 (prompt persistence, Project Root extraction, model resolution).
100
100
  - Do NOT re-invoke `okstra-gemini-exec.sh` to "double-check" or "rerun for safety" — convergence (Phase 5.5) handles cross-worker reconciliation. A single CLI dispatch per dispatched-prompt is the contract.
101
101
 
@@ -13,7 +13,7 @@
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]
16
+ # okstra-codex-exec.sh <project-root> <model-execution-value> <prompt-path> [worktree-path] [role]
17
17
  #
18
18
  # project-root / model-execution-value / prompt-path are required.
19
19
  #
@@ -24,11 +24,28 @@
24
24
  # directory alongside the primary workspace anchored at project-root. When
25
25
  # omitted or empty, no `--add-dir` is added (existing analysis-phase behavior).
26
26
  #
27
+ # role is optional and used only to label the auto-spawned tmux trace pane
28
+ # (see "trace pane" section below). When omitted, it defaults to `executor`
29
+ # if worktree-path is non-empty (the implementation-phase invariant) and
30
+ # `worker` otherwise.
31
+ #
32
+ # For linked worktrees (the okstra implementation default), the per-worktree
33
+ # git metadata (index, HEAD, refs) and the shared object database live in the
34
+ # main repository's `.git` directory — OUTSIDE the worktree-path. Without
35
+ # write access there, `git add` / `git commit` from inside the worktree fails
36
+ # with EPERM on `.git/worktrees/<name>/index.lock`, which is the documented
37
+ # failure pattern for linked-worktree commits under `workspace-write`. The
38
+ # wrapper resolves the main repo's git-common-dir via `git rev-parse` against
39
+ # the supplied worktree-path and forwards it as an additional `--add-dir`. If
40
+ # resolution fails (not a linked worktree, or git unavailable), the extra
41
+ # add-dir is silently omitted — the caller still gets the worktree add-dir
42
+ # and any commit failure surfaces as a normal sandbox EPERM.
43
+ #
27
44
  # The wrapper exits non-zero on any preflight failure.
28
45
  set -euo pipefail
29
46
 
30
- if [[ $# -lt 3 || $# -gt 4 ]]; then
31
- printf 'usage: %s <project-root> <model-execution-value> <prompt-path> [worktree-path]\n' "$(basename "$0")" >&2
47
+ if [[ $# -lt 3 || $# -gt 5 ]]; then
48
+ printf 'usage: %s <project-root> <model-execution-value> <prompt-path> [worktree-path] [role]\n' "$(basename "$0")" >&2
32
49
  exit 64
33
50
  fi
34
51
 
@@ -36,6 +53,10 @@ project_root="$1"
36
53
  model="$2"
37
54
  prompt_path="$3"
38
55
  worktree_path="${4-}"
56
+ role="${5-}"
57
+ if [[ -z "$role" ]]; then
58
+ if [[ -n "$worktree_path" ]]; then role="executor"; else role="worker"; fi
59
+ fi
39
60
 
40
61
  if [[ -z "$project_root" || ! -d "$project_root" ]]; then
41
62
  printf 'okstra-codex-exec: project-root is missing or not a directory: %q\n' "$project_root" >&2
@@ -65,8 +86,76 @@ fi
65
86
  extra_args=()
66
87
  if [[ -n "$worktree_path" ]]; then
67
88
  extra_args+=(--add-dir "$worktree_path")
89
+ # For linked worktrees, also open the main repo's `.git` so `git add` /
90
+ # `git commit` can write the per-worktree index/refs (under
91
+ # `.git/worktrees/<name>/`) and the shared object DB (`.git/objects/`).
92
+ # `--git-common-dir` resolves to the main repo's `.git` for any worktree
93
+ # (linked or main); for a main checkout it equals `<worktree>/.git` and is
94
+ # redundant-but-harmless. Failures (not-a-git-repo, git missing) are
95
+ # tolerated silently so analysis-phase callers stay unaffected.
96
+ if command -v git >/dev/null 2>&1; then
97
+ common_git_dir=$(git -C "$worktree_path" rev-parse --git-common-dir 2>/dev/null || true)
98
+ if [[ -n "$common_git_dir" ]]; then
99
+ # `rev-parse --git-common-dir` may return a path relative to the
100
+ # worktree; normalise to an absolute directory before forwarding.
101
+ if [[ "$common_git_dir" != /* ]]; then
102
+ common_git_dir="$worktree_path/$common_git_dir"
103
+ fi
104
+ if [[ -d "$common_git_dir" ]]; then
105
+ # Resolve `..` / symlinks so codex sees a canonical path.
106
+ common_git_dir=$(cd "$common_git_dir" && pwd -P)
107
+ extra_args+=(--add-dir "$common_git_dir")
108
+ fi
109
+ fi
110
+ fi
68
111
  fi
69
112
 
70
- # stdin redirect and stderr suppression are intentionally inside the wrapper
71
- # this is the entire reason this script exists.
72
- exec codex exec -C "$project_root" ${extra_args[@]+"${extra_args[@]}"} --model "$model" --sandbox workspace-write - < "$prompt_path" 2>/dev/null
113
+ # Derive a live-progress log path next to the prompt. The codex CLI streams
114
+ # its progress over stdout/stderr, but the caller (codex-worker subagent)
115
+ # only polls `BashOutput` on a 60s cadence so without a sideband, a 10–30
116
+ # minute implementation run produces no visible output until the very end.
117
+ # Mirroring both streams into a file alongside the prompt lets the human
118
+ # operator `tail -f <log-path>` from a separate pane and watch progress in
119
+ # real time, and leaves a post-mortem record on disk regardless of how the
120
+ # subagent renders the dispatch.
121
+ log_path="${prompt_path%.md}.log"
122
+ [[ "$log_path" == "$prompt_path" ]] && log_path="${prompt_path}.log"
123
+ : > "$log_path"
124
+
125
+ # When a tmux session is reachable, split a sibling pane that tails the live
126
+ # log so the operator can watch codex's progress in real time without waiting
127
+ # for the wrapper to exit. This fires in every phase the wrapper is invoked
128
+ # from (analysis, error-analysis, implementation-planning, implementation,
129
+ # …) — long-running codex dispatches are not implementation-specific. The
130
+ # new pane carries the title `codex-<role>-trace` (e.g. `codex-worker-trace`
131
+ # in analysis, `codex-executor-trace` in implementation) and uses `tail -F`
132
+ # (follow-by-name) so it survives any truncation a re-dispatch performs on
133
+ # the same log path. Failures are tolerated silently: missing $TMUX, a tmux
134
+ # that refuses to split (size constraints, locked client), or a stale socket
135
+ # all degrade to "log file is still on disk; the operator can tail it
136
+ # manually from any terminal." The wrapper does NOT switch focus to the new
137
+ # pane — control returns to the caller's pane via `tmux last-pane`.
138
+ if [[ -n "${TMUX:-}" ]]; then
139
+ trace_pane=$(tmux split-window -h -P -F '#{pane_id}' \
140
+ -c "$(dirname "$log_path")" \
141
+ "tail -F $(printf '%q' "$log_path")" 2>/dev/null || true)
142
+ if [[ -n "$trace_pane" ]]; then
143
+ tmux select-pane -t "$trace_pane" -T "codex-${role}-trace" 2>/dev/null || true
144
+ tmux last-pane 2>/dev/null || true
145
+ fi
146
+ fi
147
+
148
+ # stdin redirect, stderr capture, and pipeline mirroring are intentionally
149
+ # inside the wrapper — this is the entire reason this script exists.
150
+ #
151
+ # stdout: tee'd to both the log file (for `tail -f`) AND the wrapper's own
152
+ # stdout (so the subagent's `BashOutput` still captures the final
153
+ # text verbatim for Phase 5 synthesis).
154
+ # stderr: appended to the log file only — mirrors the prior `2>/dev/null`
155
+ # contract of keeping the wrapper's stderr stream clean.
156
+ # exit: `PIPESTATUS[0]` preserves codex's own exit code (tee always 0).
157
+ {
158
+ codex exec -C "$project_root" ${extra_args[@]+"${extra_args[@]}"} --model "$model" --sandbox workspace-write - \
159
+ < "$prompt_path" 2>> "$log_path"
160
+ } | tee -a "$log_path"
161
+ exit "${PIPESTATUS[0]}"
@@ -13,7 +13,7 @@
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]
16
+ # okstra-gemini-exec.sh <project-root> <model-execution-value> <prompt-path> [worktree-path] [role]
17
17
  #
18
18
  # project-root / model-execution-value / prompt-path are required.
19
19
  #
@@ -24,11 +24,20 @@
24
24
  # operate on the worktree alongside the primary workspace. When omitted or
25
25
  # empty, only project-root is included (existing analysis-phase behavior).
26
26
  #
27
+ # For linked worktrees, the per-worktree git metadata (index, HEAD, refs) and
28
+ # the shared object database live in the main repository's `.git` directory —
29
+ # OUTSIDE the worktree-path. Without access there, `git add` / `git commit`
30
+ # from inside the worktree fails on `.git/worktrees/<name>/index.lock`. The
31
+ # wrapper resolves the main repo's git-common-dir via `git rev-parse` against
32
+ # the supplied worktree-path and appends it to `--include-directories`. If
33
+ # resolution fails (not a linked worktree, or git unavailable), the extra
34
+ # include is silently omitted.
35
+ #
27
36
  # The wrapper exits non-zero on any preflight failure.
28
37
  set -euo pipefail
29
38
 
30
- if [[ $# -lt 3 || $# -gt 4 ]]; then
31
- printf 'usage: %s <project-root> <model-execution-value> <prompt-path> [worktree-path]\n' "$(basename "$0")" >&2
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
32
41
  exit 64
33
42
  fi
34
43
 
@@ -36,6 +45,10 @@ project_root="$1"
36
45
  model="$2"
37
46
  prompt_path="$3"
38
47
  worktree_path="${4-}"
48
+ role="${5-}"
49
+ if [[ -z "$role" ]]; then
50
+ if [[ -n "$worktree_path" ]]; then role="executor"; else role="worker"; fi
51
+ fi
39
52
 
40
53
  if [[ -z "$project_root" || ! -d "$project_root" ]]; then
41
54
  printf 'okstra-gemini-exec: project-root is missing or not a directory: %q\n' "$project_root" >&2
@@ -65,10 +78,68 @@ fi
65
78
  include_dirs="$project_root"
66
79
  if [[ -n "$worktree_path" ]]; then
67
80
  include_dirs="$project_root,$worktree_path"
81
+ # For linked worktrees, also open the main repo's `.git` so `git add` /
82
+ # `git commit` can write the per-worktree index/refs and the shared
83
+ # object DB. `--git-common-dir` resolves to the main repo's `.git` for
84
+ # any worktree (linked or main); for a main checkout it equals
85
+ # `<worktree>/.git` and is redundant-but-harmless. Failures are tolerated
86
+ # silently so analysis-phase callers stay unaffected.
87
+ if command -v git >/dev/null 2>&1; then
88
+ common_git_dir=$(git -C "$worktree_path" rev-parse --git-common-dir 2>/dev/null || true)
89
+ if [[ -n "$common_git_dir" ]]; then
90
+ if [[ "$common_git_dir" != /* ]]; then
91
+ common_git_dir="$worktree_path/$common_git_dir"
92
+ fi
93
+ if [[ -d "$common_git_dir" ]]; then
94
+ common_git_dir=$(cd "$common_git_dir" && pwd -P)
95
+ include_dirs="$include_dirs,$common_git_dir"
96
+ fi
97
+ fi
98
+ fi
99
+ fi
100
+
101
+ # Derive a live-progress log path next to the prompt. The gemini CLI streams
102
+ # its progress over stdout/stderr, but the caller (gemini-worker subagent)
103
+ # only polls `BashOutput` on a 60s cadence — so without a sideband, a long
104
+ # implementation run produces no visible output until the very end. Mirroring
105
+ # both streams into a file alongside the prompt lets the human operator
106
+ # `tail -f <log-path>` from a separate pane and watch progress in real time,
107
+ # and leaves a post-mortem record on disk.
108
+ log_path="${prompt_path%.md}.log"
109
+ [[ "$log_path" == "$prompt_path" ]] && log_path="${prompt_path}.log"
110
+ : > "$log_path"
111
+
112
+ # When a tmux session is reachable, split a sibling pane tailing the log so
113
+ # the operator can watch progress live. This fires in every phase the
114
+ # wrapper is invoked from — long-running gemini dispatches are not
115
+ # implementation-specific. Title `gemini-<role>-trace` (e.g.
116
+ # `gemini-worker-trace` in analysis, `gemini-executor-trace` in
117
+ # implementation). See the codex wrapper for the full design rationale and
118
+ # the silent-degrade failure model.
119
+ if [[ -n "${TMUX:-}" ]]; then
120
+ trace_pane=$(tmux split-window -h -P -F '#{pane_id}' \
121
+ -c "$(dirname "$log_path")" \
122
+ "tail -F $(printf '%q' "$log_path")" 2>/dev/null || true)
123
+ if [[ -n "$trace_pane" ]]; then
124
+ tmux select-pane -t "$trace_pane" -T "gemini-${role}-trace" 2>/dev/null || true
125
+ tmux last-pane 2>/dev/null || true
126
+ fi
68
127
  fi
69
128
 
70
- # stdin redirect and stderr suppression are intentionally inside the wrapper —
71
- # this is the entire reason this script exists. Gemini CLI has no `--cd` flag,
72
- # so workspace correctness is anchored via `--include-directories` plus the
73
- # Project Root referenced in the prompt body itself.
74
- exec gemini -p - -m "$model" -o text --include-directories "$include_dirs" < "$prompt_path" 2>/dev/null
129
+ # stdin redirect, stderr capture, and pipeline mirroring are intentionally
130
+ # inside the wrapper — this is the entire reason this script exists. Gemini
131
+ # CLI has no `--cd` flag, so workspace correctness is anchored via
132
+ # `--include-directories` plus the Project Root referenced in the prompt
133
+ # body itself.
134
+ #
135
+ # stdout: tee'd to both the log file (for `tail -f`) AND the wrapper's own
136
+ # stdout (so the subagent's `BashOutput` still captures the final
137
+ # text verbatim for Phase 5 synthesis).
138
+ # stderr: appended to the log file only — mirrors the prior `2>/dev/null`
139
+ # contract of keeping the wrapper's stderr stream clean.
140
+ # exit: `PIPESTATUS[0]` preserves gemini's own exit code (tee always 0).
141
+ {
142
+ gemini -p - -m "$model" -o text --include-directories "$include_dirs" \
143
+ < "$prompt_path" 2>> "$log_path"
144
+ } | tee -a "$log_path"
145
+ exit "${PIPESTATUS[0]}"
@@ -1156,7 +1156,12 @@ def render_template_file(template_path: str, output_path: str, ctx: dict) -> Non
1156
1156
 
1157
1157
  def main(argv: list[str]) -> int:
1158
1158
  if not argv:
1159
- print("usage: python3 -m okstra_ctl.render <subcommand> ...", file=sys.stderr)
1159
+ print(
1160
+ "usage: python3 -m okstra_ctl.render <subcommand> ...\n"
1161
+ " (requires PYTHONPATH=$(okstra paths --field python); "
1162
+ "normal callers go through scripts/okstra.sh instead)",
1163
+ file=sys.stderr,
1164
+ )
1160
1165
  return 2
1161
1166
  sub = argv[0]
1162
1167
  rest = argv[1:]
@@ -61,9 +61,18 @@ from .workers import normalize_workers, resolve_profile_workers
61
61
  from .workflow import compute_workflow_state
62
62
  from .worktree import provision_task_worktree
63
63
 
64
+ # Validator regex for the approval marker.
65
+ #
66
+ # Tolerates a single optional backtick on either side of the approval token,
67
+ # because the report template instructs the user to flip `[ ]` to `[x]` inside
68
+ # a markdown code span and the report-writer worker often emits a standalone
69
+ # marker line wrapped the same way (e.g. `- ` + backtick + `[x] Approved` +
70
+ # backtick). Backticks carry no semantic content here — stripping them at the
71
+ # parser level is simpler than threading a "please remove formatting" rule
72
+ # through every authoring surface.
64
73
  APPROVED_PLAN_PATTERN = re.compile(
65
- r"^[ \t]*(?:[-*+][ \t]+)?(APPROVED([ \t]|:|$)|\[x\][ \t]*Approved|"
66
- r"User[ \t]+Approval[ \t]*:[ \t]*(APPROVED|granted|yes))",
74
+ r"^[ \t]*(?:[-*+][ \t]+)?`?(APPROVED([ \t]|:|$|`)|\[x\][ \t]*Approved`?|"
75
+ r"User[ \t]+Approval[ \t]*:[ \t]*(APPROVED|granted|yes)`?)",
67
76
  re.IGNORECASE | re.MULTILINE,
68
77
  )
69
78
 
@@ -127,8 +136,14 @@ def _validate_approved_plan(path: str) -> None:
127
136
 
128
137
  # `- [ ] Approved` 라인을 정확히 한 번만 매치한다. 좌측 leading whitespace 와
129
138
  # 옵션 bullet 은 보존된 채 체크박스 안쪽 공백만 `x` 로 갱신된다.
139
+ #
140
+ # Group 1: leading whitespace + optional bullet + optional opening backtick.
141
+ # Group 2: optional closing backtick + trailing whitespace.
142
+ # Both groups are preserved verbatim in the replacement so a backtick-wrapped
143
+ # `- \`[ ] Approved\`` flips to `- \`[x] Approved\`` without losing the
144
+ # surrounding code span — the validator regex tolerates either form.
130
145
  APPROVAL_UNCHECKED_PATTERN = re.compile(
131
- r"^([ \t]*(?:[-*+][ \t]+)?)\[[ \t]\][ \t]*Approved[ \t]*$",
146
+ r"^([ \t]*(?:[-*+][ \t]+)?`?)\[[ \t]\][ \t]*Approved(`?[ \t]*)$",
132
147
  re.IGNORECASE | re.MULTILINE,
133
148
  )
134
149
 
@@ -162,7 +177,7 @@ def _apply_cli_approval(path: str) -> str:
162
177
 
163
178
  if APPROVAL_UNCHECKED_PATTERN.search(body):
164
179
  new_body, count = APPROVAL_UNCHECKED_PATTERN.subn(
165
- lambda m: f"{m.group(1)}[x] Approved", body, count=1,
180
+ lambda m: f"{m.group(1)}[x] Approved{m.group(2)}", body, count=1,
166
181
  )
167
182
  new_body = new_body.rstrip("\n") + "\n" + audit_line + "\n"
168
183
  p.write_text(new_body, encoding="utf-8")
@@ -0,0 +1,167 @@
1
+ ---
2
+ name: okstra-logs
3
+ description: Use when the user asks about okstra worker wrapper log files — listing, sizes, ages, disk usage, or wants to know what `*.log` sidecars exist for past dispatches and which ones are safe to clean up. Trigger words include "okstra logs", "로그 현황", "로그 파일", "log files", "log size", "log status", "로그 정리", "log cleanup".
4
+ ---
5
+
6
+ # OKSTRA Logs
7
+
8
+ Read-only inventory of codex/gemini wrapper log files written next to each
9
+ prompt history file (`<prompt>.log`). Reports sizes, ages, totals, and
10
+ suggests cleanup commands. **Does not delete** — the user runs whichever
11
+ `find … -delete` line they like.
12
+
13
+ ## When to Use
14
+
15
+ - The user wants to see how much disk space okstra wrapper logs consume.
16
+ - The user wants to know which tasks / phases / workers have lingering log
17
+ sidecars from past dispatches.
18
+ - The user is planning a cleanup and wants ready-to-run `find` commands
19
+ scoped by age, task-id, or task-group.
20
+
21
+ ## Background
22
+
23
+ Codex/gemini wrappers (`okstra-codex-exec.sh`, `okstra-gemini-exec.sh`)
24
+ write a sidecar log next to each prompt history file:
25
+
26
+ ```
27
+ .project-docs/okstra/tasks/<task-group>/<task-id>/runs/<phase>/prompts/
28
+ <worker>-prompt-<phase>-<seq>.md <-- prompt (git-tracked)
29
+ <worker>-prompt-<phase>-<seq>.log <-- live stdout+stderr mirror
30
+ ```
31
+
32
+ The log is truncated at each dispatch (`: > "$log_path"`) — only the latest
33
+ run for a given seq is preserved. Different seqs (`-001`, `-002`, …) keep
34
+ separate files. Long-running implementation dispatches can produce
35
+ multi-MB logs; analysis-phase dispatches are typically smaller.
36
+
37
+ ## Step 0: Verify okstra runtime + project setup
38
+
39
+ ```bash
40
+ if command -v okstra >/dev/null 2>&1; then
41
+ OKSTRA_CMD="okstra"
42
+ else
43
+ OKSTRA_CMD="npx -y okstra@latest"
44
+ fi
45
+ $OKSTRA_CMD ensure-installed >/dev/null 2>&1 || {
46
+ echo "FAIL: okstra not installed; tell the user to run: npx okstra@latest install" >&2
47
+ exit 1
48
+ }
49
+ OKSTRA_PROJECT_INFO="$($OKSTRA_CMD check-project --json)" || {
50
+ echo "FAIL: this project has no okstra setup. Tell the user to run /okstra-setup first." >&2
51
+ echo "$OKSTRA_PROJECT_INFO" >&2
52
+ exit 1
53
+ }
54
+ ```
55
+
56
+ Parse `projectRoot` from the JSON and use it as the search root for the
57
+ steps below.
58
+
59
+ ## Step 1: Inventory
60
+
61
+ Find all wrapper log files and collect metadata. Use a single `find` to
62
+ keep the I/O cost predictable, then format the results.
63
+
64
+ ```bash
65
+ PROJECT_ROOT=$(echo "$OKSTRA_PROJECT_INFO" | python3 -c 'import sys,json;print(json.load(sys.stdin)["projectRoot"])')
66
+ LOGS_ROOT="$PROJECT_ROOT/.project-docs/okstra/tasks"
67
+
68
+ # columns: size_bytes | mtime_epoch | path
69
+ find "$LOGS_ROOT" -type f -path '*/runs/*/prompts/*.log' \
70
+ -printf '%s\t%T@\t%p\n' 2>/dev/null \
71
+ | sort -k2,2nr
72
+ ```
73
+
74
+ On macOS, `find -printf` is unavailable. Fall back to `stat`:
75
+
76
+ ```bash
77
+ find "$LOGS_ROOT" -type f -path '*/runs/*/prompts/*.log' 2>/dev/null \
78
+ | while IFS= read -r p; do
79
+ stat -f '%z%t%m%t%N' "$p"
80
+ done \
81
+ | sort -k2,2nr
82
+ ```
83
+
84
+ If the result is empty, report `No wrapper log files found under <PROJECT_ROOT>` and exit.
85
+
86
+ ## Step 2: Summary table
87
+
88
+ Group results and emit two tables.
89
+
90
+ ### Table A — Top 20 largest logs
91
+
92
+ | # | Task | Phase | Worker | Seq | Size | Age | Path |
93
+ |---|------|-------|--------|-----|------|-----|------|
94
+
95
+ Parse fields from the path:
96
+ - task-group / task-id: from the `tasks/<task-group>/<task-id>/` segment
97
+ - phase: from `runs/<phase>/`
98
+ - worker: from filename prefix before `-prompt-`
99
+ - seq: from filename suffix (last 3-digit segment)
100
+
101
+ Format sizes as human-readable (KB / MB). Format age as `Nd` (days) or
102
+ `Nh` (hours) from `mtime` relative to "now".
103
+
104
+ ### Table B — Per-task totals
105
+
106
+ | Task Key | Files | Total Size | Oldest | Newest |
107
+ |----------|-------|-----------:|--------|--------|
108
+
109
+ Sort by total size descending. "Task Key" = `<project-id>:<task-group>:<task-id>` for consistency with other okstra skills.
110
+
111
+ ### Footer line
112
+
113
+ ```
114
+ Total: N files, X.X MB across M tasks under <PROJECT_ROOT>
115
+ ```
116
+
117
+ ## Step 3: Suggested cleanup commands
118
+
119
+ Emit a fenced bash block the user can copy-paste. Do NOT execute these.
120
+
121
+ ```markdown
122
+ ## Cleanup options (manual)
123
+
124
+ # 7일 이상 된 로그만 삭제
125
+ find <PROJECT_ROOT>/.project-docs/okstra/tasks \
126
+ -type f -path '*/runs/*/prompts/*.log' -mtime +7 -delete
127
+
128
+ # 30일 이상 된 로그만 삭제
129
+ find <PROJECT_ROOT>/.project-docs/okstra/tasks \
130
+ -type f -path '*/runs/*/prompts/*.log' -mtime +30 -delete
131
+
132
+ # 특정 task-group 의 로그 일괄 삭제 (예: dev-9388)
133
+ find <PROJECT_ROOT>/.project-docs/okstra/tasks/dev-9388 \
134
+ -type f -name '*.log' -delete
135
+
136
+ # 특정 task-id 의 로그 일괄 삭제 (예: dev-9428)
137
+ find <PROJECT_ROOT>/.project-docs/okstra/tasks/*/dev-9428 \
138
+ -type f -name '*.log' -delete
139
+
140
+ # 전체 일괄 삭제 (주의)
141
+ find <PROJECT_ROOT>/.project-docs/okstra/tasks \
142
+ -type f -path '*/runs/*/prompts/*.log' -delete
143
+ ```
144
+
145
+ Substitute the literal `<PROJECT_ROOT>` with the resolved absolute path so
146
+ the commands are directly copy-pasteable.
147
+
148
+ ## Step 4: Notes for the user
149
+
150
+ End the response with these short reminders:
151
+
152
+ - Logs are truncated on each re-dispatch of the same `seq`, so deleting an
153
+ in-flight run's log will cause the wrapper to recreate an empty file on
154
+ the next dispatch — no data loss beyond the current trace.
155
+ - Prompt history files (`.md`) are separate and are NOT touched by these
156
+ commands — only `.log` sidecars.
157
+ - This skill does not modify `.gitignore`. If the project commits
158
+ `.project-docs/okstra/`, the user may want to add
159
+ `.project-docs/okstra/tasks/**/runs/**/prompts/*.log` to `.gitignore`
160
+ manually to keep large logs out of git.
161
+
162
+ ## What this skill is NOT
163
+
164
+ - Does NOT delete log files. Only inventories and suggests commands.
165
+ - Does NOT touch prompt history files (`.md`), worker results, manifests,
166
+ or any other okstra state.
167
+ - Does NOT run on a schedule. Invoke explicitly when needed.
@@ -295,7 +295,7 @@ empty run-level error logs in production.
295
295
 
296
296
  - `cli-failure` events are recorded by the wrapper subagent itself (Codex / Gemini), but **directly to the run-level error log** via `okstra-error-log.py append-observed --error-type cli-failure ...` — NOT via the sidecar. The sidecar is an in-process tool-failure channel only.
297
297
  - **Wrapper invocation arity.** Both `okstra-codex-exec.sh` and `okstra-gemini-exec.sh` accept four positional arguments: `<project-root> <model> <prompt-path> [<worktree-path>]`. The fourth (worktree) argument is **mandatory for implementation phase** and optional otherwise. For codex it becomes `--add-dir <worktree>` (sandbox write access); for gemini it is appended to `--include-directories`. Omitting it during implementation causes the codex sandbox to reject every Edit/Write targeting the worktree with EPERM. Workers extract the path from the `**Worktree:**` / `EXECUTOR_WORKTREE_PATH` / `cwd for every mutating command:` line in the lead prompt.
298
- - **Background dispatch + polling contract (Codex / Gemini wrappers).** Both wrapper subagents MUST dispatch `okstra-codex-exec.sh` / `okstra-gemini-exec.sh` via `Bash(run_in_background: true)` and poll with `BashOutput(bash_id)` on a 60-second cadence, capped at 30 minutes (1800s). The legacy "single foreground `Bash` with 120000ms timeout" rule is retired it forced workers into ad-hoc background dispatch that lost stdout and silently broke Phase 5 synthesis. The new rule applies in **every phase** (analysis runs typically complete in 1–2 polls, so there is no regression for short jobs). Recording responsibilities:
298
+ - **Background dispatch + polling contract (Codex / Gemini wrappers).** Both wrapper subagents MUST dispatch `okstra-codex-exec.sh` / `okstra-gemini-exec.sh` via `Bash(run_in_background: true)` and poll with `BashOutput(bash_id)` until the shell reports `status == "completed"`, capped at 30 minutes (1800s) of wall-clock elapsed time. `BashOutput` itself is the wait primitive — call it back-to-back; do NOT insert a standalone `sleep` between polls. The Claude Code harness blocks `sleep` calls of 5 seconds or longer as a circumvention vector and explicitly forbids chaining shorter sleeps inside until-loops to work around the block. Workers that hit the contract bug must NOT self-recover with `until ...; do sleep 2; done` wrappers — that path violates the harness anti-circumvention rule, even though it superficially "works". The legacy "single foreground `Bash` with 120000ms timeout" rule, and the subsequent "60-second cadence with `sleep 60` between polls" rule, are both retired. The current rule applies in **every phase** (analysis runs typically complete in 1–2 `BashOutput` calls, so there is no regression for short jobs). Recording responsibilities:
299
299
  - Successful completion: return the wrapper's accumulated stdout from the final `BashOutput`. No log entry.
300
300
  - Non-zero `exit_code` reported by `BashOutput`: record a `cli-failure` to the run-level error log with the real `exit_code` and observed `duration-ms`.
301
301
  - 30-minute polling cap exceeded: call `KillShell(shell_id)` first, then record `cli-failure` with `--exit-code 124 --duration-ms 1800000 --message "<wrapper> exceeded 30m polling cap"`, then return the language-specific `*_CLI_TIMEOUT` sentinel.