codehere 1.0.1 → 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 (163) hide show
  1. package/README.md +36 -192
  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 +110 -7
  5. package/dist/index.js +38 -1212
  6. package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -323
  7. package/dist/presentation/cli/commands/check-command.js +178 -23
  8. package/dist/presentation/cli/commands/help-command.js +21 -285
  9. package/dist/ui.js +5 -236
  10. package/dist/utils/version.js +0 -6
  11. package/package.json +9 -38
  12. package/dist/application/services/verdict-service.js +0 -154
  13. package/dist/application/use-cases/assess-cross-vendor.js +0 -126
  14. package/dist/application/use-cases/build-cross-vendor-verdict.js +0 -118
  15. package/dist/application/use-cases/domain-trust.js +0 -110
  16. package/dist/application/use-cases/export-aat.js +0 -107
  17. package/dist/application/use-cases/generate-acceptance-criteria.js +0 -223
  18. package/dist/application/use-cases/generate-final-report.js +0 -259
  19. package/dist/application/use-cases/generate-narration.js +0 -130
  20. package/dist/application/use-cases/generate-thread-title.js +0 -181
  21. package/dist/application/use-cases/narrator-prompt-template.js +0 -158
  22. package/dist/application/use-cases/plan-auto-review.js +0 -57
  23. package/dist/application/use-cases/reconcile-audit.js +0 -100
  24. package/dist/application/use-cases/search-sessions.js +0 -70
  25. package/dist/application/use-cases/should-auto-review.js +0 -71
  26. package/dist/application/use-cases/thread-export.js +0 -245
  27. package/dist/application/use-cases/verify-claim.js +0 -456
  28. package/dist/application/use-cases/write-pass.js +0 -277
  29. package/dist/domain/audit.js +0 -11
  30. package/dist/domain/services/codebase-detector.js +0 -85
  31. package/dist/domain/task.js +0 -91
  32. package/dist/error-handler.js +0 -139
  33. package/dist/infrastructure/completion/bash-completion.js +0 -29
  34. package/dist/infrastructure/completion/fish-completion.js +0 -17
  35. package/dist/infrastructure/completion/zsh-completion.js +0 -25
  36. package/dist/infrastructure/config/config-manager.js +0 -174
  37. package/dist/infrastructure/config/product-context.js +0 -49
  38. package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
  39. package/dist/infrastructure/context/adapters/figma-source.js +0 -198
  40. package/dist/infrastructure/context/adapters/github-issue-source.js +0 -160
  41. package/dist/infrastructure/context/adapters/linear-source.js +0 -190
  42. package/dist/infrastructure/context/context-resolver.js +0 -144
  43. package/dist/infrastructure/context/context-source.js +0 -114
  44. package/dist/infrastructure/context/github-api.js +0 -110
  45. package/dist/infrastructure/context/github-device-flow.js +0 -65
  46. package/dist/infrastructure/context/oauth-flow.js +0 -207
  47. package/dist/infrastructure/context/oauth-store.js +0 -122
  48. package/dist/infrastructure/context/registry-bootstrap.js +0 -95
  49. package/dist/infrastructure/context/write-pass-resolver.js +0 -106
  50. package/dist/infrastructure/cost/budget-config.js +0 -114
  51. package/dist/infrastructure/cost/cost-router.js +0 -97
  52. package/dist/infrastructure/cost/spend-aggregator.js +0 -222
  53. package/dist/infrastructure/cost/team-ledger.js +0 -355
  54. package/dist/infrastructure/discovery/discovery-log.js +0 -85
  55. package/dist/infrastructure/hooks/audit-tip-signature.js +0 -336
  56. package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -1018
  57. package/dist/infrastructure/hooks/hook-mode.js +0 -87
  58. package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -49
  59. package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
  60. package/dist/infrastructure/hooks/session-analyzer.js +0 -177
  61. package/dist/infrastructure/observability/execution-tracer.js +0 -227
  62. package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
  63. package/dist/infrastructure/orchestration/agent-installer.js +0 -101
  64. package/dist/infrastructure/orchestration/agents-config-seed.js +0 -68
  65. package/dist/infrastructure/orchestration/aider-adapter.js +0 -611
  66. package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
  67. package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -592
  68. package/dist/infrastructure/orchestration/codex-adapter.js +0 -574
  69. package/dist/infrastructure/orchestration/conductor-adapter.js +0 -416
  70. package/dist/infrastructure/orchestration/context-handoff.js +0 -162
  71. package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
  72. package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
  73. package/dist/infrastructure/orchestration/detect-ollama.js +0 -75
  74. package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
  75. package/dist/infrastructure/orchestration/failover-config.js +0 -67
  76. package/dist/infrastructure/orchestration/failover-policy.js +0 -106
  77. package/dist/infrastructure/orchestration/gemini-adapter.js +0 -663
  78. package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -689
  79. package/dist/infrastructure/orchestration/graphify-context.js +0 -209
  80. package/dist/infrastructure/orchestration/memory-injector.js +0 -380
  81. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -388
  82. package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
  83. package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
  84. package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
  85. package/dist/infrastructure/orchestration/opencode-adapter.js +0 -580
  86. package/dist/infrastructure/orchestration/progress-aggregator.js +0 -906
  87. package/dist/infrastructure/orchestration/readiness.js +0 -103
  88. package/dist/infrastructure/orchestration/session-manager.js +0 -1590
  89. package/dist/infrastructure/orchestration/session-persistence.js +0 -394
  90. package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
  91. package/dist/infrastructure/orchestration/write-pass-store.js +0 -38
  92. package/dist/infrastructure/protocols/cloud-sync.js +0 -207
  93. package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
  94. package/dist/infrastructure/protocols/mcp-server-impl.js +0 -3935
  95. package/dist/infrastructure/protocols/mcp-tools.js +0 -401
  96. package/dist/infrastructure/protocols/probe-port.js +0 -36
  97. package/dist/infrastructure/protocols/thread-phase.js +0 -105
  98. package/dist/infrastructure/protocols/ui/app-script-artifacts.js +0 -121
  99. package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +0 -44
  100. package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +0 -56
  101. package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +0 -588
  102. package/dist/infrastructure/protocols/ui/app-script-context-sources.js +0 -168
  103. package/dist/infrastructure/protocols/ui/app-script-conversation.js +0 -67
  104. package/dist/infrastructure/protocols/ui/app-script-drawer.js +0 -115
  105. package/dist/infrastructure/protocols/ui/app-script-failover.js +0 -133
  106. package/dist/infrastructure/protocols/ui/app-script-github.js +0 -113
  107. package/dist/infrastructure/protocols/ui/app-script-observer.js +0 -104
  108. package/dist/infrastructure/protocols/ui/app-script-onboarding.js +0 -80
  109. package/dist/infrastructure/protocols/ui/app-script-overlay.js +0 -203
  110. package/dist/infrastructure/protocols/ui/app-script-recovery.js +0 -136
  111. package/dist/infrastructure/protocols/ui/app-script-render.js +0 -166
  112. package/dist/infrastructure/protocols/ui/app-script-share.js +0 -78
  113. package/dist/infrastructure/protocols/ui/app-script-sidebar.js +0 -194
  114. package/dist/infrastructure/protocols/ui/app-script-tour.js +0 -78
  115. package/dist/infrastructure/protocols/ui/app-script-wpass.js +0 -312
  116. package/dist/infrastructure/protocols/ui/app-script.js +0 -2998
  117. package/dist/infrastructure/protocols/ui/font-faces.js +0 -7
  118. package/dist/infrastructure/protocols/ui/styles.js +0 -2147
  119. package/dist/infrastructure/protocols/ui/tokens.js +0 -96
  120. package/dist/infrastructure/protocols/ui-page-v2.js +0 -320
  121. package/dist/infrastructure/protocols/verify-page.js +0 -1677
  122. package/dist/infrastructure/security/file-collection.js +0 -118
  123. package/dist/infrastructure/security/fix-code.js +0 -144
  124. package/dist/infrastructure/security/fix-secret.js +0 -89
  125. package/dist/infrastructure/security/permissions-config.js +0 -235
  126. package/dist/infrastructure/security/scan-types.js +0 -10
  127. package/dist/infrastructure/security/ship-scan.js +0 -195
  128. package/dist/infrastructure/security/trust-scorer.js +0 -724
  129. package/dist/infrastructure/security/vibe-checks.js +0 -99
  130. package/dist/infrastructure/storage/atomic-write.js +0 -86
  131. package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
  132. package/dist/infrastructure/storage/project-thread-migration.js +0 -237
  133. package/dist/infrastructure/storage/project-thread-mirror.js +0 -671
  134. package/dist/infrastructure/storage/project-thread-reader.js +0 -409
  135. package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
  136. package/dist/monitoring.js +0 -212
  137. package/dist/presentation/cli/commands/audit-command.js +0 -201
  138. package/dist/presentation/cli/commands/budget-command.js +0 -79
  139. package/dist/presentation/cli/commands/config-command.js +0 -125
  140. package/dist/presentation/cli/commands/context-source-command.js +0 -217
  141. package/dist/presentation/cli/commands/hook-command.js +0 -265
  142. package/dist/presentation/cli/commands/init-command.js +0 -590
  143. package/dist/presentation/cli/commands/memory-command.js +0 -1031
  144. package/dist/presentation/cli/commands/narrator-command.js +0 -101
  145. package/dist/presentation/cli/commands/permission-command.js +0 -105
  146. package/dist/presentation/cli/commands/protocols-builder.js +0 -254
  147. package/dist/presentation/cli/commands/protocols-command.js +0 -84
  148. package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
  149. package/dist/presentation/cli/commands/report-command.js +0 -403
  150. package/dist/presentation/cli/commands/scan-command.js +0 -52
  151. package/dist/presentation/cli/commands/scan-tool-command.js +0 -110
  152. package/dist/presentation/cli/commands/search-command.js +0 -51
  153. package/dist/presentation/cli/commands/setup-command.js +0 -330
  154. package/dist/presentation/cli/commands/status-command.js +0 -614
  155. package/dist/presentation/cli/commands/team-command.js +0 -256
  156. package/dist/presentation/cli/commands/thread-command.js +0 -370
  157. package/dist/presentation/cli/commands/trust-command.js +0 -328
  158. package/dist/presentation/cli/commands/verify-command.js +0 -298
  159. package/dist/presentation/cli/error-display.js +0 -328
  160. package/dist/presentation/cli/repl.js +0 -805
  161. package/dist/presentation/cli/task-deliverable.js +0 -153
  162. package/dist/testing/cassette.js +0 -125
  163. package/dist/testing/replay-adapter.js +0 -147
@@ -1,409 +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
- import { estimateCostUsd } from '../cost/spend-aggregator.js';
25
- const DEFAULT_PROJECTS_DIR = join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'projects');
26
- /**
27
- * Normalize a git remote URL to a stable identity key `host/org/repo`:
28
- * - strips scheme (https/http/ssh/git) + any user@ info
29
- * - handles scp-like `git@host:org/repo`
30
- * - drops a trailing `.git` and trailing slashes
31
- * - lowercases the host (path case preserved)
32
- * Returns null for empty/garbage input. Shared by the reader (identity grouping)
33
- * and the forward-mirror (write-time detection) so both compute the same id.
34
- */
35
- export function normalizeRemote(url) {
36
- if (!url)
37
- return null;
38
- let s = String(url).trim();
39
- if (!s)
40
- return null;
41
- const scp = s.match(/^[^@/]+@([^:/]+):(.+)$/); // git@host:org/repo(.git)
42
- if (scp) {
43
- s = scp[1] + '/' + scp[2];
44
- }
45
- else {
46
- s = s.replace(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//, ''); // scheme://
47
- s = s.replace(/^[^@/]+@/, ''); // user@ (ssh://user@host/…)
48
- }
49
- s = s.replace(/\.git$/i, '').replace(/\/+$/, '');
50
- if (!s)
51
- return null;
52
- const slash = s.indexOf('/');
53
- if (slash === -1)
54
- return s.toLowerCase();
55
- return s.slice(0, slash).toLowerCase() + s.slice(slash);
56
- }
57
- /** True for hosts a clone must never reach — loopback, link-local (incl. cloud
58
- * metadata 169.254.169.254), and RFC-1918 private ranges. `host` is the
59
- * `host[:port]` segment from a normalized remote. */
60
- function isBlockedCloneHost(host) {
61
- const h = String(host ?? '')
62
- .split(':')[0]
63
- .toLowerCase();
64
- if (!h)
65
- return true;
66
- if (h === 'localhost' || h === '::1' || h === '0.0.0.0' || h.endsWith('.localhost'))
67
- return true;
68
- if (/^127\./.test(h))
69
- return true; // loopback
70
- if (/^169\.254\./.test(h))
71
- return true; // link-local + cloud metadata
72
- if (/^10\./.test(h))
73
- return true; // RFC-1918
74
- if (/^192\.168\./.test(h))
75
- return true; // RFC-1918
76
- if (/^172\.(1[6-9]|2\d|3[01])\./.test(h))
77
- return true; // RFC-1918
78
- if (/^0\./.test(h))
79
- return true; // "this" network
80
- return false;
81
- }
82
- /**
83
- * Parse a user-pasted GitHub (or any git) URL into a cloneable URL + a
84
- * predictable local target directory. Pure (no IO) so the clone route can be
85
- * tested without a network or filesystem. Accepts the forms a developer
86
- * actually pastes: full https, ssh (git@host:org/repo), bare host/org/repo,
87
- * and the `org/repo` GitHub shorthand. Rejects anything without a real
88
- * host/org/repo shape (or with whitespace — a git URL never has any).
89
- *
90
- * The target dir is `<baseDir>/<repo>` — visible + predictable, NOT buried in
91
- * a hidden dir, so the user can `cd` to it. The caller decides what to do when
92
- * the dir already exists (we open it rather than re-clone — see the route).
93
- */
94
- export function parseGitHubCloneTarget(rawUrl, baseDir) {
95
- const raw = String(rawUrl ?? '').trim();
96
- if (!raw)
97
- return { ok: false, error: 'url_required' };
98
- if (/\s/.test(raw))
99
- return { ok: false, error: 'invalid_url' };
100
- let cloneUrl;
101
- if (/^[^@/]+@[^:/]+:.+/.test(raw)) {
102
- cloneUrl = raw; // git@host:org/repo(.git)
103
- }
104
- else if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(raw)) {
105
- cloneUrl = raw; // already has a scheme (https://, ssh://, …)
106
- }
107
- else if (/^[\w.-]+\.[\w.-]+\/.+/.test(raw)) {
108
- cloneUrl = 'https://' + raw; // host.tld/org/repo (no scheme)
109
- }
110
- else if (/^[\w.-]+\/[\w.-]+$/.test(raw)) {
111
- cloneUrl = 'https://github.com/' + raw; // org/repo GitHub shorthand
112
- }
113
- else {
114
- return { ok: false, error: 'invalid_url' };
115
- }
116
- // Scheme allowlist — only https + ssh reach `git clone`. Rejects file:// (local
117
- // file read), http:// (plaintext + the cloud-metadata / localhost SSRF vector),
118
- // and every other transport. The scp form (git@host:…) carries no scheme and is
119
- // allowed. This is the SSRF/file:// surface a pre-push review flagged.
120
- const scheme = cloneUrl.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):\/\//);
121
- if (scheme && !['https', 'ssh'].includes(scheme[1].toLowerCase()))
122
- return { ok: false, error: 'invalid_url' };
123
- const norm = normalizeRemote(cloneUrl); // host/org/repo, lowercased host
124
- if (!norm)
125
- return { ok: false, error: 'invalid_url' };
126
- const parts = norm.split('/');
127
- if (parts.length < 3)
128
- return { ok: false, error: 'invalid_url' }; // need host + org + repo
129
- // Internal-host denylist — don't let a clone target loopback / link-local /
130
- // RFC-1918 (the rest of the SSRF surface: metadata endpoints, internal probing).
131
- if (isBlockedCloneHost(parts[0]))
132
- return { ok: false, error: 'invalid_url' };
133
- const repo = (parts[parts.length - 1] || '').replace(/[^\w.-]/g, '');
134
- // Reject '.'/'..'/empty — '.' and '..' survive the char-class strip and would
135
- // traverse out of baseDir (join(baseDir, '..') = its parent). The clone dir
136
- // must be a fresh leaf inside baseDir, nothing else.
137
- if (!repo || repo === '.' || repo === '..')
138
- return { ok: false, error: 'invalid_url' };
139
- return { ok: true, cloneUrl, repo, dir: join(baseDir, repo) };
140
- }
141
- /** List all known projects with summary stats. Cheap; no full task content. */
142
- export function listProjects(options) {
143
- const root = options?.projectsDir ?? DEFAULT_PROJECTS_DIR;
144
- if (!existsSync(root))
145
- return [];
146
- let stat;
147
- try {
148
- stat = statSync(root);
149
- }
150
- catch {
151
- return [];
152
- }
153
- if (!stat.isDirectory())
154
- return [];
155
- const slugs = readdirSync(root).filter((entry) => {
156
- const p = join(root, entry);
157
- try {
158
- return statSync(p).isDirectory();
159
- }
160
- catch {
161
- return false;
162
- }
163
- });
164
- const perSlug = [];
165
- for (const slug of slugs) {
166
- // Pattern 4 — sidebar presence dots need agent attribution per project.
167
- // Pull the full task list so we can compute agents + per-project cost.
168
- const thread = readProjectThread(slug, { projectsDir: root });
169
- if (!thread)
170
- continue;
171
- const agentChrono = thread.tasks
172
- .filter((t) => typeof t.agent === 'string' && t.agent)
173
- .sort((a, b) => b.startedAt - a.startedAt) // most-recent first
174
- .map((t) => t.agent);
175
- // Per-project spend — same estimator the per-task /api/tasks cost uses.
176
- let totalCostCents = 0;
177
- for (const t of thread.tasks) {
178
- const u = t.artifacts?.usage;
179
- if (!u)
180
- continue;
181
- totalCostCents += estimateCostUsd(t.agent, u) * 100;
182
- }
183
- perSlug.push({
184
- slug: thread.slug,
185
- id: normalizeRemote(thread.remote) ?? 'local:' + thread.slug,
186
- remote: thread.remote,
187
- cwd: thread.cwd,
188
- title: thread.title,
189
- taskCount: thread.taskCount,
190
- totalCostCents,
191
- lastActivityAt: thread.lastActivityAt,
192
- agentChrono,
193
- });
194
- }
195
- // Second pass — merge slugs that resolve to the same project id (clones of
196
- // one git remote). Local-only projects have a unique `local:<slug>` id and
197
- // never merge. The most-recently-active member is canonical (its slug + title
198
- // win, so rename / room links keep working).
199
- const byId = new Map();
200
- for (const s of perSlug) {
201
- const g = byId.get(s.id);
202
- if (g)
203
- g.push(s);
204
- else
205
- byId.set(s.id, [s]);
206
- }
207
- const summaries = [];
208
- for (const [id, members] of byId) {
209
- members.sort((a, b) => (b.lastActivityAt ?? 0) - (a.lastActivityAt ?? 0));
210
- const head = members[0];
211
- const localPaths = [...new Set(members.map((m) => m.cwd).filter((c) => !!c))];
212
- // Recency-ordered agent stack across all members (head first).
213
- const recentAgents = [];
214
- const seen = new Set();
215
- for (const m of members) {
216
- for (const a of m.agentChrono) {
217
- if (seen.has(a))
218
- continue;
219
- seen.add(a);
220
- recentAgents.push(a);
221
- if (recentAgents.length >= 3)
222
- break;
223
- }
224
- if (recentAgents.length >= 3)
225
- break;
226
- }
227
- summaries.push({
228
- id,
229
- remote: head.remote,
230
- slug: head.slug,
231
- cwd: head.cwd,
232
- localPaths,
233
- title: head.title,
234
- taskCount: members.reduce((n, m) => n + m.taskCount, 0),
235
- totalCostCents: Math.round(members.reduce((n, m) => n + m.totalCostCents, 0) * 100) / 100,
236
- lastActivityAt: members.reduce((acc, m) => (m.lastActivityAt && (acc === null || m.lastActivityAt > acc) ? m.lastActivityAt : acc), null),
237
- lastAgent: recentAgents[0] ?? null,
238
- recentAgents,
239
- });
240
- }
241
- // Sort by recency (most recent first)
242
- summaries.sort((a, b) => (b.lastActivityAt ?? 0) - (a.lastActivityAt ?? 0));
243
- return summaries;
244
- }
245
- /** Read a single project thread by slug. Returns null if not found. */
246
- export function readProjectThread(slug, options) {
247
- const root = options?.projectsDir ?? DEFAULT_PROJECTS_DIR;
248
- const file = join(root, slug, 'thread.jsonl');
249
- if (!existsSync(file))
250
- return null;
251
- let content;
252
- try {
253
- content = readFileSync(file, 'utf-8');
254
- }
255
- catch {
256
- return null;
257
- }
258
- let cwd = null;
259
- let title = null;
260
- let remote = null;
261
- const tasksById = new Map();
262
- const archivedState = new Map(); // sessionId -> archived (latest event wins)
263
- for (const line of content.split('\n')) {
264
- const trimmed = line.trim();
265
- if (!trimmed)
266
- continue;
267
- let rec;
268
- try {
269
- rec = JSON.parse(trimmed);
270
- }
271
- catch {
272
- continue;
273
- }
274
- if (rec._meta === 'project') {
275
- if (typeof rec.cwd === 'string')
276
- cwd = rec.cwd;
277
- if (typeof rec.title === 'string')
278
- title = rec.title;
279
- if (typeof rec.remote === 'string')
280
- remote = rec.remote;
281
- }
282
- else if (rec._meta === 'project-update') {
283
- if (typeof rec.title === 'string')
284
- title = rec.title;
285
- if (typeof rec.cwd === 'string' && cwd === null)
286
- cwd = rec.cwd;
287
- // Latest non-empty remote wins; an explicit null (checked, none found)
288
- // leaves the prior value alone — we only ever upgrade toward a real remote.
289
- if (typeof rec.remote === 'string' && rec.remote)
290
- remote = rec.remote;
291
- }
292
- else if (rec._meta === 'archived' && typeof rec.sessionId === 'string') {
293
- archivedState.set(rec.sessionId, rec.archived === true);
294
- }
295
- else if (rec.type === 'task' && typeof rec.sessionId === 'string') {
296
- const existing = tasksById.get(rec.sessionId) ?? {
297
- sessionId: rec.sessionId,
298
- agent: '',
299
- prompt: '',
300
- cwd: '',
301
- startedAt: 0,
302
- };
303
- // Merge fields — later entries override earlier (covers
304
- // spawn → completion update pattern from forward-mirror)
305
- for (const k of [
306
- 'agent',
307
- 'prompt',
308
- 'cwd',
309
- 'startedAt',
310
- 'status',
311
- 'durationMs',
312
- 'completedAt',
313
- 'steps',
314
- 'artifacts',
315
- 'parentSessionId',
316
- 'finalReport',
317
- ]) {
318
- if (rec[k] !== undefined) {
319
- existing[k] = rec[k];
320
- }
321
- }
322
- tasksById.set(rec.sessionId, existing);
323
- }
324
- }
325
- // Stamp the archived flag (soft-delete) from the latest archived event per task.
326
- for (const t of tasksById.values()) {
327
- const a = archivedState.get(t.sessionId);
328
- if (a !== undefined)
329
- t.archived = a;
330
- }
331
- const tasks = [...tasksById.values()].sort((a, b) => a.startedAt - b.startedAt);
332
- const lastActivityAt = tasks.reduce((acc, t) => {
333
- const ts = t.completedAt ?? t.startedAt;
334
- if (ts && (acc === null || ts > acc))
335
- return ts;
336
- return acc;
337
- }, null);
338
- const limit = options?.taskLimit;
339
- const cappedTasks = limit === undefined ? tasks : limit === 0 ? [] : tasks.slice(-limit);
340
- return {
341
- slug,
342
- cwd,
343
- title,
344
- remote,
345
- // Count only what the UI actually shows: the sidebar tree + room list both
346
- // hide archived + demo/seed tasks, so counting them inflated the badge
347
- // (a project read "420" while showing a handful).
348
- taskCount: countVisibleTasks(tasks),
349
- tasks: cappedTasks,
350
- lastActivityAt,
351
- };
352
- }
353
- /** The honest THREAD count: distinct top-level conversations a user started.
354
- * Excludes soft-deleted (archived) and demo/seed tasks, and — the big one —
355
- * tasks with a parentSessionId, which are reply turns + auto cross-vendor
356
- * reviewers (continuations of a thread, not new threads). Counting those made a
357
- * project read "400+" while showing a handful of real threads. */
358
- function countVisibleTasks(tasks) {
359
- return tasks.filter((t) => {
360
- if (t.archived || t.parentSessionId)
361
- return false;
362
- if (/^demo-/.test(t.sessionId || ''))
363
- return false;
364
- // Failover-lineage marker ("↻ Switched from X to Y (hit a rate limit)") — a
365
- // continuation edge, not a thread the user started. Matches the sidebar tree.
366
- const p = t.prompt || '';
367
- if (p.includes('Switched from') && p.includes('rate limit'))
368
- return false;
369
- return true;
370
- }).length;
371
- }
372
- /**
373
- * Like readProjectThread, but merges every clone of the same repo (slugs that share
374
- * a git remote) into ONE thread — so a project room shows the repo's FULL history,
375
- * not just the single slug the UI happened to open. A local-only project (no remote)
376
- * has no clones to merge and is returned unchanged. Tasks are deduped by sessionId.
377
- */
378
- export function readMergedProjectThread(slug, options) {
379
- const root = options?.projectsDir ?? DEFAULT_PROJECTS_DIR;
380
- const head = readProjectThread(slug, { projectsDir: root });
381
- if (!head || !head.remote)
382
- return head;
383
- const id = normalizeRemote(head.remote);
384
- let entries;
385
- try {
386
- entries = readdirSync(root).filter((e) => /^[a-f0-9]{12}$/.test(e) && e !== slug);
387
- }
388
- catch {
389
- return head;
390
- }
391
- const byId = new Map();
392
- for (const t of head.tasks)
393
- byId.set(t.sessionId, t);
394
- for (const e of entries) {
395
- const sib = readProjectThread(e, { projectsDir: root });
396
- if (!sib || normalizeRemote(sib.remote) !== id)
397
- continue;
398
- for (const t of sib.tasks)
399
- if (!byId.has(t.sessionId))
400
- byId.set(t.sessionId, t);
401
- }
402
- const tasks = [...byId.values()].sort((a, b) => a.startedAt - b.startedAt);
403
- const lastActivityAt = tasks.reduce((acc, t) => {
404
- const ts = t.completedAt ?? t.startedAt;
405
- return ts && (acc === null || ts > acc) ? ts : acc;
406
- }, null);
407
- return { ...head, tasks, taskCount: countVisibleTasks(tasks), lastActivityAt };
408
- }
409
- //# sourceMappingURL=project-thread-reader.js.map
@@ -1,214 +0,0 @@
1
- /**
2
- * Title backfill — v0.6.0 follow-up to M9.
3
- *
4
- * The M9 hidden Title agent (generate-thread-title.ts) only fires on the FIRST
5
- * task entry written to a project's thread.jsonl. Projects seeded by the v0.6
6
- * migration (project-thread-migration.ts) bypass that path — they get a
7
- * project-meta header but no Title agent invocation. Result: 5/7 projects on
8
- * a typical dogfood machine show "(untitled)" in the /threads switcher even
9
- * though their first task carries enough signal to title from.
10
- *
11
- * This backfill closes that gap. For every project with title=null and at
12
- * least one task entry, it fires the Title agent on the first task's prompt +
13
- * cwd and appends a `{_meta: 'project-update', title: ...}` line. Reader
14
- * (project-thread-reader.ts) takes the latest project-update title.
15
- *
16
- * Idempotent: skips projects that already have a title (or that have NO tasks
17
- * to title from). Safe to run on every daemon boot. Best-effort: any LLM
18
- * failure (provider missing, timeout, rate limit) skips that project silently.
19
- *
20
- * Cost budget: ~1 LLM call per untitled project on first run after this lands.
21
- * Subsequent boots are no-ops since titles persist.
22
- */
23
- import { existsSync, readdirSync, readFileSync, appendFileSync, statSync } from 'fs';
24
- import { join } from 'path';
25
- import { homedir } from 'os';
26
- import { generateThreadTitle } from '../../application/use-cases/generate-thread-title.js';
27
- const DEFAULT_PROJECTS_DIR = join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'projects');
28
- function scanThread(slug, file) {
29
- let title = null;
30
- let firstPrompt = null;
31
- let firstCwd = null;
32
- let firstStartedAt = Number.MAX_SAFE_INTEGER;
33
- let content;
34
- try {
35
- content = readFileSync(file, 'utf-8');
36
- }
37
- catch {
38
- return { slug, title, firstPrompt, firstCwd };
39
- }
40
- for (const line of content.split('\n')) {
41
- const trimmed = line.trim();
42
- if (!trimmed)
43
- continue;
44
- let rec;
45
- try {
46
- rec = JSON.parse(trimmed);
47
- }
48
- catch {
49
- continue;
50
- }
51
- if (rec._meta === 'project') {
52
- if (typeof rec.title === 'string')
53
- title = rec.title;
54
- }
55
- else if (rec._meta === 'project-update') {
56
- if (typeof rec.title === 'string')
57
- title = rec.title;
58
- }
59
- else if (rec.type === 'task') {
60
- const startedAt = typeof rec.startedAt === 'number' ? rec.startedAt : Number.MAX_SAFE_INTEGER;
61
- if (startedAt < firstStartedAt) {
62
- firstStartedAt = startedAt;
63
- if (typeof rec.prompt === 'string')
64
- firstPrompt = rec.prompt;
65
- if (typeof rec.cwd === 'string')
66
- firstCwd = rec.cwd;
67
- }
68
- }
69
- }
70
- return { slug, title, firstPrompt, firstCwd };
71
- }
72
- /**
73
- * Backfill missing titles. Returns counts; never throws (best-effort per project).
74
- *
75
- * Disable via CODEHERE_DISABLE_TITLE_AGENT=1 (same env var that gates M9 itself —
76
- * keeps the toggle surface minimal).
77
- */
78
- export async function backfillThreadTitles(options) {
79
- const start = Date.now();
80
- const result = {
81
- scanned: 0,
82
- alreadyTitled: 0,
83
- noTasks: 0,
84
- titlesGenerated: 0,
85
- titleAgentFailed: 0,
86
- durationMs: 0,
87
- };
88
- if (process.env.CODEHERE_DISABLE_TITLE_AGENT === '1') {
89
- result.durationMs = Date.now() - start;
90
- return result;
91
- }
92
- const root = options?.projectsDir ?? DEFAULT_PROJECTS_DIR;
93
- if (!existsSync(root)) {
94
- result.durationMs = Date.now() - start;
95
- return result;
96
- }
97
- let stat;
98
- try {
99
- stat = statSync(root);
100
- }
101
- catch {
102
- result.durationMs = Date.now() - start;
103
- return result;
104
- }
105
- if (!stat.isDirectory()) {
106
- result.durationMs = Date.now() - start;
107
- return result;
108
- }
109
- let slugs;
110
- try {
111
- slugs = readdirSync(root);
112
- }
113
- catch {
114
- result.durationMs = Date.now() - start;
115
- return result;
116
- }
117
- const cap = options?.maxProjects ?? Number.MAX_SAFE_INTEGER;
118
- let backfilledCount = 0;
119
- for (const slug of slugs) {
120
- if (backfilledCount >= cap)
121
- break;
122
- const projectDir = join(root, slug);
123
- let s;
124
- try {
125
- s = statSync(projectDir);
126
- }
127
- catch {
128
- continue;
129
- }
130
- if (!s.isDirectory())
131
- continue;
132
- const file = join(projectDir, 'thread.jsonl');
133
- if (!existsSync(file))
134
- continue;
135
- result.scanned++;
136
- const scanned = scanThread(slug, file);
137
- if (scanned.title) {
138
- result.alreadyTitled++;
139
- continue;
140
- }
141
- if (!scanned.firstPrompt || !scanned.firstCwd) {
142
- // Nothing to title from — skip. (Migration-seeded headers without task
143
- // entries fall here, as do projects where the first task lost its prompt
144
- // somehow.)
145
- result.noTasks++;
146
- continue;
147
- }
148
- let title;
149
- try {
150
- title = await generateThreadTitle(scanned.firstPrompt, scanned.firstCwd);
151
- }
152
- catch {
153
- // Title agent should never throw (it returns null on failure), but guard
154
- // anyway so one bad call doesn't sink the whole backfill.
155
- title = null;
156
- }
157
- if (!title) {
158
- result.titleAgentFailed++;
159
- continue;
160
- }
161
- // Race guard: backfill scanned BEFORE the LLM call, but the LLM blocks
162
- // for seconds. A user rename in that window leaves a userOverride:true
163
- // marker the pre-scan never saw. Re-read the same file just before the
164
- // write and skip if the user got there first. (Inlined rather than calling
165
- // hasManualOverride() because that helper hardcodes the production
166
- // projects dir; backfill supports a custom projectsDir for tests.)
167
- let userBeatUs = false;
168
- try {
169
- const recheck = readFileSync(file, 'utf-8');
170
- for (const line of recheck.split('\n')) {
171
- const t = line.trim();
172
- if (!t)
173
- continue;
174
- try {
175
- const rec = JSON.parse(t);
176
- if (rec._meta === 'project-update' && rec.userOverride === true) {
177
- userBeatUs = true;
178
- break;
179
- }
180
- }
181
- catch {
182
- /* skip malformed */
183
- }
184
- }
185
- }
186
- catch {
187
- /* if we can't read, fall through and let the append fail */
188
- }
189
- if (userBeatUs) {
190
- result.alreadyTitled++;
191
- continue;
192
- }
193
- try {
194
- const updateLine = JSON.stringify({
195
- _meta: 'project-update',
196
- slug,
197
- title,
198
- cwd: scanned.firstCwd,
199
- backfilledAt: Date.now(),
200
- updatedAt: Date.now(),
201
- });
202
- appendFileSync(file, updateLine + '\n', 'utf-8');
203
- result.titlesGenerated++;
204
- backfilledCount++;
205
- }
206
- catch {
207
- // Append failure is logged via counter but doesn't sink the loop.
208
- result.titleAgentFailed++;
209
- }
210
- }
211
- result.durationMs = Date.now() - start;
212
- return result;
213
- }
214
- //# sourceMappingURL=project-thread-title-backfill.js.map