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
@@ -108,6 +108,11 @@ from okstra_ctl.worker_prompt_contract import ( # noqa: E402
108
108
  PromptRecord,
109
109
  validate_initial_prompt_records,
110
110
  )
111
+ from okstra_ctl.agent_invocation import ( # noqa: E402
112
+ AgentInvocationError,
113
+ agent_model_assignment_from_payload,
114
+ verify_agent_invocation,
115
+ )
111
116
  from okstra_ctl.worker_audit_ledger import ( # noqa: E402
112
117
  READING_CONFIRMATION_HEADING_RE,
113
118
  check_worker_results_audit,
@@ -124,6 +129,176 @@ from okstra_ctl.convergence_provenance import ( # noqa: E402
124
129
  TERMINAL_STATUSES = {"completed", "timeout", "error", "not-run"}
125
130
  ATTEMPTED_STATUSES = {"completed", "timeout", "error"}
126
131
  WORKER_DISPATCH_MODES = {"cli-wrapper", "mixed", "tmux-pane"}
132
+ _AGENT_DISPATCH_DIGEST_KEYS = (
133
+ "catalogDigest",
134
+ "assignmentDigest",
135
+ "dutyDigest",
136
+ "instructionDigest",
137
+ "promptDigest",
138
+ )
139
+
140
+
141
+ def _validate_agent_dispatch_contract(
142
+ *,
143
+ project_root: Path,
144
+ run_manifest_path: Path,
145
+ run_manifest: Mapping[str, Any],
146
+ team_state: Mapping[str, Any],
147
+ failures: list[str],
148
+ ) -> None:
149
+ """Validate invocation-to-dispatch and result associations for new runs."""
150
+ contract = run_manifest.get("agentContract")
151
+ if not isinstance(contract, Mapping) or contract.get("schemaVersion") != 1:
152
+ return
153
+ assignments = run_manifest.get("invocationAssignments")
154
+ if not isinstance(assignments, Mapping):
155
+ failures.append("agent invocation metadata is missing: invocationAssignments")
156
+ return
157
+ worker_dispatches = [
158
+ row for row in (team_state.get("workerDispatches") or [])
159
+ if isinstance(row, Mapping) and row.get("invocationId")
160
+ ]
161
+ agent_dispatches = [
162
+ row for row in (team_state.get("agentDispatches") or [])
163
+ if isinstance(row, Mapping)
164
+ ]
165
+ dispatches = [*worker_dispatches, *agent_dispatches]
166
+ ids: dict[str, Mapping[str, Any]] = {}
167
+ for row in dispatches:
168
+ dispatch_id = str(row.get("dispatchId") or "").strip()
169
+ missing = [
170
+ key for key in (
171
+ "dispatchId", "workerId", "audience", "invocationId",
172
+ "assignmentRef", "promptMetadataPath", "modelExecutionValue",
173
+ "enforcementMode", *_AGENT_DISPATCH_DIGEST_KEYS,
174
+ )
175
+ if not str(row.get(key) or "").strip()
176
+ ]
177
+ if missing:
178
+ failures.append(
179
+ "agent invocation metadata is missing from dispatch record: "
180
+ + ", ".join(missing)
181
+ )
182
+ continue
183
+ if dispatch_id in ids:
184
+ failures.append(f"agent dispatch ID is duplicated: {dispatch_id}")
185
+ continue
186
+ ids[dispatch_id] = row
187
+ assignment_ref = str(row["assignmentRef"])
188
+ try:
189
+ assignment = agent_model_assignment_from_payload(
190
+ assignments.get(assignment_ref)
191
+ )
192
+ except AgentInvocationError as exc:
193
+ failures.append(f"agent dispatch {dispatch_id}: {exc}")
194
+ continue
195
+ metadata_path = _resolve_prompt_record_path(
196
+ project_root, str(row["promptMetadataPath"])
197
+ )
198
+ errors = verify_agent_invocation(
199
+ metadata_path,
200
+ project_root=project_root,
201
+ expected_run_manifest_path=run_manifest_path,
202
+ expected_assignment=assignment,
203
+ expected_invocation_id=str(row["invocationId"]),
204
+ expected_worker_id=str(row["workerId"]),
205
+ expected_assignment_ref=assignment_ref,
206
+ expected_audience=str(row["audience"]),
207
+ )
208
+ failures.extend(
209
+ f"agent dispatch {dispatch_id}: {error}" for error in errors
210
+ )
211
+ try:
212
+ metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
213
+ digests = metadata["digests"]
214
+ except (OSError, json.JSONDecodeError, KeyError, TypeError):
215
+ digests = {}
216
+ for key in _AGENT_DISPATCH_DIGEST_KEYS:
217
+ if row.get(key) != digests.get(key):
218
+ failures.append(
219
+ f"agent dispatch {dispatch_id}: {key} does not match metadata"
220
+ )
221
+ if row.get("modelExecutionValue") != assignment.model_execution_value:
222
+ failures.append(
223
+ f"agent dispatch {dispatch_id}: modelExecutionValue does not match assignment"
224
+ )
225
+ if row.get("hostModelValue") != assignment.host_model_value:
226
+ failures.append(
227
+ f"agent dispatch {dispatch_id}: hostModelValue does not match assignment"
228
+ )
229
+ enforcement = row.get("enforcementMode")
230
+ if enforcement not in {
231
+ "core-pre-dispatch", "host-native-spec-link-gate",
232
+ }:
233
+ failures.append(
234
+ f"agent dispatch {dispatch_id}: enforcementMode is invalid"
235
+ )
236
+ if (
237
+ enforcement == "host-native-spec-link-gate"
238
+ and row.get("promptDeliveryVerified") is not False
239
+ ):
240
+ failures.append(
241
+ "host-native lead delivery cannot be marked verified"
242
+ if row.get("audience") == "lead"
243
+ else "host-native prompt delivery cannot be marked verified"
244
+ )
245
+
246
+ lead_ids = {
247
+ dispatch_id for dispatch_id, row in ids.items()
248
+ if row.get("audience") == "lead" and row.get("workerId") == "lead"
249
+ }
250
+ if not lead_ids:
251
+ failures.append("accepted lead result has no lead dispatch record")
252
+
253
+ links = [
254
+ row for row in (team_state.get("agentResultLinks") or [])
255
+ if isinstance(row, Mapping)
256
+ ]
257
+ paths: dict[str, str] = {}
258
+ dispatch_link_counts: dict[str, int] = {}
259
+ for link in links:
260
+ dispatch_id = str(link.get("dispatchId") or "").strip()
261
+ result_path = str(link.get("resultPath") or "").strip()
262
+ if dispatch_id not in ids or not result_path:
263
+ failures.append("agent result link has no matching dispatch record")
264
+ continue
265
+ dispatch_link_counts[dispatch_id] = dispatch_link_counts.get(dispatch_id, 0) + 1
266
+ previous = paths.setdefault(result_path, dispatch_id)
267
+ if previous != dispatch_id:
268
+ failures.append(
269
+ f"agent result is linked to multiple dispatches: {result_path}"
270
+ )
271
+ for dispatch_id, count in dispatch_link_counts.items():
272
+ if count > 1:
273
+ failures.append(
274
+ f"agent dispatch is linked to multiple accepted results: {dispatch_id}"
275
+ )
276
+
277
+ for worker in team_state.get("workers") or []:
278
+ if not isinstance(worker, Mapping) or worker.get("status") != "completed":
279
+ continue
280
+ worker_id = str(worker.get("workerId") or "").strip()
281
+ worker_result = str(worker.get("resultPath") or "").strip()
282
+ worker_dispatch_ids = {
283
+ dispatch_id for dispatch_id, row in ids.items()
284
+ if row.get("workerId") == worker_id
285
+ }
286
+ if worker_id and not worker_dispatch_ids:
287
+ failures.append(
288
+ f"accepted LLM result has no agent invocation record: {worker_id}"
289
+ )
290
+ continue
291
+ matching = [
292
+ link for link in links
293
+ if link.get("dispatchId") in worker_dispatch_ids
294
+ and link.get("resultPath") == worker_result
295
+ ]
296
+ if worker_id and len(matching) != 1:
297
+ failures.append(
298
+ f"accepted LLM result is not linked to its own dispatch: {worker_id}"
299
+ )
300
+ if lead_ids and not any(link.get("dispatchId") in lead_ids for link in links):
301
+ failures.append("accepted lead result has no dispatch result link")
127
302
 
128
303
 
129
304
  def utc_now() -> str:
@@ -7805,6 +7980,13 @@ def main() -> int:
7805
7980
  failures,
7806
7981
  concurrent_run_authorized=concurrent_run_authorized,
7807
7982
  )
7983
+ _validate_agent_dispatch_contract(
7984
+ project_root=project_root,
7985
+ run_manifest_path=run_manifest_path,
7986
+ run_manifest=run_manifest,
7987
+ team_state=team_state,
7988
+ failures=failures,
7989
+ )
7808
7990
  # Schema validation runs BEFORE markdown substring checks: if the
7809
7991
  # data.json is well-formed, the rendered markdown is guaranteed to
7810
7992
  # contain every required section. Substring checks below are a
@@ -80,6 +80,20 @@ export const COMMAND_REGISTRY = [
80
80
  category: "admin",
81
81
  summary: ["Extract and validate deterministic plan-body items"],
82
82
  },
83
+ {
84
+ name: "agent-prompt",
85
+ module: "./commands/execute/agent-prompt.mjs",
86
+ export: "run",
87
+ category: "admin",
88
+ summary: ["Materialize and verify auditable agent invocations"],
89
+ },
90
+ {
91
+ name: "worker-dispatch",
92
+ module: "./commands/execute/worker-dispatch.mjs",
93
+ export: "run",
94
+ category: "admin",
95
+ summary: ["Dispatch verified CLI-backed worker assignments"],
96
+ },
83
97
  {
84
98
  name: "plan-verify",
85
99
  module: "./commands/execute/plan-verify.mjs",
@@ -0,0 +1,25 @@
1
+ import { runPythonModule } from "../../lib/python-helper.mjs";
2
+
3
+ const USAGE = `okstra agent-prompt — materialize and verify auditable agent invocations
4
+
5
+ Usage:
6
+ okstra agent-prompt materialize [options]
7
+ okstra agent-prompt verify [options]
8
+ okstra agent-prompt materialize-result [options]
9
+ okstra agent-prompt complete [options]
10
+ okstra agent-prompt verify-completion [options]
11
+ okstra agent-prompt record-dispatch [options]
12
+ okstra agent-prompt link-result [options]
13
+ `;
14
+
15
+ export async function run(args) {
16
+ if (args.length === 1 && (args[0] === "--help" || args[0] === "-h")) {
17
+ process.stdout.write(USAGE);
18
+ return 0;
19
+ }
20
+ const { code } = await runPythonModule({
21
+ module: "okstra_ctl.agent_prompt_cli",
22
+ args,
23
+ });
24
+ return code ?? 1;
25
+ }
@@ -1,67 +1,10 @@
1
- import { runPythonModule } from "../../lib/python-helper.mjs";
2
- import { resolvePaths } from "../../lib/paths.mjs";
1
+ import {
2
+ buildWorkerDispatchArgs,
3
+ runWorkerDispatch,
4
+ } from "./worker-dispatch.mjs";
3
5
 
4
- const USAGE = `okstra codex-dispatch — dispatch CLI-backed workers for a prepared Codex run
5
-
6
- Usage:
7
- okstra codex-dispatch --project-root <dir> --run-manifest <path> \\
8
- [--workers <cli-assigned-worker-ids>] [--dry-run] \\
9
- [--idle-timeout-seconds <n>]
10
-
11
- This command reads an existing run manifest produced by 'okstra codex-run' and
12
- dispatches only assignments whose persisted runner is 'cli-wrapper'. When
13
- --workers is omitted, it selects that subset from the run roster. Each worker,
14
- including report-writer, uses its persisted provider, model, and registered
15
- wrapper. Native-session assignments remain owned by the current Codex host;
16
- explicitly requesting one here fails.
17
-
18
- Missing worker prompt files are generated automatically from the run manifest
19
- and active-run-context. Existing prompt files are never overwritten.
20
- When the Codex report-writer path completes, this command also runs the
21
- idempotent post-report steps: token-usage substitution, render-views,
22
- spawn-followups, and validate-run.
23
-
24
- --workspace-root and --okstra-bin are owned by this command.
25
- `;
26
-
27
- const OWNED_FLAGS = new Set(["--workspace-root", "--okstra-bin"]);
28
-
29
- function isOwnedFlag(arg) {
30
- if (OWNED_FLAGS.has(arg)) return true;
31
- return [...OWNED_FLAGS].some((flag) => arg.startsWith(`${flag}=`));
32
- }
33
-
34
- export function buildCodexDispatchArgs(args, paths) {
35
- return [
36
- "--workspace-root", paths.workspace,
37
- "--okstra-bin", paths.bin,
38
- ...args,
39
- ];
40
- }
6
+ export const buildCodexDispatchArgs = buildWorkerDispatchArgs;
41
7
 
42
8
  export async function run(args) {
43
- if (args.includes("--help") || args.includes("-h")) {
44
- process.stdout.write(USAGE);
45
- return 0;
46
- }
47
- if (args.length === 0) {
48
- process.stdout.write(USAGE);
49
- return 2;
50
- }
51
-
52
- const forbidden = args.find(isOwnedFlag);
53
- if (forbidden) {
54
- process.stderr.write(
55
- `error: ${forbidden} is set by 'okstra codex-dispatch' itself — remove it from your args\n`,
56
- );
57
- return 2;
58
- }
59
-
60
- const paths = await resolvePaths();
61
- const result = await runPythonModule({
62
- module: "okstra_ctl.codex_dispatch",
63
- args: buildCodexDispatchArgs(args, paths),
64
- stdio: "inherit-stdout",
65
- });
66
- return result.code;
9
+ return runWorkerDispatch(args, { commandName: "codex-dispatch" });
67
10
  }
@@ -0,0 +1,76 @@
1
+ import { runPythonModule } from "../../lib/python-helper.mjs";
2
+ import { resolvePaths } from "../../lib/paths.mjs";
3
+
4
+ export const USAGE = `okstra worker-dispatch — dispatch verified CLI-backed workers
5
+
6
+ Usage:
7
+ okstra worker-dispatch --project-root <dir> --run-manifest <path> \\
8
+ [--workers <cli-assigned-worker-ids>] [--dry-run] \\
9
+ [--idle-timeout-seconds <n>]
10
+
11
+ This command reads a prepared run manifest and dispatches only assignments
12
+ whose persisted runner is 'cli-wrapper'. It verifies each invocation contract
13
+ before execution and uses the persisted provider, model, and registered CLI.
14
+ Native-session assignments remain owned by the active host session.
15
+
16
+ Missing worker prompt files are generated automatically from the immutable run snapshot.
17
+ Existing prompt and metadata files are never overwritten.
18
+ When report-writer completes, this command also runs the idempotent post-report
19
+ steps: token-usage substitution, render-views, spawn-followups, and validate-run.
20
+
21
+ --workspace-root and --okstra-bin are owned by this command.
22
+ `;
23
+
24
+ const OWNED_FLAGS = new Set(["--workspace-root", "--okstra-bin"]);
25
+
26
+ function isOwnedFlag(arg) {
27
+ if (OWNED_FLAGS.has(arg)) return true;
28
+ return [...OWNED_FLAGS].some((flag) => arg.startsWith(`${flag}=`));
29
+ }
30
+
31
+ export function buildWorkerDispatchArgs(args, paths) {
32
+ return [
33
+ "--workspace-root", paths.workspace,
34
+ "--okstra-bin", paths.bin,
35
+ ...args,
36
+ ];
37
+ }
38
+
39
+ export async function runWorkerDispatch(args, { commandName = "worker-dispatch" } = {}) {
40
+ if (args.includes("--help") || args.includes("-h")) {
41
+ process.stdout.write(
42
+ commandName === "worker-dispatch"
43
+ ? USAGE
44
+ : USAGE.replaceAll("worker-dispatch", commandName),
45
+ );
46
+ return 0;
47
+ }
48
+ if (args.length === 0) {
49
+ process.stdout.write(
50
+ commandName === "worker-dispatch"
51
+ ? USAGE
52
+ : USAGE.replaceAll("worker-dispatch", commandName),
53
+ );
54
+ return 2;
55
+ }
56
+
57
+ const forbidden = args.find(isOwnedFlag);
58
+ if (forbidden) {
59
+ process.stderr.write(
60
+ `error: ${forbidden} is set by 'okstra ${commandName}' itself — remove it from your args\n`,
61
+ );
62
+ return 2;
63
+ }
64
+
65
+ const paths = await resolvePaths();
66
+ const result = await runPythonModule({
67
+ module: "okstra_ctl.worker_dispatch",
68
+ args: buildWorkerDispatchArgs(args, paths),
69
+ stdio: "inherit-stdout",
70
+ });
71
+ return result.code;
72
+ }
73
+
74
+ export async function run(args) {
75
+ return runWorkerDispatch(args);
76
+ }
@@ -88,6 +88,16 @@ export function phaseDiagnosticsToChecks(phase, phaseDiagnostics) {
88
88
  }));
89
89
  }
90
90
 
91
+ export function buildPhaseDiagnosticsArgs(phase, paths, resolvedRuntime) {
92
+ return [
93
+ phase,
94
+ process.cwd(),
95
+ paths.workspace,
96
+ homedir(),
97
+ resolvedRuntime || "",
98
+ ];
99
+ }
100
+
91
101
  async function checkPython3() {
92
102
  const r = await runProcess("python3", ["--version"]);
93
103
  if (r.code !== 0) return { ok: false, detail: `python3 not found: ${r.stderr.trim() || "missing binary"}` };
@@ -140,7 +150,7 @@ async function check(name, fn) {
140
150
  }
141
151
  }
142
152
 
143
- async function loadPhaseDiagnostics(phase, paths) {
153
+ async function loadPhaseDiagnostics(phase, paths, resolvedRuntime) {
144
154
  const script = [
145
155
  "import json, sys",
146
156
  "from okstra_ctl.doctor import phase_diagnostics",
@@ -149,6 +159,7 @@ async function loadPhaseDiagnostics(phase, paths) {
149
159
  " cwd=sys.argv[2],",
150
160
  " workspace_root=sys.argv[3],",
151
161
  " home=sys.argv[4],",
162
+ " host_runtime=sys.argv[5],",
152
163
  ")",
153
164
  "print(json.dumps(payload, ensure_ascii=False))",
154
165
  ].join("\n");
@@ -159,7 +170,7 @@ async function loadPhaseDiagnostics(phase, paths) {
159
170
  // never exists and report a false "missing agent".
160
171
  const result = await runPythonSnippet({
161
172
  script,
162
- args: [phase, process.cwd(), paths.workspace, homedir()],
173
+ args: buildPhaseDiagnosticsArgs(phase, paths, resolvedRuntime),
163
174
  });
164
175
  if (result.code !== 0 && !result.stdout.trim()) {
165
176
  return {
@@ -249,7 +260,11 @@ export async function run(args) {
249
260
 
250
261
  let phaseDiagnostics = null;
251
262
  if (opts.phase) {
252
- phaseDiagnostics = await loadPhaseDiagnostics(opts.phase, paths);
263
+ phaseDiagnostics = await loadPhaseDiagnostics(
264
+ opts.phase,
265
+ paths,
266
+ resolvedRuntime,
267
+ );
253
268
  if (phaseDiagnostics.usageError) {
254
269
  process.stderr.write(`error: ${phaseDiagnostics.reason}\n`);
255
270
  return 2;
@@ -10,9 +10,9 @@ import { normalizeHostRequest, resolveRuntime } from "../../lib/host-registry-cl
10
10
  import { OBSOLETE_SKILL_NAMES, USER_SKILL_NAMES } from "../../lib/skill-catalog.mjs";
11
11
  import {
12
12
  AGENTS_MANIFEST_REL,
13
+ LEGACY_TRANSPORT_AGENT_NAMES,
13
14
  SKILLS_MANIFEST_REL,
14
15
  } from "../../lib/install-assets.mjs";
15
- import { renderWorkerAgentDefinitions } from "../../lib/worker-agent-render.mjs";
16
16
 
17
17
 
18
18
  const USER_HOME = homedir();
@@ -389,6 +389,7 @@ async function installLinkMode(repoPath, paths, opts) {
389
389
 
390
390
  let agentResult = { installed: [] };
391
391
  if (skillTargets.some((target) => target.provider === "claude")) {
392
+ await pruneOwnedLegacyTransportAgents(paths.home, { dryRun, quiet });
392
393
  agentResult = await installAgentsLink(repoAbs, { dryRun, quiet });
393
394
  }
394
395
  await writeAgentsManifest(paths.home, agentResult.installed, { dryRun });
@@ -493,6 +494,35 @@ async function writeAgentsManifest(home, names, opts) {
493
494
  );
494
495
  }
495
496
 
497
+ async function pruneOwnedLegacyTransportAgents(home, opts) {
498
+ const { dryRun = false, quiet = false } = opts ?? {};
499
+ let previous = [];
500
+ try {
501
+ const data = JSON.parse(
502
+ await fs.readFile(join(home, AGENTS_MANIFEST_REL), "utf8"),
503
+ );
504
+ if (Array.isArray(data?.agents)) previous = data.agents;
505
+ } catch (err) {
506
+ if (err.code !== "ENOENT" && !(err instanceof SyntaxError)) throw err;
507
+ }
508
+ const ownedLegacy = LEGACY_TRANSPORT_AGENT_NAMES.filter((name) => (
509
+ previous.includes(name)
510
+ ));
511
+ for (const name of ownedLegacy) {
512
+ const target = join(CLAUDE_AGENTS_DIR, name);
513
+ if (dryRun) {
514
+ process.stdout.write(`[dry-run] remove retired agent ${target}\n`);
515
+ continue;
516
+ }
517
+ try {
518
+ await fs.unlink(target);
519
+ if (!quiet) process.stdout.write(` agents/${name}: removed retired wrapper\n`);
520
+ } catch (err) {
521
+ if (err.code !== "ENOENT") throw err;
522
+ }
523
+ }
524
+ }
525
+
496
526
  async function writeRuntimeManifest(home, runtime, opts) {
497
527
  const { dryRun = false, resolution = null, installedAssets = null } = opts ?? {};
498
528
  const data = buildRuntimeManifest(
@@ -574,11 +604,7 @@ async function installAgentsLink(repoAbs, opts) {
574
604
  const { dryRun, quiet } = opts;
575
605
  const srcRoot = join(repoAbs, "agents", "workers");
576
606
  const sourceNames = await listWorkerFiles(srcRoot);
577
- const renderedDefinitions = await renderWorkerAgentDefinitions(srcRoot);
578
- const names = [
579
- ...sourceNames,
580
- ...renderedDefinitions.map(({ name }) => name),
581
- ].sort();
607
+ const names = [...sourceNames].sort();
582
608
  if (names.length === 0) {
583
609
  if (!quiet) process.stdout.write(" agents: <repo>/agents/workers missing — skipped\n");
584
610
  return { installed: [] };
@@ -590,15 +616,6 @@ async function installAgentsLink(repoAbs, opts) {
590
616
  const action = await ensureSymlink(src, dst, { dryRun });
591
617
  if (!quiet) process.stdout.write(` agents/${name}: ${action}\n`);
592
618
  }
593
- for (const { name, content } of renderedDefinitions) {
594
- const dst = join(CLAUDE_AGENTS_DIR, name);
595
- if (dryRun) {
596
- process.stdout.write(`[dry-run] render ${name} -> ${dst}\n`);
597
- } else {
598
- await writeFileAtomic(dst, content, 0o644);
599
- }
600
- if (!quiet) process.stdout.write(` agents/${name}: rendered\n`);
601
- }
602
619
  return { installed: names };
603
620
  }
604
621
 
@@ -900,6 +917,7 @@ export async function runInstall(args) {
900
917
 
901
918
  let agentResult = { installed: [] };
902
919
  if (skillTargets.some((target) => target.provider === "claude")) {
920
+ await pruneOwnedLegacyTransportAgents(paths.home, opts);
903
921
  agentResult = await installAgentsCopy(runtimeRoot, opts);
904
922
  }
905
923
  await writeAgentsManifest(paths.home, agentResult.installed, { dryRun: opts.dryRun });
@@ -11,6 +11,7 @@ import {
11
11
  AGENTS_MANIFEST_REL,
12
12
  INSTALLED_FILES,
13
13
  INSTALLED_TREES,
14
+ LEGACY_TRANSPORT_AGENT_NAMES,
14
15
  SKILLS_MANIFEST_REL,
15
16
  } from "../../lib/install-assets.mjs";
16
17
 
@@ -23,11 +24,11 @@ export const FALLBACK_SKILL_NAMES = [
23
24
  ...new Set([...USER_SKILL_NAMES, ...OBSOLETE_SKILL_NAMES]),
24
25
  ];
25
26
 
26
- const FALLBACK_AGENT_NAMES = [
27
+ export const FALLBACK_AGENT_NAMES = [
27
28
  "claude-worker.md",
28
- "codex-worker.md",
29
- "antigravity-worker.md",
30
29
  "report-writer-worker.md",
30
+ "translator-worker.md",
31
+ ...LEGACY_TRANSPORT_AGENT_NAMES,
31
32
  ];
32
33
 
33
34
  const USER_HOME = homedir();
@@ -35,3 +35,12 @@ export const INSTALLED_FILES = [
35
35
 
36
36
  export const SKILLS_MANIFEST_REL = "installed-skills.json";
37
37
  export const AGENTS_MANIFEST_REL = "installed-agents.json";
38
+
39
+ // Exact names of the retired LLM transport wrappers. Install removes an entry
40
+ // only when the previous agent manifest proves that Okstra installed it.
41
+ export const LEGACY_TRANSPORT_AGENT_NAMES = Object.freeze([
42
+ "codex-worker.md",
43
+ "antigravity-worker.md",
44
+ "grok-worker.md",
45
+ "kimi-worker.md",
46
+ ]);