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,344 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # okstra-antigravity-exec.sh — wrapper around `agy --print` for okstra antigravity-worker
3
- #
4
- # Purpose: Claude Code's Bash permission matcher requires explicit approval for
5
- # commands that contain shell metacharacters (stdout/stderr redirects, pipes).
6
- # `agy --print "<prompt>" --model <model> --add-dir <root> 2>>log > <fifo>`
7
- # therefore triggers a permission prompt every dispatch even when `Bash(agy:*)`
8
- # is allowlisted, because 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-antigravity-exec.sh:*)
14
- #
15
- # Usage:
16
- # okstra-antigravity-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 `agy` is SIGTERM'd (then
24
- # 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. Kept in
27
- # lock-step with `okstra-codex-exec.sh` — see that wrapper for the full
28
- # design rationale.
29
- #
30
- # worktree-path is optional and used for okstra implementation phase, where the
31
- # executor must mutate files inside a git worktree that lives outside
32
- # project-root. When supplied (non-empty), it is added to agy's `--add-dir`
33
- # workspace list so the model can see and operate on the worktree alongside the
34
- # primary workspace. When omitted or empty, only project-root is added (existing
35
- # analysis-phase behavior).
36
- #
37
- # For linked worktrees, the per-worktree git metadata (index, HEAD, refs) and
38
- # the shared object database live in the main repository's `.git` directory —
39
- # OUTSIDE the worktree-path. Without access there, `git add` / `git commit`
40
- # from inside the worktree fails on `.git/worktrees/<name>/index.lock`. The
41
- # wrapper resolves the main repo's git-common-dir via `git rev-parse` against
42
- # the supplied worktree-path and adds it via `--add-dir`. If resolution fails
43
- # (not a linked worktree, or git unavailable), the extra dir is silently
44
- # omitted.
45
- #
46
- # The wrapper exits non-zero on any preflight failure.
47
2
  set -euo pipefail
48
3
 
49
- # Hard ceiling for agy's print-mode wait (Go duration). The idle watchdog below
50
- # is the primary stall detector; this is a backstop against a run that never
51
- # stalls but also never finishes.
52
- PRINT_TIMEOUT="7200s"
53
-
54
- if [[ $# -lt 3 || $# -gt 6 ]]; then
55
- printf 'usage: %s <project-root> <model-execution-value> <prompt-path> [worktree-path] [role] [idle-timeout-seconds]\n' "$(basename "$0")" >&2
56
- exit 64
57
- fi
58
-
59
- project_root="$1"
60
- model="$2"
61
- prompt_path="$3"
62
- worktree_path="${4-}"
63
- role="${5:-worker}"
64
- # Implementation executor / verifier dispatches run whole build + test suites
65
- # that legitimately emit no stdout for many minutes; the 600s idle cap reaps
66
- # them mid-suite (observed: a silent jest + tsc build TERM'd at ~929s). Default
67
- # those roles to a longer idle cap that still sits below the 1800s wall-clock
68
- # polling cap so a genuine hang is reaped early. An explicit 6th arg wins.
69
- case "$role" in
70
- executor | verifier) default_idle_timeout_secs=1500 ;;
71
- *) default_idle_timeout_secs=600 ;;
72
- esac
73
- idle_timeout_secs="${6:-$default_idle_timeout_secs}"
74
-
75
- if ! [[ "$idle_timeout_secs" =~ ^[0-9]+$ ]]; then
76
- printf 'okstra-antigravity-exec: idle-timeout-seconds must be a non-negative integer: %q\n' "$idle_timeout_secs" >&2
77
- exit 69
78
- fi
79
-
80
- if [[ -z "$project_root" || ! -d "$project_root" ]]; then
81
- printf 'okstra-antigravity-exec: project-root is missing or not a directory: %q\n' "$project_root" >&2
82
- exit 65
83
- fi
84
-
85
- if [[ -z "$model" ]]; then
86
- printf 'okstra-antigravity-exec: model-execution-value is empty\n' >&2
87
- exit 66
88
- fi
89
-
90
- if [[ -z "$prompt_path" || ! -f "$prompt_path" ]]; then
91
- printf 'okstra-antigravity-exec: prompt-path is missing or not a file: %q\n' "$prompt_path" >&2
92
- exit 67
93
- fi
94
-
95
- if ! command -v agy >/dev/null 2>&1; then
96
- printf 'okstra-antigravity-exec: agy (Antigravity CLI) is not installed on PATH\n' >&2
97
- exit 127
98
- fi
99
-
100
- if [[ -n "$worktree_path" && ! -d "$worktree_path" ]]; then
101
- printf 'okstra-antigravity-exec: worktree-path was provided but is not a directory: %q\n' "$worktree_path" >&2
102
- exit 68
103
- fi
104
-
105
- # agy has no `--cd` flag, so workspace correctness is anchored via repeatable
106
- # `--add-dir` plus the Project Root referenced in the prompt body itself.
107
- add_dir_args=(--add-dir "$project_root")
108
- if [[ -n "$worktree_path" ]]; then
109
- add_dir_args+=(--add-dir "$worktree_path")
110
- # For linked worktrees, also open the main repo's `.git` so `git add` /
111
- # `git commit` can write the per-worktree index/refs and the shared
112
- # object DB. `--git-common-dir` resolves to the main repo's `.git` for
113
- # any worktree (linked or main); for a main checkout it equals
114
- # `<worktree>/.git` and is redundant-but-harmless. Failures are tolerated
115
- # silently so analysis-phase callers stay unaffected.
116
- if command -v git >/dev/null 2>&1; then
117
- common_git_dir=$(git -C "$worktree_path" rev-parse --git-common-dir 2>/dev/null || true)
118
- if [[ -n "$common_git_dir" ]]; then
119
- if [[ "$common_git_dir" != /* ]]; then
120
- common_git_dir="$worktree_path/$common_git_dir"
121
- fi
122
- if [[ -d "$common_git_dir" ]]; then
123
- common_git_dir=$(cd "$common_git_dir" && pwd -P)
124
- add_dir_args+=(--add-dir "$common_git_dir")
125
- fi
126
- fi
127
- fi
128
- fi
129
-
130
- # Derive a live-progress log path next to the prompt. agy streams its progress
131
- # over stdout/stderr, but the caller (antigravity-worker subagent) only polls
132
- # `BashOutput` on a 60s cadence — so without a sideband, a long implementation
133
- # run produces no visible output until the very end. Mirroring both streams into
134
- # a file alongside the prompt lets the human operator `tail -f <log-path>` from a
135
- # separate pane and watch progress in real time, and leaves a post-mortem record
136
- # on disk.
137
- log_path="${prompt_path%.md}.log"
138
- [[ "$log_path" == "$prompt_path" ]] && log_path="${prompt_path}.log"
139
- : > "$log_path"
140
-
141
- # stdout-mirror FIFO path (created at the dispatch block below). Declared here,
142
- # before the EXIT trap is defined, so the trap's cleanup reference is always set
143
- # under `set -u`.
144
- stdout_fifo=""
145
-
146
- # Heartbeat sidecar (`<prompt>.status.json`). See `okstra-codex-exec.sh` for
147
- # the full design comment — kept in lock-step. Two consumers:
148
- # 1. `antigravity-worker` step 8c reads `log_path` to capture a diagnostic
149
- # tail when `exit_code == 0` but no Result file was produced.
150
- # 2. Lead's redispatch policy distinguishes "wrapper never started"
151
- # from "CLI ran but produced no artifact" via `stage`/`started_ts`.
152
- # Writes are best-effort.
153
- status_path="${prompt_path}.status.json"
154
- started_ts=$(date +%s)
155
4
  script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
156
- # Trace-pane caller resolution helper (okstra_resolve_caller_pane). The lib dir
157
- # is a bin-sibling in both repo (scripts/lib/...) and installed
158
- # (~/.okstra/bin/lib/...) layouts; degrade silently if absent.
159
- [ -r "$script_dir/lib/okstra/tmux-pane.sh" ] && . "$script_dir/lib/okstra/tmux-pane.sh"
160
- python3 "$script_dir/okstra-wrapper-status.py" \
161
- init "$status_path" "$(basename "$0")" "$role" "$$" "$started_ts" "$log_path" \
162
- >>"$log_path" 2>&1 || true
163
-
164
- # Resolve the run dir and the trace-split anchor pane. See
165
- # `okstra-codex-exec.sh` / `lib/okstra/tmux-pane.sh` for the full rationale —
166
- # kept in lock-step: derive `<RUN_DIR>` from the prompt path (paths.py SSOT) to
167
- # tag the trace pane, and resolve the caller pane by walking our ancestor PIDs
168
- # against tmux pane_pids (reliable even though `$TMUX`/`$TMUX_PANE` are stripped
169
- # and the wrapper runs backgrounded). Empty = not inside a tmux pane → skip.
170
- run_dir="$(cd "$(dirname "$prompt_path")/.." 2>/dev/null && pwd -P || true)"
171
-
172
- caller_pane=""
173
- if type okstra_resolve_caller_pane >/dev/null 2>&1; then
174
- caller_pane="$(okstra_resolve_caller_pane)"
175
- fi
176
-
177
- # Pane titles: the caller (worker) pane gets `agy-<role>`; the sibling trace
178
- # pane is that same caller title with a `-tail` suffix, so the operator can
179
- # visually pair `<caller> ↔ <caller>-tail`. `role` carries the dispatched Agent
180
- # name minus the `antigravity-` prefix (e.g. `worker-reverify-r1`, `executor`), so
181
- # the pane title equals the FleetView teammate name instead of a generic `worker`.
182
- pane_label="agy-${role}"
183
- trace_label="${pane_label}-tail"
184
-
185
- # Capture the caller pane's current title so the EXIT trap can restore it
186
- # once the wrapper returns. Empty when not in tmux or capture fails — the
187
- # restore step degrades to a no-op in that case.
188
- original_caller_title=""
189
- if [[ -n "$caller_pane" ]]; then
190
- original_caller_title=$(tmux display-message -p -t "$caller_pane" '#{pane_title}' 2>/dev/null || true)
191
- fi
192
-
193
- _okstra_status_finish() {
194
- local exit_code=$?
195
- local ended_ts
196
- ended_ts=$(date +%s)
197
- local duration_ms=$(( (ended_ts - started_ts) * 1000 ))
198
- python3 "$script_dir/okstra-wrapper-status.py" \
199
- finish "$status_path" "$exit_code" "$ended_ts" "$duration_ms" \
200
- >>"$log_path" 2>&1 || true
201
- if [[ -n "$caller_pane" && -n "$original_caller_title" ]]; then
202
- tmux select-pane -t "$caller_pane" -T "$original_caller_title" 2>/dev/null || true
203
- fi
204
- [[ -n "$stdout_fifo" ]] && rm -f "$stdout_fifo" 2>/dev/null || true
205
- }
206
- trap _okstra_status_finish EXIT
207
-
208
- # Label the caller (worker) pane now that the restore trap is armed. Any
209
- # failure after this point still rewinds the title to its prior value.
210
- if [[ -n "$caller_pane" ]]; then
211
- tmux select-pane -t "$caller_pane" -T "$pane_label" 2>/dev/null || true
212
- fi
213
-
214
- # When a tmux session is reachable, split a sibling pane tailing the log so
215
- # the operator can watch progress live. This fires in every phase the
216
- # wrapper is invoked from — long-running agy dispatches are not
217
- # implementation-specific. Title `agy-<role>-tail` so the operator
218
- # can pair it with its caller pane (`agy-<role>`). The split is
219
- # explicitly anchored to the caller pane to avoid attaching to tmux's idle
220
- # active pane. `role` is the optional 5th positional arg (defaults to
221
- # `worker`); callers that dispatch a different role (e.g. `worker-reverify-r1`)
222
- # must pass it explicitly so the pane title names the actual job.
223
- # We gate on a resolved `$caller_pane`
224
- # (non-empty only when tmux is reachable) rather than the now-stripped
225
- # `$TMUX`. See the codex wrapper for the full design rationale and the
226
- # silent-degrade failure model.
227
- if [[ -n "$caller_pane" ]]; then
228
- split_args=(-h -P -F '#{pane_id}' -c "$(dirname "$log_path")" -t "$caller_pane")
229
- trace_pane=$(tmux split-window "${split_args[@]}" \
230
- "tail -F $(printf '%q' "$log_path")" 2>/dev/null || true)
231
- if [[ -n "$trace_pane" ]]; then
232
- tmux select-pane -t "$trace_pane" -T "$trace_label" 2>/dev/null || true
233
- # Tag with this run's dir for `okstra-trace-cleanup.sh --run-dir`. See
234
- # `okstra-codex-exec.sh` for the rationale — kept in lock-step.
235
- [[ -n "$run_dir" ]] && tmux set-option -p -t "$trace_pane" @okstra_trace_run "$run_dir" 2>/dev/null || true
236
- [[ -n "$status_path" ]] && tmux set-option -p -t "$trace_pane" @okstra_status "$status_path" 2>/dev/null || true
237
- tmux last-pane 2>/dev/null || true
238
- fi
239
- fi
240
-
241
- # stdout/stderr capture and pipeline mirroring are intentionally inside the
242
- # wrapper — this is the entire reason this script exists. agy reads the prompt
243
- # as the `--print` argument (it does not read stdin), so the prompt file is
244
- # inlined via command substitution; `--dangerously-skip-permissions` keeps the
245
- # non-interactive dispatch from blocking on tool-permission prompts (the worker
246
- # is sandboxed to the supplied `--add-dir` workspace).
247
- #
248
- # stdout: agy runs in `stream-json` so the live log records the tool calls it
249
- # made — in the default `text` format agy prints only its closing
250
- # summary, leaving ~2KB of self-report where codex leaves ~487KB of
251
- # trace, and no way to check from outside whether the worker opened the
252
- # evidence it was asked to verify. `okstra-wrapper-agy-stream.py`
253
- # appends every event to the log and forwards ONLY the final response
254
- # text to the wrapper's own stdout, so what the subagent's `BashOutput`
255
- # captures for Phase 5 synthesis is unchanged by the format switch.
256
- # agy stays a single addressable PID we can SIGTERM from the watchdog.
257
- # stderr: appended to the live log only — keeps the wrapper's stderr clean.
258
- # exit: agy's own exit code is preserved by `wait`.
259
- # stdout mirror via a named FIFO instead of a `> >(tee …)` process substitution:
260
- # bash opens the substituted `/dev/fd/<n>` by pathname, which the macOS seatbelt
261
- # sandbox around a non-tmux subagent's Bash tool denies (EPERM), so agy would
262
- # never start. A FIFO under `.okstra/` is opened by an ordinary sandbox-writable
263
- # path and works in every dispatch context while keeping agy a single
264
- # addressable PID for the watchdog. See `okstra-codex-exec.sh` for the full note.
265
- stdout_fifo="${log_path}.stdout.fifo"
266
- rm -f "$stdout_fifo"
267
- mkfifo "$stdout_fifo"
268
- python3 "$script_dir/okstra-wrapper-agy-stream.py" "$log_path" < "$stdout_fifo" &
269
- stdout_tee_pid=$!
270
-
271
- # Disable git's fsmonitor for every git command agy runs in this process tree —
272
- # the main worktree's fsmonitor daemon leaks its IPC socket into task worktrees,
273
- # so git status/commit here intermittently fail with `fsmonitor_ipc__send_query`
274
- # errors. GIT_CONFIG_* scopes the override to this process tree without touching
275
- # the user's repo config. See `okstra-codex-exec.sh` for the full rationale.
276
- _gc_idx="${GIT_CONFIG_COUNT:-0}"
277
- export "GIT_CONFIG_KEY_${_gc_idx}=core.fsmonitor"
278
- export "GIT_CONFIG_VALUE_${_gc_idx}=false"
279
- export GIT_CONFIG_COUNT="$(( _gc_idx + 1 ))"
280
-
281
- agy --print "$(cat "$prompt_path")" --model "$model" "${add_dir_args[@]}" \
282
- --output-format stream-json \
283
- --print-timeout "$PRINT_TIMEOUT" --dangerously-skip-permissions \
284
- 2>> "$log_path" \
285
- > "$stdout_fifo" &
286
- agy_pid=$!
287
-
288
- # Idle watchdog — see `okstra-codex-exec.sh` for the full rationale.
289
- watchdog_pid=""
290
- if (( idle_timeout_secs > 0 )); then
291
- poll_interval=$(( idle_timeout_secs / 20 ))
292
- (( poll_interval < 5 )) && poll_interval=5
293
- (( poll_interval > 30 )) && poll_interval=30
294
- (
295
- while kill -0 "$agy_pid" 2>/dev/null; do
296
- sleep "$poll_interval"
297
- kill -0 "$agy_pid" 2>/dev/null || exit 0
298
- # Portable mtime probe: GNU (stat -c %Y) first, macOS/BSD (stat -f %m) as
299
- # the fallback, 0 as the floor. Validate each result as a bare integer
300
- # before accepting it — on Linux the BSD-ism `stat -f %m` does NOT fail
301
- # cleanly; it succeeds and prints non-numeric filesystem output, which
302
- # would break the arithmetic below under `set -u`.
303
- #
304
- # `|| true` is mandatory. macOS has no GNU `stat -c`, so that call exits 1,
305
- # and `2>/dev/null` suppresses stderr but not the exit code. This watchdog
306
- # is an explicit `( ) &` subshell, so it inherits whatever errexit setting
307
- # is in force; under `set -e` a single failed assignment kills the whole
308
- # watchdog on the first poll — silently, without a log line.
309
- last_mtime="$(stat -c %Y "$log_path" 2>/dev/null || true)"
310
- [[ "$last_mtime" =~ ^[0-9]+$ ]] || last_mtime="$(stat -f %m "$log_path" 2>/dev/null || true)"
311
- [[ "$last_mtime" =~ ^[0-9]+$ ]] || last_mtime=0
312
- now=$(date +%s)
313
- idle=$(( now - last_mtime ))
314
- if (( idle >= idle_timeout_secs )); then
315
- printf '\n[okstra wrapper] idle-watchdog: %ds without stdout — terminating agy (pid=%d)\n' \
316
- "$idle" "$agy_pid" >> "$log_path" 2>&1 || true
317
- python3 "$script_dir/okstra-wrapper-status.py" \
318
- timeout "$status_path" "$now" "$idle" >>"$log_path" 2>&1 || true
319
- kill -TERM "$agy_pid" 2>/dev/null || true
320
- sleep 5
321
- kill -KILL "$agy_pid" 2>/dev/null || true
322
- exit 0
323
- fi
324
- done
325
- ) &
326
- watchdog_pid=$!
327
- fi
328
-
329
- set +e
330
- wait "$agy_pid"
331
- agy_exit=$?
332
- set -e
333
-
334
- if [[ -n "$watchdog_pid" ]]; then
335
- kill "$watchdog_pid" 2>/dev/null || true
336
- wait "$watchdog_pid" 2>/dev/null || true
337
- fi
338
-
339
- # Drain the stdout-mirror tee (agy's closed write end gives it EOF), then remove
340
- # the FIFO (the EXIT trap also clears it on an abnormal exit).
341
- wait "$stdout_tee_pid" 2>/dev/null || true
342
- rm -f "$stdout_fifo"
343
-
344
- exit "$agy_exit"
5
+ exec python3 "$script_dir/okstra-provider-exec.py" antigravity "$@"
@@ -1,182 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # okstra-claude-exec.sh — wrapper around Claude Code non-interactive print mode.
3
2
  set -euo pipefail
4
3
 
5
- if [[ $# -lt 3 || $# -gt 6 ]]; then
6
- printf 'usage: %s <project-root> <model-execution-value> <prompt-path> [worktree-path] [role] [idle-timeout-seconds]\n' "$(basename "$0")" >&2
7
- exit 64
8
- fi
9
-
10
- project_root="$1"
11
- model="$2"
12
- prompt_path="$3"
13
- worktree_path="${4-}"
14
- role="${5:-worker}"
15
- # Implementation executor / verifier dispatches run whole build + test suites
16
- # that legitimately emit no stdout for many minutes; the 600s idle cap reaps
17
- # them mid-suite (observed: a silent jest + tsc build TERM'd at ~929s). Default
18
- # those roles to a longer idle cap that still sits below the 1800s wall-clock
19
- # polling cap so a genuine hang is reaped early. An explicit 6th arg wins.
20
- case "$role" in
21
- executor | verifier) default_idle_timeout_secs=1500 ;;
22
- *) default_idle_timeout_secs=600 ;;
23
- esac
24
- idle_timeout_secs="${6:-$default_idle_timeout_secs}"
25
-
26
- if [[ ! -d "$project_root" ]]; then
27
- printf 'okstra-claude-exec.sh: project root not found: %s\n' "$project_root" >&2
28
- exit 65
29
- fi
30
- if [[ -z "$model" ]]; then
31
- printf 'okstra-claude-exec.sh: model is required\n' >&2
32
- exit 66
33
- fi
34
- if [[ ! -f "$prompt_path" ]]; then
35
- printf 'okstra-claude-exec.sh: prompt not found: %s\n' "$prompt_path" >&2
36
- exit 67
37
- fi
38
- if [[ -n "$worktree_path" && ! -d "$worktree_path" ]]; then
39
- printf 'okstra-claude-exec.sh: worktree not found: %s\n' "$worktree_path" >&2
40
- exit 68
41
- fi
42
- if ! [[ "$idle_timeout_secs" =~ ^[0-9]+$ ]]; then
43
- printf 'okstra-claude-exec.sh: idle timeout must be a non-negative integer\n' >&2
44
- exit 69
45
- fi
46
- if ! command -v claude >/dev/null 2>&1; then
47
- printf 'okstra-claude-exec.sh: claude CLI not found\n' >&2
48
- exit 127
49
- fi
50
-
51
- # Grant tool access to the implementation worktree (and the main repo's shared
52
- # .git for linked-worktree commits) when one is supplied. The executor mutates
53
- # a git worktree that lives outside project-root; without --add-dir Claude
54
- # Code's Edit/Write tools cannot reach it. Mirrors okstra-codex-exec.sh.
55
- extra_args=()
56
- if [[ -n "$worktree_path" ]]; then
57
- extra_args+=(--add-dir "$worktree_path")
58
- if command -v git >/dev/null 2>&1; then
59
- common_git_dir=$(git -C "$worktree_path" rev-parse --git-common-dir 2>/dev/null || true)
60
- if [[ -n "$common_git_dir" ]]; then
61
- [[ "$common_git_dir" != /* ]] && common_git_dir="$worktree_path/$common_git_dir"
62
- if [[ -d "$common_git_dir" ]]; then
63
- common_git_dir=$(cd "$common_git_dir" && pwd -P)
64
- extra_args+=(--add-dir "$common_git_dir")
65
- fi
66
- fi
67
- fi
68
- fi
69
-
70
- log_path="${prompt_path%.md}.log"
71
- [[ "$log_path" == "$prompt_path" ]] && log_path="${prompt_path}.log"
72
- : > "$log_path"
73
- # stdout/stderr mirror FIFO paths (created at the dispatch block below). Declared
74
- # here, before the EXIT trap is defined, so the trap's cleanup references are
75
- # always set under `set -u`.
76
- stdout_fifo=""
77
- stderr_fifo=""
78
- status_path="${prompt_path}.status.json"
79
- started_ts=$(date +%s)
80
4
  script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
81
- python3 "$script_dir/okstra-wrapper-status.py" \
82
- init "$status_path" "$(basename "$0")" "$role" "$$" "$started_ts" "$log_path" \
83
- >>"$log_path" 2>&1 || true
84
-
85
- _okstra_status_finish() {
86
- local exit_code=$?
87
- local ended_ts
88
- ended_ts=$(date +%s)
89
- local duration_ms=$(( (ended_ts - started_ts) * 1000 ))
90
- python3 "$script_dir/okstra-wrapper-status.py" \
91
- finish "$status_path" "$exit_code" "$ended_ts" "$duration_ms" \
92
- >>"$log_path" 2>&1 || true
93
- { [[ -n "$stdout_fifo" ]] && rm -f "$stdout_fifo"; [[ -n "$stderr_fifo" ]] && rm -f "$stderr_fifo"; } 2>/dev/null || true
94
- }
95
- trap _okstra_status_finish EXIT
96
-
97
- cd "$project_root"
98
- set +e
99
- # stdout and stderr are each mirrored to the live log AND the wrapper's own
100
- # stdout/stderr. Process substitutions (`> >(tee …)`) do this, but bash opens the
101
- # substituted `/dev/fd/<n>` by pathname, and the macOS seatbelt sandbox around a
102
- # non-tmux subagent's Bash tool denies that open (EPERM) — claude then never
103
- # starts and the wrapper exits with no output. Named FIFOs under `.okstra/` are
104
- # opened by ordinary sandbox-writable paths, so the mirror works in every
105
- # dispatch context (real tmux pane OR sandboxed subagent) while keeping claude a
106
- # single addressable PID for the watchdog. See `okstra-codex-exec.sh` for the
107
- # full note.
108
- stdout_fifo="${log_path}.stdout.fifo"
109
- stderr_fifo="${log_path}.stderr.fifo"
110
- rm -f "$stdout_fifo" "$stderr_fifo"
111
- mkfifo "$stdout_fifo" "$stderr_fifo"
112
- tee -a "$log_path" < "$stdout_fifo" &
113
- stdout_tee_pid=$!
114
- tee -a "$log_path" < "$stderr_fifo" >&2 &
115
- stderr_tee_pid=$!
116
-
117
- # `--verbose` is mandatory, not cosmetic: Claude Code rejects `--print` combined
118
- # with `--output-format=stream-json` without it ("requires --verbose") and exits
119
- # 1 in under a second, so every CLI-wrapper claude worker dies before starting.
120
- claude -p --model "$model" ${extra_args[@]+"${extra_args[@]}"} --output-format=stream-json --verbose \
121
- < "$prompt_path" \
122
- > "$stdout_fifo" \
123
- 2> "$stderr_fifo" &
124
- claude_pid=$!
125
-
126
- # Idle watchdog: if the live log sees no write for $idle_timeout_secs, SIGTERM
127
- # claude (5s grace, then SIGKILL) and mark the status sidecar timed out, so a
128
- # silent hang is bounded instead of stalling the lead. Pass 0 to disable.
129
- watchdog_pid=""
130
- if (( idle_timeout_secs > 0 )); then
131
- poll_interval=$(( idle_timeout_secs / 20 ))
132
- (( poll_interval < 5 )) && poll_interval=5
133
- (( poll_interval > 30 )) && poll_interval=30
134
- (
135
- while kill -0 "$claude_pid" 2>/dev/null; do
136
- sleep "$poll_interval"
137
- kill -0 "$claude_pid" 2>/dev/null || exit 0
138
- # Portable mtime probe: GNU (stat -c %Y) first, macOS/BSD (stat -f %m) as
139
- # the fallback, 0 as the floor. Validate each result as a bare integer
140
- # before accepting it — on Linux the BSD-ism `stat -f %m` does NOT fail
141
- # cleanly; it succeeds and prints non-numeric filesystem output, which
142
- # would break the arithmetic below under `set -u`.
143
- #
144
- # `|| true` is mandatory. macOS has no GNU `stat -c`, so that call exits 1,
145
- # and `2>/dev/null` suppresses stderr but not the exit code. This watchdog
146
- # is an explicit `( ) &` subshell, so it inherits whatever errexit setting
147
- # is in force; under `set -e` a single failed assignment kills the whole
148
- # watchdog on the first poll — silently, without a log line.
149
- last_mtime="$(stat -c %Y "$log_path" 2>/dev/null || true)"
150
- [[ "$last_mtime" =~ ^[0-9]+$ ]] || last_mtime="$(stat -f %m "$log_path" 2>/dev/null || true)"
151
- [[ "$last_mtime" =~ ^[0-9]+$ ]] || last_mtime=0
152
- now=$(date +%s)
153
- idle=$(( now - last_mtime ))
154
- if (( idle >= idle_timeout_secs )); then
155
- printf '\n[okstra wrapper] idle-watchdog: %ds without stdout — terminating claude (pid=%d)\n' \
156
- "$idle" "$claude_pid" >> "$log_path" 2>&1 || true
157
- python3 "$script_dir/okstra-wrapper-status.py" \
158
- timeout "$status_path" "$now" "$idle" >>"$log_path" 2>&1 || true
159
- kill -TERM "$claude_pid" 2>/dev/null || true
160
- sleep 5
161
- kill -KILL "$claude_pid" 2>/dev/null || true
162
- exit 0
163
- fi
164
- done
165
- ) &
166
- watchdog_pid=$!
167
- fi
168
-
169
- wait "$claude_pid"
170
- claude_exit=$?
171
- set -e
172
-
173
- if [[ -n "$watchdog_pid" ]]; then
174
- kill "$watchdog_pid" 2>/dev/null || true
175
- wait "$watchdog_pid" 2>/dev/null || true
176
- fi
177
- # Drain the stdout/stderr mirror tees (claude's closed write ends give them EOF),
178
- # then remove the FIFOs (the EXIT trap also clears them on an abnormal exit).
179
- wait "$stdout_tee_pid" "$stderr_tee_pid" 2>/dev/null || true
180
- rm -f "$stdout_fifo" "$stderr_fifo"
181
-
182
- exit "$claude_exit"
5
+ exec python3 "$script_dir/okstra-provider-exec.py" claude "$@"