codehere 0.8.0 → 1.0.2

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 (119) hide show
  1. package/README.md +37 -184
  2. package/dist/application/use-cases/check-claimed-tests.js +81 -0
  3. package/dist/application/use-cases/detect-test-weakening.js +237 -0
  4. package/dist/application/use-cases/rerun-tests.js +329 -0
  5. package/dist/index.js +74 -1004
  6. package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -265
  7. package/dist/presentation/cli/commands/check-command.js +251 -0
  8. package/dist/presentation/cli/commands/help-command.js +22 -286
  9. package/dist/ui.js +19 -438
  10. package/dist/utils/version.js +0 -12
  11. package/package.json +18 -27
  12. package/dist/application/use-cases/domain-trust.js +0 -110
  13. package/dist/application/use-cases/generate-final-report.js +0 -134
  14. package/dist/application/use-cases/generate-narration.js +0 -128
  15. package/dist/application/use-cases/generate-thread-title.js +0 -178
  16. package/dist/application/use-cases/narrator-prompt-template.js +0 -158
  17. package/dist/application/use-cases/thread-export.js +0 -155
  18. package/dist/application/use-cases/verify-claim.js +0 -274
  19. package/dist/application/use-cases/write-pass.js +0 -277
  20. package/dist/domain/entities/task.js +0 -61
  21. package/dist/domain/services/codebase-detector.js +0 -91
  22. package/dist/error-handler.js +0 -251
  23. package/dist/infrastructure/completion/bash-completion.js +0 -25
  24. package/dist/infrastructure/completion/fish-completion.js +0 -22
  25. package/dist/infrastructure/completion/zsh-completion.js +0 -30
  26. package/dist/infrastructure/config/config-manager.js +0 -173
  27. package/dist/infrastructure/config/product-context.js +0 -67
  28. package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
  29. package/dist/infrastructure/context/adapters/figma-source.js +0 -163
  30. package/dist/infrastructure/context/adapters/github-issue-source.js +0 -159
  31. package/dist/infrastructure/context/adapters/linear-source.js +0 -190
  32. package/dist/infrastructure/context/context-resolver.js +0 -144
  33. package/dist/infrastructure/context/context-source.js +0 -114
  34. package/dist/infrastructure/context/oauth-flow.js +0 -207
  35. package/dist/infrastructure/context/oauth-store.js +0 -119
  36. package/dist/infrastructure/context/registry-bootstrap.js +0 -99
  37. package/dist/infrastructure/context/write-pass-resolver.js +0 -106
  38. package/dist/infrastructure/cost/budget-config.js +0 -113
  39. package/dist/infrastructure/cost/cost-router.js +0 -104
  40. package/dist/infrastructure/cost/spend-aggregator.js +0 -147
  41. package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -888
  42. package/dist/infrastructure/hooks/hook-mode.js +0 -71
  43. package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -43
  44. package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
  45. package/dist/infrastructure/hooks/session-analyzer.js +0 -177
  46. package/dist/infrastructure/observability/execution-tracer.js +0 -227
  47. package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
  48. package/dist/infrastructure/orchestration/agent-installer.js +0 -101
  49. package/dist/infrastructure/orchestration/agents-config-seed.js +0 -56
  50. package/dist/infrastructure/orchestration/aider-adapter.js +0 -447
  51. package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
  52. package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -478
  53. package/dist/infrastructure/orchestration/codex-adapter.js +0 -538
  54. package/dist/infrastructure/orchestration/conductor-adapter.js +0 -377
  55. package/dist/infrastructure/orchestration/context-handoff.js +0 -144
  56. package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
  57. package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
  58. package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
  59. package/dist/infrastructure/orchestration/failover-config.js +0 -77
  60. package/dist/infrastructure/orchestration/failover-policy.js +0 -106
  61. package/dist/infrastructure/orchestration/gemini-adapter.js +0 -583
  62. package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -558
  63. package/dist/infrastructure/orchestration/graphify-context.js +0 -254
  64. package/dist/infrastructure/orchestration/memory-injector.js +0 -380
  65. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -233
  66. package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
  67. package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
  68. package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
  69. package/dist/infrastructure/orchestration/opencode-adapter.js +0 -510
  70. package/dist/infrastructure/orchestration/progress-aggregator.js +0 -689
  71. package/dist/infrastructure/orchestration/readiness.js +0 -103
  72. package/dist/infrastructure/orchestration/session-manager.js +0 -1091
  73. package/dist/infrastructure/orchestration/session-persistence.js +0 -258
  74. package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
  75. package/dist/infrastructure/orchestration/write-pass-store.js +0 -46
  76. package/dist/infrastructure/protocols/cloud-sync.js +0 -207
  77. package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
  78. package/dist/infrastructure/protocols/mcp-server-impl.js +0 -2853
  79. package/dist/infrastructure/protocols/probe-port.js +0 -36
  80. package/dist/infrastructure/protocols/thread-phase.js +0 -105
  81. package/dist/infrastructure/protocols/ui-page-v2.js +0 -4418
  82. package/dist/infrastructure/protocols/ui-page.js +0 -2377
  83. package/dist/infrastructure/protocols/verify-page.js +0 -983
  84. package/dist/infrastructure/security/file-collection.js +0 -118
  85. package/dist/infrastructure/security/input-validator.js +0 -152
  86. package/dist/infrastructure/security/permissions-config.js +0 -234
  87. package/dist/infrastructure/security/trust-scorer.js +0 -657
  88. package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
  89. package/dist/infrastructure/storage/embeddings-path.js +0 -37
  90. package/dist/infrastructure/storage/project-thread-migration.js +0 -237
  91. package/dist/infrastructure/storage/project-thread-mirror.js +0 -524
  92. package/dist/infrastructure/storage/project-thread-reader.js +0 -172
  93. package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
  94. package/dist/infrastructure/storage/task-repository.js +0 -206
  95. package/dist/monitoring.js +0 -212
  96. package/dist/presentation/cli/commands/audit-command.js +0 -126
  97. package/dist/presentation/cli/commands/budget-command.js +0 -79
  98. package/dist/presentation/cli/commands/config-command.js +0 -125
  99. package/dist/presentation/cli/commands/context-source-command.js +0 -198
  100. package/dist/presentation/cli/commands/hook-command.js +0 -265
  101. package/dist/presentation/cli/commands/init-command.js +0 -578
  102. package/dist/presentation/cli/commands/memory-command.js +0 -1031
  103. package/dist/presentation/cli/commands/narrator-command.js +0 -101
  104. package/dist/presentation/cli/commands/permission-command.js +0 -116
  105. package/dist/presentation/cli/commands/protocols-builder.js +0 -254
  106. package/dist/presentation/cli/commands/protocols-command.js +0 -84
  107. package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
  108. package/dist/presentation/cli/commands/report-command.js +0 -387
  109. package/dist/presentation/cli/commands/scan-tool-command.js +0 -94
  110. package/dist/presentation/cli/commands/setup-command.js +0 -307
  111. package/dist/presentation/cli/commands/status-command.js +0 -661
  112. package/dist/presentation/cli/commands/thread-command.js +0 -288
  113. package/dist/presentation/cli/commands/trust-command.js +0 -306
  114. package/dist/presentation/cli/commands/verify-command.js +0 -233
  115. package/dist/presentation/cli/error-display.js +0 -336
  116. package/dist/presentation/cli/repl.js +0 -797
  117. package/dist/presentation/cli/task-deliverable.js +0 -127
  118. package/dist/testing/cassette.js +0 -125
  119. package/dist/testing/replay-adapter.js +0 -166
@@ -1,524 +0,0 @@
1
- /**
2
- * Project thread forward-mirror — writes new sessions to per-project
3
- * thread.jsonl alongside sessions.jsonl during normal daemon operation.
4
- *
5
- * v0.6.0 M1+M7 — companion to project-thread-migration.ts:
6
- * - Migration handles HISTORICAL data (sessions.jsonl → projects/<slug>/thread.jsonl)
7
- * - This handles NEW data (every spawn + completion writes to BOTH)
8
- *
9
- * Without this, the project threads created by migration are a one-time
10
- * snapshot. With this, every new session lands in both formats and the
11
- * project thread stays current as users work. UI restructure (next milestone)
12
- * then renders thread.jsonl as the primary view.
13
- *
14
- * Write pattern matches session-persistence.ts:
15
- * - Append-only (no rewrites; lock-free single writer)
16
- * - Best-effort (failures don't break the spawn path)
17
- * - Each session writes TWO entries over its lifetime:
18
- * 1. On spawn: { type: 'task', sessionId, agent, prompt, cwd, startedAt, status: 'running' }
19
- * 2. On complete: { type: 'task', sessionId, status, durationMs, completedAt, steps, artifacts }
20
- * - Reader reconstructs by sessionId (later entry overrides earlier) — same
21
- * shape as project-thread-migration.ts output.
22
- *
23
- * Filtering matches migration defaults:
24
- * - Temp dirs (/tmp, /var/folders, /private/var/folders, /private/tmp) skipped
25
- * - NULL cwd → 'unknown' bucket
26
- *
27
- * Iter 13 — built after v0.5.0-alpha.4 ship. Previously the only way new
28
- * sessions reached thread.jsonl was to re-run migration; this closes the gap.
29
- */
30
- import { existsSync, mkdirSync, appendFileSync, readFileSync } from 'fs';
31
- import { homedir } from 'os';
32
- import { join } from 'path';
33
- import { createHash } from 'crypto';
34
- const PROJECTS_DIR = join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'projects');
35
- /** Match migration module's defaults. Same regex set kept in sync. */
36
- const TEMP_DIR_PATTERNS = [/^\/tmp\//, /^\/var\/folders\//, /^\/private\/var\/folders\//, /^\/private\/tmp\//];
37
- function isTempDir(cwd) {
38
- if (!cwd)
39
- return false;
40
- return TEMP_DIR_PATTERNS.some((re) => re.test(cwd));
41
- }
42
- function slugForCwd(cwd) {
43
- if (!cwd)
44
- return 'unknown';
45
- return createHash('sha1').update(cwd).digest('hex').slice(0, 12);
46
- }
47
- function ensureProjectDir(slug) {
48
- const dir = join(PROJECTS_DIR, slug);
49
- if (!existsSync(dir))
50
- mkdirSync(dir, { recursive: true });
51
- return dir;
52
- }
53
- function appendThreadEntry(slug, entry) {
54
- // Returns true if this write SEEDED the file (first time creation), false otherwise.
55
- // Caller (mirrorSpawnedToThread) uses this signal to fire the hidden Title agent.
56
- try {
57
- const dir = ensureProjectDir(slug);
58
- const file = join(dir, 'thread.jsonl');
59
- let seeded = false;
60
- if (!existsSync(file)) {
61
- const meta = JSON.stringify({
62
- _meta: 'project',
63
- slug,
64
- cwd: null, // populated by migration; forward-mirror writes the cwd in each task entry
65
- sessionCount: 0,
66
- title: null, // populated by hidden Title agent (v0.6.0 M9) on first task entry
67
- migratedFrom: 'forward-mirror',
68
- migratedAt: Date.now(),
69
- });
70
- appendFileSync(file, meta + '\n', 'utf-8');
71
- seeded = true;
72
- }
73
- appendFileSync(file, JSON.stringify(entry) + '\n', 'utf-8');
74
- return seeded;
75
- }
76
- catch {
77
- // Best-effort. Mirror failures must never break the spawn or completion
78
- // path — sessions.jsonl is the canonical store; thread.jsonl is derived.
79
- return false;
80
- }
81
- }
82
- /** Append a project-meta update line (used by Title agent to set the title).
83
- * Returns true on successful write, false on any fs failure. Title-agent
84
- * callers ignore the return (best-effort); renameProject surfaces it as a
85
- * 500 to the HTTP layer so users don't see "renamed" when nothing landed. */
86
- function appendMetaUpdate(slug, partial) {
87
- try {
88
- const dir = ensureProjectDir(slug);
89
- const file = join(dir, 'thread.jsonl');
90
- if (!existsSync(file))
91
- return false; // can't update what doesn't exist
92
- appendFileSync(file, JSON.stringify({ _meta: 'project-update', slug, ...partial, updatedAt: Date.now() }) + '\n', 'utf-8');
93
- return true;
94
- }
95
- catch {
96
- return false;
97
- }
98
- }
99
- /**
100
- * Returns true if any prior project-update line for this slug carries
101
- * userOverride:true. Used by the async Title agent (and the periodic backfill)
102
- * to skip overwriting a title the user has manually set.
103
- *
104
- * The race this guards: spawn fires Title agent in background → user renames
105
- * before LLM returns → Title agent's later write would clobber the manual
106
- * rename because the reader takes the latest title. With this check, the
107
- * Title agent reads its own outcome moments before writing and bails if the
108
- * user got there first. Best-effort; on read failure returns false (Title
109
- * agent proceeds) — same posture as every other appendThreadEntry caller.
110
- */
111
- /**
112
- * Derive a clean Title-Cased name from a working directory path. Used as the
113
- * immediate seed title (so the sidebar never shows blank / placeholder during
114
- * the LLM call) and as the permanent fallback when the Title agent returns
115
- * null / garbage. Examples:
116
- * /Users/x/code/codehere → "Codehere"
117
- * /Users/x/work/auth_service → "Auth Service"
118
- * /Users/x/code/some-cool-thing → "Some Cool Thing"
119
- * / → "Project"
120
- */
121
- export function titleFromCwd(cwd) {
122
- if (!cwd)
123
- return 'Project';
124
- const segments = cwd.split('/').filter(Boolean);
125
- const last = segments[segments.length - 1];
126
- if (!last)
127
- return 'Project';
128
- // Replace separators with spaces, title-case each word, cap at 6 words
129
- const words = last
130
- .replace(/[._-]+/g, ' ')
131
- .replace(/([a-z])([A-Z])/g, '$1 $2') // camelCase split
132
- .split(/\s+/)
133
- .filter(Boolean)
134
- .slice(0, 6)
135
- .map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase());
136
- return words.join(' ') || 'Project';
137
- }
138
- /**
139
- * One-shot backfill for legacy garbage project titles (Bug A4 retroactive).
140
- *
141
- * The original A4 fix (rejected garbage titles + cwd fallback) only applied
142
- * to NEW projects. Existing thread.jsonl files written before the fix can
143
- * still carry titles like "Request ID: req_abc123…" or "I cannot generate…"
144
- * sitting in the latest project-update record. Those show up in the sidebar
145
- * forever until the user manually renames.
146
- *
147
- * Walks every ~/.codehere/projects/<slug>/thread.jsonl, reads the latest
148
- * project-update title, runs the same garbage detection used at write time.
149
- * If garbage, appends a new project-update with the cwd-derived fallback
150
- * title plus `_backfilled: true` marker so re-runs are no-ops.
151
- *
152
- * Manual overrides (set via Pattern 10 inline rename) are NEVER touched —
153
- * hasManualOverride guards the rewrite.
154
- *
155
- * Best-effort: any per-project failure logs to stderr (when CODEHERE_DEBUG=1)
156
- * and continues. Returns the count of titles rewritten.
157
- */
158
- export async function backfillProjectTitles() {
159
- let scanned = 0;
160
- let rewritten = 0;
161
- if (!existsSync(PROJECTS_DIR))
162
- return { scanned, rewritten };
163
- // Lazy-load sanitizeTitle to avoid a static import cycle (use-case → infra).
164
- const { sanitizeTitle } = await import('../../application/use-cases/generate-thread-title.js');
165
- let entries;
166
- try {
167
- const { readdirSync } = await import('fs');
168
- entries = readdirSync(PROJECTS_DIR);
169
- }
170
- catch {
171
- return { scanned, rewritten };
172
- }
173
- for (const slug of entries) {
174
- // Skip non-slug-shaped names + hidden files
175
- if (!/^[a-f0-9]{12}$|^unknown$/.test(slug))
176
- continue;
177
- scanned++;
178
- try {
179
- const file = join(PROJECTS_DIR, slug, 'thread.jsonl');
180
- if (!existsSync(file))
181
- continue;
182
- if (hasManualOverride(slug))
183
- continue; // Never overwrite user renames
184
- const content = readFileSync(file, 'utf-8');
185
- const lines = content.split('\n').filter((l) => l.trim());
186
- // Find the latest project-update + the most recent task cwd (for fallback)
187
- let latestTitle = null;
188
- let latestCwd = null;
189
- let alreadyBackfilled = false;
190
- for (const line of lines) {
191
- try {
192
- const rec = JSON.parse(line);
193
- if (rec._meta === 'project-update' && typeof rec.title === 'string') {
194
- latestTitle = rec.title;
195
- if (rec._backfilled === true)
196
- alreadyBackfilled = true;
197
- else
198
- alreadyBackfilled = false;
199
- }
200
- if (rec.type === 'task' && typeof rec.cwd === 'string' && rec.cwd) {
201
- latestCwd = rec.cwd;
202
- }
203
- }
204
- catch {
205
- /* skip malformed line */
206
- }
207
- }
208
- if (alreadyBackfilled)
209
- continue; // idempotent — already cleaned
210
- if (!latestTitle)
211
- continue; // nothing to check
212
- // Run the same garbage detection sanitizeTitle uses at write time.
213
- // sanitizeTitle returns '' for garbage; non-empty means clean.
214
- const cleaned = sanitizeTitle(latestTitle);
215
- if (cleaned)
216
- continue; // title is fine
217
- // Garbage — write a new project-update with cwd-derived fallback.
218
- const fallback = titleFromCwd(latestCwd);
219
- const ok = appendMetaUpdate(slug, {
220
- title: fallback,
221
- cwd: latestCwd,
222
- _backfilled: true,
223
- });
224
- if (ok) {
225
- rewritten++;
226
- if (process.env.CODEHERE_DEBUG === '1') {
227
- console.error(`[backfill] ${slug}: "${latestTitle.slice(0, 40)}" → "${fallback}"`);
228
- }
229
- }
230
- }
231
- catch {
232
- /* per-project failure → continue */
233
- }
234
- }
235
- return { scanned, rewritten };
236
- }
237
- /**
238
- * One-shot reconciliation for stale-running tasks (Bug: project-thread.jsonl
239
- * entries can stay in `status: 'running'` forever when the daemon dies
240
- * mid-task or the subprocess crashes without a clean completion event).
241
- *
242
- * The visible failure mode (per dogfood screenshots May 6): the threads
243
- * view shows RUNNING pills on 3-day-old tasks. With Pattern 8's peach-
244
- * accent running pill, the page reads as "codehere doesn't know what
245
- * state its own sessions are in" — a credibility bomb.
246
- *
247
- * Walks every ~/.codehere/projects/<slug>/thread.jsonl, reconstructs each
248
- * session's latest task state by sessionId, and for any session whose
249
- * latest state is `running` AND startedAt > 60 minutes ago, appends a
250
- * synthetic completion record marking it `crashed` with `_reconciled: true`.
251
- *
252
- * Idempotent — sessions already carrying `_reconciled: true` are skipped.
253
- * Preserves history (append-only, never rewrites prior lines).
254
- *
255
- * Best-effort: per-project failures log to stderr (CODEHERE_DEBUG=1) and
256
- * continue. Returns count of sessions reconciled.
257
- */
258
- export async function reconcileStaleProjectTasks(options) {
259
- const STALE_AFTER_MS = options?.staleAfterMs ?? 60 * 60 * 1000; // 60 minutes
260
- const now = Date.now();
261
- let scanned = 0;
262
- let reconciled = 0;
263
- if (!existsSync(PROJECTS_DIR))
264
- return { scanned, reconciled };
265
- let entries;
266
- try {
267
- const { readdirSync } = await import('fs');
268
- entries = readdirSync(PROJECTS_DIR);
269
- }
270
- catch {
271
- return { scanned, reconciled };
272
- }
273
- for (const slug of entries) {
274
- if (!/^[a-f0-9]{12}$|^unknown$/.test(slug))
275
- continue;
276
- scanned++;
277
- try {
278
- const file = join(PROJECTS_DIR, slug, 'thread.jsonl');
279
- if (!existsSync(file))
280
- continue;
281
- const content = readFileSync(file, 'utf-8');
282
- const lines = content.split('\n').filter((l) => l.trim());
283
- const bySession = new Map();
284
- for (const line of lines) {
285
- try {
286
- const rec = JSON.parse(line);
287
- if (rec.type !== 'task' || typeof rec.sessionId !== 'string')
288
- continue;
289
- const cur = bySession.get(rec.sessionId) ?? {
290
- sessionId: rec.sessionId,
291
- status: '',
292
- startedAt: 0,
293
- reconciled: false,
294
- };
295
- if (typeof rec.status === 'string')
296
- cur.status = rec.status;
297
- if (typeof rec.startedAt === 'number' && rec.startedAt > 0)
298
- cur.startedAt = rec.startedAt;
299
- if (rec._reconciled === true)
300
- cur.reconciled = true;
301
- bySession.set(rec.sessionId, cur);
302
- }
303
- catch {
304
- /* skip malformed line */
305
- }
306
- }
307
- // Find stale-running candidates and append synthetic completions.
308
- for (const state of bySession.values()) {
309
- if (state.status !== 'running')
310
- continue;
311
- if (state.reconciled)
312
- continue; // already cleaned
313
- if (state.startedAt <= 0)
314
- continue; // safety: no time data
315
- if (now - state.startedAt < STALE_AFTER_MS)
316
- continue;
317
- // appendThreadEntry returns `seeded` (first-write flag), not a
318
- // success signal. For an already-seeded file, the return is always
319
- // false on both success and failure. Best-effort approach: count
320
- // optimistically — write failures are rare and bounded by the
321
- // function's own try/catch.
322
- appendThreadEntry(slug, {
323
- type: 'task',
324
- sessionId: state.sessionId,
325
- status: 'crashed',
326
- completedAt: now,
327
- durationMs: now - state.startedAt,
328
- _reconciled: true,
329
- });
330
- reconciled++;
331
- if (process.env.CODEHERE_DEBUG === '1') {
332
- const ageMin = Math.round((now - state.startedAt) / 60000);
333
- console.error(`[reconcile] ${slug}/${state.sessionId.slice(0, 8)} stale running for ${ageMin}m → crashed`);
334
- }
335
- }
336
- }
337
- catch {
338
- /* per-project failure → continue */
339
- }
340
- }
341
- return { scanned, reconciled };
342
- }
343
- export function hasManualOverride(slug) {
344
- try {
345
- const file = join(PROJECTS_DIR, slug, 'thread.jsonl');
346
- if (!existsSync(file))
347
- return false;
348
- const content = readFileSync(file, 'utf-8');
349
- for (const line of content.split('\n')) {
350
- const t = line.trim();
351
- if (!t)
352
- continue;
353
- try {
354
- const rec = JSON.parse(t);
355
- if (rec._meta === 'project-update' && rec.userOverride === true)
356
- return true;
357
- }
358
- catch {
359
- // skip malformed line
360
- }
361
- }
362
- return false;
363
- }
364
- catch {
365
- return false;
366
- }
367
- }
368
- /**
369
- * v0.6.x — user-initiated rename. The Title agent generates a title from the
370
- * first prompt, but project intent drifts (e.g. /agent dir gets titled "Scan
371
- * Auth File" forever even after 100+ unrelated tasks). This gives the user a
372
- * mechanism to override that title without re-firing the LLM-based agent.
373
- *
374
- * Returns { ok: false, error } on validation/fs failure so the HTTP layer can
375
- * surface a meaningful 4xx instead of a silent best-effort write. Note: this
376
- * is the only project-update path that returns errors — Title agent
377
- * appendMetaUpdate is silent because LLM hiccups are normal.
378
- */
379
- export function renameProject(slug, newTitle) {
380
- // Validate slug shape — guards against path traversal in case the caller
381
- // doesn't sanitize the URL parameter (HTTP layer validates this too, defense in depth).
382
- if (!/^([a-f0-9]{12}|unknown)$/.test(slug)) {
383
- return { ok: false, error: 'Invalid project slug' };
384
- }
385
- // Validate title — strip whitespace, enforce length and single-line.
386
- const trimmed = newTitle.trim();
387
- if (!trimmed)
388
- return { ok: false, error: 'Title cannot be empty' };
389
- if (trimmed.length > 100)
390
- return { ok: false, error: 'Title too long (max 100 chars)' };
391
- if (/[\n\r]/.test(trimmed))
392
- return { ok: false, error: 'Title cannot contain newlines' };
393
- // Verify project exists — appendMetaUpdate is silent on missing files but
394
- // we want a clean 404 path for the HTTP caller.
395
- const file = join(PROJECTS_DIR, slug, 'thread.jsonl');
396
- if (!existsSync(file))
397
- return { ok: false, error: 'Project not found' };
398
- // userOverride:true tells the async Title agent + periodic backfill to skip
399
- // writing for this slug — see hasManualOverride() above. Without this flag,
400
- // a slow LLM call from spawn-time can land AFTER the user's rename and
401
- // clobber it (the reader takes the latest project-update title).
402
- if (!appendMetaUpdate(slug, { title: trimmed, userOverride: true })) {
403
- return { ok: false, error: 'Write failed' };
404
- }
405
- return { ok: true };
406
- }
407
- /**
408
- * Mirror a spawn event to the project thread. Skips temp-dir cwds (matches
409
- * migration defaults) so test-artifact dirs don't pollute the project list.
410
- *
411
- * If this spawn is the FIRST task in a brand-new project (i.e., thread.jsonl
412
- * just got seeded), fire the hidden Title agent (v0.6.0 M9) in the background
413
- * to generate a 4-6 word title from the first user prompt. The title appears
414
- * in the project-meta as a 'project-update' record; readers take the latest
415
- * value. Title generation is best-effort and silent on failure.
416
- */
417
- export function mirrorSpawnedToThread(rec) {
418
- if (isTempDir(rec.cwd))
419
- return;
420
- const slug = slugForCwd(rec.cwd);
421
- const seeded = appendThreadEntry(slug, {
422
- type: 'task',
423
- sessionId: rec.sessionId,
424
- agent: rec.agent,
425
- prompt: rec.prompt,
426
- cwd: rec.cwd,
427
- startedAt: rec.startedAt,
428
- status: 'running',
429
- });
430
- if (seeded) {
431
- // Seed an immediate cwd-derived title so the sidebar never shows a blank
432
- // / placeholder while the LLM call is in flight (or if it fails entirely).
433
- // The Title agent can later overwrite this with a richer prompt-derived
434
- // title; user manual rename always wins over both.
435
- if (!hasManualOverride(slug)) {
436
- const fallback = titleFromCwd(rec.cwd);
437
- appendMetaUpdate(slug, { title: fallback, cwd: rec.cwd });
438
- }
439
- if (process.env.CODEHERE_DISABLE_TITLE_AGENT !== '1') {
440
- // Fire Title agent in background; do not block the spawn path. Lazy-import
441
- // to avoid loading the LLM module on every spawn when not needed.
442
- void (async () => {
443
- try {
444
- const { generateThreadTitle } = await import('../../application/use-cases/generate-thread-title.js');
445
- const title = await generateThreadTitle(rec.prompt, rec.cwd);
446
- // Last-moment manual-override check: between spawn and now (LLM call
447
- // can take seconds), user may have already renamed. Don't clobber.
448
- // Also: if Title agent returned null (garbage rejected, provider
449
- // unreachable, refusal), keep the cwd-derived fallback already seeded
450
- // above — never write `(untitled)` or empty.
451
- if (title && !hasManualOverride(slug)) {
452
- appendMetaUpdate(slug, { title, cwd: rec.cwd });
453
- }
454
- }
455
- catch {
456
- // best-effort — cwd fallback already seeded
457
- }
458
- })();
459
- }
460
- }
461
- }
462
- /**
463
- * Mirror a completion event to the project thread. Same skip rules as spawn.
464
- * Reader reconstructs by sessionId — this entry's fields override the spawn
465
- * entry's where they overlap (status flips from 'running' to actual final).
466
- */
467
- export function mirrorCompletedToThread(rec) {
468
- if (isTempDir(rec.cwd))
469
- return;
470
- const slug = slugForCwd(rec.cwd);
471
- appendThreadEntry(slug, {
472
- type: 'task',
473
- sessionId: rec.sessionId,
474
- status: rec.status,
475
- durationMs: rec.durationMs,
476
- completedAt: rec.completedAt,
477
- steps: rec.steps,
478
- artifacts: rec.artifacts,
479
- });
480
- }
481
- /** Label map for raw error codes → user-facing text. */
482
- const FAILOVER_LABELS = {
483
- rate_limit: 'hit a rate limit',
484
- quota_exceeded: 'exhausted its quota',
485
- credit_low: 'ran low on credits',
486
- error: 'encountered an error',
487
- };
488
- export function mirrorFailoverToThread(rec) {
489
- if (isTempDir(rec.cwd))
490
- return;
491
- const slug = slugForCwd(rec.cwd);
492
- const reasonLabel = FAILOVER_LABELS[rec.reason] ?? 'became unavailable';
493
- const fromLabel = rec.fromAgent;
494
- const toLabel = rec.toAgent;
495
- appendThreadEntry(slug, {
496
- type: 'task',
497
- sessionId: rec.sessionId,
498
- agent: 'system',
499
- prompt: `↻ Switched from ${fromLabel} to ${toLabel} (${reasonLabel})`,
500
- status: 'completed',
501
- startedAt: Date.now(),
502
- completedAt: Date.now(),
503
- durationMs: 0,
504
- });
505
- }
506
- export function mirrorReplyToThread(rec) {
507
- if (isTempDir(rec.cwd))
508
- return;
509
- const slug = slugForCwd(rec.cwd);
510
- appendThreadEntry(slug, {
511
- type: 'task',
512
- sessionId: rec.replySessionId,
513
- parentSessionId: rec.parentSessionId,
514
- agent: rec.agent,
515
- prompt: rec.prompt,
516
- cwd: rec.cwd,
517
- startedAt: rec.startedAt,
518
- completedAt: rec.completedAt,
519
- durationMs: rec.durationMs,
520
- status: 'completed',
521
- ...(rec.result ? { result: rec.result } : {}),
522
- });
523
- }
524
- //# sourceMappingURL=project-thread-mirror.js.map
@@ -1,172 +0,0 @@
1
- /**
2
- * Project thread reader — v0.6.0 M1 (UI-prep slice).
3
- *
4
- * Reads ~/.codehere/projects/<slug>/thread.jsonl files and reconstructs
5
- * the per-project thread state for API consumers (the new UI rendering
6
- * the persistent project thread shape).
7
- *
8
- * Reconstruction rules:
9
- * - One file per project, identified by slug (sha1(cwd)[:12])
10
- * - First line is `{_meta: 'project', ...}` — project header
11
- * - Subsequent lines are `{_meta: 'project-update', ...}` (latest wins per
12
- * field — Title agent uses this to set the title) OR `{type: 'task', ...}`
13
- * (per-session entries; later entries with same sessionId override
14
- * earlier — covers spawn entry → completion entry update)
15
- *
16
- * NOT in scope of this module:
17
- * - Writing (handled by project-thread-mirror + project-thread-migration)
18
- * - UI rendering (the new thread view will consume this reader's output)
19
- * - Live SSE updates (polling is fine for the alpha; SSE in v0.6.x patch)
20
- */
21
- import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
22
- import { join } from 'path';
23
- import { homedir } from 'os';
24
- const DEFAULT_PROJECTS_DIR = join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'projects');
25
- /** List all known projects with summary stats. Cheap; no full task content. */
26
- export function listProjects(options) {
27
- const root = options?.projectsDir ?? DEFAULT_PROJECTS_DIR;
28
- if (!existsSync(root))
29
- return [];
30
- let stat;
31
- try {
32
- stat = statSync(root);
33
- }
34
- catch {
35
- return [];
36
- }
37
- if (!stat.isDirectory())
38
- return [];
39
- const slugs = readdirSync(root).filter((entry) => {
40
- const p = join(root, entry);
41
- try {
42
- return statSync(p).isDirectory();
43
- }
44
- catch {
45
- return false;
46
- }
47
- });
48
- const summaries = [];
49
- for (const slug of slugs) {
50
- // Pattern 4 — sidebar presence dots need agent attribution per project.
51
- // Pull the full task list (lightweight — agent + startedAt only) so we
52
- // can compute lastAgent + recentAgents (multi-agent stack). taskLimit:0
53
- // would drop the data we need; fetch unbounded but discard the heavy
54
- // fields (steps + artifacts) below.
55
- const thread = readProjectThread(slug, { projectsDir: root });
56
- if (thread) {
57
- const agentChrono = thread.tasks
58
- .filter((t) => typeof t.agent === 'string' && t.agent)
59
- .sort((a, b) => b.startedAt - a.startedAt) // most-recent first
60
- .map((t) => t.agent);
61
- const lastAgent = agentChrono.length > 0 ? agentChrono[0] : null;
62
- const seen = new Set();
63
- const recentAgents = [];
64
- for (const a of agentChrono) {
65
- if (seen.has(a))
66
- continue;
67
- seen.add(a);
68
- recentAgents.push(a);
69
- if (recentAgents.length >= 3)
70
- break;
71
- }
72
- summaries.push({
73
- slug: thread.slug,
74
- cwd: thread.cwd,
75
- title: thread.title,
76
- taskCount: thread.taskCount,
77
- lastActivityAt: thread.lastActivityAt,
78
- lastAgent,
79
- recentAgents,
80
- });
81
- }
82
- }
83
- // Sort by recency (most recent first)
84
- summaries.sort((a, b) => (b.lastActivityAt ?? 0) - (a.lastActivityAt ?? 0));
85
- return summaries;
86
- }
87
- /** Read a single project thread by slug. Returns null if not found. */
88
- export function readProjectThread(slug, options) {
89
- const root = options?.projectsDir ?? DEFAULT_PROJECTS_DIR;
90
- const file = join(root, slug, 'thread.jsonl');
91
- if (!existsSync(file))
92
- return null;
93
- let content;
94
- try {
95
- content = readFileSync(file, 'utf-8');
96
- }
97
- catch {
98
- return null;
99
- }
100
- let cwd = null;
101
- let title = null;
102
- const tasksById = new Map();
103
- for (const line of content.split('\n')) {
104
- const trimmed = line.trim();
105
- if (!trimmed)
106
- continue;
107
- let rec;
108
- try {
109
- rec = JSON.parse(trimmed);
110
- }
111
- catch {
112
- continue;
113
- }
114
- if (rec._meta === 'project') {
115
- if (typeof rec.cwd === 'string')
116
- cwd = rec.cwd;
117
- if (typeof rec.title === 'string')
118
- title = rec.title;
119
- }
120
- else if (rec._meta === 'project-update') {
121
- if (typeof rec.title === 'string')
122
- title = rec.title;
123
- if (typeof rec.cwd === 'string' && cwd === null)
124
- cwd = rec.cwd;
125
- }
126
- else if (rec.type === 'task' && typeof rec.sessionId === 'string') {
127
- const existing = tasksById.get(rec.sessionId) ?? {
128
- sessionId: rec.sessionId,
129
- agent: '',
130
- prompt: '',
131
- cwd: '',
132
- startedAt: 0,
133
- };
134
- // Merge fields — later entries override earlier (covers
135
- // spawn → completion update pattern from forward-mirror)
136
- for (const k of [
137
- 'agent',
138
- 'prompt',
139
- 'cwd',
140
- 'startedAt',
141
- 'status',
142
- 'durationMs',
143
- 'completedAt',
144
- 'steps',
145
- 'artifacts',
146
- ]) {
147
- if (rec[k] !== undefined) {
148
- existing[k] = rec[k];
149
- }
150
- }
151
- tasksById.set(rec.sessionId, existing);
152
- }
153
- }
154
- const tasks = [...tasksById.values()].sort((a, b) => a.startedAt - b.startedAt);
155
- const lastActivityAt = tasks.reduce((acc, t) => {
156
- const ts = t.completedAt ?? t.startedAt;
157
- if (ts && (acc === null || ts > acc))
158
- return ts;
159
- return acc;
160
- }, null);
161
- const limit = options?.taskLimit;
162
- const cappedTasks = limit === undefined ? tasks : limit === 0 ? [] : tasks.slice(-limit);
163
- return {
164
- slug,
165
- cwd,
166
- title,
167
- taskCount: tasks.length,
168
- tasks: cappedTasks,
169
- lastActivityAt,
170
- };
171
- }
172
- //# sourceMappingURL=project-thread-reader.js.map