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.
- package/README.md +6 -5
- package/docs/architecture/storage-model.md +57 -1
- package/docs/architecture.md +70 -2
- package/docs/cli.md +8 -4
- package/docs/for-ai/skills/okstra-code-review.md +3 -2
- package/docs/for-ai/skills/okstra-schedule-gen.md +3 -1
- package/docs/pr-template-usage.md +10 -6
- package/docs/project-structure-overview.md +14 -11
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +6 -5
- package/runtime/agents/workers/report-writer-worker.md +9 -4
- package/runtime/agents/workers/translator-worker.md +6 -4
- package/runtime/prompts/coding-preflight/architectures/hexagonal.md +27 -1
- package/runtime/prompts/coding-preflight/clean-code.md +13 -0
- package/runtime/prompts/duties/acceptance-critic.md +24 -0
- package/runtime/prompts/duties/acceptance-verifier.md +24 -0
- package/runtime/prompts/duties/analysis-worker.md +24 -0
- package/runtime/prompts/duties/code-reviewer.md +24 -0
- package/runtime/prompts/duties/common.md +35 -0
- package/runtime/prompts/duties/implementation-executor.md +24 -0
- package/runtime/prompts/duties/implementation-verifier.md +24 -0
- package/runtime/prompts/duties/lead.md +24 -0
- package/runtime/prompts/duties/report-writer.md +24 -0
- package/runtime/prompts/duties/reverification-worker.md +24 -0
- package/runtime/prompts/duties/schedule-verifier.md +24 -0
- package/runtime/prompts/duties/scope-critic.md +24 -0
- package/runtime/prompts/duties/translator.md +24 -0
- package/runtime/prompts/lead/convergence.md +51 -7
- package/runtime/prompts/lead/okstra-lead-contract.md +10 -20
- package/runtime/prompts/lead/plan-body-verification.md +16 -1
- package/runtime/prompts/lead/report-writer.md +20 -5
- package/runtime/prompts/lead/team-contract.md +13 -13
- package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
- package/runtime/prompts/profiles/_implementation-diff-review.md +3 -1
- package/runtime/prompts/profiles/_implementation-executor.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -1
- package/runtime/prompts/profiles/implementation.md +4 -2
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +6 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/relay.md +3 -2
- package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +8 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +33 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +13 -12
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +6 -0
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +3 -2
- package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +2 -0
- package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +3 -3
- package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +6 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +2 -1
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +6 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +2 -1
- package/runtime/python/okstra_ctl/agent_invocation.py +1502 -0
- package/runtime/python/okstra_ctl/agent_prompt_cli.py +788 -0
- package/runtime/python/okstra_ctl/codex_dispatch.py +2 -107
- package/runtime/python/okstra_ctl/context_cost.py +46 -5
- package/runtime/python/okstra_ctl/dispatch_core.py +312 -37
- package/runtime/python/okstra_ctl/dispatch_state.py +461 -36
- package/runtime/python/okstra_ctl/doctor.py +150 -16
- package/runtime/python/okstra_ctl/entrypoints/hosts.py +87 -9
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +214 -23
- package/runtime/python/okstra_ctl/path_hints.py +26 -0
- package/runtime/python/okstra_ctl/paths.py +20 -0
- package/runtime/python/okstra_ctl/ports/__init__.py +8 -0
- package/runtime/python/okstra_ctl/ports/host.py +3 -0
- package/runtime/python/okstra_ctl/ports/host_model.py +60 -0
- package/runtime/python/okstra_ctl/pr_template.py +3 -6
- package/runtime/python/okstra_ctl/registry/host_registry.py +5 -0
- package/runtime/python/okstra_ctl/render.py +217 -12
- package/runtime/python/okstra_ctl/report_finalize.py +44 -0
- package/runtime/python/okstra_ctl/run.py +368 -51
- package/runtime/python/okstra_ctl/session.py +16 -12
- package/runtime/python/okstra_ctl/team.py +11 -11
- package/runtime/python/okstra_ctl/worker_dispatch.py +104 -0
- package/runtime/python/okstra_ctl/worker_prompt_body.py +5 -38
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +32 -2
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +38 -1
- package/runtime/skills/okstra-code-review/SKILL.md +22 -3
- package/runtime/skills/okstra-run/SKILL.md +16 -1
- package/runtime/skills/okstra-schedule-gen/SKILL.md +15 -1
- package/runtime/templates/implementation-worker-preamble.md +0 -10
- package/runtime/templates/report-writer-prompt-preamble.md +0 -9
- package/runtime/templates/reports/settings.template.json +0 -11
- package/runtime/templates/worker-prompt-preamble.md +0 -10
- package/runtime/validators/lib/fixtures.sh +93 -0
- package/runtime/validators/lib/validate-assets.sh +0 -8
- package/runtime/validators/validate-run.py +182 -0
- package/src/cli-registry.mjs +14 -0
- package/src/commands/execute/agent-prompt.mjs +25 -0
- package/src/commands/execute/codex-dispatch.mjs +6 -63
- package/src/commands/execute/worker-dispatch.mjs +76 -0
- package/src/commands/lifecycle/doctor.mjs +18 -3
- package/src/commands/lifecycle/install.mjs +33 -15
- package/src/commands/lifecycle/uninstall.mjs +4 -3
- package/src/lib/install-assets.mjs +9 -0
- package/runtime/agents/workers/antigravity-worker.md +0 -259
- package/runtime/agents/workers/codex-worker.md +0 -259
- package/runtime/agents/workers/grok-worker.md +0 -259
- package/runtime/agents/workers/kimi-worker.md +0 -259
- package/runtime/prompts/coding-preflight/scripts/preedit-check.sh +0 -79
- package/runtime/templates/operating-standard.md +0 -22
- package/src/lib/worker-agent-render.mjs +0 -50
- /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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
226
|
+
def _improvement_discovery_checks(
|
|
227
|
+
workspace: Path,
|
|
228
|
+
home: Path,
|
|
229
|
+
host_runtime: str,
|
|
230
|
+
) -> list[DoctorCheck]:
|
|
157
231
|
return [
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
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
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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(
|
|
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[
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
)
|
|
304
|
-
|
|
305
|
-
|
|
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:
|