UNPKG

paqad-ai

Version:

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

2,157 lines • 74.3 kB
import { createRequire as __paqadCreateRequire } from 'module'; const require = __paqadCreateRequire(import.meta.url);
import {
  tokenize
} from "./chunk-CECHOUNN.js";
import {
  readProjectProfile
} from "./chunk-AK3HYELM.js";
import {
  DECISION_CATEGORIES,
  DECISION_CATEGORY_DEFAULTS,
  earlierCheckToKeep,
  isDecisionPacket,
  isStrictDecisionId,
  validateDecisionPacket
} from "./chunk-6XDDIYNJ.js";
import {
  readProjectEvents,
  recordProjectEvent
} from "./chunk-I2G54BH7.js";
import {
  DecisionPacketCorruptError
} from "./chunk-D6VEUSB6.js";
import {
  require_ajv
} from "./chunk-ACJQW7SB.js";
import {
  renameWithRetrySync
} from "./chunk-Z3NGWHFX.js";
import {
  getRuntimeRoot
} from "./chunk-YWAORJG6.js";
import {
  appendSessionEvent,
  currentOrdinal,
  openSessionDoc
} from "./chunk-Q353EDKY.js";
import {
  resolveSessionId
} from "./chunk-SG6LXLDH.js";
import {
  ulid
} from "./chunk-AZLA53A6.js";
import {
  PATHS
} from "./chunk-IHMBVS6H.js";
import {
  __toESM,
  init_esm_shims
} from "./chunk-IDFRWBMN.js";

// src/planning/decision-audit.ts
init_esm_shims();
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
import { dirname, join } from "path";
var DECISION_AUDIT_EVENTS = [
  "decision-pending-written",
  "decision-resolved-by-human",
  "decision-resolved-by-rule",
  "decision-resolved-by-memoization",
  "decision-resolved-by-rag-confident",
  "decision-reused",
  "decision-expired",
  "decision-superseded",
  "decision-delegated",
  "undeclared-decision-flagged",
  "decision-discarded"
];
function appendDecisionAuditEvent(projectRoot, event) {
  const path = join(projectRoot, PATHS.DECISIONS_AUDIT_LOG);
  mkdirSync(dirname(path), { recursive: true });
  appendFileSync(path, `${JSON.stringify(event)}
`);
  return path;
}
function ensureDecisionAuditLog(projectRoot) {
  const path = join(projectRoot, PATHS.DECISIONS_AUDIT_LOG);
  mkdirSync(dirname(path), { recursive: true });
  writeFileSync(path, "", { flag: "a" });
  return path;
}
function readDecisionAuditEvents(projectRoot) {
  const path = join(projectRoot, PATHS.DECISIONS_AUDIT_LOG);
  if (!existsSync(path)) {
    return [];
  }
  return readFileSync(path, "utf8").split("\n").map((line) => line.trim()).filter(Boolean).map((line) => JSON.parse(line));
}

// src/planning/decision-events.ts
init_esm_shims();
var DECISION_PAUSE_EVENT_TYPES = [
  "decision-paused",
  "decision-resolved",
  "decision-packet-corrupt",
  "decision-cap-exceeded",
  "decision-discarded"
];
function isoNow() {
  return (/* @__PURE__ */ new Date()).toISOString();
}
function toEventOption(option) {
  return {
    option_key: option.option_key,
    label: option.label,
    one_line_preview: option.one_line_preview,
    trade_off: option.trade_off,
    ...option.technical_detail !== void 0 ? { technical_detail: option.technical_detail } : {}
  };
}
function decisionPausedEvent(packet, packetPath2) {
  return {
    kind: "decision-paused",
    at: isoNow(),
    decisionId: packet.decision_id,
    category: packet.category,
    prompt: packet.question,
    question: packet.question,
    options: packet.options.map(toEventOption),
    recommendation: packet.recommendation ?? null,
    ...packet.recommendation_reason !== void 0 ? { recommendationReason: packet.recommendation_reason } : {},
    packetPath: packetPath2,
    ...packet.linked_slice_id !== void 0 ? { linkedSliceId: packet.linked_slice_id } : {}
  };
}
function decisionResolvedEvent(packet, resolver) {
  return {
    kind: "decision-resolved",
    at: isoNow(),
    decisionId: packet.decision_id,
    resolution: packet.status,
    chosenOptionKey: packet.human_response?.chosen_option_key ?? null,
    resolver,
    ...packet.human_response?.intent !== void 0 ? { intent: packet.human_response.intent } : {}
  };
}
function decisionPacketCorruptEvent(decisionId, reason) {
  return { kind: "decision-packet-corrupt", at: isoNow(), decisionId, reason };
}
function decisionCapExceededEvent(pendingCount, cap) {
  return { kind: "decision-cap-exceeded", at: isoNow(), pendingCount, cap };
}
function decisionDiscardedEvent(decisionId, reason) {
  return { kind: "decision-discarded", at: isoNow(), decisionId, reason };
}

// src/planning/decision-fingerprint.ts
init_esm_shims();
import { createHash } from "crypto";
var STOP_WORDS = /* @__PURE__ */ new Set([
  "a",
  "an",
  "and",
  "are",
  "for",
  "i",
  "is",
  "it",
  "of",
  "or",
  "should",
  "the",
  "this",
  "to",
  "we",
  "which",
  "you"
]);
var TOKEN_EQUIVALENTS = {
  choose: "choose",
  pick: "choose",
  use: "choose"
};
function computeDecisionFingerprint(input) {
  const payload = [
    input.category,
    normalizeDecisionQuestion(input.question),
    [...input.option_keys].sort().join("|"),
    buildRepoStateSignature(input.repo_state)
  ].join("::");
  return `sha256:${createHash("sha256").update(payload).digest("hex")}`;
}
function normalizeDecisionQuestion(value) {
  return tokenize2(value).map((token) => TOKEN_EQUIVALENTS[token] ?? token).filter((token) => !STOP_WORDS.has(token)).sort().join(" ");
}
function buildRepoStateSignature(input) {
  return JSON.stringify({
    active_capabilities: [...input.active_capabilities ?? []].sort(),
    packs: [...input.packs ?? []].sort(),
    stack: input.stack ?? null
  });
}
function scoreDecisionOptionOverlap(left, right) {
  const leftSet = new Set(left);
  const rightSet = new Set(right);
  const union = /* @__PURE__ */ new Set([...leftSet, ...rightSet]);
  if (union.size === 0) {
    return 1;
  }
  let intersection = 0;
  for (const value of leftSet) {
    if (rightSet.has(value)) {
      intersection += 1;
    }
  }
  return intersection / union.size;
}
function tokenize2(value) {
  return value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim().split(/\s+/).filter(Boolean);
}

// src/decisions/earlier-answers.ts
init_esm_shims();
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
import { dirname as dirname2, join as join3 } from "path";

// src/decision-reuse/index.ts
init_esm_shims();

// src/decision-reuse/types.ts
init_esm_shims();
var DECISION_REUSE_DOC_TYPE = "paqad.decision-reuse";
var DECISION_REUSE_SCHEMA_VERSION = 1;

// src/decision-reuse/schema.ts
init_esm_shims();
var import_ajv = __toESM(require_ajv(), 1);
var nullableString = { type: ["string", "null"] };
var DECISION_REUSE_SCHEMA = {
  $id: "paqad://schemas/decision-reuse.json",
  type: "object",
  additionalProperties: false,
  required: [
    "schema_version",
    "doc_type",
    "kind",
    "session_id",
    "conversation_ordinal",
    "ts",
    "adapter",
    "content_hash"
  ],
  properties: {
    schema_version: { type: "integer", const: 1 },
    doc_type: { const: DECISION_REUSE_DOC_TYPE },
    kind: { enum: ["open", "reuse", "suggestion", "standing-set", "standing-removed"] },
    session_id: { type: "string", minLength: 1 },
    conversation_ordinal: { type: "integer", minimum: 1 },
    ts: { type: "string", minLength: 1 },
    adapter: { type: "string", minLength: 1 },
    decision_id: nullableString,
    category: nullableString,
    chosen_option_key: nullableString,
    match_kind: {
      type: ["string", "null"],
      enum: ["exact", "fingerprint", "intent", "standing", "similar", null]
    },
    source_path: nullableString,
    note: nullableString,
    outcome: { type: ["string", "null"], enum: ["accepted", "changed", "none", null] },
    change: nullableString,
    for_decision: nullableString,
    rule_id: nullableString,
    content_hash: { type: "string", minLength: 1 }
  }
};
var ajv = new import_ajv.default({ allErrors: true, allowUnionTypes: true });
var compiled;
function validator() {
  if (!compiled) {
    compiled = ajv.compile(DECISION_REUSE_SCHEMA);
  }
  return compiled;
}
function validateDecisionReuseRow(row) {
  const validate = validator();
  if (validate(row)) {
    return [];
  }
  return (validate.errors ?? []).map(
    (error) => `${error.instancePath || "(root)"} ${error.message ?? "invalid"}`
  );
}

// src/decision-reuse/recorder.ts
init_esm_shims();
var APPEND_OPTS = (now) => ({
  schemaVersion: DECISION_REUSE_SCHEMA_VERSION,
  validate: (row) => validateDecisionReuseRow(row),
  now
});
function recordDecisionReuse(projectRoot, fields, ctx = {}) {
  try {
    const sessionId = resolveSessionId(
      projectRoot,
      ctx.sessionId ?? process.env.SE_SESSION ?? process.env.CLAUDE_SESSION_ID ?? null
    );
    const adapter = ctx.adapter ?? "unknown";
    const ordinal = ensureUnit(projectRoot, sessionId, adapter, ctx.now);
    return appendSessionEvent(
      projectRoot,
      DECISION_REUSE_DOC_TYPE,
      sessionId,
      ordinal,
      {
        kind: fields.kind ?? "reuse",
        conversation_ordinal: ordinal,
        adapter,
        decision_id: fields.decisionId,
        category: fields.category ?? null,
        chosen_option_key: fields.chosenOptionKey ?? null,
        match_kind: fields.matchKind ?? null,
        source_path: fields.sourcePath ?? null,
        note: fields.note ?? null,
        ...fields.outcome ? { outcome: fields.outcome } : {},
        ...fields.change ? { change: fields.change } : {},
        ...fields.forDecision ? { for_decision: fields.forDecision } : {},
        ...fields.ruleId ? { rule_id: fields.ruleId } : {}
      },
      APPEND_OPTS(ctx.now)
    );
  } catch {
    return null;
  }
}
function recordSuggestionOutcome(projectRoot, input, ctx = {}) {
  if (input.suggestion === void 0) return null;
  return recordDecisionReuse(
    projectRoot,
    {
      kind: "suggestion",
      decisionId: input.suggestion.earlier_id,
      matchKind: "intent",
      category: input.category,
      chosenOptionKey: input.chosen,
      note: input.suggestion.reason,
      outcome: input.chosen === input.suggestion.option_key ? "accepted" : "changed",
      change: input.change,
      forDecision: input.forDecision
    },
    ctx
  );
}
function ensureUnit(projectRoot, sessionId, adapter, now) {
  const current = currentOrdinal(projectRoot, DECISION_REUSE_DOC_TYPE, sessionId);
  if (current > 0) {
    return current;
  }
  return openSessionDoc(
    projectRoot,
    DECISION_REUSE_DOC_TYPE,
    sessionId,
    { adapter },
    APPEND_OPTS(now)
  ).ordinal;
}

// src/duplication/token-similarity.ts
init_esm_shims();
var DEFAULT_SHINGLE_SIZE = 4;
var SHINGLE_SEP = "\n";
function tokenizeCode(source) {
  const withoutComments = source.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\n]*/g, " ").replace(/#[^\n]*/g, " ");
  const withoutStrings = withoutComments.replace(/"(?:\\.|[^"\\])*"/g, '""').replace(/'(?:\\.|[^'\\])*'/g, "''").replace(/`(?:\\.|[^`\\])*`/g, "``");
  const tokens = withoutStrings.match(/[A-Za-z_$][A-Za-z0-9_$]*|[0-9]+|[^\sA-Za-z0-9_$]/g);
  return tokens ? tokens.map((token) => token.toLowerCase()) : [];
}
function shingles(tokens, size = DEFAULT_SHINGLE_SIZE) {
  const result = /* @__PURE__ */ new Set();
  if (tokens.length === 0) {
    return result;
  }
  if (tokens.length < size) {
    result.add(tokens.join(SHINGLE_SEP));
    return result;
  }
  for (let index = 0; index + size <= tokens.length; index += 1) {
    result.add(tokens.slice(index, index + size).join(SHINGLE_SEP));
  }
  return result;
}
function jaccard(left, right) {
  if (left.size === 0 || right.size === 0) {
    return 0;
  }
  let intersection = 0;
  for (const shingle of left) {
    if (right.has(shingle)) {
      intersection += 1;
    }
  }
  const union = left.size + right.size - intersection;
  return union === 0 ? 0 : intersection / union;
}

// src/planning/decision-precedents.ts
init_esm_shims();
import { readdirSync, readFileSync as readFileSync2 } from "fs";
import { join as join2 } from "path";

// src/decisions/packet-shape.ts
init_esm_shims();
var DAY_MS = 24 * 60 * 60 * 1e3;
var REQUESTED_BY = "paqad-decision-cli";
var STANDING_ANSWER_RESPONDER = "standing-answer";
function isDecisionCategory(value) {
  return DECISION_CATEGORIES.includes(value);
}
function richFieldsFor(packet) {
  if (!isDecisionCategory(packet.category)) return null;
  if (!Array.isArray(packet.options) || packet.options.length < 2 || packet.options.length > 5) {
    return null;
  }
  const createdMs = Date.parse(packet.created_at);
  const ttlDays = DECISION_CATEGORY_DEFAULTS[packet.category].ttl_days;
  const options = packet.options.map((option) => ({
    ...option,
    one_line_preview: option.one_line_preview ?? option.label,
    trade_off: option.trade_off ?? "Not stated when the decision was opened.",
    evidence: option.evidence ?? {}
  }));
  const fields = {
    decision_id: packet.id,
    fingerprint: computeDecisionFingerprint({
      category: packet.category,
      question: packet.title,
      option_keys: packet.options.map((option) => option.option_key),
      repo_state: {}
    }),
    question: packet.title,
    options,
    ...packet.recommendation ? { recommendation_reason: "Recommended by the agent that opened this decision." } : {},
    confidence: 1,
    requested_by: packet.origin ? `paqad-${packet.origin}` : REQUESTED_BY,
    task_session_id: packet.change ?? "no-active-change",
    status: packet.status,
    ttl_until: new Date((Number.isNaN(createdMs) ? 0 : createdMs) + ttlDays * DAY_MS).toISOString(),
    invalidation_watch: []
  };
  if (packet.status === "resolved" && typeof packet.chosen === "string") {
    fields.human_response = humanResponseFor(
      packet.chosen,
      packet.rationale,
      packet.resolved_at,
      packet.responded_by
    );
  }
  return fields;
}
function humanResponseFor(chosen, rationale, resolvedAt, respondedBy = "human") {
  return {
    chosen_option_key: chosen,
    intent: "explicit",
    explanation_rounds_used: 0,
    responded_at: resolvedAt ?? (/* @__PURE__ */ new Date(0)).toISOString(),
    responded_by: respondedBy,
    carry_over_scope: "task",
    ...rationale ? { note: rationale } : {}
  };
}
function readAnyDecisionPacket(value) {
  if (isDecisionPacket(value)) return value;
  if (typeof value !== "object" || value === null) return null;
  const candidate = value;
  if (typeof candidate.id !== "string" || typeof candidate.title !== "string" || typeof candidate.context !== "string" || typeof candidate.created_at !== "string" || candidate.status !== "pending" && candidate.status !== "resolved") {
    return null;
  }
  const rich = richFieldsFor(candidate);
  if (rich === null) return null;
  const upgraded = {
    ...candidate,
    ...rich,
    recommendation: candidate.recommendation ?? null
  };
  return isDecisionPacket(upgraded) ? upgraded : null;
}

// src/planning/decision-precedents.ts
var DEFAULT_PRECEDENT_LIMIT = 3;
var PRECEDENT_SCORE_FLOOR = 0.1;
var CATEGORY_WEIGHT = 0.5;
function jaccard2(a, b) {
  if (a.size === 0 || b.size === 0) {
    return 0;
  }
  let intersection = 0;
  for (const token of a) {
    if (b.has(token)) {
      intersection++;
    }
  }
  const union = a.size + b.size - intersection;
  return union === 0 ? 0 : intersection / union;
}
function scorePrecedent(query, packet) {
  const sameCategory = query.category === packet.category ? CATEGORY_WEIGHT : 0;
  const queryTokens = new Set(tokenize(`${query.question} ${query.context ?? ""}`));
  const packetTokens = new Set(tokenize(`${packet.question} ${packet.context}`));
  return sameCategory + (1 - CATEGORY_WEIGHT) * jaccard2(queryTokens, packetTokens);
}
function chosenLabel(packet) {
  const key = packet.human_response?.chosen_option_key;
  if (!key) {
    return null;
  }
  return packet.options.find((option) => option.option_key === key)?.label ?? key;
}
function readResolvedPackets(projectRoot) {
  const dir = join2(projectRoot, PATHS.DECISIONS_RESOLVED_DIR);
  let files;
  try {
    files = readdirSync(dir);
  } catch {
    return [];
  }
  const packets = [];
  for (const file of files) {
    if (!file.endsWith(".json")) {
      continue;
    }
    try {
      const packet = readAnyDecisionPacket(
        JSON.parse(readFileSync2(join2(dir, file), "utf8"))
      );
      if (packet) {
        packets.push(packet);
      }
    } catch {
    }
  }
  return packets;
}
function findDecisionPrecedents(projectRoot, query, options = {}) {
  const limit = options.limit ?? DEFAULT_PRECEDENT_LIMIT;
  const floor = options.floor ?? PRECEDENT_SCORE_FLOOR;
  const candidates = readResolvedPackets(projectRoot).filter(
    (packet) => packet.decision_id !== query.decision_id && (!query.fingerprint || packet.fingerprint !== query.fingerprint) && (packet.status === "resolved" || packet.status === "delegated") && Boolean(packet.human_response?.chosen_option_key)
  );
  return candidates.map((packet) => ({ packet, score: scorePrecedent(query, packet) })).filter((entry) => entry.score >= floor).sort(
    (a, b) => b.score !== a.score ? b.score - a.score : a.packet.decision_id < b.packet.decision_id ? -1 : 1
  ).slice(0, limit).map(({ packet, score }) => ({
    decision_id: packet.decision_id,
    category: packet.category,
    question: packet.question,
    chosen: chosenLabel(packet),
    rationale: packet.human_response?.note?.trim() || void 0,
    resolved_at: packet.human_response?.responded_at,
    score
  }));
}
var PRECEDENT_BLOCK_HEADING = "Similar past decisions you already resolved:";
function formatDecisionPrecedents(precedents) {
  if (precedents.length === 0) {
    return "";
  }
  const lines = precedents.map((precedent) => {
    const choice = precedent.chosen ?? "no choice recorded";
    const why = precedent.rationale ? ` \u2014 ${precedent.rationale}` : "";
    return `- [${precedent.category}] "${precedent.question}" \u2192 ${choice}${why}`;
  });
  return `${PRECEDENT_BLOCK_HEADING}
${lines.join("\n")}`;
}

// src/decisions/earlier-answers.ts
var EARLIER_SCORE_FLOOR = 0.5;
var EARLIER_LIMIT = 5;
var EARLIER_TITLE_LIMIT = 200;
var EARLIER_TITLE_MAX_CHARS = 140;
var EARLIER_METHOD_MAX_CHARS = 2e3;
var EARLIER_OUTPUT_MAX_CHARS = 24e3;
var PRIVATE_ANSWER_CATEGORIES = [
  "authoring.override",
  "authoring.agent_permission",
  "authoring.risky_script",
  "analytics.pii_consent"
];
function foldQuestionText(question) {
  return question.trim().toLowerCase().replace(/\s+/gu, " ");
}
function questionWords(question) {
  return new Set(normalizeDecisionQuestion(question).split(" ").filter(Boolean));
}
function mayBeOffered(packet, query) {
  if (packet.decision_id === query.excludeId) return false;
  if (packet.status !== "resolved" && packet.status !== "delegated") return false;
  if (!packet.human_response?.chosen_option_key) return false;
  return !PRIVATE_ANSWER_CATEGORIES.includes(packet.category) || packet.category === query.category;
}
function sameQuestionKey(packet) {
  const optionKeys = packet.options.map((option) => option.option_key).sort();
  return JSON.stringify([foldQuestionText(packet.question), optionKeys]);
}
function isNewerAnswer(packet, held) {
  const byTime = Date.parse(packet.human_response.responded_at) - Date.parse(held.human_response.responded_at);
  return byTime !== 0 ? byTime > 0 : packet.decision_id > held.decision_id;
}
function newestOfEachQuestion(packets) {
  const newest = /* @__PURE__ */ new Map();
  for (const packet of packets) {
    const key = sameQuestionKey(packet);
    const held = newest.get(key);
    if (held === void 0 || isNewerAnswer(packet, held)) newest.set(key, packet);
  }
  return [...newest.values()];
}
function toCandidate(packet) {
  const response = packet.human_response;
  const chosen = response.chosen_option_key;
  return {
    id: packet.decision_id,
    category: packet.category,
    question: packet.question,
    context: packet.context,
    chosen,
    chosen_label: packet.options.find((option) => option.option_key === chosen).label,
    why: response.note?.trim() ?? "",
    resolved_at: response.responded_at
  };
}
function compareBestFirst(left, right) {
  if (left.score !== right.score) return right.score - left.score;
  const byTime = Date.parse(right.resolved_at) - Date.parse(left.resolved_at);
  return byTime !== 0 ? byTime : left.id.localeCompare(right.id);
}
function eligiblePackets(projectRoot, query) {
  const now = (query.now ?? /* @__PURE__ */ new Date()).getTime();
  const offered = readResolvedPackets(projectRoot).filter((packet) => mayBeOffered(packet, query));
  return newestOfEachQuestion(offered).filter((packet) => Date.parse(packet.ttl_until) >= now);
}
function findEarlierQuestions(projectRoot, query) {
  const words = questionWords(query.question);
  if (words.size === 0) return [];
  return eligiblePackets(projectRoot, query).map((packet) => ({
    ...toCandidate(packet),
    score: jaccard(words, questionWords(packet.question))
  })).filter((earlier) => earlier.score >= EARLIER_SCORE_FLOOR).sort(compareBestFirst).slice(0, EARLIER_LIMIT);
}
function earlierCheckFor(projectRoot, query) {
  try {
    const found = findEarlierQuestions(projectRoot, query);
    if (found.length === 0) return null;
    return { status: "required", candidates: found.map((earlier) => earlier.id) };
  } catch {
    return null;
  }
}
function eligibleTitles(projectRoot, query) {
  return eligiblePackets(projectRoot, query).sort((left, right) => isNewerAnswer(left, right) ? -1 : 1).map((packet) => ({
    id: packet.decision_id,
    category: packet.category,
    question: packet.question
  }));
}
function listEarlierTitles(projectRoot, query) {
  return eligibleTitles(projectRoot, query).slice(0, EARLIER_TITLE_LIMIT);
}
function printedTitle(title) {
  const points = Array.from(title.question);
  return points.length > EARLIER_TITLE_MAX_CHARS ? { ...title, question: `${points.slice(0, EARLIER_TITLE_MAX_CHARS).join("")}\u2026` } : title;
}
function titlesThatFit(id, path, eligible) {
  const empty = {
    id,
    path,
    earlier_check: titleBlock(id, [], eligible.length)
  };
  let used = printDecisionOutput(empty).length;
  const fit = [];
  for (const title of eligible.slice(0, EARLIER_TITLE_LIMIT)) {
    used += JSON.stringify(printedTitle(title)).length + 1;
    if (used >= EARLIER_OUTPUT_MAX_CHARS) break;
    fit.push(title);
  }
  return fit;
}
function titleCheckFor(projectRoot, query) {
  try {
    const eligible = eligibleTitles(projectRoot, query);
    if (eligible.length === 0) return null;
    const fit = titlesThatFit(query.excludeId, query.path, eligible);
    const omitted = eligible.length - fit.length;
    return {
      status: "required",
      listed_by: "titles",
      candidates: fit.map((title) => title.id),
      ...omitted > 0 ? { older_omitted: omitted } : {}
    };
  } catch {
    return null;
  }
}
var METHOD_RELATIVE_PATH = "base/skills/decision/references/earlier-answers.md";
var EARLIER_METHOD_FALLBACK = "Scan the titles and read up to 5 that may be the same question with paqad-ai decision check <id> --read <D-id> ... Then judge meaning: compare the earlier reason with the situation now, treat shared wording as no evidence, treat different wording as no evidence either, and answer --none when unsure.";
function readEarlierMethod(runtimeRoot = getRuntimeRoot()) {
  try {
    const text = readFileSync3(join3(runtimeRoot, METHOD_RELATIVE_PATH), "utf8").trim();
    return text === "" || text.length > EARLIER_METHOD_MAX_CHARS ? EARLIER_METHOD_FALLBACK : text;
  } catch {
    return EARLIER_METHOD_FALLBACK;
  }
}
function pendingFile(projectRoot, id) {
  return join3(projectRoot, PATHS.DECISIONS_PENDING_DIR, `${id}.json`);
}
function readPendingFile(projectRoot, id) {
  if (!isStrictDecisionId(id) || !existsSync2(pendingFile(projectRoot, id))) return null;
  return JSON.parse(readFileSync3(pendingFile(projectRoot, id), "utf8"));
}
function earlierCheckRefusal(id) {
  return `This question has earlier answers to check first. Run paqad-ai decision check ${id}.`;
}
function assertEarlierCheckDone(projectRoot, id) {
  if (readPendingFile(projectRoot, id)?.earlier_check?.status === "required") {
    throw new Error(earlierCheckRefusal(id));
  }
}
function readCandidate(projectRoot, id) {
  const path = join3(projectRoot, PATHS.DECISIONS_RESOLVED_DIR, `${id}.json`);
  try {
    const packet = readAnyDecisionPacket(JSON.parse(readFileSync3(path, "utf8")));
    if (!packet?.human_response?.chosen_option_key) return null;
    return toCandidate(packet);
  } catch {
    return null;
  }
}
function earlierCheckBlock(projectRoot, id, check) {
  const { found, missing } = readOffered(projectRoot, check.candidates ?? []);
  return {
    required: true,
    candidates: found,
    ...missing.length > 0 ? { missing } : {},
    method: readEarlierMethod(),
    next: suggestCommand(id)
  };
}
function readOffered(projectRoot, ids) {
  const found = [];
  const missing = [];
  for (const offeredId of ids) {
    const candidate = readCandidate(projectRoot, offeredId);
    if (candidate) found.push(candidate);
    else missing.push(offeredId);
  }
  return { found, missing };
}
function suggestCommand(id) {
  return `paqad-ai decision suggest ${id} --earlier <D-id> --option <key> --reason <text>, or --none --reason <text>`;
}
function titleBlock(id, titles, olderOmitted = 0, missing = []) {
  return {
    required: true,
    method: readEarlierMethod(),
    next: `Read up to ${EARLIER_LIMIT} with paqad-ai decision check ${id} --read <D-id> ... Then ${suggestCommand(id)}`,
    ...olderOmitted > 0 ? { older_omitted: olderOmitted } : {},
    ...missing.length > 0 ? { missing } : {},
    titles
  };
}
function checkBlockFor(projectRoot, id, check) {
  if (check.listed_by !== "titles") return earlierCheckBlock(projectRoot, id, check);
  const { found, missing } = readOffered(projectRoot, check.candidates ?? []);
  const titles = found.map(
    ({ id: earlierId, category, question }) => printedTitle({ id: earlierId, category, question })
  );
  return titleBlock(id, titles, check.older_omitted, missing);
}
function printDecisionOutput(output) {
  const check = output.earlier_check;
  return check !== void 0 && "titles" in check ? JSON.stringify(output) : JSON.stringify(output, null, 2);
}
var NO_STANDING_ANSWER_CATEGORIES = [
  ...PRIVATE_ANSWER_CATEGORIES,
  "spec.change",
  "architecture-path",
  "spec.contradiction",
  "analytics.taxonomy_violation",
  "analytics.new_event"
];
var ALWAYS_LABEL = "Always use this answer for this kind of question";
var MAX_ALWAYS_OPTIONS = 3;
var MAX_QUESTION_ENTRIES = 4;
function shellArg(value) {
  return /^[\w:.@/=-]+$/u.test(value) ? value : `'${value.replace(/'/gu, `'\\''`)}'`;
}
function resolveCommand(id, key) {
  return `paqad-ai decision resolve ${id} ${shellArg(key)}`;
}
function plainEntry(id, option) {
  return {
    label: option.label,
    option_key: option.option_key,
    resolve: resolveCommand(id, option.option_key)
  };
}
function suggestedEntries(id, packet, suggestion) {
  const suggested = packet.options.find((option) => option.option_key === suggestion.option_key);
  if (suggested === void 0) return null;
  const first = {
    ...plainEntry(id, suggested),
    label: `Same as last time: ${suggested.label} (from ${suggestion.earlier_id})`
  };
  const rest = packet.options.filter((option) => option.option_key !== suggestion.option_key).map((option) => plainEntry(id, option));
  const always = canOfferAlways(packet) ? [
    {
      label: ALWAYS_LABEL,
      option_key: suggested.option_key,
      resolve: `${resolveCommand(id, suggested.option_key)} --always`,
      always: true
    }
  ] : [];
  return [first, ...rest, ...always];
}
function canOfferAlways(packet) {
  return packet.id !== void 0 && packet.options.length <= MAX_ALWAYS_OPTIONS && !NO_STANDING_ANSWER_CATEGORIES.includes(packet.category);
}
function buildQuestionBlock(id, packet) {
  const entries = (packet.suggestion && suggestedEntries(id, packet, packet.suggestion)) ?? packet.options.map((option) => plainEntry(id, option));
  const question = packet.title ?? packet.question ?? "";
  const options = entries.slice(0, MAX_QUESTION_ENTRIES);
  if (packet.id === void 0) {
    const notShown = entries.slice(MAX_QUESTION_ENTRIES);
    return { question, options, ...notShown.length > 0 ? { not_shown: notShown } : {} };
  }
  return {
    question,
    options,
    other: `paqad-ai decision resolve ${id} other --other "<text>"`
  };
}
function requirePendingFile(projectRoot, id) {
  const packet = readPendingFile(projectRoot, id);
  if (packet !== null) return packet;
  const resolved = isStrictDecisionId(id) && existsSync2(join3(projectRoot, PATHS.DECISIONS_RESOLVED_DIR, `${id}.json`));
  throw new Error(
    resolved ? `Decision ${id} is already resolved.` : `Decision ${id} was not found.`
  );
}
function checkDecision(projectRoot, id) {
  const packet = requirePendingFile(projectRoot, id);
  if (packet.earlier_check?.status === "required") {
    return { id, earlier_check: checkBlockFor(projectRoot, id, packet.earlier_check) };
  }
  return { id, earlier_check: { required: false }, question: buildQuestionBlock(id, packet) };
}
function readEarlierAnswers(projectRoot, id, ids) {
  const check = requirePendingFile(projectRoot, id).earlier_check;
  if (check === void 0) {
    throw new Error(`Decision ${id} has no earlier answers to read.`);
  }
  const wanted = [...new Set(ids)];
  if (wanted.length > EARLIER_LIMIT) {
    throw new Error(
      `Read at most ${EARLIER_LIMIT} earlier answers per call. Got ${wanted.length}.`
    );
  }
  const offered = check.candidates ?? [];
  const notOffered = wanted.filter((earlier) => !offered.includes(earlier));
  if (notOffered.length > 0) {
    throw new Error(`${notOffered.join(", ")} was not offered for ${id}, so it cannot be read.`);
  }
  const { found, missing } = readOffered(projectRoot, wanted);
  return { id, read: found, ...missing.length > 0 ? { missing } : {} };
}
function standingSayLine(label, ruleId) {
  return `I used your standing answer: ${label} (rule ${ruleId}), so I did not ask. Say "ask me anyway" to be asked this time (--ask-anyway), or "remove that rule" to stop it (paqad-ai decision standing remove ${ruleId}).`;
}
function formatCreateOutput(projectRoot, result) {
  if (result.reused) {
    return {
      reused: true,
      id: result.id,
      chosen: result.reused.chosen,
      label: result.reused.label,
      match: "standing",
      say: standingSayLine(result.reused.label, result.reused.rule)
    };
  }
  const check = readPendingFile(projectRoot, result.id)?.earlier_check;
  if (!check) return { id: result.id, path: result.path };
  return {
    id: result.id,
    path: result.path,
    earlier_check: checkBlockFor(projectRoot, result.id, check)
  };
}
function writePendingFile(projectRoot, id, packet) {
  const path = pendingFile(projectRoot, id);
  mkdirSync2(dirname2(path), { recursive: true });
  const tempPath = `${path}.tmp`;
  writeFileSync2(tempPath, `${JSON.stringify(packet, null, 2)}
`);
  renameWithRetrySync(tempPath, path);
}
function assertSuggestable(projectRoot, id, packet, input) {
  const check = packet.earlier_check;
  if (input.none) return;
  const earlier = input.earlier;
  const offered = check.candidates ?? [];
  if (!offered.includes(earlier)) {
    const list = check.listed_by === "titles" ? "" : ` Offered: ${offered.join(", ")}.`;
    throw new Error(`${earlier} was not offered for ${id}.${list}`);
  }
  if (readCandidate(projectRoot, earlier) === null) {
    throw new Error(`${earlier} can no longer be read, so it cannot be suggested for ${id}.`);
  }
  if (!packet.options.some((option) => option.option_key === input.option)) {
    const keys = packet.options.map((option) => option.option_key).join(", ");
    throw new Error(`Option "${input.option}" is not one of ${id}'s options (${keys}).`);
  }
}
function suggestEarlierAnswer(projectRoot, id, input) {
  const reason = input.reason?.trim() ?? "";
  if (Boolean(input.earlier) === Boolean(input.none)) {
    throw new Error("Give exactly one of --earlier <D-id> or --none.");
  }
  if (input.earlier && !input.option) {
    throw new Error("--earlier needs --option <key>.");
  }
  if (reason === "") {
    throw new Error("A suggestion needs a non-empty --reason.");
  }
  const packet = requirePendingFile(projectRoot, id);
  if (packet.earlier_check === void 0) {
    throw new Error(`Decision ${id} has no earlier answers to check.`);
  }
  if (packet.earlier_check.status !== "required") {
    throw new Error(`Decision ${id} has already been checked.`);
  }
  assertSuggestable(projectRoot, id, packet, input);
  writePendingFile(
    projectRoot,
    id,
    input.none ? withNoneFound(packet, reason) : withSuggestion(packet, input, reason)
  );
  if (input.none) {
    recordDecisionReuse(projectRoot, {
      kind: "suggestion",
      decisionId: null,
      category: packet.category,
      note: reason,
      outcome: "none",
      change: packet.change,
      forDecision: id
    });
  }
  return checkDecision(projectRoot, id);
}
function withNoneFound(packet, reason) {
  return {
    ...packet,
    earlier_check: { ...packet.earlier_check, status: "done", none_reason: reason }
  };
}
function withSuggestion(packet, input, reason) {
  const optionKey = input.option;
  return {
    ...packet,
    earlier_check: { ...packet.earlier_check, status: "done" },
    suggestion: {
      earlier_id: input.earlier,
      option_key: optionKey,
      reason,
      at: (/* @__PURE__ */ new Date()).toISOString()
    },
    recommendation: optionKey,
    recommendation_reason: reason
  };
}

// src/planning/decision-copy.ts
init_esm_shims();
var BANNED_WORDS = [
  "abstraction",
  "polymorphic",
  "dry",
  "solid",
  "coupling",
  "cohesion",
  "encapsulation",
  "idiomatic",
  "refactor",
  "hoist",
  "monomorphic",
  "deterministic",
  "idempotent",
  "cardinality",
  "invariant",
  "predicate",
  "instantiation"
];
function lintDecisionCopy(packet) {
  const issues = [];
  if (wordCount(packet.question) > 15) {
    issues.push({ field: "question", message: "Question must be 15 words or fewer." });
  }
  if (!isGrade8Friendly(packet.question)) {
    issues.push({ field: "question", message: "Question must stay at grade-8 readability." });
  }
  checkBanned("question", packet.question, issues);
  if (packet.recommendation_reason) {
    checkBanned("recommendation_reason", packet.recommendation_reason, issues);
    if (!isGrade8Friendly(packet.recommendation_reason)) {
      issues.push({
        field: "recommendation_reason",
        message: "Recommendation reason must stay at grade-8 readability."
      });
    }
  }
  for (const [index, option] of packet.options.entries()) {
    const prefix = `options[${index}]`;
    const labelWords = wordCount(option.label);
    if (labelWords < 2 || labelWords > 5) {
      issues.push({ field: `${prefix}.label`, message: "Option label must be 2 to 5 words." });
    }
    if (!startsWithVerb(option.label)) {
      issues.push({ field: `${prefix}.label`, message: "Option label must start with a verb." });
    }
    if (!/^if you pick this,/i.test(option.one_line_preview)) {
      issues.push({
        field: `${prefix}.one_line_preview`,
        message: 'One-line preview must start with "If you pick this,".'
      });
    }
    if (!/^You give up:\s/i.test(option.trade_off)) {
      issues.push({
        field: `${prefix}.trade_off`,
        message: 'Trade-off must start with "You give up:".'
      });
    }
    checkBanned(`${prefix}.label`, option.label, issues);
    checkBanned(`${prefix}.one_line_preview`, option.one_line_preview, issues);
    checkBanned(`${prefix}.trade_off`, option.trade_off, issues);
    if (!isGrade8Friendly(option.label)) {
      issues.push({
        field: `${prefix}.label`,
        message: "Option label must stay at grade-8 readability."
      });
    }
  }
  return issues;
}
function checkBanned(field, value, issues) {
  const normalized = value.toLowerCase();
  for (const banned of BANNED_WORDS) {
    if (normalized.includes(banned)) {
      issues.push({ field, message: `Banned word "${banned}" is not allowed here.` });
    }
  }
}
function wordCount(value) {
  return value.trim().split(/\s+/).filter(Boolean).length;
}
function isGrade8Friendly(value) {
  return value.split(/\s+/).filter(Boolean).every((word) => word.replace(/[^a-z]/gi, "").length <= 12);
}
function startsWithVerb(value) {
  return /^(Reuse|Make|Keep|Extract|Take|Use|Switch|Stay|Skip|Pick)\b/i.test(value.trim());
}

// src/planning/decision-store.ts
init_esm_shims();
import {
  existsSync as existsSync3,
  mkdirSync as mkdirSync3,
  readFileSync as readFileSync4,
  readdirSync as readdirSync2,
  renameSync,
  statSync,
  unlinkSync,
  writeFileSync as writeFileSync3
} from "fs";
import { basename, dirname as dirname3, join as join4, relative } from "path";

// src/planning/decision-ledger.ts
init_esm_shims();
var DECISION_EVIDENCE_DOC_TYPE = "decision-evidence";
var DECISION_EVIDENCE_SCHEMA_VERSION = 1;
function recordDecisionOpened(projectRoot, fields) {
  recordProjectEvent(
    projectRoot,
    DECISION_EVIDENCE_DOC_TYPE,
    {
      kind: "opened",
      decision_id: fields.decisionId,
      category: fields.category,
      title: fields.title,
      created_at: fields.createdAt
    },
    DECISION_EVIDENCE_SCHEMA_VERSION
  );
}
function recordDecisionResolved(projectRoot, decisionId, status, resolver) {
  recordProjectEvent(
    projectRoot,
    DECISION_EVIDENCE_DOC_TYPE,
    { kind: "resolved", decision_id: decisionId, status, resolver },
    DECISION_EVIDENCE_SCHEMA_VERSION
  );
}
function recordDecisionSuperseded(projectRoot, decisionId) {
  recordProjectEvent(
    projectRoot,
    DECISION_EVIDENCE_DOC_TYPE,
    { kind: "superseded", decision_id: decisionId },
    DECISION_EVIDENCE_SCHEMA_VERSION
  );
}
function recordDecisionDiscarded(projectRoot, decisionId, reason) {
  recordProjectEvent(
    projectRoot,
    DECISION_EVIDENCE_DOC_TYPE,
    { kind: "discarded", decision_id: decisionId, reason },
    DECISION_EVIDENCE_SCHEMA_VERSION
  );
}
function recordDecisionExpired(projectRoot, decisionId) {
  recordProjectEvent(
    projectRoot,
    DECISION_EVIDENCE_DOC_TYPE,
    { kind: "expired", decision_id: decisionId },
    DECISION_EVIDENCE_SCHEMA_VERSION
  );
}
function readDecisionEvidence(projectRoot) {
  const latest = /* @__PURE__ */ new Map();
  for (const row of readProjectEvents(projectRoot, DECISION_EVIDENCE_DOC_TYPE)) {
    const id = row.decision_id;
    const kind = row.kind;
    if (typeof id === "string" && typeof kind === "string") {
      latest.set(id, { kind, row });
    }
  }
  const pending = [];
  let resolvedCount = 0;
  let expiredCount = 0;
  for (const [id, { kind, row }] of latest) {
    if (kind === "opened") {
      const title = typeof row.title === "string" && row.title.length > 0 ? row.title : id;
      const createdAt = typeof row.created_at === "string" && row.created_at.length > 0 ? row.created_at : null;
      pending.push({ id, title, createdAt });
    } else if (kind === "resolved" || kind === "superseded") {
      resolvedCount += 1;
    } else if (kind === "expired") {
      expiredCount += 1;
    }
  }
  return { pending, resolvedCount, expiredCount };
}

// src/planning/decision-store.ts
var MAX_PENDING_DECISIONS = 20;
var DecisionCapExceededError = class extends Error {
  constructor(pendingCount, cap) {
    super(`Pending decision cap reached (${pendingCount}/${cap}).`);
    this.pendingCount = pendingCount;
    this.cap = cap;
    this.name = "DecisionCapExceededError";
  }
  pendingCount;
  cap;
};
var DecisionStore = class {
  constructor(projectRoot, options = {}) {
    this.projectRoot = projectRoot;
    this.onEvent = options.onEvent;
  }
  projectRoot;
  onEvent;
  initialize() {
    for (const relativePath of [
      PATHS.DECISIONS_PENDING_DIR,
      PATHS.DECISIONS_RESOLVED_DIR,
      PATHS.DECISIONS_EXPIRED_DIR
    ]) {
      const dir = join4(this.projectRoot, relativePath);
      mkdirSync3(dir, { recursive: true });
      writeFileSync3(join4(dir, ".gitkeep"), "", { flag: "a" });
    }
    ensureDecisionAuditLog(this.projectRoot);
    if (!existsSync3(this.indexPath())) {
      this.writeIndex({ fingerprints: {}, decisions: {} });
    }
  }
  nextDecisionId() {
    return `D-${ulid()}`;
  }
  writePending(packet) {
    this.initialize();
    const existingTaskPending = this.findPendingDecisionForTask(packet.task_session_id);
    if (existingTaskPending && existingTaskPending !== packet.decision_id) {
      throw new Error(
        `Task ${packet.task_session_id} already has a pending decision (${existingTaskPending}).`
      );
    }
    this.enforcePendingCap(packet.decision_id);
    this.assertWritablePacket(packet);
    const enriched = this.withEarlierCheck(this.enrichWithPrecedents(packet));
    const path = this.packetPath(PATHS.DECISIONS_PENDING_DIR, enriched.decision_id);
    atomicWriteJson(path, enriched);
    this.appendAudit("decision-pending-written", enriched);
    recordDecisionOpened(this.projectRoot, {
      decisionId: enriched.decision_id,
      category: enriched.category,
      title: enriched.question,
      createdAt: enriched.created_at
    });
    this.emit(decisionPausedEvent(enriched, this.relativePendingPath(enriched.decision_id)));
    return path;
  }
  enrichWithPrecedents(packet) {
    if (packet.context.includes(PRECEDENT_BLOCK_HEADING)) {
      return packet;
    }
    try {
      const precedents = findDecisionPrecedents(this.projectRoot, {
        decision_id: packet.decision_id,
        fingerprint: packet.fingerprint,
        category: packet.category,
        question: packet.question,
        context: packet.context
      });
      const block = formatDecisionPrecedents(precedents);
      return block ? { ...packet, context: `${packet.context}

${block}` } : packet;
    } catch {
      return packet;
    }
  }
  withEarlierCheck(packet) {
    if (packet.earlier_check !== void 0) {
      return packet;
    }
    const check = earlierCheckFor(this.projectRoot, {
      question: packet.question,
      excludeId: packet.decision_id,
      category: packet.category
    });
    return check === null ? packet : { ...packet, earlier_check: check };
  }
  enforcePendingCap(decisionId) {
    const cap = this.maxPendingDecisions();
    const pendingCount = this.listPendingDecisionIds().filter((id) => id !== decisionId).length;
    if (pendingCount >= cap) {
      this.emit(decisionCapExceededEvent(pendingCount, cap));
      throw new DecisionCapExceededError(pendingCount, cap);
    }
  }
  maxPendingDecisions() {
    const configured = readProjectProfile(this.projectRoot)?.custom?.decisions?.max_pending;
    return typeof configured === "number" && configured > 0 ? configured : MAX_PENDING_DECISIONS;
  }
  discard(input) {
    this.initialize();
    const result = this.readPendingResult(input.decisionId);
    if (!result.packet) {
      throw new Error(
        `Cannot discard decision ${input.decisionId}: no valid pending packet${result.error ? ` (${result.error})` : ""}.`
      );
    }
    const packet = result.packet;
    this.deletePending(input.decisionId);
    this.appendAudit("decision-discarded", packet);
    recordDecisionDiscarded(this.projectRoot, packet.decision_id, input.reason);
    this.emit(decisionDiscardedEvent(packet.decision_id, input.reason));
    return packet;
  }
  emit(event) {
    this.onEvent?.(event);
  }
  relativePendingPath(decisionId) {
    return `${PATHS.DECISIONS_PENDING_DIR}/${decisionId}.json`;
  }
  resolve(input) {
    const pending = this.readPending(input.decisionId);
    if (!pending) {
      throw new Error(`Pending decision ${input.decisionId} not found.`);
    }
    const resolved = this.writeResolvedPacket({
      packet: pending,
      humanResponse: input.humanResponse,
      event: input.humanResponse.intent === "delegated" ? "decision-delegated" : "decision-resolved-by-human",
      respondedByProvider: input.respondedByProvider
    });
    unlinkSync(this.packetPath(PATHS.DECISIONS_PENDING_DIR, input.decisionId));
    return this.packetPath(PATHS.DECISIONS_RESOLVED_DIR, resolved.decision_id);
  }
  resolveExisting(input) {
    const resolved = this.writeResolvedPacket(input);
    return this.packetPath(PATHS.DECISIONS_RESOLVED_DIR, resolved.decision_id);
  }
  deferUndeclaredDecision(input) {
    const path = this.writePending(input.packet);
    this.appendAudit("undeclared-decision-flagged", input.packet, input.provider);
    return path;
  }
  findReusableDecision(packet) {
    const index = this.readIndex();
    const exactDecisionId = index.fingerprints[packet.fingerprint];
    if (exactDecisionId) {
      const exactMatch = this.readReusableDecision(exactDecisionId);
      if (exactMatch) {
        this.appendAudit("decision-reused", exactMatch);
        this.recordReuseToLedger(exactMatch, "exact");
        return exactDecisionId;
      }
      delete index.fingerprints[packet.fingerprint];
      this.writeIndex(index);
    }
    const packetOptionKeys = packet.options.map((option) => option.option_key);
    let bestMatch = null;
    for (const entry of Object.values(index.decisions)) {
      if (entry.category !== packet.category || entry.chosen_option_key === null) {
        continue;
      }
      if (!packetOptionKeys.includes(entry.chosen_option_key)) {
        continue;
      }
      const resolved = this.readReusableDecision(entry.decision_id);
      if (!resolved) {
        continue;
      }
      const score = scoreDecisionOptionOverlap(packetOptionKeys, entry.option_keys);
      if (score >= 0.8 && (bestMatch === null || score > bestMatch.score)) {
        bestMatch = { id: entry.decision_id, score };
      }
    }
    if (bestMatch) {
      const resolved = this.readResolved(bestMatch.id);
      if (resolved) {
        this.appendAudit("decision-reused", resolved);
        this.recordReuseToLedger(resolved, "fingerprint");
      }
    }
    return bestMatch?.id ?? null;
  }
  recordReuseToLedger(packet, matchKind) {
    recordDecisionReuse(
      this.projectRoot,
      {
        decisionId: packet.decision_id,
        category: packet.category,
        chosenOptionKey: packet.human_response?.chosen_option_key ?? null,
        matchKind,
        sourcePath: join4(PATHS.DECISIONS_RESOLVED_DIR, `${packet.decision_id}.json`)
      },
      {}
    );
  }
  readPending(decisionId) {
    return this.readPacket(this.packetPath(PATHS.DECISIONS_PENDING_DIR, decisionId));
  }
  readPendingResult(decisionId) {
    const path = this.packetPath(PATHS.DECISIONS_PENDING_DIR, decisionId);
    if (!existsSync3(path)) {
      return { packet: null };
    }
    try {
      return { packet: this.readPacket(path) };
    } catch (error) {
      return {
        packet: null,
        error: error instanceof Error ? error.message : String(error)
      };
    }
  }
  readResolved(decisionId) {
    return this.readPacket(this.packetPath(PATHS.DECISIONS_RESOLVED_DIR, decisionId));
  }
  deletePending(decisionId) {
    const path = this.packetPath(PATHS.DECISIONS_PENDING_DIR, decisionId);
    if (existsSync3(path)) {
      unlinkSync(path);
    }
  }
  listPendingDecisionIds() {
    return this.listDirectoryIds(PATHS.DECISIONS_PENDING_DIR).sort(
      (left, right) => left.localeCompare(right, void 0, { numeric: true })
    );
  }
  findPendingDecisionForTask(taskSessionId) {
    for (const decisionId of this.listPendingDecisionIds()) {
      const result = this.readPendingResult(decisionId);
      if (result.packet?.task_session_id === taskSessionId) {
        return decisionId;
      }
    }
    return null;
  }
  expireResolvedDecision(decisionId) {
    const source = this.packetPath(PATHS.DECISIONS_RESOLVED_DIR, decisionId);
    const target = this.packetPath(PATHS.DECISIONS_EXPIRED_DIR, decisionId);
    const packet = this.readResolved(decisionId);
    if (!packet) {
      throw new Error(`Resolved decision ${decisionId} not found.`);
    }
    const expired = { ...packet, status: "expired" };
    atomicWriteJson(target, expired);
    unlinkSync(source);
    const index = this.readIndex();
    if (index.fingerprints[packet.fingerprint] === decisionId) {
      delete index.fingerprints[packet.fingerprint];
    }
    if (index.decisions[decisionId]) {
      index.decisions[decisionId] = { ...index.decisions[decisionId], status: "expired" };
    }
    this.writeIndex(index);
    this.appendAudit("decision-expired", expired);
    recordDecisionExpired(this.projectRoot, expired.decision_id);
    return target;
  }
  tagRulesDiscovery(decisionId, tag) {
    const path = this.packetPath(PATHS.DECISIONS_RESOLVED_DIR, decisionId);
    if (!existsSync3(path)) {
      return "missing";
    }
    const raw = JSON.parse(readFileSync4(path, "utf8"));
    if (raw.rules_discovery !== void 0) {
      return "already";
    }
    atomicWriteJson(path, { ...raw, rules_discovery: tag });
    return "tagged";
  }
  hasInvalidation(packet) {
    if (!packet.human_response?.responded_at) {
      return false;
    }
    const respondedAt = new Date(packet.human_response.responded_at).getTime();
    return packet.invalidation_watch.some((path) => {
      const absolute = join4(this.projectRoot, path);
      return existsSync3(absolute) && statSync(absolute).mtimeMs > respondedAt;
    });
  }
  readPacket(path) {
    if (!existsSync3(path)) {
      return null;
    }
    const parsed = JSON.parse(readFileSync4(path, "utf8"));
    const packet = readAnyDecisionPacket(parsed);
    if (packet === null) {
      const reasons = validateDecisionPacket(parsed).join("; ");
      const decisionId = typeof parsed?.decision_id === "string" ? parsed.decision_id : basename(path).replace(/\.json$/, "");
      throw new DecisionPacketCorruptError(`Decision packet at ${path} is invalid. ${reasons}`, {
        decision_id: decisionId,
        reason: reasons,
        packet_path: relative(this.projectRoot, path)
      });
    }
    return packet;
  }
  readReusableDecision(decisionId) {
    const packet = this.readResolved(decisionId);
    if (!packet) {
      return null;
    }
    if (packet.status !== "resolved" && packet.status !== "delegated") {
      return null;
    }
    if (packet.human_response?.chosen_option_key === null) {
      return null;
    }
    if (isExpired(packet)) {
      this.expireResolvedDecision(decisionId);
      return null;
    }
    if (this.hasInvalidation(packet)) {
      this.expireResolvedDecision(decisionId);
      return null;
    }
    return packet;
  }
  assertWritablePacket(packet) {
    const validationErrors = validateDecisionPacket(packet);
    if (validationErrors.length > 0) {
      throw new Error(
        `Decision packet ${packet.decision_id} is invalid: ${validationErrors.join("; ")}`
      );
    }
    if (!isStrictDecisionId(packet.decision_id)) {
      throw new Error(
        `Decision id "${packet.decision_id}" must be the collision-free D-<ULID> form on creation (mint it via DecisionStore.nextDecisionId / the decision skill; never hand-write a sequential D-{N}).`
      );
    }
    const copyIssues = lintDecisionCopy(packet);
    if (copyIssues.length > 0) {
      throw new Error(
        `Decision packet ${packet.decision_id} failed copy lint: ${copyIssues.map((issue) => `${issue.field} ${issue.message}`).join("; ")}`
      );
    }
  }
  appendAudit(event, packet, provider = packet.requested_by) {
    appendDecisionAuditEvent(this.projectRoot, {
      event,
      decision_id: packet.decision_id,
      fingerprint: packet.fingerprint,
      task_session_id: packet.task_session_id,
      provider,
      timestamp: (/* @__PURE__ */ new Date()).toISOString(),
      category: packet.category,
      responded_by: packet.human_response?.responded_by,
      chosen_option_key: packet.human_response?.chosen_option_key ?? null,
      intent: packet.human_response?.intent
    });
  }
  readIndex() {
    this.initialize();
    return JSON.parse(readFileSync4(this.indexPath(), "utf8"));
  }
  writeIndex(index) {
    atomicWriteJson(this.indexPath(), index);
  }
  indexPath() {
    return join4(this.projectRoot, PATHS.DECISIONS_INDEX);
  }
  packetPath(root, decisionId) {
    return join4(this.projectRoot, root, `${decisionId}.json`);
  }
  listDirectoryIds(relativeDir) {
    const absoluteDir = join4(this.projectRoot, relativeDir);
    if (!existsSync3(absoluteDir)) {
      return [];
    }
    return readdirSync2(absoluteDir).filter((file) => /^D-(?:\d+|[0-9A-HJKMNP-TV-Z]{26})\.json$/.test(file)).map((file) => file.replace(/\.json$/, ""));
  }
  writeResolvedPacket(input) {
    const resolved = {
      ...input.packet,
      ...input.packet.earlier_check ? { earlier_check: earlierCheckToKeep(input.packet.earlier_check) } : {},
      status: input.humanResponse.intent === "delegated" ? "delegated" : "resolved",
      human_response: input.humanResponse
    };
    const resolvedPath = this.packetPath(PATHS.DECISIONS_RESOLVED_DIR, resolved.decision_id);
    this.supersedeConflictingDecision(resolved);
    atomicWriteJson(resolvedPath, resolved);
    const index = this.readIndex();
    index.fingerprints[resolved.fingerprint] = resolved.decision_id;
    index.decisions[resolved.decision_id] = {
      decision_id: resolved.decision_id,
      fingerprint: resolved.fingerprint,
      category: resolved.category,
      chosen_option_key: resolved.human_response?.chosen_option_key ?? null,
      responded_at: resolved.human_response?.responded_at ?? resolved.created_at,
      status: resolved.status,
      option_keys: resolved.options.map((option) => option.option_key).sort()
    };
    this.writeIndex(index);
    this.appendAudit(input.event, resolved, input.respondedByProvider);
    recordDecisionResolved(
      this.projectRoot,
      resolved.decision_id,
      resolved.status,
      resolverFromAuditEvent(input.event)
    );
    this.emit(decisionResolvedEvent(resolved, resolverFromAuditEvent(input.event)));
    return resolved;
  }
  supersedeConflictingDecision(packet) {
    const index = this.readIndex();
    const existingDecisionId = index.fingerprints[packet.fingerprint];
    if (!existingDecisionId || existingDecisionId === packet.decision_id) {
      return;
    }
    const existing = this.readResolved(existingDecisionId);
    if (!existing) {
      return;
    }
    if (existing.human_response?.chosen_option_key === packet.human_response?.chosen_option_key) {
      return;
    }
    const superseded = { ...existing, status: "superseded" };
    atomicWriteJson(this.packetPath(PATHS.DECISIONS_RESOLVED_DIR, existingDecisionId), superseded);
    index.decisions[existingDecisionId] = {
      ...index.decisions[existingDecisionId],
      status: "superseded"
    };
    this.writeIndex(index);
    this.appendAudit("decision-superseded", superseded);
    recordDecisionSuperseded(this.projectRoot, existingDecisionId);
  }
};
function atomicWriteJson(path, value) {
  mkdirSync3(dirname3(path), { recursive: true });
  const tempPath = `${path}.tmp`;
  writeFileSync3(tempPath, JSON.stringify(value, null, 2) + "\n");
  renameSync(tempPath, path);
}
function isExpired(packet) {
  return Date.parse(packet.ttl_until) <= Date.now();
}
function resolverFromAuditEvent(event) {
  switch (event) {
    case "decision-resolved-by-human":
    case "decision-delegated":
      return "human";
    case "decision-resolved-by-rule":
      return "rule";
    case "decision-resolved-by-rag-confident":
      return "rag-confident";
    case "decision-resolved-by-memoization":
      return "memoization";
    default:
      return event;
  }
}

// src/decisions/standing-answers.ts
init_esm_shims();
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
import { dirname as dirname4, join as join5 } from "path";
var MIN_FINGERPRINT_OPTIONS = 2;
var MAX_FINGERPRINT_OPTIONS = 5;
var STANDING_FILE_UNREADABLE = `${PATHS.DECISIONS_STANDING_ANSWERS} cannot be read. Fix or remove the file first; it was not changed.`;
function standingPath(projectRoot) {
  return join5(projectRoot, PATHS.DECISIONS_STANDING_ANSWERS);
}
function isStandingAnswer(value) {
  if (typeof value !== "object" || value === null) return false;
  const rule = value;
  return [
    "id",
    "category",
    "fingerprint",
    "question",
    "chosen",
    "from_decision",
    "created_at"
  ].every((key) => typeof rule[key] === "string");
}
function readRules(projectRoot) {
  if (!existsSync4(standingPath(projectRoot))) return [];
  try {
    const parsed = JSON.parse(readFileSync5(standingPath(projectRoot), "utf8"));
    return Array.isArray(parsed.rules) ? parsed.rules.filter(isStandingAnswer) : null;
  } catch {
    return null;
  }
}
function writeRules(projectRoot, rules) {
  const path = standingPath(projectRoot);
  mkdirSync4(dirname4(path), { recursive: true });
  const tempPath = `${path}.tmp`;
  writeFileSync4(tempPath, `${JSON.stringify({ rules }, null, 2)}
`);
  renameWithRetrySync(tempPath, path);
}
function recordRuleRow(projectRoot, kind, rule) {
  recordDecisionReuse(projectRoot, {
    kind,
    decisionId: rule.from_decision,
    category: rule.category,
    chosenOptionKey: rule.chosen,
    ruleId: rule.id
  });
}
function isStandingFileReadable(projectRoot) {
  return readRules(projectRoot) !== null;
}
function listStandingAnswers(projectRoot) {
  const rules = readRules(projectRoot);
  if (rules === null) throw new Error(STANDING_FILE_UNREADABLE);
  return rules;
}
function sameText(left, right) {
  return foldQuestionText(left) === foldQuestionText(right);
}
function fingerprintFor(question) {
  const count = question.optionKeys.length;
  if (!isDecisionCategory(question.category) || count < MIN_FINGERPRINT_OPTIONS || count > MAX_FINGERPRINT_OPTIONS) {
    return null;
  }
  return computeDecisionFingerprint({
    category: question.category,
    question: question.question,
    option_keys: [...question.optionKeys],
    repo_state: {}
  });
}
function findStandingAnswer(projectRoot, question) {
  if (NO_STANDING_ANSWER_CATEGORIES.includes(question.category)) return null;
  const fingerprint = fingerprintFor(question);
  if (fingerprint === null) return null;
  const rules = readRules(projectRoot) ?? [];
  return rules.find(
    (rule) => rule.category === question.category && rule.fingerprint === fingerprint && sameText(rule.question, question.question) && question.optionKeys.includes(rule.chosen)
  ) ?? null;
}
function addStandingAnswer(projectRoot, input) {
  const rules = listStandingAnswers(projectRoot);
  const fingerprint = fingerprintFor(input);
  if (fingerprint === null) {
    throw new Error(`A standing answer cannot apply to ${input.fromDecision}.`);
  }
  const rule = {
    id: `R-${ulid()}`,
    category: input.category,
    fingerprint,
    question: input.question,
    chosen: input.chosen,
    from_decision: input.fromDecision,
    created_at: (/* @__PURE__ */ new Date()).toISOString()
  };
  const kept = rules.filter(
    (existing) => existing.category !== rule.category || existing.fingerprint !== rule.fingerprint || !sameText(existing.question, rule.question)
  );
  writeRules(projectRoot, [...kept, rule]);
  recordRuleRow(projectRoot, "standing-set", rule);
  return rule;
}
function removeStandingAnswer(projectRoot, id) {
  const rules = listStandingAnswers(projectRoot);
  const rule = rules.find((candidate) => candidate.id === id);
  if (rule === void 0) throw new Error(`Standing answer ${id} was not found.`);
  writeRules(
    projectRoot,
    rules.filter((candidate) => candidate.id !== id)
  );
  recordRuleRow(projectRoot, "standing-removed", rule);
  return rule;
}
function assertNotStageAgent(action, agentId = process.env.SE_AGENT_ID) {
  if (agentId?.trim()) {
    throw new Error(
      `A stage agent cannot ${action}. Only the main conversation or the person can do this.`
    );
  }
}

// src/decisions/authoring.ts
init_esm_shims();
import {
  existsSync as existsSync5,
  mkdirSync as mkdirSync5,
  readFileSync as readFileSync6,
  readdirSync as readdirSync3,
  unlinkSync as unlinkSync2,
  writeFileSync as writeFileSync5
} from "fs";
import { dirname as dirname5, join as join6 } from "path";
function mintDecisionId() {
  return `D-${ulid()}`;
}
function isContractDecisionId(id) {
  return isStrictDecisionId(id);
}
function assertContractDecisionId(id) {
  if (!isContractDecisionId(id)) {
    throw new Error(
      `Decision id "${id}" must be the collision-free D-<ULID> form. Mint one with mintDecisionId() (never hand-write a sequential D-{N}).`
    );
  }
}
function validateCreateInput(input) {
  const errors = [];
  if (input.category.trim().length === 0) {
    errors.push("category is required");
  }
  if (input.title.trim().length === 0) {
    errors.push("title is required");
  }
  if (input.context.trim().length === 0) {
    errors.push("context is required");
  }
  if (!Array.isArray(input.options) || input.options.length < 2) {
    errors.push("at least 2 options are required");
  } else {
    const seen = /* @__PURE__ */ new Set();
    for (const [index, option] of input.options.entries()) {
      if (typeof option?.option_key !== "string" || option.option_key.trim().length === 0) {
        errors.push(`options[${index}].option_key is required`);
        continue;
      }
      if (typeof option.label !== "string" || option.label.trim().length === 0) {
        errors.push(`options[${index}].label is required`);
      }
      if (seen.has(option.option_key)) {
        errors.push(`options[${index}].option_key "${option.option_key}" is duplicated`);
      }
      seen.add(option.option_key);
    }
  }
  if (input.recommendation !== void 0 && input.recommendation !== null && !input.options?.some((option) => option.option_key === input.recommendation)) {
    errors.push("recommendation must reference an option_key");
  }
  return errors;
}
function createPendingDecision(projectRoot, input) {
  const errors = validateCreateInput(input);
  if (errors.length > 0) {
    throw new Error(`Cannot create decision packet: ${errors.join("; ")}`);
  }
  const id = mintDecisionId();
  const packet = {
    id,
    category: input.category,
    title: input.title,
    context: input.context,
    options: input.options.map(contractOption),
    recommendation: input.recommendation ?? null,
    created_at: (/* @__PURE__ */ new Date()).toISOString(),
    status: "pending",
    ...input.origin !== void 0 ? { origin: input.origin } : {},
    ...input.change ? { change: input.change } : {}
  };
  const rule = input.askAnyway ? null : findStandingAnswer(projectRoot, {
    category: packet.category,
    question: packet.title,
    optionKeys: packet.options.map((option) => option.option_key)
  });
  if (rule !== null) return answerFromStandingRule(projectRoot, packet, rule);
  const path = packetPath(projectRoot, PATHS.DECISIONS_PENDING_DIR, id);
  const check = earlierCheckOnOpen(projectRoot, packet, path, input.agentWorded === true);
  if (check !== null) packet.earlier_check = check;
  atomicWriteJson2(path, withRichFields(packet));
  return { id, path };
}
function earlierCheckOnOpen(projectRoot, packet, path, listTitles) {
  if (richFieldsFor(packet) === null) return null;
  const { id: excludeId, category } = packet;
  return listTitles ? titleCheckFor(projectRoot, { excludeId, category, path }) : earlierCheckFor(projectRoot, { question: packet.title, excludeId, category });
}
function answerFromStandingRule(projectRoot, packet, rule) {
  const resolved = {
    ...packet,
    status: "resolved",
    chosen: rule.chosen,
    rationale: `Standing answer ${rule.id}`,
    resolved_at: (/* @__PURE__ */ new Date()).toISOString(),
    responded_by: STANDING_ANSWER_RESPONDER
  };
  const path = packetPath(projectRoot, PATHS.DECISIONS_RESOLVED_DIR, packet.id);
  const file = withRichFields(resolved);
  atomicWriteJson2(path, file);
  const written = readAnyDecisionPacket(file);
  if (written !== null) {
    appendDecisionAuditEvent(projectRoot, {
      event: "decision-resolved-by-rule",
      decision_id: written.decision_id,
      fingerprint: written.fingerprint,
      task_session_id: written.task_session_id,
      provider: STANDING_ANSWER_RESPONDER,
      timestamp: resolved.resolved_at,
      category: written.category,
      responded_by: STANDING_ANSWER_RESPONDER,
      chosen_option_key: rule.chosen,
      intent: written.human_response?.intent
    });
  }
  recordDecisionReuse(projectRoot, {
    kind: "reuse",
    decisionId: rule.from_decision,
    matchKind: "standing",
    category: packet.category,
    chosenOptionKey: rule.chosen,
    sourcePath: packetPathRelative(packet.id),
    note: `Standing answer ${rule.id} answered ${packet.id}`,
    change: packet.change,
    forDecision: packet.id,
    ruleId: rule.id
  });
  const label = packet.options.find((option) => option.option_key === rule.chosen).label;
  return { id: packet.id, path, reused: { rule: rule.id, chosen: rule.chosen, label } };
}
function assertAlwaysAllowed(projectRoot, id, other) {
  assertNotStageAgent("set a standing answer");
  if (other) {
    throw new Error("--always cannot be used with --other: a write-in could never match again.");
  }
  const pendingPath = packetPath(projectRoot, PATHS.DECISIONS_PENDING_DIR, id);
  if (!existsSync5(pendingPath)) return;
  const stored = JSON.parse(readFileSync6(pendingPath, "utf8"));
  if (stored.id === void 0) {
    throw new Error(
      `--always is refused for ${id}: rules do not apply to packets opened this way.`
    );
  }
  if (richFieldsFor(stored) === null) {
    throw new Error(`--always is refused for ${id}: this question has no stable fingerprint.`);
  }
  if (NO_STANDING_ANSWER_CATEGORIES.includes(stored.category ?? "")) {
    throw new Error(`--always is refused for the category ${stored.category}.`);
  }
  if (!isStandingFileReadable(projectRoot)) {
    throw new Error(STANDING_FILE_UNREADABLE);
  }
}
function answerPendingDecision(projectRoot, id, chosen, rationale = "", options = {}) {
  if (options.always) assertAlwaysAllowed(projectRoot, id);
  const result = resolvePendingDecision(projectRoot, id, chosen, rationale, options);
  if (!options.always) return result;
  const rule = addStandingAnswer(projectRoot, {
    category: result.packet.category,
    question: result.packet.title,
    optionKeys: result.packet.options.map((option) => option.option_key),
    chosen,
    fromDecision: id
  });
  return { ...result, rule };
}
function resolvePendingDecision(projectRoot, id, chosen, rationale = "", options = {}) {
  assertContractDecisionId(id);
  const pendingPath = packetPath(projectRoot, PATHS.DECISIONS_PENDING_DIR, id);
  if (!existsSync5(pendingPath)) {
    throw new Error(`Pending decision ${id} not found at ${pendingPath}.`);
  }
  assertEarlierCheckDone(projectRoot, id);
  const stored = JSON.parse(readFileSync6(pendingPath, "utf8"));
  if (stored.id === void 0 && stored.decision_id !== void 0) {
    return resolveStorePacket(projectRoot, id, stored, chosen, rationale);
  }
  const pending = stored;
  if (!pending.options?.some((option) => option.option_key === chosen)) {
    const keys = pending.options?.map((option) => option.option_key).join(", ") ?? "";
    throw new Error(`Chosen option "${chosen}" is not one of the packet's options (${keys}).`);
  }
  const resolved = {
    ...pending,
    ...pending.earlier_check ? { earlier_check: earlierCheckToKeep(pending.earlier_check) } : {},
    ...pending.change === void 0 && options.change ? { change: options.change } : {},
    status: "resolved",
    chosen,
    rationale,
    resolved_at: (/* @__PURE__ */ new Date()).toISOString()
  };
  const resolvedPath = packetPath(projectRoot, PATHS.DECISIONS_RESOLVED_DIR, id);
  atomicWriteJson2(resolvedPath, withRichFields(resolved));
  unlinkSync2(pendingPath);
  recordSuggestionOutcome(projectRoot, {
    forDecision: id,
    category: resolved.category,
    chosen,
    change: resolved.change,
    suggestion: resolved.suggestion
  });
  return { path: resolvedPath, packet: resolved };
}
function resolveStorePacket(projectRoot, id, stored, chosen, rationale) {
  if (!stored.options?.some((option) => option.option_key === chosen)) {
    const keys = stored.options?.map((option) => option.option_key).join(", ") ?? "";
    throw new Error(`Chosen option "${chosen}" is not one of the packet's options (${keys}).`);
  }
  const resolvedAt = (/* @__PURE__ */ new Date()).toISOString();
  const path = new DecisionStore(projectRoot).resolve({
    decisionId: id,
    humanResponse: {
      chosen_option_key: chosen,
      intent: "explicit",
      explanation_rounds_used: 0,
      responded_at: resolvedAt,
      responded_by: "paqad-decision-cli",
      carry_over_scope: "session",
      ...rationale ? { note: rationale } : {}
    }
  });
  recordSuggestionOutcome(projectRoot, {
    forDecision: id,
    category: stored.category,
    chosen,
    suggestion: stored.suggestion
  });
  return {
    path,
    packet: {
      id,
      category: stored.category ?? "",
      title: stored.question ?? "",
      context: stored.context ?? "",
      options: stored.options,
      recommendation: stored.recommendation ?? null,
      created_at: stored.created_at ?? resolvedAt,
      status: "resolved",
      chosen,
      rationale,
      resolved_at: resolvedAt
    }
  };
}
function withRichFields(packet) {
  const contractOnly = { ...packet, options: packet.options.map(contractOption) };
  for (const key of RICH_ONLY_KEYS) delete contractOnly[key];
  const rich = richFieldsFor(contractOnly);
  if (rich === null) return contractOnly;
  return {
    ...contractOnly,
    ...rich,
    ...packet.suggestion ? { recommendation_reason: packet.suggestion.reason } : {}
  };
}
var RICH_ONLY_KEYS = [
  "decision_id",
  "fingerprint",
  "question",
  "recommendation_reason",
  "confidence",
  "requested_by",
  "task_session_id",
  "ttl_until",
  "invalidation_watch",
  "human_response"
];
function contractOption(option) {
  return {
    option_key: option.option_key,
    label: option.label,
    ...option.one_line_preview ? { one_line_preview: option.one_line_preview } : {},
    ...option.trade_off ? { trade_off: option.trade_off } : {},
    ...option.evidence !== void 0 ? { evidence: option.evidence } : {}
  };
}
function readContractDecisions(projectRoot) {
  const rows = [];
  const sources = [
    [PATHS.DECISIONS_PENDING_DIR, "pending"],
    [PATHS.DECISIONS_RESOLVED_DIR, "resolved"]
  ];
  for (const [relativeDir, status] of sources) {
    const abs = join6(projectRoot, relativeDir);
    let files;
    try {
      files = readdirSync3(abs);
    } catch {
      continue;
    }
    for (const file of files.filter((name) => name.endsWith(".json")).sort()) {
      try {
        const packet = JSON.parse(readFileSync6(join6(abs, file), "utf8"));
        rows.push({ packet, status });
      } catch {
      }
    }
  }
  return rows;
}
function listContractDecisions(projectRoot) {
  return readContractDecisions(projectRoot).map(({ packet, status }) => ({
    id: packet.id,
    category: packet.category,
    title: packet.title,
    status
  }));
}
var WRITE_IN_REFUSED_ON_STORE_SHAPE = "This question takes only its listed options, so --other can't be used. Pick one of them.";
function addWriteInOption(projectRoot, id, label) {
  assertContractDecisionId(id);
  if (label.trim().length === 0) {
    throw new Error("A write-in option needs a non-empty label.");
  }
  const pendingPath = packetPath(projectRoot, PATHS.DECISIONS_PENDING_DIR, id);
  if (!existsSync5(pendingPath)) {
    throw new Error(`Pending decision ${id} not found at ${pendingPath}.`);
  }
  const stored = JSON.parse(readFileSync6(pendingPath, "utf8"));
  if (stored.id === void 0 && stored.decision_id !== void 0) {
    throw new Error(WRITE_IN_REFUSED_ON_STORE_SHAPE);
  }
  const pending = stored;
  const existing = new Set(pending.options.map((option) => option.option_key));
  let key = "other";
  let suffix = 2;
  while (existing.has(key)) {
    key = `other-${suffix++}`;
  }
  pending.options = [...pending.options, { option_key: key, label }];
  atomicWriteJson2(pendingPath, withRichFields(pending));
  return key;
}
function packetPathRelative(id) {
  return join6(PATHS.DECISIONS_RESOLVED_DIR, `${id}.json`);
}
function packetPath(projectRoot, relativeDir, id) {
  return join6(projectRoot, relativeDir, `${id}.json`);
}
function atomicWriteJson2(path, value) {
  mkdirSync5(dirname5(path), { recursive: true });
  const tempPath = `${path}.tmp`;
  writeFileSync5(tempPath, `${JSON.stringify(value, null, 2)}
`);
  renameWithRetrySync(tempPath, path);
}

export {
  DECISION_REUSE_DOC_TYPE,
  recordDecisionReuse,
  recordSuggestionOutcome,
  DECISION_AUDIT_EVENTS,
  appendDecisionAuditEvent,
  ensureDecisionAuditLog,
  readDecisionAuditEvents,
  DECISION_EVIDENCE_DOC_TYPE,
  readDecisionEvidence,
  DECISION_PAUSE_EVENT_TYPES,
  decisionPausedEvent,
  decisionResolvedEvent,
  decisionPacketCorruptEvent,
  decisionCapExceededEvent,
  decisionDiscardedEvent,
  computeDecisionFingerprint,
  normalizeDecisionQuestion,
  buildRepoStateSignature,
  scoreDecisionOptionOverlap,
  readAnyDecisionPacket,
  readResolvedPackets,
  findDecisionPrecedents,
  tokenizeCode,
  shingles,
  jaccard,
  EARLIER_SCORE_FLOOR,
  EARLIER_LIMIT,
  EARLIER_TITLE_LIMIT,
  EARLIER_TITLE_MAX_CHARS,
  EARLIER_METHOD_MAX_CHARS,
  EARLIER_OUTPUT_MAX_CHARS,
  PRIVATE_ANSWER_CATEGORIES,
  foldQuestionText,
  findEarlierQuestions,
  earlierCheckFor,
  listEarlierTitles,
  titleCheckFor,
  EARLIER_METHOD_FALLBACK,
  readEarlierMethod,
  earlierCheckRefusal,
  assertEarlierCheckDone,
  earlierCheckBlock,
  printDecisionOutput,
  NO_STANDING_ANSWER_CATEGORIES,
  ALWAYS_LABEL,
  MAX_ALWAYS_OPTIONS,
  MAX_QUESTION_ENTRIES,
  buildQuestionBlock,
  checkDecision,
  readEarlierAnswers,
  formatCreateOutput,
  suggestEarlierAnswer,
  lintDecisionCopy,
  MAX_PENDING_DECISIONS,
  DecisionCapExceededError,
  DecisionStore,
  STANDING_FILE_UNREADABLE,
  isStandingFileReadable,
  listStandingAnswers,
  findStandingAnswer,
  addStandingAnswer,
  removeStandingAnswer,
  assertNotStageAgent,
  mintDecisionId,
  isContractDecisionId,
  assertContractDecisionId,
  createPendingDecision,
  assertAlwaysAllowed,
  answerPendingDecision,
  resolvePendingDecision,
  readContractDecisions,
  listContractDecisions,
  WRITE_IN_REFUSED_ON_STORE_SHAPE,
  addWriteInOption
};