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
|
@@ -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
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
9
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, ...
|
|
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
|
-
|
|
265
|
-
|
|
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
|
-
|
|
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
|
|
417
|
-
//
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
//
|
|
421
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
432
|
-
|
|
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:
|
|
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
|
-
|
|
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`);
|