okstra 0.163.2 → 0.164.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 +7 -5
- package/docs/architecture.md +12 -7
- package/docs/cli.md +8 -5
- package/docs/project-structure-overview.md +7 -5
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +1 -0
- 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.sh +87 -91
- package/runtime/prompts/lead/adapters/cmux.md +1 -1
- package/runtime/prompts/lead/okstra-lead-contract.md +1 -0
- package/runtime/prompts/profiles/implementation-planning.md +1 -1
- 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} +111 -0
- 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} +52 -0
- 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 +35 -0
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +55 -0
- package/runtime/python/okstra_ctl/adapters/providers/claude/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +43 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +32 -0
- package/runtime/python/okstra_ctl/adapters/providers/grok/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +38 -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 +240 -29
- package/runtime/python/okstra_ctl/dispatch_state.py +17 -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 +66 -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/entrypoints/__init__.py +1 -0
- package/runtime/python/okstra_ctl/entrypoints/hosts.py +334 -0
- 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/run.py +80 -58
- package/runtime/python/okstra_ctl/session.py +1 -1
- package/runtime/python/okstra_ctl/team.py +44 -32
- package/runtime/python/okstra_ctl/wizard.py +162 -64
- package/runtime/python/okstra_ctl/worker_audit_ledger.py +29 -4
- package/runtime/python/okstra_token_usage/collect.py +34 -6
- package/runtime/schemas/final-report-v1.0.schema.json +3989 -1085
- package/runtime/schemas/final-report-v2.0.schema.json +5622 -1451
- package/runtime/skills/okstra-run/SKILL.md +73 -28
- package/runtime/templates/implementation-worker-preamble.md +1 -1
- package/runtime/templates/reports/final-report.template.md +3 -3
- package/runtime/templates/reports/html/i18n/en.json +3 -1
- package/runtime/templates/reports/html/i18n/ko.json +3 -1
- package/runtime/templates/reports/html/tasks/implementation-planning.template.html +1 -1
- 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 +8 -7
- package/runtime/validators/validate_session_conformance.py +7 -2
- 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/python/okstra_ctl/lead_runtime.py +0 -115
- package/runtime/python/okstra_ctl/runner_resolution.py +0 -103
- package/src/lib/runtime-readiness.mjs +0 -90
- package/src/lib/runtime-resolver.mjs +0 -123
|
@@ -30,11 +30,18 @@ from okstra_project.dirs import TASK_MANIFEST_FILENAME, OKSTRA_DIR_NAME, project
|
|
|
30
30
|
# 이는 silent drift 위험이 있어 SSOT import 로 통합한다.
|
|
31
31
|
from . import fix_cycles
|
|
32
32
|
from .analysis_inputs import ANALYSIS_TASK_TYPES
|
|
33
|
+
from .application.resolve_assignment import (
|
|
34
|
+
resolve_assignment_runner,
|
|
35
|
+
resolve_lead_provider,
|
|
36
|
+
)
|
|
37
|
+
from .application.dispatch_assignments import dispatch_assignments
|
|
38
|
+
from .adapters.dispatch import default_worker_dispatch_port, with_worker_dispatch
|
|
39
|
+
from .adapters.dispatch.cmux import dispatch_port_for_terminal_backend
|
|
33
40
|
from .paths import okstra_home
|
|
34
|
-
from .dispatch_state import BACKEND_CMUX_PANE
|
|
35
|
-
from .lead_runtime import lead_runtime_info, with_cmux_dispatch
|
|
36
41
|
from .models import UnknownProviderError, provider_ids, provider_spec
|
|
37
|
-
from .
|
|
42
|
+
from .ports.worker_dispatch import WorkerDispatchRequest
|
|
43
|
+
from .registry.host_registry import default_host_registry
|
|
44
|
+
from .registry.provider_registry import default_provider_registry
|
|
38
45
|
from .path_hints import compact_active_run_context, hydrate_run_context
|
|
39
46
|
from .paths import task_timeline_file
|
|
40
47
|
from .worker_artifacts import artifacts_from_context
|
|
@@ -80,27 +87,49 @@ def _lead_runtime(ctx: dict) -> str:
|
|
|
80
87
|
return ctx.get("LEAD_RUNTIME", "") or "claude-code"
|
|
81
88
|
|
|
82
89
|
|
|
83
|
-
def
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
90
|
+
def _lead_descriptor(ctx: dict):
|
|
91
|
+
return default_host_registry().resolve(_lead_runtime(ctx)).descriptor
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _worker_dispatch_plan(ctx: dict):
|
|
95
|
+
host_adapter = default_host_registry().resolve(_lead_runtime(ctx))
|
|
96
|
+
fallback_port = default_worker_dispatch_port(host_adapter.descriptor)
|
|
97
|
+
port = dispatch_port_for_terminal_backend(
|
|
98
|
+
ctx.get("TERMINAL_BACKEND", ""),
|
|
99
|
+
fallback_port,
|
|
100
|
+
)
|
|
101
|
+
decorated = with_worker_dispatch(host_adapter, port)
|
|
102
|
+
request = WorkerDispatchRequest(
|
|
103
|
+
project_root=Path(ctx.get("PROJECT_ROOT", "")),
|
|
104
|
+
run_manifest=Path(ctx.get("RUN_MANIFEST_PATH", "")),
|
|
105
|
+
)
|
|
106
|
+
return dispatch_assignments(request, decorated.worker_dispatch())
|
|
88
107
|
|
|
89
108
|
|
|
90
109
|
def _lead_agent(ctx: dict) -> str:
|
|
91
|
-
return
|
|
110
|
+
return _lead_descriptor(ctx).agent_id
|
|
92
111
|
|
|
93
112
|
|
|
94
113
|
def _lead_agent_label(ctx: dict) -> str:
|
|
95
|
-
return
|
|
114
|
+
return _lead_descriptor(ctx).agent_label
|
|
96
115
|
|
|
97
116
|
|
|
98
117
|
def _lead_role(ctx: dict) -> str:
|
|
99
|
-
return
|
|
118
|
+
return _lead_descriptor(ctx).role
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _lead_adapter(ctx: dict, plan) -> dict:
|
|
122
|
+
descriptor = default_host_registry().resolve(_lead_runtime(ctx)).descriptor
|
|
123
|
+
return {
|
|
124
|
+
"name": plan.adapter_name or descriptor.id,
|
|
125
|
+
"dispatchMode": plan.dispatch_mode or descriptor.dispatch_mode,
|
|
126
|
+
"sessionAccounting": descriptor.session_accounting,
|
|
127
|
+
}
|
|
100
128
|
|
|
101
129
|
|
|
102
|
-
def
|
|
103
|
-
|
|
130
|
+
def _lead_adapter_contract(ctx: dict, plan) -> str:
|
|
131
|
+
descriptor = default_host_registry().resolve(_lead_runtime(ctx)).descriptor
|
|
132
|
+
return plan.relay_contract or descriptor.relay_contract
|
|
104
133
|
|
|
105
134
|
|
|
106
135
|
def _runtime_resolution(ctx: dict) -> dict:
|
|
@@ -257,31 +286,71 @@ def _resolve_workers(ctx: dict) -> list[str]:
|
|
|
257
286
|
]
|
|
258
287
|
|
|
259
288
|
|
|
260
|
-
def _assignment_runner_for_ctx(
|
|
261
|
-
|
|
262
|
-
|
|
289
|
+
def _assignment_runner_for_ctx(
|
|
290
|
+
ctx: dict,
|
|
291
|
+
provider: str,
|
|
292
|
+
role: str,
|
|
293
|
+
requested_runner: str = "",
|
|
294
|
+
) -> str:
|
|
295
|
+
host = default_host_registry().resolve(_lead_runtime(ctx)).descriptor
|
|
296
|
+
spec = default_provider_registry().resolve(provider)
|
|
297
|
+
return resolve_assignment_runner(
|
|
298
|
+
host=host,
|
|
299
|
+
provider=spec,
|
|
300
|
+
role=role,
|
|
301
|
+
requested_runner=requested_runner,
|
|
302
|
+
).runner
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _resolved_lead_assignment(
|
|
306
|
+
ctx: dict,
|
|
307
|
+
requested_provider: str,
|
|
308
|
+
requested_runner: str = "",
|
|
309
|
+
):
|
|
310
|
+
host_registry = default_host_registry()
|
|
311
|
+
provider_registry = default_provider_registry()
|
|
312
|
+
assignment = resolve_lead_provider(
|
|
313
|
+
host_id=_lead_runtime(ctx),
|
|
314
|
+
requested_provider=requested_provider,
|
|
315
|
+
host_registry=host_registry,
|
|
316
|
+
provider_registry=provider_registry,
|
|
317
|
+
)
|
|
318
|
+
if requested_runner:
|
|
319
|
+
resolution = resolve_assignment_runner(
|
|
320
|
+
host=host_registry.resolve(_lead_runtime(ctx)).descriptor,
|
|
321
|
+
provider=provider_registry.resolve(assignment.provider),
|
|
322
|
+
role="lead",
|
|
323
|
+
requested_runner=requested_runner,
|
|
324
|
+
)
|
|
325
|
+
return assignment.provider, resolution.runner
|
|
326
|
+
return assignment.provider, assignment.runner
|
|
263
327
|
|
|
264
328
|
|
|
265
329
|
def _lead_assignment(ctx: dict) -> dict:
|
|
330
|
+
parsed = None
|
|
266
331
|
raw = ctx.get("LEAD_ASSIGNMENT_JSON", "")
|
|
267
332
|
if raw:
|
|
268
333
|
try:
|
|
269
|
-
|
|
270
|
-
if isinstance(
|
|
271
|
-
|
|
334
|
+
candidate = json.loads(raw)
|
|
335
|
+
if isinstance(candidate, dict):
|
|
336
|
+
parsed = candidate
|
|
272
337
|
except json.JSONDecodeError:
|
|
273
338
|
pass
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
or "
|
|
339
|
+
source = parsed or {}
|
|
340
|
+
provider, runner = _resolved_lead_assignment(
|
|
341
|
+
ctx,
|
|
342
|
+
source.get("provider", "") or ctx.get("LEAD_PROVIDER", ""),
|
|
343
|
+
source.get("runner", ""),
|
|
278
344
|
)
|
|
279
345
|
return {
|
|
346
|
+
**source,
|
|
280
347
|
"role": "lead",
|
|
281
348
|
"provider": provider,
|
|
282
|
-
"model": ctx.get("LEAD_MODEL", ""),
|
|
283
|
-
"modelExecutionValue":
|
|
284
|
-
|
|
349
|
+
"model": source.get("model", ctx.get("LEAD_MODEL", "")),
|
|
350
|
+
"modelExecutionValue": source.get(
|
|
351
|
+
"modelExecutionValue", ctx.get("LEAD_MODEL_EXECUTION_VALUE", "")
|
|
352
|
+
),
|
|
353
|
+
"runner": runner,
|
|
285
354
|
}
|
|
286
355
|
|
|
287
356
|
|
|
@@ -291,7 +360,7 @@ def _worker_assignments(ctx: dict) -> list[dict]:
|
|
|
291
360
|
try:
|
|
292
361
|
parsed = json.loads(raw)
|
|
293
362
|
if isinstance(parsed, list) and all(isinstance(row, dict) for row in parsed):
|
|
294
|
-
return parsed
|
|
363
|
+
return [_resolved_worker_assignment(ctx, row) for row in parsed]
|
|
295
364
|
except json.JSONDecodeError:
|
|
296
365
|
pass
|
|
297
366
|
catalog = _worker_catalog(ctx, include_assignments=False)
|
|
@@ -302,12 +371,39 @@ def _worker_assignments(ctx: dict) -> list[dict]:
|
|
|
302
371
|
"provider": catalog[worker_id]["agent"],
|
|
303
372
|
"model": catalog[worker_id]["model"],
|
|
304
373
|
"modelExecutionValue": catalog[worker_id]["modelExecutionValue"],
|
|
305
|
-
"runner": _assignment_runner_for_ctx(
|
|
374
|
+
"runner": _assignment_runner_for_ctx(
|
|
375
|
+
ctx,
|
|
376
|
+
catalog[worker_id]["agent"],
|
|
377
|
+
"report-writer" if worker_id == "report-writer" else "analyser",
|
|
378
|
+
),
|
|
306
379
|
}
|
|
307
380
|
for worker_id in _resolve_workers(ctx)
|
|
308
381
|
]
|
|
309
382
|
|
|
310
383
|
|
|
384
|
+
def _resolved_worker_assignment(ctx: dict, source: dict) -> dict:
|
|
385
|
+
worker_id = source.get("workerId", "")
|
|
386
|
+
role = source.get("role", "") or (
|
|
387
|
+
"report-writer" if worker_id == "report-writer" else "analyser"
|
|
388
|
+
)
|
|
389
|
+
provider = source.get("provider", "") or _fallback_worker_provider(
|
|
390
|
+
ctx, worker_id
|
|
391
|
+
)
|
|
392
|
+
runner = _assignment_runner_for_ctx(
|
|
393
|
+
ctx,
|
|
394
|
+
provider,
|
|
395
|
+
role,
|
|
396
|
+
source.get("runner", ""),
|
|
397
|
+
)
|
|
398
|
+
return {
|
|
399
|
+
**source,
|
|
400
|
+
"workerId": worker_id,
|
|
401
|
+
"role": role,
|
|
402
|
+
"provider": provider,
|
|
403
|
+
"runner": runner,
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
|
|
311
407
|
def _worker_catalog(ctx: dict, *, include_assignments: bool = True) -> dict:
|
|
312
408
|
assignments = {} if not include_assignments else {
|
|
313
409
|
row.get("workerId"): row for row in _worker_assignments(ctx)
|
|
@@ -329,7 +425,11 @@ def _worker_catalog(ctx: dict, *, include_assignments: bool = True) -> dict:
|
|
|
329
425
|
"agentLabel": provider_info.display_label,
|
|
330
426
|
"provider": provider,
|
|
331
427
|
"runner": assignment.get(
|
|
332
|
-
"runner", _assignment_runner_for_ctx(
|
|
428
|
+
"runner", _assignment_runner_for_ctx(
|
|
429
|
+
ctx,
|
|
430
|
+
provider,
|
|
431
|
+
"report-writer" if worker_id == "report-writer" else "analyser",
|
|
432
|
+
),
|
|
333
433
|
),
|
|
334
434
|
"model": assignment.get("model", model),
|
|
335
435
|
"modelExecutionValue": assignment.get("modelExecutionValue", execution),
|
|
@@ -551,6 +651,7 @@ def render_active_run_context(active_context_path: str, ctx: dict) -> None:
|
|
|
551
651
|
def render_team_state(team_state_path: str, ctx: dict) -> None:
|
|
552
652
|
selected = _resolve_workers(ctx)
|
|
553
653
|
catalog = _worker_catalog(ctx)
|
|
654
|
+
worker_dispatch_plan = _worker_dispatch_plan(ctx)
|
|
554
655
|
workers = []
|
|
555
656
|
for w in selected:
|
|
556
657
|
m = catalog[w]
|
|
@@ -577,7 +678,7 @@ def render_team_state(team_state_path: str, ctx: dict) -> None:
|
|
|
577
678
|
"leadRuntime": _lead_runtime(ctx),
|
|
578
679
|
"leadAssignment": _lead_assignment(ctx),
|
|
579
680
|
"workerAssignments": _worker_assignments(ctx),
|
|
580
|
-
"leadAdapter": _lead_adapter(ctx),
|
|
681
|
+
"leadAdapter": _lead_adapter(ctx, worker_dispatch_plan),
|
|
581
682
|
"leadEventsPath": ctx.get("LEAD_EVENTS_RELATIVE_PATH", ""),
|
|
582
683
|
"runDirectoryPath": ctx.get("RUN_DIR_RELATIVE_PATH", ""),
|
|
583
684
|
"workflowState": ctx.get("CURRENT_RUN_STATUS", ""),
|
|
@@ -1112,6 +1213,7 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
|
|
|
1112
1213
|
except Exception:
|
|
1113
1214
|
existing = {}
|
|
1114
1215
|
reviewers = _resolve_workers(ctx)
|
|
1216
|
+
worker_dispatch_plan = _worker_dispatch_plan(ctx)
|
|
1115
1217
|
catalog = _worker_catalog(ctx)
|
|
1116
1218
|
required_worker_roles = _required_worker_roles(ctx, reviewers)
|
|
1117
1219
|
worker_prompt_paths = {item: catalog[item]["promptPath"] for item in reviewers}
|
|
@@ -1170,7 +1272,7 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
|
|
|
1170
1272
|
"leadRuntime": _lead_runtime(ctx),
|
|
1171
1273
|
"leadAssignment": _lead_assignment(ctx),
|
|
1172
1274
|
"workerAssignments": _worker_assignments(ctx),
|
|
1173
|
-
"leadAdapter": _lead_adapter(ctx),
|
|
1275
|
+
"leadAdapter": _lead_adapter(ctx, worker_dispatch_plan),
|
|
1174
1276
|
"workCategory": work_category,
|
|
1175
1277
|
# user-managed status (set-work-status CLI) — carried across re-renders
|
|
1176
1278
|
"workStatus": existing.get("workStatus", ""),
|
|
@@ -1308,7 +1410,7 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
|
|
|
1308
1410
|
"leadSession": {
|
|
1309
1411
|
"sessionId": ctx.get("CLAUDE_SESSION_ID", ""),
|
|
1310
1412
|
"resumeCommandPath": ctx.get("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", ""),
|
|
1311
|
-
"accounting":
|
|
1413
|
+
"accounting": _lead_descriptor(ctx).session_accounting,
|
|
1312
1414
|
},
|
|
1313
1415
|
"convergence": convergence_block,
|
|
1314
1416
|
"createdAt": existing.get("createdAt") or ctx.get("RUN_TIMESTAMP_ISO", ""),
|
|
@@ -1418,6 +1520,7 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
|
|
|
1418
1520
|
except Exception:
|
|
1419
1521
|
task_manifest = {}
|
|
1420
1522
|
reviewers = _resolve_workers(ctx)
|
|
1523
|
+
worker_dispatch_plan = _worker_dispatch_plan(ctx)
|
|
1421
1524
|
catalog = _worker_catalog(ctx)
|
|
1422
1525
|
required_worker_roles = _required_worker_roles(ctx, reviewers)
|
|
1423
1526
|
worker_prompt_paths = {item: catalog[item]["promptPath"] for item in reviewers}
|
|
@@ -1452,7 +1555,7 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
|
|
|
1452
1555
|
"runtimeResolution": _runtime_resolution(ctx),
|
|
1453
1556
|
"leadAssignment": _lead_assignment(ctx),
|
|
1454
1557
|
"workerAssignments": _worker_assignments(ctx),
|
|
1455
|
-
"leadAdapter": _lead_adapter(ctx),
|
|
1558
|
+
"leadAdapter": _lead_adapter(ctx, worker_dispatch_plan),
|
|
1456
1559
|
"workCategory": task_manifest.get(
|
|
1457
1560
|
"workCategory", ctx.get("WORKFLOW_WORK_CATEGORY", "unknown")
|
|
1458
1561
|
),
|
|
@@ -1957,6 +2060,7 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
|
|
|
1957
2060
|
selected = _resolve_workers(ctx)
|
|
1958
2061
|
catalog = _worker_catalog(ctx)
|
|
1959
2062
|
lead_runtime = _lead_runtime(ctx)
|
|
2063
|
+
worker_dispatch_plan = _worker_dispatch_plan(ctx)
|
|
1960
2064
|
lead_role = _lead_role(ctx)
|
|
1961
2065
|
lead_agent_label = _lead_agent_label(ctx)
|
|
1962
2066
|
lead_model = ctx.get("LEAD_MODEL", "")
|
|
@@ -1966,7 +2070,7 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
|
|
|
1966
2070
|
home_prompts / "lead" / "okstra-lead-contract.md"
|
|
1967
2071
|
)
|
|
1968
2072
|
adapter_contract_path = ctx.get("OKSTRA_LEAD_ADAPTER_CONTRACT_PATH") or str(
|
|
1969
|
-
home_prompts /
|
|
2073
|
+
home_prompts / _lead_adapter_contract(ctx, worker_dispatch_plan)
|
|
1970
2074
|
)
|
|
1971
2075
|
context_loader_path = ctx.get("OKSTRA_CONTEXT_LOADER_PATH") or str(
|
|
1972
2076
|
home_prompts / "lead" / "context-loader.md"
|
|
@@ -2163,27 +2267,21 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
|
|
|
2163
2267
|
"schema and write rules. Do not re-inline either contract."
|
|
2164
2268
|
)
|
|
2165
2269
|
|
|
2166
|
-
|
|
2270
|
+
lead_descriptor = _lead_descriptor(ctx)
|
|
2271
|
+
if lead_descriptor.has_claude_session:
|
|
2167
2272
|
lead_session_block = (
|
|
2168
2273
|
"## Session\n"
|
|
2169
2274
|
"\n"
|
|
2170
|
-
"-
|
|
2171
|
-
"-
|
|
2172
|
-
)
|
|
2173
|
-
elif lead_runtime == "codex":
|
|
2174
|
-
lead_session_block = (
|
|
2175
|
-
"## Session\n"
|
|
2176
|
-
"\n"
|
|
2177
|
-
"- Lead runtime: `codex`\n"
|
|
2178
|
-
"- Codex run resume is artifact/checkpoint based; `okstra codex-run` "
|
|
2179
|
-
"does not create a Claude session id."
|
|
2275
|
+
f"- Session ID: `{ctx.get('CLAUDE_SESSION_ID', '')}`\n"
|
|
2276
|
+
f"- Resume: `{ctx.get('CLAUDE_RESUME_COMMAND_RELATIVE_PATH', '')}`"
|
|
2180
2277
|
)
|
|
2181
2278
|
else:
|
|
2182
2279
|
lead_session_block = (
|
|
2183
2280
|
"## Session\n"
|
|
2184
2281
|
"\n"
|
|
2185
|
-
f"-
|
|
2186
|
-
f"-
|
|
2282
|
+
f"- Lead runtime: `{lead_descriptor.id}`\n"
|
|
2283
|
+
f"- Lead session accounting: {lead_descriptor.session_accounting}\n"
|
|
2284
|
+
"- Resume behavior is provided by the selected host lead-session port."
|
|
2187
2285
|
)
|
|
2188
2286
|
|
|
2189
2287
|
# Compute results (deterministic from ctx, 덮어쓰기)
|
|
@@ -57,14 +57,19 @@ from .material import (
|
|
|
57
57
|
from .final_report_schema import load_schema_version
|
|
58
58
|
from .final_report_paths import final_report_data_path as _final_report_data_path
|
|
59
59
|
from .lead_events import LeadEvent, append_lead_event
|
|
60
|
-
from .
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
from .application.resolve_assignment import (
|
|
61
|
+
resolve_assignment_runner,
|
|
62
|
+
resolve_lead_provider,
|
|
63
|
+
)
|
|
64
|
+
from .domain.host import (
|
|
65
|
+
HostCapabilityMismatch,
|
|
66
|
+
HostNotRegistered,
|
|
67
|
+
ProviderUnavailable,
|
|
64
68
|
)
|
|
65
69
|
from .model_discovery import normalize_execution_for_dispatch
|
|
66
70
|
from .models import (
|
|
67
71
|
ModelAssignment,
|
|
72
|
+
UnknownProviderError,
|
|
68
73
|
default_model,
|
|
69
74
|
lead_launch_argv,
|
|
70
75
|
lead_launch_spec,
|
|
@@ -75,11 +80,8 @@ from .models import (
|
|
|
75
80
|
resolve_model_metadata,
|
|
76
81
|
)
|
|
77
82
|
from .schema_excerpt import build_schema_excerpt
|
|
78
|
-
from .
|
|
79
|
-
|
|
80
|
-
native_provider_for_host,
|
|
81
|
-
resolve_runner,
|
|
82
|
-
)
|
|
83
|
+
from .registry.host_registry import default_host_registry
|
|
84
|
+
from .registry.provider_registry import default_provider_registry
|
|
83
85
|
from .report_contract import CURRENT_REPORT_SCHEMA_VERSION
|
|
84
86
|
from .path_resolve import relative_to_project_root, resolve_user_file
|
|
85
87
|
from .render import (
|
|
@@ -326,15 +328,16 @@ def _set_data_json_approved_true_if_present(path: Path) -> bool:
|
|
|
326
328
|
PrepareError = _stage_targets.PrepareError
|
|
327
329
|
|
|
328
330
|
|
|
329
|
-
_ALLOWED_LEAD_RUNTIMES = ALLOWED_LEAD_RUNTIMES
|
|
330
|
-
|
|
331
|
-
|
|
332
331
|
def _normalize_lead_runtime(value: str) -> str:
|
|
333
332
|
runtime = (value or "claude-code").strip()
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
333
|
+
registry = default_host_registry()
|
|
334
|
+
try:
|
|
335
|
+
return registry.resolve(runtime).descriptor.id
|
|
336
|
+
except HostNotRegistered as exc:
|
|
337
|
+
allowed = ", ".join(registry.ids())
|
|
338
|
+
raise PrepareError(
|
|
339
|
+
f"unsupported lead runtime: {runtime} (allowed: {allowed})"
|
|
340
|
+
) from exc
|
|
338
341
|
|
|
339
342
|
|
|
340
343
|
@dataclass
|
|
@@ -726,7 +729,8 @@ def _brief_sha256(path: Path) -> str:
|
|
|
726
729
|
|
|
727
730
|
|
|
728
731
|
def _record_artifact_runtime_render_only_event(inp: PrepareInputs, ctx: dict) -> None:
|
|
729
|
-
|
|
732
|
+
descriptor = default_host_registry().resolve(inp.lead_runtime).descriptor
|
|
733
|
+
if descriptor.session_accounting != "artifact-only" or not inp.render_only:
|
|
730
734
|
return
|
|
731
735
|
append_lead_event(
|
|
732
736
|
Path(ctx["LEAD_EVENTS_PATH"]),
|
|
@@ -1446,9 +1450,20 @@ def _model_for_role(
|
|
|
1446
1450
|
|
|
1447
1451
|
|
|
1448
1452
|
def _assignment_runner(lead_runtime: str, provider: str, role: str) -> str:
|
|
1453
|
+
host_registry = default_host_registry()
|
|
1454
|
+
provider_registry = default_provider_registry()
|
|
1449
1455
|
try:
|
|
1450
|
-
return
|
|
1451
|
-
|
|
1456
|
+
return resolve_assignment_runner(
|
|
1457
|
+
host=host_registry.resolve(lead_runtime).descriptor,
|
|
1458
|
+
provider=provider_registry.resolve(provider),
|
|
1459
|
+
role=role,
|
|
1460
|
+
).runner
|
|
1461
|
+
except (
|
|
1462
|
+
HostCapabilityMismatch,
|
|
1463
|
+
HostNotRegistered,
|
|
1464
|
+
ProviderUnavailable,
|
|
1465
|
+
UnknownProviderError,
|
|
1466
|
+
) as exc:
|
|
1452
1467
|
raise PrepareError(str(exc)) from exc
|
|
1453
1468
|
|
|
1454
1469
|
|
|
@@ -1468,23 +1483,22 @@ def _resolve_worker_models(inp: PrepareInputs) -> dict:
|
|
|
1468
1483
|
provider, overrides.get(provider, legacy.get(provider, "")),
|
|
1469
1484
|
"analyser", default_override,
|
|
1470
1485
|
)
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1486
|
+
try:
|
|
1487
|
+
lead_assignment = resolve_lead_provider(
|
|
1488
|
+
host_id=inp.lead_runtime,
|
|
1489
|
+
requested_provider=inp.lead_provider,
|
|
1490
|
+
host_registry=default_host_registry(),
|
|
1491
|
+
provider_registry=default_provider_registry(),
|
|
1492
|
+
)
|
|
1493
|
+
except (
|
|
1494
|
+
HostCapabilityMismatch,
|
|
1495
|
+
HostNotRegistered,
|
|
1496
|
+
ProviderUnavailable,
|
|
1497
|
+
UnknownProviderError,
|
|
1498
|
+
) as exc:
|
|
1499
|
+
raise PrepareError(str(exc)) from exc
|
|
1500
|
+
lead_provider = lead_assignment.provider
|
|
1485
1501
|
report_writer_provider = inp.report_writer_provider.strip().lower() or "claude"
|
|
1486
|
-
if not provider_supports_role(lead_provider, "lead"):
|
|
1487
|
-
raise PrepareError(f"provider {lead_provider!r} does not support the lead role")
|
|
1488
1502
|
if not provider_supports_role(report_writer_provider, "report-writer"):
|
|
1489
1503
|
raise PrepareError(
|
|
1490
1504
|
f"provider {report_writer_provider!r} does not support the report-writer role"
|
|
@@ -2736,7 +2750,7 @@ def main(argv: list[str]) -> int:
|
|
|
2736
2750
|
p.add_argument(
|
|
2737
2751
|
"--lead-runtime",
|
|
2738
2752
|
default="claude-code",
|
|
2739
|
-
choices=list(
|
|
2753
|
+
choices=list(default_host_registry().ids()),
|
|
2740
2754
|
dest="lead_runtime",
|
|
2741
2755
|
help=(
|
|
2742
2756
|
"Lead runtime adapter. Default: claude-code. `codex` renders "
|
|
@@ -2964,8 +2978,7 @@ def main(argv: list[str]) -> int:
|
|
|
2964
2978
|
else:
|
|
2965
2979
|
print(f"okstra current run dir: {ctx['RUN_DIR']}")
|
|
2966
2980
|
print(f"final report path: {ctx['FINAL_REPORT_PATH']}")
|
|
2967
|
-
|
|
2968
|
-
lead_provider = lead_runtime_info(lead_runtime_name).agent
|
|
2981
|
+
lead_provider = ctx.get("LEAD_PROVIDER", "")
|
|
2969
2982
|
launch = lead_launch_spec(lead_provider)
|
|
2970
2983
|
print(f"lead model: {ctx['LEAD_MODEL']}")
|
|
2971
2984
|
print(f"claude session id: {ctx['CLAUDE_SESSION_ID']}")
|
|
@@ -2977,30 +2990,39 @@ def main(argv: list[str]) -> int:
|
|
|
2977
2990
|
# lead. The argv is assembled here rather than in the caller so provider
|
|
2978
2991
|
# launch knowledge stays in the catalog and every front end — the bash
|
|
2979
2992
|
# wrapper and the Node CLI — starts the lead identically.
|
|
2980
|
-
|
|
2981
|
-
machine = {
|
|
2982
|
-
"leadRuntime": lead_runtime_name,
|
|
2983
|
-
"leadProvider": lead_provider,
|
|
2984
|
-
"leadExecutable": launch.executable,
|
|
2985
|
-
"leadSessionId": ctx["CLAUDE_SESSION_ID"],
|
|
2986
|
-
"leadModelExecutionValue": ctx["LEAD_MODEL_EXECUTION_VALUE"],
|
|
2987
|
-
"projectRoot": ctx["PROJECT_ROOT"],
|
|
2988
|
-
"promptFile": str(prompt_file),
|
|
2989
|
-
# Non-empty only when starting this lead lowers a protection the
|
|
2990
|
-
# user should agree to first. The front end asks; okstra does not
|
|
2991
|
-
# waive it silently.
|
|
2992
|
-
"sandboxWaiverNote": launch.sandbox_waiver_note,
|
|
2993
|
-
"launchArgv": lead_launch_argv(
|
|
2994
|
-
lead_provider,
|
|
2995
|
-
model=ctx["LEAD_MODEL_EXECUTION_VALUE"],
|
|
2996
|
-
session_id=ctx["CLAUDE_SESSION_ID"],
|
|
2997
|
-
prompt=prompt_file.read_text(encoding="utf-8"),
|
|
2998
|
-
),
|
|
2999
|
-
}
|
|
2993
|
+
machine = _lead_launch_payload(ctx)
|
|
3000
2994
|
print(f"__OKSTRA_LAUNCH__ {json.dumps(machine)}")
|
|
3001
2995
|
return 0
|
|
3002
2996
|
|
|
3003
2997
|
|
|
2998
|
+
def _lead_launch_payload(ctx: dict[str, object]) -> dict[str, object]:
|
|
2999
|
+
lead_runtime = str(ctx.get("LEAD_RUNTIME", "claude-code"))
|
|
3000
|
+
lead_provider = str(ctx.get("LEAD_PROVIDER", ""))
|
|
3001
|
+
launch = lead_launch_spec(lead_provider)
|
|
3002
|
+
prompt_file = (
|
|
3003
|
+
Path(str(ctx["INSTRUCTION_SET_PATH"])) / "lead-execution-prompt.md"
|
|
3004
|
+
)
|
|
3005
|
+
launch_argv = lead_launch_argv(
|
|
3006
|
+
lead_provider,
|
|
3007
|
+
model=str(ctx["LEAD_MODEL_EXECUTION_VALUE"]),
|
|
3008
|
+
session_id=str(ctx["CLAUDE_SESSION_ID"]),
|
|
3009
|
+
prompt=prompt_file.read_text(encoding="utf-8"),
|
|
3010
|
+
)
|
|
3011
|
+
launch_prefix_size = 1 + len(launch.sandbox_waiver)
|
|
3012
|
+
return {
|
|
3013
|
+
"leadRuntime": lead_runtime,
|
|
3014
|
+
"leadProvider": lead_provider,
|
|
3015
|
+
"leadExecutable": launch.executable,
|
|
3016
|
+
"leadSessionId": ctx["CLAUDE_SESSION_ID"],
|
|
3017
|
+
"leadModelExecutionValue": ctx["LEAD_MODEL_EXECUTION_VALUE"],
|
|
3018
|
+
"projectRoot": ctx["PROJECT_ROOT"],
|
|
3019
|
+
"promptFile": str(prompt_file),
|
|
3020
|
+
"sandboxWaiverNote": launch.sandbox_waiver_note,
|
|
3021
|
+
"launchArgv": launch_argv,
|
|
3022
|
+
"launchRequestArgv": launch_argv[launch_prefix_size:],
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
|
|
3004
3026
|
if __name__ == "__main__":
|
|
3005
3027
|
import sys
|
|
3006
3028
|
raise SystemExit(main(sys.argv[1:]))
|
|
@@ -186,7 +186,7 @@ cat >&2 <<EOF
|
|
|
186
186
|
EOF
|
|
187
187
|
|
|
188
188
|
cd "$PROJECT_ROOT"
|
|
189
|
-
exec
|
|
189
|
+
exec "$HOME/.okstra/bin/okstra.sh" --resume-session "$SESSION_ID" --lead-runtime claude-code --project-root "$PROJECT_ROOT"
|
|
190
190
|
"""
|
|
191
191
|
resume_command_path.write_text(body, encoding="utf-8")
|
|
192
192
|
os.chmod(resume_command_path, 0o755)
|