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
@@ -1,21 +1,22 @@
1
+ import { writeFileAtomic } from '../storage/atomic-write.js';
1
2
  /**
2
3
  * Seed ~/.codehere/agents.yaml on first daemon start.
3
4
  *
4
- * Ships with NO pre-populated adapters. Users who want codehere to drive a
5
- * CLI agent beyond the built-in four (Claude Code, Codex, Aider, OpenCode)
6
- * add their own entry to `~/.codehere/agents.yaml` and restart the daemon.
5
+ * Ships with ONE pre-wired generic adapter — Cursor — because it's been verified
6
+ * on a real machine (cursor-agent headless, authed via the Cursor subscription).
7
+ * It stays hidden in the UI until `cursor-agent` is installed + logged in, so it
8
+ * never inflates the agent count dishonestly. Any other CLI agent: users add their
9
+ * own entry to `~/.codehere/agents.yaml` and restart the daemon.
7
10
  *
8
- * Rationale: shipping scaffolded entries we hadn't actually verified on real
9
- * machines produced a "9 agents supported" claim that wasn't honest. The
10
- * generic-cli-adapter mechanism is still available — users opt in per agent
11
- * by editing the YAML themselves.
11
+ * Rationale: the bar for shipping a scaffolded entry is "verified on a real
12
+ * machine," not "plausible." Cursor met it; the rest stay opt-in via the YAML.
12
13
  *
13
14
  * Re-seed never overwrites an existing file.
14
15
  */
15
- import { existsSync, mkdirSync, writeFileSync } from 'fs';
16
+ import { existsSync, mkdirSync } from 'fs';
16
17
  import { homedir } from 'os';
17
18
  import { join, dirname } from 'path';
18
- const CONFIG_PATH = join(homedir(), '.codehere', 'agents.yaml');
19
+ const CONFIG_PATH = join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'agents.yaml');
19
20
  const DEFAULT_YAML = `# codehere — pluggable agent adapters
20
21
  #
21
22
  # Built-in adapters (no configuration needed — just install the CLI):
@@ -40,14 +41,25 @@ const DEFAULT_YAML = `# codehere — pluggable agent adapters
40
41
  # install_command: brew install my-agent-cli # optional · enables "Install" button
41
42
  # install_url: https://example.com/docs # optional · docs link
42
43
 
43
- agents: []
44
+ # Cursor — verified on a real machine (2026-06-22): cursor-agent runs headless and
45
+ # authenticates with your Cursor subscription, so it's a cost-bounded cross-vendor
46
+ # peer to the metered APIs. Hidden until \`cursor-agent\` is installed + logged in.
47
+ agents:
48
+ - id: cursor
49
+ label: Cursor
50
+ command: cursor-agent
51
+ args: [-p, --output-format, stream-json, -f, --model, gpt-5.5-high]
52
+ prompt_via: argv
53
+ format: jsonl
54
+ install_command: curl https://cursor.com/install -fsS | bash
55
+ install_url: https://docs.cursor.com/cli
44
56
  `;
45
57
  export function seedAgentsConfig() {
46
58
  try {
47
59
  if (existsSync(CONFIG_PATH))
48
60
  return;
49
61
  mkdirSync(dirname(CONFIG_PATH), { recursive: true });
50
- writeFileSync(CONFIG_PATH, DEFAULT_YAML, 'utf-8');
62
+ writeFileAtomic(CONFIG_PATH, DEFAULT_YAML);
51
63
  }
52
64
  catch {
53
65
  /* best-effort */
@@ -18,6 +18,7 @@ import { randomUUID } from 'crypto';
18
18
  import { startRecording } from './cassette-recorder.js';
19
19
  import { ProgressAggregator } from './progress-aggregator.js';
20
20
  import { resolveAgentSpawn } from './mocks/resolve-agent-spawn.js';
21
+ import { detectOllama } from './detect-ollama.js';
21
22
  import { applyMemoryPrefix } from './memory-injector.js';
22
23
  import { mirrorToolCallToAudit } from '../hooks/mirror-tool-call-to-audit.js';
23
24
  import { snapshotCwd, extractArtifacts } from './artifacts-extractor.js';
@@ -27,6 +28,12 @@ const HEADLESS_FLAGS = [
27
28
  '--no-pretty', // disable ANSI colors in output
28
29
  '--no-stream', // stream=false → one chunk per turn, cleaner stdout parsing
29
30
  '--no-auto-commits', // don't auto-commit — leave file changes for artifacts-extractor
31
+ // Aider detects URLs in the message and, under --yes-always, silently
32
+ // scrapes them. Our injected context (memory pack, repo docs) legitimately
33
+ // contains URLs — live 2026-07-02 a verify run burned its whole turn failing
34
+ // to scrape `codehere.uk` (markdown backtick glued onto the URL) and that
35
+ // noise became the report. Headless runs never want surprise scraping.
36
+ '--no-detect-urls',
30
37
  ];
31
38
  // v0.7 HIGH-1 fix — extended ERROR_PATTERNS for the 18+ provider families
32
39
  // Aider supports. Pre-fix: 4 regexes shaped only for Anthropic + OpenAI text.
@@ -39,7 +46,9 @@ const ERROR_PATTERNS = [
39
46
  {
40
47
  code: 'credit_low',
41
48
  // Anthropic + OpenAI canonical + DeepSeek ("Insufficient Balance").
42
- regex: /credit balance is too low|insufficient.*credit|no credits|you exceeded your current quota|insufficient.balance/i,
49
+ // Adjacent words only — a spanning /insufficient.*credit/ combines distant
50
+ // benign prose under the de-wrap collapse (2026-06-07 cold review).
51
+ regex: /credit balance is too low|insufficient\s+credits?\b|no credits\b|you exceeded your current quota|insufficient\s+balance/i,
43
52
  message: 'The API key has no credit (or balance is too low).',
44
53
  recovery: 'Aider needs a billed API key with balance. Top up at your provider (console.anthropic.com / ' +
45
54
  'platform.openai.com / platform.deepseek.com), or switch the task to Claude Code / Codex / Gemini ' +
@@ -49,7 +58,7 @@ const ERROR_PATTERNS = [
49
58
  code: 'auth_invalid',
50
59
  // Anthropic + OpenAI + Anthropic's actual "invalid x-api-key" + Gemini's
51
60
  // "API key not valid" + Bedrock's "UnrecognizedClientException" + generic.
52
- regex: /invalid api key|invalid x-api-key|api key not valid|authentication.*failed|401 unauth|please provide your.*api key|unrecognizedclientexception|the security token included in the request is invalid/i,
61
+ regex: /invalid api key|invalid x-api-key|api key not valid|authentication\s+(?:request\s+)?failed|401 unauth|please provide your\s+\S{0,20}\s*api key|unrecognizedclientexception|the security token included in the request is invalid/i,
53
62
  message: 'Aider auth was rejected or missing.',
54
63
  recovery: 'Check the API key for your model provider (ANTHROPIC_API_KEY, OPENAI_API_KEY, DEEPSEEK_API_KEY, ' +
55
64
  'GEMINI_API_KEY, AWS_ACCESS_KEY_ID for Bedrock, etc.). Re-export and retry.',
@@ -57,13 +66,19 @@ const ERROR_PATTERNS = [
57
66
  {
58
67
  code: 'rate_limit',
59
68
  // Generic 429 + named providers (Gemini RESOURCE_EXHAUSTED, Bedrock ThrottlingException).
60
- regex: /rate.?limit(ed)?|429|too many requests|resource.exhausted|throttlingexception/i,
69
+ // Deliberately requires error-shaped context ("rate limited", "rate limit
70
+ // exceeded") — a bare /rate.?limit/ matches the literal filename
71
+ // rate-limit.ts in an "Applied edit to …" line and flags a SUCCESSFUL run
72
+ // as failed (2026-06-07 batch cold review, HIGH).
73
+ regex: /\brate.?limited\b|rate.?limit\s*(?:exceeded|reached|hit|error)|hit.{0,20}rate.?limit|\b429\b|too many requests|resource.?exhausted|throttlingexception/i,
61
74
  message: 'Aider was rate-limited by the provider.',
62
75
  recovery: 'Wait a minute and retry, or switch --model via extraArgs to a different provider.',
63
76
  },
64
77
  {
65
78
  code: 'model_unavailable',
66
- regex: /model.*(not found|not available|unavailable|does not exist)|invalidmodelid/i,
79
+ // Bounded gap (≤40 chars, no sentence boundary) so "model X not found"
80
+ // matches but "model" + distant "not found" in prose doesn't combine.
81
+ regex: /model\b[^.!?]{0,40}(not found|not available|unavailable|does not exist)|invalidmodelid/i,
67
82
  message: 'Aider could not find the model.',
68
83
  recovery: 'Run `aider --list-models` and pass `--model <name>` via extraArgs.',
69
84
  },
@@ -76,11 +91,58 @@ const ERROR_PATTERNS = [
76
91
  recovery: 'Network or provider endpoint issue. For local Ollama: confirm `ollama serve` is running. ' +
77
92
  'For cloud providers: check internet + provider status page.',
78
93
  },
94
+ // 2026-06-07 user-#1 dogfood (session 4b6bc481): aider HARD-WRAPS its error
95
+ // body at ~80 cols, so "Your credit\nbalance is too low" lands on two lines.
96
+ // The per-line scan above never sees the contiguous phrase → no failover →
97
+ // exit 0 → a no-op run presented as "verified". The litellm exception CLASS
98
+ // name always rides ONE line (it's the prefix before the wrapped JSON), so
99
+ // these are wrap-proof fingerprints. Keep them last so the more specific
100
+ // phrase patterns above win when they DO land on a single line.
101
+ {
102
+ code: 'credit_low',
103
+ regex: /litellm\.(?:BadRequestError|InsufficientCreditsError).*(?:credit|balance|quota)/i,
104
+ message: 'The API key has no credit (or balance is too low).',
105
+ recovery: 'Aider needs a billed API key with balance. Top up at your provider (console.anthropic.com / ' +
106
+ 'platform.openai.com / platform.deepseek.com), or switch the task to Claude Code / Codex / Gemini ' +
107
+ 'which use subscription or free-tier auth.',
108
+ },
109
+ {
110
+ code: 'auth_invalid',
111
+ regex: /litellm\.(?:AuthenticationError|PermissionDeniedError)/i,
112
+ message: 'Aider auth was rejected or missing.',
113
+ recovery: 'Check the API key for your model provider (ANTHROPIC_API_KEY, OPENAI_API_KEY, DEEPSEEK_API_KEY, ' +
114
+ 'GEMINI_API_KEY, AWS_ACCESS_KEY_ID for Bedrock, etc.). Re-export and retry.',
115
+ },
116
+ {
117
+ code: 'rate_limit',
118
+ regex: /litellm\.RateLimitError/i,
119
+ message: 'Aider was rate-limited by the provider.',
120
+ recovery: 'Wait a minute and retry, or switch --model via extraArgs to a different provider.',
121
+ },
122
+ {
123
+ code: 'model_unavailable',
124
+ regex: /litellm\.NotFoundError/i,
125
+ message: 'Aider could not find the model.',
126
+ recovery: 'Run `aider --list-models` and pass `--model <name>` via extraArgs.',
127
+ },
128
+ {
129
+ code: 'network_failure',
130
+ regex: /litellm\.(?:APIConnectionError|Timeout|ServiceUnavailableError)/i,
131
+ message: "Aider couldn't reach the model provider.",
132
+ recovery: 'Network or provider endpoint issue. For local Ollama: confirm `ollama serve` is running. ' +
133
+ 'For cloud providers: check internet + provider status page.',
134
+ },
79
135
  ];
80
- function scanForKnownError(text) {
136
+ export function scanForKnownError(text) {
137
+ // Aider wraps long error bodies across lines; collapse whitespace so a phrase
138
+ // split by a hard wrap ("Your credit\nbalance is too low") is matched as one
139
+ // contiguous string. Scan BOTH the raw text (preserves single-line patterns
140
+ // like litellm class names) and the de-wrapped text (recovers wrapped phrases).
141
+ const dewrapped = text.replace(/\s+/g, ' ');
81
142
  for (const p of ERROR_PATTERNS) {
82
- if (p.regex.test(text))
143
+ if (p.regex.test(text) || p.regex.test(dewrapped)) {
83
144
  return { code: p.code, message: p.message, recovery: p.recovery };
145
+ }
84
146
  }
85
147
  return null;
86
148
  }
@@ -105,7 +167,16 @@ export function spawnAider(prompt, options = {}) {
105
167
  let stepsSnapshot = [];
106
168
  let artifactsSnapshot;
107
169
  let textAccum = '';
170
+ // Bounded tail of recent stdout for error scanning. Aider hard-wraps error
171
+ // bodies at ~80 cols, so a wrapped phrase spans 2-3 ADJACENT lines — a small
172
+ // tail catches it. Scanning the WHOLE accumulated buffer instead would let
173
+ // prose from unrelated parts of a successful run combine into a phrase match
174
+ // (2026-06-07 batch cold review, HIGH: a session about rate limiting read as
175
+ // a rate-limit failure).
176
+ let scanTail = '';
177
+ const SCAN_TAIL_CHARS = 600;
108
178
  let usageSnapshot;
179
+ let aiderModel; // model aider announced in stdout → per-model cost
109
180
  let errorEmitted = false;
110
181
  const emit = (event) => {
111
182
  buffer.push(event);
@@ -120,6 +191,12 @@ export function spawnAider(prompt, options = {}) {
120
191
  if (event.type === 'stdout' && event.data?.line) {
121
192
  const line = String(event.data.line);
122
193
  textAccum += line + '\n';
194
+ // Aider announces its model: "Main model: claude-3-5-sonnet-... with ...".
195
+ if (!aiderModel) {
196
+ const mm = line.match(/(?:main )?model:\s*([A-Za-z0-9._/:-]+)/i);
197
+ if (mm)
198
+ aiderModel = mm[1];
199
+ }
123
200
  // #110 slice 3 — mirror aider tool calls to the audit log. Match
124
201
  // the same stdout patterns the aggregator parses (per
125
202
  // learning_aider.md): "Running <cmd>" → Bash, "Applied edit to
@@ -156,6 +233,7 @@ export function spawnAider(prompt, options = {}) {
156
233
  usageSnapshot = {
157
234
  inputTokens: (usageSnapshot?.inputTokens ?? 0) + sent,
158
235
  outputTokens: (usageSnapshot?.outputTokens ?? 0) + recv,
236
+ ...(aiderModel ? { model: aiderModel } : {}),
159
237
  };
160
238
  }
161
239
  }
@@ -163,7 +241,21 @@ export function spawnAider(prompt, options = {}) {
163
241
  const data = event.data;
164
242
  let text = '';
165
243
  if ((event.type === 'stderr' || event.type === 'stdout') && typeof data?.line === 'string') {
166
- text = data.line;
244
+ // Scan a bounded RECENT tail, not just this one line and not the whole
245
+ // buffer. Aider hard-wraps error bodies across lines (user-#1 dogfood,
246
+ // session 4b6bc481), so a single-line scan misses "Your credit\nbalance
247
+ // is too low" — but the whole accumulated buffer lets distant prose
248
+ // combine into false matches. The wrapped phrase always sits on
249
+ // adjacent lines, so the tail window catches it. The de-wrap collapse
250
+ // in scanForKnownError joins the wrapped phrase. stderr is scanned
251
+ // line-by-line (it isn't accumulated).
252
+ if (event.type === 'stdout') {
253
+ scanTail = (scanTail + '\n' + data.line).slice(-SCAN_TAIL_CHARS);
254
+ text = scanTail;
255
+ }
256
+ else {
257
+ text = data.line;
258
+ }
167
259
  }
168
260
  if (text) {
169
261
  const hit = scanForKnownError(text);
@@ -193,9 +285,28 @@ export function spawnAider(prompt, options = {}) {
193
285
  }
194
286
  });
195
287
  const spawnEnv = { ...process.env, ...(options.env ?? {}) };
288
+ // Local-first reliable vendor: when the user hasn't pinned a model (no
289
+ // AIDER_MODEL / CODEHERE_AIDER_MODEL env, no --model in extraArgs) and Ollama
290
+ // is serving a local model, default aider to it. That turns aider into a FREE,
291
+ // RELIABLE, LOCAL second vendor (no rate limits, no cloud key, no waste) — the
292
+ // unlock for the cross-vendor wedge on a machine that has Ollama. Always
293
+ // overridable; opt out with CODEHERE_NO_LOCAL_OLLAMA=1.
294
+ const extra = options.extraArgs ?? [];
295
+ const modelPinned = !!spawnEnv.AIDER_MODEL ||
296
+ !!spawnEnv.CODEHERE_AIDER_MODEL ||
297
+ extra.some((a) => a === '--model' || a.startsWith('--model='));
298
+ const localArgs = [];
299
+ if (!modelPinned && spawnEnv.CODEHERE_NO_LOCAL_OLLAMA !== '1') {
300
+ const ollama = detectOllama();
301
+ if (ollama.available && ollama.model) {
302
+ localArgs.push('--model', `ollama_chat/${ollama.model}`);
303
+ if (!spawnEnv.OLLAMA_API_BASE)
304
+ spawnEnv.OLLAMA_API_BASE = 'http://localhost:11434';
305
+ }
306
+ }
196
307
  let child;
197
308
  try {
198
- const sp = resolveAgentSpawn('aider', 'aider', ['--message', finalPrompt, ...HEADLESS_FLAGS, ...(options.extraArgs ?? [])], finalPrompt);
309
+ const sp = resolveAgentSpawn('aider', 'aider', ['--message', finalPrompt, ...HEADLESS_FLAGS, ...localArgs, ...extra], finalPrompt);
199
310
  child = spawn(sp.command, sp.args, {
200
311
  cwd,
201
312
  env: spawnEnv,
@@ -261,19 +372,28 @@ export function spawnAider(prompt, options = {}) {
261
372
  // Preserve 'error' if we already flipped to it on codehere:error.
262
373
  statusValue =
263
374
  killRequested || signal !== null ? 'killed' : errorEmitted ? 'error' : code === 0 ? 'completed' : 'error';
264
- if (aggregator.finalize(statusValue === 'completed' ? 'completed' : statusValue === 'killed' ? 'killed' : 'error')) {
265
- stepsSnapshot = aggregator.steps.map((s) => ({ ...s }));
266
- emit({ type: 'codehere:steps', timestamp: Date.now(), data: { steps: stepsSnapshot } });
267
- }
375
+ const finalizeChanged = aggregator.finalize(statusValue === 'completed' ? 'completed' : statusValue === 'killed' ? 'killed' : 'error');
376
+ // Extract artifacts BEFORE emitting steps so the git-diff fallback can run.
268
377
  if (statusValue === 'completed') {
269
378
  try {
270
379
  artifactsSnapshot = { ...extractArtifacts(snapshot, textAccum), usage: usageSnapshot };
271
- emit({ type: 'codehere:artifacts', timestamp: Date.now(), data: artifactsSnapshot });
272
380
  }
273
381
  catch {
274
382
  /* best-effort */
275
383
  }
276
384
  }
385
+ // Aider parity safety net: if the brittle stdout-parse produced no
386
+ // file-action steps but git shows real changes, synthesize them from the
387
+ // diff so the timeline + report can't claim "did nothing" on a real edit.
388
+ const fallbackChanged = aggregator.applyFileFallback(artifactsSnapshot?.files);
389
+ // PM steps BEFORE artifacts BEFORE exit (client closes SSE on exit; order matters).
390
+ if (finalizeChanged || fallbackChanged) {
391
+ stepsSnapshot = aggregator.steps.map((s) => ({ ...s }));
392
+ emit({ type: 'codehere:steps', timestamp: Date.now(), data: { steps: stepsSnapshot } });
393
+ }
394
+ if (artifactsSnapshot) {
395
+ emit({ type: 'codehere:artifacts', timestamp: Date.now(), data: artifactsSnapshot });
396
+ }
277
397
  emit({
278
398
  type: 'exit',
279
399
  timestamp: Date.now(),
@@ -344,7 +464,11 @@ export function spawnAider(prompt, options = {}) {
344
464
  let stderr = '';
345
465
  // Route follow-ups through the mock seam too — under CODEHERE_MOCK=1 this
346
466
  // hits the mock REPL, not real aider. (Was a real-spend leak.)
347
- const sp2 = resolveAgentSpawn('aider', 'aider', ['--restore-chat-history', '--message', text, ...HEADLESS_FLAGS], text);
467
+ // BUG-1 (E2E 2.4) · pin the follow-up to the SAME model as the original
468
+ // spawn (localArgs + extra). Without them, a free ollama session's reply
469
+ // fell back to aider's default cloud model + env API key — a silent
470
+ // vendor/spend switch, observed live 2026-07-18.
471
+ const sp2 = resolveAgentSpawn('aider', 'aider', ['--restore-chat-history', '--message', text, ...HEADLESS_FLAGS, ...localArgs, ...extra], text);
348
472
  const followup = spawn(sp2.command, sp2.args, {
349
473
  cwd,
350
474
  env: spawnEnv,
@@ -413,33 +537,73 @@ export const AiderAdapter = {
413
537
  installHint: { command: 'brew install aider', url: 'https://aider.chat/docs/install.html' },
414
538
  };
415
539
  }
416
- // Aider has no subscription path — needs a BILLED API key with credit.
417
- // If neither Anthropic nor OpenAI key is present → needs_auth.
418
- // If a key IS present but the user is running on a Claude Code subscription
419
- // (no billing configured), Aider will hit credit_low at runtime. We can't
420
- // detect that pre-spawn without making an API call. Flag the risk in the
421
- // detail string so the UI can surface it.
540
+ // Aider has no subscription path, but it routes through LiteLLM — so it can
541
+ // run on a BILLED key (Anthropic/OpenAI, needs credit) OR for FREE on a
542
+ // Gemini AI-Studio key / DeepSeek / a local Ollama model, by passing
543
+ // `--model <provider>/<model>`. The pre-May "needs a BILLED key" hint was
544
+ // misleading for free-tier-first users (Phase 1): it sent them to top up a
545
+ // card when a free Gemini key would have worked. We can't verify credit
546
+ // pre-spawn without an API call, so we surface BOTH paths honestly.
422
547
  const hasAnthropic = !!process.env.ANTHROPIC_API_KEY;
423
548
  const hasOpenai = !!process.env.OPENAI_API_KEY;
424
- if (!hasAnthropic && !hasOpenai) {
549
+ const hasGemini = !!process.env.GEMINI_API_KEY || !!process.env.GOOGLE_API_KEY;
550
+ const hasDeepseek = !!process.env.DEEPSEEK_API_KEY;
551
+ const FREE_PATH = 'Free option: set GEMINI_API_KEY (aistudio.google.com/apikey) and run aider with `--model gemini/gemini-2.5-flash`, or point it at a local Ollama model (`--model ollama/…`) — aider routes through LiteLLM, so no paid key is required.';
552
+ // A configured model + its matching key = genuinely READY (the user picked a
553
+ // working combo, so don't fall through to billed_key_required). AIDER_MODEL
554
+ // is aider's own env override (= `--model`); it's how codehere routes aider
555
+ // to a free provider, since the task intake doesn't pass --model. A local
556
+ // model (ollama/…) needs no cloud key — ready by config, like claude's
557
+ // subscription (runtime catches a down endpoint via the network fingerprint).
558
+ const model = (process.env.AIDER_MODEL || '').toLowerCase();
559
+ const modelReady = (model.startsWith('gemini') && hasGemini) ||
560
+ (model.startsWith('deepseek') && hasDeepseek) ||
561
+ (model.startsWith('openai') && hasOpenai) ||
562
+ (model.startsWith('anthropic') && hasAnthropic) ||
563
+ model.startsWith('ollama') ||
564
+ model.startsWith('ollama_chat');
565
+ if (modelReady) {
566
+ return { id: this.id, label: this.label, state: 'ready', installed: true };
567
+ }
568
+ // Local-first: a serving Ollama model makes aider ready for FREE with no
569
+ // cloud key. The spawn path auto-routes aider to it (detect-ollama) when no
570
+ // model is pinned — so readiness agrees, or the intake gate would refuse a
571
+ // spawn that will actually run. Mirrors the spawn-side guard on the ENV pins
572
+ // (AIDER_MODEL / CODEHERE_AIDER_MODEL / CODEHERE_NO_LOCAL_OLLAMA); it cannot
573
+ // see a per-task extraArgs `--model` (readiness has never seen extraArgs —
574
+ // the product's /api/task intake doesn't pass them, so this is unreachable
575
+ // through the surface, just honestly noted).
576
+ const localPinnedAway = !!process.env.CODEHERE_AIDER_MODEL || process.env.CODEHERE_NO_LOCAL_OLLAMA === '1';
577
+ if (!model && !localPinnedAway && detectOllama().available) {
578
+ return { id: this.id, label: this.label, state: 'ready', installed: true };
579
+ }
580
+ if (!hasAnthropic && !hasOpenai && !hasGemini && !hasDeepseek) {
425
581
  return {
426
582
  id: this.id,
427
583
  label: this.label,
428
584
  state: 'needs_auth',
429
585
  installed: true,
430
586
  authHint: {
431
- detail: 'Aider needs a BILLED API key (Anthropic or OpenAI). Unlike Claude Code, it has no subscription path.',
432
- url: 'https://console.anthropic.com',
587
+ command: 'export GEMINI_API_KEY=... # free tier: https://aistudio.google.com/apikey',
588
+ detail: `Aider has no subscription path, but it doesn't need a paid key either. ${FREE_PATH} A billed Anthropic/OpenAI key with credit also works.`,
589
+ url: 'https://aistudio.google.com/apikey',
433
590
  },
434
591
  };
435
592
  }
593
+ // A provider key is present. If it's only the free Gemini/DeepSeek key,
594
+ // aider runs at $0 but needs `--model …` to target that provider. If it's a
595
+ // billed Anthropic/OpenAI key, that key needs real credit. Either way the
596
+ // key's mere presence doesn't guarantee a successful first call.
597
+ const onlyFree = !hasAnthropic && !hasOpenai && (hasGemini || hasDeepseek);
436
598
  return {
437
599
  id: this.id,
438
600
  label: this.label,
439
601
  state: 'billed_key_required',
440
602
  installed: true,
441
603
  authHint: {
442
- detail: 'API key detected. Aider requires the key to have billing credit (not just presence). If you run on a Claude Code subscription, Aider will fail at the first model call.',
604
+ detail: onlyFree
605
+ ? `Free provider key detected (${hasGemini ? 'Gemini' : 'DeepSeek'}). Aider runs at $0 on it, but target it explicitly with \`--model ${hasGemini ? 'gemini/gemini-2.5-flash' : 'deepseek/deepseek-chat'}\` — aider's default model expects an Anthropic/OpenAI key.`
606
+ : `API key detected. Aider needs the key to have billing credit (not just presence). If you run on a Claude Code subscription, Aider will fail at the first model call. ${FREE_PATH}`,
443
607
  },
444
608
  };
445
609
  },
@@ -43,7 +43,12 @@ function statBaseline(fullPath) {
43
43
  function captureDirtyBaseline(cwd, repoRoot) {
44
44
  const baseline = new Map();
45
45
  try {
46
- const out = execFileSync('git', ['status', '--porcelain', '-z'], {
46
+ // -uall: porcelain normally collapses an untracked DIRECTORY into one
47
+ // "?? dir/" entry, while changedFiles() lists its contents per-file via
48
+ // ls-files --others — so the baseline could never match those paths and a
49
+ // whole pre-existing untracked dir got attributed to the agent (2026-06-07
50
+ // v1 walk: memory-init's .codehere/ scaffolding showed as 8 agent edits).
51
+ const out = execFileSync('git', ['status', '--porcelain', '-uall', '-z'], {
47
52
  cwd,
48
53
  encoding: 'utf-8',
49
54
  timeout: 5000,
@@ -117,6 +122,13 @@ export function snapshotCwd(cwd) {
117
122
  }
118
123
  }
119
124
  /** Collect {path, +, -} from git diff (numstat) + untracked files (wc -l). */
125
+ // codehere's own per-project state dir. Its contents (memory, protocols,
126
+ // skills, context packs) are product plumbing, not the user's code — they
127
+ // never belong in the room's "what the agent changed" list, even when the
128
+ // memory write-back touches them mid-session.
129
+ function isInternalPath(path) {
130
+ return path === '.codehere' || path.startsWith('.codehere/') || path.includes('/.codehere/');
131
+ }
120
132
  function changedFiles(snap) {
121
133
  if (!snap.isGitRepo)
122
134
  return [];
@@ -134,6 +146,8 @@ function changedFiles(snap) {
134
146
  if (!m)
135
147
  continue;
136
148
  const [, aStr, rStr, path] = m;
149
+ if (isInternalPath(path))
150
+ continue;
137
151
  out.push({
138
152
  path,
139
153
  added: aStr === '-' ? 0 : parseInt(aStr, 10) || 0,
@@ -155,6 +169,8 @@ function changedFiles(snap) {
155
169
  for (const rel of untracked.split('\n')) {
156
170
  if (!rel.trim())
157
171
  continue;
172
+ if (isInternalPath(rel))
173
+ continue;
158
174
  // Count lines in the new file. Untracked paths come from git relative
159
175
  // to repoRoot — must join with repoRoot, not cwd, when cwd is a subdir.
160
176
  try {
@@ -315,8 +331,50 @@ function filterBySpawnBaseline(snap, files) {
315
331
  }
316
332
  });
317
333
  }
334
+ /** The spawned tool's OWN housekeeping — never the task's work product. Live
335
+ * 2026-07-02: aider wrote .aider* + a .gitignore line on a run that did
336
+ * nothing else, the audit confirmed those writes on disk, and a no-op run
337
+ * graded proven/green off its tool's litter. Chat histories, tool caches,
338
+ * and the .gitignore edits tools make to hide them don't count as work.
339
+ * .gitignore is dropped ONLY when every added line is tool litter — a task
340
+ * whose real job is editing .gitignore still counts. */
341
+ const TOOL_HOUSEKEEPING = /(^|\/)\.(aider|claude|codehere|gemini|codex|opencode)[^/]*($|\/)/i;
342
+ const LITTER_LINE = /^\s*($|#|\.?(aider|claude|codehere|gemini|codex|opencode))/i;
343
+ function gitignoreOnlyLitter(snap, path) {
344
+ try {
345
+ const d = execFileSync('git', ['diff', snap.head ?? 'HEAD', '--', path], {
346
+ cwd: snap.cwd,
347
+ encoding: 'utf-8',
348
+ timeout: 3000,
349
+ stdio: ['ignore', 'pipe', 'ignore'],
350
+ });
351
+ let added = d
352
+ .split('\n')
353
+ .filter((l) => l.startsWith('+') && !l.startsWith('+++'))
354
+ .map((l) => l.slice(1));
355
+ if (added.length === 0) {
356
+ // Untracked (brand-new) .gitignore: git diff HEAD shows nothing — the
357
+ // whole file is the addition.
358
+ added = readFileSync(join(snap.cwd, path), 'utf-8').split('\n');
359
+ }
360
+ const lines = added.filter((l) => l.trim().length > 0);
361
+ return lines.length > 0 && lines.every((l) => LITTER_LINE.test(l));
362
+ }
363
+ catch {
364
+ return false; // can't inspect — keep it counted (honest over-count beats hiding work)
365
+ }
366
+ }
367
+ function dropToolHousekeeping(snap, files) {
368
+ return files.filter((f) => {
369
+ if (TOOL_HOUSEKEEPING.test(f.path))
370
+ return false;
371
+ if (/(^|\/)\.gitignore$/.test(f.path))
372
+ return !gitignoreOnlyLitter(snap, f.path);
373
+ return true;
374
+ });
375
+ }
318
376
  export function extractArtifacts(snap, collectedStdout) {
319
- const raw = changedFiles(snap);
377
+ const raw = dropToolHousekeeping(snap, changedFiles(snap));
320
378
  const sessionScoped = filterByMtime(snap, raw);
321
379
  const agentLikely = filterBySpawnBaseline(snap, sessionScoped);
322
380
  const files = annotateTrust(snap, agentLikely);
@@ -38,7 +38,7 @@ const NULL_RECORDER = {
38
38
  export function startRecording(opts) {
39
39
  if (process.env[ENV_FLAG] !== '1')
40
40
  return NULL_RECORDER;
41
- const outDir = opts.outDir ?? join(homedir(), '.codehere', 'cassettes', opts.adapter);
41
+ const outDir = opts.outDir ?? join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'cassettes', opts.adapter);
42
42
  const scenario = `organic-${opts.sessionId.slice(0, 8)}`;
43
43
  const stamp = new Date().toISOString().replace(/[:.]/g, '-');
44
44
  const path = join(outDir, `${scenario}-${stamp}.jsonl`);