trellis
Version:
Agentic State Engine — event-sourced causal graph with branching, decision traces, and realtime sync for AI-native applications
1,602 lines (1,595 loc) • 110 kB
JavaScript
import {
init_identity,
signMessage,
verifySignature
} from "./chunk-KFJMKL4Y.js";
import {
init_test_manifest,
loadTestManifest,
resolveIssueStartCriteria
} from "./chunk-PBH357QR.js";
import {
ISSUE_TYPES,
branchEntityId,
branchHeadEntity,
criterionEntityId,
decisionEntityId,
dirEntityId,
fileEntityId,
init_types,
issueEntityId,
laneEntityId,
writerPrincipal
} from "./chunk-E2CFJKLU.js";
import {
createVcsOp,
hashVcsOp,
init_ops
} from "./chunk-GRWQPKYK.js";
import {
__esm
} from "./chunk-2ESYSVXG.js";
// src/vcs/decompose.ts
function zoneOwnerPrincipal(zoneId) {
const m = /^turtle:\/\/(.+?)\/zone\/.+$/.exec(zoneId);
return m ? `identity:${m[1]}` : null;
}
function dirname(p) {
const i = p.lastIndexOf("/");
return i <= 0 ? "." : p.slice(0, i);
}
function pickFacts(input) {
if (!Array.isArray(input)) return [];
return input.filter((item) => {
const fact = item;
return typeof fact.e === "string" && typeof fact.a === "string" && (typeof fact.v === "string" || typeof fact.v === "number" || typeof fact.v === "boolean");
});
}
function pickLinks(input) {
if (!Array.isArray(input)) return [];
return input.filter((item) => {
const link = item;
return typeof link.e1 === "string" && typeof link.a === "string" && typeof link.e2 === "string";
});
}
function claimRetractionFacts(entityId, vcs) {
const facts = [];
if (vcs.claimedLaneId) {
facts.push({ e: entityId, a: "claimedLaneId", v: vcs.claimedLaneId });
}
if (vcs.claimedSessionId) {
facts.push({
e: entityId,
a: "claimedSessionId",
v: vcs.claimedSessionId
});
}
if (vcs.claimedAt) {
facts.push({ e: entityId, a: "claimedAt", v: vcs.claimedAt });
}
return facts;
}
function decompose(op) {
const result = {
addFacts: [],
addLinks: [],
deleteFacts: [],
deleteLinks: []
};
const vcs = op.vcs;
if (!vcs) return result;
if (vcs.issueId && vcs.oldIssueStatus) {
result.deleteFacts.push({
e: issueEntityId(vcs.issueId),
a: "status",
v: vcs.oldIssueStatus
});
}
switch (op.kind) {
case "vcs:fileAdd": {
if (!vcs.filePath) break;
const eid = fileEntityId(vcs.filePath);
const dir = dirname(vcs.filePath);
const did = dirEntityId(dir === "." ? "" : dir);
result.addFacts.push(
{ e: eid, a: "type", v: "FileNode" },
{ e: eid, a: "path", v: vcs.filePath }
);
if (vcs.contentHash) {
result.addFacts.push({ e: eid, a: "contentHash", v: vcs.contentHash });
}
if (vcs.size !== void 0) {
result.addFacts.push({ e: eid, a: "size", v: vcs.size });
}
if (vcs.language) {
result.addFacts.push({ e: eid, a: "language", v: vcs.language });
}
result.addFacts.push({ e: eid, a: "lastModified", v: op.timestamp });
result.addFacts.push(
{ e: did, a: "type", v: "DirectoryNode" },
{ e: did, a: "path", v: dir === "." ? "" : dir }
);
result.addLinks.push({ e1: did, a: "contains", e2: eid });
break;
}
case "vcs:fileModify": {
if (!vcs.filePath) break;
const eid = fileEntityId(vcs.filePath);
if (vcs.oldContentHash) {
result.deleteFacts.push({
e: eid,
a: "contentHash",
v: vcs.oldContentHash
});
}
if (vcs.contentHash) {
result.addFacts.push({ e: eid, a: "contentHash", v: vcs.contentHash });
}
if (vcs.size !== void 0) {
result.addFacts.push({ e: eid, a: "size", v: vcs.size });
}
result.addFacts.push({ e: eid, a: "lastModified", v: op.timestamp });
break;
}
case "vcs:fileDelete": {
if (!vcs.filePath) break;
const eid = fileEntityId(vcs.filePath);
const dir = dirname(vcs.filePath);
const did = dirEntityId(dir === "." ? "" : dir);
result.deleteFacts.push(
{ e: eid, a: "type", v: "FileNode" },
{ e: eid, a: "path", v: vcs.filePath }
);
if (vcs.contentHash) {
result.deleteFacts.push({
e: eid,
a: "contentHash",
v: vcs.contentHash
});
}
result.deleteLinks.push({ e1: did, a: "contains", e2: eid });
break;
}
case "vcs:fileRename": {
if (!vcs.filePath || !vcs.oldFilePath) break;
const eid = fileEntityId(vcs.oldFilePath);
const oldDir = dirname(vcs.oldFilePath);
const newDir = dirname(vcs.filePath);
const oldDid = dirEntityId(oldDir === "." ? "" : oldDir);
const newDid = dirEntityId(newDir === "." ? "" : newDir);
result.deleteFacts.push({ e: eid, a: "path", v: vcs.oldFilePath });
result.addFacts.push({ e: eid, a: "path", v: vcs.filePath });
result.addFacts.push({ e: eid, a: "lastModified", v: op.timestamp });
result.deleteLinks.push({ e1: oldDid, a: "contains", e2: eid });
result.addFacts.push(
{ e: newDid, a: "type", v: "DirectoryNode" },
{ e: newDid, a: "path", v: newDir === "." ? "" : newDir }
);
result.addLinks.push({ e1: newDid, a: "contains", e2: eid });
break;
}
case "vcs:branchCreate": {
if (!vcs.branchName) break;
const bid = `branch:${vcs.branchName}`;
result.addFacts.push(
{ e: bid, a: "type", v: "Branch" },
{ e: bid, a: "name", v: vcs.branchName },
{ e: bid, a: "createdAt", v: op.timestamp },
{ e: bid, a: "createdBy", v: op.agentId }
);
if (vcs.targetOpHash) {
result.addFacts.push({ e: bid, a: "headOpHash", v: vcs.targetOpHash });
}
if (vcs.baseBranch) {
result.addLinks.push({
e1: bid,
a: "forkedFrom",
e2: `branch:${vcs.baseBranch}`
});
}
break;
}
case "vcs:branchDelete": {
if (!vcs.branchName) break;
const bid = `branch:${vcs.branchName}`;
result.deleteFacts.push(
{ e: bid, a: "type", v: "Branch" },
{ e: bid, a: "name", v: vcs.branchName }
);
break;
}
case "vcs:branchAdvance": {
if (!vcs.branchName || !vcs.targetOpHash) break;
const wid = writerPrincipal(op);
const bid = branchHeadEntity(vcs.branchName, wid);
result.addFacts.push({ e: bid, a: "headOpHash", v: vcs.targetOpHash });
break;
}
case "vcs:milestoneCreate": {
if (!vcs.milestoneId) break;
const mid = vcs.milestoneId;
result.addFacts.push(
{ e: mid, a: "type", v: "Milestone" },
{ e: mid, a: "createdAt", v: op.timestamp },
{ e: mid, a: "createdBy", v: op.agentId }
);
if (vcs.message) {
result.addFacts.push({ e: mid, a: "message", v: vcs.message });
}
if (vcs.fromOpHash) {
result.addFacts.push({ e: mid, a: "fromOpHash", v: vcs.fromOpHash });
}
if (vcs.toOpHash) {
result.addFacts.push({ e: mid, a: "toOpHash", v: vcs.toOpHash });
}
break;
}
case "vcs:checkpointCreate": {
const cid = `checkpoint:${op.hash}`;
result.addFacts.push(
{ e: cid, a: "type", v: "Checkpoint" },
{ e: cid, a: "createdAt", v: op.timestamp },
{ e: cid, a: "atOpHash", v: op.hash }
);
if (vcs.trigger) {
result.addFacts.push({ e: cid, a: "trigger", v: vcs.trigger });
}
break;
}
// ----- Issue tracking -----
case "vcs:issueCreate": {
if (!vcs.issueId) break;
const eid = issueEntityId(vcs.issueId);
result.addFacts.push(
{ e: eid, a: "type", v: "Issue" },
// ADR 0026: `type` is the ENTITY type (Issue); `issueType` is what kind
// of issue it is (epic/issue/spike/msg). Absent ⇒ 'issue'.
{ e: eid, a: "issueType", v: vcs.issueType ?? "issue" },
{ e: eid, a: "status", v: vcs.issueStatus ?? "backlog" },
{ e: eid, a: "createdAt", v: op.timestamp },
{ e: eid, a: "createdBy", v: op.agentId }
);
if (vcs.issueTitle) {
result.addFacts.push({ e: eid, a: "title", v: vcs.issueTitle });
}
if (vcs.issueDescription) {
result.addFacts.push({
e: eid,
a: "description",
v: vcs.issueDescription
});
}
if (vcs.issuePriority) {
result.addFacts.push({ e: eid, a: "priority", v: vcs.issuePriority });
}
if (vcs.issueLabels && vcs.issueLabels.length > 0) {
result.addFacts.push({
e: eid,
a: "labels",
v: vcs.issueLabels.join(",")
});
}
if (vcs.issueAssignee) {
result.addFacts.push({ e: eid, a: "assignee", v: vcs.issueAssignee });
}
if (vcs.parentIssueId) {
result.addLinks.push({
e1: eid,
a: "childOf",
e2: issueEntityId(vcs.parentIssueId)
});
}
break;
}
case "vcs:issueUpdate": {
if (!vcs.issueId) break;
const eid = issueEntityId(vcs.issueId);
if (vcs.issueType) {
for (const prior of ISSUE_TYPES) {
result.deleteFacts.push({ e: eid, a: "issueType", v: prior });
}
result.addFacts.push({ e: eid, a: "issueType", v: vcs.issueType });
}
if (vcs.issueStatus) {
result.addFacts.push({ e: eid, a: "status", v: vcs.issueStatus });
}
if (vcs.issuePriority) {
result.addFacts.push({ e: eid, a: "priority", v: vcs.issuePriority });
}
if (vcs.issueLabels) {
result.addFacts.push({
e: eid,
a: "labels",
v: vcs.issueLabels.join(",")
});
}
if (vcs.issueTitle) {
result.addFacts.push({ e: eid, a: "title", v: vcs.issueTitle });
}
if (vcs.issueAssignee) {
result.addFacts.push({ e: eid, a: "assignee", v: vcs.issueAssignee });
}
if (vcs.issueDescription !== void 0) {
result.addFacts.push({
e: eid,
a: "description",
v: vcs.issueDescription
});
}
if (vcs.oldParentIssueId) {
result.deleteLinks.push({
e1: eid,
a: "childOf",
e2: issueEntityId(vcs.oldParentIssueId)
});
}
if (vcs.parentIssueId) {
result.addLinks.push({
e1: eid,
a: "childOf",
e2: issueEntityId(vcs.parentIssueId)
});
}
break;
}
case "vcs:issueStart": {
if (!vcs.issueId) break;
const eid = issueEntityId(vcs.issueId);
result.addFacts.push(
{ e: eid, a: "status", v: "in_progress" },
{ e: eid, a: "startedAt", v: op.timestamp }
);
if (vcs.issueAssignee) {
result.addFacts.push({ e: eid, a: "assignee", v: vcs.issueAssignee });
}
if (vcs.branchName) {
result.addLinks.push({
e1: eid,
a: "trackedOn",
e2: `branch:${vcs.branchName}`
});
}
break;
}
case "vcs:issuePause": {
if (!vcs.issueId) break;
const eid = issueEntityId(vcs.issueId);
result.addFacts.push(
{ e: eid, a: "status", v: "paused" },
{ e: eid, a: "pausedAt", v: op.timestamp }
);
if (vcs.pauseNote) {
result.addFacts.push({ e: eid, a: "pauseNote", v: vcs.pauseNote });
}
result.deleteFacts.push(...claimRetractionFacts(eid, vcs));
break;
}
case "vcs:issueResume": {
if (!vcs.issueId) break;
const eid = issueEntityId(vcs.issueId);
result.addFacts.push(
{ e: eid, a: "status", v: "in_progress" },
{ e: eid, a: "resumedAt", v: op.timestamp },
{ e: eid, a: "pauseNote", v: "" }
);
break;
}
case "vcs:issueClose": {
if (!vcs.issueId) break;
const eid = issueEntityId(vcs.issueId);
result.addFacts.push(
{ e: eid, a: "status", v: "closed" },
{ e: eid, a: "closedAt", v: op.timestamp }
);
result.deleteFacts.push(...claimRetractionFacts(eid, vcs));
break;
}
case "vcs:issueClaim": {
if (!vcs.issueId || !vcs.claimedLaneId) break;
const eid = issueEntityId(vcs.issueId);
result.addFacts.push(
{ e: eid, a: "claimedLaneId", v: vcs.claimedLaneId },
{ e: eid, a: "claimedAt", v: op.timestamp }
);
if (vcs.claimedSessionId) {
result.addFacts.push({
e: eid,
a: "claimedSessionId",
v: vcs.claimedSessionId
});
}
break;
}
case "vcs:issueClaimRelease": {
if (!vcs.issueId) break;
const eid = issueEntityId(vcs.issueId);
result.deleteFacts.push(...claimRetractionFacts(eid, vcs));
break;
}
case "vcs:issueReopen": {
if (!vcs.issueId) break;
const eid = issueEntityId(vcs.issueId);
result.addFacts.push({ e: eid, a: "status", v: "queue" });
break;
}
case "vcs:criterionAdd": {
if (!vcs.criterionId || !vcs.issueId) break;
const ceid = vcs.criterionId;
result.addFacts.push(
{ e: ceid, a: "type", v: "Criterion" },
{ e: ceid, a: "status", v: "pending" },
{ e: ceid, a: "createdAt", v: op.timestamp }
);
if (vcs.criterionDescription) {
result.addFacts.push({
e: ceid,
a: "description",
v: vcs.criterionDescription
});
}
if (vcs.criterionCommand) {
result.addFacts.push({
e: ceid,
a: "command",
v: vcs.criterionCommand
});
}
if (vcs.criterionSuite) {
result.addFacts.push({
e: ceid,
a: "suite",
v: vcs.criterionSuite
});
}
result.addLinks.push({
e1: ceid,
a: "criterionOf",
e2: issueEntityId(vcs.issueId)
});
break;
}
case "vcs:criterionUpdate": {
if (!vcs.criterionId) break;
const ceid = vcs.criterionId;
if (vcs.criterionStatus) {
for (const prior of ["pending", "passed", "failed"]) {
result.deleteFacts.push({ e: ceid, a: "status", v: prior });
}
result.addFacts.push({ e: ceid, a: "status", v: vcs.criterionStatus });
}
if (vcs.criterionOutput) {
result.addFacts.push({
e: ceid,
a: "lastOutput",
v: vcs.criterionOutput
});
}
result.addFacts.push({ e: ceid, a: "lastRunAt", v: op.timestamp });
break;
}
case "vcs:criterionRemove": {
if (!vcs.criterionId) break;
result.addFacts.push({ e: vcs.criterionId, a: "retracted", v: true });
break;
}
case "vcs:testRun": {
if (!vcs.testRunId) break;
const rid = vcs.testRunId;
result.addFacts.push(
{ e: rid, a: "type", v: "TestRun" },
{ e: rid, a: "createdAt", v: op.timestamp },
{ e: rid, a: "createdBy", v: op.agentId }
);
if (vcs.testRunStatus) {
result.addFacts.push({ e: rid, a: "status", v: vcs.testRunStatus });
}
if (vcs.testRunSuite) {
result.addFacts.push({ e: rid, a: "suite", v: vcs.testRunSuite });
}
if (vcs.testRunCommand) {
result.addFacts.push({ e: rid, a: "command", v: vcs.testRunCommand });
}
if (vcs.testRunOutput) {
result.addFacts.push({ e: rid, a: "lastOutput", v: vcs.testRunOutput });
}
if (vcs.testRunExitCode !== void 0) {
result.addFacts.push({
e: rid,
a: "exitCode",
v: vcs.testRunExitCode
});
}
if (vcs.testRunDurationMs !== void 0) {
result.addFacts.push({
e: rid,
a: "durationMs",
v: vcs.testRunDurationMs
});
}
if (vcs.testRunTrigger) {
result.addFacts.push({ e: rid, a: "trigger", v: vcs.testRunTrigger });
}
if (vcs.laneId) {
result.addFacts.push({ e: rid, a: "laneId", v: vcs.laneId });
}
if (vcs.issueId) {
result.addLinks.push({
e1: rid,
a: "testRunOf",
e2: issueEntityId(vcs.issueId)
});
}
break;
}
// ----- Issue blocking -----
case "vcs:issueBlock": {
if (!vcs.issueId || !vcs.blockedByIssueId) break;
const eid = issueEntityId(vcs.issueId);
const blockerEid = issueEntityId(vcs.blockedByIssueId);
result.addLinks.push({ e1: eid, a: "blockedBy", e2: blockerEid });
break;
}
case "vcs:issueUnblock": {
if (!vcs.issueId || !vcs.blockedByIssueId) break;
const eid = issueEntityId(vcs.issueId);
const blockerEid = issueEntityId(vcs.blockedByIssueId);
result.deleteLinks.push({ e1: eid, a: "blockedBy", e2: blockerEid });
break;
}
// ----- Decision traces -----
case "vcs:decisionRecord": {
if (!vcs.decisionId) break;
const did = decisionEntityId(vcs.decisionId);
result.addFacts.push(
{ e: did, a: "type", v: "Decision" },
{ e: did, a: "createdAt", v: op.timestamp },
{ e: did, a: "createdBy", v: op.agentId }
);
if (vcs.decisionToolName) {
result.addFacts.push({
e: did,
a: "toolName",
v: vcs.decisionToolName
});
}
if (vcs.decisionToolInput) {
result.addFacts.push({
e: did,
a: "toolInput",
v: vcs.decisionToolInput
});
}
if (vcs.decisionToolOutput) {
result.addFacts.push({
e: did,
a: "outputSummary",
v: vcs.decisionToolOutput
});
}
if (vcs.decisionContext) {
result.addFacts.push({ e: did, a: "context", v: vcs.decisionContext });
}
if (vcs.decisionRationale) {
result.addFacts.push({
e: did,
a: "rationale",
v: vcs.decisionRationale
});
}
if (vcs.decisionAlternatives) {
result.addFacts.push({
e: did,
a: "alternatives",
v: vcs.decisionAlternatives
});
}
break;
}
case "vcs:chatMessage": {
if (!vcs.chatSessionId) break;
const conv = `conversation:${vcs.chatSessionId}`;
const msg = `message:${op.hash}`;
result.addFacts.push(
{ e: conv, a: "type", v: "Conversation" },
{ e: conv, a: "createdAt", v: op.timestamp },
{ e: msg, a: "type", v: "ChatMessage" },
{ e: msg, a: "role", v: vcs.chatRole ?? "assistant" },
{ e: msg, a: "text", v: vcs.chatText ?? "" },
{ e: msg, a: "createdAt", v: op.timestamp },
{ e: msg, a: "createdBy", v: op.agentId }
);
if (vcs.chatLaneId) {
result.addFacts.push({ e: msg, a: "laneId", v: vcs.chatLaneId });
}
if (vcs.chatToolName) {
result.addFacts.push({ e: msg, a: "toolName", v: vcs.chatToolName });
}
if (typeof vcs.chatTokens === "number") {
result.addFacts.push({ e: msg, a: "tokens", v: vcs.chatTokens });
}
result.addLinks.push({ e1: conv, a: "hasMessage", e2: msg });
break;
}
case "vcs:laneCreate": {
if (!vcs.laneId) break;
const lid = laneEntityId(vcs.laneId);
result.addFacts.push(
{ e: lid, a: "type", v: "AgentLane" },
{ e: lid, a: "status", v: "active" },
{ e: lid, a: "createdAt", v: op.timestamp },
{ e: lid, a: "createdBy", v: op.agentId }
);
if (vcs.baseBranch) {
result.addFacts.push({ e: lid, a: "baseBranch", v: vcs.baseBranch });
}
if (vcs.baseOpHash) {
result.addFacts.push({ e: lid, a: "baseOpHash", v: vcs.baseOpHash });
}
if (vcs.targetBranch) {
result.addFacts.push({ e: lid, a: "targetBranch", v: vcs.targetBranch });
}
if (vcs.baseOpHash) {
result.addFacts.push({ e: lid, a: "headOpHash", v: vcs.baseOpHash });
}
if (vcs.issueId) {
result.addFacts.push({ e: lid, a: "issueId", v: vcs.issueId });
}
if (vcs.sessionId) {
result.addFacts.push({ e: lid, a: "sessionId", v: vcs.sessionId });
}
if (vcs.parentLaneId) {
result.addFacts.push({ e: lid, a: "parentLaneId", v: vcs.parentLaneId });
result.addLinks.push({
e1: lid,
a: "forkedFrom",
e2: laneEntityId(vcs.parentLaneId)
});
}
if (vcs.forkKind) {
result.addFacts.push({ e: lid, a: "forkKind", v: vcs.forkKind });
}
if (vcs.virtualBaseOpHash) {
result.addFacts.push({
e: lid,
a: "virtualBaseOpHash",
v: vcs.virtualBaseOpHash
});
}
break;
}
case "vcs:laneDrop": {
if (!vcs.laneId) break;
const lid = laneEntityId(vcs.laneId);
if (vcs.laneStatus) {
result.deleteFacts.push({ e: lid, a: "status", v: "active" });
result.addFacts.push({ e: lid, a: "status", v: vcs.laneStatus });
}
break;
}
case "vcs:lanePromoteStart": {
if (!vcs.laneId) break;
const lid = laneEntityId(vcs.laneId);
result.deleteFacts.push({ e: lid, a: "status", v: "active" });
result.addFacts.push({ e: lid, a: "status", v: "promoting" });
break;
}
case "vcs:laneGc": {
break;
}
case "vcs:lanePromoteComplete": {
if (!vcs.laneId) break;
const lid = laneEntityId(vcs.laneId);
result.deleteFacts.push({ e: lid, a: "status", v: "promoting" });
result.addFacts.push({ e: lid, a: "status", v: "promoted" });
if (vcs.targetBranch) {
result.addFacts.push({ e: lid, a: "promotedToBranch", v: vcs.targetBranch });
}
break;
}
case "vcs:lanePromoteAbort": {
if (!vcs.laneId) break;
const lid = laneEntityId(vcs.laneId);
result.deleteFacts.push({ e: lid, a: "status", v: "promoting" });
result.addFacts.push({ e: lid, a: "status", v: "active" });
break;
}
// ----- EAV store (CMS / knowledge graph) -----
// -----------------------------------------------------------------------
// Zone capability model (ADR 0022)
// -----------------------------------------------------------------------
case "vcs:zoneDefine": {
if (!vcs.zoneId) break;
const e = `zone:${vcs.zoneId}`;
const owner = zoneOwnerPrincipal(vcs.zoneId);
if (!owner) break;
result.addFacts.push(
{ e, a: "type", v: "Zone" },
{ e, a: "zoneId", v: vcs.zoneId },
{ e, a: "alias", v: vcs.zoneAlias ?? "" },
{ e, a: "defaultVisibility", v: vcs.zoneDefaultVisibility ?? 0 },
// The zone owner is Owner from creation — authority is self-describing
// in the id, so this is derived rather than asserted.
{
e,
a: `grant:${owner}`,
v: 3
/* CapabilityLevel.Owner */
}
);
if (vcs.zoneParent) {
result.addFacts.push({ e, a: "parentZone", v: vcs.zoneParent });
}
break;
}
case "vcs:zoneRename": {
if (!vcs.zoneId || vcs.zoneAlias === void 0) break;
const e = `zone:${vcs.zoneId}`;
if (vcs.oldZoneAlias !== void 0) {
result.deleteFacts.push({ e, a: "alias", v: vcs.oldZoneAlias });
}
result.addFacts.push({ e, a: "alias", v: vcs.zoneAlias });
break;
}
case "vcs:grantSet": {
if (!vcs.zoneId || !vcs.grantPrincipal || !vcs.grantLevel) break;
const e = `zone:${vcs.zoneId}`;
const a = `grant:${vcs.grantPrincipal}`;
for (const prior of [1, 2, 3]) {
result.deleteFacts.push({ e, a, v: prior });
}
result.addFacts.push({ e, a, v: vcs.grantLevel });
break;
}
case "vcs:grantRetract": {
if (!vcs.zoneId || !vcs.grantPrincipal) break;
const e = `zone:${vcs.zoneId}`;
const a = `grant:${vcs.grantPrincipal}`;
for (const prior of [1, 2, 3]) {
result.deleteFacts.push({ e, a, v: prior });
}
break;
}
case "vcs:storeAssert": {
result.addFacts.push(...pickFacts(vcs.facts));
break;
}
case "vcs:storeRetract": {
result.deleteFacts.push(...pickFacts(vcs.facts));
break;
}
case "vcs:storeLink": {
result.addLinks.push(...pickLinks(vcs.links));
break;
}
case "vcs:storeUnlink": {
result.deleteLinks.push(...pickLinks(vcs.links));
break;
}
case "vcs:gitSync": {
if (vcs.gitBranch) {
const bid = branchEntityId(vcs.gitBranch);
result.addFacts.push(
{ e: bid, a: "type", v: "Branch" },
{ e: bid, a: "name", v: vcs.gitBranch }
);
if (vcs.gitCommitHash) {
result.addFacts.push({ e: bid, a: "lastGitCommit", v: vcs.gitCommitHash });
}
}
break;
}
case "vcs:repoAttest": {
if (vcs.repoId) {
const ledger = `ledger:${vcs.repoId}`;
result.addFacts.push(
{ e: ledger, a: "type", v: "Ledger" },
{ e: ledger, a: "repoId", v: vcs.repoId }
);
if (vcs.repoOwner) {
result.addFacts.push({ e: ledger, a: "owner", v: vcs.repoOwner });
}
if (vcs.repoName) {
result.addFacts.push({ e: ledger, a: "name", v: vcs.repoName });
}
if (vcs.projectKind) {
result.addFacts.push({ e: ledger, a: "kind", v: vcs.projectKind });
}
if (vcs.signedBy) {
result.addFacts.push({
e: ledger,
a: "attestedBy",
v: vcs.signedBy
});
}
}
break;
}
}
return result;
}
var init_decompose = __esm({
"src/vcs/decompose.ts"() {
"use strict";
init_types();
init_types();
init_types();
}
});
// src/vcs/branch.ts
import { existsSync, readFileSync, writeFileSync } from "fs";
import { join } from "path";
function shouldAdvanceBranchHead(kind) {
return !BRANCH_ADVANCE_SKIP_KINDS.has(kind);
}
function getBranchHeadOpHash(ctx, branchName, principal) {
let advances = ctx.readAllOps().filter(
(op) => op.kind === "vcs:branchAdvance" && op.vcs?.branchName === branchName && op.vcs?.targetOpHash
);
if (principal) {
advances = advances.filter(
(op) => writerPrincipal(op) === principal
);
}
const sorted = advances.sort(
(a, b) => a.timestamp.localeCompare(b.timestamp) || a.hash.localeCompare(b.hash)
);
return sorted.at(-1)?.vcs?.targetOpHash;
}
async function createBranch(ctx, name, currentBranch) {
const existing = ctx.store.getFactsByAttribute("type").filter((f) => f.v === "Branch" && f.e === `branch:${name}`);
if (existing.length > 0) {
throw new Error(`Branch '${name}' already exists`);
}
const op = await createVcsOp("vcs:branchCreate", {
agentId: ctx.agentId,
previousHash: ctx.getLastOp()?.hash,
vcs: {
branchName: name,
baseBranch: currentBranch,
targetOpHash: ctx.getLastOp()?.hash
}
});
await ctx.applyOp(op);
return op;
}
function switchBranch(ctx, name) {
const branchFacts = ctx.store.getFactsByEntity(`branch:${name}`).filter((f) => f.a === "type" && f.v === "Branch");
if (branchFacts.length === 0) {
throw new Error(`Branch '${name}' does not exist`);
}
}
function listBranches(ctx, currentBranch) {
const branchFacts = ctx.store.getFactsByAttribute("type").filter((f) => f.v === "Branch");
return branchFacts.map((f) => {
const nameFact = ctx.store.getFactsByEntity(f.e).find((ef) => ef.a === "name");
const createdFact = ctx.store.getFactsByEntity(f.e).find((ef) => ef.a === "createdAt");
const name = nameFact?.v ?? f.e.replace("branch:", "");
return {
name,
isCurrent: name === currentBranch,
createdAt: createdFact?.v
};
});
}
async function deleteBranch(ctx, name, currentBranch) {
if (name === currentBranch) {
throw new Error(`Cannot delete the current branch '${name}'`);
}
const branchFacts = ctx.store.getFactsByEntity(`branch:${name}`).filter((f) => f.a === "type" && f.v === "Branch");
if (branchFacts.length === 0) {
throw new Error(`Branch '${name}' does not exist`);
}
const op = await createVcsOp("vcs:branchDelete", {
agentId: ctx.agentId,
previousHash: ctx.getLastOp()?.hash,
vcs: { branchName: name }
});
await ctx.applyOp(op);
return op;
}
function saveBranchState(rootPath, state) {
const statePath = join(rootPath, ".trellis", "state.json");
writeFileSync(statePath, JSON.stringify(state));
}
function loadBranchState(rootPath) {
const statePath = join(rootPath, ".trellis", "state.json");
if (existsSync(statePath)) {
try {
const raw = readFileSync(statePath, "utf-8");
const state = JSON.parse(raw);
if (state.currentBranch) {
return {
currentBranch: state.currentBranch,
activeLaneId: state.activeLaneId
};
}
} catch {
}
}
return { currentBranch: "main" };
}
var BRANCH_ADVANCE_SKIP_KINDS;
var init_branch = __esm({
"src/vcs/branch.ts"() {
"use strict";
init_ops();
init_types();
BRANCH_ADVANCE_SKIP_KINDS = /* @__PURE__ */ new Set([
"vcs:branchAdvance",
"vcs:branchCreate",
"vcs:branchDelete",
"vcs:checkpointCreate"
]);
}
});
// src/vcs/milestone.ts
async function createMilestone(ctx, message, opts) {
const ops = ctx.readAllOps();
const toOpHash = opts?.toOpHash ?? ops[ops.length - 1]?.hash;
let fromOpHash = opts?.fromOpHash;
if (!fromOpHash) {
const milestones = ops.filter((o) => o.kind === "vcs:milestoneCreate");
if (milestones.length > 0) {
const lastMilestone = milestones[milestones.length - 1];
fromOpHash = lastMilestone.vcs?.toOpHash ?? lastMilestone.hash;
} else {
fromOpHash = ops[0]?.hash;
}
}
const idBase = `${message}:${Date.now()}`;
const msgUint8 = new TextEncoder().encode(idBase);
const hashBuffer = await crypto.subtle.digest("SHA-256", msgUint8);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
const milestoneId = `milestone:${hashHex.slice(0, 12)}`;
const fromIdx = ops.findIndex((o) => o.hash === fromOpHash);
const toIdx = ops.findIndex((o) => o.hash === toOpHash);
const rangeOps = fromIdx >= 0 && toIdx >= 0 ? ops.slice(fromIdx, toIdx + 1) : ops;
const affectedFiles = [
...new Set(
rangeOps.filter((o) => o.vcs?.filePath).map((o) => o.vcs.filePath)
)
];
const op = await createVcsOp("vcs:milestoneCreate", {
agentId: ctx.agentId,
previousHash: ctx.getLastOp()?.hash,
vcs: {
milestoneId,
message,
fromOpHash,
toOpHash
}
});
await ctx.applyOp(op);
for (const file of affectedFiles) {
ctx.store.addFacts([{ e: milestoneId, a: "affectsFile", v: file }]);
}
return op;
}
function listMilestones(ctx) {
const milestoneFacts = ctx.store.getFactsByAttribute("type").filter((f) => f.v === "Milestone");
return milestoneFacts.map((f) => {
const facts = ctx.store.getFactsByEntity(f.e);
const get = (attr) => facts.find((ef) => ef.a === attr)?.v;
const affectedFiles = facts.filter((ef) => ef.a === "affectsFile").map((ef) => ef.v);
return {
id: f.e,
message: get("message"),
createdAt: get("createdAt"),
createdBy: get("createdBy"),
fromOpHash: get("fromOpHash"),
toOpHash: get("toOpHash"),
affectedFiles
};
});
}
var init_milestone = __esm({
"src/vcs/milestone.ts"() {
"use strict";
init_ops();
}
});
// src/vcs/checkpoint.ts
async function createCheckpoint(ctx, trigger = "manual") {
const op = await createVcsOp("vcs:checkpointCreate", {
agentId: ctx.agentId,
previousHash: ctx.getLastOp()?.hash,
vcs: { trigger }
});
await ctx.applyOp(op);
return op;
}
function listCheckpoints(ctx) {
const cpFacts = ctx.store.getFactsByAttribute("type").filter((f) => f.v === "Checkpoint");
return cpFacts.map((f) => {
const facts = ctx.store.getFactsByEntity(f.e);
const get = (attr) => facts.find((ef) => ef.a === attr)?.v;
return {
id: f.e,
createdAt: get("createdAt"),
trigger: get("trigger"),
atOpHash: get("atOpHash")
};
});
}
var init_checkpoint = __esm({
"src/vcs/checkpoint.ts"() {
"use strict";
init_ops();
}
});
// src/vcs/diff.ts
function diffFileStates(stateA, stateB, blobResolver) {
const diffs = [];
for (const [path, bState] of stateB) {
if (bState.deleted) continue;
const aState = stateA.get(path);
if (!aState || aState.deleted) {
diffs.push({
kind: "fileAdded",
path,
newContentHash: bState.contentHash
});
} else if (aState.contentHash !== bState.contentHash) {
const diff = {
kind: "fileModified",
path,
oldContentHash: aState.contentHash,
newContentHash: bState.contentHash
};
if (blobResolver && aState.contentHash && bState.contentHash) {
const oldContent = blobResolver.get(aState.contentHash);
const newContent = blobResolver.get(bState.contentHash);
if (oldContent && newContent) {
diff.unifiedDiff = generateUnifiedDiff(
path,
oldContent.toString("utf-8"),
newContent.toString("utf-8")
);
}
}
diffs.push(diff);
}
}
for (const [path, aState] of stateA) {
if (aState.deleted) continue;
const bState = stateB.get(path);
if (!bState || bState.deleted) {
diffs.push({
kind: "fileDeleted",
path,
oldContentHash: aState.contentHash
});
}
}
const stats = {
added: diffs.filter((d) => d.kind === "fileAdded").length,
modified: diffs.filter((d) => d.kind === "fileModified").length,
removed: diffs.filter((d) => d.kind === "fileDeleted").length,
renamed: diffs.filter((d) => d.kind === "fileRenamed").length
};
return {
diffs,
filesChanged: diffs.map((d) => d.path),
stats
};
}
function buildFileStateAtOp(ops, atOpHash) {
const state = /* @__PURE__ */ new Map();
for (const op of ops) {
if (op.vcs?.filePath) {
switch (op.kind) {
case "vcs:fileAdd":
case "vcs:fileModify":
state.set(op.vcs.filePath, { contentHash: op.vcs.contentHash });
break;
case "vcs:fileDelete":
state.set(op.vcs.filePath, { deleted: true });
break;
case "vcs:fileRename":
if (op.vcs.oldFilePath) {
state.set(op.vcs.oldFilePath, { deleted: true });
}
state.set(op.vcs.filePath, { contentHash: op.vcs.contentHash });
break;
}
}
if (atOpHash && op.hash === atOpHash) break;
}
return state;
}
function diffOpRange(ops, fromHash, toHash, blobResolver) {
const stateA = buildFileStateAtOp(ops, fromHash);
const stateB = buildFileStateAtOp(ops, toHash);
return diffFileStates(stateA, stateB, blobResolver);
}
function generateUnifiedDiff(filePath, oldText, newText, contextLines = 3) {
const oldLines = oldText.split("\n");
const newLines = newText.split("\n");
const edits = myersDiff(oldLines, newLines);
const hunks = buildHunks(edits, contextLines);
if (hunks.length === 0) return "";
const lines = [`--- a/${filePath}`, `+++ b/${filePath}`];
for (const hunk of hunks) {
lines.push(
`@@ -${hunk.oldStart},${hunk.oldCount} +${hunk.newStart},${hunk.newCount} @@`
);
for (const edit of hunk.edits) {
switch (edit.kind) {
case "equal":
lines.push(` ${edit.line}`);
break;
case "delete":
lines.push(`-${edit.line}`);
break;
case "insert":
lines.push(`+${edit.line}`);
break;
}
}
}
return lines.join("\n");
}
function myersDiff(oldLines, newLines) {
const n = oldLines.length;
const m = newLines.length;
if (n === 0 && m === 0) return [];
if (n === 0)
return newLines.map((line) => ({ kind: "insert", line }));
if (m === 0)
return oldLines.map((line) => ({ kind: "delete", line }));
const max = n + m;
const size = 2 * max + 1;
const v = new Int32Array(size);
const trace = [];
const off = max;
outer: for (let d = 0; d <= max; d++) {
trace.push(v.slice());
for (let k = -d; k <= d; k += 2) {
let x2;
if (k === -d || k !== d && v[k - 1 + off] < v[k + 1 + off]) {
x2 = v[k + 1 + off];
} else {
x2 = v[k - 1 + off] + 1;
}
let y2 = x2 - k;
while (x2 < n && y2 < m && oldLines[x2] === newLines[y2]) {
x2++;
y2++;
}
v[k + off] = x2;
if (x2 >= n && y2 >= m) break outer;
}
}
let x = n;
let y = m;
const edits = [];
for (let d = trace.length - 1; d >= 0; d--) {
const tv = trace[d];
const k = x - y;
let prevK;
if (d === 0) {
while (x > 0 && y > 0) {
x--;
y--;
edits.push({ kind: "equal", line: oldLines[x] });
}
break;
}
if (k === -d || k !== d && tv[k - 1 + off] < tv[k + 1 + off]) {
prevK = k + 1;
} else {
prevK = k - 1;
}
const prevX = tv[prevK + off];
const prevY = prevX - prevK;
while (x > prevX && y > prevY) {
x--;
y--;
edits.push({ kind: "equal", line: oldLines[x] });
}
if (prevK === k + 1) {
y--;
edits.push({ kind: "insert", line: newLines[y] });
} else {
x--;
edits.push({ kind: "delete", line: oldLines[x] });
}
}
edits.reverse();
return edits;
}
function buildHunks(edits, contextLines) {
if (edits.length === 0) return [];
const changeIndices = [];
for (let i = 0; i < edits.length; i++) {
if (edits[i].kind !== "equal") {
changeIndices.push(i);
}
}
if (changeIndices.length === 0) return [];
const hunks = [];
let hunkStart = Math.max(0, changeIndices[0] - contextLines);
let hunkEnd = Math.min(edits.length - 1, changeIndices[0] + contextLines);
for (let i = 1; i < changeIndices.length; i++) {
const changeStart = changeIndices[i] - contextLines;
const changeEnd = Math.min(
edits.length - 1,
changeIndices[i] + contextLines
);
if (changeStart <= hunkEnd + 1) {
hunkEnd = changeEnd;
} else {
hunks.push(createHunk(edits, hunkStart, hunkEnd));
hunkStart = changeStart;
hunkEnd = changeEnd;
}
}
hunks.push(createHunk(edits, hunkStart, hunkEnd));
return hunks;
}
function createHunk(edits, start, end) {
const hunkEdits = edits.slice(start, end + 1);
let oldLine = 1;
let newLine = 1;
for (let i = 0; i < start; i++) {
if (edits[i].kind === "equal" || edits[i].kind === "delete") oldLine++;
if (edits[i].kind === "equal" || edits[i].kind === "insert") newLine++;
}
let oldCount = 0;
let newCount = 0;
for (const edit of hunkEdits) {
if (edit.kind === "equal" || edit.kind === "delete") oldCount++;
if (edit.kind === "equal" || edit.kind === "insert") newCount++;
}
return {
oldStart: oldLine,
oldCount,
newStart: newLine,
newCount,
edits: hunkEdits
};
}
var init_diff = __esm({
"src/vcs/diff.ts"() {
"use strict";
}
});
// src/vcs/merge.ts
function threeWayMerge(base, ours, theirs, blobResolver) {
const mergedFiles = /* @__PURE__ */ new Map();
const conflicts = [];
const allPaths = /* @__PURE__ */ new Set();
for (const [p, s] of base) if (!s.deleted) allPaths.add(p);
for (const [p, s] of ours) if (!s.deleted) allPaths.add(p);
for (const [p, s] of theirs) if (!s.deleted) allPaths.add(p);
for (const [p, s] of ours) if (s.deleted) allPaths.add(p);
for (const [p, s] of theirs) if (s.deleted) allPaths.add(p);
for (const path of allPaths) {
const b = base.get(path);
const o = ours.get(path);
const t = theirs.get(path);
const baseExists = b && !b.deleted;
const oursExists = o && !o.deleted;
const theirsExists = t && !t.deleted;
const baseHash = baseExists ? b.contentHash : void 0;
const oursHash = oursExists ? o.contentHash : void 0;
const theirsHash = theirsExists ? t.contentHash : void 0;
if (oursHash === theirsHash) {
continue;
}
if (theirsHash === baseHash && oursHash !== baseHash) {
if (!oursExists) {
mergedFiles.set(path, null);
}
continue;
}
if (oursHash === baseHash && theirsHash !== baseHash) {
if (!theirsExists) {
mergedFiles.set(path, null);
} else if (theirsHash && blobResolver) {
const content = blobResolver?.get(theirsHash);
if (content) {
mergedFiles.set(path, content.toString("utf-8"));
}
}
continue;
}
if (!baseExists && oursExists && theirsExists) {
if (oursHash === theirsHash) {
continue;
}
const oursContent = oursHash && blobResolver ? blobResolver?.get(oursHash)?.toString("utf-8") : void 0;
const theirsContent = theirsHash && blobResolver ? blobResolver?.get(theirsHash)?.toString("utf-8") : void 0;
if (oursContent !== void 0 && theirsContent !== void 0) {
const textResult = threeWayTextMerge("", oursContent, theirsContent);
if (textResult.clean) {
mergedFiles.set(path, textResult.merged);
continue;
}
conflicts.push({
path,
kind: "add-add",
ours: oursContent,
theirs: theirsContent,
mergedWithMarkers: textResult.merged
});
} else {
conflicts.push({ path, kind: "add-add", ours: oursContent, theirs: theirsContent });
}
continue;
}
if (oursExists && !theirsExists) {
conflicts.push({
path,
kind: "modify-delete",
ours: oursHash && blobResolver ? blobResolver?.get(oursHash)?.toString("utf-8") : void 0
});
continue;
}
if (!oursExists && theirsExists) {
conflicts.push({
path,
kind: "modify-delete",
theirs: theirsHash && blobResolver ? blobResolver?.get(theirsHash)?.toString("utf-8") : void 0
});
continue;
}
if (oursExists && theirsExists && oursHash !== theirsHash) {
const baseContent = baseHash && blobResolver ? blobResolver?.get(baseHash)?.toString("utf-8") : void 0;
const oursContent = oursHash && blobResolver ? blobResolver?.get(oursHash)?.toString("utf-8") : void 0;
const theirsContent = theirsHash && blobResolver ? blobResolver?.get(theirsHash)?.toString("utf-8") : void 0;
if (baseContent !== void 0 && oursContent !== void 0 && theirsContent !== void 0) {
const textResult = threeWayTextMerge(baseContent, oursContent, theirsContent);
if (textResult.clean) {
mergedFiles.set(path, textResult.merged);
} else {
conflicts.push({
path,
kind: "modify-modify",
base: baseContent,
ours: oursContent,
theirs: theirsContent,
mergedWithMarkers: textResult.merged
});
}
} else {
conflicts.push({
path,
kind: "modify-modify",
base: baseContent,
ours: oursContent,
theirs: theirsContent
});
}
continue;
}
}
const added = [...mergedFiles.values()].filter((v) => v !== null).length;
const deleted = [...mergedFiles.values()].filter((v) => v === null).length;
return {
clean: conflicts.length === 0,
mergedFiles,
conflicts,
stats: {
added,
modified: added,
// in a merge context, additions from theirs are "modified"
deleted,
conflicted: conflicts.length
}
};
}
function threeWayTextMerge(baseText, oursText, theirsText) {
const baseLines = baseText.split("\n");
const oursLines = oursText.split("\n");
const theirsLines = theirsText.split("\n");
const oursChanges = computeLineChanges(baseLines, oursLines);
const theirsChanges = computeLineChanges(baseLines, theirsLines);
const result = [];
let clean = true;
let baseIdx = 0;
let oursIdx = 0;
let theirsIdx = 0;
while (baseIdx < baseLines.length || oursIdx < oursLines.length || theirsIdx < theirsLines.length) {
const oursChange = oursChanges.get(baseIdx);
const theirsChange = theirsChanges.get(baseIdx);
if (baseIdx >= baseLines.length) {
while (oursIdx < oursLines.length) {
result.push(oursLines[oursIdx++]);
}
while (theirsIdx < theirsLines.length) {
result.push(theirsLines[theirsIdx++]);
}
break;
}
if (!oursChange && !theirsChange) {
result.push(baseLines[baseIdx]);
baseIdx++;
oursIdx++;
theirsIdx++;
} else if (oursChange && !theirsChange) {
applyChange(oursChange, result);
baseIdx += oursChange.baseCount;
oursIdx += oursChange.newCount;
theirsIdx += oursChange.baseCount;
} else if (!oursChange && theirsChange) {
applyChange(theirsChange, result);
baseIdx += theirsChange.baseCount;
oursIdx += theirsChange.baseCount;
theirsIdx += theirsChange.newCount;
} else if (oursChange && theirsChange) {
if (oursChange.baseCount === theirsChange.baseCount && oursChange.newLines.join("\n") === theirsChange.newLines.join("\n")) {
applyChange(oursChange, result);
baseIdx += oursChange.baseCount;
oursIdx += oursChange.newCount;
theirsIdx += theirsChange.newCount;
} else {
clean = false;
result.push("<<<<<<< ours");
for (const line of oursChange.newLines) result.push(line);
result.push("=======");
for (const line of theirsChange.newLines) result.push(line);
result.push(">>>>>>> theirs");
baseIdx += Math.max(oursChange.baseCount, theirsChange.baseCount);
oursIdx += oursChange.newCount;
theirsIdx += theirsChange.newCount;
}
}
}
return { clean, merged: result.join("\n") };
}
function applyChange(change, result) {
for (const line of change.newLines) {
result.push(line);
}
}
function computeLineChanges(baseLines, newLines) {
const changes = /* @__PURE__ */ new Map();
const matches = lcsMatch(baseLines, newLines);
let baseIdx = 0;
let newIdx = 0;
for (const match of matches) {
if (baseIdx < match.baseIdx || newIdx < match.newIdx) {
const baseCount = match.baseIdx - baseIdx;
const newCount = match.newIdx - newIdx;
if (baseCount > 0 || newCount > 0) {
changes.set(baseIdx, {
baseStart: baseIdx,
baseCount,
newCount,
newLines: newLines.slice(newIdx, newIdx + newCount)
});
}
}
baseIdx = match.baseIdx + 1;
newIdx = match.newIdx + 1;
}
if (baseIdx < baseLines.length || newIdx < newLines.length) {
const baseCount = baseLines.length - baseIdx;
const newCount = newLines.length - newIdx;
if (baseCount > 0 || newCount > 0) {
changes.set(baseIdx, {
baseStart: baseIdx,
baseCount,
newCount,
newLines: newLines.slice(newIdx)
});
}
}
return changes;
}
function lcsMatch(a, b) {
const n = a.length;
const m = b.length;
if (n === 0 || m === 0) return [];
const dp = Array.from(
{ length: n + 1 },
() => new Array(m + 1).fill(0)
);
for (let i2 = 1; i2 <= n; i2++) {
for (let j2 = 1; j2 <= m; j2++) {
if (a[i2 - 1] === b[j2 - 1]) {
dp[i2][j2] = dp[i2 - 1][j2 - 1] + 1;
} else {
dp[i2][j2] = Math.max(dp[i2 - 1][j2], dp[i2][j2 - 1]);
}
}
}
const matches = [];
let i = n;
let j = m;
while (i > 0 && j > 0) {
if (a[i - 1] === b[j - 1]) {
matches.unshift({ baseIdx: i - 1, newIdx: j - 1 });
i--;
j--;
} else if (dp[i - 1][j] > dp[i][j - 1]) {
i--;
} else {
j--;
}
}
return matches;
}
var init_merge = __esm({
"src/vcs/merge.ts"() {
"use strict";
}
});
// src/vcs/issue.ts
import {
existsSync as existsSync2,
readFileSync as readFileSync2,
writeFileSync as writeFileSync2,
mkdirSync,
openSync,
closeSync,
unlinkSync
} from "fs";
import { join as join2, dirname as dirname2 } from "path";
function getIssueCounterPath(rootPath) {
return join2(rootPath, ".trellis", "issue-counter.json");
}
function getLaneIssueCounterPath(rootPath, laneId) {
return join2(
rootPath,
".trellis",
"issue-counters",
`${encodeURIComponent(laneId)}.json`
);
}
function bumpIssueCounterAtLeast(rootPath, n) {
const counterPath = getIssueCounterPath(rootPath);
const dir = dirname2(counterPath);
if (!existsSync2(dir)) mkdirSync(dir, { recursive: true });
let counter = 0;
if (existsSync2(counterPath)) {
try {
counter = JSON.parse(readFileSync2(counterPath, "utf-8")).counter ?? 0;
} catch {
}
}
if (n > counter) {
writeFileSync2(counterPath, JSON.stringify({ counter: n }, null, 2));
}
}
function nextIssueId(rootPath, laneId) {
const laneScope = laneId?.trim();
const counterPath = getIssueCounterPath(rootPath);
const scopedCounterPath = laneScope ? getLaneIssueCounterPath(rootPath, laneScope) : counterPath;
const dir = dirname2(scopedCounterPath);
if (!existsSync2(dir)) mkdirSync(dir, { recursive: true });
const lockPath = `${scopedCounterPath}.lock`;
const deadline = Date.now() + 5e3;
let lockFd;
while (Date.now() < deadline) {
try {
lockFd = openSync(lockPath, "wx");
break;
} catch (err) {
if (err?.code !== "EEXIST") {
throw err;
}
}
}
if (lockFd === void 0) {
throw new Error(
`Timed out waiting for issue counter lock: ${lockPath}. Another Trellis process may be stalled.`
);
}
try {
let counter = 0;
if (existsSync2(scopedCounterPath)) {
try {
counter = JSON.parse(readFileSync2(scopedCounterPath, "utf-8")).counter ?? 0;
} catch {
}
}
counter++;
writeFileSync2(scopedCounterPath, JSON.stringify({ counter }, null, 2));
if (laneScope) return `issue:${laneScope}:${counter}`;
return `TRL-${counter}`;
} finally {
closeSync(lockFd);
try