okstra 0.33.0 → 0.34.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.33.0",
3
+ "version": "0.34.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.33.0",
3
- "builtAt": "2026-05-19T16:04:42.528Z",
2
+ "package": "0.34.1",
3
+ "builtAt": "2026-05-19T16:34:08.806Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -82,6 +82,27 @@ User-utterance interpretation rule:
82
82
  - If the current phase's outputs are already complete and the user clearly wants to advance, reply with the phase-transition checklist above and the exact next-run command. Wait for explicit user confirmation before any action that belongs to the next phase.
83
83
  - If `nextRecommendedPhase` is `implementation-planning`, the next run produces a **plan**, not code. The next run after that is `implementation`.
84
84
 
85
+ ## Progress reporting (BLOCKING)
86
+
87
+ A single okstra run frequently spans 30–120 minutes of wall-clock time with multi-minute silent windows while workers run. Without explicit progress signals the user cannot distinguish "still working" from "hung", so Lead MUST emit a single short progress line at each of the checkpoints below — as plain user-facing text in a separate brief message (not buried inside a tool call). One line per checkpoint, format: `PROGRESS: <phase-id> <verb-phrase>`.
88
+
89
+ Required checkpoints:
90
+
91
+ - `PROGRESS: phase-1-intake reading task bundle` — at the start of Phase 1, before issuing parallel Read calls.
92
+ - `PROGRESS: phase-1-intake complete` — after all intake reads return.
93
+ - `PROGRESS: phase-2-prompts preparing <N> worker prompts` — at the start of Phase 2, before any `Write` to the assigned prompt paths.
94
+ - `PROGRESS: phase-3-team-create attempting TeamCreate` — immediately before the `TeamCreate` call.
95
+ - `PROGRESS: phase-4-dispatch worker=<role> model=<model>` — once per worker, immediately before the `Agent` / wrapper call.
96
+ - `PROGRESS: phase-5-collect worker=<role> status=<terminal-status>` — once per worker, immediately after the result file is verified.
97
+ - `PROGRESS: phase-5.5-convergence round=<N> queue=<count>` — at the start of each convergence round (Phase 5.5).
98
+ - `PROGRESS: phase-6-synthesis dispatching report-writer-worker` — at the start of Phase 6.
99
+ - `PROGRESS: phase-7-persist updating manifests` — at the start of Phase 7.
100
+ - `PROGRESS: complete final-report=<relative-path>` — final summary line, after all persistence.
101
+
102
+ These lines are the only structured signal the user has during a long run. Do NOT replace them with prose ("Now I'm starting Phase 2..."), do NOT skip a checkpoint because "the previous message already said that", and do NOT batch multiple checkpoints into one. Each line stands alone so the user (or any operator scraping stdout) can timestamp it externally.
103
+
104
+ `okstra-run` (in-session) surfaces these lines to the user directly; the bash-spawned path leaves them in the session jsonl for post-hoc retrieval. Neither path requires any additional formatting from Lead — emit the literal `PROGRESS:` prefix and the rest of the line as plain text.
105
+
85
106
  ## Default model assignments
86
107
 
87
108
  Unless the task bundle overrides:
@@ -129,21 +150,27 @@ Executor is chosen at run-prep time via `--executor <claude|codex|gemini>` (or `
129
150
 
130
151
  Treat cross verify input as a task bundle, not as a single file. If the user did not specify an explicit task key or task path, use `.project-docs/okstra/discovery/latest-task.json` as the current-task convenience pointer. If task browsing, task-id disambiguation, or project-level task inventory is needed, inspect `.project-docs/okstra/discovery/task-catalog.json` first.
131
152
 
132
- After context-loader completes, read the following files. The ordering below reflects logical priority for synthesis; for execution, lead MUST issue all Read calls **in a single message (parallel reads)** these files are independent and serial reads waste several seconds per run with no benefit.
153
+ After context-loader completes, read **only the five mandatory files below** in a single parallel-Read message at the start of Phase 1. The other instruction-set files are loaded lazily at the phase that actually needs them see "Lazy reading discipline" below. This split came from observed lead-token bloat: in `fontsninja-classifier-v2:dev-9461:dev-9495` RD-001 the lead burned 71 M tokens (97 % cache_read) largely because every phase entry re-absorbed a 93 KB instruction-set baseline that included files only one downstream phase ever actually used.
154
+
155
+ **Mandatory at Phase 1 start (parallel Read, one message):**
133
156
 
134
157
  1. `task-manifest.json` (found by context-loader)
135
- 2. `task-index.md` only if a quick human summary is useful
136
- 3. `instruction-set/analysis-profile.md`
137
- 4. `instruction-set/analysis-material.md`
138
- 5. `instruction-set/reference-expectations.md`
139
- 6. `instruction-set/task-brief.md`
140
- 7. `instruction-set/final-report-template.md`
141
- 8. the current run manifest under `runs/<task-type>/manifests/`
142
- 9. the current run team-state artifact
158
+ 2. `instruction-set/task-brief.md` needed to compose every worker prompt
159
+ 3. `instruction-set/analysis-profile.md` — needed to compose worker prompts and pick the right `Required workers:` block
160
+ 4. the current run manifest under `runs/<task-type>/manifests/`
161
+ 5. the current run team-state artifact
162
+
163
+ **Lazy reading discipline (do NOT read at Phase 1):**
164
+
165
+ - `task-index.md` — only when the user explicitly asks for a human summary or when history disambiguation is required.
166
+ - `instruction-set/analysis-material.md` — read at Phase 2 only if it is referenced by `analysis-profile.md` or by the brief. Many task bundles have no material file (the placeholder `> 자료가 제공되지 않았습니다` is canonical); in that case skip.
167
+ - `instruction-set/reference-expectations.md` — read at Phase 6 synthesis (or whenever the report-writer worker is dispatched) — it informs the match/gap assessment, not worker dispatch.
168
+ - `instruction-set/final-report-template.md` — never read by Lead. The Report writer worker reads it as part of its own [Required reading]; Lead only references its path when dispatching.
169
+ - `history/timeline.json` — read only on user request or when carry-in resolution requires it.
143
170
 
144
- Extract: task key, task type, work category, workflow lifecycle snapshot, selected worker roster, assigned models, worker result paths, worker prompt history paths, current run prompt directory, final report path, final status path, validator path, resume helper path, config-file references, deployment-manifest references, and their expected values or invariants.
171
+ Extract from the five mandatory files: task key, task type, work category, workflow lifecycle snapshot, selected worker roster, assigned models, worker result paths, worker prompt history paths, current run prompt directory, final report path, final status path, validator path, resume helper path, config-file references, deployment-manifest references, and their expected values or invariants.
145
172
 
146
- If previous run reports exist, use as historical context only. If `history/timeline.json` exists, use it to review past runs. If discovery metadata or current artifacts conflict with a newer user instruction, prefer the user instruction. If `reference-expectations.md` explicitly says expectations were not provided, treat that as missing information and say `I don't know` rather than inventing expected states.
173
+ If previous run reports exist, use as historical context only. If discovery metadata or current artifacts conflict with a newer user instruction, prefer the user instruction. If `reference-expectations.md` explicitly says expectations were not provided (you can confirm this without reading the file if the brief's "Expected state" section is empty), treat that as missing information and say `I don't know` rather than inventing expected states.
147
174
 
148
175
  ## Phase 2 — Phase 5: Prompt preparation, team creation, execution, fallback
149
176
 
@@ -316,6 +343,7 @@ After persistence, reply briefly in Korean with: completion status, final report
316
343
  | Letting `convergence.maxRounds` default to 2 for `requirements-discovery` | Resolve effective default to `1` for discovery and record in convergence state artifact |
317
344
  | Issuing serial Read calls in Phase 1 | The intake files are independent — issue all Read calls in a single message (parallel) |
318
345
  | Flagging the claude-worker dispatch prompt as "incomplete" because it lacks `[Required reading]` / `[Error reporting]` blocks | Intentional asymmetry — see [okstra-team-contract](./skills/okstra-team-contract/SKILL.md) "Asymmetry between claude-worker and codex/gemini-worker prompts" |
346
+ | Waiting silently while the dispatched `claude-worker` Agent call returns nothing for many minutes (the dev-9495 pattern: two 28+25-minute hangs before lead manually `tmux kill-pane`d) | The claude-worker MUST append a `- PROGRESS: <stage> <ISO-UTC>` line to its audit sidecar (`runs/<task-type>/worker-results/claude-worker-audit-<task-type>-<seq>.md`) at least every 5 minutes (see `agents/workers/claude-worker.md` "Heartbeat" rule). If the sidecar is absent or its mtime is >5 minutes stale, treat the dispatch as `timeout` and redispatch once with a byte-identical prompt; after a second silent hang, record terminal status `timeout` with the missing-sidecar reason in team-state. Lead cannot poll mid-Agent-call but MUST inspect the audit sidecar immediately when the Agent call finally returns — a missing sidecar after `completed` is itself a contract violation per the heartbeat rule |
319
347
  | Re-sending confirmed findings (`full-consensus`/`partial-consensus`/`worker-unique`) to a worker in Round 2 | Queue pruning rule — see [okstra-convergence](./skills/okstra-convergence/SKILL.md) "Round 1-N: Re-verification Loop (queue-pruned)" |
320
348
  | Aggregating a `timeout`/`error` reverify dispatch as `DISAGREE` | Worker failure handling — record as `verification-error` and add to `skippedWorkers[]`. See [okstra-convergence](./skills/okstra-convergence/SKILL.md) "Worker failure handling in reverify" |
321
349
  | Skipping `--substitute-data` in the Phase 7 collector run | Always pass the flag — see [okstra-report-writer](./skills/okstra-report-writer/SKILL.md) "Phase 7 token-usage collector" |
@@ -59,6 +59,7 @@ Before producing any output, you MUST read every input file enumerated in the `[
59
59
  - Use a single `Read` call per file with no `offset` and no `limit`. If a file is genuinely too large for one read, page through it with explicit `offset` / `limit` calls that together cover the entire file, and record the page boundaries in your Findings.
60
60
  - For the carry-in clarification response, walk every row of `## 5. Clarification Items` (`C-001`, `C-002`, ...) in full, including rows whose `User input` cell is blank — a blank `User input` with `Status=open` is itself a signal you must surface, not skip. Skimming these rows is the most common failure mode here; the fact that the file you will eventually contribute to has a structurally similar section 5 is NOT a license to skim.
61
61
  - Before listing any Findings, write a Reading Confirmation block to your **audit sidecar** at `runs/<task-type>/worker-results/claude-worker-audit-<task-type>-<seq>.md` (sibling to your main worker-results file — substitute `claude-worker-<task-type>-<seq>.md` → `claude-worker-audit-<task-type>-<seq>.md`). The sidecar's body begins with `# Claude Worker Audit — <task-key>` followed by one short line per input file confirming end-to-end reading (e.g. `- Read task-brief.md end-to-end (147 lines).`). Do NOT include a `## 0. Reading Confirmation` heading in the main worker-results file — the validator now fails worker-results that contain one. If you cannot truthfully confirm a file end-to-end, record a `tool-failure` in the errors sidecar instead of fabricating Findings.
62
+ - **Heartbeat — write the audit sidecar EARLY and APPEND per stage (BLOCKING).** Because this worker runs as an in-process Agent or a fresh-session tmux pane, the lead has no `BashOutput`-style liveness signal while waiting for your return. The audit sidecar is the only signal that survives a silent hang. Write the sidecar immediately after extracting `Project Root` and the assigned paths — BEFORE the per-file end-to-end reads — with just the heading line (`# Claude Worker Audit — <task-key>`) and one `- PROGRESS: started <ISO-8601-UTC>` line. Then APPEND one short progress line per stage as you advance: `read-<filename>`, `analysis-start`, `findings-draft-start`, `findings-draft-complete`, `write-result-start`. Each line: `- PROGRESS: <stage> <ISO-8601-UTC>`. The append cadence MUST NOT exceed 5 minutes — if a single analysis stage is taking longer, emit a `- PROGRESS: in-stage:<stage> <ISO-8601-UTC>` heartbeat. A 5-minute stale sidecar mtime is the canonical "this worker has hung" signal for the operator (the lead is blocked on the Agent call and cannot detect this itself, but a human watching via `tail -F <audit-sidecar>` from another terminal can). Sidecar write/append uses `Write` (for the initial creation) and `Edit` / heredoc `>>` for the per-stage append — heredoc append is the lighter option once the file exists.
62
63
  - Do not skip a file because its name suggests its content is already familiar from a prior run. Each file is canonical for the current run only.
63
64
 
64
65
  ## Worker Output Structure
@@ -13,10 +13,21 @@
13
13
  # Bash($HOME/.okstra/bin/okstra-codex-exec.sh:*)
14
14
  #
15
15
  # Usage:
16
- # okstra-codex-exec.sh <project-root> <model-execution-value> <prompt-path> [worktree-path] [role]
16
+ # okstra-codex-exec.sh <project-root> <model-execution-value> <prompt-path> [worktree-path] [role] [idle-timeout-seconds]
17
17
  #
18
18
  # project-root / model-execution-value / prompt-path are required.
19
19
  #
20
+ # idle-timeout-seconds is optional (default 600 = 10 minutes). When > 0, an
21
+ # in-process watchdog polls the live-log mtime; if no stdout/stderr write
22
+ # occurs for that many seconds, the underlying `codex exec` is SIGTERM'd
23
+ # (then SIGKILL'd after a 5-second grace), the status sidecar gets a
24
+ # `{timeout: true, idle_seconds, idle_at_ts, terminated_by: "idle-watchdog"}`
25
+ # marker, and the wrapper exits non-zero. Pass `0` to disable. Default
26
+ # exists because silent worker hangs are the dominant lead-time waste —
27
+ # observed 28+25 minutes on hung claude-worker dispatches before manual
28
+ # kill; a 10-minute cap costs ≤10m per hang while leaving long but live
29
+ # runs untouched.
30
+ #
20
31
  # worktree-path is optional and used for okstra implementation phase, where the
21
32
  # executor must mutate files inside a git worktree that lives outside
22
33
  # project-root. When supplied (non-empty), it is forwarded to codex as
@@ -56,8 +67,8 @@
56
67
  # The wrapper exits non-zero on any preflight failure.
57
68
  set -euo pipefail
58
69
 
59
- if [[ $# -lt 3 || $# -gt 5 ]]; then
60
- printf 'usage: %s <project-root> <model-execution-value> <prompt-path> [worktree-path] [role]\n' "$(basename "$0")" >&2
70
+ if [[ $# -lt 3 || $# -gt 6 ]]; then
71
+ printf 'usage: %s <project-root> <model-execution-value> <prompt-path> [worktree-path] [role] [idle-timeout-seconds]\n' "$(basename "$0")" >&2
61
72
  exit 64
62
73
  fi
63
74
 
@@ -66,6 +77,12 @@ model="$2"
66
77
  prompt_path="$3"
67
78
  worktree_path="${4-}"
68
79
  role="${5:-worker}"
80
+ idle_timeout_secs="${6:-600}"
81
+
82
+ if ! [[ "$idle_timeout_secs" =~ ^[0-9]+$ ]]; then
83
+ printf 'okstra-codex-exec: idle-timeout-seconds must be a non-negative integer: %q\n' "$idle_timeout_secs" >&2
84
+ exit 69
85
+ fi
69
86
 
70
87
  if [[ -z "$project_root" || ! -d "$project_root" ]]; then
71
88
  printf 'okstra-codex-exec: project-root is missing or not a directory: %q\n' "$project_root" >&2
@@ -247,14 +264,64 @@ fi
247
264
  # stdin redirect, stderr capture, and pipeline mirroring are intentionally
248
265
  # inside the wrapper — this is the entire reason this script exists.
249
266
  #
250
- # stdout: tee'd to both the log file (for `tail -f`) AND the wrapper's own
267
+ # stdout: tee'd to both the live log (for `tail -f`) AND the wrapper's own
251
268
  # stdout (so the subagent's `BashOutput` still captures the final
252
- # text verbatim for Phase 5 synthesis).
253
- # stderr: appended to the log file only mirrors the prior `2>/dev/null`
269
+ # text verbatim for Phase 5 synthesis). Implemented via process
270
+ # substitution so codex itself stays a single addressable PID we
271
+ # can SIGTERM from the watchdog.
272
+ # stderr: appended to the live log only — mirrors the prior `2>/dev/null`
254
273
  # contract of keeping the wrapper's stderr stream clean.
255
- # exit: `PIPESTATUS[0]` preserves codex's own exit code (tee always 0).
256
- {
257
- codex exec -C "$project_root" ${extra_args[@]+"${extra_args[@]}"} --model "$model" --sandbox workspace-write - \
258
- < "$prompt_path" 2>> "$log_path"
259
- } | tee -a "$log_path"
260
- exit "${PIPESTATUS[0]}"
274
+ # exit: codex's own exit code is preserved by `wait`.
275
+ codex exec -C "$project_root" ${extra_args[@]+"${extra_args[@]}"} --model "$model" --sandbox workspace-write - \
276
+ < "$prompt_path" \
277
+ 2>> "$log_path" \
278
+ > >(tee -a "$log_path") &
279
+ codex_pid=$!
280
+
281
+ # Idle watchdog: poll the live log's mtime; if no write (stdout or stderr)
282
+ # arrives for $idle_timeout_secs, SIGTERM codex, give it a 5-second grace,
283
+ # then SIGKILL. Record the termination cause in the status sidecar so the
284
+ # caller (lead) can distinguish "ran to completion with non-zero exit" from
285
+ # "killed because it went silent". Set 0 to disable entirely.
286
+ watchdog_pid=""
287
+ if (( idle_timeout_secs > 0 )); then
288
+ poll_interval=$(( idle_timeout_secs / 20 ))
289
+ (( poll_interval < 5 )) && poll_interval=5
290
+ (( poll_interval > 30 )) && poll_interval=30
291
+ (
292
+ while kill -0 "$codex_pid" 2>/dev/null; do
293
+ sleep "$poll_interval"
294
+ kill -0 "$codex_pid" 2>/dev/null || exit 0
295
+ last_mtime=$(stat -f %m "$log_path" 2>/dev/null || stat -c %Y "$log_path" 2>/dev/null || printf '0')
296
+ now=$(date +%s)
297
+ idle=$(( now - last_mtime ))
298
+ if (( idle >= idle_timeout_secs )); then
299
+ printf '\n[okstra wrapper] idle-watchdog: %ds without stdout — terminating codex (pid=%d)\n' \
300
+ "$idle" "$codex_pid" >> "$log_path" 2>&1 || true
301
+ python3 "$script_dir/okstra-wrapper-status.py" \
302
+ timeout "$status_path" "$now" "$idle" >>"$log_path" 2>&1 || true
303
+ kill -TERM "$codex_pid" 2>/dev/null || true
304
+ sleep 5
305
+ kill -KILL "$codex_pid" 2>/dev/null || true
306
+ exit 0
307
+ fi
308
+ done
309
+ ) &
310
+ watchdog_pid=$!
311
+ fi
312
+
313
+ set +e
314
+ wait "$codex_pid"
315
+ codex_exit=$?
316
+ set -e
317
+
318
+ if [[ -n "$watchdog_pid" ]]; then
319
+ kill "$watchdog_pid" 2>/dev/null || true
320
+ wait "$watchdog_pid" 2>/dev/null || true
321
+ fi
322
+
323
+ # Drain the process-substitution tee so the final lines reach the live log
324
+ # and the caller's stdout before exit.
325
+ wait 2>/dev/null || true
326
+
327
+ exit "$codex_exit"
@@ -13,10 +13,19 @@
13
13
  # Bash($HOME/.okstra/bin/okstra-gemini-exec.sh:*)
14
14
  #
15
15
  # Usage:
16
- # okstra-gemini-exec.sh <project-root> <model-execution-value> <prompt-path> [worktree-path] [role]
16
+ # okstra-gemini-exec.sh <project-root> <model-execution-value> <prompt-path> [worktree-path] [role] [idle-timeout-seconds]
17
17
  #
18
18
  # project-root / model-execution-value / prompt-path are required.
19
19
  #
20
+ # idle-timeout-seconds is optional (default 600 = 10 minutes). When > 0, an
21
+ # in-process watchdog polls the live-log mtime; if no stdout/stderr write
22
+ # occurs for that many seconds, the underlying `gemini` is SIGTERM'd (then
23
+ # SIGKILL'd after a 5-second grace), the status sidecar gets a
24
+ # `{timeout: true, idle_seconds, idle_at_ts, terminated_by: "idle-watchdog"}`
25
+ # marker, and the wrapper exits non-zero. Pass `0` to disable. Kept in
26
+ # lock-step with `okstra-codex-exec.sh` — see that wrapper for the full
27
+ # design rationale.
28
+ #
20
29
  # worktree-path is optional and used for okstra implementation phase, where the
21
30
  # executor must mutate files inside a git worktree that lives outside
22
31
  # project-root. When supplied (non-empty), it is appended to gemini's
@@ -36,8 +45,8 @@
36
45
  # The wrapper exits non-zero on any preflight failure.
37
46
  set -euo pipefail
38
47
 
39
- if [[ $# -lt 3 || $# -gt 5 ]]; then
40
- printf 'usage: %s <project-root> <model-execution-value> <prompt-path> [worktree-path] [role]\n' "$(basename "$0")" >&2
48
+ if [[ $# -lt 3 || $# -gt 6 ]]; then
49
+ printf 'usage: %s <project-root> <model-execution-value> <prompt-path> [worktree-path] [role] [idle-timeout-seconds]\n' "$(basename "$0")" >&2
41
50
  exit 64
42
51
  fi
43
52
 
@@ -46,6 +55,12 @@ model="$2"
46
55
  prompt_path="$3"
47
56
  worktree_path="${4-}"
48
57
  role="${5:-worker}"
58
+ idle_timeout_secs="${6:-600}"
59
+
60
+ if ! [[ "$idle_timeout_secs" =~ ^[0-9]+$ ]]; then
61
+ printf 'okstra-gemini-exec: idle-timeout-seconds must be a non-negative integer: %q\n' "$idle_timeout_secs" >&2
62
+ exit 69
63
+ fi
49
64
 
50
65
  if [[ -z "$project_root" || ! -d "$project_root" ]]; then
51
66
  printf 'okstra-gemini-exec: project-root is missing or not a directory: %q\n' "$project_root" >&2
@@ -189,14 +204,58 @@ fi
189
204
  # `--include-directories` plus the Project Root referenced in the prompt
190
205
  # body itself.
191
206
  #
192
- # stdout: tee'd to both the log file (for `tail -f`) AND the wrapper's own
207
+ # stdout: tee'd to both the live log (for `tail -f`) AND the wrapper's own
193
208
  # stdout (so the subagent's `BashOutput` still captures the final
194
- # text verbatim for Phase 5 synthesis).
195
- # stderr: appended to the log file only mirrors the prior `2>/dev/null`
209
+ # text verbatim for Phase 5 synthesis). Implemented via process
210
+ # substitution so gemini itself stays a single addressable PID we
211
+ # can SIGTERM from the watchdog.
212
+ # stderr: appended to the live log only — mirrors the prior `2>/dev/null`
196
213
  # contract of keeping the wrapper's stderr stream clean.
197
- # exit: `PIPESTATUS[0]` preserves gemini's own exit code (tee always 0).
198
- {
199
- gemini -p - -m "$model" -o text --include-directories "$include_dirs" \
200
- < "$prompt_path" 2>> "$log_path"
201
- } | tee -a "$log_path"
202
- exit "${PIPESTATUS[0]}"
214
+ # exit: gemini's own exit code is preserved by `wait`.
215
+ gemini -p - -m "$model" -o text --include-directories "$include_dirs" \
216
+ < "$prompt_path" \
217
+ 2>> "$log_path" \
218
+ > >(tee -a "$log_path") &
219
+ gemini_pid=$!
220
+
221
+ # Idle watchdog — see `okstra-codex-exec.sh` for the full rationale.
222
+ watchdog_pid=""
223
+ if (( idle_timeout_secs > 0 )); then
224
+ poll_interval=$(( idle_timeout_secs / 20 ))
225
+ (( poll_interval < 5 )) && poll_interval=5
226
+ (( poll_interval > 30 )) && poll_interval=30
227
+ (
228
+ while kill -0 "$gemini_pid" 2>/dev/null; do
229
+ sleep "$poll_interval"
230
+ kill -0 "$gemini_pid" 2>/dev/null || exit 0
231
+ last_mtime=$(stat -f %m "$log_path" 2>/dev/null || stat -c %Y "$log_path" 2>/dev/null || printf '0')
232
+ now=$(date +%s)
233
+ idle=$(( now - last_mtime ))
234
+ if (( idle >= idle_timeout_secs )); then
235
+ printf '\n[okstra wrapper] idle-watchdog: %ds without stdout — terminating gemini (pid=%d)\n' \
236
+ "$idle" "$gemini_pid" >> "$log_path" 2>&1 || true
237
+ python3 "$script_dir/okstra-wrapper-status.py" \
238
+ timeout "$status_path" "$now" "$idle" >>"$log_path" 2>&1 || true
239
+ kill -TERM "$gemini_pid" 2>/dev/null || true
240
+ sleep 5
241
+ kill -KILL "$gemini_pid" 2>/dev/null || true
242
+ exit 0
243
+ fi
244
+ done
245
+ ) &
246
+ watchdog_pid=$!
247
+ fi
248
+
249
+ set +e
250
+ wait "$gemini_pid"
251
+ gemini_exit=$?
252
+ set -e
253
+
254
+ if [[ -n "$watchdog_pid" ]]; then
255
+ kill "$watchdog_pid" 2>/dev/null || true
256
+ wait "$watchdog_pid" 2>/dev/null || true
257
+ fi
258
+
259
+ wait 2>/dev/null || true
260
+
261
+ exit "$gemini_exit"
@@ -3,6 +3,10 @@
3
3
  You are `Claude lead` for project `{{PROJECT_ID}}`.
4
4
  Invoke the `okstra` skill now. Read the manifests below for all task metadata, paths, model assignments, and worker roster.
5
5
 
6
+ ## Progress reporting (BLOCKING)
7
+
8
+ Emit one `PROGRESS: <phase-id> <verb-phrase>` line as plain user-facing text at every checkpoint enumerated in `agents/SKILL.md` "Progress reporting (BLOCKING)" — phase-1-intake start/complete, phase-2-prompts, phase-3-team-create, phase-4-dispatch (per worker), phase-5-collect (per worker), phase-5.5-convergence (per round), phase-6-synthesis, phase-7-persist, and final `complete`. One line per checkpoint, never batched, never replaced with prose. This is the only signal the user has during multi-minute silent windows.
9
+
6
10
  ## Current Phase Boundary
7
11
 
8
12
  - Current lifecycle phase: `{{WORKFLOW_CURRENT_PHASE}}`
@@ -18,6 +18,7 @@ session id 등) 를 덧붙여 전달한다.
18
18
  from __future__ import annotations
19
19
 
20
20
  import json
21
+ import re
21
22
  import sys
22
23
  from pathlib import Path
23
24
 
@@ -47,6 +48,44 @@ def _write_json(path: Path, payload: dict) -> None:
47
48
  _write_text(path, json.dumps(payload, indent=2, ensure_ascii=False) + "\n")
48
49
 
49
50
 
51
+ _PHASE_BLOCK_RE = re.compile(
52
+ r"\{% if header\.taskType == '(implementation-planning|release-handoff|implementation|final-verification)' %\}\n(.*?)\{% endif %\}\n",
53
+ re.DOTALL,
54
+ )
55
+
56
+
57
+ def _strip_phase_blocks(text: str, current_phase: str) -> str:
58
+ """Resolve phase-conditional blocks (`{% if header.taskType == 'X' %}
59
+ ... {% endif %}`) against *current_phase*.
60
+
61
+ Blocks whose target equals *current_phase* keep their body (jinja
62
+ markers dropped); blocks targeting a different phase are removed
63
+ entirely. When *current_phase* is empty or not one of the four
64
+ block-targetable phases (e.g. `requirements-discovery`,
65
+ `error-analysis`), every block is dropped — correct because none of
66
+ the `## 4.5` / `4.6` / `4.7` / `4.8` deliverable sections apply
67
+ there.
68
+
69
+ Observed (fontsninja-classifier-v2 RD run): the raw final-report
70
+ template copied into instruction-set/final-report-template.md was
71
+ 43 KB / 631 lines; ~30 KB / ~330 lines belonged to the four other
72
+ phases' deliverables and was never relevant to that run. Stripping
73
+ at copy time cuts the lead/report-writer's baseline by ~7 K tokens
74
+ per phase entry.
75
+
76
+ Inline conditionals (those that begin and end on the same line) are
77
+ intentionally untouched — the regex only matches block-form
78
+ `{% if ... %}\\n ... \\n{% endif %}\\n`.
79
+ """
80
+
81
+ def repl(m: "re.Match[str]") -> str:
82
+ target_phase = m.group(1)
83
+ body = m.group(2)
84
+ return body if target_phase == current_phase else ""
85
+
86
+ return _PHASE_BLOCK_RE.sub(repl, text)
87
+
88
+
50
89
  _FM_DEFAULT = "no-classification"
51
90
 
52
91
  _FM_TAGS_BASE = []
@@ -1252,6 +1291,7 @@ def render_task_index(template_path: str, output_path: str, ctx: dict) -> None:
1252
1291
  rendered = template
1253
1292
  for k, v in mapping.items():
1254
1293
  rendered = rendered.replace(k, v)
1294
+ rendered = _strip_phase_blocks(rendered, ctx.get("ANALYSIS_TYPE", ""))
1255
1295
  _write_text(Path(output_path), rendered.rstrip() + "\n")
1256
1296
 
1257
1297
 
@@ -1591,6 +1631,7 @@ def render_template_file(template_path: str, output_path: str, ctx: dict) -> Non
1591
1631
  rendered = template
1592
1632
  for k, v in mapping.items():
1593
1633
  rendered = rendered.replace(k, v)
1634
+ rendered = _strip_phase_blocks(rendered, ctx.get("ANALYSIS_TYPE", ""))
1594
1635
  _write_text(Path(output_path), rendered.rstrip() + "\n")
1595
1636
 
1596
1637