codehere 0.4.1 → 0.5.0-alpha.3

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 (24) hide show
  1. package/README.md +64 -7
  2. package/dist/application/use-cases/generate-final-report.js +144 -0
  3. package/dist/index.js +47 -4
  4. package/dist/infrastructure/hooks/claude-sdk-hook.js +18 -3
  5. package/dist/infrastructure/orchestration/aider-adapter.js +12 -2
  6. package/dist/infrastructure/orchestration/claude-code-adapter.js +70 -1
  7. package/dist/infrastructure/orchestration/codex-adapter.js +6 -1
  8. package/dist/infrastructure/orchestration/conductor-adapter.js +12 -2
  9. package/dist/infrastructure/orchestration/gemini-adapter.js +41 -5
  10. package/dist/infrastructure/orchestration/generic-cli-adapter.js +10 -2
  11. package/dist/infrastructure/orchestration/memory-injector.js +337 -0
  12. package/dist/infrastructure/orchestration/opencode-adapter.js +34 -4
  13. package/dist/infrastructure/orchestration/progress-aggregator.js +119 -0
  14. package/dist/infrastructure/orchestration/readiness.js +2 -5
  15. package/dist/infrastructure/orchestration/session-manager.js +65 -2
  16. package/dist/infrastructure/orchestration/session-persistence.js +13 -1
  17. package/dist/infrastructure/protocols/mcp-server-impl.js +86 -14
  18. package/dist/infrastructure/protocols/ui-page.js +665 -67
  19. package/dist/presentation/cli/commands/init-command.js +345 -24
  20. package/dist/presentation/cli/commands/memory-command.js +1017 -0
  21. package/dist/presentation/cli/commands/protocols-builder.js +254 -0
  22. package/dist/presentation/cli/commands/protocols-command.js +84 -0
  23. package/dist/presentation/cli/commands/repo-map-builder.js +247 -0
  24. package/package.json +4 -3
@@ -79,11 +79,8 @@ export function detectReadiness(env = process.env) {
79
79
  return {
80
80
  state: 'api_key_shadow',
81
81
  severity: 'warn',
82
- title: 'Using your Claude Code subscription',
83
- detail: `ANTHROPIC_API_KEY is set in your environment. ` +
84
- `codehere unsets it for spawned tasks so they use your Claude subscription — ` +
85
- `the same auth your interactive \`claude\` uses. Remove the key from your shell ` +
86
- `config to dismiss this warning.`,
82
+ title: 'Using Claude subscription auth',
83
+ detail: '`ANTHROPIC_API_KEY` is set. Spawned tasks ignore it and use your `claude` login instead. Unset it in your shell to hide this.',
87
84
  action: {
88
85
  label: 'Where is it set?',
89
86
  command: 'grep -l ANTHROPIC_API_KEY ~/.zshrc ~/.bashrc ~/.zprofile 2>/dev/null',
@@ -23,6 +23,7 @@ import { buildConductorAdapter } from './conductor-adapter.js';
23
23
  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
+ import { generateFinalReport, printFinalReportToTerminal } from '../../application/use-cases/generate-final-report.js';
26
27
  // ── Agent registry ─────────────────────────────────────────────────────────────
27
28
  // All backends codehere can spawn. Order here drives the default UI dropdown
28
29
  // order. Built-in adapters first; user-config-driven ones appended from
@@ -96,6 +97,18 @@ export class SessionManager {
96
97
  * These subprocesses are long-dead; we surface them so the user doesn't lose
97
98
  * their Final reports across restarts. */
98
99
  persistedCache = loadRecent(20);
100
+ /** When a spawned session exits, we capture the completion timestamp here
101
+ * so the API layer can show duration on still-in-memory sessions (the
102
+ * TaskSession interface intentionally has no completedAt; only PersistedSession
103
+ * does, so anything reading session.completedAt got undefined pre-fix).
104
+ * Surfaced May 2 by the cold-start dogfood — `duration: NaNs` in /api/tasks
105
+ * for completed-but-not-yet-restarted sessions. Bug 2 sister-fix. */
106
+ completedAtBySession = new Map();
107
+ /** PM-UX Layer 3 · Final Report cache (iteration 11 MVP, UI-integration iter 12).
108
+ * Populated when generateFinalReport resolves in the exit handler. Web UI
109
+ * reads via getFinalReport() through the /api/tasks serializer so non-technical
110
+ * users see the prose summary without opening a terminal. */
111
+ finalReportBySession = new Map();
99
112
  spawn(prompt, agentId = 'claude-code', options) {
100
113
  const adapter = getAdapter(agentId);
101
114
  if (!adapter) {
@@ -121,14 +134,50 @@ export class SessionManager {
121
134
  if (event.type !== 'exit')
122
135
  return;
123
136
  const data = event.data;
137
+ const completedAt = Date.now();
138
+ // Surface completedAt to API consumers reading the still-in-memory
139
+ // session (Bug 2 sister-fix, May 2). Without this, /api/tasks shows
140
+ // status=completed but no completedAt → NaN duration in the UI.
141
+ this.completedAtBySession.set(session.sessionId, completedAt);
124
142
  appendCompleted({
125
143
  id: session.sessionId,
126
144
  status: session.status,
127
- durationMs: Number(data?.durationMs ?? Date.now() - session.startedAt),
128
- completedAt: Date.now(),
145
+ durationMs: Number(data?.durationMs ?? completedAt - session.startedAt),
146
+ completedAt,
129
147
  steps: session.steps ? [...session.steps] : undefined,
130
148
  artifacts: session.artifacts,
131
149
  });
150
+ // PM-UX Layer 3 · Final Report MVP. Fire-and-forget; the report appears
151
+ // in the terminal a few seconds after exit. Failure modes return null
152
+ // silently (claude CLI not installed, timeout, non-zero exit). No SSE,
153
+ // no UI integration in this MVP — terminal output only so the dogfood
154
+ // loop sees the artifact immediately. See `generate-final-report.ts`.
155
+ const reportInput = {
156
+ sessionId: session.sessionId,
157
+ prompt: session.prompt,
158
+ agent: session.agent,
159
+ status: session.status,
160
+ durationMs: completedAt - session.startedAt,
161
+ steps: session.steps ? [...session.steps] : [],
162
+ artifacts: session.artifacts,
163
+ };
164
+ generateFinalReport(reportInput)
165
+ .then((report) => {
166
+ if (report) {
167
+ // Cache for web UI consumption via /api/tasks (iter 12 — non-technical
168
+ // users live in the web UI, not the terminal).
169
+ this.finalReportBySession.set(session.sessionId, report);
170
+ try {
171
+ printFinalReportToTerminal(reportInput, report);
172
+ }
173
+ catch {
174
+ // Don't let a print failure crash the daemon.
175
+ }
176
+ }
177
+ })
178
+ .catch(() => {
179
+ // generateFinalReport never throws by contract; defensive only.
180
+ });
132
181
  unsubscribe();
133
182
  });
134
183
  return session;
@@ -137,6 +186,20 @@ export class SessionManager {
137
186
  listPersisted() {
138
187
  return this.persistedCache;
139
188
  }
189
+ /** Completion timestamp for an in-memory session, if it has exited.
190
+ * Returns undefined if the session is still running OR was never spawned
191
+ * through this manager. Bug 2 sister-fix (May 2): TaskSession itself has
192
+ * no completedAt field; this method lets the API layer compute durations. */
193
+ getCompletedAt(sessionId) {
194
+ return this.completedAtBySession.get(sessionId);
195
+ }
196
+ /** PM-UX Layer 3 · prose Final Report for an in-memory exited session.
197
+ * Returns undefined while generation is in flight OR if the provider chain
198
+ * failed (no claude-free, no claude, no credit, etc). The web UI reads this
199
+ * through the /api/tasks serializer to render the prose summary inline. */
200
+ getFinalReport(sessionId) {
201
+ return this.finalReportBySession.get(sessionId);
202
+ }
140
203
  get(sessionId) {
141
204
  return this.sessions.get(sessionId);
142
205
  }
@@ -96,7 +96,19 @@ export function loadRecent(limit = 20) {
96
96
  prompt: s.prompt,
97
97
  cwd: s.cwd,
98
98
  startedAt: s.startedAt,
99
- status: c.status,
99
+ // BUG 2 (May 1 2026) — defensive normalization for zombie records.
100
+ // Pre-fix sessions persisted before commit <THIS> were written with
101
+ // status='running' even when their completed-record carried real
102
+ // durationMs/completedAt — the conductor adapter mirrored its status
103
+ // AFTER emitting the exit event. The fix at the source (conductor-
104
+ // adapter.ts) prevents new zombies; this normalization repairs old
105
+ // records on read so the UI doesn't show a "running" badge on a
106
+ // session that obviously finished. We have no ground truth on what
107
+ // the actual exit code WAS for these old records, so 'completed' is
108
+ // the safest default — they'd been displayed as completed in the UI
109
+ // had this been a session-manager.ts bug. Mark explicitly via a
110
+ // synthetic note so debug viewers can tell normalization happened.
111
+ status: c.status === 'running' && c.completedAt > 0 ? 'completed' : c.status,
100
112
  durationMs: c.durationMs,
101
113
  completedAt: c.completedAt,
102
114
  steps: c.steps,
@@ -437,10 +437,24 @@ export function startMCPServer(options = {}) {
437
437
  // ── E2 · Per-agent readiness (onboarding surface) ─────────────────────────
438
438
  // Full registry snapshot: install state + auth state + hints. Powers the
439
439
  // setup-aware agent dropdown and the one-click swap flow on auth errors.
440
+ //
441
+ // Apr 29: Conductor moved to a separate `auto` field. The web UI IS the
442
+ // Conductor surface — exposing "conductor" as a peer in the agents array
443
+ // is the "Gmail asks you to pick 'email' before composing" anti-pattern.
444
+ // But Conductor's readiness state IS the Auto option's readiness, so the
445
+ // UI needs it preserved (codex Round 1 caught: fresh install with no
446
+ // specialists installed would otherwise lose the setup prompt — Auto
447
+ // gets selected by default, submission fails at /api/task because
448
+ // conductor.isInstalled() returns false). The separate `auto` field
449
+ // keeps the setup hint plumbing intact while preventing the dropdown
450
+ // from showing Conductor as a peer choice.
440
451
  if (req.method === 'GET' && req.url === '/api/readiness/agents') {
441
452
  try {
453
+ const all = readinessForAllAgents();
454
+ const auto = all.find((a) => a.id === 'conductor');
455
+ const agents = all.filter((a) => a.id !== 'conductor');
442
456
  res.writeHead(200, { 'Content-Type': 'application/json' });
443
- res.end(JSON.stringify({ agents: readinessForAllAgents() }));
457
+ res.end(JSON.stringify({ agents, auto }));
444
458
  }
445
459
  catch (e) {
446
460
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -485,9 +499,22 @@ export function startMCPServer(options = {}) {
485
499
  // log/spawn. Stops log injection (`agent: "claude-code\n[FAKE]"`),
486
500
  // future-proofs against partial-match exploits, and fails fast
487
501
  // with a clear 400 instead of letting SessionManager.spawn throw.
502
+ //
503
+ // Apr 29: default flipped from 'claude-code' to 'conductor'. The web
504
+ // UI is the Conductor surface; tasks submitted without an explicit
505
+ // agent route through the supervisor (rule-based routing today,
506
+ // LLM classifier in v0.6). Empty string is treated like undefined
507
+ // so the form submitting `agent: ""` (Auto option) hits the same
508
+ // path. Specialists remain explicit overrides via the dropdown.
509
+ // Type-check is preserved: non-string non-undefined values still 400.
510
+ if (agent !== undefined && agent !== null && typeof agent !== 'string') {
511
+ res.writeHead(400, { 'Content-Type': 'application/json' });
512
+ res.end(JSON.stringify({ error: 'agent must be a string or omitted' }));
513
+ return;
514
+ }
488
515
  const validAgentIds = new Set(allAdapters().map((a) => a.id));
489
- const agentId = agent ?? 'claude-code';
490
- if (typeof agentId !== 'string' || !validAgentIds.has(agentId)) {
516
+ const agentId = (typeof agent === 'string' && agent.length > 0) ? agent : 'conductor';
517
+ if (!validAgentIds.has(agentId)) {
491
518
  res.writeHead(400, { 'Content-Type': 'application/json' });
492
519
  res.end(JSON.stringify({
493
520
  error: `Unknown agent: ${typeof agentId === 'string' ? agentId.slice(0, 64) : '<non-string>'}`,
@@ -521,16 +548,29 @@ export function startMCPServer(options = {}) {
521
548
  return;
522
549
  }
523
550
  if (req.method === 'GET' && req.url === '/api/tasks') {
524
- const spawned = manager.list().map((s) => ({
525
- sessionId: s.sessionId,
526
- prompt: s.prompt,
527
- startedAt: s.startedAt,
528
- status: s.status,
529
- agent: s.agent,
530
- steps: s.steps ?? [],
531
- artifacts: s.artifacts,
532
- provenance: 'spawned',
533
- }));
551
+ const spawned = manager.list().map((s) => {
552
+ // Bug 2 sister-fix (May 2): include completedAt for in-memory exited
553
+ // sessions so the UI can compute duration. TaskSession interface
554
+ // intentionally lacks completedAt; SessionManager tracks it separately
555
+ // in completedAtBySession map (populated when exit event fires).
556
+ const completedAt = manager.getCompletedAt(s.sessionId);
557
+ const finalReport = manager.getFinalReport(s.sessionId);
558
+ return {
559
+ sessionId: s.sessionId,
560
+ prompt: s.prompt,
561
+ startedAt: s.startedAt,
562
+ status: s.status,
563
+ agent: s.agent,
564
+ steps: s.steps ?? [],
565
+ artifacts: s.artifacts,
566
+ ...(completedAt !== undefined ? { completedAt, durationMs: completedAt - s.startedAt } : {}),
567
+ ...(finalReport !== undefined ? { finalReport } : {}),
568
+ // Iter 12 — surface canAppendMessage so the UI knows whether to
569
+ // render the reply input or a disabled "follow-up unsupported" state.
570
+ canAppendMessage: s.canAppendMessage === true,
571
+ provenance: 'spawned',
572
+ };
573
+ });
534
574
  const activeAgents = manager.listActiveAgents();
535
575
  const persisted = manager.listPersisted();
536
576
  // Layer 7: timestamp of most recent hook-audit line (= "last edit on this machine")
@@ -554,7 +594,7 @@ export function startMCPServer(options = {}) {
554
594
  return;
555
595
  }
556
596
  // /api/task/:id/events or /api/task/:id/kill
557
- const m = req.url.match(/^\/api\/task\/([^/]+)\/(events|kill)$/);
597
+ const m = req.url.match(/^\/api\/task\/([^/]+)\/(events|kill|message)$/);
558
598
  if (m) {
559
599
  const [, sessionId, verb] = m;
560
600
  const session = manager.get(sessionId);
@@ -585,6 +625,38 @@ export function startMCPServer(options = {}) {
585
625
  res.end(JSON.stringify({ sessionId, status: session.status }));
586
626
  return;
587
627
  }
628
+ // Iter 12 — POST /api/task/:id/message — follow-up message to a
629
+ // completed session. The reply-to-thread surface in the web UI calls
630
+ // this. Body: { text }. Response: { result, durationMs }.
631
+ if (verb === 'message' && req.method === 'POST') {
632
+ if (!session.canAppendMessage || !session.appendMessage) {
633
+ res.writeHead(400, { 'Content-Type': 'application/json' });
634
+ res.end(JSON.stringify({ error: `Agent ${session.agent} does not support follow-up messages yet.` }));
635
+ return;
636
+ }
637
+ let body = '';
638
+ req.on('data', (c) => { body += c.toString('utf-8'); });
639
+ req.on('end', async () => {
640
+ try {
641
+ const parsed = JSON.parse(body || '{}');
642
+ const text = (parsed.text ?? '').trim();
643
+ if (!text) {
644
+ res.writeHead(400, { 'Content-Type': 'application/json' });
645
+ res.end(JSON.stringify({ error: 'text is required' }));
646
+ return;
647
+ }
648
+ const out = await session.appendMessage(text);
649
+ res.writeHead(200, { 'Content-Type': 'application/json' });
650
+ res.end(JSON.stringify({ sessionId, ...out }));
651
+ }
652
+ catch (err) {
653
+ const msg = err instanceof Error ? err.message : String(err);
654
+ res.writeHead(500, { 'Content-Type': 'application/json' });
655
+ res.end(JSON.stringify({ error: msg }));
656
+ }
657
+ });
658
+ return;
659
+ }
588
660
  }
589
661
  res.writeHead(405, { 'Content-Type': 'application/json' });
590
662
  res.end(JSON.stringify({ error: 'Method not allowed for this path' }));