@rness/cli 0.8.0 → 0.9.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.
@@ -797,7 +797,7 @@ function classifyProbe(url, exitCode, stderr) {
797
797
  var STDERR_FLUSH_MS = 200;
798
798
  async function probeRemote(url, timeoutMs = 6e4, credentials) {
799
799
  positional(url, "repository url");
800
- return new Promise((resolve6) => {
800
+ return new Promise((resolve7) => {
801
801
  const auth = withCredentials(credentials);
802
802
  const child = spawn(
803
803
  "git",
@@ -816,7 +816,7 @@ async function probeRemote(url, timeoutMs = 6e4, credentials) {
816
816
  settled = true;
817
817
  clearTimeout(timer);
818
818
  child.stderr?.destroy();
819
- resolve6(probe);
819
+ resolve7(probe);
820
820
  };
821
821
  const timer = setTimeout(() => {
822
822
  child.kill("SIGKILL");
@@ -1198,8 +1198,8 @@ function warn(text) {
1198
1198
  var WORD = [" \u2588\u2580\u2588 \u2588\u2584 \u2588 \u2588\u2580\u2580 \u2588\u2580\u2580 \u2588\u2580\u2580", " \u2588\u2580\u2584 \u2588 \u2580\u2588 \u2588\u2588\u2584 \u2584\u2584\u2588 \u2584\u2584\u2588"];
1199
1199
  var FROM = [63, 185, 80];
1200
1200
  var TO = [57, 197, 207];
1201
- function gradient(line) {
1202
- const chars = [...line];
1201
+ function gradient(line2) {
1202
+ const chars = [...line2];
1203
1203
  const last = Math.max(1, chars.length - 1);
1204
1204
  const painted = chars.map((ch, i) => {
1205
1205
  if (ch === " ") return ch;
@@ -1216,7 +1216,7 @@ function banner(version, stream = process.stdout) {
1216
1216
  const coloured = paint("done", "x", stream) !== "x";
1217
1217
  const trueColour = coloured && typeof stream.hasColors === "function" && stream.hasColors(2 ** 24);
1218
1218
  const word = WORD.map(
1219
- (line) => trueColour ? gradient(line) : paint("done", line, stream)
1219
+ (line2) => trueColour ? gradient(line2) : paint("done", line2, stream)
1220
1220
  );
1221
1221
  return [
1222
1222
  "",
@@ -1248,7 +1248,7 @@ async function detectGithubSsh(opts) {
1248
1248
  ...opts.interactive ? [] : ["-o", "BatchMode=yes"],
1249
1249
  "git@github.com"
1250
1250
  ];
1251
- return new Promise((resolve6) => {
1251
+ return new Promise((resolve7) => {
1252
1252
  const child = spawn2(opts.command ?? "ssh", args, {
1253
1253
  stdio: [opts.interactive ? "inherit" : "ignore", "ignore", "pipe"]
1254
1254
  });
@@ -1261,7 +1261,7 @@ async function detectGithubSsh(opts) {
1261
1261
  settled = true;
1262
1262
  clearTimeout(timer);
1263
1263
  child.stderr?.destroy();
1264
- resolve6(access2);
1264
+ resolve7(access2);
1265
1265
  };
1266
1266
  const timer = setTimeout(() => {
1267
1267
  child.kill("SIGKILL");
@@ -1390,7 +1390,7 @@ function reportError(e) {
1390
1390
  }
1391
1391
 
1392
1392
  // src/version.ts
1393
- var VERSION = "0.8.0";
1393
+ var VERSION = "0.9.0";
1394
1394
 
1395
1395
  // src/core/block.ts
1396
1396
  import { createHash } from "crypto";
@@ -1427,9 +1427,9 @@ function renderBlock({
1427
1427
  scope,
1428
1428
  org,
1429
1429
  depth,
1430
- context
1430
+ context: context2
1431
1431
  }) {
1432
- const standards = context.collections.find((c) => c.name === "standards")?.files ?? [];
1432
+ const standards = context2.collections.find((c) => c.name === "standards")?.files ?? [];
1433
1433
  const rules = standards.length === 0 ? ["_No standards apply to this scope yet._"] : standards.map(
1434
1434
  (f) => `<!-- rness: standards/${f.rel} -->
1435
1435
  ${toLf(f.content).trim()}`
@@ -1442,8 +1442,8 @@ ${rules.join("\n\n")}`;
1442
1442
  const text = [BEGIN, header(scope, hash), body, END].join("\n");
1443
1443
  return { text, hash, bytes: Buffer.byteLength(text, "utf8") };
1444
1444
  }
1445
- function parseHeader(line) {
1446
- const m = HEADER.exec(line.trimEnd());
1445
+ function parseHeader(line2) {
1446
+ const m = HEADER.exec(line2.trimEnd());
1447
1447
  if (m === null) return null;
1448
1448
  return { version: m[1] ?? null, scope: m[2] ?? "", hash: m[3] ?? "" };
1449
1449
  }
@@ -1614,8 +1614,8 @@ import { join as join8 } from "path";
1614
1614
  function findBlock(lines) {
1615
1615
  const begins = [];
1616
1616
  const ends = [];
1617
- lines.forEach((line, i) => {
1618
- const l = line.trimEnd();
1617
+ lines.forEach((line2, i) => {
1618
+ const l = line2.trimEnd();
1619
1619
  if (l === BEGIN) begins.push(i);
1620
1620
  if (l === END) ends.push(i);
1621
1621
  });
@@ -1695,14 +1695,37 @@ var TARGETS = {
1695
1695
  claude: {
1696
1696
  name: "claude",
1697
1697
  label: "Claude Code",
1698
- file: ".claude/settings.json",
1699
- // Read the context repository from a repository without a prompt. The
1700
- // path is resolved against the project directory, and only once Claude
1701
- // Code has trusted it (verified on 2.1.284, spec 0011 §7).
1702
- guarantees: [
1698
+ files: [
1703
1699
  {
1704
- path: ["permissions", "additionalDirectories"],
1705
- contains: "../../.rness"
1700
+ file: ".claude/settings.json",
1701
+ guarantees: [
1702
+ // Read the context repository from a repository without a prompt.
1703
+ // Resolved against the project directory, and applied once Claude
1704
+ // Code has trusted it (verified on 2.1.284, spec 0011 §7).
1705
+ {
1706
+ path: ["permissions", "additionalDirectories"],
1707
+ contains: "../../.rness"
1708
+ },
1709
+ // Start the rness MCP server of .mcp.json without asking.
1710
+ { path: ["enabledMcpjsonServers"], contains: "rness" }
1711
+ ]
1712
+ },
1713
+ {
1714
+ file: ".mcp.json",
1715
+ guarantees: [
1716
+ // The pinned copy, by a path relative to the repository — the
1717
+ // directory Claude Code starts the server in (spec 0014 §6).
1718
+ {
1719
+ path: ["mcpServers", "rness"],
1720
+ value: {
1721
+ command: "node",
1722
+ args: [
1723
+ "../../.rness/node_modules/@rness/cli/dist/bin/rness.js",
1724
+ "mcp"
1725
+ ]
1726
+ }
1727
+ }
1728
+ ]
1706
1729
  }
1707
1730
  ]
1708
1731
  }
@@ -1728,7 +1751,7 @@ function parse3(text) {
1728
1751
  }
1729
1752
  return isObject(data) ? { ok: true, data } : { ok: false, reason: "not a JSON object" };
1730
1753
  }
1731
- function listAt(root, g, create) {
1754
+ function parentOf(root, g, create) {
1732
1755
  let node = root;
1733
1756
  for (const [i, key] of g.path.slice(0, -1).entries()) {
1734
1757
  const next = node[key];
@@ -1742,17 +1765,32 @@ function listAt(root, g, create) {
1742
1765
  };
1743
1766
  node = node[key];
1744
1767
  }
1768
+ return { kind: "node", node };
1769
+ }
1770
+ function apply(root, g, create) {
1771
+ const found = parentOf(root, g, create);
1772
+ if (found.kind === "absent") return { holds: false, changed: false };
1773
+ if (found.kind === "invalid") return { invalid: found.reason };
1774
+ const parent = found.node;
1745
1775
  const last = g.path[g.path.length - 1] ?? "";
1746
- const value = node[last];
1776
+ const value = parent[last];
1777
+ if ("value" in g) {
1778
+ if (value !== void 0) return { holds: true, changed: false };
1779
+ if (!create) return { holds: false, changed: false };
1780
+ parent[last] = structuredClone(g.value);
1781
+ return { holds: true, changed: true };
1782
+ }
1747
1783
  if (value === void 0) {
1748
- if (!create) return { kind: "absent" };
1749
- const list = [];
1750
- node[last] = list;
1751
- return { kind: "list", list };
1784
+ if (!create) return { holds: false, changed: false };
1785
+ parent[last] = [g.contains];
1786
+ return { holds: true, changed: true };
1752
1787
  }
1753
1788
  if (!Array.isArray(value))
1754
- return { kind: "invalid", reason: `${g.path.join(".")} is not a list` };
1755
- return { kind: "list", list: value };
1789
+ return { invalid: `${g.path.join(".")} is not a list` };
1790
+ if (value.includes(g.contains)) return { holds: true, changed: false };
1791
+ if (!create) return { holds: false, changed: false };
1792
+ value.push(g.contains);
1793
+ return { holds: true, changed: true };
1756
1794
  }
1757
1795
  function styleOf(text) {
1758
1796
  const indent = text.split(/\r?\n/).slice(1).map((l) => /^[ \t]+/.exec(l)?.[0]).find((w) => w !== void 0);
@@ -1767,13 +1805,9 @@ function ensureGuarantees(existing, guarantees) {
1767
1805
  if (!parsed.ok) return { kind: "invalid", reason: parsed.reason };
1768
1806
  let changed = false;
1769
1807
  for (const g of guarantees) {
1770
- const found = listAt(parsed.data, g, true);
1771
- if (found.kind === "invalid")
1772
- return { kind: "invalid", reason: found.reason };
1773
- if (found.kind === "list" && !found.list.includes(g.contains)) {
1774
- found.list.push(g.contains);
1775
- changed = true;
1776
- }
1808
+ const r = apply(parsed.data, g, true);
1809
+ if ("invalid" in r) return { kind: "invalid", reason: r.invalid };
1810
+ changed ||= r.changed;
1777
1811
  }
1778
1812
  if (existing === null)
1779
1813
  return {
@@ -1792,15 +1826,14 @@ function missingGuarantees(existing, guarantees) {
1792
1826
  if (!parsed.ok) return { invalid: parsed.reason };
1793
1827
  const missing = [];
1794
1828
  for (const g of guarantees) {
1795
- const found = listAt(parsed.data, g, false);
1796
- if (found.kind === "invalid") return { invalid: found.reason };
1797
- if (found.kind === "absent" || !found.list.includes(g.contains))
1798
- missing.push(g);
1829
+ const r = apply(parsed.data, g, false);
1830
+ if ("invalid" in r) return { invalid: r.invalid };
1831
+ if (!r.holds) missing.push(g);
1799
1832
  }
1800
1833
  return missing;
1801
1834
  }
1802
1835
  function describeGuarantee(g) {
1803
- return `${g.path.join(".")} lacks ${g.contains}`;
1836
+ return "value" in g ? `${g.path.join(".")} is missing` : `${g.path.join(".")} lacks ${g.contains}`;
1804
1837
  }
1805
1838
 
1806
1839
  // src/core/agent-targets.ts
@@ -1809,11 +1842,12 @@ async function filesOf(root, manifest, targets) {
1809
1842
  for (const repo of Object.keys(manifest.repos)) {
1810
1843
  if (!await exists(join9(root, "org", repo))) continue;
1811
1844
  for (const target of targets)
1812
- files.push({
1813
- target,
1814
- file: join9(root, "org", repo, ...target.file.split("/")),
1815
- label: `org/${repo}/${target.file}`
1816
- });
1845
+ for (const owned of target.files)
1846
+ files.push({
1847
+ guarantees: owned.guarantees,
1848
+ file: join9(root, "org", repo, ...owned.file.split("/")),
1849
+ label: `org/${repo}/${owned.file}`
1850
+ });
1817
1851
  }
1818
1852
  return files;
1819
1853
  }
@@ -1824,14 +1858,14 @@ function declared(manifest) {
1824
1858
  }
1825
1859
  async function agentTargets(root, manifest, opts) {
1826
1860
  const outcomes = [];
1827
- for (const { target, file, label } of await filesOf(
1861
+ for (const { guarantees, file, label } of await filesOf(
1828
1862
  root,
1829
1863
  manifest,
1830
1864
  declared(manifest)
1831
1865
  )) {
1832
1866
  const existing = await readOrNull(file);
1833
1867
  if (opts.check) {
1834
- const missing = missingGuarantees(existing, target.guarantees);
1868
+ const missing = missingGuarantees(existing, guarantees);
1835
1869
  if ("invalid" in missing)
1836
1870
  outcomes.push({ label, status: "invalid", detail: missing.invalid });
1837
1871
  else if (missing.length > 0)
@@ -1843,7 +1877,7 @@ async function agentTargets(root, manifest, opts) {
1843
1877
  else outcomes.push({ label, status: "unchanged", detail: null });
1844
1878
  continue;
1845
1879
  }
1846
- const ensured = ensureGuarantees(existing, target.guarantees);
1880
+ const ensured = ensureGuarantees(existing, guarantees);
1847
1881
  if (ensured.kind === "invalid") {
1848
1882
  outcomes.push({ label, status: "invalid", detail: ensured.reason });
1849
1883
  continue;
@@ -1865,14 +1899,14 @@ async function leftoverTargets(root, manifest) {
1865
1899
  (t) => !agents.includes(t.name)
1866
1900
  );
1867
1901
  const leftovers = [];
1868
- for (const { target, file, label } of await filesOf(
1902
+ for (const { guarantees, file, label } of await filesOf(
1869
1903
  root,
1870
1904
  manifest,
1871
1905
  undeclared
1872
1906
  )) {
1873
1907
  const existing = await readOrNull(file);
1874
1908
  if (existing === null) continue;
1875
- const missing = missingGuarantees(existing, target.guarantees);
1909
+ const missing = missingGuarantees(existing, guarantees);
1876
1910
  if (!("invalid" in missing) && missing.length === 0) leftovers.push(label);
1877
1911
  }
1878
1912
  return leftovers;
@@ -1920,7 +1954,7 @@ async function step(options, work) {
1920
1954
  const rawErr = process.stderr.write.bind(
1921
1955
  process.stderr
1922
1956
  );
1923
- const guard = (raw) => ((...args) => {
1957
+ const guard2 = (raw) => ((...args) => {
1924
1958
  erase();
1925
1959
  return raw(...args);
1926
1960
  });
@@ -1935,8 +1969,8 @@ async function step(options, work) {
1935
1969
  } else {
1936
1970
  draw("\u2026");
1937
1971
  }
1938
- stream.write = guard(rawWrite);
1939
- if (process.stderr !== stream) process.stderr.write = guard(rawErr);
1972
+ stream.write = guard2(rawWrite);
1973
+ if (process.stderr !== stream) process.stderr.write = guard2(rawErr);
1940
1974
  try {
1941
1975
  return await work();
1942
1976
  } finally {
@@ -1983,8 +2017,8 @@ var plainUi = {
1983
2017
  { label: options.doing, animate: options.git !== true },
1984
2018
  work
1985
2019
  );
1986
- const line = options.quiet === true ? null : done?.(result) ?? null;
1987
- if (line !== null) this.line(line[0], line[1]);
2020
+ const line2 = options.quiet === true ? null : done?.(result) ?? null;
2021
+ if (line2 !== null) this.line(line2[0], line2[1]);
1988
2022
  return result;
1989
2023
  }
1990
2024
  };
@@ -2017,7 +2051,7 @@ function sessionUi(p) {
2017
2051
  p.log.warn(text);
2018
2052
  },
2019
2053
  note: (title, lines) => {
2020
- p.note(lines.join("\n"), title, { format: (line) => grey(line) });
2054
+ p.note(lines.join("\n"), title, { format: (line2) => grey(line2) });
2021
2055
  },
2022
2056
  hint: (text) => {
2023
2057
  p.log.message(paint("idle", text));
@@ -2031,20 +2065,20 @@ function sessionUi(p) {
2031
2065
  async step(options, work, done) {
2032
2066
  const label = options.sentence ?? tidy(options.doing);
2033
2067
  const finish2 = (result) => done?.(result) ?? null;
2034
- const label_ = (line) => `${line[1]} ${line[0]}`;
2068
+ const label_ = (line2) => `${line2[1]} ${line2[0]}`;
2035
2069
  if (options.git === true && !this.gitIsSilent) {
2036
2070
  p.log.message(paint("idle", `${label}\u2026`));
2037
2071
  const result = await work();
2038
- const line = finish2(result);
2039
- if (line !== null) say(line[0], label_(line));
2072
+ const line2 = finish2(result);
2073
+ if (line2 !== null) say(line2[0], label_(line2));
2040
2074
  return result;
2041
2075
  }
2042
2076
  const spin = p.spinner();
2043
2077
  spin.start(label);
2044
2078
  try {
2045
2079
  const result = await work();
2046
- const line = finish2(result);
2047
- spin.stop(line === null ? label : label_(line));
2080
+ const line2 = finish2(result);
2081
+ spin.stop(line2 === null ? label : label_(line2));
2048
2082
  return result;
2049
2083
  } catch (e) {
2050
2084
  spin.error(`${label} \u2014 failed`);
@@ -2263,7 +2297,7 @@ async function syncCommand(opts, deps = {}) {
2263
2297
  const written = [
2264
2298
  ...MANAGED_FILES,
2265
2299
  ...(manifest.agents ?? []).flatMap(
2266
- (a) => Object.hasOwn(TARGETS, a) ? [TARGETS[a].file] : []
2300
+ (a) => Object.hasOwn(TARGETS, a) ? TARGETS[a].files.map((f) => f.file) : []
2267
2301
  )
2268
2302
  ];
2269
2303
  if (!await isClean(dir, written)) {
@@ -2316,7 +2350,7 @@ async function syncCommand(opts, deps = {}) {
2316
2350
  lines.push(["skipped", `${t.label} (symlink)`]);
2317
2351
  continue;
2318
2352
  }
2319
- const context = await assembleContext({
2353
+ const context2 = await assembleContext({
2320
2354
  rnessDir: ws.rnessDir,
2321
2355
  manifest,
2322
2356
  scope: t.scope
@@ -2325,7 +2359,7 @@ async function syncCommand(opts, deps = {}) {
2325
2359
  scope: t.scope,
2326
2360
  org,
2327
2361
  depth: t.depth,
2328
- context
2362
+ context: context2
2329
2363
  });
2330
2364
  if (block.bytes > BLOCK_SIZE_WARNING) {
2331
2365
  ui.warn(
@@ -3062,10 +3096,10 @@ async function runPinnedSync(root) {
3062
3096
  function summariseSync(stdout) {
3063
3097
  const counts = /* @__PURE__ */ new Map();
3064
3098
  const others = [];
3065
- for (const line of stdout.split("\n")) {
3066
- if (line.trim() === "") continue;
3067
- const verb = /^(updated|unchanged|stale)\s/.exec(line)?.[1];
3068
- if (verb === void 0) others.push(line);
3099
+ for (const line2 of stdout.split("\n")) {
3100
+ if (line2.trim() === "") continue;
3101
+ const verb = /^(updated|unchanged|stale)\s/.exec(line2)?.[1];
3102
+ if (verb === void 0) others.push(line2);
3069
3103
  else counts.set(verb, (counts.get(verb) ?? 0) + 1);
3070
3104
  }
3071
3105
  const total = [...counts.values()].reduce((a, b) => a + b, 0);
@@ -3269,8 +3303,8 @@ async function mergeScaffold(rnessDir, commit, base) {
3269
3303
  "--name-status",
3270
3304
  "HEAD"
3271
3305
  ]);
3272
- for (const line of status2.split("\n")) {
3273
- const [letter = "", path = ""] = line.split(" ");
3306
+ for (const line2 of status2.split("\n")) {
3307
+ const [letter = "", path = ""] = line2.split(" ");
3274
3308
  if (path === "" || conflicts.includes(path)) continue;
3275
3309
  if (letter === "A") changes.push(["added", path]);
3276
3310
  else if (letter === "D") changes.push(["removed", path]);
@@ -3291,6 +3325,24 @@ async function isRepository2(rnessDir) {
3291
3325
  const real = (p) => realpath2(p).catch(() => resolve3(p));
3292
3326
  return await real(r.stdout.trim()) === await real(rnessDir);
3293
3327
  }
3328
+ async function withoutRedundantKeeps(rnessDir, files) {
3329
+ const kept = [];
3330
+ for (const file of files) {
3331
+ if (file.path.endsWith("/.gitkeep")) {
3332
+ const dir = file.path.slice(0, -"/.gitkeep".length);
3333
+ const listed = await run(rnessDir, [
3334
+ "ls-tree",
3335
+ "--name-only",
3336
+ "HEAD",
3337
+ "--",
3338
+ `${dir}/`
3339
+ ]);
3340
+ if (listed.code === 0 && listed.stdout.trim() !== "") continue;
3341
+ }
3342
+ kept.push(file);
3343
+ }
3344
+ return kept;
3345
+ }
3294
3346
 
3295
3347
  // src/core/new-workspace.ts
3296
3348
  async function isEmptyDir(dir) {
@@ -3395,8 +3447,8 @@ async function syncBlocks(ui, root, shown, inPlace, sshWorks) {
3395
3447
  return 1;
3396
3448
  }
3397
3449
  const outcome = syncOutcome(result, { sshWorks: sshWorks === true });
3398
- for (const line of outcome.hints) ui.hint(line);
3399
- for (const line of outcome.errors) ui.error(line);
3450
+ for (const line2 of outcome.hints) ui.hint(line2);
3451
+ for (const line2 of outcome.errors) ui.error(line2);
3400
3452
  return outcome.code;
3401
3453
  }
3402
3454
 
@@ -3501,7 +3553,7 @@ async function createBlank(input) {
3501
3553
  `Workspace \`${name}\` is ready in ${shown}/.`,
3502
3554
  "",
3503
3555
  "Next:",
3504
- ...next.map((line) => ` ${line}`),
3556
+ ...next.map((line2) => ` ${line2}`),
3505
3557
  ""
3506
3558
  ].join("\n")
3507
3559
  );
@@ -3512,8 +3564,8 @@ async function createBlank(input) {
3512
3564
  function firstLine4(text) {
3513
3565
  return text.split("\n").find((l) => l.trim() !== "")?.trim() ?? "unknown error";
3514
3566
  }
3515
- function splitSpecs(list) {
3516
- return list.split(",").map((s) => s.trim()).filter((s) => s !== "");
3567
+ function splitSpecs(list2) {
3568
+ return list2.split(",").map((s) => s.trim()).filter((s) => s !== "");
3517
3569
  }
3518
3570
  function cancelled(ui) {
3519
3571
  ui.cancelled();
@@ -4144,7 +4196,7 @@ ${next2.map((l) => ` ${l}`).join("\n")}
4144
4196
  `Workspace for \`${org}\` is ready in ${shown}/.`,
4145
4197
  "",
4146
4198
  "Next:",
4147
- ...next.map((line) => ` ${line}`),
4199
+ ...next.map((line2) => ` ${line2}`),
4148
4200
  ""
4149
4201
  ].join("\n")
4150
4202
  );
@@ -4185,11 +4237,67 @@ async function logoutCommand() {
4185
4237
  }
4186
4238
  }
4187
4239
 
4240
+ // src/core/contract.ts
4241
+ import { join as join20 } from "path";
4242
+ var STATUSES = {
4243
+ adr: ["Proposed", "Accepted", "Rejected", "Superseded"],
4244
+ specs: [
4245
+ "Draft",
4246
+ "Proposed",
4247
+ "Approved",
4248
+ "Implemented",
4249
+ "Superseded",
4250
+ "Rejected"
4251
+ ],
4252
+ plans: ["Draft", "Ready", "In progress", "Blocked", "Completed", "Abandoned"]
4253
+ };
4254
+ var SKIP = /* @__PURE__ */ new Set(["adr/0000-template.md"]);
4255
+ var PLACEMENT_KEYS = ["scopes", "scope"];
4256
+ async function checkContract(rnessDir) {
4257
+ const problems = [];
4258
+ for (const name of COLLECTIONS) {
4259
+ const allowed = STATUSES[name];
4260
+ const items = await collectMarkdown(join20(rnessDir, name));
4261
+ for (const item of items) {
4262
+ const where = `${name}/${item.rel}`;
4263
+ if (SKIP.has(where)) continue;
4264
+ if (item.fieldsError !== null) {
4265
+ problems.push(`${where}: invalid front matter (${item.fieldsError})`);
4266
+ continue;
4267
+ }
4268
+ if (item.fields !== null) {
4269
+ for (const key of PLACEMENT_KEYS) {
4270
+ if (Object.hasOwn(item.fields, key)) {
4271
+ problems.push(
4272
+ `${where}: front matter "${key}" is not supported \u2014 the directory decides the scope; move the file`
4273
+ );
4274
+ }
4275
+ }
4276
+ }
4277
+ if (allowed === void 0) continue;
4278
+ if (item.fields === null) {
4279
+ problems.push(`${where}: missing a front-matter block`);
4280
+ continue;
4281
+ }
4282
+ if (item.title === null) problems.push(`${where}: missing a "# " title`);
4283
+ const status2 = item.fields["status"];
4284
+ if (status2 === void 0 || status2 === null || status2 === "") {
4285
+ problems.push(`${where}: missing "status"`);
4286
+ } else if (typeof status2 !== "string" || !allowed.includes(status2)) {
4287
+ problems.push(
4288
+ `${where}: unknown status ${JSON.stringify(status2)} (expected ${allowed.join(", ")})`
4289
+ );
4290
+ }
4291
+ }
4292
+ }
4293
+ return problems;
4294
+ }
4295
+
4188
4296
  // src/commands/upgrade.ts
4189
4297
  import { execFile as execFile6 } from "child_process";
4190
4298
  import { mkdtemp as mkdtemp3, readFile as readFile9, rm as rm7 } from "fs/promises";
4191
4299
  import { tmpdir as tmpdir3 } from "os";
4192
- import { basename as basename5, join as join20, relative as relative5 } from "path";
4300
+ import { basename as basename5, join as join21, relative as relative5 } from "path";
4193
4301
  import { promisify as promisify6 } from "util";
4194
4302
  var execFileP6 = promisify6(execFile6);
4195
4303
  var NOTES = "https://github.com/rness-dev/rness/tree/main/packages/cli#readme";
@@ -4209,10 +4317,10 @@ async function latestVersion() {
4209
4317
  async function scaffoldFor(version, rnessDir) {
4210
4318
  if (version === VERSION) return { dir: scaffoldDir() };
4211
4319
  const installed = await readPinnedCli(rnessDir);
4212
- const inside = installed === null ? null : join20(installed.dir, "scaffold");
4213
- if (installed?.version === version && inside !== null && await exists(join20(inside, "package.json")))
4320
+ const inside = installed === null ? null : join21(installed.dir, "scaffold");
4321
+ if (installed?.version === version && inside !== null && await exists(join21(inside, "package.json")))
4214
4322
  return { dir: inside };
4215
- const scratch = await mkdtemp3(join20(tmpdir3(), "rness-pack-"));
4323
+ const scratch = await mkdtemp3(join21(tmpdir3(), "rness-pack-"));
4216
4324
  const cleanup = () => rm7(scratch, { recursive: true, force: true });
4217
4325
  try {
4218
4326
  const { stdout } = await execFileP6(
@@ -4227,8 +4335,8 @@ async function scaffoldFor(version, rnessDir) {
4227
4335
  { timeout: 6e4 }
4228
4336
  );
4229
4337
  const tarball = stdout.trim().split("\n").at(-1) ?? "";
4230
- await execFileP6("tar", ["-xzf", join20(scratch, tarball), "-C", scratch]);
4231
- return { dir: join20(scratch, "package", "scaffold"), cleanup };
4338
+ await execFileP6("tar", ["-xzf", join21(scratch, tarball), "-C", scratch]);
4339
+ return { dir: join21(scratch, "package", "scaffold"), cleanup };
4232
4340
  } catch (e) {
4233
4341
  await cleanup();
4234
4342
  throw new Error(
@@ -4240,7 +4348,7 @@ async function scaffoldFor(version, rnessDir) {
4240
4348
  async function declaredManager(rnessDir, pm) {
4241
4349
  try {
4242
4350
  const pkg = JSON.parse(
4243
- await readFile9(join20(rnessDir, "package.json"), "utf8")
4351
+ await readFile9(join21(rnessDir, "package.json"), "utf8")
4244
4352
  );
4245
4353
  if (typeof pkg.packageManager === "string") return pkg.packageManager;
4246
4354
  } catch {
@@ -4263,7 +4371,7 @@ function finish(ui, cwd, rnessDir, label, target) {
4263
4371
  status("upgraded", `${label} to @rness/cli ${target}`),
4264
4372
  "",
4265
4373
  "Next:",
4266
- ...next.map((line) => ` ${line}`),
4374
+ ...next.map((line2) => ` ${line2}`),
4267
4375
  ""
4268
4376
  ].join("\n")
4269
4377
  );
@@ -4341,7 +4449,11 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
4341
4449
  }
4342
4450
  const pm = await workspacePackageManager(ws.rnessDir);
4343
4451
  if (pin.spec === target && installed === target)
4344
- ui.line("scaffold", `@rness/cli ${target} \u2014 merging it into ${label}`);
4452
+ ui.line(
4453
+ "scaffold",
4454
+ `@rness/cli ${target} \u2014 merging it into ${label}`,
4455
+ `Merging the @rness/cli ${target} scaffold into ${label}`
4456
+ );
4345
4457
  else if (pin.spec === target)
4346
4458
  ui.line(
4347
4459
  "install",
@@ -4378,13 +4490,19 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
4378
4490
  if (base === null)
4379
4491
  ui.line(
4380
4492
  "adopting",
4381
- `the @rness/cli ${target} scaffold (no scaffold commit in ${label} yet)`
4493
+ `the @rness/cli ${target} scaffold (no scaffold commit in ${label} yet)`,
4494
+ `Adopting the @rness/cli ${target} scaffold \u2014 ${label} has no scaffold commit yet`
4495
+ );
4496
+ else
4497
+ ui.line(
4498
+ "merging",
4499
+ `the @rness/cli ${target} scaffold`,
4500
+ `Merging the @rness/cli ${target} scaffold`
4382
4501
  );
4383
- else ui.line("merging", `the @rness/cli ${target} scaffold`);
4384
4502
  const commit = await buildScaffoldCommit(
4385
4503
  ws.rnessDir,
4386
4504
  base?.commit ?? null,
4387
- files,
4505
+ base === null ? await withoutRedundantKeeps(ws.rnessDir, files) : files,
4388
4506
  target
4389
4507
  );
4390
4508
  const result = await mergeScaffold(
@@ -4407,7 +4525,7 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
4407
4525
  await source.cleanup?.();
4408
4526
  }
4409
4527
  }
4410
- const packageFile = join20(ws.rnessDir, "package.json");
4528
+ const packageFile = join21(ws.rnessDir, "package.json");
4411
4529
  const restore = /* @__PURE__ */ new Map([
4412
4530
  [packageFile, await readFile9(packageFile, "utf8")]
4413
4531
  ]);
@@ -4462,7 +4580,7 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
4462
4580
  }
4463
4581
 
4464
4582
  // src/core/blocks.ts
4465
- import { join as join21 } from "path";
4583
+ import { join as join22 } from "path";
4466
4584
  var PROBLEM = /* @__PURE__ */ new Set(["stale", "malformed"]);
4467
4585
  async function checkBlocks({
4468
4586
  root,
@@ -4479,7 +4597,7 @@ async function checkBlocks({
4479
4597
  warnings: checks.filter((c) => !PROBLEM.has(c.status) && c.message !== null).map((c) => c.message),
4480
4598
  checks
4481
4599
  });
4482
- if (!await exists(join21(root, "org"))) return derive();
4600
+ if (!await exists(join22(root, "org"))) return derive();
4483
4601
  const targets = [
4484
4602
  { scope: null, dir: root, depth: 0, file: "AGENTS.md", label: "AGENTS.md" },
4485
4603
  { scope: null, dir: root, depth: 0, file: "CLAUDE.md", label: "CLAUDE.md" }
@@ -4488,7 +4606,7 @@ async function checkBlocks({
4488
4606
  const segments = entry.path.split("/");
4489
4607
  targets.push({
4490
4608
  scope: name,
4491
- dir: join21(root, ...segments),
4609
+ dir: join22(root, ...segments),
4492
4610
  depth: segments.length,
4493
4611
  file: "AGENTS.md",
4494
4612
  label: `${entry.path}/AGENTS.md`
@@ -4503,7 +4621,7 @@ async function checkBlocks({
4503
4621
  );
4504
4622
  continue;
4505
4623
  }
4506
- const text = await readOrNull(join21(t.dir, t.file));
4624
+ const text = await readOrNull(join22(t.dir, t.file));
4507
4625
  const lines = text === null ? [] : text.split(/\r?\n/);
4508
4626
  const found = findBlock(lines);
4509
4627
  if (found.kind === "none") {
@@ -4518,7 +4636,7 @@ async function checkBlocks({
4518
4636
  note(t.label, "malformed", `${t.label}: ${found.message}`);
4519
4637
  continue;
4520
4638
  }
4521
- const context = await assembleContext({
4639
+ const context2 = await assembleContext({
4522
4640
  rnessDir,
4523
4641
  manifest,
4524
4642
  scope: t.scope
@@ -4527,7 +4645,7 @@ async function checkBlocks({
4527
4645
  scope: t.scope,
4528
4646
  org,
4529
4647
  depth: t.depth,
4530
- context
4648
+ context: context2
4531
4649
  });
4532
4650
  const pointerLeft = t.file === "CLAUDE.md" && lines.some((l) => l.trim() === "@AGENTS.md");
4533
4651
  if (isCurrentBlock(lines.slice(found.begin, found.end + 1), fresh2.hash) && !pointerLeft)
@@ -4542,62 +4660,6 @@ async function checkBlocks({
4542
4660
  return derive();
4543
4661
  }
4544
4662
 
4545
- // src/core/contract.ts
4546
- import { join as join22 } from "path";
4547
- var STATUSES = {
4548
- adr: ["Proposed", "Accepted", "Rejected", "Superseded"],
4549
- specs: [
4550
- "Draft",
4551
- "Proposed",
4552
- "Approved",
4553
- "Implemented",
4554
- "Superseded",
4555
- "Rejected"
4556
- ],
4557
- plans: ["Draft", "Ready", "In progress", "Blocked", "Completed", "Abandoned"]
4558
- };
4559
- var SKIP = /* @__PURE__ */ new Set(["adr/0000-template.md"]);
4560
- var PLACEMENT_KEYS = ["scopes", "scope"];
4561
- async function checkContract(rnessDir) {
4562
- const problems = [];
4563
- for (const name of COLLECTIONS) {
4564
- const allowed = STATUSES[name];
4565
- const items = await collectMarkdown(join22(rnessDir, name));
4566
- for (const item of items) {
4567
- const where = `${name}/${item.rel}`;
4568
- if (SKIP.has(where)) continue;
4569
- if (item.fieldsError !== null) {
4570
- problems.push(`${where}: invalid front matter (${item.fieldsError})`);
4571
- continue;
4572
- }
4573
- if (item.fields !== null) {
4574
- for (const key of PLACEMENT_KEYS) {
4575
- if (Object.hasOwn(item.fields, key)) {
4576
- problems.push(
4577
- `${where}: front matter "${key}" is not supported \u2014 the directory decides the scope; move the file`
4578
- );
4579
- }
4580
- }
4581
- }
4582
- if (allowed === void 0) continue;
4583
- if (item.fields === null) {
4584
- problems.push(`${where}: missing a front-matter block`);
4585
- continue;
4586
- }
4587
- if (item.title === null) problems.push(`${where}: missing a "# " title`);
4588
- const status2 = item.fields["status"];
4589
- if (status2 === void 0 || status2 === null || status2 === "") {
4590
- problems.push(`${where}: missing "status"`);
4591
- } else if (typeof status2 !== "string" || !allowed.includes(status2)) {
4592
- problems.push(
4593
- `${where}: unknown status ${JSON.stringify(status2)} (expected ${allowed.join(", ")})`
4594
- );
4595
- }
4596
- }
4597
- }
4598
- return problems;
4599
- }
4600
-
4601
4663
  // src/commands/validate.ts
4602
4664
  async function validateCommand(opts, deps = {}) {
4603
4665
  const cwd = opts.cwd ?? process.cwd();
@@ -4686,9 +4748,9 @@ async function readStdin() {
4686
4748
  async function gitCredentialCommand(operation, input) {
4687
4749
  if (operation !== "get") return 0;
4688
4750
  const fields = /* @__PURE__ */ new Map();
4689
- for (const line of (input ?? await readStdin()).split("\n")) {
4690
- const eq = line.indexOf("=");
4691
- if (eq > 0) fields.set(line.slice(0, eq), line.slice(eq + 1).trimEnd());
4751
+ for (const line2 of (input ?? await readStdin()).split("\n")) {
4752
+ const eq = line2.indexOf("=");
4753
+ if (eq > 0) fields.set(line2.slice(0, eq), line2.slice(eq + 1).trimEnd());
4692
4754
  }
4693
4755
  if (fields.get("protocol") !== "https" || fields.get("host") !== "github.com")
4694
4756
  return 0;
@@ -4700,6 +4762,409 @@ password=${resolved.token}
4700
4762
  return 0;
4701
4763
  }
4702
4764
 
4765
+ // src/commands/mcp.ts
4766
+ import { createInterface } from "readline";
4767
+
4768
+ // src/mcp/protocol.ts
4769
+ var MODERN_VERSIONS = ["2026-07-28"];
4770
+ var LEGACY_VERSIONS = [
4771
+ "2025-11-25",
4772
+ "2025-06-18",
4773
+ "2025-03-26",
4774
+ "2024-11-05"
4775
+ ];
4776
+ var SUPPORTED = [...MODERN_VERSIONS, ...LEGACY_VERSIONS];
4777
+ var VERSION_KEY = "io.modelcontextprotocol/protocolVersion";
4778
+ var CAPABILITIES_KEY = "io.modelcontextprotocol/clientCapabilities";
4779
+ var SERVER_INFO_KEY = "io.modelcontextprotocol/serverInfo";
4780
+ function isObject2(value) {
4781
+ return value !== null && typeof value === "object" && !Array.isArray(value);
4782
+ }
4783
+ function errorLine(id, code, message, data) {
4784
+ return JSON.stringify({
4785
+ jsonrpc: "2.0",
4786
+ id,
4787
+ error: data === void 0 ? { code, message } : { code, message, data }
4788
+ });
4789
+ }
4790
+ function createProtocol(opts) {
4791
+ const serverInfo = { name: opts.name, version: opts.version };
4792
+ return {
4793
+ async handle(line2) {
4794
+ let message;
4795
+ try {
4796
+ message = JSON.parse(line2);
4797
+ } catch {
4798
+ return errorLine(null, -32700, "Parse error");
4799
+ }
4800
+ const rawId = isObject2(message) ? message["id"] : void 0;
4801
+ const id = typeof rawId === "string" || typeof rawId === "number" ? rawId : null;
4802
+ if (!isObject2(message) || message["jsonrpc"] !== "2.0" || typeof message["method"] !== "string")
4803
+ return errorLine(id, -32600, "Invalid Request");
4804
+ if (!("id" in message)) return null;
4805
+ if (id === null) return errorLine(null, -32600, "Invalid Request");
4806
+ const method = message["method"];
4807
+ const params = isObject2(message["params"]) ? message["params"] : {};
4808
+ const meta = isObject2(params["_meta"]) ? params["_meta"] : null;
4809
+ const modern = meta !== null && typeof meta[VERSION_KEY] === "string";
4810
+ if (modern) {
4811
+ const requested = meta[VERSION_KEY];
4812
+ if (!MODERN_VERSIONS.includes(requested))
4813
+ return errorLine(id, -32022, "Unsupported protocol version", {
4814
+ supported: SUPPORTED,
4815
+ requested
4816
+ });
4817
+ if (!isObject2(meta[CAPABILITIES_KEY]))
4818
+ return errorLine(
4819
+ id,
4820
+ -32602,
4821
+ `Invalid params: ${CAPABILITIES_KEY} is required`
4822
+ );
4823
+ }
4824
+ const answer = (result) => JSON.stringify({
4825
+ jsonrpc: "2.0",
4826
+ id,
4827
+ result: modern ? {
4828
+ resultType: "complete",
4829
+ ...result,
4830
+ _meta: { [SERVER_INFO_KEY]: serverInfo }
4831
+ } : result
4832
+ });
4833
+ switch (method) {
4834
+ case "server/discover":
4835
+ return answer({
4836
+ supportedVersions: SUPPORTED,
4837
+ capabilities: { tools: {} }
4838
+ });
4839
+ case "initialize": {
4840
+ const asked = params["protocolVersion"];
4841
+ const protocolVersion = typeof asked === "string" && LEGACY_VERSIONS.includes(asked) ? asked : LEGACY_VERSIONS[0];
4842
+ return JSON.stringify({
4843
+ jsonrpc: "2.0",
4844
+ id,
4845
+ result: {
4846
+ protocolVersion,
4847
+ capabilities: { tools: {} },
4848
+ serverInfo
4849
+ }
4850
+ });
4851
+ }
4852
+ case "ping":
4853
+ return answer({});
4854
+ case "tools/list":
4855
+ return answer({
4856
+ tools: opts.tools.map((t) => ({
4857
+ name: t.name,
4858
+ title: t.title,
4859
+ description: t.description,
4860
+ inputSchema: t.inputSchema
4861
+ }))
4862
+ });
4863
+ case "tools/call": {
4864
+ const name = params["name"];
4865
+ const tool = opts.tools.find((t) => t.name === name);
4866
+ if (tool === void 0)
4867
+ return errorLine(id, -32602, `Unknown tool: ${String(name)}`);
4868
+ const args = isObject2(params["arguments"]) ? params["arguments"] : {};
4869
+ try {
4870
+ const r = await tool.call(args);
4871
+ return answer({
4872
+ content: [{ type: "text", text: r.text }],
4873
+ isError: r.isError === true
4874
+ });
4875
+ } catch (e) {
4876
+ return answer({
4877
+ content: [
4878
+ {
4879
+ type: "text",
4880
+ text: `${tool.name} failed: ${e instanceof Error ? e.message : String(e)}`
4881
+ }
4882
+ ],
4883
+ isError: true
4884
+ });
4885
+ }
4886
+ }
4887
+ default:
4888
+ return errorLine(id, -32601, "Method not found");
4889
+ }
4890
+ }
4891
+ };
4892
+ }
4893
+
4894
+ // src/mcp/tools.ts
4895
+ import { readFile as readFile10, realpath as realpath3, stat as stat4 } from "fs/promises";
4896
+ import { isAbsolute, join as join23, posix as posix4, relative as relative6, resolve as resolve6, sep as sep4 } from "path";
4897
+ var LABELS = {
4898
+ standards: "Standards",
4899
+ adr: "Decisions",
4900
+ specs: "Specifications",
4901
+ plans: "Plans",
4902
+ skills: "Skills"
4903
+ };
4904
+ var TEMPLATE = "adr/0000-template.md";
4905
+ var READ_LIMIT = 256 * 1024;
4906
+ var SEARCH_LIMIT = 20;
4907
+ var LINES_PER_HIT = 3;
4908
+ var LINE_WIDTH = 200;
4909
+ var refuse = (text) => ({ text, isError: true });
4910
+ async function guard(run3) {
4911
+ try {
4912
+ return await run3();
4913
+ } catch (e) {
4914
+ return refuse(e instanceof Error ? e.message : String(e));
4915
+ }
4916
+ }
4917
+ async function workspaceAt(cwd) {
4918
+ const ws = await findWorkspace(cwd);
4919
+ return { ws, manifest: await loadManifest(ws.rnessDir) };
4920
+ }
4921
+ function titleOf(heading, id) {
4922
+ if (id === null) return heading;
4923
+ const rest = new RegExp(`^${id}\\s*[\u2014\u2013:-]\\s*(.+)$`).exec(heading)?.[1];
4924
+ return rest ?? heading;
4925
+ }
4926
+ async function documents(rnessDir, only) {
4927
+ const docs = [];
4928
+ for (const collection of COLLECTIONS) {
4929
+ if (only !== void 0 && collection !== only) continue;
4930
+ for (const item of await collectMarkdown(join23(rnessDir, collection))) {
4931
+ const path = `${collection}/${item.rel}`;
4932
+ if (path === TEMPLATE) continue;
4933
+ const status2 = item.fields?.["status"];
4934
+ const id = /^(\d{4})-/.exec(posix4.basename(item.rel))?.[1] ?? null;
4935
+ docs.push({
4936
+ collection,
4937
+ path,
4938
+ scope: ownerScope(item.rel),
4939
+ id,
4940
+ status: typeof status2 === "string" ? status2 : null,
4941
+ title: titleOf(item.title ?? posix4.basename(item.rel, ".md"), id),
4942
+ text: item.body
4943
+ });
4944
+ }
4945
+ }
4946
+ return docs;
4947
+ }
4948
+ function line(d, withScope = false) {
4949
+ const head = [d.id, d.status].filter((x) => x !== null).join(" ");
4950
+ return `- ${head === "" ? "" : `${head} \u2014 `}${d.title}${withScope ? ` \u2014 ${d.scope ?? "global"}` : ""} \u2014 ${d.path}`;
4951
+ }
4952
+ function isCollection(value) {
4953
+ return COLLECTIONS.includes(value);
4954
+ }
4955
+ var collectionError = (value) => refuse(
4956
+ `unknown collection ${JSON.stringify(value)}; one of ${COLLECTIONS.join(", ")}`
4957
+ );
4958
+ async function context(cwd, args) {
4959
+ const { ws, manifest } = await workspaceAt(cwd);
4960
+ let scope;
4961
+ if (args["scope"] !== void 0) {
4962
+ const known = Object.keys(manifest.scopes).sort();
4963
+ if (typeof args["scope"] !== "string" || !Object.hasOwn(manifest.scopes, args["scope"]))
4964
+ return refuse(
4965
+ `unknown scope ${JSON.stringify(args["scope"])}; known: ${known.length === 0 ? "none" : known.join(", ")}`
4966
+ );
4967
+ scope = args["scope"];
4968
+ } else
4969
+ scope = resolveScope(
4970
+ manifest,
4971
+ relative6(ws.root, cwd).split(sep4).join(posix4.sep)
4972
+ );
4973
+ const entry = scope === null ? void 0 : manifest.scopes[scope];
4974
+ const where = scope === null || entry === void 0 ? "global scope" : `scope ${scope} (${entry.path}${entry.extends.length > 0 ? `, extends ${entry.extends.join(", ")}` : ""})`;
4975
+ const ctx = await assembleContext({ rnessDir: ws.rnessDir, manifest, scope });
4976
+ const index = new Map(
4977
+ (await documents(ws.rnessDir)).map((d) => [d.path, d])
4978
+ );
4979
+ const out = [`Workspace ${workspaceName(manifest, ws.root)} \xB7 ${where}`];
4980
+ for (const collection of ctx.collections) {
4981
+ const docs = collection.files.map((f) => index.get(`${collection.name}/${f.rel}`)).filter((d) => d !== void 0);
4982
+ if (docs.length === 0) continue;
4983
+ out.push(`${LABELS[collection.name]} (${docs.length}):`);
4984
+ for (const d of docs) out.push(line(d));
4985
+ }
4986
+ out.push("Read one with rness_read; search with rness_search.");
4987
+ return { text: out.join("\n") };
4988
+ }
4989
+ async function list(cwd, args) {
4990
+ const { ws } = await workspaceAt(cwd);
4991
+ const collection = args["collection"];
4992
+ if (!isCollection(collection)) return collectionError(collection);
4993
+ const status2 = args["status"];
4994
+ const allowed = STATUSES[collection];
4995
+ if (status2 !== void 0) {
4996
+ if (allowed === void 0)
4997
+ return refuse(`${collection} have no status to filter on`);
4998
+ if (typeof status2 !== "string" || !allowed.includes(status2))
4999
+ return refuse(
5000
+ `unknown status ${JSON.stringify(status2)} for ${collection}; one of ${allowed.join(", ")}`
5001
+ );
5002
+ }
5003
+ const docs = (await documents(ws.rnessDir, collection)).filter(
5004
+ (d) => status2 === void 0 || d.status === status2
5005
+ );
5006
+ if (docs.length === 0)
5007
+ return {
5008
+ text: `No document in ${collection}${status2 === void 0 ? "" : ` with status ${String(status2)}`}.`
5009
+ };
5010
+ return {
5011
+ text: [
5012
+ `${LABELS[collection]} (${docs.length}):`,
5013
+ ...docs.map((d) => line(d, true))
5014
+ ].join("\n")
5015
+ };
5016
+ }
5017
+ async function read(cwd, args) {
5018
+ const { ws } = await workspaceAt(cwd);
5019
+ const path = args["path"];
5020
+ if (typeof path !== "string" || path.trim() === "")
5021
+ return refuse("path is required, relative to .rness/");
5022
+ const segments = path.split(/[\\/]/);
5023
+ if (isAbsolute(path) || segments.includes("..") || segments[0] === ".git" || segments.includes("node_modules"))
5024
+ return refuse(
5025
+ `${path}: not a document of .rness/ (a relative path, no "..", nothing under .git/ or node_modules/)`
5026
+ );
5027
+ const root = await realpath3(ws.rnessDir);
5028
+ let real;
5029
+ try {
5030
+ real = await realpath3(resolve6(ws.rnessDir, path));
5031
+ } catch {
5032
+ return refuse(`${path}: no such file in .rness/`);
5033
+ }
5034
+ if (!real.startsWith(`${root}${sep4}`))
5035
+ return refuse(`${path}: leads outside .rness/`);
5036
+ const info = await stat4(real);
5037
+ if (!info.isFile()) return refuse(`${path}: not a file`);
5038
+ if (info.size > READ_LIMIT)
5039
+ return refuse(
5040
+ `${path}: ${info.size} bytes, over the 256 KiB rness_read returns`
5041
+ );
5042
+ return { text: await readFile10(real, "utf8") };
5043
+ }
5044
+ async function search(cwd, args) {
5045
+ const { ws } = await workspaceAt(cwd);
5046
+ const query = args["query"];
5047
+ if (typeof query !== "string" || query.trim() === "")
5048
+ return refuse("query is required");
5049
+ const collection = args["collection"];
5050
+ if (collection !== void 0 && !isCollection(collection))
5051
+ return collectionError(collection);
5052
+ const needle = query.toLowerCase();
5053
+ const hits = (await documents(ws.rnessDir, collection)).map((d) => ({
5054
+ d,
5055
+ lines: d.text.split(/\r?\n/).map((text, i) => ({ n: i + 1, text })).filter((l) => l.text.toLowerCase().includes(needle))
5056
+ })).filter((h) => h.lines.length > 0).sort(
5057
+ (a, b) => b.lines.length - a.lines.length || a.d.path.localeCompare(b.d.path)
5058
+ );
5059
+ if (hits.length === 0)
5060
+ return { text: `No document matches ${JSON.stringify(query)}.` };
5061
+ const shown = hits.slice(0, SEARCH_LIMIT);
5062
+ const out = [
5063
+ `${hits.length} document${hits.length === 1 ? "" : "s"} match${hits.length === 1 ? "es" : ""} ${JSON.stringify(query)}${hits.length > SEARCH_LIMIT ? ` (the ${SEARCH_LIMIT} most matching)` : ""}`
5064
+ ];
5065
+ for (const h of shown) {
5066
+ out.push(line(h.d, true));
5067
+ for (const l of h.lines.slice(0, LINES_PER_HIT))
5068
+ out.push(` ${l.n}: ${l.text.trim().slice(0, LINE_WIDTH)}`);
5069
+ }
5070
+ return { text: out.join("\n") };
5071
+ }
5072
+ var collectionProperty = {
5073
+ type: "string",
5074
+ enum: [...COLLECTIONS],
5075
+ description: "standards (rules), adr (decisions), specs (specifications), plans, skills"
5076
+ };
5077
+ function mcpTools(cwd) {
5078
+ return [
5079
+ {
5080
+ name: "rness_context",
5081
+ title: "rness: what applies here",
5082
+ description: "The rness scope of the current repository (or the one named) and what applies to it: the standards, decisions (ADRs), specifications and plans of that scope and of the scopes it extends \u2014 ids, statuses, titles and paths, no bodies. Start here, then read a document with rness_read.",
5083
+ inputSchema: {
5084
+ type: "object",
5085
+ properties: {
5086
+ scope: {
5087
+ type: "string",
5088
+ description: "A scope of rness.json; default: the one owning the working directory"
5089
+ }
5090
+ },
5091
+ additionalProperties: false
5092
+ },
5093
+ call: (args) => guard(() => context(cwd, args))
5094
+ },
5095
+ {
5096
+ name: "rness_list",
5097
+ title: "rness: a collection",
5098
+ description: "Every document of one collection of .rness/, across all scopes, with id, status, title, scope and path; optionally only those with a given status (for example the Accepted decisions).",
5099
+ inputSchema: {
5100
+ type: "object",
5101
+ properties: {
5102
+ collection: collectionProperty,
5103
+ status: {
5104
+ type: "string",
5105
+ description: "adr: Proposed, Accepted, Rejected, Superseded; specs: Draft, Proposed, Approved, Implemented, Superseded, Rejected; plans: Draft, Ready, In progress, Blocked, Completed, Abandoned"
5106
+ }
5107
+ },
5108
+ required: ["collection"],
5109
+ additionalProperties: false
5110
+ },
5111
+ call: (args) => guard(() => list(cwd, args))
5112
+ },
5113
+ {
5114
+ name: "rness_read",
5115
+ title: "rness: read a document",
5116
+ description: "One file of .rness/ as it is, by its path relative to .rness/ (as rness_context, rness_list and rness_search give it). Nothing outside .rness/.",
5117
+ inputSchema: {
5118
+ type: "object",
5119
+ properties: {
5120
+ path: {
5121
+ type: "string",
5122
+ description: "For example adr/0006-workspace.md"
5123
+ }
5124
+ },
5125
+ required: ["path"],
5126
+ additionalProperties: false
5127
+ },
5128
+ call: (args) => guard(() => read(cwd, args))
5129
+ },
5130
+ {
5131
+ name: "rness_search",
5132
+ title: "rness: search the context",
5133
+ description: "Where a subject is decided, specified or planned: a case-insensitive search of the documents of .rness/, most matching first, with the matching lines.",
5134
+ inputSchema: {
5135
+ type: "object",
5136
+ properties: {
5137
+ query: { type: "string", description: "Text to find" },
5138
+ collection: collectionProperty
5139
+ },
5140
+ required: ["query"],
5141
+ additionalProperties: false
5142
+ },
5143
+ call: (args) => guard(() => search(cwd, args))
5144
+ }
5145
+ ];
5146
+ }
5147
+
5148
+ // src/commands/mcp.ts
5149
+ async function mcpCommand(opts, io = {
5150
+ input: process.stdin,
5151
+ output: process.stdout
5152
+ }) {
5153
+ const protocol = createProtocol({
5154
+ name: "rness",
5155
+ version: VERSION,
5156
+ tools: mcpTools(opts.cwd ?? process.cwd())
5157
+ });
5158
+ const lines = createInterface({ input: io.input, crlfDelay: Infinity });
5159
+ for await (const line2 of lines) {
5160
+ if (line2.trim() === "") continue;
5161
+ const answer = await protocol.handle(line2);
5162
+ if (answer !== null) io.output.write(`${answer}
5163
+ `);
5164
+ }
5165
+ return 0;
5166
+ }
5167
+
4703
5168
  // src/cli.ts
4704
5169
  function buildProgram(state) {
4705
5170
  const program = new Command().name("rness").description("Configuration-plane CLI for rness workspaces").version(VERSION, "-v, --version", "print the version").exitOverride().showHelpAfterError().configureOutput({
@@ -4744,6 +5209,11 @@ function buildProgram(state) {
4744
5209
  ).option("-y, --yes", "do not ask for confirmation").addOption(new Option("--cwd <dir>").hideHelp()).action(async (opts) => {
4745
5210
  state.code = await syncCommand(opts);
4746
5211
  });
5212
+ program.command("mcp").description(
5213
+ "Serve the workspace context to agents over MCP (stdio, read-only)"
5214
+ ).addOption(new Option("--cwd <dir>").hideHelp()).action(async (opts) => {
5215
+ state.code = await mcpCommand(opts);
5216
+ });
4747
5217
  program.command("add").description(
4748
5218
  "Clone (or adopt) a repository under org/ and declare it in rness.json"
4749
5219
  ).argument("<repo>", "<repo>, <owner>/<repo>, or a clone URL").option(
@@ -4937,13 +5407,13 @@ export {
4937
5407
  loginCommand,
4938
5408
  createCommand,
4939
5409
  logoutCommand,
4940
- upgradeCommand,
4941
- checkBlocks,
4942
5410
  STATUSES,
4943
5411
  checkContract,
5412
+ upgradeCommand,
5413
+ checkBlocks,
4944
5414
  validateCommand,
4945
5415
  run2 as run,
4946
5416
  OWN_COMMANDS,
4947
5417
  catchUp
4948
5418
  };
4949
- //# sourceMappingURL=chunk-VQMX52HT.js.map
5419
+ //# sourceMappingURL=chunk-DL72BCPT.js.map