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,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
|
-
|
|
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 "$@"
|