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
@@ -6,7 +6,7 @@ import { getPackageRoot } from "../../lib/version.mjs";
6
6
  import { resolvePaths } from "../../lib/paths.mjs";
7
7
  import { fileExists } from "../../lib/proc.mjs";
8
8
  import { buildRuntimeManifest, RUNTIMES_MANIFEST_REL } from "../../lib/runtime-manifest.mjs";
9
- import { normalizeRuntimeRequest, resolveRuntime } from "../../lib/runtime-resolver.mjs";
9
+ import { normalizeHostRequest, resolveRuntime } from "../../lib/host-registry-client.mjs";
10
10
  import { OBSOLETE_SKILL_NAMES, USER_SKILL_NAMES } from "../../lib/skill-catalog.mjs";
11
11
  import {
12
12
  AGENTS_MANIFEST_REL,
@@ -52,10 +52,6 @@ const BIN_ENTRYPOINTS = [
52
52
  // depend on. Intentionally an exact allowlist, not a directory wildcard.
53
53
  const REQUIRED_PROMPT_RESOURCE_FILES = Object.freeze([
54
54
  ["prompts", "lead", "okstra-lead-contract.md"],
55
- ["prompts", "lead", "adapters", "claude-code.md"],
56
- ["prompts", "lead", "adapters", "codex.md"],
57
- ["prompts", "lead", "adapters", "antigravity.md"],
58
- ["prompts", "lead", "adapters", "external.md"],
59
55
  ["prompts", "lead", "context-loader.md"],
60
56
  ["prompts", "lead", "team-contract.md"],
61
57
  ["prompts", "lead", "convergence.md"],
@@ -77,7 +73,7 @@ const INSTALL_USAGE = `okstra install — install runtime into ~/.okstra
77
73
 
78
74
  Usage:
79
75
  okstra install Install/update runtime (copy mode)
80
- okstra install --runtime <claude-code|codex|antigravity|external|all>
76
+ okstra install --runtime <host-id|alias|all>
81
77
  okstra install --link <p> Dev only: symlink ~/.okstra/{lib/python,bin}
82
78
  to the source repo at <p>. agents/ resolves
83
79
  to <p>/agents. Not advertised to end users.
@@ -128,19 +124,14 @@ const ENSURE_USAGE = `okstra ensure-installed — idempotent install check
128
124
 
129
125
  Usage:
130
126
  okstra ensure-installed Verify install; reinstall if version/files drift
131
- okstra ensure-installed --runtime <claude-code|codex|antigravity|external|all>
127
+ okstra ensure-installed --runtime <host-id|alias|all>
132
128
  okstra ensure-installed -q Same, but suppress success output
133
129
 
134
130
  Skills call this on every run. Returns 0 quickly when the install is fresh.
135
131
  `;
136
132
 
137
- const INSTALL_RUNTIMES = new Set([
138
- "auto", "claude-code", "codex", "antigravity", "external", "all",
139
- ]);
140
-
141
- export function runtimeIncludesClaudeAssets(runtime, resolution = null) {
142
- if (runtime === "claude-code" || runtime === "all") return true;
143
- return runtime === "auto" && resolution?.resolvedRuntime === "claude-code";
133
+ export function runtimeIncludesClaudeAssets(resolution) {
134
+ return resolution?.installTargets?.includes("claude") === true;
144
135
  }
145
136
 
146
137
  function readRuntimeFlag(args, index) {
@@ -150,21 +141,17 @@ function readRuntimeFlag(args, index) {
150
141
  }
151
142
  const next = args[index + 1];
152
143
  if (!next || next.startsWith("--")) {
153
- throw new Error("--runtime requires one of: claude-code, codex, antigravity, external, all");
144
+ throw new Error("--runtime requires a host ID, alias, or all");
154
145
  }
155
146
  return { value: next, nextIndex: index + 1 };
156
147
  }
157
148
 
158
149
  export function validateInstallRuntime(value) {
159
- const runtime = normalizeRuntimeRequest(value);
160
- if (!INSTALL_RUNTIMES.has(runtime)) {
161
- throw new Error(`unknown runtime '${runtime}' (expected: auto, claude-code, codex, antigravity, external, all)`);
162
- }
163
- return runtime;
150
+ return normalizeHostRequest(value);
164
151
  }
165
152
 
166
- function resolveInstallRuntime(runtime, command) {
167
- const resolution = resolveRuntime({
153
+ async function resolveInstallRuntime(runtime, command) {
154
+ const resolution = await resolveRuntime({
168
155
  requestedRuntime: runtime,
169
156
  command,
170
157
  env: process.env,
@@ -172,13 +159,8 @@ function resolveInstallRuntime(runtime, command) {
172
159
  });
173
160
  if (resolution.ok || runtime !== "auto") return resolution;
174
161
  return {
175
- ok: false,
176
- requestedRuntime: "auto",
177
- resolvedRuntime: null,
178
- host: "unknown",
162
+ ...resolution,
179
163
  confidence: "none",
180
- reason: resolution.reason,
181
- fallbackFrom: null,
182
164
  warnings: [resolution.reason],
183
165
  };
184
166
  }
@@ -792,7 +774,7 @@ export async function runInstall(args) {
792
774
  const pkgRoot = getPackageRoot();
793
775
  const runtimeRoot = join(pkgRoot, "runtime");
794
776
  const paths = await resolvePaths();
795
- const runtimeResolution = resolveInstallRuntime(opts.runtime, "install");
777
+ const runtimeResolution = await resolveInstallRuntime(opts.runtime, "install");
796
778
  if (!runtimeResolution.ok && opts.runtime !== "auto") {
797
779
  process.stderr.write(`error: ${runtimeResolution.reason}\n`);
798
780
  return 2;
@@ -968,6 +950,44 @@ async function promptResourceDriftReasons(paths) {
968
950
  return reasons;
969
951
  }
970
952
 
953
+ async function hostRelayDriftReasons(paths) {
954
+ const reasons = [];
955
+ const hostsRoot = join(paths.pythonpath, "okstra_ctl", "adapters", "hosts");
956
+ let entries;
957
+ try {
958
+ entries = await fs.readdir(hostsRoot, { withFileTypes: true });
959
+ } catch (err) {
960
+ if (err.code === "ENOENT") return [`missing ${hostsRoot}`];
961
+ throw err;
962
+ }
963
+ for (const entry of entries) {
964
+ if (!entry.isDirectory()) continue;
965
+ const adapterRoot = join(hostsRoot, entry.name);
966
+ const manifestPath = join(adapterRoot, "manifest.json");
967
+ if (!(await fileExists(manifestPath))) continue;
968
+ let manifest;
969
+ try {
970
+ manifest = JSON.parse(await fs.readFile(manifestPath, "utf8"));
971
+ } catch {
972
+ reasons.push(`invalid ${manifestPath}`);
973
+ continue;
974
+ }
975
+ const relayContract = manifest?.relayContract;
976
+ if (typeof relayContract !== "string" || !relayContract) {
977
+ reasons.push(`invalid relayContract in ${manifestPath}`);
978
+ continue;
979
+ }
980
+ const relayPath = resolveAbs(adapterRoot, relayContract);
981
+ const relayRelative = relative(adapterRoot, relayPath);
982
+ if (relayRelative.startsWith("..") || resolveAbs(adapterRoot) === relayPath) {
983
+ reasons.push(`invalid relayContract in ${manifestPath}`);
984
+ } else if (!(await fileExists(relayPath))) {
985
+ reasons.push(`missing ${relayPath}`);
986
+ }
987
+ }
988
+ return reasons;
989
+ }
990
+
971
991
  async function agentDriftReasons(paths) {
972
992
  const reasons = [];
973
993
  const workersDir = join(paths.agents, "workers");
@@ -1051,7 +1071,7 @@ export async function runEnsureInstalled(args) {
1051
1071
  return 2;
1052
1072
  }
1053
1073
  const paths = await resolvePaths();
1054
- const runtimeResolution = resolveInstallRuntime(opts.runtime, "ensure-installed");
1074
+ const runtimeResolution = await resolveInstallRuntime(opts.runtime, "ensure-installed");
1055
1075
  if (!runtimeResolution.ok && opts.runtime !== "auto") {
1056
1076
  process.stderr.write(`error: ${runtimeResolution.reason}\n`);
1057
1077
  return 2;
@@ -1093,6 +1113,9 @@ export async function runEnsureInstalled(args) {
1093
1113
  for (const reason of await promptResourceDriftReasons(paths)) {
1094
1114
  reasons.push(reason);
1095
1115
  }
1116
+ for (const reason of await hostRelayDriftReasons(paths)) {
1117
+ reasons.push(reason);
1118
+ }
1096
1119
  const skillTargets = await detectSkillTargets();
1097
1120
  for (const reason of await skillTargetDriftReasons(skillTargets)) {
1098
1121
  reasons.push(reason);
@@ -3,7 +3,7 @@ import { runEnsureInstalled } from "./install.mjs";
3
3
  import { missingHelperScripts } from "../../lib/helper-scripts.mjs";
4
4
  import { resolveInstalledScript } from "../../lib/python-helper.mjs";
5
5
  import { resolvePaths } from "../../lib/paths.mjs";
6
- import { checkRuntimeReadiness } from "../../lib/runtime-readiness.mjs";
6
+ import { checkRuntimeReadiness } from "../../lib/host-registry-client.mjs";
7
7
 
8
8
  const USAGE = `okstra preflight — one-call skill preflight (ensure-installed + check-project)
9
9
 
@@ -17,7 +17,7 @@ verifies the target project has .okstra/project.json. Prints one JSON object:
17
17
  ok: false {ok, stage, reason, ...}
18
18
  stage: install | helper_scripts_missing | resolve |
19
19
  project_json_missing | project_json_invalid |
20
- python | parse
20
+ runtime_readiness | python | parse
21
21
 
22
22
  Options:
23
23
  --runtime <name> Runtime whose assets must be present (e.g. claude-code);
@@ -26,8 +26,8 @@ Options:
26
26
  --json Default output format (kept for symmetry).
27
27
 
28
28
  Exit codes:
29
- 0 runtime fresh and project registered; inspect runtimeReadiness before dispatch
30
- 1 runtime install failed, or project setup missing/invalid
29
+ 0 runtime fresh, project registered, and current host ready
30
+ 1 runtime install failed, project setup missing/invalid, or host not ready
31
31
  2 PROJECT_ROOT could not be resolved from cwd (pass --cwd), or bad flags
32
32
  `;
33
33
 
@@ -110,6 +110,16 @@ export async function run(args) {
110
110
  runtime: opts.runtime,
111
111
  projectRoot: payload.projectRoot,
112
112
  });
113
+ if (["blocked", "unknown"].includes(runtimeReadiness.status)) {
114
+ emit({
115
+ ...payload,
116
+ ok: false,
117
+ stage: "runtime_readiness",
118
+ reason: `current host readiness is ${runtimeReadiness.status}`,
119
+ runtimeReadiness,
120
+ });
121
+ return 1;
122
+ }
113
123
  emit({ ...payload, runtimeReadiness });
114
124
  return code;
115
125
  }
@@ -0,0 +1,176 @@
1
+ import { homedir } from "node:os";
2
+
3
+ import { runPythonModule } from "./python-helper.mjs";
4
+
5
+ export const RUNTIME_AUTO = "auto";
6
+
7
+ const HOST_REQUEST_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
8
+ const PLAIN_TEXT_INPUT = "plain_text_input";
9
+
10
+ export class HostRegistryClientError extends Error {
11
+ constructor(message) {
12
+ super(message);
13
+ this.name = "HostRegistryClientError";
14
+ }
15
+ }
16
+
17
+ async function callHosts(runPython, args) {
18
+ const result = await runPython({
19
+ module: "okstra_ctl.entrypoints.hosts",
20
+ args,
21
+ stdio: "capture",
22
+ });
23
+ if (result.code !== 0) {
24
+ const detail = String(result.stderr || "").trim() || `Python exited ${result.code}`;
25
+ throw new HostRegistryClientError(detail);
26
+ }
27
+ const stdout = String(result.stdout || "").trim();
28
+ if (!stdout) throw new HostRegistryClientError("host registry returned empty stdout");
29
+ try {
30
+ return JSON.parse(stdout);
31
+ } catch (error) {
32
+ throw new HostRegistryClientError(`host registry returned invalid JSON: ${error.message}`);
33
+ }
34
+ }
35
+
36
+ function resolutionContext(env = {}, capabilities = {}) {
37
+ return {
38
+ env: { OKSTRA_RUNTIME_HOST: String(env?.OKSTRA_RUNTIME_HOST ?? "") },
39
+ capabilities: {
40
+ claudeSkillHandoff: capabilities?.claudeSkillHandoff === true,
41
+ tmuxAvailable: capabilities?.tmuxAvailable === true,
42
+ },
43
+ };
44
+ }
45
+
46
+ export function createHostRegistryClient({ runPython = runPythonModule } = {}) {
47
+ return {
48
+ async catalog() {
49
+ return await callHosts(runPython, ["catalog", "--json"]);
50
+ },
51
+ async resolveHostRuntime({ requestedRuntime, command, env, capabilities }) {
52
+ return await callHosts(runPython, [
53
+ "resolve",
54
+ "--requested", requestedRuntime,
55
+ "--command", command,
56
+ "--context-json", JSON.stringify(resolutionContext(env, capabilities)),
57
+ ]);
58
+ },
59
+ async probeHost({
60
+ hostRuntime,
61
+ entryMode,
62
+ projectRoot,
63
+ homeDir,
64
+ availableFunctions = [],
65
+ }) {
66
+ const args = [
67
+ "probe",
68
+ "--host", hostRuntime,
69
+ "--entry-mode", entryMode,
70
+ "--project-root", projectRoot,
71
+ "--home-dir", homeDir,
72
+ ];
73
+ for (const availableFunction of availableFunctions) {
74
+ args.push("--available-function", availableFunction);
75
+ }
76
+ return await callHosts(runPython, args);
77
+ },
78
+ };
79
+ }
80
+
81
+ const hostRegistryClient = createHostRegistryClient();
82
+
83
+ export function normalizeHostRequest(value) {
84
+ const runtime = String(value ?? "").trim() || RUNTIME_AUTO;
85
+ if (!HOST_REQUEST_PATTERN.test(runtime)) {
86
+ throw new Error(
87
+ `invalid runtime '${runtime}' (expected auto, all, or a lowercase host ID)`,
88
+ );
89
+ }
90
+ return runtime;
91
+ }
92
+
93
+ function failedResolution(requestedRuntime, error) {
94
+ return {
95
+ ok: false,
96
+ requestedRuntime,
97
+ resolvedRuntime: null,
98
+ launchMode: null,
99
+ installTargets: [],
100
+ host: "unknown",
101
+ confidence: "low",
102
+ reason: error.message,
103
+ fallbackFrom: null,
104
+ warnings: [],
105
+ };
106
+ }
107
+
108
+ function catalogResolution(catalog) {
109
+ const hosts = Array.isArray(catalog?.hosts) ? catalog.hosts : [];
110
+ const catalogHostIds = hosts.map((host) => host.id);
111
+ const installTargets = Array.from(new Set(
112
+ hosts.flatMap((host) => host.install_targets ?? host.installTargets ?? []),
113
+ )).sort();
114
+ return {
115
+ ok: true,
116
+ requestedRuntime: "all",
117
+ resolvedRuntime: "all",
118
+ launchMode: "catalog",
119
+ installTargets,
120
+ catalogHostIds,
121
+ host: "explicit",
122
+ confidence: "high",
123
+ reason: "All registered host runtimes requested.",
124
+ fallbackFrom: null,
125
+ warnings: [],
126
+ };
127
+ }
128
+
129
+ export async function loadHostCatalog() {
130
+ return await hostRegistryClient.catalog();
131
+ }
132
+
133
+ export async function resolveHostRuntime(options) {
134
+ return await hostRegistryClient.resolveHostRuntime(options);
135
+ }
136
+
137
+ export async function probeHost(options) {
138
+ return await hostRegistryClient.probeHost(options);
139
+ }
140
+
141
+ export async function resolveRuntime({
142
+ requestedRuntime = RUNTIME_AUTO,
143
+ command = "run",
144
+ env = {},
145
+ capabilities = {},
146
+ } = {}) {
147
+ const request = normalizeHostRequest(requestedRuntime);
148
+ try {
149
+ if (request === "all") return catalogResolution(await loadHostCatalog());
150
+ return await resolveHostRuntime({
151
+ requestedRuntime: request,
152
+ command,
153
+ env,
154
+ capabilities,
155
+ });
156
+ } catch (error) {
157
+ if (!(error instanceof HostRegistryClientError)) throw error;
158
+ return failedResolution(request, error);
159
+ }
160
+ }
161
+
162
+ export async function checkRuntimeReadiness({
163
+ runtime,
164
+ projectRoot,
165
+ homeDir = homedir(),
166
+ hostRegistryClient = { probeHost },
167
+ }) {
168
+ if (!runtime) return { runtime: "auto", status: "ready", checks: [] };
169
+ return await hostRegistryClient.probeHost({
170
+ hostRuntime: runtime,
171
+ entryMode: "current-session",
172
+ projectRoot,
173
+ homeDir,
174
+ availableFunctions: [PLAIN_TEXT_INPUT],
175
+ });
176
+ }
@@ -1,9 +1,7 @@
1
1
  export const RUNTIMES_MANIFEST_REL = "installed-runtimes.json";
2
2
  export const RUNTIME_MANIFEST_SCHEMA_VERSION = 2;
3
- export const EXPLICIT_RUNTIME_VALUES = ["claude-code", "codex", "antigravity", "external"];
4
-
5
3
  export function runtimeListForSelection(runtime, resolution = null) {
6
- if (runtime === "all") return [...EXPLICIT_RUNTIME_VALUES];
4
+ if (runtime === "all") return [...(resolution?.catalogHostIds ?? [])];
7
5
  if (runtime === "auto") {
8
6
  const resolved = resolution?.resolvedRuntime;
9
7
  return resolved && resolved !== "all" ? [resolved] : [];
@@ -11,10 +9,8 @@ export function runtimeListForSelection(runtime, resolution = null) {
11
9
  return [runtime];
12
10
  }
13
11
 
14
- function includesClaudeRuntime(runtime, resolution = null) {
15
- if (runtime === "all") return true;
16
- if (runtime === "claude-code") return true;
17
- return runtime === "auto" && resolution?.resolvedRuntime === "claude-code";
12
+ function includesClaudeAssets(resolution) {
13
+ return resolution?.installTargets?.includes("claude") === true;
18
14
  }
19
15
 
20
16
  function defaultResolution(runtime) {
@@ -22,6 +18,8 @@ function defaultResolution(runtime) {
22
18
  return {
23
19
  requestedRuntime: runtime,
24
20
  resolvedRuntime: runtime,
21
+ launchMode: null,
22
+ installTargets: [],
25
23
  host: "explicit",
26
24
  confidence: "high",
27
25
  reason: `Explicit runtime '${runtime}' requested.`,
@@ -37,7 +35,7 @@ export function buildRuntimeManifest(
37
35
  installedAssets = null,
38
36
  ) {
39
37
  const runtimeResolution = resolution ?? defaultResolution(runtime);
40
- const includesClaude = includesClaudeRuntime(runtime, runtimeResolution);
38
+ const includesClaude = includesClaudeAssets(runtimeResolution);
41
39
  const defaultAssets = {
42
40
  sharedRuntime: true,
43
41
  claudeSkills: includesClaude,
@@ -1,61 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Split `agy --output-format stream-json` output for okstra-antigravity-exec.sh.
3
-
4
- Everything agy emits is appended to the live log, so the log becomes an audit
5
- trail of what the worker actually did — which tools it called, on which paths,
6
- for how long. In the default `text` format agy prints only its closing summary,
7
- so an antigravity dispatch left ~2KB of self-report against codex's ~487KB of
8
- tool trace, and whether the worker ever opened the evidence it was asked to
9
- verify could not be checked from outside.
10
-
11
- stdout stays what the dispatcher's contract expects: the final response text
12
- only, so the subagent's captured output is unchanged by the format switch.
13
- """
14
- from __future__ import annotations
15
-
16
- import json
17
- import sys
18
-
19
-
20
- def main() -> int:
21
- if len(sys.argv) < 2:
22
- print("usage: okstra-wrapper-agy-stream.py <log-path>", file=sys.stderr)
23
- return 2
24
- log_path = sys.argv[1]
25
- final: str | None = None
26
- # Lines agy emitted that are not stream-json events. Normally empty; kept so
27
- # a non-JSON emitter (a stub, an older agy, a crash banner) still reaches
28
- # stdout instead of being silently swallowed.
29
- passthrough: list[str] = []
30
- with open(log_path, "a", encoding="utf-8") as log:
31
- for line in sys.stdin:
32
- log.write(line)
33
- log.flush()
34
- stripped = line.strip()
35
- if not stripped:
36
- continue
37
- try:
38
- event = json.loads(stripped)
39
- except json.JSONDecodeError:
40
- passthrough.append(line)
41
- continue
42
- if isinstance(event, dict) and event.get("event") == "result":
43
- result = event.get("result")
44
- if isinstance(result, dict):
45
- final = result.get("response") or ""
46
- if final is not None:
47
- sys.stdout.write(final)
48
- elif passthrough:
49
- sys.stdout.writelines(passthrough)
50
- else:
51
- print(
52
- "okstra-wrapper-agy-stream: no result event in agy output — "
53
- f"see {log_path}",
54
- file=sys.stderr,
55
- )
56
- sys.stdout.flush()
57
- return 0
58
-
59
-
60
- if __name__ == "__main__":
61
- raise SystemExit(main())