okstra 0.167.0 → 0.169.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.
Files changed (102) hide show
  1. package/README.md +6 -5
  2. package/docs/architecture/storage-model.md +57 -1
  3. package/docs/architecture.md +70 -2
  4. package/docs/cli.md +8 -4
  5. package/docs/for-ai/skills/okstra-code-review.md +3 -2
  6. package/docs/for-ai/skills/okstra-schedule-gen.md +3 -1
  7. package/docs/pr-template-usage.md +10 -6
  8. package/docs/project-structure-overview.md +14 -11
  9. package/package.json +1 -1
  10. package/runtime/BUILD.json +2 -2
  11. package/runtime/agents/workers/claude-worker.md +6 -5
  12. package/runtime/agents/workers/report-writer-worker.md +9 -4
  13. package/runtime/agents/workers/translator-worker.md +6 -4
  14. package/runtime/prompts/coding-preflight/architectures/hexagonal.md +27 -1
  15. package/runtime/prompts/coding-preflight/clean-code.md +13 -0
  16. package/runtime/prompts/duties/acceptance-critic.md +24 -0
  17. package/runtime/prompts/duties/acceptance-verifier.md +24 -0
  18. package/runtime/prompts/duties/analysis-worker.md +24 -0
  19. package/runtime/prompts/duties/code-reviewer.md +24 -0
  20. package/runtime/prompts/duties/common.md +35 -0
  21. package/runtime/prompts/duties/implementation-executor.md +24 -0
  22. package/runtime/prompts/duties/implementation-verifier.md +24 -0
  23. package/runtime/prompts/duties/lead.md +24 -0
  24. package/runtime/prompts/duties/report-writer.md +24 -0
  25. package/runtime/prompts/duties/reverification-worker.md +24 -0
  26. package/runtime/prompts/duties/schedule-verifier.md +24 -0
  27. package/runtime/prompts/duties/scope-critic.md +24 -0
  28. package/runtime/prompts/duties/translator.md +24 -0
  29. package/runtime/prompts/lead/convergence.md +51 -7
  30. package/runtime/prompts/lead/okstra-lead-contract.md +10 -20
  31. package/runtime/prompts/lead/plan-body-verification.md +16 -1
  32. package/runtime/prompts/lead/report-writer.md +20 -5
  33. package/runtime/prompts/lead/team-contract.md +13 -13
  34. package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
  35. package/runtime/prompts/profiles/_implementation-diff-review.md +3 -1
  36. package/runtime/prompts/profiles/_implementation-executor.md +1 -1
  37. package/runtime/prompts/profiles/_implementation-verifier.md +3 -1
  38. package/runtime/prompts/profiles/implementation.md +4 -2
  39. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +6 -0
  40. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/relay.md +3 -2
  41. package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +8 -0
  42. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +33 -0
  43. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +13 -12
  44. package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +6 -0
  45. package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +3 -2
  46. package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +2 -0
  47. package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +3 -3
  48. package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +6 -0
  49. package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +2 -1
  50. package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +6 -0
  51. package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +2 -1
  52. package/runtime/python/okstra_ctl/agent_invocation.py +1502 -0
  53. package/runtime/python/okstra_ctl/agent_prompt_cli.py +788 -0
  54. package/runtime/python/okstra_ctl/codex_dispatch.py +2 -107
  55. package/runtime/python/okstra_ctl/context_cost.py +46 -5
  56. package/runtime/python/okstra_ctl/dispatch_core.py +312 -37
  57. package/runtime/python/okstra_ctl/dispatch_state.py +461 -36
  58. package/runtime/python/okstra_ctl/doctor.py +150 -16
  59. package/runtime/python/okstra_ctl/entrypoints/hosts.py +87 -9
  60. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +214 -23
  61. package/runtime/python/okstra_ctl/path_hints.py +26 -0
  62. package/runtime/python/okstra_ctl/paths.py +20 -0
  63. package/runtime/python/okstra_ctl/ports/__init__.py +8 -0
  64. package/runtime/python/okstra_ctl/ports/host.py +3 -0
  65. package/runtime/python/okstra_ctl/ports/host_model.py +60 -0
  66. package/runtime/python/okstra_ctl/pr_template.py +3 -6
  67. package/runtime/python/okstra_ctl/registry/host_registry.py +5 -0
  68. package/runtime/python/okstra_ctl/render.py +217 -12
  69. package/runtime/python/okstra_ctl/report_finalize.py +44 -0
  70. package/runtime/python/okstra_ctl/run.py +368 -51
  71. package/runtime/python/okstra_ctl/session.py +16 -12
  72. package/runtime/python/okstra_ctl/team.py +11 -11
  73. package/runtime/python/okstra_ctl/worker_dispatch.py +104 -0
  74. package/runtime/python/okstra_ctl/worker_prompt_body.py +5 -38
  75. package/runtime/python/okstra_ctl/worker_prompt_contract.py +32 -2
  76. package/runtime/python/okstra_ctl/worker_prompt_policy.py +38 -1
  77. package/runtime/skills/okstra-code-review/SKILL.md +22 -3
  78. package/runtime/skills/okstra-run/SKILL.md +16 -1
  79. package/runtime/skills/okstra-schedule-gen/SKILL.md +15 -1
  80. package/runtime/templates/implementation-worker-preamble.md +0 -10
  81. package/runtime/templates/report-writer-prompt-preamble.md +0 -9
  82. package/runtime/templates/reports/settings.template.json +0 -11
  83. package/runtime/templates/worker-prompt-preamble.md +0 -10
  84. package/runtime/validators/lib/fixtures.sh +93 -0
  85. package/runtime/validators/lib/validate-assets.sh +0 -8
  86. package/runtime/validators/validate-run.py +182 -0
  87. package/src/cli-registry.mjs +14 -0
  88. package/src/commands/execute/agent-prompt.mjs +25 -0
  89. package/src/commands/execute/codex-dispatch.mjs +6 -63
  90. package/src/commands/execute/worker-dispatch.mjs +76 -0
  91. package/src/commands/lifecycle/doctor.mjs +18 -3
  92. package/src/commands/lifecycle/install.mjs +33 -15
  93. package/src/commands/lifecycle/uninstall.mjs +4 -3
  94. package/src/lib/install-assets.mjs +9 -0
  95. package/runtime/agents/workers/antigravity-worker.md +0 -259
  96. package/runtime/agents/workers/codex-worker.md +0 -259
  97. package/runtime/agents/workers/grok-worker.md +0 -259
  98. package/runtime/agents/workers/kimi-worker.md +0 -259
  99. package/runtime/prompts/coding-preflight/scripts/preedit-check.sh +0 -79
  100. package/runtime/templates/operating-standard.md +0 -22
  101. package/src/lib/worker-agent-render.mjs +0 -50
  102. /package/runtime/templates/{prd → pr}/pr-body.template.md +0 -0
@@ -2,6 +2,7 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import json
5
+ import os
5
6
  import subprocess
6
7
  from dataclasses import asdict, dataclass
7
8
  from pathlib import Path
@@ -10,6 +11,7 @@ from typing import Iterable
10
11
  from okstra_project import ResolverError, project_json_path, resolve_project_root
11
12
 
12
13
  from . import improvement_lenses, worktree_registry
14
+ from .models import provider_wrappers
13
15
  from .workers import resolve_profile_workers
14
16
  from .worktree import is_git_work_tree, main_worktree_path
15
17
 
@@ -39,6 +41,7 @@ def phase_diagnostics(
39
41
  cwd: str | Path,
40
42
  workspace_root: str | Path,
41
43
  home: str | Path,
44
+ host_runtime: str,
42
45
  ) -> dict[str, object]:
43
46
  """Return JSON-serialisable readiness checks for one lifecycle phase."""
44
47
  if phase not in SUPPORTED_PHASES:
@@ -56,7 +59,11 @@ def phase_diagnostics(
56
59
  checks.append(_profile_check(workspace, phase))
57
60
 
58
61
  if project_root is not None:
59
- checks.extend(_phase_checks(phase, project_root, workspace, home_path))
62
+ checks.extend(
63
+ _phase_checks(
64
+ phase, project_root, workspace, home_path, host_runtime
65
+ )
66
+ )
60
67
 
61
68
  return {
62
69
  "ok": all(check.ok for check in checks),
@@ -89,9 +96,68 @@ def _project_checks(cwd: Path) -> tuple[Path | None, list[DoctorCheck]]:
89
96
  f"{project_json} not found — run okstra setup in this project first",
90
97
  )
91
98
  )
99
+ architecture = _architecture_declaration_check(project_root)
100
+ if architecture is not None:
101
+ checks.append(architecture)
92
102
  return project_root, checks
93
103
 
94
104
 
105
+ _LAYOUT_SCAN_SKIP = frozenset(
106
+ {".git", ".okstra", "node_modules", "dist", "build", "coverage", "__pycache__"}
107
+ )
108
+
109
+
110
+ def _ports_and_adapters_marker(project_root: Path) -> str:
111
+ """The first ports-and-adapters layout signal found, or "".
112
+
113
+ Only the two signals the pack states mechanically (`architectures/hexagonal.md`
114
+ Stage 3 row): a `ports/` directory beside a `domain/` one, and a `*.port.*`
115
+ file. The other two — NestJS hex split, abstract classes at a boundary — are
116
+ worker judgment and are deliberately not guessed at here.
117
+ """
118
+ for current, dirnames, filenames in os.walk(project_root):
119
+ dirnames[:] = [d for d in dirnames if d not in _LAYOUT_SCAN_SKIP]
120
+ here = set(dirnames)
121
+ if "ports" in here and "domain" in here:
122
+ return str(Path(current).relative_to(project_root) / "ports")
123
+ for name in filenames:
124
+ if ".port." in name:
125
+ return str(Path(current).relative_to(project_root) / name)
126
+ return ""
127
+
128
+
129
+ def _architecture_declaration_check(project_root: Path) -> DoctorCheck | None:
130
+ """Report placement rules running advisory because no style was declared.
131
+
132
+ The routed coding-preflight pack promotes its placement rules from advisory to
133
+ blocking only under `architecture.style` in `project.json`
134
+ (`architectures/hexagonal.md` Severity). With the key absent they stay advisory,
135
+ a verifier records `architecture-style: none`, and the run reads as fully gated
136
+ — the downgrade is invisible to both lead and user. A project that genuinely is
137
+ not ports-and-adapters silences this by declaring `none`, which is why the raw
138
+ value is read here: `resolve_architecture()` maps absent and `none` alike.
139
+ """
140
+ try:
141
+ payload = json.loads(project_json_path(project_root).read_text(encoding="utf-8"))
142
+ except (OSError, ValueError):
143
+ return None
144
+ architecture = payload.get("architecture") if isinstance(payload, dict) else None
145
+ declared = architecture.get("style") if isinstance(architecture, dict) else None
146
+ if isinstance(declared, str) and declared.strip():
147
+ return _ok("architecture style", f"declared: {declared.strip()}")
148
+ marker = _ports_and_adapters_marker(project_root)
149
+ if not marker:
150
+ return None
151
+ return _ok(
152
+ "architecture style",
153
+ f"not declared, but the layout shows a ports-and-adapters signal ({marker}) "
154
+ "— placement rules run advisory, so a misplaced domain object or a concrete "
155
+ "adapter injection is recorded rather than blocking. Declare "
156
+ '`architecture: {"style": "hexagonal"}` in .okstra/project.json to gate '
157
+ 'them, or `"none"` to say the layout is not ports-and-adapters.',
158
+ )
159
+
160
+
95
161
  def _profile_check(workspace: Path, phase: str) -> DoctorCheck:
96
162
  profile = _profile_path(workspace, phase)
97
163
  if profile.is_file():
@@ -104,15 +170,18 @@ def _phase_checks(
104
170
  project_root: Path,
105
171
  workspace: Path,
106
172
  home: Path,
173
+ host_runtime: str,
107
174
  ) -> list[DoctorCheck]:
108
175
  if phase == "implementation":
109
- return _implementation_checks(project_root, workspace, home)
176
+ return _implementation_checks(
177
+ project_root, workspace, home, host_runtime
178
+ )
110
179
  if phase == "final-verification":
111
180
  return _final_verification_checks(workspace)
112
181
  if phase == "release-handoff":
113
182
  return _release_handoff_checks(project_root)
114
183
  if phase == "improvement-discovery":
115
- return _improvement_discovery_checks(workspace, home)
184
+ return _improvement_discovery_checks(workspace, home, host_runtime)
116
185
  return []
117
186
 
118
187
 
@@ -120,12 +189,13 @@ def _implementation_checks(
120
189
  project_root: Path,
121
190
  workspace: Path,
122
191
  home: Path,
192
+ host_runtime: str,
123
193
  ) -> list[DoctorCheck]:
124
194
  return [
125
195
  _git_work_tree_check(project_root),
126
196
  _git_worktree_support_check(project_root),
127
197
  _approved_plan_validator_check(),
128
- _worker_agents_check(home, workspace, "implementation"),
198
+ *_worker_dispatch_checks(home, workspace, "implementation", host_runtime),
129
199
  ]
130
200
 
131
201
 
@@ -153,10 +223,15 @@ def _release_handoff_checks(project_root: Path) -> list[DoctorCheck]:
153
223
  ]
154
224
 
155
225
 
156
- def _improvement_discovery_checks(workspace: Path, home: Path) -> list[DoctorCheck]:
226
+ def _improvement_discovery_checks(
227
+ workspace: Path,
228
+ home: Path,
229
+ host_runtime: str,
230
+ ) -> list[DoctorCheck]:
157
231
  return [
158
- _worker_agents_check(home, workspace, "improvement-discovery"),
159
- _file_check("antigravity worker agent", _agent_path(home, "antigravity")),
232
+ *_worker_dispatch_checks(
233
+ home, workspace, "improvement-discovery", host_runtime
234
+ ),
160
235
  _lens_whitelist_check(),
161
236
  ]
162
237
 
@@ -182,18 +257,77 @@ def _approved_plan_validator_check() -> DoctorCheck:
182
257
  return _ok("approved-plan validator", "okstra plan-validate is available")
183
258
 
184
259
 
185
- def _worker_agents_check(home: Path, workspace: Path, phase: str) -> DoctorCheck:
260
+ def _worker_dispatch_checks(
261
+ home: Path,
262
+ workspace: Path,
263
+ phase: str,
264
+ host_runtime: str,
265
+ ) -> list[DoctorCheck]:
186
266
  workers = resolve_profile_workers(_profile_path(workspace, phase))
187
267
  if not workers:
188
- return _ok("worker agents", "no worker agents required")
189
- missing = [worker for worker in workers if not _agent_path(home, worker).is_file()]
190
- roster = ",".join(workers)
191
- if missing:
192
- return _fail(
193
- "worker agents",
194
- f"missing {','.join(missing)}; profile requires {roster}",
268
+ return [_ok("worker dispatch", "no workers required")]
269
+ if not host_runtime:
270
+ return [_fail("worker dispatch", "resolved host runtime is required")]
271
+
272
+ native_provider = {
273
+ "claude-code": "claude",
274
+ "codex": "codex",
275
+ "antigravity": "antigravity",
276
+ "grok": "grok",
277
+ "kimi": "kimi",
278
+ }.get(host_runtime)
279
+ providers = ["claude" if worker == "report-writer" else worker for worker in workers]
280
+ checks: list[DoctorCheck] = []
281
+
282
+ if host_runtime == "claude-code":
283
+ native_agents = []
284
+ if "claude" in workers:
285
+ native_agents.append("claude")
286
+ if "report-writer" in workers:
287
+ native_agents.append("report-writer")
288
+ for agent in native_agents:
289
+ checks.append(
290
+ _file_check(
291
+ f"host-native agent: {agent}", _agent_path(home, agent)
292
+ )
293
+ )
294
+
295
+ cli_providers = sorted({
296
+ provider for provider in providers if provider != native_provider
297
+ })
298
+ if cli_providers:
299
+ dispatch_module = _first_file(
300
+ workspace / "scripts" / "okstra_ctl" / "worker_dispatch.py",
301
+ workspace / "python" / "okstra_ctl" / "worker_dispatch.py",
195
302
  )
196
- return _ok("worker agents", f"installed: {roster}")
303
+ checks.append(
304
+ _file_check("worker-dispatch entrypoint", dispatch_module)
305
+ )
306
+ wrappers = provider_wrappers("analyser")
307
+ for provider in cli_providers:
308
+ wrapper = wrappers.get(provider, "")
309
+ if not wrapper:
310
+ checks.append(
311
+ _fail(
312
+ f"CLI provider strategy: {provider}",
313
+ "provider registry has no analyser wrapper",
314
+ )
315
+ )
316
+ continue
317
+ path = _first_file(
318
+ workspace / "scripts" / wrapper,
319
+ workspace / "bin" / wrapper,
320
+ )
321
+ checks.append(_file_check(f"CLI provider strategy: {provider}", path))
322
+ if not checks:
323
+ checks.append(
324
+ _ok("worker dispatch", f"native host owns roster on {host_runtime}")
325
+ )
326
+ return checks
327
+
328
+
329
+ def _first_file(*paths: Path) -> Path:
330
+ return next((path for path in paths if path.is_file()), paths[0])
197
331
 
198
332
 
199
333
  def _worktree_registry_check() -> DoctorCheck:
@@ -9,6 +9,14 @@ from contextlib import contextmanager
9
9
  from dataclasses import asdict
10
10
  from pathlib import Path
11
11
 
12
+ from ..agent_invocation import (
13
+ AgentInvocationError,
14
+ AgentModelAssignment,
15
+ agent_model_assignment_from_payload,
16
+ verify_agent_invocation,
17
+ )
18
+ from ..dispatch_state import DispatchError, record_verified_agent_dispatch
19
+
12
20
  from ..application.start_run import start_run
13
21
  from ..application.resume_run import resume_run
14
22
  from ..domain.host import (
@@ -24,6 +32,7 @@ from ..domain.host import (
24
32
  ProviderUnavailable,
25
33
  )
26
34
  from ..registry.host_registry import HostAdapterRegistry, default_host_registry
35
+ from ..models import lead_launch_argv, lead_launch_spec
27
36
 
28
37
 
29
38
  _ENTRYPOINT_ERRORS = (
@@ -33,6 +42,7 @@ _ENTRYPOINT_ERRORS = (
33
42
  HostUnavailable,
34
43
  ProviderNotRegistered,
35
44
  ProviderUnavailable,
45
+ DispatchError,
36
46
  ValueError,
37
47
  )
38
48
 
@@ -252,9 +262,24 @@ def _launch_plan_payload(
252
262
  available_functions=frozenset(),
253
263
  interaction_surface="terminal",
254
264
  )
255
- project_root, request_argv = _launch_request(args, adapter.descriptor.id)
265
+ project_root, request_argv, dispatch_authority = _launch_request(
266
+ args, adapter.descriptor.id
267
+ )
256
268
  request = LeadStartRequest(project_root, "", request_argv)
257
269
  plan = start_run(request, context, adapter)
270
+ if dispatch_authority is not None:
271
+ run_manifest_path, metadata_path, _assignment = dispatch_authority
272
+ record_verified_agent_dispatch(
273
+ project_root=project_root,
274
+ run_manifest_path=run_manifest_path,
275
+ metadata_path=metadata_path,
276
+ enforcement_mode=(
277
+ "core-pre-dispatch"
278
+ if plan.mode == "spawn-process"
279
+ else "host-native-spec-link-gate"
280
+ ),
281
+ allow_native_core=plan.mode == "spawn-process",
282
+ )
258
283
  return {"ok": True, "_project_root": str(project_root), **asdict(plan)}
259
284
 
260
285
 
@@ -283,9 +308,13 @@ def _resume_plan_payload(
283
308
  def _launch_request(
284
309
  args: argparse.Namespace,
285
310
  canonical_host_id: str,
286
- ) -> tuple[Path, tuple[str, ...]]:
311
+ ) -> tuple[
312
+ Path,
313
+ tuple[str, ...],
314
+ tuple[Path, Path, AgentModelAssignment] | None,
315
+ ]:
287
316
  if not args.prepared_launch_json:
288
- return Path(args.project_root), ()
317
+ return Path(args.project_root), (), None
289
318
  try:
290
319
  prepared = json.loads(args.prepared_launch_json)
291
320
  except json.JSONDecodeError as exc:
@@ -295,14 +324,63 @@ def _launch_request(
295
324
  if prepared.get("leadRuntime") != canonical_host_id:
296
325
  raise ValueError("prepared launch runtime does not match selected host")
297
326
  project_root = prepared.get("projectRoot")
298
- request_argv = prepared.get("launchRequestArgv")
299
327
  if not isinstance(project_root, str) or not project_root:
300
328
  raise ValueError("prepared launch is missing projectRoot")
301
- if not isinstance(request_argv, list) or not all(
302
- isinstance(arg, str) for arg in request_argv
303
- ):
304
- raise ValueError("prepared launch is missing launchRequestArgv")
305
- return Path(project_root), tuple(request_argv)
329
+ root = Path(project_root)
330
+ run_manifest_value = prepared.get("runManifestPath")
331
+ metadata_value = prepared.get("leadPromptMetadataPath")
332
+ if not isinstance(run_manifest_value, str) or not run_manifest_value:
333
+ raise ValueError("prepared launch is missing runManifestPath")
334
+ if not isinstance(metadata_value, str) or not metadata_value:
335
+ raise ValueError("prepared launch is missing leadPromptMetadataPath")
336
+ run_manifest_path = Path(run_manifest_value)
337
+ metadata_path = Path(metadata_value)
338
+ try:
339
+ manifest = json.loads(run_manifest_path.read_text(encoding="utf-8"))
340
+ resources = manifest["resources"]
341
+ assignment = agent_model_assignment_from_payload(
342
+ manifest["invocationAssignments"]["lead"]
343
+ )
344
+ prompt_path = root / resources["leadExecutionPromptPath"]
345
+ canonical_metadata = root / resources["leadPromptMetadataPath"]
346
+ metadata = json.loads(canonical_metadata.read_text(encoding="utf-8"))
347
+ if metadata["prompt"]["path"] != resources["leadExecutionPromptPath"]:
348
+ raise KeyError("lead prompt resource does not match metadata")
349
+ except (OSError, json.JSONDecodeError, KeyError, TypeError, AgentInvocationError) as exc:
350
+ raise ValueError("lead invocation authority is incomplete") from exc
351
+ if metadata_path.resolve(strict=False) != canonical_metadata.resolve(strict=False):
352
+ raise ValueError("prepared lead metadata does not match run manifest")
353
+ if prepared.get("leadProvider") != assignment.provider:
354
+ raise ValueError("prepared lead provider does not match run manifest")
355
+ if prepared.get("leadModelExecutionValue") != assignment.model_execution_value:
356
+ raise ValueError("prepared lead model does not match run manifest")
357
+ errors = verify_agent_invocation(
358
+ metadata_path,
359
+ project_root=root,
360
+ expected_run_manifest_path=run_manifest_path,
361
+ expected_assignment=assignment,
362
+ expected_worker_id="lead",
363
+ expected_assignment_ref="lead",
364
+ expected_audience="lead",
365
+ )
366
+ if errors:
367
+ raise ValueError("lead invocation verification failed: " + "; ".join(errors))
368
+ session_id = prepared.get("leadSessionId")
369
+ if not isinstance(session_id, str):
370
+ raise ValueError("prepared launch is missing leadSessionId")
371
+ argv = lead_launch_argv(
372
+ assignment.provider,
373
+ model=assignment.model_execution_value,
374
+ session_id=session_id,
375
+ prompt=prompt_path.read_text(encoding="utf-8"),
376
+ )
377
+ launch = lead_launch_spec(assignment.provider)
378
+ prefix_size = 1 + len(launch.sandbox_waiver)
379
+ return (
380
+ root,
381
+ tuple(argv[prefix_size:]),
382
+ (run_manifest_path, canonical_metadata, assignment),
383
+ )
306
384
 
307
385
 
308
386
  def _write_null_launch_plan(payload: dict[str, object]) -> None: