okstra 0.163.2 → 0.165.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/README.md +8 -6
  2. package/docs/architecture.md +24 -15
  3. package/docs/cli.md +15 -8
  4. package/docs/for-ai/README.md +2 -2
  5. package/docs/for-ai/skills/okstra-inspect.md +2 -2
  6. package/docs/for-ai/skills/okstra-user-response.md +2 -2
  7. package/docs/project-structure-overview.md +22 -14
  8. package/package.json +1 -1
  9. package/runtime/BUILD.json +2 -2
  10. package/runtime/agents/workers/antigravity-worker.md +9 -7
  11. package/runtime/agents/workers/claude-worker.md +1 -0
  12. package/runtime/agents/workers/codex-worker.md +9 -7
  13. package/runtime/agents/workers/grok-worker.md +6 -4
  14. package/runtime/agents/workers/kimi-worker.md +6 -4
  15. package/runtime/bin/lib/okstra/cli.sh +5 -0
  16. package/runtime/bin/lib/okstra/globals.sh +2 -0
  17. package/runtime/bin/lib/okstra/usage.sh +5 -5
  18. package/runtime/bin/okstra-antigravity-exec.sh +1 -340
  19. package/runtime/bin/okstra-claude-exec.sh +1 -178
  20. package/runtime/bin/okstra-codex-exec.sh +1 -467
  21. package/runtime/bin/okstra-provider-exec.py +165 -190
  22. package/runtime/bin/okstra-trace-cleanup.sh +14 -7
  23. package/runtime/bin/okstra-wrapper-status.py +26 -19
  24. package/runtime/bin/okstra.sh +87 -91
  25. package/runtime/prompts/lead/adapters/cmux.md +2 -2
  26. package/runtime/prompts/lead/convergence.md +36 -8
  27. package/runtime/prompts/lead/okstra-lead-contract.md +24 -1
  28. package/runtime/prompts/lead/plan-body-verification.md +9 -1
  29. package/runtime/prompts/lead/report-writer.md +1 -0
  30. package/runtime/prompts/lead/team-contract.md +3 -3
  31. package/runtime/prompts/profiles/_common-contract.md +9 -1
  32. package/runtime/prompts/profiles/_coverage-critic.md +1 -1
  33. package/runtime/prompts/profiles/_implementation-diff-review.md +3 -1
  34. package/runtime/prompts/profiles/_implementation-self-check.md +1 -1
  35. package/runtime/prompts/profiles/_implementation-verifier.md +3 -1
  36. package/runtime/prompts/profiles/implementation-planning.md +6 -4
  37. package/runtime/python/okstra_ctl/adapters/accounting/__init__.py +11 -0
  38. package/runtime/python/okstra_ctl/adapters/accounting/claude_jsonl.py +17 -0
  39. package/runtime/python/okstra_ctl/adapters/accounting/cli_artifact.py +17 -0
  40. package/runtime/python/okstra_ctl/adapters/accounting/unavailable.py +19 -0
  41. package/runtime/python/okstra_ctl/adapters/dispatch/__init__.py +92 -0
  42. package/runtime/python/okstra_ctl/adapters/dispatch/cli_wrapper.py +54 -0
  43. package/runtime/python/okstra_ctl/adapters/dispatch/cmux.py +68 -0
  44. package/runtime/python/okstra_ctl/adapters/dispatch/native_team.py +13 -0
  45. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +60 -0
  46. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/manifest.json +1 -0
  47. package/runtime/{prompts/lead/adapters/antigravity.md → python/okstra_ctl/adapters/hosts/antigravity/relay.md} +52 -0
  48. package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +292 -0
  49. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +120 -0
  50. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/manifest.json +1 -0
  51. package/runtime/{prompts/lead/adapters/claude-code.md → python/okstra_ctl/adapters/hosts/claude-code/relay.md} +112 -1
  52. package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +60 -0
  53. package/runtime/python/okstra_ctl/adapters/hosts/codex/manifest.json +1 -0
  54. package/runtime/{prompts/lead/adapters/codex.md → python/okstra_ctl/adapters/hosts/codex/relay.md} +52 -0
  55. package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +72 -0
  56. package/runtime/python/okstra_ctl/adapters/hosts/external/manifest.json +1 -0
  57. package/runtime/{prompts/lead/adapters/external.md → python/okstra_ctl/adapters/hosts/external/relay.md} +53 -1
  58. package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +63 -0
  59. package/runtime/python/okstra_ctl/adapters/hosts/grok/manifest.json +1 -0
  60. package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +90 -0
  61. package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +63 -0
  62. package/runtime/python/okstra_ctl/adapters/hosts/kimi/manifest.json +1 -0
  63. package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +90 -0
  64. package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +183 -0
  65. package/runtime/python/okstra_ctl/adapters/providers/antigravity/manifest.json +1 -0
  66. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +110 -0
  67. package/runtime/python/okstra_ctl/adapters/providers/claude/manifest.json +1 -0
  68. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +84 -0
  69. package/runtime/python/okstra_ctl/adapters/providers/codex/manifest.json +1 -0
  70. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +76 -0
  71. package/runtime/python/okstra_ctl/adapters/providers/grok/manifest.json +1 -0
  72. package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +80 -0
  73. package/runtime/python/okstra_ctl/adapters/providers/kimi/manifest.json +1 -0
  74. package/runtime/python/okstra_ctl/application/__init__.py +1 -0
  75. package/runtime/python/okstra_ctl/application/advance_wizard.py +25 -0
  76. package/runtime/python/okstra_ctl/application/collect_usage.py +15 -0
  77. package/runtime/python/okstra_ctl/application/dispatch_assignments.py +15 -0
  78. package/runtime/python/okstra_ctl/application/resolve_assignment.py +93 -0
  79. package/runtime/python/okstra_ctl/application/resume_run.py +21 -0
  80. package/runtime/python/okstra_ctl/application/start_run.py +21 -0
  81. package/runtime/python/okstra_ctl/codex_dispatch.py +49 -826
  82. package/runtime/python/okstra_ctl/dispatch_core.py +244 -29
  83. package/runtime/python/okstra_ctl/dispatch_state.py +27 -0
  84. package/runtime/python/okstra_ctl/domain/__init__.py +34 -0
  85. package/runtime/python/okstra_ctl/domain/host.py +100 -0
  86. package/runtime/python/okstra_ctl/domain/provider.py +70 -0
  87. package/runtime/python/okstra_ctl/domain/wizard/__init__.py +19 -0
  88. package/runtime/python/okstra_ctl/domain/wizard/interaction.py +140 -0
  89. package/runtime/python/okstra_ctl/domain/worker_exec.py +102 -0
  90. package/runtime/python/okstra_ctl/domain/worker_role.py +34 -0
  91. package/runtime/python/okstra_ctl/domain/worker_stream.py +261 -0
  92. package/runtime/python/okstra_ctl/entrypoints/__init__.py +1 -0
  93. package/runtime/python/okstra_ctl/entrypoints/hosts.py +334 -0
  94. package/runtime/python/okstra_ctl/incremental_scope.py +16 -4
  95. package/runtime/python/okstra_ctl/models.py +54 -269
  96. package/runtime/python/okstra_ctl/ports/__init__.py +15 -0
  97. package/runtime/python/okstra_ctl/ports/host.py +32 -0
  98. package/runtime/python/okstra_ctl/ports/interaction.py +15 -0
  99. package/runtime/python/okstra_ctl/ports/lead_session.py +25 -0
  100. package/runtime/python/okstra_ctl/ports/usage_accounting.py +23 -0
  101. package/runtime/python/okstra_ctl/ports/worker_dispatch.py +32 -0
  102. package/runtime/python/okstra_ctl/registry/__init__.py +13 -0
  103. package/runtime/python/okstra_ctl/registry/factory_loader.py +32 -0
  104. package/runtime/python/okstra_ctl/registry/host_discovery.py +124 -0
  105. package/runtime/python/okstra_ctl/registry/host_registry.py +365 -0
  106. package/runtime/python/okstra_ctl/registry/provider_registry.py +149 -0
  107. package/runtime/python/okstra_ctl/render.py +145 -47
  108. package/runtime/python/okstra_ctl/report_html/common.py +71 -25
  109. package/runtime/python/okstra_ctl/report_html/models.py +5 -0
  110. package/runtime/python/okstra_ctl/report_html/render.py +1 -1
  111. package/runtime/python/okstra_ctl/report_html/run_usage.py +19 -0
  112. package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +14 -0
  113. package/runtime/python/okstra_ctl/report_views.py +44 -16
  114. package/runtime/python/okstra_ctl/run.py +80 -58
  115. package/runtime/python/okstra_ctl/session.py +1 -1
  116. package/runtime/python/okstra_ctl/stage_citations.py +52 -15
  117. package/runtime/python/okstra_ctl/team.py +44 -32
  118. package/runtime/python/okstra_ctl/user_response.py +45 -29
  119. package/runtime/python/okstra_ctl/wizard.py +175 -73
  120. package/runtime/python/okstra_ctl/worker_audit_ledger.py +29 -4
  121. package/runtime/python/okstra_ctl/worker_prompt_policy.py +10 -3
  122. package/runtime/python/okstra_ctl/worker_request.py +140 -0
  123. package/runtime/python/okstra_ctl/worker_runner.py +622 -0
  124. package/runtime/python/okstra_token_usage/collect.py +42 -7
  125. package/runtime/python/okstra_token_usage/report.py +42 -0
  126. package/runtime/python/okstra_token_usage/task_totals.py +88 -0
  127. package/runtime/schemas/final-report-v1.0.schema.json +4040 -1066
  128. package/runtime/schemas/final-report-v2.0.schema.json +5673 -1412
  129. package/runtime/skills/okstra-inspect/SKILL.md +1 -2
  130. package/runtime/skills/okstra-inspect/facets/logs.md +5 -5
  131. package/runtime/skills/okstra-inspect/facets/run-audit.md +3 -3
  132. package/runtime/skills/okstra-run/SKILL.md +74 -29
  133. package/runtime/skills/okstra-user-response/SKILL.md +15 -5
  134. package/runtime/templates/implementation-worker-preamble.md +1 -1
  135. package/runtime/templates/report-writer-prompt-preamble.md +1 -0
  136. package/runtime/templates/reports/final-report.template.md +3 -3
  137. package/runtime/templates/reports/html/assets/base.css +8 -4
  138. package/runtime/templates/reports/html/base.template.html +12 -6
  139. package/runtime/templates/reports/html/i18n/en.json +32 -7
  140. package/runtime/templates/reports/html/i18n/ko.json +32 -7
  141. package/runtime/templates/reports/html/macros/forms.html +9 -3
  142. package/runtime/templates/reports/html/tasks/implementation-planning.template.html +14 -19
  143. package/runtime/templates/reports/report.js +59 -26
  144. package/runtime/templates/reports/user-response.template.md +12 -8
  145. package/runtime/templates/worker-prompt-preamble.md +1 -1
  146. package/runtime/validators/validate-implementation-plan-stages.py +17 -22
  147. package/runtime/validators/validate-report-views.py +0 -39
  148. package/runtime/validators/validate-run.py +96 -14
  149. package/runtime/validators/validate_session_conformance.py +69 -3
  150. package/src/cli-registry.mjs +0 -7
  151. package/src/commands/execute/render-bundle.mjs +4 -4
  152. package/src/commands/execute/run.mjs +8 -25
  153. package/src/commands/execute/wizard.mjs +33 -13
  154. package/src/commands/lifecycle/doctor.mjs +10 -10
  155. package/src/commands/lifecycle/install.mjs +53 -30
  156. package/src/commands/lifecycle/preflight.mjs +14 -4
  157. package/src/lib/host-registry-client.mjs +176 -0
  158. package/src/lib/runtime-manifest.mjs +6 -8
  159. package/runtime/bin/okstra-wrapper-agy-stream.py +0 -61
  160. package/runtime/python/okstra_ctl/error_issue.py +0 -640
  161. package/runtime/python/okstra_ctl/issue_signals.py +0 -186
  162. package/runtime/python/okstra_ctl/lead_runtime.py +0 -115
  163. package/runtime/python/okstra_ctl/runner_resolution.py +0 -103
  164. package/runtime/skills/okstra-inspect/facets/error-issue.md +0 -77
  165. package/src/commands/inspect/error-issue.mjs +0 -27
  166. package/src/lib/runtime-readiness.mjs +0 -90
  167. package/src/lib/runtime-resolver.mjs +0 -123
@@ -0,0 +1,334 @@
1
+ """JSON entrypoint for host catalog, resolution, and launch plans."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import os
7
+ import sys
8
+ from contextlib import contextmanager
9
+ from dataclasses import asdict
10
+ from pathlib import Path
11
+
12
+ from ..application.start_run import start_run
13
+ from ..application.resume_run import resume_run
14
+ from ..domain.host import (
15
+ HostAdapterContractError,
16
+ HostCapabilityMismatch,
17
+ HostNotRegistered,
18
+ HostResolutionContext,
19
+ HostSessionContext,
20
+ HostUnavailable,
21
+ LeadStartRequest,
22
+ LeadResumeRequest,
23
+ ProviderNotRegistered,
24
+ ProviderUnavailable,
25
+ )
26
+ from ..registry.host_registry import HostAdapterRegistry, default_host_registry
27
+
28
+
29
+ _ENTRYPOINT_ERRORS = (
30
+ HostAdapterContractError,
31
+ HostCapabilityMismatch,
32
+ HostNotRegistered,
33
+ HostUnavailable,
34
+ ProviderNotRegistered,
35
+ ProviderUnavailable,
36
+ ValueError,
37
+ )
38
+
39
+
40
+ def catalog_payload(registry: HostAdapterRegistry) -> dict[str, object]:
41
+ return {
42
+ "schemaVersion": 1,
43
+ "hosts": [asdict(descriptor) for descriptor in registry.catalog()],
44
+ }
45
+
46
+
47
+ def resolve_payload(
48
+ registry: HostAdapterRegistry,
49
+ requested_host: str,
50
+ context: HostResolutionContext,
51
+ ) -> dict[str, object]:
52
+ descriptor, host, reason = registry.resolve_request_details(
53
+ requested_host, context
54
+ )
55
+ return {
56
+ "ok": True,
57
+ "requestedRuntime": requested_host.strip().lower() or "auto",
58
+ "resolvedRuntime": descriptor.id,
59
+ "launchMode": descriptor.launch_mode,
60
+ "installTargets": sorted(descriptor.install_targets),
61
+ "host": host,
62
+ "confidence": "high",
63
+ "reason": reason,
64
+ "fallbackFrom": None,
65
+ "warnings": [],
66
+ }
67
+
68
+
69
+ def main(
70
+ argv: list[str] | None = None,
71
+ *,
72
+ registry: HostAdapterRegistry | None = None,
73
+ ) -> int:
74
+ parser = _build_parser()
75
+ args = parser.parse_args(argv)
76
+ try:
77
+ active_registry = registry or default_host_registry()
78
+ payload = _command_payload(args, active_registry)
79
+ except _ENTRYPOINT_ERRORS as exc:
80
+ sys.stderr.write(f"{exc}\n")
81
+ return 2
82
+ if getattr(args, "output_format", "json") == "null":
83
+ _write_null_launch_plan(payload)
84
+ else:
85
+ public_payload = {
86
+ key: value for key, value in payload.items()
87
+ if not key.startswith("_")
88
+ }
89
+ print(json.dumps(public_payload, default=_json_default, sort_keys=True))
90
+ return 0
91
+
92
+
93
+ def _build_parser() -> argparse.ArgumentParser:
94
+ parser = argparse.ArgumentParser(prog="okstra-hosts")
95
+ commands = parser.add_subparsers(dest="command", required=True)
96
+ catalog = commands.add_parser("catalog")
97
+ catalog.add_argument("--json", action="store_true")
98
+ resolve = commands.add_parser("resolve")
99
+ resolve.add_argument("--requested", required=True)
100
+ resolve.add_argument("--command", dest="runtime_command", required=True)
101
+ resolve.add_argument("--context-json", required=True)
102
+ launch = commands.add_parser("launch-plan")
103
+ _add_session_plan_args(launch)
104
+ launch.add_argument("--prepared-launch-json", default="")
105
+ resume = commands.add_parser("resume-plan")
106
+ _add_session_plan_args(resume)
107
+ resume.add_argument("--session-id", required=True)
108
+ resume.add_argument("--task-type", default="")
109
+ resume.add_argument("--request-arg", action="append", default=[])
110
+ probe = commands.add_parser("probe")
111
+ probe.add_argument("--host", required=True)
112
+ probe.add_argument(
113
+ "--entry-mode",
114
+ required=True,
115
+ choices=("spawn-process", "current-session"),
116
+ )
117
+ probe.add_argument("--project-root", required=True)
118
+ probe.add_argument("--home-dir", required=True)
119
+ probe.add_argument("--available-function", action="append", default=[])
120
+ return parser
121
+
122
+
123
+ def _add_session_plan_args(parser: argparse.ArgumentParser) -> None:
124
+ parser.add_argument("--host", required=True)
125
+ parser.add_argument(
126
+ "--entry-mode",
127
+ required=True,
128
+ choices=("spawn-process", "current-session"),
129
+ )
130
+ parser.add_argument(
131
+ "--format",
132
+ dest="output_format",
133
+ choices=("json", "null"),
134
+ default="json",
135
+ )
136
+ parser.add_argument("--project-root", default=str(Path.cwd()))
137
+
138
+
139
+ def _command_payload(
140
+ args: argparse.Namespace, registry: HostAdapterRegistry
141
+ ) -> dict[str, object]:
142
+ if args.command == "catalog":
143
+ return catalog_payload(registry)
144
+ if args.command == "resolve":
145
+ context = _parse_resolution_context(args.context_json)
146
+ try:
147
+ return resolve_payload(registry, args.requested, context)
148
+ except HostUnavailable as exc:
149
+ raise HostUnavailable(
150
+ _unclaimed_host_message(registry, args.runtime_command)
151
+ ) from exc
152
+ except HostNotRegistered as exc:
153
+ if args.requested.strip().lower() in {"", "auto"} and context.explicit_env_host:
154
+ allowed = "|".join(registry.ids())
155
+ raise HostNotRegistered(
156
+ f"invalid OKSTRA_RUNTIME_HOST {context.explicit_env_host!r} "
157
+ f"(expected: {allowed})."
158
+ ) from exc
159
+ raise
160
+ if args.command == "probe":
161
+ return _probe_payload(registry, args)
162
+ if args.command == "resume-plan":
163
+ return _resume_plan_payload(registry, args)
164
+ return _launch_plan_payload(registry, args)
165
+
166
+
167
+ def _probe_payload(
168
+ registry: HostAdapterRegistry,
169
+ args: argparse.Namespace,
170
+ ) -> dict[str, object]:
171
+ adapter = registry.resolve(args.host)
172
+ context = HostSessionContext(
173
+ host_id=adapter.descriptor.id,
174
+ entry_mode=args.entry_mode,
175
+ available_functions=frozenset(args.available_function),
176
+ interaction_surface="preflight",
177
+ )
178
+ with _probe_paths(args.project_root, args.home_dir):
179
+ readiness = adapter.probe(context)
180
+ statuses = {check.get("status") for check in readiness.checks}
181
+ status = "unknown" if "unavailable" in statuses else (
182
+ "ready" if readiness.ok else "blocked"
183
+ )
184
+ return {
185
+ "runtime": adapter.descriptor.id,
186
+ "status": status,
187
+ "checks": list(readiness.checks),
188
+ "relayContract": adapter.descriptor.relay_contract,
189
+ }
190
+
191
+
192
+ @contextmanager
193
+ def _probe_paths(project_root: str, home_dir: str):
194
+ keys = ("OKSTRA_PROBE_PROJECT_ROOT", "OKSTRA_PROBE_HOME_DIR")
195
+ previous = {key: os.environ.get(key) for key in keys}
196
+ os.environ[keys[0]] = project_root
197
+ os.environ[keys[1]] = home_dir
198
+ try:
199
+ yield
200
+ finally:
201
+ for key, value in previous.items():
202
+ if value is None:
203
+ os.environ.pop(key, None)
204
+ else:
205
+ os.environ[key] = value
206
+
207
+
208
+ def _parse_resolution_context(raw_context: str) -> HostResolutionContext:
209
+ try:
210
+ raw = json.loads(raw_context)
211
+ except json.JSONDecodeError as exc:
212
+ raise ValueError("invalid host resolution context JSON") from exc
213
+ if not isinstance(raw, dict):
214
+ raise ValueError("invalid host resolution context JSON")
215
+ env = raw.get("env", {})
216
+ capabilities = raw.get("capabilities", {})
217
+ if not isinstance(env, dict) or not isinstance(capabilities, dict):
218
+ raise ValueError("invalid host resolution context JSON")
219
+ explicit_env_host = env.get("OKSTRA_RUNTIME_HOST", "")
220
+ if not isinstance(explicit_env_host, str):
221
+ raise ValueError("invalid host resolution context JSON")
222
+ signals = frozenset(
223
+ signal
224
+ for capability, signal in (
225
+ ("claudeSkillHandoff", "claude-skill-handoff"),
226
+ ("tmuxAvailable", "tmux"),
227
+ )
228
+ if capabilities.get(capability) is True
229
+ )
230
+ return HostResolutionContext(explicit_env_host, signals)
231
+
232
+
233
+ def _unclaimed_host_message(registry: HostAdapterRegistry, command: str) -> str:
234
+ runtime_flag = "--runtime" if command in {
235
+ "install", "ensure-installed", "doctor"
236
+ } else "--lead-runtime"
237
+ allowed = "|".join(registry.ids())
238
+ return (
239
+ f"host unknown: set OKSTRA_RUNTIME_HOST={allowed} or pass "
240
+ f"{runtime_flag} explicitly."
241
+ )
242
+
243
+
244
+ def _launch_plan_payload(
245
+ registry: HostAdapterRegistry,
246
+ args: argparse.Namespace,
247
+ ) -> dict[str, object]:
248
+ adapter = registry.resolve(args.host)
249
+ context = HostSessionContext(
250
+ host_id=adapter.descriptor.id,
251
+ entry_mode=args.entry_mode,
252
+ available_functions=frozenset(),
253
+ interaction_surface="terminal",
254
+ )
255
+ project_root, request_argv = _launch_request(args, adapter.descriptor.id)
256
+ request = LeadStartRequest(project_root, "", request_argv)
257
+ plan = start_run(request, context, adapter)
258
+ return {"ok": True, "_project_root": str(project_root), **asdict(plan)}
259
+
260
+
261
+ def _resume_plan_payload(
262
+ registry: HostAdapterRegistry,
263
+ args: argparse.Namespace,
264
+ ) -> dict[str, object]:
265
+ adapter = registry.resolve(args.host)
266
+ context = HostSessionContext(
267
+ host_id=adapter.descriptor.id,
268
+ entry_mode=args.entry_mode,
269
+ available_functions=frozenset(),
270
+ interaction_surface="terminal",
271
+ )
272
+ project_root = Path(args.project_root)
273
+ request = LeadResumeRequest(
274
+ project_root,
275
+ args.task_type,
276
+ args.session_id,
277
+ tuple(args.request_arg),
278
+ )
279
+ plan = resume_run(request, context, adapter)
280
+ return {"ok": True, "_project_root": str(project_root), **asdict(plan)}
281
+
282
+
283
+ def _launch_request(
284
+ args: argparse.Namespace,
285
+ canonical_host_id: str,
286
+ ) -> tuple[Path, tuple[str, ...]]:
287
+ if not args.prepared_launch_json:
288
+ return Path(args.project_root), ()
289
+ try:
290
+ prepared = json.loads(args.prepared_launch_json)
291
+ except json.JSONDecodeError as exc:
292
+ raise ValueError("invalid prepared launch JSON") from exc
293
+ if not isinstance(prepared, dict):
294
+ raise ValueError("prepared launch JSON must be an object")
295
+ if prepared.get("leadRuntime") != canonical_host_id:
296
+ raise ValueError("prepared launch runtime does not match selected host")
297
+ project_root = prepared.get("projectRoot")
298
+ request_argv = prepared.get("launchRequestArgv")
299
+ if not isinstance(project_root, str) or not project_root:
300
+ 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)
306
+
307
+
308
+ def _write_null_launch_plan(payload: dict[str, object]) -> None:
309
+ fields = [
310
+ payload["_project_root"],
311
+ payload["sandbox_note"],
312
+ str(len(payload["env"])),
313
+ ]
314
+ fields.extend(
315
+ f"{key}={value}" for key, value in payload["env"].items()
316
+ )
317
+ fields.extend(payload["argv"])
318
+ for field in fields:
319
+ value = str(field)
320
+ if "\0" in value:
321
+ raise ValueError("launch plan field contains a null byte")
322
+ sys.stdout.write(value + "\0")
323
+
324
+
325
+ def _json_default(value: object) -> object:
326
+ if isinstance(value, (frozenset, set)):
327
+ return sorted(value)
328
+ if isinstance(value, Path):
329
+ return str(value)
330
+ raise TypeError(f"not JSON serializable: {type(value).__name__}")
331
+
332
+
333
+ if __name__ == "__main__":
334
+ raise SystemExit(main())
@@ -293,20 +293,32 @@ def decide_scope(
293
293
  if not prev_base_sha or prev_base_sha != cur_base_sha:
294
294
  return IncrementalDecision(
295
295
  "full", [], [],
296
- f"base-ref changed ({prev_base_sha!r} -> {cur_base_sha!r}); prior analysis invalid",
296
+ "the branch this plan was written against has moved on "
297
+ f"({prev_base_sha or 'unrecorded'} -> {cur_base_sha or 'unrecorded'}), "
298
+ "so none of the prior stages can be reused as they stand",
297
299
  )
298
300
  if not impacted_stages:
299
- return IncrementalDecision("full", [], [], "no impacted stages resolved; falling back to full")
301
+ return IncrementalDecision(
302
+ "full", [], [],
303
+ "no stage of the prior plan could be tied to the answers, so there is "
304
+ "nothing to narrow the rework down to",
305
+ )
300
306
  all_stages = {num for num, _ in stages}
301
307
  closure = downstream_stage_closure(stages, set(impacted_stages))
302
308
  if len(closure) * 2 > len(all_stages):
303
309
  return IncrementalDecision(
304
310
  "full", [], [],
305
- f"closure {len(closure)}/{len(all_stages)} exceeds {int(cutoff_ratio * 100)}% cutoff",
311
+ f"the answers reach {len(closure)} of the plan's {len(all_stages)} stages — "
312
+ f"past the {int(cutoff_ratio * 100)}% mark where replanning outright costs "
313
+ "less than tracking what carried over",
306
314
  )
307
315
  reverify = sorted(closure)
308
316
  carry = sorted(all_stages - closure)
309
- return IncrementalDecision("incremental", reverify, carry, f"closure {reverify} within cutoff")
317
+ return IncrementalDecision(
318
+ "incremental", reverify, carry,
319
+ f"the answers reach {len(reverify)} of the plan's {len(all_stages)} stages; "
320
+ "the rest is reused as written",
321
+ )
310
322
 
311
323
 
312
324
  def _preview_result(args) -> dict: