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.
- package/README.md +8 -6
- package/docs/architecture.md +24 -15
- package/docs/cli.md +15 -8
- package/docs/for-ai/README.md +2 -2
- package/docs/for-ai/skills/okstra-inspect.md +2 -2
- package/docs/for-ai/skills/okstra-user-response.md +2 -2
- package/docs/project-structure-overview.md +22 -14
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/antigravity-worker.md +9 -7
- package/runtime/agents/workers/claude-worker.md +1 -0
- package/runtime/agents/workers/codex-worker.md +9 -7
- package/runtime/agents/workers/grok-worker.md +6 -4
- package/runtime/agents/workers/kimi-worker.md +6 -4
- package/runtime/bin/lib/okstra/cli.sh +5 -0
- package/runtime/bin/lib/okstra/globals.sh +2 -0
- package/runtime/bin/lib/okstra/usage.sh +5 -5
- package/runtime/bin/okstra-antigravity-exec.sh +1 -340
- package/runtime/bin/okstra-claude-exec.sh +1 -178
- package/runtime/bin/okstra-codex-exec.sh +1 -467
- package/runtime/bin/okstra-provider-exec.py +165 -190
- package/runtime/bin/okstra-trace-cleanup.sh +14 -7
- package/runtime/bin/okstra-wrapper-status.py +26 -19
- package/runtime/bin/okstra.sh +87 -91
- package/runtime/prompts/lead/adapters/cmux.md +2 -2
- package/runtime/prompts/lead/convergence.md +36 -8
- package/runtime/prompts/lead/okstra-lead-contract.md +24 -1
- package/runtime/prompts/lead/plan-body-verification.md +9 -1
- package/runtime/prompts/lead/report-writer.md +1 -0
- package/runtime/prompts/lead/team-contract.md +3 -3
- package/runtime/prompts/profiles/_common-contract.md +9 -1
- package/runtime/prompts/profiles/_coverage-critic.md +1 -1
- package/runtime/prompts/profiles/_implementation-diff-review.md +3 -1
- package/runtime/prompts/profiles/_implementation-self-check.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -1
- package/runtime/prompts/profiles/implementation-planning.md +6 -4
- package/runtime/python/okstra_ctl/adapters/accounting/__init__.py +11 -0
- package/runtime/python/okstra_ctl/adapters/accounting/claude_jsonl.py +17 -0
- package/runtime/python/okstra_ctl/adapters/accounting/cli_artifact.py +17 -0
- package/runtime/python/okstra_ctl/adapters/accounting/unavailable.py +19 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/__init__.py +92 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/cli_wrapper.py +54 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/cmux.py +68 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/native_team.py +13 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +60 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/antigravity.md → python/okstra_ctl/adapters/hosts/antigravity/relay.md} +52 -0
- package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +292 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +120 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/claude-code.md → python/okstra_ctl/adapters/hosts/claude-code/relay.md} +112 -1
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +60 -0
- package/runtime/python/okstra_ctl/adapters/hosts/codex/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/codex.md → python/okstra_ctl/adapters/hosts/codex/relay.md} +52 -0
- package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +72 -0
- package/runtime/python/okstra_ctl/adapters/hosts/external/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/external.md → python/okstra_ctl/adapters/hosts/external/relay.md} +53 -1
- package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +63 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +90 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +63 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +90 -0
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +183 -0
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +110 -0
- package/runtime/python/okstra_ctl/adapters/providers/claude/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +84 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +76 -0
- package/runtime/python/okstra_ctl/adapters/providers/grok/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +80 -0
- package/runtime/python/okstra_ctl/adapters/providers/kimi/manifest.json +1 -0
- package/runtime/python/okstra_ctl/application/__init__.py +1 -0
- package/runtime/python/okstra_ctl/application/advance_wizard.py +25 -0
- package/runtime/python/okstra_ctl/application/collect_usage.py +15 -0
- package/runtime/python/okstra_ctl/application/dispatch_assignments.py +15 -0
- package/runtime/python/okstra_ctl/application/resolve_assignment.py +93 -0
- package/runtime/python/okstra_ctl/application/resume_run.py +21 -0
- package/runtime/python/okstra_ctl/application/start_run.py +21 -0
- package/runtime/python/okstra_ctl/codex_dispatch.py +49 -826
- package/runtime/python/okstra_ctl/dispatch_core.py +244 -29
- package/runtime/python/okstra_ctl/dispatch_state.py +27 -0
- package/runtime/python/okstra_ctl/domain/__init__.py +34 -0
- package/runtime/python/okstra_ctl/domain/host.py +100 -0
- package/runtime/python/okstra_ctl/domain/provider.py +70 -0
- package/runtime/python/okstra_ctl/domain/wizard/__init__.py +19 -0
- package/runtime/python/okstra_ctl/domain/wizard/interaction.py +140 -0
- package/runtime/python/okstra_ctl/domain/worker_exec.py +102 -0
- package/runtime/python/okstra_ctl/domain/worker_role.py +34 -0
- package/runtime/python/okstra_ctl/domain/worker_stream.py +261 -0
- package/runtime/python/okstra_ctl/entrypoints/__init__.py +1 -0
- package/runtime/python/okstra_ctl/entrypoints/hosts.py +334 -0
- package/runtime/python/okstra_ctl/incremental_scope.py +16 -4
- package/runtime/python/okstra_ctl/models.py +54 -269
- package/runtime/python/okstra_ctl/ports/__init__.py +15 -0
- package/runtime/python/okstra_ctl/ports/host.py +32 -0
- package/runtime/python/okstra_ctl/ports/interaction.py +15 -0
- package/runtime/python/okstra_ctl/ports/lead_session.py +25 -0
- package/runtime/python/okstra_ctl/ports/usage_accounting.py +23 -0
- package/runtime/python/okstra_ctl/ports/worker_dispatch.py +32 -0
- package/runtime/python/okstra_ctl/registry/__init__.py +13 -0
- package/runtime/python/okstra_ctl/registry/factory_loader.py +32 -0
- package/runtime/python/okstra_ctl/registry/host_discovery.py +124 -0
- package/runtime/python/okstra_ctl/registry/host_registry.py +365 -0
- package/runtime/python/okstra_ctl/registry/provider_registry.py +149 -0
- package/runtime/python/okstra_ctl/render.py +145 -47
- package/runtime/python/okstra_ctl/report_html/common.py +71 -25
- package/runtime/python/okstra_ctl/report_html/models.py +5 -0
- package/runtime/python/okstra_ctl/report_html/render.py +1 -1
- package/runtime/python/okstra_ctl/report_html/run_usage.py +19 -0
- package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +14 -0
- package/runtime/python/okstra_ctl/report_views.py +44 -16
- package/runtime/python/okstra_ctl/run.py +80 -58
- package/runtime/python/okstra_ctl/session.py +1 -1
- package/runtime/python/okstra_ctl/stage_citations.py +52 -15
- package/runtime/python/okstra_ctl/team.py +44 -32
- package/runtime/python/okstra_ctl/user_response.py +45 -29
- package/runtime/python/okstra_ctl/wizard.py +175 -73
- package/runtime/python/okstra_ctl/worker_audit_ledger.py +29 -4
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +10 -3
- package/runtime/python/okstra_ctl/worker_request.py +140 -0
- package/runtime/python/okstra_ctl/worker_runner.py +622 -0
- package/runtime/python/okstra_token_usage/collect.py +42 -7
- package/runtime/python/okstra_token_usage/report.py +42 -0
- package/runtime/python/okstra_token_usage/task_totals.py +88 -0
- package/runtime/schemas/final-report-v1.0.schema.json +4040 -1066
- package/runtime/schemas/final-report-v2.0.schema.json +5673 -1412
- package/runtime/skills/okstra-inspect/SKILL.md +1 -2
- package/runtime/skills/okstra-inspect/facets/logs.md +5 -5
- package/runtime/skills/okstra-inspect/facets/run-audit.md +3 -3
- package/runtime/skills/okstra-run/SKILL.md +74 -29
- package/runtime/skills/okstra-user-response/SKILL.md +15 -5
- package/runtime/templates/implementation-worker-preamble.md +1 -1
- package/runtime/templates/report-writer-prompt-preamble.md +1 -0
- package/runtime/templates/reports/final-report.template.md +3 -3
- package/runtime/templates/reports/html/assets/base.css +8 -4
- package/runtime/templates/reports/html/base.template.html +12 -6
- package/runtime/templates/reports/html/i18n/en.json +32 -7
- package/runtime/templates/reports/html/i18n/ko.json +32 -7
- package/runtime/templates/reports/html/macros/forms.html +9 -3
- package/runtime/templates/reports/html/tasks/implementation-planning.template.html +14 -19
- package/runtime/templates/reports/report.js +59 -26
- package/runtime/templates/reports/user-response.template.md +12 -8
- package/runtime/templates/worker-prompt-preamble.md +1 -1
- package/runtime/validators/validate-implementation-plan-stages.py +17 -22
- package/runtime/validators/validate-report-views.py +0 -39
- package/runtime/validators/validate-run.py +96 -14
- package/runtime/validators/validate_session_conformance.py +69 -3
- package/src/cli-registry.mjs +0 -7
- package/src/commands/execute/render-bundle.mjs +4 -4
- package/src/commands/execute/run.mjs +8 -25
- package/src/commands/execute/wizard.mjs +33 -13
- package/src/commands/lifecycle/doctor.mjs +10 -10
- package/src/commands/lifecycle/install.mjs +53 -30
- package/src/commands/lifecycle/preflight.mjs +14 -4
- package/src/lib/host-registry-client.mjs +176 -0
- package/src/lib/runtime-manifest.mjs +6 -8
- package/runtime/bin/okstra-wrapper-agy-stream.py +0 -61
- package/runtime/python/okstra_ctl/error_issue.py +0 -640
- package/runtime/python/okstra_ctl/issue_signals.py +0 -186
- package/runtime/python/okstra_ctl/lead_runtime.py +0 -115
- package/runtime/python/okstra_ctl/runner_resolution.py +0 -103
- package/runtime/skills/okstra-inspect/facets/error-issue.md +0 -77
- package/src/commands/inspect/error-issue.mjs +0 -27
- package/src/lib/runtime-readiness.mjs +0 -90
- 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
|
-
|
|
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-
|
|
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 "$@"
|