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.
- package/dist/cli/index.js +1 -1
- package/dist/embeddings/index.js +1 -1
- package/dist/{index-7gvjxt27.js → index-2917tjd8.js} +1 -1
- package/package.json +2 -10
- package/dist/transformers.node-bx3q9d7k.js +0 -33130
- package/src/cli/index.ts +0 -3356
- package/src/core/agents/harness.ts +0 -380
- package/src/core/agents/index.ts +0 -18
- package/src/core/agents/types.ts +0 -90
- package/src/core/index.ts +0 -118
- package/src/core/kernel/middleware.ts +0 -44
- package/src/core/kernel/trellis-kernel.ts +0 -593
- package/src/core/ontology/builtins.ts +0 -248
- package/src/core/ontology/index.ts +0 -34
- package/src/core/ontology/registry.ts +0 -209
- package/src/core/ontology/types.ts +0 -124
- package/src/core/ontology/validator.ts +0 -382
- package/src/core/persist/backend.ts +0 -74
- package/src/core/persist/sqlite-backend.ts +0 -298
- package/src/core/plugins/index.ts +0 -17
- package/src/core/plugins/registry.ts +0 -322
- package/src/core/plugins/types.ts +0 -126
- package/src/core/query/datalog.ts +0 -188
- package/src/core/query/engine.ts +0 -370
- package/src/core/query/index.ts +0 -34
- package/src/core/query/parser.ts +0 -481
- package/src/core/query/types.ts +0 -200
- package/src/core/store/eav-store.ts +0 -467
- package/src/decisions/auto-capture.ts +0 -136
- package/src/decisions/hooks.ts +0 -163
- package/src/decisions/index.ts +0 -261
- package/src/decisions/types.ts +0 -103
- package/src/embeddings/auto-embed.ts +0 -248
- package/src/embeddings/chunker.ts +0 -327
- package/src/embeddings/index.ts +0 -48
- package/src/embeddings/model.ts +0 -112
- package/src/embeddings/search.ts +0 -305
- package/src/embeddings/store.ts +0 -313
- package/src/embeddings/types.ts +0 -92
- package/src/engine.ts +0 -1125
- package/src/garden/cluster.ts +0 -330
- package/src/garden/garden.ts +0 -306
- package/src/garden/index.ts +0 -29
- package/src/git/git-exporter.ts +0 -286
- package/src/git/git-importer.ts +0 -329
- package/src/git/git-reader.ts +0 -189
- package/src/git/index.ts +0 -22
- package/src/identity/governance.ts +0 -211
- package/src/identity/identity.ts +0 -224
- package/src/identity/index.ts +0 -30
- package/src/identity/signing-middleware.ts +0 -97
- package/src/index.ts +0 -29
- package/src/links/index.ts +0 -49
- package/src/links/lifecycle.ts +0 -400
- package/src/links/parser.ts +0 -484
- package/src/links/ref-index.ts +0 -186
- package/src/links/resolver.ts +0 -314
- package/src/links/types.ts +0 -108
- package/src/mcp/index.ts +0 -22
- package/src/mcp/server.ts +0 -1278
- package/src/semantic/csharp-parser.ts +0 -493
- package/src/semantic/go-parser.ts +0 -585
- package/src/semantic/index.ts +0 -34
- package/src/semantic/java-parser.ts +0 -456
- package/src/semantic/python-parser.ts +0 -659
- package/src/semantic/ruby-parser.ts +0 -446
- package/src/semantic/rust-parser.ts +0 -784
- package/src/semantic/semantic-merge.ts +0 -210
- package/src/semantic/ts-parser.ts +0 -681
- package/src/semantic/types.ts +0 -175
- package/src/sync/http-transport.ts +0 -144
- package/src/sync/index.ts +0 -43
- package/src/sync/memory-transport.ts +0 -66
- package/src/sync/multi-repo.ts +0 -200
- package/src/sync/reconciler.ts +0 -237
- package/src/sync/sync-engine.ts +0 -258
- package/src/sync/types.ts +0 -104
- package/src/sync/ws-transport.ts +0 -145
- package/src/ui/client.html +0 -695
- package/src/ui/server.ts +0 -419
- package/src/vcs/blob-store.ts +0 -124
- package/src/vcs/branch.ts +0 -150
- package/src/vcs/checkpoint.ts +0 -64
- package/src/vcs/decompose.ts +0 -469
- package/src/vcs/diff.ts +0 -409
- package/src/vcs/engine-context.ts +0 -26
- package/src/vcs/index.ts +0 -23
- package/src/vcs/issue.ts +0 -800
- package/src/vcs/merge.ts +0 -425
- package/src/vcs/milestone.ts +0 -124
- package/src/vcs/ops.ts +0 -59
- package/src/vcs/types.ts +0 -213
- package/src/vcs/vcs-middleware.ts +0 -81
- package/src/watcher/fs-watcher.ts +0 -255
- package/src/watcher/index.ts +0 -9
- 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
|
-
}
|