@rness/cli 0.8.1 → 0.9.1

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.
@@ -743,6 +743,21 @@ async function isClean(dir, ignore = []) {
743
743
  dir
744
744
  )).trim() === "";
745
745
  }
746
+ async function changedPaths(dir, paths) {
747
+ if (paths.length === 0) return [];
748
+ const entries = (await git(
749
+ ["status", "--porcelain", "-z", "--untracked-files=all", "--", ...paths],
750
+ dir
751
+ )).split("\0");
752
+ const changed = [];
753
+ for (let i = 0; i < entries.length; i++) {
754
+ const entry = entries[i] ?? "";
755
+ if (entry.length < 4) continue;
756
+ changed.push(entry.slice(3));
757
+ if (entry[0] === "R" || entry[0] === "C") i++;
758
+ }
759
+ return changed;
760
+ }
746
761
  async function pullFastForward(dir, credentials) {
747
762
  await git(["pull", "--ff-only", "--quiet"], dir, credentials);
748
763
  }
@@ -797,7 +812,7 @@ function classifyProbe(url, exitCode, stderr) {
797
812
  var STDERR_FLUSH_MS = 200;
798
813
  async function probeRemote(url, timeoutMs = 6e4, credentials) {
799
814
  positional(url, "repository url");
800
- return new Promise((resolve6) => {
815
+ return new Promise((resolve7) => {
801
816
  const auth = withCredentials(credentials);
802
817
  const child = spawn(
803
818
  "git",
@@ -816,7 +831,7 @@ async function probeRemote(url, timeoutMs = 6e4, credentials) {
816
831
  settled = true;
817
832
  clearTimeout(timer);
818
833
  child.stderr?.destroy();
819
- resolve6(probe);
834
+ resolve7(probe);
820
835
  };
821
836
  const timer = setTimeout(() => {
822
837
  child.kill("SIGKILL");
@@ -1198,8 +1213,8 @@ function warn(text) {
1198
1213
  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
1214
  var FROM = [63, 185, 80];
1200
1215
  var TO = [57, 197, 207];
1201
- function gradient(line) {
1202
- const chars = [...line];
1216
+ function gradient(line2) {
1217
+ const chars = [...line2];
1203
1218
  const last = Math.max(1, chars.length - 1);
1204
1219
  const painted = chars.map((ch, i) => {
1205
1220
  if (ch === " ") return ch;
@@ -1216,7 +1231,7 @@ function banner(version, stream = process.stdout) {
1216
1231
  const coloured = paint("done", "x", stream) !== "x";
1217
1232
  const trueColour = coloured && typeof stream.hasColors === "function" && stream.hasColors(2 ** 24);
1218
1233
  const word = WORD.map(
1219
- (line) => trueColour ? gradient(line) : paint("done", line, stream)
1234
+ (line2) => trueColour ? gradient(line2) : paint("done", line2, stream)
1220
1235
  );
1221
1236
  return [
1222
1237
  "",
@@ -1248,7 +1263,7 @@ async function detectGithubSsh(opts) {
1248
1263
  ...opts.interactive ? [] : ["-o", "BatchMode=yes"],
1249
1264
  "git@github.com"
1250
1265
  ];
1251
- return new Promise((resolve6) => {
1266
+ return new Promise((resolve7) => {
1252
1267
  const child = spawn2(opts.command ?? "ssh", args, {
1253
1268
  stdio: [opts.interactive ? "inherit" : "ignore", "ignore", "pipe"]
1254
1269
  });
@@ -1261,7 +1276,7 @@ async function detectGithubSsh(opts) {
1261
1276
  settled = true;
1262
1277
  clearTimeout(timer);
1263
1278
  child.stderr?.destroy();
1264
- resolve6(access2);
1279
+ resolve7(access2);
1265
1280
  };
1266
1281
  const timer = setTimeout(() => {
1267
1282
  child.kill("SIGKILL");
@@ -1390,7 +1405,7 @@ function reportError(e) {
1390
1405
  }
1391
1406
 
1392
1407
  // src/version.ts
1393
- var VERSION = "0.8.1";
1408
+ var VERSION = "0.9.1";
1394
1409
 
1395
1410
  // src/core/block.ts
1396
1411
  import { createHash } from "crypto";
@@ -1427,9 +1442,9 @@ function renderBlock({
1427
1442
  scope,
1428
1443
  org,
1429
1444
  depth,
1430
- context
1445
+ context: context2
1431
1446
  }) {
1432
- const standards = context.collections.find((c) => c.name === "standards")?.files ?? [];
1447
+ const standards = context2.collections.find((c) => c.name === "standards")?.files ?? [];
1433
1448
  const rules = standards.length === 0 ? ["_No standards apply to this scope yet._"] : standards.map(
1434
1449
  (f) => `<!-- rness: standards/${f.rel} -->
1435
1450
  ${toLf(f.content).trim()}`
@@ -1442,8 +1457,8 @@ ${rules.join("\n\n")}`;
1442
1457
  const text = [BEGIN, header(scope, hash), body, END].join("\n");
1443
1458
  return { text, hash, bytes: Buffer.byteLength(text, "utf8") };
1444
1459
  }
1445
- function parseHeader(line) {
1446
- const m = HEADER.exec(line.trimEnd());
1460
+ function parseHeader(line2) {
1461
+ const m = HEADER.exec(line2.trimEnd());
1447
1462
  if (m === null) return null;
1448
1463
  return { version: m[1] ?? null, scope: m[2] ?? "", hash: m[3] ?? "" };
1449
1464
  }
@@ -1614,8 +1629,8 @@ import { join as join8 } from "path";
1614
1629
  function findBlock(lines) {
1615
1630
  const begins = [];
1616
1631
  const ends = [];
1617
- lines.forEach((line, i) => {
1618
- const l = line.trimEnd();
1632
+ lines.forEach((line2, i) => {
1633
+ const l = line2.trimEnd();
1619
1634
  if (l === BEGIN) begins.push(i);
1620
1635
  if (l === END) ends.push(i);
1621
1636
  });
@@ -1695,14 +1710,38 @@ var TARGETS = {
1695
1710
  claude: {
1696
1711
  name: "claude",
1697
1712
  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: [
1713
+ files: [
1703
1714
  {
1704
- path: ["permissions", "additionalDirectories"],
1705
- contains: "../../.rness"
1715
+ file: ".claude/settings.json",
1716
+ guarantees: [
1717
+ // Read the context repository from a repository without a prompt.
1718
+ // Resolved against the project directory, and applied once Claude
1719
+ // Code has trusted it (verified on 2.1.284, spec 0011 §7).
1720
+ {
1721
+ path: ["permissions", "additionalDirectories"],
1722
+ contains: "../../.rness"
1723
+ }
1724
+ ]
1725
+ },
1726
+ {
1727
+ file: ".mcp.json",
1728
+ guarantees: [
1729
+ // The pinned copy, by a path relative to the repository — the
1730
+ // directory Claude Code starts the server in (spec 0014 §6). Not
1731
+ // pre-approved in settings: each developer approves it once, since
1732
+ // a repository approving its own command would run any change to
1733
+ // it unasked (spec 0014 §3, 0.9.1).
1734
+ {
1735
+ path: ["mcpServers", "rness"],
1736
+ value: {
1737
+ command: "node",
1738
+ args: [
1739
+ "../../.rness/node_modules/@rness/cli/dist/bin/rness.js",
1740
+ "mcp"
1741
+ ]
1742
+ }
1743
+ }
1744
+ ]
1706
1745
  }
1707
1746
  ]
1708
1747
  }
@@ -1728,7 +1767,7 @@ function parse3(text) {
1728
1767
  }
1729
1768
  return isObject(data) ? { ok: true, data } : { ok: false, reason: "not a JSON object" };
1730
1769
  }
1731
- function listAt(root, g, create) {
1770
+ function parentOf(root, g, create) {
1732
1771
  let node = root;
1733
1772
  for (const [i, key] of g.path.slice(0, -1).entries()) {
1734
1773
  const next = node[key];
@@ -1742,17 +1781,32 @@ function listAt(root, g, create) {
1742
1781
  };
1743
1782
  node = node[key];
1744
1783
  }
1784
+ return { kind: "node", node };
1785
+ }
1786
+ function apply(root, g, create) {
1787
+ const found = parentOf(root, g, create);
1788
+ if (found.kind === "absent") return { holds: false, changed: false };
1789
+ if (found.kind === "invalid") return { invalid: found.reason };
1790
+ const parent = found.node;
1745
1791
  const last = g.path[g.path.length - 1] ?? "";
1746
- const value = node[last];
1792
+ const value = parent[last];
1793
+ if ("value" in g) {
1794
+ if (value !== void 0) return { holds: true, changed: false };
1795
+ if (!create) return { holds: false, changed: false };
1796
+ parent[last] = structuredClone(g.value);
1797
+ return { holds: true, changed: true };
1798
+ }
1747
1799
  if (value === void 0) {
1748
- if (!create) return { kind: "absent" };
1749
- const list = [];
1750
- node[last] = list;
1751
- return { kind: "list", list };
1800
+ if (!create) return { holds: false, changed: false };
1801
+ parent[last] = [g.contains];
1802
+ return { holds: true, changed: true };
1752
1803
  }
1753
1804
  if (!Array.isArray(value))
1754
- return { kind: "invalid", reason: `${g.path.join(".")} is not a list` };
1755
- return { kind: "list", list: value };
1805
+ return { invalid: `${g.path.join(".")} is not a list` };
1806
+ if (value.includes(g.contains)) return { holds: true, changed: false };
1807
+ if (!create) return { holds: false, changed: false };
1808
+ value.push(g.contains);
1809
+ return { holds: true, changed: true };
1756
1810
  }
1757
1811
  function styleOf(text) {
1758
1812
  const indent = text.split(/\r?\n/).slice(1).map((l) => /^[ \t]+/.exec(l)?.[0]).find((w) => w !== void 0);
@@ -1767,13 +1821,9 @@ function ensureGuarantees(existing, guarantees) {
1767
1821
  if (!parsed.ok) return { kind: "invalid", reason: parsed.reason };
1768
1822
  let changed = false;
1769
1823
  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
- }
1824
+ const r = apply(parsed.data, g, true);
1825
+ if ("invalid" in r) return { kind: "invalid", reason: r.invalid };
1826
+ changed ||= r.changed;
1777
1827
  }
1778
1828
  if (existing === null)
1779
1829
  return {
@@ -1792,15 +1842,14 @@ function missingGuarantees(existing, guarantees) {
1792
1842
  if (!parsed.ok) return { invalid: parsed.reason };
1793
1843
  const missing = [];
1794
1844
  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);
1845
+ const r = apply(parsed.data, g, false);
1846
+ if ("invalid" in r) return { invalid: r.invalid };
1847
+ if (!r.holds) missing.push(g);
1799
1848
  }
1800
1849
  return missing;
1801
1850
  }
1802
1851
  function describeGuarantee(g) {
1803
- return `${g.path.join(".")} lacks ${g.contains}`;
1852
+ return "value" in g ? `${g.path.join(".")} is missing` : `${g.path.join(".")} lacks ${g.contains}`;
1804
1853
  }
1805
1854
 
1806
1855
  // src/core/agent-targets.ts
@@ -1809,11 +1858,12 @@ async function filesOf(root, manifest, targets) {
1809
1858
  for (const repo of Object.keys(manifest.repos)) {
1810
1859
  if (!await exists(join9(root, "org", repo))) continue;
1811
1860
  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
- });
1861
+ for (const owned of target.files)
1862
+ files.push({
1863
+ guarantees: owned.guarantees,
1864
+ file: join9(root, "org", repo, ...owned.file.split("/")),
1865
+ label: `org/${repo}/${owned.file}`
1866
+ });
1817
1867
  }
1818
1868
  return files;
1819
1869
  }
@@ -1822,16 +1872,23 @@ function declared(manifest) {
1822
1872
  (name) => Object.hasOwn(TARGETS, name) ? [TARGETS[name]] : []
1823
1873
  );
1824
1874
  }
1875
+ var BLOCK_FILES = ["AGENTS.md", "CLAUDE.md"];
1876
+ function writtenFiles(manifest) {
1877
+ return [
1878
+ ...BLOCK_FILES,
1879
+ ...declared(manifest).flatMap((t) => t.files.map((f) => f.file))
1880
+ ];
1881
+ }
1825
1882
  async function agentTargets(root, manifest, opts) {
1826
1883
  const outcomes = [];
1827
- for (const { target, file, label } of await filesOf(
1884
+ for (const { guarantees, file, label } of await filesOf(
1828
1885
  root,
1829
1886
  manifest,
1830
1887
  declared(manifest)
1831
1888
  )) {
1832
1889
  const existing = await readOrNull(file);
1833
1890
  if (opts.check) {
1834
- const missing = missingGuarantees(existing, target.guarantees);
1891
+ const missing = missingGuarantees(existing, guarantees);
1835
1892
  if ("invalid" in missing)
1836
1893
  outcomes.push({ label, status: "invalid", detail: missing.invalid });
1837
1894
  else if (missing.length > 0)
@@ -1843,7 +1900,7 @@ async function agentTargets(root, manifest, opts) {
1843
1900
  else outcomes.push({ label, status: "unchanged", detail: null });
1844
1901
  continue;
1845
1902
  }
1846
- const ensured = ensureGuarantees(existing, target.guarantees);
1903
+ const ensured = ensureGuarantees(existing, guarantees);
1847
1904
  if (ensured.kind === "invalid") {
1848
1905
  outcomes.push({ label, status: "invalid", detail: ensured.reason });
1849
1906
  continue;
@@ -1865,14 +1922,14 @@ async function leftoverTargets(root, manifest) {
1865
1922
  (t) => !agents.includes(t.name)
1866
1923
  );
1867
1924
  const leftovers = [];
1868
- for (const { target, file, label } of await filesOf(
1925
+ for (const { guarantees, file, label } of await filesOf(
1869
1926
  root,
1870
1927
  manifest,
1871
1928
  undeclared
1872
1929
  )) {
1873
1930
  const existing = await readOrNull(file);
1874
1931
  if (existing === null) continue;
1875
- const missing = missingGuarantees(existing, target.guarantees);
1932
+ const missing = missingGuarantees(existing, guarantees);
1876
1933
  if (!("invalid" in missing) && missing.length === 0) leftovers.push(label);
1877
1934
  }
1878
1935
  return leftovers;
@@ -1920,7 +1977,7 @@ async function step(options, work) {
1920
1977
  const rawErr = process.stderr.write.bind(
1921
1978
  process.stderr
1922
1979
  );
1923
- const guard = (raw) => ((...args) => {
1980
+ const guard2 = (raw) => ((...args) => {
1924
1981
  erase();
1925
1982
  return raw(...args);
1926
1983
  });
@@ -1935,8 +1992,8 @@ async function step(options, work) {
1935
1992
  } else {
1936
1993
  draw("\u2026");
1937
1994
  }
1938
- stream.write = guard(rawWrite);
1939
- if (process.stderr !== stream) process.stderr.write = guard(rawErr);
1995
+ stream.write = guard2(rawWrite);
1996
+ if (process.stderr !== stream) process.stderr.write = guard2(rawErr);
1940
1997
  try {
1941
1998
  return await work();
1942
1999
  } finally {
@@ -1983,8 +2040,8 @@ var plainUi = {
1983
2040
  { label: options.doing, animate: options.git !== true },
1984
2041
  work
1985
2042
  );
1986
- const line = options.quiet === true ? null : done?.(result) ?? null;
1987
- if (line !== null) this.line(line[0], line[1]);
2043
+ const line2 = options.quiet === true ? null : done?.(result) ?? null;
2044
+ if (line2 !== null) this.line(line2[0], line2[1]);
1988
2045
  return result;
1989
2046
  }
1990
2047
  };
@@ -2017,7 +2074,7 @@ function sessionUi(p) {
2017
2074
  p.log.warn(text);
2018
2075
  },
2019
2076
  note: (title, lines) => {
2020
- p.note(lines.join("\n"), title, { format: (line) => grey(line) });
2077
+ p.note(lines.join("\n"), title, { format: (line2) => grey(line2) });
2021
2078
  },
2022
2079
  hint: (text) => {
2023
2080
  p.log.message(paint("idle", text));
@@ -2031,20 +2088,20 @@ function sessionUi(p) {
2031
2088
  async step(options, work, done) {
2032
2089
  const label = options.sentence ?? tidy(options.doing);
2033
2090
  const finish2 = (result) => done?.(result) ?? null;
2034
- const label_ = (line) => `${line[1]} ${line[0]}`;
2091
+ const label_ = (line2) => `${line2[1]} ${line2[0]}`;
2035
2092
  if (options.git === true && !this.gitIsSilent) {
2036
2093
  p.log.message(paint("idle", `${label}\u2026`));
2037
2094
  const result = await work();
2038
- const line = finish2(result);
2039
- if (line !== null) say(line[0], label_(line));
2095
+ const line2 = finish2(result);
2096
+ if (line2 !== null) say(line2[0], label_(line2));
2040
2097
  return result;
2041
2098
  }
2042
2099
  const spin = p.spinner();
2043
2100
  spin.start(label);
2044
2101
  try {
2045
2102
  const result = await work();
2046
- const line = finish2(result);
2047
- spin.stop(line === null ? label : label_(line));
2103
+ const line2 = finish2(result);
2104
+ spin.stop(line2 === null ? label : label_(line2));
2048
2105
  return result;
2049
2106
  } catch (e) {
2050
2107
  spin.error(`${label} \u2014 failed`);
@@ -2070,7 +2127,6 @@ async function makeUi(terminal) {
2070
2127
  }
2071
2128
 
2072
2129
  // src/commands/sync.ts
2073
- var MANAGED_FILES = ["AGENTS.md", "CLAUDE.md"];
2074
2130
  function targetsOf(manifest, root, only) {
2075
2131
  const targets = [];
2076
2132
  if (only === void 0)
@@ -2260,13 +2316,7 @@ async function syncCommand(opts, deps = {}) {
2260
2316
  }
2261
2317
  } else if (opts.pull === true && !check) {
2262
2318
  try {
2263
- const written = [
2264
- ...MANAGED_FILES,
2265
- ...(manifest.agents ?? []).flatMap(
2266
- (a) => Object.hasOwn(TARGETS, a) ? [TARGETS[a].file] : []
2267
- )
2268
- ];
2269
- if (!await isClean(dir, written)) {
2319
+ if (!await isClean(dir, writtenFiles(manifest))) {
2270
2320
  lines.push(["skipped", `${label} (working tree not clean)`]);
2271
2321
  continue;
2272
2322
  }
@@ -2309,14 +2359,14 @@ async function syncCommand(opts, deps = {}) {
2309
2359
  continue;
2310
2360
  }
2311
2361
  const [agentsLinked, claudeLinked] = await Promise.all(
2312
- MANAGED_FILES.map((f) => isSymlink(join10(t.dir, f)))
2362
+ BLOCK_FILES.map((f) => isSymlink(join10(t.dir, f)))
2313
2363
  );
2314
2364
  const atRoot = t.scope === null;
2315
2365
  if (!atRoot && (agentsLinked === true || claudeLinked === true)) {
2316
2366
  lines.push(["skipped", `${t.label} (symlink)`]);
2317
2367
  continue;
2318
2368
  }
2319
- const context = await assembleContext({
2369
+ const context2 = await assembleContext({
2320
2370
  rnessDir: ws.rnessDir,
2321
2371
  manifest,
2322
2372
  scope: t.scope
@@ -2325,7 +2375,7 @@ async function syncCommand(opts, deps = {}) {
2325
2375
  scope: t.scope,
2326
2376
  org,
2327
2377
  depth: t.depth,
2328
- context
2378
+ context: context2
2329
2379
  });
2330
2380
  if (block.bytes > BLOCK_SIZE_WARNING) {
2331
2381
  ui.warn(
@@ -3062,10 +3112,10 @@ async function runPinnedSync(root) {
3062
3112
  function summariseSync(stdout) {
3063
3113
  const counts = /* @__PURE__ */ new Map();
3064
3114
  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);
3115
+ for (const line2 of stdout.split("\n")) {
3116
+ if (line2.trim() === "") continue;
3117
+ const verb = /^(updated|unchanged|stale)\s/.exec(line2)?.[1];
3118
+ if (verb === void 0) others.push(line2);
3069
3119
  else counts.set(verb, (counts.get(verb) ?? 0) + 1);
3070
3120
  }
3071
3121
  const total = [...counts.values()].reduce((a, b) => a + b, 0);
@@ -3177,12 +3227,33 @@ function scaffoldMessage(subject, version) {
3177
3227
 
3178
3228
  ${SCAFFOLD_TRAILER}: ${version}`;
3179
3229
  }
3230
+ async function commitUpgrade(rnessDir, message) {
3231
+ const add = await run(rnessDir, ["add", "-A"]);
3232
+ if (add.code !== 0) return { kind: "refused", output: add.stderr.trim() };
3233
+ const staged = await run(rnessDir, ["diff", "--cached", "--quiet"]);
3234
+ if (staged.code === 0 && !await mergeInProgress(rnessDir))
3235
+ return { kind: "nothing" };
3236
+ const commit = await run(rnessDir, ["commit", "-q", "-m", message]);
3237
+ if (commit.code !== 0)
3238
+ return {
3239
+ kind: "refused",
3240
+ output: `${commit.stdout}
3241
+ ${commit.stderr}`.trim()
3242
+ };
3243
+ return { kind: "committed" };
3244
+ }
3245
+ async function mergeInProgress(dir) {
3246
+ const r = await run(dir, ["rev-parse", "-q", "--verify", "MERGE_HEAD"]);
3247
+ return r.code === 0;
3248
+ }
3180
3249
  async function findScaffoldBase(rnessDir) {
3250
+ const merging = await mergeInProgress(rnessDir);
3181
3251
  const log = await run(rnessDir, [
3182
3252
  "log",
3183
3253
  "--topo-order",
3184
3254
  `--format=%H%x1f%P%x1f%s%x1f%(trailers:key=${SCAFFOLD_TRAILER},valueonly,separator=%x2C)%x1e`,
3185
- "HEAD"
3255
+ "HEAD",
3256
+ ...merging ? ["MERGE_HEAD"] : []
3186
3257
  ]);
3187
3258
  if (log.code !== 0) return null;
3188
3259
  const commits = log.stdout.split("").map((r) => r.trim()).filter((r) => r !== "").map((r) => {
@@ -3269,8 +3340,8 @@ async function mergeScaffold(rnessDir, commit, base) {
3269
3340
  "--name-status",
3270
3341
  "HEAD"
3271
3342
  ]);
3272
- for (const line of status2.split("\n")) {
3273
- const [letter = "", path = ""] = line.split(" ");
3343
+ for (const line2 of status2.split("\n")) {
3344
+ const [letter = "", path = ""] = line2.split(" ");
3274
3345
  if (path === "" || conflicts.includes(path)) continue;
3275
3346
  if (letter === "A") changes.push(["added", path]);
3276
3347
  else if (letter === "D") changes.push(["removed", path]);
@@ -3413,8 +3484,8 @@ async function syncBlocks(ui, root, shown, inPlace, sshWorks) {
3413
3484
  return 1;
3414
3485
  }
3415
3486
  const outcome = syncOutcome(result, { sshWorks: sshWorks === true });
3416
- for (const line of outcome.hints) ui.hint(line);
3417
- for (const line of outcome.errors) ui.error(line);
3487
+ for (const line2 of outcome.hints) ui.hint(line2);
3488
+ for (const line2 of outcome.errors) ui.error(line2);
3418
3489
  return outcome.code;
3419
3490
  }
3420
3491
 
@@ -3519,7 +3590,7 @@ async function createBlank(input) {
3519
3590
  `Workspace \`${name}\` is ready in ${shown}/.`,
3520
3591
  "",
3521
3592
  "Next:",
3522
- ...next.map((line) => ` ${line}`),
3593
+ ...next.map((line2) => ` ${line2}`),
3523
3594
  ""
3524
3595
  ].join("\n")
3525
3596
  );
@@ -3530,8 +3601,8 @@ async function createBlank(input) {
3530
3601
  function firstLine4(text) {
3531
3602
  return text.split("\n").find((l) => l.trim() !== "")?.trim() ?? "unknown error";
3532
3603
  }
3533
- function splitSpecs(list) {
3534
- return list.split(",").map((s) => s.trim()).filter((s) => s !== "");
3604
+ function splitSpecs(list2) {
3605
+ return list2.split(",").map((s) => s.trim()).filter((s) => s !== "");
3535
3606
  }
3536
3607
  function cancelled(ui) {
3537
3608
  ui.cancelled();
@@ -4162,7 +4233,7 @@ ${next2.map((l) => ` ${l}`).join("\n")}
4162
4233
  `Workspace for \`${org}\` is ready in ${shown}/.`,
4163
4234
  "",
4164
4235
  "Next:",
4165
- ...next.map((line) => ` ${line}`),
4236
+ ...next.map((line2) => ` ${line2}`),
4166
4237
  ""
4167
4238
  ].join("\n")
4168
4239
  );
@@ -4203,11 +4274,67 @@ async function logoutCommand() {
4203
4274
  }
4204
4275
  }
4205
4276
 
4277
+ // src/core/contract.ts
4278
+ import { join as join20 } from "path";
4279
+ var STATUSES = {
4280
+ adr: ["Proposed", "Accepted", "Rejected", "Superseded"],
4281
+ specs: [
4282
+ "Draft",
4283
+ "Proposed",
4284
+ "Approved",
4285
+ "Implemented",
4286
+ "Superseded",
4287
+ "Rejected"
4288
+ ],
4289
+ plans: ["Draft", "Ready", "In progress", "Blocked", "Completed", "Abandoned"]
4290
+ };
4291
+ var SKIP = /* @__PURE__ */ new Set(["adr/0000-template.md"]);
4292
+ var PLACEMENT_KEYS = ["scopes", "scope"];
4293
+ async function checkContract(rnessDir) {
4294
+ const problems = [];
4295
+ for (const name of COLLECTIONS) {
4296
+ const allowed = STATUSES[name];
4297
+ const items = await collectMarkdown(join20(rnessDir, name));
4298
+ for (const item of items) {
4299
+ const where = `${name}/${item.rel}`;
4300
+ if (SKIP.has(where)) continue;
4301
+ if (item.fieldsError !== null) {
4302
+ problems.push(`${where}: invalid front matter (${item.fieldsError})`);
4303
+ continue;
4304
+ }
4305
+ if (item.fields !== null) {
4306
+ for (const key of PLACEMENT_KEYS) {
4307
+ if (Object.hasOwn(item.fields, key)) {
4308
+ problems.push(
4309
+ `${where}: front matter "${key}" is not supported \u2014 the directory decides the scope; move the file`
4310
+ );
4311
+ }
4312
+ }
4313
+ }
4314
+ if (allowed === void 0) continue;
4315
+ if (item.fields === null) {
4316
+ problems.push(`${where}: missing a front-matter block`);
4317
+ continue;
4318
+ }
4319
+ if (item.title === null) problems.push(`${where}: missing a "# " title`);
4320
+ const status2 = item.fields["status"];
4321
+ if (status2 === void 0 || status2 === null || status2 === "") {
4322
+ problems.push(`${where}: missing "status"`);
4323
+ } else if (typeof status2 !== "string" || !allowed.includes(status2)) {
4324
+ problems.push(
4325
+ `${where}: unknown status ${JSON.stringify(status2)} (expected ${allowed.join(", ")})`
4326
+ );
4327
+ }
4328
+ }
4329
+ }
4330
+ return problems;
4331
+ }
4332
+
4206
4333
  // src/commands/upgrade.ts
4207
4334
  import { execFile as execFile6 } from "child_process";
4208
4335
  import { mkdtemp as mkdtemp3, readFile as readFile9, rm as rm7 } from "fs/promises";
4209
4336
  import { tmpdir as tmpdir3 } from "os";
4210
- import { basename as basename5, join as join20, relative as relative5 } from "path";
4337
+ import { basename as basename5, join as join21, relative as relative5 } from "path";
4211
4338
  import { promisify as promisify6 } from "util";
4212
4339
  var execFileP6 = promisify6(execFile6);
4213
4340
  var NOTES = "https://github.com/rness-dev/rness/tree/main/packages/cli#readme";
@@ -4227,10 +4354,10 @@ async function latestVersion() {
4227
4354
  async function scaffoldFor(version, rnessDir) {
4228
4355
  if (version === VERSION) return { dir: scaffoldDir() };
4229
4356
  const installed = await readPinnedCli(rnessDir);
4230
- const inside = installed === null ? null : join20(installed.dir, "scaffold");
4231
- if (installed?.version === version && inside !== null && await exists(join20(inside, "package.json")))
4357
+ const inside = installed === null ? null : join21(installed.dir, "scaffold");
4358
+ if (installed?.version === version && inside !== null && await exists(join21(inside, "package.json")))
4232
4359
  return { dir: inside };
4233
- const scratch = await mkdtemp3(join20(tmpdir3(), "rness-pack-"));
4360
+ const scratch = await mkdtemp3(join21(tmpdir3(), "rness-pack-"));
4234
4361
  const cleanup = () => rm7(scratch, { recursive: true, force: true });
4235
4362
  try {
4236
4363
  const { stdout } = await execFileP6(
@@ -4245,8 +4372,8 @@ async function scaffoldFor(version, rnessDir) {
4245
4372
  { timeout: 6e4 }
4246
4373
  );
4247
4374
  const tarball = stdout.trim().split("\n").at(-1) ?? "";
4248
- await execFileP6("tar", ["-xzf", join20(scratch, tarball), "-C", scratch]);
4249
- return { dir: join20(scratch, "package", "scaffold"), cleanup };
4375
+ await execFileP6("tar", ["-xzf", join21(scratch, tarball), "-C", scratch]);
4376
+ return { dir: join21(scratch, "package", "scaffold"), cleanup };
4250
4377
  } catch (e) {
4251
4378
  await cleanup();
4252
4379
  throw new Error(
@@ -4258,21 +4385,43 @@ async function scaffoldFor(version, rnessDir) {
4258
4385
  async function declaredManager(rnessDir, pm) {
4259
4386
  try {
4260
4387
  const pkg = JSON.parse(
4261
- await readFile9(join20(rnessDir, "package.json"), "utf8")
4388
+ await readFile9(join21(rnessDir, "package.json"), "utf8")
4262
4389
  );
4263
4390
  if (typeof pkg.packageManager === "string") return pkg.packageManager;
4264
4391
  } catch {
4265
4392
  }
4266
4393
  return `${pm}@${await packageManagerVersion(pm)}`;
4267
4394
  }
4268
- function finish(ui, cwd, rnessDir, label, target) {
4395
+ async function clonesToCommit(root, cwd, manifest) {
4396
+ const files = writtenFiles(manifest);
4397
+ const clones = [];
4398
+ for (const repo of Object.keys(manifest.repos)) {
4399
+ const dir = join21(root, "org", repo);
4400
+ if (!await exists(dir)) continue;
4401
+ try {
4402
+ const paths = await changedPaths(dir, files);
4403
+ if (paths.length > 0)
4404
+ clones.push({ dir: relative5(cwd, dir) || ".", paths });
4405
+ } catch {
4406
+ }
4407
+ }
4408
+ return clones;
4409
+ }
4410
+ function finish(ui, cwd, rnessDir, label, target, ending) {
4269
4411
  const rel = relative5(cwd, rnessDir) || ".";
4270
- const next = [
4271
- `git -C ${rel} add -A && git -C ${rel} commit -m "chore: rness ${target}"`,
4272
- "# teammates: git pull \u2014 the next rness command installs it"
4273
- ];
4412
+ const message = `chore: rness ${target}`;
4413
+ const next = [];
4414
+ if (ending.commit === "committed") next.push(`git -C ${rel} push`);
4415
+ if (ending.commit === "refused")
4416
+ next.push(`git -C ${rel} commit -m "${message}"`);
4417
+ for (const c of ending.clones)
4418
+ next.push(
4419
+ `git -C ${c.dir} add ${c.paths.join(" ")} && git -C ${c.dir} commit -m "${message}"`
4420
+ );
4421
+ if (ending.commit !== "nothing")
4422
+ next.push("# teammates: git pull \u2014 the next rness command installs it");
4274
4423
  if (ui.session) {
4275
- ui.note("Next steps", next);
4424
+ if (next.length > 0) ui.note("Next steps", next);
4276
4425
  ui.outro(`${label} runs @rness/cli ${target}`);
4277
4426
  } else
4278
4427
  process.stdout.write(
@@ -4280,12 +4429,10 @@ function finish(ui, cwd, rnessDir, label, target) {
4280
4429
  "",
4281
4430
  status("upgraded", `${label} to @rness/cli ${target}`),
4282
4431
  "",
4283
- "Next:",
4284
- ...next.map((line) => ` ${line}`),
4285
- ""
4432
+ ...next.length > 0 ? ["Next:", ...next.map((line2) => ` ${line2}`), ""] : []
4286
4433
  ].join("\n")
4287
4434
  );
4288
- return 0;
4435
+ return ending.commit === "refused" ? 1 : 0;
4289
4436
  }
4290
4437
  async function upgradeCommand(version, opts, terminal = defaultTerminal, deps = {}) {
4291
4438
  const cwd = opts.cwd ?? process.cwd();
@@ -4334,6 +4481,13 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
4334
4481
  `);
4335
4482
  return 1;
4336
4483
  }
4484
+ if (await mergeInProgress(ws.rnessDir)) {
4485
+ process.stderr.write(
4486
+ `${label} has a merge in progress; commit it, or abort it with git -C ${relative5(cwd, ws.rnessDir) || "."} merge --abort, then ${rnessCommand()} upgrade
4487
+ `
4488
+ );
4489
+ return 1;
4490
+ }
4337
4491
  if (!await isClean(ws.rnessDir)) {
4338
4492
  process.stderr.write(
4339
4493
  `${label} has uncommitted changes; commit or stash them, then ${rnessCommand()} upgrade
@@ -4435,62 +4589,85 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
4435
4589
  await source.cleanup?.();
4436
4590
  }
4437
4591
  }
4438
- const packageFile = join20(ws.rnessDir, "package.json");
4592
+ const packageFile = join21(ws.rnessDir, "package.json");
4439
4593
  const restore = /* @__PURE__ */ new Map([
4440
4594
  [packageFile, await readFile9(packageFile, "utf8")]
4441
4595
  ]);
4442
4596
  const moved = await readPin(ws.rnessDir);
4443
4597
  if (moved !== null && moved.spec !== target)
4444
4598
  await writePin(ws.rnessDir, moved.spec, target);
4445
- if (pin.spec === target && installed === target)
4446
- return finish(ui, cwd, ws.rnessDir, label, target);
4447
- try {
4448
- await ui.step(
4449
- { doing: `installing dependencies with ${pm}` },
4450
- () => installDependencies(pm, ws.rnessDir),
4451
- () => ["installed", `dependencies with ${pm}`]
4452
- );
4453
- } catch (e) {
4454
- const message = e instanceof Error ? e.message : String(e);
4455
- ui.error(message);
4456
- if (!merged) {
4457
- for (const [file, text] of restore) await writeFileAtomic(file, text);
4458
- ui.hint(
4459
- `restored ${relative5(cwd, packageFile) || packageFile}`,
4460
- "stderr"
4599
+ const mustInstall = !(pin.spec === target && installed === target);
4600
+ if (mustInstall) {
4601
+ try {
4602
+ await ui.step(
4603
+ { doing: `installing dependencies with ${pm}` },
4604
+ () => installDependencies(pm, ws.rnessDir),
4605
+ () => ["installed", `dependencies with ${pm}`]
4461
4606
  );
4607
+ } catch (e) {
4608
+ const message2 = e instanceof Error ? e.message : String(e);
4609
+ ui.error(message2);
4610
+ if (!merged) {
4611
+ for (const [file, text] of restore) await writeFileAtomic(file, text);
4612
+ ui.hint(
4613
+ `restored ${relative5(cwd, packageFile) || packageFile}`,
4614
+ "stderr"
4615
+ );
4616
+ }
4617
+ if (merged)
4618
+ ui.hint(
4619
+ `the scaffold merge is still staged; undo it with: git -C ${relative5(cwd, ws.rnessDir) || "."} merge --abort`,
4620
+ "stderr"
4621
+ );
4622
+ if (/minimum.?release.?age|NO_MATURE/i.test(message2))
4623
+ ui.hint(
4624
+ "pnpm refuses versions published less than 24 h ago: add '@rness/cli' under minimumReleaseAgeExclude in .rness/pnpm-workspace.yaml",
4625
+ "stderr"
4626
+ );
4627
+ return 1;
4462
4628
  }
4463
- if (merged)
4464
- ui.hint(
4465
- `the scaffold merge is still staged; undo it with: git -C ${relative5(cwd, ws.rnessDir) || "."} merge --abort`,
4466
- "stderr"
4467
- );
4468
- if (/minimum.?release.?age|NO_MATURE/i.test(message))
4469
- ui.hint(
4470
- "pnpm refuses versions published less than 24 h ago: add '@rness/cli' under minimumReleaseAgeExclude in .rness/pnpm-workspace.yaml",
4471
- "stderr"
4472
- );
4473
- return 1;
4474
- }
4475
- const now = (await readPinnedCli(ws.rnessDir))?.version ?? null;
4476
- if (now !== target) {
4477
- process.stderr.write(
4478
- `${pm} install left @rness/cli ${now ?? "not installed"} in ${label}, not ${target}
4629
+ const now = (await readPinnedCli(ws.rnessDir))?.version ?? null;
4630
+ if (now !== target) {
4631
+ process.stderr.write(
4632
+ `${pm} install left @rness/cli ${now ?? "not installed"} in ${label}, not ${target}
4479
4633
  `
4480
- );
4481
- return 1;
4634
+ );
4635
+ return 1;
4636
+ }
4482
4637
  }
4483
4638
  ui.gitIsSilent = true;
4484
4639
  const code = await syncBlocks(ui, ws.root, basename5(ws.root));
4485
4640
  if (code !== 0) return code;
4486
- return finish(ui, cwd, ws.rnessDir, label, target);
4641
+ const message = `chore: rness ${target}`;
4642
+ const committed = await commitUpgrade(ws.rnessDir, message);
4643
+ if (committed.kind === "committed")
4644
+ ui.line(
4645
+ "committed",
4646
+ `${label} \u2014 ${message}`,
4647
+ `Committed ${label}: ${message}`
4648
+ );
4649
+ if (committed.kind === "refused") {
4650
+ ui.error(
4651
+ `the commit of ${label} was refused; everything is staged \u2014 deal with what git said, then commit`
4652
+ );
4653
+ for (const line2 of committed.output.split("\n"))
4654
+ if (line2.trim() !== "") ui.hint(line2, "stderr");
4655
+ }
4656
+ return finish(ui, cwd, ws.rnessDir, label, target, {
4657
+ commit: committed.kind,
4658
+ clones: await clonesToCommit(
4659
+ ws.root,
4660
+ cwd,
4661
+ await loadManifest(ws.rnessDir)
4662
+ )
4663
+ });
4487
4664
  } catch (e) {
4488
4665
  return reportError(e);
4489
4666
  }
4490
4667
  }
4491
4668
 
4492
4669
  // src/core/blocks.ts
4493
- import { join as join21 } from "path";
4670
+ import { join as join22 } from "path";
4494
4671
  var PROBLEM = /* @__PURE__ */ new Set(["stale", "malformed"]);
4495
4672
  async function checkBlocks({
4496
4673
  root,
@@ -4507,7 +4684,7 @@ async function checkBlocks({
4507
4684
  warnings: checks.filter((c) => !PROBLEM.has(c.status) && c.message !== null).map((c) => c.message),
4508
4685
  checks
4509
4686
  });
4510
- if (!await exists(join21(root, "org"))) return derive();
4687
+ if (!await exists(join22(root, "org"))) return derive();
4511
4688
  const targets = [
4512
4689
  { scope: null, dir: root, depth: 0, file: "AGENTS.md", label: "AGENTS.md" },
4513
4690
  { scope: null, dir: root, depth: 0, file: "CLAUDE.md", label: "CLAUDE.md" }
@@ -4516,7 +4693,7 @@ async function checkBlocks({
4516
4693
  const segments = entry.path.split("/");
4517
4694
  targets.push({
4518
4695
  scope: name,
4519
- dir: join21(root, ...segments),
4696
+ dir: join22(root, ...segments),
4520
4697
  depth: segments.length,
4521
4698
  file: "AGENTS.md",
4522
4699
  label: `${entry.path}/AGENTS.md`
@@ -4531,7 +4708,7 @@ async function checkBlocks({
4531
4708
  );
4532
4709
  continue;
4533
4710
  }
4534
- const text = await readOrNull(join21(t.dir, t.file));
4711
+ const text = await readOrNull(join22(t.dir, t.file));
4535
4712
  const lines = text === null ? [] : text.split(/\r?\n/);
4536
4713
  const found = findBlock(lines);
4537
4714
  if (found.kind === "none") {
@@ -4546,7 +4723,7 @@ async function checkBlocks({
4546
4723
  note(t.label, "malformed", `${t.label}: ${found.message}`);
4547
4724
  continue;
4548
4725
  }
4549
- const context = await assembleContext({
4726
+ const context2 = await assembleContext({
4550
4727
  rnessDir,
4551
4728
  manifest,
4552
4729
  scope: t.scope
@@ -4555,7 +4732,7 @@ async function checkBlocks({
4555
4732
  scope: t.scope,
4556
4733
  org,
4557
4734
  depth: t.depth,
4558
- context
4735
+ context: context2
4559
4736
  });
4560
4737
  const pointerLeft = t.file === "CLAUDE.md" && lines.some((l) => l.trim() === "@AGENTS.md");
4561
4738
  if (isCurrentBlock(lines.slice(found.begin, found.end + 1), fresh2.hash) && !pointerLeft)
@@ -4570,62 +4747,6 @@ async function checkBlocks({
4570
4747
  return derive();
4571
4748
  }
4572
4749
 
4573
- // src/core/contract.ts
4574
- import { join as join22 } from "path";
4575
- var STATUSES = {
4576
- adr: ["Proposed", "Accepted", "Rejected", "Superseded"],
4577
- specs: [
4578
- "Draft",
4579
- "Proposed",
4580
- "Approved",
4581
- "Implemented",
4582
- "Superseded",
4583
- "Rejected"
4584
- ],
4585
- plans: ["Draft", "Ready", "In progress", "Blocked", "Completed", "Abandoned"]
4586
- };
4587
- var SKIP = /* @__PURE__ */ new Set(["adr/0000-template.md"]);
4588
- var PLACEMENT_KEYS = ["scopes", "scope"];
4589
- async function checkContract(rnessDir) {
4590
- const problems = [];
4591
- for (const name of COLLECTIONS) {
4592
- const allowed = STATUSES[name];
4593
- const items = await collectMarkdown(join22(rnessDir, name));
4594
- for (const item of items) {
4595
- const where = `${name}/${item.rel}`;
4596
- if (SKIP.has(where)) continue;
4597
- if (item.fieldsError !== null) {
4598
- problems.push(`${where}: invalid front matter (${item.fieldsError})`);
4599
- continue;
4600
- }
4601
- if (item.fields !== null) {
4602
- for (const key of PLACEMENT_KEYS) {
4603
- if (Object.hasOwn(item.fields, key)) {
4604
- problems.push(
4605
- `${where}: front matter "${key}" is not supported \u2014 the directory decides the scope; move the file`
4606
- );
4607
- }
4608
- }
4609
- }
4610
- if (allowed === void 0) continue;
4611
- if (item.fields === null) {
4612
- problems.push(`${where}: missing a front-matter block`);
4613
- continue;
4614
- }
4615
- if (item.title === null) problems.push(`${where}: missing a "# " title`);
4616
- const status2 = item.fields["status"];
4617
- if (status2 === void 0 || status2 === null || status2 === "") {
4618
- problems.push(`${where}: missing "status"`);
4619
- } else if (typeof status2 !== "string" || !allowed.includes(status2)) {
4620
- problems.push(
4621
- `${where}: unknown status ${JSON.stringify(status2)} (expected ${allowed.join(", ")})`
4622
- );
4623
- }
4624
- }
4625
- }
4626
- return problems;
4627
- }
4628
-
4629
4750
  // src/commands/validate.ts
4630
4751
  async function validateCommand(opts, deps = {}) {
4631
4752
  const cwd = opts.cwd ?? process.cwd();
@@ -4714,9 +4835,9 @@ async function readStdin() {
4714
4835
  async function gitCredentialCommand(operation, input) {
4715
4836
  if (operation !== "get") return 0;
4716
4837
  const fields = /* @__PURE__ */ new Map();
4717
- for (const line of (input ?? await readStdin()).split("\n")) {
4718
- const eq = line.indexOf("=");
4719
- if (eq > 0) fields.set(line.slice(0, eq), line.slice(eq + 1).trimEnd());
4838
+ for (const line2 of (input ?? await readStdin()).split("\n")) {
4839
+ const eq = line2.indexOf("=");
4840
+ if (eq > 0) fields.set(line2.slice(0, eq), line2.slice(eq + 1).trimEnd());
4720
4841
  }
4721
4842
  if (fields.get("protocol") !== "https" || fields.get("host") !== "github.com")
4722
4843
  return 0;
@@ -4728,6 +4849,409 @@ password=${resolved.token}
4728
4849
  return 0;
4729
4850
  }
4730
4851
 
4852
+ // src/commands/mcp.ts
4853
+ import { createInterface } from "readline";
4854
+
4855
+ // src/mcp/protocol.ts
4856
+ var MODERN_VERSIONS = ["2026-07-28"];
4857
+ var LEGACY_VERSIONS = [
4858
+ "2025-11-25",
4859
+ "2025-06-18",
4860
+ "2025-03-26",
4861
+ "2024-11-05"
4862
+ ];
4863
+ var SUPPORTED = [...MODERN_VERSIONS, ...LEGACY_VERSIONS];
4864
+ var VERSION_KEY = "io.modelcontextprotocol/protocolVersion";
4865
+ var CAPABILITIES_KEY = "io.modelcontextprotocol/clientCapabilities";
4866
+ var SERVER_INFO_KEY = "io.modelcontextprotocol/serverInfo";
4867
+ function isObject2(value) {
4868
+ return value !== null && typeof value === "object" && !Array.isArray(value);
4869
+ }
4870
+ function errorLine(id, code, message, data) {
4871
+ return JSON.stringify({
4872
+ jsonrpc: "2.0",
4873
+ id,
4874
+ error: data === void 0 ? { code, message } : { code, message, data }
4875
+ });
4876
+ }
4877
+ function createProtocol(opts) {
4878
+ const serverInfo = { name: opts.name, version: opts.version };
4879
+ return {
4880
+ async handle(line2) {
4881
+ let message;
4882
+ try {
4883
+ message = JSON.parse(line2);
4884
+ } catch {
4885
+ return errorLine(null, -32700, "Parse error");
4886
+ }
4887
+ const rawId = isObject2(message) ? message["id"] : void 0;
4888
+ const id = typeof rawId === "string" || typeof rawId === "number" ? rawId : null;
4889
+ if (!isObject2(message) || message["jsonrpc"] !== "2.0" || typeof message["method"] !== "string")
4890
+ return errorLine(id, -32600, "Invalid Request");
4891
+ if (!("id" in message)) return null;
4892
+ if (id === null) return errorLine(null, -32600, "Invalid Request");
4893
+ const method = message["method"];
4894
+ const params = isObject2(message["params"]) ? message["params"] : {};
4895
+ const meta = isObject2(params["_meta"]) ? params["_meta"] : null;
4896
+ const modern = meta !== null && typeof meta[VERSION_KEY] === "string";
4897
+ if (modern) {
4898
+ const requested = meta[VERSION_KEY];
4899
+ if (!MODERN_VERSIONS.includes(requested))
4900
+ return errorLine(id, -32022, "Unsupported protocol version", {
4901
+ supported: SUPPORTED,
4902
+ requested
4903
+ });
4904
+ if (!isObject2(meta[CAPABILITIES_KEY]))
4905
+ return errorLine(
4906
+ id,
4907
+ -32602,
4908
+ `Invalid params: ${CAPABILITIES_KEY} is required`
4909
+ );
4910
+ }
4911
+ const answer = (result) => JSON.stringify({
4912
+ jsonrpc: "2.0",
4913
+ id,
4914
+ result: modern ? {
4915
+ resultType: "complete",
4916
+ ...result,
4917
+ _meta: { [SERVER_INFO_KEY]: serverInfo }
4918
+ } : result
4919
+ });
4920
+ switch (method) {
4921
+ case "server/discover":
4922
+ return answer({
4923
+ supportedVersions: SUPPORTED,
4924
+ capabilities: { tools: {} }
4925
+ });
4926
+ case "initialize": {
4927
+ const asked = params["protocolVersion"];
4928
+ const protocolVersion = typeof asked === "string" && LEGACY_VERSIONS.includes(asked) ? asked : LEGACY_VERSIONS[0];
4929
+ return JSON.stringify({
4930
+ jsonrpc: "2.0",
4931
+ id,
4932
+ result: {
4933
+ protocolVersion,
4934
+ capabilities: { tools: {} },
4935
+ serverInfo
4936
+ }
4937
+ });
4938
+ }
4939
+ case "ping":
4940
+ return answer({});
4941
+ case "tools/list":
4942
+ return answer({
4943
+ tools: opts.tools.map((t) => ({
4944
+ name: t.name,
4945
+ title: t.title,
4946
+ description: t.description,
4947
+ inputSchema: t.inputSchema
4948
+ }))
4949
+ });
4950
+ case "tools/call": {
4951
+ const name = params["name"];
4952
+ const tool = opts.tools.find((t) => t.name === name);
4953
+ if (tool === void 0)
4954
+ return errorLine(id, -32602, `Unknown tool: ${String(name)}`);
4955
+ const args = isObject2(params["arguments"]) ? params["arguments"] : {};
4956
+ try {
4957
+ const r = await tool.call(args);
4958
+ return answer({
4959
+ content: [{ type: "text", text: r.text }],
4960
+ isError: r.isError === true
4961
+ });
4962
+ } catch (e) {
4963
+ return answer({
4964
+ content: [
4965
+ {
4966
+ type: "text",
4967
+ text: `${tool.name} failed: ${e instanceof Error ? e.message : String(e)}`
4968
+ }
4969
+ ],
4970
+ isError: true
4971
+ });
4972
+ }
4973
+ }
4974
+ default:
4975
+ return errorLine(id, -32601, "Method not found");
4976
+ }
4977
+ }
4978
+ };
4979
+ }
4980
+
4981
+ // src/mcp/tools.ts
4982
+ import { readFile as readFile10, realpath as realpath3, stat as stat4 } from "fs/promises";
4983
+ import { isAbsolute, join as join23, posix as posix4, relative as relative6, resolve as resolve6, sep as sep4 } from "path";
4984
+ var LABELS = {
4985
+ standards: "Standards",
4986
+ adr: "Decisions",
4987
+ specs: "Specifications",
4988
+ plans: "Plans",
4989
+ skills: "Skills"
4990
+ };
4991
+ var TEMPLATE = "adr/0000-template.md";
4992
+ var READ_LIMIT = 256 * 1024;
4993
+ var SEARCH_LIMIT = 20;
4994
+ var LINES_PER_HIT = 3;
4995
+ var LINE_WIDTH = 200;
4996
+ var refuse = (text) => ({ text, isError: true });
4997
+ async function guard(run3) {
4998
+ try {
4999
+ return await run3();
5000
+ } catch (e) {
5001
+ return refuse(e instanceof Error ? e.message : String(e));
5002
+ }
5003
+ }
5004
+ async function workspaceAt(cwd) {
5005
+ const ws = await findWorkspace(cwd);
5006
+ return { ws, manifest: await loadManifest(ws.rnessDir) };
5007
+ }
5008
+ function titleOf(heading, id) {
5009
+ if (id === null) return heading;
5010
+ const rest = new RegExp(`^${id}\\s*[\u2014\u2013:-]\\s*(.+)$`).exec(heading)?.[1];
5011
+ return rest ?? heading;
5012
+ }
5013
+ async function documents(rnessDir, only) {
5014
+ const docs = [];
5015
+ for (const collection of COLLECTIONS) {
5016
+ if (only !== void 0 && collection !== only) continue;
5017
+ for (const item of await collectMarkdown(join23(rnessDir, collection))) {
5018
+ const path = `${collection}/${item.rel}`;
5019
+ if (path === TEMPLATE) continue;
5020
+ const status2 = item.fields?.["status"];
5021
+ const id = /^(\d{4})-/.exec(posix4.basename(item.rel))?.[1] ?? null;
5022
+ docs.push({
5023
+ collection,
5024
+ path,
5025
+ scope: ownerScope(item.rel),
5026
+ id,
5027
+ status: typeof status2 === "string" ? status2 : null,
5028
+ title: titleOf(item.title ?? posix4.basename(item.rel, ".md"), id),
5029
+ text: item.body
5030
+ });
5031
+ }
5032
+ }
5033
+ return docs;
5034
+ }
5035
+ function line(d, withScope = false) {
5036
+ const head = [d.id, d.status].filter((x) => x !== null).join(" ");
5037
+ return `- ${head === "" ? "" : `${head} \u2014 `}${d.title}${withScope ? ` \u2014 ${d.scope ?? "global"}` : ""} \u2014 ${d.path}`;
5038
+ }
5039
+ function isCollection(value) {
5040
+ return COLLECTIONS.includes(value);
5041
+ }
5042
+ var collectionError = (value) => refuse(
5043
+ `unknown collection ${JSON.stringify(value)}; one of ${COLLECTIONS.join(", ")}`
5044
+ );
5045
+ async function context(cwd, args) {
5046
+ const { ws, manifest } = await workspaceAt(cwd);
5047
+ let scope;
5048
+ if (args["scope"] !== void 0) {
5049
+ const known = Object.keys(manifest.scopes).sort();
5050
+ if (typeof args["scope"] !== "string" || !Object.hasOwn(manifest.scopes, args["scope"]))
5051
+ return refuse(
5052
+ `unknown scope ${JSON.stringify(args["scope"])}; known: ${known.length === 0 ? "none" : known.join(", ")}`
5053
+ );
5054
+ scope = args["scope"];
5055
+ } else
5056
+ scope = resolveScope(
5057
+ manifest,
5058
+ relative6(ws.root, cwd).split(sep4).join(posix4.sep)
5059
+ );
5060
+ const entry = scope === null ? void 0 : manifest.scopes[scope];
5061
+ const where = scope === null || entry === void 0 ? "global scope" : `scope ${scope} (${entry.path}${entry.extends.length > 0 ? `, extends ${entry.extends.join(", ")}` : ""})`;
5062
+ const ctx = await assembleContext({ rnessDir: ws.rnessDir, manifest, scope });
5063
+ const index = new Map(
5064
+ (await documents(ws.rnessDir)).map((d) => [d.path, d])
5065
+ );
5066
+ const out = [`Workspace ${workspaceName(manifest, ws.root)} \xB7 ${where}`];
5067
+ for (const collection of ctx.collections) {
5068
+ const docs = collection.files.map((f) => index.get(`${collection.name}/${f.rel}`)).filter((d) => d !== void 0);
5069
+ if (docs.length === 0) continue;
5070
+ out.push(`${LABELS[collection.name]} (${docs.length}):`);
5071
+ for (const d of docs) out.push(line(d));
5072
+ }
5073
+ out.push("Read one with rness_read; search with rness_search.");
5074
+ return { text: out.join("\n") };
5075
+ }
5076
+ async function list(cwd, args) {
5077
+ const { ws } = await workspaceAt(cwd);
5078
+ const collection = args["collection"];
5079
+ if (!isCollection(collection)) return collectionError(collection);
5080
+ const status2 = args["status"];
5081
+ const allowed = STATUSES[collection];
5082
+ if (status2 !== void 0) {
5083
+ if (allowed === void 0)
5084
+ return refuse(`${collection} have no status to filter on`);
5085
+ if (typeof status2 !== "string" || !allowed.includes(status2))
5086
+ return refuse(
5087
+ `unknown status ${JSON.stringify(status2)} for ${collection}; one of ${allowed.join(", ")}`
5088
+ );
5089
+ }
5090
+ const docs = (await documents(ws.rnessDir, collection)).filter(
5091
+ (d) => status2 === void 0 || d.status === status2
5092
+ );
5093
+ if (docs.length === 0)
5094
+ return {
5095
+ text: `No document in ${collection}${status2 === void 0 ? "" : ` with status ${String(status2)}`}.`
5096
+ };
5097
+ return {
5098
+ text: [
5099
+ `${LABELS[collection]} (${docs.length}):`,
5100
+ ...docs.map((d) => line(d, true))
5101
+ ].join("\n")
5102
+ };
5103
+ }
5104
+ async function read(cwd, args) {
5105
+ const { ws } = await workspaceAt(cwd);
5106
+ const path = args["path"];
5107
+ if (typeof path !== "string" || path.trim() === "")
5108
+ return refuse("path is required, relative to .rness/");
5109
+ const segments = path.split(/[\\/]/);
5110
+ if (isAbsolute(path) || segments.includes("..") || segments[0] === ".git" || segments.includes("node_modules"))
5111
+ return refuse(
5112
+ `${path}: not a document of .rness/ (a relative path, no "..", nothing under .git/ or node_modules/)`
5113
+ );
5114
+ const root = await realpath3(ws.rnessDir);
5115
+ let real;
5116
+ try {
5117
+ real = await realpath3(resolve6(ws.rnessDir, path));
5118
+ } catch {
5119
+ return refuse(`${path}: no such file in .rness/`);
5120
+ }
5121
+ if (!real.startsWith(`${root}${sep4}`))
5122
+ return refuse(`${path}: leads outside .rness/`);
5123
+ const info = await stat4(real);
5124
+ if (!info.isFile()) return refuse(`${path}: not a file`);
5125
+ if (info.size > READ_LIMIT)
5126
+ return refuse(
5127
+ `${path}: ${info.size} bytes, over the 256 KiB rness_read returns`
5128
+ );
5129
+ return { text: await readFile10(real, "utf8") };
5130
+ }
5131
+ async function search(cwd, args) {
5132
+ const { ws } = await workspaceAt(cwd);
5133
+ const query = args["query"];
5134
+ if (typeof query !== "string" || query.trim() === "")
5135
+ return refuse("query is required");
5136
+ const collection = args["collection"];
5137
+ if (collection !== void 0 && !isCollection(collection))
5138
+ return collectionError(collection);
5139
+ const needle = query.toLowerCase();
5140
+ const hits = (await documents(ws.rnessDir, collection)).map((d) => ({
5141
+ d,
5142
+ lines: d.text.split(/\r?\n/).map((text, i) => ({ n: i + 1, text })).filter((l) => l.text.toLowerCase().includes(needle))
5143
+ })).filter((h) => h.lines.length > 0).sort(
5144
+ (a, b) => b.lines.length - a.lines.length || a.d.path.localeCompare(b.d.path)
5145
+ );
5146
+ if (hits.length === 0)
5147
+ return { text: `No document matches ${JSON.stringify(query)}.` };
5148
+ const shown = hits.slice(0, SEARCH_LIMIT);
5149
+ const out = [
5150
+ `${hits.length} document${hits.length === 1 ? "" : "s"} match${hits.length === 1 ? "es" : ""} ${JSON.stringify(query)}${hits.length > SEARCH_LIMIT ? ` (the ${SEARCH_LIMIT} most matching)` : ""}`
5151
+ ];
5152
+ for (const h of shown) {
5153
+ out.push(line(h.d, true));
5154
+ for (const l of h.lines.slice(0, LINES_PER_HIT))
5155
+ out.push(` ${l.n}: ${l.text.trim().slice(0, LINE_WIDTH)}`);
5156
+ }
5157
+ return { text: out.join("\n") };
5158
+ }
5159
+ var collectionProperty = {
5160
+ type: "string",
5161
+ enum: [...COLLECTIONS],
5162
+ description: "standards (rules), adr (decisions), specs (specifications), plans, skills"
5163
+ };
5164
+ function mcpTools(cwd) {
5165
+ return [
5166
+ {
5167
+ name: "rness_context",
5168
+ title: "rness: what applies here",
5169
+ 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.",
5170
+ inputSchema: {
5171
+ type: "object",
5172
+ properties: {
5173
+ scope: {
5174
+ type: "string",
5175
+ description: "A scope of rness.json; default: the one owning the working directory"
5176
+ }
5177
+ },
5178
+ additionalProperties: false
5179
+ },
5180
+ call: (args) => guard(() => context(cwd, args))
5181
+ },
5182
+ {
5183
+ name: "rness_list",
5184
+ title: "rness: a collection",
5185
+ 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).",
5186
+ inputSchema: {
5187
+ type: "object",
5188
+ properties: {
5189
+ collection: collectionProperty,
5190
+ status: {
5191
+ type: "string",
5192
+ description: "adr: Proposed, Accepted, Rejected, Superseded; specs: Draft, Proposed, Approved, Implemented, Superseded, Rejected; plans: Draft, Ready, In progress, Blocked, Completed, Abandoned"
5193
+ }
5194
+ },
5195
+ required: ["collection"],
5196
+ additionalProperties: false
5197
+ },
5198
+ call: (args) => guard(() => list(cwd, args))
5199
+ },
5200
+ {
5201
+ name: "rness_read",
5202
+ title: "rness: read a document",
5203
+ 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/.",
5204
+ inputSchema: {
5205
+ type: "object",
5206
+ properties: {
5207
+ path: {
5208
+ type: "string",
5209
+ description: "For example adr/0006-workspace.md"
5210
+ }
5211
+ },
5212
+ required: ["path"],
5213
+ additionalProperties: false
5214
+ },
5215
+ call: (args) => guard(() => read(cwd, args))
5216
+ },
5217
+ {
5218
+ name: "rness_search",
5219
+ title: "rness: search the context",
5220
+ 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.",
5221
+ inputSchema: {
5222
+ type: "object",
5223
+ properties: {
5224
+ query: { type: "string", description: "Text to find" },
5225
+ collection: collectionProperty
5226
+ },
5227
+ required: ["query"],
5228
+ additionalProperties: false
5229
+ },
5230
+ call: (args) => guard(() => search(cwd, args))
5231
+ }
5232
+ ];
5233
+ }
5234
+
5235
+ // src/commands/mcp.ts
5236
+ async function mcpCommand(opts, io = {
5237
+ input: process.stdin,
5238
+ output: process.stdout
5239
+ }) {
5240
+ const protocol = createProtocol({
5241
+ name: "rness",
5242
+ version: VERSION,
5243
+ tools: mcpTools(opts.cwd ?? process.cwd())
5244
+ });
5245
+ const lines = createInterface({ input: io.input, crlfDelay: Infinity });
5246
+ for await (const line2 of lines) {
5247
+ if (line2.trim() === "") continue;
5248
+ const answer = await protocol.handle(line2);
5249
+ if (answer !== null) io.output.write(`${answer}
5250
+ `);
5251
+ }
5252
+ return 0;
5253
+ }
5254
+
4731
5255
  // src/cli.ts
4732
5256
  function buildProgram(state) {
4733
5257
  const program = new Command().name("rness").description("Configuration-plane CLI for rness workspaces").version(VERSION, "-v, --version", "print the version").exitOverride().showHelpAfterError().configureOutput({
@@ -4772,6 +5296,11 @@ function buildProgram(state) {
4772
5296
  ).option("-y, --yes", "do not ask for confirmation").addOption(new Option("--cwd <dir>").hideHelp()).action(async (opts) => {
4773
5297
  state.code = await syncCommand(opts);
4774
5298
  });
5299
+ program.command("mcp").description(
5300
+ "Serve the workspace context to agents over MCP (stdio, read-only)"
5301
+ ).addOption(new Option("--cwd <dir>").hideHelp()).action(async (opts) => {
5302
+ state.code = await mcpCommand(opts);
5303
+ });
4775
5304
  program.command("add").description(
4776
5305
  "Clone (or adopt) a repository under org/ and declare it in rness.json"
4777
5306
  ).argument("<repo>", "<repo>, <owner>/<repo>, or a clone URL").option(
@@ -4965,13 +5494,13 @@ export {
4965
5494
  loginCommand,
4966
5495
  createCommand,
4967
5496
  logoutCommand,
4968
- upgradeCommand,
4969
- checkBlocks,
4970
5497
  STATUSES,
4971
5498
  checkContract,
5499
+ upgradeCommand,
5500
+ checkBlocks,
4972
5501
  validateCommand,
4973
5502
  run2 as run,
4974
5503
  OWN_COMMANDS,
4975
5504
  catchUp
4976
5505
  };
4977
- //# sourceMappingURL=chunk-ONLF4VFA.js.map
5506
+ //# sourceMappingURL=chunk-N3F6N6GA.js.map