codehere 0.7.1 → 1.0.1
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.
- package/README.md +22 -13
- package/dist/application/services/verdict-service.js +154 -0
- package/dist/application/use-cases/assess-cross-vendor.js +126 -0
- package/dist/application/use-cases/build-cross-vendor-verdict.js +118 -0
- package/dist/application/use-cases/export-aat.js +107 -0
- package/dist/application/use-cases/generate-acceptance-criteria.js +223 -0
- package/dist/application/use-cases/generate-final-report.js +140 -15
- package/dist/application/use-cases/generate-narration.js +5 -3
- package/dist/application/use-cases/generate-thread-title.js +5 -2
- package/dist/application/use-cases/narrator-prompt-template.js +1 -1
- package/dist/application/use-cases/plan-auto-review.js +57 -0
- package/dist/application/use-cases/reconcile-audit.js +100 -0
- package/dist/application/use-cases/rerun-tests.js +226 -0
- package/dist/application/use-cases/search-sessions.js +70 -0
- package/dist/application/use-cases/should-auto-review.js +71 -0
- package/dist/application/use-cases/thread-export.js +90 -0
- package/dist/application/use-cases/verify-claim.js +187 -5
- package/dist/domain/audit.js +11 -0
- package/dist/domain/services/codebase-detector.js +0 -6
- package/dist/domain/task.js +91 -0
- package/dist/error-handler.js +3 -115
- package/dist/index.js +279 -31
- package/dist/infrastructure/completion/bash-completion.js +7 -3
- package/dist/infrastructure/completion/fish-completion.js +7 -12
- package/dist/infrastructure/completion/zsh-completion.js +7 -12
- package/dist/infrastructure/config/config-manager.js +4 -3
- package/dist/infrastructure/config/product-context.js +1 -19
- package/dist/infrastructure/context/adapters/figma-source.js +37 -2
- package/dist/infrastructure/context/adapters/github-issue-source.js +3 -2
- package/dist/infrastructure/context/github-api.js +110 -0
- package/dist/infrastructure/context/github-device-flow.js +65 -0
- package/dist/infrastructure/context/oauth-store.js +5 -2
- package/dist/infrastructure/context/registry-bootstrap.js +0 -4
- package/dist/infrastructure/cost/budget-config.js +4 -3
- package/dist/infrastructure/cost/cost-router.js +16 -23
- package/dist/infrastructure/cost/spend-aggregator.js +91 -16
- package/dist/infrastructure/cost/team-ledger.js +355 -0
- package/dist/infrastructure/discovery/discovery-log.js +85 -0
- package/dist/infrastructure/hooks/audit-tip-signature.js +336 -0
- package/dist/infrastructure/hooks/claude-sdk-hook.js +139 -3
- package/dist/infrastructure/hooks/hook-mode.js +20 -4
- package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +8 -2
- package/dist/infrastructure/hooks/pre-prompt-scan.js +3 -3
- package/dist/infrastructure/observability/execution-tracer.js +1 -1
- package/dist/infrastructure/orchestration/agents-config-seed.js +23 -11
- package/dist/infrastructure/orchestration/aider-adapter.js +188 -24
- package/dist/infrastructure/orchestration/artifacts-extractor.js +60 -2
- package/dist/infrastructure/orchestration/cassette-recorder.js +1 -1
- package/dist/infrastructure/orchestration/claude-code-adapter.js +117 -3
- package/dist/infrastructure/orchestration/codex-adapter.js +52 -16
- package/dist/infrastructure/orchestration/conductor-adapter.js +46 -7
- package/dist/infrastructure/orchestration/context-handoff.js +32 -14
- package/dist/infrastructure/orchestration/detect-ollama.js +75 -0
- package/dist/infrastructure/orchestration/failover-config.js +1 -11
- package/dist/infrastructure/orchestration/gemini-adapter.js +92 -12
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +134 -3
- package/dist/infrastructure/orchestration/graphify-context.js +14 -59
- package/dist/infrastructure/orchestration/memory-injector.js +4 -1
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +167 -9
- package/dist/infrastructure/orchestration/opencode-adapter.js +84 -14
- package/dist/infrastructure/orchestration/progress-aggregator.js +275 -23
- package/dist/infrastructure/orchestration/session-manager.js +593 -94
- package/dist/infrastructure/orchestration/session-persistence.js +177 -41
- package/dist/infrastructure/orchestration/thread-key-store.js +1 -1
- package/dist/infrastructure/orchestration/write-pass-store.js +0 -8
- package/dist/infrastructure/protocols/cloud-sync.js +2 -2
- package/dist/infrastructure/protocols/mcp-helpers.js +1 -1
- package/dist/infrastructure/protocols/mcp-server-impl.js +1498 -354
- package/dist/infrastructure/protocols/mcp-tools.js +401 -0
- package/dist/infrastructure/protocols/ui/app-script-artifacts.js +121 -0
- package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +44 -0
- package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +56 -0
- package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +588 -0
- package/dist/infrastructure/protocols/ui/app-script-context-sources.js +168 -0
- package/dist/infrastructure/protocols/ui/app-script-conversation.js +67 -0
- package/dist/infrastructure/protocols/ui/app-script-drawer.js +115 -0
- package/dist/infrastructure/protocols/ui/app-script-failover.js +133 -0
- package/dist/infrastructure/protocols/ui/app-script-github.js +113 -0
- package/dist/infrastructure/protocols/ui/app-script-observer.js +104 -0
- package/dist/infrastructure/protocols/ui/app-script-onboarding.js +80 -0
- package/dist/infrastructure/protocols/ui/app-script-overlay.js +203 -0
- package/dist/infrastructure/protocols/ui/app-script-recovery.js +136 -0
- package/dist/infrastructure/protocols/ui/app-script-render.js +166 -0
- package/dist/infrastructure/protocols/ui/app-script-share.js +78 -0
- package/dist/infrastructure/protocols/ui/app-script-sidebar.js +194 -0
- package/dist/infrastructure/protocols/ui/app-script-tour.js +78 -0
- package/dist/infrastructure/protocols/ui/app-script-wpass.js +312 -0
- package/dist/infrastructure/protocols/ui/app-script.js +2998 -0
- package/dist/infrastructure/protocols/ui/font-faces.js +7 -0
- package/dist/infrastructure/protocols/ui/styles.js +2147 -0
- package/dist/infrastructure/protocols/ui/tokens.js +96 -0
- package/dist/infrastructure/protocols/ui-page-v2.js +116 -4126
- package/dist/infrastructure/protocols/verify-page.js +884 -148
- package/dist/infrastructure/security/fix-code.js +144 -0
- package/dist/infrastructure/security/fix-secret.js +89 -0
- package/dist/infrastructure/security/permissions-config.js +3 -2
- package/dist/infrastructure/security/scan-types.js +10 -0
- package/dist/infrastructure/security/ship-scan.js +195 -0
- package/dist/infrastructure/security/trust-scorer.js +70 -3
- package/dist/infrastructure/security/vibe-checks.js +99 -0
- package/dist/infrastructure/storage/atomic-write.js +86 -0
- package/dist/infrastructure/storage/project-thread-mirror.js +151 -4
- package/dist/infrastructure/storage/project-thread-reader.js +262 -25
- package/dist/infrastructure/storage/project-thread-title-backfill.js +1 -1
- package/dist/monitoring.js +2 -2
- package/dist/presentation/cli/commands/audit-command.js +79 -4
- package/dist/presentation/cli/commands/budget-command.js +2 -2
- package/dist/presentation/cli/commands/check-command.js +96 -0
- package/dist/presentation/cli/commands/context-source-command.js +20 -1
- package/dist/presentation/cli/commands/help-command.js +5 -5
- package/dist/presentation/cli/commands/hook-command.js +5 -5
- package/dist/presentation/cli/commands/init-command.js +89 -66
- package/dist/presentation/cli/commands/memory-command.js +28 -28
- package/dist/presentation/cli/commands/permission-command.js +1 -12
- package/dist/presentation/cli/commands/protocols-builder.js +2 -2
- package/dist/presentation/cli/commands/protocols-command.js +4 -4
- package/dist/presentation/cli/commands/repo-map-builder.js +1 -1
- package/dist/presentation/cli/commands/report-command.js +48 -32
- package/dist/presentation/cli/commands/scan-command.js +52 -0
- package/dist/presentation/cli/commands/scan-tool-command.js +24 -8
- package/dist/presentation/cli/commands/search-command.js +51 -0
- package/dist/presentation/cli/commands/setup-command.js +31 -8
- package/dist/presentation/cli/commands/status-command.js +39 -86
- package/dist/presentation/cli/commands/team-command.js +256 -0
- package/dist/presentation/cli/commands/thread-command.js +87 -5
- package/dist/presentation/cli/commands/trust-command.js +53 -31
- package/dist/presentation/cli/commands/verify-command.js +74 -9
- package/dist/presentation/cli/error-display.js +0 -8
- package/dist/presentation/cli/repl.js +15 -7
- package/dist/presentation/cli/task-deliverable.js +36 -10
- package/dist/testing/replay-adapter.js +0 -19
- package/dist/ui.js +51 -239
- package/dist/utils/version.js +0 -6
- package/package.json +30 -10
- package/dist/domain/entities/task.js +0 -61
- package/dist/infrastructure/protocols/ui-page.js +0 -2377
- package/dist/infrastructure/security/input-validator.js +0 -152
- package/dist/infrastructure/storage/embeddings-path.js +0 -37
- package/dist/infrastructure/storage/task-repository.js +0 -206
|
@@ -65,8 +65,17 @@ function readStdin(timeoutMs = 1500) {
|
|
|
65
65
|
setTimeout(finish, timeoutMs);
|
|
66
66
|
});
|
|
67
67
|
}
|
|
68
|
-
|
|
68
|
+
// CODEHERE_MOCK_SLOW multiplies step delays so the "watch it work" running
|
|
69
|
+
// state can actually be observed — for dogfood screenshots and the record-replay
|
|
70
|
+
// webm. Default 1 (fast, for tests). e.g. CODEHERE_MOCK_SLOW=8 ≈ 1s/step.
|
|
71
|
+
const SLOW = Math.max(1, Number(process.env.CODEHERE_MOCK_SLOW) || 1);
|
|
72
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms * SLOW));
|
|
69
73
|
const out = (o) => process.stdout.write(JSON.stringify(o) + '\n');
|
|
74
|
+
// Mock-currency rule: every agent emits token usage in ITS vendor's exact
|
|
75
|
+
// shape, so each adapter's cost-parse branch runs under CODEHERE_MOCK instead
|
|
76
|
+
// of being dead-until-a-real-run. Small fixed values → the session cost
|
|
77
|
+
// estimate is deterministic and non-zero under mock.
|
|
78
|
+
const MOCK_TOKENS = { input: 1200, output: 800 };
|
|
70
79
|
// Derive a few plausible steps from the prompt. Heuristic, not modeled — enough
|
|
71
80
|
// for the execution path to produce real steps/artifacts without an API call.
|
|
72
81
|
function plannedSteps(p) {
|
|
@@ -82,11 +91,114 @@ function plannedSteps(p) {
|
|
|
82
91
|
steps.push({ tool: 'Bash', input: { command: 'npm run build' }, result: 'build clean' });
|
|
83
92
|
return steps;
|
|
84
93
|
}
|
|
94
|
+
/** The wrap-up say every real agent emits before its result. The mock never
|
|
95
|
+
* did, so a mock report's only prose was the "On it" ack and every glance
|
|
96
|
+
* led with narration instead of the finding (journey-QA 2026-07-06).
|
|
97
|
+
* Finding first, in the same artifacts plannedSteps claims. */
|
|
98
|
+
function wrapUpText(steps) {
|
|
99
|
+
const edit = steps.find((s) => s.tool === 'Edit');
|
|
100
|
+
const check = steps.find((s) => s.tool === 'Bash');
|
|
101
|
+
const wrote = edit
|
|
102
|
+
? 'Wrote the change to ' + String(edit.input.file_path) + ' (' + edit.result.split(' ·')[0] + ')'
|
|
103
|
+
: 'No file changes';
|
|
104
|
+
const checked = check ? String(check.input.command) + ' — ' + check.result : '';
|
|
105
|
+
return wrote + (checked ? '; ' + checked : '') + '. (mock · no API spend)';
|
|
106
|
+
}
|
|
85
107
|
async function runClaudeCode() {
|
|
86
|
-
const
|
|
87
|
-
|
|
108
|
+
const resumeId = argVal('--resume');
|
|
109
|
+
const sid = resumeId || 'mock-' + Math.random().toString(36).slice(2, 10);
|
|
110
|
+
out({
|
|
111
|
+
type: 'system',
|
|
112
|
+
subtype: 'init',
|
|
113
|
+
session_id: sid,
|
|
114
|
+
tools: ['Read', 'Edit', 'Bash', 'AskUserQuestion'],
|
|
115
|
+
model: 'mock',
|
|
116
|
+
});
|
|
88
117
|
await sleep(120);
|
|
118
|
+
// Continuation (answering a question / a follow-up): acknowledge + do the work.
|
|
119
|
+
if (resumeId) {
|
|
120
|
+
const ans = prompt.replace(/^[\s\S]*?My answer:\s*/i, '').trim() || prompt;
|
|
121
|
+
out({
|
|
122
|
+
type: 'assistant',
|
|
123
|
+
message: {
|
|
124
|
+
content: [{ type: 'thinking', thinking: 'The user chose: ' + ans + '. I will implement that path now.' }],
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
await sleep(160);
|
|
128
|
+
out({
|
|
129
|
+
type: 'assistant',
|
|
130
|
+
message: { content: [{ type: 'text', text: 'Got it — going with **' + ans + '**. Implementing now.' }] },
|
|
131
|
+
});
|
|
132
|
+
await sleep(160);
|
|
133
|
+
out({
|
|
134
|
+
type: 'assistant',
|
|
135
|
+
message: { content: [{ type: 'tool_use', id: 'r0', name: 'Edit', input: { file_path: MOCK_EDIT_FILE } }] },
|
|
136
|
+
});
|
|
137
|
+
await sleep(160);
|
|
138
|
+
out({
|
|
139
|
+
type: 'user',
|
|
140
|
+
message: {
|
|
141
|
+
content: [
|
|
142
|
+
{ type: 'tool_result', tool_use_id: 'r0', content: '+18 lines · applied your choice', is_error: false },
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
await sleep(140);
|
|
147
|
+
out({
|
|
148
|
+
type: 'result',
|
|
149
|
+
subtype: 'success',
|
|
150
|
+
session_id: sid,
|
|
151
|
+
result: 'Done — applied your answer (' + ans + ').',
|
|
152
|
+
usage: { input_tokens: MOCK_TOKENS.input, output_tokens: MOCK_TOKENS.output },
|
|
153
|
+
});
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
89
156
|
out({ type: 'assistant', message: { content: [{ type: 'text', text: 'On it — ' + prompt }] } });
|
|
157
|
+
// Ambiguous prompt → ask the user, then pause (the headless tool can't be
|
|
158
|
+
// answered, so the turn ends; codehere surfaces the card + resumes on answer).
|
|
159
|
+
if (/\bask\b|which|approach|prefer|option|client|server/.test(prompt.toLowerCase())) {
|
|
160
|
+
await sleep(160);
|
|
161
|
+
out({
|
|
162
|
+
type: 'assistant',
|
|
163
|
+
message: {
|
|
164
|
+
content: [
|
|
165
|
+
{
|
|
166
|
+
type: 'tool_use',
|
|
167
|
+
id: 'tu_ask',
|
|
168
|
+
name: 'AskUserQuestion',
|
|
169
|
+
input: {
|
|
170
|
+
questions: [
|
|
171
|
+
{
|
|
172
|
+
question: 'Which approach should I use?',
|
|
173
|
+
header: 'Approach',
|
|
174
|
+
options: [
|
|
175
|
+
{ label: 'Client-side only', description: 'fast, no backend changes' },
|
|
176
|
+
{ label: 'Client + server', description: 'safer — validates on the server too' },
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
await sleep(120);
|
|
186
|
+
out({
|
|
187
|
+
type: 'user',
|
|
188
|
+
message: {
|
|
189
|
+
content: [{ type: 'tool_result', tool_use_id: 'tu_ask', is_error: true, content: 'awaiting your answer' }],
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
await sleep(120);
|
|
193
|
+
out({
|
|
194
|
+
type: 'result',
|
|
195
|
+
subtype: 'success',
|
|
196
|
+
session_id: sid,
|
|
197
|
+
result: 'Waiting for your answer to continue.',
|
|
198
|
+
usage: { input_tokens: MOCK_TOKENS.input, output_tokens: MOCK_TOKENS.output },
|
|
199
|
+
});
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
90
202
|
const steps = plannedSteps(prompt);
|
|
91
203
|
for (let i = 0; i < steps.length; i++) {
|
|
92
204
|
const s = steps[i];
|
|
@@ -100,11 +212,17 @@ async function runClaudeCode() {
|
|
|
100
212
|
});
|
|
101
213
|
}
|
|
102
214
|
await sleep(140);
|
|
215
|
+
// The wrap-up say (real claude always emits one) — the report leads with it.
|
|
216
|
+
out({ type: 'assistant', message: { content: [{ type: 'text', text: wrapUpText(steps) }] } });
|
|
217
|
+
await sleep(120);
|
|
218
|
+
// Real claude stream-json carries the session's usage totals on the final
|
|
219
|
+
// `result` event (input_tokens / output_tokens / cache_*_input_tokens).
|
|
103
220
|
out({
|
|
104
221
|
type: 'result',
|
|
105
222
|
subtype: 'success',
|
|
106
223
|
session_id: sid,
|
|
107
224
|
result: 'Completed (mock): ' + prompt + '. ' + steps.length + ' steps · no API spend.',
|
|
225
|
+
usage: { input_tokens: MOCK_TOKENS.input, output_tokens: MOCK_TOKENS.output },
|
|
108
226
|
});
|
|
109
227
|
}
|
|
110
228
|
// codex exec --json format · thread.started → item.completed events → turn.completed
|
|
@@ -123,7 +241,10 @@ async function runCodex() {
|
|
|
123
241
|
}
|
|
124
242
|
if (/linear|github|jira|figma|mcp|issue|ticket/.test(lc)) {
|
|
125
243
|
await sleep(120);
|
|
126
|
-
out({
|
|
244
|
+
out({
|
|
245
|
+
type: 'item.completed',
|
|
246
|
+
item: { type: 'mcp_tool_call', server: 'linear', tool: 'get_issue', arguments: { id: 'INFRA-247' } },
|
|
247
|
+
});
|
|
127
248
|
}
|
|
128
249
|
const steps = plannedSteps(prompt);
|
|
129
250
|
for (const s of steps) {
|
|
@@ -150,12 +271,18 @@ async function runCodex() {
|
|
|
150
271
|
}
|
|
151
272
|
await sleep(120);
|
|
152
273
|
out({ type: 'turn.completed', usage: { input_tokens: 1200, output_tokens: 340 } });
|
|
153
|
-
out({ type: 'item.completed', item: { type: 'agent_message', text:
|
|
274
|
+
out({ type: 'item.completed', item: { type: 'agent_message', text: wrapUpText(steps) } });
|
|
154
275
|
}
|
|
155
276
|
// aider · plain-text stdout · "Applied edit to <p>" + "Running <cmd>"
|
|
156
277
|
async function runAider() {
|
|
157
278
|
process.stdout.write('Aider v0.x (mock)\n');
|
|
158
279
|
const steps = plannedSteps(prompt);
|
|
280
|
+
// Real aider narrates BEFORE its edits/commands (its prose precedes the
|
|
281
|
+
// "Applied edit"/"Running" lines; nothing prose-shaped follows the last
|
|
282
|
+
// command — the adapter attributes post-Running lines to that command's
|
|
283
|
+
// output). Mock-currency: same order, so the say capture sees the finding.
|
|
284
|
+
await sleep(140);
|
|
285
|
+
process.stdout.write(wrapUpText(steps).replace(/^Wrote the change to/, 'I will write the change to') + '\n');
|
|
159
286
|
for (const s of steps) {
|
|
160
287
|
await sleep(170);
|
|
161
288
|
if (s.tool === 'Bash')
|
|
@@ -166,12 +293,22 @@ async function runAider() {
|
|
|
166
293
|
process.stdout.write(String(s.input.file_path) + '\n'); // read → plain mention
|
|
167
294
|
}
|
|
168
295
|
await sleep(120);
|
|
169
|
-
|
|
296
|
+
// Real aider prints a cost summary line at turn end; the adapter's token
|
|
297
|
+
// extraction parses exactly this "Tokens: Nk sent, N received" shape.
|
|
298
|
+
process.stdout.write('Tokens: 1.2k sent, 800 received. Cost: $0.0046 message, $0.0046 session.\n');
|
|
170
299
|
}
|
|
171
|
-
// gemini · JSON tool_use / tool_result (tool_id, tool_name, parameters)
|
|
300
|
+
// gemini · JSON tool_use / tool_result (tool_id, tool_name, parameters).
|
|
301
|
+
// Narration matches the REAL gemini-cli stream: `message` events with
|
|
302
|
+
// role:'assistant' + delta:true chunks (verified against gemini-cli 0.39.1 —
|
|
303
|
+
// assistant messages are ALWAYS delta'd; a `content` event type doesn't
|
|
304
|
+
// exist). Mock-currency rule: the mock emits what the real CLI emits, or
|
|
305
|
+
// mock runs silently exercise dead branches (this gap hid the no-say bug
|
|
306
|
+
// that made every real gemini report degrade to the deterministic fallback).
|
|
172
307
|
async function runGemini() {
|
|
173
308
|
out({ type: 'session', session_id: 'mock-gem-' + Math.random().toString(36).slice(2, 8) });
|
|
174
309
|
await sleep(120);
|
|
310
|
+
out({ type: 'message', role: 'assistant', content: 'Working on it (mock): ', delta: true });
|
|
311
|
+
out({ type: 'message', role: 'assistant', content: prompt.slice(0, 60), delta: true });
|
|
175
312
|
const steps = plannedSteps(prompt);
|
|
176
313
|
for (let i = 0; i < steps.length; i++) {
|
|
177
314
|
const s = steps[i];
|
|
@@ -183,7 +320,19 @@ async function runGemini() {
|
|
|
183
320
|
out({ type: 'tool_result', tool_id: id, output: s.result });
|
|
184
321
|
}
|
|
185
322
|
await sleep(120);
|
|
186
|
-
out({ type: '
|
|
323
|
+
out({ type: 'message', role: 'assistant', content: wrapUpText(steps), delta: true });
|
|
324
|
+
// Real gemini carries token usage ONLY on the final success `result` event,
|
|
325
|
+
// under `stats` (the adapter reads input_tokens/output_tokens/cached).
|
|
326
|
+
out({
|
|
327
|
+
type: 'result',
|
|
328
|
+
status: 'success',
|
|
329
|
+
stats: {
|
|
330
|
+
total_tokens: MOCK_TOKENS.input + MOCK_TOKENS.output,
|
|
331
|
+
input_tokens: MOCK_TOKENS.input,
|
|
332
|
+
output_tokens: MOCK_TOKENS.output,
|
|
333
|
+
cached: 0,
|
|
334
|
+
},
|
|
335
|
+
});
|
|
187
336
|
}
|
|
188
337
|
// opencode · JSON tool_use with part:{tool,state:{input,output,status}}
|
|
189
338
|
async function runOpencode() {
|
|
@@ -194,7 +343,16 @@ async function runOpencode() {
|
|
|
194
343
|
out({ type: 'tool_use', part: { tool, state: { input: s.input, output: s.result, status: 'completed' } } });
|
|
195
344
|
}
|
|
196
345
|
await sleep(120);
|
|
197
|
-
|
|
346
|
+
// Real opencode carries token usage on step_finish events under
|
|
347
|
+
// part.tokens.{input,output,cache.{read,write}}; the adapter sums them.
|
|
348
|
+
out({
|
|
349
|
+
type: 'step_finish',
|
|
350
|
+
part: { tokens: { input: MOCK_TOKENS.input, output: MOCK_TOKENS.output, cache: { read: 0, write: 0 } } },
|
|
351
|
+
});
|
|
352
|
+
// Real opencode carries narrative text in part.text (the adapter's summary
|
|
353
|
+
// extraction and the aggregator's say capture both read that shape) — the
|
|
354
|
+
// prior top-level `text` field was mock drift no real parser ever saw.
|
|
355
|
+
out({ type: 'text', part: { text: wrapUpText(steps) } });
|
|
198
356
|
}
|
|
199
357
|
async function main() {
|
|
200
358
|
if (!prompt)
|
|
@@ -19,8 +19,9 @@ import { homedir } from 'os';
|
|
|
19
19
|
import { join } from 'path';
|
|
20
20
|
import { promisify } from 'util';
|
|
21
21
|
import { ProgressAggregator } from './progress-aggregator.js';
|
|
22
|
-
import { resolveAgentSpawn } from './mocks/resolve-agent-spawn.js';
|
|
22
|
+
import { resolveAgentSpawn, isMockMode } from './mocks/resolve-agent-spawn.js';
|
|
23
23
|
import { applyMemoryPrefix } from './memory-injector.js';
|
|
24
|
+
import { detectOllama } from './detect-ollama.js';
|
|
24
25
|
import { mirrorToolCallToAudit } from '../hooks/mirror-tool-call-to-audit.js';
|
|
25
26
|
import { snapshotCwd, extractArtifacts } from './artifacts-extractor.js';
|
|
26
27
|
const execFileAsync = promisify(execFile);
|
|
@@ -220,6 +221,22 @@ export function spawnOpenCode(prompt, options = {}) {
|
|
|
220
221
|
}
|
|
221
222
|
};
|
|
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
|
+
}
|
|
223
240
|
let child;
|
|
224
241
|
try {
|
|
225
242
|
// OpenCode's `run` expects the message as positional args, not stdin.
|
|
@@ -228,7 +245,7 @@ export function spawnOpenCode(prompt, options = {}) {
|
|
|
228
245
|
// parseOpenCodeSessionIdByTitle for the why — opencode buffers the
|
|
229
246
|
// ses_* id until LLM completion, so we can't capture it mid-stream).
|
|
230
247
|
const titleTag = `${CODEHERE_TITLE_PREFIX}${sessionId}`;
|
|
231
|
-
const sp = resolveAgentSpawn('opencode', 'opencode', [...HEADLESS_FLAGS, '--title', titleTag, finalPrompt, ...(options.extraArgs ?? [])], finalPrompt);
|
|
248
|
+
const sp = resolveAgentSpawn('opencode', 'opencode', [...HEADLESS_FLAGS, '--title', titleTag, ..._localArgs, finalPrompt, ...(options.extraArgs ?? [])], finalPrompt);
|
|
232
249
|
child = spawn(sp.command, sp.args, {
|
|
233
250
|
cwd,
|
|
234
251
|
env: spawnEnv,
|
|
@@ -268,6 +285,7 @@ export function spawnOpenCode(prompt, options = {}) {
|
|
|
268
285
|
// CODEHERE_RECORD_CASSETTES=1, so zero overhead by default.
|
|
269
286
|
const recorder = startRecording({ adapter: 'opencode', sessionId, prompt: finalPrompt, cwd });
|
|
270
287
|
let stdoutBuf = '';
|
|
288
|
+
let eventsParsed = 0;
|
|
271
289
|
child.stdout?.on('data', (chunk) => {
|
|
272
290
|
const text = chunk.toString('utf-8');
|
|
273
291
|
recorder.onStdout(text);
|
|
@@ -280,9 +298,13 @@ export function spawnOpenCode(prompt, options = {}) {
|
|
|
280
298
|
continue;
|
|
281
299
|
try {
|
|
282
300
|
const data = JSON.parse(line);
|
|
301
|
+
eventsParsed++;
|
|
283
302
|
emit({ type: data.type ?? 'opencode_event', timestamp: Date.now(), data });
|
|
284
303
|
}
|
|
285
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++;
|
|
286
308
|
emit({ type: 'stdout', timestamp: Date.now(), data: { line } });
|
|
287
309
|
}
|
|
288
310
|
}
|
|
@@ -295,23 +317,63 @@ export function spawnOpenCode(prompt, options = {}) {
|
|
|
295
317
|
emit({ type: 'stderr', timestamp: Date.now(), data: { line } });
|
|
296
318
|
}
|
|
297
319
|
});
|
|
298
|
-
|
|
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) => {
|
|
299
325
|
recorder.onExit(code ?? -1, signal);
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
}
|
|
306
|
-
if (statusValue === 'completed') {
|
|
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 = '';
|
|
307
331
|
try {
|
|
308
|
-
|
|
309
|
-
|
|
332
|
+
const data = JSON.parse(remnant);
|
|
333
|
+
eventsParsed++;
|
|
334
|
+
emit({ type: data.type ?? 'opencode_event', timestamp: Date.now(), data });
|
|
310
335
|
}
|
|
311
336
|
catch {
|
|
312
|
-
|
|
337
|
+
eventsParsed++;
|
|
338
|
+
emit({ type: 'stdout', timestamp: Date.now(), data: { line: remnant } });
|
|
313
339
|
}
|
|
314
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
|
+
}
|
|
315
377
|
emit({
|
|
316
378
|
type: 'exit',
|
|
317
379
|
timestamp: Date.now(),
|
|
@@ -390,6 +452,10 @@ export function spawnOpenCode(prompt, options = {}) {
|
|
|
390
452
|
// gemini adapter's choice (and is well above any LLM-tied internal
|
|
391
453
|
// timer opencode might have).
|
|
392
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');
|
|
393
459
|
const { stdout: listOut } = await execFileAsync('opencode', ['session', 'list', '--format', 'json'], {
|
|
394
460
|
cwd,
|
|
395
461
|
env: spawnEnv,
|
|
@@ -410,7 +476,11 @@ export function spawnOpenCode(prompt, options = {}) {
|
|
|
410
476
|
return new Promise((resolve, reject) => {
|
|
411
477
|
let stdout = '';
|
|
412
478
|
let stderr = '';
|
|
413
|
-
|
|
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, {
|
|
414
484
|
cwd,
|
|
415
485
|
env: spawnEnv,
|
|
416
486
|
stdio: ['ignore', 'pipe', 'pipe'],
|