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,268 +0,0 @@
1
- /**
2
- * Cross-tool agent definition reader — v0.6.0 M8.
3
- *
4
- * Reads agent definitions written for OTHER tools (OpenCode, Claude Code) and
5
- * normalizes them into codehere's internal shape so they appear in the agents
6
- * pane alongside codehere's built-in adapters.
7
- *
8
- * Built from Spike-4 prototype (agent/spike/v0.6.0-cross-tool-parser.mjs);
9
- * production version uses the `yaml` lib for proper 2-level nesting + multi-
10
- * line scalars + quoted-string handling, recurses subdirectories per the
11
- * AGENTS.md hierarchical convention, skips files without frontmatter, and
12
- * surfaces unknown frontmatter keys as warnings (not silent drops).
13
- *
14
- * Sources read in precedence order (later overrides earlier on name conflict):
15
- * 1. ~/.claude/agents/ (global Claude Code subagents)
16
- * 2. ~/.opencode/agents/ (global OpenCode agents)
17
- * 3. ~/.codehere/agents/ (global codehere agents)
18
- * 4. <cwd>/.claude/agents/ (project Claude Code)
19
- * 5. <cwd>/.opencode/agents/ (project OpenCode)
20
- * 6. <cwd>/.codehere/agents/ (project codehere)
21
- *
22
- * Format normalization handles divergences:
23
- * - Claude `name` field vs OpenCode (filename = name)
24
- * - Claude `tools: "A, B, C"` (string) vs OpenCode `tools: {a: true, b: false}` (object)
25
- * - Claude no `mode` (subagent implicit) vs OpenCode `mode: primary|subagent|all`
26
- * - OpenCode `permission` block vs Claude (no permission block)
27
- *
28
- * NOT in scope of this module:
29
- * - Permission ENFORCEMENT (this just READS them for display + future M5)
30
- * - Template variable substitution ({{PROJECT_NAME}} stays as raw text)
31
- * - Authoring (`codehere agent create` is C6, deferred)
32
- */
33
- import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
34
- import { join } from 'path';
35
- import { homedir } from 'os';
36
- import YAML from 'yaml';
37
- /**
38
- * Pre-process Claude Code-style frontmatter that contains unquoted colons in
39
- * description/prompt values (e.g. "description: ... Examples:\n<example>\nContext: ...").
40
- * Strict YAML parsers reject these; Claude Code itself uses a more permissive
41
- * reader. We force-quote affected values before parsing.
42
- *
43
- * Heuristic: any top-level `description:` or `prompt:` line whose value
44
- * contains a `: ` (colon-space) AND isn't already quoted/block-scalar gets
45
- * single-quoted with internal single quotes doubled (YAML escape).
46
- */
47
- function preprocessFrontmatter(fmText) {
48
- return fmText
49
- .split('\n')
50
- .map((line) => {
51
- const m = line.match(/^(description|prompt):\s*(.+)$/);
52
- if (!m)
53
- return line;
54
- const [, key, value] = m;
55
- const trimmed = value.trim();
56
- if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
57
- (trimmed.startsWith("'") && trimmed.endsWith("'")) ||
58
- trimmed.startsWith('|') ||
59
- trimmed.startsWith('>')) {
60
- return line; // already a safe scalar form
61
- }
62
- if (!/:\s/.test(trimmed))
63
- return line; // no problematic embedded colons
64
- const escaped = trimmed.replace(/'/g, "''");
65
- return `${key}: '${escaped}'`;
66
- })
67
- .join('\n');
68
- }
69
- /** Recursively walk a dir, return all .md files (sorted for determinism). */
70
- function listMdFilesRecursive(dir) {
71
- if (!existsSync(dir))
72
- return [];
73
- let stat;
74
- try {
75
- stat = statSync(dir);
76
- }
77
- catch {
78
- return [];
79
- }
80
- if (!stat.isDirectory())
81
- return [];
82
- const out = [];
83
- let entries;
84
- try {
85
- entries = readdirSync(dir);
86
- }
87
- catch {
88
- return [];
89
- }
90
- for (const entry of entries.sort()) {
91
- if (entry.startsWith('.'))
92
- continue;
93
- const p = join(dir, entry);
94
- let s;
95
- try {
96
- s = statSync(p);
97
- }
98
- catch {
99
- continue;
100
- }
101
- if (s.isDirectory()) {
102
- out.push(...listMdFilesRecursive(p));
103
- }
104
- else if (s.isFile() && entry.endsWith('.md') && entry !== 'README.md') {
105
- out.push(p);
106
- }
107
- }
108
- return out;
109
- }
110
- /** Parse a single agent file. Returns null if no frontmatter or parse fails. */
111
- export function parseAgentFile(filePath, defaultFormat = 'unknown') {
112
- let text;
113
- try {
114
- text = readFileSync(filePath, 'utf-8');
115
- }
116
- catch {
117
- return null;
118
- }
119
- if (!text.startsWith('---'))
120
- return null; // no frontmatter, not an agent file
121
- const closeIdx = text.indexOf('\n---', 4);
122
- if (closeIdx === -1)
123
- return null;
124
- const fmText = text.slice(4, closeIdx).trim();
125
- const body = text.slice(closeIdx + 4).trim();
126
- let fm;
127
- try {
128
- fm = YAML.parse(preprocessFrontmatter(fmText));
129
- if (!fm || typeof fm !== 'object')
130
- return null;
131
- }
132
- catch {
133
- return null;
134
- }
135
- // Source format inference: presence of mode/permission/object-tools = OpenCode;
136
- // presence of name + string-tools = Claude. Fall back to default by location.
137
- const hasObjectTools = typeof fm.tools === 'object' && fm.tools !== null && !Array.isArray(fm.tools);
138
- const sourceFormat = fm.mode || fm.permission || hasObjectTools
139
- ? 'opencode'
140
- : fm.name || typeof fm.tools === 'string'
141
- ? 'claude'
142
- : defaultFormat;
143
- // Tools normalization
144
- let tools = null;
145
- if (typeof fm.tools === 'string') {
146
- tools = fm.tools
147
- .split(',')
148
- .map((t) => t.trim().toLowerCase())
149
- .filter(Boolean);
150
- }
151
- else if (hasObjectTools) {
152
- tools = [];
153
- for (const [k, v] of Object.entries(fm.tools)) {
154
- if (v === true)
155
- tools.push(k.toLowerCase());
156
- }
157
- }
158
- // Filename without .md = fallback name
159
- const filenameName = filePath.split('/').pop()?.replace(/\.md$/, '') ?? 'unnamed';
160
- // Mode normalization
161
- const modeRaw = typeof fm.mode === 'string' ? fm.mode : null;
162
- const mode = modeRaw === 'primary' || modeRaw === 'subagent' || modeRaw === 'all'
163
- ? modeRaw
164
- : sourceFormat === 'claude'
165
- ? 'subagent'
166
- : 'all';
167
- // Track unknown frontmatter keys (everything outside our normalized set)
168
- const knownKeys = new Set([
169
- 'name',
170
- 'description',
171
- 'mode',
172
- 'model',
173
- 'temperature',
174
- 'tools',
175
- 'permission',
176
- 'color',
177
- 'hidden',
178
- 'top_p',
179
- 'steps',
180
- 'prompt',
181
- 'disable',
182
- ]);
183
- const unknownKeys = Object.keys(fm).filter((k) => !knownKeys.has(k));
184
- return {
185
- name: typeof fm.name === 'string' ? fm.name : filenameName,
186
- description: typeof fm.description === 'string' ? fm.description : '(no description)',
187
- sourceFormat,
188
- sourcePath: filePath,
189
- mode,
190
- model: typeof fm.model === 'string' ? fm.model : null,
191
- temperature: typeof fm.temperature === 'number' ? fm.temperature : null,
192
- tools,
193
- permission: typeof fm.permission === 'object' && fm.permission !== null ? fm.permission : null,
194
- color: typeof fm.color === 'string' ? fm.color : null,
195
- hidden: fm.hidden === true,
196
- body,
197
- unknownKeys,
198
- };
199
- }
200
- /** All locations to scan, in precedence order (later overrides earlier on name conflict).
201
- * Dedupes the dir list — when cwd == homeDir, project + global paths collapse and we
202
- * shouldn't double-count files. Test isolation also relies on this (homeDir override
203
- * to a project root). */
204
- function agentDirsToScan(cwd, homeOverride) {
205
- const home = homeOverride ?? homedir();
206
- const dirs = [
207
- { dir: join(home, '.claude/agents'), defaultFormat: 'claude' },
208
- { dir: join(home, '.opencode/agents'), defaultFormat: 'opencode' },
209
- { dir: join(home, '.codehere/agents'), defaultFormat: 'codehere' },
210
- ];
211
- if (cwd) {
212
- dirs.push({ dir: join(cwd, '.claude/agents'), defaultFormat: 'claude' });
213
- dirs.push({ dir: join(cwd, '.opencode/agents'), defaultFormat: 'opencode' });
214
- dirs.push({ dir: join(cwd, '.codehere/agents'), defaultFormat: 'codehere' });
215
- }
216
- // Dedupe by dir path (preserves order — first occurrence wins)
217
- const seen = new Set();
218
- return dirs.filter((d) => {
219
- if (seen.has(d.dir))
220
- return false;
221
- seen.add(d.dir);
222
- return true;
223
- });
224
- }
225
- /** Read all cross-tool agent definitions from global + project dirs. Idempotent + cheap (~10ms for ~20 files); safe to call on every readiness query.
226
- *
227
- * @param cwd Project working directory whose .claude/.opencode/.codehere agents subdirs to scan
228
- * @param options.homeDir Override home directory (testing only — production uses homedir())
229
- */
230
- export function readCrossToolAgents(cwd, options) {
231
- const byName = new Map();
232
- let filesParsed = 0;
233
- let filesSkippedNoFrontmatter = 0;
234
- let filesSkippedParseError = 0;
235
- let nameCollisions = 0;
236
- const dirs = agentDirsToScan(cwd, options?.homeDir);
237
- for (const { dir, defaultFormat } of dirs) {
238
- for (const filePath of listMdFilesRecursive(dir)) {
239
- let agent;
240
- try {
241
- agent = parseAgentFile(filePath, defaultFormat);
242
- }
243
- catch {
244
- filesSkippedParseError++;
245
- continue;
246
- }
247
- if (!agent) {
248
- filesSkippedNoFrontmatter++;
249
- continue;
250
- }
251
- filesParsed++;
252
- if (byName.has(agent.name))
253
- nameCollisions++;
254
- byName.set(agent.name, agent); // later wins
255
- }
256
- }
257
- return {
258
- agents: [...byName.values()].sort((a, b) => a.name.localeCompare(b.name)),
259
- diagnostics: {
260
- sourcesScanned: dirs.length,
261
- filesParsed,
262
- filesSkippedNoFrontmatter,
263
- filesSkippedParseError,
264
- nameCollisions,
265
- },
266
- };
267
- }
268
- //# sourceMappingURL=cross-tool-agents.js.map
@@ -1,37 +0,0 @@
1
- /**
2
- * Infrastructure: Embeddings Path Helper
3
- * Determines per-codebase embeddings database path
4
- */
5
- import { join } from 'path';
6
- import { existsSync, mkdirSync } from 'fs';
7
- import { findCodebaseRoot } from '../../domain/services/codebase-detector.js';
8
- /**
9
- * Get embeddings database path for a specific codebase
10
- * Uses codebase root detection to ensure per-repo isolation
11
- *
12
- * @param cwd - Current working directory (defaults to process.cwd())
13
- * @returns Object with root path, database path, and cache directory
14
- */
15
- export function getEmbeddingsDbPath(cwd = process.cwd()) {
16
- const codebaseInfo = findCodebaseRoot(cwd);
17
- if (!codebaseInfo) {
18
- // Fallback to cwd if codebase root can't be determined
19
- const root = cwd;
20
- const dbPath = join(root, '.codehere-cache', 'embeddings.sqlite');
21
- const cacheDir = join(root, '.codehere-cache');
22
- // Ensure cache directory exists
23
- if (!existsSync(cacheDir)) {
24
- mkdirSync(cacheDir, { recursive: true });
25
- }
26
- return { root, dbPath, cacheDir };
27
- }
28
- const root = codebaseInfo.rootPath;
29
- const dbPath = join(root, '.codehere-cache', 'embeddings.sqlite');
30
- const cacheDir = join(root, '.codehere-cache');
31
- // Ensure cache directory exists
32
- if (!existsSync(cacheDir)) {
33
- mkdirSync(cacheDir, { recursive: true });
34
- }
35
- return { root, dbPath, cacheDir };
36
- }
37
- //# sourceMappingURL=embeddings-path.js.map
@@ -1,237 +0,0 @@
1
- /**
2
- * Project thread migration — sessions.jsonl → projects/<slug>/thread.jsonl.
3
- *
4
- * v0.6.0 M1+M7 foundation. The new schema groups sessions by their cwd
5
- * (project) so the persistent project thread shape (one continuous chat per
6
- * project) has a place to live. The original sessions.jsonl is preserved
7
- * for one release as back-compat.
8
- *
9
- * Built from Spike-1 prototype (agent/spike/v0.6.0-migrate-sessions-to-threads.mjs).
10
- * Spike validated lossless + reversible on 612 real sessions in 5ms.
11
- *
12
- * Production additions over the spike:
13
- * - Atomic per-project writes (.tmp + rename) so partial failures don't
14
- * leave half-written thread.jsonl files
15
- * - Idempotent: skips writing if target thread.jsonl already exists
16
- * (re-run on daemon start is safe)
17
- * - Temp-dir filter: skips /tmp/, /var/folders/, /private/var/folders/ by
18
- * default (configurable) — these are test artifacts, not real projects
19
- * - Abandoned-session marker: sessions with no completed event get
20
- * status='abandoned' instead of silent null
21
- * - Configurable input/output paths so tests can use temp fixtures
22
- *
23
- * NOT in scope for this module (handled by callers):
24
- * - CLI interface (codehere migrate-threads — TBD subcommand)
25
- * - Auto-run on daemon start
26
- * - Forward-mirroring (writing to thread.jsonl alongside sessions.jsonl
27
- * during normal daemon operation)
28
- *
29
- * See docs/research/v0.6.0-spike-1-migration.md for the validation history.
30
- */
31
- import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync, readdirSync, renameSync } from 'fs';
32
- import { join } from 'path';
33
- import { createHash } from 'crypto';
34
- /** Path patterns considered ephemeral and skipped from project list. */
35
- const DEFAULT_TEMP_DIR_PATTERNS = [/^\/tmp\//, /^\/var\/folders\//, /^\/private\/var\/folders\//, /^\/private\/tmp\//];
36
- /** sha1(cwd)[:12] — stable, short, filesystem-safe. */
37
- function slugForCwd(cwd, unknownLabel) {
38
- if (!cwd)
39
- return unknownLabel;
40
- return createHash('sha1').update(cwd).digest('hex').slice(0, 12);
41
- }
42
- function isTempDir(cwd) {
43
- if (!cwd)
44
- return false;
45
- return DEFAULT_TEMP_DIR_PATTERNS.some((re) => re.test(cwd));
46
- }
47
- /** Read JSONL events; tolerate malformed lines (count + skip). */
48
- function readEvents(path) {
49
- if (!existsSync(path))
50
- return { events: [], malformed: 0 };
51
- const text = readFileSync(path, 'utf-8');
52
- const lines = text.split('\n').filter((l) => l.trim());
53
- const events = [];
54
- let malformed = 0;
55
- for (const line of lines) {
56
- try {
57
- events.push(JSON.parse(line));
58
- }
59
- catch {
60
- malformed++;
61
- }
62
- }
63
- return { events, malformed };
64
- }
65
- /** Roll up event-log entries into one record per session. */
66
- function rollupSessions(events) {
67
- const byId = new Map();
68
- const completedIds = new Set();
69
- for (const e of events) {
70
- const id = (e.id ?? e.sessionId);
71
- if (!id)
72
- continue;
73
- if (e.t === 'completed' || e.completedAt !== undefined)
74
- completedIds.add(id);
75
- const existing = byId.get(id) ?? { id, abandoned: true };
76
- const existingAny = existing;
77
- for (const k of [
78
- 'agent',
79
- 'prompt',
80
- 'cwd',
81
- 'startedAt',
82
- 'status',
83
- 'durationMs',
84
- 'completedAt',
85
- 'steps',
86
- 'artifacts',
87
- ]) {
88
- if (e[k] !== undefined && existingAny[k] === undefined) {
89
- existingAny[k] = e[k];
90
- }
91
- }
92
- byId.set(id, existing);
93
- }
94
- // Mark abandoned (sessions with no completed event)
95
- for (const s of byId.values()) {
96
- s.abandoned = !completedIds.has(s.id);
97
- if (s.abandoned && !s.status)
98
- s.status = 'abandoned';
99
- }
100
- return [...byId.values()];
101
- }
102
- function groupByCwd(sessions, unknownLabel) {
103
- const groups = new Map();
104
- for (const s of sessions) {
105
- const cwd = s.cwd ?? null;
106
- const slug = slugForCwd(cwd, unknownLabel);
107
- const group = groups.get(slug) ?? { slug, cwd, sessions: [] };
108
- group.sessions.push(s);
109
- groups.set(slug, group);
110
- }
111
- return [...groups.values()];
112
- }
113
- /** Atomic write: write to .tmp then rename. Caller ensures dir exists. */
114
- function atomicWriteFile(path, content) {
115
- const tmp = `${path}.tmp.${process.pid}.${Date.now()}`;
116
- writeFileSync(tmp, content);
117
- renameSync(tmp, path);
118
- }
119
- function buildThreadJsonl(group) {
120
- const lines = [];
121
- lines.push(JSON.stringify({
122
- _meta: 'project',
123
- slug: group.slug,
124
- cwd: group.cwd,
125
- sessionCount: group.sessions.length,
126
- migratedFrom: 'sessions.jsonl',
127
- migratedAt: Date.now(),
128
- }));
129
- const sorted = [...group.sessions].sort((a, b) => (a.startedAt ?? 0) - (b.startedAt ?? 0));
130
- for (const s of sorted) {
131
- lines.push(JSON.stringify({
132
- type: 'task',
133
- sessionId: s.id,
134
- agent: s.agent,
135
- prompt: s.prompt,
136
- cwd: s.cwd,
137
- startedAt: s.startedAt,
138
- status: s.status,
139
- durationMs: s.durationMs,
140
- completedAt: s.completedAt,
141
- steps: s.steps,
142
- artifacts: s.artifacts,
143
- }));
144
- }
145
- return lines.join('\n') + '\n';
146
- }
147
- /**
148
- * Run the migration. Idempotent: skips projects whose thread.jsonl already
149
- * exists (so daemon-startup auto-runs are safe).
150
- */
151
- export function migrate(options) {
152
- const startedAt = Date.now();
153
- const skipTempDirs = options.skipTempDirs ?? true;
154
- const unknownLabel = options.unknownCwdLabel ?? 'unknown';
155
- const { events } = readEvents(options.sourceJsonl);
156
- const sessions = rollupSessions(events);
157
- const groups = groupByCwd(sessions, unknownLabel);
158
- let threadsWritten = 0;
159
- let threadsSkippedExisting = 0;
160
- let unknownCwdSessions = 0;
161
- const abandonedSessions = sessions.filter((s) => s.abandoned).length;
162
- let tempDirSessionsSkipped = 0;
163
- for (const group of groups) {
164
- if (skipTempDirs && isTempDir(group.cwd)) {
165
- tempDirSessionsSkipped += group.sessions.length;
166
- continue;
167
- }
168
- if (group.cwd === null)
169
- unknownCwdSessions += group.sessions.length;
170
- const dir = join(options.targetDir, group.slug);
171
- const threadFile = join(dir, 'thread.jsonl');
172
- if (existsSync(threadFile)) {
173
- threadsSkippedExisting++;
174
- continue;
175
- }
176
- mkdirSync(dir, { recursive: true });
177
- atomicWriteFile(threadFile, buildThreadJsonl(group));
178
- threadsWritten++;
179
- }
180
- return {
181
- sessionsProcessed: sessions.length,
182
- threadsWritten,
183
- threadsSkippedExisting,
184
- unknownCwdSessions,
185
- abandonedSessions,
186
- tempDirSessionsSkipped,
187
- durationMs: Date.now() - startedAt,
188
- };
189
- }
190
- /** Dry-run: same as migrate but writes nothing. Returns what WOULD happen. */
191
- export function dryRun(options) {
192
- const startedAt = Date.now();
193
- const skipTempDirs = options.skipTempDirs ?? true;
194
- const unknownLabel = options.unknownCwdLabel ?? 'unknown';
195
- const { events } = readEvents(options.sourceJsonl);
196
- const sessions = rollupSessions(events);
197
- const groups = groupByCwd(sessions, unknownLabel);
198
- let threadsWritten = 0;
199
- let threadsSkippedExisting = 0;
200
- let unknownCwdSessions = 0;
201
- const abandonedSessions = sessions.filter((s) => s.abandoned).length;
202
- let tempDirSessionsSkipped = 0;
203
- for (const group of groups) {
204
- if (skipTempDirs && isTempDir(group.cwd)) {
205
- tempDirSessionsSkipped += group.sessions.length;
206
- continue;
207
- }
208
- if (group.cwd === null)
209
- unknownCwdSessions += group.sessions.length;
210
- const threadFile = join(options.targetDir, group.slug, 'thread.jsonl');
211
- if (existsSync(threadFile))
212
- threadsSkippedExisting++;
213
- else
214
- threadsWritten++;
215
- }
216
- return {
217
- sessionsProcessed: sessions.length,
218
- threadsWritten,
219
- threadsSkippedExisting,
220
- unknownCwdSessions,
221
- abandonedSessions,
222
- tempDirSessionsSkipped,
223
- durationMs: Date.now() - startedAt,
224
- };
225
- }
226
- /**
227
- * Rollback: removes the projects directory. Source sessions.jsonl is never
228
- * touched. Returns the count of removed project subdirectories.
229
- */
230
- export function rollback(targetDir) {
231
- if (!existsSync(targetDir))
232
- return { removedDirs: 0 };
233
- const subdirs = readdirSync(targetDir);
234
- rmSync(targetDir, { recursive: true });
235
- return { removedDirs: subdirs.length };
236
- }
237
- //# sourceMappingURL=project-thread-migration.js.map