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,121 +1,184 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Run an external LLM CLI with the shared okstra wrapper contract.
|
|
2
|
+
"""Run an external LLM CLI with the shared okstra wrapper contract.
|
|
3
|
+
|
|
4
|
+
Argument parsing and provider lookup only — the run itself belongs to
|
|
5
|
+
``okstra_ctl.worker_runner``, which every provider shares. What stays here is
|
|
6
|
+
the request the provider strategies then take on trust: resolved paths, the
|
|
7
|
+
write scope in the order the CLIs are told it, and the role's idle budget.
|
|
8
|
+
"""
|
|
3
9
|
from __future__ import annotations
|
|
4
10
|
|
|
5
|
-
import json
|
|
6
11
|
import os
|
|
7
|
-
import selectors
|
|
8
12
|
import shutil
|
|
9
|
-
import signal
|
|
10
|
-
import subprocess
|
|
11
13
|
import sys
|
|
12
|
-
import time
|
|
13
14
|
from dataclasses import dataclass
|
|
14
15
|
from pathlib import Path
|
|
15
|
-
from typing import
|
|
16
|
+
from typing import Any, Mapping
|
|
17
|
+
|
|
18
|
+
_HERE = Path(__file__).resolve().parent
|
|
19
|
+
# ``okstra_ctl`` sits beside this file in the repo (``scripts/``) but under
|
|
20
|
+
# ``~/.okstra/lib/python/`` once installed, and the four-line shell entrypoints
|
|
21
|
+
# that exec this script set no PYTHONPATH. Offer both, repo first.
|
|
22
|
+
_HOME_LIB = (
|
|
23
|
+
Path(os.environ.get("OKSTRA_HOME", str(Path.home() / ".okstra"))) / "lib" / "python"
|
|
24
|
+
)
|
|
25
|
+
sys.path.insert(0, str(_HERE))
|
|
26
|
+
if _HOME_LIB.is_dir() and str(_HOME_LIB) not in sys.path:
|
|
27
|
+
sys.path.append(str(_HOME_LIB))
|
|
28
|
+
|
|
29
|
+
from okstra_ctl.domain.provider import ProviderSpec, UnknownProviderError # noqa: E402
|
|
30
|
+
from okstra_ctl.domain.worker_exec import ( # noqa: E402
|
|
31
|
+
ExecutionStrategy,
|
|
32
|
+
WorkerExecRequest,
|
|
33
|
+
)
|
|
34
|
+
from okstra_ctl.registry.provider_registry import ( # noqa: E402
|
|
35
|
+
default_provider_registry,
|
|
36
|
+
)
|
|
37
|
+
from okstra_ctl.worker_request import build_request, idle_timeout # noqa: E402
|
|
38
|
+
from okstra_ctl.worker_runner import LIVE, QUIET, run_worker # noqa: E402
|
|
39
|
+
|
|
40
|
+
_USAGE = (
|
|
41
|
+
"usage: okstra-provider-exec.py <provider> <project-root> "
|
|
42
|
+
"<model-execution-value> <prompt-path> [worktree-path] [role] "
|
|
43
|
+
"[idle-timeout-seconds] [--presentation live|quiet]"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
_PRESENTATION_FLAG = "--presentation"
|
|
47
|
+
_PRESENTATIONS = (LIVE, QUIET)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class PreflightError(Exception):
|
|
51
|
+
def __init__(self, exit_code: int, message: str) -> None:
|
|
52
|
+
super().__init__(message)
|
|
53
|
+
self.exit_code = exit_code
|
|
16
54
|
|
|
17
55
|
|
|
18
56
|
@dataclass(frozen=True)
|
|
19
|
-
class
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
57
|
+
class Invocation:
|
|
58
|
+
strategy: ExecutionStrategy
|
|
59
|
+
request: WorkerExecRequest
|
|
60
|
+
presentation: str
|
|
61
|
+
log_path: Path
|
|
62
|
+
status_path: Path
|
|
63
|
+
status_extra: Mapping[str, Any]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def parse_invocation(argv: list[str]) -> Invocation:
|
|
67
|
+
"""Resolve the wrapper's positional contract into one runnable dispatch."""
|
|
68
|
+
positional, presentation = _take_presentation(argv)
|
|
69
|
+
if not 4 <= len(positional) <= 7:
|
|
70
|
+
raise PreflightError(64, _USAGE)
|
|
71
|
+
provider_id, project_root_raw, model, prompt_raw = positional[:4]
|
|
72
|
+
worktree_raw = positional[4] if len(positional) >= 5 else ""
|
|
73
|
+
role = positional[5] if len(positional) >= 6 and positional[5] else "worker"
|
|
74
|
+
timeout_raw = positional[6] if len(positional) >= 7 and positional[6] else ""
|
|
75
|
+
|
|
76
|
+
spec = _provider_spec(provider_id)
|
|
77
|
+
project_root = _existing_dir(project_root_raw, 65, "project-root")
|
|
78
|
+
if not model:
|
|
79
|
+
raise PreflightError(66, "model-execution-value is empty")
|
|
80
|
+
prompt_path = _existing_file(prompt_raw, 67, "prompt-path")
|
|
81
|
+
idle_timeout_seconds = _idle_timeout(timeout_raw, role)
|
|
82
|
+
worktree = (
|
|
83
|
+
_existing_dir(worktree_raw, 68, "worktree-path") if worktree_raw else None
|
|
84
|
+
)
|
|
23
85
|
|
|
86
|
+
request = build_request(
|
|
87
|
+
prompt_text=prompt_path.read_text(encoding="utf-8"),
|
|
88
|
+
model=model,
|
|
89
|
+
project_root=project_root,
|
|
90
|
+
worktree_path=worktree,
|
|
91
|
+
role=role,
|
|
92
|
+
idle_timeout_seconds=idle_timeout_seconds,
|
|
93
|
+
)
|
|
94
|
+
strategy = spec.exec_strategy
|
|
95
|
+
_check_command(strategy, request)
|
|
96
|
+
return Invocation(
|
|
97
|
+
strategy=strategy,
|
|
98
|
+
request=request,
|
|
99
|
+
presentation=presentation,
|
|
100
|
+
log_path=_log_path(prompt_path),
|
|
101
|
+
status_path=Path(f"{prompt_path}.status.json"),
|
|
102
|
+
status_extra={"wrapper": spec.wrapper, "role": role},
|
|
103
|
+
)
|
|
24
104
|
|
|
25
|
-
def _grok_args(prompt: str, model: str, cwd: str) -> list[str]:
|
|
26
|
-
return [
|
|
27
|
-
"grok",
|
|
28
|
-
"-p",
|
|
29
|
-
prompt,
|
|
30
|
-
"-m",
|
|
31
|
-
model,
|
|
32
|
-
"--output-format",
|
|
33
|
-
"streaming-json",
|
|
34
|
-
"--cwd",
|
|
35
|
-
cwd,
|
|
36
|
-
]
|
|
37
105
|
|
|
106
|
+
def _take_presentation(argv: list[str]) -> tuple[list[str], str]:
|
|
107
|
+
"""Split the one flag out of an otherwise positional argv.
|
|
108
|
+
|
|
109
|
+
Defaults to ``quiet``. ``live`` is only ever right where a screen was
|
|
110
|
+
declared, and the only callers that can declare one are the pane backends —
|
|
111
|
+
which pass the flag explicitly. Defaulting the other way assumed a screen
|
|
112
|
+
that a subagent dispatch does not have, and sent every worker's progress
|
|
113
|
+
into its caller's context window instead.
|
|
114
|
+
"""
|
|
115
|
+
positional: list[str] = []
|
|
116
|
+
presentation = QUIET
|
|
117
|
+
index = 0
|
|
118
|
+
while index < len(argv):
|
|
119
|
+
if argv[index] != _PRESENTATION_FLAG:
|
|
120
|
+
positional.append(argv[index])
|
|
121
|
+
index += 1
|
|
122
|
+
continue
|
|
123
|
+
if index + 1 >= len(argv):
|
|
124
|
+
raise PreflightError(64, f"{_PRESENTATION_FLAG} needs a value: {_USAGE}")
|
|
125
|
+
presentation = argv[index + 1]
|
|
126
|
+
index += 2
|
|
127
|
+
if presentation not in _PRESENTATIONS:
|
|
128
|
+
allowed = " | ".join(_PRESENTATIONS)
|
|
129
|
+
raise PreflightError(
|
|
130
|
+
64, f"unsupported presentation {presentation!r}. Allowed values: {allowed}"
|
|
131
|
+
)
|
|
132
|
+
return positional, presentation
|
|
38
133
|
|
|
39
|
-
def _kimi_args(prompt: str, model: str, _cwd: str) -> list[str]:
|
|
40
|
-
return ["kimi", "-p", prompt, "-m", model, "--output-format", "stream-json"]
|
|
41
134
|
|
|
135
|
+
def _provider_spec(provider_id: str) -> ProviderSpec:
|
|
136
|
+
try:
|
|
137
|
+
spec = default_provider_registry().resolve(provider_id)
|
|
138
|
+
except UnknownProviderError as exc:
|
|
139
|
+
raise PreflightError(64, str(exc)) from exc
|
|
140
|
+
if spec.exec_strategy is None:
|
|
141
|
+
raise PreflightError(
|
|
142
|
+
64, f"provider {provider_id!r} has no execution strategy to run"
|
|
143
|
+
)
|
|
144
|
+
return spec
|
|
42
145
|
|
|
43
|
-
PROVIDERS = {
|
|
44
|
-
"grok": ProviderCommand("grok", "okstra-grok-exec.sh", _grok_args),
|
|
45
|
-
"kimi": ProviderCommand("kimi", "okstra-kimi-exec.sh", _kimi_args),
|
|
46
|
-
}
|
|
47
146
|
|
|
147
|
+
def _existing_dir(raw: str, exit_code: int, label: str) -> Path:
|
|
148
|
+
"""Existence only — `build_request` owns the resolving."""
|
|
149
|
+
path = Path(raw) if raw else None
|
|
150
|
+
if path is None or not path.is_dir():
|
|
151
|
+
raise PreflightError(
|
|
152
|
+
exit_code, f"{label} is missing or not a directory: {raw!r}"
|
|
153
|
+
)
|
|
154
|
+
return path
|
|
48
155
|
|
|
49
|
-
@dataclass(frozen=True)
|
|
50
|
-
class Invocation:
|
|
51
|
-
provider: ProviderCommand
|
|
52
|
-
project_root: Path
|
|
53
|
-
model: str
|
|
54
|
-
prompt_path: Path
|
|
55
|
-
execution_root: Path
|
|
56
|
-
role: str
|
|
57
|
-
idle_timeout_seconds: int
|
|
58
156
|
|
|
157
|
+
def _existing_file(raw: str, exit_code: int, label: str) -> Path:
|
|
158
|
+
path = Path(raw) if raw else None
|
|
159
|
+
if path is None or not path.is_file():
|
|
160
|
+
raise PreflightError(exit_code, f"{label} is missing or not a file: {raw!r}")
|
|
161
|
+
return path.resolve()
|
|
59
162
|
|
|
60
|
-
class PreflightError(Exception):
|
|
61
|
-
def __init__(self, exit_code: int, message: str) -> None:
|
|
62
|
-
super().__init__(message)
|
|
63
|
-
self.exit_code = exit_code
|
|
64
163
|
|
|
164
|
+
def _idle_timeout(raw: str, role: str) -> int:
|
|
165
|
+
try:
|
|
166
|
+
return idle_timeout(raw, role)
|
|
167
|
+
except ValueError as exc:
|
|
168
|
+
raise PreflightError(69, str(exc)) from exc
|
|
65
169
|
|
|
66
|
-
def _parse_invocation(argv: list[str]) -> Invocation:
|
|
67
|
-
if len(argv) < 4 or len(argv) > 7:
|
|
68
|
-
raise PreflightError(
|
|
69
|
-
64,
|
|
70
|
-
"usage: okstra-provider-exec.py <provider> <project-root> <model-execution-value> "
|
|
71
|
-
"<prompt-path> [worktree-path] [role] [idle-timeout-seconds]",
|
|
72
|
-
)
|
|
73
|
-
provider_id, project_root_raw, model, prompt_raw = argv[:4]
|
|
74
|
-
provider = PROVIDERS.get(provider_id)
|
|
75
|
-
if provider is None:
|
|
76
|
-
raise PreflightError(64, f"unsupported provider: {provider_id}")
|
|
77
|
-
worktree_raw = argv[4] if len(argv) >= 5 else ""
|
|
78
|
-
role = argv[5] if len(argv) >= 6 and argv[5] else "worker"
|
|
79
|
-
default_timeout = 1500 if role in {"executor", "verifier"} else 600
|
|
80
|
-
timeout_raw = argv[6] if len(argv) >= 7 else str(default_timeout)
|
|
81
|
-
return _validate_invocation(
|
|
82
|
-
provider, project_root_raw, model, prompt_raw, worktree_raw, role, timeout_raw
|
|
83
|
-
)
|
|
84
170
|
|
|
171
|
+
def _check_command(strategy: ExecutionStrategy, request: WorkerExecRequest) -> None:
|
|
172
|
+
"""Refuse a missing CLI before the run leaves any artifact behind.
|
|
85
173
|
|
|
86
|
-
|
|
87
|
-
provider
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
) -> Invocation:
|
|
95
|
-
project_root = Path(project_root_raw)
|
|
96
|
-
prompt_path = Path(prompt_raw)
|
|
97
|
-
if not project_root_raw or not project_root.is_dir():
|
|
98
|
-
raise PreflightError(65, f"project-root is missing or not a directory: {project_root_raw!r}")
|
|
99
|
-
if not model:
|
|
100
|
-
raise PreflightError(66, "model-execution-value is empty")
|
|
101
|
-
if not prompt_raw or not prompt_path.is_file():
|
|
102
|
-
raise PreflightError(67, f"prompt-path is missing or not a file: {prompt_raw!r}")
|
|
103
|
-
if not timeout_raw.isdigit():
|
|
104
|
-
raise PreflightError(69, f"idle-timeout-seconds must be a non-negative integer: {timeout_raw!r}")
|
|
105
|
-
execution_root = Path(worktree_raw) if worktree_raw else project_root
|
|
106
|
-
if worktree_raw and not execution_root.is_dir():
|
|
107
|
-
raise PreflightError(68, f"worktree-path was provided but is not a directory: {worktree_raw!r}")
|
|
108
|
-
if shutil.which(provider.binary) is None:
|
|
109
|
-
raise PreflightError(127, f"{provider.binary} CLI is not installed on PATH")
|
|
110
|
-
return Invocation(
|
|
111
|
-
provider=provider,
|
|
112
|
-
project_root=project_root.resolve(),
|
|
113
|
-
model=model,
|
|
114
|
-
prompt_path=prompt_path.resolve(),
|
|
115
|
-
execution_root=execution_root.resolve(),
|
|
116
|
-
role=role,
|
|
117
|
-
idle_timeout_seconds=int(timeout_raw),
|
|
118
|
-
)
|
|
174
|
+
Building the command is the only truthful way to learn which binary this
|
|
175
|
+
provider runs, and it is a pure call. The check stays out of the runner so a
|
|
176
|
+
refused dispatch leaves no `started` status sidecar for the liveness probe to
|
|
177
|
+
read as a worker that launched.
|
|
178
|
+
"""
|
|
179
|
+
binary = strategy.build_command(request).argv[0]
|
|
180
|
+
if shutil.which(binary) is None:
|
|
181
|
+
raise PreflightError(127, f"{binary} CLI is not installed on PATH")
|
|
119
182
|
|
|
120
183
|
|
|
121
184
|
def _log_path(prompt_path: Path) -> Path:
|
|
@@ -124,105 +187,17 @@ def _log_path(prompt_path: Path) -> Path:
|
|
|
124
187
|
return Path(f"{prompt_path}.log")
|
|
125
188
|
|
|
126
189
|
|
|
127
|
-
def _write_status(path: Path, status: dict[str, object]) -> None:
|
|
128
|
-
temporary = Path(f"{path}.tmp")
|
|
129
|
-
temporary.write_text(json.dumps(status, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
130
|
-
os.replace(temporary, path)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def _terminate_process(process: subprocess.Popen[bytes]) -> None:
|
|
134
|
-
try:
|
|
135
|
-
os.killpg(process.pid, signal.SIGTERM)
|
|
136
|
-
except ProcessLookupError:
|
|
137
|
-
return
|
|
138
|
-
try:
|
|
139
|
-
process.wait(timeout=5)
|
|
140
|
-
except subprocess.TimeoutExpired:
|
|
141
|
-
try:
|
|
142
|
-
os.killpg(process.pid, signal.SIGKILL)
|
|
143
|
-
except ProcessLookupError:
|
|
144
|
-
pass
|
|
145
|
-
process.wait()
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
def _stream_process(
|
|
149
|
-
process: subprocess.Popen[bytes], log_file, idle_timeout_seconds: int
|
|
150
|
-
) -> tuple[int, bool, int]:
|
|
151
|
-
selector = selectors.DefaultSelector()
|
|
152
|
-
assert process.stdout is not None
|
|
153
|
-
selector.register(process.stdout, selectors.EVENT_READ)
|
|
154
|
-
last_output = time.monotonic()
|
|
155
|
-
timed_out = False
|
|
156
|
-
idle_seconds = 0
|
|
157
|
-
while selector.get_map():
|
|
158
|
-
for key, _ in selector.select(timeout=0.25):
|
|
159
|
-
chunk = os.read(key.fd, 8192)
|
|
160
|
-
if not chunk:
|
|
161
|
-
selector.unregister(key.fileobj)
|
|
162
|
-
continue
|
|
163
|
-
last_output = time.monotonic()
|
|
164
|
-
sys.stdout.buffer.write(chunk)
|
|
165
|
-
sys.stdout.buffer.flush()
|
|
166
|
-
log_file.write(chunk)
|
|
167
|
-
log_file.flush()
|
|
168
|
-
idle_seconds = int(time.monotonic() - last_output)
|
|
169
|
-
if idle_timeout_seconds and idle_seconds >= idle_timeout_seconds and process.poll() is None:
|
|
170
|
-
timed_out = True
|
|
171
|
-
_terminate_process(process)
|
|
172
|
-
exit_code = process.wait()
|
|
173
|
-
return (124 if timed_out else exit_code), timed_out, idle_seconds
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
def _run(invocation: Invocation) -> int:
|
|
177
|
-
prompt = invocation.prompt_path.read_text(encoding="utf-8")
|
|
178
|
-
command = invocation.provider.build_args(prompt, invocation.model, str(invocation.execution_root))
|
|
179
|
-
status_path = Path(f"{invocation.prompt_path}.status.json")
|
|
180
|
-
log_path = _log_path(invocation.prompt_path)
|
|
181
|
-
started_ts = int(time.time())
|
|
182
|
-
started_monotonic = time.monotonic()
|
|
183
|
-
status: dict[str, object] = {
|
|
184
|
-
"schemaVersion": 1,
|
|
185
|
-
"wrapper": invocation.provider.wrapper,
|
|
186
|
-
"role": invocation.role,
|
|
187
|
-
"pid": os.getpid(),
|
|
188
|
-
"started_ts": started_ts,
|
|
189
|
-
"log_path": str(log_path),
|
|
190
|
-
"stage": "started",
|
|
191
|
-
}
|
|
192
|
-
_write_status(status_path, status)
|
|
193
|
-
with log_path.open("wb") as log_file:
|
|
194
|
-
process = subprocess.Popen(
|
|
195
|
-
command,
|
|
196
|
-
cwd=invocation.execution_root,
|
|
197
|
-
stdout=subprocess.PIPE,
|
|
198
|
-
stderr=subprocess.STDOUT,
|
|
199
|
-
start_new_session=True,
|
|
200
|
-
)
|
|
201
|
-
exit_code, timed_out, idle_seconds = _stream_process(
|
|
202
|
-
process, log_file, invocation.idle_timeout_seconds
|
|
203
|
-
)
|
|
204
|
-
ended_ts = int(time.time())
|
|
205
|
-
status.update(
|
|
206
|
-
stage="exited",
|
|
207
|
-
exit_code=exit_code,
|
|
208
|
-
ended_ts=ended_ts,
|
|
209
|
-
duration_ms=int((time.monotonic() - started_monotonic) * 1000),
|
|
210
|
-
)
|
|
211
|
-
if timed_out:
|
|
212
|
-
status.update(
|
|
213
|
-
timeout=True,
|
|
214
|
-
idle_at_ts=ended_ts,
|
|
215
|
-
idle_seconds=idle_seconds,
|
|
216
|
-
terminated_by="idle-watchdog",
|
|
217
|
-
)
|
|
218
|
-
_write_status(status_path, status)
|
|
219
|
-
return exit_code
|
|
220
|
-
|
|
221
|
-
|
|
222
190
|
def main(argv: list[str]) -> int:
|
|
223
191
|
try:
|
|
224
|
-
invocation =
|
|
225
|
-
return
|
|
192
|
+
invocation = parse_invocation(argv[1:])
|
|
193
|
+
return run_worker(
|
|
194
|
+
invocation.strategy,
|
|
195
|
+
invocation.request,
|
|
196
|
+
presentation=invocation.presentation,
|
|
197
|
+
log_path=invocation.log_path,
|
|
198
|
+
status_path=invocation.status_path,
|
|
199
|
+
status_extra=invocation.status_extra,
|
|
200
|
+
)
|
|
226
201
|
except PreflightError as exc:
|
|
227
202
|
print(f"okstra-provider-exec: {exc}", file=sys.stderr)
|
|
228
203
|
return exc.exit_code
|
|
@@ -2,13 +2,20 @@
|
|
|
2
2
|
#
|
|
3
3
|
# okstra-trace-cleanup.sh — close tmux panes created during okstra runs.
|
|
4
4
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
5
|
+
# Worker-compute panes are tmux-pane backend siblings. Their dispatcher tags
|
|
6
|
+
# each pane it owns with a pane-level user option (`@okstra_worker_run=<RUN_DIR>`),
|
|
7
|
+
# so panes are found server-wide by tag — no tmux env var or pane-id registry is
|
|
8
|
+
# needed, and the run-scoped tag keeps concurrent okstra runs from closing each
|
|
9
|
+
# other's panes.
|
|
10
|
+
#
|
|
11
|
+
# Trace panes were `tail -F` siblings the provider wrappers split, tagged
|
|
12
|
+
# `@okstra_trace_run` / `@okstra_status`. Those wrappers are now four-line
|
|
13
|
+
# entrypoints and worker progress renders into the worker's own pane, so
|
|
14
|
+
# NOTHING SPAWNS A TRACE PANE and neither tag has a writer left. The trace
|
|
15
|
+
# paths below still run and simply match nothing; `--reclaim-completed`, which
|
|
16
|
+
# keys on `@okstra_status`, is inert for the same reason. Kept rather than
|
|
17
|
+
# deleted because the hooks that call this script are already seeded on user
|
|
18
|
+
# machines — retiring the trace machinery is a deliberate follow-up.
|
|
12
19
|
#
|
|
13
20
|
# Two invocation shapes:
|
|
14
21
|
#
|
|
@@ -1,27 +1,34 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""okstra-wrapper-status.py —
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
`
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
2
|
+
"""okstra-wrapper-status.py — standalone CLI for the worker status sidecar.
|
|
3
|
+
|
|
4
|
+
A CLI worker dispatch runs a long-running CLI in the background, and the
|
|
5
|
+
polling channel that watches it carries only stdout plus a binary
|
|
6
|
+
`running`/`completed` state. Several recovery decisions need more —
|
|
7
|
+
specifically, "did this worker start at all, when, and how did it finish?" — so
|
|
8
|
+
a small JSON sidecar is written at `<prompt-path>.status.json` that survives
|
|
9
|
+
independent of the polling channel.
|
|
10
|
+
|
|
11
|
+
**Nothing calls this script.** Every provider entrypoint now runs through
|
|
12
|
+
`scripts/okstra_ctl/worker_runner.py`, which writes the same document
|
|
13
|
+
in-process; the shell wrappers that shelled out to this file were its only
|
|
14
|
+
callers and they are gone. It is kept for now rather than deleted because
|
|
15
|
+
removing it also means touching the build sync list, the install payload, its
|
|
16
|
+
own test module and the docs that name it — a deliberate follow-up, not a side
|
|
17
|
+
effect of the wrapper migration. `scripts/okstra_ctl/wrapper_status.py` is the
|
|
18
|
+
reader, and what it reads is what the runner writes.
|
|
19
|
+
|
|
20
|
+
Consumers of the sidecar:
|
|
21
|
+
|
|
22
|
+
* A CLI worker's diagnostic step: read `log_path` to capture a tail when
|
|
23
|
+
`exit_code == 0` but the canonical Result file is absent.
|
|
24
|
+
* Lead: cross-check `started_ts` / `ended_ts` to distinguish "worker hung
|
|
18
25
|
before CLI launched" from "CLI finished but never wrote artifact" when
|
|
19
26
|
applying the redispatch policy (see team-contract "Lead Redispatch
|
|
20
27
|
Policy on Result-Missing").
|
|
21
28
|
|
|
22
|
-
Failures are deliberately non-fatal for the caller — the
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
Failures are deliberately non-fatal for the caller — the caller's main job is
|
|
30
|
+
to run the underlying CLI; a missing sidecar must not break that. On any error
|
|
31
|
+
the script prints a one-line diagnostic to stderr and exits 0.
|
|
25
32
|
|
|
26
33
|
Schema (schemaVersion 1):
|
|
27
34
|
|
package/runtime/bin/okstra.sh
CHANGED
|
@@ -58,10 +58,87 @@ okstra_py() {
|
|
|
58
58
|
PYTHONPATH="$OKSTRA_PYTHONPATH:${PYTHONPATH-}" python3 "$@"
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
confirm_sandbox_waiver() {
|
|
62
|
+
local sandbox_note="$1"
|
|
63
|
+
local lead_executable="$2"
|
|
64
|
+
local cancel_message="$3"
|
|
65
|
+
local assume_yes_message="$4"
|
|
66
|
+
local sandbox_answer
|
|
67
|
+
|
|
68
|
+
[[ -z "$sandbox_note" ]] && return
|
|
69
|
+
printf '\n%s\n' "$sandbox_note" >&2
|
|
70
|
+
if [[ "$ASSUME_YES" == "true" ]]; then
|
|
71
|
+
[[ -n "$assume_yes_message" ]] && printf '%s\n\n' "$assume_yes_message" >&2
|
|
72
|
+
return 0
|
|
73
|
+
fi
|
|
74
|
+
printf 'Start %s without its sandbox? [y/N] ' "$lead_executable" >&2
|
|
75
|
+
read -r sandbox_answer < /dev/tty || sandbox_answer=""
|
|
76
|
+
if [[ "$sandbox_answer" != "y" && "$sandbox_answer" != "Y" ]]; then
|
|
77
|
+
printf '%s\n' "$cancel_message" >&2
|
|
78
|
+
exit 1
|
|
79
|
+
fi
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
execute_launch_plan() {
|
|
83
|
+
local cancel_message="$1"
|
|
84
|
+
local assume_yes_message="$2"
|
|
85
|
+
shift 2
|
|
86
|
+
local launch_plan_file
|
|
87
|
+
local sandbox_note
|
|
88
|
+
local env_count
|
|
89
|
+
local argv_offset
|
|
90
|
+
local lead_executable
|
|
91
|
+
local field
|
|
92
|
+
local -a launch_fields
|
|
93
|
+
local -a launch_env
|
|
94
|
+
local -a launch_argv
|
|
95
|
+
|
|
96
|
+
launch_plan_file="$(mktemp "${TMPDIR:-/tmp}/okstra-launch-plan.XXXXXX")"
|
|
97
|
+
if ! okstra_py -m okstra_ctl.entrypoints.hosts "$@" \
|
|
98
|
+
--format null > "$launch_plan_file"; then
|
|
99
|
+
rm -f "$launch_plan_file"
|
|
100
|
+
exit 1
|
|
101
|
+
fi
|
|
102
|
+
launch_fields=()
|
|
103
|
+
while IFS= read -r -d '' field; do
|
|
104
|
+
launch_fields+=("$field")
|
|
105
|
+
done < "$launch_plan_file"
|
|
106
|
+
rm -f "$launch_plan_file"
|
|
107
|
+
if [[ ${#launch_fields[@]} -lt 4 ]] || \
|
|
108
|
+
[[ ! "${launch_fields[2]}" =~ ^[0-9]+$ ]]; then
|
|
109
|
+
printf 'okstra: invalid launch plan\n' >&2
|
|
110
|
+
exit 1
|
|
111
|
+
fi
|
|
112
|
+
sandbox_note="${launch_fields[1]}"
|
|
113
|
+
env_count="${launch_fields[2]}"
|
|
114
|
+
argv_offset=$((env_count + 3))
|
|
115
|
+
if [[ ${#launch_fields[@]} -le $argv_offset ]]; then
|
|
116
|
+
printf 'okstra: invalid launch plan\n' >&2
|
|
117
|
+
exit 1
|
|
118
|
+
fi
|
|
119
|
+
launch_env=("${launch_fields[@]:3:env_count}")
|
|
120
|
+
launch_argv=("${launch_fields[@]:argv_offset}")
|
|
121
|
+
lead_executable="${launch_argv[0]}"
|
|
122
|
+
confirm_sandbox_waiver \
|
|
123
|
+
"$sandbox_note" "$lead_executable" "$cancel_message" "$assume_yes_message"
|
|
124
|
+
cd "${launch_fields[0]}"
|
|
125
|
+
exec env "${launch_env[@]}" "${launch_argv[@]}"
|
|
126
|
+
}
|
|
127
|
+
|
|
61
128
|
parse_cli_arguments "$@"
|
|
62
129
|
split_task_key
|
|
63
130
|
|
|
64
131
|
PROJECT_ROOT="$(resolve_project_root_safe "$PROJECT_ROOT_OVERRIDE")"
|
|
132
|
+
if [[ "$RESUME_SESSION_MODE" == "true" ]]; then
|
|
133
|
+
if [[ -z "$PROJECT_ROOT" ]]; then
|
|
134
|
+
PROJECT_ROOT="$(resolve_project_root_strict "$PROJECT_ROOT_OVERRIDE")" || exit 1
|
|
135
|
+
fi
|
|
136
|
+
execute_launch_plan \
|
|
137
|
+
"okstra: resume cancelled" \
|
|
138
|
+
"" \
|
|
139
|
+
resume-plan --host "$LEAD_RUNTIME" --entry-mode spawn-process \
|
|
140
|
+
--project-root "$PROJECT_ROOT" --session-id "$RESUME_SESSION_ID"
|
|
141
|
+
fi
|
|
65
142
|
if [[ "$RESUME_CLARIFICATION_MODE" == "true" ]]; then
|
|
66
143
|
if [[ -z "$PROJECT_ROOT" ]]; then
|
|
67
144
|
PROJECT_ROOT="$(resolve_project_root_strict "$PROJECT_ROOT_OVERRIDE")" \
|
|
@@ -80,47 +157,11 @@ if [[ "$LAUNCH_ONLY" == "true" ]]; then
|
|
|
80
157
|
if [[ -z "$PROJECT_ROOT" ]]; then
|
|
81
158
|
PROJECT_ROOT="$(resolve_project_root_strict "$PROJECT_ROOT_OVERRIDE")" || exit 1
|
|
82
159
|
fi
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
provider = lead_runtime_info(sys.argv[1]).agent
|
|
89
|
-
print(bare_lead_launch_argv(provider)[0], lead_launch_spec(provider).sandbox_waiver_note)
|
|
90
|
-
PY
|
|
91
|
-
)
|
|
92
|
-
if ! command -v "$LEAD_EXECUTABLE" >/dev/null 2>&1; then
|
|
93
|
-
printf '%s command not found\n' "$LEAD_EXECUTABLE" >&2
|
|
94
|
-
exit 1
|
|
95
|
-
fi
|
|
96
|
-
if [[ -n "$SANDBOX_NOTE" ]]; then
|
|
97
|
-
okstra_py - "$LEAD_RUNTIME" <<'PY' >&2
|
|
98
|
-
import sys
|
|
99
|
-
from okstra_ctl.lead_runtime import lead_runtime_info
|
|
100
|
-
from okstra_ctl.models import lead_launch_spec
|
|
101
|
-
print("\n" + lead_launch_spec(lead_runtime_info(sys.argv[1]).agent).sandbox_waiver_note)
|
|
102
|
-
PY
|
|
103
|
-
if [[ "$ASSUME_YES" != "true" ]]; then
|
|
104
|
-
printf 'Start %s without its sandbox? [y/N] ' "$LEAD_EXECUTABLE" >&2
|
|
105
|
-
read -r _sandbox_answer < /dev/tty || _sandbox_answer=""
|
|
106
|
-
if [[ "$_sandbox_answer" != "y" && "$_sandbox_answer" != "Y" ]]; then
|
|
107
|
-
printf 'okstra: cancelled\n' >&2
|
|
108
|
-
exit 1
|
|
109
|
-
fi
|
|
110
|
-
fi
|
|
111
|
-
fi
|
|
112
|
-
LAUNCH_ARGV=()
|
|
113
|
-
while IFS= read -r -d '' _arg; do LAUNCH_ARGV+=("$_arg"); done < <(
|
|
114
|
-
okstra_py - "$LEAD_RUNTIME" <<'PY'
|
|
115
|
-
import sys
|
|
116
|
-
from okstra_ctl.lead_runtime import lead_runtime_info
|
|
117
|
-
from okstra_ctl.models import bare_lead_launch_argv
|
|
118
|
-
for a in bare_lead_launch_argv(lead_runtime_info(sys.argv[1]).agent):
|
|
119
|
-
sys.stdout.write(a + "\0")
|
|
120
|
-
PY
|
|
121
|
-
)
|
|
122
|
-
cd "$PROJECT_ROOT"
|
|
123
|
-
exec "${LAUNCH_ARGV[@]}"
|
|
160
|
+
execute_launch_plan \
|
|
161
|
+
"okstra: cancelled" \
|
|
162
|
+
"" \
|
|
163
|
+
launch-plan --host "$LEAD_RUNTIME" --entry-mode spawn-process \
|
|
164
|
+
--project-root "$PROJECT_ROOT"
|
|
124
165
|
fi
|
|
125
166
|
|
|
126
167
|
autofill_from_manifest
|
|
@@ -232,57 +273,12 @@ if [[ -z "$LAUNCH_JSON" ]]; then
|
|
|
232
273
|
exit 1
|
|
233
274
|
fi
|
|
234
275
|
|
|
235
|
-
read -r LEAD_EXECUTABLE PROJECT_ROOT_FROM_PY < <(
|
|
236
|
-
okstra_py - "$LAUNCH_JSON" <<'PY'
|
|
237
|
-
import json, sys
|
|
238
|
-
d = json.loads(sys.argv[1])
|
|
239
|
-
print(d["leadExecutable"], d["projectRoot"])
|
|
240
|
-
PY
|
|
241
|
-
)
|
|
242
|
-
|
|
243
|
-
if ! command -v "$LEAD_EXECUTABLE" >/dev/null 2>&1; then
|
|
244
|
-
printf '%s command not found\n' "$LEAD_EXECUTABLE" >&2
|
|
245
|
-
exit 1
|
|
246
|
-
fi
|
|
247
|
-
|
|
248
|
-
# Starting some leads means lowering a protection that CLI applies to itself.
|
|
249
|
-
# okstra never waives it silently: the note says what is given up, and nothing
|
|
250
|
-
# starts without an explicit yes.
|
|
251
|
-
SANDBOX_NOTE="$(
|
|
252
|
-
okstra_py - "$LAUNCH_JSON" <<'PY'
|
|
253
|
-
import json, sys
|
|
254
|
-
print(json.loads(sys.argv[1])["sandboxWaiverNote"])
|
|
255
|
-
PY
|
|
256
|
-
)"
|
|
257
|
-
if [[ -n "$SANDBOX_NOTE" ]]; then
|
|
258
|
-
printf '\n%s\n' "$SANDBOX_NOTE" >&2
|
|
259
|
-
if [[ "$ASSUME_YES" == "true" ]]; then
|
|
260
|
-
printf 'Proceeding without the sandbox (--yes).\n\n' >&2
|
|
261
|
-
else
|
|
262
|
-
printf 'Start %s without its sandbox? [y/N] ' "$LEAD_EXECUTABLE" >&2
|
|
263
|
-
read -r SANDBOX_ANSWER < /dev/tty || SANDBOX_ANSWER=""
|
|
264
|
-
if [[ "$SANDBOX_ANSWER" != "y" && "$SANDBOX_ANSWER" != "Y" ]]; then
|
|
265
|
-
printf 'okstra: cancelled. The task bundle is prepared; rerun to resume it.\n' >&2
|
|
266
|
-
exit 1
|
|
267
|
-
fi
|
|
268
|
-
fi
|
|
269
|
-
fi
|
|
270
|
-
|
|
271
|
-
# NUL-separated so a prompt carrying newlines or quotes survives the handoff.
|
|
272
|
-
LAUNCH_ARGV=()
|
|
273
|
-
while IFS= read -r -d '' LAUNCH_ARG; do
|
|
274
|
-
LAUNCH_ARGV+=("$LAUNCH_ARG")
|
|
275
|
-
done < <(
|
|
276
|
-
okstra_py - "$LAUNCH_JSON" <<'PY'
|
|
277
|
-
import json, sys
|
|
278
|
-
for arg in json.loads(sys.argv[1])["launchArgv"]:
|
|
279
|
-
sys.stdout.write(arg + "\0")
|
|
280
|
-
PY
|
|
281
|
-
)
|
|
282
|
-
|
|
283
276
|
# Note: per-session --settings injection was removed. okstra-ctl prepare
|
|
284
277
|
# provisions <PROJECT_ROOT>/.claude/settings.local.json as a symlink to
|
|
285
278
|
# ~/.okstra/templates/settings.local.json, which Claude Code auto-loads
|
|
286
279
|
# whenever it runs inside that project — no CLI flag required.
|
|
287
|
-
|
|
288
|
-
|
|
280
|
+
execute_launch_plan \
|
|
281
|
+
"okstra: cancelled. The task bundle is prepared; rerun to resume it." \
|
|
282
|
+
"Proceeding without the sandbox (--yes)." \
|
|
283
|
+
launch-plan --host "$LEAD_RUNTIME" --entry-mode spawn-process \
|
|
284
|
+
--prepared-launch-json "$LAUNCH_JSON"
|