okstra 0.163.2 → 0.165.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.
Files changed (167) hide show
  1. package/README.md +8 -6
  2. package/docs/architecture.md +24 -15
  3. package/docs/cli.md +15 -8
  4. package/docs/for-ai/README.md +2 -2
  5. package/docs/for-ai/skills/okstra-inspect.md +2 -2
  6. package/docs/for-ai/skills/okstra-user-response.md +2 -2
  7. package/docs/project-structure-overview.md +22 -14
  8. package/package.json +1 -1
  9. package/runtime/BUILD.json +2 -2
  10. package/runtime/agents/workers/antigravity-worker.md +9 -7
  11. package/runtime/agents/workers/claude-worker.md +1 -0
  12. package/runtime/agents/workers/codex-worker.md +9 -7
  13. package/runtime/agents/workers/grok-worker.md +6 -4
  14. package/runtime/agents/workers/kimi-worker.md +6 -4
  15. package/runtime/bin/lib/okstra/cli.sh +5 -0
  16. package/runtime/bin/lib/okstra/globals.sh +2 -0
  17. package/runtime/bin/lib/okstra/usage.sh +5 -5
  18. package/runtime/bin/okstra-antigravity-exec.sh +1 -340
  19. package/runtime/bin/okstra-claude-exec.sh +1 -178
  20. package/runtime/bin/okstra-codex-exec.sh +1 -467
  21. package/runtime/bin/okstra-provider-exec.py +165 -190
  22. package/runtime/bin/okstra-trace-cleanup.sh +14 -7
  23. package/runtime/bin/okstra-wrapper-status.py +26 -19
  24. package/runtime/bin/okstra.sh +87 -91
  25. package/runtime/prompts/lead/adapters/cmux.md +2 -2
  26. package/runtime/prompts/lead/convergence.md +36 -8
  27. package/runtime/prompts/lead/okstra-lead-contract.md +24 -1
  28. package/runtime/prompts/lead/plan-body-verification.md +9 -1
  29. package/runtime/prompts/lead/report-writer.md +1 -0
  30. package/runtime/prompts/lead/team-contract.md +3 -3
  31. package/runtime/prompts/profiles/_common-contract.md +9 -1
  32. package/runtime/prompts/profiles/_coverage-critic.md +1 -1
  33. package/runtime/prompts/profiles/_implementation-diff-review.md +3 -1
  34. package/runtime/prompts/profiles/_implementation-self-check.md +1 -1
  35. package/runtime/prompts/profiles/_implementation-verifier.md +3 -1
  36. package/runtime/prompts/profiles/implementation-planning.md +6 -4
  37. package/runtime/python/okstra_ctl/adapters/accounting/__init__.py +11 -0
  38. package/runtime/python/okstra_ctl/adapters/accounting/claude_jsonl.py +17 -0
  39. package/runtime/python/okstra_ctl/adapters/accounting/cli_artifact.py +17 -0
  40. package/runtime/python/okstra_ctl/adapters/accounting/unavailable.py +19 -0
  41. package/runtime/python/okstra_ctl/adapters/dispatch/__init__.py +92 -0
  42. package/runtime/python/okstra_ctl/adapters/dispatch/cli_wrapper.py +54 -0
  43. package/runtime/python/okstra_ctl/adapters/dispatch/cmux.py +68 -0
  44. package/runtime/python/okstra_ctl/adapters/dispatch/native_team.py +13 -0
  45. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +60 -0
  46. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/manifest.json +1 -0
  47. package/runtime/{prompts/lead/adapters/antigravity.md → python/okstra_ctl/adapters/hosts/antigravity/relay.md} +52 -0
  48. package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +292 -0
  49. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +120 -0
  50. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/manifest.json +1 -0
  51. package/runtime/{prompts/lead/adapters/claude-code.md → python/okstra_ctl/adapters/hosts/claude-code/relay.md} +112 -1
  52. package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +60 -0
  53. package/runtime/python/okstra_ctl/adapters/hosts/codex/manifest.json +1 -0
  54. package/runtime/{prompts/lead/adapters/codex.md → python/okstra_ctl/adapters/hosts/codex/relay.md} +52 -0
  55. package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +72 -0
  56. package/runtime/python/okstra_ctl/adapters/hosts/external/manifest.json +1 -0
  57. package/runtime/{prompts/lead/adapters/external.md → python/okstra_ctl/adapters/hosts/external/relay.md} +53 -1
  58. package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +63 -0
  59. package/runtime/python/okstra_ctl/adapters/hosts/grok/manifest.json +1 -0
  60. package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +90 -0
  61. package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +63 -0
  62. package/runtime/python/okstra_ctl/adapters/hosts/kimi/manifest.json +1 -0
  63. package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +90 -0
  64. package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +183 -0
  65. package/runtime/python/okstra_ctl/adapters/providers/antigravity/manifest.json +1 -0
  66. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +110 -0
  67. package/runtime/python/okstra_ctl/adapters/providers/claude/manifest.json +1 -0
  68. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +84 -0
  69. package/runtime/python/okstra_ctl/adapters/providers/codex/manifest.json +1 -0
  70. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +76 -0
  71. package/runtime/python/okstra_ctl/adapters/providers/grok/manifest.json +1 -0
  72. package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +80 -0
  73. package/runtime/python/okstra_ctl/adapters/providers/kimi/manifest.json +1 -0
  74. package/runtime/python/okstra_ctl/application/__init__.py +1 -0
  75. package/runtime/python/okstra_ctl/application/advance_wizard.py +25 -0
  76. package/runtime/python/okstra_ctl/application/collect_usage.py +15 -0
  77. package/runtime/python/okstra_ctl/application/dispatch_assignments.py +15 -0
  78. package/runtime/python/okstra_ctl/application/resolve_assignment.py +93 -0
  79. package/runtime/python/okstra_ctl/application/resume_run.py +21 -0
  80. package/runtime/python/okstra_ctl/application/start_run.py +21 -0
  81. package/runtime/python/okstra_ctl/codex_dispatch.py +49 -826
  82. package/runtime/python/okstra_ctl/dispatch_core.py +244 -29
  83. package/runtime/python/okstra_ctl/dispatch_state.py +27 -0
  84. package/runtime/python/okstra_ctl/domain/__init__.py +34 -0
  85. package/runtime/python/okstra_ctl/domain/host.py +100 -0
  86. package/runtime/python/okstra_ctl/domain/provider.py +70 -0
  87. package/runtime/python/okstra_ctl/domain/wizard/__init__.py +19 -0
  88. package/runtime/python/okstra_ctl/domain/wizard/interaction.py +140 -0
  89. package/runtime/python/okstra_ctl/domain/worker_exec.py +102 -0
  90. package/runtime/python/okstra_ctl/domain/worker_role.py +34 -0
  91. package/runtime/python/okstra_ctl/domain/worker_stream.py +261 -0
  92. package/runtime/python/okstra_ctl/entrypoints/__init__.py +1 -0
  93. package/runtime/python/okstra_ctl/entrypoints/hosts.py +334 -0
  94. package/runtime/python/okstra_ctl/incremental_scope.py +16 -4
  95. package/runtime/python/okstra_ctl/models.py +54 -269
  96. package/runtime/python/okstra_ctl/ports/__init__.py +15 -0
  97. package/runtime/python/okstra_ctl/ports/host.py +32 -0
  98. package/runtime/python/okstra_ctl/ports/interaction.py +15 -0
  99. package/runtime/python/okstra_ctl/ports/lead_session.py +25 -0
  100. package/runtime/python/okstra_ctl/ports/usage_accounting.py +23 -0
  101. package/runtime/python/okstra_ctl/ports/worker_dispatch.py +32 -0
  102. package/runtime/python/okstra_ctl/registry/__init__.py +13 -0
  103. package/runtime/python/okstra_ctl/registry/factory_loader.py +32 -0
  104. package/runtime/python/okstra_ctl/registry/host_discovery.py +124 -0
  105. package/runtime/python/okstra_ctl/registry/host_registry.py +365 -0
  106. package/runtime/python/okstra_ctl/registry/provider_registry.py +149 -0
  107. package/runtime/python/okstra_ctl/render.py +145 -47
  108. package/runtime/python/okstra_ctl/report_html/common.py +71 -25
  109. package/runtime/python/okstra_ctl/report_html/models.py +5 -0
  110. package/runtime/python/okstra_ctl/report_html/render.py +1 -1
  111. package/runtime/python/okstra_ctl/report_html/run_usage.py +19 -0
  112. package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +14 -0
  113. package/runtime/python/okstra_ctl/report_views.py +44 -16
  114. package/runtime/python/okstra_ctl/run.py +80 -58
  115. package/runtime/python/okstra_ctl/session.py +1 -1
  116. package/runtime/python/okstra_ctl/stage_citations.py +52 -15
  117. package/runtime/python/okstra_ctl/team.py +44 -32
  118. package/runtime/python/okstra_ctl/user_response.py +45 -29
  119. package/runtime/python/okstra_ctl/wizard.py +175 -73
  120. package/runtime/python/okstra_ctl/worker_audit_ledger.py +29 -4
  121. package/runtime/python/okstra_ctl/worker_prompt_policy.py +10 -3
  122. package/runtime/python/okstra_ctl/worker_request.py +140 -0
  123. package/runtime/python/okstra_ctl/worker_runner.py +622 -0
  124. package/runtime/python/okstra_token_usage/collect.py +42 -7
  125. package/runtime/python/okstra_token_usage/report.py +42 -0
  126. package/runtime/python/okstra_token_usage/task_totals.py +88 -0
  127. package/runtime/schemas/final-report-v1.0.schema.json +4040 -1066
  128. package/runtime/schemas/final-report-v2.0.schema.json +5673 -1412
  129. package/runtime/skills/okstra-inspect/SKILL.md +1 -2
  130. package/runtime/skills/okstra-inspect/facets/logs.md +5 -5
  131. package/runtime/skills/okstra-inspect/facets/run-audit.md +3 -3
  132. package/runtime/skills/okstra-run/SKILL.md +74 -29
  133. package/runtime/skills/okstra-user-response/SKILL.md +15 -5
  134. package/runtime/templates/implementation-worker-preamble.md +1 -1
  135. package/runtime/templates/report-writer-prompt-preamble.md +1 -0
  136. package/runtime/templates/reports/final-report.template.md +3 -3
  137. package/runtime/templates/reports/html/assets/base.css +8 -4
  138. package/runtime/templates/reports/html/base.template.html +12 -6
  139. package/runtime/templates/reports/html/i18n/en.json +32 -7
  140. package/runtime/templates/reports/html/i18n/ko.json +32 -7
  141. package/runtime/templates/reports/html/macros/forms.html +9 -3
  142. package/runtime/templates/reports/html/tasks/implementation-planning.template.html +14 -19
  143. package/runtime/templates/reports/report.js +59 -26
  144. package/runtime/templates/reports/user-response.template.md +12 -8
  145. package/runtime/templates/worker-prompt-preamble.md +1 -1
  146. package/runtime/validators/validate-implementation-plan-stages.py +17 -22
  147. package/runtime/validators/validate-report-views.py +0 -39
  148. package/runtime/validators/validate-run.py +96 -14
  149. package/runtime/validators/validate_session_conformance.py +69 -3
  150. package/src/cli-registry.mjs +0 -7
  151. package/src/commands/execute/render-bundle.mjs +4 -4
  152. package/src/commands/execute/run.mjs +8 -25
  153. package/src/commands/execute/wizard.mjs +33 -13
  154. package/src/commands/lifecycle/doctor.mjs +10 -10
  155. package/src/commands/lifecycle/install.mjs +53 -30
  156. package/src/commands/lifecycle/preflight.mjs +14 -4
  157. package/src/lib/host-registry-client.mjs +176 -0
  158. package/src/lib/runtime-manifest.mjs +6 -8
  159. package/runtime/bin/okstra-wrapper-agy-stream.py +0 -61
  160. package/runtime/python/okstra_ctl/error_issue.py +0 -640
  161. package/runtime/python/okstra_ctl/issue_signals.py +0 -186
  162. package/runtime/python/okstra_ctl/lead_runtime.py +0 -115
  163. package/runtime/python/okstra_ctl/runner_resolution.py +0 -103
  164. package/runtime/skills/okstra-inspect/facets/error-issue.md +0 -77
  165. package/src/commands/inspect/error-issue.mjs +0 -27
  166. package/src/lib/runtime-readiness.mjs +0 -90
  167. package/src/lib/runtime-resolver.mjs +0 -123
@@ -1,471 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # okstra-codex-exec.sh — wrapper around `codex exec` for okstra codex-worker
3
- #
4
- # Purpose: Claude Code's Bash permission matcher requires explicit approval for
5
- # commands that contain shell metacharacters (stdin/stderr redirects, pipes).
6
- # `codex exec ... - < <prompt-path> 2>/dev/null` therefore triggers a permission
7
- # prompt every dispatch even when `Bash(codex exec:*)` is allowlisted, because
8
- # the redirect tokens disqualify the simple-prefix match.
9
- #
10
- # This wrapper accepts positional arguments and performs the redirect inside
11
- # the script body, so the caller can allowlist a single non-redirect form:
12
- #
13
- # Bash($HOME/.okstra/bin/okstra-codex-exec.sh:*)
14
- #
15
- # Usage:
16
- # okstra-codex-exec.sh <project-root> <model-execution-value> <prompt-path> [worktree-path] [role] [idle-timeout-seconds]
17
- #
18
- # project-root / model-execution-value / prompt-path are required.
19
- #
20
- # idle-timeout-seconds is optional (default 600s, or 1500s for the build-running
21
- # executor/verifier roles — see the role case below). When > 0, an
22
- # in-process watchdog polls the live-log mtime; if no stdout/stderr write
23
- # occurs for that many seconds, the underlying `codex exec` is SIGTERM'd
24
- # (then SIGKILL'd after a 5-second grace), the status sidecar gets a
25
- # `{timeout: true, idle_seconds, idle_at_ts, terminated_by: "idle-watchdog"}`
26
- # marker, and the wrapper exits non-zero. Pass `0` to disable. Default
27
- # exists because silent worker hangs are the dominant lead-time waste —
28
- # observed 28+25 minutes on hung claude-worker dispatches before manual
29
- # kill; a 10-minute cap costs ≤10m per hang while leaving long but live
30
- # runs untouched.
31
- #
32
- # worktree-path is optional and used for okstra implementation phase, where the
33
- # executor must mutate files inside a git worktree that lives outside
34
- # project-root. When supplied (non-empty), it is forwarded to codex as
35
- # `--add-dir <worktree-path>` so the codex sandbox grants write access to that
36
- # directory alongside the primary workspace anchored at project-root. When
37
- # omitted or empty, no `--add-dir` is added (existing analysis-phase behavior).
38
- #
39
- # role is optional and used only to label the auto-spawned tmux trace pane
40
- # (see "trace pane" section below). When omitted, it defaults to `worker`;
41
- # the dispatching agent passes it explicitly (always `worker`) per the
42
- # wrapper-invocation contract in agents/workers/_cli-wrapper-template.md.
43
- #
44
- # When role == `verifier`, the wrapper additionally grants the codex
45
- # `workspace-write` sandbox write access to `~/.cargo` and `~/.rustup` (when
46
- # they exist). Cargo's package-cache flock (`~/.cargo/.package-cache`) and
47
- # the registry/cache trees live OUTSIDE the workspace, so without these
48
- # extra `--add-dir` entries any `cargo build/test/clippy` invoked by the
49
- # verifier fails with `Resource temporarily unavailable` on the global
50
- # flock — which is the documented FU-V1 verifier-harness failure. Only the
51
- # `verifier` role gets this extension; other roles (`worker`, `executor`,
52
- # any custom label) keep the prior policy. Override per-role extras via the
53
- # `OKSTRA_CODEX_VERIFIER_EXTRA_DIRS` env var (colon-separated absolute
54
- # paths; empty disables the verifier extension entirely).
55
- #
56
- # For linked worktrees (the okstra implementation default), the per-worktree
57
- # git metadata (index, HEAD, refs) and the shared object database live in the
58
- # main repository's `.git` directory — OUTSIDE the worktree-path. Without
59
- # write access there, `git add` / `git commit` from inside the worktree fails
60
- # with EPERM on `.git/worktrees/<name>/index.lock`, which is the documented
61
- # failure pattern for linked-worktree commits under `workspace-write`. The
62
- # wrapper resolves the main repo's git-common-dir via `git rev-parse` against
63
- # the supplied worktree-path and forwards it as an additional `--add-dir`. If
64
- # resolution fails (not a linked worktree, or git unavailable), the extra
65
- # add-dir is silently omitted — the caller still gets the worktree add-dir
66
- # and any commit failure surfaces as a normal sandbox EPERM.
67
- #
68
- # The wrapper exits non-zero on any preflight failure.
69
2
  set -euo pipefail
70
3
 
71
- if [[ $# -lt 3 || $# -gt 6 ]]; then
72
- printf 'usage: %s <project-root> <model-execution-value> <prompt-path> [worktree-path] [role] [idle-timeout-seconds]\n' "$(basename "$0")" >&2
73
- exit 64
74
- fi
75
-
76
- project_root="$1"
77
- model="$2"
78
- prompt_path="$3"
79
- worktree_path="${4-}"
80
- role="${5:-worker}"
81
- # Implementation executor / verifier dispatches run whole build + test suites
82
- # that legitimately emit no stdout for many minutes; the 600s idle cap reaps
83
- # them mid-suite (observed: a silent jest + tsc build TERM'd at ~929s). Default
84
- # those roles to a longer idle cap that still sits below the 1800s wall-clock
85
- # polling cap so a genuine hang is reaped early. An explicit 6th arg wins.
86
- case "$role" in
87
- executor | verifier) default_idle_timeout_secs=1500 ;;
88
- *) default_idle_timeout_secs=600 ;;
89
- esac
90
- idle_timeout_secs="${6:-$default_idle_timeout_secs}"
91
-
92
- if ! [[ "$idle_timeout_secs" =~ ^[0-9]+$ ]]; then
93
- printf 'okstra-codex-exec: idle-timeout-seconds must be a non-negative integer: %q\n' "$idle_timeout_secs" >&2
94
- exit 69
95
- fi
96
-
97
- if [[ -z "$project_root" || ! -d "$project_root" ]]; then
98
- printf 'okstra-codex-exec: project-root is missing or not a directory: %q\n' "$project_root" >&2
99
- exit 65
100
- fi
101
-
102
- if [[ -z "$model" ]]; then
103
- printf 'okstra-codex-exec: model-execution-value is empty\n' >&2
104
- exit 66
105
- fi
106
-
107
- if [[ -z "$prompt_path" || ! -f "$prompt_path" ]]; then
108
- printf 'okstra-codex-exec: prompt-path is missing or not a file: %q\n' "$prompt_path" >&2
109
- exit 67
110
- fi
111
-
112
- if ! command -v codex >/dev/null 2>&1; then
113
- printf 'okstra-codex-exec: codex CLI is not installed on PATH\n' >&2
114
- exit 127
115
- fi
116
-
117
- if [[ -n "$worktree_path" && ! -d "$worktree_path" ]]; then
118
- printf 'okstra-codex-exec: worktree-path was provided but is not a directory: %q\n' "$worktree_path" >&2
119
- exit 68
120
- fi
121
-
122
- extra_args=()
123
- if [[ -n "$worktree_path" ]]; then
124
- extra_args+=(--add-dir "$worktree_path")
125
- # For linked worktrees, also open the main repo's `.git` so `git add` /
126
- # `git commit` can write the per-worktree index/refs (under
127
- # `.git/worktrees/<name>/`) and the shared object DB (`.git/objects/`).
128
- # `--git-common-dir` resolves to the main repo's `.git` for any worktree
129
- # (linked or main); for a main checkout it equals `<worktree>/.git` and is
130
- # redundant-but-harmless. Failures (not-a-git-repo, git missing) are
131
- # tolerated silently so analysis-phase callers stay unaffected.
132
- if command -v git >/dev/null 2>&1; then
133
- common_git_dir=$(git -C "$worktree_path" rev-parse --git-common-dir 2>/dev/null || true)
134
- if [[ -n "$common_git_dir" ]]; then
135
- # `rev-parse --git-common-dir` may return a path relative to the
136
- # worktree; normalise to an absolute directory before forwarding.
137
- if [[ "$common_git_dir" != /* ]]; then
138
- common_git_dir="$worktree_path/$common_git_dir"
139
- fi
140
- if [[ -d "$common_git_dir" ]]; then
141
- # Resolve `..` / symlinks so codex sees a canonical path.
142
- common_git_dir=$(cd "$common_git_dir" && pwd -P)
143
- extra_args+=(--add-dir "$common_git_dir")
144
- fi
145
- fi
146
- fi
147
- fi
148
-
149
- # Verifier-role sandbox extension (see header comment for rationale).
150
- if [[ "$role" == "verifier" ]]; then
151
- if [[ -n "${OKSTRA_CODEX_VERIFIER_EXTRA_DIRS+x}" ]]; then
152
- verifier_extra_raw="$OKSTRA_CODEX_VERIFIER_EXTRA_DIRS"
153
- else
154
- verifier_extra_raw="${HOME}/.cargo:${HOME}/.rustup"
155
- fi
156
- if [[ -n "$verifier_extra_raw" ]]; then
157
- IFS=':' read -r -a verifier_extra_dirs <<< "$verifier_extra_raw"
158
- for verifier_dir in "${verifier_extra_dirs[@]}"; do
159
- [[ -z "$verifier_dir" ]] && continue
160
- if [[ -d "$verifier_dir" ]]; then
161
- verifier_dir_abs=$(cd "$verifier_dir" && pwd -P)
162
- extra_args+=(--add-dir "$verifier_dir_abs")
163
- fi
164
- done
165
- fi
166
- fi
167
-
168
- # Derive a live-progress log path next to the prompt. The codex CLI streams
169
- # its progress over stdout/stderr, but the caller (codex-worker subagent)
170
- # only polls `BashOutput` on a 60s cadence — so without a sideband, a 10–30
171
- # minute implementation run produces no visible output until the very end.
172
- # Mirroring both streams into a file alongside the prompt lets the human
173
- # operator `tail -f <log-path>` from a separate pane and watch progress in
174
- # real time, and leaves a post-mortem record on disk regardless of how the
175
- # subagent renders the dispatch.
176
- log_path="${prompt_path%.md}.log"
177
- [[ "$log_path" == "$prompt_path" ]] && log_path="${prompt_path}.log"
178
- : > "$log_path"
179
-
180
- # Per-block line cap applied to the LOG COPY of codex's stdout (see
181
- # okstra_log_mirror below). Workers read their required inputs end-to-end per
182
- # templates/worker-prompt-preamble.md "Reading rules", so one report read can
183
- # dump 170KB+ into the log; observed sidecar logs reach 8MB and account for
184
- # ~93% of a project's `.okstra/` bytes (`okstra log-report` inventories them).
185
- # 120 lines keeps each block's command and the head of its output — enough to
186
- # reconstruct what ran — while cutting the whole-file dumps that dominate.
187
- log_block_line_cap=120
188
-
189
- # Mirror stdin to stdout verbatim AND to <log-path> with a per-block cap.
190
- #
191
- # Only the log copy is capped. The stdout passthrough is never filtered, so the
192
- # dispatching subagent's `BashOutput` still receives codex's output verbatim for
193
- # Phase 5 synthesis, and the model's own context is unaffected — the log is a
194
- # *copy* of output codex already produced, not an input to anything except a
195
- # human post-mortem and the `tail -n 10` diagnostic in
196
- # agents/workers/_cli-wrapper-template.md.
197
- #
198
- # A "block" starts at one of codex's own top-level markers. Misreading a marker
199
- # only resets the counter (less truncation); it can never drop a line from
200
- # stdout. The marker set is codex-specific — the claude wrapper emits
201
- # `--output-format=stream-json` and must NOT reuse this filter as-is.
202
- okstra_log_mirror() {
203
- awk -v logf="$1" -v cap="$2" '
204
- function drain() {
205
- if (elided > 0) {
206
- printf " [okstra log-mirror] %d line(s) elided from this block\n", elided >> logf
207
- fflush(logf)
208
- elided = 0
209
- }
210
- }
211
- /^(exec|codex|thinking|user)$/ { drain(); kept = 0 }
212
- {
213
- print
214
- fflush()
215
- kept++
216
- if (kept <= cap) {
217
- print >> logf
218
- fflush(logf)
219
- } else {
220
- elided++
221
- # Periodic drain keeps the log mtime moving: the idle watchdog below
222
- # polls that mtime as its liveness proxy and would otherwise SIGTERM
223
- # codex partway through a long elided block.
224
- if (elided >= 500) drain()
225
- }
226
- }
227
- END { drain(); close(logf) }
228
- '
229
- }
230
-
231
- # stdout-mirror FIFO path (created at the dispatch block below). Declared here,
232
- # before the EXIT trap is defined, so the trap's cleanup reference is always set
233
- # under `set -u`.
234
- stdout_fifo=""
235
-
236
- # Heartbeat sidecar (`<prompt>.status.json`). The codex CLI streams progress
237
- # over stdout/stderr but the only structured signal the caller subagent
238
- # polls is `BashOutput`'s binary `running`/`completed` state. The sidecar
239
- # records `started_ts`, `pid`, `log_path` up-front and — via the EXIT trap
240
- # below — `exit_code`, `ended_ts`, `duration_ms` on termination. Two
241
- # consumers:
242
- # 1. `codex-worker` step 8c reads `log_path` to capture a diagnostic
243
- # tail when `exit_code == 0` but no Result file was produced.
244
- # 2. Lead's redispatch policy distinguishes "wrapper never started"
245
- # from "CLI ran but produced no artifact" via `stage`/`started_ts`.
246
- # Writes are best-effort; sidecar failures must NOT break the dispatch
247
- # (the underlying CLI run is the wrapper's primary job).
248
- status_path="${prompt_path}.status.json"
249
- started_ts=$(date +%s)
250
4
  script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
251
- # Trace-pane caller resolution helper (okstra_resolve_caller_pane). The lib dir
252
- # is a bin-sibling in both repo (scripts/lib/...) and installed
253
- # (~/.okstra/bin/lib/...) layouts; degrade silently if absent.
254
- [ -r "$script_dir/lib/okstra/tmux-pane.sh" ] && . "$script_dir/lib/okstra/tmux-pane.sh"
255
- python3 "$script_dir/okstra-wrapper-status.py" \
256
- init "$status_path" "$(basename "$0")" "$role" "$$" "$started_ts" "$log_path" \
257
- >>"$log_path" 2>&1 || true
258
-
259
- # Derive the okstra run dir from the prompt path. paths.py is the SSOT:
260
- # dispatched prompts live at `<RUN_DIR>/prompts/<cli>-worker-prompt<NNN>.md`,
261
- # so the run dir is two levels up. Used to tag the trace pane so cleanup can
262
- # find exactly this run's panes without any tmux env var. Empty if the
263
- # derivation fails — every dependent step below then degrades to a no-op.
264
- run_dir="$(cd "$(dirname "$prompt_path")/.." 2>/dev/null && pwd -P || true)"
265
-
266
- # Resolve the pane THIS wrapper actually runs in by walking our ancestor PIDs
267
- # and matching tmux pane_pids (see lib/okstra/tmux-pane.sh). Reliable
268
- # regardless of $TMUX/$TMUX_PANE (stripped by Claude Code's Bash tool) and of
269
- # which tmux client is currently active — a bare `tmux display-message` would
270
- # instead return the most-recently-active client's pane, frequently a DIFFERENT
271
- # session than the okstra run, which is why earlier approaches mis-placed or
272
- # dropped the trace pane. Empty = not inside a tmux pane (e.g. Claude launched
273
- # from the GUI app) → the trace split below is skipped.
274
- caller_pane=""
275
- if type okstra_resolve_caller_pane >/dev/null 2>&1; then
276
- caller_pane="$(okstra_resolve_caller_pane)"
277
- fi
278
-
279
- # Pane titles: the caller (worker) pane gets `codex-<role>`; the sibling trace
280
- # pane is that same caller title with a `-tail` suffix, so the operator can
281
- # visually pair `<caller> ↔ <caller>-tail`. `role` carries the dispatched Agent
282
- # name minus the `codex-` prefix (e.g. `worker-reverify-r1`, `executor`), so the
283
- # pane title equals the FleetView teammate name instead of a generic `worker`.
284
- pane_label="codex-${role}"
285
- trace_label="${pane_label}-tail"
286
-
287
- # Capture the caller pane's current title so the EXIT trap can restore it
288
- # once the wrapper returns. Empty when not in tmux or capture fails — the
289
- # restore step degrades to a no-op in that case.
290
- original_caller_title=""
291
- if [[ -n "$caller_pane" ]]; then
292
- original_caller_title=$(tmux display-message -p -t "$caller_pane" '#{pane_title}' 2>/dev/null || true)
293
- fi
294
-
295
- _okstra_status_finish() {
296
- local exit_code=$?
297
- local ended_ts
298
- ended_ts=$(date +%s)
299
- local duration_ms=$(( (ended_ts - started_ts) * 1000 ))
300
- python3 "$script_dir/okstra-wrapper-status.py" \
301
- finish "$status_path" "$exit_code" "$ended_ts" "$duration_ms" \
302
- >>"$log_path" 2>&1 || true
303
- if [[ -n "$caller_pane" && -n "$original_caller_title" ]]; then
304
- tmux select-pane -t "$caller_pane" -T "$original_caller_title" 2>/dev/null || true
305
- fi
306
- [[ -n "$stdout_fifo" ]] && rm -f "$stdout_fifo" 2>/dev/null || true
307
- }
308
- trap _okstra_status_finish EXIT
309
-
310
- # Label the caller (worker) pane now that the restore trap is armed. Any
311
- # failure after this point still rewinds the title to its prior value.
312
- if [[ -n "$caller_pane" ]]; then
313
- tmux select-pane -t "$caller_pane" -T "$pane_label" 2>/dev/null || true
314
- fi
315
-
316
- # When a tmux session is reachable, split a sibling pane that tails the live
317
- # log so the operator can watch codex's progress in real time without waiting
318
- # for the wrapper to exit. This fires in every phase the wrapper is invoked
319
- # from (analysis, error-analysis, implementation-planning, implementation,
320
- # …) — long-running codex dispatches are not implementation-specific. The
321
- # new pane carries the title `codex-<role>-tail` so the operator can
322
- # pair it with its caller pane (`codex-<role>`). The split is
323
- # explicitly anchored to the caller pane (`-t "$caller_pane"`) to avoid
324
- # attaching to tmux's idle active pane. `role` is the optional 5th
325
- # positional arg (defaults to `worker`); callers that dispatch a different
326
- # role (e.g. `executor`, `worker-reverify-r1`) must pass it explicitly so the
327
- # pane title names the actual job.
328
- # The pane uses `tail -F` (follow-by-name) so it survives any truncation a
329
- # re-dispatch performs on the same log path. We gate on a resolved
330
- # `$caller_pane` (non-empty only when tmux is reachable) rather than the
331
- # now-stripped `$TMUX`. Failures are tolerated silently: no tmux, a tmux
332
- # that refuses to split (size constraints, locked client), or a stale
333
- # socket all degrade to "log file is still on disk; the operator can tail
334
- # it manually from any terminal." The wrapper does NOT switch focus to the
335
- # new pane — control returns to the caller's pane via `tmux last-pane`.
336
- if [[ -n "$caller_pane" ]]; then
337
- split_args=(-h -P -F '#{pane_id}' -c "$(dirname "$log_path")" -t "$caller_pane")
338
- trace_pane=$(tmux split-window "${split_args[@]}" \
339
- "tail -F $(printf '%q' "$log_path")" 2>/dev/null || true)
340
- if [[ -n "$trace_pane" ]]; then
341
- tmux select-pane -t "$trace_pane" -T "$trace_label" 2>/dev/null || true
342
- # Tag the spawned pane with THIS run's dir so `okstra-trace-cleanup.sh
343
- # --run-dir <RUN_DIR>` (see that script + `_common-contract.md`) can find
344
- # and close exactly this run's trace panes — discovered server-wide by
345
- # tag, needing no tmux env var, no pane-id registry, and no active-pane
346
- # assumption. The run-scoped tag also stops concurrent okstra runs from
347
- # stomping each other's trace panes.
348
- [[ -n "$run_dir" ]] && tmux set-option -p -t "$trace_pane" @okstra_trace_run "$run_dir" 2>/dev/null || true
349
- [[ -n "$status_path" ]] && tmux set-option -p -t "$trace_pane" @okstra_status "$status_path" 2>/dev/null || true
350
- tmux last-pane 2>/dev/null || true
351
- fi
352
- fi
353
-
354
- # stdin redirect, stderr capture, and pipeline mirroring are intentionally
355
- # inside the wrapper — this is the entire reason this script exists.
356
- #
357
- # stdout: mirrored to both the live log (for `tail -f`, capped per block by
358
- # okstra_log_mirror) AND the wrapper's own stdout (verbatim, so the
359
- # subagent's `BashOutput` still captures the final text for Phase 5
360
- # synthesis). Implemented via a FIFO so codex itself stays a single
361
- # addressable PID we can SIGTERM from the watchdog.
362
- # stderr: appended to the live log only — mirrors the prior `2>/dev/null`
363
- # contract of keeping the wrapper's stderr stream clean.
364
- # exit: codex's own exit code is preserved by `wait`.
365
- #
366
- # approval_policy=never: this wrapper runs codex fully non-interactively — stdin
367
- # is the prompt file (EOF after it) and stdout/stderr are pipes (process
368
- # substitution), not a TTY. Under codex's default `on-request` policy the agent
369
- # BLOCKS on an approval prompt the instant it wants to act outside the
370
- # workspace-write sandbox (e.g. read a sibling repo the prompt references); with
371
- # no TTY to answer, codex ends the turn after a few seconds with exit 0 and NO
372
- # model output — the silent empty-result failure this wrapper otherwise reports
373
- # as a spurious success. `never` returns sandbox-escalation failures to the
374
- # model instead of prompting, so the turn proceeds. The sandbox stays
375
- # `workspace-write`: this removes the approval gate, not the sandbox. It is the
376
- # codex-side equivalent of the antigravity wrapper's `--dangerously-skip-permissions`.
377
- # stdout must reach BOTH the live log and the wrapper's own stdout (so the
378
- # subagent's `BashOutput` captures the final text for Phase 5 synthesis). A
379
- # process substitution (`> >(tee …)`) does this, but bash opens the substituted
380
- # `/dev/fd/<n>` by pathname, and the macOS seatbelt sandbox wrapping a non-tmux
381
- # subagent's Bash tool denies that open (EPERM) — codex then never starts and
382
- # the wrapper exits with no output (the documented non-tmux dispatch failure). A
383
- # named FIFO is opened by an ordinary path under `.okstra/` (sandbox-writable),
384
- # so the mirror works in every dispatch context (real tmux pane OR sandboxed
385
- # subagent) while keeping codex a single addressable PID for the watchdog.
386
- stdout_fifo="${log_path}.stdout.fifo"
387
- rm -f "$stdout_fifo"
388
- mkfifo "$stdout_fifo"
389
- okstra_log_mirror "$log_path" "$log_block_line_cap" < "$stdout_fifo" &
390
- stdout_tee_pid=$!
391
-
392
- # Disable git's fsmonitor for every git command codex runs in this process
393
- # tree. The main worktree's fsmonitor daemon leaks its IPC socket into task
394
- # worktrees, so git status/commit here intermittently fail with
395
- # `fsmonitor_ipc__send_query` errors (workers otherwise recover by hand with
396
- # `git -c core.fsmonitor=false`). Injecting it via GIT_CONFIG_* scopes the
397
- # override to this process tree without touching the user's repo config, and
398
- # appends after any GIT_CONFIG_* the caller already set.
399
- _gc_idx="${GIT_CONFIG_COUNT:-0}"
400
- export "GIT_CONFIG_KEY_${_gc_idx}=core.fsmonitor"
401
- export "GIT_CONFIG_VALUE_${_gc_idx}=false"
402
- export GIT_CONFIG_COUNT="$(( _gc_idx + 1 ))"
403
-
404
- codex exec -C "$project_root" ${extra_args[@]+"${extra_args[@]}"} --model "$model" --sandbox workspace-write -c approval_policy=never - \
405
- < "$prompt_path" \
406
- 2>> "$log_path" \
407
- > "$stdout_fifo" &
408
- codex_pid=$!
409
-
410
- # Idle watchdog: poll the live log's mtime; if no write (stdout or stderr)
411
- # arrives for $idle_timeout_secs, SIGTERM codex, give it a 5-second grace,
412
- # then SIGKILL. Record the termination cause in the status sidecar so the
413
- # caller (lead) can distinguish "ran to completion with non-zero exit" from
414
- # "killed because it went silent". Set 0 to disable entirely.
415
- watchdog_pid=""
416
- if (( idle_timeout_secs > 0 )); then
417
- poll_interval=$(( idle_timeout_secs / 20 ))
418
- (( poll_interval < 5 )) && poll_interval=5
419
- (( poll_interval > 30 )) && poll_interval=30
420
- (
421
- while kill -0 "$codex_pid" 2>/dev/null; do
422
- sleep "$poll_interval"
423
- kill -0 "$codex_pid" 2>/dev/null || exit 0
424
- # Portable mtime probe: GNU (stat -c %Y) first, macOS/BSD (stat -f %m) as
425
- # the fallback, 0 as the floor. Validate each result as a bare integer
426
- # before accepting it — on Linux the BSD-ism `stat -f %m` does NOT fail
427
- # cleanly; it succeeds and prints non-numeric filesystem output, which
428
- # would break the arithmetic below under `set -u`.
429
- #
430
- # `|| true` is mandatory. macOS has no GNU `stat -c`, so that call exits 1,
431
- # and `2>/dev/null` suppresses stderr but not the exit code. This watchdog
432
- # is an explicit `( ) &` subshell, so it inherits whatever errexit setting
433
- # is in force; under `set -e` a single failed assignment kills the whole
434
- # watchdog on the first poll — silently, without a log line.
435
- last_mtime="$(stat -c %Y "$log_path" 2>/dev/null || true)"
436
- [[ "$last_mtime" =~ ^[0-9]+$ ]] || last_mtime="$(stat -f %m "$log_path" 2>/dev/null || true)"
437
- [[ "$last_mtime" =~ ^[0-9]+$ ]] || last_mtime=0
438
- now=$(date +%s)
439
- idle=$(( now - last_mtime ))
440
- if (( idle >= idle_timeout_secs )); then
441
- printf '\n[okstra wrapper] idle-watchdog: %ds without stdout — terminating codex (pid=%d)\n' \
442
- "$idle" "$codex_pid" >> "$log_path" 2>&1 || true
443
- python3 "$script_dir/okstra-wrapper-status.py" \
444
- timeout "$status_path" "$now" "$idle" >>"$log_path" 2>&1 || true
445
- kill -TERM "$codex_pid" 2>/dev/null || true
446
- sleep 5
447
- kill -KILL "$codex_pid" 2>/dev/null || true
448
- exit 0
449
- fi
450
- done
451
- ) &
452
- watchdog_pid=$!
453
- fi
454
-
455
- set +e
456
- wait "$codex_pid"
457
- codex_exit=$?
458
- set -e
459
-
460
- if [[ -n "$watchdog_pid" ]]; then
461
- kill "$watchdog_pid" 2>/dev/null || true
462
- wait "$watchdog_pid" 2>/dev/null || true
463
- fi
464
-
465
- # Drain the stdout-mirror tee so the final lines reach the live log and the
466
- # caller's stdout before exit. codex's closed write end gives the tee EOF; then
467
- # remove the FIFO (the EXIT trap also clears it on an abnormal exit).
468
- wait "$stdout_tee_pid" 2>/dev/null || true
469
- rm -f "$stdout_fifo"
470
-
471
- exit "$codex_exit"
5
+ exec python3 "$script_dir/okstra-provider-exec.py" codex "$@"