@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,503 @@
1
+ /**
2
+ * Factory (Droid) CLI Provider Adapter for External Ralph Loop
3
+ *
4
+ * Provides support for Factory's Droid CLI as a provider for
5
+ * autonomous task execution in Ralph loops.
6
+ *
7
+ * Droid CLI differences from Claude:
8
+ * - Binary: `droid` instead of `claude`
9
+ * - Headless mode: `droid exec` subcommand
10
+ * - Permission bypass: `--skip-permissions-unsafe` instead of `--dangerously-skip-permissions`
11
+ * - Output format: `--output-format text|stream-json` (supports stream-json!)
12
+ * - Session resume: `-s/--session-id <id>` with prompt required
13
+ * - Model selection: `-m/--model <id>` (direct model IDs, not generic)
14
+ * - No --print flag (exec is already non-interactive)
15
+ * - No --append-system-prompt (inject into main prompt)
16
+ * - No --max-budget-usd flag
17
+ * - No --max-turns flag
18
+ * - No --mcp-config flag (but has MCP subcommand)
19
+ * - No --agent flag
20
+ * - Autonomy levels: --auto low|medium|high (alternative to skip-permissions)
21
+ *
22
+ * @implements Plan: Multi-Provider Support for External Ralph Loop
23
+ */
24
+
25
+ import { ProviderAdapter, registerProvider } from './provider-adapter.mjs';
26
+ import { classifyWorkerHealthFailure, healthEventMatchesBinding } from './worker-health.mjs';
27
+
28
+ /** Model mapping from generic names to Factory/Droid model IDs */
29
+ const MODEL_MAP = {
30
+ 'opus': 'claude-opus-5',
31
+ 'sonnet': 'claude-sonnet-5',
32
+ 'haiku': 'claude-haiku-4-5-20251001',
33
+ };
34
+
35
+ /**
36
+ * Version-bound direct-worker surface (roctinam/aiwg#2902, B-OG-37).
37
+ *
38
+ * Qualified against droid-cli 0.238.0 `droid exec --help` (help/version
39
+ * syntax only, captured on host 'noodle' 2026-10-10,
40
+ * .aiwg/working/s1-scratch/cli-help/droid-0.238.0.txt; never live
41
+ * qualification):
42
+ *
43
+ * droid exec [--auto low] -m M -r E --cwd W --output-format stream-json < briefPath
44
+ *
45
+ * `exec` is Droid's non-interactive mode ("Run non-interactively (for
46
+ * scripts/automation)"); `-m/--model`, `-r/--reasoning-effort`, `--auto`,
47
+ * `--cwd` and `-o/--output-format` are all `exec`-level flags on 0.238.0
48
+ * `exec --help`. There is no file-output flag (`-o` selects the *format*,
49
+ * not a destination path): the final is read from the captured stdout
50
+ * stream instead, never a separate file. There is no positional `prompt`
51
+ * requirement either — `exec --help`'s own Examples show
52
+ * `echo "analyze code" | droid exec` and `cat prompt.txt | droid exec --auto
53
+ * medium` with no `prompt` argument at all, so omitting the positional and
54
+ * piping the brief to stdin (`stdin: 'brief-text'`) is help-evidenced, not
55
+ * invented; the brief never enters argv. `--skip-permissions-unsafe`
56
+ * ("Bypass all checks - DANGEROUS!") is Ralph-loop legacy
57
+ * (`buildSessionArgs` below) and is never emitted by the direct profile.
58
+ * The generic cross-provider sandbox contract this profile accepts is
59
+ * `read-only`/`workspace-write`; `workspace-write` maps to `--auto low`
60
+ * ("File creation/modification in non-system directories") and
61
+ * `read-only` maps to no `--auto` flag at all — `exec --help`'s own
62
+ * "Autonomy Levels" section documents the no-flags default as read-only,
63
+ * so omitting it is the exact, help-evidenced way to request it, not an
64
+ * inherited default. `medium`/`high` are out of this contract's scope and
65
+ * are never emitted. `exec --help`'s "Autonomy Levels" section is also the
66
+ * sole permission gate `exec` exposes — there is no separate
67
+ * approval-prompt flag, because `exec` is non-interactive by construction
68
+ * regardless of autonomy level. Reasoning-effort levels are model-dependent
69
+ * (`exec --help`'s "Model details" lists the per-model valid sets); an
70
+ * explicit value drawn from the observed union of every model's levels is
71
+ * required and the CLI's per-model default is never inherited.
72
+ *
73
+ * `--output-format stream-json` and the terminal `json` object shape are
74
+ * doc-sourced, not help-verified (the installed CLI was never run with a
75
+ * prompt): https://docs.factory.com/droid-exec/overview documents
76
+ * `--output-format json` producing exactly one terminal object,
77
+ * `{"type":"result","subtype":"success","is_error":false,"duration_ms":N,
78
+ * "num_turns":N,"result":"...","session_id":"..."}`, and
79
+ * https://docs.factory.com/sdk/typescript documents the same message
80
+ * vocabulary the CLI's JSON-RPC/stream protocol shares across surfaces:
81
+ * default types `assistant`, `user`, `tool_call`, `tool_result`, `hook`,
82
+ * `error` and the terminal `result` (subtypes `success`, `interrupted`,
83
+ * `error_during_execution`, `error_structured_output`). This grammar
84
+ * drives `observeHealthEvent`/`parseDirectFinal` below but is explicitly
85
+ * doc-sourced evidence (grade D), never live qualification, and the
86
+ * registry `qualification.status` stays `unqualified`.
87
+ *
88
+ * Authentication travels via `FACTORY_API_KEY` in the environment
89
+ * (`exec --help`'s "Authentication" section); `HOME` stays on the env
90
+ * allowlist because `--worktree-dir`'s deprecation notice points at
91
+ * `~/.factory/settings.json` for persistent settings.
92
+ *
93
+ * Installed-module shape: static `interfaceVersion` and `register` exports
94
+ * so the #2885 installed loader can approve this file by digest under
95
+ * `installed:factory` (mirroring #2897 codex-adapter.mjs / #2911
96
+ * openhuman-adapter.mjs). The bottom self-registration targets the legacy
97
+ * Ralph-loop registry only; the direct worker profile loads through the
98
+ * installed loader.
99
+ */
100
+ export const FACTORY_SUPPORTED_VERSIONS = Object.freeze(['0.238.0']);
101
+ /** Union of every reasoning-effort label observed across models in
102
+ * `exec --help`'s "Model details" section (including `none`, the sole
103
+ * level for "Auto Model" and one of the levels for GPT-6 Sol/Luna and
104
+ * GPT-5.6 Sol/Sol Fast/Terra/Luna); the valid subset is model-dependent
105
+ * and unqualified until live evidence. */
106
+ export const FACTORY_DIRECT_EFFORT_VALUES = Object.freeze(['none', 'off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']);
107
+ /** Generic cross-provider sandbox contract this profile accepts
108
+ * (`WorkerExecution.sandbox`). `--skip-permissions-unsafe` and the
109
+ * `medium`/`high` `--auto` tiers are legacy/unsafe or out of this
110
+ * contract's scope and are never emitted by the direct profile. */
111
+ export const FACTORY_DIRECT_SANDBOX_MODES = Object.freeze(['read-only', 'workspace-write']);
112
+ /** `--auto` is droid's only scoped autonomy flag: `workspace-write` maps to
113
+ * the least-privileged writable tier (`low`, "File creation/modification
114
+ * in non-system directories"); `read-only` maps to no `--auto` flag at
115
+ * all, because omitting it is help-evidenced as the CLI's own read-only
116
+ * mode (`exec --help`'s "Autonomy Levels": "DEFAULT (no flags) -
117
+ * Read-only mode") and there is no more-explicit way to request it. */
118
+ const FACTORY_SANDBOX_TO_AUTO = Object.freeze({ 'read-only': null, 'workspace-write': 'low' });
119
+ /** `exec` has no separate approval-prompt flag: non-interactive by
120
+ * construction, so only the no-prompt policy is ever requested. */
121
+ export const FACTORY_DIRECT_APPROVAL_POLICIES = Object.freeze(['never']);
122
+ /** Brief transport for the direct profile: piped stdin, never argv. */
123
+ export const FACTORY_DIRECT_STDIN = 'brief-text';
124
+ /** Generic Ralph-loop aliases resolve through MODEL_MAP above; the direct
125
+ * profile requires an exact observed catalog model instead. */
126
+ const FACTORY_GENERIC_ALIASES = Object.freeze(['opus', 'sonnet', 'haiku']);
127
+ /** `exec --help`'s "Available Models" section lists `auto` as "Auto
128
+ * Model" with "supports reasoning: No; supported: [none]" in "Model
129
+ * details" — the CLI itself, not the resolved selection, then picks the
130
+ * real model. `-m auto` would silently violate the explicit-model
131
+ * invariant exactly as an omitted `-m` would (the CLI's own documented
132
+ * default-by-omission is `gpt-6-sol`, per `exec --help`'s
133
+ * `-m, --model <id> ... (default: gpt-6-sol)`); buildWorkerArgs already
134
+ * refuses omission by requiring a non-empty string, and this refuses the
135
+ * equivalent `auto` escape hatch case-insensitively. */
136
+ const FACTORY_RESERVED_MODEL_VALUES = Object.freeze(['auto']);
137
+
138
+ export class FactoryAdapter extends ProviderAdapter {
139
+ /**
140
+ * Doc-sourced `assistant`/`user`/`tool_call`/`tool_result`/`hook`/`error`/
141
+ * `result` vocabulary (module doc above). No session-start event is
142
+ * evidenced, so the handshake stays `process-only`; a standalone `error`
143
+ * event is fatal exactly like a `result` with `is_error: true`, so a later
144
+ * clean terminal can never paper over it. Session exports and another
145
+ * provider's event names cannot qualify this grammar.
146
+ *
147
+ * Terminal `result` success requires AFFIRMATIVE markers, never an
148
+ * absence of negative ones: this doc-sourced grammar (grade D, never
149
+ * live-observed) is not trusted to omit fields consistently, so a bare
150
+ * `{"type":"result"}` or any other field combination that is not
151
+ * exactly the documented success shape — `subtype === 'success'`,
152
+ * `is_error === false`, and a non-empty `result` string — fails closed
153
+ * rather than defaulting to success. An `interrupted`/
154
+ * `error_during_execution`/`error_structured_output`/unknown `subtype`,
155
+ * a missing or non-`false` `is_error`, or an empty/missing `result` are
156
+ * all failures even if the other fields look clean.
157
+ * @returns {import('./worker-health.mjs').HealthObservation | null}
158
+ */
159
+ observeHealthEvent(event, source, binding) {
160
+ if (source?.kind !== 'stdout' || !event || typeof event !== 'object' || typeof event.type !== 'string') return null;
161
+ if (!healthEventMatchesBinding({ sessionId: event.session_id }, binding)) return null;
162
+ if (event.type === 'result') {
163
+ const hasResultContent = typeof event.result === 'string' && event.result.trim().length > 0;
164
+ const success = event.subtype === 'success' && event.is_error === false && hasResultContent;
165
+ return { kind: 'terminal', success, key: event.subtype ?? 'result' };
166
+ }
167
+ if (event.type === 'error') {
168
+ return { ...classifyWorkerHealthFailure({ type: 'error', message: event.message ?? event.error?.message }, { source, provider: 'factory' }),
169
+ fatal: true };
170
+ }
171
+ if (event.type === 'tool_result') {
172
+ const id = event.tool_call_id ?? event.id;
173
+ return (typeof id === 'string' || typeof id === 'number') ? { kind: 'progress', key: String(id) } : { kind: 'activity' };
174
+ }
175
+ if (event.type === 'tool_call' || event.type === 'assistant' || event.type === 'user' || event.type === 'hook') {
176
+ return { kind: 'activity' };
177
+ }
178
+ return null;
179
+ }
180
+
181
+ /**
182
+ * Driver-owned argv after the binary for the version-bound direct-shell
183
+ * profile. Every control is explicit: exact observed model (never an
184
+ * alias or the CLI's `gpt-6-sol` default), explicit reasoning effort
185
+ * (never a per-model default), the scoped `--auto` autonomy level (never
186
+ * `--skip-permissions-unsafe`), the conductor workdir as `--cwd` and the
187
+ * `stream-json` event stream. No prompt argument is emitted: the brief
188
+ * arrives on stdin (`stdin: 'brief-text'`), so it never enters argv.
189
+ *
190
+ * @param {{ selected?: { model?: unknown, effort?: { value?: unknown } }, cwd?: unknown, brief?: unknown }} invocation
191
+ * @param {unknown} _profile
192
+ * @param {{ sandbox?: unknown, approval?: unknown }} [permissions]
193
+ * @returns {string[]}
194
+ */
195
+ buildWorkerArgs(invocation, _profile, permissions) {
196
+ const model = invocation?.selected?.model;
197
+ if (typeof model !== 'string' || !model || model.includes('\0')) {
198
+ throw new Error('Factory worker requires an explicit model');
199
+ }
200
+ if (FACTORY_GENERIC_ALIASES.includes(model.toLowerCase())) {
201
+ throw new Error(`Factory worker requires an exact observed model, not the generic alias '${model}'`);
202
+ }
203
+ if (FACTORY_RESERVED_MODEL_VALUES.includes(model.toLowerCase())) {
204
+ throw new Error(`Factory worker requires an explicit observed model, not the CLI's own auto-selection '${model}'`);
205
+ }
206
+ const effort = invocation?.selected?.effort?.value ?? null;
207
+ if (effort === null || !FACTORY_DIRECT_EFFORT_VALUES.includes(effort)) {
208
+ throw new Error('Factory worker requires an explicit qualified reasoning effort (never a CLI default)');
209
+ }
210
+ const sandbox = permissions?.sandbox ?? 'workspace-write';
211
+ if (!FACTORY_DIRECT_SANDBOX_MODES.includes(sandbox)) {
212
+ throw new Error(`Factory worker sandbox is not a qualified direct mode: ${String(sandbox)}`);
213
+ }
214
+ const approval = permissions?.approval ?? 'never';
215
+ if (!FACTORY_DIRECT_APPROVAL_POLICIES.includes(approval)) {
216
+ throw new Error(`Factory worker approval is not a qualified direct policy: ${String(approval)}`);
217
+ }
218
+ const cwd = invocation?.cwd;
219
+ if (typeof cwd !== 'string' || !cwd.startsWith('/') || cwd.includes('\0')) {
220
+ throw new Error('Factory worker requires an absolute workspace');
221
+ }
222
+ const brief = invocation?.brief;
223
+ if (typeof brief !== 'string' || !brief || brief.includes('\0')) {
224
+ throw new Error('Factory worker requires a non-empty brief');
225
+ }
226
+ const auto = FACTORY_SANDBOX_TO_AUTO[sandbox];
227
+ const args = ['exec'];
228
+ if (auto) args.push('--auto', auto);
229
+ args.push('-m', model, '-r', effort, '--cwd', cwd, '--output-format', 'stream-json');
230
+ return args;
231
+ }
232
+
233
+ /**
234
+ * `exec` reads the brief from stdin to EOF and has no documented stdin
235
+ * abort channel, so cancellation stays with owned process control.
236
+ * @returns {null}
237
+ */
238
+ getAbortInput() {
239
+ return null;
240
+ }
241
+
242
+ /**
243
+ * Direct-profile final extraction from the captured `--output-format
244
+ * stream-json` stdout stream (module doc above). Unlike the legacy
245
+ * `parseOutput()` below (pinned regex-based shape, never changed here),
246
+ * this is JSONL-aware and fails closed on its own, without relying on the
247
+ * health observer: a malformed line, a standalone `error` event, or ANY
248
+ * `result` line that is not the documented success shape (`subtype ===
249
+ * 'success'`, `is_error === false`, non-empty `result`) rejects the whole
250
+ * stream. A later success never replaces an earlier failure, and an error
251
+ * after a success is never ignored. Returns the last successful `result`.
252
+ * @param {string} stdout
253
+ * @returns {Object|null}
254
+ */
255
+ parseDirectFinal(stdout) {
256
+ if (typeof stdout !== 'string' || !stdout.trim()) return null;
257
+ let final = null;
258
+ for (const line of stdout.split('\n')) {
259
+ const trimmed = line.trim();
260
+ if (!trimmed) continue;
261
+ let event;
262
+ try {
263
+ event = JSON.parse(trimmed);
264
+ } catch {
265
+ return null;
266
+ }
267
+ if (!event || typeof event !== 'object' || Array.isArray(event)) return null;
268
+ if (event.type === 'error') return null;
269
+ if (event.type !== 'result') continue;
270
+ const hasResultContent = typeof event.result === 'string' && event.result.trim().length > 0;
271
+ if (!(event.subtype === 'success' && event.is_error === false && hasResultContent)) return null;
272
+ final = event;
273
+ }
274
+ return final;
275
+ }
276
+
277
+
278
+ /** @returns {string} */
279
+ getBinary() {
280
+ return 'droid';
281
+ }
282
+
283
+ /** @returns {string} */
284
+ getName() {
285
+ return 'factory';
286
+ }
287
+
288
+ /**
289
+ * Factory supports stream-json output and session resume.
290
+ * @returns {import('./provider-adapter.mjs').ProviderCapabilities}
291
+ */
292
+ getCapabilities() {
293
+ return {
294
+ streamJson: true,
295
+ sessionResume: true,
296
+ budgetControl: false,
297
+ systemPrompt: false,
298
+ agentMode: false,
299
+ mcpConfig: false,
300
+ maxTurns: false,
301
+ };
302
+ }
303
+
304
+ /**
305
+ * Build args for the main headless session.
306
+ *
307
+ * Factory uses `droid exec --skip-permissions-unsafe` for headless operation.
308
+ *
309
+ * @param {import('./provider-adapter.mjs').SessionArgs} options
310
+ * @returns {string[]}
311
+ */
312
+ buildSessionArgs(options) {
313
+ const args = [
314
+ 'exec',
315
+ // SECURITY: --skip-permissions-unsafe bypasses ALL permission prompts
316
+ // Equivalent to Claude's --dangerously-skip-permissions
317
+ '--skip-permissions-unsafe',
318
+ '--output-format', 'stream-json',
319
+ ];
320
+
321
+ // Model selection (map generic to Factory model IDs)
322
+ if (options.model) {
323
+ args.push('-m', this.mapModel(options.model));
324
+ }
325
+
326
+ // Session resume
327
+ if (options.sessionId) {
328
+ args.push('-s', options.sessionId);
329
+ }
330
+
331
+ // Budget control not supported
332
+ if (options.budget) {
333
+ this.warnUnsupported('budgetControl', 'Budget control (--max-budget-usd)');
334
+ }
335
+
336
+ // Max turns not supported
337
+ if (options.maxTurns) {
338
+ this.warnUnsupported('maxTurns', 'Max turns (--max-turns)');
339
+ }
340
+
341
+ // MCP configuration not supported via flag
342
+ if (options.mcpConfig) {
343
+ this.warnUnsupported('mcpConfig', 'MCP configuration (--mcp-config)');
344
+ }
345
+
346
+ // System prompt: Factory doesn't support --append-system-prompt,
347
+ // so we prepend it to the main prompt
348
+ let prompt = options.prompt;
349
+ if (options.systemPrompt) {
350
+ prompt = `[System Context]\n${options.systemPrompt}\n\n[Task]\n${prompt}`;
351
+ }
352
+
353
+ // The prompt itself (must be last)
354
+ args.push(prompt);
355
+
356
+ return args;
357
+ }
358
+
359
+ /**
360
+ * Build args for short analysis calls (spawnSync).
361
+ *
362
+ * Uses read-only mode (no --skip-permissions-unsafe) since analysis
363
+ * calls only need to read and reason, not modify files.
364
+ *
365
+ * @param {import('./provider-adapter.mjs').AnalysisArgs} options
366
+ * @returns {string[]}
367
+ */
368
+ buildAnalysisArgs(options) {
369
+ const args = [
370
+ 'exec',
371
+ '--output-format', 'text',
372
+ ];
373
+
374
+ // Model selection
375
+ if (options.model) {
376
+ args.push('-m', this.mapModel(options.model));
377
+ }
378
+
379
+ // Agent flag not supported by Factory
380
+ if (options.agent) {
381
+ // Silently skip - agent context will be in the prompt itself
382
+ }
383
+
384
+ // The analysis prompt (must be last)
385
+ args.push(options.prompt);
386
+
387
+ return args;
388
+ }
389
+
390
+ /**
391
+ * Map generic model names to Factory/Droid model IDs.
392
+ *
393
+ * @param {string} genericModel
394
+ * @returns {string}
395
+ */
396
+ mapModel(genericModel) {
397
+ const mapped = MODEL_MAP[genericModel.toLowerCase()];
398
+ if (mapped) return mapped;
399
+ // Pass through if already a Factory model name or unknown
400
+ return genericModel;
401
+ }
402
+
403
+ /**
404
+ * Environment overrides for headless Factory sessions.
405
+ * @returns {Object<string, string>}
406
+ */
407
+ getEnvOverrides() {
408
+ return {
409
+ CI: 'true',
410
+ };
411
+ }
412
+
413
+ /**
414
+ * Factory does not expose session transcripts in a known file path.
415
+ * @returns {null}
416
+ */
417
+ getTranscriptPath() {
418
+ return null;
419
+ }
420
+
421
+ /**
422
+ * Parse Factory output — supports stream-json or text.
423
+ *
424
+ * @param {string} stdout
425
+ * @returns {Object|null}
426
+ */
427
+ parseOutput(stdout) {
428
+ try {
429
+ const jsonMatch = stdout.match(/\{[\s\S]*\}/);
430
+ if (!jsonMatch) return null;
431
+ return JSON.parse(jsonMatch[0]);
432
+ } catch {
433
+ return null;
434
+ }
435
+ }
436
+ }
437
+
438
+ // Self-register on import for the legacy Ralph loop only. The direct worker
439
+ // profile loads through the #2885 installed loader instead (see below).
440
+ registerProvider('factory', () => new FactoryAdapter());
441
+
442
+ /**
443
+ * #2995: `FactoryAdapter.parseOutput` above is the legacy-pinned,
444
+ * loose-regex candidate parser (`test/unit/ralph/factory-adapter.test.mjs`
445
+ * and the legacy Ralph loop depend on its exact shape) — it extracts the
446
+ * FIRST `{...}` to the LAST `}` across the whole stdout, so it fails
447
+ * (returns null) on any realistic multi-event `stream-json` run (assistant/
448
+ * tool_call/tool_result lines before the final `result` line), and it is
449
+ * loose in the other direction too (a non-affirmative or empty-text result
450
+ * can still parse as a truthy object). The installed-module loader's
451
+ * `extractFinal` hook IS `instance.parseOutput` (src/models/
452
+ * worker-installation.mjs ~l.730) and session-launcher.mjs's gate
453
+ * (~l.462) throws unless that hook returns a non-null, non-`success:false`
454
+ * result — so a real direct-shell run could never reach `parseWorkerFinal`
455
+ * at all with the legacy parser wired to it.
456
+ *
457
+ * `FactoryDirectAdapter` is the Claude/Hermes `-direct`-subclass pattern:
458
+ * only `parseOutput` changes, to the ALREADY-correct `parseDirectFinal`
459
+ * (above — one rule, never a second one), which returns null unless the
460
+ * whole stream is a single droid-shaped success terminal with non-empty
461
+ * text. `parseWorkerFinal` only needs the strict version here (no
462
+ * generic-envelope fallback): this class is never reachable through the
463
+ * legacy bare `registerProvider('factory', ...)` id above, only through
464
+ * the installed-module `register(factory)` export below, so the legacy
465
+ * group-mode contract (test/unit/ralph-external/worker-finals.test.ts)
466
+ * never sees it.
467
+ */
468
+ export class FactoryDirectAdapter extends FactoryAdapter {
469
+ // #2995 round 4 (item 3): production resolves the installed adapter by
470
+ // `adapter.id` (now `factory-direct`, item B) and
471
+ // src/models/worker-installation.mjs's `inspectWorkerInstallation`
472
+ // requires `adapter.id === instance.getName()`. `getBinary()` (the real
473
+ // `droid` executable) and the `provider: 'factory'` literals inside
474
+ // `observeHealthEvent` are untouched -- only this identity label changes.
475
+ getName() { return 'factory-direct'; }
476
+
477
+ parseOutput(stdout) {
478
+ return this.parseDirectFinal(stdout);
479
+ }
480
+
481
+ /**
482
+ * @param {string|Uint8Array} output
483
+ * @param {Object} [options]
484
+ * @returns {Promise<import('../../../src/mission-protocol/types.js').WorkerClaims>}
485
+ */
486
+ async parseWorkerFinal(output, options = {}) {
487
+ if (options.format === 'mcp') return super.parseWorkerFinal(output, options);
488
+ const text = typeof output === 'string' ? output : Buffer.from(output).toString('utf8');
489
+ const final = this.parseDirectFinal(text);
490
+ if (!final) throw new Error('Factory worker stream did not produce a successful final result');
491
+ return super.parseWorkerFinal(final.result, { ...options, format: 'json' });
492
+ }
493
+ }
494
+
495
+ /** Installed-module interface version (mirrors #2897 codex-adapter.mjs). */
496
+ export const interfaceVersion = 1;
497
+
498
+ /** Installed-module registration: exactly the one approved adapter ID. */
499
+ export function register(factory) {
500
+ factory('factory-direct', () => new FactoryDirectAdapter());
501
+ }
502
+
503
+ export default FactoryAdapter;