@aiwg/cli 2026.10.0 → 2026.10.3

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 (168) hide show
  1. package/agentic/code/providers/capability-matrix.yaml +270 -0
  2. package/agentic/code/providers/model-capabilities.v1.json +3091 -24
  3. package/bin/aiwg.mjs +86 -21
  4. package/dist/src/a2a/codecs.js +9 -0
  5. package/dist/src/artifacts/corpus-tools/vision-extract.js +105 -15
  6. package/dist/src/artifacts/repair.js +9 -4
  7. package/dist/src/channel/manager.mjs +59 -10
  8. package/dist/src/cli/child-process-result.js +40 -0
  9. package/dist/src/cli/env.js +38 -16
  10. package/dist/src/cli/flag-parsing.js +236 -0
  11. package/dist/src/cli/handlers/decision.js +297 -2
  12. package/dist/src/cli/handlers/diagnose.js +9 -2
  13. package/dist/src/cli/handlers/feedback.js +14 -5
  14. package/dist/src/cli/handlers/init.js +245 -30
  15. package/dist/src/cli/handlers/mc.js +23 -0
  16. package/dist/src/cli/handlers/models.js +69 -6
  17. package/dist/src/cli/handlers/ralph.js +3 -0
  18. package/dist/src/cli/handlers/refresh.js +16 -0
  19. package/dist/src/cli/handlers/regenerate.js +14 -0
  20. package/dist/src/cli/handlers/runtime-info.js +24 -9
  21. package/dist/src/cli/handlers/script-runner.js +16 -6
  22. package/dist/src/cli/handlers/session.js +396 -62
  23. package/dist/src/cli/handlers/sessions.js +146 -40
  24. package/dist/src/cli/handlers/setup-manifest.js +439 -39
  25. package/dist/src/cli/handlers/setup.js +139 -39
  26. package/dist/src/cli/handlers/subcommands.js +3 -1
  27. package/dist/src/cli/handlers/team.js +18 -8
  28. package/dist/src/cli/handlers/use.js +13 -0
  29. package/dist/src/cli/handlers/utilities.js +106 -23
  30. package/dist/src/cli/prompt-utils.js +72 -0
  31. package/dist/src/cli/provider-resolution.js +11 -0
  32. package/dist/src/config/aiwg-config.js +268 -4
  33. package/dist/src/config/cli.js +146 -19
  34. package/dist/src/config/dispatch-thresholds-runtime.mjs +38 -0
  35. package/dist/src/config/project-artifacts-health.mjs +14 -3
  36. package/dist/src/config/user-config.js +36 -10
  37. package/dist/src/decision/adapters/jev.js +80 -28
  38. package/dist/src/decision/ask.js +352 -0
  39. package/dist/src/decision/driver.js +4 -0
  40. package/dist/src/decision/entry.js +38 -6
  41. package/dist/src/decision/heldout/collector.js +10 -1
  42. package/dist/src/decision/index.js +3 -0
  43. package/dist/src/decision/interop.js +60 -8
  44. package/dist/src/decision/jev-credentials.js +121 -0
  45. package/dist/src/decision/ledger.js +839 -0
  46. package/dist/src/decision/validate.js +6 -3
  47. package/dist/src/effects/dispatch-verifier.js +252 -0
  48. package/dist/src/effects/index.js +1 -0
  49. package/dist/src/features/installer.js +2 -2
  50. package/dist/src/flow/graph-metadata.js +5 -0
  51. package/dist/src/gates/providers/loader.js +3 -1
  52. package/dist/src/installation/manager-command.mjs +1 -1
  53. package/dist/src/installation/manager.mjs +17 -2
  54. package/dist/src/lint/runner.js +2 -2
  55. package/dist/src/mcp/adapters/codex-runtime.js +360 -56
  56. package/dist/src/mcp/cli.mjs +587 -77
  57. package/dist/src/mcp/config-file.mjs +279 -0
  58. package/dist/src/mcp/config-layers.mjs +348 -0
  59. package/dist/src/mcp/credentials.mjs +484 -0
  60. package/dist/src/mcp/grok-build-config.mjs +16 -26
  61. package/dist/src/mcp/helpers.mjs +106 -2
  62. package/dist/src/mcp/omp-config.mjs +50 -29
  63. package/dist/src/mcp/profiles.js +97 -23
  64. package/dist/src/mcp/profiles.mjs +99 -27
  65. package/dist/src/mcp/registry.js +264 -57
  66. package/dist/src/mcp/registry.mjs +198 -45
  67. package/dist/src/mcp/server.mjs +403 -20
  68. package/dist/src/mcp/toml-string.mjs +9 -0
  69. package/dist/src/mcp/toml-strip-mcp.mjs +135 -0
  70. package/dist/src/mcp/tool-filters.mjs +357 -0
  71. package/dist/src/mcp/tools/agentic-sandbox.mjs +3 -1
  72. package/dist/src/mcp/tools/decision.mjs +58 -0
  73. package/dist/src/mcp/tools/orchestration.mjs +38 -0
  74. package/dist/src/mcp/tools/subsystems.mjs +39 -8
  75. package/dist/src/mcp/tools/worker-invoke.mjs +1539 -0
  76. package/dist/src/mcp/tools/worker-shell-control.mjs +258 -0
  77. package/dist/src/mcp/tools/worker-stdin-runtime.mjs +79 -0
  78. package/dist/src/memory/project-registry.js +2 -2
  79. package/dist/src/mission-protocol/codecs.js +639 -1
  80. package/dist/src/mission-protocol/types.js +110 -0
  81. package/dist/src/mission-protocol/worker-status.js +2 -0
  82. package/dist/src/models/config-loader.js +70 -13
  83. package/dist/src/models/grok-build-models.js +93 -41
  84. package/dist/src/models/model-capabilities.v1.json +3091 -24
  85. package/dist/src/models/model-discovery.js +862 -63
  86. package/dist/src/models/provider-policy.js +625 -11
  87. package/dist/src/models/qualification-digest.mjs +17 -0
  88. package/dist/src/models/resolver.js +259 -0
  89. package/dist/src/models/worker-direct-readiness.js +529 -0
  90. package/dist/src/models/worker-dispatch-offer.js +169 -0
  91. package/dist/src/models/worker-health-readiness.js +131 -0
  92. package/dist/src/models/worker-installation.mjs +879 -0
  93. package/dist/src/models/wrapper-route.js +18 -0
  94. package/dist/src/ops/registry.js +9 -14
  95. package/dist/src/plugins/standalone-packager.js +4 -3
  96. package/dist/src/process/bounded-child-process.mjs +387 -0
  97. package/dist/src/process/cli-command.mjs +75 -0
  98. package/dist/src/providers/capability-matrix.js +8 -0
  99. package/dist/src/providers/capability-matrix.yaml +270 -0
  100. package/dist/src/providers/grok-build-headless.js +43 -21
  101. package/dist/src/providers/provider-definitions.js +9 -1
  102. package/dist/src/providers/provider-definitions.mjs +9 -2
  103. package/dist/src/serve/executor-registry.js +16 -24
  104. package/dist/src/serve/fleet-mission-conductor.js +9 -0
  105. package/dist/src/serve/mission-conductor.js +9 -0
  106. package/dist/src/serve/shared-host-scheduler.js +2 -0
  107. package/dist/src/serve/worker-invocation.js +538 -0
  108. package/dist/src/serve/worker-stdin-frame.mjs +107 -0
  109. package/dist/src/sessions/adapter-registry.js +45 -0
  110. package/dist/src/sessions/adapters/generic.js +37 -3
  111. package/dist/src/sessions/adapters/grokbot-locator.js +111 -0
  112. package/dist/src/sessions/adapters/grokbot.js +423 -24
  113. package/dist/src/sessions/adapters/muse-workspace-scan.js +108 -0
  114. package/dist/src/sessions/adapters/muse.js +68 -29
  115. package/dist/src/sessions/adapters/pi.js +1 -0
  116. package/dist/src/sessions/batch-contracts.js +13 -1
  117. package/dist/src/sessions/batch-import.js +23 -42
  118. package/dist/src/sessions/candidates.js +47 -11
  119. package/dist/src/sessions/contracts.js +35 -0
  120. package/dist/src/sessions/coverage-preflight.js +134 -0
  121. package/dist/src/sessions/discovery.js +3 -0
  122. package/dist/src/sessions/grok-build-cli.js +67 -5
  123. package/dist/src/sessions/importer.js +24 -8
  124. package/dist/src/sessions/index.js +2 -0
  125. package/dist/src/sessions/repository.js +54 -13
  126. package/dist/src/sessions/workspace-discovery.js +99 -37
  127. package/dist/src/skills/adapters/clawhub.js +10 -8
  128. package/dist/src/skills/adapters/openclaw.js +10 -7
  129. package/dist/src/smiths/context-pipeline/parallelism-section.js +192 -3
  130. package/dist/src/smiths/context-pipeline/workspace-context.js +116 -9
  131. package/dist/src/smiths/hook-bridge/codex-translator.js +6 -5
  132. package/dist/src/smiths/mcpsmith/example.js +1 -1
  133. package/dist/src/smiths/toolsmith/runtime-discovery.mjs +10 -14
  134. package/dist/src/storage/backends/logseq.js +68 -7
  135. package/dist/src/storage/fortemi-qualification-receipt.js +2 -2
  136. package/dist/src/testing/generators/test-code-generator.js +57 -30
  137. package/dist/src/uhp/client.js +11 -1
  138. package/dist/src/uhp/mission.js +5 -0
  139. package/dist/src/update/checker.mjs +5 -5
  140. package/dist/src/update/notifier.mjs +54 -2
  141. package/dist/src/writing/fidelity.js +18 -1
  142. package/package.json +18 -1
  143. package/schemas/mission-v1.schema.json +2429 -0
  144. package/schemas/models/model-catalog.v1.schema.json +301 -0
  145. package/schemas/models/model-config-compatibility.v1.schema.json +281 -0
  146. package/schemas/models/model-policy.v1.schema.json +14 -0
  147. package/schemas/models/provider-model-capabilities.v1.schema.json +1066 -0
  148. package/tools/_resolve-impl.mjs +39 -0
  149. package/tools/agents/deploy-agents.mjs +15 -2
  150. package/tools/agents/providers/base.mjs +63 -43
  151. package/tools/agents/providers/grokbot.mjs +11 -12
  152. package/tools/agents/providers/hermes.mjs +6 -3
  153. package/tools/agents/providers/omp.mjs +5 -2
  154. package/tools/agents/providers/openclaw-translator.mjs +19 -5
  155. package/tools/providers/deepseek-harness-transport.mjs +358 -0
  156. package/tools/providers/omp-transport.mjs +192 -0
  157. package/tools/ralph-external/lib/claude-adapter.mjs +389 -0
  158. package/tools/ralph-external/lib/codex-adapter.mjs +432 -0
  159. package/tools/ralph-external/lib/deepseek-harness-adapter.mjs +397 -0
  160. package/tools/ralph-external/lib/factory-adapter.mjs +503 -0
  161. package/tools/ralph-external/lib/muse-adapter.mjs +364 -0
  162. package/tools/ralph-external/lib/omp-adapter.mjs +315 -0
  163. package/tools/ralph-external/lib/opencode-adapter.mjs +575 -0
  164. package/tools/ralph-external/lib/openhuman-adapter.mjs +75 -0
  165. package/tools/ralph-external/lib/pi-adapter.mjs +333 -0
  166. package/tools/ralph-external/lib/provider-adapter.mjs +690 -0
  167. package/tools/ralph-external/lib/worker-health.mjs +439 -0
  168. package/vscode-extension/schemas/aiwg.config.v1.json +1953 -0
@@ -0,0 +1,575 @@
1
+ /**
2
+ * OpenCode CLI Provider Adapter for External Ralph Loop
3
+ *
4
+ * Provides support for OpenCode as a provider for
5
+ * autonomous task execution in Ralph loops.
6
+ *
7
+ * OpenCode CLI differences from Claude:
8
+ * - Binary: `opencode` instead of `claude`
9
+ * - Headless mode: `opencode run` subcommand
10
+ * - Format flag: `--format json` instead of `--output-format stream-json`
11
+ * - No --dangerously-skip-permissions (no permission bypass needed)
12
+ * - No --print flag (run subcommand is already non-interactive)
13
+ * - No --append-system-prompt (inject into main prompt)
14
+ * - No --max-budget-usd flag
15
+ * - No --max-turns flag
16
+ * - No --mcp-config flag
17
+ * - Session resume via -s/--session instead of --session-id
18
+ * - Model format: provider/model (e.g., opencode/big-pickle, anthropic/claude-sonnet-5)
19
+ * - Agent flag: --agent
20
+ * - JSON output: newline-delimited events, text in type="text" events
21
+ *
22
+ * Model tiers:
23
+ * - Free tier (default): opencode/* models (opencode/big-pickle, etc.)
24
+ * - Anthropic account: anthropic/claude-* models available after `opencode auth add`
25
+ *
26
+ * @implements Plan: Multi-Provider Support for External Ralph Loop
27
+ */
28
+
29
+ /**
30
+ * Version-bound direct-worker surface (roctinam/aiwg#2904, B-OG-39).
31
+ *
32
+ * Qualified against OpenCode CLI 2.0.26 `opencode --help` / `opencode run
33
+ * --help` (help/version syntax captured on host 'noodle' 2026-10-10; see
34
+ * .aiwg/working/s1-scratch/cli-help/opencode-2.0.26.txt) plus one live
35
+ * smoke run and binary disassembly (.aiwg/working/s1-scratch/
36
+ * smoke-opencode.jsonl, 2026-10-10) that redefine final-output validity
37
+ * below; that live evidence is scoped to `parseOutput`'s rule and never
38
+ * promotes the registry's `qualification.status` beyond `unqualified`:
39
+ *
40
+ * opencode run --format json -m M[#E] -f F "Follow the instructions in
41
+ * the attached file exactly."
42
+ *
43
+ * Stdin is confirmed unsupported, not merely undocumented: the conductor
44
+ * disassembled the installed v2.0.26 binary on host 'noodle' and the
45
+ * compiled chunk implementing `run` has no stdin/isTTY handling at all.
46
+ * `run [flags] [<message...>]` takes the message as a trailing positional
47
+ * argument, but `--file`/`-f` ("File to attach to the message") lets the
48
+ * brief ride as an attached file instead: this adapter writes the brief to
49
+ * `briefPath` and passes it via `-f`, so brief bytes never enter argv. The
50
+ * trailing positional is a fixed, content-free instruction
51
+ * (`OPENCODE_FIXED_MESSAGE`) that never carries brief text; `buildWorkerArgs`
52
+ * refuses anything else in that position. `-m/--model` takes
53
+ * `provider/model#variant`; binary disassembly resolves what `#variant`
54
+ * means: the installed CLI builds each model's variant list with a
55
+ * function (minified as `qU(model, manual)`) that maps
56
+ * `model.capabilities.supports.reasoning_effort` (a model-declared string
57
+ * list, e.g. low/medium/high) directly onto variant ids via
58
+ * `{ id: value, settings: { reasoningEffort: value, ... } }`, or — for
59
+ * extended-thinking models exposing `max_thinking_budget` instead — onto
60
+ * two fixed ids, `"max"` and `"high"`. So `#variant` IS a reasoning-effort
61
+ * selector, but the valid id set is per-model (drawn from that model's own
62
+ * catalog capability), not a fixed global enum; `buildWorkerArgs` appends
63
+ * an explicit effort as `#<effort>`. Whether the real CLI refuses an id a
64
+ * model does not expose, or silently falls back to default reasoning, is
65
+ * unverified, so the effort control stays unqualified. There is no `--dir`/`--cwd`
66
+ * flag on `run`: the shared renderer's `cd ${workspace}` sets the process
67
+ * working directory instead. The only permission-adjacent flag is `--auto`
68
+ * ("Auto-approve permissions that are not explicitly denied"), a blanket
69
+ * bypass this profile never emits. What happens to a permission request
70
+ * when `--auto` is absent in non-interactive `run` is not fully resolved:
71
+ * disassembly shows a shared `permission.asked` -> `permission.replied`
72
+ * protocol where an unanswered/unselected request resolves to a `"reject"`
73
+ * outcome (`_==="once"||_==="always"?_:"reject"`, with
74
+ * `requestPermission(...).catch(()=>{return})` on no responder) — this is
75
+ * evidence of reject-on-no-answer rather than an indefinite hang, but it is
76
+ * traced through the ACP/session-protocol layer, not independently
77
+ * reproduced against the plain `run` subcommand, so approval stays
78
+ * `unqualified`/unavailable rather than asserted. `opencode serve` is an
79
+ * HTTP API (not MCP) and `opencode mcp` manages MCP *clients* the agent can
80
+ * call out to, not a worker-serving endpoint, so the AIWG `worker-invoke`
81
+ * bridge remains the MCP route (native `mcp-server`-style hosting is not
82
+ * advertised).
83
+ *
84
+ * Installed-module shape: static `interfaceVersion` and `register` exports
85
+ * so the #2885 installed loader can approve this file by digest under
86
+ * `installed:opencode` (mirroring #2897 codex-adapter.mjs / #2911
87
+ * openhuman-adapter.mjs). The bottom self-registration targets the legacy
88
+ * Ralph-loop registry only; the direct worker profile loads through the
89
+ * installed loader.
90
+ */
91
+ import { ProviderAdapter, registerProvider } from './provider-adapter.mjs';
92
+ import { classifyWorkerHealthFailure, healthEventMatchesBinding } from './worker-health.mjs';
93
+
94
+ export const OPENCODE_SUPPORTED_VERSIONS = Object.freeze(['2.0.26']);
95
+ /** Stdin is confirmed unsupported by the compiled v2.0.26 `run` chunk (see
96
+ * module doc); the brief travels via `-f`/`--file` instead. */
97
+ export const OPENCODE_DIRECT_STDIN = 'closed';
98
+ /** Fixed, content-free positional message: never the brief, so brief bytes
99
+ * never reach argv even though `-f` carries the real content. */
100
+ export const OPENCODE_FIXED_MESSAGE = 'Follow the instructions in the attached file exactly.';
101
+ /** No scoped sandbox flag is evidenced; `--auto` is a blanket bypass and is
102
+ * never emitted, so the direct profile only accepts externally-enforced
103
+ * sandboxing. */
104
+ export const OPENCODE_DIRECT_SANDBOX_MODES = Object.freeze(['external']);
105
+ /** Generic Ralph-loop aliases resolve through MODEL_MAP below; the direct
106
+ * profile requires an exact observed catalog model instead. */
107
+ const OPENCODE_GENERIC_ALIASES = Object.freeze(['opus', 'sonnet', 'haiku']);
108
+
109
+ /**
110
+ * Model mapping from generic names to OpenCode provider/model format.
111
+ *
112
+ * Free tier uses opencode/* models. Users who have connected an Anthropic
113
+ * account via `opencode auth add` can pass anthropic/* IDs directly
114
+ * (e.g., --model anthropic/claude-sonnet-5) and they pass through unchanged
115
+ * because mapModel() only substitutes known generic aliases.
116
+ */
117
+ const MODEL_MAP = {
118
+ 'opus': 'opencode/big-pickle',
119
+ 'sonnet': 'opencode/big-pickle',
120
+ 'haiku': 'opencode/big-pickle',
121
+ };
122
+
123
+ export class OpenCodeAdapter extends ProviderAdapter {
124
+ /**
125
+ * Own JSONL grammar observed from `opencode run --format json`
126
+ * (test/fixtures/providers/opencode/health/stream.ndjson, #2896):
127
+ * `step_start` (a model step began), `text` (streamed answer text in
128
+ * `part.text`) and `step_finish` (the step ended, `part.reason` carries
129
+ * `"stop"` on a clean finish). Handshake stays `process-only`: these are
130
+ * step-scoped events inside one run, not an independently-evidenced
131
+ * session-establishment signal, so only identity correlation (`setup`)
132
+ * and the terminal/error outcome are reported; accepted-start and
133
+ * distinct-item progress remain unavailable pending qualification.
134
+ *
135
+ * `step_start`/`step_finish` are BOTH OPTIONAL (roctinam/aiwg#2904
136
+ * follow-up, live 2.0.26 smoke run + binary disassembly, host 'noodle',
137
+ * 2026-10-10): the run command's event loop returns silently on
138
+ * `session.execution.succeeded` with no JSON event of any kind, and a
139
+ * live run that never produced a `session.step.*` pair emitted a single
140
+ * `text` event and nothing else — confirmed success, not a truncation.
141
+ * Their absence is therefore never treated as failure here; see
142
+ * `parseOutput` for the strict final-validity rule this implies.
143
+ */
144
+ observeHealthEvent(event, source, binding) {
145
+ if (source.kind !== 'stdout' || !event || typeof event !== 'object') return null;
146
+ if (!healthEventMatchesBinding({ sessionId: event.sessionID }, binding)) return null;
147
+ if (event.type === 'step_start') return { kind: 'setup', sessionId: event.sessionID };
148
+ // --format json is JSONL despite the legacy streamJson=false boolean.
149
+ if (event.type === 'text' && typeof event.part?.text === 'string') {
150
+ return { kind: 'activity', key: event.part.id };
151
+ }
152
+ if (event.type === 'step_finish') {
153
+ if (event.part?.reason === 'stop') return { kind: 'terminal', success: true, key: event.part?.id ?? 'step_finish' };
154
+ return { ...classifyWorkerHealthFailure({ type: 'error', message: event.part?.reason }, { source, provider: 'opencode' }),
155
+ reasonCode: 'PROVIDER_ERROR', fatal: true };
156
+ }
157
+ return null;
158
+ }
159
+ /** @returns {string} */
160
+ getBinary() {
161
+ return 'opencode';
162
+ }
163
+
164
+ /** @returns {string} */
165
+ getName() {
166
+ return 'opencode';
167
+ }
168
+
169
+ /**
170
+ * OpenCode has limited capabilities compared to Claude.
171
+ * Supports session resume, model selection, agent mode, and JSON output.
172
+ * @returns {import('./provider-adapter.mjs').ProviderCapabilities}
173
+ */
174
+ getCapabilities() {
175
+ return {
176
+ streamJson: false,
177
+ sessionResume: true,
178
+ budgetControl: false,
179
+ systemPrompt: false,
180
+ agentMode: true,
181
+ mcpConfig: false,
182
+ maxTurns: false,
183
+ };
184
+ }
185
+
186
+ /**
187
+ * Driver-owned argv after the binary for the version-bound direct-shell
188
+ * profile. Every control is explicit: exact observed model via `-m`
189
+ * (with an explicit effort appended as `#<variant>`, see module doc),
190
+ * `--format json` for the evidenced JSONL stream, and the brief as an
191
+ * attached file via `-f` (no stdin transport is evidenced; see the
192
+ * module doc comment) — the positional argument is always the fixed,
193
+ * content-free instruction, never brief text. A sandbox mode other than
194
+ * externally-enforced is refused rather than guessed, since it has no
195
+ * evidenced CLI mapping.
196
+ *
197
+ * @param {{ selected?: { model?: unknown, effort?: { value?: unknown } }, cwd?: unknown, briefPath?: unknown }} invocation
198
+ * @param {unknown} _profile
199
+ * @param {{ sandbox?: unknown }} [permissions]
200
+ * @returns {string[]}
201
+ */
202
+ buildWorkerArgs(invocation, _profile, permissions) {
203
+ const model = invocation?.selected?.model;
204
+ if (typeof model !== 'string' || !model || model.includes('\0')) {
205
+ throw new Error('OpenCode worker requires an explicit model');
206
+ }
207
+ if (OPENCODE_GENERIC_ALIASES.includes(model.toLowerCase())) {
208
+ throw new Error(`OpenCode worker requires an exact observed model, not the generic alias '${model}'`);
209
+ }
210
+ // #variant is a reasoning-effort selector (binary-evidenced; see module
211
+ // doc), but the valid id set is model-specific and not enumerated here,
212
+ // so a syntactically valid id is appended. Whether the real CLI refuses
213
+ // an undeclared id or silently falls back is unverified (effort stays
214
+ // unqualified).
215
+ const effort = invocation?.selected?.effort?.value ?? null;
216
+ let modelToken = model;
217
+ if (effort !== null) {
218
+ if (typeof effort !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(effort)) {
219
+ throw new Error('OpenCode worker effort must be a plain variant id (no "#", "/", whitespace or control characters)');
220
+ }
221
+ modelToken = `${model}#${effort}`;
222
+ }
223
+ const sandbox = permissions?.sandbox ?? 'external';
224
+ if (!OPENCODE_DIRECT_SANDBOX_MODES.includes(sandbox)) {
225
+ throw new Error(`OpenCode worker sandbox is not a qualified direct mode: ${String(sandbox)}`);
226
+ }
227
+ const cwd = invocation?.cwd;
228
+ if (typeof cwd !== 'string' || !cwd.startsWith('/') || cwd.includes('\0')) {
229
+ throw new Error('OpenCode worker requires an absolute workspace');
230
+ }
231
+ // Stdin is confirmed unsupported (binary disassembly; see module doc),
232
+ // so the brief rides on `-f`/`--file` as a path, never as argv content.
233
+ const briefPath = invocation?.briefPath;
234
+ if (typeof briefPath !== 'string' || !briefPath.startsWith('/') || briefPath.includes('\0')) {
235
+ throw new Error('OpenCode worker requires an absolute brief-file path');
236
+ }
237
+ return ['run', '--format', 'json', '-m', modelToken, '-f', briefPath, OPENCODE_FIXED_MESSAGE];
238
+ }
239
+
240
+ /** No evidenced stdin abort channel for `opencode run`. */
241
+ getAbortInput() {
242
+ return null;
243
+ }
244
+
245
+ /**
246
+ * Build args for the main headless session.
247
+ *
248
+ * OpenCode uses `opencode run --format json` for headless operation.
249
+ *
250
+ * @param {import('./provider-adapter.mjs').SessionArgs} options
251
+ * @returns {string[]}
252
+ */
253
+ buildSessionArgs(options) {
254
+ const args = [
255
+ 'run',
256
+ '--format', 'json',
257
+ ];
258
+
259
+ // Model selection (map generic to OpenCode provider/model format)
260
+ if (options.model) {
261
+ args.push('-m', this.mapModel(options.model));
262
+ }
263
+
264
+ // Session resume via -s flag
265
+ if (options.sessionId) {
266
+ args.push('-s', options.sessionId);
267
+ }
268
+
269
+ // Budget control not supported
270
+ if (options.budget) {
271
+ this.warnUnsupported('budgetControl', 'Budget control (--max-budget-usd)');
272
+ }
273
+
274
+ // Max turns not supported
275
+ if (options.maxTurns) {
276
+ this.warnUnsupported('maxTurns', 'Max turns (--max-turns)');
277
+ }
278
+
279
+ // MCP configuration not supported
280
+ if (options.mcpConfig) {
281
+ this.warnUnsupported('mcpConfig', 'MCP configuration (--mcp-config)');
282
+ }
283
+
284
+ // System prompt: OpenCode doesn't support --append-system-prompt,
285
+ // so we prepend it to the main prompt
286
+ let prompt = options.prompt;
287
+ if (options.systemPrompt) {
288
+ prompt = `[System Context]\n${options.systemPrompt}\n\n[Task]\n${prompt}`;
289
+ }
290
+
291
+ // The prompt itself (must be last)
292
+ args.push(prompt);
293
+
294
+ return args;
295
+ }
296
+
297
+ /**
298
+ * Build args for short analysis calls (spawnSync).
299
+ *
300
+ * @param {import('./provider-adapter.mjs').AnalysisArgs} options
301
+ * @returns {string[]}
302
+ */
303
+ buildAnalysisArgs(options) {
304
+ const args = [
305
+ 'run',
306
+ '--format', 'json',
307
+ ];
308
+
309
+ // Model selection
310
+ if (options.model) {
311
+ args.push('-m', this.mapModel(options.model));
312
+ }
313
+
314
+ // Agent flag supported by OpenCode
315
+ if (options.agent) {
316
+ args.push('--agent', options.agent);
317
+ }
318
+
319
+ // The analysis prompt (must be last)
320
+ args.push(options.prompt);
321
+
322
+ return args;
323
+ }
324
+
325
+ /**
326
+ * Map generic model names to OpenCode provider/model format.
327
+ *
328
+ * @param {string} genericModel
329
+ * @returns {string}
330
+ */
331
+ mapModel(genericModel) {
332
+ const mapped = MODEL_MAP[genericModel.toLowerCase()];
333
+ if (mapped) return mapped;
334
+ // Pass through if already an OpenCode model name or unknown
335
+ return genericModel;
336
+ }
337
+
338
+ /**
339
+ * Environment overrides for headless OpenCode sessions.
340
+ * @returns {Object<string, string>}
341
+ */
342
+ getEnvOverrides() {
343
+ return {
344
+ CI: 'true',
345
+ };
346
+ }
347
+
348
+ /**
349
+ * OpenCode does not store session transcripts in a known location.
350
+ * @returns {null}
351
+ */
352
+ getTranscriptPath() {
353
+ return null;
354
+ }
355
+
356
+ /**
357
+ * Parse OpenCode output — extracts text from newline-delimited JSON
358
+ * events under a strict, binary-evidenced final-validity rule
359
+ * (roctinam/aiwg#2904 follow-up, 2026-10-10).
360
+ *
361
+ * There is NO positive session-terminal event in this stream in ANY
362
+ * mode: disassembly of the installed `opencode` 2.0.26 binary shows the
363
+ * `run` command's event loop returns silently on
364
+ * `session.execution.succeeded` ("...if(t.type==="session.execution.
365
+ * succeeded")return}") — no JSON line is ever written for it. A live
366
+ * 2.0.26 smoke run confirmed this empirically: a successful,
367
+ * single-turn reply produced exactly one `text` event and nothing
368
+ * else — no `step_start`, no `step_finish`
369
+ * (.aiwg/working/s1-scratch/smoke-opencode.jsonl). `step_start`/
370
+ * `step_finish` ARE still evidenced for streams that have them (older
371
+ * CLI, or a turn that does produce discrete `session.step.*` events):
372
+ * `step_finish`'s `part.reason` is unconditionally written from
373
+ * `t.data.finish` whenever a step actually ends, so when one IS present
374
+ * it stays authoritative and a non-`"stop"` reason (or an `error`
375
+ * event, emitted from `session.step.failed`/`session.execution.failed`/
376
+ * a non-user `session.execution.interrupted`) still fails the run even
377
+ * at exit 0.
378
+ *
379
+ * The resulting rule, since no positive whole-run terminal exists:
380
+ * reject a malformed/unparseable line, an unrecognized event type, any
381
+ * `error` event, a present `step_finish` whose reason is not `"stop"`,
382
+ * or trailing content after a `step_finish`. Otherwise accept only if
383
+ * at least one `text` part completed (`part.time.end` present and
384
+ * finite) OR a `step_finish` with reason `"stop"` was seen — a `text`
385
+ * event with no recorded `time.end` (e.g. a dropped mid-stream
386
+ * connection) does not count on its own.
387
+ *
388
+ * Residual risk, stated plainly: a provider that truncates the stream
389
+ * right after flushing one complete `text` part, before any further
390
+ * content or an error event, is indistinguishable from a genuine
391
+ * single-part success — there is no stronger signal available from
392
+ * this CLI's `run --format json` output to rule that out.
393
+ *
394
+ * @param {string} stdout
395
+ * @returns {{ text: string }|null}
396
+ */
397
+ parseOutput(stdout) {
398
+ if (typeof stdout !== 'string' || !stdout.trim()) return null;
399
+ let text = '';
400
+ let sawCompleteText = false;
401
+ let stepFinishStop = null; // null: none seen; otherwise true/false
402
+ let terminalSeen = false;
403
+ for (const raw of stdout.split('\n')) {
404
+ const line = raw.trim();
405
+ if (!line) continue;
406
+ if (terminalSeen) return null; // trailing content after step_finish
407
+ let event;
408
+ try { event = JSON.parse(line); } catch { return null; } // malformed frame: fail closed
409
+ if (!event || typeof event !== 'object' || typeof event.type !== 'string') return null;
410
+ if (event.type === 'step_start') continue;
411
+ else if (event.type === 'text') {
412
+ if (typeof event.part?.text !== 'string') return null;
413
+ text += event.part.text;
414
+ if (Number.isFinite(event.part?.time?.end)) sawCompleteText = true;
415
+ } else if (event.type === 'step_finish') { stepFinishStop = event.part?.reason === 'stop'; terminalSeen = true; }
416
+ else if (event.type === 'error') return null; // binary-evidenced failure event (session.step/execution.failed)
417
+ else return null; // unrecognized event type: never invented, never ignored
418
+ }
419
+ if (stepFinishStop === false) return null;
420
+ if (!sawCompleteText && stepFinishStop !== true) return null; // no positive signal at all
421
+ return { text };
422
+ }
423
+
424
+ /**
425
+ * #2995 round 4 (item 1): strict direct-worker-profile final, separate
426
+ * from the legacy `parseOutput` candidate above (left byte-for-byte
427
+ * unchanged: it is also the legacy Ralph-loop group-mode candidate,
428
+ * `registerProvider('opencode', ...)` below, and `test/unit/
429
+ * ralph-external/worker-finals.test.ts` depends on it falling back to
430
+ * the generic envelope).
431
+ *
432
+ * `step_finish.part.reason` is NOT a two-valued stop/fail field: a
433
+ * multi-step run (tool use, then an answer) ends EACH intermediate step
434
+ * with its own `step_finish`, whose reason names why that step itself
435
+ * ended, not the whole run. The 2.0.26 binary (conductor strings read on
436
+ * noodle, 2026-10-11) normalizes finish reasons to `stop`, `length`,
437
+ * `tool-calls`, `content-filter`, `error`, `other` and `unknown`, and its
438
+ * own UI code treats exactly `tool-calls` (and `unknown`) as not final:
439
+ * `T.finish && !["tool-calls","unknown"].includes(T.finish)`. So
440
+ * `tool-calls` is the one evidenced "this step ended because the model
441
+ * called a tool; another step follows" reason. Every other reason,
442
+ * including `unknown` and any unrecognized value, fails closed:
443
+ * CONTINUE_REASONS is the only carve-out from "non-stop fails".
444
+ *
445
+ * Algorithm: track one candidate per OPEN step (`step_start` always
446
+ * resets it — an earlier step's text is never the final answer).
447
+ * Within a step, a later COMPLETE (`time.end` present) `text` part
448
+ * REPLACES the candidate (no evidence of a within-step text boundary
449
+ * requiring concatenation); an INCOMPLETE `text` part arriving after a
450
+ * complete candidate already exists corrupts that step's candidate
451
+ * permanently (a dropped/truncated stream is never trusted over an
452
+ * earlier value). A `step_finish` with reason `"stop"` is terminal
453
+ * ONLY when nothing legitimately follows it (enforced structurally,
454
+ * same as before) and requires a non-corrupted, non-empty candidate
455
+ * from THAT step. A `step_finish` with a CONTINUE_REASONS reason is not
456
+ * terminal and not a failure: the run proceeds (the next `step_start`
457
+ * resets the candidate as normal). Any other `step_finish` reason, or
458
+ * an `error` event, fails closed. When the stream has no step markers
459
+ * at all (the live 2.0.26 shape evidenced in .aiwg/working/s1-scratch/
460
+ * smoke-opencode.jsonl — a single `text` event and nothing else), the
461
+ * same replace/corrupt rule applies to the whole stream's standalone
462
+ * `text` events.
463
+ * @param {string} stdout
464
+ * @returns {{ text: string }|null}
465
+ */
466
+ parseDirectFinal(stdout) {
467
+ if (typeof stdout !== 'string' || !stdout.trim()) return null;
468
+ const CONTINUE_REASONS = ['tool-calls'];
469
+ let sawAnyStep = false;
470
+ let stepText = null;
471
+ let stepCorrupted = false;
472
+ let finalStepText = null;
473
+ let terminal = false;
474
+ let failed = false;
475
+ for (const raw of stdout.split('\n')) {
476
+ const line = raw.trim();
477
+ if (!line) continue;
478
+ if (terminal) return null; // nothing may follow a terminal step_finish(stop)
479
+ let event;
480
+ try { event = JSON.parse(line); } catch { return null; }
481
+ if (!event || typeof event !== 'object' || typeof event.type !== 'string') return null;
482
+ if (event.type === 'step_start') {
483
+ // A new step begins: any earlier step's candidate is never the final answer.
484
+ sawAnyStep = true; stepText = null; stepCorrupted = false;
485
+ } else if (event.type === 'text') {
486
+ if (typeof event.part?.text !== 'string') return null;
487
+ if (Number.isFinite(event.part?.time?.end)) {
488
+ stepText = event.part.text; // a later complete part REPLACES, never concatenates
489
+ } else {
490
+ // An incomplete part after an established candidate corrupts it
491
+ // permanently; an incomplete part with no prior candidate is
492
+ // simply "no positive signal yet" (handled by the end-of-stream
493
+ // checks below).
494
+ if (stepText !== null) stepCorrupted = true;
495
+ }
496
+ } else if (event.type === 'step_finish') {
497
+ sawAnyStep = true;
498
+ const reason = event.part?.reason;
499
+ if (reason === 'stop') {
500
+ terminal = true;
501
+ finalStepText = stepCorrupted ? null : stepText;
502
+ } else if (!CONTINUE_REASONS.includes(reason)) {
503
+ failed = true; // a recognized-failure or unrecognized reason fails closed
504
+ }
505
+ // CONTINUE_REASONS: not terminal, not a failure; the next step_start resets the candidate.
506
+ } else if (event.type === 'error') return null;
507
+ // The 2.0.26 run emitter writes exactly six event types: error, reasoning, step_finish, step_start, text and
508
+ // tool_use (conductor strings read on noodle, 2026-10-11). tool_use and reasoning are legitimate non-answer
509
+ // events: they never contribute answer text. Anything else is outside the emitter's vocabulary and rejects.
510
+ else if (event.type !== 'tool_use' && event.type !== 'reasoning') return null;
511
+ }
512
+ if (failed) return null;
513
+ if (sawAnyStep) {
514
+ if (!terminal || finalStepText === null || !finalStepText.trim()) return null;
515
+ return { text: finalStepText };
516
+ }
517
+ if (stepCorrupted || stepText === null || !stepText.trim()) return null;
518
+ return { text: stepText };
519
+ }
520
+ }
521
+
522
+ // Self-register on import for the legacy Ralph loop only. The direct worker
523
+ // profile loads through the #2885 installed loader instead (see below).
524
+ registerProvider('opencode', () => new OpenCodeAdapter());
525
+
526
+ /**
527
+ * #2995 round 3: `OpenCodeDirectAdapter` is the Claude-precedent `-direct`
528
+ * subclass, registered under its OWN id (`opencode-direct`, never the
529
+ * bare `opencode` legacy id above) so loading this installed module
530
+ * cannot overwrite the legacy registration in-process (src/models/
531
+ * worker-installation.mjs ~l.550). `parseOutput` here is the strict,
532
+ * last-step-only `parseDirectFinal` rule (never the loose, concatenating,
533
+ * generic-accepting legacy candidate), and `parseWorkerFinal` is strict
534
+ * with no generic-envelope fallback: this class is never reachable from
535
+ * the legacy bare id, so the legacy group-mode contract (test/unit/
536
+ * ralph-external/worker-finals.test.ts) never sees it.
537
+ */
538
+ export class OpenCodeDirectAdapter extends OpenCodeAdapter {
539
+ // #2995 round 4 (item 3): production resolves the installed adapter by
540
+ // `adapter.id` (now `opencode-direct`, item B) and src/models/
541
+ // worker-installation.mjs's `inspectWorkerInstallation` requires
542
+ // `adapter.id === instance.getName()`. `getBinary()` and the
543
+ // `provider: 'opencode'` literal inside `observeHealthEvent` are
544
+ // untouched.
545
+ getName() { return 'opencode-direct'; }
546
+
547
+ parseOutput(stdout) {
548
+ return this.parseDirectFinal(stdout);
549
+ }
550
+
551
+ /**
552
+ * @param {string|Uint8Array} output
553
+ * @param {Object} [options]
554
+ * @returns {Promise<import('../../../src/mission-protocol/types.js').WorkerClaims>}
555
+ */
556
+ async parseWorkerFinal(output, options = {}) {
557
+ if (options.format === 'mcp') return super.parseWorkerFinal(output, options);
558
+ const text = typeof output === 'string' ? output : Buffer.from(output).toString('utf8');
559
+ const final = this.parseDirectFinal(text);
560
+ if (!final || typeof final.text !== 'string' || !final.text.trim()) {
561
+ throw new Error('OpenCode worker stream did not produce a successful final answer');
562
+ }
563
+ return super.parseWorkerFinal(final.text, { ...options, format: 'json' });
564
+ }
565
+ }
566
+
567
+ /** Installed-module interface version (mirrors #2897 codex-adapter.mjs). */
568
+ export const interfaceVersion = 1;
569
+
570
+ /** Installed-module registration: exactly the one approved adapter ID. */
571
+ export function register(factory) {
572
+ factory('opencode-direct', () => new OpenCodeDirectAdapter());
573
+ }
574
+
575
+ export default OpenCodeAdapter;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * OpenHuman direct-worker driver (B-OG-47, roctinam/aiwg#2911).
3
+ *
4
+ * Version-bound to the source-evidenced `openhuman-core` 0.64.14 one-shot RPC
5
+ * call (tinyhumansai/openhuman@216ea322a3c1269dada70751ac5c08eac7dc7596):
6
+ *
7
+ * openhuman-core --model M call --method openhuman.agent_chat --params-stdin
8
+ *
9
+ * `--params-stdin` reads stdin to EOF and is mutually exclusive with
10
+ * `--params` (crates/openhuman-core/src/core/cli.rs:445-468); the result is
11
+ * pretty-printed JSON on stdout (cli.rs:486). The params document
12
+ * (`message`, `model_override`, `cwd`; agent/schemas.rs:97-105) is framed by
13
+ * the conductor from the brief (`stdin: 'brief-json'`,
14
+ * src/serve/worker-stdin-frame.mjs), so prompt text never enters argv.
15
+ *
16
+ * Installed-module shape: self-contained, with static `interfaceVersion` and
17
+ * `register` exports, so the #2885 loader can approve it by digest under
18
+ * `installed:openhuman`. It is not a Ralph loop provider (no session,
19
+ * resume, abort or effort surface is evidenced). Documentation/source
20
+ * evidence and synthetic fixtures never qualify it: the bundled profile stays
21
+ * unqualified until an authorized live run.
22
+ */
23
+
24
+ export const interfaceVersion = 1;
25
+
26
+ export const OPENHUMAN_SUPPORTED_VERSIONS = Object.freeze(['0.64.14']);
27
+ export const OPENHUMAN_SOURCE_REVISION = '216ea322a3c1269dada70751ac5c08eac7dc7596';
28
+ export const OPENHUMAN_CHAT_METHOD = 'openhuman.agent_chat';
29
+
30
+ export class OpenHumanAdapter {
31
+ getBinary() { return 'openhuman-core'; }
32
+ getName() { return 'openhuman'; }
33
+ getCapabilities() {
34
+ return { streamJson: false, sessionResume: false, budgetControl: false, systemPrompt: false,
35
+ agentMode: false, mcpConfig: false, maxTurns: false, rpcAbort: false };
36
+ }
37
+ buildSessionArgs() {
38
+ throw new Error('OpenHuman has no qualified Ralph loop session; use the direct worker profile');
39
+ }
40
+ buildAnalysisArgs() { return this.buildSessionArgs(); }
41
+
42
+ /** Driver-owned argv after the binary: no brief text, no permission flags
43
+ * (no per-call sandbox/approval control is evidenced; the qualified external
44
+ * boundary owns them) and never a guessed effort. */
45
+ buildWorkerArgs(invocation, _profile, _permissions) {
46
+ const effort = invocation?.selected?.effort?.value ?? null;
47
+ if (effort !== null) throw new Error('OpenHuman has no evidenced effort control');
48
+ const model = invocation?.selected?.model;
49
+ if (typeof model !== 'string' || !model || model.includes('\0')) throw new Error('OpenHuman worker requires an explicit model');
50
+ const cwd = invocation?.cwd;
51
+ if (typeof cwd !== 'string' || !cwd.startsWith('/') || cwd.includes('\0')) throw new Error('OpenHuman worker requires an absolute workspace');
52
+ return ['--model', model, 'call', '--method', OPENHUMAN_CHAT_METHOD, '--params-stdin'];
53
+ }
54
+
55
+ /** Final-only output: no stream grammar is evidenced, so no event is invented. */
56
+ observeHealthEvent() { return null; }
57
+
58
+ /** Strict single JSON object; truncated or non-object output is not a final. */
59
+ parseOutput(stdout) {
60
+ if (typeof stdout !== 'string' || !stdout.trim()) return null;
61
+ try {
62
+ const value = JSON.parse(stdout);
63
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
64
+ } catch { return null; }
65
+ }
66
+
67
+ getAbortInput() { return null; }
68
+ }
69
+
70
+ /** Installed-module registration: exactly the one approved adapter ID. */
71
+ export function register(factory) {
72
+ factory('openhuman', () => new OpenHumanAdapter());
73
+ }
74
+
75
+ export default OpenHumanAdapter;