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.
Files changed (34) hide show
  1. package/README.md +18 -8
  2. package/dist/application/use-cases/generate-narration.js +111 -0
  3. package/dist/application/use-cases/generate-thread-title.js +58 -4
  4. package/dist/application/use-cases/narrator-prompt-template.js +158 -0
  5. package/dist/index.js +75 -6
  6. package/dist/infrastructure/cost/budget-config.js +113 -0
  7. package/dist/infrastructure/cost/spend-aggregator.js +147 -0
  8. package/dist/infrastructure/hooks/claude-sdk-hook.js +280 -4
  9. package/dist/infrastructure/hooks/hook-mode.js +71 -0
  10. package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +43 -0
  11. package/dist/infrastructure/hooks/pre-prompt-scan.js +122 -0
  12. package/dist/infrastructure/orchestration/aider-adapter.js +28 -0
  13. package/dist/infrastructure/orchestration/artifacts-extractor.js +128 -8
  14. package/dist/infrastructure/orchestration/claude-code-adapter.js +1 -0
  15. package/dist/infrastructure/orchestration/codex-adapter.js +14 -0
  16. package/dist/infrastructure/orchestration/failover-classifier.js +151 -0
  17. package/dist/infrastructure/orchestration/failover-policy.js +106 -0
  18. package/dist/infrastructure/orchestration/gemini-adapter.js +116 -14
  19. package/dist/infrastructure/orchestration/opencode-adapter.js +100 -18
  20. package/dist/infrastructure/orchestration/progress-aggregator.js +141 -6
  21. package/dist/infrastructure/orchestration/session-manager.js +117 -10
  22. package/dist/infrastructure/orchestration/session-persistence.js +28 -0
  23. package/dist/infrastructure/protocols/mcp-server-impl.js +564 -13
  24. package/dist/infrastructure/protocols/ui-page.js +2728 -245
  25. package/dist/infrastructure/security/permissions-config.js +234 -0
  26. package/dist/infrastructure/storage/project-thread-mirror.js +379 -24
  27. package/dist/infrastructure/storage/project-thread-reader.js +23 -1
  28. package/dist/infrastructure/storage/project-thread-title-backfill.js +28 -0
  29. package/dist/presentation/cli/commands/budget-command.js +79 -0
  30. package/dist/presentation/cli/commands/narrator-command.js +101 -0
  31. package/dist/presentation/cli/commands/permission-command.js +116 -0
  32. package/dist/presentation/cli/commands/scan-tool-command.js +36 -7
  33. package/dist/utils/typo-guard.js +77 -17
  34. 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) => ({ id: a.id, label: a.label, installed: a.isInstalled() }));
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
- if (a.checkReadiness)
56
- return a.checkReadiness();
57
- // Fallback for adapters that haven't implemented checkReadiness yet: treat
58
- // "installed" as "ready", "not installed" as "needs_install without hint".
59
- const installed = a.isInstalled();
60
- return installed
61
- ? { id: a.id, label: a.label, state: 'ready', installed: true }
62
- : { id: a.id, label: a.label, state: 'needs_install', installed: false };
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
  }