codehere 1.0.1 → 1.0.2

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 (163) hide show
  1. package/README.md +36 -192
  2. package/dist/application/use-cases/check-claimed-tests.js +81 -0
  3. package/dist/application/use-cases/detect-test-weakening.js +237 -0
  4. package/dist/application/use-cases/rerun-tests.js +110 -7
  5. package/dist/index.js +38 -1212
  6. package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -323
  7. package/dist/presentation/cli/commands/check-command.js +178 -23
  8. package/dist/presentation/cli/commands/help-command.js +21 -285
  9. package/dist/ui.js +5 -236
  10. package/dist/utils/version.js +0 -6
  11. package/package.json +9 -38
  12. package/dist/application/services/verdict-service.js +0 -154
  13. package/dist/application/use-cases/assess-cross-vendor.js +0 -126
  14. package/dist/application/use-cases/build-cross-vendor-verdict.js +0 -118
  15. package/dist/application/use-cases/domain-trust.js +0 -110
  16. package/dist/application/use-cases/export-aat.js +0 -107
  17. package/dist/application/use-cases/generate-acceptance-criteria.js +0 -223
  18. package/dist/application/use-cases/generate-final-report.js +0 -259
  19. package/dist/application/use-cases/generate-narration.js +0 -130
  20. package/dist/application/use-cases/generate-thread-title.js +0 -181
  21. package/dist/application/use-cases/narrator-prompt-template.js +0 -158
  22. package/dist/application/use-cases/plan-auto-review.js +0 -57
  23. package/dist/application/use-cases/reconcile-audit.js +0 -100
  24. package/dist/application/use-cases/search-sessions.js +0 -70
  25. package/dist/application/use-cases/should-auto-review.js +0 -71
  26. package/dist/application/use-cases/thread-export.js +0 -245
  27. package/dist/application/use-cases/verify-claim.js +0 -456
  28. package/dist/application/use-cases/write-pass.js +0 -277
  29. package/dist/domain/audit.js +0 -11
  30. package/dist/domain/services/codebase-detector.js +0 -85
  31. package/dist/domain/task.js +0 -91
  32. package/dist/error-handler.js +0 -139
  33. package/dist/infrastructure/completion/bash-completion.js +0 -29
  34. package/dist/infrastructure/completion/fish-completion.js +0 -17
  35. package/dist/infrastructure/completion/zsh-completion.js +0 -25
  36. package/dist/infrastructure/config/config-manager.js +0 -174
  37. package/dist/infrastructure/config/product-context.js +0 -49
  38. package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
  39. package/dist/infrastructure/context/adapters/figma-source.js +0 -198
  40. package/dist/infrastructure/context/adapters/github-issue-source.js +0 -160
  41. package/dist/infrastructure/context/adapters/linear-source.js +0 -190
  42. package/dist/infrastructure/context/context-resolver.js +0 -144
  43. package/dist/infrastructure/context/context-source.js +0 -114
  44. package/dist/infrastructure/context/github-api.js +0 -110
  45. package/dist/infrastructure/context/github-device-flow.js +0 -65
  46. package/dist/infrastructure/context/oauth-flow.js +0 -207
  47. package/dist/infrastructure/context/oauth-store.js +0 -122
  48. package/dist/infrastructure/context/registry-bootstrap.js +0 -95
  49. package/dist/infrastructure/context/write-pass-resolver.js +0 -106
  50. package/dist/infrastructure/cost/budget-config.js +0 -114
  51. package/dist/infrastructure/cost/cost-router.js +0 -97
  52. package/dist/infrastructure/cost/spend-aggregator.js +0 -222
  53. package/dist/infrastructure/cost/team-ledger.js +0 -355
  54. package/dist/infrastructure/discovery/discovery-log.js +0 -85
  55. package/dist/infrastructure/hooks/audit-tip-signature.js +0 -336
  56. package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -1018
  57. package/dist/infrastructure/hooks/hook-mode.js +0 -87
  58. package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -49
  59. package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
  60. package/dist/infrastructure/hooks/session-analyzer.js +0 -177
  61. package/dist/infrastructure/observability/execution-tracer.js +0 -227
  62. package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
  63. package/dist/infrastructure/orchestration/agent-installer.js +0 -101
  64. package/dist/infrastructure/orchestration/agents-config-seed.js +0 -68
  65. package/dist/infrastructure/orchestration/aider-adapter.js +0 -611
  66. package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
  67. package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -592
  68. package/dist/infrastructure/orchestration/codex-adapter.js +0 -574
  69. package/dist/infrastructure/orchestration/conductor-adapter.js +0 -416
  70. package/dist/infrastructure/orchestration/context-handoff.js +0 -162
  71. package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
  72. package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
  73. package/dist/infrastructure/orchestration/detect-ollama.js +0 -75
  74. package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
  75. package/dist/infrastructure/orchestration/failover-config.js +0 -67
  76. package/dist/infrastructure/orchestration/failover-policy.js +0 -106
  77. package/dist/infrastructure/orchestration/gemini-adapter.js +0 -663
  78. package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -689
  79. package/dist/infrastructure/orchestration/graphify-context.js +0 -209
  80. package/dist/infrastructure/orchestration/memory-injector.js +0 -380
  81. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -388
  82. package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
  83. package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
  84. package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
  85. package/dist/infrastructure/orchestration/opencode-adapter.js +0 -580
  86. package/dist/infrastructure/orchestration/progress-aggregator.js +0 -906
  87. package/dist/infrastructure/orchestration/readiness.js +0 -103
  88. package/dist/infrastructure/orchestration/session-manager.js +0 -1590
  89. package/dist/infrastructure/orchestration/session-persistence.js +0 -394
  90. package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
  91. package/dist/infrastructure/orchestration/write-pass-store.js +0 -38
  92. package/dist/infrastructure/protocols/cloud-sync.js +0 -207
  93. package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
  94. package/dist/infrastructure/protocols/mcp-server-impl.js +0 -3935
  95. package/dist/infrastructure/protocols/mcp-tools.js +0 -401
  96. package/dist/infrastructure/protocols/probe-port.js +0 -36
  97. package/dist/infrastructure/protocols/thread-phase.js +0 -105
  98. package/dist/infrastructure/protocols/ui/app-script-artifacts.js +0 -121
  99. package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +0 -44
  100. package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +0 -56
  101. package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +0 -588
  102. package/dist/infrastructure/protocols/ui/app-script-context-sources.js +0 -168
  103. package/dist/infrastructure/protocols/ui/app-script-conversation.js +0 -67
  104. package/dist/infrastructure/protocols/ui/app-script-drawer.js +0 -115
  105. package/dist/infrastructure/protocols/ui/app-script-failover.js +0 -133
  106. package/dist/infrastructure/protocols/ui/app-script-github.js +0 -113
  107. package/dist/infrastructure/protocols/ui/app-script-observer.js +0 -104
  108. package/dist/infrastructure/protocols/ui/app-script-onboarding.js +0 -80
  109. package/dist/infrastructure/protocols/ui/app-script-overlay.js +0 -203
  110. package/dist/infrastructure/protocols/ui/app-script-recovery.js +0 -136
  111. package/dist/infrastructure/protocols/ui/app-script-render.js +0 -166
  112. package/dist/infrastructure/protocols/ui/app-script-share.js +0 -78
  113. package/dist/infrastructure/protocols/ui/app-script-sidebar.js +0 -194
  114. package/dist/infrastructure/protocols/ui/app-script-tour.js +0 -78
  115. package/dist/infrastructure/protocols/ui/app-script-wpass.js +0 -312
  116. package/dist/infrastructure/protocols/ui/app-script.js +0 -2998
  117. package/dist/infrastructure/protocols/ui/font-faces.js +0 -7
  118. package/dist/infrastructure/protocols/ui/styles.js +0 -2147
  119. package/dist/infrastructure/protocols/ui/tokens.js +0 -96
  120. package/dist/infrastructure/protocols/ui-page-v2.js +0 -320
  121. package/dist/infrastructure/protocols/verify-page.js +0 -1677
  122. package/dist/infrastructure/security/file-collection.js +0 -118
  123. package/dist/infrastructure/security/fix-code.js +0 -144
  124. package/dist/infrastructure/security/fix-secret.js +0 -89
  125. package/dist/infrastructure/security/permissions-config.js +0 -235
  126. package/dist/infrastructure/security/scan-types.js +0 -10
  127. package/dist/infrastructure/security/ship-scan.js +0 -195
  128. package/dist/infrastructure/security/trust-scorer.js +0 -724
  129. package/dist/infrastructure/security/vibe-checks.js +0 -99
  130. package/dist/infrastructure/storage/atomic-write.js +0 -86
  131. package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
  132. package/dist/infrastructure/storage/project-thread-migration.js +0 -237
  133. package/dist/infrastructure/storage/project-thread-mirror.js +0 -671
  134. package/dist/infrastructure/storage/project-thread-reader.js +0 -409
  135. package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
  136. package/dist/monitoring.js +0 -212
  137. package/dist/presentation/cli/commands/audit-command.js +0 -201
  138. package/dist/presentation/cli/commands/budget-command.js +0 -79
  139. package/dist/presentation/cli/commands/config-command.js +0 -125
  140. package/dist/presentation/cli/commands/context-source-command.js +0 -217
  141. package/dist/presentation/cli/commands/hook-command.js +0 -265
  142. package/dist/presentation/cli/commands/init-command.js +0 -590
  143. package/dist/presentation/cli/commands/memory-command.js +0 -1031
  144. package/dist/presentation/cli/commands/narrator-command.js +0 -101
  145. package/dist/presentation/cli/commands/permission-command.js +0 -105
  146. package/dist/presentation/cli/commands/protocols-builder.js +0 -254
  147. package/dist/presentation/cli/commands/protocols-command.js +0 -84
  148. package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
  149. package/dist/presentation/cli/commands/report-command.js +0 -403
  150. package/dist/presentation/cli/commands/scan-command.js +0 -52
  151. package/dist/presentation/cli/commands/scan-tool-command.js +0 -110
  152. package/dist/presentation/cli/commands/search-command.js +0 -51
  153. package/dist/presentation/cli/commands/setup-command.js +0 -330
  154. package/dist/presentation/cli/commands/status-command.js +0 -614
  155. package/dist/presentation/cli/commands/team-command.js +0 -256
  156. package/dist/presentation/cli/commands/thread-command.js +0 -370
  157. package/dist/presentation/cli/commands/trust-command.js +0 -328
  158. package/dist/presentation/cli/commands/verify-command.js +0 -298
  159. package/dist/presentation/cli/error-display.js +0 -328
  160. package/dist/presentation/cli/repl.js +0 -805
  161. package/dist/presentation/cli/task-deliverable.js +0 -153
  162. package/dist/testing/cassette.js +0 -125
  163. package/dist/testing/replay-adapter.js +0 -147
@@ -1,580 +0,0 @@
1
- /**
2
- * OpenCode adapter — fourth concrete AgentAdapter.
3
- *
4
- * `opencode run --format json "<prompt>"`
5
- *
6
- * OpenCode's --format json emits newline-delimited JSON (same general shape as
7
- * Claude Code and Codex). Event types observed: step_start, text, step_finish
8
- * (carries token counts + cost). Tool-call events likely fire in larger tasks
9
- * (unconfirmed on probe) — renderer falls through gracefully for unknowns.
10
- *
11
- * Same spawn-pattern pattern as Codex.
12
- */
13
- import { spawn, execFile, execFileSync } from 'child_process';
14
- import { EventEmitter } from 'events';
15
- import { randomUUID } from 'crypto';
16
- import { startRecording } from './cassette-recorder.js';
17
- import { existsSync } from 'fs';
18
- import { homedir } from 'os';
19
- import { join } from 'path';
20
- import { promisify } from 'util';
21
- import { ProgressAggregator } from './progress-aggregator.js';
22
- import { resolveAgentSpawn, isMockMode } from './mocks/resolve-agent-spawn.js';
23
- import { applyMemoryPrefix } from './memory-injector.js';
24
- import { detectOllama } from './detect-ollama.js';
25
- import { mirrorToolCallToAudit } from '../hooks/mirror-tool-call-to-audit.js';
26
- import { snapshotCwd, extractArtifacts } from './artifacts-extractor.js';
27
- const execFileAsync = promisify(execFile);
28
- const HEADLESS_FLAGS = ['run', '--format', 'json'];
29
- /**
30
- * Tag prefix written into opencode's `--title` so we can find a session by
31
- * codehere's own UUID later (opencode's `ses_*` ID isn't observable until
32
- * the LLM call completes — see appendMessage docblock for the full story).
33
- */
34
- const CODEHERE_TITLE_PREFIX = 'codehere-';
35
- /**
36
- * Parse `opencode session list --format json` output and return the opencode
37
- * session ID (`ses_*`) for the row whose title starts with our codehere
38
- * tag. Pure function so tests don't need a real opencode binary.
39
- *
40
- * Schema verified live (2026-05-04):
41
- * [{ "id": "ses_212f...", "title": "...", "updated": <ms>, "created": <ms>,
42
- * "projectId": "...", "directory": "..." }, ...]
43
- *
44
- * Returns the session whose title is exactly `<prefix><codehereSessionId>`.
45
- * Falls back to null if not found, the input is malformed, or the array is
46
- * empty. Caller falls back to --continue in that case.
47
- */
48
- export function parseOpenCodeSessionIdByTitle(listJson, codehereSessionId) {
49
- if (!codehereSessionId)
50
- return null;
51
- const wantedTitle = `${CODEHERE_TITLE_PREFIX}${codehereSessionId}`;
52
- let parsed;
53
- try {
54
- parsed = JSON.parse(listJson);
55
- }
56
- catch {
57
- return null;
58
- }
59
- if (!Array.isArray(parsed))
60
- return null;
61
- for (const row of parsed) {
62
- if (!row || typeof row !== 'object')
63
- continue;
64
- const r = row;
65
- if (typeof r.id === 'string' && typeof r.title === 'string' && r.title === wantedTitle) {
66
- // Defense in depth: opencode's IDs are always `ses_<base32-ish>`. If
67
- // an attacker (or upstream bug) injects a row with a poisoned id like
68
- // ` --rm-rf` we don't want to feed it back as a CLI arg.
69
- if (/^ses_[A-Za-z0-9]+$/.test(r.id))
70
- return r.id;
71
- }
72
- }
73
- return null;
74
- }
75
- const ERROR_PATTERNS = [
76
- {
77
- code: 'auth_invalid',
78
- regex: /not\s+logged\s+in|authentication\s+failed|401 unauth|no credentials/i,
79
- message: 'OpenCode auth missing or rejected.',
80
- recovery: 'Run `opencode providers` (alias `opencode auth`) to configure a provider, then retry.',
81
- },
82
- {
83
- code: 'rate_limit',
84
- regex: /rate.?limit(ed)?|429|too many requests/i,
85
- message: 'OpenCode rate-limited the request.',
86
- recovery: 'Wait a minute and retry, or swap provider via --model.',
87
- },
88
- ];
89
- function scanForKnownError(text) {
90
- for (const p of ERROR_PATTERNS) {
91
- if (p.regex.test(text))
92
- return { code: p.code, message: p.message, recovery: p.recovery };
93
- }
94
- return null;
95
- }
96
- export function spawnOpenCode(prompt, options = {}) {
97
- const sessionId = randomUUID();
98
- const startedAt = Date.now();
99
- const emitter = new EventEmitter();
100
- const buffer = [];
101
- let statusValue = 'running';
102
- let killRequested = false;
103
- const cwd = options.cwd ?? process.cwd();
104
- // F3 Task 8 — memory injection (same shape as every other adapter).
105
- const injected = applyMemoryPrefix(prompt, cwd, options);
106
- const finalPrompt = injected.prompt;
107
- const snapshot = snapshotCwd(cwd);
108
- const aggregator = new ProgressAggregator('opencode');
109
- let stepsSnapshot = [];
110
- let artifactsSnapshot;
111
- let textAccum = '';
112
- let usageSnapshot;
113
- let errorEmitted = false;
114
- const emit = (event) => {
115
- buffer.push(event);
116
- emitter.emit('event', event);
117
- if (aggregator.ingest(event)) {
118
- stepsSnapshot = aggregator.steps.map((s) => ({ ...s }));
119
- const snapEvt = { type: 'codehere:steps', timestamp: Date.now(), data: { steps: stepsSnapshot } };
120
- buffer.push(snapEvt);
121
- emitter.emit('event', snapEvt);
122
- }
123
- // OpenCode token accounting: step_finish events carry .part.tokens.{input,output,cache.{read,write}}.
124
- // Sum across all steps for session total.
125
- if (event.type === 'step_finish') {
126
- const part = event.data?.part;
127
- const t = part?.tokens;
128
- if (t) {
129
- const input = Number(t.input ?? 0);
130
- const output = Number(t.output ?? 0);
131
- const cache = t.cache ?? {};
132
- const acc = usageSnapshot ?? { inputTokens: 0, outputTokens: 0 };
133
- acc.inputTokens += input;
134
- // OpenCode's output_tokens is sometimes negative on error/cancellation; floor at 0
135
- acc.outputTokens += Math.max(0, output);
136
- if (cache.read)
137
- acc.cacheReadTokens = (acc.cacheReadTokens ?? 0) + Number(cache.read);
138
- if (cache.write)
139
- acc.cacheCreationTokens = (acc.cacheCreationTokens ?? 0) + Number(cache.write);
140
- usageSnapshot = acc;
141
- }
142
- }
143
- // Text for test-summary parsing: pull from `text` events' part.text
144
- if (event.type === 'text') {
145
- const txt = event.data?.part?.text;
146
- if (txt)
147
- textAccum += txt + '\n';
148
- }
149
- // #114 — cross-vendor audit mirror for opencode (closes the gap left by
150
- // #110 slice 4, which wired Codex/Aider/Gemini but deferred opencode).
151
- // OpenCode's `run --format json` emits `tool_use` events with shape
152
- // { type: 'tool_use', part: { tool: 'bash'|'read'|'write'|..., state:
153
- // { input: {...}, output, status, ... } } } (verified via deepwiki +
154
- // takopi cheatsheet). Same post-hoc visibility surface as Codex/Aider/
155
- // Gemini — no synchronous block until upstream pre-tool hooks land.
156
- //
157
- // v0.7 CRIT-2 fix — only mirror on state.status === 'completed' to
158
- // avoid double-firing (state transitions running → completed both arrive
159
- // as tool_use events; mirroring twice doubles audit entries + trust
160
- // scans). Per docs/contracts/opencode.md, status is canonical.
161
- if (event.type === 'tool_use') {
162
- const part = event.data?.part;
163
- // CRIT-2 guard: skip in-flight running state. Only audit on terminal.
164
- const status = part?.state?.status;
165
- if (status && status !== 'completed') {
166
- return;
167
- }
168
- const rawTool = String(part?.tool ?? 'tool').toLowerCase();
169
- // Normalize to Claude-cased tool names so /history filter chips group
170
- // cross-vendor (a bash call from claude-code and opencode show under
171
- // the same "Bash" filter chip).
172
- const toolNameMap = {
173
- bash: 'Bash',
174
- read: 'Read',
175
- write: 'Write',
176
- edit: 'Edit',
177
- grep: 'Grep',
178
- glob: 'Glob',
179
- webfetch: 'WebFetch',
180
- websearch: 'WebSearch',
181
- task: 'Task',
182
- };
183
- const toolName = toolNameMap[rawTool] ?? rawTool;
184
- const input = part?.state?.input ?? {};
185
- const inputStr = (typeof input.command === 'string' && input.command) ||
186
- (typeof input.file_path === 'string' && input.file_path) ||
187
- (typeof input.path === 'string' && input.path) ||
188
- (typeof input.pattern === 'string' && input.pattern) ||
189
- (typeof input.query === 'string' && input.query) ||
190
- JSON.stringify(input).slice(0, 500);
191
- mirrorToolCallToAudit({
192
- sessionId,
193
- agent: 'opencode',
194
- toolName,
195
- toolInput: String(inputStr),
196
- cwd,
197
- });
198
- }
199
- if (!errorEmitted) {
200
- const data = event.data;
201
- let text = '';
202
- if (event.type === 'stderr' && typeof data?.line === 'string')
203
- text = data.line;
204
- else if (event.type === 'stdout' && typeof data?.line === 'string')
205
- text = data.line;
206
- else if (data && typeof data === 'object')
207
- text = JSON.stringify(data);
208
- if (text) {
209
- const hit = scanForKnownError(text);
210
- if (hit) {
211
- errorEmitted = true;
212
- const errEvent = {
213
- type: 'codehere:error',
214
- timestamp: Date.now(),
215
- data: { code: hit.code, message: hit.message, recovery: hit.recovery },
216
- };
217
- buffer.push(errEvent);
218
- emitter.emit('event', errEvent);
219
- }
220
- }
221
- }
222
- };
223
- const spawnEnv = { ...process.env, ...(options.env ?? {}) };
224
- // Cost-bounded by default: when the user hasn't pinned a model and Ollama is
225
- // serving one locally, route opencode to it (opencode lists it as
226
- // `ollama/<model>`). This makes opencode a FREE, LOCAL vendor on a machine
227
- // with Ollama — the same pattern proven on aider (aider-adapter.ts ~314) —
228
- // so the "vendor-neutral + cost-bounded" promise isn't aider-only. Without
229
- // it, opencode falls to its config default (e.g. a paid byteplus model),
230
- // silently spending. Always overridable: --model in extraArgs, or
231
- // CODEHERE_NO_LOCAL_OLLAMA=1 to opt out.
232
- const _extra = options.extraArgs ?? [];
233
- const _modelPinned = _extra.some((a) => a === '--model' || a.startsWith('--model='));
234
- const _localArgs = [];
235
- if (!_modelPinned && spawnEnv.CODEHERE_NO_LOCAL_OLLAMA !== '1') {
236
- const ollama = detectOllama();
237
- if (ollama.available && ollama.model)
238
- _localArgs.push('--model', `ollama/${ollama.model}`);
239
- }
240
- let child;
241
- try {
242
- // OpenCode's `run` expects the message as positional args, not stdin.
243
- // --title 'codehere-<sessionId>' tags the session with codehere's own
244
- // UUID; appendMessage later looks it up by this title (see
245
- // parseOpenCodeSessionIdByTitle for the why — opencode buffers the
246
- // ses_* id until LLM completion, so we can't capture it mid-stream).
247
- const titleTag = `${CODEHERE_TITLE_PREFIX}${sessionId}`;
248
- const sp = resolveAgentSpawn('opencode', 'opencode', [...HEADLESS_FLAGS, '--title', titleTag, ..._localArgs, finalPrompt, ...(options.extraArgs ?? [])], finalPrompt);
249
- child = spawn(sp.command, sp.args, {
250
- cwd,
251
- env: spawnEnv,
252
- stdio: ['ignore', 'pipe', 'pipe'],
253
- });
254
- }
255
- catch (err) {
256
- statusValue = 'error';
257
- const msg = err instanceof Error ? err.message : String(err);
258
- queueMicrotask(() => emit({ type: 'error', timestamp: Date.now(), data: { message: msg } }));
259
- return {
260
- sessionId,
261
- startedAt,
262
- prompt,
263
- agent: 'opencode',
264
- get status() {
265
- return statusValue;
266
- },
267
- get steps() {
268
- return stepsSnapshot;
269
- },
270
- get artifacts() {
271
- return artifactsSnapshot;
272
- },
273
- subscribe(handler) {
274
- for (const e of buffer)
275
- handler(e);
276
- emitter.on('event', handler);
277
- return () => emitter.off('event', handler);
278
- },
279
- kill() {
280
- /* already not running */
281
- },
282
- };
283
- }
284
- // Organic cassette recording (S2b · capture-first plan) · no-op unless
285
- // CODEHERE_RECORD_CASSETTES=1, so zero overhead by default.
286
- const recorder = startRecording({ adapter: 'opencode', sessionId, prompt: finalPrompt, cwd });
287
- let stdoutBuf = '';
288
- let eventsParsed = 0;
289
- child.stdout?.on('data', (chunk) => {
290
- const text = chunk.toString('utf-8');
291
- recorder.onStdout(text);
292
- stdoutBuf += text;
293
- let idx;
294
- while ((idx = stdoutBuf.indexOf('\n')) >= 0) {
295
- const line = stdoutBuf.slice(0, idx).trim();
296
- stdoutBuf = stdoutBuf.slice(idx + 1);
297
- if (!line)
298
- continue;
299
- try {
300
- const data = JSON.parse(line);
301
- eventsParsed++;
302
- emit({ type: data.type ?? 'opencode_event', timestamp: Date.now(), data });
303
- }
304
- catch {
305
- // Non-JSON output still counts as observed work — the no-op guard below
306
- // must fire only when opencode produced NOTHING at all (refuter 2026-07-18).
307
- eventsParsed++;
308
- emit({ type: 'stdout', timestamp: Date.now(), data: { line } });
309
- }
310
- }
311
- });
312
- child.stderr?.on('data', (chunk) => {
313
- const text = chunk.toString('utf-8');
314
- recorder.onStderr(text);
315
- for (const line of text.split('\n')) {
316
- if (line.trim())
317
- emit({ type: 'stderr', timestamp: Date.now(), data: { line } });
318
- }
319
- });
320
- // 'close', not 'exit' — opencode buffers stdout until the LLM call completes,
321
- // so the final burst can be delivered AFTER 'exit' fires. 'close' waits for
322
- // all stdio to drain, which the zero-events guard below depends on (refuter
323
- // 2026-07-18: checking in 'exit' could flip a true success to error).
324
- child.on('close', (code, signal) => {
325
- recorder.onExit(code ?? -1, signal);
326
- // Flush a final line that arrived without a trailing newline — it would
327
- // otherwise sit unparsed in stdoutBuf and read as "no output".
328
- const remnant = stdoutBuf.trim();
329
- if (remnant) {
330
- stdoutBuf = '';
331
- try {
332
- const data = JSON.parse(remnant);
333
- eventsParsed++;
334
- emit({ type: data.type ?? 'opencode_event', timestamp: Date.now(), data });
335
- }
336
- catch {
337
- eventsParsed++;
338
- emit({ type: 'stdout', timestamp: Date.now(), data: { line: remnant } });
339
- }
340
- }
341
- // Trust the PROCESS's own exit code for success. The catch-all error-pattern
342
- // scan above flips `errorEmitted` on any transient "rate limit"/"error" token
343
- // anywhere in the stream — which falsely marked a SUCCESSFUL local-model run
344
- // as a failure (opencode read+edited the file, said "I've added…", exited 0).
345
- // For a trust product, lying that a success failed is the worst bug. So a
346
- // clean exit (code 0, not killed) = completed; errorEmitted only carries the
347
- // reason for a genuine non-zero failure.
348
- statusValue = killRequested || signal !== null ? 'killed' : code === 0 ? 'completed' : 'error';
349
- // BUG-8 (E2E 3.4) · the inverse lie: exit 0 with ZERO observed output lines
350
- // means opencode did nothing observable. "Done" over a no-op misleads worse
351
- // than an honest failure.
352
- if (statusValue === 'completed' && eventsParsed === 0) {
353
- statusValue = 'error';
354
- emit({
355
- type: 'error',
356
- timestamp: Date.now(),
357
- data: {
358
- message: 'opencode exited cleanly but reported no work at all (no events, no output). ' +
359
- 'Usually a provider/model config issue — try `opencode run "say hi"` in this folder to see the real behavior.',
360
- },
361
- });
362
- }
363
- if (aggregator.finalize(statusValue === 'completed' ? 'completed' : statusValue === 'killed' ? 'killed' : 'error')) {
364
- stepsSnapshot = aggregator.steps.map((s) => ({ ...s }));
365
- emit({ type: 'codehere:steps', timestamp: Date.now(), data: { steps: stepsSnapshot } });
366
- }
367
- // Capture artifacts ALWAYS, not only on completion — a failed run that still
368
- // edited files must report those files, never "no files changed" (the second
369
- // half of the same lie). The honesty layer downstream needs the real diff.
370
- try {
371
- artifactsSnapshot = { ...extractArtifacts(snapshot, textAccum), usage: usageSnapshot };
372
- emit({ type: 'codehere:artifacts', timestamp: Date.now(), data: artifactsSnapshot });
373
- }
374
- catch {
375
- /* best-effort */
376
- }
377
- emit({
378
- type: 'exit',
379
- timestamp: Date.now(),
380
- data: { code, signal, status: statusValue, durationMs: Date.now() - startedAt },
381
- });
382
- });
383
- child.on('error', (err) => {
384
- statusValue = 'error';
385
- emit({ type: 'error', timestamp: Date.now(), data: { message: err.message } });
386
- });
387
- return {
388
- sessionId,
389
- startedAt,
390
- prompt,
391
- agent: 'opencode',
392
- memoryInjection: injected.record,
393
- get status() {
394
- return statusValue;
395
- },
396
- get steps() {
397
- return stepsSnapshot;
398
- },
399
- get artifacts() {
400
- return artifactsSnapshot;
401
- },
402
- subscribe(handler) {
403
- for (const e of buffer)
404
- handler(e);
405
- emitter.on('event', handler);
406
- return () => emitter.off('event', handler);
407
- },
408
- kill() {
409
- if (statusValue !== 'running')
410
- return;
411
- killRequested = true;
412
- child.kill('SIGTERM');
413
- setTimeout(() => {
414
- if (statusValue === 'running') {
415
- try {
416
- child.kill('SIGKILL');
417
- }
418
- catch {
419
- /* already dead */
420
- }
421
- }
422
- }, 3000);
423
- },
424
- canAppendMessage: true,
425
- /**
426
- * v0.6.x — UUID→ses_* resolution (replaces the prior --continue shortcut).
427
- * OpenCode's `run --format json` buffers stdout until the LLM call
428
- * completes, so the ses_* session ID isn't observable mid-stream like
429
- * claude-code's session_id or codex's thread_id.
430
- *
431
- * Strategy: spawn() above tags the session with `--title codehere-<our
432
- * UUID>`. Here, run `opencode session list --format json` (clean array,
433
- * no parsing of column-aligned text), find the row whose title matches
434
- * our tag, capture its ses_* id, and use `-s <ses_*>`. Falls back to
435
- * --continue if anything fails (binary missing, no match, parse error)
436
- * — same posture as before for safety, never worse.
437
- *
438
- * Concurrency win: spawning a sibling opencode session in the same cwd
439
- * between original spawn and reply no longer hijacks the reply target.
440
- *
441
- * Output parsing: same shape as spawn — text events carry assistant text.
442
- */
443
- async appendMessage(text) {
444
- const startedAt = Date.now();
445
- // Resolve our codehere session UUID → opencode ses_* id asynchronously.
446
- // Async (not execFileSync) because this runs in the daemon's HTTP
447
- // request path; sync exec would block the event loop for up to the
448
- // timeout, freezing every other in-flight session.
449
- let resumeArgs = ['--continue'];
450
- try {
451
- // Override timeout via env for slow boxes; default 15s mirrors the
452
- // gemini adapter's choice (and is well above any LLM-tied internal
453
- // timer opencode might have).
454
- const timeoutMs = Number(process.env.CODEHERE_OPENCODE_LIST_TIMEOUT_MS) || 15000;
455
- // S9 · never execute the real binary under mock — the session-id
456
- // refinement is meaningless against the mock REPL anyway.
457
- if (isMockMode())
458
- throw new Error('mock mode: skip real session list');
459
- const { stdout: listOut } = await execFileAsync('opencode', ['session', 'list', '--format', 'json'], {
460
- cwd,
461
- env: spawnEnv,
462
- encoding: 'utf-8',
463
- timeout: timeoutMs,
464
- // opencode's session-list output can grow as the user accumulates
465
- // sessions. Default exec maxBuffer is 1MB; bump to 8MB so a long
466
- // history doesn't ENOBUFS the lookup.
467
- maxBuffer: 8 * 1024 * 1024,
468
- });
469
- const sesId = parseOpenCodeSessionIdByTitle(listOut, sessionId);
470
- if (sesId)
471
- resumeArgs = ['-s', sesId];
472
- }
473
- catch {
474
- // Binary missing, slow auth, or no list — fall through to --continue.
475
- }
476
- return new Promise((resolve, reject) => {
477
- let stdout = '';
478
- let stderr = '';
479
- // S9 · route follow-ups through the mock seam too — under
480
- // CODEHERE_MOCK=1 this hits the mock REPL, not real opencode.
481
- // (Was a real-spend leak on reply; mirrors the codex fix.)
482
- const sp2 = resolveAgentSpawn('opencode', 'opencode', [...HEADLESS_FLAGS, ...resumeArgs, text], text);
483
- const followup = spawn(sp2.command, sp2.args, {
484
- cwd,
485
- env: spawnEnv,
486
- stdio: ['ignore', 'pipe', 'pipe'],
487
- });
488
- followup.on('error', (err) => reject(err));
489
- followup.stdout?.on('data', (chunk) => {
490
- stdout += chunk.toString('utf-8');
491
- });
492
- followup.stderr?.on('data', (chunk) => {
493
- stderr += chunk.toString('utf-8');
494
- });
495
- followup.on('exit', (code) => {
496
- if (code !== 0) {
497
- reject(new Error(`opencode ${resumeArgs.join(' ')} exited ${code}: ${stderr.slice(0, 300)}`));
498
- return;
499
- }
500
- // Parse JSONL — extract assistant text from `text` events
501
- // (same shape spawn() consumes via textAccum logic above).
502
- const fragments = [];
503
- for (const line of stdout.split('\n')) {
504
- const trimmed = line.trim();
505
- if (!trimmed)
506
- continue;
507
- try {
508
- const obj = JSON.parse(trimmed);
509
- if (obj.type === 'text' && obj.part?.text) {
510
- fragments.push(obj.part.text);
511
- }
512
- }
513
- catch {
514
- /* non-JSON — skip */
515
- }
516
- }
517
- const result = fragments.join('\n').trim() || '(opencode returned no text events)';
518
- resolve({ result, durationMs: Date.now() - startedAt });
519
- });
520
- });
521
- },
522
- };
523
- }
524
- export const OpenCodeAdapter = {
525
- id: 'opencode',
526
- label: 'OpenCode',
527
- description: "Runs several agents at once for genuinely independent jobs. You won't see live progress while it works — the trade-off for the speed of doing things in parallel.",
528
- processPattern: /(?:^|\/)opencode(?:\s|$)/,
529
- isInstalled() {
530
- try {
531
- const out = execFileSync('which', ['opencode'], { encoding: 'utf-8', timeout: 2000 });
532
- return out.trim().length > 0;
533
- }
534
- catch {
535
- return false;
536
- }
537
- },
538
- spawn(prompt, options) {
539
- return spawnOpenCode(prompt, options);
540
- },
541
- checkReadiness() {
542
- const installed = this.isInstalled();
543
- if (!installed) {
544
- return {
545
- id: this.id,
546
- label: this.label,
547
- state: 'needs_install',
548
- installed: false,
549
- installHint: { command: 'curl -fsSL https://opencode.ai/install | bash', url: 'https://opencode.ai' },
550
- };
551
- }
552
- // OpenCode manages its own provider credentials via `opencode auth`
553
- // (alias `opencode providers`). The auth file lives at
554
- // $XDG_DATA_HOME/opencode/auth.json (default ~/.local/share/opencode/auth.json
555
- // on Linux/macOS) — created with mode 600 the first time a provider is added.
556
- //
557
- // Pre-May-1 we returned 'ready' on install alone, deferring to runtime to
558
- // catch missing-auth via ERROR_PATTERNS.auth_missing. That was dishonest:
559
- // /api/readiness/agents would say 'ready' → user would spawn → ~50s later
560
- // the task errored with no clear next step. Surfaced May 1 2026 by the
561
- // multi-agent dogfood test alongside the same-shape gemini fix.
562
- const xdgData = process.env.XDG_DATA_HOME || join(homedir(), '.local', 'share');
563
- const opencodeAuthPath = join(xdgData, 'opencode', 'auth.json');
564
- if (!existsSync(opencodeAuthPath)) {
565
- return {
566
- id: this.id,
567
- label: this.label,
568
- state: 'needs_auth',
569
- installed: true,
570
- authHint: {
571
- command: 'opencode auth # interactive provider/key wizard',
572
- url: 'https://opencode.ai/docs/configuration',
573
- detail: `OpenCode found on PATH but no provider configured (${opencodeAuthPath} missing). Run \`opencode auth\` once to add an AI provider — supports 75+ providers BYOK including Anthropic, OpenAI, Mistral, etc.`,
574
- },
575
- };
576
- }
577
- return { id: this.id, label: this.label, state: 'ready', installed: true };
578
- },
579
- };
580
- //# sourceMappingURL=opencode-adapter.js.map