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.
- package/README.md +64 -7
- package/dist/application/use-cases/generate-final-report.js +144 -0
- package/dist/index.js +47 -4
- package/dist/infrastructure/hooks/claude-sdk-hook.js +18 -3
- package/dist/infrastructure/orchestration/aider-adapter.js +12 -2
- package/dist/infrastructure/orchestration/claude-code-adapter.js +70 -1
- package/dist/infrastructure/orchestration/codex-adapter.js +6 -1
- package/dist/infrastructure/orchestration/conductor-adapter.js +12 -2
- package/dist/infrastructure/orchestration/gemini-adapter.js +41 -5
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +10 -2
- package/dist/infrastructure/orchestration/memory-injector.js +337 -0
- package/dist/infrastructure/orchestration/opencode-adapter.js +34 -4
- package/dist/infrastructure/orchestration/progress-aggregator.js +119 -0
- package/dist/infrastructure/orchestration/readiness.js +2 -5
- package/dist/infrastructure/orchestration/session-manager.js +65 -2
- package/dist/infrastructure/orchestration/session-persistence.js +13 -1
- package/dist/infrastructure/protocols/mcp-server-impl.js +86 -14
- package/dist/infrastructure/protocols/ui-page.js +665 -67
- package/dist/presentation/cli/commands/init-command.js +345 -24
- package/dist/presentation/cli/commands/memory-command.js +1017 -0
- package/dist/presentation/cli/commands/protocols-builder.js +254 -0
- package/dist/presentation/cli/commands/protocols-command.js +84 -0
- package/dist/presentation/cli/commands/repo-map-builder.js +247 -0
- 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
|
|
83
|
-
detail: `ANTHROPIC_API_KEY is set
|
|
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 ??
|
|
128
|
-
completedAt
|
|
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
|
-
|
|
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
|
|
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
|
|
490
|
-
if (
|
|
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
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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' }));
|