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.
- package/README.md +36 -192
- package/dist/application/use-cases/check-claimed-tests.js +81 -0
- package/dist/application/use-cases/detect-test-weakening.js +237 -0
- package/dist/application/use-cases/rerun-tests.js +110 -7
- package/dist/index.js +38 -1212
- package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -323
- package/dist/presentation/cli/commands/check-command.js +178 -23
- package/dist/presentation/cli/commands/help-command.js +21 -285
- package/dist/ui.js +5 -236
- package/dist/utils/version.js +0 -6
- package/package.json +9 -38
- package/dist/application/services/verdict-service.js +0 -154
- package/dist/application/use-cases/assess-cross-vendor.js +0 -126
- package/dist/application/use-cases/build-cross-vendor-verdict.js +0 -118
- package/dist/application/use-cases/domain-trust.js +0 -110
- package/dist/application/use-cases/export-aat.js +0 -107
- package/dist/application/use-cases/generate-acceptance-criteria.js +0 -223
- package/dist/application/use-cases/generate-final-report.js +0 -259
- package/dist/application/use-cases/generate-narration.js +0 -130
- package/dist/application/use-cases/generate-thread-title.js +0 -181
- package/dist/application/use-cases/narrator-prompt-template.js +0 -158
- package/dist/application/use-cases/plan-auto-review.js +0 -57
- package/dist/application/use-cases/reconcile-audit.js +0 -100
- package/dist/application/use-cases/search-sessions.js +0 -70
- package/dist/application/use-cases/should-auto-review.js +0 -71
- package/dist/application/use-cases/thread-export.js +0 -245
- package/dist/application/use-cases/verify-claim.js +0 -456
- package/dist/application/use-cases/write-pass.js +0 -277
- package/dist/domain/audit.js +0 -11
- package/dist/domain/services/codebase-detector.js +0 -85
- package/dist/domain/task.js +0 -91
- package/dist/error-handler.js +0 -139
- package/dist/infrastructure/completion/bash-completion.js +0 -29
- package/dist/infrastructure/completion/fish-completion.js +0 -17
- package/dist/infrastructure/completion/zsh-completion.js +0 -25
- package/dist/infrastructure/config/config-manager.js +0 -174
- package/dist/infrastructure/config/product-context.js +0 -49
- package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
- package/dist/infrastructure/context/adapters/figma-source.js +0 -198
- package/dist/infrastructure/context/adapters/github-issue-source.js +0 -160
- package/dist/infrastructure/context/adapters/linear-source.js +0 -190
- package/dist/infrastructure/context/context-resolver.js +0 -144
- package/dist/infrastructure/context/context-source.js +0 -114
- package/dist/infrastructure/context/github-api.js +0 -110
- package/dist/infrastructure/context/github-device-flow.js +0 -65
- package/dist/infrastructure/context/oauth-flow.js +0 -207
- package/dist/infrastructure/context/oauth-store.js +0 -122
- package/dist/infrastructure/context/registry-bootstrap.js +0 -95
- package/dist/infrastructure/context/write-pass-resolver.js +0 -106
- package/dist/infrastructure/cost/budget-config.js +0 -114
- package/dist/infrastructure/cost/cost-router.js +0 -97
- package/dist/infrastructure/cost/spend-aggregator.js +0 -222
- package/dist/infrastructure/cost/team-ledger.js +0 -355
- package/dist/infrastructure/discovery/discovery-log.js +0 -85
- package/dist/infrastructure/hooks/audit-tip-signature.js +0 -336
- package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -1018
- package/dist/infrastructure/hooks/hook-mode.js +0 -87
- package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -49
- package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
- package/dist/infrastructure/hooks/session-analyzer.js +0 -177
- package/dist/infrastructure/observability/execution-tracer.js +0 -227
- package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
- package/dist/infrastructure/orchestration/agent-installer.js +0 -101
- package/dist/infrastructure/orchestration/agents-config-seed.js +0 -68
- package/dist/infrastructure/orchestration/aider-adapter.js +0 -611
- package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
- package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -592
- package/dist/infrastructure/orchestration/codex-adapter.js +0 -574
- package/dist/infrastructure/orchestration/conductor-adapter.js +0 -416
- package/dist/infrastructure/orchestration/context-handoff.js +0 -162
- package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
- package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
- package/dist/infrastructure/orchestration/detect-ollama.js +0 -75
- package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
- package/dist/infrastructure/orchestration/failover-config.js +0 -67
- package/dist/infrastructure/orchestration/failover-policy.js +0 -106
- package/dist/infrastructure/orchestration/gemini-adapter.js +0 -663
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -689
- package/dist/infrastructure/orchestration/graphify-context.js +0 -209
- package/dist/infrastructure/orchestration/memory-injector.js +0 -380
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -388
- package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
- package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
- package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
- package/dist/infrastructure/orchestration/opencode-adapter.js +0 -580
- package/dist/infrastructure/orchestration/progress-aggregator.js +0 -906
- package/dist/infrastructure/orchestration/readiness.js +0 -103
- package/dist/infrastructure/orchestration/session-manager.js +0 -1590
- package/dist/infrastructure/orchestration/session-persistence.js +0 -394
- package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
- package/dist/infrastructure/orchestration/write-pass-store.js +0 -38
- package/dist/infrastructure/protocols/cloud-sync.js +0 -207
- package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
- package/dist/infrastructure/protocols/mcp-server-impl.js +0 -3935
- package/dist/infrastructure/protocols/mcp-tools.js +0 -401
- package/dist/infrastructure/protocols/probe-port.js +0 -36
- package/dist/infrastructure/protocols/thread-phase.js +0 -105
- package/dist/infrastructure/protocols/ui/app-script-artifacts.js +0 -121
- package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +0 -44
- package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +0 -56
- package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +0 -588
- package/dist/infrastructure/protocols/ui/app-script-context-sources.js +0 -168
- package/dist/infrastructure/protocols/ui/app-script-conversation.js +0 -67
- package/dist/infrastructure/protocols/ui/app-script-drawer.js +0 -115
- package/dist/infrastructure/protocols/ui/app-script-failover.js +0 -133
- package/dist/infrastructure/protocols/ui/app-script-github.js +0 -113
- package/dist/infrastructure/protocols/ui/app-script-observer.js +0 -104
- package/dist/infrastructure/protocols/ui/app-script-onboarding.js +0 -80
- package/dist/infrastructure/protocols/ui/app-script-overlay.js +0 -203
- package/dist/infrastructure/protocols/ui/app-script-recovery.js +0 -136
- package/dist/infrastructure/protocols/ui/app-script-render.js +0 -166
- package/dist/infrastructure/protocols/ui/app-script-share.js +0 -78
- package/dist/infrastructure/protocols/ui/app-script-sidebar.js +0 -194
- package/dist/infrastructure/protocols/ui/app-script-tour.js +0 -78
- package/dist/infrastructure/protocols/ui/app-script-wpass.js +0 -312
- package/dist/infrastructure/protocols/ui/app-script.js +0 -2998
- package/dist/infrastructure/protocols/ui/font-faces.js +0 -7
- package/dist/infrastructure/protocols/ui/styles.js +0 -2147
- package/dist/infrastructure/protocols/ui/tokens.js +0 -96
- package/dist/infrastructure/protocols/ui-page-v2.js +0 -320
- package/dist/infrastructure/protocols/verify-page.js +0 -1677
- package/dist/infrastructure/security/file-collection.js +0 -118
- package/dist/infrastructure/security/fix-code.js +0 -144
- package/dist/infrastructure/security/fix-secret.js +0 -89
- package/dist/infrastructure/security/permissions-config.js +0 -235
- package/dist/infrastructure/security/scan-types.js +0 -10
- package/dist/infrastructure/security/ship-scan.js +0 -195
- package/dist/infrastructure/security/trust-scorer.js +0 -724
- package/dist/infrastructure/security/vibe-checks.js +0 -99
- package/dist/infrastructure/storage/atomic-write.js +0 -86
- package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
- package/dist/infrastructure/storage/project-thread-migration.js +0 -237
- package/dist/infrastructure/storage/project-thread-mirror.js +0 -671
- package/dist/infrastructure/storage/project-thread-reader.js +0 -409
- package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
- package/dist/monitoring.js +0 -212
- package/dist/presentation/cli/commands/audit-command.js +0 -201
- package/dist/presentation/cli/commands/budget-command.js +0 -79
- package/dist/presentation/cli/commands/config-command.js +0 -125
- package/dist/presentation/cli/commands/context-source-command.js +0 -217
- package/dist/presentation/cli/commands/hook-command.js +0 -265
- package/dist/presentation/cli/commands/init-command.js +0 -590
- package/dist/presentation/cli/commands/memory-command.js +0 -1031
- package/dist/presentation/cli/commands/narrator-command.js +0 -101
- package/dist/presentation/cli/commands/permission-command.js +0 -105
- package/dist/presentation/cli/commands/protocols-builder.js +0 -254
- package/dist/presentation/cli/commands/protocols-command.js +0 -84
- package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
- package/dist/presentation/cli/commands/report-command.js +0 -403
- package/dist/presentation/cli/commands/scan-command.js +0 -52
- package/dist/presentation/cli/commands/scan-tool-command.js +0 -110
- package/dist/presentation/cli/commands/search-command.js +0 -51
- package/dist/presentation/cli/commands/setup-command.js +0 -330
- package/dist/presentation/cli/commands/status-command.js +0 -614
- package/dist/presentation/cli/commands/team-command.js +0 -256
- package/dist/presentation/cli/commands/thread-command.js +0 -370
- package/dist/presentation/cli/commands/trust-command.js +0 -328
- package/dist/presentation/cli/commands/verify-command.js +0 -298
- package/dist/presentation/cli/error-display.js +0 -328
- package/dist/presentation/cli/repl.js +0 -805
- package/dist/presentation/cli/task-deliverable.js +0 -153
- package/dist/testing/cassette.js +0 -125
- 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
|