trellis 2.0.13 → 2.1.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 (96) hide show
  1. package/dist/cli/index.js +1 -1
  2. package/dist/embeddings/index.js +1 -1
  3. package/dist/{index-7gvjxt27.js → index-2917tjd8.js} +1 -1
  4. package/package.json +2 -10
  5. package/dist/transformers.node-bx3q9d7k.js +0 -33130
  6. package/src/cli/index.ts +0 -3356
  7. package/src/core/agents/harness.ts +0 -380
  8. package/src/core/agents/index.ts +0 -18
  9. package/src/core/agents/types.ts +0 -90
  10. package/src/core/index.ts +0 -118
  11. package/src/core/kernel/middleware.ts +0 -44
  12. package/src/core/kernel/trellis-kernel.ts +0 -593
  13. package/src/core/ontology/builtins.ts +0 -248
  14. package/src/core/ontology/index.ts +0 -34
  15. package/src/core/ontology/registry.ts +0 -209
  16. package/src/core/ontology/types.ts +0 -124
  17. package/src/core/ontology/validator.ts +0 -382
  18. package/src/core/persist/backend.ts +0 -74
  19. package/src/core/persist/sqlite-backend.ts +0 -298
  20. package/src/core/plugins/index.ts +0 -17
  21. package/src/core/plugins/registry.ts +0 -322
  22. package/src/core/plugins/types.ts +0 -126
  23. package/src/core/query/datalog.ts +0 -188
  24. package/src/core/query/engine.ts +0 -370
  25. package/src/core/query/index.ts +0 -34
  26. package/src/core/query/parser.ts +0 -481
  27. package/src/core/query/types.ts +0 -200
  28. package/src/core/store/eav-store.ts +0 -467
  29. package/src/decisions/auto-capture.ts +0 -136
  30. package/src/decisions/hooks.ts +0 -163
  31. package/src/decisions/index.ts +0 -261
  32. package/src/decisions/types.ts +0 -103
  33. package/src/embeddings/auto-embed.ts +0 -248
  34. package/src/embeddings/chunker.ts +0 -327
  35. package/src/embeddings/index.ts +0 -48
  36. package/src/embeddings/model.ts +0 -112
  37. package/src/embeddings/search.ts +0 -305
  38. package/src/embeddings/store.ts +0 -313
  39. package/src/embeddings/types.ts +0 -92
  40. package/src/engine.ts +0 -1125
  41. package/src/garden/cluster.ts +0 -330
  42. package/src/garden/garden.ts +0 -306
  43. package/src/garden/index.ts +0 -29
  44. package/src/git/git-exporter.ts +0 -286
  45. package/src/git/git-importer.ts +0 -329
  46. package/src/git/git-reader.ts +0 -189
  47. package/src/git/index.ts +0 -22
  48. package/src/identity/governance.ts +0 -211
  49. package/src/identity/identity.ts +0 -224
  50. package/src/identity/index.ts +0 -30
  51. package/src/identity/signing-middleware.ts +0 -97
  52. package/src/index.ts +0 -29
  53. package/src/links/index.ts +0 -49
  54. package/src/links/lifecycle.ts +0 -400
  55. package/src/links/parser.ts +0 -484
  56. package/src/links/ref-index.ts +0 -186
  57. package/src/links/resolver.ts +0 -314
  58. package/src/links/types.ts +0 -108
  59. package/src/mcp/index.ts +0 -22
  60. package/src/mcp/server.ts +0 -1278
  61. package/src/semantic/csharp-parser.ts +0 -493
  62. package/src/semantic/go-parser.ts +0 -585
  63. package/src/semantic/index.ts +0 -34
  64. package/src/semantic/java-parser.ts +0 -456
  65. package/src/semantic/python-parser.ts +0 -659
  66. package/src/semantic/ruby-parser.ts +0 -446
  67. package/src/semantic/rust-parser.ts +0 -784
  68. package/src/semantic/semantic-merge.ts +0 -210
  69. package/src/semantic/ts-parser.ts +0 -681
  70. package/src/semantic/types.ts +0 -175
  71. package/src/sync/http-transport.ts +0 -144
  72. package/src/sync/index.ts +0 -43
  73. package/src/sync/memory-transport.ts +0 -66
  74. package/src/sync/multi-repo.ts +0 -200
  75. package/src/sync/reconciler.ts +0 -237
  76. package/src/sync/sync-engine.ts +0 -258
  77. package/src/sync/types.ts +0 -104
  78. package/src/sync/ws-transport.ts +0 -145
  79. package/src/ui/client.html +0 -695
  80. package/src/ui/server.ts +0 -419
  81. package/src/vcs/blob-store.ts +0 -124
  82. package/src/vcs/branch.ts +0 -150
  83. package/src/vcs/checkpoint.ts +0 -64
  84. package/src/vcs/decompose.ts +0 -469
  85. package/src/vcs/diff.ts +0 -409
  86. package/src/vcs/engine-context.ts +0 -26
  87. package/src/vcs/index.ts +0 -23
  88. package/src/vcs/issue.ts +0 -800
  89. package/src/vcs/merge.ts +0 -425
  90. package/src/vcs/milestone.ts +0 -124
  91. package/src/vcs/ops.ts +0 -59
  92. package/src/vcs/types.ts +0 -213
  93. package/src/vcs/vcs-middleware.ts +0 -81
  94. package/src/watcher/fs-watcher.ts +0 -255
  95. package/src/watcher/index.ts +0 -9
  96. package/src/watcher/ingestion.ts +0 -116
package/src/engine.ts DELETED
@@ -1,1125 +0,0 @@
1
- /**
2
- * TrellisVCS Engine
3
- *
4
- * The composition root that ties together the trellis-core kernel,
5
- * the file watcher, the ingestion pipeline, and VCS middleware.
6
- *
7
- * Usage:
8
- * const engine = new TrellisVcsEngine({ rootPath: '/path/to/repo' });
9
- * await engine.init(); // scan + create initial ops
10
- * engine.watch(); // start continuous monitoring
11
- * engine.stop(); // stop watcher
12
- */
13
-
14
- import {
15
- existsSync,
16
- mkdirSync,
17
- readFileSync,
18
- writeFileSync,
19
- copyFileSync,
20
- } from 'fs';
21
- import { readFile } from 'fs/promises';
22
- import { join, dirname } from 'path';
23
- import { EAVStore } from './core/store/eav-store.js';
24
- import type { Fact, Link } from './core/store/eav-store.js';
25
- import { FileWatcher, type ScanProgress } from './watcher/fs-watcher.js';
26
- import { Ingestion } from './watcher/ingestion.js';
27
- import { decompose } from './vcs/decompose.js';
28
- import { createVcsOp, isVcsOpKind } from './vcs/ops.js';
29
- import type { VcsOp, TrellisVcsConfig, FileChangeEvent } from './vcs/types.js';
30
- import { DEFAULT_CONFIG } from './vcs/types.js';
31
- import { BlobStore } from './vcs/blob-store.js';
32
- import type { EngineContext } from './vcs/engine-context.js';
33
- import * as branchMod from './vcs/branch.js';
34
- import * as milestoneMod from './vcs/milestone.js';
35
- import * as checkpointMod from './vcs/checkpoint.js';
36
- import * as diffMod from './vcs/diff.js';
37
- import * as mergeMod from './vcs/merge.js';
38
- import * as issueMod from './vcs/issue.js';
39
- import * as decisionMod from './decisions/index.js';
40
- import { IdeaGarden, buildMilestonedOpHashes } from './garden/index.js';
41
- import {
42
- typescriptParser,
43
- pythonParser,
44
- goParser,
45
- rustParser,
46
- rubyParser,
47
- javaParser,
48
- csharpParser,
49
- } from './semantic/index.js';
50
- import type {
51
- ParseResult,
52
- SemanticPatch,
53
- ParserAdapter,
54
- } from './semantic/types.js';
55
-
56
- // ---------------------------------------------------------------------------
57
- // Persistent op log (lightweight SQLite-free version for P0)
58
- // ---------------------------------------------------------------------------
59
-
60
- /**
61
- * A simple JSON-file-backed op log for P0.
62
- * Will be replaced by SqliteKernelBackend integration in P1.
63
- */
64
- class JsonOpLog {
65
- private ops: VcsOp[] = [];
66
- private filePath: string;
67
-
68
- constructor(filePath: string) {
69
- this.filePath = filePath;
70
- }
71
-
72
- load(): void {
73
- if (existsSync(this.filePath)) {
74
- const raw = readFileSync(this.filePath, 'utf-8');
75
- try {
76
- this.ops = JSON.parse(raw);
77
- } catch (err) {
78
- // Attempt to load from backup
79
- const backupPath = this.filePath + '.bak';
80
- if (existsSync(backupPath)) {
81
- const backupRaw = readFileSync(backupPath, 'utf-8');
82
- this.ops = JSON.parse(backupRaw);
83
- // Restore the backup over the corrupted file
84
- writeFileSync(this.filePath, backupRaw);
85
- } else {
86
- throw new Error(
87
- `Corrupted ops.json and no backup found. Run \`trellis repair\` to attempt recovery.`,
88
- );
89
- }
90
- }
91
- }
92
- }
93
-
94
- append(op: VcsOp): void {
95
- this.ops.push(op);
96
- this.flush();
97
- }
98
-
99
- readAll(): VcsOp[] {
100
- return [...this.ops];
101
- }
102
-
103
- getLastOp(): VcsOp | undefined {
104
- return this.ops.length > 0 ? this.ops[this.ops.length - 1] : undefined;
105
- }
106
-
107
- count(): number {
108
- return this.ops.length;
109
- }
110
-
111
- private flush(): void {
112
- const dir = dirname(this.filePath);
113
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
114
- // Backup-on-write: keep one generation of backup
115
- if (existsSync(this.filePath)) {
116
- const backupPath = this.filePath + '.bak';
117
- try {
118
- copyFileSync(this.filePath, backupPath);
119
- } catch {
120
- // Best-effort backup — don't block writes
121
- }
122
- }
123
- writeFileSync(this.filePath, JSON.stringify(this.ops, null, 2));
124
- }
125
-
126
- /**
127
- * Attempt to repair a corrupted ops.json by truncating to the
128
- * last valid entry. Returns the number of recovered ops.
129
- */
130
- static repair(filePath: string): { recovered: number; lost: number } {
131
- if (!existsSync(filePath)) {
132
- return { recovered: 0, lost: 0 };
133
- }
134
-
135
- const raw = readFileSync(filePath, 'utf-8');
136
-
137
- // Try parsing as-is first
138
- try {
139
- const ops = JSON.parse(raw);
140
- return { recovered: ops.length, lost: 0 };
141
- } catch {
142
- // Corrupted — attempt truncation repair
143
- }
144
-
145
- // Find the last complete object by locating the last valid hash line
146
- const lastHash = raw.lastIndexOf('"hash": "trellis:op:');
147
- if (lastHash === -1) {
148
- // Check backup
149
- const bakPath = filePath + '.bak';
150
- if (existsSync(bakPath)) {
151
- const bakRaw = readFileSync(bakPath, 'utf-8');
152
- try {
153
- const ops = JSON.parse(bakRaw);
154
- writeFileSync(filePath, bakRaw);
155
- return { recovered: ops.length, lost: 0 };
156
- } catch {
157
- // Backup also corrupted
158
- }
159
- }
160
- writeFileSync(filePath, '[]');
161
- return { recovered: 0, lost: -1 };
162
- }
163
-
164
- // Find end of the hash line → closing brace of that object
165
- const endOfLine = raw.indexOf('\n', lastHash);
166
- const closingBrace = raw.indexOf(' }', endOfLine);
167
- if (closingBrace === -1) {
168
- writeFileSync(filePath, '[]');
169
- return { recovered: 0, lost: -1 };
170
- }
171
-
172
- const fixed = raw.slice(0, closingBrace + 3) + '\n]';
173
- try {
174
- const ops = JSON.parse(fixed);
175
- // Save repaired + backup of corrupted
176
- writeFileSync(filePath + '.corrupted', raw);
177
- writeFileSync(filePath, fixed);
178
- return { recovered: ops.length, lost: 0 };
179
- } catch {
180
- writeFileSync(filePath + '.corrupted', raw);
181
- writeFileSync(filePath, '[]');
182
- return { recovered: 0, lost: -1 };
183
- }
184
- }
185
- }
186
-
187
- // ---------------------------------------------------------------------------
188
- // .gitignore reader
189
- // ---------------------------------------------------------------------------
190
-
191
- /**
192
- * Parse an ignore file (.gitignore or .trellisignore) and return normalized
193
- * patterns. Strips comments, blank lines, and trailing slashes.
194
- */
195
- function parseIgnoreFile(filePath: string): string[] {
196
- if (!existsSync(filePath)) return [];
197
- try {
198
- const content = readFileSync(filePath, 'utf-8');
199
- return content
200
- .split('\n')
201
- .map((line) => line.trim())
202
- .filter((line) => line.length > 0 && !line.startsWith('#'))
203
- .map((line) => line.replace(/\/$/, '')); // strip trailing slash
204
- } catch {
205
- return [];
206
- }
207
- }
208
-
209
- /**
210
- * Read ignore patterns from both .gitignore and .trellisignore.
211
- * .trellisignore allows ignoring paths that are tracked by Git but
212
- * should not be tracked by TrellisVCS (e.g. source-linked dependencies).
213
- */
214
- function readIgnorePatterns(rootPath: string): string[] {
215
- return [
216
- ...parseIgnoreFile(join(rootPath, '.gitignore')),
217
- ...parseIgnoreFile(join(rootPath, '.trellisignore')),
218
- ];
219
- }
220
-
221
- // ---------------------------------------------------------------------------
222
- // Config persistence
223
- // ---------------------------------------------------------------------------
224
-
225
- interface PersistedConfig {
226
- rootPath: string;
227
- ignorePatterns: string[];
228
- debounceMs: number;
229
- defaultBranch: string;
230
- agentId: string;
231
- createdAt: string;
232
- }
233
-
234
- // ---------------------------------------------------------------------------
235
- // Engine
236
- // ---------------------------------------------------------------------------
237
-
238
- export interface InitProgress {
239
- phase: 'discovering' | 'hashing' | 'recording' | 'done';
240
- current: number;
241
- total: number;
242
- message: string;
243
- }
244
-
245
- export class TrellisVcsEngine {
246
- private config: TrellisVcsConfig;
247
- private store: EAVStore;
248
- private opLog: JsonOpLog;
249
- private watcher: FileWatcher | null = null;
250
- private ingestion: Ingestion | null = null;
251
- private agentId: string;
252
- private currentBranch: string = 'main';
253
- private checkpointOpCount: number = 0;
254
- private checkpointThreshold: number = 100;
255
- private _pendingAutoCheckpoint: boolean = false;
256
- private _blobStore: BlobStore | null = null;
257
-
258
- constructor(
259
- opts: { rootPath: string; agentId?: string } & Partial<TrellisVcsConfig>,
260
- ) {
261
- // Merge default ignore patterns with .gitignore if present
262
- const gitignorePatterns = readIgnorePatterns(opts.rootPath);
263
- const mergedIgnore = [
264
- ...new Set([
265
- ...(opts.ignorePatterns ?? DEFAULT_CONFIG.ignorePatterns),
266
- ...gitignorePatterns,
267
- ]),
268
- ];
269
-
270
- this.config = {
271
- rootPath: opts.rootPath,
272
- ignorePatterns: mergedIgnore,
273
- debounceMs: opts.debounceMs ?? DEFAULT_CONFIG.debounceMs,
274
- defaultBranch: opts.defaultBranch ?? DEFAULT_CONFIG.defaultBranch,
275
- dbPath: opts.dbPath ?? DEFAULT_CONFIG.dbPath,
276
- };
277
- this.agentId = opts.agentId ?? `agent:${process.env.USER ?? 'unknown'}`;
278
- this.store = new EAVStore();
279
- this.opLog = new JsonOpLog(
280
- join(this.config.rootPath, '.trellis', 'ops.json'),
281
- );
282
- }
283
-
284
- // -------------------------------------------------------------------------
285
- // Lifecycle
286
- // -------------------------------------------------------------------------
287
-
288
- /**
289
- * Initialize a new TrellisVCS repo. Creates .trellis/ directory and config.
290
- */
291
- async initRepo(opts?: {
292
- onProgress?: (progress: InitProgress) => void;
293
- }): Promise<{ opsCreated: number }> {
294
- const trellisDir = join(this.config.rootPath, '.trellis');
295
- if (!existsSync(trellisDir)) {
296
- mkdirSync(trellisDir, { recursive: true });
297
- }
298
-
299
- // Initialize blob store
300
- this._blobStore = new BlobStore(trellisDir);
301
-
302
- // Write config
303
- const configPath = join(trellisDir, 'config.json');
304
- const persistedConfig: PersistedConfig = {
305
- rootPath: this.config.rootPath,
306
- ignorePatterns: this.config.ignorePatterns,
307
- debounceMs: this.config.debounceMs,
308
- defaultBranch: this.config.defaultBranch,
309
- agentId: this.agentId,
310
- createdAt: new Date().toISOString(),
311
- };
312
- writeFileSync(configPath, JSON.stringify(persistedConfig, null, 2));
313
-
314
- // Load existing ops (empty for new repo)
315
- this.opLog.load();
316
-
317
- // Create initial branch op
318
- const branchOp = await createVcsOp('vcs:branchCreate', {
319
- agentId: this.agentId,
320
- previousHash: this.opLog.getLastOp()?.hash,
321
- vcs: {
322
- branchName: this.config.defaultBranch,
323
- },
324
- });
325
- this.applyOp(branchOp);
326
-
327
- // Scan filesystem and create file-add ops for all existing files
328
- const scanner = new FileWatcher({
329
- rootPath: this.config.rootPath,
330
- ignorePatterns: [...this.config.ignorePatterns, '.trellis'],
331
- debounceMs: this.config.debounceMs,
332
- onEvent: () => {},
333
- });
334
- const events = await scanner.scan({
335
- onProgress: (progress: ScanProgress) => {
336
- if (progress.phase === 'done') {
337
- return;
338
- }
339
- opts?.onProgress?.({
340
- phase: progress.phase,
341
- current: progress.current,
342
- total: progress.total,
343
- message: progress.message,
344
- });
345
- },
346
- });
347
-
348
- let opsCreated = 1; // branch op
349
- opts?.onProgress?.({
350
- phase: 'recording',
351
- current: 0,
352
- total: events.length,
353
- message: `Recording ${events.length} initial file operations…`,
354
- });
355
- for (const event of events) {
356
- // Store file content in blob store
357
- if (event.contentHash) {
358
- try {
359
- const absPath = join(this.config.rootPath, event.path);
360
- const content = await readFile(absPath);
361
- await this._blobStore!.put(content);
362
- } catch {}
363
- }
364
-
365
- const op = await createVcsOp('vcs:fileAdd', {
366
- agentId: this.agentId,
367
- previousHash: this.opLog.getLastOp()?.hash,
368
- vcs: {
369
- filePath: event.path,
370
- contentHash: event.contentHash,
371
- size: event.size,
372
- },
373
- });
374
- this.applyOp(op);
375
- opsCreated++;
376
- const recordedFiles = opsCreated - 1;
377
- if (recordedFiles % 25 === 0 || recordedFiles === events.length) {
378
- opts?.onProgress?.({
379
- phase: 'recording',
380
- current: recordedFiles,
381
- total: events.length,
382
- message: `Recorded ${recordedFiles}/${events.length} initial file ops`,
383
- });
384
- }
385
- }
386
-
387
- await this.flushAutoCheckpoint();
388
- opts?.onProgress?.({
389
- phase: 'done',
390
- current: opsCreated,
391
- total: opsCreated,
392
- message: `Initialized repository with ${opsCreated} operations`,
393
- });
394
- return { opsCreated };
395
- }
396
-
397
- /**
398
- * Open an existing TrellisVCS repo. Loads ops and replays into EAV store.
399
- */
400
- open(): { opsReplayed: number } {
401
- this.opLog.load();
402
-
403
- // Initialize blob store
404
- const trellisDir = join(this.config.rootPath, '.trellis');
405
- this._blobStore = new BlobStore(trellisDir);
406
-
407
- // Load config
408
- const configPath = join(this.config.rootPath, '.trellis', 'config.json');
409
- if (existsSync(configPath)) {
410
- const raw = readFileSync(configPath, 'utf-8');
411
- const persisted: PersistedConfig = JSON.parse(raw);
412
- this.agentId = persisted.agentId;
413
- // Re-merge persisted patterns with .gitignore + .trellisignore
414
- const filePatterns = readIgnorePatterns(this.config.rootPath);
415
- this.config.ignorePatterns = [
416
- ...new Set([...persisted.ignorePatterns, ...filePatterns]),
417
- ];
418
- this.config.debounceMs = persisted.debounceMs;
419
- this.config.defaultBranch = persisted.defaultBranch;
420
- }
421
-
422
- // Load branch state
423
- this.loadCurrentBranch();
424
-
425
- // Replay all ops into the EAV store
426
- const ops = this.opLog.readAll();
427
- for (const op of ops) {
428
- this.replayOp(op);
429
- }
430
-
431
- return { opsReplayed: ops.length };
432
- }
433
-
434
- /**
435
- * Start watching the filesystem for changes.
436
- */
437
- watch(): void {
438
- this.ingestion = new Ingestion({
439
- agentId: this.agentId,
440
- lastOpHash: this.opLog.getLastOp()?.hash,
441
- onOp: (op) => this.applyOp(op),
442
- });
443
-
444
- this.watcher = new FileWatcher({
445
- rootPath: this.config.rootPath,
446
- ignorePatterns: [...this.config.ignorePatterns, '.trellis'],
447
- debounceMs: this.config.debounceMs,
448
- onEvent: async (event) => {
449
- // Store blob for file adds/modifies
450
- if (
451
- (event.type === 'add' || event.type === 'modify') &&
452
- event.contentHash &&
453
- this._blobStore
454
- ) {
455
- try {
456
- const absPath = join(this.config.rootPath, event.path);
457
- const content = await readFile(absPath);
458
- await this._blobStore.put(content);
459
- } catch {}
460
- }
461
- await this.ingestion!.process(event);
462
- },
463
- });
464
-
465
- // Scan to populate known files map, reconcile against op log for
466
- // untracked files, then start watching for live changes.
467
- this.watcher.scan().then(async (scanEvents) => {
468
- // Build set of paths already tracked in the op log
469
- const trackedPaths = new Set(this.trackedFiles().map((f) => f.path));
470
-
471
- // Emit fileAdd ops for files on disk that aren't in the op log
472
- for (const event of scanEvents) {
473
- if (!trackedPaths.has(event.path)) {
474
- // Store blob
475
- if (event.contentHash && this._blobStore) {
476
- try {
477
- const absPath = join(this.config.rootPath, event.path);
478
- const content = await readFile(absPath);
479
- await this._blobStore.put(content);
480
- } catch {}
481
- }
482
- await this.ingestion!.process(event);
483
- }
484
- }
485
-
486
- this.watcher!.start();
487
- });
488
- }
489
-
490
- /**
491
- * Stop watching.
492
- */
493
- stop(): void {
494
- this.watcher?.stop();
495
- this.watcher = null;
496
- this.ingestion = null;
497
- }
498
-
499
- // -------------------------------------------------------------------------
500
- // Queries
501
- // -------------------------------------------------------------------------
502
-
503
- /**
504
- * Returns all ops in the causal stream.
505
- */
506
- getOps(): VcsOp[] {
507
- return this.opLog.readAll();
508
- }
509
-
510
- /**
511
- * Returns the total number of ops.
512
- */
513
- getOpCount(): number {
514
- return this.opLog.count();
515
- }
516
-
517
- /**
518
- * Returns the EAV store for direct querying.
519
- */
520
- getStore(): EAVStore {
521
- return this.store;
522
- }
523
-
524
- /**
525
- * Returns the blob store for content retrieval.
526
- */
527
- getBlobStore(): BlobStore | null {
528
- return this._blobStore;
529
- }
530
-
531
- /**
532
- * Returns the current status: tracked files, last op, branch info.
533
- */
534
- status(): {
535
- branch: string;
536
- totalOps: number;
537
- trackedFiles: number;
538
- lastOp: VcsOp | undefined;
539
- recentOps: VcsOp[];
540
- } {
541
- const ops = this.opLog.readAll();
542
- const fileEntities = this.store
543
- .getFactsByAttribute('type')
544
- .filter((f) => f.v === 'FileNode');
545
-
546
- return {
547
- branch: this.currentBranch,
548
- totalOps: ops.length,
549
- trackedFiles: fileEntities.length,
550
- lastOp: ops[ops.length - 1],
551
- recentOps: ops.slice(-10),
552
- };
553
- }
554
-
555
- /**
556
- * Returns op history, optionally filtered by file path.
557
- */
558
- log(opts?: { limit?: number; filePath?: string }): VcsOp[] {
559
- let ops = this.opLog.readAll();
560
-
561
- if (opts?.filePath) {
562
- ops = ops.filter((op) => {
563
- const vcs = op.vcs;
564
- return (
565
- vcs?.filePath === opts.filePath || vcs?.oldFilePath === opts.filePath
566
- );
567
- });
568
- }
569
-
570
- if (opts?.limit) {
571
- ops = ops.slice(-opts.limit);
572
- }
573
-
574
- return ops;
575
- }
576
-
577
- /**
578
- * Returns all tracked file paths and their content hashes.
579
- */
580
- trackedFiles(): Array<{ path: string; contentHash: string | undefined }> {
581
- const fileTypeFacts = this.store
582
- .getFactsByAttribute('type')
583
- .filter((f) => f.v === 'FileNode');
584
-
585
- return fileTypeFacts.map((f) => {
586
- const pathFacts = this.store
587
- .getFactsByEntity(f.e)
588
- .filter((ef) => ef.a === 'path');
589
- const hashFacts = this.store
590
- .getFactsByEntity(f.e)
591
- .filter((ef) => ef.a === 'contentHash');
592
- return {
593
- path: (pathFacts[0]?.v as string) ?? f.e,
594
- contentHash: hashFacts[0]?.v as string | undefined,
595
- };
596
- });
597
- }
598
-
599
- /**
600
- * Returns the root path of the repository.
601
- */
602
- getRootPath(): string {
603
- return this.config.rootPath;
604
- }
605
-
606
- /**
607
- * Checks if a .trellis directory exists at the root path.
608
- */
609
- static isRepo(rootPath: string): boolean {
610
- return existsSync(join(rootPath, '.trellis', 'config.json'));
611
- }
612
-
613
- static repair(rootPath: string): { recovered: number; lost: number } {
614
- const opsPath = join(rootPath, '.trellis', 'ops.json');
615
- return JsonOpLog.repair(opsPath);
616
- }
617
-
618
- // -------------------------------------------------------------------------
619
- // Branch Management (delegated to src/vcs/branch.ts)
620
- // -------------------------------------------------------------------------
621
-
622
- async createBranch(name: string): Promise<VcsOp> {
623
- const op = await branchMod.createBranch(
624
- this._ctx(),
625
- name,
626
- this.currentBranch,
627
- );
628
- await this.flushAutoCheckpoint();
629
- return op;
630
- }
631
-
632
- switchBranch(name: string): void {
633
- branchMod.switchBranch(this._ctx(), name);
634
- this.currentBranch = name;
635
- branchMod.saveBranchState(this.config.rootPath, { currentBranch: name });
636
- }
637
-
638
- listBranches(): branchMod.BranchInfo[] {
639
- return branchMod.listBranches(this._ctx(), this.currentBranch);
640
- }
641
-
642
- async deleteBranch(name: string): Promise<VcsOp> {
643
- const op = await branchMod.deleteBranch(
644
- this._ctx(),
645
- name,
646
- this.currentBranch,
647
- );
648
- await this.flushAutoCheckpoint();
649
- return op;
650
- }
651
-
652
- getCurrentBranch(): string {
653
- return this.currentBranch;
654
- }
655
-
656
- // -------------------------------------------------------------------------
657
- // Milestones (delegated to src/vcs/milestone.ts)
658
- // -------------------------------------------------------------------------
659
-
660
- async createMilestone(
661
- message: string,
662
- opts?: { fromOpHash?: string; toOpHash?: string },
663
- ): Promise<VcsOp> {
664
- const op = await milestoneMod.createMilestone(this._ctx(), message, opts);
665
- await this.flushAutoCheckpoint();
666
- return op;
667
- }
668
-
669
- listMilestones(): milestoneMod.MilestoneInfo[] {
670
- return milestoneMod.listMilestones(this._ctx());
671
- }
672
-
673
- // -------------------------------------------------------------------------
674
- // Checkpoints (delegated to src/vcs/checkpoint.ts)
675
- // -------------------------------------------------------------------------
676
-
677
- async createCheckpoint(
678
- trigger: checkpointMod.CheckpointTrigger = 'manual',
679
- ): Promise<VcsOp> {
680
- const op = await checkpointMod.createCheckpoint(this._ctx(), trigger);
681
- this.checkpointOpCount = 0;
682
- return op;
683
- }
684
-
685
- listCheckpoints(): checkpointMod.CheckpointInfo[] {
686
- return checkpointMod.listCheckpoints(this._ctx());
687
- }
688
-
689
- setCheckpointThreshold(threshold: number): void {
690
- this.checkpointThreshold = threshold;
691
- }
692
-
693
- // -------------------------------------------------------------------------
694
- // Diff & Merge (delegated to src/vcs/diff.ts, src/vcs/merge.ts)
695
- // -------------------------------------------------------------------------
696
-
697
- /**
698
- * Diff two branches by comparing their file states.
699
- */
700
- diffBranches(branchA: string, branchB: string): diffMod.DiffResult {
701
- const ops = this.opLog.readAll();
702
- // Build file state for each branch by walking all ops
703
- // (branch-scoped filtering comes later; for now, single linear stream)
704
- const stateA = diffMod.buildFileStateAtOp(ops);
705
- const stateB = diffMod.buildFileStateAtOp(ops);
706
- return diffMod.diffFileStates(stateA, stateB, this._blobStore);
707
- }
708
-
709
- /**
710
- * Diff between two op hashes in the causal stream.
711
- */
712
- diffOps(fromHash: string, toHash: string): diffMod.DiffResult {
713
- return diffMod.diffOpRange(
714
- this.opLog.readAll(),
715
- fromHash,
716
- toHash,
717
- this._blobStore,
718
- );
719
- }
720
-
721
- /**
722
- * Diff the current state against a specific op hash (e.g. a milestone).
723
- */
724
- diffFromOp(opHash: string): diffMod.DiffResult {
725
- const ops = this.opLog.readAll();
726
- const stateA = diffMod.buildFileStateAtOp(ops, opHash);
727
- const stateB = diffMod.buildFileStateAtOp(ops);
728
- return diffMod.diffFileStates(stateA, stateB, this._blobStore);
729
- }
730
-
731
- /**
732
- * Three-way merge: merge source branch state into current branch state.
733
- * Uses the fork-point (branch creation op) as the common ancestor.
734
- */
735
- mergeBranch(sourceBranch: string): mergeMod.MergeResult {
736
- const ops = this.opLog.readAll();
737
-
738
- // Find the branch creation op to determine fork point
739
- const branchOp = ops.find(
740
- (o) =>
741
- o.kind === 'vcs:branchCreate' && o.vcs?.branchName === sourceBranch,
742
- );
743
- const forkHash = branchOp?.vcs?.targetOpHash;
744
-
745
- // Build three states
746
- const base = forkHash
747
- ? diffMod.buildFileStateAtOp(ops, forkHash)
748
- : new Map<string, diffMod.FileState>();
749
- const ours = diffMod.buildFileStateAtOp(ops); // current full state
750
- const theirs = diffMod.buildFileStateAtOp(ops); // same stream for now
751
-
752
- return mergeMod.threeWayMerge(base, ours, theirs, this._blobStore);
753
- }
754
-
755
- // -------------------------------------------------------------------------
756
- // Semantic Parsing (delegated to src/semantic/)
757
- // -------------------------------------------------------------------------
758
-
759
- private _parsers: ParserAdapter[] = [
760
- typescriptParser,
761
- pythonParser,
762
- goParser,
763
- rustParser,
764
- rubyParser,
765
- javaParser,
766
- csharpParser,
767
- ];
768
-
769
- /**
770
- * Parse a file's content into AST-level entities.
771
- */
772
- parseFile(content: string, filePath: string): ParseResult | null {
773
- const ext = filePath.split('.').pop() ?? '';
774
- const parser = this._parsers.find((p) =>
775
- p.languages.some((lang) => {
776
- if (lang === 'typescript') return ext === 'ts';
777
- if (lang === 'javascript')
778
- return ext === 'js' || ext === 'mjs' || ext === 'cjs';
779
- if (lang === 'tsx') return ext === 'tsx';
780
- if (lang === 'jsx') return ext === 'jsx';
781
- if (lang === 'python') return ext === 'py' || ext === 'pyi';
782
- if (lang === 'go') return ext === 'go';
783
- if (lang === 'rust') return ext === 'rs';
784
- if (lang === 'ruby') return ext === 'rb';
785
- if (lang === 'java') return ext === 'java';
786
- if (lang === 'csharp') return ext === 'cs';
787
- return false;
788
- }),
789
- );
790
- if (!parser) return null;
791
- return parser.parse(content, filePath);
792
- }
793
-
794
- /**
795
- * Compute semantic diff between two versions of a file.
796
- */
797
- semanticDiff(
798
- oldContent: string,
799
- newContent: string,
800
- filePath: string,
801
- ): SemanticPatch[] {
802
- const parser = this._parsers.find((p) =>
803
- p.languages.some((lang) => {
804
- const ext = filePath.split('.').pop() ?? '';
805
- if (lang === 'typescript') return ext === 'ts';
806
- if (lang === 'javascript')
807
- return ext === 'js' || ext === 'mjs' || ext === 'cjs';
808
- if (lang === 'tsx') return ext === 'tsx';
809
- if (lang === 'jsx') return ext === 'jsx';
810
- if (lang === 'python') return ext === 'py' || ext === 'pyi';
811
- if (lang === 'go') return ext === 'go';
812
- if (lang === 'rust') return ext === 'rs';
813
- if (lang === 'ruby') return ext === 'rb';
814
- if (lang === 'java') return ext === 'java';
815
- if (lang === 'csharp') return ext === 'cs';
816
- return false;
817
- }),
818
- );
819
- if (!parser) return [];
820
- const oldResult = parser.parse(oldContent, filePath);
821
- const newResult = parser.parse(newContent, filePath);
822
- return parser.diff(oldResult, newResult);
823
- }
824
-
825
- // -------------------------------------------------------------------------
826
- // Idea Garden (delegated to src/garden/)
827
- // -------------------------------------------------------------------------
828
-
829
- private _garden: IdeaGarden | null = null;
830
-
831
- /**
832
- * Get the Idea Garden instance for exploring abandoned work.
833
- */
834
- garden(): IdeaGarden {
835
- if (!this._garden) {
836
- this._garden = new IdeaGarden({
837
- readAllOps: () => this.opLog.readAll(),
838
- getMilestonedOpHashes: () =>
839
- buildMilestonedOpHashes(this.opLog.readAll()),
840
- });
841
- }
842
- return this._garden;
843
- }
844
-
845
- // -------------------------------------------------------------------------
846
- // Issue Management (delegated to src/vcs/issue.ts)
847
- // -------------------------------------------------------------------------
848
-
849
- async createIssue(
850
- title: string,
851
- opts?: {
852
- priority?: 'critical' | 'high' | 'medium' | 'low';
853
- labels?: string[];
854
- assignee?: string;
855
- parentId?: string;
856
- description?: string;
857
- status?: 'backlog' | 'queue';
858
- criteria?: Array<{ description: string; command?: string }>;
859
- },
860
- ): Promise<VcsOp> {
861
- const op = await issueMod.createIssue(
862
- this._ctx(),
863
- this.config.rootPath,
864
- title,
865
- opts,
866
- );
867
- await this.flushAutoCheckpoint();
868
- return op;
869
- }
870
-
871
- async updateIssue(
872
- id: string,
873
- updates: {
874
- title?: string;
875
- description?: string;
876
- priority?: 'critical' | 'high' | 'medium' | 'low';
877
- labels?: string[];
878
- assignee?: string;
879
- status?: 'backlog' | 'queue' | 'in_progress' | 'paused' | 'closed';
880
- },
881
- ): Promise<VcsOp> {
882
- const op = await issueMod.updateIssue(this._ctx(), id, updates);
883
- await this.flushAutoCheckpoint();
884
- return op;
885
- }
886
-
887
- async startIssue(id: string): Promise<VcsOp> {
888
- const issue = issueMod.getIssue(this._ctx(), id);
889
- if (!issue) throw new Error(`Issue ${id} not found.`);
890
-
891
- const slug = (issue.title ?? id)
892
- .toLowerCase()
893
- .replace(/[^a-z0-9]+/g, '-')
894
- .replace(/^-|-$/g, '')
895
- .slice(0, 40);
896
- const branchName = `issue/${id}-${slug}`;
897
-
898
- // Create the branch
899
- await this.createBranch(branchName);
900
-
901
- // Emit the issueStart op
902
- const op = await issueMod.startIssue(this._ctx(), id, branchName);
903
-
904
- // Switch to the branch
905
- this.switchBranch(branchName);
906
-
907
- await this.flushAutoCheckpoint();
908
- return op;
909
- }
910
-
911
- async pauseIssue(id: string, note: string): Promise<VcsOp> {
912
- const op = await issueMod.pauseIssue(this._ctx(), id, note);
913
-
914
- // Switch back to default branch
915
- this.switchBranch(this.config.defaultBranch);
916
-
917
- await this.flushAutoCheckpoint();
918
- return op;
919
- }
920
-
921
- async resumeIssue(id: string): Promise<VcsOp> {
922
- const issue = issueMod.getIssue(this._ctx(), id);
923
- if (!issue) throw new Error(`Issue ${id} not found.`);
924
- if (!issue.branchName)
925
- throw new Error(`Issue ${id} has no tracked branch.`);
926
-
927
- const op = await issueMod.resumeIssue(this._ctx(), id);
928
-
929
- // Switch to the issue branch
930
- this.switchBranch(issue.branchName);
931
-
932
- await this.flushAutoCheckpoint();
933
- return op;
934
- }
935
-
936
- async closeIssue(
937
- id: string,
938
- opts?: { confirm?: boolean },
939
- ): Promise<{ op?: VcsOp; criteriaResults: issueMod.CriterionResult[] }> {
940
- const result = await issueMod.closeIssue(this._ctx(), id, opts);
941
- if (result.op) {
942
- await this.flushAutoCheckpoint();
943
- }
944
- return result;
945
- }
946
-
947
- async triageIssue(id: string): Promise<VcsOp> {
948
- const op = await issueMod.triageIssue(this._ctx(), id);
949
- await this.flushAutoCheckpoint();
950
- return op;
951
- }
952
-
953
- async reopenIssue(id: string): Promise<VcsOp> {
954
- const op = await issueMod.reopenIssue(this._ctx(), id);
955
- await this.flushAutoCheckpoint();
956
- return op;
957
- }
958
-
959
- checkCompletionReadiness(): issueMod.CompletionReadiness {
960
- return issueMod.checkCompletionReadiness(this._ctx());
961
- }
962
-
963
- async assignIssue(id: string, agentId: string): Promise<VcsOp> {
964
- const op = await issueMod.assignIssue(this._ctx(), id, agentId);
965
- await this.flushAutoCheckpoint();
966
- return op;
967
- }
968
-
969
- async blockIssue(id: string, blockedById: string): Promise<VcsOp> {
970
- const op = await issueMod.blockIssue(this._ctx(), id, blockedById);
971
- await this.flushAutoCheckpoint();
972
- return op;
973
- }
974
-
975
- async unblockIssue(id: string, blockedById: string): Promise<VcsOp> {
976
- const op = await issueMod.unblockIssue(this._ctx(), id, blockedById);
977
- await this.flushAutoCheckpoint();
978
- return op;
979
- }
980
-
981
- async addCriterion(
982
- issueId: string,
983
- description: string,
984
- command?: string,
985
- ): Promise<VcsOp> {
986
- const op = await issueMod.addCriterion(
987
- this._ctx(),
988
- issueId,
989
- description,
990
- command,
991
- );
992
- await this.flushAutoCheckpoint();
993
- return op;
994
- }
995
-
996
- async setCriterionStatus(
997
- issueId: string,
998
- criterionIndex: number,
999
- status: 'passed' | 'failed' | 'pending',
1000
- ): Promise<VcsOp> {
1001
- const op = await issueMod.setCriterionStatus(
1002
- this._ctx(),
1003
- issueId,
1004
- criterionIndex,
1005
- status,
1006
- );
1007
- await this.flushAutoCheckpoint();
1008
- return op;
1009
- }
1010
-
1011
- async runCriteria(issueId: string): Promise<issueMod.CriterionResult[]> {
1012
- return issueMod.runCriteria(this._ctx(), issueId, this.config.rootPath);
1013
- }
1014
-
1015
- listIssues(filters?: issueMod.IssueFilters): issueMod.IssueInfo[] {
1016
- return issueMod.listIssues(this._ctx(), filters);
1017
- }
1018
-
1019
- getIssue(id: string): issueMod.IssueInfo | null {
1020
- return issueMod.getIssue(this._ctx(), id);
1021
- }
1022
-
1023
- getActiveIssues(): issueMod.IssueInfo[] {
1024
- return issueMod.getActiveIssues(this._ctx());
1025
- }
1026
-
1027
- // -------------------------------------------------------------------------
1028
- // Decision Traces
1029
- // -------------------------------------------------------------------------
1030
-
1031
- async recordDecision(input: decisionMod.DecisionInput): Promise<VcsOp> {
1032
- const op = await decisionMod.recordDecision(
1033
- this._ctx(),
1034
- this.config.rootPath,
1035
- input,
1036
- );
1037
- await this.flushAutoCheckpoint();
1038
- return op;
1039
- }
1040
-
1041
- queryDecisions(filter?: decisionMod.DecisionFilter): decisionMod.Decision[] {
1042
- return decisionMod.queryDecisions(this._ctx(), filter);
1043
- }
1044
-
1045
- getDecisionChain(entityId: string): decisionMod.Decision[] {
1046
- return decisionMod.getDecisionChain(this._ctx(), entityId);
1047
- }
1048
-
1049
- getDecision(id: string): decisionMod.Decision | null {
1050
- return decisionMod.getDecision(this._ctx(), id);
1051
- }
1052
-
1053
- // -------------------------------------------------------------------------
1054
- // Internal
1055
- // -------------------------------------------------------------------------
1056
-
1057
- private _ctx(): EngineContext {
1058
- return {
1059
- store: this.store,
1060
- agentId: this.agentId,
1061
- readAllOps: () => this.opLog.readAll(),
1062
- getLastOp: () => this.opLog.getLastOp(),
1063
- applyOp: (op) => this.applyOp(op),
1064
- };
1065
- }
1066
-
1067
- private applyOp(op: VcsOp): void {
1068
- // Decompose VCS op into EAV primitives and apply to store
1069
- const decomposed = decompose(op);
1070
-
1071
- if (decomposed.deleteFacts.length > 0) {
1072
- this.store.deleteFacts(decomposed.deleteFacts);
1073
- }
1074
- if (decomposed.deleteLinks.length > 0) {
1075
- this.store.deleteLinks(decomposed.deleteLinks);
1076
- }
1077
- if (decomposed.addFacts.length > 0) {
1078
- this.store.addFacts(decomposed.addFacts);
1079
- }
1080
- if (decomposed.addLinks.length > 0) {
1081
- this.store.addLinks(decomposed.addLinks);
1082
- }
1083
-
1084
- // Persist to op log
1085
- this.opLog.append(op);
1086
-
1087
- // Auto-checkpoint logic: set flag, flushed by public async callers
1088
- if (op.kind !== 'vcs:checkpointCreate' && this.checkpointThreshold > 0) {
1089
- this.checkpointOpCount++;
1090
- if (this.checkpointOpCount >= this.checkpointThreshold) {
1091
- this._pendingAutoCheckpoint = true;
1092
- }
1093
- }
1094
- }
1095
-
1096
- private async flushAutoCheckpoint(): Promise<void> {
1097
- if (this._pendingAutoCheckpoint) {
1098
- this._pendingAutoCheckpoint = false;
1099
- await this.createCheckpoint('op-count');
1100
- }
1101
- }
1102
-
1103
- private loadCurrentBranch(): void {
1104
- const state = branchMod.loadBranchState(this.config.rootPath);
1105
- this.currentBranch = state.currentBranch;
1106
- }
1107
-
1108
- private replayOp(op: VcsOp): void {
1109
- // Same as applyOp but doesn't persist (ops are already in the log)
1110
- const decomposed = decompose(op);
1111
-
1112
- if (decomposed.deleteFacts.length > 0) {
1113
- this.store.deleteFacts(decomposed.deleteFacts);
1114
- }
1115
- if (decomposed.deleteLinks.length > 0) {
1116
- this.store.deleteLinks(decomposed.deleteLinks);
1117
- }
1118
- if (decomposed.addFacts.length > 0) {
1119
- this.store.addFacts(decomposed.addFacts);
1120
- }
1121
- if (decomposed.addLinks.length > 0) {
1122
- this.store.addLinks(decomposed.addLinks);
1123
- }
1124
- }
1125
- }