arcane-cli 1.20.2 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ const require = __createRequire(import.meta.url);
5
5
  // src/index.ts
6
6
  import { Command, Option } from "commander";
7
7
  import { fileURLToPath } from "url";
8
- import { dirname as dirname15, join as join31 } from "path";
8
+ import { dirname as dirname15, join as join32 } from "path";
9
9
 
10
10
  // src/package-info.ts
11
11
  import { createRequire } from "module";
@@ -1695,14 +1695,14 @@ async function syncUserTierFanout(options) {
1695
1695
  const ownsClient = (client2) => owned === void 0 || client2 !== void 0 && owned.includes(client2);
1696
1696
  const previous = options.previous ?? {};
1697
1697
  const dryRun = Boolean(options.dryRun);
1698
- const record = {};
1698
+ const record2 = {};
1699
1699
  const outcomes = [];
1700
1700
  const desiredPaths = new Set(desired.map((f) => f.relativePath));
1701
1701
  for (const id of rendered.missing) {
1702
1702
  for (const client2 of SPELL_FANOUT_CLIENTS) {
1703
1703
  const relativePath = USER_FANOUT_PATHS[client2](id);
1704
1704
  desiredPaths.add(relativePath);
1705
- if (previous[relativePath] !== void 0) record[relativePath] = previous[relativePath];
1705
+ if (previous[relativePath] !== void 0) record2[relativePath] = previous[relativePath];
1706
1706
  outcomes.push({ relativePath, status: "unrenderable" });
1707
1707
  }
1708
1708
  }
@@ -1717,7 +1717,7 @@ async function syncUserTierFanout(options) {
1717
1717
  await mkdir2(dirname2(target), { recursive: true });
1718
1718
  await writeFile3(target, file.content, "utf-8");
1719
1719
  }
1720
- record[file.relativePath] = newHash;
1720
+ record2[file.relativePath] = newHash;
1721
1721
  outcomes.push({ relativePath: file.relativePath, status: "written" });
1722
1722
  continue;
1723
1723
  }
@@ -1726,24 +1726,24 @@ async function syncUserTierFanout(options) {
1726
1726
  continue;
1727
1727
  }
1728
1728
  if (!await fileMatchesHash(target, recorded)) {
1729
- record[file.relativePath] = recorded;
1729
+ record2[file.relativePath] = recorded;
1730
1730
  outcomes.push({ relativePath: file.relativePath, status: "customized" });
1731
1731
  continue;
1732
1732
  }
1733
1733
  const currentHash = await hashFile(target);
1734
1734
  if (currentHash === newHash) {
1735
- record[file.relativePath] = newHash;
1735
+ record2[file.relativePath] = newHash;
1736
1736
  outcomes.push({ relativePath: file.relativePath, status: "unchanged" });
1737
1737
  continue;
1738
1738
  }
1739
1739
  if (!dryRun) await writeFile3(target, file.content, "utf-8");
1740
- record[file.relativePath] = newHash;
1740
+ record2[file.relativePath] = newHash;
1741
1741
  outcomes.push({ relativePath: file.relativePath, status: "written" });
1742
1742
  }
1743
1743
  for (const [relativePath, recorded] of Object.entries(previous)) {
1744
1744
  if (desiredPaths.has(relativePath)) continue;
1745
1745
  if (!ownsClient(clientOfFanoutPath(relativePath))) {
1746
- record[relativePath] = recorded;
1746
+ record2[relativePath] = recorded;
1747
1747
  continue;
1748
1748
  }
1749
1749
  validateTargetPath(options.homeDir, relativePath);
@@ -1754,7 +1754,7 @@ async function syncUserTierFanout(options) {
1754
1754
  continue;
1755
1755
  }
1756
1756
  if (state === "other" || !await fileMatchesHash(target, recorded)) {
1757
- record[relativePath] = recorded;
1757
+ record2[relativePath] = recorded;
1758
1758
  outcomes.push({ relativePath, status: "kept-edited" });
1759
1759
  continue;
1760
1760
  }
@@ -1765,7 +1765,7 @@ async function syncUserTierFanout(options) {
1765
1765
  }
1766
1766
  outcomes.push({ relativePath, status: "pruned" });
1767
1767
  }
1768
- return { record, outcomes };
1768
+ return { record: record2, outcomes };
1769
1769
  }
1770
1770
  function summarizeFanout(outcomes) {
1771
1771
  const counts = {
@@ -1834,14 +1834,14 @@ ${paths("kept-edited")}`
1834
1834
  }
1835
1835
  return lines;
1836
1836
  }
1837
- async function inspectUserTierFanout(homeDir, record) {
1837
+ async function inspectUserTierFanout(homeDir, record2) {
1838
1838
  const health = {
1839
1839
  total: 0,
1840
1840
  byClient: { codex: 0, claude: 0, "copilot-agents": 0 },
1841
1841
  missing: [],
1842
1842
  customized: []
1843
1843
  };
1844
- for (const [relativePath, recorded] of Object.entries(record ?? {})) {
1844
+ for (const [relativePath, recorded] of Object.entries(record2 ?? {})) {
1845
1845
  health.total++;
1846
1846
  const client2 = clientOfFanoutPath(relativePath);
1847
1847
  if (client2 !== void 0) health.byClient[client2]++;
@@ -4086,8 +4086,8 @@ function hookInForce(outcome) {
4086
4086
  }
4087
4087
  async function applyBlockedPushControls(cwd, options = {}) {
4088
4088
  const messages = [];
4089
- const say = (level, text2) => {
4090
- const message = { level, text: text2 };
4089
+ const say = (level, text3) => {
4090
+ const message = { level, text: text3 };
4091
4091
  messages.push(message);
4092
4092
  options.emit?.(message);
4093
4093
  };
@@ -5052,8 +5052,8 @@ function arraysEqual(a, b) {
5052
5052
  return a.every((line2, idx) => line2 === b[idx]);
5053
5053
  }
5054
5054
  var MAX_LINES_FOR_LCS = 4e3;
5055
- function splitLines(text2) {
5056
- return text2.split("\n");
5055
+ function splitLines(text3) {
5056
+ return text3.split("\n");
5057
5057
  }
5058
5058
  function merge3(base, ours, theirs) {
5059
5059
  const baseLines = splitLines(base);
@@ -6457,18 +6457,18 @@ function titleAfterId(rest) {
6457
6457
  return rest.replace(/^\s*[—–:-]+\s*/u, "").trim();
6458
6458
  }
6459
6459
  function parseDecisions(content) {
6460
- const text2 = content.replace(/\r\n?/g, "\n");
6460
+ const text3 = content.replace(/\r\n?/g, "\n");
6461
6461
  const positions = [];
6462
6462
  let match2;
6463
6463
  HEADING.lastIndex = 0;
6464
- while ((match2 = HEADING.exec(text2)) !== null) {
6464
+ while ((match2 = HEADING.exec(text3)) !== null) {
6465
6465
  positions.push({ id: `${match2[1]}-${match2[2]}`, heading: `${match2[1]}-${match2[2]}${match2[3]}`, index: match2.index });
6466
6466
  }
6467
6467
  const entries = [];
6468
6468
  const warnings = [];
6469
6469
  positions.forEach(({ id, heading, index }, i) => {
6470
- const end = i + 1 < positions.length ? positions[i + 1].index : text2.length;
6471
- const section = text2.slice(index, end);
6470
+ const end = i + 1 < positions.length ? positions[i + 1].index : text3.length;
6471
+ const section = text3.slice(index, end);
6472
6472
  const dateMatch = /^\*\*Date:\*\*\s*(\d{4}-\d{2}-\d{2})/m.exec(section);
6473
6473
  const statusMatch = /^\*\*Status:\*\*\s*(\S+)/m.exec(section);
6474
6474
  if (!dateMatch && !statusMatch) {
@@ -6515,12 +6515,12 @@ async function collectDecisions(root) {
6515
6515
  var ITEM = /^\s*- \[( |x|X)\] (.*)$/;
6516
6516
  var HEADING2 = /^#{1,6}\s+(.+?)\s*#*\s*$/;
6517
6517
  var TEXT_LIMIT = 200;
6518
- function normalizeItemText(text2) {
6519
- return text2.replace(/\s+/g, " ").trim();
6518
+ function normalizeItemText(text3) {
6519
+ return text3.replace(/\s+/g, " ").trim();
6520
6520
  }
6521
- function itemLocator(section, text2) {
6521
+ function itemLocator(section, text3) {
6522
6522
  const slug = section === null ? "" : headingAnchor(section);
6523
- const hash = createHash3("sha256").update(normalizeItemText(text2), "utf8").digest("hex").slice(0, 12);
6523
+ const hash = createHash3("sha256").update(normalizeItemText(text3), "utf8").digest("hex").slice(0, 12);
6524
6524
  return `${slug === "" ? "root" : slug}/${hash}`;
6525
6525
  }
6526
6526
  function parseBook(content) {
@@ -6537,13 +6537,13 @@ function parseBook(content) {
6537
6537
  const item = ITEM.exec(line2);
6538
6538
  if (!item) continue;
6539
6539
  const checked = item[1] !== " ";
6540
- const text2 = item[2].trim();
6540
+ const text3 = item[2].trim();
6541
6541
  if (checked) done++;
6542
6542
  else open++;
6543
6543
  items.push({
6544
- text: text2.length > TEXT_LIMIT ? text2.slice(0, TEXT_LIMIT) : text2,
6544
+ text: text3.length > TEXT_LIMIT ? text3.slice(0, TEXT_LIMIT) : text3,
6545
6545
  section,
6546
- locator: itemLocator(section, text2),
6546
+ locator: itemLocator(section, text3),
6547
6547
  checked
6548
6548
  });
6549
6549
  }
@@ -6608,10 +6608,10 @@ function featureTracking(frontmatter) {
6608
6608
  };
6609
6609
  }
6610
6610
  function parsePrd(content, displayPath) {
6611
- const text2 = content.replace(/\r\n?/g, "\n");
6612
- const heading = H1.exec(text2);
6611
+ const text3 = content.replace(/\r\n?/g, "\n");
6612
+ const heading = H1.exec(text3);
6613
6613
  const empty = { title: heading ? heading[1] : null, status: null, implementation_status: null, tracking: null };
6614
- const match2 = FRONTMATTER.exec(text2);
6614
+ const match2 = FRONTMATTER.exec(text3);
6615
6615
  if (!match2) return { fields: empty, warning: `features: ${displayPath} has no frontmatter` };
6616
6616
  let parsed;
6617
6617
  try {
@@ -6961,13 +6961,13 @@ import { join as join17 } from "path";
6961
6961
  var JOURNAL_DIR = "journal";
6962
6962
  var ENTRY_NAME = /^(\d{4}-\d{2}-\d{2})-(.+)\.md$/;
6963
6963
  function journalTitle(content) {
6964
- const text2 = content.replace(/\r\n?/g, "\n");
6965
- const frontmatter = /^---\n([\s\S]*?)\n---/.exec(text2);
6964
+ const text3 = content.replace(/\r\n?/g, "\n");
6965
+ const frontmatter = /^---\n([\s\S]*?)\n---/.exec(text3);
6966
6966
  if (frontmatter) {
6967
6967
  const title = /^title:\s*(.+)$/m.exec(frontmatter[1]);
6968
6968
  if (title) return title[1].trim().replace(/^(["'])(.*)\1$/, "$2");
6969
6969
  }
6970
- const heading = /^# (.+?)\s*$/m.exec(text2);
6970
+ const heading = /^# (.+?)\s*$/m.exec(text3);
6971
6971
  return heading ? heading[1].trim() : null;
6972
6972
  }
6973
6973
  async function collectJournal(root) {
@@ -7405,9 +7405,9 @@ async function collectState(cwd, packageVersion, options) {
7405
7405
 
7406
7406
  // src/commands/state.ts
7407
7407
  var USAGE_POINTER = "spell state prints one JSON document to stdout: run `spell state --json` (spell status is the human view).";
7408
- function writeStdout(text2) {
7408
+ function writeStdout(text3) {
7409
7409
  return new Promise((resolve10, reject) => {
7410
- process.stdout.write(text2, (error2) => error2 ? reject(error2) : resolve10());
7410
+ process.stdout.write(text3, (error2) => error2 ? reject(error2) : resolve10());
7411
7411
  });
7412
7412
  }
7413
7413
  async function runState(targetDir, packageVersion, options = {}, deps = {}) {
@@ -7417,13 +7417,13 @@ async function runState(targetDir, packageVersion, options = {}, deps = {}) {
7417
7417
  return;
7418
7418
  }
7419
7419
  const collect = deps.collect ?? collectState;
7420
- let text2;
7420
+ let text3;
7421
7421
  try {
7422
7422
  const doc = await collect(targetDir, packageVersion, {
7423
7423
  online: options.online === true,
7424
7424
  includeLocalPaths: options.includeLocalPaths === true
7425
7425
  });
7426
- text2 = serializeState(doc);
7426
+ text3 = serializeState(doc);
7427
7427
  } catch (error2) {
7428
7428
  if (error2 instanceof ManifestNotFoundError) {
7429
7429
  console.error('Not initialized. Run "spell init" first.');
@@ -7436,7 +7436,7 @@ async function runState(targetDir, packageVersion, options = {}, deps = {}) {
7436
7436
  return;
7437
7437
  }
7438
7438
  try {
7439
- await (deps.write ?? writeStdout)(text2);
7439
+ await (deps.write ?? writeStdout)(text3);
7440
7440
  } catch (error2) {
7441
7441
  console.error(`spell state: stdout is not writable (${error2 instanceof Error ? error2.message : String(error2)})`);
7442
7442
  process.exitCode = 1;
@@ -7638,8 +7638,8 @@ async function uninstallUserTier(options, storeRoot, manifest) {
7638
7638
  }
7639
7639
 
7640
7640
  // src/commands/doctor.ts
7641
- import { access as access4, readdir as readdir11, readFile as readFile19, mkdir as mkdir7, copyFile as fsCopyFile } from "fs/promises";
7642
- import { join as join24, dirname as dirname12, resolve as resolve7 } from "path";
7641
+ import { access as access4, readdir as readdir11, readFile as readFile20, mkdir as mkdir7, copyFile as fsCopyFile } from "fs/promises";
7642
+ import { join as join25, dirname as dirname12, resolve as resolve7 } from "path";
7643
7643
 
7644
7644
  // src/config/incidents.ts
7645
7645
  var INCIDENT_QUEUE = [
@@ -7665,37 +7665,37 @@ function evaluateIncidentGate(records) {
7665
7665
  const blockers = [];
7666
7666
  const acceptedRisks = [];
7667
7667
  const seen = /* @__PURE__ */ new Set();
7668
- for (const record of records) {
7669
- if (seen.has(record.id)) {
7670
- blockers.push(`${record.id}: duplicate incident record`);
7668
+ for (const record2 of records) {
7669
+ if (seen.has(record2.id)) {
7670
+ blockers.push(`${record2.id}: duplicate incident record`);
7671
7671
  continue;
7672
7672
  }
7673
- seen.add(record.id);
7674
- if (record.confidence === "research") continue;
7675
- if (!PROTECTED_IMPACTS.has(record.impact)) continue;
7676
- if (!record.severity) {
7677
- blockers.push(`${record.id}: confirmed ${record.impact} incident is missing severity`);
7673
+ seen.add(record2.id);
7674
+ if (record2.confidence === "research") continue;
7675
+ if (!PROTECTED_IMPACTS.has(record2.impact)) continue;
7676
+ if (!record2.severity) {
7677
+ blockers.push(`${record2.id}: confirmed ${record2.impact} incident is missing severity`);
7678
7678
  continue;
7679
7679
  }
7680
- if (!BLOCKING_SEVERITIES.has(record.severity)) continue;
7681
- if (record.state === "resolved") {
7682
- if (!record.resolution?.trim()) {
7683
- blockers.push(`${record.id}: resolved incident is missing resolution evidence`);
7680
+ if (!BLOCKING_SEVERITIES.has(record2.severity)) continue;
7681
+ if (record2.state === "resolved") {
7682
+ if (!record2.resolution?.trim()) {
7683
+ blockers.push(`${record2.id}: resolved incident is missing resolution evidence`);
7684
7684
  }
7685
7685
  continue;
7686
7686
  }
7687
- if (record.state === "deferred") {
7688
- if (!record.deferral || !DEFERRAL_PATTERN.test(record.deferral)) {
7687
+ if (record2.state === "deferred") {
7688
+ if (!record2.deferral || !DEFERRAL_PATTERN.test(record2.deferral)) {
7689
7689
  blockers.push(
7690
- `${record.id}: deferral must be exactly "Deferred <date> \u2014 known open, accepting the risk."`
7690
+ `${record2.id}: deferral must be exactly "Deferred <date> \u2014 known open, accepting the risk."`
7691
7691
  );
7692
7692
  } else {
7693
- acceptedRisks.push(`${record.id}: ${record.deferral}`);
7693
+ acceptedRisks.push(`${record2.id}: ${record2.deferral}`);
7694
7694
  }
7695
7695
  continue;
7696
7696
  }
7697
7697
  blockers.push(
7698
- `${record.id}: unresolved ${record.severity} ${record.impact} incident blocks release`
7698
+ `${record2.id}: unresolved ${record2.severity} ${record2.impact} incident blocks release`
7699
7699
  );
7700
7700
  }
7701
7701
  return {
@@ -7977,10 +7977,10 @@ function stripFrontmatter(content) {
7977
7977
  const block = FRONTMATTER_PATTERN2.exec(content);
7978
7978
  return block ? content.slice(block[0].length) : content;
7979
7979
  }
7980
- function stripFencedCode(text2) {
7980
+ function stripFencedCode(text3) {
7981
7981
  const kept = [];
7982
7982
  let fence = null;
7983
- for (const line2 of text2.split(/\r?\n/)) {
7983
+ for (const line2 of text3.split(/\r?\n/)) {
7984
7984
  if (fence) {
7985
7985
  const close = /^ {0,3}(`{3,}|~{3,})[ \t]*$/.exec(line2);
7986
7986
  if (close && close[1][0] === fence.char && close[1].length >= fence.length) fence = null;
@@ -7997,8 +7997,8 @@ function stripFencedCode(text2) {
7997
7997
  }
7998
7998
  return kept.join("\n");
7999
7999
  }
8000
- function stripInlineCode(text2) {
8001
- return text2.split(/\n[ \t]*\n/).map((paragraph) => {
8000
+ function stripInlineCode(text3) {
8001
+ return text3.split(/\n[ \t]*\n/).map((paragraph) => {
8002
8002
  let out = "";
8003
8003
  let i = 0;
8004
8004
  while (i < paragraph.length) {
@@ -8356,9 +8356,9 @@ function createSessionStorage(options = {}) {
8356
8356
  const interpret = (result, source) => {
8357
8357
  if (result.state === "signed-out") return null;
8358
8358
  if (result.state === "corrupt") return { state: "corrupt", source, reason: result.reason };
8359
- const record = parseSessionRecord(result.secret);
8360
- if (record === null) return { state: "corrupt", source, reason: "the stored session is not valid" };
8361
- return { state: "ok", source, record };
8359
+ const record2 = parseSessionRecord(result.secret);
8360
+ if (record2 === null) return { state: "corrupt", source, reason: "the stored session is not valid" };
8361
+ return { state: "ok", source, record: record2 };
8362
8362
  };
8363
8363
  return {
8364
8364
  load() {
@@ -8376,8 +8376,8 @@ function createSessionStorage(options = {}) {
8376
8376
  }
8377
8377
  return keychainError ? { state: "signed-out", keychainError } : { state: "signed-out" };
8378
8378
  },
8379
- save(record, { insecure }) {
8380
- const serialized = JSON.stringify(record);
8379
+ save(record2, { insecure }) {
8380
+ const serialized = JSON.stringify(record2);
8381
8381
  if (insecure) {
8382
8382
  file.save(serialized);
8383
8383
  try {
@@ -8606,10 +8606,10 @@ function send(port, token, method, path5, body, options = {}) {
8606
8606
  res.on("error", () => done({ kind: "unreachable" }));
8607
8607
  res.on("end", () => {
8608
8608
  const status = res.statusCode ?? 0;
8609
- const text2 = Buffer.concat(chunks).toString("utf8");
8609
+ const text3 = Buffer.concat(chunks).toString("utf8");
8610
8610
  let parsed = null;
8611
8611
  try {
8612
- parsed = text2.length > 0 ? JSON.parse(text2) : null;
8612
+ parsed = text3.length > 0 ? JSON.parse(text3) : null;
8613
8613
  } catch {
8614
8614
  parsed = null;
8615
8615
  }
@@ -8727,6 +8727,77 @@ function openEventFeed(callbacks, options = {}) {
8727
8727
 
8728
8728
  // src/commands/doctor.ts
8729
8729
  import { statSync } from "fs";
8730
+
8731
+ // src/modules/run-grants.ts
8732
+ import { readFile as readFile19 } from "fs/promises";
8733
+ import { join as join24 } from "path";
8734
+ var RUN_ACTIONS = [
8735
+ "commit",
8736
+ "push",
8737
+ "create-pr",
8738
+ "assign-pr",
8739
+ "approve-pr",
8740
+ "complete-pr",
8741
+ "delete-remote-branch",
8742
+ "run-dev-pipeline"
8743
+ ];
8744
+ var record = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
8745
+ var text = (v) => typeof v === "string" && v.trim().length > 0;
8746
+ var strings = (v) => Array.isArray(v) && v.every(text);
8747
+ var actions = (v) => strings(v) && v.every((a) => RUN_ACTIONS.some((known) => known === a));
8748
+ function timestamp(v) {
8749
+ if (typeof v !== "string" || !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z$/.test(v)) return false;
8750
+ const n = Date.parse(v);
8751
+ return Number.isFinite(n) && new Date(n).toISOString() === v;
8752
+ }
8753
+ function isRunGrant(v) {
8754
+ if (!record(v) || !record(v.scope)) return false;
8755
+ return [v.id, v.grantedBy, v.operatorWords, v.revocation].every(text) && timestamp(v.grantedAt) && timestamp(v.expiresAt) && Date.parse(v.expiresAt) > Date.parse(v.grantedAt) && (v.status === "active" || v.status === "revoked") && [v.scope.repository, v.scope.task, v.scope.branchPrefix].every(text) && actions(v.permittedActions) && v.permittedActions.length > 0 && strings(v.excludedActions) && strings(v.requiredChecks) && (!v.permittedActions.includes("complete-pr") || v.requiredChecks.length > 0);
8756
+ }
8757
+ function parseRunGrants(value) {
8758
+ if (!record(value)) throw new Error("expected a delegations object");
8759
+ if (value.runGrants === void 0) return [];
8760
+ if (!Array.isArray(value.runGrants) || !value.runGrants.every(isRunGrant)) {
8761
+ throw new Error("runGrants must be an array of well-formed, expiring run grants");
8762
+ }
8763
+ const grants = value.runGrants;
8764
+ if (new Set(grants.map((g) => g.id)).size !== grants.length) {
8765
+ throw new Error("duplicate run grant ID");
8766
+ }
8767
+ return grants;
8768
+ }
8769
+ async function readRunGrants(targetDir) {
8770
+ let raw;
8771
+ try {
8772
+ raw = await readFile19(join24(targetDir, ".arcane", "delegations.json"), "utf8");
8773
+ } catch (error2) {
8774
+ if (error2.code === "ENOENT") return [];
8775
+ throw error2;
8776
+ }
8777
+ return parseRunGrants(JSON.parse(raw));
8778
+ }
8779
+ function evaluateRunGrant(grants, request2) {
8780
+ const deny = (reason) => ({ matches: false, reason });
8781
+ const candidates = grants.filter((g) => g.id === request2.id);
8782
+ if (candidates.length !== 1) return deny("missing or duplicate grant");
8783
+ const grant = candidates[0];
8784
+ if (!isRunGrant(grant)) return deny("invalid grant");
8785
+ if (!Number.isFinite(request2.now.getTime())) return deny("invalid clock");
8786
+ if (grant.status !== "active") return deny("revoked grant");
8787
+ if (request2.now.getTime() < Date.parse(grant.grantedAt)) return deny("grant not yet active");
8788
+ if (request2.now.getTime() >= Date.parse(grant.expiresAt)) return deny("expired grant");
8789
+ if (grant.scope.repository !== request2.repository || grant.scope.task !== request2.task) {
8790
+ return deny("repository or task outside scope");
8791
+ }
8792
+ const prefix = grant.scope.branchPrefix;
8793
+ const boundary = prefix.endsWith("/") || prefix.endsWith("-");
8794
+ if (!(request2.branch === prefix || request2.branch.startsWith(boundary ? prefix : `${prefix}-`) || !boundary && request2.branch.startsWith(`${prefix}/`))) return deny("branch outside scope");
8795
+ if (grant.excludedActions.some((a) => a === request2.action)) return deny("excluded action");
8796
+ if (!grant.permittedActions.some((a) => a === request2.action)) return deny("action not permitted");
8797
+ return { matches: true, reason: "record matches; verify operator consent and execution/platform gates", grant };
8798
+ }
8799
+
8800
+ // src/commands/doctor.ts
8730
8801
  function isRecord7(value) {
8731
8802
  return typeof value === "object" && value !== null && !Array.isArray(value);
8732
8803
  }
@@ -8745,13 +8816,13 @@ async function checkNodeVersion() {
8745
8816
  }
8746
8817
  async function checkArcaneManifest(targetDir) {
8747
8818
  const name = "Arcane manifest (.arcane.json)";
8748
- const manifestPath2 = join24(targetDir, ".arcane.json");
8819
+ const manifestPath2 = join25(targetDir, ".arcane.json");
8749
8820
  try {
8750
8821
  await access4(manifestPath2);
8751
8822
  } catch {
8752
- const selfHostedManifestPath = join24(targetDir, "src", "assets", ".arcane.json");
8823
+ const selfHostedManifestPath = join25(targetDir, "src", "assets", ".arcane.json");
8753
8824
  try {
8754
- const raw = await readFile19(selfHostedManifestPath, "utf8");
8825
+ const raw = await readFile20(selfHostedManifestPath, "utf8");
8755
8826
  const manifest = JSON.parse(raw);
8756
8827
  if (isRecord7(manifest) && manifest["selfHosted"] === true && manifest["tracking_mode"] === "internal") {
8757
8828
  return {
@@ -8770,7 +8841,7 @@ async function checkArcaneManifest(targetDir) {
8770
8841
  };
8771
8842
  }
8772
8843
  try {
8773
- const raw = await readFile19(manifestPath2, "utf8");
8844
+ const raw = await readFile20(manifestPath2, "utf8");
8774
8845
  const manifest = JSON.parse(raw);
8775
8846
  if (!isRecord7(manifest) || typeof manifest["version"] !== "string" || !manifest["version"] || !Array.isArray(manifest["components"])) {
8776
8847
  return {
@@ -8855,7 +8926,7 @@ async function checkSessionContinuity(targetDir) {
8855
8926
  const results = [];
8856
8927
  for (const file of SESSION_CONTINUITY_FILES) {
8857
8928
  const name = `Session file: ${file}`;
8858
- const filePath = join24(targetDir, file);
8929
+ const filePath = join25(targetDir, file);
8859
8930
  try {
8860
8931
  await access4(filePath);
8861
8932
  results.push({ name, passed: true, message: "present" });
@@ -8873,7 +8944,7 @@ async function checkSessionContinuity(targetDir) {
8873
8944
  async function fixSessionContinuity(targetDir, assetsDir) {
8874
8945
  const created = [];
8875
8946
  for (const file of SESSION_CONTINUITY_FILES) {
8876
- const destPath = join24(targetDir, file);
8947
+ const destPath = join25(targetDir, file);
8877
8948
  try {
8878
8949
  await access4(destPath);
8879
8950
  } catch {
@@ -8891,7 +8962,7 @@ async function fixSessionContinuity(targetDir, assetsDir) {
8891
8962
  );
8892
8963
  continue;
8893
8964
  }
8894
- const srcPath = join24(assetsDir, file);
8965
+ const srcPath = join25(assetsDir, file);
8895
8966
  await mkdir7(dirname12(destPath), { recursive: true });
8896
8967
  await fsCopyFile(srcPath, destPath);
8897
8968
  created.push(file);
@@ -9070,7 +9141,7 @@ async function checkGovernancePlaceholders(targetDir) {
9070
9141
  const name = "Governance placeholders (ARC-051)";
9071
9142
  let scan;
9072
9143
  try {
9073
- scan = await scanGovernancePlaceholders(join24(targetDir, ".arcane", "governance"));
9144
+ scan = await scanGovernancePlaceholders(join25(targetDir, ".arcane", "governance"));
9074
9145
  } catch (err) {
9075
9146
  return {
9076
9147
  name,
@@ -9157,10 +9228,10 @@ async function checkNewlyAvailableComponents(targetDir) {
9157
9228
  }
9158
9229
  async function checkMcpConfig(targetDir) {
9159
9230
  const name = "MCP config (per-server timeout, I12)";
9160
- const filePath = join24(targetDir, ".mcp.json");
9231
+ const filePath = join25(targetDir, ".mcp.json");
9161
9232
  let raw;
9162
9233
  try {
9163
- raw = await readFile19(filePath, "utf8");
9234
+ raw = await readFile20(filePath, "utf8");
9164
9235
  } catch {
9165
9236
  return { name, passed: true, blocking: false, message: "no .mcp.json \u2014 nothing to check" };
9166
9237
  }
@@ -9240,10 +9311,10 @@ function isDelegationsFile(value) {
9240
9311
  }
9241
9312
  async function checkDelegations(targetDir) {
9242
9313
  const name = "Standing delegations (T13)";
9243
- const filePath = join24(targetDir, ".arcane", "delegations.json");
9314
+ const filePath = join25(targetDir, ".arcane", "delegations.json");
9244
9315
  let raw;
9245
9316
  try {
9246
- raw = await readFile19(filePath, "utf8");
9317
+ raw = await readFile20(filePath, "utf8");
9247
9318
  } catch {
9248
9319
  return { name, passed: true, blocking: false, message: "no delegations recorded" };
9249
9320
  }
@@ -9279,6 +9350,23 @@ async function checkDelegations(targetDir) {
9279
9350
  message: `${active.length} active: ${summary}`
9280
9351
  };
9281
9352
  }
9353
+ async function checkRunGrants(targetDir, now = /* @__PURE__ */ new Date()) {
9354
+ const name = "Run grants (ARC-061)";
9355
+ try {
9356
+ const grants = await readRunGrants(targetDir);
9357
+ const active = grants.filter((g) => g.status === "active" && Date.parse(g.grantedAt) <= now.getTime() && now.getTime() < Date.parse(g.expiresAt));
9358
+ const inactive = grants.length - active.length;
9359
+ return {
9360
+ name,
9361
+ passed: true,
9362
+ blocking: false,
9363
+ showDetails: grants.length > 0,
9364
+ message: grants.length === 0 ? "no run grants recorded" : `${active.length} active run grant(s): ${active.map((g) => `${g.id} (expires ${g.expiresAt}; task: ${g.scope.task}; branch: ${g.scope.branchPrefix})`).join("; ")}; ${inactive} expired, future or revoked`
9365
+ };
9366
+ } catch {
9367
+ return { name, passed: false, blocking: false, message: "run grant file unreadable or invalid; no run authority can be inferred" };
9368
+ }
9369
+ }
9282
9370
  async function checkPlatformBranchPolicy(targetDir) {
9283
9371
  const name = "Platform branch/merge policy (T11)";
9284
9372
  const targets = await pushTargets(targetDir);
@@ -9343,7 +9431,7 @@ async function checkManagedFileModes(targetDir) {
9343
9431
  if (!isClientShimPath(file) || asLinks.includes(file)) continue;
9344
9432
  let body;
9345
9433
  try {
9346
- body = await readFile19(join24(targetDir, file), "utf-8");
9434
+ body = await readFile20(join25(targetDir, file), "utf-8");
9347
9435
  } catch {
9348
9436
  continue;
9349
9437
  }
@@ -9396,7 +9484,7 @@ async function checkSpellScope(targetDir, homeDir = resolveHomeDir()) {
9396
9484
  let probed = false;
9397
9485
  const probe2 = "spell-status.md";
9398
9486
  try {
9399
- const entries = await readdir11(join24(storeRoot, USER_TIER_SPELLS_DIR));
9487
+ const entries = await readdir11(join25(storeRoot, USER_TIER_SPELLS_DIR));
9400
9488
  const spells = entries.filter((e) => e.startsWith("spell-") && e.endsWith(".md"));
9401
9489
  spellCount = spells.length;
9402
9490
  probed = spells.includes(probe2);
@@ -9456,6 +9544,7 @@ async function runDoctor(targetDir, options = {}, assetsDir) {
9456
9544
  checkPushPolicy(targetDir),
9457
9545
  checkPlatformBranchPolicy(targetDir),
9458
9546
  checkDelegations(targetDir),
9547
+ checkRunGrants(targetDir),
9459
9548
  checkMcpConfig(targetDir),
9460
9549
  checkUserTier(resolveHomeDir(), options.packageVersion),
9461
9550
  checkSpellScope(targetDir),
@@ -9473,7 +9562,7 @@ async function runDoctor(targetDir, options = {}, assetsDir) {
9473
9562
  const icon = skipped ? "\u2013" : result.passed ? "\u2713" : result.blocking === false ? "\u26A0" : "\u2717";
9474
9563
  const label2 = skipped ? "skip" : result.passed ? "pass" : result.blocking === false ? "warn" : "FAIL";
9475
9564
  console.log(` ${icon} [${label2}] ${result.name}`);
9476
- if (!result.passed || skipped) {
9565
+ if (!result.passed || skipped || result.showDetails === true) {
9477
9566
  console.log(` ${result.message}`);
9478
9567
  }
9479
9568
  if (!result.passed && result.blocking !== false) {
@@ -9518,17 +9607,31 @@ async function runDoctor(targetDir, options = {}, assetsDir) {
9518
9607
  }
9519
9608
  }
9520
9609
 
9610
+ // src/commands/grant.ts
9611
+ async function checkRunGrant(targetDir, request2) {
9612
+ try {
9613
+ return evaluateRunGrant(await readRunGrants(targetDir), request2);
9614
+ } catch {
9615
+ return { matches: false, reason: "grant file unreadable or invalid" };
9616
+ }
9617
+ }
9618
+ async function runGrantCheck(options) {
9619
+ const result = await checkRunGrant(process.cwd(), { ...options, now: /* @__PURE__ */ new Date() });
9620
+ console.log(JSON.stringify({ matches: result.matches, reason: result.reason }));
9621
+ if (!result.matches) process.exitCode = 1;
9622
+ }
9623
+
9521
9624
  // src/commands/ward.ts
9522
- import { readFile as readFile21 } from "fs/promises";
9625
+ import { readFile as readFile22 } from "fs/promises";
9523
9626
 
9524
9627
  // src/modules/ward.ts
9525
- import { readdir as readdir12, readFile as readFile20 } from "fs/promises";
9526
- import { join as join25, relative as relative3 } from "path";
9628
+ import { readdir as readdir12, readFile as readFile21 } from "fs/promises";
9629
+ import { join as join26, relative as relative3 } from "path";
9527
9630
  async function scanFileAllRules(absolutePath, displayPath, rules) {
9528
9631
  const findings = [];
9529
9632
  let content;
9530
9633
  try {
9531
- content = await readFile20(absolutePath, "utf8");
9634
+ content = await readFile21(absolutePath, "utf8");
9532
9635
  } catch {
9533
9636
  return findings;
9534
9637
  }
@@ -9586,7 +9689,7 @@ async function scanFilenames(root, rules, repoRoot = root) {
9586
9689
  return findings;
9587
9690
  }
9588
9691
  for (const entry of entries) {
9589
- const absolutePath = join25(root, entry.name);
9692
+ const absolutePath = join26(root, entry.name);
9590
9693
  if (entry.isDirectory()) {
9591
9694
  if (SKIP_DIRECTORIES.has(entry.name)) continue;
9592
9695
  findings.push(...await scanFilenames(absolutePath, rules, repoRoot));
@@ -9623,7 +9726,7 @@ async function runWard(repoRoot, options = {}) {
9623
9726
  const findings2 = await scanFileAllRules(absolutePath, displayPath, allRules);
9624
9727
  if (findings2.length > 0) {
9625
9728
  try {
9626
- fileLines.set(displayPath, (await readFile20(absolutePath, "utf8")).split("\n"));
9729
+ fileLines.set(displayPath, (await readFile21(absolutePath, "utf8")).split("\n"));
9627
9730
  } catch {
9628
9731
  }
9629
9732
  }
@@ -9651,7 +9754,7 @@ function parseTermsFileContent(raw) {
9651
9754
  }
9652
9755
  async function readTermsFile(filePath) {
9653
9756
  try {
9654
- return parseTermsFileContent(await readFile21(filePath, "utf8"));
9757
+ return parseTermsFileContent(await readFile22(filePath, "utf8"));
9655
9758
  } catch (error2) {
9656
9759
  console.warn(
9657
9760
  `\u26A0 arcane: could not read --terms-file "${filePath}": ${error2.message}`
@@ -9906,16 +10009,16 @@ import * as client from "openid-client";
9906
10009
 
9907
10010
  // src/modules/result-pages.ts
9908
10011
  import { createHash as createHash5 } from "crypto";
9909
- import { readFile as readFile22 } from "fs/promises";
9910
- import { join as join26 } from "path";
10012
+ import { readFile as readFile23 } from "fs/promises";
10013
+ import { join as join27 } from "path";
9911
10014
  var RESULT_KINDS = ["success", "denied", "failure"];
9912
10015
  function resultPagePath(kind) {
9913
- return join26("sign-in", `sign-in-callback-${kind}.html`);
10016
+ return join27("sign-in", `sign-in-callback-${kind}.html`);
9914
10017
  }
9915
10018
  function styleHashes(html) {
9916
10019
  return [...html.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/gi)].map((match2) => {
9917
- const text2 = match2[1].replace(/\r\n?/g, "\n");
9918
- return `'sha256-${createHash5("sha256").update(text2, "utf8").digest("base64")}'`;
10020
+ const text3 = match2[1].replace(/\r\n?/g, "\n");
10021
+ return `'sha256-${createHash5("sha256").update(text3, "utf8").digest("base64")}'`;
9919
10022
  });
9920
10023
  }
9921
10024
  function resultPageHeaders(html) {
@@ -9958,7 +10061,7 @@ var PLAIN_RESULT_PAGES = {
9958
10061
  async function loadResultPages(assetsDir) {
9959
10062
  try {
9960
10063
  const [success, denied, failure] = await Promise.all(
9961
- RESULT_KINDS.map(async (kind) => resultPage(await readFile22(join26(assetsDir, resultPagePath(kind)), "utf8")))
10064
+ RESULT_KINDS.map(async (kind) => resultPage(await readFile23(join27(assetsDir, resultPagePath(kind)), "utf8")))
9962
10065
  );
9963
10066
  return { success, denied, failure };
9964
10067
  } catch {
@@ -10196,9 +10299,9 @@ function toSessionRecord(result, environment) {
10196
10299
  expiresAt: result.expiresAt.toISOString()
10197
10300
  };
10198
10301
  }
10199
- function describeAccount(record) {
10200
- const who = record.email ? `${record.email} (${record.sub})` : record.sub;
10201
- return record.environment === "development" ? `${who} [development]` : who;
10302
+ function describeAccount(record2) {
10303
+ const who = record2.email ? `${record2.email} (${record2.sub})` : record2.sub;
10304
+ return record2.environment === "development" ? `${who} [development]` : who;
10202
10305
  }
10203
10306
  async function runLogin(options, deps) {
10204
10307
  let environment;
@@ -10229,11 +10332,11 @@ async function runLogin(options, deps) {
10229
10332
  process.exit(1);
10230
10333
  return;
10231
10334
  }
10232
- let record;
10335
+ let record2;
10233
10336
  const saveValidatedSession = (result) => {
10234
10337
  const next = toSessionRecord(result, environment);
10235
10338
  deps.storage.save(next, { insecure: Boolean(options.insecureStorage) });
10236
- record = next;
10339
+ record2 = next;
10237
10340
  };
10238
10341
  try {
10239
10342
  if (options.deviceCode) {
@@ -10258,7 +10361,7 @@ async function runLogin(options, deps) {
10258
10361
  printWarning("No browser could be opened. Run `spell login --device-code` to sign in on another device.");
10259
10362
  }
10260
10363
  });
10261
- if (!record) throw new Error("The browser sign-in ended without storing a session.");
10364
+ if (!record2) throw new Error("The browser sign-in ended without storing a session.");
10262
10365
  }
10263
10366
  } catch (error2) {
10264
10367
  if (error2 instanceof KeychainUnavailableError) {
@@ -10273,8 +10376,8 @@ async function runLogin(options, deps) {
10273
10376
  process.exit(1);
10274
10377
  return;
10275
10378
  }
10276
- if (!record) throw new Error("The sign-in completed without a stored session.");
10277
- printSuccess(`Signed in as ${describeAccount(record)}.`);
10379
+ if (!record2) throw new Error("The sign-in completed without a stored session.");
10380
+ printSuccess(`Signed in as ${describeAccount(record2)}.`);
10278
10381
  if (options.insecureStorage) printWarning(INSECURE_STORAGE_WARNING);
10279
10382
  }
10280
10383
 
@@ -10342,21 +10445,21 @@ async function runWhoami(options, deps = defaultWhoamiDeps()) {
10342
10445
  process.exit(1);
10343
10446
  return;
10344
10447
  }
10345
- const { record, source } = loaded;
10346
- printSuccess(`Signed in as ${describeAccount(record)}.`);
10347
- const expires = new Date(record.expiresAt);
10448
+ const { record: record2, source } = loaded;
10449
+ printSuccess(`Signed in as ${describeAccount(record2)}.`);
10450
+ const expires = new Date(record2.expiresAt);
10348
10451
  const expired = expires.getTime() <= deps.now().getTime();
10349
- printInfo(`Signed in on ${new Date(record.obtainedAt).toLocaleString()}.`);
10452
+ printInfo(`Signed in on ${new Date(record2.obtainedAt).toLocaleString()}.`);
10350
10453
  printInfo(
10351
10454
  expired ? `The last token expired on ${expires.toLocaleString()}; the session refreshes on next use.` : `The current token is valid until ${expires.toLocaleString()}.`
10352
10455
  );
10353
10456
  printInfo(`Stored in ${source === "keychain" ? "the OS keychain" : "the insecure session file"}.`);
10354
10457
  if (source === "file") printWarning(INSECURE_STORAGE_WARNING);
10355
10458
  if (!options.verify) return;
10356
- const environment = IDENTITY_ENVIRONMENTS[record.environment];
10459
+ const environment = IDENTITY_ENVIRONMENTS[record2.environment];
10357
10460
  try {
10358
10461
  const config = await deps.loadConfiguration(environment);
10359
- const fresh = await deps.refreshSession(config, record.refreshToken, SIGN_IN_SCOPES);
10462
+ const fresh = await deps.refreshSession(config, record2.refreshToken, SIGN_IN_SCOPES);
10360
10463
  deps.storage.save(toSessionRecord(fresh, environment), { insecure: source === "file" });
10361
10464
  printSuccess(`Verified with the identity service at ${environment.signInHost}; the session is active.`);
10362
10465
  } catch (error2) {
@@ -10369,15 +10472,15 @@ async function runWhoami(options, deps = defaultWhoamiDeps()) {
10369
10472
 
10370
10473
  // src/commands/report.ts
10371
10474
  import { access as access5 } from "fs/promises";
10372
- import { join as join29 } from "path";
10475
+ import { join as join30 } from "path";
10373
10476
 
10374
10477
  // src/modules/show-report/generate.ts
10375
- import { mkdir as mkdir8, readdir as readdir13, readFile as readFile24, writeFile as writeFile11 } from "fs/promises";
10376
- import { basename as basename4, dirname as dirname13, isAbsolute, join as join28, relative as relative4, resolve as resolve9 } from "path";
10478
+ import { mkdir as mkdir8, readdir as readdir13, readFile as readFile25, writeFile as writeFile11 } from "fs/promises";
10479
+ import { basename as basename4, dirname as dirname13, isAbsolute, join as join29, relative as relative4, resolve as resolve9 } from "path";
10377
10480
 
10378
10481
  // src/modules/show-report/model.ts
10379
- import { readFile as readFile23 } from "fs/promises";
10380
- import { join as join27 } from "path";
10482
+ import { readFile as readFile24 } from "fs/promises";
10483
+ import { join as join28 } from "path";
10381
10484
 
10382
10485
  // src/modules/show-report/plan-parser.ts
10383
10486
  function parseFrontmatter(content) {
@@ -10512,8 +10615,8 @@ function computeCoverageOutcome(content) {
10512
10615
  const dispositioned = total - parkedCount;
10513
10616
  return `${total} of ${total} items dispositioned \u2014 ${dispositioned} routed to an epic, ${parkedCount} parked`;
10514
10617
  }
10515
- function stripMarkdown(text2) {
10516
- return text2.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\*\*/g, "").replace(/`/g, "").trim();
10618
+ function stripMarkdown(text3) {
10619
+ return text3.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\*\*/g, "").replace(/`/g, "").trim();
10517
10620
  }
10518
10621
 
10519
10622
  // src/modules/show-report/queue-parser.ts
@@ -10559,8 +10662,8 @@ function parseVerificationLedgerSection(content, programNameWords) {
10559
10662
  }
10560
10663
  return rows;
10561
10664
  }
10562
- function stripMarkdown2(text2) {
10563
- return text2.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\*\*/g, "").replace(/`/g, "").trim();
10665
+ function stripMarkdown2(text3) {
10666
+ return text3.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\*\*/g, "").replace(/`/g, "").trim();
10564
10667
  }
10565
10668
 
10566
10669
  // src/modules/show-report/sources.ts
@@ -10628,7 +10731,7 @@ async function getCast(cwd, fromRef, toRef, generatedOutputs = []) {
10628
10731
  // src/modules/show-report/model.ts
10629
10732
  async function readOptional(path5) {
10630
10733
  try {
10631
- return await readFile23(path5, "utf8");
10734
+ return await readFile24(path5, "utf8");
10632
10735
  } catch {
10633
10736
  return null;
10634
10737
  }
@@ -10696,9 +10799,9 @@ function buildNeedsYou(queueEntries) {
10696
10799
  return entry;
10697
10800
  });
10698
10801
  }
10699
- function truncateAtWord(text2, limit) {
10700
- if (text2.length <= limit) return text2;
10701
- const window = text2.slice(0, limit - 1);
10802
+ function truncateAtWord(text3, limit) {
10803
+ if (text3.length <= limit) return text3;
10804
+ const window = text3.slice(0, limit - 1);
10702
10805
  const lastSpace = window.lastIndexOf(" ");
10703
10806
  const head = lastSpace > 0 ? window.slice(0, lastSpace) : window;
10704
10807
  return `${head.replace(/[\s,;:.—-]+$/, "")}\u2026`;
@@ -10724,13 +10827,13 @@ function buildCorrections(ledgerRows, programName) {
10724
10827
  };
10725
10828
  }
10726
10829
  async function buildShowReportModel(options) {
10727
- const planAbsPath = join27(options.rootDir, options.planRelPath);
10728
- const queueAbsPath = join27(options.rootDir, options.queueRelPath);
10830
+ const planAbsPath = join28(options.rootDir, options.planRelPath);
10831
+ const queueAbsPath = join28(options.rootDir, options.queueRelPath);
10729
10832
  const ledgerRelPath = "docs/verification-ledger.md";
10730
- const ledgerAbsPath = join27(options.rootDir, ledgerRelPath);
10731
- const decisionsAbsPath = join27(options.rootDir, "DECISIONS.md");
10833
+ const ledgerAbsPath = join28(options.rootDir, ledgerRelPath);
10834
+ const decisionsAbsPath = join28(options.rootDir, "DECISIONS.md");
10732
10835
  const [planContent, queueContent, ledgerContent, decisionsContent] = await Promise.all([
10733
- readFile23(planAbsPath, "utf8"),
10836
+ readFile24(planAbsPath, "utf8"),
10734
10837
  readOptional(queueAbsPath),
10735
10838
  readOptional(ledgerAbsPath),
10736
10839
  readOptional(decisionsAbsPath)
@@ -10824,11 +10927,11 @@ var CATEGORY_ORDER = [
10824
10927
  "docs",
10825
10928
  "platform"
10826
10929
  ];
10827
- function escapeHtml(text2) {
10828
- return text2.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
10930
+ function escapeHtml(text3) {
10931
+ return text3.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
10829
10932
  }
10830
- function inlineMarkupToHtml(text2) {
10831
- const escaped = escapeHtml(text2);
10933
+ function inlineMarkupToHtml(text3) {
10934
+ const escaped = escapeHtml(text3);
10832
10935
  const withLinks = escaped.replace(
10833
10936
  /\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g,
10834
10937
  '<a href="$2">$1</a>'
@@ -10926,7 +11029,7 @@ function toPosix(path5) {
10926
11029
  async function programFromPlan(rootDir, slug, planRelPath) {
10927
11030
  let planContent;
10928
11031
  try {
10929
- planContent = await readFile24(join28(rootDir, planRelPath), "utf8");
11032
+ planContent = await readFile25(join29(rootDir, planRelPath), "utf8");
10930
11033
  } catch {
10931
11034
  return { skipped: { slug, reason: `no PLAN.md at ${planRelPath}` } };
10932
11035
  }
@@ -10938,13 +11041,13 @@ async function programFromPlan(rootDir, slug, planRelPath) {
10938
11041
  program: {
10939
11042
  slug,
10940
11043
  planRelPath,
10941
- queueRelPath: toPosix(join28(dirname13(planRelPath), "OPERATOR-QUEUE.md")),
11044
+ queueRelPath: toPosix(join29(dirname13(planRelPath), "OPERATOR-QUEUE.md")),
10942
11045
  ledgerProgramName: programNameFromTitle(frontmatter.title ?? slug)
10943
11046
  }
10944
11047
  };
10945
11048
  }
10946
11049
  async function discoverPrograms(rootDir) {
10947
- const plansDir = join28(rootDir, "docs", "plans");
11050
+ const plansDir = join29(rootDir, "docs", "plans");
10948
11051
  let entries;
10949
11052
  try {
10950
11053
  entries = (await readdir13(plansDir, { withFileTypes: true })).filter((e) => e.isDirectory());
@@ -10971,14 +11074,14 @@ function normalizeLineEndings2(content) {
10971
11074
  }
10972
11075
  async function readOptionalFile(path5) {
10973
11076
  try {
10974
- return await readFile24(path5, "utf8");
11077
+ return await readFile25(path5, "utf8");
10975
11078
  } catch (error2) {
10976
11079
  if (error2.code === "ENOENT") return null;
10977
11080
  throw error2;
10978
11081
  }
10979
11082
  }
10980
11083
  async function generateReport(rootDir, program2, templateHtml, theme = "auto") {
10981
- const planContent = await readFile24(join28(rootDir, program2.planRelPath), "utf8");
11084
+ const planContent = await readFile25(join29(rootDir, program2.planRelPath), "utf8");
10982
11085
  const frontmatter = parseFrontmatter(planContent);
10983
11086
  const model = await buildShowReportModel({
10984
11087
  rootDir,
@@ -10998,7 +11101,7 @@ async function generateReport(rootDir, program2, templateHtml, theme = "auto") {
10998
11101
  };
10999
11102
  }
11000
11103
  async function runReportGeneration(options) {
11001
- const templateHtml = await readFile24(options.templatePath, "utf8");
11104
+ const templateHtml = await readFile25(options.templatePath, "utf8");
11002
11105
  const theme = options.theme ?? "auto";
11003
11106
  const drifted = [];
11004
11107
  const repaired = [];
@@ -11016,12 +11119,12 @@ async function runReportGeneration(options) {
11016
11119
  [`${outRel}/show-report.html`, generated.html]
11017
11120
  ];
11018
11121
  for (const [relPath, expected] of outputs) {
11019
- const actual = await readOptionalFile(join28(options.rootDir, relPath));
11122
+ const actual = await readOptionalFile(join29(options.rootDir, relPath));
11020
11123
  if (actual === null || normalizeLineEndings2(actual) !== normalizeLineEndings2(expected)) {
11021
11124
  drifted.push(`${relPath} does not match a regeneration from its sources.`);
11022
11125
  if (options.mode === "write") {
11023
11126
  await mkdir8(outAbs, { recursive: true });
11024
- await writeFile11(join28(options.rootDir, relPath), expected, "utf8");
11127
+ await writeFile11(join29(options.rootDir, relPath), expected, "utf8");
11025
11128
  repaired.push(relPath);
11026
11129
  }
11027
11130
  }
@@ -11032,7 +11135,7 @@ async function runReportGeneration(options) {
11032
11135
 
11033
11136
  // src/commands/report.ts
11034
11137
  async function runReport(targetDir, options = {}, assetsDir) {
11035
- const templatePath = join29(assetsDir, "report", "show-report.template.html");
11138
+ const templatePath = join30(assetsDir, "report", "show-report.template.html");
11036
11139
  try {
11037
11140
  await access5(templatePath);
11038
11141
  } catch {
@@ -11156,12 +11259,12 @@ function mintRef(type, generate = uuidv7) {
11156
11259
  function mintSessionRef() {
11157
11260
  return mintRef("session");
11158
11261
  }
11159
- function parseRef(text2, expected) {
11160
- if (typeof text2 !== "string") return { ok: false, reason: "a reference must be a string" };
11161
- if (text2.length === 0) return { ok: false, reason: "a reference must not be empty" };
11162
- if (text2 !== text2.trim()) return { ok: false, reason: "a reference must not carry surrounding whitespace" };
11163
- if (text2 !== text2.toLowerCase()) return { ok: false, reason: "a reference must be lowercase" };
11164
- const parts = text2.split(":");
11262
+ function parseRef(text3, expected) {
11263
+ if (typeof text3 !== "string") return { ok: false, reason: "a reference must be a string" };
11264
+ if (text3.length === 0) return { ok: false, reason: "a reference must not be empty" };
11265
+ if (text3 !== text3.trim()) return { ok: false, reason: "a reference must not carry surrounding whitespace" };
11266
+ if (text3 !== text3.toLowerCase()) return { ok: false, reason: "a reference must be lowercase" };
11267
+ const parts = text3.split(":");
11165
11268
  if (parts.length !== 3 || parts[0] !== "arcane") {
11166
11269
  return { ok: false, reason: 'a reference must have the form "arcane:<type>:<uuid>"' };
11167
11270
  }
@@ -11398,7 +11501,7 @@ var RpcError = class extends Error {
11398
11501
  function isObject(value) {
11399
11502
  return typeof value === "object" && value !== null && !Array.isArray(value);
11400
11503
  }
11401
- function text(value, isError = false) {
11504
+ function text2(value, isError = false) {
11402
11505
  return { content: [{ type: "text", text: value }], isError };
11403
11506
  }
11404
11507
  function stringArg(args, key) {
@@ -11420,10 +11523,10 @@ function serveMcp(options) {
11420
11523
  const fail = (id, error2) => write({ jsonrpc: "2.0", id, error: error2 });
11421
11524
  const report = async (args) => {
11422
11525
  const status = stringArg(args, "status");
11423
- if (status === void 0) return text("status is required: one of working, waiting, blocked, idle.", true);
11526
+ if (status === void 0) return text2("status is required: one of working, waiting, blocked, idle.", true);
11424
11527
  const needs = args["required_actions"];
11425
11528
  if (needs !== void 0 && (!Array.isArray(needs) || needs.some((n) => typeof n !== "string"))) {
11426
- return text("required_actions must be a list of strings.", true);
11529
+ return text2("required_actions must be a list of strings.", true);
11427
11530
  }
11428
11531
  const flags = { status };
11429
11532
  for (const key of ["task", "spell", "message", "agent", "harness"]) {
@@ -11431,21 +11534,21 @@ function serveMcp(options) {
11431
11534
  if (v !== void 0) flags[key] = v;
11432
11535
  }
11433
11536
  if (Array.isArray(needs)) flags.requiredActions = needs;
11434
- if (resolveEndpoint(deps) === null) return text(NOT_CONFIGURED_TEXT);
11537
+ if (resolveEndpoint(deps) === null) return text2(NOT_CONFIGURED_TEXT);
11435
11538
  const where = stringArg(args, "cwd") ?? cwd;
11436
11539
  const outcome = await sendReport(where, flags, deps);
11437
- if (outcome.kind === "refused") return text(outcome.line, true);
11438
- if (outcome.kind === "silent") return text(`Nothing was reported: ${where} is not inside an Arcane repository, or the service did not answer.`);
11439
- const record = outcome.record;
11440
- return { ...text(`Reported ${record.status} for ${record.session}.`), structuredContent: record };
11540
+ if (outcome.kind === "refused") return text2(outcome.line, true);
11541
+ if (outcome.kind === "silent") return text2(`Nothing was reported: ${where} is not inside an Arcane repository, or the service did not answer.`);
11542
+ const record2 = outcome.record;
11543
+ return { ...text2(`Reported ${record2.status} for ${record2.session}.`), structuredContent: record2 };
11441
11544
  };
11442
11545
  const end = async (args) => {
11443
- if (resolveEndpoint(deps) === null) return text(NOT_CONFIGURED_TEXT);
11546
+ if (resolveEndpoint(deps) === null) return text2(NOT_CONFIGURED_TEXT);
11444
11547
  const where = stringArg(args, "cwd") ?? cwd;
11445
11548
  const outcome = await sendEnd(where, deps);
11446
- if (outcome.kind === "refused") return text(outcome.line, true);
11447
- if (outcome.kind === "silent") return text(`No session was open for ${where}.`);
11448
- return { ...text(`Ended ${outcome.record.session}.`), structuredContent: outcome.record };
11549
+ if (outcome.kind === "refused") return text2(outcome.line, true);
11550
+ if (outcome.kind === "silent") return text2(`No session was open for ${where}.`);
11551
+ return { ...text2(`Ended ${outcome.record.session}.`), structuredContent: outcome.record };
11449
11552
  };
11450
11553
  const findResource = (uri) => {
11451
11554
  const resource = PRESENCE_RESOURCES.find((r) => r.uri === uri);
@@ -11591,7 +11694,7 @@ function serveMcp(options) {
11591
11694
  if (name === "presence_report") return await report(args);
11592
11695
  if (name === "presence_end") return await end(args);
11593
11696
  } catch (error2) {
11594
- if (error2 instanceof RpcError && error2.code === -32602) return text(error2.message, true);
11697
+ if (error2 instanceof RpcError && error2.code === -32602) return text2(error2.message, true);
11595
11698
  throw error2;
11596
11699
  }
11597
11700
  throw new RpcError(-32602, `Unknown tool: ${String(name)}`);
@@ -11742,14 +11845,14 @@ function closed(value, field, required, optional = []) {
11742
11845
  if (!isObject2(value)) throw new Refusal("invalid_field", field, `${field ?? "the body"} must be an object`);
11743
11846
  const allowed = /* @__PURE__ */ new Set([...required, ...optional]);
11744
11847
  for (const key of Object.keys(value)) {
11745
- if (!allowed.has(key)) throw new Refusal("unknown_field", join30(field, key), `${join30(field, key)} is not a field of this contract`);
11848
+ if (!allowed.has(key)) throw new Refusal("unknown_field", join31(field, key), `${join31(field, key)} is not a field of this contract`);
11746
11849
  }
11747
11850
  for (const key of required) {
11748
- if (!(key in value)) throw new Refusal("missing_field", join30(field, key), `${join30(field, key)} is required`);
11851
+ if (!(key in value)) throw new Refusal("missing_field", join31(field, key), `${join31(field, key)} is required`);
11749
11852
  }
11750
11853
  return value;
11751
11854
  }
11752
- function join30(parent, key) {
11855
+ function join31(parent, key) {
11753
11856
  return parent === void 0 ? key : `${parent}.${key}`;
11754
11857
  }
11755
11858
  function nonEmpty(value, field) {
@@ -11885,10 +11988,10 @@ var JSON_HEADERS = {
11885
11988
  "X-Content-Type-Options": "nosniff"
11886
11989
  };
11887
11990
  function send2(res, status, body, extra = {}) {
11888
- const text2 = `${JSON.stringify(body)}
11991
+ const text3 = `${JSON.stringify(body)}
11889
11992
  `;
11890
- res.writeHead(status, { ...JSON_HEADERS, "Content-Length": Buffer.byteLength(text2), ...extra });
11891
- res.end(text2);
11993
+ res.writeHead(status, { ...JSON_HEADERS, "Content-Length": Buffer.byteLength(text3), ...extra });
11994
+ res.end(text3);
11892
11995
  }
11893
11996
  function error(res, status, code, message, field, extra = {}) {
11894
11997
  const body = { error: field === void 0 ? { code, message } : { code, field, message } };
@@ -11935,9 +12038,9 @@ function readBody(req, res) {
11935
12038
  });
11936
12039
  });
11937
12040
  }
11938
- function parseJson(text2) {
12041
+ function parseJson(text3) {
11939
12042
  try {
11940
- return { ok: true, value: JSON.parse(text2) };
12043
+ return { ok: true, value: JSON.parse(text3) };
11941
12044
  } catch {
11942
12045
  return { ok: false };
11943
12046
  }
@@ -12051,13 +12154,13 @@ data: ${JSON.stringify(hello)}
12051
12154
  if (path5 === "/v0/events") return events(req, res);
12052
12155
  if (path5 === "/v0/sessions/{session}") {
12053
12156
  const parsed = parseRef(found.param, "session");
12054
- const record = parsed.ok ? table.get(found.param ?? "") : null;
12055
- if (record === null) return error(res, 404, "not_found", "no such session");
12056
- return send2(res, 200, record);
12157
+ const record2 = parsed.ok ? table.get(found.param ?? "") : null;
12158
+ if (record2 === null) return error(res, 404, "not_found", "no such session");
12159
+ return send2(res, 200, record2);
12057
12160
  }
12058
- const text2 = await readBody(req, res);
12059
- if (text2 === null) return;
12060
- const json = parseJson(text2);
12161
+ const text3 = await readBody(req, res);
12162
+ if (text3 === null) return;
12163
+ const json = parseJson(text3);
12061
12164
  if (!json.ok) return error(res, 400, "invalid_json", "the body is not valid JSON");
12062
12165
  if (path5 === "/v0/sessions/report") {
12063
12166
  const result2 = validateReport(json.value);
@@ -12066,8 +12169,8 @@ data: ${JSON.stringify(hello)}
12066
12169
  if (isNew && table.count() >= MAX_SESSIONS) {
12067
12170
  return error(res, 503, "too_many_sessions", `at most ${MAX_SESSIONS} sessions are held at once`, void 0, { "Retry-After": "60" });
12068
12171
  }
12069
- const { record, created } = table.report(result2.value);
12070
- return send2(res, created ? 201 : 200, record);
12172
+ const { record: record2, created } = table.report(result2.value);
12173
+ return send2(res, created ? 201 : 200, record2);
12071
12174
  }
12072
12175
  const result = validateEnd(json.value);
12073
12176
  if (!result.ok) return error(res, 400, result.code, result.message, result.field);
@@ -12097,8 +12200,8 @@ data: ${JSON.stringify(hello)}
12097
12200
  }
12098
12201
 
12099
12202
  // src/modules/presence/sessions.ts
12100
- function copy(record) {
12101
- return structuredClone(record);
12203
+ function copy(record2) {
12204
+ return structuredClone(record2);
12102
12205
  }
12103
12206
  function sameRequiredActions(a, b) {
12104
12207
  if (a.length !== b.length) return false;
@@ -12115,9 +12218,9 @@ function createSessionTable(options) {
12115
12218
  const expireAfterMs = options.expireAfterMs ?? EXPIRE_AFTER_MS;
12116
12219
  const entries = /* @__PURE__ */ new Map();
12117
12220
  const listeners = /* @__PURE__ */ new Set();
12118
- const emit = (name, record) => {
12119
- const event = { event: name, data: copy(record) };
12120
- for (const listener of listeners) listener({ event: name, data: copy(record) });
12221
+ const emit = (name, record2) => {
12222
+ const event = { event: name, data: copy(record2) };
12223
+ for (const listener of listeners) listener({ event: name, data: copy(record2) });
12121
12224
  return event;
12122
12225
  };
12123
12226
  return {
@@ -12128,7 +12231,7 @@ function createSessionTable(options) {
12128
12231
  const requiredActions2 = (body.required_actions ?? []).map((n) => ({ action: n.action, reason: n.reason }));
12129
12232
  const existing = entries.get(key);
12130
12233
  if (existing === void 0) {
12131
- const record2 = {
12234
+ const record3 = {
12132
12235
  session: options.mint(),
12133
12236
  agent: structuredClone(body.agent),
12134
12237
  harness: body.harness,
@@ -12144,41 +12247,41 @@ function createSessionTable(options) {
12144
12247
  ended_at: null,
12145
12248
  stale: false
12146
12249
  };
12147
- entries.set(key, { record: record2, reportedMs: ms });
12148
- emit("session.started", record2);
12149
- return { record: copy(record2), created: true, event: "session.started" };
12250
+ entries.set(key, { record: record3, reportedMs: ms });
12251
+ emit("session.started", record3);
12252
+ return { record: copy(record3), created: true, event: "session.started" };
12150
12253
  }
12151
- const record = existing.record;
12152
- const wasStale = record.stale;
12153
- const needsChanged = !sameRequiredActions(record.required_actions, requiredActions2);
12154
- record.agent = structuredClone(body.agent);
12155
- record.harness = body.harness;
12156
- record.repository = structuredClone(body.repository);
12157
- record.checkout = structuredClone(body.checkout);
12158
- if (body.spell !== void 0) record.spell = body.spell;
12159
- if (body.task !== void 0) record.task = body.task;
12160
- if (body.message !== void 0) record.message = body.message;
12161
- record.status = body.status;
12162
- record.required_actions = requiredActions2;
12163
- record.reported_at = at;
12164
- record.stale = false;
12254
+ const record2 = existing.record;
12255
+ const wasStale = record2.stale;
12256
+ const needsChanged = !sameRequiredActions(record2.required_actions, requiredActions2);
12257
+ record2.agent = structuredClone(body.agent);
12258
+ record2.harness = body.harness;
12259
+ record2.repository = structuredClone(body.repository);
12260
+ record2.checkout = structuredClone(body.checkout);
12261
+ if (body.spell !== void 0) record2.spell = body.spell;
12262
+ if (body.task !== void 0) record2.task = body.task;
12263
+ if (body.message !== void 0) record2.message = body.message;
12264
+ record2.status = body.status;
12265
+ record2.required_actions = requiredActions2;
12266
+ record2.reported_at = at;
12267
+ record2.stale = false;
12165
12268
  existing.reportedMs = ms;
12166
12269
  const event = wasStale ? "session.resumed" : needsChanged ? "session.action_required" : "session.updated";
12167
- emit(event, record);
12168
- return { record: copy(record), created: false, event };
12270
+ emit(event, record2);
12271
+ return { record: copy(record2), created: false, event };
12169
12272
  },
12170
12273
  end(checkoutPath) {
12171
12274
  const existing = entries.get(checkoutPath);
12172
12275
  if (existing === void 0) return null;
12173
12276
  entries.delete(checkoutPath);
12174
- const record = existing.record;
12175
- record.status = "done";
12176
- record.ended_at = new Date(now()).toISOString();
12177
- emit("session.ended", record);
12178
- return copy(record);
12277
+ const record2 = existing.record;
12278
+ record2.status = "done";
12279
+ record2.ended_at = new Date(now()).toISOString();
12280
+ emit("session.ended", record2);
12281
+ return copy(record2);
12179
12282
  },
12180
12283
  get(session) {
12181
- for (const { record } of entries.values()) if (record.session === session) return copy(record);
12284
+ for (const { record: record2 } of entries.values()) if (record2.session === session) return copy(record2);
12182
12285
  return null;
12183
12286
  },
12184
12287
  snapshot() {
@@ -12474,7 +12577,7 @@ async function runPresenceMcp() {
12474
12577
 
12475
12578
  // src/index.ts
12476
12579
  import { realpathSync as realpathSync2 } from "fs";
12477
- var ASSETS_DIR = process.env["ARCANE_ASSETS_DIR"] ?? join31(dirname15(fileURLToPath(import.meta.url)), "assets");
12580
+ var ASSETS_DIR = process.env["ARCANE_ASSETS_DIR"] ?? join32(dirname15(fileURLToPath(import.meta.url)), "assets");
12478
12581
  var program = new Command();
12479
12582
  var USER_FLAG_DESCRIPTION = "Operate on the per-user tier at ~/.arcane \u2014 spells shared by every repository on this machine, fanned out to ~/.agents/skills (Codex, Copilot) and ~/.claude/commands (Claude Code)";
12480
12583
  var USER_TIER_AGENTS_DESCRIPTION = "Operate on the per-user tier at ~/.arcane \u2014 one agent roster shared by every repository on this machine, rendered to ~/.copilot/agents (VS Code agent modes)";
@@ -12693,6 +12796,7 @@ agentsCommand.command("list").description("Show the current agent roster with as
12693
12796
  const scope = opts.user ? "user" : "repo";
12694
12797
  await runAgentsList(agentsTargetDir(scope, process.cwd()), scope);
12695
12798
  });
12799
+ program.command("grant").description("Read-only run-grant record checks; operator consent and execution/platform gates remain").command("check").requiredOption("--id <id>", "Run grant ID").requiredOption("--repository <repository>", "Exact repository identity recorded in scope").requiredOption("--task <task>", "Exact approved task ID").requiredOption("--branch <branch>", "Current source branch").requiredOption("--action <action>", "Action to check (for example commit or complete-pr)").action(runGrantCheck);
12696
12800
  var presenceCommand = program.command("presence").description("The local presence channel: which session works on what, right now (ARC-060). Loopback only; nothing runs until `serve`");
12697
12801
  presenceCommand.command("serve").description("Run the presence service in the foreground on 127.0.0.1 until Ctrl+C or `spell presence stop`").option("--port <port>", "Port on 127.0.0.1 (default 27226, or ARCANE_PRESENCE_PORT); 0 picks a free one").option("--rotate", "Replace the token in ~/.arcane/presence/token; every subscriber must re-read it").action(async (opts) => {
12698
12802
  await runPresenceServe(opts);