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,671 +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
- import { execFileSync } from 'child_process';
35
- import { normalizeRemote } from './project-thread-reader.js';
36
- const PROJECTS_DIR = join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'projects');
37
- /** Match migration module's defaults. Same regex set kept in sync. */
38
- const TEMP_DIR_PATTERNS = [/^\/tmp\//, /^\/var\/folders\//, /^\/private\/var\/folders\//, /^\/private\/tmp\//];
39
- function isTempDir(cwd) {
40
- if (!cwd)
41
- return false;
42
- return TEMP_DIR_PATTERNS.some((re) => re.test(cwd));
43
- }
44
- function slugForCwd(cwd) {
45
- if (!cwd)
46
- return 'unknown';
47
- return createHash('sha1').update(cwd).digest('hex').slice(0, 12);
48
- }
49
- /**
50
- * Detect the git remote for a cwd → normalized `host/org/repo` (project
51
- * identity). Best-effort + fast: a short-timeout `git remote get-url origin`,
52
- * silent on not-a-repo / no-remote / git-missing. Called at most once per
53
- * project (write-time seed + the daemon-start backfill), never on a read poll.
54
- */
55
- function detectRemote(cwd) {
56
- if (!cwd)
57
- return null;
58
- try {
59
- const out = execFileSync('git', ['-C', cwd, 'remote', 'get-url', 'origin'], {
60
- encoding: 'utf-8',
61
- timeout: 2000,
62
- stdio: ['ignore', 'pipe', 'ignore'],
63
- });
64
- return normalizeRemote(out.trim());
65
- }
66
- catch {
67
- return null;
68
- }
69
- }
70
- function ensureProjectDir(slug) {
71
- const dir = join(PROJECTS_DIR, slug);
72
- if (!existsSync(dir))
73
- mkdirSync(dir, { recursive: true });
74
- return dir;
75
- }
76
- function appendThreadEntry(slug, entry) {
77
- // Returns true if this write SEEDED the file (first time creation), false otherwise.
78
- // Caller (mirrorSpawnedToThread) uses this signal to fire the hidden Title agent.
79
- try {
80
- const dir = ensureProjectDir(slug);
81
- const file = join(dir, 'thread.jsonl');
82
- let seeded = false;
83
- if (!existsSync(file)) {
84
- const meta = JSON.stringify({
85
- _meta: 'project',
86
- slug,
87
- cwd: null, // populated by migration; forward-mirror writes the cwd in each task entry
88
- sessionCount: 0,
89
- title: null, // populated by hidden Title agent (v0.6.0 M9) on first task entry
90
- migratedFrom: 'forward-mirror',
91
- migratedAt: Date.now(),
92
- });
93
- appendFileSync(file, meta + '\n', 'utf-8');
94
- seeded = true;
95
- }
96
- appendFileSync(file, JSON.stringify(entry) + '\n', 'utf-8');
97
- return seeded;
98
- }
99
- catch {
100
- // Best-effort. Mirror failures must never break the spawn or completion
101
- // path — sessions.jsonl is the canonical store; thread.jsonl is derived.
102
- return false;
103
- }
104
- }
105
- /** Append a project-meta update line (used by Title agent to set the title).
106
- * Returns true on successful write, false on any fs failure. Title-agent
107
- * callers ignore the return (best-effort); renameProject surfaces it as a
108
- * 500 to the HTTP layer so users don't see "renamed" when nothing landed. */
109
- function appendMetaUpdate(slug, partial) {
110
- try {
111
- const dir = ensureProjectDir(slug);
112
- const file = join(dir, 'thread.jsonl');
113
- if (!existsSync(file))
114
- return false; // can't update what doesn't exist
115
- appendFileSync(file, JSON.stringify({ _meta: 'project-update', slug, ...partial, updatedAt: Date.now() }) + '\n', 'utf-8');
116
- return true;
117
- }
118
- catch {
119
- return false;
120
- }
121
- }
122
- /**
123
- * Returns true if any prior project-update line for this slug carries
124
- * userOverride:true. Used by the async Title agent (and the periodic backfill)
125
- * to skip overwriting a title the user has manually set.
126
- *
127
- * The race this guards: spawn fires Title agent in background → user renames
128
- * before LLM returns → Title agent's later write would clobber the manual
129
- * rename because the reader takes the latest title. With this check, the
130
- * Title agent reads its own outcome moments before writing and bails if the
131
- * user got there first. Best-effort; on read failure returns false (Title
132
- * agent proceeds) — same posture as every other appendThreadEntry caller.
133
- */
134
- /**
135
- * Derive a clean Title-Cased name from a working directory path. Used as the
136
- * immediate seed title (so the sidebar never shows blank / placeholder during
137
- * the LLM call) and as the permanent fallback when the Title agent returns
138
- * null / garbage. Examples:
139
- * /Users/x/code/codehere → "Codehere"
140
- * /Users/x/work/auth_service → "Auth Service"
141
- * /Users/x/code/some-cool-thing → "Some Cool Thing"
142
- * / → "Project"
143
- */
144
- export function titleFromCwd(cwd) {
145
- if (!cwd)
146
- return 'Project';
147
- const segments = cwd.split('/').filter(Boolean);
148
- const last = segments[segments.length - 1];
149
- if (!last)
150
- return 'Project';
151
- // Replace separators with spaces, title-case each word, cap at 6 words
152
- const words = last
153
- .replace(/[._-]+/g, ' ')
154
- .replace(/([a-z])([A-Z])/g, '$1 $2') // camelCase split
155
- .split(/\s+/)
156
- .filter(Boolean)
157
- .slice(0, 6)
158
- .map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase());
159
- return words.join(' ') || 'Project';
160
- }
161
- /**
162
- * One-shot backfill for legacy garbage project titles (Bug A4 retroactive).
163
- *
164
- * The original A4 fix (rejected garbage titles + cwd fallback) only applied
165
- * to NEW projects. Existing thread.jsonl files written before the fix can
166
- * still carry titles like "Request ID: req_abc123…" or "I cannot generate…"
167
- * sitting in the latest project-update record. Those show up in the sidebar
168
- * forever until the user manually renames.
169
- *
170
- * Walks every ~/.codehere/projects/<slug>/thread.jsonl, reads the latest
171
- * project-update title, runs the same garbage detection used at write time.
172
- * If garbage, appends a new project-update with the cwd-derived fallback
173
- * title plus `_backfilled: true` marker so re-runs are no-ops.
174
- *
175
- * Manual overrides (set via Pattern 10 inline rename) are NEVER touched —
176
- * hasManualOverride guards the rewrite.
177
- *
178
- * Best-effort: any per-project failure logs to stderr (when CODEHERE_DEBUG=1)
179
- * and continues. Returns the count of titles rewritten.
180
- */
181
- export async function backfillProjectTitles() {
182
- let scanned = 0;
183
- let rewritten = 0;
184
- if (!existsSync(PROJECTS_DIR))
185
- return { scanned, rewritten };
186
- // Lazy-load sanitizeTitle to avoid a static import cycle (use-case → infra).
187
- const { sanitizeTitle } = await import('../../application/use-cases/generate-thread-title.js');
188
- let entries;
189
- try {
190
- const { readdirSync } = await import('fs');
191
- entries = readdirSync(PROJECTS_DIR);
192
- }
193
- catch {
194
- return { scanned, rewritten };
195
- }
196
- for (const slug of entries) {
197
- // Skip non-slug-shaped names + hidden files
198
- if (!/^[a-f0-9]{12}$|^unknown$/.test(slug))
199
- continue;
200
- scanned++;
201
- try {
202
- const file = join(PROJECTS_DIR, slug, 'thread.jsonl');
203
- if (!existsSync(file))
204
- continue;
205
- if (hasManualOverride(slug))
206
- continue; // Never overwrite user renames
207
- const content = readFileSync(file, 'utf-8');
208
- const lines = content.split('\n').filter((l) => l.trim());
209
- // Find the latest project-update + the most recent task cwd (for fallback)
210
- let latestTitle = null;
211
- let latestCwd = null;
212
- let alreadyBackfilled = false;
213
- for (const line of lines) {
214
- try {
215
- const rec = JSON.parse(line);
216
- if (rec._meta === 'project-update' && typeof rec.title === 'string') {
217
- latestTitle = rec.title;
218
- if (rec._backfilled === true)
219
- alreadyBackfilled = true;
220
- else
221
- alreadyBackfilled = false;
222
- }
223
- if (rec.type === 'task' && typeof rec.cwd === 'string' && rec.cwd) {
224
- latestCwd = rec.cwd;
225
- }
226
- }
227
- catch {
228
- /* skip malformed line */
229
- }
230
- }
231
- if (alreadyBackfilled)
232
- continue; // idempotent — already cleaned
233
- if (!latestTitle)
234
- continue; // nothing to check
235
- // Run the same garbage detection sanitizeTitle uses at write time.
236
- // sanitizeTitle returns '' for garbage; non-empty means clean.
237
- const cleaned = sanitizeTitle(latestTitle);
238
- if (cleaned)
239
- continue; // title is fine
240
- // Garbage — write a new project-update with cwd-derived fallback.
241
- const fallback = titleFromCwd(latestCwd);
242
- const ok = appendMetaUpdate(slug, {
243
- title: fallback,
244
- cwd: latestCwd,
245
- _backfilled: true,
246
- });
247
- if (ok) {
248
- rewritten++;
249
- if (process.env.CODEHERE_DEBUG === '1') {
250
- console.error(`[backfill] ${slug}: "${latestTitle.slice(0, 40)}" › "${fallback}"`);
251
- }
252
- }
253
- }
254
- catch {
255
- /* per-project failure → continue */
256
- }
257
- }
258
- return { scanned, rewritten };
259
- }
260
- /**
261
- * One-time backfill of the git remote into _meta for projects that predate
262
- * write-time detection (or whose seed-time detection found none). Idempotent:
263
- * any project carrying a non-null remote OR a `_remoteChecked` marker is
264
- * skipped, so this runs the git probe at most once per project across the
265
- * daemon's lifetime. Best-effort; per-project failures continue.
266
- *
267
- * Wired at daemon start alongside backfillProjectTitles. Returns the count of
268
- * projects for which a real remote was detected.
269
- */
270
- export async function backfillProjectRemotes() {
271
- let scanned = 0;
272
- let detected = 0;
273
- if (!existsSync(PROJECTS_DIR))
274
- return { scanned, detected };
275
- let entries;
276
- try {
277
- const { readdirSync } = await import('fs');
278
- entries = readdirSync(PROJECTS_DIR);
279
- }
280
- catch {
281
- return { scanned, detected };
282
- }
283
- for (const slug of entries) {
284
- if (!/^[a-f0-9]{12}$|^unknown$/.test(slug))
285
- continue;
286
- scanned++;
287
- try {
288
- const file = join(PROJECTS_DIR, slug, 'thread.jsonl');
289
- if (!existsSync(file))
290
- continue;
291
- const content = readFileSync(file, 'utf-8');
292
- const lines = content.split('\n').filter((l) => l.trim());
293
- let alreadyChecked = false;
294
- let haveRemote = false;
295
- let latestCwd = null;
296
- for (const line of lines) {
297
- try {
298
- const rec = JSON.parse(line);
299
- if (rec._meta === 'project' || rec._meta === 'project-update') {
300
- if (rec._remoteChecked === true)
301
- alreadyChecked = true;
302
- if (typeof rec.remote === 'string' && rec.remote)
303
- haveRemote = true;
304
- if (typeof rec.cwd === 'string' && rec.cwd && !latestCwd)
305
- latestCwd = rec.cwd;
306
- }
307
- if (rec.type === 'task' && typeof rec.cwd === 'string' && rec.cwd)
308
- latestCwd = rec.cwd;
309
- }
310
- catch {
311
- /* skip malformed line */
312
- }
313
- }
314
- if (alreadyChecked || haveRemote)
315
- continue; // idempotent — already settled
316
- const remote = detectRemote(latestCwd);
317
- // Stamp _remoteChecked even on a null result so we never re-probe a
318
- // remote-less project. The reader ignores a null remote (keeps it null).
319
- const ok = appendMetaUpdate(slug, { remote: remote ?? null, _remoteChecked: true });
320
- if (ok && remote) {
321
- detected++;
322
- if (process.env.CODEHERE_DEBUG === '1') {
323
- console.error(`[backfill-remote] ${slug}: ${remote}`);
324
- }
325
- }
326
- }
327
- catch {
328
- /* per-project failure → continue */
329
- }
330
- }
331
- return { scanned, detected };
332
- }
333
- /**
334
- * One-shot reconciliation for stale-running tasks (Bug: project-thread.jsonl
335
- * entries can stay in `status: 'running'` forever when the daemon dies
336
- * mid-task or the subprocess crashes without a clean completion event).
337
- *
338
- * The visible failure mode (per dogfood screenshots May 6): the threads
339
- * view shows RUNNING pills on 3-day-old tasks. With Pattern 8's peach-
340
- * accent running pill, the page reads as "codehere doesn't know what
341
- * state its own sessions are in" — a credibility bomb.
342
- *
343
- * Walks every ~/.codehere/projects/<slug>/thread.jsonl, reconstructs each
344
- * session's latest task state by sessionId, and for any session whose
345
- * latest state is `running` AND startedAt > 60 minutes ago, appends a
346
- * synthetic completion record marking it `crashed` with `_reconciled: true`.
347
- *
348
- * Idempotent — sessions already carrying `_reconciled: true` are skipped.
349
- * Preserves history (append-only, never rewrites prior lines).
350
- *
351
- * Best-effort: per-project failures log to stderr (CODEHERE_DEBUG=1) and
352
- * continue. Returns count of sessions reconciled.
353
- */
354
- export async function reconcileStaleProjectTasks(options) {
355
- const STALE_AFTER_MS = options?.staleAfterMs ?? 60 * 60 * 1000; // 60 minutes
356
- const now = Date.now();
357
- let scanned = 0;
358
- let reconciled = 0;
359
- if (!existsSync(PROJECTS_DIR))
360
- return { scanned, reconciled };
361
- let entries;
362
- try {
363
- const { readdirSync } = await import('fs');
364
- entries = readdirSync(PROJECTS_DIR);
365
- }
366
- catch {
367
- return { scanned, reconciled };
368
- }
369
- for (const slug of entries) {
370
- if (!/^[a-f0-9]{12}$|^unknown$/.test(slug))
371
- continue;
372
- scanned++;
373
- try {
374
- const file = join(PROJECTS_DIR, slug, 'thread.jsonl');
375
- if (!existsSync(file))
376
- continue;
377
- const content = readFileSync(file, 'utf-8');
378
- const lines = content.split('\n').filter((l) => l.trim());
379
- const bySession = new Map();
380
- for (const line of lines) {
381
- try {
382
- const rec = JSON.parse(line);
383
- if (rec.type !== 'task' || typeof rec.sessionId !== 'string')
384
- continue;
385
- const cur = bySession.get(rec.sessionId) ?? {
386
- sessionId: rec.sessionId,
387
- status: '',
388
- startedAt: 0,
389
- reconciled: false,
390
- };
391
- if (typeof rec.status === 'string')
392
- cur.status = rec.status;
393
- if (typeof rec.startedAt === 'number' && rec.startedAt > 0)
394
- cur.startedAt = rec.startedAt;
395
- if (rec._reconciled === true)
396
- cur.reconciled = true;
397
- bySession.set(rec.sessionId, cur);
398
- }
399
- catch {
400
- /* skip malformed line */
401
- }
402
- }
403
- // Find stale-running candidates and append synthetic completions.
404
- for (const state of bySession.values()) {
405
- if (state.status !== 'running')
406
- continue;
407
- if (state.reconciled)
408
- continue; // already cleaned
409
- if (state.startedAt <= 0)
410
- continue; // safety: no time data
411
- if (now - state.startedAt < STALE_AFTER_MS)
412
- continue;
413
- // appendThreadEntry returns `seeded` (first-write flag), not a
414
- // success signal. For an already-seeded file, the return is always
415
- // false on both success and failure. Best-effort approach: count
416
- // optimistically — write failures are rare and bounded by the
417
- // function's own try/catch.
418
- appendThreadEntry(slug, {
419
- type: 'task',
420
- sessionId: state.sessionId,
421
- status: 'crashed',
422
- completedAt: now,
423
- durationMs: now - state.startedAt,
424
- _reconciled: true,
425
- });
426
- reconciled++;
427
- if (process.env.CODEHERE_DEBUG === '1') {
428
- const ageMin = Math.round((now - state.startedAt) / 60000);
429
- console.error(`[reconcile] ${slug}/${state.sessionId.slice(0, 8)} stale running for ${ageMin}m › crashed`);
430
- }
431
- }
432
- }
433
- catch {
434
- /* per-project failure → continue */
435
- }
436
- }
437
- return { scanned, reconciled };
438
- }
439
- export function hasManualOverride(slug) {
440
- try {
441
- const file = join(PROJECTS_DIR, slug, 'thread.jsonl');
442
- if (!existsSync(file))
443
- return false;
444
- const content = readFileSync(file, 'utf-8');
445
- for (const line of content.split('\n')) {
446
- const t = line.trim();
447
- if (!t)
448
- continue;
449
- try {
450
- const rec = JSON.parse(t);
451
- if (rec._meta === 'project-update' && rec.userOverride === true)
452
- return true;
453
- }
454
- catch {
455
- // skip malformed line
456
- }
457
- }
458
- return false;
459
- }
460
- catch {
461
- return false;
462
- }
463
- }
464
- /**
465
- * v0.6.x — user-initiated rename. The Title agent generates a title from the
466
- * first prompt, but project intent drifts (e.g. /agent dir gets titled "Scan
467
- * Auth File" forever even after 100+ unrelated tasks). This gives the user a
468
- * mechanism to override that title without re-firing the LLM-based agent.
469
- *
470
- * Returns { ok: false, error } on validation/fs failure so the HTTP layer can
471
- * surface a meaningful 4xx instead of a silent best-effort write. Note: this
472
- * is the only project-update path that returns errors — Title agent
473
- * appendMetaUpdate is silent because LLM hiccups are normal.
474
- */
475
- export function renameProject(slug, newTitle) {
476
- // Validate slug shape — guards against path traversal in case the caller
477
- // doesn't sanitize the URL parameter (HTTP layer validates this too, defense in depth).
478
- if (!/^([a-f0-9]{12}|unknown)$/.test(slug)) {
479
- return { ok: false, error: 'Invalid project slug' };
480
- }
481
- // Validate title — strip whitespace, enforce length and single-line.
482
- const trimmed = newTitle.trim();
483
- if (!trimmed)
484
- return { ok: false, error: 'Title cannot be empty' };
485
- if (trimmed.length > 100)
486
- return { ok: false, error: 'Title too long (max 100 chars)' };
487
- if (/[\n\r]/.test(trimmed))
488
- return { ok: false, error: 'Title cannot contain newlines' };
489
- // Verify project exists — appendMetaUpdate is silent on missing files but
490
- // we want a clean 404 path for the HTTP caller.
491
- const file = join(PROJECTS_DIR, slug, 'thread.jsonl');
492
- if (!existsSync(file))
493
- return { ok: false, error: 'Project not found' };
494
- // userOverride:true tells the async Title agent + periodic backfill to skip
495
- // writing for this slug — see hasManualOverride() above. Without this flag,
496
- // a slow LLM call from spawn-time can land AFTER the user's rename and
497
- // clobber it (the reader takes the latest project-update title).
498
- if (!appendMetaUpdate(slug, { title: trimmed, userOverride: true })) {
499
- return { ok: false, error: 'Write failed' };
500
- }
501
- return { ok: true };
502
- }
503
- /**
504
- * Archive (soft-delete) or restore a task. Append-only and audit-safe: writes an
505
- * `{_meta:'archived', sessionId, archived}` event to the thread (latest wins).
506
- * The task record is NEVER deleted — the proof stays; the room just hides it from
507
- * the active list. Restore = the same event with archived:false.
508
- */
509
- export function setTaskArchived(slug, sessionId, archived) {
510
- if (!/^([a-f0-9]{12}|unknown)$/.test(slug))
511
- return { ok: false, error: 'Invalid project slug' };
512
- if (!sessionId || typeof sessionId !== 'string' || sessionId.length > 200 || /[\n\r]/.test(sessionId)) {
513
- return { ok: false, error: 'Invalid sessionId' };
514
- }
515
- const file = join(PROJECTS_DIR, slug, 'thread.jsonl');
516
- if (!existsSync(file))
517
- return { ok: false, error: 'Project not found' };
518
- try {
519
- appendFileSync(file, JSON.stringify({ _meta: 'archived', slug, sessionId, archived: !!archived, updatedAt: Date.now() }) + '\n', 'utf-8');
520
- return { ok: true };
521
- }
522
- catch {
523
- return { ok: false, error: 'Write failed' };
524
- }
525
- }
526
- /**
527
- * Mirror a spawn event to the project thread. Skips temp-dir cwds (matches
528
- * migration defaults) so test-artifact dirs don't pollute the project list.
529
- *
530
- * If this spawn is the FIRST task in a brand-new project (i.e., thread.jsonl
531
- * just got seeded), fire the hidden Title agent (v0.6.0 M9) in the background
532
- * to generate a 4-6 word title from the first user prompt. The title appears
533
- * in the project-meta as a 'project-update' record; readers take the latest
534
- * value. Title generation is best-effort and silent on failure.
535
- */
536
- export function mirrorSpawnedToThread(rec) {
537
- if (isTempDir(rec.cwd))
538
- return;
539
- const slug = slugForCwd(rec.cwd);
540
- const seeded = appendThreadEntry(slug, {
541
- type: 'task',
542
- sessionId: rec.sessionId,
543
- agent: rec.agent,
544
- prompt: rec.prompt,
545
- cwd: rec.cwd,
546
- startedAt: rec.startedAt,
547
- status: 'running',
548
- ...(rec.batchId ? { batchId: rec.batchId } : {}),
549
- ...(rec.parentSessionId ? { parentSessionId: rec.parentSessionId } : {}),
550
- });
551
- if (seeded) {
552
- // Seed an immediate cwd-derived title so the sidebar never shows a blank
553
- // / placeholder while the LLM call is in flight (or if it fails entirely).
554
- // The Title agent can later overwrite this with a richer prompt-derived
555
- // title; user manual rename always wins over both.
556
- if (!hasManualOverride(slug)) {
557
- const fallback = titleFromCwd(rec.cwd);
558
- appendMetaUpdate(slug, { title: fallback, cwd: rec.cwd });
559
- }
560
- // Detect + cache the git remote ONCE at project creation. Project identity
561
- // (and clone-collapse in listProjects) reads this from _meta — no git
562
- // shell-out on the hot read path. Only written when a remote is found;
563
- // remote-less / non-repo projects are settled by the daemon-start backfill.
564
- const remote = detectRemote(rec.cwd);
565
- if (remote)
566
- appendMetaUpdate(slug, { remote, _remoteChecked: true });
567
- if (process.env.CODEHERE_DISABLE_TITLE_AGENT !== '1') {
568
- // Fire Title agent in background; do not block the spawn path. Lazy-import
569
- // to avoid loading the LLM module on every spawn when not needed.
570
- void (async () => {
571
- try {
572
- const { generateThreadTitle } = await import('../../application/use-cases/generate-thread-title.js');
573
- const title = await generateThreadTitle(rec.prompt, rec.cwd);
574
- // Last-moment manual-override check: between spawn and now (LLM call
575
- // can take seconds), user may have already renamed. Don't clobber.
576
- // Also: if Title agent returned null (garbage rejected, provider
577
- // unreachable, refusal), keep the cwd-derived fallback already seeded
578
- // above — never write `(untitled)` or empty.
579
- if (title && !hasManualOverride(slug)) {
580
- appendMetaUpdate(slug, { title, cwd: rec.cwd });
581
- }
582
- }
583
- catch {
584
- // best-effort — cwd fallback already seeded
585
- }
586
- })();
587
- }
588
- }
589
- }
590
- /**
591
- * Mirror a completion event to the project thread. Same skip rules as spawn.
592
- * Reader reconstructs by sessionId — this entry's fields override the spawn
593
- * entry's where they overlap (status flips from 'running' to actual final).
594
- */
595
- export function mirrorCompletedToThread(rec) {
596
- if (isTempDir(rec.cwd))
597
- return;
598
- const slug = slugForCwd(rec.cwd);
599
- appendThreadEntry(slug, {
600
- type: 'task',
601
- sessionId: rec.sessionId,
602
- status: rec.status,
603
- durationMs: rec.durationMs,
604
- completedAt: rec.completedAt,
605
- steps: rec.steps,
606
- artifacts: rec.artifacts,
607
- });
608
- }
609
- /**
610
- * Persist a generated Final Report summary to the project thread. The report is
611
- * produced ASYNC after the session exits, so mirrorCompletedToThread already ran
612
- * without it — this second append fills finalReport in. The reader merges task
613
- * records by sessionId field-by-field, so this partial entry layers onto the
614
- * existing completion record. Without it the summary lives only in the in-memory
615
- * cache + sessions.jsonl: the conversation view loses it on a daemon restart, and
616
- * a cross-agent hand-off reads "(no summary recorded)" instead of the real work.
617
- */
618
- export function mirrorFinalReportToThread(rec) {
619
- if (isTempDir(rec.cwd))
620
- return;
621
- const slug = slugForCwd(rec.cwd);
622
- appendThreadEntry(slug, {
623
- type: 'task',
624
- sessionId: rec.sessionId,
625
- finalReport: { summary: rec.summary, ...(rec.source ? { source: rec.source } : {}) },
626
- });
627
- }
628
- /** Label map for raw error codes → user-facing text. */
629
- const FAILOVER_LABELS = {
630
- rate_limit: 'hit a rate limit',
631
- quota_exceeded: 'exhausted its quota',
632
- credit_low: 'ran low on credits',
633
- error: 'encountered an error',
634
- };
635
- export function mirrorFailoverToThread(rec) {
636
- if (isTempDir(rec.cwd))
637
- return;
638
- const slug = slugForCwd(rec.cwd);
639
- const reasonLabel = FAILOVER_LABELS[rec.reason] ?? 'became unavailable';
640
- const fromLabel = rec.fromAgent;
641
- const toLabel = rec.toAgent;
642
- appendThreadEntry(slug, {
643
- type: 'task',
644
- sessionId: rec.sessionId,
645
- agent: 'system',
646
- prompt: `Switched from ${fromLabel} to ${toLabel} (${reasonLabel}) — warm restart with context, not a state transfer`,
647
- status: 'completed',
648
- startedAt: Date.now(),
649
- completedAt: Date.now(),
650
- durationMs: 0,
651
- });
652
- }
653
- export function mirrorReplyToThread(rec) {
654
- if (isTempDir(rec.cwd))
655
- return;
656
- const slug = slugForCwd(rec.cwd);
657
- appendThreadEntry(slug, {
658
- type: 'task',
659
- sessionId: rec.replySessionId,
660
- parentSessionId: rec.parentSessionId,
661
- agent: rec.agent,
662
- prompt: rec.prompt,
663
- cwd: rec.cwd,
664
- startedAt: rec.startedAt,
665
- completedAt: rec.completedAt,
666
- durationMs: rec.durationMs,
667
- status: 'completed',
668
- ...(rec.result ? { result: rec.result } : {}),
669
- });
670
- }
671
- //# sourceMappingURL=project-thread-mirror.js.map