@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,333 @@
1
+ /** Pi Coding Agent adapter for headless External Ralph sessions. */
2
+ import { spawnSync } from 'child_process';
3
+ import { homedir } from 'os';
4
+ import { join } from 'path';
5
+ import { ProviderAdapter, registerProvider } from './provider-adapter.mjs';
6
+ import { healthEventMatchesBinding } from './worker-health.mjs';
7
+
8
+ /**
9
+ * Version-bound direct-worker surface (roctinam/aiwg#2910, B-OG-45).
10
+ *
11
+ * Qualified against pi 1.1.0 `--help` (help/version syntax only,
12
+ * 2026-10-09; never live qualification; upgraded from the originally
13
+ * qualified 0.87.1 -- the `--help` diff at
14
+ * `.aiwg/working/s1-scratch/cli-help/pi-diff.txt` shows every flag this
15
+ * profile emits is unchanged, plus a new `--no-mcp`):
16
+ *
17
+ * pi --mode json --no-approve --no-context-files --no-extensions \
18
+ * --no-skills --no-mcp --model M --thinking E < briefPath
19
+ *
20
+ * `--mode json` forces non-interactive JSONL output regardless of TTY or
21
+ * `--print` (installed pi 1.1.0 bundle, `resolveAppMode()`: `parsed.mode
22
+ * === "json"` short-circuits to `"json"` before any print/interactive
23
+ * check), so no separate `-p/--print` flag is needed. `--no-approve`
24
+ * ("Ignore project-local files for this run") alone is NOT a complete
25
+ * isolation boundary: it sets project trust to false but does not stop
26
+ * context loading. The installed pi 1.1.0 bundle (`chunk-OIM2DMFI.js`,
27
+ * static read 2026-10-09: `agentsFiles = this.noContextFiles ? [] :
28
+ * loadProjectContextFiles(...)`) loads ancestor `AGENTS.md`/`CLAUDE.md`
29
+ * files independently of trust unless `noContextFiles` is set. The direct
30
+ * profile therefore also emits `--no-context-files`
31
+ * (`-nc`, "Disable AGENTS.md and CLAUDE.md discovery and loading"),
32
+ * `--no-extensions` (`-ne`) and `--no-skills` (`-ns`) -- the same discovery
33
+ * surfaces OMP's profile disables -- so no untrusted workspace file
34
+ * (context, extension or skill) is ever loaded into the worker. 1.1.0
35
+ * adds built-in MCP support, on by default; the direct profile also emits
36
+ * `--no-mcp` ("Disable built-in MCP support: no servers connect and no
37
+ * MCP tools", `pi --help`) for the same reason -- an unvetted MCP server
38
+ * configuration is an untrusted-workspace surface like
39
+ * extensions/skills, and the AIWG `worker-invoke` bridge remains the one
40
+ * MCP route. The direct profile never emits `--approve` ("Trust
41
+ * project-local files for this run"). There is no documented sandbox,
42
+ * approval-prompt, output-file, output-schema, step-cap or child-denial
43
+ * flag (`pi --help`, 2026-10-09): those controls stay unavailable rather
44
+ * than invented, and the final answer is extracted from the stdout JSONL
45
+ * stream (`parseOutput()`), never a `-o`-style file. There is no
46
+ * `--cwd`/`-C` flag either; the launcher's shell `cd` to the workspace is
47
+ * the only workdir binding. The brief travels on stdin, never argv:
48
+ * installed pi 1.1.0 bundle (`chunk-OIM2DMFI.js`, `readPipedStdin()` /
49
+ * `buildInitialMessage()`, static read 2026-10-09, byte-identical to the
50
+ * 0.87.1 evidence) shows piped, non-TTY stdin is trimmed and becomes the
51
+ * sole initial-message part whenever no positional message/file argument
52
+ * is given, so omitting the positional message and piping the brief is
53
+ * sufficient -- and an all-whitespace brief would trim to nothing and
54
+ * must never be sent.
55
+ */
56
+ export const PI_DIRECT_VERSION = '1.1.0';
57
+ /** Mirrors providers.pi.effortValues in model-capabilities.v1.json (hardcoded,
58
+ * not read via a repo-relative path: the installed-module loader copies this
59
+ * file to an arbitrary package root, so import.meta.url-relative reads to the
60
+ * repository are not safe here). */
61
+ export const PI_DIRECT_EFFORT_VALUES = Object.freeze(['minimal', 'low', 'medium', 'high', 'xhigh']);
62
+ /** Brief transport for the direct profile: piped stdin, never argv. */
63
+ export const PI_DIRECT_STDIN = 'brief-text';
64
+
65
+ /**
66
+ * Pi CLI versions this adapter is qualified against. `isAvailable()` fails
67
+ * closed outside this list (#2550): Pi's `--mode json` contract (stdin
68
+ * handling, JSONL framing, `agent_settled`) is verified per version by
69
+ * `npm run smoke:pi:live`, and an unqualified release must not be driven by
70
+ * an unattended loop on the strength of `pi --version` exiting 0.
71
+ */
72
+ export const PI_SUPPORTED_VERSIONS = Object.freeze(['0.85.0']);
73
+
74
+ export function normalizePiVersion(version) {
75
+ return String(version ?? '').trim().replace(/^pi\s+/i, '').replace(/^v/, '');
76
+ }
77
+
78
+ export function isSupportedPiVersion(version) {
79
+ return PI_SUPPORTED_VERSIONS.includes(normalizePiVersion(version));
80
+ }
81
+
82
+ export class PiAdapter extends ProviderAdapter {
83
+ observeHealthEvent(event, source, binding) {
84
+ if (source.kind !== 'stdout' || !event || typeof event !== 'object') return null;
85
+ if (!healthEventMatchesBinding({ sessionId: event.sessionId ?? (event.type === 'session' ? event.id : undefined) }, binding)) return null;
86
+ if (event.type === 'session') return { kind: 'setup', sessionId: event.id };
87
+ if (event.type === 'agent_start') return { kind: 'accepted' };
88
+ if (event.type === 'agent_end') {
89
+ if (event.willRetry === true || (Array.isArray(event.messages) && event.messages.some(message => message.stopReason === 'error' || message.stopReason === 'aborted'))) {
90
+ return { kind: 'error', reasonCode: 'provider-failed' };
91
+ }
92
+ // agent_end can precede retry/continuation: settlement requires Pi's
93
+ // own agent_settled event. Completed output can still be progress.
94
+ if (Array.isArray(event.messages) && event.messages.some(message => message.role === 'assistant')) {
95
+ return { kind: 'progress', key: JSON.stringify(event.messages) };
96
+ }
97
+ return { kind: 'activity' };
98
+ }
99
+ if (event.type === 'agent_settled') return { kind: 'terminal', success: true, key: 'agent_settled' };
100
+ return null;
101
+ }
102
+ getBinary() { return process.env.AIWG_PI_BIN || 'pi'; }
103
+ getName() { return 'pi'; }
104
+ getCapabilities() {
105
+ // rpcAbort is false on purpose. Pi 0.85.0 reads stdin commands only in
106
+ // `--mode rpc`; in `--mode json` it reads piped stdin as prompt text and
107
+ // blocks until EOF (readPipedStdin in dist/main.js). Opening a stdin pipe
108
+ // to send an abort frame therefore keeps the session from ever starting,
109
+ // so the launcher must leave stdin closed and cancel with the bounded
110
+ // TERM/KILL path (#2550).
111
+ return { streamJson: true, sessionResume: true, budgetControl: false,
112
+ systemPrompt: true, agentMode: false, mcpConfig: false, maxTurns: false,
113
+ rpcAbort: false };
114
+ }
115
+ async isAvailable() {
116
+ const node = spawnSync(process.execPath, ['-p', 'process.versions.node'], { encoding: 'utf8', timeout: 5000 });
117
+ const [major, minor] = String(node.stdout).trim().split('.').map(Number);
118
+ if (node.status !== 0 || major < 22 || (major === 22 && minor < 19)) return false;
119
+ if (!await super.isAvailable()) return false;
120
+ return isSupportedPiVersion(await this.getVersion());
121
+ }
122
+ buildSessionArgs(options) {
123
+ const args = ['--mode', 'json', '--no-approve'];
124
+ if (options.model) args.push('--model', this.mapModel(options.model));
125
+ if (options.thinking) args.push('--thinking', options.thinking);
126
+ if (options.sessionId) args.push('--session', options.sessionId);
127
+ if (options.tools?.length) args.push('--tools', options.tools.join(','));
128
+ if (options.systemPrompt) args.push('--append-system-prompt', options.systemPrompt);
129
+ if (options.budget) this.warnUnsupported('budgetControl', 'Budget control');
130
+ if (options.maxTurns) this.warnUnsupported('maxTurns', 'Max turns');
131
+ if (options.mcpConfig) this.warnUnsupported('mcpConfig', 'MCP configuration');
132
+ args.push(options.prompt);
133
+ return args;
134
+ }
135
+ buildAnalysisArgs(options) { return this.buildSessionArgs(options); }
136
+
137
+ /**
138
+ * Driver-owned argv after the binary for the version-bound direct-shell
139
+ * profile. `--no-approve`, `--no-context-files`, `--no-extensions`,
140
+ * `--no-skills` and `--no-mcp` are all fixed (never `--approve`, and
141
+ * never omitted — `--no-approve` alone does not stop ancestor
142
+ * AGENTS.md/CLAUDE.md loading); model and thinking level are always
143
+ * explicit (never a CLI default or alias); the brief is validated here
144
+ * but never placed in
145
+ * argv — it travels on stdin (`stdin: 'brief-text'`). There is no
146
+ * `-C/--cwd` flag to render: the launcher's own `cd` to the workspace is
147
+ * the only binding.
148
+ *
149
+ * @param {{ selected?: { model?: unknown, effort?: { value?: unknown } }, cwd?: unknown, brief?: unknown }} invocation
150
+ * @param {unknown} _profile
151
+ * @param {unknown} _permissions
152
+ * @returns {string[]}
153
+ */
154
+ buildWorkerArgs(invocation, _profile, _permissions) {
155
+ const model = invocation?.selected?.model;
156
+ if (typeof model !== 'string' || !model || model.includes('\0')) {
157
+ throw new Error('Pi worker requires an explicit model');
158
+ }
159
+ const effort = invocation?.selected?.effort?.value ?? null;
160
+ if (effort === null || !PI_DIRECT_EFFORT_VALUES.includes(effort)) {
161
+ throw new Error('Pi worker requires an explicit qualified thinking level (never a CLI default)');
162
+ }
163
+ const cwd = invocation?.cwd;
164
+ if (typeof cwd !== 'string' || !cwd.startsWith('/') || cwd.includes('\0')) {
165
+ throw new Error('Pi worker requires an absolute workspace');
166
+ }
167
+ const brief = invocation?.brief;
168
+ // Pi's own readPipedStdin() trims the stream; an all-whitespace brief
169
+ // would resolve to no initial message at all, so it is refused here.
170
+ if (typeof brief !== 'string' || !brief.trim() || brief.includes('\0')) {
171
+ throw new Error('Pi worker requires a non-empty brief');
172
+ }
173
+ return ['--mode', 'json', '--no-approve', '--no-context-files', '--no-extensions', '--no-skills', '--no-mcp',
174
+ '--model', model, '--thinking', effort];
175
+ }
176
+
177
+ mapModel(model) { return model; }
178
+ getEnvOverrides() { return { CI: 'true', NO_COLOR: '1' }; }
179
+ /** No stdin abort in `--mode json`; see getCapabilities() (#2550). */
180
+ getAbortInput() { return null; }
181
+ getTranscriptPath(sessionId) {
182
+ if (!sessionId) return null;
183
+ if (sessionId.endsWith('.jsonl') || sessionId.includes('/')) return sessionId;
184
+ const root = process.env.PI_CODING_AGENT_SESSION_DIR;
185
+ return root ? join(root, `${sessionId}.jsonl`) : join(homedir(), '.pi', 'agent', 'sessions', `${sessionId}.jsonl`);
186
+ }
187
+ /**
188
+ * Legacy Ralph-loop contract (#2550), unchanged: parse `--mode json`
189
+ * output into Pi's own JSONL events and report settlement only.
190
+ * `settled` is an `agent_settled` event with no `agent_end` signalling
191
+ * `willRetry: true`. This is the exact shape
192
+ * `test/unit/ralph/pi-adapter.test.mjs` and the launcher depend on — it
193
+ * does NOT extract or require a final answer; see `parseDirectFinal()`
194
+ * for the strict direct-worker-profile contract that does.
195
+ *
196
+ * @param {string} stdout
197
+ * @returns {{ events: object[], settled: boolean } | null}
198
+ */
199
+ parseOutput(stdout) {
200
+ const events = [];
201
+ for (const raw of stdout.split('\n')) {
202
+ const line = raw.endsWith('\r') ? raw.slice(0, -1) : raw;
203
+ if (!line) continue;
204
+ try { events.push(JSON.parse(line)); } catch { return null; }
205
+ }
206
+ return events.length ? { events, settled: events.some(event => event.type === 'agent_settled')
207
+ && !events.some(event => event.type === 'agent_end' && event.willRetry === true) } : null;
208
+ }
209
+
210
+ /**
211
+ * Strict direct-worker-profile final: reuses `parseOutput()`'s event
212
+ * parsing, then additionally requires a non-empty extracted assistant
213
+ * answer and fails on any error signal (a standalone `error` event, an
214
+ * `agent_end` with `willRetry: true`, or an `agent_end` assistant
215
+ * message with `stopReason` `error`/`aborted`) even at exit zero. `text`
216
+ * is the last assistant message's concatenated text content across all
217
+ * `agent_end` events (a later `agent_end` can supersede an earlier one
218
+ * before `agent_settled` arrives); `text` is non-null only when
219
+ * `settled` is true. Used only by the direct worker profile
220
+ * (roctinam/aiwg#2910) and the installed-module path — never the legacy
221
+ * Ralph loop, which calls `parseOutput()` directly.
222
+ *
223
+ * @param {string} stdout
224
+ * @returns {{ events: object[], settled: boolean, text: string|null } | null}
225
+ */
226
+ parseDirectFinal(stdout) {
227
+ const events = [];
228
+ for (const raw of stdout.split('\n')) {
229
+ const line = raw.endsWith('\r') ? raw.slice(0, -1) : raw;
230
+ if (!line) continue;
231
+ try { events.push(JSON.parse(line)); } catch { return null; }
232
+ }
233
+ if (!events.length) return null;
234
+ let failed = false;
235
+ let settledEvent = false;
236
+ let text = null;
237
+ for (const event of events) {
238
+ // #2995 round 3: `agent_settled` is documented as terminal; ANY event
239
+ // observed after it (including a second agent_start) breaks that
240
+ // contract and fails the parse, exactly like Factory/Cursor's
241
+ // post-terminal check.
242
+ if (settledEvent) { failed = true; continue; }
243
+ if (event.type === 'error') failed = true;
244
+ if (event.type === 'agent_settled') { settledEvent = true; continue; }
245
+ if (event.type !== 'agent_end') continue;
246
+ // #2995 round 3: an `agent_end` with NO `messages` array at all (not
247
+ // just an explicit empty array) must CLEAR any earlier candidate
248
+ // text -- never let an earlier claim survive a later answerless
249
+ // terminal.
250
+ if (!Array.isArray(event.messages)) { text = null; continue; }
251
+ if (event.willRetry === true || event.messages.some(message => message.stopReason === 'error' || message.stopReason === 'aborted')) {
252
+ failed = true;
253
+ }
254
+ // Pi accumulates messages chronologically: the final answer is the LAST
255
+ // assistant message of the LAST agent_end. An earlier non-empty message
256
+ // must never stand in for an empty final one.
257
+ const assistant = event.messages.filter(message => message.role === 'assistant').at(-1);
258
+ if (!assistant) { text = null; continue; }
259
+ const extracted = Array.isArray(assistant.content)
260
+ ? assistant.content.filter(part => part.type === 'text').map(part => part.text).join('\n')
261
+ : (typeof assistant.text === 'string' ? assistant.text : null);
262
+ text = extracted && extracted.trim() ? extracted : null;
263
+ }
264
+ const settled = settledEvent && !failed && Boolean(text);
265
+ return { events, settled, text: settled ? text : null };
266
+ }
267
+
268
+ }
269
+
270
+ // Self-register on import for the legacy Ralph loop only. The direct worker
271
+ // profile loads through the #2885 installed loader instead (see below).
272
+ registerProvider('pi', () => new PiAdapter());
273
+
274
+ /**
275
+ * #2995: `PiAdapter.parseOutput` above is the legacy-pinned, settlement-only
276
+ * candidate parser (`test/unit/ralph/pi-adapter.test.mjs` and the legacy
277
+ * Ralph loop depend on its exact `{events,settled}` shape, with no answer
278
+ * requirement at all) — it is non-null whenever the stream parses as JSONL,
279
+ * regardless of whether the run actually settled successfully with a
280
+ * non-empty answer. The installed-module loader's `extractFinal` hook IS
281
+ * `instance.parseOutput` (src/models/worker-installation.mjs ~l.730) and
282
+ * session-launcher.mjs's gate (~l.462) throws unless that hook returns
283
+ * null for anything that is not an affirmative success — so wiring the
284
+ * legacy parser there would let an unsettled or empty-answer run pass the
285
+ * gate as a false candidate.
286
+ *
287
+ * `PiDirectAdapter` is the Claude/Hermes `-direct`-subclass pattern: only
288
+ * `parseOutput` changes, to a thin wrapper over the ALREADY-correct
289
+ * `parseDirectFinal` (above) that returns null unless `settled` is true
290
+ * AND `text` is a non-empty string — never a second, looser rule.
291
+ * `parseWorkerFinal` only needs the strict version here (no
292
+ * generic-envelope fallback): this class is never reachable through the
293
+ * legacy bare `registerProvider('pi', ...)` id above, only through the
294
+ * installed-module `register(factory)` export below, so the legacy
295
+ * group-mode contract (test/unit/ralph-external/worker-finals.test.ts)
296
+ * never sees it.
297
+ */
298
+ export class PiDirectAdapter extends PiAdapter {
299
+ // #2995 round 4 (item 3): production resolves the installed adapter by
300
+ // `adapter.id` (now `pi-direct`, item B) and src/models/
301
+ // worker-installation.mjs's `inspectWorkerInstallation` requires
302
+ // `adapter.id === instance.getName()`. `getBinary()` is untouched.
303
+ getName() { return 'pi-direct'; }
304
+
305
+ /** @param {string} stdout @returns {{events:object[],settled:true,text:string}|null} */
306
+ parseOutput(stdout) {
307
+ const final = this.parseDirectFinal(stdout);
308
+ return final && final.settled && typeof final.text === 'string' && final.text.trim() ? final : null;
309
+ }
310
+
311
+ /**
312
+ * @param {string|Uint8Array} output
313
+ * @param {Object} [options]
314
+ * @returns {Promise<import('../../../src/mission-protocol/types.js').WorkerClaims>}
315
+ */
316
+ async parseWorkerFinal(output, options = {}) {
317
+ if (options.format === 'mcp') return super.parseWorkerFinal(output, options);
318
+ const text = typeof output === 'string' ? output : Buffer.from(output).toString('utf8');
319
+ const final = this.parseOutput(text);
320
+ if (!final) throw new Error('Pi worker stream did not produce a settled, non-empty final answer');
321
+ return super.parseWorkerFinal(final.text, { ...options, format: 'json' });
322
+ }
323
+ }
324
+
325
+ /** Installed-module interface version (mirrors #2897 codex-adapter.mjs). */
326
+ export const interfaceVersion = 1;
327
+
328
+ /** Installed-module registration: exactly the one approved adapter ID. */
329
+ export function register(factory) {
330
+ factory('pi-direct', () => new PiDirectAdapter());
331
+ }
332
+
333
+ export default PiAdapter;