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 +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/SKILL.md +39 -11
- package/runtime/agents/workers/claude-worker.md +1 -0
- package/runtime/bin/okstra-codex-exec.sh +79 -12
- package/runtime/bin/okstra-gemini-exec.sh +71 -12
- package/runtime/prompts/launch.template.md +4 -0
- package/runtime/python/okstra_ctl/render.py +41 -0
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
package/runtime/agents/SKILL.md
CHANGED
|
@@ -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
|
|
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-
|
|
136
|
-
3. `instruction-set/analysis-profile.md`
|
|
137
|
-
4. `
|
|
138
|
-
5.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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:
|
|
256
|
-
{
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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:
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
|