codehere 0.5.0-alpha.3 → 0.5.0-alpha.5

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.
@@ -24,6 +24,7 @@ import { seedAgentsConfig } from './agents-config-seed.js';
24
24
  import { AUDIT_FILE } from '../hooks/claude-sdk-hook.js';
25
25
  import { appendSpawned, appendCompleted, loadRecent } from './session-persistence.js';
26
26
  import { generateFinalReport, printFinalReportToTerminal } from '../../application/use-cases/generate-final-report.js';
27
+ import { mirrorSpawnedToThread, mirrorCompletedToThread } from '../storage/project-thread-mirror.js';
27
28
  // ── Agent registry ─────────────────────────────────────────────────────────────
28
29
  // All backends codehere can spawn. Order here drives the default UI dropdown
29
30
  // order. Built-in adapters first; user-config-driven ones appended from
@@ -130,6 +131,15 @@ export class SessionManager {
130
131
  cwd,
131
132
  startedAt: session.startedAt,
132
133
  });
134
+ // v0.6.0 M1+M7 — forward-mirror to per-project thread.jsonl alongside
135
+ // sessions.jsonl. Best-effort; mirror failures don't break the spawn path.
136
+ mirrorSpawnedToThread({
137
+ sessionId: session.sessionId,
138
+ agent: session.agent,
139
+ prompt,
140
+ cwd,
141
+ startedAt: session.startedAt,
142
+ });
133
143
  const unsubscribe = session.subscribe((event) => {
134
144
  if (event.type !== 'exit')
135
145
  return;
@@ -147,6 +157,16 @@ export class SessionManager {
147
157
  steps: session.steps ? [...session.steps] : undefined,
148
158
  artifacts: session.artifacts,
149
159
  });
160
+ // v0.6.0 M1+M7 — forward-mirror completion to project thread.
161
+ mirrorCompletedToThread({
162
+ sessionId: session.sessionId,
163
+ cwd,
164
+ status: session.status,
165
+ durationMs: Number(data?.durationMs ?? completedAt - session.startedAt),
166
+ completedAt,
167
+ steps: session.steps ? [...session.steps] : undefined,
168
+ artifacts: session.artifacts,
169
+ });
150
170
  // PM-UX Layer 3 · Final Report MVP. Fire-and-forget; the report appears
151
171
  // in the terminal a few seconds after exit. Failure modes return null
152
172
  // silently (claude CLI not installed, timeout, non-zero exit). No SSE,
@@ -25,9 +25,11 @@ import { fileURLToPath } from 'url';
25
25
  import { collectFilesForMCP, scoreFileForMCP } from './mcp-helpers.js';
26
26
  import { scanPayload, calculateRiskScore, AUDIT_FILE } from '../hooks/claude-sdk-hook.js';
27
27
  import { getVersion } from '../../utils/version.js';
28
- import { TASK_HTML, HISTORY_HTML } from './ui-page.js';
28
+ import { TASK_HTML, HISTORY_HTML, THREAD_HTML } from './ui-page.js';
29
29
  import { SessionManager, availableAgents, readinessForAllAgents, getAdapter, allAdapters, } from '../orchestration/session-manager.js';
30
30
  import { isAbsolute } from 'path';
31
+ import { readCrossToolAgents } from '../storage/cross-tool-agents.js';
32
+ import { listProjects, readProjectThread } from '../storage/project-thread-reader.js';
31
33
  import { installAgent } from '../orchestration/agent-installer.js';
32
34
  import { detectReadiness } from '../orchestration/readiness.js';
33
35
  // ── Tool definitions ─────────────────────────────────────────────────────────
@@ -81,6 +83,28 @@ const TOOLS = [
81
83
  // ── Tool handlers ────────────────────────────────────────────────────────────
82
84
  import { homedir } from 'os';
83
85
  const AUDIT_FILE_PATH = join(homedir(), '.codehere', 'audit', 'hook-events.jsonl');
86
+ /**
87
+ * Dev-only behavior-observer injection (v0.6.0 — elu joined the team roster
88
+ * 2026-05-03 as behavior reviewer). Activated only when CODEHERE_DEV_ELU=1
89
+ * is set in the founder's local env; never in shipped releases. The script's
90
+ * public key is meant for client-side embedding (analytics tracking key, not
91
+ * a secret) so committing the URL is safe.
92
+ *
93
+ * Why runtime injection instead of editing TASK_HTML/HISTORY_HTML directly:
94
+ * the templates stay identical between dev and release builds. The only
95
+ * delta is the env var. This keeps the release surface clean (zero-telemetry
96
+ * value per PRD §1.5 #2 stays intact) while letting elu observe founder
97
+ * dogfood sessions.
98
+ *
99
+ * Removal trigger lives in team_roster.md memory: if elu produces zero
100
+ * unique findings within one month, remove this helper + roster entry.
101
+ */
102
+ const ELU_DEV_SCRIPT = '<script async src="https://elu.dev/v1/elu_pk_live_HrKo9yFFLx03tz9xuSKFcE2Jpv.js"></script>';
103
+ function withDevElu(html) {
104
+ if (process.env.CODEHERE_DEV_ELU !== '1')
105
+ return html;
106
+ return html.replace('</head>', `${ELU_DEV_SCRIPT}\n</head>`);
107
+ }
84
108
  function handleTrust(params) {
85
109
  const { filepath, code, verbose } = params;
86
110
  let content;
@@ -270,13 +294,19 @@ export function startMCPServer(options = {}) {
270
294
  // v0.3 primary UI: task intake + active sessions
271
295
  if (req.method === 'GET' && (req.url === '/' || req.url === '/index.html')) {
272
296
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
273
- res.end(TASK_HTML);
297
+ res.end(withDevElu(TASK_HTML));
274
298
  return;
275
299
  }
276
300
  // History / audit feed (what we built Day 0; now secondary)
277
301
  if (req.method === 'GET' && (req.url === '/history' || req.url === '/history/')) {
278
302
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
279
- res.end(HISTORY_HTML);
303
+ res.end(withDevElu(HISTORY_HTML));
304
+ return;
305
+ }
306
+ // v0.6.0 M1 — project thread view; consumes /api/projects + /api/projects/:slug
307
+ if (req.method === 'GET' && (req.url === '/threads' || req.url === '/threads/' || req.url?.startsWith('/threads?'))) {
308
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
309
+ res.end(withDevElu(THREAD_HTML));
280
310
  return;
281
311
  }
282
312
  // Backwards-compat: /ui used to be the Mission Control audit view — now at /history
@@ -453,8 +483,28 @@ export function startMCPServer(options = {}) {
453
483
  const all = readinessForAllAgents();
454
484
  const auto = all.find((a) => a.id === 'conductor');
455
485
  const agents = all.filter((a) => a.id !== 'conductor');
486
+ // v0.6.0 M8 — surface cross-tool agent definitions (.opencode/agents/,
487
+ // .claude/agents/, .codehere/agents/) alongside the built-in adapters.
488
+ // These don't have a `state` (ready/needs_install) — they're definitions
489
+ // not runtimes. UI can render them as "available specialists" tagged
490
+ // with their source format. Best-effort: failures fall back to no
491
+ // cross-tool agents listed.
492
+ let crossTool = [];
493
+ try {
494
+ const result = readCrossToolAgents(process.cwd());
495
+ crossTool = result.agents.map((a) => ({
496
+ name: a.name,
497
+ description: a.description.length > 240 ? a.description.slice(0, 237) + '…' : a.description,
498
+ sourceFormat: a.sourceFormat,
499
+ mode: a.mode,
500
+ sourcePath: a.sourcePath,
501
+ }));
502
+ }
503
+ catch {
504
+ // best-effort; ignore parse failures
505
+ }
456
506
  res.writeHead(200, { 'Content-Type': 'application/json' });
457
- res.end(JSON.stringify({ agents, auto }));
507
+ res.end(JSON.stringify({ agents, auto, crossTool }));
458
508
  }
459
509
  catch (e) {
460
510
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -475,6 +525,49 @@ export function startMCPServer(options = {}) {
475
525
  }
476
526
  return;
477
527
  }
528
+ // ── v0.6.0 M1 project thread routes ──────────────────────────────────────
529
+ // GET /api/projects — list all known projects (slug, cwd, title, taskCount, lastActivityAt)
530
+ // GET /api/projects/:slug — single project thread (meta + tasks merged by sessionId)
531
+ // Reader source of truth: ~/.codehere/projects/<slug>/thread.jsonl (per-project append-only log
532
+ // populated by migration + forward-mirror). Drives the new thread-view UI.
533
+ if (req.method === 'GET' && req.url === '/api/projects') {
534
+ try {
535
+ const projects = listProjects();
536
+ res.writeHead(200, { 'Content-Type': 'application/json' });
537
+ res.end(JSON.stringify({ projects }));
538
+ }
539
+ catch (e) {
540
+ res.writeHead(500, { 'Content-Type': 'application/json' });
541
+ res.end(JSON.stringify({ error: 'list_projects_failed', detail: String(e) }));
542
+ }
543
+ return;
544
+ }
545
+ if (req.method === 'GET' && req.url?.startsWith('/api/projects/')) {
546
+ try {
547
+ // Slug is the segment after /api/projects/. Strip optional query string.
548
+ const slug = req.url.replace('/api/projects/', '').split('?')[0];
549
+ // Slug shape is sha1[:12] (12 hex chars) OR the literal 'unknown' bucket.
550
+ // Reject anything else to keep the surface tight (and stop ../ traversal).
551
+ if (!/^([a-f0-9]{12}|unknown)$/.test(slug)) {
552
+ res.writeHead(400, { 'Content-Type': 'application/json' });
553
+ res.end(JSON.stringify({ error: 'invalid_slug' }));
554
+ return;
555
+ }
556
+ const thread = readProjectThread(slug);
557
+ if (!thread) {
558
+ res.writeHead(404, { 'Content-Type': 'application/json' });
559
+ res.end(JSON.stringify({ error: 'project_not_found', slug }));
560
+ return;
561
+ }
562
+ res.writeHead(200, { 'Content-Type': 'application/json' });
563
+ res.end(JSON.stringify({ thread }));
564
+ }
565
+ catch (e) {
566
+ res.writeHead(500, { 'Content-Type': 'application/json' });
567
+ res.end(JSON.stringify({ error: 'read_project_failed', detail: String(e) }));
568
+ }
569
+ return;
570
+ }
478
571
  // ── F1 task orchestration routes ─────────────────────────────────────────
479
572
  // POST /api/task — { prompt, cwd? } → { sessionId }
480
573
  // GET /api/tasks — list all sessions
@@ -538,6 +631,11 @@ export function startMCPServer(options = {}) {
538
631
  startedAt: session.startedAt,
539
632
  status: session.status,
540
633
  agent: session.agent,
634
+ // Iter 13 — include canAppendMessage so the freshly-attached
635
+ // session card can correctly enable/disable reply input on the
636
+ // first render. Without this, ALL just-spawned sessions render
637
+ // with reply disabled even when the adapter supports it.
638
+ canAppendMessage: session.canAppendMessage === true,
541
639
  }));
542
640
  }
543
641
  catch (e) {