codehere 0.5.0-alpha.5 → 0.6.0-alpha.1
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 +18 -8
- package/dist/application/use-cases/generate-narration.js +111 -0
- package/dist/application/use-cases/generate-thread-title.js +58 -4
- package/dist/application/use-cases/narrator-prompt-template.js +158 -0
- package/dist/index.js +75 -6
- package/dist/infrastructure/cost/budget-config.js +113 -0
- package/dist/infrastructure/cost/spend-aggregator.js +147 -0
- package/dist/infrastructure/hooks/claude-sdk-hook.js +280 -4
- package/dist/infrastructure/hooks/hook-mode.js +71 -0
- package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +43 -0
- package/dist/infrastructure/hooks/pre-prompt-scan.js +122 -0
- package/dist/infrastructure/orchestration/aider-adapter.js +28 -0
- package/dist/infrastructure/orchestration/artifacts-extractor.js +128 -8
- package/dist/infrastructure/orchestration/claude-code-adapter.js +1 -0
- package/dist/infrastructure/orchestration/codex-adapter.js +14 -0
- package/dist/infrastructure/orchestration/failover-classifier.js +151 -0
- package/dist/infrastructure/orchestration/failover-policy.js +106 -0
- package/dist/infrastructure/orchestration/gemini-adapter.js +116 -14
- package/dist/infrastructure/orchestration/opencode-adapter.js +100 -18
- package/dist/infrastructure/orchestration/progress-aggregator.js +141 -6
- package/dist/infrastructure/orchestration/session-manager.js +117 -10
- package/dist/infrastructure/orchestration/session-persistence.js +28 -0
- package/dist/infrastructure/protocols/mcp-server-impl.js +564 -13
- package/dist/infrastructure/protocols/ui-page.js +2728 -245
- package/dist/infrastructure/security/permissions-config.js +234 -0
- package/dist/infrastructure/storage/project-thread-mirror.js +379 -24
- package/dist/infrastructure/storage/project-thread-reader.js +23 -1
- package/dist/infrastructure/storage/project-thread-title-backfill.js +28 -0
- package/dist/presentation/cli/commands/budget-command.js +79 -0
- package/dist/presentation/cli/commands/narrator-command.js +101 -0
- package/dist/presentation/cli/commands/permission-command.js +116 -0
- package/dist/presentation/cli/commands/scan-tool-command.js +36 -7
- package/dist/utils/typo-guard.js +77 -17
- package/package.json +1 -1
|
@@ -22,9 +22,12 @@ import { buildGenericAdapter, loadAgentsConfig } from './generic-cli-adapter.js'
|
|
|
22
22
|
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
|
-
import { appendSpawned, appendCompleted, loadRecent } from './session-persistence.js';
|
|
25
|
+
import { appendSpawned, appendCompleted, appendFinalReport, loadRecent } from './session-persistence.js';
|
|
26
26
|
import { generateFinalReport, printFinalReportToTerminal } from '../../application/use-cases/generate-final-report.js';
|
|
27
27
|
import { mirrorSpawnedToThread, mirrorCompletedToThread } from '../storage/project-thread-mirror.js';
|
|
28
|
+
import { scanPromptBeforeSpawn } from '../hooks/pre-prompt-scan.js';
|
|
29
|
+
import { generateNarration } from '../../application/use-cases/generate-narration.js';
|
|
30
|
+
import { randomUUID } from 'crypto';
|
|
28
31
|
// ── Agent registry ─────────────────────────────────────────────────────────────
|
|
29
32
|
// All backends codehere can spawn. Order here drives the default UI dropdown
|
|
30
33
|
// order. Built-in adapters first; user-config-driven ones appended from
|
|
@@ -47,19 +50,25 @@ const REGISTRY = [
|
|
|
47
50
|
// Conductor is appended to the same array so UIs that iterate REGISTRY see it.
|
|
48
51
|
REGISTRY.push(buildConductorAdapter(() => REGISTRY));
|
|
49
52
|
export function availableAgents() {
|
|
50
|
-
return REGISTRY.map((a) => ({
|
|
53
|
+
return REGISTRY.map((a) => ({
|
|
54
|
+
id: a.id,
|
|
55
|
+
label: a.label,
|
|
56
|
+
installed: a.isInstalled(),
|
|
57
|
+
...(a.description ? { description: a.description } : {}),
|
|
58
|
+
}));
|
|
51
59
|
}
|
|
52
60
|
/** Per-agent readiness across the full registry. Powers the setup-aware UI. */
|
|
53
61
|
export function readinessForAllAgents() {
|
|
54
62
|
return REGISTRY.map((a) => {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
const base = a.checkReadiness
|
|
64
|
+
? a.checkReadiness()
|
|
65
|
+
: a.isInstalled()
|
|
66
|
+
? { id: a.id, label: a.label, state: 'ready', installed: true }
|
|
67
|
+
: { id: a.id, label: a.label, state: 'needs_install', installed: false };
|
|
68
|
+
// v0.6 S5 — mirror the adapter's "when to use me" description into the
|
|
69
|
+
// readiness payload so the dashboard dropdown can surface it as a
|
|
70
|
+
// plain-English hover tooltip without a separate API call.
|
|
71
|
+
return a.description ? { ...base, description: a.description } : base;
|
|
63
72
|
});
|
|
64
73
|
}
|
|
65
74
|
export function getAdapter(id) {
|
|
@@ -105,11 +114,24 @@ export class SessionManager {
|
|
|
105
114
|
* Surfaced May 2 by the cold-start dogfood — `duration: NaNs` in /api/tasks
|
|
106
115
|
* for completed-but-not-yet-restarted sessions. Bug 2 sister-fix. */
|
|
107
116
|
completedAtBySession = new Map();
|
|
117
|
+
/** v0.6.x Slack-shape spike — cwd per session. The "Continue thread" intake
|
|
118
|
+
* button needs to find the most recent in-memory session for the current
|
|
119
|
+
* cwd; cwd isn't on the TaskSession interface and adapters don't all expose
|
|
120
|
+
* it, so we track it here at spawn time. */
|
|
121
|
+
cwdBySession = new Map();
|
|
108
122
|
/** PM-UX Layer 3 · Final Report cache (iteration 11 MVP, UI-integration iter 12).
|
|
109
123
|
* Populated when generateFinalReport resolves in the exit handler. Web UI
|
|
110
124
|
* reads via getFinalReport() through the /api/tasks serializer so non-technical
|
|
111
125
|
* users see the prose summary without opening a terminal. */
|
|
112
126
|
finalReportBySession = new Map();
|
|
127
|
+
/** v0.6 M3 · PM Narrator. Mid-task plain-English summaries cached per
|
|
128
|
+
* session so the API can serve the latest narration without re-running
|
|
129
|
+
* the LLM call on every frontend poll. Latest-only — older narrations
|
|
130
|
+
* are overwritten when a new one fires. Per claim_user_persona HIGH:
|
|
131
|
+
* non-technical users read narration, not tool calls. */
|
|
132
|
+
narrationBySession = new Map();
|
|
133
|
+
narrationTimers = new Map();
|
|
134
|
+
narrationStepsAtLastFire = new Map();
|
|
113
135
|
spawn(prompt, agentId = 'claude-code', options) {
|
|
114
136
|
const adapter = getAdapter(agentId);
|
|
115
137
|
if (!adapter) {
|
|
@@ -118,12 +140,31 @@ export class SessionManager {
|
|
|
118
140
|
if (!adapter.isInstalled()) {
|
|
119
141
|
throw new Error(`Agent ${adapter.label} (${adapter.id}) is not installed on this machine`);
|
|
120
142
|
}
|
|
143
|
+
// Cross-vendor pre-prompt scan (task #110, slice 1). Closes the gap
|
|
144
|
+
// the FAQ now claims as "v0.5.x → v0.6 track": every adapter — not
|
|
145
|
+
// just claude-code — gets a safety scan on the user's prompt before
|
|
146
|
+
// spawn. Catches API-key paste, prompt-injection patterns, etc.
|
|
147
|
+
// Always writes one audit entry (synthetic toolName 'pre_prompt');
|
|
148
|
+
// refuses spawn ONLY when getHookMode() === 'block' AND the prompt
|
|
149
|
+
// has critical/high findings. Other modes (advisory/log-only/off)
|
|
150
|
+
// log + proceed.
|
|
151
|
+
const preScanSessionId = randomUUID();
|
|
152
|
+
const scan = scanPromptBeforeSpawn({
|
|
153
|
+
sessionId: preScanSessionId,
|
|
154
|
+
agent: agentId,
|
|
155
|
+
prompt,
|
|
156
|
+
cwd: options?.cwd,
|
|
157
|
+
});
|
|
158
|
+
if (!scan.allowed) {
|
|
159
|
+
throw new Error(scan.blockReason || 'pre-prompt scan blocked the spawn');
|
|
160
|
+
}
|
|
121
161
|
const session = adapter.spawn(prompt, options);
|
|
122
162
|
this.sessions.set(session.sessionId, session);
|
|
123
163
|
// Persistence: write the spawned record immediately so a daemon crash still
|
|
124
164
|
// leaves an audit trail of what was attempted. Subscribe to exit so we can
|
|
125
165
|
// capture the final status + steps + artifacts when the session settles.
|
|
126
166
|
const cwd = options?.cwd ?? process.cwd();
|
|
167
|
+
this.cwdBySession.set(session.sessionId, cwd);
|
|
127
168
|
appendSpawned({
|
|
128
169
|
id: session.sessionId,
|
|
129
170
|
agent: session.agent,
|
|
@@ -187,6 +228,17 @@ export class SessionManager {
|
|
|
187
228
|
// Cache for web UI consumption via /api/tasks (iter 12 — non-technical
|
|
188
229
|
// users live in the web UI, not the terminal).
|
|
189
230
|
this.finalReportBySession.set(session.sessionId, report);
|
|
231
|
+
// v0.6.x — persist to disk so the report survives daemon restart.
|
|
232
|
+
// Pre-fix: founder's Mac restart 2026-05-03 left a session card
|
|
233
|
+
// showing "Generating report…" forever because reports lived only
|
|
234
|
+
// in this in-memory Map. Append-only finalReport record paired
|
|
235
|
+
// with the existing spawned/completed records by sessionId.
|
|
236
|
+
appendFinalReport({
|
|
237
|
+
id: session.sessionId,
|
|
238
|
+
summary: report.summary,
|
|
239
|
+
generatedAt: report.generatedAt,
|
|
240
|
+
durationMs: report.durationMs,
|
|
241
|
+
});
|
|
190
242
|
try {
|
|
191
243
|
printFinalReportToTerminal(reportInput, report);
|
|
192
244
|
}
|
|
@@ -198,10 +250,61 @@ export class SessionManager {
|
|
|
198
250
|
.catch(() => {
|
|
199
251
|
// generateFinalReport never throws by contract; defensive only.
|
|
200
252
|
});
|
|
253
|
+
// v0.6 M3 — clean up the narration timer for this session.
|
|
254
|
+
const t = this.narrationTimers.get(session.sessionId);
|
|
255
|
+
if (t) {
|
|
256
|
+
clearInterval(t);
|
|
257
|
+
this.narrationTimers.delete(session.sessionId);
|
|
258
|
+
}
|
|
201
259
|
unsubscribe();
|
|
202
260
|
});
|
|
261
|
+
// v0.6 M3 — PM Narrator. Mid-task plain-English summaries fired
|
|
262
|
+
// every 30s while the session is running. Heuristic: only fire if
|
|
263
|
+
// the step count grew by ≥3 since the last narration (otherwise
|
|
264
|
+
// we'd narrate the same state with new words). Fire-and-forget;
|
|
265
|
+
// failures (no claude binary, timeout, malformed output) silently
|
|
266
|
+
// skip — narration is enrichment, not the primary surface.
|
|
267
|
+
this.startNarrationTimer(session);
|
|
203
268
|
return session;
|
|
204
269
|
}
|
|
270
|
+
/** v0.6 M3 — periodic narration loop per session. */
|
|
271
|
+
startNarrationTimer(session) {
|
|
272
|
+
if (process.env.CODEHERE_DISABLE_NARRATOR === '1')
|
|
273
|
+
return;
|
|
274
|
+
this.narrationStepsAtLastFire.set(session.sessionId, 0);
|
|
275
|
+
const intervalMs = Number(process.env.CODEHERE_NARRATOR_INTERVAL_MS ?? 30_000);
|
|
276
|
+
const minStepDelta = Number(process.env.CODEHERE_NARRATOR_MIN_STEPS ?? 3);
|
|
277
|
+
const timer = setInterval(() => {
|
|
278
|
+
// Skip if session has settled.
|
|
279
|
+
if (session.status !== 'running')
|
|
280
|
+
return;
|
|
281
|
+
const steps = session.steps ?? [];
|
|
282
|
+
const lastFireSteps = this.narrationStepsAtLastFire.get(session.sessionId) ?? 0;
|
|
283
|
+
if (steps.length - lastFireSteps < minStepDelta)
|
|
284
|
+
return;
|
|
285
|
+
// Mark BEFORE the async call to prevent double-firing if the LLM
|
|
286
|
+
// is slow.
|
|
287
|
+
this.narrationStepsAtLastFire.set(session.sessionId, steps.length);
|
|
288
|
+
const elapsedMs = Date.now() - session.startedAt;
|
|
289
|
+
void generateNarration({
|
|
290
|
+
sessionId: session.sessionId,
|
|
291
|
+
prompt: session.prompt,
|
|
292
|
+
agent: session.agent,
|
|
293
|
+
elapsedMs,
|
|
294
|
+
steps: [...steps],
|
|
295
|
+
})
|
|
296
|
+
.then((out) => {
|
|
297
|
+
if (out)
|
|
298
|
+
this.narrationBySession.set(session.sessionId, out);
|
|
299
|
+
})
|
|
300
|
+
.catch(() => { });
|
|
301
|
+
}, intervalMs);
|
|
302
|
+
this.narrationTimers.set(session.sessionId, timer);
|
|
303
|
+
}
|
|
304
|
+
/** v0.6 M3 — latest narration for a session, or undefined. */
|
|
305
|
+
getNarration(sessionId) {
|
|
306
|
+
return this.narrationBySession.get(sessionId);
|
|
307
|
+
}
|
|
205
308
|
/** Persisted (read-only) sessions from prior daemon runs. Cached at boot. */
|
|
206
309
|
listPersisted() {
|
|
207
310
|
return this.persistedCache;
|
|
@@ -213,6 +316,10 @@ export class SessionManager {
|
|
|
213
316
|
getCompletedAt(sessionId) {
|
|
214
317
|
return this.completedAtBySession.get(sessionId);
|
|
215
318
|
}
|
|
319
|
+
/** v0.6.x Slack-shape spike — return cwd for an in-memory session. */
|
|
320
|
+
getCwd(sessionId) {
|
|
321
|
+
return this.cwdBySession.get(sessionId);
|
|
322
|
+
}
|
|
216
323
|
/** PM-UX Layer 3 · prose Final Report for an in-memory exited session.
|
|
217
324
|
* Returns undefined while generation is in flight OR if the provider chain
|
|
218
325
|
* failed (no claude-free, no claude, no credit, etc). The web UI reads this
|
|
@@ -46,6 +46,27 @@ export function appendCompleted(rec) {
|
|
|
46
46
|
/* best-effort */
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* v0.6.x — persist a Final Report so it survives daemon restart.
|
|
51
|
+
*
|
|
52
|
+
* Pre-fix: Final Reports lived only in SessionManager.finalReportBySession
|
|
53
|
+
* (in-memory Map) and were lost on every daemon bounce. Founder hit this
|
|
54
|
+
* 2026-05-03 after a Mac restart: completed sessions showed
|
|
55
|
+
* "Generating report…" forever because there was no on-disk record + no
|
|
56
|
+
* "report-unavailable" state in the UI.
|
|
57
|
+
*
|
|
58
|
+
* Append-only: Final Report fires AFTER the CompletedRecord (generateFinalReport
|
|
59
|
+
* is async post-exit), so it gets its own record type. Reader merges by id.
|
|
60
|
+
*/
|
|
61
|
+
export function appendFinalReport(rec) {
|
|
62
|
+
try {
|
|
63
|
+
ensureDir();
|
|
64
|
+
appendFileSync(SESSIONS_FILE, JSON.stringify({ t: 'finalReport', ...rec }) + '\n', 'utf-8');
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
/* best-effort */
|
|
68
|
+
}
|
|
69
|
+
}
|
|
49
70
|
/**
|
|
50
71
|
* Walk the session log backward, pair spawned+completed records by id, return
|
|
51
72
|
* the most recent `limit` completed pairs sorted newest-first.
|
|
@@ -67,6 +88,9 @@ export function loadRecent(limit = 20) {
|
|
|
67
88
|
}
|
|
68
89
|
const spawned = new Map();
|
|
69
90
|
const completed = [];
|
|
91
|
+
/** v0.6.x — Final Reports keyed by sessionId. Last write wins
|
|
92
|
+
* (rare in practice; reports fire once per session). */
|
|
93
|
+
const finalReports = new Map();
|
|
70
94
|
for (const line of content.split('\n')) {
|
|
71
95
|
const trimmed = line.trim();
|
|
72
96
|
if (!trimmed)
|
|
@@ -82,6 +106,8 @@ export function loadRecent(limit = 20) {
|
|
|
82
106
|
spawned.set(rec.id, rec);
|
|
83
107
|
else if (rec.t === 'completed')
|
|
84
108
|
completed.push(rec);
|
|
109
|
+
else if (rec.t === 'finalReport')
|
|
110
|
+
finalReports.set(rec.id, rec);
|
|
85
111
|
}
|
|
86
112
|
const merged = [];
|
|
87
113
|
// Walk completed in reverse so we return newest-first
|
|
@@ -90,6 +116,7 @@ export function loadRecent(limit = 20) {
|
|
|
90
116
|
const s = spawned.get(c.id);
|
|
91
117
|
if (!s)
|
|
92
118
|
continue; // orphan completed (shouldn't happen but harmless)
|
|
119
|
+
const fr = finalReports.get(c.id);
|
|
93
120
|
merged.push({
|
|
94
121
|
sessionId: s.id,
|
|
95
122
|
agent: s.agent,
|
|
@@ -113,6 +140,7 @@ export function loadRecent(limit = 20) {
|
|
|
113
140
|
completedAt: c.completedAt,
|
|
114
141
|
steps: c.steps,
|
|
115
142
|
artifacts: c.artifacts,
|
|
143
|
+
...(fr ? { finalReport: { summary: fr.summary, generatedAt: fr.generatedAt, durationMs: fr.durationMs } } : {}),
|
|
116
144
|
provenance: 'persisted',
|
|
117
145
|
});
|
|
118
146
|
}
|