okstra 0.18.2 → 0.19.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.
@@ -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.2",
3
+ "version": "0.19.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.18.2",
3
- "builtAt": "2026-05-13T13:51:20.597Z",
2
+ "package": "0.19.0",
3
+ "builtAt": "2026-05-13T15:17:28.853Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -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")
@@ -2,6 +2,7 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import json
5
+ from datetime import datetime
5
6
  from pathlib import Path
6
7
  from .blocks import na_block, usage_block
7
8
  from .claude import claude_session_totals, find_claude_team_sessions
@@ -11,6 +12,76 @@ from .paths import claude_project_dir, utc_now
11
12
  from .pricing import codex_cost_usd, gemini_cost_usd
12
13
 
13
14
 
15
+ def match_prefixes(worker_id: str) -> list[str]:
16
+ """Return the agentName prefixes that should be attributed to ``worker_id``.
17
+
18
+ The Agent harness records the `name` arg on every dispatch as `agentName`
19
+ in the subagent jsonl. Lead frequently appends suffixes (`-002`,
20
+ `-reverify-r1`, `-impl`, `-2`) when it dispatches the same role multiple
21
+ times or in different sub-flows. We treat every `agentName` matching one of
22
+ these prefixes — either exactly or as `<prefix>-<suffix>` — as belonging
23
+ to this worker so its tokens get aggregated. For implementation runs the
24
+ executor variant `<provider>-executor` is also attributed back to the
25
+ matching provider worker.
26
+ """
27
+ if not worker_id:
28
+ return []
29
+ if worker_id == "report-writer":
30
+ return ["report-writer"]
31
+ prefixes = [worker_id]
32
+ if not worker_id.endswith("-worker"):
33
+ prefixes.append(f"{worker_id}-worker")
34
+ prefixes.append(f"{worker_id}-executor")
35
+ return prefixes
36
+
37
+
38
+ def agent_matches(agent_name: str, prefixes: list[str]) -> bool:
39
+ if not agent_name:
40
+ return False
41
+ for prefix in prefixes:
42
+ if agent_name == prefix or agent_name.startswith(f"{prefix}-"):
43
+ return True
44
+ return False
45
+
46
+
47
+ def _aggregate_totals(items: list[dict]) -> dict:
48
+ """Sum token + tool counters across multiple session totals dicts.
49
+
50
+ `startedAt` / `endedAt` collapse to the union window; `durationMs` is
51
+ recomputed from that window so re-tries and convergence rounds count
52
+ against a single contiguous span. `model` and `agentName` keep the first
53
+ non-empty value (the canonical role identity).
54
+ """
55
+ aggregate: dict = {
56
+ "totalTokens": 0, "inputTokens": 0, "outputTokens": 0,
57
+ "cacheCreationTokens": 0, "cacheReadTokens": 0,
58
+ "toolUses": 0, "durationMs": 0,
59
+ "agentName": None, "model": None,
60
+ "startedAt": None, "endedAt": None,
61
+ }
62
+ for t in items:
63
+ for k in ("totalTokens", "inputTokens", "outputTokens",
64
+ "cacheCreationTokens", "cacheReadTokens", "toolUses"):
65
+ aggregate[k] += t.get(k, 0) or 0
66
+ if aggregate["agentName"] is None and t.get("agentName"):
67
+ aggregate["agentName"] = t["agentName"]
68
+ if aggregate["model"] is None and t.get("model"):
69
+ aggregate["model"] = t["model"]
70
+ s, e = t.get("startedAt"), t.get("endedAt")
71
+ if s and (aggregate["startedAt"] is None or s < aggregate["startedAt"]):
72
+ aggregate["startedAt"] = s
73
+ if e and (aggregate["endedAt"] is None or e > aggregate["endedAt"]):
74
+ aggregate["endedAt"] = e
75
+ if aggregate["startedAt"] and aggregate["endedAt"]:
76
+ try:
77
+ a = datetime.fromisoformat(aggregate["startedAt"].replace("Z", "+00:00"))
78
+ b = datetime.fromisoformat(aggregate["endedAt"].replace("Z", "+00:00"))
79
+ aggregate["durationMs"] = max(0, int((b - a).total_seconds() * 1000))
80
+ except ValueError:
81
+ pass
82
+ return aggregate
83
+
84
+
14
85
  def collect(team_state_path: Path, project_root: Path | None = None) -> dict:
15
86
  state = json.loads(team_state_path.read_text())
16
87
  cwd = project_root or _infer_project_root(team_state_path, state)
@@ -26,19 +97,20 @@ def collect(team_state_path: Path, project_root: Path | None = None) -> dict:
26
97
  team_name = f"okstra-{task_id}" if task_id else ""
27
98
  lead_sid = (state.get("lead") or {}).get("sessionId")
28
99
 
29
- # 1) Claude sessions (lead + claude-side workers).
100
+ # 1) Claude sessions (lead + claude-side workers). Cache totals at scan
101
+ # time so we don't re-read the jsonl when a worker matches multiple
102
+ # sessions.
30
103
  claude_sessions = find_claude_team_sessions(cwd, team_name, lead_sid)
31
- by_agent: dict[str, tuple[str, Path]] = {}
104
+ by_agent: dict[str, list[tuple[str, Path, dict]]] = {}
32
105
  lead_path: Path | None = None
33
106
  for sid, path in claude_sessions.items():
34
107
  if sid == lead_sid:
35
108
  lead_path = path
36
109
  continue
37
- # Read agentName lazily.
38
110
  totals = claude_session_totals(path)
39
111
  agent = totals.get("agentName")
40
112
  if agent:
41
- by_agent[agent] = (sid, path)
113
+ by_agent.setdefault(agent, []).append((sid, path, totals))
42
114
 
43
115
  # Lead.
44
116
  if lead_path is not None:
@@ -50,35 +122,39 @@ def collect(team_state_path: Path, project_root: Path | None = None) -> dict:
50
122
  f"lead session jsonl not found under {claude_project_dir(cwd)} (sessionId={lead_sid})"
51
123
  )
52
124
 
53
- # Workers.
125
+ # Workers — match by prefix and aggregate every session that belongs to
126
+ # the same role (re-dispatches with `-002`, convergence `-reverify-r1`,
127
+ # implementation `-executor`, report-writer `-impl` / `-2`, etc.).
54
128
  for worker in state.get("workers", []):
55
129
  worker_id = worker.get("workerId")
56
130
  agent = worker.get("agent")
57
- # Subagent agentName convention: workerId itself is "claude" but agentName is "claude-worker"; report-writer == "report-writer".
58
- agent_name_candidates = []
59
- if worker_id:
60
- agent_name_candidates.append(worker_id)
61
- if not worker_id.endswith("-worker") and worker_id != "report-writer":
62
- agent_name_candidates.append(f"{worker_id}-worker")
63
-
64
- # Claude wrapper jsonl (also for codex/gemini-worker, since these are Claude subagents).
65
- wrapper = None
66
- for cand in agent_name_candidates:
67
- if cand in by_agent:
68
- wrapper = by_agent[cand]
69
- break
70
- if wrapper is None:
71
- worker["usage"] = na_block(f"no Claude subagent jsonl found with agentName matching {agent_name_candidates}")
131
+ prefixes = match_prefixes(worker_id) if worker_id else []
132
+
133
+ matched: list[tuple[str, Path, dict]] = []
134
+ for agent_name, entries in by_agent.items():
135
+ if agent_matches(agent_name, prefixes):
136
+ matched.extend(entries)
137
+
138
+ if not matched:
139
+ worker["usage"] = na_block(
140
+ f"no Claude subagent jsonl found with agentName matching prefixes {prefixes}"
141
+ )
72
142
  continue
73
- sid, path = wrapper
74
- totals = claude_session_totals(path)
75
- block = usage_block(totals, source="claude-jsonl")
76
- block["sessionId"] = sid
77
143
 
78
- # For codex/gemini workers, also try to find the underlying CLI session
79
- # within the wrapper subagent's time window.
80
- wrapper_start = totals.get("startedAt") or ""
81
- wrapper_end = totals.get("endedAt") or ""
144
+ # Stable order by startedAt so the "primary" session is the first one.
145
+ matched.sort(key=lambda x: x[2].get("startedAt") or "")
146
+ primary_sid, _primary_path, _primary_totals = matched[0]
147
+ aggregate = _aggregate_totals([t for _, _, t in matched])
148
+ block = usage_block(aggregate, source="claude-jsonl")
149
+ block["sessionId"] = primary_sid
150
+ if len(matched) > 1:
151
+ block["additionalSessionIds"] = [sid for sid, _, _ in matched[1:]]
152
+ block["matchedAgentNames"] = sorted({t.get("agentName") for _, _, t in matched if t.get("agentName")})
153
+
154
+ # For codex/gemini workers, find every CLI session that fell inside the
155
+ # aggregated wrapper window.
156
+ wrapper_start = aggregate.get("startedAt") or ""
157
+ wrapper_end = aggregate.get("endedAt") or ""
82
158
  if agent in ("codex", "gemini"):
83
159
  if agent == "codex":
84
160
  cli = find_codex_session(cwd, wrapper_start, wrapper_end)
@@ -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.
@@ -90,7 +90,7 @@ Behaviour contract:
90
90
  After the spawner completes, the report-writer worker MUST update Section 6 ("Recommended Next Steps") to list every newly created task-key together with its entry command, so the user can pick the follow-up up immediately:
91
91
 
92
92
  ```
93
- - Follow-up: `<task-group>/<new-task-id>` — `okstra --task-key <task-group>/<new-task-id> --task-type <suggested>`
93
+ - Follow-up: `<task-group>/<new-task-id>` — Claude Code 세션 안 `/okstra-run task-key=<task-group>/<new-task-id> task-type=<suggested>` / 별도 터미널 `scripts/okstra.sh --task-key <task-group>/<new-task-id> --task-type <suggested>`
94
94
  ```
95
95
 
96
96
  ## Phase 7 token-usage collector (BLOCKING)
@@ -13,8 +13,12 @@
13
13
  > 다음 `implementation` run은 아래 체크박스가 `[x]`로 표시되어 있을 때에만 진입할 수 있습니다 (`okstra_ctl.run._validate_approved_plan` 가 이 마커를 line-anchored 정규식으로 검사하여 통과/거부합니다). 본문(`Sections 1`–`4.5`)을 끝까지 읽고, `4.5.9 Open Questions`가 비어 있거나 모두 해소된 뒤 승인해 주세요.
14
14
 
15
15
  - 승인 여부 (사용자가 직접 편집): `- [ ] Approved` ← 승인하려면 `[ ]` 를 `[x]` 로 변경하여 저장하세요.
16
- - 승인 후 다음 단계 명령어 (방법 A — 수동 편집): `okstra --task-key {{TASK_KEY}} --task-type implementation --approved-plan <이 보고서 경로>`
17
- - 승인 + 실행 번에 (방법 B — CLI 자체를 승인 의사로): `okstra --task-key {{TASK_KEY}} --task-type implementation --approved-plan <이 보고서 경로> --approve`
16
+ - 승인 후 다음 단계 명령어 (방법 A — 수동 편집):
17
+ - Claude Code 세션 안: `/okstra-run task-key={{TASK_KEY}} task-type=implementation approved-plan=<이 보고서 경로>`
18
+ - 별도 터미널: `scripts/okstra.sh --task-key {{TASK_KEY}} --task-type implementation --approved-plan <이 보고서 경로>`
19
+ - 승인 + 실행 한 번에 (방법 B — 진입 명령 자체를 승인 의사로):
20
+ - Claude Code 세션 안: `/okstra-run task-key={{TASK_KEY}} task-type=implementation approved-plan=<이 보고서 경로> approve`
21
+ - 별도 터미널: `scripts/okstra.sh --task-key {{TASK_KEY}} --task-type implementation --approved-plan <이 보고서 경로> --approve`
18
22
  - 방법 B 는 `--approve` 입력 행위 자체를 승인 의사로 모델링합니다. 런타임이 본 블록의 체크박스를 자동으로 `[x]` 로 바꾸고, 본 섹션 하단에 `승인 일시 (CLI ack): <ISO8601>` audit 라인을 한 줄 덧붙입니다.
19
23
  - 승인을 보류하거나 거부하려면 체크박스는 `[ ]` 로 두고 `--approve` 도 사용하지 마세요. 필요한 변경 사항은 `4.5.9 Open Questions` 또는 `Section 5 Clarification Requests` 에 기록한 뒤 같은 phase 를 재실행해 주세요.
20
24
 
@@ -270,7 +274,11 @@ H1 이 `skip` 이거나 H3 가 `cancel` 인 경우, 본 섹션 다음의 4.6.4 ~
270
274
  - `resolved`: 다음 run에서 lead가 답변을 받아 검증을 마쳤습니다.
271
275
  - `obsolete`: 이후 분석 결과로 더 이상 필요 없어진 항목입니다.
272
276
 
273
- 이 보고서에 답을 채우신 뒤에는 `okstra --resume-clarification --task-key {{TASK_KEY}}` 한 줄로 같은 phase를 다시 실행하실 수 있습니다(자동으로 `$EDITOR`가 이 파일을 열고, 저장하면 같은 phase가 `--clarification-response`로 carry-in 되어 재실행됩니다). 스크립트로 자동화하실 때는 기존 형식 `okstra --task-key {{TASK_KEY}} --task-type {{TASK_TYPE}} --clarification-response <이 파일 경로>`도 그대로 사용하실 수 있습니다.
277
+ 이 보고서에 답을 채우신 뒤에는 한 줄로 같은 phase를 다시 실행하실 수 있습니다(자동으로 `$EDITOR`가 이 파일을 열고, 저장하면 같은 phase가 `--clarification-response`로 carry-in 되어 재실행됩니다).
278
+ - Claude Code 세션 안: `/okstra-run resume-clarification task-key={{TASK_KEY}}`
279
+ - 별도 터미널: `scripts/okstra.sh --resume-clarification --task-key {{TASK_KEY}}`
280
+
281
+ 스크립트로 자동화하실 때는 셸 형식 `scripts/okstra.sh --task-key {{TASK_KEY}} --task-type {{TASK_TYPE}} --clarification-response <이 파일 경로>`도 그대로 사용하실 수 있습니다. Node `okstra` admin CLI 는 `--task-key`/`--task-type`/`--resume-clarification` 을 받지 않으므로 위 두 진입점 중 하나를 사용하세요.
274
282
 
275
283
  ### 5.1 추가 자료 요청 (Additional Materials Requested)
276
284
 
@@ -298,16 +306,22 @@ H1 이 `skip` 이거나 H3 가 `cancel` 인 경우, 본 섹션 다음의 4.6.4 ~
298
306
 
299
307
  This section is **always present** in every final report — never omit the heading. If there are no concrete actions to take, write the single line `- No further action required. Final verdict in section 2 stands.` under the heading and stop.
300
308
 
301
- When concrete actions exist, list them as a numbered list using the rules below. Each item must include the exact command(s) the user can copy-paste. Prefer the `--task-key` shorthand for follow-up runs and `--resume-clarification` for clarification answer turn-arounds; show the equivalent full-args form only when useful.
309
+ When concrete actions exist, list them as a numbered list using the rules below. Each item must include the exact command(s) the user can copy-paste. Show **both** the Claude Code in-session form (`/okstra-run …`) and the external-terminal shell form (`scripts/okstra.sh …`) — the Node `okstra` admin CLI does NOT accept `--task-key` / `--task-type` / `--resume-clarification`. Prefer the `task-key` shorthand for follow-up runs and `resume-clarification` for clarification answer turn-arounds; show the equivalent full-args form only when useful.
302
310
 
303
311
  1. **Highest-priority next action.** State what to do and why in one sentence, then the command. Example shortcut forms:
304
- - Same phase rerun: `okstra --task-key {{TASK_KEY}} --task-type {{TASK_TYPE}}`
305
- - Next phase: `okstra --task-key {{TASK_KEY}} --task-type <next-phase>` (omit `--task-type` to use the manifest's `workflow.nextRecommendedPhase` automatically when it is a concrete phase, not `pending-routing-decision` / `done-or-follow-up`).
312
+ - Same phase rerun:
313
+ - Claude Code 세션 안: `/okstra-run task-key={{TASK_KEY}} task-type={{TASK_TYPE}}`
314
+ - 별도 터미널: `scripts/okstra.sh --task-key {{TASK_KEY}} --task-type {{TASK_TYPE}}`
315
+ - Next phase (omit `task-type` to use the manifest's `workflow.nextRecommendedPhase` automatically when it is a concrete phase, not `pending-routing-decision` / `done-or-follow-up`):
316
+ - Claude Code 세션 안: `/okstra-run task-key={{TASK_KEY}} task-type=<next-phase>`
317
+ - 별도 터미널: `scripts/okstra.sh --task-key {{TASK_KEY}} --task-type <next-phase>`
306
318
  2. **Additional verification needed before implementation or release.** List read-only checks (test commands, log queries, dashboard URLs) that the user should run before moving to the next phase. No state-mutating commands here.
307
319
  3. **Follow-up tasks or related tasks if needed.** Reference them by `task-key` when they already exist; otherwise describe the new brief to draft.
308
320
  4. **If section 5 has any `open` rows**, the highest-priority next step MUST be the clarification turn-around. Show both forms:
309
- - Preferred (interactive): `okstra --resume-clarification --task-key {{TASK_KEY}}` — opens this file in `$EDITOR`, then auto-reruns the same phase with `--clarification-response` carry-in.
310
- - Scripted: `okstra --task-key {{TASK_KEY}} --task-type {{TASK_TYPE}} --clarification-response <path-to-this-file-after-editing>`.
321
+ - Preferred (interactive) — opens this file in `$EDITOR`, then auto-reruns the same phase with `--clarification-response` carry-in:
322
+ - Claude Code 세션 안: `/okstra-run resume-clarification task-key={{TASK_KEY}}`
323
+ - 별도 터미널: `scripts/okstra.sh --resume-clarification --task-key {{TASK_KEY}}`
324
+ - Scripted: `scripts/okstra.sh --task-key {{TASK_KEY}} --task-type {{TASK_TYPE}} --clarification-response <path-to-this-file-after-editing>`.
311
325
 
312
326
  Empty-state placeholder, copy verbatim when nothing else applies:
313
327