codehere 0.7.0 → 0.8.0

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 (29) hide show
  1. package/dist/application/use-cases/generate-final-report.js +86 -191
  2. package/dist/application/use-cases/narrator-prompt-template.js +1 -1
  3. package/dist/index.js +10 -6
  4. package/dist/infrastructure/cost/budget-config.js +1 -1
  5. package/dist/infrastructure/cost/spend-aggregator.js +1 -1
  6. package/dist/infrastructure/hooks/claude-sdk-hook.js +7 -1
  7. package/dist/infrastructure/orchestration/agents-config-seed.js +1 -1
  8. package/dist/infrastructure/orchestration/aider-adapter.js +4 -1
  9. package/dist/infrastructure/orchestration/cassette-recorder.js +1 -1
  10. package/dist/infrastructure/orchestration/claude-code-adapter.js +5 -1
  11. package/dist/infrastructure/orchestration/codex-adapter.js +4 -9
  12. package/dist/infrastructure/orchestration/conductor-adapter.js +9 -3
  13. package/dist/infrastructure/orchestration/failover-config.js +1 -1
  14. package/dist/infrastructure/orchestration/generic-cli-adapter.js +2 -2
  15. package/dist/infrastructure/orchestration/graphify-context.js +18 -13
  16. package/dist/infrastructure/orchestration/memory-injector.js +50 -2
  17. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +25 -25
  18. package/dist/infrastructure/orchestration/progress-aggregator.js +41 -6
  19. package/dist/infrastructure/orchestration/session-manager.js +18 -0
  20. package/dist/infrastructure/orchestration/session-persistence.js +2 -2
  21. package/dist/infrastructure/protocols/cloud-sync.js +2 -2
  22. package/dist/infrastructure/protocols/mcp-server-impl.js +66 -2
  23. package/dist/infrastructure/protocols/ui-page-v2.js +260 -40
  24. package/dist/infrastructure/protocols/verify-page.js +42 -0
  25. package/dist/infrastructure/storage/project-thread-mirror.js +1 -1
  26. package/dist/infrastructure/storage/project-thread-reader.js +1 -1
  27. package/dist/infrastructure/storage/project-thread-title-backfill.js +1 -1
  28. package/dist/presentation/cli/commands/init-command.js +66 -55
  29. package/package.json +2 -1
@@ -1,107 +1,84 @@
1
1
  /**
2
- * Final Report — PM-UX Layer 3 · MVP (v0.5.0-alpha.3+).
2
+ * Final Report — the headline deliverable. At task exit, produce a plain-English
3
+ * summary of what the agent did.
3
4
  *
4
- * At task exit, generates a 3-paragraph plain-English summary of what the
5
- * agent did, what was touched, and what the user should look at. Prints to
6
- * stdout so the dogfood-while-building loop sees the artifact immediately.
5
+ * Engine (locked 2026-06-03, after the real dogfood found NO report on a real
6
+ * conductor run): REUSE THE AGENT SESSION. The agent already narrates what it
7
+ * did (kind:'say' steps — e.g. "Added subtract(a,b) to utils.js:5"); we use that
8
+ * directly. No fresh summarizer spawn.
7
9
  *
8
- * Provider strategy (MVP — try free first, fall back to paid):
9
- * - Tries `claude-free --bare -p "<prompt>"` first. The `claude-free`
10
- * wrapper is what actually routes through the FCC proxy at :8082
11
- * (verified 2026-05-02 dogfood: plain `claude --bare` does NOT route
12
- * through FCC — it hits paid Claude. The May 1 memory rule was wrong.)
13
- * - Falls back to `claude --bare -p "<prompt>"` if `claude-free` is not
14
- * installed (paid path; will fail gracefully if no credit).
15
- * - If neither binary is installed, returns null silently (no report).
10
+ * Why the old path was replaced: it spawned a SEPARATE provider binary chain
11
+ * (`claude-free` → `claude`) to summarize. `claude-free` (the FCC/NIM proxy
12
+ * binary) is on PATH but its proxy was down → 30s hang → null; the `claude`
13
+ * fallback also returned null in the conductor exit-handler. Net: a completed
14
+ * real task showed "⚠ Final Report unavailable" — the headline deliverable
15
+ * missing. Reusing the agent's own words is cheaper, has full context, and can't
16
+ * be taken down by an external proxy.
16
17
  *
17
- * Env override: set CODEHERE_REPORT_BINARY=<binary-name> to force a
18
- * specific provider (e.g. for testing or non-default setups).
19
- *
20
- * Iterations 5-9 (2026-05-02) refuted the "we own the supervisory layer"
21
- * thesis. Final Report is the keystone delivery moment per the v3 chain
22
- * (claim_market_position) — it's the artifact the dogfood loop produces
23
- * that none of the surveyed competitors (ComposioHQ, Overstory, Praktor,
24
- * Claude Squad, AI-Agents-Orchestrator) advertise.
18
+ * Fallback (locked): when the agent left no usable summary (or it looks like a
19
+ * provider failure), build a DETERMINISTIC report from the recorded steps +
20
+ * artifacts. It states ONLY verified facts, says plainly that no written summary
21
+ * was available, and NEVER implies success — so a completed task always shows a
22
+ * deliverable, and it's honest about which kind it is (see `source`).
25
23
  */
26
- import { spawn } from 'child_process';
27
- import { isMockMode } from '../../infrastructure/orchestration/mocks/resolve-agent-spawn.js';
28
- import { mockFinalReport } from '../../infrastructure/orchestration/mocks/mock-synthesis.js';
29
- const PROMPT_TEMPLATE = (i) => `You are a PM reporting back to a developer about what their AI coding agent
30
- just did. Write 3 short paragraphs in plain English. No headers, no bullet
31
- lists, no markdown — just three paragraphs separated by blank lines.
32
-
33
- CRITICAL anti-hallucination rule: ground every claim in the structured data
34
- below. You CANNOT see the contents of files, the output of commands, or
35
- anything beyond what's listed under "Steps the agent took" and "Files
36
- changed". If the data doesn't show specific filenames, do NOT invent any.
37
- If a step ran a command but its output isn't shown to you, describe what
38
- the step ATTEMPTED, not what it found. Saying "I don't have the output to
39
- report on" is correct; making up plausible-sounding details is wrong and
40
- the developer will catch the lie.
41
-
42
- Paragraph 1: What the agent ATTEMPTED. Verb-led. Describe steps as listed.
43
- Mention specific filenames ONLY if they appear in the data
44
- below. 1-3 sentences.
45
- Paragraph 2: What was TOUCHED. Reference ONLY the "Files changed" list
46
- below. If the list is empty AND "File tracking" says git is
47
- available, say "No files were changed." If "File tracking" says
48
- git is NOT available, say codehere could not track file changes
49
- because this folder is not a git repository — do NOT say no files
50
- were changed, because the agent may well have changed files
51
- codehere cannot see. 1-2 sentences.
52
- Paragraph 3: What the developer should LOOK AT. If steps + files data show
53
- nothing risky, say so explicitly ("nothing to verify; the
54
- session was read-only" or similar). 1-2 sentences.
55
-
56
- Be specific where the data lets you. Don't pad. Don't editorialize. Don't
57
- praise the agent. Don't fabricate file names, line counts, or content
58
- descriptions you cannot derive from the data below.
59
-
60
- The task was: ${i.prompt}
61
- The agent used: ${i.agent}
62
- Duration: ${i.durationMs}ms
63
- Status: ${i.status}
64
-
65
- Steps the agent took:
66
- ${i.steps.length ? i.steps.map((s) => `- ${s.label} (${s.state})`).join('\n') : '(no structured steps captured)'}
67
-
68
- File tracking: ${i.artifacts?.noGitTracking ? 'git NOT available (this folder is not a git repository — file changes cannot be tracked)' : 'git available'}
69
-
70
- Files changed:
71
- ${i.artifacts?.files?.length
72
- ? i.artifacts.files
73
- .map((f) => `- ${f.path} (+${f.added}/-${f.removed}${f.trust !== undefined ? `, trust: ${f.trust}` : ''})`)
74
- .join('\n')
75
- : '(no file artifacts captured)'}
76
-
77
- Token usage: ${i.artifacts?.usage?.inputTokens ?? 0} in / ${i.artifacts?.usage?.outputTokens ?? 0} out
78
-
79
- Now write the 3 paragraphs, grounded ONLY in the data above.`;
80
- const REPORT_TIMEOUT_MS = 30_000;
81
- /** Provider chain: try free first, then paid. Override with CODEHERE_REPORT_BINARY. */
82
- function selectProviderBinary() {
83
- const override = process.env.CODEHERE_REPORT_BINARY;
84
- if (override)
85
- return [override];
86
- return ['claude-free', 'claude'];
87
- }
24
+ const MAX_SUMMARY_CHARS = 1600;
88
25
  /**
89
- * UAT-003 — patterns that indicate the wrapped agent's session terminated
90
- * because of a network / auth / quota failure rather than a real task.
91
- * When we see these in the would-be summary text, we prepend a plain-English
92
- * recovery hint instead of letting the raw provider error echo through.
93
- *
94
- * Pre-fix: bare `exit` in REPL → Conductor spawned claude-code → real API
95
- * call failed with "All connection attempts failed Request ID: req_...";
96
- * Final Report quoted that text verbatim. Per Core Values #4 + #6, this is
97
- * unacceptable — the user sees a Request ID that means nothing.
26
+ * UAT-003 — patterns indicating the agent's own text is actually a network /
27
+ * auth / quota failure, not a real summary. Such "summaries" must NOT be shown
28
+ * as the report; they fall through to the deterministic fallback.
98
29
  */
99
30
  const NETWORK_FAILURE_PATTERNS = /(?:all connection attempts failed|ECONNREFUSED|ENOTFOUND|fetch failed|network error|request id:\s*req_|connection timed out|getaddrinfo)/i;
100
31
  function isLikelyNetworkFailureSummary(text) {
101
32
  return NETWORK_FAILURE_PATTERNS.test(text);
102
33
  }
34
+ /**
35
+ * ENGINE: the agent's own narration. kind:'say' steps are the words the agent
36
+ * already emitted (reasoning + wrap-up). Joined newest-context-last, they ARE
37
+ * the summary — no extra spawn. Returns null if the agent said nothing usable.
38
+ */
39
+ function extractAgentSummary(steps) {
40
+ const says = steps
41
+ .filter((s) => s.kind === 'say' && typeof s.label === 'string' && s.label.trim().length > 0)
42
+ .map((s) => s.label.trim());
43
+ if (!says.length)
44
+ return null;
45
+ const text = says.join('\n\n').slice(0, MAX_SUMMARY_CHARS);
46
+ // Too short to be a real summary (e.g. a one-word ack) → let the fallback give facts.
47
+ return text.length >= 12 ? text : null;
48
+ }
49
+ /**
50
+ * FALLBACK: deterministic, honest. Facts only, from steps + artifacts. Says
51
+ * plainly that no written summary was available. Never implies success.
52
+ */
53
+ function buildDeterministicReport(input) {
54
+ const files = input.artifacts?.files ?? [];
55
+ const toolSteps = input.steps.filter((s) => s.kind !== 'say');
56
+ const noGit = input.artifacts?.noGitTracking;
57
+ const agentName = input.agent === 'conductor' ? 'the agent codehere chose' : input.agent;
58
+ const parts = [
59
+ `No plain-English summary was available for this run — ${agentName} didn't leave a written summary, and codehere doesn't fabricate one. Here are the recorded facts.`,
60
+ `The session ran ${input.steps.length} step${input.steps.length === 1 ? '' : 's'}` +
61
+ (toolSteps.length
62
+ ? ` (${toolSteps.slice(0, 5).map((s) => s.label).join(', ')}${toolSteps.length > 5 ? ', …' : ''})`
63
+ : '') +
64
+ `.`,
65
+ ];
66
+ if (noGit) {
67
+ parts.push(`This folder isn't a git repository, so codehere couldn't track which files changed — check the working tree yourself.`);
68
+ }
69
+ else if (files.length) {
70
+ parts.push(`Files changed: ` +
71
+ files
72
+ .map((f) => `${f.path} (+${f.added}/-${f.removed}${f.trust !== undefined ? `, trust ${f.trust}` : ''})`)
73
+ .join('; ') +
74
+ `.`);
75
+ }
76
+ else {
77
+ parts.push(`No files were changed.`);
78
+ }
79
+ return { summary: parts.join('\n\n'), generatedAt: Date.now(), durationMs: 0, source: 'deterministic' };
80
+ }
103
81
  function buildErrorRecoveryReport(input, durationMs) {
104
- // Tailor the hint to the specific failure shape we can detect from input.
105
82
  const agentName = input.agent === 'conductor' ? 'the agent codehere chose' : input.agent;
106
83
  return {
107
84
  summary: `The ${agentName} session ended without completing the task. ` +
@@ -112,114 +89,32 @@ function buildErrorRecoveryReport(input, durationMs) {
112
89
  `codehere couldn't confirm any file changes from this attempt.`,
113
90
  generatedAt: Date.now(),
114
91
  durationMs,
92
+ source: 'error-recovery',
115
93
  };
116
94
  }
117
95
  /**
118
- * Generate a Final Report. Returns null if no provider is reachable.
119
- * Never throws — failure modes return null + log a debug line.
120
- *
121
- * UAT-003 — when the input.status is 'error', we skip the LLM call entirely
122
- * and return a deterministic recovery hint. Saves tokens on a known-failed
123
- * task AND gives the user a plain-English next step instead of waiting for
124
- * a possibly-unhelpful LLM summary.
96
+ * Generate a Final Report. Reuses the agent's own summary; falls back to a
97
+ * deterministic, honest facts report. A completed task ALWAYS gets a report
98
+ * (never null for non-error terminal states) — no external dependency.
125
99
  */
126
100
  export async function generateFinalReport(input) {
127
- // CODEHERE_MOCK — synthesize a grounded report with zero API so the full
128
- // demo flow (incl. this headline deliverable + the verify cross-check) runs
129
- // under mock. Errors still get the deterministic recovery report below.
130
- if (isMockMode() && input.status !== 'error') {
131
- return mockFinalReport(input);
132
- }
133
- // Short-circuit on errored sessions — don't waste tokens summarizing nothing.
101
+ // No mock short-circuit: under CODEHERE_MOCK the SAME engine runs, so the
102
+ // narration gap is reproduced faithfully — claude/aider (emit `say`) get a
103
+ // rich agent-summary; codex/gemini/opencode (no `say`) get the honest
104
+ // deterministic report, exactly as on a real run. (A prior mock-only
105
+ // short-circuit synthesized a report from steps for every agent, hiding the
106
+ // cross-vendor gap — you couldn't dogfood "codex's report is thin" under mock.)
107
+ // Errored session — deterministic recovery hint, no summarization of nothing.
134
108
  if (input.status === 'error') {
135
109
  return buildErrorRecoveryReport(input, 0);
136
110
  }
137
- const prompt = PROMPT_TEMPLATE(input);
138
- for (const binary of selectProviderBinary()) {
139
- const result = await tryProvider(binary, prompt, input.sessionId);
140
- if (result) {
141
- // Defense-in-depth: even on 'completed' status, if the generated
142
- // summary leaks a provider network-failure string (e.g., the wrapped
143
- // agent exited 0 but its assistant text was an error message), wrap
144
- // the summary with a recovery preamble.
145
- if (isLikelyNetworkFailureSummary(result.summary)) {
146
- return {
147
- summary: `Couldn't reach the provider cleanly — the agent's session text looks like a network or auth failure.\n\n` +
148
- `What to try: check your internet, run \`codehere health\`, or rerun in a minute. ` +
149
- `If it persists, switch agents from the dropdown.\n\n` +
150
- `— Raw agent message —\n${result.summary}`,
151
- generatedAt: result.generatedAt,
152
- durationMs: result.durationMs,
153
- };
154
- }
155
- return result;
156
- }
111
+ // ENGINE: reuse the agent's own narration (no fresh spawn).
112
+ const agentSummary = extractAgentSummary(input.steps);
113
+ if (agentSummary && !isLikelyNetworkFailureSummary(agentSummary)) {
114
+ return { summary: agentSummary, generatedAt: Date.now(), durationMs: 0, source: 'agent-summary' };
157
115
  }
158
- return null;
159
- }
160
- function tryProvider(binary, prompt, sessionId) {
161
- const startedAt = Date.now();
162
- return new Promise((resolve) => {
163
- let stdout = '';
164
- let stderr = '';
165
- let settled = false;
166
- const finish = (result) => {
167
- if (settled)
168
- return;
169
- settled = true;
170
- resolve(result);
171
- };
172
- // Provider args per-binary:
173
- // - `claude-free` (FCC proxy binary) needs `--bare` to skip session
174
- // setup that the proxy doesn't implement.
175
- // - Standard `claude` (the Claude Code Max CLI) MUST NOT use `--bare`
176
- // because `--bare` explicitly rejects OAuth and keychain auth — and
177
- // Claude Code Max users authenticate via OAuth. With `--bare`, the
178
- // fallback path returned empty silently, no Final Report ever
179
- // reached the UI, and the dashboard showed `✓ Final Report` on a
180
- // missing artifact. (May 21, 2026 — root cause of CRIT #1.)
181
- const args = binary === 'claude-free' ? ['--bare', '-p', prompt] : ['-p', prompt];
182
- const child = spawn(binary, args, {
183
- stdio: ['ignore', 'pipe', 'pipe'],
184
- });
185
- const timeout = setTimeout(() => {
186
- child.kill('SIGTERM');
187
- if (process.env.CODEHERE_DEBUG === '1') {
188
- console.error(`[final-report] ${binary} timeout after ${REPORT_TIMEOUT_MS}ms for ${sessionId}`);
189
- }
190
- finish(null);
191
- }, REPORT_TIMEOUT_MS);
192
- child.on('error', (err) => {
193
- clearTimeout(timeout);
194
- // ENOENT = binary not installed. Silent fallback to next provider in chain.
195
- if (process.env.CODEHERE_DEBUG === '1') {
196
- console.error(`[final-report] ${binary} unavailable: ${err.message}`);
197
- }
198
- finish(null);
199
- });
200
- child.stdout?.on('data', (chunk) => {
201
- stdout += chunk.toString('utf8');
202
- });
203
- child.stderr?.on('data', (chunk) => {
204
- stderr += chunk.toString('utf8');
205
- });
206
- child.on('exit', (code) => {
207
- clearTimeout(timeout);
208
- if (code !== 0) {
209
- if (process.env.CODEHERE_DEBUG === '1') {
210
- console.error(`[final-report] ${binary} exited ${code}: ${stderr.slice(0, 200)}`);
211
- }
212
- finish(null);
213
- return;
214
- }
215
- const summary = stdout.trim();
216
- if (!summary) {
217
- finish(null);
218
- return;
219
- }
220
- finish({ summary, generatedAt: Date.now(), durationMs: Date.now() - startedAt });
221
- });
222
- });
116
+ // FALLBACK: deterministic, honest — always a deliverable, never a fake ✓.
117
+ return buildDeterministicReport(input);
223
118
  }
224
119
  /**
225
120
  * Pretty-print to terminal. Used by session-manager when a spawned task exits.
@@ -79,7 +79,7 @@ Write the paragraph now. Do not preface with "Update:" or anything similar.
79
79
  Plain prose. No "I'm currently…" — narrate the AGENT, not yourself.
80
80
  `;
81
81
  function templateFile() {
82
- return join(homedir(), '.codehere', 'narrator-prompt.md');
82
+ return join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'narrator-prompt.md');
83
83
  }
84
84
  /** Read the user-customized template if present and valid. Falls back to
85
85
  * baked-in default on missing file, parse failure, or missing required
package/dist/index.js CHANGED
@@ -661,8 +661,8 @@ program
661
661
  try {
662
662
  const { migrate } = await import('./infrastructure/storage/project-thread-migration.js');
663
663
  const result = migrate({
664
- sourceJsonl: join(homedir(), '.codehere/sessions.jsonl'),
665
- targetDir: join(homedir(), '.codehere/projects'),
664
+ sourceJsonl: join(process.env.CODEHERE_HOME || homedir(), '.codehere/sessions.jsonl'),
665
+ targetDir: join(process.env.CODEHERE_HOME || homedir(), '.codehere/projects'),
666
666
  });
667
667
  if (result.threadsWritten > 0) {
668
668
  console.log(colors.dim(` v0.6 migration: created ${result.threadsWritten} project thread(s) from ${result.sessionsProcessed} sessions (${result.durationMs}ms)`));
@@ -765,8 +765,12 @@ program
765
765
  console.log(colors.dim(` version: ${existing.version}`));
766
766
  }
767
767
  newline();
768
- console.log(colors.dim(' Opening browser...'));
769
- await openBrowser();
768
+ // Honor --no-open here too — found via the v1 stranger walk: a second
769
+ // `start --no-open` against a running daemon popped a browser anyway.
770
+ if (shouldOpen) {
771
+ console.log(colors.dim(' Opening browser...'));
772
+ await openBrowser();
773
+ }
770
774
  process.exit(0);
771
775
  }
772
776
  console.error(error(`Port ${port} is in use by another process.`));
@@ -947,8 +951,8 @@ program
947
951
  program
948
952
  .command('migrate-threads', { hidden: true }) // hidden from --help (v0.6.0 internal — auto-runs on daemon start once wired)
949
953
  .description('Migrate sessions.jsonl into per-project thread.jsonl files (v0.6.0 schema)')
950
- .option('--source <path>', 'Source sessions.jsonl', `${homedir()}/.codehere/sessions.jsonl`)
951
- .option('--target <dir>', 'Target projects dir', `${homedir()}/.codehere/projects`)
954
+ .option('--source <path>', 'Source sessions.jsonl', `${process.env.CODEHERE_HOME || homedir()}/.codehere/sessions.jsonl`)
955
+ .option('--target <dir>', 'Target projects dir', `${process.env.CODEHERE_HOME || homedir()}/.codehere/projects`)
952
956
  .option('--dry-run', 'Report what migrate WOULD do, write nothing', false)
953
957
  .option('--rollback', 'Remove the projects dir; sessions.jsonl untouched', false)
954
958
  .option('--keep-temp-dirs', 'Include /tmp + /var/folders cwds (default skips them)', false)
@@ -29,7 +29,7 @@ const DEFAULT_CONFIG = {
29
29
  warnAtPct: 70,
30
30
  };
31
31
  function configFile() {
32
- return join(homedir(), '.codehere', 'budget.yaml');
32
+ return join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'budget.yaml');
33
33
  }
34
34
  /** Parse a tiny subset of YAML — `key: value` pairs, # comments, blank lines.
35
35
  * No nesting, no lists, no multi-line. Same approach as permissions-config:
@@ -25,7 +25,7 @@ import { join } from 'path';
25
25
  /** Resolved at call time, not module load, so test setups that swap $HOME
26
26
  * after import still get a fresh path on each invocation. */
27
27
  function sessionsFile() {
28
- return join(homedir(), '.codehere', 'sessions.jsonl');
28
+ return join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'sessions.jsonl');
29
29
  }
30
30
  /** USD per million OUTPUT tokens at the agent's typical 2026 tier. Input is
31
31
  * estimated at 1/5 of output rate (Anthropic + OpenAI both publish ~5x
@@ -396,8 +396,14 @@ export function calculateRiskScore(findings) {
396
396
  /**
397
397
  * Audit log path — defaults to ~/.codehere/audit/hook-events.jsonl.
398
398
  * Override at process start with CODEHERE_AUDIT_FILE env var (used by tests, optional for users).
399
+ * Honors CODEHERE_HOME like sessions/config/threads — found via the v1 stranger
400
+ * walk: an "isolated" daemon (verify-all's :3779, the walk's scratch home) was
401
+ * writing mock audit entries into the REAL ~/.codehere/audit while verify-all
402
+ * printed "Real ~/.codehere untouched" (the footer showed 162 entries · 51
403
+ * agents in a brand-new home — all of them pollution from past mock runs).
399
404
  */
400
- export const AUDIT_FILE = process.env.CODEHERE_AUDIT_FILE || join(homedir(), '.codehere', 'audit', 'hook-events.jsonl');
405
+ export const AUDIT_FILE = process.env.CODEHERE_AUDIT_FILE ||
406
+ join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'audit', 'hook-events.jsonl');
401
407
  const GENESIS_PREV_HASH = '0'.repeat(64);
402
408
  /**
403
409
  * Canonicalize an audit entry for hashing. We sort keys alphabetically and
@@ -15,7 +15,7 @@
15
15
  import { existsSync, mkdirSync, writeFileSync } from 'fs';
16
16
  import { homedir } from 'os';
17
17
  import { join, dirname } from 'path';
18
- const CONFIG_PATH = join(homedir(), '.codehere', 'agents.yaml');
18
+ const CONFIG_PATH = join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'agents.yaml');
19
19
  const DEFAULT_YAML = `# codehere — pluggable agent adapters
20
20
  #
21
21
  # Built-in adapters (no configuration needed — just install the CLI):
@@ -342,7 +342,10 @@ export function spawnAider(prompt, options = {}) {
342
342
  return new Promise((resolve, reject) => {
343
343
  let stdout = '';
344
344
  let stderr = '';
345
- const followup = spawn('aider', ['--restore-chat-history', '--message', text, ...HEADLESS_FLAGS], {
345
+ // Route follow-ups through the mock seam too — under CODEHERE_MOCK=1 this
346
+ // 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);
348
+ const followup = spawn(sp2.command, sp2.args, {
346
349
  cwd,
347
350
  env: spawnEnv,
348
351
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -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`);
@@ -384,7 +384,11 @@ export function spawnClaudeCode(prompt, options = {}) {
384
384
  }
385
385
  let stdout = '';
386
386
  let stderr = '';
387
- const followup = spawn('claude', ['-p', text, '--resume', claudeSessionId, ...HEADLESS_FLAGS], {
387
+ // Route follow-ups through the mock seam too — under CODEHERE_MOCK=1 this
388
+ // hits the mock REPL, not the real binary. (Was a real-spend leak:
389
+ // appendMessage spawned `claude` directly regardless of mock mode.)
390
+ const sp2 = resolveAgentSpawn('claude-code', 'claude', ['-p', text, '--resume', claudeSessionId, ...HEADLESS_FLAGS], text);
391
+ const followup = spawn(sp2.command, sp2.args, {
388
392
  cwd,
389
393
  env: spawnEnv,
390
394
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -456,15 +456,10 @@ export function spawnCodex(prompt, options = {}) {
456
456
  let stdout = '';
457
457
  let stderr = '';
458
458
  // codex exec resume <SESSION_ID> - → reads prompt from stdin
459
- const followup = spawn('codex', [
460
- 'exec',
461
- '--json',
462
- '--skip-git-repo-check',
463
- '--dangerously-bypass-approvals-and-sandbox',
464
- 'resume',
465
- codexThreadId,
466
- '-',
467
- ], {
459
+ // Route follow-ups through the mock seam too — under CODEHERE_MOCK=1 this
460
+ // 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);
462
+ const followup = spawn(sp2.command, sp2.args, {
468
463
  cwd,
469
464
  env: spawnEnv,
470
465
  stdio: ['pipe', 'pipe', 'pipe'],
@@ -53,9 +53,15 @@ const ROUTING_RULES = [
53
53
  reason: 'targeted fixes favor Aider',
54
54
  },
55
55
  {
56
- triggers: ['scaffold', 'generate', 'create new', 'bootstrap', 'set up'],
57
- preferred: ['opencode', 'claude-code', 'codex'],
58
- reason: 'greenfield generation favors OpenCode',
56
+ // Narrowed 2026-06-02 (root-cause fix for "randomly uses opencode"): bare
57
+ // 'generate'/'set up' are far too common ("generate a report", "set up the
58
+ // config") and were silently routing ordinary tasks to opencode. AND
59
+ // claude-code now leads — opencode has NO mid-stream visibility (buffered
60
+ // output), so it must never be the surprise default for a "watch it work"
61
+ // product; it stays available via the explicit agent picker.
62
+ triggers: ['scaffold', 'bootstrap', 'create a new project', 'greenfield', 'new project from scratch'],
63
+ preferred: ['claude-code', 'codex', 'opencode'],
64
+ reason: 'greenfield scaffolding — claude-code default (visible); opencode opt-in only',
59
65
  },
60
66
  {
61
67
  triggers: ['add tests', 'write tests', 'test coverage', 'unit test', 'integration test'],
@@ -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.
@@ -38,8 +38,8 @@ import { randomUUID } from 'crypto';
38
38
  import { ProgressAggregator } from './progress-aggregator.js';
39
39
  import { snapshotCwd, extractArtifacts } from './artifacts-extractor.js';
40
40
  import { applyMemoryPrefix } from './memory-injector.js';
41
- const CONFIG_PATH = join(homedir(), '.codehere', 'agents.yaml');
42
- const CONFIG_PATH_JSON = join(homedir(), '.codehere', 'agents.json');
41
+ const CONFIG_PATH = join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'agents.yaml');
42
+ const CONFIG_PATH_JSON = join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'agents.json');
43
43
  /** Minimal YAML parser for the narrow shape above. Intentionally NOT a full YAML
44
44
  * parser — avoids dragging in a dep. Handles: `key: value`, lists with `- key: val`,
45
45
  * nested lists of simple strings via `[a, b]`, comments after `#`. Errors loud.
@@ -185,24 +185,29 @@ export function loadRecentContextPack(cwd) {
185
185
  * Returns the original prompt if graphify-ts fails or is unavailable.
186
186
  *
187
187
  * Respects CODEHERE_GRAPHIFY env var:
188
- * - unset or "auto" (default): compile on spawn if graph doesn't exist
189
- * - "force": always compile, even if pack exists
190
- * - "off": skip entirely
188
+ * - unset or "auto" (default): enrich ONLY from a pre-existing graph/pack;
189
+ * NEVER compile on the spawn hot path (root-cause fix 2026-06-02 for
190
+ * "every task calls graphify" — `npx graphify-ts pack` was shelling out
191
+ * synchronously on every spawn, slowing crucial tasks + prepending a
192
+ * verbose context blob the user never asked for).
193
+ * - "force": compile a fresh pack on spawn (explicit opt-in for the cost).
194
+ * - "off": skip entirely.
191
195
  */
192
196
  export function enrichPromptWithPackSync(prompt, cwd, { force = false } = {}) {
193
197
  const mode = process.env.CODEHERE_GRAPHIFY ?? 'auto';
194
198
  if (mode === 'off')
195
199
  return prompt;
196
- // Check for existing pack to avoid recompiling on every spawn
197
- const existingPack = loadRecentContextPack(cwd);
198
- const graphPath = getGraphPath(cwd);
199
- const graphExists = existsSync(graphPath);
200
- if (!force && existingPack && graphExists) {
201
- // Reuse existing pack — just enrich the prompt with a compact reference
202
- const reference = `[graphify-ts context pack: ${existingPack.nodeCount} nodes, ${existingPack.edgeCount} edges, pack=${existingPack.packId}]`;
203
- return `[graphify context: ${reference} — agent should call retrieve() for full pack]\n\n${prompt}`;
204
- }
205
- // Compile a fresh pack
200
+ const forceCompile = force || mode === 'force';
201
+ // 'auto' must NEVER touch the user's prompt — not by compiling (npx subprocess
202
+ // on the hot path) AND not by prepending an existing pack's "[graphify context:
203
+ // …]" reference blob. That blob leaked into the visible prompt, the Final
204
+ // Report summary, and the conversation ("On it — [graphify context: 1017
205
+ // nodes…]") — user-reported 2026-06-02 with a graph present. Structural
206
+ // context enrichment is now 'force'-only opt-in; a normal task gets a clean prompt.
207
+ if (!forceCompile)
208
+ return prompt;
209
+ // Compile a fresh pack (force only)
210
+ // Compile a fresh pack (force only)
206
211
  const pack = compileContextPackSync(prompt, cwd, { task: 'explain', budget: 2500 });
207
212
  if (!pack) {
208
213
  return prompt;
@@ -29,7 +29,7 @@
29
29
  * AGENTS.md / session.md). Same byte-equality-based detection pattern as
30
30
  * memory-command.ts (per feedback_implementation_patterns_v05.md Pattern #1).
31
31
  */
32
- import { existsSync, readFileSync, realpathSync } from 'fs';
32
+ import { existsSync, readFileSync, realpathSync, writeFileSync } from 'fs';
33
33
  import { join, dirname, resolve } from 'path';
34
34
  const DEFAULT_MAX_TOKENS = 3000;
35
35
  const DEFAULT_MAX_LESSONS = 10;
@@ -279,8 +279,11 @@ export function buildMemoryPrefix(projectRoot, options = {}) {
279
279
  *
280
280
  * Stops at the filesystem root. Symlink-following on read is the right
281
281
  * asymmetry per ADR-012 (the symlink defense applies to writes, not reads).
282
+ *
283
+ * Exported for the daemon's /api/memory/status route (the room's init nudge
284
+ * must report the SAME readiness the injector itself would act on).
282
285
  */
283
- function findProjectRoot(startPath) {
286
+ export function findProjectRoot(startPath) {
284
287
  let current;
285
288
  try {
286
289
  current = realpathSync(resolve(startPath));
@@ -329,4 +332,49 @@ export function applyMemoryPrefix(prompt, cwd, options) {
329
332
  record: { included: r.included, tokens: r.tokens, truncated: r.truncated },
330
333
  };
331
334
  }
335
+ /**
336
+ * Write-back side of the shared-room hand-off: append a finished turn's outcome
337
+ * to the project's `working/session.md` so the NEXT agent spawned in this
338
+ * project reads it via buildMemoryPrefix() — closing the loop ("agents take
339
+ * turns in a shared room, each building on the last").
340
+ *
341
+ * Resolves the SAME project root as the read path (findProjectRoot), so it only
342
+ * writes when the project is codehere-initialized — it never creates `.codehere/`
343
+ * in an uninitialized repo. The scratch is capped (the injector also budgets it).
344
+ * Best-effort: a memory-write failure must never break a run.
345
+ */
346
+ export function recordTurnToWorkingMemory(cwd, entry) {
347
+ try {
348
+ const projectRoot = findProjectRoot(cwd);
349
+ if (projectRoot === null)
350
+ return; // not a codehere-initialized project
351
+ const dir = join(projectRoot, '.codehere', 'memory', 'working');
352
+ if (!existsSync(dir))
353
+ return;
354
+ const p = join(dir, 'session.md');
355
+ const HEADER = '# Working session log - codehere auto-records each agent turn so the next one picks up here\n';
356
+ const agent = (entry.agent || 'agent').replace(/\s+/g, ' ').trim().slice(0, 40);
357
+ const prompt = (entry.prompt || '').replace(/\s+/g, ' ').trim().slice(0, 120);
358
+ const summary = (entry.summary || '').replace(/\s+/g, ' ').trim().slice(0, 600);
359
+ const files = entry.files ? ` - ${entry.files} file${entry.files === 1 ? '' : 's'} changed` : '';
360
+ const block = `\n## ${agent}${prompt ? ' - ' + prompt : ''}${files}\n${summary || '(no written summary)'}\n`;
361
+ let cur = '';
362
+ try {
363
+ cur = existsSync(p) ? readFileSync(p, 'utf8') : '';
364
+ }
365
+ catch {
366
+ cur = '';
367
+ }
368
+ if (!cur.trim() || looksLikeInitSeed(cur, 'session'))
369
+ cur = HEADER;
370
+ cur += block;
371
+ const CAP = 8000;
372
+ if (cur.length > CAP)
373
+ cur = HEADER + cur.slice(cur.length - (CAP - HEADER.length));
374
+ writeFileSync(p, cur, 'utf8');
375
+ }
376
+ catch {
377
+ /* never break a run on a memory-write failure */
378
+ }
379
+ }
332
380
  //# sourceMappingURL=memory-injector.js.map