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.
- package/docs/kr/architecture.md +12 -2
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/bin/okstra-codex-exec.sh +95 -6
- package/runtime/bin/okstra-gemini-exec.sh +79 -8
- package/runtime/python/okstra_ctl/render.py +6 -1
- package/runtime/python/okstra_ctl/run.py +19 -4
- package/runtime/python/okstra_token_usage/collect.py +104 -28
- package/runtime/skills/okstra-logs/SKILL.md +167 -0
- package/runtime/skills/okstra-report-writer/SKILL.md +1 -1
- package/runtime/templates/reports/final-report.template.md +22 -8
package/docs/kr/architecture.md
CHANGED
|
@@ -105,10 +105,20 @@ okstra 의 prepare 책임은 단일 python 진입점 [`okstra_ctl.run.prepare_ta
|
|
|
105
105
|
|
|
106
106
|
### Python module 진입점 (single authority)
|
|
107
107
|
|
|
108
|
-
|
|
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
package/runtime/BUILD.json
CHANGED
|
@@ -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
|
|
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
|
-
#
|
|
71
|
-
#
|
|
72
|
-
|
|
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
|
|
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
|
|
71
|
-
# this is the entire reason this script exists. Gemini
|
|
72
|
-
# so workspace correctness is anchored via
|
|
73
|
-
# Project Root referenced in the prompt
|
|
74
|
-
|
|
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(
|
|
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]+)
|
|
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]+)
|
|
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[
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
#
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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 — 수동 편집):
|
|
17
|
-
-
|
|
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
|
-
이 보고서에 답을 채우신 뒤에는
|
|
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.
|
|
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:
|
|
305
|
-
|
|
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)
|
|
310
|
-
|
|
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
|
|