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.
- package/dist/application/use-cases/generate-final-report.js +86 -191
- package/dist/application/use-cases/narrator-prompt-template.js +1 -1
- package/dist/index.js +10 -6
- package/dist/infrastructure/cost/budget-config.js +1 -1
- package/dist/infrastructure/cost/spend-aggregator.js +1 -1
- package/dist/infrastructure/hooks/claude-sdk-hook.js +7 -1
- package/dist/infrastructure/orchestration/agents-config-seed.js +1 -1
- package/dist/infrastructure/orchestration/aider-adapter.js +4 -1
- package/dist/infrastructure/orchestration/cassette-recorder.js +1 -1
- package/dist/infrastructure/orchestration/claude-code-adapter.js +5 -1
- package/dist/infrastructure/orchestration/codex-adapter.js +4 -9
- package/dist/infrastructure/orchestration/conductor-adapter.js +9 -3
- package/dist/infrastructure/orchestration/failover-config.js +1 -1
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +2 -2
- package/dist/infrastructure/orchestration/graphify-context.js +18 -13
- package/dist/infrastructure/orchestration/memory-injector.js +50 -2
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +25 -25
- package/dist/infrastructure/orchestration/progress-aggregator.js +41 -6
- package/dist/infrastructure/orchestration/session-manager.js +18 -0
- package/dist/infrastructure/orchestration/session-persistence.js +2 -2
- package/dist/infrastructure/protocols/cloud-sync.js +2 -2
- package/dist/infrastructure/protocols/mcp-server-impl.js +66 -2
- package/dist/infrastructure/protocols/ui-page-v2.js +260 -40
- package/dist/infrastructure/protocols/verify-page.js +42 -0
- package/dist/infrastructure/storage/project-thread-mirror.js +1 -1
- package/dist/infrastructure/storage/project-thread-reader.js +1 -1
- package/dist/infrastructure/storage/project-thread-title-backfill.js +1 -1
- package/dist/presentation/cli/commands/init-command.js +66 -55
- package/package.json +2 -1
|
@@ -1,107 +1,84 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Final Report —
|
|
2
|
+
* Final Report — the headline deliverable. At task exit, produce a plain-English
|
|
3
|
+
* summary of what the agent did.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
-
|
|
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
|
|
90
|
-
*
|
|
91
|
-
*
|
|
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.
|
|
119
|
-
*
|
|
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
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
//
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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
|
-
|
|
769
|
-
|
|
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 ||
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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):
|
|
189
|
-
*
|
|
190
|
-
*
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|