trellis 3.1.31 → 3.1.34

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 (53) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +1 -1
  3. package/dist/cli/index.d.ts +1 -0
  4. package/dist/cli/index.d.ts.map +1 -1
  5. package/dist/cli/index.js +320 -24
  6. package/dist/cli/lane.d.ts +6 -0
  7. package/dist/cli/lane.d.ts.map +1 -0
  8. package/dist/cms/client.d.ts +1 -1
  9. package/dist/cms/client.d.ts.map +1 -1
  10. package/dist/cms/index.js +0 -16
  11. package/dist/cms/types.d.ts +1 -1
  12. package/dist/cms/types.d.ts.map +1 -1
  13. package/dist/context/manager.d.ts +1 -1
  14. package/dist/context/manager.d.ts.map +1 -1
  15. package/dist/context/types.d.ts +1 -1
  16. package/dist/context/types.d.ts.map +1 -1
  17. package/dist/decisions/index.js +2 -2
  18. package/dist/engine.d.ts +86 -2
  19. package/dist/engine.d.ts.map +1 -1
  20. package/dist/{index-65z0xfjw.js → index-4cdr7x2x.js} +2 -2
  21. package/dist/{index-r5064492.js → index-nq520y6k.js} +7 -3
  22. package/dist/{index-v9b4hqa7.js → index-nzb3am4f.js} +4 -1
  23. package/dist/{index-8f2z3b0h.js → index-rv1p47gp.js} +996 -172
  24. package/dist/{index-1tv9sbwp.js → index-w5wccer3.js} +1 -1
  25. package/dist/{index-p3nnc7ac.js → index-xhcp6xrn.js} +462 -31
  26. package/dist/index.js +39 -7
  27. package/dist/mcp/server.d.ts.map +1 -1
  28. package/dist/plugins/agent-memory/graph-context-manager.d.ts +5 -0
  29. package/dist/plugins/agent-memory/graph-context-manager.d.ts.map +1 -1
  30. package/dist/{remote-manager-8qbz3mrn.js → remote-manager-xvbg4230.js} +4 -4
  31. package/dist/vcs/branch.d.ts +6 -0
  32. package/dist/vcs/branch.d.ts.map +1 -1
  33. package/dist/vcs/decompose.d.ts.map +1 -1
  34. package/dist/vcs/engine-context.d.ts +9 -2
  35. package/dist/vcs/engine-context.d.ts.map +1 -1
  36. package/dist/vcs/index.d.ts +2 -0
  37. package/dist/vcs/index.d.ts.map +1 -1
  38. package/dist/vcs/index.js +36 -4
  39. package/dist/vcs/issue.d.ts +2 -0
  40. package/dist/vcs/issue.d.ts.map +1 -1
  41. package/dist/vcs/lane-materialize.d.ts +45 -0
  42. package/dist/vcs/lane-materialize.d.ts.map +1 -0
  43. package/dist/vcs/lane-promote.d.ts +56 -0
  44. package/dist/vcs/lane-promote.d.ts.map +1 -0
  45. package/dist/vcs/lane.d.ts +55 -0
  46. package/dist/vcs/lane.d.ts.map +1 -0
  47. package/dist/vcs/op-log.d.ts +29 -0
  48. package/dist/vcs/op-log.d.ts.map +1 -0
  49. package/dist/vcs/types.d.ts +15 -1
  50. package/dist/vcs/types.d.ts.map +1 -1
  51. package/dist/watcher/ingestion.d.ts +1 -0
  52. package/dist/watcher/ingestion.d.ts.map +1 -1
  53. package/package.json +1 -1
@@ -1,6 +1,8 @@
1
1
  // @bun
2
2
  import {
3
3
  BlobStore,
4
+ JsonOpLog,
5
+ LaneOpLog,
4
6
  addCriterion,
5
7
  assignIssue,
6
8
  blockIssue,
@@ -10,12 +12,14 @@ import {
10
12
  createBranch,
11
13
  createCheckpoint,
12
14
  createIssue,
15
+ createLaneMeta,
13
16
  createMilestone,
14
17
  decompose,
15
18
  deleteBranch,
16
19
  diffFileStates,
17
20
  diffOpRange,
18
21
  getActiveIssues,
22
+ getBranchHeadOpHash,
19
23
  getIssue,
20
24
  init_blob_store,
21
25
  init_branch,
@@ -23,39 +27,50 @@ import {
23
27
  init_decompose,
24
28
  init_diff,
25
29
  init_issue,
30
+ init_lane,
26
31
  init_merge,
27
32
  init_milestone,
33
+ init_op_log,
34
+ laneDir,
28
35
  listBranches,
29
36
  listCheckpoints,
30
37
  listIssues,
38
+ listLaneMetas,
31
39
  listMilestones,
32
40
  loadBranchState,
41
+ loadLaneMeta,
33
42
  pauseIssue,
34
43
  reopenIssue,
44
+ resolveLaneHeadFromJournal,
35
45
  resumeIssue,
36
46
  runCriteria,
37
47
  saveBranchState,
48
+ saveLaneMeta,
38
49
  setCriterionStatus,
50
+ shouldAdvanceBranchHead,
39
51
  startIssue,
40
52
  switchBranch,
41
53
  threeWayMerge,
54
+ threeWayTextMerge,
42
55
  triageIssue,
43
56
  unblockIssue,
44
- updateIssue
45
- } from "./index-p3nnc7ac.js";
57
+ updateIssue,
58
+ updateLaneHead
59
+ } from "./index-xhcp6xrn.js";
46
60
  import {
47
61
  getDecision,
48
62
  getDecisionChain,
49
63
  init_decisions,
50
64
  queryDecisions,
51
65
  recordDecision
52
- } from "./index-65z0xfjw.js";
66
+ } from "./index-4cdr7x2x.js";
53
67
  import {
54
68
  DEFAULT_CONFIG,
55
69
  createVcsOp,
56
70
  init_ops,
57
- init_types
58
- } from "./index-v9b4hqa7.js";
71
+ init_types,
72
+ isVcsOpKind
73
+ } from "./index-nzb3am4f.js";
59
74
  import {
60
75
  EAVStore,
61
76
  init_eav_store
@@ -308,6 +323,9 @@ class Ingestion {
308
323
  getLastOpHash() {
309
324
  return this.lastOpHash;
310
325
  }
326
+ setLastOpHash(hash) {
327
+ this.lastOpHash = hash;
328
+ }
311
329
  }
312
330
  var EXT_LANGUAGE;
313
331
  var init_ingestion = __esm(() => {
@@ -5333,171 +5351,443 @@ var init_semantic = __esm(() => {
5333
5351
  init_semantic_merge();
5334
5352
  });
5335
5353
 
5336
- // src/engine.ts
5337
- import {
5338
- existsSync as existsSync4,
5339
- mkdirSync as mkdirSync3,
5340
- readFileSync as readFileSync4,
5341
- writeFileSync as writeFileSync3,
5342
- copyFileSync,
5343
- openSync,
5344
- closeSync,
5345
- unlinkSync,
5346
- renameSync
5347
- } from "fs";
5348
- import { readFile as readFile2 } from "fs/promises";
5349
- import { join as join5, dirname } from "path";
5350
-
5351
- class JsonOpLog {
5352
- ops = [];
5353
- filePath;
5354
- lockPath;
5355
- constructor(filePath) {
5356
- this.filePath = filePath;
5357
- this.lockPath = `${filePath}.lock`;
5358
- }
5359
- load() {
5360
- if (existsSync4(this.filePath)) {
5361
- const raw = readFileSync4(this.filePath, "utf-8");
5362
- try {
5363
- this.ops = JSON.parse(raw);
5364
- } catch (err) {
5365
- const backupPath = this.filePath + ".bak";
5366
- if (existsSync4(backupPath)) {
5367
- const backupRaw = readFileSync4(backupPath, "utf-8");
5368
- this.ops = JSON.parse(backupRaw);
5369
- writeFileSync3(this.filePath, backupRaw);
5370
- } else {
5371
- throw new Error(`Corrupted ops.json and no backup found. Run \`trellis repair\` to attempt recovery.`);
5372
- }
5373
- }
5374
- }
5375
- }
5376
- append(op) {
5377
- this.withLock(() => {
5378
- const diskOps = this.readOpsFromDisk();
5379
- this.ops = diskOps;
5380
- if (this.ops.some((existing) => existing.hash === op.hash)) {
5381
- return;
5382
- }
5383
- this.ops.push(op);
5384
- this.flush();
5385
- });
5386
- }
5387
- readAll() {
5388
- return [...this.ops];
5354
+ // src/vcs/lane-promote.ts
5355
+ function replayOpIntoStore(store, op) {
5356
+ const d = decompose(op);
5357
+ if (d.deleteFacts.length > 0)
5358
+ store.deleteFacts(d.deleteFacts);
5359
+ if (d.deleteLinks.length > 0)
5360
+ store.deleteLinks(d.deleteLinks);
5361
+ if (d.addFacts.length > 0)
5362
+ store.addFacts(d.addFacts);
5363
+ if (d.addLinks.length > 0)
5364
+ store.addLinks(d.addLinks);
5365
+ }
5366
+ function resolveBranchHeadFromOps(ops, branchName) {
5367
+ for (let i = ops.length - 1;i >= 0; i--) {
5368
+ const op = ops[i];
5369
+ if (op.kind === "vcs:branchAdvance" && op.vcs?.branchName === branchName && op.vcs.targetOpHash) {
5370
+ return op.vcs.targetOpHash;
5371
+ }
5372
+ }
5373
+ return ops[ops.length - 1]?.hash;
5374
+ }
5375
+ function buildStoreUpTo(ops, atOpHash) {
5376
+ const store = new EAVStore;
5377
+ for (const op of ops) {
5378
+ replayOpIntoStore(store, op);
5379
+ if (atOpHash && op.hash === atOpHash)
5380
+ break;
5389
5381
  }
5390
- getLastOp() {
5391
- return this.ops.length > 0 ? this.ops[this.ops.length - 1] : undefined;
5382
+ return store;
5383
+ }
5384
+ function getFactValue(store, entity, attribute) {
5385
+ const facts = store.getFactsByEntity(entity).filter((f) => f.a === attribute);
5386
+ return facts[facts.length - 1]?.v;
5387
+ }
5388
+ function entityAttributes(store, entity) {
5389
+ const map = new Map;
5390
+ for (const fact of store.getFactsByEntity(entity)) {
5391
+ map.set(fact.a, fact.v);
5392
5392
  }
5393
- count() {
5394
- return this.ops.length;
5393
+ return map;
5394
+ }
5395
+ function collectTouchedAttributes(op) {
5396
+ const d = decompose(op);
5397
+ const map = new Map;
5398
+ const touch = (fact) => {
5399
+ if (!map.has(fact.e))
5400
+ map.set(fact.e, new Set);
5401
+ map.get(fact.e).add(fact.a);
5402
+ };
5403
+ for (const f of d.addFacts)
5404
+ touch(f);
5405
+ for (const f of d.deleteFacts)
5406
+ touch(f);
5407
+ return map;
5408
+ }
5409
+ function collectTouchedEntities(op) {
5410
+ const d = decompose(op);
5411
+ const entities = new Set;
5412
+ for (const f of [...d.addFacts, ...d.deleteFacts])
5413
+ entities.add(f.e);
5414
+ for (const l of [...d.addLinks, ...d.deleteLinks]) {
5415
+ entities.add(l.e1);
5416
+ entities.add(l.e2);
5395
5417
  }
5396
- flush() {
5397
- const dir = dirname(this.filePath);
5398
- if (!existsSync4(dir))
5399
- mkdirSync3(dir, { recursive: true });
5400
- if (existsSync4(this.filePath)) {
5401
- const backupPath = this.filePath + ".bak";
5402
- try {
5403
- copyFileSync(this.filePath, backupPath);
5404
- } catch {}
5418
+ return entities;
5419
+ }
5420
+ function atomsEqual(a, b) {
5421
+ if (a === b)
5422
+ return true;
5423
+ if (a === undefined || b === undefined)
5424
+ return false;
5425
+ return String(a) === String(b);
5426
+ }
5427
+ function buildLaneFileState(laneOps, throughIndex) {
5428
+ const slice = laneOps.slice(0, throughIndex + 1);
5429
+ return buildFileStateAtOp(slice);
5430
+ }
5431
+ function filePathsFromOp(op) {
5432
+ const paths = [];
5433
+ if (op.vcs?.filePath)
5434
+ paths.push(op.vcs.filePath);
5435
+ if (op.vcs?.oldFilePath)
5436
+ paths.push(op.vcs.oldFilePath);
5437
+ return paths;
5438
+ }
5439
+ function detectEntityConflicts(op, baseStore, headStore) {
5440
+ const conflicts = [];
5441
+ const d = decompose(op);
5442
+ const touchedEntities = collectTouchedEntities(op);
5443
+ const laneAttrsByEntity = collectTouchedAttributes(op);
5444
+ for (const fact of d.addFacts) {
5445
+ const headValue = getFactValue(headStore, fact.e, fact.a);
5446
+ const baseValue = getFactValue(baseStore, fact.e, fact.a);
5447
+ if (atomsEqual(headValue, fact.v))
5448
+ continue;
5449
+ if (!atomsEqual(headValue, baseValue) && headValue !== undefined) {
5450
+ conflicts.push({
5451
+ class: "hard",
5452
+ laneOpHash: op.hash,
5453
+ entityId: fact.e,
5454
+ attribute: fact.a,
5455
+ integrationValue: headValue,
5456
+ laneValue: fact.v,
5457
+ suggestion: "manual",
5458
+ message: `Integration changed ${fact.e}.${fact.a} since fork`
5459
+ });
5405
5460
  }
5406
- const tempPath = `${this.filePath}.tmp`;
5407
- writeFileSync3(tempPath, JSON.stringify(this.ops, null, 2));
5408
- renameSync(tempPath, this.filePath);
5409
5461
  }
5410
- readOpsFromDisk() {
5411
- if (!existsSync4(this.filePath)) {
5412
- return [];
5413
- }
5414
- const raw = readFileSync4(this.filePath, "utf-8");
5415
- try {
5416
- const parsed = JSON.parse(raw);
5417
- return Array.isArray(parsed) ? parsed : [];
5418
- } catch {
5419
- const backupPath = this.filePath + ".bak";
5420
- if (existsSync4(backupPath)) {
5421
- const backupRaw = readFileSync4(backupPath, "utf-8");
5422
- const parsedBackup = JSON.parse(backupRaw);
5423
- writeFileSync3(this.filePath, backupRaw);
5424
- return Array.isArray(parsedBackup) ? parsedBackup : [];
5462
+ for (const entityId of touchedEntities) {
5463
+ const laneAttrs = laneAttrsByEntity.get(entityId) ?? new Set;
5464
+ if (laneAttrs.size === 0)
5465
+ continue;
5466
+ const baseAttrs = entityAttributes(baseStore, entityId);
5467
+ const headAttrs = entityAttributes(headStore, entityId);
5468
+ for (const [attribute, headValue] of headAttrs) {
5469
+ if (laneAttrs.has(attribute))
5470
+ continue;
5471
+ const baseValue = baseAttrs.get(attribute);
5472
+ if (!atomsEqual(headValue, baseValue)) {
5473
+ conflicts.push({
5474
+ class: "soft",
5475
+ laneOpHash: op.hash,
5476
+ entityId,
5477
+ attribute,
5478
+ integrationValue: headValue,
5479
+ suggestion: "manual",
5480
+ message: `Integration updated ${entityId}.${attribute} while lane touched the same entity`
5481
+ });
5425
5482
  }
5426
- throw new Error(`Corrupted ops.json and no backup found. Run \`trellis repair\` to attempt recovery.`);
5427
5483
  }
5428
5484
  }
5429
- withLock(fn) {
5430
- const dir = dirname(this.filePath);
5431
- if (!existsSync4(dir))
5432
- mkdirSync3(dir, { recursive: true });
5433
- const deadline = Date.now() + 5000;
5434
- let lockFd;
5435
- while (Date.now() < deadline) {
5436
- try {
5437
- lockFd = openSync(this.lockPath, "wx");
5438
- break;
5439
- } catch (err) {
5440
- if (err?.code !== "EEXIST") {
5441
- throw err;
5442
- }
5485
+ return conflicts;
5486
+ }
5487
+ function blobText(blobStore, hash) {
5488
+ if (!hash || !blobStore)
5489
+ return;
5490
+ const content = blobStore.get(hash);
5491
+ if (!content)
5492
+ return;
5493
+ const text = content.toString("utf-8");
5494
+ return text.endsWith(`
5495
+ `) ? text.slice(0, -1) : text;
5496
+ }
5497
+ function mergeLaneFile(path, base, ours, theirs, blobStore) {
5498
+ const b = base.get(path);
5499
+ const o = ours.get(path);
5500
+ const t = theirs.get(path);
5501
+ const baseHash = b && !b.deleted ? b.contentHash : undefined;
5502
+ const oursHash = o && !o.deleted ? o.contentHash : undefined;
5503
+ const theirsHash = t && !t.deleted ? t.contentHash : undefined;
5504
+ if (oursHash === theirsHash)
5505
+ return { clean: true };
5506
+ if (theirsHash === baseHash && oursHash !== baseHash)
5507
+ return { clean: true };
5508
+ if (oursHash === baseHash && theirsHash !== baseHash) {
5509
+ const theirsContent2 = blobText(blobStore, theirsHash);
5510
+ return { clean: true, merged: theirsContent2 };
5511
+ }
5512
+ const baseContent = blobText(blobStore, baseHash) ?? "";
5513
+ const oursContent = blobText(blobStore, oursHash);
5514
+ const theirsContent = blobText(blobStore, theirsHash);
5515
+ if (oursContent === undefined || theirsContent === undefined) {
5516
+ return { clean: false, conflictKind: "modify-modify" };
5517
+ }
5518
+ const textResult = threeWayTextMerge(baseContent, oursContent, theirsContent);
5519
+ if (textResult.clean) {
5520
+ return { clean: true, merged: textResult.merged };
5521
+ }
5522
+ return { clean: false, conflictKind: "modify-modify" };
5523
+ }
5524
+ function detectFileConflict(op, integrationOps, laneOps, laneOpIndex, baseOpHash, snapshotHead, blobStore) {
5525
+ const paths = filePathsFromOp(op);
5526
+ if (paths.length === 0)
5527
+ return {};
5528
+ const base = buildFileStateAtOp(integrationOps, baseOpHash);
5529
+ const ours = buildFileStateAtOp(integrationOps, snapshotHead);
5530
+ const theirs = buildLaneFileState(laneOps, laneOpIndex);
5531
+ for (const path of paths) {
5532
+ const baseState = base.get(path);
5533
+ const headState = ours.get(path);
5534
+ const laneState = theirs.get(path);
5535
+ const baseHash = baseState && !baseState.deleted ? baseState.contentHash : undefined;
5536
+ const headHash = headState && !headState.deleted ? headState.contentHash : undefined;
5537
+ const laneHash = laneState && !laneState.deleted ? laneState.contentHash : undefined;
5538
+ if (headHash === laneHash)
5539
+ continue;
5540
+ if (headHash === baseHash && laneHash !== baseHash)
5541
+ continue;
5542
+ if (laneHash === baseHash && headHash !== baseHash)
5543
+ continue;
5544
+ const mergeResult = mergeLaneFile(path, base, ours, theirs, blobStore);
5545
+ if (mergeResult.clean) {
5546
+ if (mergeResult.merged !== undefined) {
5547
+ return { mergedContent: mergeResult.merged };
5443
5548
  }
5549
+ continue;
5444
5550
  }
5445
- if (lockFd === undefined) {
5446
- throw new Error(`Timed out waiting for ops log lock: ${this.lockPath}. Another Trellis process may be stalled.`);
5551
+ return {
5552
+ conflict: {
5553
+ class: "file",
5554
+ laneOpHash: op.hash,
5555
+ filePath: path,
5556
+ suggestion: "manual",
5557
+ message: `File conflict on ${path} (${mergeResult.conflictKind ?? "modify-modify"})`
5558
+ }
5559
+ };
5560
+ }
5561
+ return {};
5562
+ }
5563
+ function isBlockingConflict(conflict) {
5564
+ return conflict.class === "soft" || conflict.class === "hard" || conflict.class === "file";
5565
+ }
5566
+ async function planLanePromote(params) {
5567
+ const {
5568
+ laneId,
5569
+ meta,
5570
+ targetBranch,
5571
+ snapshotHead,
5572
+ integrationOps,
5573
+ laneOps,
5574
+ parentLaneOps,
5575
+ blobStore
5576
+ } = params;
5577
+ const baseStore = buildStoreUpTo(integrationOps, meta.baseOpHash);
5578
+ const headStore = buildStoreUpTo(integrationOps, snapshotHead);
5579
+ const fileLaneOps = meta.forkKind === "child" && parentLaneOps?.length ? [...parentLaneOps, ...laneOps] : laneOps;
5580
+ const childOpOffset = parentLaneOps?.length ?? 0;
5581
+ const opsToReplay = [];
5582
+ const conflicts = [];
5583
+ let safeOpCount = 0;
5584
+ for (let i = 0;i < laneOps.length; i++) {
5585
+ const op = laneOps[i];
5586
+ if (SKIP_PROMOTE_KINDS.has(op.kind))
5587
+ continue;
5588
+ if (FILE_OP_KINDS2.has(op.kind)) {
5589
+ const fileResult = detectFileConflict(op, integrationOps, fileLaneOps, childOpOffset + i, meta.baseOpHash, snapshotHead, blobStore);
5590
+ if (fileResult.conflict) {
5591
+ conflicts.push(fileResult.conflict);
5592
+ continue;
5593
+ }
5594
+ opsToReplay.push({
5595
+ sourceOp: op,
5596
+ mergedContent: fileResult.mergedContent
5597
+ });
5598
+ safeOpCount++;
5599
+ continue;
5447
5600
  }
5448
- try {
5449
- fn();
5450
- } finally {
5451
- closeSync(lockFd);
5452
- try {
5453
- unlinkSync(this.lockPath);
5454
- } catch {}
5601
+ const decomposed = decompose(op);
5602
+ if (decomposed.addFacts.length > 0 && decomposed.deleteFacts.length === 0 && decomposed.addFacts.every((fact) => atomsEqual(getFactValue(headStore, fact.e, fact.a), fact.v))) {
5603
+ continue;
5455
5604
  }
5605
+ const entityConflicts = detectEntityConflicts(op, baseStore, headStore);
5606
+ const blockingEntity = entityConflicts.filter(isBlockingConflict);
5607
+ conflicts.push(...entityConflicts);
5608
+ if (blockingEntity.length > 0)
5609
+ continue;
5610
+ opsToReplay.push({ sourceOp: op });
5611
+ safeOpCount++;
5612
+ replayOpIntoStore(headStore, op);
5613
+ }
5614
+ const blockingConflicts = conflicts.filter(isBlockingConflict);
5615
+ return {
5616
+ laneId,
5617
+ targetBranch,
5618
+ snapshotHead,
5619
+ baseOpHash: meta.baseOpHash,
5620
+ opsToReplay,
5621
+ conflicts,
5622
+ blockingConflicts,
5623
+ safeOpCount,
5624
+ canPromote: blockingConflicts.length === 0 && opsToReplay.length > 0
5625
+ };
5626
+ }
5627
+ async function rechainOpForIntegration(op, previousHash) {
5628
+ if (!isVcsOpKindSafe(op.kind)) {
5629
+ throw new Error(`Cannot rechain op kind '${op.kind}' for integration replay`);
5456
5630
  }
5457
- static repair(filePath) {
5458
- if (!existsSync4(filePath)) {
5459
- return { recovered: 0, lost: 0 };
5631
+ return createVcsOp(op.kind, {
5632
+ agentId: op.agentId,
5633
+ previousHash,
5634
+ vcs: { ...op.vcs }
5635
+ });
5636
+ }
5637
+ function isVcsOpKindSafe(kind) {
5638
+ return kind.startsWith("vcs:");
5639
+ }
5640
+ function formatPromoteExplain(plan) {
5641
+ const lines = [
5642
+ `Lane promote plan: ${plan.laneId}`,
5643
+ ` Target branch: ${plan.targetBranch}`,
5644
+ ` Fork base: ${plan.baseOpHash}`,
5645
+ ` Snapshot head: ${plan.snapshotHead}`,
5646
+ ` Ops to replay: ${plan.opsToReplay.length}`,
5647
+ ` Safe ops: ${plan.safeOpCount}`
5648
+ ];
5649
+ if (plan.blockingConflicts.length === 0) {
5650
+ lines.push("", plan.canPromote ? "\u2713 Ready to promote" : "No ops to replay");
5651
+ return lines.join(`
5652
+ `);
5653
+ }
5654
+ lines.push("", `Blocking conflicts (${plan.blockingConflicts.length}):`);
5655
+ for (const c of plan.blockingConflicts) {
5656
+ const where = c.filePath ?? (c.entityId && c.attribute ? `${c.entityId}.${c.attribute}` : c.entityId);
5657
+ lines.push(` [${c.class}] ${where ?? c.laneOpHash.slice(0, 24)}`);
5658
+ if (c.message)
5659
+ lines.push(` ${c.message}`);
5660
+ if (c.integrationValue !== undefined) {
5661
+ lines.push(` integration: ${String(c.integrationValue)}`);
5460
5662
  }
5461
- const raw = readFileSync4(filePath, "utf-8");
5462
- try {
5463
- const ops = JSON.parse(raw);
5464
- return { recovered: ops.length, lost: 0 };
5465
- } catch {}
5466
- const lastHash = raw.lastIndexOf('"hash": "trellis:op:');
5467
- if (lastHash === -1) {
5468
- const bakPath = filePath + ".bak";
5469
- if (existsSync4(bakPath)) {
5470
- const bakRaw = readFileSync4(bakPath, "utf-8");
5471
- try {
5472
- const ops = JSON.parse(bakRaw);
5473
- writeFileSync3(filePath, bakRaw);
5474
- return { recovered: ops.length, lost: 0 };
5475
- } catch {}
5663
+ if (c.laneValue !== undefined) {
5664
+ lines.push(` lane: ${String(c.laneValue)}`);
5665
+ }
5666
+ }
5667
+ return lines.join(`
5668
+ `);
5669
+ }
5670
+ var SKIP_PROMOTE_KINDS, FILE_OP_KINDS2;
5671
+ var init_lane_promote = __esm(() => {
5672
+ init_eav_store();
5673
+ init_decompose();
5674
+ init_diff();
5675
+ init_merge();
5676
+ init_ops();
5677
+ SKIP_PROMOTE_KINDS = new Set([
5678
+ "vcs:branchAdvance",
5679
+ "vcs:laneCreate",
5680
+ "vcs:laneDrop",
5681
+ "vcs:lanePromoteStart",
5682
+ "vcs:lanePromoteComplete",
5683
+ "vcs:lanePromoteAbort"
5684
+ ]);
5685
+ FILE_OP_KINDS2 = new Set([
5686
+ "vcs:fileAdd",
5687
+ "vcs:fileModify",
5688
+ "vcs:fileDelete",
5689
+ "vcs:fileRename"
5690
+ ]);
5691
+ });
5692
+
5693
+ // src/vcs/lane-materialize.ts
5694
+ function emptyMaterializationStats() {
5695
+ return {
5696
+ integrationOpsReplayed: 0,
5697
+ laneOpsReplayed: 0,
5698
+ integrationCacheHit: false
5699
+ };
5700
+ }
5701
+ function replayOpIntoStore2(store, op) {
5702
+ const d = decompose(op);
5703
+ if (d.deleteFacts.length > 0)
5704
+ store.deleteFacts(d.deleteFacts);
5705
+ if (d.deleteLinks.length > 0)
5706
+ store.deleteLinks(d.deleteLinks);
5707
+ if (d.addFacts.length > 0)
5708
+ store.addFacts(d.addFacts);
5709
+ if (d.addLinks.length > 0)
5710
+ store.addLinks(d.addLinks);
5711
+ }
5712
+ function cloneStore(source) {
5713
+ const clone = new EAVStore;
5714
+ clone.restore(source.snapshot());
5715
+ return clone;
5716
+ }
5717
+ function materializeIntegrationOps(ops, cache, tailHash) {
5718
+ if (cache && cache.tailHash === tailHash) {
5719
+ return {
5720
+ store: cache.store,
5721
+ cache,
5722
+ stats: {
5723
+ integrationOpsReplayed: 0,
5724
+ laneOpsReplayed: 0,
5725
+ integrationCacheHit: true,
5726
+ integrationTailHash: tailHash
5476
5727
  }
5477
- writeFileSync3(filePath, "[]");
5478
- return { recovered: 0, lost: -1 };
5479
- }
5480
- const endOfLine = raw.indexOf(`
5481
- `, lastHash);
5482
- const closingBrace = raw.indexOf(" }", endOfLine);
5483
- if (closingBrace === -1) {
5484
- writeFileSync3(filePath, "[]");
5485
- return { recovered: 0, lost: -1 };
5486
- }
5487
- const fixed = raw.slice(0, closingBrace + 3) + `
5488
- ]`;
5489
- try {
5490
- const ops = JSON.parse(fixed);
5491
- writeFileSync3(filePath + ".corrupted", raw);
5492
- writeFileSync3(filePath, fixed);
5493
- return { recovered: ops.length, lost: 0 };
5494
- } catch {
5495
- writeFileSync3(filePath + ".corrupted", raw);
5496
- writeFileSync3(filePath, "[]");
5497
- return { recovered: 0, lost: -1 };
5728
+ };
5729
+ }
5730
+ const store = new EAVStore;
5731
+ for (const op of ops) {
5732
+ replayOpIntoStore2(store, op);
5733
+ }
5734
+ return {
5735
+ store,
5736
+ cache: { tailHash, store },
5737
+ stats: {
5738
+ integrationOpsReplayed: ops.length,
5739
+ laneOpsReplayed: 0,
5740
+ integrationCacheHit: false,
5741
+ integrationTailHash: tailHash
5498
5742
  }
5743
+ };
5744
+ }
5745
+ function overlayLaneOps(integrationStore, laneOps) {
5746
+ const store = cloneStore(integrationStore);
5747
+ for (const op of laneOps) {
5748
+ replayOpIntoStore2(store, op);
5499
5749
  }
5750
+ return { store, laneOpsReplayed: laneOps.length };
5500
5751
  }
5752
+ function materializeChildForkEntry(integrationOps, baseOpHash, parentLaneOps, childLaneOps) {
5753
+ const store = new EAVStore;
5754
+ let integrationReplayed = 0;
5755
+ for (const op of integrationOps) {
5756
+ replayOpIntoStore2(store, op);
5757
+ integrationReplayed++;
5758
+ if (op.hash === baseOpHash)
5759
+ break;
5760
+ }
5761
+ for (const op of parentLaneOps) {
5762
+ replayOpIntoStore2(store, op);
5763
+ }
5764
+ for (const op of childLaneOps) {
5765
+ replayOpIntoStore2(store, op);
5766
+ }
5767
+ return {
5768
+ store,
5769
+ stats: {
5770
+ integrationOpsReplayed: integrationReplayed,
5771
+ laneOpsReplayed: parentLaneOps.length + childLaneOps.length,
5772
+ integrationCacheHit: false,
5773
+ integrationTailHash: integrationOps[integrationOps.length - 1]?.hash
5774
+ }
5775
+ };
5776
+ }
5777
+ var init_lane_materialize = __esm(() => {
5778
+ init_eav_store();
5779
+ init_decompose();
5780
+ });
5781
+
5782
+ // src/engine.ts
5783
+ import {
5784
+ existsSync as existsSync4,
5785
+ mkdirSync as mkdirSync3,
5786
+ readFileSync as readFileSync4,
5787
+ writeFileSync as writeFileSync3
5788
+ } from "fs";
5789
+ import { readFile as readFile2 } from "fs/promises";
5790
+ import { join as join5 } from "path";
5501
5791
  function parseIgnoreFile(filePath) {
5502
5792
  if (!existsSync4(filePath))
5503
5793
  return [];
@@ -5551,6 +5841,10 @@ class TrellisVcsEngine {
5551
5841
  checkpointThreshold = 100;
5552
5842
  _pendingAutoCheckpoint = false;
5553
5843
  _blobStore = null;
5844
+ activeLaneId;
5845
+ activeLaneLog = null;
5846
+ integrationCache = null;
5847
+ materializationStats = emptyMaterializationStats();
5554
5848
  constructor(opts) {
5555
5849
  const gitignorePatterns = readIgnorePatterns(opts.rootPath);
5556
5850
  const mergedIgnore = [
@@ -5595,7 +5889,7 @@ class TrellisVcsEngine {
5595
5889
  branchName: this.config.defaultBranch
5596
5890
  }
5597
5891
  });
5598
- this.applyOp(branchOp);
5892
+ await this.applyOp(branchOp);
5599
5893
  const scanner = new FileWatcher({
5600
5894
  rootPath: this.config.rootPath,
5601
5895
  ignorePatterns: [...this.config.ignorePatterns, ".trellis"],
@@ -5639,7 +5933,7 @@ class TrellisVcsEngine {
5639
5933
  size: event.size
5640
5934
  }
5641
5935
  });
5642
- this.applyOp(op);
5936
+ await this.applyOp(op);
5643
5937
  opsCreated++;
5644
5938
  const scannedFiles = opsCreated - 1;
5645
5939
  if (scannedFiles % 25 === 0 || scannedFiles === events.length) {
@@ -5688,16 +5982,19 @@ class TrellisVcsEngine {
5688
5982
  this.config.defaultBranch = persisted.defaultBranch;
5689
5983
  }
5690
5984
  this.loadCurrentBranch();
5691
- const ops = this.opLog.readAll();
5692
- for (const op of ops) {
5693
- this.replayOp(op);
5694
- }
5695
- return { opsReplayed: ops.length };
5985
+ const integrationOps = this.opLog.readAll();
5986
+ const laneOps = this.activeLaneLog ? this.activeLaneLog.readAll() : undefined;
5987
+ const activeMeta = this.activeLaneId ? this.getLaneMeta(this.activeLaneId) : undefined;
5988
+ this.refreshMaterializedStore(integrationOps, laneOps, activeMeta);
5989
+ const laneReplayed = this.materializationStats.laneOpsReplayed;
5990
+ return {
5991
+ opsReplayed: this.materializationStats.integrationOpsReplayed + laneReplayed
5992
+ };
5696
5993
  }
5697
5994
  watch() {
5698
5995
  this.ingestion = new Ingestion({
5699
5996
  agentId: this.agentId,
5700
- lastOpHash: this.opLog.getLastOp()?.hash,
5997
+ lastOpHash: this.getActiveJournal().getLastOp()?.hash,
5701
5998
  onOp: (op) => this.applyOp(op)
5702
5999
  });
5703
6000
  this.watcher = new FileWatcher({
@@ -5802,7 +6099,11 @@ class TrellisVcsEngine {
5802
6099
  switchBranch(name) {
5803
6100
  switchBranch(this._ctx(), name);
5804
6101
  this.currentBranch = name;
5805
- saveBranchState(this.config.rootPath, { currentBranch: name });
6102
+ const state = loadBranchState(this.config.rootPath);
6103
+ saveBranchState(this.config.rootPath, {
6104
+ ...state,
6105
+ currentBranch: name
6106
+ });
5806
6107
  }
5807
6108
  listBranches() {
5808
6109
  return listBranches(this._ctx(), this.currentBranch);
@@ -5815,6 +6116,344 @@ class TrellisVcsEngine {
5815
6116
  getCurrentBranch() {
5816
6117
  return this.currentBranch;
5817
6118
  }
6119
+ getBranchHeadOpHash(branchName = this.currentBranch) {
6120
+ return getBranchHeadOpHash(this._ctx(), branchName);
6121
+ }
6122
+ getActiveLaneId() {
6123
+ return this.activeLaneId;
6124
+ }
6125
+ getMaterializationStats() {
6126
+ return { ...this.materializationStats };
6127
+ }
6128
+ listLanes() {
6129
+ return listLaneMetas(this.trellisDir());
6130
+ }
6131
+ getIntegrationOpCount() {
6132
+ return this.opLog.count();
6133
+ }
6134
+ getLaneOpCount(laneId) {
6135
+ const log = new LaneOpLog(laneDir(this.trellisDir(), laneId));
6136
+ log.load();
6137
+ return log.count();
6138
+ }
6139
+ getLaneMeta(laneId) {
6140
+ return loadLaneMeta(this.trellisDir(), laneId);
6141
+ }
6142
+ findLaneForIssue(issueId) {
6143
+ const normalized = issueId.startsWith("issue:") ? issueId : `issue:${issueId}`;
6144
+ return this.listLanes().find((lane) => lane.issueId === normalized && lane.status === "active");
6145
+ }
6146
+ async syncEnvLaneFromEnv() {
6147
+ const laneId = process.env.TRELLIS_LANE_ID?.trim();
6148
+ if (!laneId)
6149
+ return;
6150
+ if (this.activeLaneId === laneId)
6151
+ return;
6152
+ if (this.activeLaneId) {
6153
+ throw new Error(`TRELLIS_LANE_ID=${laneId} conflicts with active lane '${this.activeLaneId}'`);
6154
+ }
6155
+ await this.enterLane(laneId);
6156
+ }
6157
+ summarizeLane(laneId) {
6158
+ const meta = this.getLaneMeta(laneId);
6159
+ if (!meta) {
6160
+ throw new Error(`Lane not found: ${laneId}`);
6161
+ }
6162
+ const log = new LaneOpLog(laneDir(this.trellisDir(), laneId));
6163
+ log.load();
6164
+ const ops = log.readAll();
6165
+ const filePaths = [
6166
+ ...new Set(ops.map((op) => op.vcs?.filePath ?? op.vcs?.oldFilePath).filter((p) => Boolean(p)))
6167
+ ];
6168
+ return {
6169
+ meta,
6170
+ ops,
6171
+ filePaths,
6172
+ integrationHead: this.getBranchHeadOpHash(meta.targetBranch)
6173
+ };
6174
+ }
6175
+ async createLane(opts) {
6176
+ if (this.activeLaneId) {
6177
+ throw new Error(`Cannot create a lane while inside lane '${this.activeLaneId}' \u2014 leave first`);
6178
+ }
6179
+ const baseBranch = opts?.fromBranch ?? this.currentBranch;
6180
+ const baseOpHash = getBranchHeadOpHash(this._ctx(), baseBranch) ?? this.opLog.getLastOp()?.hash;
6181
+ if (!baseOpHash) {
6182
+ throw new Error(`No integration head on branch '${baseBranch}' to fork lane from`);
6183
+ }
6184
+ const meta = createLaneMeta(this.trellisDir(), {
6185
+ baseBranch,
6186
+ baseOpHash,
6187
+ targetBranch: opts?.targetBranch ?? baseBranch,
6188
+ agentId: this.agentId,
6189
+ issueId: opts?.issueId,
6190
+ sessionId: opts?.sessionId,
6191
+ worktreePath: opts?.worktreePath
6192
+ });
6193
+ const op = await createVcsOp("vcs:laneCreate", {
6194
+ agentId: this.agentId,
6195
+ previousHash: this.opLog.getLastOp()?.hash,
6196
+ vcs: {
6197
+ laneId: meta.id,
6198
+ baseBranch: meta.baseBranch,
6199
+ baseOpHash: meta.baseOpHash,
6200
+ targetBranch: meta.targetBranch,
6201
+ issueId: meta.issueId
6202
+ }
6203
+ });
6204
+ await this.applyOp(op);
6205
+ return meta;
6206
+ }
6207
+ async forkLane(parentLaneId, opts) {
6208
+ if (this.activeLaneId) {
6209
+ throw new Error(`Cannot fork a lane while inside lane '${this.activeLaneId}' \u2014 leave first`);
6210
+ }
6211
+ const parent = loadLaneMeta(this.trellisDir(), parentLaneId);
6212
+ if (!parent) {
6213
+ throw new Error(`Lane not found: ${parentLaneId}`);
6214
+ }
6215
+ if (parent.status !== "active") {
6216
+ throw new Error(`Lane '${parentLaneId}' is ${parent.status} \u2014 cannot fork`);
6217
+ }
6218
+ const forkKind = opts?.forkKind ?? "sibling";
6219
+ const forkedAt = new Date().toISOString();
6220
+ const parentLog = new LaneOpLog(laneDir(this.trellisDir(), parentLaneId));
6221
+ parentLog.load();
6222
+ const parentLaneOps = parentLog.readAll();
6223
+ const parentHead = resolveLaneHeadFromJournal(parent, parentLaneOps);
6224
+ if (forkKind === "child") {
6225
+ const meta2 = createLaneMeta(this.trellisDir(), {
6226
+ baseBranch: parent.baseBranch,
6227
+ baseOpHash: parent.baseOpHash,
6228
+ targetBranch: parent.targetBranch,
6229
+ agentId: this.agentId,
6230
+ issueId: opts?.issueId ?? parent.issueId,
6231
+ sessionId: opts?.sessionId,
6232
+ worktreePath: opts?.worktreePath,
6233
+ parentLaneId: parent.id,
6234
+ forkKind: "child",
6235
+ forkedAt,
6236
+ virtualBaseOpHash: parentHead
6237
+ });
6238
+ const op2 = await createVcsOp("vcs:laneCreate", {
6239
+ agentId: this.agentId,
6240
+ previousHash: this.opLog.getLastOp()?.hash,
6241
+ vcs: {
6242
+ laneId: meta2.id,
6243
+ baseBranch: meta2.baseBranch,
6244
+ baseOpHash: meta2.baseOpHash,
6245
+ targetBranch: meta2.targetBranch,
6246
+ issueId: meta2.issueId,
6247
+ sessionId: meta2.sessionId,
6248
+ parentLaneId: parent.id,
6249
+ forkKind: "child",
6250
+ virtualBaseOpHash: parentHead
6251
+ }
6252
+ });
6253
+ await this.applyOp(op2);
6254
+ return meta2;
6255
+ }
6256
+ const meta = createLaneMeta(this.trellisDir(), {
6257
+ baseBranch: parent.baseBranch,
6258
+ baseOpHash: parent.baseOpHash,
6259
+ targetBranch: parent.targetBranch,
6260
+ agentId: this.agentId,
6261
+ issueId: opts?.issueId ?? parent.issueId,
6262
+ sessionId: opts?.sessionId,
6263
+ worktreePath: opts?.worktreePath,
6264
+ parentLaneId: parent.id,
6265
+ forkKind: "sibling",
6266
+ forkedAt
6267
+ });
6268
+ const op = await createVcsOp("vcs:laneCreate", {
6269
+ agentId: this.agentId,
6270
+ previousHash: this.opLog.getLastOp()?.hash,
6271
+ vcs: {
6272
+ laneId: meta.id,
6273
+ baseBranch: meta.baseBranch,
6274
+ baseOpHash: meta.baseOpHash,
6275
+ targetBranch: meta.targetBranch,
6276
+ issueId: meta.issueId,
6277
+ sessionId: meta.sessionId,
6278
+ parentLaneId: parent.id,
6279
+ forkKind: "sibling"
6280
+ }
6281
+ });
6282
+ await this.applyOp(op);
6283
+ return meta;
6284
+ }
6285
+ async enterLane(laneId) {
6286
+ if (this.activeLaneId) {
6287
+ throw new Error(`Already in lane '${this.activeLaneId}' \u2014 leave before entering another`);
6288
+ }
6289
+ const meta = loadLaneMeta(this.trellisDir(), laneId);
6290
+ if (!meta) {
6291
+ throw new Error(`Lane not found: ${laneId}`);
6292
+ }
6293
+ if (meta.status !== "active") {
6294
+ throw new Error(`Lane '${laneId}' is ${meta.status} \u2014 cannot enter`);
6295
+ }
6296
+ this.activeLaneId = laneId;
6297
+ this.activeLaneLog = new LaneOpLog(laneDir(this.trellisDir(), laneId));
6298
+ this.activeLaneLog.load();
6299
+ this.refreshMaterializedStore(this.opLog.readAll(), this.activeLaneLog.readAll(), meta);
6300
+ saveBranchState(this.config.rootPath, {
6301
+ currentBranch: this.currentBranch,
6302
+ activeLaneId: laneId
6303
+ });
6304
+ this.syncIngestionLastOpHash();
6305
+ return meta;
6306
+ }
6307
+ async leaveLane() {
6308
+ if (!this.activeLaneId)
6309
+ return;
6310
+ this.activeLaneId = undefined;
6311
+ this.activeLaneLog = null;
6312
+ saveBranchState(this.config.rootPath, {
6313
+ currentBranch: this.currentBranch
6314
+ });
6315
+ this.restoreIntegrationOnlyStore();
6316
+ this.syncIngestionLastOpHash();
6317
+ }
6318
+ async dropLane(laneId) {
6319
+ if (this.activeLaneId === laneId) {
6320
+ await this.leaveLane();
6321
+ }
6322
+ const meta = loadLaneMeta(this.trellisDir(), laneId);
6323
+ if (!meta) {
6324
+ throw new Error(`Lane not found: ${laneId}`);
6325
+ }
6326
+ if (meta.status === "dropped")
6327
+ return;
6328
+ meta.status = "dropped";
6329
+ meta.updatedAt = new Date().toISOString();
6330
+ saveLaneMeta(this.trellisDir(), meta);
6331
+ const op = await createVcsOp("vcs:laneDrop", {
6332
+ agentId: this.agentId,
6333
+ previousHash: this.opLog.getLastOp()?.hash,
6334
+ vcs: {
6335
+ laneId: meta.id,
6336
+ laneStatus: "dropped"
6337
+ }
6338
+ });
6339
+ await this.applyOp(op);
6340
+ }
6341
+ async promoteLane(laneId, opts) {
6342
+ const meta = this.getLaneMeta(laneId);
6343
+ if (!meta) {
6344
+ throw new Error(`Lane not found: ${laneId}`);
6345
+ }
6346
+ if (meta.status !== "active") {
6347
+ throw new Error(`Lane '${laneId}' is ${meta.status} \u2014 cannot promote`);
6348
+ }
6349
+ if (this.activeLaneId === laneId) {
6350
+ await this.leaveLane();
6351
+ } else if (this.activeLaneId) {
6352
+ throw new Error(`Cannot promote while inside lane '${this.activeLaneId}' \u2014 leave first`);
6353
+ }
6354
+ const targetBranch = opts?.toBranch ?? meta.targetBranch;
6355
+ const integrationOps = this.opLog.readAll();
6356
+ const snapshotHead = resolveBranchHeadFromOps(integrationOps, targetBranch) ?? getBranchHeadOpHash(this._ctx(), targetBranch);
6357
+ if (!snapshotHead) {
6358
+ throw new Error(`No head on branch '${targetBranch}' to promote onto`);
6359
+ }
6360
+ const laneLog = new LaneOpLog(laneDir(this.trellisDir(), laneId));
6361
+ laneLog.load();
6362
+ const laneOps = laneLog.readAll();
6363
+ let parentLaneOps;
6364
+ if (meta.forkKind === "child" && meta.parentLaneId) {
6365
+ parentLaneOps = this.loadLaneJournalOps(meta.parentLaneId);
6366
+ }
6367
+ const plan = await planLanePromote({
6368
+ laneId,
6369
+ meta,
6370
+ targetBranch,
6371
+ snapshotHead,
6372
+ integrationOps: this.opLog.readAll(),
6373
+ laneOps,
6374
+ parentLaneOps,
6375
+ blobStore: this._blobStore
6376
+ });
6377
+ if (opts?.dryRun || !plan.canPromote) {
6378
+ return { ...plan, promoted: false };
6379
+ }
6380
+ meta.status = "promoting";
6381
+ meta.updatedAt = new Date().toISOString();
6382
+ saveLaneMeta(this.trellisDir(), meta);
6383
+ const startOp = await createVcsOp("vcs:lanePromoteStart", {
6384
+ agentId: this.agentId,
6385
+ previousHash: this.opLog.getLastOp()?.hash,
6386
+ vcs: {
6387
+ laneId,
6388
+ targetBranch,
6389
+ baseOpHash: meta.baseOpHash
6390
+ }
6391
+ });
6392
+ await this.applyOp(startOp, { skipBranchAdvance: true });
6393
+ const currentHead = resolveBranchHeadFromOps(this.opLog.readAll(), targetBranch) ?? getBranchHeadOpHash(this._ctx(), targetBranch);
6394
+ if (currentHead !== snapshotHead) {
6395
+ meta.status = "active";
6396
+ meta.updatedAt = new Date().toISOString();
6397
+ saveLaneMeta(this.trellisDir(), meta);
6398
+ const abortOp = await createVcsOp("vcs:lanePromoteAbort", {
6399
+ agentId: this.agentId,
6400
+ previousHash: this.opLog.getLastOp()?.hash,
6401
+ vcs: { laneId }
6402
+ });
6403
+ await this.applyOp(abortOp, { skipBranchAdvance: true });
6404
+ throw new Error(`Integration head moved during promote \u2014 retry after integration settles`);
6405
+ }
6406
+ let previousHash = this.opLog.getLastOp()?.hash;
6407
+ let lastReplayedHash;
6408
+ let opsAppended = 0;
6409
+ for (const action of plan.opsToReplay) {
6410
+ let opToApply;
6411
+ if (action.mergedContent !== undefined && action.sourceOp.vcs?.filePath) {
6412
+ const contentHash = await this._blobStore.put(Buffer.from(action.mergedContent, "utf-8"));
6413
+ opToApply = await createVcsOp("vcs:fileModify", {
6414
+ agentId: action.sourceOp.agentId,
6415
+ previousHash,
6416
+ vcs: {
6417
+ filePath: action.sourceOp.vcs.filePath,
6418
+ contentHash,
6419
+ laneId: action.sourceOp.vcs.laneId ?? laneId
6420
+ }
6421
+ });
6422
+ } else {
6423
+ opToApply = await rechainOpForIntegration(action.sourceOp, previousHash);
6424
+ }
6425
+ await this.applyOp(opToApply, { skipBranchAdvance: true });
6426
+ previousHash = opToApply.hash;
6427
+ lastReplayedHash = opToApply.hash;
6428
+ opsAppended++;
6429
+ }
6430
+ if (lastReplayedHash) {
6431
+ await this.appendBranchAdvance(lastReplayedHash);
6432
+ }
6433
+ const completeOp = await createVcsOp("vcs:lanePromoteComplete", {
6434
+ agentId: this.agentId,
6435
+ previousHash: this.opLog.getLastOp()?.hash,
6436
+ vcs: {
6437
+ laneId,
6438
+ targetBranch,
6439
+ laneStatus: "promoted"
6440
+ }
6441
+ });
6442
+ await this.applyOp(completeOp, { skipBranchAdvance: true });
6443
+ meta.status = "promoted";
6444
+ meta.headOpHash = lastReplayedHash ?? meta.headOpHash;
6445
+ meta.updatedAt = new Date().toISOString();
6446
+ saveLaneMeta(this.trellisDir(), meta);
6447
+ this.invalidateIntegrationCache();
6448
+ this.refreshMaterializedStore(this.opLog.readAll());
6449
+ this.syncIngestionLastOpHash();
6450
+ return {
6451
+ ...plan,
6452
+ promoted: true,
6453
+ integrationOpsAppended: opsAppended + 2,
6454
+ completeOpHash: completeOp.hash
6455
+ };
6456
+ }
5818
6457
  async createMilestone(message, opts) {
5819
6458
  const op = await createMilestone(this._ctx(), message, opts);
5820
6459
  await this.flushAutoCheckpoint();
@@ -5947,7 +6586,10 @@ class TrellisVcsEngine {
5947
6586
  await this.flushAutoCheckpoint();
5948
6587
  return op;
5949
6588
  }
5950
- async startIssue(id) {
6589
+ async startIssue(id, opts) {
6590
+ if (this.activeLaneId) {
6591
+ await this.leaveLane();
6592
+ }
5951
6593
  const issue = getIssue(this._ctx(), id);
5952
6594
  if (!issue)
5953
6595
  throw new Error(`Issue ${id} not found.`);
@@ -5956,16 +6598,27 @@ class TrellisVcsEngine {
5956
6598
  await this.createBranch(branchName);
5957
6599
  const op = await startIssue(this._ctx(), id, branchName);
5958
6600
  this.switchBranch(branchName);
6601
+ if (opts?.lane !== false) {
6602
+ const issueKey = id.startsWith("issue:") ? id : `issue:${id}`;
6603
+ let lane = this.findLaneForIssue(issueKey);
6604
+ if (!lane) {
6605
+ lane = await this.createLane({ issueId: issueKey });
6606
+ }
6607
+ await this.enterLane(lane.id);
6608
+ }
5959
6609
  await this.flushAutoCheckpoint();
5960
6610
  return op;
5961
6611
  }
5962
6612
  async pauseIssue(id, note) {
6613
+ if (this.activeLaneId) {
6614
+ await this.leaveLane();
6615
+ }
5963
6616
  const op = await pauseIssue(this._ctx(), id, note);
5964
6617
  this.switchBranch(this.config.defaultBranch);
5965
6618
  await this.flushAutoCheckpoint();
5966
6619
  return op;
5967
6620
  }
5968
- async resumeIssue(id) {
6621
+ async resumeIssue(id, opts) {
5969
6622
  const issue = getIssue(this._ctx(), id);
5970
6623
  if (!issue)
5971
6624
  throw new Error(`Issue ${id} not found.`);
@@ -5973,10 +6626,20 @@ class TrellisVcsEngine {
5973
6626
  throw new Error(`Issue ${id} has no tracked branch.`);
5974
6627
  const op = await resumeIssue(this._ctx(), id);
5975
6628
  this.switchBranch(issue.branchName);
6629
+ if (opts?.lane !== false) {
6630
+ const issueKey = id.startsWith("issue:") ? id : `issue:${id}`;
6631
+ const lane = this.findLaneForIssue(issueKey);
6632
+ if (lane) {
6633
+ await this.enterLane(lane.id);
6634
+ }
6635
+ }
5976
6636
  await this.flushAutoCheckpoint();
5977
6637
  return op;
5978
6638
  }
5979
6639
  async closeIssue(id, opts) {
6640
+ if (this.activeLaneId) {
6641
+ await this.leaveLane();
6642
+ }
5980
6643
  const result = await closeIssue(this._ctx(), id, opts);
5981
6644
  if (result.op) {
5982
6645
  await this.flushAutoCheckpoint();
@@ -6051,13 +6714,106 @@ class TrellisVcsEngine {
6051
6714
  return {
6052
6715
  store: this.store,
6053
6716
  agentId: this.agentId,
6054
- readAllOps: () => this.opLog.readAll(),
6055
- getLastOp: () => this.opLog.getLastOp(),
6056
- applyOp: (op) => this.applyOp(op)
6717
+ readAllOps: () => this.getActiveJournal().readAll(),
6718
+ getLastOp: () => this.getActiveJournal().getLastOp(),
6719
+ applyOp: (op, opts) => this.applyOp(op, opts)
6057
6720
  };
6058
6721
  }
6059
- applyOp(op) {
6060
- const decomposed = decompose(op);
6722
+ trellisDir() {
6723
+ return join5(this.config.rootPath, ".trellis");
6724
+ }
6725
+ getActiveJournal() {
6726
+ if (this.activeLaneId && this.activeLaneLog) {
6727
+ return this.activeLaneLog;
6728
+ }
6729
+ return this.opLog;
6730
+ }
6731
+ invalidateIntegrationCache() {
6732
+ this.integrationCache = null;
6733
+ }
6734
+ loadLaneJournalOps(laneId) {
6735
+ const log = new LaneOpLog(laneDir(this.trellisDir(), laneId));
6736
+ log.load();
6737
+ return log.readAll();
6738
+ }
6739
+ refreshMaterializedStore(integrationOps, laneOps, meta) {
6740
+ const laneMeta = meta ?? (this.activeLaneId ? this.getLaneMeta(this.activeLaneId) : undefined);
6741
+ if (laneMeta?.forkKind === "child" && laneMeta.parentLaneId) {
6742
+ const parentLaneOps = this.loadLaneJournalOps(laneMeta.parentLaneId);
6743
+ const { store: store2, stats: stats2 } = materializeChildForkEntry(integrationOps, laneMeta.baseOpHash, parentLaneOps, laneOps ?? []);
6744
+ this.store = store2;
6745
+ this.materializationStats = stats2;
6746
+ return;
6747
+ }
6748
+ const tailHash = integrationOps[integrationOps.length - 1]?.hash;
6749
+ const { store, cache, stats } = materializeIntegrationOps(integrationOps, this.integrationCache, tailHash);
6750
+ this.integrationCache = cache;
6751
+ if (laneOps !== undefined) {
6752
+ const overlay = overlayLaneOps(store, laneOps);
6753
+ this.store = overlay.store;
6754
+ this.materializationStats = {
6755
+ ...stats,
6756
+ laneOpsReplayed: overlay.laneOpsReplayed
6757
+ };
6758
+ return;
6759
+ }
6760
+ this.store = store;
6761
+ this.materializationStats = stats;
6762
+ }
6763
+ restoreIntegrationOnlyStore() {
6764
+ const integrationOps = this.opLog.readAll();
6765
+ const tailHash = integrationOps[integrationOps.length - 1]?.hash;
6766
+ const { store, cache, stats } = materializeIntegrationOps(integrationOps, this.integrationCache, tailHash);
6767
+ this.integrationCache = cache;
6768
+ this.store = store;
6769
+ this.materializationStats = {
6770
+ ...stats,
6771
+ laneOpsReplayed: 0
6772
+ };
6773
+ }
6774
+ rebuildStore(ops) {
6775
+ this.store = new EAVStore;
6776
+ for (const op of ops) {
6777
+ this.replayOp(op);
6778
+ }
6779
+ }
6780
+ syncIngestionLastOpHash() {
6781
+ if (this.ingestion) {
6782
+ this.ingestion.setLastOpHash(this.getActiveJournal().getLastOp()?.hash);
6783
+ }
6784
+ }
6785
+ stampLaneId(op) {
6786
+ if (!this.activeLaneId)
6787
+ return;
6788
+ op.vcs = { ...op.vcs, laneId: this.activeLaneId };
6789
+ }
6790
+ requireActiveLaneLog() {
6791
+ if (!this.activeLaneId || !this.activeLaneLog) {
6792
+ throw new Error("No active lane journal");
6793
+ }
6794
+ return this.activeLaneLog;
6795
+ }
6796
+ isIssueIntegrationOp(kind) {
6797
+ return ISSUE_INTEGRATION_KINDS.has(kind);
6798
+ }
6799
+ async applyOp(op, opts) {
6800
+ const inLane = Boolean(this.activeLaneId);
6801
+ const forceIntegration = Boolean(opts?.allowIntegrationWrite) || inLane && this.isIssueIntegrationOp(op.kind);
6802
+ let opToApply = op;
6803
+ if (inLane && forceIntegration) {
6804
+ const intLast = this.opLog.getLastOp();
6805
+ if (intLast?.hash !== op.previousHash && isVcsOpKind(op.kind)) {
6806
+ opToApply = await createVcsOp(op.kind, {
6807
+ agentId: op.agentId,
6808
+ previousHash: intLast?.hash,
6809
+ vcs: op.vcs ?? {}
6810
+ });
6811
+ }
6812
+ }
6813
+ if (inLane && !forceIntegration) {
6814
+ this.stampLaneId(opToApply);
6815
+ }
6816
+ const decomposed = decompose(opToApply);
6061
6817
  if (decomposed.deleteFacts.length > 0) {
6062
6818
  this.store.deleteFacts(decomposed.deleteFacts);
6063
6819
  }
@@ -6070,13 +6826,55 @@ class TrellisVcsEngine {
6070
6826
  if (decomposed.addLinks.length > 0) {
6071
6827
  this.store.addLinks(decomposed.addLinks);
6072
6828
  }
6073
- this.opLog.append(op);
6074
- if (op.kind !== "vcs:checkpointCreate" && this.checkpointThreshold > 0) {
6829
+ if (inLane && !forceIntegration) {
6830
+ const laneLog = this.requireActiveLaneLog();
6831
+ laneLog.append(opToApply);
6832
+ updateLaneHead(this.trellisDir(), this.activeLaneId, opToApply.hash);
6833
+ if (opToApply.kind !== "vcs:checkpointCreate" && this.checkpointThreshold > 0) {
6834
+ this.checkpointOpCount++;
6835
+ if (this.checkpointOpCount >= this.checkpointThreshold) {
6836
+ this._pendingAutoCheckpoint = true;
6837
+ }
6838
+ }
6839
+ return;
6840
+ }
6841
+ this.opLog.append(opToApply);
6842
+ if (inLane && forceIntegration) {
6843
+ const meta = this.getLaneMeta(this.activeLaneId);
6844
+ this.refreshMaterializedStore(this.opLog.readAll(), this.activeLaneLog.readAll(), meta);
6845
+ } else if (!inLane) {
6846
+ if (!this.integrationCache) {
6847
+ this.integrationCache = {
6848
+ tailHash: opToApply.hash,
6849
+ store: this.store
6850
+ };
6851
+ } else {
6852
+ this.integrationCache.tailHash = opToApply.hash;
6853
+ }
6854
+ }
6855
+ if (opToApply.kind !== "vcs:checkpointCreate" && this.checkpointThreshold > 0) {
6075
6856
  this.checkpointOpCount++;
6076
6857
  if (this.checkpointOpCount >= this.checkpointThreshold) {
6077
6858
  this._pendingAutoCheckpoint = true;
6078
6859
  }
6079
6860
  }
6861
+ if (!opts?.skipBranchAdvance && shouldAdvanceBranchHead(opToApply.kind)) {
6862
+ await this.appendBranchAdvance(opToApply.hash);
6863
+ }
6864
+ }
6865
+ async appendBranchAdvance(targetOpHash) {
6866
+ const advanceOp = await createVcsOp("vcs:branchAdvance", {
6867
+ agentId: this.agentId,
6868
+ previousHash: this.opLog.getLastOp()?.hash,
6869
+ vcs: {
6870
+ branchName: this.currentBranch,
6871
+ targetOpHash
6872
+ }
6873
+ });
6874
+ await this.applyOp(advanceOp, {
6875
+ skipBranchAdvance: true,
6876
+ allowIntegrationWrite: true
6877
+ });
6080
6878
  }
6081
6879
  async flushAutoCheckpoint() {
6082
6880
  if (this._pendingAutoCheckpoint) {
@@ -6087,6 +6885,17 @@ class TrellisVcsEngine {
6087
6885
  loadCurrentBranch() {
6088
6886
  const state = loadBranchState(this.config.rootPath);
6089
6887
  this.currentBranch = state.currentBranch;
6888
+ this.activeLaneId = state.activeLaneId;
6889
+ this.activeLaneLog = null;
6890
+ if (this.activeLaneId) {
6891
+ const meta = loadLaneMeta(this.trellisDir(), this.activeLaneId);
6892
+ if (meta && meta.status === "active") {
6893
+ this.activeLaneLog = new LaneOpLog(laneDir(this.trellisDir(), this.activeLaneId));
6894
+ this.activeLaneLog.load();
6895
+ } else {
6896
+ this.activeLaneId = undefined;
6897
+ }
6898
+ }
6090
6899
  }
6091
6900
  replayOp(op) {
6092
6901
  const decomposed = decompose(op);
@@ -6104,7 +6913,7 @@ class TrellisVcsEngine {
6104
6913
  }
6105
6914
  }
6106
6915
  }
6107
- var TRELLIS_GITIGNORE_ENTRY = ".trellis/";
6916
+ var TRELLIS_GITIGNORE_ENTRY = ".trellis/", ISSUE_INTEGRATION_KINDS;
6108
6917
  var init_engine = __esm(() => {
6109
6918
  init_eav_store();
6110
6919
  init_fs_watcher();
@@ -6125,6 +6934,21 @@ var init_engine = __esm(() => {
6125
6934
  init_infer();
6126
6935
  init_profile();
6127
6936
  init_write();
6937
+ init_op_log();
6938
+ init_lane();
6939
+ init_lane_promote();
6940
+ init_lane_materialize();
6941
+ ISSUE_INTEGRATION_KINDS = new Set([
6942
+ "vcs:issueCreate",
6943
+ "vcs:issueStart",
6944
+ "vcs:issuePause",
6945
+ "vcs:issueResume",
6946
+ "vcs:issueClose",
6947
+ "vcs:issueReopen",
6948
+ "vcs:criterionUpdate",
6949
+ "vcs:issueBlock",
6950
+ "vcs:issueUnblock"
6951
+ ]);
6128
6952
  });
6129
6953
 
6130
- export { FileWatcher, init_fs_watcher, Ingestion, init_ingestion, inferProjectContext, init_infer, loadProfile, saveProfile, hasProfile, promptForProfile, updateProfile, init_profile, writeAgentScaffold, writeIdeScaffold, init_write, TrellisVcsEngine, init_engine };
6954
+ export { FileWatcher, init_fs_watcher, Ingestion, init_ingestion, inferProjectContext, init_infer, loadProfile, saveProfile, hasProfile, promptForProfile, updateProfile, init_profile, writeAgentScaffold, writeIdeScaffold, init_write, formatPromoteExplain, init_lane_promote, TrellisVcsEngine, init_engine };