okstra 0.1.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 +36 -0
- package/bin/okstra +62 -0
- package/package.json +30 -0
- package/runtime/.gitkeep +0 -0
- package/runtime/BUILD.json +5 -0
- package/runtime/agents/SKILL.md +243 -0
- package/runtime/agents/TODO.md +168 -0
- package/runtime/agents/workers/claude-worker.md +106 -0
- package/runtime/agents/workers/codex-worker.md +179 -0
- package/runtime/agents/workers/gemini-worker.md +179 -0
- package/runtime/agents/workers/report-writer-worker.md +116 -0
- package/runtime/bin/okstra-central.sh +152 -0
- package/runtime/bin/okstra-codex-exec.sh +53 -0
- package/runtime/bin/okstra-error-log.py +295 -0
- package/runtime/bin/okstra-gemini-exec.sh +55 -0
- package/runtime/bin/okstra-token-usage.py +46 -0
- package/runtime/bin/okstra.sh +162 -0
- package/runtime/prompts/launch.template.md +52 -0
- package/runtime/prompts/profiles/error-analysis.md +43 -0
- package/runtime/prompts/profiles/final-verification.md +37 -0
- package/runtime/prompts/profiles/implementation-planning.md +85 -0
- package/runtime/prompts/profiles/implementation.md +71 -0
- package/runtime/prompts/profiles/requirements-discovery.md +43 -0
- package/runtime/python/lib/okstra/cli.sh +227 -0
- package/runtime/python/lib/okstra/globals.sh +157 -0
- package/runtime/python/lib/okstra/interactive.sh +411 -0
- package/runtime/python/lib/okstra/project-resolver.sh +57 -0
- package/runtime/python/lib/okstra/usage.sh +98 -0
- package/runtime/python/lib/okstra-ctl/cmd-batch.sh +59 -0
- package/runtime/python/lib/okstra-ctl/cmd-list.sh +35 -0
- package/runtime/python/lib/okstra-ctl/cmd-open.sh +36 -0
- package/runtime/python/lib/okstra-ctl/cmd-projects.sh +26 -0
- package/runtime/python/lib/okstra-ctl/cmd-reconcile.sh +27 -0
- package/runtime/python/lib/okstra-ctl/cmd-reindex.sh +38 -0
- package/runtime/python/lib/okstra-ctl/cmd-rerun.sh +326 -0
- package/runtime/python/lib/okstra-ctl/cmd-show.sh +27 -0
- package/runtime/python/lib/okstra-ctl/cmd-tail.sh +76 -0
- package/runtime/python/lib/okstra-ctl/main.sh +41 -0
- package/runtime/python/lib/okstra-ctl/prepare.sh +29 -0
- package/runtime/python/lib/okstra-ctl/usage.sh +23 -0
- package/runtime/python/okstra_ctl/__init__.py +125 -0
- package/runtime/python/okstra_ctl/backfill.py +253 -0
- package/runtime/python/okstra_ctl/batch.py +62 -0
- package/runtime/python/okstra_ctl/ids.py +84 -0
- package/runtime/python/okstra_ctl/index.py +216 -0
- package/runtime/python/okstra_ctl/invocation.py +49 -0
- package/runtime/python/okstra_ctl/jsonl.py +84 -0
- package/runtime/python/okstra_ctl/listing.py +156 -0
- package/runtime/python/okstra_ctl/locks.py +42 -0
- package/runtime/python/okstra_ctl/material.py +62 -0
- package/runtime/python/okstra_ctl/models.py +63 -0
- package/runtime/python/okstra_ctl/path_resolve.py +40 -0
- package/runtime/python/okstra_ctl/paths.py +251 -0
- package/runtime/python/okstra_ctl/project_meta.py +51 -0
- package/runtime/python/okstra_ctl/reconcile.py +166 -0
- package/runtime/python/okstra_ctl/render.py +1065 -0
- package/runtime/python/okstra_ctl/resolver.py +54 -0
- package/runtime/python/okstra_ctl/run.py +674 -0
- package/runtime/python/okstra_ctl/run_context.py +166 -0
- package/runtime/python/okstra_ctl/seeding.py +97 -0
- package/runtime/python/okstra_ctl/sequence.py +53 -0
- package/runtime/python/okstra_ctl/session.py +33 -0
- package/runtime/python/okstra_ctl/tmux.py +27 -0
- package/runtime/python/okstra_ctl/workers.py +64 -0
- package/runtime/python/okstra_ctl/workflow.py +182 -0
- package/runtime/python/okstra_project/__init__.py +41 -0
- package/runtime/python/okstra_project/resolver.py +126 -0
- package/runtime/python/okstra_project/state.py +170 -0
- package/runtime/python/okstra_token_usage/__init__.py +26 -0
- package/runtime/python/okstra_token_usage/blocks.py +62 -0
- package/runtime/python/okstra_token_usage/claude.py +97 -0
- package/runtime/python/okstra_token_usage/cli.py +84 -0
- package/runtime/python/okstra_token_usage/codex.py +80 -0
- package/runtime/python/okstra_token_usage/collect.py +161 -0
- package/runtime/python/okstra_token_usage/gemini.py +77 -0
- package/runtime/python/okstra_token_usage/jsonl_io.py +18 -0
- package/runtime/python/okstra_token_usage/paths.py +22 -0
- package/runtime/python/okstra_token_usage/pricing.py +71 -0
- package/runtime/python/okstra_token_usage/report.py +64 -0
- package/runtime/templates/prd/brief.template.md +273 -0
- package/runtime/templates/project-docs/task-index.template.md +65 -0
- package/runtime/templates/reports/error-analysis-input.template.md +80 -0
- package/runtime/templates/reports/final-report.template.md +167 -0
- package/runtime/templates/reports/final-verification-input.template.md +67 -0
- package/runtime/templates/reports/implementation-input.template.md +81 -0
- package/runtime/templates/reports/implementation-planning-input.template.md +93 -0
- package/runtime/templates/reports/quick-input.template.md +64 -0
- package/runtime/templates/reports/schedule.template.md +168 -0
- package/runtime/templates/reports/settings.template.json +101 -0
- package/runtime/templates/reports/task-brief.template.md +165 -0
- package/runtime/validators/lib/common.sh +44 -0
- package/runtime/validators/lib/fixtures.sh +322 -0
- package/runtime/validators/lib/paths.sh +44 -0
- package/runtime/validators/lib/runners.sh +140 -0
- package/runtime/validators/lib/summary.sh +15 -0
- package/runtime/validators/lib/validate-assets.sh +44 -0
- package/runtime/validators/lib/validate-prompt-metadata.sh +267 -0
- package/runtime/validators/lib/validate-tasks.sh +335 -0
- package/runtime/validators/validate-run.py +568 -0
- package/runtime/validators/validate-schedule.py +665 -0
- package/runtime/validators/validate-workflow.sh +190 -0
- package/src/doctor.mjs +127 -0
- package/src/install.mjs +355 -0
- package/src/paths.mjs +132 -0
- package/src/uninstall.mjs +122 -0
- package/src/version.mjs +20 -0
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
"""prepare_task_bundle — the single python entrypoint that materializes a
|
|
2
|
+
complete okstra task bundle on disk.
|
|
3
|
+
|
|
4
|
+
This function replaces the ~50-step wiring that previously lived in bash
|
|
5
|
+
`okstra.sh`. It is called by:
|
|
6
|
+
- `okstra.sh` (thin wrapper: argv → prepare_task_bundle → optional exec claude)
|
|
7
|
+
- `okstra-run` skill (collects inputs via AskUserQuestion → calls this)
|
|
8
|
+
- okstra-ctl rerun (passes recorded invocation argv through)
|
|
9
|
+
|
|
10
|
+
The function is read-modify-write on disk inside per-task mutex; it does not
|
|
11
|
+
mutate the calling process environment or rely on inherited env values for
|
|
12
|
+
any per-run identity. The only env vars honored are user-knob defaults
|
|
13
|
+
(`OKSTRA_DEFAULT_*_MODEL`, `OKSTRA_HOME`) — these are intentional config, not
|
|
14
|
+
state passing, and are read once at the start.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
import shutil
|
|
22
|
+
import subprocess
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Optional
|
|
26
|
+
|
|
27
|
+
from okstra_project import upsert_project_json
|
|
28
|
+
from .material import (
|
|
29
|
+
build_analysis_material,
|
|
30
|
+
related_tasks_bullets,
|
|
31
|
+
related_tasks_inline,
|
|
32
|
+
resolve_related_tasks,
|
|
33
|
+
)
|
|
34
|
+
from .models import resolve_model_metadata
|
|
35
|
+
from .path_resolve import relative_to_project_root, resolve_user_file
|
|
36
|
+
from .render import (
|
|
37
|
+
render_latest_task_discovery,
|
|
38
|
+
render_reference_expectations,
|
|
39
|
+
render_run_manifest,
|
|
40
|
+
render_task_catalog_discovery,
|
|
41
|
+
render_task_index,
|
|
42
|
+
render_task_manifest,
|
|
43
|
+
render_team_state,
|
|
44
|
+
render_template_file,
|
|
45
|
+
render_timeline,
|
|
46
|
+
)
|
|
47
|
+
from .run_context import compute_and_write_run_context, write_run_inputs
|
|
48
|
+
from .seeding import (
|
|
49
|
+
cleanup_obsolete_generated_docs,
|
|
50
|
+
render_runtime_settings_file,
|
|
51
|
+
verify_installation,
|
|
52
|
+
)
|
|
53
|
+
from .session import generate_claude_session_id, write_claude_resume_command_file
|
|
54
|
+
from .workers import normalize_workers, resolve_profile_workers
|
|
55
|
+
from .workflow import compute_workflow_state
|
|
56
|
+
|
|
57
|
+
APPROVED_PLAN_PATTERN = re.compile(
|
|
58
|
+
r"^[ \t]*(APPROVED([ \t]|:|$)|\[x\][ \t]*Approved|"
|
|
59
|
+
r"User[ \t]+Approval[ \t]*:[ \t]*(APPROVED|granted|yes))",
|
|
60
|
+
re.IGNORECASE | re.MULTILINE,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class PrepareError(Exception):
|
|
65
|
+
"""surface to caller — task bundle prepare failed."""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class PrepareInputs:
|
|
70
|
+
workspace_root: Path
|
|
71
|
+
project_root: Path
|
|
72
|
+
project_id: str
|
|
73
|
+
task_group: str
|
|
74
|
+
task_id: str
|
|
75
|
+
task_type: str
|
|
76
|
+
brief_path: Path # absolute, already resolved
|
|
77
|
+
directive: str = ""
|
|
78
|
+
workers_override: str = ""
|
|
79
|
+
lead_model: str = ""
|
|
80
|
+
claude_model: str = ""
|
|
81
|
+
codex_model: str = ""
|
|
82
|
+
gemini_model: str = ""
|
|
83
|
+
report_writer_model: str = ""
|
|
84
|
+
related_tasks_raw: str = ""
|
|
85
|
+
approved_plan_path: str = ""
|
|
86
|
+
clarification_response_path: str = "" # absolute or empty
|
|
87
|
+
render_only: bool = False
|
|
88
|
+
refresh_assets: bool = False
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class PrepareOutputs:
|
|
93
|
+
ctx: dict
|
|
94
|
+
prompt_text: str
|
|
95
|
+
runtime_settings_path: Optional[Path]
|
|
96
|
+
extras: dict = field(default_factory=dict)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _default(name: str, fallback: str) -> str:
|
|
100
|
+
return os.environ.get(name, "") or fallback
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _validate_approved_plan(path: str) -> None:
|
|
104
|
+
p = Path(path)
|
|
105
|
+
if not p.is_file():
|
|
106
|
+
raise PrepareError(f"approved plan file not found: {path}")
|
|
107
|
+
if not APPROVED_PLAN_PATTERN.search(p.read_text(encoding="utf-8", errors="replace")):
|
|
108
|
+
raise PrepareError(
|
|
109
|
+
f"approved plan has no recognised user-approval marker: {path}\n"
|
|
110
|
+
' expected one of (case-insensitive, line-anchored): "APPROVED", '
|
|
111
|
+
'"[x] Approved", "User Approval: APPROVED|granted|yes"'
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _ensure_task_directories(ctx: dict) -> None:
|
|
116
|
+
for key in (
|
|
117
|
+
"TASK_ROOT", "INSTRUCTION_SET_DIR", "RUNS_DIR", "HISTORY_DIR",
|
|
118
|
+
"RUN_DIR", "RUN_MANIFESTS_DIR", "RUN_STATE_DIR", "RUN_PROMPTS_DIR",
|
|
119
|
+
"RUN_REPORTS_DIR", "RUN_STATUS_DIR", "RUN_SESSIONS_DIR",
|
|
120
|
+
"RUN_LOGS_DIR", "WORKER_RESULTS_DIR", "OKSTRA_DISCOVERY_DIR",
|
|
121
|
+
):
|
|
122
|
+
Path(ctx[key]).mkdir(parents=True, exist_ok=True)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _migrate_legacy_run_artifacts(ctx: dict) -> None:
|
|
126
|
+
"""run/<task-type>/ 바로 아래에 남아 있을 수 있는 legacy 파일을 카테고리
|
|
127
|
+
하위 디렉터리(`manifests/`, `state/`, ...) 로 이동한다.
|
|
128
|
+
"""
|
|
129
|
+
project_root = Path(ctx["PROJECT_ROOT"])
|
|
130
|
+
task_root = Path(ctx["TASK_ROOT"])
|
|
131
|
+
run_dir = Path(ctx["RUN_DIR"])
|
|
132
|
+
if not run_dir.is_dir():
|
|
133
|
+
return
|
|
134
|
+
legacy = [
|
|
135
|
+
("run-manifest-", ".json", Path(ctx["RUN_MANIFESTS_DIR"])),
|
|
136
|
+
("team-state-", ".json", Path(ctx["RUN_STATE_DIR"])),
|
|
137
|
+
("claude-execution-prompt-", ".md", Path(ctx["RUN_PROMPTS_DIR"])),
|
|
138
|
+
("final-report-", ".md", Path(ctx["RUN_REPORTS_DIR"])),
|
|
139
|
+
("final-", ".status", Path(ctx["RUN_STATUS_DIR"])),
|
|
140
|
+
("claude-resume-", ".sh", Path(ctx["RUN_SESSIONS_DIR"])),
|
|
141
|
+
]
|
|
142
|
+
rewrites: dict[str, str] = {}
|
|
143
|
+
for entry in run_dir.iterdir():
|
|
144
|
+
if not entry.is_file():
|
|
145
|
+
continue
|
|
146
|
+
for prefix, suffix, target_dir in legacy:
|
|
147
|
+
if not (entry.name.startswith(prefix) and entry.name.endswith(suffix)):
|
|
148
|
+
continue
|
|
149
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
150
|
+
dest = target_dir / entry.name
|
|
151
|
+
old_rel = str(entry.relative_to(project_root))
|
|
152
|
+
new_rel = str(dest.relative_to(project_root))
|
|
153
|
+
if dest.exists():
|
|
154
|
+
try:
|
|
155
|
+
if entry.read_bytes() == dest.read_bytes():
|
|
156
|
+
entry.unlink()
|
|
157
|
+
rewrites[old_rel] = new_rel
|
|
158
|
+
except Exception:
|
|
159
|
+
pass
|
|
160
|
+
break
|
|
161
|
+
shutil.move(str(entry), str(dest))
|
|
162
|
+
rewrites[old_rel] = new_rel
|
|
163
|
+
break
|
|
164
|
+
if not rewrites:
|
|
165
|
+
return
|
|
166
|
+
for path in task_root.rglob("*"):
|
|
167
|
+
if not path.is_file() or path.suffix not in {".json", ".md", ".txt", ".sh"}:
|
|
168
|
+
continue
|
|
169
|
+
try:
|
|
170
|
+
original = path.read_text(encoding="utf-8")
|
|
171
|
+
except Exception:
|
|
172
|
+
continue
|
|
173
|
+
updated = original
|
|
174
|
+
for o, n in rewrites.items():
|
|
175
|
+
updated = updated.replace(o, n)
|
|
176
|
+
if updated != original:
|
|
177
|
+
path.write_text(updated, encoding="utf-8")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _record_start(
|
|
181
|
+
*,
|
|
182
|
+
workspace_root: Path,
|
|
183
|
+
ctx: dict,
|
|
184
|
+
initial_status: str,
|
|
185
|
+
canonical_argv: list[str],
|
|
186
|
+
cwd: str,
|
|
187
|
+
brief_sha256: str,
|
|
188
|
+
) -> None:
|
|
189
|
+
"""record_start hook 호출. okstra-central.sh 의 bash wrapper 와 같은 동작
|
|
190
|
+
이지만 python 직접 호출이라 환경 변수 의존 없음.
|
|
191
|
+
"""
|
|
192
|
+
import fcntl
|
|
193
|
+
import json as _json
|
|
194
|
+
from datetime import datetime, timezone
|
|
195
|
+
from okstra_ctl import record_start
|
|
196
|
+
from okstra_ctl.run_context import _okstra_home # type: ignore
|
|
197
|
+
|
|
198
|
+
home = _okstra_home()
|
|
199
|
+
home.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
200
|
+
os.chmod(home, 0o700)
|
|
201
|
+
# bootstrap (okstra_central_bootstrap 와 동등) — 디렉터리·jsonl 파일 보장.
|
|
202
|
+
for sub in ("archive", ".locks", "batches", "projects"):
|
|
203
|
+
(home / sub).mkdir(exist_ok=True)
|
|
204
|
+
for f in ("active.jsonl", "recent.jsonl"):
|
|
205
|
+
(home / f).touch(exist_ok=True)
|
|
206
|
+
state_file = home / "state.json"
|
|
207
|
+
if not state_file.exists():
|
|
208
|
+
state_file.write_text(_json.dumps({
|
|
209
|
+
"schemaVersion": "1",
|
|
210
|
+
"createdAt": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
211
|
+
"backfilledAt": None,
|
|
212
|
+
}, indent=2) + "\n", encoding="utf-8")
|
|
213
|
+
os.chmod(state_file, 0o600)
|
|
214
|
+
lockfile = home / ".lock"
|
|
215
|
+
lockfile.touch()
|
|
216
|
+
with lockfile.open("r+") as lock:
|
|
217
|
+
fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
|
|
218
|
+
record_start(
|
|
219
|
+
home,
|
|
220
|
+
project_id=ctx["PROJECT_ID"],
|
|
221
|
+
project_root=ctx["PROJECT_ROOT"],
|
|
222
|
+
task_group=ctx["TASK_GROUP"],
|
|
223
|
+
task_id=ctx["TASK_ID"],
|
|
224
|
+
task_type=ctx.get("ANALYSIS_TYPE", ""),
|
|
225
|
+
run_seq=int(ctx["RUN_MANIFESTS_SEQ"]),
|
|
226
|
+
when=ctx["RUN_TIMESTAMP_ISO"],
|
|
227
|
+
workers=[w for w in ctx.get("SELECTED_REVIEWERS", "").split(",") if w],
|
|
228
|
+
lead_model=ctx.get("LEAD_MODEL_DISPLAY", ""),
|
|
229
|
+
run_dir_rel=ctx.get("RUN_DIR_RELATIVE_PATH", ""),
|
|
230
|
+
final_report_rel=ctx.get("FINAL_REPORT_RELATIVE_PATH", ""),
|
|
231
|
+
final_status_rel=ctx.get("FINAL_STATUS_RELATIVE_PATH", ""),
|
|
232
|
+
argv=canonical_argv,
|
|
233
|
+
cwd=cwd,
|
|
234
|
+
env_overrides={},
|
|
235
|
+
initial_status=initial_status,
|
|
236
|
+
brief_sha256=brief_sha256,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _brief_sha256(path: Path) -> str:
|
|
241
|
+
import hashlib
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
with Path(path).open("rb") as f:
|
|
245
|
+
return hashlib.sha256(f.read()).hexdigest()
|
|
246
|
+
except OSError:
|
|
247
|
+
return ""
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _canonical_argv(inp: PrepareInputs, ctx: dict) -> list[str]:
|
|
251
|
+
"""rerun 충실 재현을 위한 canonical argv 재구성."""
|
|
252
|
+
workers = inp.workers_override or ctx.get("SELECTED_REVIEWERS", "")
|
|
253
|
+
pairs = [
|
|
254
|
+
("--task-type", inp.task_type),
|
|
255
|
+
("--project-id", inp.project_id),
|
|
256
|
+
("--task-group", inp.task_group),
|
|
257
|
+
("--task-id", inp.task_id),
|
|
258
|
+
("--task-brief", str(inp.brief_path)),
|
|
259
|
+
("--directive", inp.directive),
|
|
260
|
+
("--approved-plan", inp.approved_plan_path),
|
|
261
|
+
("--clarification-response", inp.clarification_response_path),
|
|
262
|
+
("--workers", workers),
|
|
263
|
+
("--lead-model", inp.lead_model or ctx.get("LEAD_MODEL_DISPLAY", "")),
|
|
264
|
+
("--claude-model", inp.claude_model or ctx.get("CLAUDE_WORKER_MODEL_DISPLAY", "")),
|
|
265
|
+
("--codex-model", inp.codex_model or ctx.get("CODEX_WORKER_MODEL_DISPLAY", "")),
|
|
266
|
+
("--gemini-model", inp.gemini_model or ctx.get("GEMINI_WORKER_MODEL_DISPLAY", "")),
|
|
267
|
+
("--report-writer-model", inp.report_writer_model or ctx.get("REPORT_WRITER_MODEL_DISPLAY", "")),
|
|
268
|
+
("--related-tasks", inp.related_tasks_raw),
|
|
269
|
+
]
|
|
270
|
+
argv: list[str] = []
|
|
271
|
+
for flag, val in pairs:
|
|
272
|
+
if val:
|
|
273
|
+
argv.extend([flag, val])
|
|
274
|
+
if inp.render_only:
|
|
275
|
+
argv.append("--render-only")
|
|
276
|
+
if inp.refresh_assets:
|
|
277
|
+
argv.append("--refresh-assets")
|
|
278
|
+
argv.append("--yes")
|
|
279
|
+
return argv
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
283
|
+
"""Produce a complete okstra task bundle on disk. See module docstring."""
|
|
284
|
+
workspace_root = Path(inp.workspace_root)
|
|
285
|
+
project_root = Path(inp.project_root)
|
|
286
|
+
|
|
287
|
+
# ---- validate inputs ----
|
|
288
|
+
profile_dir = workspace_root / "prompts" / "profiles"
|
|
289
|
+
profile_file = profile_dir / f"{inp.task_type}.md"
|
|
290
|
+
if not profile_file.is_file():
|
|
291
|
+
raise PrepareError(
|
|
292
|
+
f"analysis profile file not found for task-type {inp.task_type}: {profile_file}"
|
|
293
|
+
)
|
|
294
|
+
prompt_template = workspace_root / "prompts" / "launch.template.md"
|
|
295
|
+
if not prompt_template.is_file():
|
|
296
|
+
raise PrepareError(f"okstra prompt template not found: {prompt_template}")
|
|
297
|
+
task_index_template = (
|
|
298
|
+
workspace_root / "templates" / "project-docs" / "task-index.template.md"
|
|
299
|
+
)
|
|
300
|
+
final_report_template = (
|
|
301
|
+
workspace_root / "templates" / "reports" / "final-report.template.md"
|
|
302
|
+
)
|
|
303
|
+
source_skill = workspace_root / "agents" / "SKILL.md"
|
|
304
|
+
run_validator = workspace_root / "validators" / "validate-run.py"
|
|
305
|
+
for required in (task_index_template, final_report_template, run_validator, source_skill):
|
|
306
|
+
if not required.is_file():
|
|
307
|
+
raise PrepareError(f"required okstra template or source skill missing: {required}")
|
|
308
|
+
if not project_root.is_dir():
|
|
309
|
+
raise PrepareError(f"project root not found: {project_root}")
|
|
310
|
+
if not inp.brief_path.is_file():
|
|
311
|
+
raise PrepareError(f"task brief not found: {inp.brief_path}")
|
|
312
|
+
if inp.task_type == "implementation":
|
|
313
|
+
if not inp.approved_plan_path:
|
|
314
|
+
raise PrepareError(
|
|
315
|
+
"task-type implementation requires --approved-plan <path-to-final-report.md>"
|
|
316
|
+
)
|
|
317
|
+
_validate_approved_plan(inp.approved_plan_path)
|
|
318
|
+
if inp.clarification_response_path and not Path(inp.clarification_response_path).is_file():
|
|
319
|
+
raise PrepareError(
|
|
320
|
+
f"clarification response file not found: {inp.clarification_response_path}"
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
# ---- installation check ----
|
|
324
|
+
verify_installation(workspace_root)
|
|
325
|
+
|
|
326
|
+
# ---- project.json upsert (self-registration) ----
|
|
327
|
+
from okstra_project import ResolverError
|
|
328
|
+
|
|
329
|
+
try:
|
|
330
|
+
upsert_project_json(project_root, inp.project_id)
|
|
331
|
+
except ResolverError as exc:
|
|
332
|
+
raise PrepareError(f"project.json upsert failed: {exc}") from exc
|
|
333
|
+
|
|
334
|
+
# ---- workers resolution ----
|
|
335
|
+
profile_workers_csv = ",".join(resolve_profile_workers(profile_file))
|
|
336
|
+
workers = normalize_workers(inp.workers_override or profile_workers_csv)
|
|
337
|
+
if not workers:
|
|
338
|
+
raise PrepareError(f"no workers resolved for profile: {inp.task_type}")
|
|
339
|
+
selected_reviewers = ",".join(workers)
|
|
340
|
+
|
|
341
|
+
# ---- model assignments ----
|
|
342
|
+
lead_default = _default("OKSTRA_DEFAULT_LEAD_MODEL", "opus")
|
|
343
|
+
claude_default = _default("OKSTRA_DEFAULT_CLAUDE_MODEL", "sonnet")
|
|
344
|
+
codex_default = _default("OKSTRA_DEFAULT_CODEX_MODEL", "gpt-5.5")
|
|
345
|
+
gemini_default = _default("OKSTRA_DEFAULT_GEMINI_MODEL", "auto")
|
|
346
|
+
report_writer_default = _default("OKSTRA_DEFAULT_REPORT_WRITER_MODEL", lead_default)
|
|
347
|
+
lead = resolve_model_metadata(
|
|
348
|
+
provider="claude", raw_value=inp.lead_model,
|
|
349
|
+
default_display=lead_default, default_execution=lead_default,
|
|
350
|
+
)
|
|
351
|
+
cw = resolve_model_metadata(
|
|
352
|
+
provider="claude", raw_value=inp.claude_model,
|
|
353
|
+
default_display=claude_default, default_execution=claude_default,
|
|
354
|
+
)
|
|
355
|
+
co = resolve_model_metadata(
|
|
356
|
+
provider="codex", raw_value=inp.codex_model,
|
|
357
|
+
default_display=codex_default, default_execution=codex_default,
|
|
358
|
+
)
|
|
359
|
+
ge = resolve_model_metadata(
|
|
360
|
+
provider="gemini", raw_value=inp.gemini_model,
|
|
361
|
+
default_display=gemini_default, default_execution=gemini_default,
|
|
362
|
+
)
|
|
363
|
+
rw = resolve_model_metadata(
|
|
364
|
+
provider="claude", raw_value=inp.report_writer_model,
|
|
365
|
+
default_display=report_writer_default, default_execution=report_writer_default,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
# ---- paths under per-task mutex (writes run-context-*.json) ----
|
|
369
|
+
# OKSTRA_RUN_SEQ_OVERRIDE: okstra-ctl rerun / 테스트 hook 이 미리 reserve
|
|
370
|
+
# 한 seq 를 강제하는 user-knob 환경 변수.
|
|
371
|
+
raw_override = os.environ.get("OKSTRA_RUN_SEQ_OVERRIDE", "").strip()
|
|
372
|
+
run_seq_override = int(raw_override) if raw_override else None
|
|
373
|
+
ctx = compute_and_write_run_context(
|
|
374
|
+
workspace_root=workspace_root, project_root=project_root,
|
|
375
|
+
project_id=inp.project_id, task_group=inp.task_group, task_id=inp.task_id,
|
|
376
|
+
task_type=inp.task_type, run_seq_override=run_seq_override,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
claude_session_id = "" if inp.render_only else generate_claude_session_id()
|
|
380
|
+
|
|
381
|
+
# ---- material + related-tasks ----
|
|
382
|
+
profile_content = profile_file.read_text(encoding="utf-8")
|
|
383
|
+
review_material = build_analysis_material(inp.brief_path, inp.directive)
|
|
384
|
+
related_items = resolve_related_tasks(
|
|
385
|
+
task_manifest_path=Path(ctx["TASK_MANIFEST_FILE"]),
|
|
386
|
+
raw_related=inp.related_tasks_raw,
|
|
387
|
+
)
|
|
388
|
+
related_tasks_json_str = json.dumps(related_items, ensure_ascii=False)
|
|
389
|
+
bullets = related_tasks_bullets(related_items)
|
|
390
|
+
inline = related_tasks_inline(related_items)
|
|
391
|
+
|
|
392
|
+
# ---- relative paths for brief + clarification ----
|
|
393
|
+
brief_relative = relative_to_project_root(inp.brief_path, project_root)
|
|
394
|
+
clarification_relative = (
|
|
395
|
+
relative_to_project_root(Path(inp.clarification_response_path), project_root)
|
|
396
|
+
if inp.clarification_response_path else ""
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
# ---- initial workflow state (current_run_status not yet known) ----
|
|
400
|
+
initial_task_status = "ready-for-claude"
|
|
401
|
+
initial_run_status = "not-run"
|
|
402
|
+
workflow_state = compute_workflow_state(
|
|
403
|
+
task_type=inp.task_type,
|
|
404
|
+
current_run_status=initial_run_status,
|
|
405
|
+
current_task_status=initial_task_status,
|
|
406
|
+
render_only=inp.render_only,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
# ---- assemble full ctx (the values render functions expect) ----
|
|
410
|
+
ctx.update({
|
|
411
|
+
"REVIEW_PROFILE": inp.task_type,
|
|
412
|
+
"SELECTED_REVIEWERS": selected_reviewers,
|
|
413
|
+
"CLAUDE_SESSION_ID": claude_session_id,
|
|
414
|
+
"CLARIFICATION_RESPONSE_PATH": inp.clarification_response_path,
|
|
415
|
+
"CLARIFICATION_RESPONSE_FILE": inp.clarification_response_path,
|
|
416
|
+
"CLARIFICATION_RESPONSE_RELATIVE_PATH": clarification_relative,
|
|
417
|
+
"BRIEF_FILE_PATH": str(inp.brief_path),
|
|
418
|
+
"BRIEF_RELATIVE_PATH": brief_relative,
|
|
419
|
+
"LEAD_MODEL_DISPLAY": lead.display,
|
|
420
|
+
"LEAD_MODEL_EXECUTION_VALUE": lead.execution,
|
|
421
|
+
"CLAUDE_WORKER_MODEL_DISPLAY": cw.display,
|
|
422
|
+
"CLAUDE_WORKER_MODEL_EXECUTION_VALUE": cw.execution,
|
|
423
|
+
"CODEX_WORKER_MODEL_DISPLAY": co.display,
|
|
424
|
+
"CODEX_WORKER_MODEL_EXECUTION_VALUE": co.execution,
|
|
425
|
+
"GEMINI_WORKER_MODEL_DISPLAY": ge.display,
|
|
426
|
+
"GEMINI_WORKER_MODEL_EXECUTION_VALUE": ge.execution,
|
|
427
|
+
"REPORT_WRITER_MODEL_DISPLAY": rw.display,
|
|
428
|
+
"REPORT_WRITER_MODEL_EXECUTION_VALUE": rw.execution,
|
|
429
|
+
"RELATED_TASKS_JSON": related_tasks_json_str,
|
|
430
|
+
"RELATED_TASKS_BULLETS": bullets,
|
|
431
|
+
"RELATED_TASKS_INLINE": inline,
|
|
432
|
+
"CURRENT_TASK_STATUS": initial_task_status,
|
|
433
|
+
"CURRENT_RUN_STATUS": initial_run_status,
|
|
434
|
+
"VALIDATION_STATUS": "not-run",
|
|
435
|
+
"VALIDATION_UPDATED_AT": "",
|
|
436
|
+
"VALIDATION_FAILURES_JSON": "[]",
|
|
437
|
+
"LATEST_REPORT_PATH": "",
|
|
438
|
+
"LATEST_REPORT_RELATIVE_PATH": "",
|
|
439
|
+
"RENDER_ONLY": "true" if inp.render_only else "false",
|
|
440
|
+
**workflow_state,
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
# ---- prepare directories + cleanup ----
|
|
444
|
+
_ensure_task_directories(ctx)
|
|
445
|
+
_migrate_legacy_run_artifacts(ctx)
|
|
446
|
+
cleanup_obsolete_generated_docs(
|
|
447
|
+
project_root=project_root, instruction_set_dir=Path(ctx["INSTRUCTION_SET_DIR"]),
|
|
448
|
+
)
|
|
449
|
+
if not inp.render_only:
|
|
450
|
+
write_claude_resume_command_file(
|
|
451
|
+
resume_command_path=Path(ctx["CLAUDE_RESUME_COMMAND_FILE"]),
|
|
452
|
+
project_root=project_root, claude_session_id=claude_session_id,
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
# ---- write instruction-set scaffolding ----
|
|
456
|
+
instruction_set = Path(ctx["INSTRUCTION_SET_DIR"])
|
|
457
|
+
instruction_set.mkdir(parents=True, exist_ok=True)
|
|
458
|
+
(instruction_set / "analysis-profile.md").write_text(profile_content, encoding="utf-8")
|
|
459
|
+
(instruction_set / "analysis-material.md").write_text(review_material, encoding="utf-8")
|
|
460
|
+
shutil.copyfile(inp.brief_path, instruction_set / "task-brief.md")
|
|
461
|
+
if inp.clarification_response_path:
|
|
462
|
+
shutil.copyfile(
|
|
463
|
+
inp.clarification_response_path,
|
|
464
|
+
instruction_set / "clarification-response.md",
|
|
465
|
+
)
|
|
466
|
+
if inp.directive:
|
|
467
|
+
(instruction_set / "directive.txt").write_text(inp.directive + "\n", encoding="utf-8")
|
|
468
|
+
render_reference_expectations(
|
|
469
|
+
str(inp.brief_path), str(instruction_set / "reference-expectations.md"), ctx,
|
|
470
|
+
)
|
|
471
|
+
render_template_file(
|
|
472
|
+
str(final_report_template), ctx["FINAL_REPORT_TEMPLATE_FILE"], ctx,
|
|
473
|
+
)
|
|
474
|
+
render_template_file(
|
|
475
|
+
str(prompt_template), str(instruction_set / "claude-execution-prompt.md"), ctx,
|
|
476
|
+
)
|
|
477
|
+
prompt_text = (instruction_set / "claude-execution-prompt.md").read_text(encoding="utf-8")
|
|
478
|
+
Path(ctx["RUN_PROMPT_SNAPSHOT_FILE"]).parent.mkdir(parents=True, exist_ok=True)
|
|
479
|
+
Path(ctx["RUN_PROMPT_SNAPSHOT_FILE"]).write_text(prompt_text, encoding="utf-8")
|
|
480
|
+
|
|
481
|
+
# ---- run-inputs persistence ----
|
|
482
|
+
write_run_inputs(
|
|
483
|
+
project_root=project_root,
|
|
484
|
+
run_manifests_dir=Path(ctx["RUN_MANIFESTS_DIR"]),
|
|
485
|
+
task_type_segment=ctx["TASK_TYPE_SEGMENT"],
|
|
486
|
+
seq=ctx["RUN_MANIFESTS_SEQ"],
|
|
487
|
+
inputs={
|
|
488
|
+
"taskBriefPath": brief_relative,
|
|
489
|
+
"taskBriefAbsolutePath": str(inp.brief_path),
|
|
490
|
+
"directive": inp.directive,
|
|
491
|
+
"workers": selected_reviewers,
|
|
492
|
+
"leadModel": lead.display,
|
|
493
|
+
"claudeModel": cw.display,
|
|
494
|
+
"codexModel": co.display,
|
|
495
|
+
"geminiModel": ge.display,
|
|
496
|
+
"reportWriterModel": rw.display,
|
|
497
|
+
"relatedTasks": inp.related_tasks_raw,
|
|
498
|
+
"approvedPlanPath": inp.approved_plan_path,
|
|
499
|
+
"clarificationResponsePath": inp.clarification_response_path,
|
|
500
|
+
"renderOnly": inp.render_only,
|
|
501
|
+
"refreshAssets": inp.refresh_assets,
|
|
502
|
+
},
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
# ---- final status before manifest writes ----
|
|
506
|
+
if inp.render_only:
|
|
507
|
+
ctx["CURRENT_TASK_STATUS"] = "instruction-set-generated"
|
|
508
|
+
ctx["CURRENT_RUN_STATUS"] = "prepared"
|
|
509
|
+
ctx["LATEST_REPORT_PATH"] = ctx["FINAL_REPORT_FILE"]
|
|
510
|
+
ctx["LATEST_REPORT_RELATIVE_PATH"] = ctx["FINAL_REPORT_RELATIVE_PATH"]
|
|
511
|
+
else:
|
|
512
|
+
ctx["CURRENT_TASK_STATUS"] = "claude-session-started"
|
|
513
|
+
ctx["CURRENT_RUN_STATUS"] = "in-progress"
|
|
514
|
+
ctx["LATEST_REPORT_PATH"] = ctx["FINAL_REPORT_FILE"]
|
|
515
|
+
ctx["LATEST_REPORT_RELATIVE_PATH"] = ctx["FINAL_REPORT_RELATIVE_PATH"]
|
|
516
|
+
ctx.update(compute_workflow_state(
|
|
517
|
+
task_type=inp.task_type,
|
|
518
|
+
current_run_status=ctx["CURRENT_RUN_STATUS"],
|
|
519
|
+
current_task_status=ctx["CURRENT_TASK_STATUS"],
|
|
520
|
+
render_only=inp.render_only,
|
|
521
|
+
))
|
|
522
|
+
render_team_state(ctx["TEAM_STATE_FILE"], ctx)
|
|
523
|
+
render_task_manifest(ctx["TASK_MANIFEST_FILE"], ctx)
|
|
524
|
+
render_task_index(str(task_index_template), ctx["TASK_INDEX_FILE"], ctx)
|
|
525
|
+
render_run_manifest(ctx["RUN_MANIFEST_FILE"], ctx)
|
|
526
|
+
render_timeline(ctx["TIMELINE_FILE"], ctx)
|
|
527
|
+
render_task_catalog_discovery(ctx["OKSTRA_TASK_CATALOG_FILE"], ctx)
|
|
528
|
+
render_latest_task_discovery(ctx["OKSTRA_LATEST_TASK_FILE"], ctx)
|
|
529
|
+
|
|
530
|
+
# ---- central index ----
|
|
531
|
+
initial_status = "prepared" if inp.render_only else "running"
|
|
532
|
+
canonical_argv = _canonical_argv(inp, ctx)
|
|
533
|
+
try:
|
|
534
|
+
_record_start(
|
|
535
|
+
workspace_root=workspace_root,
|
|
536
|
+
ctx=ctx,
|
|
537
|
+
initial_status=initial_status,
|
|
538
|
+
canonical_argv=canonical_argv,
|
|
539
|
+
cwd=os.getcwd(),
|
|
540
|
+
brief_sha256=_brief_sha256(inp.brief_path),
|
|
541
|
+
)
|
|
542
|
+
except Exception as exc:
|
|
543
|
+
print(
|
|
544
|
+
f"okstra-central: record_start failed; central index will be incomplete ({exc})",
|
|
545
|
+
file=__import__("sys").stderr,
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
runtime_settings_path = None
|
|
549
|
+
if not inp.render_only:
|
|
550
|
+
runtime_settings_path = render_runtime_settings_file(
|
|
551
|
+
workspace_root=workspace_root, run_dir=Path(ctx["RUN_DIR"]),
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
return PrepareOutputs(
|
|
555
|
+
ctx=ctx,
|
|
556
|
+
prompt_text=prompt_text,
|
|
557
|
+
runtime_settings_path=runtime_settings_path,
|
|
558
|
+
extras={"profile_content": profile_content},
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def claude_is_available() -> bool:
|
|
563
|
+
"""`claude` CLI 가 PATH 에 있는지 확인."""
|
|
564
|
+
return shutil.which("claude") is not None
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def main(argv: list[str]) -> int:
|
|
568
|
+
"""CLI dispatcher for bash thin-wrapper use. Parses a flat list of argv
|
|
569
|
+
(the same flags `okstra.sh` accepts), runs prepare_task_bundle, prints
|
|
570
|
+
summary lines + prompt to stdout."""
|
|
571
|
+
import argparse
|
|
572
|
+
|
|
573
|
+
p = argparse.ArgumentParser()
|
|
574
|
+
p.add_argument("--workspace-root", required=True)
|
|
575
|
+
p.add_argument("--project-root", required=True)
|
|
576
|
+
p.add_argument("--project-id", required=True)
|
|
577
|
+
p.add_argument("--task-group", required=True)
|
|
578
|
+
p.add_argument("--task-id", required=True)
|
|
579
|
+
p.add_argument("--task-type", required=True)
|
|
580
|
+
p.add_argument("--task-brief", required=True, dest="task_brief")
|
|
581
|
+
p.add_argument("--directive", default="")
|
|
582
|
+
p.add_argument("--workers", default="", dest="workers_override")
|
|
583
|
+
p.add_argument("--lead-model", default="")
|
|
584
|
+
p.add_argument("--claude-model", default="")
|
|
585
|
+
p.add_argument("--codex-model", default="")
|
|
586
|
+
p.add_argument("--gemini-model", default="")
|
|
587
|
+
p.add_argument("--report-writer-model", default="")
|
|
588
|
+
p.add_argument("--related-tasks", default="", dest="related_tasks_raw")
|
|
589
|
+
p.add_argument("--approved-plan", default="", dest="approved_plan_path")
|
|
590
|
+
p.add_argument("--clarification-response", default="", dest="clarification_response_path")
|
|
591
|
+
p.add_argument("--render-only", action="store_true", dest="render_only")
|
|
592
|
+
p.add_argument("--refresh-assets", action="store_true", dest="refresh_assets")
|
|
593
|
+
args = p.parse_args(argv)
|
|
594
|
+
|
|
595
|
+
project_root = Path(args.project_root).expanduser().resolve()
|
|
596
|
+
brief_abs = resolve_user_file(args.task_brief, project_root)
|
|
597
|
+
if brief_abs is None:
|
|
598
|
+
print(f"task brief not found: {args.task_brief}", file=__import__("sys").stderr)
|
|
599
|
+
return 1
|
|
600
|
+
clarification_abs = ""
|
|
601
|
+
if args.clarification_response_path:
|
|
602
|
+
cr = resolve_user_file(args.clarification_response_path, project_root)
|
|
603
|
+
if cr is None:
|
|
604
|
+
print(
|
|
605
|
+
f"clarification response file not found: {args.clarification_response_path}",
|
|
606
|
+
file=__import__("sys").stderr,
|
|
607
|
+
)
|
|
608
|
+
return 1
|
|
609
|
+
clarification_abs = str(cr)
|
|
610
|
+
|
|
611
|
+
inputs = PrepareInputs(
|
|
612
|
+
workspace_root=Path(args.workspace_root).resolve(),
|
|
613
|
+
project_root=project_root,
|
|
614
|
+
project_id=args.project_id,
|
|
615
|
+
task_group=args.task_group,
|
|
616
|
+
task_id=args.task_id,
|
|
617
|
+
task_type=args.task_type,
|
|
618
|
+
brief_path=brief_abs,
|
|
619
|
+
directive=args.directive,
|
|
620
|
+
workers_override=args.workers_override,
|
|
621
|
+
lead_model=args.lead_model,
|
|
622
|
+
claude_model=args.claude_model,
|
|
623
|
+
codex_model=args.codex_model,
|
|
624
|
+
gemini_model=args.gemini_model,
|
|
625
|
+
report_writer_model=args.report_writer_model,
|
|
626
|
+
related_tasks_raw=args.related_tasks_raw,
|
|
627
|
+
approved_plan_path=args.approved_plan_path,
|
|
628
|
+
clarification_response_path=clarification_abs,
|
|
629
|
+
render_only=args.render_only,
|
|
630
|
+
refresh_assets=args.refresh_assets,
|
|
631
|
+
)
|
|
632
|
+
try:
|
|
633
|
+
out = prepare_task_bundle(inputs)
|
|
634
|
+
except PrepareError as exc:
|
|
635
|
+
print(str(exc), file=__import__("sys").stderr)
|
|
636
|
+
return 1
|
|
637
|
+
|
|
638
|
+
ctx = out.ctx
|
|
639
|
+
# summary block — bash wrapper consumes (and may pipe to user).
|
|
640
|
+
print(f"okstra task key: {ctx['TASK_KEY']}")
|
|
641
|
+
print(f"okstra task root: {ctx['TASK_ROOT']}")
|
|
642
|
+
print(f"okstra latest task discovery file: {ctx['OKSTRA_LATEST_TASK_FILE']}")
|
|
643
|
+
print(f"okstra task catalog file: {ctx['OKSTRA_TASK_CATALOG_FILE']}")
|
|
644
|
+
print(f"okstra instruction-set: {ctx['INSTRUCTION_SET_DIR']}")
|
|
645
|
+
print(f"okstra reference expectations: {ctx['REFERENCE_EXPECTATIONS_FILE']}")
|
|
646
|
+
print(f"okstra final report template: {ctx['FINAL_REPORT_TEMPLATE_FILE']}")
|
|
647
|
+
if inputs.render_only:
|
|
648
|
+
print()
|
|
649
|
+
print(out.prompt_text, end="")
|
|
650
|
+
else:
|
|
651
|
+
print(f"okstra current run dir: {ctx['RUN_DIR']}")
|
|
652
|
+
print(f"final report path: {ctx['FINAL_REPORT_FILE']}")
|
|
653
|
+
print(f"lead model: {ctx['LEAD_MODEL_DISPLAY']}")
|
|
654
|
+
print(f"claude session id: {ctx['CLAUDE_SESSION_ID']}")
|
|
655
|
+
print(f"resume command file: {ctx['CLAUDE_RESUME_COMMAND_FILE']}")
|
|
656
|
+
print("launch mode: interactive Claude handoff")
|
|
657
|
+
print(f"claude working directory: {ctx['PROJECT_ROOT']}")
|
|
658
|
+
print()
|
|
659
|
+
# In non-render-only mode emit a small JSON the bash wrapper can parse
|
|
660
|
+
# to build the `claude` exec command. Wrapper exec's; we don't.
|
|
661
|
+
machine = {
|
|
662
|
+
"claudeSessionId": ctx["CLAUDE_SESSION_ID"],
|
|
663
|
+
"leadModelExecutionValue": ctx["LEAD_MODEL_EXECUTION_VALUE"],
|
|
664
|
+
"projectRoot": ctx["PROJECT_ROOT"],
|
|
665
|
+
"runtimeSettingsFile": str(out.runtime_settings_path) if out.runtime_settings_path else "",
|
|
666
|
+
"promptFile": str(Path(ctx["INSTRUCTION_SET_DIR"]) / "claude-execution-prompt.md"),
|
|
667
|
+
}
|
|
668
|
+
print(f"__OKSTRA_LAUNCH__ {json.dumps(machine)}")
|
|
669
|
+
return 0
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
if __name__ == "__main__":
|
|
673
|
+
import sys
|
|
674
|
+
raise SystemExit(main(sys.argv[1:]))
|