UNPKG

paqad-ai

Version:

Spec-driven development framework — AI agents that think before they type

1,526 lines • 51.3 kB
import { createRequire as __paqadCreateRequire } from 'module'; const require = __paqadCreateRequire(import.meta.url);
import {
  releaseLock,
  tryAcquireLock
} from "./chunk-TINPSX3N.js";
import {
  changeOwnTickets,
  computeCommitSplit
} from "./chunk-BTDDEK7C.js";
import {
  BACKSTOP_WRITER,
  COMPLETION_DEFERRED_KIND,
  ENVELOPE_HEADER_PROPERTIES,
  ORCHESTRATOR_AGENT,
  RECIPE_INJECTED_KIND,
  RECORD_REPAIR_KIND,
  REPORT_SOURCES,
  SKILL_USED_KIND,
  SPEC_CORRECTION_KIND,
  SPEC_STEP_KIND,
  SPEC_STEP_OUTCOMES,
  STAGE_AGENT_KIND,
  STAGE_EVIDENCE_DOC_TYPE,
  STAGE_EVIDENCE_SCHEMA_V2,
  STAGE_EVIDENCE_SCHEMA_VERSION,
  STAGE_FAMILY_KIND_LIST,
  STAGE_HANDBACK_KIND,
  adoptableInFlightOnBranch,
  augmentWithBundleArtifacts,
  foldRowsWithKey,
  heldInFlightOnBranch,
  isBundleClosed,
  isBundleMaterialized,
  isFeatureFinished,
  listFeatureDirs,
  readChangeConstants,
  readFeatureRecord,
  reconcileSessionControl,
  seedFeatureRecord,
  updateFeatureRecord
} from "./chunk-7TBVBCDH.js";
import {
  UNTITLED_FEATURE_TITLE,
  buildDocumentEnvelope,
  mintFeatureDirName,
  stampBundleRow
} from "./chunk-RJMJYPHL.js";
import {
  forkBranchOf,
  readGitState
} from "./chunk-BOEVU2BD.js";
import {
  STAGE_EFFORT_LEVELS,
  STAGE_MODELS,
  require_ajv
} from "./chunk-ACJQW7SB.js";
import {
  sleepSync
} from "./chunk-Z3NGWHFX.js";
import {
  readUnitFile
} from "./chunk-Q353EDKY.js";
import {
  resolveSessionId
} from "./chunk-SG6LXLDH.js";
import {
  markDone,
  readSessionControl,
  resumeFeature,
  setActiveFeature
} from "./chunk-FMQ5YRSP.js";
import {
  dirname,
  featureChangeKey,
  featureDir,
  featureFilePath,
  join,
  parseFeatureDirName
} from "./chunk-AZLA53A6.js";
import {
  __toESM,
  init_esm_shims
} from "./chunk-IDFRWBMN.js";

// src/feature-evidence/stage-ledger.ts
init_esm_shims();
import { readFileSync as readFileSync4 } from "fs";
import { join as join6 } from "path";

// src/feature-evidence/delivery.ts
init_esm_shims();
import { execFileSync as execFileSync2 } from "child_process";
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "fs";
import { dirname as dirname2, join as join3 } from "path";

// src/pipeline/git-paths.ts
init_esm_shims();
import { execFileSync } from "child_process";
import { createHash } from "crypto";
import { copyFileSync, existsSync, mkdtempSync, readFileSync, rmSync } from "fs";
import { tmpdir } from "os";
import { join as join2, resolve } from "path";
var DELETED_AT_START = "deleted";
var PINNED_DIFF_ARGS = [
  "-c",
  "core.quotepath=off",
  "diff",
  "--no-color",
  "--no-ext-diff",
  "--no-textconv",
  "--src-prefix=a/",
  "--dst-prefix=b/",
  "--submodule=short",
  "--find-renames"
];
function gitPathList(projectRoot, args) {
  try {
    return execFileSync("git", args, {
      cwd: projectRoot,
      encoding: "utf8",
      stdio: ["ignore", "pipe", "ignore"],
      timeout: 5e3,
      maxBuffer: 16 * 1024 * 1024
    }).split("\0").filter((entry) => entry.length > 0);
  } catch {
    return null;
  }
}
function digestWorkingFile(projectRoot, relativePath) {
  try {
    return createHash("sha256").update(readFileSync(join2(projectRoot, relativePath))).digest("hex");
  } catch {
    return null;
  }
}
function changedFilesSince(projectRoot, start) {
  const diff = gitPathList(projectRoot, ["diff", "--name-only", "--relative", "-z", start.commit]);
  const untracked = gitPathList(projectRoot, ["ls-files", "--others", "--exclude-standard", "-z"]);
  if (diff === null || untracked === null) return null;
  const merged = start.mergedBase ? gitPathList(projectRoot, ["diff", "--name-only", "--relative", "-z", start.mergedBase]) : null;
  if (start.mergedBase && merged === null) return null;
  const differsFromBase = merged ? new Set(merged) : null;
  const worktree = start.worktree ?? {};
  return [
    .../* @__PURE__ */ new Set([
      ...diff.filter((path) => !differsFromBase || differsFromBase.has(path)),
      ...untracked
    ])
  ].map((path) => path.replace(/\\/g, "/")).filter((path) => !isPaqadArtifactPath(path)).filter((path) => {
    const atStart = worktree[path];
    return atStart === void 0 || (digestWorkingFile(projectRoot, path) ?? DELETED_AT_START) !== atStart;
  }).sort();
}
function parseAddedLines(diff) {
  const added = {};
  let path = null;
  let inHunk = false;
  for (const line of diff.split("\n")) {
    if (line.startsWith("diff --git ")) {
      path = null;
      inHunk = false;
    } else if (!inHunk && line.startsWith("+++ ")) {
      const name = line.slice(4).replace(/\t$/u, "");
      path = name.startsWith("b/") ? name.slice(2) : null;
    } else if (line.startsWith("@@")) {
      inHunk = true;
    } else if (inHunk && path !== null && line.startsWith("+")) {
      (added[path] ??= []).push(line.slice(1));
    }
  }
  return Object.fromEntries(Object.entries(added).map(([file, lines]) => [file, lines.join("\n")]));
}
function readGit(projectRoot, args, env) {
  return execFileSync("git", args, {
    cwd: projectRoot,
    encoding: "utf8",
    stdio: ["ignore", "pipe", "ignore"],
    timeout: 3e4,
    maxBuffer: 256 * 1024 * 1024,
    ...env ? { env } : {}
  });
}
function diffSeeingUntracked(projectRoot, diffArgs, untracked) {
  const scratch = mkdtempSync(join2(tmpdir(), "paqad-index-"));
  try {
    const index = resolve(
      projectRoot,
      readGit(projectRoot, ["rev-parse", "--git-path", "index"]).trim()
    );
    const tempIndex = join2(scratch, "index");
    if (existsSync(index)) copyFileSync(index, tempIndex);
    const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
    execFileSync(
      "git",
      ["--literal-pathspecs", "add", "-N", "--pathspec-from-file=-", "--pathspec-file-nul"],
      {
        cwd: projectRoot,
        input: untracked.join("\0"),
        stdio: ["pipe", "ignore", "ignore"],
        timeout: 3e4,
        env
      }
    );
    return readGit(projectRoot, diffArgs, env);
  } catch {
    return null;
  } finally {
    rmSync(scratch, { recursive: true, force: true });
  }
}
function addedLinesSince(projectRoot, start) {
  const changed = changedFilesSince(projectRoot, start);
  const untracked = gitPathList(projectRoot, ["ls-files", "--others", "--exclude-standard", "-z"]);
  if (changed === null || untracked === null) return null;
  const inChange = new Set(changed);
  const newFiles = untracked.filter(
    (path) => !path.endsWith("/") && inChange.has(path.replace(/\\/g, "/"))
  );
  const diffArgs = [...PINNED_DIFF_ARGS, "-U0", "--relative", start.commit];
  const seen = newFiles.length === 0 ? null : diffSeeingUntracked(projectRoot, diffArgs, newFiles);
  let diff;
  try {
    diff = seen ?? readGit(projectRoot, diffArgs);
  } catch {
    return null;
  }
  const added = Object.fromEntries(
    Object.entries(parseAddedLines(diff)).filter(([path]) => inChange.has(path))
  );
  if (seen !== null) return added;
  for (const path of newFiles) {
    const normalized = path.replace(/\\/g, "/");
    try {
      added[normalized] = readFileSync(join2(projectRoot, normalized), "utf8");
    } catch {
    }
  }
  return added;
}
function isPaqadArtifactPath(filePath) {
  const normalized = filePath.replace(/\\/g, "/");
  return normalized === ".paqad" || normalized.startsWith(".paqad/") || normalized.includes("/.paqad/");
}

// src/feature-evidence/bundle-document.ts
init_esm_shims();
import { mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "fs";
var UNKNOWN_DOCUMENT_SESSION = "unknown";
function documentSessionId(projectRoot, dirName, sessionId) {
  const given = sessionId?.trim();
  if (given) return given;
  return readFeatureRecord(projectRoot, dirName)?.session_id ?? UNKNOWN_DOCUMENT_SESSION;
}
function stampFeatureDocument(input) {
  return buildDocumentEnvelope({
    docType: input.docType,
    change: featureChangeKey(input.dirName),
    sessionId: documentSessionId(input.projectRoot, input.dirName, input.sessionId),
    schemaVersion: input.schemaVersion,
    now: input.now,
    body: input.body
  });
}
function readFeatureIndexEntries(projectRoot, dirName, file, key) {
  try {
    const parsed = JSON.parse(
      readFileSync2(join(projectRoot, featureFilePath(dirName, file)), "utf8")
    );
    return Array.isArray(parsed[key]) ? parsed[key] : null;
  } catch {
    return null;
  }
}
function writeFeatureIndexDocument(input) {
  const rel = featureFilePath(input.dirName, input.file);
  const current = readFeatureIndexEntries(
    input.projectRoot,
    input.dirName,
    input.file,
    input.key
  );
  if (current === null && input.entries.length === 0) {
    return { path: null, written: false };
  }
  if (current !== null && JSON.stringify(current) === JSON.stringify(input.entries)) {
    return { path: rel, written: false };
  }
  const doc = stampFeatureDocument({
    projectRoot: input.projectRoot,
    dirName: input.dirName,
    docType: input.docType,
    schemaVersion: input.schemaVersion,
    sessionId: input.sessionId,
    body: { [input.key]: input.entries },
    now: input.now
  });
  const abs = join(input.projectRoot, rel);
  mkdirSync(dirname(abs), { recursive: true });
  const tmp = `${abs}.tmp-${process.pid}`;
  writeFileSync(tmp, `${JSON.stringify(doc, null, 2)}
`, "utf8");
  renameSync(tmp, abs);
  return { path: rel, written: true };
}

// src/feature-evidence/delivery.ts
var DELIVERY_DOC_TYPE = "paqad.delivery";
var DELIVERY_SCHEMA_VERSION = 2;
var TICKET_ACTION_IDS = [
  "on_pr_open",
  "on_ci_green",
  "on_hld_post"
];
function emptyCommitDecision() {
  return { asked: false, answer: null, recorded_at: null };
}
function emptyDelivery() {
  return {
    schema_version: DELIVERY_SCHEMA_VERSION,
    doc_type: DELIVERY_DOC_TYPE,
    commits: [],
    head_sha: null,
    merge_commit: null,
    commit_decision: emptyCommitDecision(),
    last_link_attempt: null
  };
}
function isDelivery(value) {
  if (typeof value !== "object" || value === null) return false;
  const r = value;
  return r.doc_type === DELIVERY_DOC_TYPE && Array.isArray(r.commits);
}
function atomicWriteJson(absPath, value) {
  mkdirSync2(dirname2(absPath), { recursive: true });
  const tmp = `${absPath}.tmp`;
  writeFileSync2(tmp, `${JSON.stringify(value, null, 2)}
`, "utf8");
  renameSync2(tmp, absPath);
}
function readFeatureDelivery(projectRoot, dirName) {
  try {
    const parsed = JSON.parse(
      readFileSync3(join3(projectRoot, featureFilePath(dirName, "delivery")), "utf8")
    );
    if (isDelivery(parsed)) return parsed;
  } catch {
  }
  return emptyDelivery();
}
function writeFeatureDelivery(projectRoot, dirName, record, recordedAt, sessionId) {
  if (record.branch || record.base_branch) {
    const current = readFeatureRecord(projectRoot, dirName);
    recordBranch(
      projectRoot,
      dirName,
      current?.branch ? null : record.branch,
      current?.base_branch ? null : record.base_branch
    );
  }
  const stamped = stampFeatureDocument({
    projectRoot,
    dirName,
    docType: DELIVERY_DOC_TYPE,
    schemaVersion: DELIVERY_SCHEMA_VERSION,
    sessionId,
    now: recordedAt === void 0 ? void 0 : () => new Date(recordedAt),
    body: {
      commits: record.commits,
      head_sha: record.head_sha,
      merge_commit: record.merge_commit,
      commit_decision: record.commit_decision ?? emptyCommitDecision(),
      last_link_attempt: record.last_link_attempt ?? null,
      start_commit: record.start_commit ?? null,
      ...record.start_worktree ? { start_worktree: record.start_worktree } : {},
      ...record.start_worktree_truncated ? { start_worktree_truncated: true } : {},
      commit_split: record.commit_split ?? null,
      push: record.push ?? null,
      pr: record.pr ?? null,
      ...record.extra_prs && record.extra_prs.length > 0 ? { extra_prs: record.extra_prs } : {},
      ci: record.ci ?? null,
      ...record.ci_read ? { ci_read: record.ci_read } : {},
      ...record.ticket_actions && record.ticket_actions.length > 0 ? { ticket_actions: record.ticket_actions } : {}
    }
  });
  atomicWriteJson(join3(projectRoot, featureFilePath(dirName, "delivery")), stamped);
  return stamped;
}
function featureDeliveryBranch(projectRoot, dirName) {
  return readChangeConstants(projectRoot, dirName).branch ?? readFeatureDelivery(projectRoot, dirName).branch ?? null;
}
function recordBranch(projectRoot, dirName, branch, baseBranch) {
  const patch = {};
  if (branch) patch.branch = branch;
  if (baseBranch) patch.base_branch = baseBranch;
  if (Object.keys(patch).length > 0) {
    updateFeatureRecord(projectRoot, dirName, patch);
  }
}
function refreshCommitSplit(projectRoot, dirName, record) {
  try {
    const feature = readFeatureRecord(projectRoot, dirName);
    const fork = forkBranchOf(projectRoot, readGitState(projectRoot).branch);
    record.commit_split = computeCommitSplit(projectRoot, {
      baseBranch: fork ?? feature?.base_branch ?? void 0,
      startCommit: record.start_commit ?? null,
      ownTickets: changeOwnTickets(feature?.issue, feature?.branch ?? null)
    });
  } catch {
  }
  return record;
}
function seedFeatureDelivery(projectRoot, dirName, input) {
  const record = readFeatureDelivery(projectRoot, dirName);
  record.commit_decision = record.commit_decision ?? emptyCommitDecision();
  if (!record.start_commit && input.startCommit) {
    record.start_commit = input.startCommit;
    const worktree = captureStartWorktree(projectRoot);
    if (worktree) {
      record.start_worktree = worktree.files;
      if (worktree.truncated) record.start_worktree_truncated = true;
    }
  }
  refreshCommitSplit(projectRoot, dirName, record);
  return writeFeatureDelivery(projectRoot, dirName, record, input.recordedAt, input.sessionId);
}
var START_WORKTREE_CAP = 2e3;
function captureStartWorktree(projectRoot) {
  const changed = gitPathList(projectRoot, ["diff", "--name-only", "--relative", "-z", "HEAD"]);
  const untracked = gitPathList(projectRoot, ["ls-files", "--others", "--exclude-standard", "-z"]);
  if (changed === null || untracked === null) return null;
  const paths = [.../* @__PURE__ */ new Set([...changed, ...untracked])].filter((path) => !isPaqadArtifactPath(path)).sort();
  const files = {};
  for (const path of paths.slice(0, START_WORKTREE_CAP)) {
    files[path] = digestWorkingFile(projectRoot, path) ?? DELETED_AT_START;
  }
  return { files, truncated: paths.length > START_WORKTREE_CAP };
}
function readChangeStartPoint(projectRoot, dirName, fallbackBase) {
  const delivery = readFeatureDelivery(projectRoot, dirName);
  const worktree = delivery.start_worktree ?? null;
  const start = delivery.start_commit ?? null;
  const state = readGitState(projectRoot, fallbackBase ? { baseBranch: fallbackBase } : {});
  const fork = forkBranchOf(projectRoot, state.branch);
  if (start && git(projectRoot, ["merge-base", "--is-ancestor", start, "HEAD"]) !== void 0) {
    const merged = latestMergedBase(projectRoot, start, state.head_commit, [
      fork,
      state.base_branch
    ]);
    return {
      commit: merged.after ?? start,
      source: "start_commit",
      base: null,
      worktree,
      mergedBase: merged.after ? null : merged.beside
    };
  }
  const forkPoint = fork ? git(projectRoot, ["merge-base", "HEAD", fork]) : void 0;
  if (fork && forkPoint) {
    return { commit: forkPoint, source: "fork-point", base: fork, worktree };
  }
  if (state.base_commit) {
    return {
      commit: state.base_commit,
      source: "fallback-base",
      base: state.base_branch ?? null,
      worktree
    };
  }
  return null;
}
function latestMergedBase(projectRoot, start, head, bases) {
  const descends = (older, newer) => git(projectRoot, ["merge-base", "--is-ancestor", older, newer]) !== void 0;
  const latest = { after: null, beside: null };
  for (const base of new Set(bases.filter((name) => Boolean(name)))) {
    const point = git(projectRoot, ["merge-base", "HEAD", base]);
    if (!point || point === start || point === head || descends(point, start)) continue;
    const slot = descends(start, point) ? "after" : "beside";
    const current = latest[slot];
    if (current === null || descends(current, point)) latest[slot] = point;
  }
  return latest;
}
function setCommitDecision(projectRoot, dirName, answer, recordedAt) {
  const record = readFeatureDelivery(projectRoot, dirName);
  record.commit_decision = { asked: answer !== "user-requested", answer, recorded_at: recordedAt };
  return writeFeatureDelivery(projectRoot, dirName, record, recordedAt);
}
function recordLinkAttempt(projectRoot, dirName, reason, capturedAt) {
  const record = readFeatureDelivery(projectRoot, dirName);
  record.last_link_attempt = `${capturedAt}: ${reason}`;
  return writeFeatureDelivery(projectRoot, dirName, record, capturedAt);
}
function git(projectRoot, args) {
  try {
    return execFileSync2("git", args, {
      cwd: projectRoot,
      encoding: "utf8",
      stdio: ["ignore", "pipe", "ignore"]
    }).trim();
  } catch {
    return void 0;
  }
}
var FIELD_SEPARATOR = String.fromCharCode(31);
function commitsSinceBase(projectRoot, baseRef, headRef = "HEAD") {
  const range = baseRef ? `${baseRef}..${headRef}` : headRef;
  const out = git(projectRoot, ["log", range, "--format=%H%x1f%s"]);
  if (!out) return [];
  return out.split("\n").filter(Boolean).map((line) => {
    const [sha, ...rest] = line.split(FIELD_SEPARATOR);
    return { sha, subject: rest.join(FIELD_SEPARATOR) };
  });
}
function reconcileDeliveryFromGit(projectRoot, dirName, capturedAt, options = {}) {
  const state = readGitState(projectRoot, { baseBranch: options.baseBranch });
  const record = readFeatureDelivery(projectRoot, dirName);
  const seen = new Set(record.commits.map((c) => c.sha));
  for (const commit of commitsSinceBase(projectRoot, state.base_branch)) {
    if (!seen.has(commit.sha)) {
      record.commits.push(commit);
      seen.add(commit.sha);
    }
  }
  recordBranch(projectRoot, dirName, state.branch, state.base_branch);
  record.head_sha = state.head_commit ?? record.head_sha;
  refreshCommitSplit(projectRoot, dirName, record);
  return writeFeatureDelivery(projectRoot, dirName, record, capturedAt);
}
function stampMergeCommit(projectRoot, dirName, mergeSha, capturedAt) {
  const record = readFeatureDelivery(projectRoot, dirName);
  record.merge_commit = mergeSha;
  return writeFeatureDelivery(projectRoot, dirName, record, capturedAt);
}
function resolveDeliveryFeatureByBranch(projectRoot, branch, activeDirName) {
  const matches = listFeatureDirs(projectRoot).filter(
    (dirName) => featureDeliveryBranch(projectRoot, dirName) === branch
  );
  if (matches.length === 0) return null;
  if (activeDirName && matches.includes(activeDirName)) return activeDirName;
  const newest = (dirs) => [...dirs].sort((x, y) => featureChangeKey(x).localeCompare(featureChangeKey(y))).at(-1) ?? null;
  const notDone = matches.filter(
    (dirName) => !isFeatureFinished(readFeatureRecord(projectRoot, dirName)?.status)
  );
  return newest(notDone.length > 0 ? notDone : matches);
}
function recordCommitForBranch(projectRoot, active, commit, capturedAt) {
  const branch = git(projectRoot, ["branch", "--show-current"]);
  const dirName = (branch ? resolveDeliveryFeatureByBranch(projectRoot, branch, active) : null) ?? active;
  if (!dirName) return null;
  const record = readFeatureDelivery(projectRoot, dirName);
  if (featureDeliveryBranch(projectRoot, dirName) === null) {
    recordBranch(projectRoot, dirName, branch);
  }
  if (!record.commits.some((c) => c.sha === commit.sha)) {
    record.commits.push(commit);
  }
  record.head_sha = commit.sha;
  refreshCommitSplit(projectRoot, dirName, record);
  writeFeatureDelivery(projectRoot, dirName, record, capturedAt);
  return dirName;
}
var CI_RANK = { pass: 2, fail: 2, pending: 1, unknown: 0 };
function definedFields(patch) {
  return Object.fromEntries(
    Object.entries(patch).filter(([, value]) => value !== void 0)
  );
}
function mergeCi(previous, next) {
  if (!previous || previous.sha !== next.sha) return next;
  if (CI_RANK[next.state] < CI_RANK[previous.state]) return previous;
  return { ...previous, ...definedFields(next) };
}
function readingIsOlder(record, readAt) {
  if (readAt === void 0) return false;
  const started = Date.parse(readAt);
  return [record.pr?.at, record.ci?.at].some(
    (stored) => stored !== void 0 && Date.parse(stored) > started
  );
}
function recordShip(projectRoot, dirName, patch, at) {
  const record = readFeatureDelivery(projectRoot, dirName);
  const stale = readingIsOlder(record, patch.read_at);
  if (patch.push)
    record.push = { ...record.push, ...definedFields(patch.push), at };
  if (patch.ci_read) record.ci_read = { ...patch.ci_read, at };
  if (patch.pr && !stale) {
    record.pr = {
      number: null,
      url: null,
      draft: null,
      ...record.pr,
      ...definedFields(patch.pr),
      first_at: record.pr?.first_at ?? record.pr?.at ?? at,
      at
    };
  }
  if (patch.extra_prs) {
    record.extra_prs = patch.extra_prs.map((pr) => ({
      number: null,
      url: null,
      draft: null,
      ...definedFields(pr),
      at
    }));
  }
  if (patch.ci && !stale) record.ci = mergeCi(record.ci, { ...patch.ci, at });
  return writeFeatureDelivery(projectRoot, dirName, record, at);
}
function queueTicketAction(projectRoot, dirName, action, at) {
  const record = readFeatureDelivery(projectRoot, dirName);
  const others = (record.ticket_actions ?? []).filter((entry) => entry.id !== action.id);
  record.ticket_actions = [...others, action];
  return writeFeatureDelivery(projectRoot, dirName, record, at);
}
function settleTicketAction(projectRoot, dirName, id, state, reason, at) {
  const record = readFeatureDelivery(projectRoot, dirName);
  const actions = record.ticket_actions ?? [];
  const found = actions.find((entry) => entry.id === id);
  if (!found) {
    const known = actions.map((entry) => entry.id).join(", ") || "none";
    return {
      ok: false,
      message: `No ticket move ${id} is queued for this change (queued: ${known}).`
    };
  }
  const settled = { ...found, state, reason, at };
  record.ticket_actions = actions.map((entry) => entry.id === id ? settled : entry);
  writeFeatureDelivery(projectRoot, dirName, record, at);
  return { ok: true, action: settled };
}
function pendingTicketActions(record) {
  return (record.ticket_actions ?? []).filter((entry) => entry.state === "pending");
}

// src/feature-evidence/ledger-append.ts
init_esm_shims();
import { createHash as createHash2 } from "crypto";
import {
  closeSync,
  existsSync as existsSync3,
  fstatSync,
  fsyncSync,
  mkdirSync as mkdirSync4,
  openSync,
  readSync,
  readdirSync as readdirSync2,
  rmdirSync as rmdirSync2,
  writeSync
} from "fs";
import { dirname as dirname4, join as join5 } from "path";

// src/core/fs/file-lock.ts
init_esm_shims();
import { randomBytes } from "crypto";
import {
  existsSync as existsSync2,
  mkdirSync as mkdirSync3,
  readdirSync,
  renameSync as renameSync3,
  rmdirSync,
  statSync,
  unlinkSync,
  utimesSync,
  writeFileSync as writeFileSync3
} from "fs";
import { dirname as dirname3, join as join4 } from "path";
var FileLockTimeoutError = class extends Error {
  constructor(lockDir, timeoutMs) {
    super(
      `could not take the lock ${lockDir} within ${timeoutMs} ms; another paqad process holds it. Retry the command; if no other paqad command is running, remove that directory.`
    );
    this.name = "FileLockTimeoutError";
  }
};
function withFileLockSync(lockDir, fn, options = {}) {
  const timeoutMs = options.timeoutMs ?? 15e3;
  const staleMs = options.staleMs ?? 1e4;
  const retryMs = options.retryMs ?? 20;
  const now = options.now ?? Date.now;
  const sleep = options.sleep ?? sleepSync;
  const deadline = now() + timeoutMs;
  for (; ; ) {
    const outcome = tryAcquireLock(lockDir, { staleLockMs: staleMs, now });
    if (outcome.acquired) break;
    if (now() >= deadline) throw new FileLockTimeoutError(lockDir, timeoutMs);
    sleep(retryMs);
  }
  try {
    return fn();
  } finally {
    releaseLock(lockDir);
  }
}
var OWNER_PREFIX = "owner-";
var CLAIM_PREFIX = "claim-";
var FileLockLostError = class extends Error {
  constructor(lockDir) {
    super(`the lock ${lockDir} was taken over while this process was paused`);
    this.name = "FileLockLostError";
  }
};
function uniqueName(prefix) {
  return `${prefix}${process.pid}-${randomBytes(6).toString("hex")}`;
}
function holderIsDead(name) {
  const pid = Number(name.slice(OWNER_PREFIX.length).split("-")[0]);
  if (!Number.isInteger(pid) || pid <= 0) return false;
  try {
    process.kill(pid, 0);
    return false;
  } catch (error) {
    return error.code === "ESRCH";
  }
}
function tryTakeOwned(lockDir) {
  try {
    mkdirSync3(lockDir);
  } catch {
    return null;
  }
  const owner = uniqueName(OWNER_PREFIX);
  try {
    writeFileSync3(join4(lockDir, owner), "");
    if (readdirSync(lockDir).length === 1) return ownedLock(lockDir, owner);
    unlinkSync(join4(lockDir, owner));
    rmdirSync(lockDir);
  } catch {
  }
  return null;
}
function ownedLock(lockDir, owner) {
  const token = join4(lockDir, owner);
  return {
    assertHeld() {
      try {
        const at = /* @__PURE__ */ new Date();
        utimesSync(lockDir, at, at);
      } catch {
      }
      if (!existsSync2(token)) throw new FileLockLostError(lockDir);
    },
    release() {
      try {
        unlinkSync(token);
        rmdirSync(lockDir);
      } catch {
      }
    },
    movedTo: (moved) => ownedLock(moved, owner)
  };
}
function reclaimIfStale(lockDir, staleMs, now) {
  let names;
  let aged;
  try {
    names = readdirSync(lockDir);
    aged = now() - statSync(lockDir).mtimeMs > staleMs;
  } catch {
    return;
  }
  const owners = names.filter((name) => name.startsWith(OWNER_PREFIX));
  if (!aged && !(owners.length > 0 && owners.every(holderIsDead))) return;
  const claims = names.filter((name) => name.startsWith(CLAIM_PREFIX));
  try {
    for (const owner of owners) {
      const claim = uniqueName(CLAIM_PREFIX);
      renameSync3(join4(lockDir, owner), join4(lockDir, claim));
      claims.push(claim);
    }
    for (const claim of claims) unlinkSync(join4(lockDir, claim));
    rmdirSync(lockDir);
  } catch {
  }
}
function acquireOwnedFileLock(lockDir, options = {}) {
  const timeoutMs = options.timeoutMs ?? 15e3;
  const staleMs = options.staleMs ?? 1e4;
  const retryMs = options.retryMs ?? 20;
  const now = options.now ?? Date.now;
  const sleep = options.sleep ?? sleepSync;
  const deadline = now() + timeoutMs;
  for (let parentMade = false; ; parentMade = true) {
    const taken = tryTakeOwned(lockDir);
    if (taken) return taken;
    if (!parentMade) {
      try {
        mkdirSync3(dirname3(lockDir), { recursive: true });
      } catch {
      }
    }
    reclaimIfStale(lockDir, staleMs, now);
    const retaken = tryTakeOwned(lockDir);
    if (retaken) return retaken;
    if (now() >= deadline) throw new FileLockTimeoutError(lockDir, timeoutMs);
    sleep(retryMs);
  }
}
function withOwnedFileLockSync(lockDir, fn, options = {}) {
  const lock = acquireOwnedFileLock(lockDir, options);
  try {
    return fn(lock);
  } finally {
    lock.release();
  }
}

// src/stage-evidence/schema.ts
init_esm_shims();
var import_ajv = __toESM(require_ajv(), 1);
var nullableString = { type: ["string", "null"] };
var ROW_PROPERTIES = {
  stage: nullableString,
  event_status: {
    type: ["string", "null"],
    enum: ["started", "completed", "skipped", "failed", "redone", "inferred", null]
  },
  evidence_source: {
    type: ["string", "null"],
    enum: ["live-mark", "inferred-artifact", "inferred-git", "redo", null]
  },
  artifact_paths: { type: ["array", "null"], items: { type: "string" } },
  artifact_digest: nullableString,
  subject_digest: nullableString,
  agent: { type: "string", minLength: 1 },
  session_source: { type: ["string", "null"], enum: ["host", "env", "cache", null] },
  note: nullableString
};
var STAGE_EVIDENCE_SCHEMA_V22 = {
  $id: "paqad://schemas/stage-evidence-v2.json",
  type: "object",
  additionalProperties: false,
  required: [
    "schema_version",
    "doc_type",
    "change",
    "session_id",
    "recorded_at",
    "content_hash",
    "kind",
    "agent"
  ],
  properties: {
    ...ENVELOPE_HEADER_PROPERTIES,
    schema_version: { type: "integer", const: STAGE_EVIDENCE_SCHEMA_V2 },
    doc_type: { const: STAGE_EVIDENCE_DOC_TYPE },
    kind: {
      enum: [
        ...STAGE_FAMILY_KIND_LIST,
        STAGE_AGENT_KIND,
        SPEC_STEP_KIND,
        SPEC_CORRECTION_KIND,
        COMPLETION_DEFERRED_KIND,
        RECORD_REPAIR_KIND
      ]
    },
    ...ROW_PROPERTIES,
    reason: { type: "string", minLength: 1 },
    tasks: { type: "integer", minimum: 0 },
    agent_id: { type: "string", minLength: 1 },
    stages: { type: "array", items: { type: "string", minLength: 1 } },
    host_pid: { type: "integer", minimum: 1 },
    releases: { type: "array", items: { type: "string", minLength: 1 } },
    stage_agents_running: { type: "array", items: { type: "string", minLength: 1 } },
    tokens_used: { type: "integer", minimum: 0 },
    tokens_not_recarried: { type: "integer", minimum: 0 },
    estimate: { type: "boolean" },
    requested_effort: { enum: STAGE_EFFORT_LEVELS },
    requested_model: { enum: STAGE_MODELS },
    report_status: { enum: ["DONE", "DONE_WITH_CONCERNS", "NEEDS_CONTEXT", "BLOCKED"] },
    report_stated: { type: "boolean" },
    advance: { type: "boolean" },
    must_not: {
      type: "array",
      items: { enum: ["asked_human", "bundle_write", "out_of_stage_edit"] }
    },
    step: { type: "string", minLength: 1 },
    outcome: { enum: SPEC_STEP_OUTCOMES },
    artifact_hash: { type: "string" },
    tokens: { type: "integer", minimum: 0 },
    spec_id: { type: "string", minLength: 1 },
    changed_sections: { type: "array", items: { type: "string" } }
  },
  allOf: [
    {
      if: { properties: { kind: { const: STAGE_AGENT_KIND } }, required: ["kind"] },
      then: { required: ["stage", "tokens_used", "tokens_not_recarried", "estimate"] }
    },
    {
      if: { properties: { kind: { const: SPEC_STEP_KIND } }, required: ["kind"] },
      then: { required: ["step", "outcome", "artifact_hash"] }
    },
    {
      if: { properties: { kind: { const: SPEC_CORRECTION_KIND } }, required: ["kind"] },
      then: { required: ["spec_id", "changed_sections"] }
    },
    {
      if: { properties: { kind: { const: COMPLETION_DEFERRED_KIND } }, required: ["kind"] },
      then: { required: ["reason", "tasks"] }
    },
    {
      if: { properties: { kind: { const: RECORD_REPAIR_KIND } }, required: ["kind"] },
      then: { required: ["reason"] }
    }
  ]
};
var V3_ROW_PROPERTIES = {
  event_status: {
    type: ["string", "null"],
    enum: ["started", "completed", "skipped", "failed", "redone", "inferred", "denied", null]
  },
  idempotency_key: { type: "string", minLength: 1 },
  prev_hash: nullableString,
  chain_note: { type: "string", minLength: 1 },
  unchecked: { type: "string", minLength: 1 },
  scope: { type: "string", minLength: 1 },
  repairs_row: { type: "integer", minimum: 1 },
  repairs_hash: { type: "string", minLength: 1 },
  decision: nullableString,
  report_source: { enum: REPORT_SOURCES },
  report_hash: { type: "string", minLength: 1 },
  tool_use_id: { type: "string", minLength: 1 },
  refusal_id: { type: "string", minLength: 1 },
  stated_status: { enum: ["DONE", "DONE_WITH_CONCERNS", "NEEDS_CONTEXT", "BLOCKED"] },
  gate_checked: { type: "boolean" },
  api_turns: { type: "integer", minimum: 0 },
  recipe: { type: "string", minLength: 1 },
  recipe_hash: { type: "string", minLength: 1 },
  recipe_root: { type: "string", minLength: 1 },
  truncated: { type: "boolean" },
  source: { const: "script" },
  script: { type: "string", minLength: 1 },
  output_hash: { type: "string", minLength: 1 },
  stale_ok: { type: "array", items: { type: "string", minLength: 1 } }
};
var STAGE_EVIDENCE_SCHEMA = {
  ...STAGE_EVIDENCE_SCHEMA_V22,
  $id: "paqad://schemas/stage-evidence.json",
  properties: {
    ...STAGE_EVIDENCE_SCHEMA_V22.properties,
    ...V3_ROW_PROPERTIES,
    schema_version: { type: "integer", const: STAGE_EVIDENCE_SCHEMA_VERSION },
    kind: {
      enum: [
        ...STAGE_EVIDENCE_SCHEMA_V22.properties.kind.enum,
        STAGE_HANDBACK_KIND,
        RECIPE_INJECTED_KIND,
        SKILL_USED_KIND
      ]
    }
  },
  allOf: [
    ...STAGE_EVIDENCE_SCHEMA_V22.allOf,
    {
      if: { properties: { kind: { const: STAGE_HANDBACK_KIND } }, required: ["kind"] },
      then: { required: ["stage", "event_status", "agent_id"] }
    },
    {
      if: { properties: { kind: { const: RECIPE_INJECTED_KIND } }, required: ["kind"] },
      then: { required: ["stage", "recipe", "recipe_hash"] }
    },
    {
      if: { properties: { kind: { const: SKILL_USED_KIND } }, required: ["kind"] },
      then: { required: ["stage", "recipe", "source", "script", "output_hash"] }
    }
  ]
};
var STAGE_EVIDENCE_SCHEMA_V1 = {
  $id: "paqad://schemas/stage-evidence-v1.json",
  type: "object",
  additionalProperties: false,
  required: [
    "schema_version",
    "doc_type",
    "kind",
    "session_id",
    "conversation_ordinal",
    "ts",
    "adapter",
    "agent",
    "content_hash"
  ],
  properties: {
    schema_version: { type: "integer", const: 1 },
    doc_type: { const: STAGE_EVIDENCE_DOC_TYPE },
    session_id: { type: "string", minLength: 1 },
    conversation_ordinal: { type: "integer", minimum: 1 },
    ts: { type: "string", minLength: 1 },
    content_hash: { type: "string", minLength: 1 },
    kind: { enum: STAGE_FAMILY_KIND_LIST },
    ...ROW_PROPERTIES,
    adapter: { type: "string", minLength: 1 },
    lane: { type: ["string", "null"], enum: ["fast", "graduated", "full", null] },
    branch: nullableString
  }
};
var ajv = new import_ajv.default({ allErrors: true, allowUnionTypes: true });
var compiled;
function validator(row) {
  compiled ??= {
    current: ajv.compile(STAGE_EVIDENCE_SCHEMA),
    v2: ajv.compile(STAGE_EVIDENCE_SCHEMA_V22),
    v1: ajv.compile(STAGE_EVIDENCE_SCHEMA_V1)
  };
  const version = row?.schema_version;
  if (version === 1) return compiled.v1;
  return version === STAGE_EVIDENCE_SCHEMA_V2 ? compiled.v2 : compiled.current;
}
function formatValidationError(error) {
  return `${error.instancePath || "(root)"} ${error.message ?? "invalid"}`;
}
function validateStageEvidenceRow(row) {
  const validate = validator(row);
  if (validate(row)) {
    return [];
  }
  const errors = validate.errors;
  return errors.map(formatValidationError);
}

// src/feature-evidence/ledger-append.ts
var BUNDLE_LOCK_NAME = ".lock";
var LEDGER_LOCK_TIMEOUT_MS = 200;
var LEDGER_LOCK_STALE_MS = 6e4;
var LOST_LOCK_RETRIES = 3;
var DEDUPE_WINDOW_MS = 1e4;
var LOCK_TIMEOUT_NOTE = "lock-timeout";
var AFTER_TORN_LINE_NOTE = "after-torn-line";
var TAIL_BYTES = 256 * 1024;
var BODY_HASH_EXCLUDED = /* @__PURE__ */ new Set([
  "ts",
  "recorded_at",
  "note",
  "prev_hash",
  "chain_note",
  "content_hash",
  "idempotency_key",
  "host_pid",
  "artifact_paths"
]);
function bundleLockPath(projectRoot, dirName) {
  return join5(projectRoot, featureDir(dirName), BUNDLE_LOCK_NAME);
}
function bodyHash(row) {
  const body = {};
  for (const key of Object.keys(row).sort()) {
    if (!BODY_HASH_EXCLUDED.has(key)) body[key] = row[key];
  }
  return createHash2("sha256").update(JSON.stringify(body)).digest("hex");
}
function textField(value) {
  return typeof value === "string" && value !== "" ? value : null;
}
function rowKey(change, sessionId, row) {
  const owner = textField(row.agent_id) ?? sessionId;
  const stage = textField(row.stage) ?? "-";
  const status = textField(row.event_status) ?? "-";
  const id = textField(row.tool_use_id) ?? textField(row.refusal_id);
  const final = id ?? bodyHash(row);
  return {
    key: `${change}:${owner}:${stage}:${String(row.kind)}:${status}:${final}`,
    byValue: id !== null
  };
}
function readFileTail(path) {
  let fd;
  try {
    fd = openSync(path, "r");
  } catch (error) {
    if (error.code === "ENOENT") return { text: "", endsClean: true };
    throw error;
  }
  try {
    const size = fstatSync(fd).size;
    const length = Math.min(size, TAIL_BYTES);
    const buffer = Buffer.alloc(length);
    readSync(fd, buffer, 0, length, size - length);
    const text = buffer.toString("utf8");
    return { text, endsClean: text === "" || text.endsWith("\n") };
  } finally {
    closeSync(fd);
  }
}
function parseRow(line) {
  try {
    const parsed = JSON.parse(line);
    return parsed && typeof parsed === "object" ? parsed : null;
  } catch {
    return null;
  }
}
function newestRow(lines) {
  for (let index = lines.length - 1; index >= 0; index -= 1) {
    const row = parseRow(lines[index]);
    if (row !== null) return row;
  }
  return null;
}
function sameStageMoved(row, candidate) {
  return (row.stage ?? null) === (candidate.stage ?? null) && row.kind !== candidate.kind;
}
function existingDuplicate(lines, key, nowMs, candidate) {
  const stored = key.byValue ? JSON.stringify(key.key).slice(1, -1) : null;
  for (let index = lines.length - 1; index >= 0; index -= 1) {
    const line = lines[index];
    if (stored !== null && !line.includes(stored)) continue;
    const row = parseRow(line);
    if (row === null) continue;
    if (row.idempotency_key !== key.key) {
      if (!key.byValue && sameStageMoved(row, candidate)) return null;
      continue;
    }
    if (key.byValue) return row;
    const at = Date.parse(String(row.recorded_at ?? ""));
    return !Number.isNaN(at) && Math.abs(nowMs - at) <= DEDUPE_WINDOW_MS ? row : null;
  }
  return null;
}
function openForAppend(path, beforeCreate) {
  for (let created = false; ; created = true) {
    try {
      return openSync(path, "a");
    } catch (error) {
      if (created || error.code !== "ENOENT") throw error;
    }
    beforeCreate();
    mkdirSync4(dirname4(path), { recursive: true });
  }
}
function writeLine(path, line, guard) {
  const fd = openForAppend(path, guard);
  try {
    guard();
    writeSync(fd, line);
    fsyncSync(fd);
  } finally {
    closeSync(fd);
  }
}
function appendRow(input, lock) {
  const { projectRoot, sessionId, dirName, now } = input;
  const row = {
    ...input.row,
    agent: input.row.agent ?? ORCHESTRATOR_AGENT
  };
  const path = join5(projectRoot, featureFilePath(dirName, "stageEvidence"));
  const change = featureChangeKey(dirName);
  const key = rowKey(change, sessionId, row);
  const tail = readFileTail(path);
  const lines = tail.text.split("\n");
  const stampedAt = (now ?? (() => /* @__PURE__ */ new Date()))();
  const duplicate = row.kind === "verify" ? null : existingDuplicate(lines, key, stampedAt.getTime(), row);
  if (duplicate) return duplicate;
  const locked = lock !== null;
  const chain = {
    prev_hash: locked ? textField(newestRow(lines)?.content_hash) : null,
    ...locked ? {} : { chain_note: LOCK_TIMEOUT_NOTE },
    ...locked && !tail.endsClean ? { chain_note: AFTER_TORN_LINE_NOTE } : {}
  };
  const stamped = stampBundleRow({
    docType: STAGE_EVIDENCE_DOC_TYPE,
    change,
    sessionId,
    schemaVersion: STAGE_EVIDENCE_SCHEMA_VERSION,
    row: { ...row, idempotency_key: key.key, ...chain },
    validate: (candidate) => validateStageEvidenceRow(candidate),
    now: () => stampedAt
  });
  const guard = () => {
    if (lock) {
      lock.assertHeld();
      return;
    }
    const target = renamedBundle(projectRoot, dirName);
    if (target !== null) throw new BundleMoved(target);
  };
  writeLine(path, `${tail.endsClean ? "" : "\n"}${JSON.stringify(stamped)}
`, guard);
  return stamped;
}
function renamedBundle(projectRoot, dirName) {
  if (existsSync3(join5(projectRoot, featureFilePath(dirName, "stageEvidence")))) return null;
  const ulid = parseFeatureDirName(dirName)?.ulid;
  if (!ulid) return null;
  const base = dirname4(join5(projectRoot, featureDir(dirName)));
  let names;
  try {
    names = readdirSync2(base);
  } catch {
    return null;
  }
  const sameChange = names.filter((name) => parseFeatureDirName(name)?.ulid === ulid);
  const holding = (file) => (name) => existsSync3(join5(projectRoot, featureFilePath(name, file)));
  const target = sameChange.find(holding("stageEvidence")) ?? sameChange.find(holding("feature")) ?? sameChange.sort()[0];
  return target === void 0 || target === dirName ? null : target;
}
var BundleMoved = class extends Error {
  constructor(target) {
    super(`bundle moved to ${target}`);
    this.target = target;
  }
  target;
};
function appendWithoutLock(input) {
  const target = renamedBundle(input.projectRoot, input.dirName);
  if (target !== null) return appendWithoutLock({ ...input, dirName: target });
  try {
    return appendRow(input, null);
  } catch (error) {
    if (error instanceof BundleMoved) {
      return appendWithoutLock({ ...input, dirName: error.target });
    }
    throw error;
  }
}
function appendChainedStageRow(input) {
  const { projectRoot, dirName } = input;
  const moved = renamedBundle(projectRoot, dirName);
  if (moved !== null) return appendChainedStageRow({ ...input, dirName: moved });
  for (let attempt = 0; attempt < LOST_LOCK_RETRIES; attempt += 1) {
    let entered = false;
    try {
      return withOwnedFileLockSync(
        bundleLockPath(projectRoot, dirName),
        (lock) => {
          entered = true;
          const target = renamedBundle(projectRoot, dirName);
          if (target !== null) throw new BundleMoved(target);
          return appendRow(input, lock);
        },
        { timeoutMs: LEDGER_LOCK_TIMEOUT_MS, staleMs: LEDGER_LOCK_STALE_MS }
      );
    } catch (error) {
      if (error instanceof BundleMoved) {
        try {
          rmdirSync2(join5(projectRoot, featureDir(dirName)));
        } catch {
        }
        return appendChainedStageRow({ ...input, dirName: error.target });
      }
      if (error instanceof FileLockLostError) {
        const target = renamedBundle(projectRoot, dirName);
        if (target !== null) return appendChainedStageRow({ ...input, dirName: target });
        continue;
      }
      if (entered && !(error instanceof FileLockTimeoutError)) throw error;
      break;
    }
  }
  return appendWithoutLock(input);
}

// src/feature-evidence/stage-ledger.ts
function resolveActiveFeature(projectRoot, sessionId, input = {}) {
  if (input.title !== void 0) {
    return mintAndActivate(projectRoot, sessionId, input.title, input);
  }
  const active = reconcileSessionControl(projectRoot, sessionId, input.now);
  if (active) {
    return active;
  }
  const inFlight = adoptableInFlightOnBranch(projectRoot, sessionId, input.now);
  if (inFlight.length >= 2) {
    throw new Error(
      `stage isolation: ${inFlight.length} in-flight feature bundles on this branch (${inFlight.join(", ")}) \u2014 a change must keep one identity, so paqad will not mint a third. Close or finish the extra bundle(s), or pass an explicit change (stage start --title \u2026), before continuing.`
    );
  }
  if (inFlight.length === 0) {
    const held = heldInFlightOnBranch(projectRoot, sessionId, input.now);
    if (held.length > 0) {
      const running = held.map((h) => `${h.dirName} (stage ${h.stage})`).join(", ");
      throw new Error(
        `another session is running a stage in the change on this branch: ${running}. paqad will not take that change over or write into it. Pass --title "<what you are building>" to stage start to open this session's own change.`
      );
    }
  }
  return mintAndActivate(projectRoot, sessionId, UNTITLED_FEATURE_TITLE, input);
}
function mintAndActivate(projectRoot, sessionId, title, input) {
  const minted = mintFeatureDirName({
    title,
    issue: input.issue,
    ulid: input.ulid,
    ulidSeed: input.ulidSeed
  });
  setActiveFeature(projectRoot, sessionId, minted.dirName, { lane: input.lane, now: input.now });
  return minted.dirName;
}
function featureStagePath(dirName) {
  return featureFilePath(dirName, "stageEvidence");
}
function appendFeatureStageRow(projectRoot, sessionId, dirName, row, now) {
  return appendChainedStageRow({ projectRoot, sessionId, dirName, row, now });
}
function recordChangeConstants(projectRoot, dirName, input, now) {
  const patch = {};
  if (input.adapter && input.adapter !== BACKSTOP_WRITER) patch.adapter = input.adapter;
  if (input.lane) patch.lane = input.lane;
  if (input.lane && input.laneSource) patch.laneSource = input.laneSource;
  if (input.branch) patch.branch = input.branch;
  if (input.baseBranch) patch.base_branch = input.baseBranch;
  if (input.kind) patch.kind = input.kind;
  if (Object.keys(patch).length > 0) {
    updateFeatureRecord(projectRoot, dirName, patch, now);
  }
}
function readFeatureStageUnit(projectRoot, dirName) {
  return readUnitFile(projectRoot, featureStagePath(dirName));
}
function foldFeature(projectRoot, sessionId, dirName) {
  const rows = readFeatureStageUnit(projectRoot, dirName);
  const fold = foldRowsWithKey(rows, {
    sessionId,
    changeKey: dirName,
    promptOrdinal: 0,
    lane: readChangeConstants(projectRoot, dirName, rows).lane
  });
  return augmentWithBundleArtifacts(fold, {
    plan: bundleFileNonEmpty(projectRoot, dirName, "plan"),
    specification: bundleFileNonEmpty(projectRoot, dirName, "specification")
  });
}
function recordedLaneOrFull(projectRoot, sessionId, dirName) {
  if (!dirName) return "full";
  try {
    return foldFeature(projectRoot, sessionId, dirName).lane ?? "full";
  } catch {
    return "full";
  }
}
function bundleFileNonEmpty(projectRoot, dirName, file) {
  try {
    return readFileSync4(join6(projectRoot, featureFilePath(dirName, file))).length > 0;
  } catch {
    return false;
  }
}
function currentFeature(projectRoot, sessionId, now) {
  return reconcileSessionControl(projectRoot, sessionId, now);
}
function resolveActiveChange(projectRoot, hint) {
  const sessionId = resolveSessionId(
    projectRoot,
    hint ?? process.env.SE_SESSION ?? process.env.CLAUDE_SESSION_ID ?? null
  );
  const dirName = currentFeature(projectRoot, sessionId);
  return dirName ? { dirName, sessionId, key: featureChangeKey(dirName) } : null;
}
function activeChangeKey(projectRoot, hint) {
  return resolveActiveChange(projectRoot, hint)?.key ?? null;
}
function peekActiveChangeKey(projectRoot, hint) {
  const sessionId = (hint ?? process.env.SE_SESSION ?? process.env.CLAUDE_SESSION_ID ?? "").trim();
  if (sessionId === "") return null;
  const active = readSessionControl(projectRoot, sessionId).active;
  if (active === null || !isBundleMaterialized(projectRoot, active)) return null;
  if (isBundleClosed(projectRoot, active)) return null;
  return featureChangeKey(active);
}
function openFeatureChange(projectRoot, sessionId, input) {
  const dirName = resolveActiveFeature(projectRoot, sessionId, input);
  const gitState = readGitState(projectRoot);
  const seeded = seedFeatureRecord(projectRoot, dirName, {
    adapter: input.adapter,
    sessionId,
    lane: input.lane ?? null,
    branch: gitState.branch ?? null,
    baseBranch: gitState.base_branch ?? null,
    kind: input.kind,
    now: input.now
  });
  if (seeded) {
    recordChangeConstants(
      projectRoot,
      dirName,
      {
        adapter: input.adapter,
        lane: input.lane ?? null,
        branch: gitState.branch ?? null,
        baseBranch: gitState.base_branch ?? null,
        kind: input.kind
      },
      input.now
    );
  }
  const hasOpen = readFeatureStageUnit(projectRoot, dirName).some((row) => row.kind === "open");
  if (!hasOpen) {
    appendFeatureStageRow(projectRoot, sessionId, dirName, { kind: "open" }, input.now);
    try {
      seedFeatureDelivery(projectRoot, dirName, {
        sessionId,
        recordedAt: (input.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
        startCommit: gitState.head_commit ?? null
      });
    } catch {
    }
  }
  return dirName;
}
function closeActiveFeature(projectRoot, sessionId, now) {
  const active = currentFeature(projectRoot, sessionId);
  if (!active) {
    return;
  }
  const rows = readFeatureStageUnit(projectRoot, active);
  if (rows.length > 0 && !isBundleClosed(projectRoot, active)) {
    appendFeatureStageRow(
      projectRoot,
      sessionId,
      active,
      {
        kind: "close",
        event_status: "completed",
        note: "closed; active pointer released"
      },
      now
    );
  }
  updateFeatureRecord(projectRoot, active, { status: "done" }, now);
  markDone(projectRoot, sessionId, active, now);
}
function resolveFeatureRef(projectRoot, sessionId, ref) {
  const control = readSessionControl(projectRoot, sessionId);
  const known = [...control.paused].reverse();
  if (control.active) {
    known.push(control.active);
  }
  const onDisk = listFeatureDirs(projectRoot).filter((dirName) => !known.includes(dirName));
  const needle = ref.trim().replace(/^#/, "");
  for (const candidates of [known, onDisk]) {
    const match = matchFeatureRef(candidates, ref, needle);
    if (match) {
      return match;
    }
  }
  return null;
}
function matchFeatureRef(candidates, ref, needle) {
  for (const dirName of candidates) {
    if (dirName === ref || dirName === needle) {
      return dirName;
    }
    const parts = parseFeatureDirName(dirName);
    if (!parts) {
      continue;
    }
    if (parts.ulid === needle || parts.issue === needle || parts.slug === needle) {
      return dirName;
    }
  }
  for (const dirName of candidates) {
    const parts = parseFeatureDirName(dirName);
    if (parts && parts.slug.includes(needle)) {
      return dirName;
    }
  }
  return null;
}
function resumeFeatureByRef(projectRoot, sessionId, ref, now) {
  const dirName = resolveFeatureRef(projectRoot, sessionId, ref);
  if (!dirName) {
    return null;
  }
  if (isBundleClosed(projectRoot, dirName)) {
    appendFeatureStageRow(
      projectRoot,
      sessionId,
      dirName,
      { kind: "open", note: "reopened via resume" },
      now
    );
    updateFeatureRecord(projectRoot, dirName, { status: "active" }, now);
  }
  if (resumeFeature(projectRoot, sessionId, dirName, now)) {
    return dirName;
  }
  setActiveFeature(projectRoot, sessionId, dirName, { now, writtenBy: "resume" });
  return dirName;
}

export {
  PINNED_DIFF_ARGS,
  gitPathList,
  changedFilesSince,
  addedLinesSince,
  isPaqadArtifactPath,
  documentSessionId,
  stampFeatureDocument,
  readFeatureIndexEntries,
  writeFeatureIndexDocument,
  TICKET_ACTION_IDS,
  readFeatureDelivery,
  refreshCommitSplit,
  readChangeStartPoint,
  setCommitDecision,
  recordLinkAttempt,
  reconcileDeliveryFromGit,
  stampMergeCommit,
  resolveDeliveryFeatureByBranch,
  recordCommitForBranch,
  recordShip,
  queueTicketAction,
  settleTicketAction,
  pendingTicketActions,
  FileLockTimeoutError,
  withFileLockSync,
  acquireOwnedFileLock,
  BUNDLE_LOCK_NAME,
  LEDGER_LOCK_STALE_MS,
  bundleLockPath,
  resolveActiveFeature,
  featureStagePath,
  appendFeatureStageRow,
  recordChangeConstants,
  readFeatureStageUnit,
  foldFeature,
  recordedLaneOrFull,
  currentFeature,
  resolveActiveChange,
  activeChangeKey,
  peekActiveChangeKey,
  openFeatureChange,
  closeActiveFeature,
  resolveFeatureRef,
  resumeFeatureByRef
};