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,592 +0,0 @@
1
- /**
2
- * Claude Code adapter — F1 foundation of the PM layer (v0.3).
3
- *
4
- * Wraps `claude -p "<task>" --output-format=stream-json …` as a subprocess
5
- * and exposes a tight interface: spawn, subscribe to events, kill.
6
- *
7
- * Deep module: everything about process management, stream parsing, line
8
- * buffering, SIGTERM→SIGKILL escalation, and late-joiner replay is hidden
9
- * behind four exported names (TaskSession type, spawnClaudeCode, TaskEvent,
10
- * TaskStatus). Callers never see a child_process handle.
11
- *
12
- * Safety note: spawned sessions inherit the user's global PreToolUse hook
13
- * via ~/.claude/settings.json automatically — we do NOT re-wire it here.
14
- * That's F0 doing its job for free.
15
- */
16
- import { spawn } from 'child_process';
17
- import { execFileSync } from 'child_process';
18
- import { startRecording } from './cassette-recorder.js';
19
- import { resolveAgentSpawn } from './mocks/resolve-agent-spawn.js';
20
- import { EventEmitter } from 'events';
21
- import { randomUUID } from 'crypto';
22
- import { ProgressAggregator } from './progress-aggregator.js';
23
- import { snapshotCwd, extractArtifacts } from './artifacts-extractor.js';
24
- import { applyMemoryPrefix } from './memory-injector.js';
25
- import { mirrorToolCallToAudit } from '../hooks/mirror-tool-call-to-audit.js';
26
- // Runtime-error fingerprints → structured codehere:error events with recovery hints.
27
- // Matched against any stderr line or assistant-text content we see on the stream.
28
- const ERROR_PATTERNS = [
29
- {
30
- code: 'credit_low',
31
- regex: /credit balance is too low/i,
32
- message: 'Your Claude API account has no credits.',
33
- recovery: 'codehere tries to use your Claude Code subscription by default. If you are seeing this, ' +
34
- 'ANTHROPIC_API_KEY is still being used somehow. Unset it in your shell (e.g. remove from ~/.zshrc), ' +
35
- 'or top up at console.anthropic.com.',
36
- },
37
- {
38
- code: 'auth_invalid',
39
- regex: /invalid api key|authentication.*failed|not authenticated|401 unauth/i,
40
- message: 'Claude Code auth was rejected.',
41
- recovery: 'Run `claude` in a terminal, sign in, then retry the task.',
42
- },
43
- {
44
- code: 'rate_limit',
45
- regex: /rate.?limit(ed)?|429|too many requests/i,
46
- message: 'Claude rate-limited the request.',
47
- recovery: 'Wait a minute and retry. If this persists, check your Claude usage dashboard.',
48
- },
49
- ];
50
- function scanForKnownError(text) {
51
- for (const p of ERROR_PATTERNS) {
52
- if (p.regex.test(text))
53
- return { code: p.code, message: p.message, recovery: p.recovery };
54
- }
55
- return null;
56
- }
57
- // v0.7 MED-2 fix — dropped `--include-partial-messages` and
58
- // `--include-hook-events` from the headless flags. Per docs/contracts/claude-code.md
59
- // §7 the adapter doesn't consume partial-message deltas (only full assistant
60
- // blocks) and doesn't surface hook events in any UI. Both flags inflated
61
- // stream volume + token cost without benefit. If we later need either
62
- // (e.g., to wire a hook visualization in v0.8), re-add with explicit
63
- // parsing intent.
64
- const HEADLESS_FLAGS = [
65
- '--output-format',
66
- 'stream-json',
67
- '--verbose',
68
- // Pre-grant common tools so -p mode doesn't dead-end at interactive permission
69
- // prompts we cannot surface to the user. Safety is still enforced by codehere's
70
- // PreToolUse hook (F0) on every tool call — that's the real gate.
71
- '--allowedTools',
72
- 'Bash',
73
- 'Edit',
74
- 'Write',
75
- 'Read',
76
- 'Grep',
77
- 'Glob',
78
- 'MultiEdit',
79
- 'NotebookEdit',
80
- // AskUserQuestion is now SURFACED by codehere (Layer 3): the agent's question
81
- // renders as a card, the user answers, and --resume continues the same session
82
- // (SPIKE 0 confirmed real claude emits the question cleanly + resumes without
83
- // the thinking-signature wall). So it's pre-granted alongside the rest.
84
- 'AskUserQuestion',
85
- // ExitPlanMode stays disallowed — no plan-approval surface yet, so it would
86
- // still dead-end the headless run.
87
- '--disallowedTools',
88
- 'ExitPlanMode',
89
- ];
90
- /**
91
- * Resume a completed claude session for a follow-up turn — STANDALONE (no live
92
- * session object). claude keeps its own session store, so `claude --resume
93
- * <session_id>` continues with full context regardless of whether codehere kept
94
- * the session in memory. This is what lets a goal be continued AFTER a daemon
95
- * restart, by reconstructing from the persisted (nativeSessionId, cwd).
96
- * (resume-after-restart fix, dogfood 2026-06-09.)
97
- */
98
- export function resumeClaudeMessage(nativeSessionId, cwd, text) {
99
- const startedAt = Date.now();
100
- return new Promise((resolve, reject) => {
101
- const env = { ...process.env };
102
- // Subscription-first, same auth the ORIGINAL spawn used: every reachable
103
- // daemon spawn path runs with preferSubscription=true (session-manager
104
- // hardcodes it; readiness only emits false in the not_installed state,
105
- // which can't spawn) — so the session being resumed was created with these
106
- // keys stripped, and resuming must match or the auth context would differ
107
- // mid-goal. If a preferSubscription=false spawn path ever becomes
108
- // reachable, persist the preference and honor it here.
109
- delete env.ANTHROPIC_API_KEY;
110
- delete env.ANTHROPIC_AUTH_TOKEN;
111
- delete env.CLAUDE_API_KEY;
112
- const sp = resolveAgentSpawn('claude-code', 'claude', ['-p', text, '--resume', nativeSessionId, ...HEADLESS_FLAGS], text);
113
- const child = spawn(sp.command, sp.args, { cwd, env: env, stdio: ['ignore', 'pipe', 'pipe'] });
114
- let stdout = '';
115
- let stderr = '';
116
- child.on('error', reject);
117
- child.stdout?.on('data', (c) => (stdout += c.toString('utf-8')));
118
- child.stderr?.on('data', (c) => (stderr += c.toString('utf-8')));
119
- child.on('exit', (code) => {
120
- if (code !== 0) {
121
- reject(new Error(`claude --resume exited ${code}: ${stderr.slice(0, 200)}`));
122
- return;
123
- }
124
- // The final 'result' event repeats the assistant's last text — prefer it
125
- // alone when present (joining both doubled the reply on the first real run).
126
- const fragments = [];
127
- let resultText;
128
- for (const line of stdout.split('\n')) {
129
- const t = line.trim();
130
- if (!t)
131
- continue;
132
- try {
133
- const o = JSON.parse(t);
134
- if (o.type === 'assistant' && o.message?.content) {
135
- for (const b of o.message.content)
136
- if (b.type === 'text' && b.text)
137
- fragments.push(b.text);
138
- }
139
- else if (o.type === 'result' && typeof o.result === 'string') {
140
- resultText = o.result;
141
- }
142
- }
143
- catch {
144
- /* non-JSON line — skip */
145
- }
146
- }
147
- const replyText = (resultText ?? fragments.join('')).trim();
148
- resolve({ result: replyText || '(the agent left no text response)', durationMs: Date.now() - startedAt });
149
- });
150
- });
151
- }
152
- export function spawnClaudeCode(prompt, options = {}) {
153
- const sessionId = randomUUID();
154
- const startedAt = Date.now();
155
- const emitter = new EventEmitter();
156
- const buffer = [];
157
- let statusValue = 'running';
158
- let killRequested = false;
159
- // PM UX Layer 2+3 state. Shared helper modules; per-session instances.
160
- const cwd = options.cwd ?? process.cwd();
161
- // F3 Task 8 — inject project memory (AGENTS.md + lessons + repo_map slice)
162
- // into the prompt prefix unless the caller opted out. Pure, side-effect-
163
- // free; returns unchanged prompt + record=undefined when there's no
164
- // .codehere/ at cwd.
165
- const injected = applyMemoryPrefix(prompt, cwd, options);
166
- const finalPrompt = injected.prompt;
167
- const snapshot = snapshotCwd(cwd);
168
- const aggregator = new ProgressAggregator('claude-code');
169
- let stepsSnapshot = [];
170
- let artifactsSnapshot;
171
- let textAccum = ''; // accumulates assistant text + result summary for test-count parsing
172
- let usageSnapshot; // token accumulator (principle #7)
173
- let modelSeen; // real model id from assistant events → per-model cost
174
- let errorEmitted = false;
175
- const emit = (event) => {
176
- buffer.push(event);
177
- emitter.emit('event', event);
178
- // Layer 2: feed aggregator; on step change, emit synthetic snapshot event.
179
- if (aggregator.ingest(event)) {
180
- stepsSnapshot = aggregator.steps.map((s) => ({ ...s }));
181
- const snapEvt = { type: 'codehere:steps', timestamp: Date.now(), data: { steps: stepsSnapshot } };
182
- buffer.push(snapEvt);
183
- emitter.emit('event', snapEvt);
184
- }
185
- // Accumulate text for test-summary parsing (Layer 3) + mirror tool calls to
186
- // the hash-chained audit.
187
- if (event.type === 'assistant') {
188
- const msg = event.data?.message;
189
- if (!modelSeen && msg?.model)
190
- modelSeen = msg.model; // capture the real model for per-model pricing
191
- if (msg?.content) {
192
- for (const b of msg.content) {
193
- if (b.type === 'text' && b.text)
194
- textAccum += b.text + '\n';
195
- else if (b.type === 'tool_use' && b.name) {
196
- // Audit-trail backstop for claude (#proof-pillar). Claude's PreToolUse
197
- // hook is the BLOCKING gate, but headless `claude -p` (how codehere
198
- // spawns it) does NOT execute that hook into the audit — MEASURED twice
199
- // 2026-06-08: a spawned claude REVIEW (read-only) left 0 entries, and a
200
- // spawned claude WRITE produced ONLY this mirror's entry (no hook entry
201
- // under the session). So the hook is silent for spawned claude and the
202
- // mirror is the SOLE audit source — no double-count. Mirror each
203
- // tool_use post-hoc like the other 4 adapters so "what every agent did"
204
- // is provable for claude too. mirrored:true marks it.
205
- // CAVEAT: if a future claude DOES fire PreToolUse hooks in headless mode,
206
- // hooked tools (Bash/Edit/Write/MultiEdit/NotebookEdit) would double —
207
- // then dedup on (sessionId,toolName,action) in the audit readers, or
208
- // mirror only the hook's complement. Not needed today (hook is silent).
209
- const params = (b.input ?? {});
210
- const toolInput = (typeof params.command === 'string' && params.command) ||
211
- (typeof params.file_path === 'string' && params.file_path) ||
212
- (typeof params.path === 'string' && params.path) ||
213
- (typeof params.pattern === 'string' && params.pattern) ||
214
- JSON.stringify(params).slice(0, 500);
215
- mirrorToolCallToAudit({
216
- sessionId,
217
- agent: 'claude-code',
218
- toolName: b.name,
219
- toolInput: String(toolInput),
220
- cwd,
221
- });
222
- }
223
- }
224
- }
225
- }
226
- else if (event.type === 'result') {
227
- const d = event.data;
228
- if (d?.result)
229
- textAccum += d.result + '\n';
230
- // Claude Code's stream-json `result` event is the final summary — carries
231
- // the full session's usage totals. One event per session; overwrite, don't sum.
232
- if (d?.usage) {
233
- usageSnapshot = {
234
- inputTokens: Number(d.usage.input_tokens ?? 0),
235
- outputTokens: Number(d.usage.output_tokens ?? 0),
236
- cacheReadTokens: d.usage.cache_read_input_tokens ? Number(d.usage.cache_read_input_tokens) : undefined,
237
- cacheCreationTokens: d.usage.cache_creation_input_tokens
238
- ? Number(d.usage.cache_creation_input_tokens)
239
- : undefined,
240
- ...(modelSeen ? { model: modelSeen } : {}),
241
- };
242
- }
243
- }
244
- else if (event.type === 'user') {
245
- const msg = event.data?.message;
246
- if (msg?.content) {
247
- for (const b of msg.content) {
248
- if (b.type === 'tool_result') {
249
- const raw = Array.isArray(b.content)
250
- ? b.content.map((c) => c.text ?? '').join('')
251
- : String(b.content ?? '');
252
- textAccum += raw + '\n';
253
- }
254
- }
255
- }
256
- }
257
- // F1b: scan for known failure patterns once per session
258
- if (!errorEmitted) {
259
- const data = event.data;
260
- let text = '';
261
- if (event.type === 'stderr' && data && typeof data.line === 'string') {
262
- text = data.line;
263
- }
264
- else if (event.type === 'assistant' && data && typeof data === 'object') {
265
- const msg = data.message;
266
- if (msg?.content) {
267
- text = msg.content.map((b) => (b.type === 'text' ? (b.text ?? '') : '')).join(' ');
268
- }
269
- }
270
- else if (event.type === 'result' && data && typeof data.result === 'string') {
271
- text = String(data.result ?? '');
272
- }
273
- if (text) {
274
- const hit = scanForKnownError(text);
275
- if (hit) {
276
- errorEmitted = true;
277
- const errEvent = {
278
- type: 'codehere:error',
279
- timestamp: Date.now(),
280
- data: { code: hit.code, message: hit.message, recovery: hit.recovery },
281
- };
282
- buffer.push(errEvent);
283
- emitter.emit('event', errEvent);
284
- }
285
- }
286
- }
287
- };
288
- const preferSubscription = options.preferSubscription !== false;
289
- const spawnEnv = { ...process.env, ...(options.env ?? {}) };
290
- if (preferSubscription) {
291
- // Force keychain/OAuth path — matches what the user's interactive `claude` uses.
292
- delete spawnEnv.ANTHROPIC_API_KEY;
293
- delete spawnEnv.ANTHROPIC_AUTH_TOKEN;
294
- delete spawnEnv.CLAUDE_API_KEY;
295
- }
296
- let child;
297
- try {
298
- const sp = resolveAgentSpawn('claude-code', 'claude', [
299
- '-p',
300
- finalPrompt,
301
- ...HEADLESS_FLAGS,
302
- ...(options.extraArgs ?? []),
303
- ]);
304
- child = spawn(sp.command, sp.args, {
305
- cwd,
306
- env: spawnEnv,
307
- stdio: ['ignore', 'pipe', 'pipe'],
308
- });
309
- }
310
- catch (err) {
311
- // Recorder is short-circuit no-op when CODEHERE_RECORD_CASSETTES != '1'
312
- // so the spawn-failure path doesn't even pay the closure cost.
313
- statusValue = 'error';
314
- const msg = err instanceof Error ? err.message : String(err);
315
- // Emit synchronously so subscribers added immediately after see it
316
- queueMicrotask(() => emit({ type: 'error', timestamp: Date.now(), data: { message: msg } }));
317
- return {
318
- sessionId,
319
- startedAt,
320
- prompt,
321
- agent: 'claude-code',
322
- get status() {
323
- return statusValue;
324
- },
325
- get steps() {
326
- return stepsSnapshot;
327
- },
328
- get artifacts() {
329
- return artifactsSnapshot;
330
- },
331
- subscribe(handler) {
332
- for (const e of buffer)
333
- handler(e);
334
- emitter.on('event', handler);
335
- return () => emitter.off('event', handler);
336
- },
337
- kill() {
338
- /* already not running */
339
- },
340
- };
341
- }
342
- // ── Organic cassette recording (S2b · capture-first plan) ─────────────────
343
- // No-op unless CODEHERE_RECORD_CASSETTES=1 · zero overhead by default.
344
- // Captures the raw byte stream + exit so a future replay can reproduce
345
- // this exact session through the mock adapter without a live agent call.
346
- const recorder = startRecording({
347
- adapter: 'claude-code',
348
- sessionId,
349
- prompt: finalPrompt,
350
- cwd,
351
- });
352
- // ── stdout: newline-delimited JSON from `--output-format stream-json` ────────
353
- let stdoutBuf = '';
354
- // Iter 13 — capture the claude-internal session_id from the init event so
355
- // appendMessage can use `--resume <session-id>` (reliable) instead of `-c`
356
- // (which picks "most recent in cwd" and can collide with other concurrent
357
- // claude sessions OR with conversations containing thinking blocks the
358
- // headless API can't replay).
359
- let claudeSessionId;
360
- child.stdout?.on('data', (chunk) => {
361
- const text = chunk.toString('utf-8');
362
- recorder.onStdout(text);
363
- stdoutBuf += text;
364
- let idx;
365
- while ((idx = stdoutBuf.indexOf('\n')) >= 0) {
366
- const line = stdoutBuf.slice(0, idx).trim();
367
- stdoutBuf = stdoutBuf.slice(idx + 1);
368
- if (!line)
369
- continue;
370
- try {
371
- const data = JSON.parse(line);
372
- if (claudeSessionId === undefined &&
373
- data.type === 'system' &&
374
- data.subtype === 'init' &&
375
- typeof data.session_id === 'string') {
376
- claudeSessionId = data.session_id;
377
- }
378
- emit({ type: data.type ?? 'message', timestamp: Date.now(), data });
379
- }
380
- catch {
381
- // Non-JSON stdout line — preserve so callers can see raw output
382
- emit({ type: 'stdout', timestamp: Date.now(), data: { line } });
383
- }
384
- }
385
- });
386
- // ── stderr: Claude prints debug/errors here; forward raw ─────────────────────
387
- child.stderr?.on('data', (chunk) => {
388
- const text = chunk.toString('utf-8');
389
- recorder.onStderr(text);
390
- for (const line of text.split('\n')) {
391
- if (line.trim())
392
- emit({ type: 'stderr', timestamp: Date.now(), data: { line } });
393
- }
394
- });
395
- child.on('exit', (code, signal) => {
396
- recorder.onExit(code ?? -1, signal);
397
- statusValue =
398
- killRequested || signal !== null ? 'killed' : errorEmitted ? 'error' : code === 0 ? 'completed' : 'error';
399
- // Emit synthetic PM events BEFORE 'exit' so the client has time to render
400
- // them before its onmessage handler closes the SSE stream in response to exit.
401
- // (Client must close on exit to avoid leaking connections — but close() cancels
402
- // pending messages, so order matters.)
403
- // Layer 2: flip any still-active steps to done/failed so the UI stops spinning.
404
- if (aggregator.finalize(statusValue === 'completed' ? 'completed' : statusValue === 'killed' ? 'killed' : 'error')) {
405
- stepsSnapshot = aggregator.steps.map((s) => ({ ...s }));
406
- emit({ type: 'codehere:steps', timestamp: Date.now(), data: { steps: stepsSnapshot } });
407
- }
408
- // Layer 3: on clean completion, extract artifacts (git diff + trust + tests + tokens) and announce.
409
- if (statusValue === 'completed') {
410
- try {
411
- artifactsSnapshot = { ...extractArtifacts(snapshot, textAccum), usage: usageSnapshot };
412
- emit({ type: 'codehere:artifacts', timestamp: Date.now(), data: artifactsSnapshot });
413
- }
414
- catch {
415
- /* best-effort; don't break exit path */
416
- }
417
- }
418
- emit({
419
- type: 'exit',
420
- timestamp: Date.now(),
421
- data: { code, signal, status: statusValue, durationMs: Date.now() - startedAt },
422
- });
423
- });
424
- child.on('error', (err) => {
425
- statusValue = 'error';
426
- emit({ type: 'error', timestamp: Date.now(), data: { message: err.message } });
427
- });
428
- return {
429
- sessionId,
430
- startedAt,
431
- prompt,
432
- agent: 'claude-code',
433
- canAppendMessage: true,
434
- memoryInjection: injected.record,
435
- // claude's own session_id, captured from the init event — persisted so a
436
- // follow-up turn can resume even after a daemon restart (resume-after-restart).
437
- get nativeSessionId() {
438
- return claudeSessionId;
439
- },
440
- get status() {
441
- return statusValue;
442
- },
443
- get steps() {
444
- return stepsSnapshot;
445
- },
446
- get artifacts() {
447
- return artifactsSnapshot;
448
- },
449
- subscribe(handler) {
450
- for (const e of buffer)
451
- handler(e);
452
- emitter.on('event', handler);
453
- return () => emitter.off('event', handler);
454
- },
455
- kill() {
456
- if (statusValue !== 'running')
457
- return;
458
- killRequested = true;
459
- child.kill('SIGTERM');
460
- // Escalate if it doesn't die in 3s
461
- setTimeout(() => {
462
- if (statusValue === 'running') {
463
- try {
464
- child.kill('SIGKILL');
465
- }
466
- catch {
467
- /* already dead */
468
- }
469
- }
470
- }, 3000);
471
- },
472
- /**
473
- * Iter 12 (with iter-13 fix) — follow-up message to a completed session.
474
- *
475
- * Iter 12 used `claude -p "<text>" -c` which picks "most recent
476
- * conversation in current directory". Iter 13 dogfood proved this
477
- * fragile: in a cwd with multiple concurrent claude sessions OR with
478
- * conversations containing thinking blocks, -c either picks the wrong
479
- * session or hits an API-replay error ("Invalid signature in thinking
480
- * block"). Fix: capture the claude session_id from this session's init
481
- * event (above) and use `--resume <session-id>` for follow-ups —
482
- * unambiguous and replay-safe.
483
- *
484
- * Scope: still one-shot Q&A. No streaming. Returns assistant's response
485
- * text. If we never captured a session_id (init event never fired —
486
- * shouldn't happen in healthy claude runs), reject explicitly.
487
- */
488
- appendMessage(text) {
489
- const startedAt = Date.now();
490
- return new Promise((resolve, reject) => {
491
- if (!claudeSessionId) {
492
- reject(new Error('Cannot reply: original claude session_id was never captured (init event missed)'));
493
- return;
494
- }
495
- let stdout = '';
496
- let stderr = '';
497
- // Route follow-ups through the mock seam too — under CODEHERE_MOCK=1 this
498
- // hits the mock REPL, not the real binary. (Was a real-spend leak:
499
- // appendMessage spawned `claude` directly regardless of mock mode.)
500
- const sp2 = resolveAgentSpawn('claude-code', 'claude', ['-p', text, '--resume', claudeSessionId, ...HEADLESS_FLAGS], text);
501
- const followup = spawn(sp2.command, sp2.args, {
502
- cwd,
503
- env: spawnEnv,
504
- stdio: ['ignore', 'pipe', 'pipe'],
505
- });
506
- followup.on('error', (err) => reject(err));
507
- followup.stdout?.on('data', (chunk) => {
508
- stdout += chunk.toString('utf-8');
509
- });
510
- followup.stderr?.on('data', (chunk) => {
511
- stderr += chunk.toString('utf-8');
512
- });
513
- followup.on('exit', (code) => {
514
- if (code !== 0) {
515
- reject(new Error(`claude -c exited ${code}: ${stderr.slice(0, 200)}`));
516
- return;
517
- }
518
- // Parse stream-json. Route every line through the SAME emit() pipeline
519
- // as the original run (Layer 3 merge): emit() ingests into the shared
520
- // aggregator, refreshes stepsSnapshot, and notifies session-manager —
521
- // so the continuation's steps (say/think/tool/output) merge into
522
- // session.steps, ride SSE, and show in the timeline. Was text-only.
523
- const lines = stdout.split('\n').filter((l) => l.trim());
524
- const fragments = [];
525
- for (const line of lines) {
526
- try {
527
- const obj = JSON.parse(line);
528
- emit({ type: obj.type ?? 'message', timestamp: Date.now(), data: obj });
529
- if (obj.type === 'assistant' && obj.message?.content) {
530
- for (const block of obj.message.content) {
531
- if (block.type === 'text' && block.text)
532
- fragments.push(block.text);
533
- }
534
- }
535
- else if (obj.type === 'result' && typeof obj.result === 'string') {
536
- fragments.push(obj.result);
537
- }
538
- }
539
- catch {
540
- // Non-JSON line — skip
541
- }
542
- }
543
- // Dedupe trivially: if 'result' contained the same text as the
544
- // last assistant block, take just one. Conservative: prefer 'result'
545
- // if present (it's the final summary), else join assistants.
546
- const result = fragments.length ? fragments[fragments.length - 1] : '(no response captured)';
547
- resolve({ result, durationMs: Date.now() - startedAt });
548
- });
549
- });
550
- },
551
- };
552
- }
553
- // ── AgentAdapter implementation ─────────────────────────────────────────────
554
- export const ClaudeCodeAdapter = {
555
- id: 'claude-code',
556
- label: 'Claude Code',
557
- description: 'Good default for editing code, running tests, and shell commands. Strongest safety net: codehere can stop risky actions before they run, not just record them after.',
558
- processPattern: /(?:^|\/)claude(?:\s|$)/,
559
- isInstalled() {
560
- try {
561
- const out = execFileSync('which', ['claude'], { encoding: 'utf-8', timeout: 2000 });
562
- return out.trim().length > 0;
563
- }
564
- catch {
565
- return false;
566
- }
567
- },
568
- spawn(prompt, options) {
569
- return spawnClaudeCode(prompt, options);
570
- },
571
- checkReadiness() {
572
- const installed = this.isInstalled();
573
- if (!installed) {
574
- return {
575
- id: this.id,
576
- label: this.label,
577
- state: 'needs_install',
578
- installed: false,
579
- installHint: {
580
- command: 'npm install -g @anthropic-ai/claude-code',
581
- url: 'https://docs.claude.com/en/docs/claude-code/setup',
582
- },
583
- };
584
- }
585
- // Claude Code supports BOTH subscription (OAuth via keychain) and API key.
586
- // "Ready" if either path works. We can't verify subscription without
587
- // calling the API, so we treat any Claude Code install as ready by default —
588
- // runtime F1b error-fingerprints catch real auth failures after spawn.
589
- return { id: this.id, label: this.label, state: 'ready', installed: true };
590
- },
591
- };
592
- //# sourceMappingURL=claude-code-adapter.js.map