codehere 0.8.0 → 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 (119) hide show
  1. package/README.md +37 -184
  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 +329 -0
  5. package/dist/index.js +74 -1004
  6. package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -265
  7. package/dist/presentation/cli/commands/check-command.js +251 -0
  8. package/dist/presentation/cli/commands/help-command.js +22 -286
  9. package/dist/ui.js +19 -438
  10. package/dist/utils/version.js +0 -12
  11. package/package.json +18 -27
  12. package/dist/application/use-cases/domain-trust.js +0 -110
  13. package/dist/application/use-cases/generate-final-report.js +0 -134
  14. package/dist/application/use-cases/generate-narration.js +0 -128
  15. package/dist/application/use-cases/generate-thread-title.js +0 -178
  16. package/dist/application/use-cases/narrator-prompt-template.js +0 -158
  17. package/dist/application/use-cases/thread-export.js +0 -155
  18. package/dist/application/use-cases/verify-claim.js +0 -274
  19. package/dist/application/use-cases/write-pass.js +0 -277
  20. package/dist/domain/entities/task.js +0 -61
  21. package/dist/domain/services/codebase-detector.js +0 -91
  22. package/dist/error-handler.js +0 -251
  23. package/dist/infrastructure/completion/bash-completion.js +0 -25
  24. package/dist/infrastructure/completion/fish-completion.js +0 -22
  25. package/dist/infrastructure/completion/zsh-completion.js +0 -30
  26. package/dist/infrastructure/config/config-manager.js +0 -173
  27. package/dist/infrastructure/config/product-context.js +0 -67
  28. package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
  29. package/dist/infrastructure/context/adapters/figma-source.js +0 -163
  30. package/dist/infrastructure/context/adapters/github-issue-source.js +0 -159
  31. package/dist/infrastructure/context/adapters/linear-source.js +0 -190
  32. package/dist/infrastructure/context/context-resolver.js +0 -144
  33. package/dist/infrastructure/context/context-source.js +0 -114
  34. package/dist/infrastructure/context/oauth-flow.js +0 -207
  35. package/dist/infrastructure/context/oauth-store.js +0 -119
  36. package/dist/infrastructure/context/registry-bootstrap.js +0 -99
  37. package/dist/infrastructure/context/write-pass-resolver.js +0 -106
  38. package/dist/infrastructure/cost/budget-config.js +0 -113
  39. package/dist/infrastructure/cost/cost-router.js +0 -104
  40. package/dist/infrastructure/cost/spend-aggregator.js +0 -147
  41. package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -888
  42. package/dist/infrastructure/hooks/hook-mode.js +0 -71
  43. package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -43
  44. package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
  45. package/dist/infrastructure/hooks/session-analyzer.js +0 -177
  46. package/dist/infrastructure/observability/execution-tracer.js +0 -227
  47. package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
  48. package/dist/infrastructure/orchestration/agent-installer.js +0 -101
  49. package/dist/infrastructure/orchestration/agents-config-seed.js +0 -56
  50. package/dist/infrastructure/orchestration/aider-adapter.js +0 -447
  51. package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
  52. package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -478
  53. package/dist/infrastructure/orchestration/codex-adapter.js +0 -538
  54. package/dist/infrastructure/orchestration/conductor-adapter.js +0 -377
  55. package/dist/infrastructure/orchestration/context-handoff.js +0 -144
  56. package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
  57. package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
  58. package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
  59. package/dist/infrastructure/orchestration/failover-config.js +0 -77
  60. package/dist/infrastructure/orchestration/failover-policy.js +0 -106
  61. package/dist/infrastructure/orchestration/gemini-adapter.js +0 -583
  62. package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -558
  63. package/dist/infrastructure/orchestration/graphify-context.js +0 -254
  64. package/dist/infrastructure/orchestration/memory-injector.js +0 -380
  65. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -233
  66. package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
  67. package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
  68. package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
  69. package/dist/infrastructure/orchestration/opencode-adapter.js +0 -510
  70. package/dist/infrastructure/orchestration/progress-aggregator.js +0 -689
  71. package/dist/infrastructure/orchestration/readiness.js +0 -103
  72. package/dist/infrastructure/orchestration/session-manager.js +0 -1091
  73. package/dist/infrastructure/orchestration/session-persistence.js +0 -258
  74. package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
  75. package/dist/infrastructure/orchestration/write-pass-store.js +0 -46
  76. package/dist/infrastructure/protocols/cloud-sync.js +0 -207
  77. package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
  78. package/dist/infrastructure/protocols/mcp-server-impl.js +0 -2853
  79. package/dist/infrastructure/protocols/probe-port.js +0 -36
  80. package/dist/infrastructure/protocols/thread-phase.js +0 -105
  81. package/dist/infrastructure/protocols/ui-page-v2.js +0 -4418
  82. package/dist/infrastructure/protocols/ui-page.js +0 -2377
  83. package/dist/infrastructure/protocols/verify-page.js +0 -983
  84. package/dist/infrastructure/security/file-collection.js +0 -118
  85. package/dist/infrastructure/security/input-validator.js +0 -152
  86. package/dist/infrastructure/security/permissions-config.js +0 -234
  87. package/dist/infrastructure/security/trust-scorer.js +0 -657
  88. package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
  89. package/dist/infrastructure/storage/embeddings-path.js +0 -37
  90. package/dist/infrastructure/storage/project-thread-migration.js +0 -237
  91. package/dist/infrastructure/storage/project-thread-mirror.js +0 -524
  92. package/dist/infrastructure/storage/project-thread-reader.js +0 -172
  93. package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
  94. package/dist/infrastructure/storage/task-repository.js +0 -206
  95. package/dist/monitoring.js +0 -212
  96. package/dist/presentation/cli/commands/audit-command.js +0 -126
  97. package/dist/presentation/cli/commands/budget-command.js +0 -79
  98. package/dist/presentation/cli/commands/config-command.js +0 -125
  99. package/dist/presentation/cli/commands/context-source-command.js +0 -198
  100. package/dist/presentation/cli/commands/hook-command.js +0 -265
  101. package/dist/presentation/cli/commands/init-command.js +0 -578
  102. package/dist/presentation/cli/commands/memory-command.js +0 -1031
  103. package/dist/presentation/cli/commands/narrator-command.js +0 -101
  104. package/dist/presentation/cli/commands/permission-command.js +0 -116
  105. package/dist/presentation/cli/commands/protocols-builder.js +0 -254
  106. package/dist/presentation/cli/commands/protocols-command.js +0 -84
  107. package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
  108. package/dist/presentation/cli/commands/report-command.js +0 -387
  109. package/dist/presentation/cli/commands/scan-tool-command.js +0 -94
  110. package/dist/presentation/cli/commands/setup-command.js +0 -307
  111. package/dist/presentation/cli/commands/status-command.js +0 -661
  112. package/dist/presentation/cli/commands/thread-command.js +0 -288
  113. package/dist/presentation/cli/commands/trust-command.js +0 -306
  114. package/dist/presentation/cli/commands/verify-command.js +0 -233
  115. package/dist/presentation/cli/error-display.js +0 -336
  116. package/dist/presentation/cli/repl.js +0 -797
  117. package/dist/presentation/cli/task-deliverable.js +0 -127
  118. package/dist/testing/cassette.js +0 -125
  119. package/dist/testing/replay-adapter.js +0 -166
@@ -1,120 +0,0 @@
1
- /**
2
- * cassette-recorder · organic instrumentation for S2b of the capture-first plan.
3
- *
4
- * What it does:
5
- * When CODEHERE_RECORD_CASSETTES=1 is set, every adapter spawn writes the
6
- * raw stdout/stderr byte stream + exit to a JSONL cassette under
7
- * ~/.codehere/cassettes/<adapter>/<scenario>-<ISO>.jsonl. The schema matches
8
- * the locked L3b format (agent/src/testing/cassette.ts) so the same loader
9
- * + replay infrastructure works on captures from real production sessions.
10
- *
11
- * Why this exists:
12
- * Per chat 2026-05-23 (path 3 hybrid), we agreed cassettes accumulate from
13
- * the founder's normal use rather than from dedicated capture sessions
14
- * requiring fresh API keys. This module is the "accumulates" mechanism.
15
- *
16
- * Design choices:
17
- * - OFF by default. Zero overhead when the env var is unset. The 5 adapters
18
- * all call `startRecording()`, but it returns a no-op recorder if the
19
- * env var isn't set, so the per-chunk hooks become free function calls.
20
- * - Best-effort writes. A failed cassette never breaks the spawn path.
21
- * - No PII scrubbing here · the user is recording their own sessions on
22
- * their own machine. If we ever publish cassettes externally, scrubbing
23
- * belongs in an export tool, not in the recorder.
24
- * - One cassette per session (no append between sessions). Easier to reason
25
- * about lifecycle, easier to discard / re-run, easier to ship in a PR.
26
- */
27
- import { appendFileSync, mkdirSync } from 'fs';
28
- import { homedir } from 'os';
29
- import { dirname, join } from 'path';
30
- const ENV_FLAG = 'CODEHERE_RECORD_CASSETTES';
31
- const NULL_RECORDER = {
32
- enabled: false,
33
- path: '',
34
- onStdout() { },
35
- onStderr() { },
36
- onExit() { },
37
- };
38
- export function startRecording(opts) {
39
- if (process.env[ENV_FLAG] !== '1')
40
- return NULL_RECORDER;
41
- const outDir = opts.outDir ?? join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'cassettes', opts.adapter);
42
- const scenario = `organic-${opts.sessionId.slice(0, 8)}`;
43
- const stamp = new Date().toISOString().replace(/[:.]/g, '-');
44
- const path = join(outDir, `${scenario}-${stamp}.jsonl`);
45
- try {
46
- mkdirSync(dirname(path), { recursive: true });
47
- }
48
- catch {
49
- return NULL_RECORDER;
50
- }
51
- const start = Date.now();
52
- const header = {
53
- t: 0,
54
- meta: {
55
- adapter: opts.adapter,
56
- scenario,
57
- captured_at: new Date().toISOString(),
58
- binary_version: opts.binary_version ?? 'unknown',
59
- origin: 'real',
60
- prompt: opts.prompt,
61
- cwd_hint: hintCwd(opts.cwd),
62
- schema: 1,
63
- },
64
- };
65
- try {
66
- appendFileSync(path, JSON.stringify(header) + '\n', 'utf-8');
67
- }
68
- catch {
69
- return NULL_RECORDER;
70
- }
71
- let sealed = false;
72
- return {
73
- enabled: true,
74
- path,
75
- onStdout(chunk) {
76
- if (sealed)
77
- return;
78
- writeEvent(path, { t: Date.now() - start, stream: 'stdout', data: chunk });
79
- },
80
- onStderr(chunk) {
81
- if (sealed)
82
- return;
83
- writeEvent(path, { t: Date.now() - start, stream: 'stderr', data: chunk });
84
- },
85
- onExit(exit_code, signal) {
86
- if (sealed)
87
- return;
88
- sealed = true;
89
- writeEvent(path, { t: Date.now() - start, exit_code, signal });
90
- },
91
- };
92
- }
93
- /* ──────────────────────────────────────────────────────────────────────── */
94
- /* Internals */
95
- /* ──────────────────────────────────────────────────────────────────────── */
96
- function writeEvent(path, payload) {
97
- try {
98
- appendFileSync(path, JSON.stringify(payload) + '\n', 'utf-8');
99
- }
100
- catch {
101
- // best-effort · never break the spawn path
102
- }
103
- }
104
- function hintCwd(cwd) {
105
- if (!cwd)
106
- return '.';
107
- const home = homedir();
108
- if (cwd === home)
109
- return '~';
110
- if (cwd.startsWith(home + '/'))
111
- return '~/' + cwd.slice(home.length + 1);
112
- return cwd;
113
- }
114
- /** Probe used by tests + the future status command. */
115
- export function isRecordingEnabled() {
116
- return process.env[ENV_FLAG] === '1';
117
- }
118
- /** Returns the env flag name so callers can document it consistently. */
119
- export const RECORD_ENV_FLAG = ENV_FLAG;
120
- //# sourceMappingURL=cassette-recorder.js.map
@@ -1,478 +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
- // Runtime-error fingerprints → structured codehere:error events with recovery hints.
26
- // Matched against any stderr line or assistant-text content we see on the stream.
27
- const ERROR_PATTERNS = [
28
- {
29
- code: 'credit_low',
30
- regex: /credit balance is too low/i,
31
- message: 'Your Claude API account has no credits.',
32
- recovery: 'codehere tries to use your Claude Code subscription by default. If you are seeing this, ' +
33
- 'ANTHROPIC_API_KEY is still being used somehow. Unset it in your shell (e.g. remove from ~/.zshrc), ' +
34
- 'or top up at console.anthropic.com.',
35
- },
36
- {
37
- code: 'auth_invalid',
38
- regex: /invalid api key|authentication.*failed|not authenticated|401 unauth/i,
39
- message: 'Claude Code auth was rejected.',
40
- recovery: 'Run `claude` in a terminal, sign in, then retry the task.',
41
- },
42
- {
43
- code: 'rate_limit',
44
- regex: /rate.?limit(ed)?|429|too many requests/i,
45
- message: 'Claude rate-limited the request.',
46
- recovery: 'Wait a minute and retry. If this persists, check your Claude usage dashboard.',
47
- },
48
- ];
49
- function scanForKnownError(text) {
50
- for (const p of ERROR_PATTERNS) {
51
- if (p.regex.test(text))
52
- return { code: p.code, message: p.message, recovery: p.recovery };
53
- }
54
- return null;
55
- }
56
- // v0.7 MED-2 fix — dropped `--include-partial-messages` and
57
- // `--include-hook-events` from the headless flags. Per docs/contracts/claude-code.md
58
- // §7 the adapter doesn't consume partial-message deltas (only full assistant
59
- // blocks) and doesn't surface hook events in any UI. Both flags inflated
60
- // stream volume + token cost without benefit. If we later need either
61
- // (e.g., to wire a hook visualization in v0.8), re-add with explicit
62
- // parsing intent.
63
- const HEADLESS_FLAGS = [
64
- '--output-format',
65
- 'stream-json',
66
- '--verbose',
67
- // Pre-grant common tools so -p mode doesn't dead-end at interactive permission
68
- // prompts we cannot surface to the user. Safety is still enforced by codehere's
69
- // PreToolUse hook (F0) on every tool call — that's the real gate.
70
- '--allowedTools',
71
- 'Bash',
72
- 'Edit',
73
- 'Write',
74
- 'Read',
75
- 'Grep',
76
- 'Glob',
77
- 'MultiEdit',
78
- 'NotebookEdit',
79
- ];
80
- export function spawnClaudeCode(prompt, options = {}) {
81
- const sessionId = randomUUID();
82
- const startedAt = Date.now();
83
- const emitter = new EventEmitter();
84
- const buffer = [];
85
- let statusValue = 'running';
86
- let killRequested = false;
87
- // PM UX Layer 2+3 state. Shared helper modules; per-session instances.
88
- const cwd = options.cwd ?? process.cwd();
89
- // F3 Task 8 — inject project memory (AGENTS.md + lessons + repo_map slice)
90
- // into the prompt prefix unless the caller opted out. Pure, side-effect-
91
- // free; returns unchanged prompt + record=undefined when there's no
92
- // .codehere/ at cwd.
93
- const injected = applyMemoryPrefix(prompt, cwd, options);
94
- const finalPrompt = injected.prompt;
95
- const snapshot = snapshotCwd(cwd);
96
- const aggregator = new ProgressAggregator('claude-code');
97
- let stepsSnapshot = [];
98
- let artifactsSnapshot;
99
- let textAccum = ''; // accumulates assistant text + result summary for test-count parsing
100
- let usageSnapshot; // token accumulator (principle #7)
101
- let errorEmitted = false;
102
- const emit = (event) => {
103
- buffer.push(event);
104
- emitter.emit('event', event);
105
- // Layer 2: feed aggregator; on step change, emit synthetic snapshot event.
106
- if (aggregator.ingest(event)) {
107
- stepsSnapshot = aggregator.steps.map((s) => ({ ...s }));
108
- const snapEvt = { type: 'codehere:steps', timestamp: Date.now(), data: { steps: stepsSnapshot } };
109
- buffer.push(snapEvt);
110
- emitter.emit('event', snapEvt);
111
- }
112
- // Accumulate text for test-summary parsing (Layer 3).
113
- if (event.type === 'assistant') {
114
- const msg = event.data?.message;
115
- if (msg?.content) {
116
- for (const b of msg.content) {
117
- if (b.type === 'text' && b.text)
118
- textAccum += b.text + '\n';
119
- }
120
- }
121
- }
122
- else if (event.type === 'result') {
123
- const d = event.data;
124
- if (d?.result)
125
- textAccum += d.result + '\n';
126
- // Claude Code's stream-json `result` event is the final summary — carries
127
- // the full session's usage totals. One event per session; overwrite, don't sum.
128
- if (d?.usage) {
129
- usageSnapshot = {
130
- inputTokens: Number(d.usage.input_tokens ?? 0),
131
- outputTokens: Number(d.usage.output_tokens ?? 0),
132
- cacheReadTokens: d.usage.cache_read_input_tokens ? Number(d.usage.cache_read_input_tokens) : undefined,
133
- cacheCreationTokens: d.usage.cache_creation_input_tokens
134
- ? Number(d.usage.cache_creation_input_tokens)
135
- : undefined,
136
- };
137
- }
138
- }
139
- else if (event.type === 'user') {
140
- const msg = event.data?.message;
141
- if (msg?.content) {
142
- for (const b of msg.content) {
143
- if (b.type === 'tool_result') {
144
- const raw = Array.isArray(b.content)
145
- ? b.content.map((c) => c.text ?? '').join('')
146
- : String(b.content ?? '');
147
- textAccum += raw + '\n';
148
- }
149
- }
150
- }
151
- }
152
- // F1b: scan for known failure patterns once per session
153
- if (!errorEmitted) {
154
- const data = event.data;
155
- let text = '';
156
- if (event.type === 'stderr' && data && typeof data.line === 'string') {
157
- text = data.line;
158
- }
159
- else if (event.type === 'assistant' && data && typeof data === 'object') {
160
- const msg = data.message;
161
- if (msg?.content) {
162
- text = msg.content.map((b) => (b.type === 'text' ? (b.text ?? '') : '')).join(' ');
163
- }
164
- }
165
- else if (event.type === 'result' && data && typeof data.result === 'string') {
166
- text = String(data.result ?? '');
167
- }
168
- if (text) {
169
- const hit = scanForKnownError(text);
170
- if (hit) {
171
- errorEmitted = true;
172
- const errEvent = {
173
- type: 'codehere:error',
174
- timestamp: Date.now(),
175
- data: { code: hit.code, message: hit.message, recovery: hit.recovery },
176
- };
177
- buffer.push(errEvent);
178
- emitter.emit('event', errEvent);
179
- }
180
- }
181
- }
182
- };
183
- const preferSubscription = options.preferSubscription !== false;
184
- const spawnEnv = { ...process.env, ...(options.env ?? {}) };
185
- if (preferSubscription) {
186
- // Force keychain/OAuth path — matches what the user's interactive `claude` uses.
187
- delete spawnEnv.ANTHROPIC_API_KEY;
188
- delete spawnEnv.ANTHROPIC_AUTH_TOKEN;
189
- delete spawnEnv.CLAUDE_API_KEY;
190
- }
191
- let child;
192
- try {
193
- const sp = resolveAgentSpawn('claude-code', 'claude', [
194
- '-p',
195
- finalPrompt,
196
- ...HEADLESS_FLAGS,
197
- ...(options.extraArgs ?? []),
198
- ]);
199
- child = spawn(sp.command, sp.args, {
200
- cwd,
201
- env: spawnEnv,
202
- stdio: ['ignore', 'pipe', 'pipe'],
203
- });
204
- }
205
- catch (err) {
206
- // Recorder is short-circuit no-op when CODEHERE_RECORD_CASSETTES != '1'
207
- // so the spawn-failure path doesn't even pay the closure cost.
208
- statusValue = 'error';
209
- const msg = err instanceof Error ? err.message : String(err);
210
- // Emit synchronously so subscribers added immediately after see it
211
- queueMicrotask(() => emit({ type: 'error', timestamp: Date.now(), data: { message: msg } }));
212
- return {
213
- sessionId,
214
- startedAt,
215
- prompt,
216
- agent: 'claude-code',
217
- get status() {
218
- return statusValue;
219
- },
220
- get steps() {
221
- return stepsSnapshot;
222
- },
223
- get artifacts() {
224
- return artifactsSnapshot;
225
- },
226
- subscribe(handler) {
227
- for (const e of buffer)
228
- handler(e);
229
- emitter.on('event', handler);
230
- return () => emitter.off('event', handler);
231
- },
232
- kill() {
233
- /* already not running */
234
- },
235
- };
236
- }
237
- // ── Organic cassette recording (S2b · capture-first plan) ─────────────────
238
- // No-op unless CODEHERE_RECORD_CASSETTES=1 · zero overhead by default.
239
- // Captures the raw byte stream + exit so a future replay can reproduce
240
- // this exact session through the mock adapter without a live agent call.
241
- const recorder = startRecording({
242
- adapter: 'claude-code',
243
- sessionId,
244
- prompt: finalPrompt,
245
- cwd,
246
- });
247
- // ── stdout: newline-delimited JSON from `--output-format stream-json` ────────
248
- let stdoutBuf = '';
249
- // Iter 13 — capture the claude-internal session_id from the init event so
250
- // appendMessage can use `--resume <session-id>` (reliable) instead of `-c`
251
- // (which picks "most recent in cwd" and can collide with other concurrent
252
- // claude sessions OR with conversations containing thinking blocks the
253
- // headless API can't replay).
254
- let claudeSessionId;
255
- child.stdout?.on('data', (chunk) => {
256
- const text = chunk.toString('utf-8');
257
- recorder.onStdout(text);
258
- stdoutBuf += text;
259
- let idx;
260
- while ((idx = stdoutBuf.indexOf('\n')) >= 0) {
261
- const line = stdoutBuf.slice(0, idx).trim();
262
- stdoutBuf = stdoutBuf.slice(idx + 1);
263
- if (!line)
264
- continue;
265
- try {
266
- const data = JSON.parse(line);
267
- if (claudeSessionId === undefined &&
268
- data.type === 'system' &&
269
- data.subtype === 'init' &&
270
- typeof data.session_id === 'string') {
271
- claudeSessionId = data.session_id;
272
- }
273
- emit({ type: data.type ?? 'message', timestamp: Date.now(), data });
274
- }
275
- catch {
276
- // Non-JSON stdout line — preserve so callers can see raw output
277
- emit({ type: 'stdout', timestamp: Date.now(), data: { line } });
278
- }
279
- }
280
- });
281
- // ── stderr: Claude prints debug/errors here; forward raw ─────────────────────
282
- child.stderr?.on('data', (chunk) => {
283
- const text = chunk.toString('utf-8');
284
- recorder.onStderr(text);
285
- for (const line of text.split('\n')) {
286
- if (line.trim())
287
- emit({ type: 'stderr', timestamp: Date.now(), data: { line } });
288
- }
289
- });
290
- child.on('exit', (code, signal) => {
291
- recorder.onExit(code ?? -1, signal);
292
- statusValue =
293
- killRequested || signal !== null ? 'killed' : errorEmitted ? 'error' : code === 0 ? 'completed' : 'error';
294
- // Emit synthetic PM events BEFORE 'exit' so the client has time to render
295
- // them before its onmessage handler closes the SSE stream in response to exit.
296
- // (Client must close on exit to avoid leaking connections — but close() cancels
297
- // pending messages, so order matters.)
298
- // Layer 2: flip any still-active steps to done/failed so the UI stops spinning.
299
- if (aggregator.finalize(statusValue === 'completed' ? 'completed' : statusValue === 'killed' ? 'killed' : 'error')) {
300
- stepsSnapshot = aggregator.steps.map((s) => ({ ...s }));
301
- emit({ type: 'codehere:steps', timestamp: Date.now(), data: { steps: stepsSnapshot } });
302
- }
303
- // Layer 3: on clean completion, extract artifacts (git diff + trust + tests + tokens) and announce.
304
- if (statusValue === 'completed') {
305
- try {
306
- artifactsSnapshot = { ...extractArtifacts(snapshot, textAccum), usage: usageSnapshot };
307
- emit({ type: 'codehere:artifacts', timestamp: Date.now(), data: artifactsSnapshot });
308
- }
309
- catch {
310
- /* best-effort; don't break exit path */
311
- }
312
- }
313
- emit({
314
- type: 'exit',
315
- timestamp: Date.now(),
316
- data: { code, signal, status: statusValue, durationMs: Date.now() - startedAt },
317
- });
318
- });
319
- child.on('error', (err) => {
320
- statusValue = 'error';
321
- emit({ type: 'error', timestamp: Date.now(), data: { message: err.message } });
322
- });
323
- return {
324
- sessionId,
325
- startedAt,
326
- prompt,
327
- agent: 'claude-code',
328
- canAppendMessage: true,
329
- memoryInjection: injected.record,
330
- get status() {
331
- return statusValue;
332
- },
333
- get steps() {
334
- return stepsSnapshot;
335
- },
336
- get artifacts() {
337
- return artifactsSnapshot;
338
- },
339
- subscribe(handler) {
340
- for (const e of buffer)
341
- handler(e);
342
- emitter.on('event', handler);
343
- return () => emitter.off('event', handler);
344
- },
345
- kill() {
346
- if (statusValue !== 'running')
347
- return;
348
- killRequested = true;
349
- child.kill('SIGTERM');
350
- // Escalate if it doesn't die in 3s
351
- setTimeout(() => {
352
- if (statusValue === 'running') {
353
- try {
354
- child.kill('SIGKILL');
355
- }
356
- catch {
357
- /* already dead */
358
- }
359
- }
360
- }, 3000);
361
- },
362
- /**
363
- * Iter 12 (with iter-13 fix) — follow-up message to a completed session.
364
- *
365
- * Iter 12 used `claude -p "<text>" -c` which picks "most recent
366
- * conversation in current directory". Iter 13 dogfood proved this
367
- * fragile: in a cwd with multiple concurrent claude sessions OR with
368
- * conversations containing thinking blocks, -c either picks the wrong
369
- * session or hits an API-replay error ("Invalid signature in thinking
370
- * block"). Fix: capture the claude session_id from this session's init
371
- * event (above) and use `--resume <session-id>` for follow-ups —
372
- * unambiguous and replay-safe.
373
- *
374
- * Scope: still one-shot Q&A. No streaming. Returns assistant's response
375
- * text. If we never captured a session_id (init event never fired —
376
- * shouldn't happen in healthy claude runs), reject explicitly.
377
- */
378
- appendMessage(text) {
379
- const startedAt = Date.now();
380
- return new Promise((resolve, reject) => {
381
- if (!claudeSessionId) {
382
- reject(new Error('Cannot reply: original claude session_id was never captured (init event missed)'));
383
- return;
384
- }
385
- let stdout = '';
386
- let stderr = '';
387
- // Route follow-ups through the mock seam too — under CODEHERE_MOCK=1 this
388
- // hits the mock REPL, not the real binary. (Was a real-spend leak:
389
- // appendMessage spawned `claude` directly regardless of mock mode.)
390
- const sp2 = resolveAgentSpawn('claude-code', 'claude', ['-p', text, '--resume', claudeSessionId, ...HEADLESS_FLAGS], text);
391
- const followup = spawn(sp2.command, sp2.args, {
392
- cwd,
393
- env: spawnEnv,
394
- stdio: ['ignore', 'pipe', 'pipe'],
395
- });
396
- followup.on('error', (err) => reject(err));
397
- followup.stdout?.on('data', (chunk) => {
398
- stdout += chunk.toString('utf-8');
399
- });
400
- followup.stderr?.on('data', (chunk) => {
401
- stderr += chunk.toString('utf-8');
402
- });
403
- followup.on('exit', (code) => {
404
- if (code !== 0) {
405
- reject(new Error(`claude -c exited ${code}: ${stderr.slice(0, 200)}`));
406
- return;
407
- }
408
- // Parse stream-json — extract assistant message text from the
409
- // 'assistant' typed lines and the final 'result' line.
410
- const lines = stdout.split('\n').filter((l) => l.trim());
411
- const fragments = [];
412
- for (const line of lines) {
413
- try {
414
- const obj = JSON.parse(line);
415
- if (obj.type === 'assistant' && obj.message?.content) {
416
- for (const block of obj.message.content) {
417
- if (block.type === 'text' && block.text)
418
- fragments.push(block.text);
419
- }
420
- }
421
- else if (obj.type === 'result' && typeof obj.result === 'string') {
422
- fragments.push(obj.result);
423
- }
424
- }
425
- catch {
426
- // Non-JSON line — skip
427
- }
428
- }
429
- // Dedupe trivially: if 'result' contained the same text as the
430
- // last assistant block, take just one. Conservative: prefer 'result'
431
- // if present (it's the final summary), else join assistants.
432
- const result = fragments.length ? fragments[fragments.length - 1] : '(no response captured)';
433
- resolve({ result, durationMs: Date.now() - startedAt });
434
- });
435
- });
436
- },
437
- };
438
- }
439
- // ── AgentAdapter implementation ─────────────────────────────────────────────
440
- export const ClaudeCodeAdapter = {
441
- id: 'claude-code',
442
- label: 'Claude Code',
443
- 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.',
444
- processPattern: /(?:^|\/)claude(?:\s|$)/,
445
- isInstalled() {
446
- try {
447
- const out = execFileSync('which', ['claude'], { encoding: 'utf-8', timeout: 2000 });
448
- return out.trim().length > 0;
449
- }
450
- catch {
451
- return false;
452
- }
453
- },
454
- spawn(prompt, options) {
455
- return spawnClaudeCode(prompt, options);
456
- },
457
- checkReadiness() {
458
- const installed = this.isInstalled();
459
- if (!installed) {
460
- return {
461
- id: this.id,
462
- label: this.label,
463
- state: 'needs_install',
464
- installed: false,
465
- installHint: {
466
- command: 'npm install -g @anthropic-ai/claude-code',
467
- url: 'https://docs.claude.com/en/docs/claude-code/setup',
468
- },
469
- };
470
- }
471
- // Claude Code supports BOTH subscription (OAuth via keychain) and API key.
472
- // "Ready" if either path works. We can't verify subscription without
473
- // calling the API, so we treat any Claude Code install as ready by default —
474
- // runtime F1b error-fingerprints catch real auth failures after spawn.
475
- return { id: this.id, label: this.label, state: 'ready', installed: true };
476
- },
477
- };
478
- //# sourceMappingURL=claude-code-adapter.js.map