codehere 0.7.1 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -13
- package/dist/application/services/verdict-service.js +154 -0
- package/dist/application/use-cases/assess-cross-vendor.js +126 -0
- package/dist/application/use-cases/build-cross-vendor-verdict.js +118 -0
- package/dist/application/use-cases/export-aat.js +107 -0
- package/dist/application/use-cases/generate-acceptance-criteria.js +223 -0
- package/dist/application/use-cases/generate-final-report.js +140 -15
- package/dist/application/use-cases/generate-narration.js +5 -3
- package/dist/application/use-cases/generate-thread-title.js +5 -2
- package/dist/application/use-cases/narrator-prompt-template.js +1 -1
- package/dist/application/use-cases/plan-auto-review.js +57 -0
- package/dist/application/use-cases/reconcile-audit.js +100 -0
- package/dist/application/use-cases/rerun-tests.js +226 -0
- package/dist/application/use-cases/search-sessions.js +70 -0
- package/dist/application/use-cases/should-auto-review.js +71 -0
- package/dist/application/use-cases/thread-export.js +90 -0
- package/dist/application/use-cases/verify-claim.js +187 -5
- package/dist/domain/audit.js +11 -0
- package/dist/domain/services/codebase-detector.js +0 -6
- package/dist/domain/task.js +91 -0
- package/dist/error-handler.js +3 -115
- package/dist/index.js +279 -31
- package/dist/infrastructure/completion/bash-completion.js +7 -3
- package/dist/infrastructure/completion/fish-completion.js +7 -12
- package/dist/infrastructure/completion/zsh-completion.js +7 -12
- package/dist/infrastructure/config/config-manager.js +4 -3
- package/dist/infrastructure/config/product-context.js +1 -19
- package/dist/infrastructure/context/adapters/figma-source.js +37 -2
- package/dist/infrastructure/context/adapters/github-issue-source.js +3 -2
- package/dist/infrastructure/context/github-api.js +110 -0
- package/dist/infrastructure/context/github-device-flow.js +65 -0
- package/dist/infrastructure/context/oauth-store.js +5 -2
- package/dist/infrastructure/context/registry-bootstrap.js +0 -4
- package/dist/infrastructure/cost/budget-config.js +4 -3
- package/dist/infrastructure/cost/cost-router.js +16 -23
- package/dist/infrastructure/cost/spend-aggregator.js +91 -16
- package/dist/infrastructure/cost/team-ledger.js +355 -0
- package/dist/infrastructure/discovery/discovery-log.js +85 -0
- package/dist/infrastructure/hooks/audit-tip-signature.js +336 -0
- package/dist/infrastructure/hooks/claude-sdk-hook.js +139 -3
- package/dist/infrastructure/hooks/hook-mode.js +20 -4
- package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +8 -2
- package/dist/infrastructure/hooks/pre-prompt-scan.js +3 -3
- package/dist/infrastructure/observability/execution-tracer.js +1 -1
- package/dist/infrastructure/orchestration/agents-config-seed.js +23 -11
- package/dist/infrastructure/orchestration/aider-adapter.js +188 -24
- package/dist/infrastructure/orchestration/artifacts-extractor.js +60 -2
- package/dist/infrastructure/orchestration/cassette-recorder.js +1 -1
- package/dist/infrastructure/orchestration/claude-code-adapter.js +117 -3
- package/dist/infrastructure/orchestration/codex-adapter.js +52 -16
- package/dist/infrastructure/orchestration/conductor-adapter.js +46 -7
- package/dist/infrastructure/orchestration/context-handoff.js +32 -14
- package/dist/infrastructure/orchestration/detect-ollama.js +75 -0
- package/dist/infrastructure/orchestration/failover-config.js +1 -11
- package/dist/infrastructure/orchestration/gemini-adapter.js +92 -12
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +134 -3
- package/dist/infrastructure/orchestration/graphify-context.js +14 -59
- package/dist/infrastructure/orchestration/memory-injector.js +4 -1
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +167 -9
- package/dist/infrastructure/orchestration/opencode-adapter.js +84 -14
- package/dist/infrastructure/orchestration/progress-aggregator.js +275 -23
- package/dist/infrastructure/orchestration/session-manager.js +593 -94
- package/dist/infrastructure/orchestration/session-persistence.js +177 -41
- package/dist/infrastructure/orchestration/thread-key-store.js +1 -1
- package/dist/infrastructure/orchestration/write-pass-store.js +0 -8
- package/dist/infrastructure/protocols/cloud-sync.js +2 -2
- package/dist/infrastructure/protocols/mcp-helpers.js +1 -1
- package/dist/infrastructure/protocols/mcp-server-impl.js +1498 -354
- package/dist/infrastructure/protocols/mcp-tools.js +401 -0
- package/dist/infrastructure/protocols/ui/app-script-artifacts.js +121 -0
- package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +44 -0
- package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +56 -0
- package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +588 -0
- package/dist/infrastructure/protocols/ui/app-script-context-sources.js +168 -0
- package/dist/infrastructure/protocols/ui/app-script-conversation.js +67 -0
- package/dist/infrastructure/protocols/ui/app-script-drawer.js +115 -0
- package/dist/infrastructure/protocols/ui/app-script-failover.js +133 -0
- package/dist/infrastructure/protocols/ui/app-script-github.js +113 -0
- package/dist/infrastructure/protocols/ui/app-script-observer.js +104 -0
- package/dist/infrastructure/protocols/ui/app-script-onboarding.js +80 -0
- package/dist/infrastructure/protocols/ui/app-script-overlay.js +203 -0
- package/dist/infrastructure/protocols/ui/app-script-recovery.js +136 -0
- package/dist/infrastructure/protocols/ui/app-script-render.js +166 -0
- package/dist/infrastructure/protocols/ui/app-script-share.js +78 -0
- package/dist/infrastructure/protocols/ui/app-script-sidebar.js +194 -0
- package/dist/infrastructure/protocols/ui/app-script-tour.js +78 -0
- package/dist/infrastructure/protocols/ui/app-script-wpass.js +312 -0
- package/dist/infrastructure/protocols/ui/app-script.js +2998 -0
- package/dist/infrastructure/protocols/ui/font-faces.js +7 -0
- package/dist/infrastructure/protocols/ui/styles.js +2147 -0
- package/dist/infrastructure/protocols/ui/tokens.js +96 -0
- package/dist/infrastructure/protocols/ui-page-v2.js +116 -4126
- package/dist/infrastructure/protocols/verify-page.js +884 -148
- package/dist/infrastructure/security/fix-code.js +144 -0
- package/dist/infrastructure/security/fix-secret.js +89 -0
- package/dist/infrastructure/security/permissions-config.js +3 -2
- package/dist/infrastructure/security/scan-types.js +10 -0
- package/dist/infrastructure/security/ship-scan.js +195 -0
- package/dist/infrastructure/security/trust-scorer.js +70 -3
- package/dist/infrastructure/security/vibe-checks.js +99 -0
- package/dist/infrastructure/storage/atomic-write.js +86 -0
- package/dist/infrastructure/storage/project-thread-mirror.js +151 -4
- package/dist/infrastructure/storage/project-thread-reader.js +262 -25
- package/dist/infrastructure/storage/project-thread-title-backfill.js +1 -1
- package/dist/monitoring.js +2 -2
- package/dist/presentation/cli/commands/audit-command.js +79 -4
- package/dist/presentation/cli/commands/budget-command.js +2 -2
- package/dist/presentation/cli/commands/check-command.js +96 -0
- package/dist/presentation/cli/commands/context-source-command.js +20 -1
- package/dist/presentation/cli/commands/help-command.js +5 -5
- package/dist/presentation/cli/commands/hook-command.js +5 -5
- package/dist/presentation/cli/commands/init-command.js +89 -66
- package/dist/presentation/cli/commands/memory-command.js +28 -28
- package/dist/presentation/cli/commands/permission-command.js +1 -12
- package/dist/presentation/cli/commands/protocols-builder.js +2 -2
- package/dist/presentation/cli/commands/protocols-command.js +4 -4
- package/dist/presentation/cli/commands/repo-map-builder.js +1 -1
- package/dist/presentation/cli/commands/report-command.js +48 -32
- package/dist/presentation/cli/commands/scan-command.js +52 -0
- package/dist/presentation/cli/commands/scan-tool-command.js +24 -8
- package/dist/presentation/cli/commands/search-command.js +51 -0
- package/dist/presentation/cli/commands/setup-command.js +31 -8
- package/dist/presentation/cli/commands/status-command.js +39 -86
- package/dist/presentation/cli/commands/team-command.js +256 -0
- package/dist/presentation/cli/commands/thread-command.js +87 -5
- package/dist/presentation/cli/commands/trust-command.js +53 -31
- package/dist/presentation/cli/commands/verify-command.js +74 -9
- package/dist/presentation/cli/error-display.js +0 -8
- package/dist/presentation/cli/repl.js +15 -7
- package/dist/presentation/cli/task-deliverable.js +36 -10
- package/dist/testing/replay-adapter.js +0 -19
- package/dist/ui.js +51 -239
- package/dist/utils/version.js +0 -6
- package/package.json +30 -10
- package/dist/domain/entities/task.js +0 -61
- package/dist/infrastructure/protocols/ui-page.js +0 -2377
- package/dist/infrastructure/security/input-validator.js +0 -152
- package/dist/infrastructure/storage/embeddings-path.js +0 -37
- package/dist/infrastructure/storage/task-repository.js +0 -206
|
@@ -22,6 +22,7 @@ import { randomUUID } from 'crypto';
|
|
|
22
22
|
import { ProgressAggregator } from './progress-aggregator.js';
|
|
23
23
|
import { snapshotCwd, extractArtifacts } from './artifacts-extractor.js';
|
|
24
24
|
import { applyMemoryPrefix } from './memory-injector.js';
|
|
25
|
+
import { mirrorToolCallToAudit } from '../hooks/mirror-tool-call-to-audit.js';
|
|
25
26
|
// Runtime-error fingerprints → structured codehere:error events with recovery hints.
|
|
26
27
|
// Matched against any stderr line or assistant-text content we see on the stream.
|
|
27
28
|
const ERROR_PATTERNS = [
|
|
@@ -76,7 +77,78 @@ const HEADLESS_FLAGS = [
|
|
|
76
77
|
'Glob',
|
|
77
78
|
'MultiEdit',
|
|
78
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',
|
|
79
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
|
+
}
|
|
80
152
|
export function spawnClaudeCode(prompt, options = {}) {
|
|
81
153
|
const sessionId = randomUUID();
|
|
82
154
|
const startedAt = Date.now();
|
|
@@ -98,6 +170,7 @@ export function spawnClaudeCode(prompt, options = {}) {
|
|
|
98
170
|
let artifactsSnapshot;
|
|
99
171
|
let textAccum = ''; // accumulates assistant text + result summary for test-count parsing
|
|
100
172
|
let usageSnapshot; // token accumulator (principle #7)
|
|
173
|
+
let modelSeen; // real model id from assistant events → per-model cost
|
|
101
174
|
let errorEmitted = false;
|
|
102
175
|
const emit = (event) => {
|
|
103
176
|
buffer.push(event);
|
|
@@ -109,13 +182,44 @@ export function spawnClaudeCode(prompt, options = {}) {
|
|
|
109
182
|
buffer.push(snapEvt);
|
|
110
183
|
emitter.emit('event', snapEvt);
|
|
111
184
|
}
|
|
112
|
-
// Accumulate text for test-summary parsing (Layer 3)
|
|
185
|
+
// Accumulate text for test-summary parsing (Layer 3) + mirror tool calls to
|
|
186
|
+
// the hash-chained audit.
|
|
113
187
|
if (event.type === 'assistant') {
|
|
114
188
|
const msg = event.data?.message;
|
|
189
|
+
if (!modelSeen && msg?.model)
|
|
190
|
+
modelSeen = msg.model; // capture the real model for per-model pricing
|
|
115
191
|
if (msg?.content) {
|
|
116
192
|
for (const b of msg.content) {
|
|
117
193
|
if (b.type === 'text' && b.text)
|
|
118
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
|
+
}
|
|
119
223
|
}
|
|
120
224
|
}
|
|
121
225
|
}
|
|
@@ -133,6 +237,7 @@ export function spawnClaudeCode(prompt, options = {}) {
|
|
|
133
237
|
cacheCreationTokens: d.usage.cache_creation_input_tokens
|
|
134
238
|
? Number(d.usage.cache_creation_input_tokens)
|
|
135
239
|
: undefined,
|
|
240
|
+
...(modelSeen ? { model: modelSeen } : {}),
|
|
136
241
|
};
|
|
137
242
|
}
|
|
138
243
|
}
|
|
@@ -327,6 +432,11 @@ export function spawnClaudeCode(prompt, options = {}) {
|
|
|
327
432
|
agent: 'claude-code',
|
|
328
433
|
canAppendMessage: true,
|
|
329
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
|
+
},
|
|
330
440
|
get status() {
|
|
331
441
|
return statusValue;
|
|
332
442
|
},
|
|
@@ -405,13 +515,17 @@ export function spawnClaudeCode(prompt, options = {}) {
|
|
|
405
515
|
reject(new Error(`claude -c exited ${code}: ${stderr.slice(0, 200)}`));
|
|
406
516
|
return;
|
|
407
517
|
}
|
|
408
|
-
// Parse stream-json
|
|
409
|
-
//
|
|
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.
|
|
410
523
|
const lines = stdout.split('\n').filter((l) => l.trim());
|
|
411
524
|
const fragments = [];
|
|
412
525
|
for (const line of lines) {
|
|
413
526
|
try {
|
|
414
527
|
const obj = JSON.parse(line);
|
|
528
|
+
emit({ type: obj.type ?? 'message', timestamp: Date.now(), data: obj });
|
|
415
529
|
if (obj.type === 'assistant' && obj.message?.content) {
|
|
416
530
|
for (const block of obj.message.content) {
|
|
417
531
|
if (block.type === 'text' && block.text)
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
import { spawn, execFileSync } from 'child_process';
|
|
17
17
|
import { EventEmitter } from 'events';
|
|
18
18
|
import { randomUUID } from 'crypto';
|
|
19
|
+
import { existsSync } from 'fs';
|
|
20
|
+
import { homedir } from 'os';
|
|
21
|
+
import { join } from 'path';
|
|
19
22
|
import { startRecording } from './cassette-recorder.js';
|
|
20
23
|
import { ProgressAggregator } from './progress-aggregator.js';
|
|
21
24
|
import { snapshotCwd, extractArtifacts } from './artifacts-extractor.js';
|
|
@@ -108,7 +111,7 @@ export function spawnCodex(prompt, options = {}) {
|
|
|
108
111
|
// - mcp_tool_call (MCP servers) — NEW, mirrored as Tool with server prefix
|
|
109
112
|
// - collab_tool_call (SUBAGENT FAN-OUT, multi-agent v2 GA 2026-05-18) — NEW
|
|
110
113
|
// · spawn_agent / send_input / wait / close_agent / resume_agent
|
|
111
|
-
// · mirrored to audit as "Subagent" toolName so
|
|
114
|
+
// · mirrored to audit as "Subagent" toolName so the audit feed shows the
|
|
112
115
|
// child-spawn activity. Without this, multi-agent Codex tasks fall
|
|
113
116
|
// silently through (audit + cost claim breach per CRIT-1).
|
|
114
117
|
// · Token usage of children remains UNDER-REPORTED in this fix —
|
|
@@ -172,7 +175,7 @@ export function spawnCodex(prompt, options = {}) {
|
|
|
172
175
|
}
|
|
173
176
|
else if (item.type === 'collab_tool_call') {
|
|
174
177
|
// SUBAGENT FAN-OUT — the v0.7 fix that closes CRIT-1's audit gap.
|
|
175
|
-
// Mirror every collab call to the audit log so
|
|
178
|
+
// Mirror every collab call to the audit log so the audit feed shows the
|
|
176
179
|
// subagent activity. Tool naming convention: "Subagent.<tool>".
|
|
177
180
|
const collabTool = String(item.tool ?? 'collab');
|
|
178
181
|
const receivers = (item.receiver_thread_ids ?? []).join(',');
|
|
@@ -239,17 +242,21 @@ export function spawnCodex(prompt, options = {}) {
|
|
|
239
242
|
const errObj = event.data?.error;
|
|
240
243
|
const message = String(errObj?.message ?? '');
|
|
241
244
|
if (message) {
|
|
245
|
+
errorEmitted = true;
|
|
246
|
+
// A recognized error gets codehere's curated message + recovery. An
|
|
247
|
+
// UNRECOGNIZED one (e.g. ChatGPT-plan "You've hit your usage limit — try
|
|
248
|
+
// again Jul 14") must NOT be swallowed: surface codex's REAL words so the
|
|
249
|
+
// user sees the actual, actionable reason instead of a generic guess.
|
|
242
250
|
const hit = scanForKnownError(message);
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
251
|
+
const errEvent = {
|
|
252
|
+
type: 'codehere:error',
|
|
253
|
+
timestamp: Date.now(),
|
|
254
|
+
data: hit
|
|
255
|
+
? { code: hit.code, message: hit.message, recovery: hit.recovery }
|
|
256
|
+
: { code: 'agent_error', message, recovery: '' },
|
|
257
|
+
};
|
|
258
|
+
buffer.push(errEvent);
|
|
259
|
+
emitter.emit('event', errEvent);
|
|
253
260
|
}
|
|
254
261
|
}
|
|
255
262
|
if (!errorEmitted) {
|
|
@@ -458,7 +465,15 @@ export function spawnCodex(prompt, options = {}) {
|
|
|
458
465
|
// codex exec resume <SESSION_ID> - → reads prompt from stdin
|
|
459
466
|
// Route follow-ups through the mock seam too — under CODEHERE_MOCK=1 this
|
|
460
467
|
// hits the mock REPL, not real codex. (Was a real-spend leak.)
|
|
461
|
-
const sp2 = resolveAgentSpawn('codex', 'codex', [
|
|
468
|
+
const sp2 = resolveAgentSpawn('codex', 'codex', [
|
|
469
|
+
'exec',
|
|
470
|
+
'--json',
|
|
471
|
+
'--skip-git-repo-check',
|
|
472
|
+
'--dangerously-bypass-approvals-and-sandbox',
|
|
473
|
+
'resume',
|
|
474
|
+
codexThreadId,
|
|
475
|
+
'-',
|
|
476
|
+
], text);
|
|
462
477
|
const followup = spawn(sp2.command, sp2.args, {
|
|
463
478
|
cwd,
|
|
464
479
|
env: spawnEnv,
|
|
@@ -529,9 +544,30 @@ export const CodexAdapter = {
|
|
|
529
544
|
installHint: { command: 'npm install -g @openai/codex', url: 'https://github.com/openai/codex-cli' },
|
|
530
545
|
};
|
|
531
546
|
}
|
|
532
|
-
// Codex supports
|
|
533
|
-
//
|
|
534
|
-
//
|
|
547
|
+
// Codex supports two auth paths, both detectable fs/env-only (no spawn —
|
|
548
|
+
// same honesty fix gemini/opencode got May 1: returning 'ready' on install
|
|
549
|
+
// alone sent users into a ~40s spawn that errored "not logged in" with no
|
|
550
|
+
// next step):
|
|
551
|
+
// file: $CODEX_HOME/auth.json (default ~/.codex/auth.json, mode 600) —
|
|
552
|
+
// written by `codex login` (ChatGPT account OAuth) or a stored key
|
|
553
|
+
// env: OPENAI_API_KEY (billed API path)
|
|
554
|
+
const codexHome = process.env.CODEX_HOME || join(homedir(), '.codex');
|
|
555
|
+
const codexAuthPath = join(codexHome, 'auth.json');
|
|
556
|
+
const hasFileAuth = existsSync(codexAuthPath);
|
|
557
|
+
const hasEnvAuth = !!process.env.OPENAI_API_KEY;
|
|
558
|
+
if (!hasFileAuth && !hasEnvAuth) {
|
|
559
|
+
return {
|
|
560
|
+
id: this.id,
|
|
561
|
+
label: this.label,
|
|
562
|
+
state: 'needs_auth',
|
|
563
|
+
installed: true,
|
|
564
|
+
authHint: {
|
|
565
|
+
command: 'codex login # ChatGPT account (Plus/Pro) — no per-token billing',
|
|
566
|
+
url: 'https://github.com/openai/codex#authentication',
|
|
567
|
+
detail: `Codex found on PATH but no auth detected (${codexAuthPath} missing and no OPENAI_API_KEY). Cheapest path: \`codex login\` with a ChatGPT Plus/Pro account (flat, no per-token charge). Or set OPENAI_API_KEY for the billed API.`,
|
|
568
|
+
},
|
|
569
|
+
};
|
|
570
|
+
}
|
|
535
571
|
return { id: this.id, label: this.label, state: 'ready', installed: true };
|
|
536
572
|
},
|
|
537
573
|
};
|
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
*/
|
|
37
37
|
import { EventEmitter } from 'events';
|
|
38
38
|
import { randomUUID } from 'crypto';
|
|
39
|
+
import { loadBudget, isOverBudget } from '../cost/budget-config.js';
|
|
40
|
+
import { computeSpendToday, estimateCostUsd } from '../cost/spend-aggregator.js';
|
|
39
41
|
const ROUTING_RULES = [
|
|
40
42
|
{
|
|
41
43
|
triggers: ['review', 'audit', 'explain', 'understand', 'analyze', 'analyse', 'why', 'what does'],
|
|
@@ -235,19 +237,49 @@ function spawnConductor(prompt, options, lookup) {
|
|
|
235
237
|
const emitter = new EventEmitter();
|
|
236
238
|
const buffer = [];
|
|
237
239
|
let statusValue = 'running';
|
|
238
|
-
// Look up
|
|
239
|
-
|
|
240
|
-
|
|
240
|
+
// Look up usable specialists (excluding Conductor itself to avoid recursion).
|
|
241
|
+
// READY, not merely installed — user-#1 dogfood (2026-06-07): Auto routed a
|
|
242
|
+
// task to aider whose readiness already said billed_key_required; it died on
|
|
243
|
+
// its startup banner and the run landed as a green "completed" that did
|
|
244
|
+
// nothing. Mock mode keeps the installed-only pool (mock spawns never hit
|
|
245
|
+
// real vendors; CI has no real CLIs to be "ready").
|
|
246
|
+
const conductorMockMode = process.env.CODEHERE_MOCK === '1' || process.env.CODEHERE_MOCK === 'true';
|
|
247
|
+
const installedAgents = lookup().filter((a) => a.id !== 'conductor' && a.isInstalled());
|
|
248
|
+
const installedAgentIds = installedAgents
|
|
249
|
+
.filter((a) => conductorMockMode || !a.checkReadiness || a.checkReadiness().state === 'ready')
|
|
241
250
|
.map((a) => a.id);
|
|
242
|
-
|
|
243
|
-
//
|
|
251
|
+
// Cost-bounded fail-over (Phase 2 Pillar 2 · Ega-approved 2026-06-08): when
|
|
252
|
+
// today's spend has hit the daily budget, conductor routes to a FREE specialist
|
|
253
|
+
// (gemini/opencode) instead of burning more — the cost-bounded promise. Explicit
|
|
254
|
+
// paid-agent picks are refused upstream (429); conductor, the auto choice, fails
|
|
255
|
+
// over HERE, observed in the routing event. If no free agent is ready it falls
|
|
256
|
+
// through to the honest no-eligible-agent error below — it NEVER spawns a paid
|
|
257
|
+
// agent over budget.
|
|
258
|
+
const budgetCfg = loadBudget();
|
|
259
|
+
const budgetBounded = budgetCfg.dailyUsd !== null && isOverBudget(computeSpendToday().totalUsd, budgetCfg);
|
|
260
|
+
const eligibleAgentIds = budgetBounded
|
|
261
|
+
? installedAgentIds.filter((id) => estimateCostUsd(id, { inputTokens: 1, outputTokens: 1 }) === 0)
|
|
262
|
+
: installedAgentIds;
|
|
263
|
+
const decision = pickSpecialist(prompt, eligibleAgentIds);
|
|
264
|
+
// If the pool is empty, emit an error event and return a non-running session.
|
|
265
|
+
// Two distinct causes deserve two distinct messages: nothing installed vs
|
|
266
|
+
// installed-but-not-ready (e.g. every vendor unbilled/unauthed — telling that
|
|
267
|
+
// user to "install" a vendor they already have would misdirect them).
|
|
244
268
|
if (!decision) {
|
|
245
269
|
statusValue = 'error';
|
|
246
270
|
const errEvent = {
|
|
247
271
|
type: 'error',
|
|
248
272
|
timestamp: Date.now(),
|
|
249
273
|
data: {
|
|
250
|
-
message:
|
|
274
|
+
message: budgetBounded
|
|
275
|
+
? `Daily budget reached and no FREE agent (gemini, opencode) is ready to fail over to. ` +
|
|
276
|
+
`Raise the cap (\`codehere budget set <amount>\`) or set up a free agent (e.g. \`export GEMINI_API_KEY=…\` — free tier), then retry. ` +
|
|
277
|
+
`codehere won't spend over your budget.`
|
|
278
|
+
: installedAgents.length === 0
|
|
279
|
+
? 'Conductor has no specialists to delegate to. Install at least one of: Claude Code, Codex, Aider, OpenCode.'
|
|
280
|
+
: 'Conductor found installed agents, but none are ready to run (' +
|
|
281
|
+
installedAgents.map((a) => a.id).join(', ') +
|
|
282
|
+
'). Run `codehere health` to see what each one needs — usually an API key or billing.',
|
|
251
283
|
},
|
|
252
284
|
};
|
|
253
285
|
buffer.push(errEvent);
|
|
@@ -261,8 +293,10 @@ function spawnConductor(prompt, options, lookup) {
|
|
|
261
293
|
data: {
|
|
262
294
|
conductorSessionId: sessionId,
|
|
263
295
|
delegatedTo: decision.agentId,
|
|
264
|
-
|
|
296
|
+
// Surface the cost-bounded fail-over so it's never a silent vendor switch.
|
|
297
|
+
reason: budgetBounded ? `daily budget reached · routed to a free agent · ${decision.reason}` : decision.reason,
|
|
265
298
|
promptExcerpt: prompt.slice(0, 200),
|
|
299
|
+
...(budgetBounded ? { budgetBounded: true } : {}),
|
|
266
300
|
},
|
|
267
301
|
};
|
|
268
302
|
// Buffer replay on subscribe() handles delivery to the first subscriber. Do
|
|
@@ -320,6 +354,11 @@ function spawnConductor(prompt, options, lookup) {
|
|
|
320
354
|
startedAt,
|
|
321
355
|
prompt,
|
|
322
356
|
agent: 'conductor',
|
|
357
|
+
// 1.3 · the specialist's runtime sessionId IS the id the audit hook keys
|
|
358
|
+
// this task's tool calls by. Persisting it lets cross-vendor attribution
|
|
359
|
+
// join the chain for conductor-routed (Auto) tasks — without it, every such
|
|
360
|
+
// task reads "couldn't attribute" even though its writes are in the chain.
|
|
361
|
+
delegateSessionId: specialistSession.sessionId,
|
|
323
362
|
get status() {
|
|
324
363
|
return statusValue;
|
|
325
364
|
},
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Protocol:
|
|
10
10
|
* 1. loadHandoffContext(cwd, { fromSessionId?, sessionId?, maxAgeMs? })
|
|
11
11
|
* — scans .codehere/context-packs.jsonl for the most relevant pack
|
|
12
|
-
* 2. enrichPromptWithHandoff(prompt, cwd
|
|
12
|
+
* 2. enrichPromptWithHandoff(prompt, handoff) — 2-arg (LOW #9: this header used to claim a cwd arg the signature never had)
|
|
13
13
|
* — merges the handoff context into a prompt string
|
|
14
14
|
* 3. session handoff is automatic during failover (wired in session-manager)
|
|
15
15
|
*
|
|
@@ -102,6 +102,37 @@ export function enrichPromptWithHandoff(prompt, handoff) {
|
|
|
102
102
|
prompt,
|
|
103
103
|
].join('\n');
|
|
104
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Prepend a conversational hand-off preamble so a DIFFERENT agent picking up an
|
|
107
|
+
* open thread actually receives what the prior agent was asked + what it did —
|
|
108
|
+
* not a blind restart. This is the cross-agent hand-off the room promises
|
|
109
|
+
* ("any agent picks up where the last left off"). The summary it injects is the
|
|
110
|
+
* SAME one the user sees in the room hand-off render, so the agent and the human
|
|
111
|
+
* read one truth. Pure: the caller looks up the parent turn; this just formats.
|
|
112
|
+
* Returns the prompt unchanged when there's no usable parent context.
|
|
113
|
+
*/
|
|
114
|
+
export function enrichPromptWithThreadHandoff(prompt, parent) {
|
|
115
|
+
if (!parent || !parent.agent)
|
|
116
|
+
return prompt;
|
|
117
|
+
const cut = (s, n) => {
|
|
118
|
+
const t = String(s ?? '')
|
|
119
|
+
.trim()
|
|
120
|
+
.replace(/\s+/g, ' ');
|
|
121
|
+
return t.length > n ? t.slice(0, n - 1) + '…' : t;
|
|
122
|
+
};
|
|
123
|
+
const askedLine = parent.prompt ? `They were asked: "${cut(parent.prompt, 300)}"` : null;
|
|
124
|
+
const didLine = cut(parent.summary, 600)
|
|
125
|
+
? `What they did: ${cut(parent.summary, 600)}`
|
|
126
|
+
: 'What they did: (no summary was recorded — inspect the files they touched before continuing)';
|
|
127
|
+
return [
|
|
128
|
+
`[Continuing a thread — the agent "${parent.agent}" worked on this project immediately before you.]`,
|
|
129
|
+
...(askedLine ? [askedLine] : []),
|
|
130
|
+
didLine,
|
|
131
|
+
`Build on that work — do not redo it. Handle the request below as the next step in the same thread.`,
|
|
132
|
+
`---`,
|
|
133
|
+
prompt,
|
|
134
|
+
].join('\n');
|
|
135
|
+
}
|
|
105
136
|
// ── Failover handoff (P2 integration point) ────────────────────────────────────
|
|
106
137
|
/**
|
|
107
138
|
* Build a handoff context for a failover target.
|
|
@@ -128,17 +159,4 @@ export function buildFailoverHandoff(cwd, { failedAgent, failedPrompt }) {
|
|
|
128
159
|
status: 'failed',
|
|
129
160
|
};
|
|
130
161
|
}
|
|
131
|
-
// ── Handoff summary (for UI / API) ─────────────────────────────────────────────
|
|
132
|
-
/**
|
|
133
|
-
* Generate a human-readable handoff summary for the dashboard.
|
|
134
|
-
*/
|
|
135
|
-
export function formatHandoffSummary(handoff) {
|
|
136
|
-
const ageMs = Date.now() - handoff.timestamp;
|
|
137
|
-
const ageStr = ageMs < 60_000
|
|
138
|
-
? `${Math.round(ageMs / 1000)}s ago`
|
|
139
|
-
: ageMs < 3600_000
|
|
140
|
-
? `${Math.round(ageMs / 60_000)}m ago`
|
|
141
|
-
: `${Math.round(ageMs / 3600_000)}h ago`;
|
|
142
|
-
return `Previous session (${handoff.agent}) completed ${ageStr}. ${handoff.nodeCount} nodes, ${handoff.edgeCount} edges in workspace graph.`;
|
|
143
|
-
}
|
|
144
162
|
//# sourceMappingURL=context-handoff.js.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect a local Ollama install with at least one model — so codehere can offer
|
|
3
|
+
* a RELIABLE, FREE, LOCAL second vendor (aider → ollama) instead of depending on
|
|
4
|
+
* flaky free tiers or the user's cloud billing. This is the unlock for the
|
|
5
|
+
* cross-vendor wedge (V2) on a machine that has Ollama: a local model that
|
|
6
|
+
* completes real coding tasks with no rate limits and no key.
|
|
7
|
+
*
|
|
8
|
+
* Cheap + cached: `ollama list` is shelled at most once per TTL window; if Ollama
|
|
9
|
+
* isn't installed/running the call fails fast and we cache "not available", so the
|
|
10
|
+
* common no-Ollama path costs ~one bounded spawn every 30s, never per task.
|
|
11
|
+
*/
|
|
12
|
+
import { execFileSync } from 'node:child_process';
|
|
13
|
+
let cache;
|
|
14
|
+
let cachedAt = 0;
|
|
15
|
+
const TTL_MS = 30_000;
|
|
16
|
+
/** Embedding / non-chat models aider can't drive as a coder. Routing to one
|
|
17
|
+
* would fail silently, so they're never auto-picked (self-audit 2026-06-09). */
|
|
18
|
+
const NON_CHAT = /embed|rerank|bge|nomic|minilm|e5-|gte-|clip|whisper|moondream|llava-phi/i;
|
|
19
|
+
/** Pick the model to run: an explicit CODEHERE_OLLAMA_MODEL override if it's
|
|
20
|
+
* actually installed, else the first GENERATIVE model. We don't guess a "best"
|
|
21
|
+
* coder (a /coder/ bias picked an 18GB model — slow); we only EXCLUDE models
|
|
22
|
+
* aider can't use. The user knows their hardware — CODEHERE_OLLAMA_MODEL is the
|
|
23
|
+
* knob (refuter + self-audit 2026-06-09). */
|
|
24
|
+
function pickModel(models) {
|
|
25
|
+
if (models.length === 0)
|
|
26
|
+
return undefined;
|
|
27
|
+
const override = process.env.CODEHERE_OLLAMA_MODEL;
|
|
28
|
+
if (override && models.includes(override))
|
|
29
|
+
return override; // explicit wins, even if "non-chat"
|
|
30
|
+
// :cloud tags are ollama's CLOUD-proxied models — no local weights, billed
|
|
31
|
+
// account required. Auto-picking one breaks this module's whole promise
|
|
32
|
+
// (reliable, free, local) with "couldn't reach the model provider" — hit
|
|
33
|
+
// live 2026-07-02 when glm-5.2:cloud was first in `ollama list`. Explicit
|
|
34
|
+
// override above still allows them; auto-pick never does.
|
|
35
|
+
return models.find((m) => !NON_CHAT.test(m) && !/:cloud$/i.test(m)) ?? undefined;
|
|
36
|
+
}
|
|
37
|
+
/** Detect a usable local Ollama. Never throws. */
|
|
38
|
+
export function detectOllama() {
|
|
39
|
+
const now = Date.now();
|
|
40
|
+
if (cache && now - cachedAt < TTL_MS)
|
|
41
|
+
return cache;
|
|
42
|
+
let info = { available: false };
|
|
43
|
+
try {
|
|
44
|
+
// Short timeout: this is a SYNC call and readiness runs it on the daemon's
|
|
45
|
+
// event loop (the UI polls readiness), so a HUNG ollama must not block the
|
|
46
|
+
// daemon for long. 700ms once per TTL window is a tolerable worst case; the
|
|
47
|
+
// common (fast or absent) paths return in ~20ms (refuter 2026-06-09).
|
|
48
|
+
const out = execFileSync('ollama', ['list'], {
|
|
49
|
+
timeout: 700,
|
|
50
|
+
encoding: 'utf8',
|
|
51
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
52
|
+
});
|
|
53
|
+
// `ollama list` prints a header row then "NAME ID SIZE MODIFIED" rows.
|
|
54
|
+
const models = out
|
|
55
|
+
.split('\n')
|
|
56
|
+
.slice(1)
|
|
57
|
+
.map((l) => l.trim().split(/\s+/)[0])
|
|
58
|
+
.filter((m) => m && m !== 'NAME');
|
|
59
|
+
const model = pickModel(models);
|
|
60
|
+
if (model)
|
|
61
|
+
info = { available: true, model };
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
/* ollama not installed / not serving → not available */
|
|
65
|
+
}
|
|
66
|
+
cache = info;
|
|
67
|
+
cachedAt = now;
|
|
68
|
+
return info;
|
|
69
|
+
}
|
|
70
|
+
/** @internal test seam — clear the memoized detection. */
|
|
71
|
+
export function __resetOllamaCache() {
|
|
72
|
+
cache = undefined;
|
|
73
|
+
cachedAt = 0;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=detect-ollama.js.map
|
|
@@ -23,7 +23,7 @@ import { readFileSync, existsSync } from 'fs';
|
|
|
23
23
|
import { homedir } from 'os';
|
|
24
24
|
import { join } from 'path';
|
|
25
25
|
import { validateFailoverChain } from './failover-policy.js';
|
|
26
|
-
const GLOBAL_CONFIG_PATH = join(homedir(), '.codehere', 'config.json');
|
|
26
|
+
const GLOBAL_CONFIG_PATH = join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'config.json');
|
|
27
27
|
/**
|
|
28
28
|
* Load failover configuration from ~/.codehere/config.json.
|
|
29
29
|
* Returns undefined if the config file does not exist or has no failover section.
|
|
@@ -64,14 +64,4 @@ export function loadFailoverConfig() {
|
|
|
64
64
|
return undefined;
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
-
/**
|
|
68
|
-
* Get the resolved failover chain: user-configured if present, else default.
|
|
69
|
-
* Pure function — no I/O. Caller loads config and passes it in.
|
|
70
|
-
*/
|
|
71
|
-
export function resolveChain(userChain) {
|
|
72
|
-
if (userChain && userChain.length > 0)
|
|
73
|
-
return userChain;
|
|
74
|
-
// Re-export the default from failover-policy
|
|
75
|
-
return ['claude-code', 'codex', 'aider', 'opencode', 'gemini'];
|
|
76
|
-
}
|
|
77
67
|
//# sourceMappingURL=failover-config.js.map
|