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.
Files changed (139) hide show
  1. package/README.md +22 -13
  2. package/dist/application/services/verdict-service.js +154 -0
  3. package/dist/application/use-cases/assess-cross-vendor.js +126 -0
  4. package/dist/application/use-cases/build-cross-vendor-verdict.js +118 -0
  5. package/dist/application/use-cases/export-aat.js +107 -0
  6. package/dist/application/use-cases/generate-acceptance-criteria.js +223 -0
  7. package/dist/application/use-cases/generate-final-report.js +140 -15
  8. package/dist/application/use-cases/generate-narration.js +5 -3
  9. package/dist/application/use-cases/generate-thread-title.js +5 -2
  10. package/dist/application/use-cases/narrator-prompt-template.js +1 -1
  11. package/dist/application/use-cases/plan-auto-review.js +57 -0
  12. package/dist/application/use-cases/reconcile-audit.js +100 -0
  13. package/dist/application/use-cases/rerun-tests.js +226 -0
  14. package/dist/application/use-cases/search-sessions.js +70 -0
  15. package/dist/application/use-cases/should-auto-review.js +71 -0
  16. package/dist/application/use-cases/thread-export.js +90 -0
  17. package/dist/application/use-cases/verify-claim.js +187 -5
  18. package/dist/domain/audit.js +11 -0
  19. package/dist/domain/services/codebase-detector.js +0 -6
  20. package/dist/domain/task.js +91 -0
  21. package/dist/error-handler.js +3 -115
  22. package/dist/index.js +279 -31
  23. package/dist/infrastructure/completion/bash-completion.js +7 -3
  24. package/dist/infrastructure/completion/fish-completion.js +7 -12
  25. package/dist/infrastructure/completion/zsh-completion.js +7 -12
  26. package/dist/infrastructure/config/config-manager.js +4 -3
  27. package/dist/infrastructure/config/product-context.js +1 -19
  28. package/dist/infrastructure/context/adapters/figma-source.js +37 -2
  29. package/dist/infrastructure/context/adapters/github-issue-source.js +3 -2
  30. package/dist/infrastructure/context/github-api.js +110 -0
  31. package/dist/infrastructure/context/github-device-flow.js +65 -0
  32. package/dist/infrastructure/context/oauth-store.js +5 -2
  33. package/dist/infrastructure/context/registry-bootstrap.js +0 -4
  34. package/dist/infrastructure/cost/budget-config.js +4 -3
  35. package/dist/infrastructure/cost/cost-router.js +16 -23
  36. package/dist/infrastructure/cost/spend-aggregator.js +91 -16
  37. package/dist/infrastructure/cost/team-ledger.js +355 -0
  38. package/dist/infrastructure/discovery/discovery-log.js +85 -0
  39. package/dist/infrastructure/hooks/audit-tip-signature.js +336 -0
  40. package/dist/infrastructure/hooks/claude-sdk-hook.js +139 -3
  41. package/dist/infrastructure/hooks/hook-mode.js +20 -4
  42. package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +8 -2
  43. package/dist/infrastructure/hooks/pre-prompt-scan.js +3 -3
  44. package/dist/infrastructure/observability/execution-tracer.js +1 -1
  45. package/dist/infrastructure/orchestration/agents-config-seed.js +23 -11
  46. package/dist/infrastructure/orchestration/aider-adapter.js +188 -24
  47. package/dist/infrastructure/orchestration/artifacts-extractor.js +60 -2
  48. package/dist/infrastructure/orchestration/cassette-recorder.js +1 -1
  49. package/dist/infrastructure/orchestration/claude-code-adapter.js +117 -3
  50. package/dist/infrastructure/orchestration/codex-adapter.js +52 -16
  51. package/dist/infrastructure/orchestration/conductor-adapter.js +46 -7
  52. package/dist/infrastructure/orchestration/context-handoff.js +32 -14
  53. package/dist/infrastructure/orchestration/detect-ollama.js +75 -0
  54. package/dist/infrastructure/orchestration/failover-config.js +1 -11
  55. package/dist/infrastructure/orchestration/gemini-adapter.js +92 -12
  56. package/dist/infrastructure/orchestration/generic-cli-adapter.js +134 -3
  57. package/dist/infrastructure/orchestration/graphify-context.js +14 -59
  58. package/dist/infrastructure/orchestration/memory-injector.js +4 -1
  59. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +167 -9
  60. package/dist/infrastructure/orchestration/opencode-adapter.js +84 -14
  61. package/dist/infrastructure/orchestration/progress-aggregator.js +275 -23
  62. package/dist/infrastructure/orchestration/session-manager.js +593 -94
  63. package/dist/infrastructure/orchestration/session-persistence.js +177 -41
  64. package/dist/infrastructure/orchestration/thread-key-store.js +1 -1
  65. package/dist/infrastructure/orchestration/write-pass-store.js +0 -8
  66. package/dist/infrastructure/protocols/cloud-sync.js +2 -2
  67. package/dist/infrastructure/protocols/mcp-helpers.js +1 -1
  68. package/dist/infrastructure/protocols/mcp-server-impl.js +1498 -354
  69. package/dist/infrastructure/protocols/mcp-tools.js +401 -0
  70. package/dist/infrastructure/protocols/ui/app-script-artifacts.js +121 -0
  71. package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +44 -0
  72. package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +56 -0
  73. package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +588 -0
  74. package/dist/infrastructure/protocols/ui/app-script-context-sources.js +168 -0
  75. package/dist/infrastructure/protocols/ui/app-script-conversation.js +67 -0
  76. package/dist/infrastructure/protocols/ui/app-script-drawer.js +115 -0
  77. package/dist/infrastructure/protocols/ui/app-script-failover.js +133 -0
  78. package/dist/infrastructure/protocols/ui/app-script-github.js +113 -0
  79. package/dist/infrastructure/protocols/ui/app-script-observer.js +104 -0
  80. package/dist/infrastructure/protocols/ui/app-script-onboarding.js +80 -0
  81. package/dist/infrastructure/protocols/ui/app-script-overlay.js +203 -0
  82. package/dist/infrastructure/protocols/ui/app-script-recovery.js +136 -0
  83. package/dist/infrastructure/protocols/ui/app-script-render.js +166 -0
  84. package/dist/infrastructure/protocols/ui/app-script-share.js +78 -0
  85. package/dist/infrastructure/protocols/ui/app-script-sidebar.js +194 -0
  86. package/dist/infrastructure/protocols/ui/app-script-tour.js +78 -0
  87. package/dist/infrastructure/protocols/ui/app-script-wpass.js +312 -0
  88. package/dist/infrastructure/protocols/ui/app-script.js +2998 -0
  89. package/dist/infrastructure/protocols/ui/font-faces.js +7 -0
  90. package/dist/infrastructure/protocols/ui/styles.js +2147 -0
  91. package/dist/infrastructure/protocols/ui/tokens.js +96 -0
  92. package/dist/infrastructure/protocols/ui-page-v2.js +116 -4126
  93. package/dist/infrastructure/protocols/verify-page.js +884 -148
  94. package/dist/infrastructure/security/fix-code.js +144 -0
  95. package/dist/infrastructure/security/fix-secret.js +89 -0
  96. package/dist/infrastructure/security/permissions-config.js +3 -2
  97. package/dist/infrastructure/security/scan-types.js +10 -0
  98. package/dist/infrastructure/security/ship-scan.js +195 -0
  99. package/dist/infrastructure/security/trust-scorer.js +70 -3
  100. package/dist/infrastructure/security/vibe-checks.js +99 -0
  101. package/dist/infrastructure/storage/atomic-write.js +86 -0
  102. package/dist/infrastructure/storage/project-thread-mirror.js +151 -4
  103. package/dist/infrastructure/storage/project-thread-reader.js +262 -25
  104. package/dist/infrastructure/storage/project-thread-title-backfill.js +1 -1
  105. package/dist/monitoring.js +2 -2
  106. package/dist/presentation/cli/commands/audit-command.js +79 -4
  107. package/dist/presentation/cli/commands/budget-command.js +2 -2
  108. package/dist/presentation/cli/commands/check-command.js +96 -0
  109. package/dist/presentation/cli/commands/context-source-command.js +20 -1
  110. package/dist/presentation/cli/commands/help-command.js +5 -5
  111. package/dist/presentation/cli/commands/hook-command.js +5 -5
  112. package/dist/presentation/cli/commands/init-command.js +89 -66
  113. package/dist/presentation/cli/commands/memory-command.js +28 -28
  114. package/dist/presentation/cli/commands/permission-command.js +1 -12
  115. package/dist/presentation/cli/commands/protocols-builder.js +2 -2
  116. package/dist/presentation/cli/commands/protocols-command.js +4 -4
  117. package/dist/presentation/cli/commands/repo-map-builder.js +1 -1
  118. package/dist/presentation/cli/commands/report-command.js +48 -32
  119. package/dist/presentation/cli/commands/scan-command.js +52 -0
  120. package/dist/presentation/cli/commands/scan-tool-command.js +24 -8
  121. package/dist/presentation/cli/commands/search-command.js +51 -0
  122. package/dist/presentation/cli/commands/setup-command.js +31 -8
  123. package/dist/presentation/cli/commands/status-command.js +39 -86
  124. package/dist/presentation/cli/commands/team-command.js +256 -0
  125. package/dist/presentation/cli/commands/thread-command.js +87 -5
  126. package/dist/presentation/cli/commands/trust-command.js +53 -31
  127. package/dist/presentation/cli/commands/verify-command.js +74 -9
  128. package/dist/presentation/cli/error-display.js +0 -8
  129. package/dist/presentation/cli/repl.js +15 -7
  130. package/dist/presentation/cli/task-deliverable.js +36 -10
  131. package/dist/testing/replay-adapter.js +0 -19
  132. package/dist/ui.js +51 -239
  133. package/dist/utils/version.js +0 -6
  134. package/package.json +30 -10
  135. package/dist/domain/entities/task.js +0 -61
  136. package/dist/infrastructure/protocols/ui-page.js +0 -2377
  137. package/dist/infrastructure/security/input-validator.js +0 -152
  138. package/dist/infrastructure/storage/embeddings-path.js +0 -37
  139. 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 — extract assistant message text from the
409
- // 'assistant' typed lines and the final 'result' line.
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 /history shows the
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 /history shows the
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
- if (hit) {
244
- errorEmitted = true;
245
- const errEvent = {
246
- type: 'codehere:error',
247
- timestamp: Date.now(),
248
- data: { code: hit.code, message: hit.message, recovery: hit.recovery },
249
- };
250
- buffer.push(errEvent);
251
- emitter.emit('event', errEvent);
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', ['exec', '--json', '--skip-git-repo-check', '--dangerously-bypass-approvals-and-sandbox', 'resume', codexThreadId, '-'], text);
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 ChatGPT account auth via `codex login` (similar to Claude
533
- // Code's OAuth) OR OPENAI_API_KEY. Can't verify without calling out — let
534
- // runtime catch real failures.
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 installed specialists (excluding Conductor itself to avoid recursion)
239
- const installedAgentIds = lookup()
240
- .filter((a) => a.id !== 'conductor' && a.isInstalled())
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
- const decision = pickSpecialist(prompt, installedAgentIds);
243
- // If nothing's installed, emit an error event and return a non-running session.
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: 'Conductor has no specialists to delegate to. Install at least one of: Claude Code, Codex, Aider, OpenCode.',
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
- reason: decision.reason,
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, handoff)
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