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
@@ -15,33 +15,30 @@
15
15
  * runs end-to-end. For replaying REAL captured runs, that's the cassette system.
16
16
  */
17
17
  import { existsSync, readFileSync, writeFileSync } from 'fs';
18
- import { join, isAbsolute, basename } from 'path';
18
+ import { join } from 'path';
19
19
  function argVal(flag) {
20
20
  const i = process.argv.indexOf(flag);
21
21
  return i >= 0 && i + 1 < process.argv.length ? process.argv[i + 1] : undefined;
22
22
  }
23
- /** The file the mock claims to edit — derived from the prompt, used for both
24
- * the emitted step AND the real on-disk edit so they stay consistent. */
25
- function targetFile(p) {
26
- return /(\b[\w./-]+\.(ts|tsx|js|py|go|rs|md))\b/.exec(p)?.[1] || 'src/feature.ts';
27
- }
28
- /** CODEHERE_MOCK fidelity: actually apply a small, safe edit so the REAL
29
- * artifacts pipeline (git diff → trust score → Final Report → verify) has a
30
- * genuine change to report. The demo's "1 file changed · trust" stays honest —
31
- * it is a real diff on disk, just authored by the mock, not a model. */
32
- function materializeEdit(file) {
23
+ /** The single dedicated file the mock edits — claimed in the step AND written on
24
+ * disk, so they stay consistent. ALWAYS a mock-only file, NEVER an existing real
25
+ * source file. The prior version derived a path from the prompt and appended to
26
+ * it; when the daemon's cwd was a real repo and the prompt carried injected
27
+ * memory (which lists repo paths like `src/ui.ts`), it corrupted real tracked
28
+ * source. gitignored so it never rides along. */
29
+ const MOCK_EDIT_FILE = 'codehere-mock-change.md';
30
+ /** CODEHERE_MOCK fidelity: apply a small, safe on-disk edit so the run has a
31
+ * genuine change — WITHOUT touching real source. Writes only to the dedicated
32
+ * MOCK_EDIT_FILE. Note: that file is gitignored, so INSIDE a git repo the
33
+ * artifacts extractor (git ls-files --others --exclude-standard) won't count it
34
+ * — but verify-all runs the mock in a NON-git sandbox where file counts don't
35
+ * apply, and the polished demo uses seeded sessions, not live mock edits. */
36
+ function materializeEdit() {
33
37
  try {
34
- const cwd = process.cwd();
35
- let target = isAbsolute(file) ? file : join(cwd, file);
36
- if (!target.startsWith(cwd))
37
- target = join(cwd, basename(file)); // no traversal
38
- const marker = `\n// codehere(mock): change applied for "${prompt.slice(0, 60).replace(/"/g, "'")}"\n`;
39
- if (existsSync(target)) {
40
- writeFileSync(target, readFileSync(target, 'utf-8') + marker);
41
- }
42
- else if (!file.includes('/')) {
43
- writeFileSync(target, marker.trimStart()); // top-level only — never mkdir
44
- }
38
+ const target = join(process.cwd(), MOCK_EDIT_FILE);
39
+ const header = '# codehere mock scratch — synthetic edits under CODEHERE_MOCK (safe to delete)\n\n';
40
+ const marker = `codehere(mock): change applied for "${prompt.slice(0, 60).replace(/"/g, "'")}"\n`;
41
+ writeFileSync(target, (existsSync(target) ? readFileSync(target, 'utf-8') : header) + marker);
45
42
  }
46
43
  catch {
47
44
  /* best-effort — mock fidelity must never crash the run */
@@ -74,7 +71,7 @@ const out = (o) => process.stdout.write(JSON.stringify(o) + '\n');
74
71
  // for the execution path to produce real steps/artifacts without an API call.
75
72
  function plannedSteps(p) {
76
73
  const lc = p.toLowerCase();
77
- const file = targetFile(p);
74
+ const file = MOCK_EDIT_FILE;
78
75
  const steps = [
79
76
  { tool: 'Read', input: { file_path: 'REPO_MAP.md' }, result: 'context loaded · 84 files indexed' },
80
77
  { tool: 'Edit', input: { file_path: file }, result: '+' + (20 + (p.length % 90)) + ' lines · change applied' },
@@ -197,14 +194,17 @@ async function runOpencode() {
197
194
  out({ type: 'tool_use', part: { tool, state: { input: s.input, output: s.result, status: 'completed' } } });
198
195
  }
199
196
  await sleep(120);
200
- out({ type: 'text', text: 'Completed (mock): ' + prompt });
197
+ // Real opencode carries narrative text in part.text (the adapter's summary
198
+ // extraction and the aggregator's say capture both read that shape) — the
199
+ // prior top-level `text` field was mock drift no real parser ever saw.
200
+ out({ type: 'text', part: { text: 'Completed (mock): ' + prompt } });
201
201
  }
202
202
  async function main() {
203
203
  if (!prompt)
204
204
  prompt = (await readStdin()) || 'task';
205
205
  // Apply the real on-disk edit before emitting the run so the artifacts
206
206
  // extractor (which diffs at exit) sees a genuine change.
207
- materializeEdit(targetFile(prompt));
207
+ materializeEdit();
208
208
  switch (agent) {
209
209
  case 'claude-code':
210
210
  case 'claude':
@@ -169,6 +169,14 @@ export class ProgressAggregator {
169
169
  // real gap surfaced by the mock harness). Build a step on the terminal
170
170
  // (status==='completed') event so opencode is a first-class agent.
171
171
  ingestOpencode(type, data) {
172
+ // OpenCode's narrative text rides `text` events with part.text (the same
173
+ // shape the adapter's summary extraction reads) — cross-vendor say capture
174
+ // per the v1 spec, so opencode's Final Report stops degrading to the
175
+ // deterministic fallback.
176
+ if (type === 'text') {
177
+ const part = (data.part ?? {});
178
+ return this.pushSay(String(part.text ?? ''));
179
+ }
172
180
  if (type !== 'tool_use')
173
181
  return false;
174
182
  const part = (data.part ?? {});
@@ -213,6 +221,24 @@ export class ProgressAggregator {
213
221
  const idx = this._steps.push({ label, state, startedAt: Date.now() }) - 1;
214
222
  return idx;
215
223
  }
224
+ /** The agent's own narration as a kind:'say' step — the "watch it work"
225
+ * signal AND what the Final Report engine reuses as the summary. Cross-
226
+ * vendor by decision of the v1 spec (the room thread is "tool steps woven
227
+ * with the agent's plain-English narration" — for every agent, not just
228
+ * claude/aider). Same 500-char cap as the original claude capture. */
229
+ pushSay(text) {
230
+ const said = String(text ?? '').trim();
231
+ if (!said)
232
+ return false;
233
+ this._steps.push({
234
+ kind: 'say',
235
+ label: said.length > 500 ? said.slice(0, 500) + '\\u2026' : said,
236
+ state: 'done',
237
+ startedAt: Date.now(),
238
+ completedAt: Date.now(),
239
+ });
240
+ return true;
241
+ }
216
242
  ingestClaude(type, data) {
217
243
  if (type === 'assistant') {
218
244
  const msg = (data.message ?? {});
@@ -322,6 +348,12 @@ export class ProgressAggregator {
322
348
  return true;
323
349
  }
324
350
  }
351
+ // Gemini's narrative text rides `content` events — same cross-vendor say
352
+ // capture as claude/aider/codex (v1 spec: narration woven into the thread,
353
+ // and the Final Report engine reuses it as the summary).
354
+ if (type === 'content' && typeof data.text === 'string') {
355
+ return this.pushSay(data.text);
356
+ }
325
357
  return false;
326
358
  }
327
359
  /**
@@ -642,12 +674,15 @@ export class ProgressAggregator {
642
674
  });
643
675
  return true;
644
676
  }
645
- // NOTE (mock-currency 2026-06-02): codex `agent_message` is intentionally NOT
646
- // captured here — a prior decision (pm-layers.test.ts "just chatter, not a
647
- // step") drops it. This is INCONSISTENT with ingestClaude/ingestAider, which
648
- // DO capture agent narration as kind:'say' (per #45 + A1 "watch it work").
649
- // Resolving that inconsistency is a product taste call → flagged for Ega in
650
- // mock-currency-2026-06-02.md, not changed unilaterally by the autonomous loop.
677
+ // agent_message = codex's own narration. Captured as kind:'say' since the
678
+ // v1 spec settled the taste call that used to be flagged here (pm-layers
679
+ // "just chatter" decision): the room thread is "tool steps woven with the
680
+ // agent's plain-English narration" for EVERY vendor, and the Final Report
681
+ // engine reuses 'say' steps as the summary — without this, codex always
682
+ // degraded to the deterministic fallback.
683
+ if (itemType === 'agent_message') {
684
+ return this.pushSay(String(item.text ?? ''));
685
+ }
651
686
  return false;
652
687
  }
653
688
  }
@@ -20,6 +20,7 @@ import { OpenCodeAdapter } from './opencode-adapter.js';
20
20
  import { GeminiAdapter } from './gemini-adapter.js';
21
21
  import { buildGenericAdapter, loadAgentsConfig } from './generic-cli-adapter.js';
22
22
  import { buildConductorAdapter } from './conductor-adapter.js';
23
+ import { recordTurnToWorkingMemory } from './memory-injector.js';
23
24
  import { seedAgentsConfig } from './agents-config-seed.js';
24
25
  import { AUDIT_FILE } from '../hooks/claude-sdk-hook.js';
25
26
  import { appendSpawned, appendCompleted, appendFinalReport, loadRecent, } from './session-persistence.js';
@@ -308,7 +309,22 @@ export class SessionManager {
308
309
  summary: report.summary,
309
310
  generatedAt: report.generatedAt,
310
311
  durationMs: report.durationMs,
312
+ source: report.source,
311
313
  });
314
+ // Shared-room hand-off: record this turn's outcome to the project's
315
+ // working memory so the NEXT agent spawned here reads it (closes the
316
+ // loop with applyMemoryPrefix). Best-effort; only initialized projects.
317
+ try {
318
+ recordTurnToWorkingMemory(cwd, {
319
+ agent: session.agent,
320
+ prompt: session.prompt,
321
+ summary: report.summary,
322
+ files: session.artifacts?.files?.length,
323
+ });
324
+ }
325
+ catch {
326
+ /* memory write must never break the run */
327
+ }
312
328
  try {
313
329
  printFinalReportToTerminal(reportInput, report);
314
330
  }
@@ -473,6 +489,7 @@ export class SessionManager {
473
489
  summary: report.summary,
474
490
  generatedAt: report.generatedAt,
475
491
  durationMs: report.durationMs,
492
+ source: report.source,
476
493
  });
477
494
  try {
478
495
  printFinalReportToTerminal(reportInput, report);
@@ -669,6 +686,7 @@ export class SessionManager {
669
686
  summary: report.summary,
670
687
  generatedAt: report.generatedAt,
671
688
  durationMs: report.durationMs,
689
+ source: report.source,
672
690
  });
673
691
  try {
674
692
  printFinalReportToTerminal(reportInput, report);
@@ -151,7 +151,7 @@ export function loadSessionById(idOrPrefix) {
151
151
  steps: c.steps,
152
152
  artifacts: c.artifacts,
153
153
  ...(c.turns ? { turns: c.turns } : {}),
154
- ...(fr ? { finalReport: { summary: fr.summary, generatedAt: fr.generatedAt, durationMs: fr.durationMs } } : {}),
154
+ ...(fr ? { finalReport: { summary: fr.summary, generatedAt: fr.generatedAt, durationMs: fr.durationMs, source: fr.source } } : {}),
155
155
  ...(failoverFrom ? { failoverFrom } : {}),
156
156
  ...(failoverTo ? { failoverTo } : {}),
157
157
  ...(failoverReason ? { failoverReason } : {}),
@@ -222,7 +222,7 @@ export function loadRecent(limit = 20) {
222
222
  steps: c.steps,
223
223
  artifacts: c.artifacts,
224
224
  ...(c.turns ? { turns: c.turns } : {}),
225
- ...(fr ? { finalReport: { summary: fr.summary, generatedAt: fr.generatedAt, durationMs: fr.durationMs } } : {}),
225
+ ...(fr ? { finalReport: { summary: fr.summary, generatedAt: fr.generatedAt, durationMs: fr.durationMs, source: fr.source } } : {}),
226
226
  ...((c.failoverFrom ?? s.failoverFrom) ? { failoverFrom: c.failoverFrom ?? s.failoverFrom } : {}),
227
227
  ...((c.failoverTo ?? s.failoverTo) ? { failoverTo: c.failoverTo ?? s.failoverTo } : {}),
228
228
  ...((c.failoverReason ?? s.failoverReason) ? { failoverReason: c.failoverReason ?? s.failoverReason } : {}),
@@ -16,8 +16,8 @@ import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'fs';
16
16
  import { join, dirname } from 'path';
17
17
  import { homedir, hostname } from 'os';
18
18
  const CLOUD_URL = 'https://codehere.uk/api/sync';
19
- const TOKEN_PATH = join(homedir(), '.codehere', 'cloud-token');
20
- const SESSIONS_FILE = join(homedir(), '.codehere', 'sessions.jsonl');
19
+ const TOKEN_PATH = join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'cloud-token');
20
+ const SESSIONS_FILE = join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'sessions.jsonl');
21
21
  // Lazy-loaded session manager — imported once to avoid circular deps
22
22
  let _sessionManager = null;
23
23
  async function getSessionManager() {
@@ -46,6 +46,8 @@ import { begin as oauthBegin, complete as oauthComplete, getProvider as getOAuth
46
46
  import { saveToken, clearToken, connectionStatus } from '../context/oauth-store.js';
47
47
  import { SessionManager, availableAgents, readinessForAllAgents, getAdapter, allAdapters, } from '../orchestration/session-manager.js';
48
48
  import { isAbsolute } from 'path';
49
+ import { findProjectRoot } from '../orchestration/memory-injector.js';
50
+ import { scaffoldProjectAt } from '../../presentation/cli/commands/init-command.js';
49
51
  import { readCrossToolAgents } from '../storage/cross-tool-agents.js';
50
52
  import { listProjects, readProjectThread } from '../storage/project-thread-reader.js';
51
53
  import { mirrorReplyToThread, renameProject } from '../storage/project-thread-mirror.js';
@@ -444,7 +446,9 @@ export function startMCPServer(options = {}) {
444
446
  // Health check
445
447
  if (req.method === 'GET' && req.url === '/health') {
446
448
  res.writeHead(200, { 'Content-Type': 'application/json' });
447
- res.end(JSON.stringify({ status: 'ok', server: 'codehere-mcp', version: getVersion(), tools: TOOLS.length }));
449
+ // `mock` lets a no-spend tool (verify-all) confirm a daemon is mock BEFORE
450
+ // attaching — so it can refuse to drive a LIVE daemon while claiming "no spend".
451
+ res.end(JSON.stringify({ status: 'ok', server: 'codehere-mcp', version: getVersion(), tools: TOOLS.length, mock: process.env.CODEHERE_MOCK === '1' }));
448
452
  return;
449
453
  }
450
454
  // v2 Task-Centric PM UI (primary — served at /)
@@ -543,7 +547,7 @@ export function startMCPServer(options = {}) {
543
547
  // Tails sessions.jsonl and pushes "change" events so the /v2 UI can
544
548
  // re-fetch task state instantly instead of waiting 8s for the next poll.
545
549
  // Same pattern as /events (audit log tail) but for sessions.
546
- const SESSIONS_FILE = join(homedir(), '.codehere', 'sessions.jsonl');
550
+ const SESSIONS_FILE = join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'sessions.jsonl');
547
551
  if (req.method === 'GET' && req.url === '/api/events/sessions') {
548
552
  res.writeHead(200, {
549
553
  'Content-Type': 'text/event-stream',
@@ -1209,6 +1213,66 @@ export function startMCPServer(options = {}) {
1209
1213
  }
1210
1214
  return;
1211
1215
  }
1216
+ // ── v1 project-memory routes (the room's init nudge) ─────────────────────
1217
+ // Found by the stranger walk: web-only users never run `codehere init`, so
1218
+ // the spec's "agent remembers the project" half of nothing-lost silently
1219
+ // doesn't apply. Status reports the SAME readiness the injector acts on
1220
+ // (shared findProjectRoot); init runs the SAME hardened scaffold the CLI
1221
+ // does (shared scaffoldProjectAt — symlink defenses included).
1222
+ if (req.method === 'GET' && req.url?.startsWith('/api/memory/status')) {
1223
+ try {
1224
+ const u = new URL(req.url, `http://${req.headers.host || '127.0.0.1'}`);
1225
+ const cwd = u.searchParams.get('cwd') || '';
1226
+ if (!cwd || !isAbsolute(cwd) || cwd.includes('\0')) {
1227
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1228
+ res.end(JSON.stringify({ error: 'cwd (absolute path) required' }));
1229
+ return;
1230
+ }
1231
+ const root = findProjectRoot(cwd);
1232
+ const ready = !!root && existsSync(join(root, '.codehere', 'memory', 'working'));
1233
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1234
+ res.end(JSON.stringify({ ready, projectRoot: root }));
1235
+ }
1236
+ catch (e) {
1237
+ res.writeHead(500, { 'Content-Type': 'application/json' });
1238
+ res.end(JSON.stringify({ error: 'memory_status_failed', detail: String(e) }));
1239
+ }
1240
+ return;
1241
+ }
1242
+ if (req.method === 'POST' && req.url === '/api/memory/init') {
1243
+ (async () => {
1244
+ try {
1245
+ const body = await readBody(req, res);
1246
+ let cwd = '';
1247
+ try {
1248
+ cwd = String(JSON.parse(body || '{}').cwd || '');
1249
+ }
1250
+ catch {
1251
+ /* falls through to the validation below */
1252
+ }
1253
+ if (!cwd || !isAbsolute(cwd) || cwd.includes('\0') || !existsSync(cwd) || !statSync(cwd).isDirectory()) {
1254
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1255
+ res.end(JSON.stringify({ error: 'cwd (absolute path to an existing directory) required' }));
1256
+ return;
1257
+ }
1258
+ const r = scaffoldProjectAt(cwd);
1259
+ if (!r.ok) {
1260
+ res.writeHead(409, { 'Content-Type': 'application/json' });
1261
+ res.end(JSON.stringify({ ok: false, reason: r.reason }));
1262
+ return;
1263
+ }
1264
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1265
+ res.end(JSON.stringify({ ok: true, created: r.created.length, skipped: r.skipped.length, refused: r.refused.length + r.refusedDirs.length }));
1266
+ }
1267
+ catch (e) {
1268
+ res.writeHead(500, { 'Content-Type': 'application/json' });
1269
+ res.end(JSON.stringify({ error: 'memory_init_failed', detail: String(e) }));
1270
+ }
1271
+ })().catch(() => {
1272
+ /* handled above — keep the route from rejecting unhandled */
1273
+ });
1274
+ return;
1275
+ }
1212
1276
  // ── v0.6.0 M1 project thread routes ──────────────────────────────────────
1213
1277
  // GET /api/projects — list all known projects (slug, cwd, title, taskCount, lastActivityAt)
1214
1278
  // GET /api/projects/:slug — single project thread (meta + tasks merged by sessionId)