@rness/cli 0.9.2 → 0.11.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.
@@ -558,8 +558,8 @@ async function getOrgMembership(org, options) {
558
558
  }
559
559
  if (status2 === 404) return "not-member";
560
560
  if (status2 === 403) {
561
- const message = body !== null && typeof body === "object" ? String(body["message"] ?? "") : "";
562
- if (/OAuth App access restrictions/i.test(message)) return "restricted";
561
+ const message2 = body !== null && typeof body === "object" ? String(body["message"] ?? "") : "";
562
+ if (/OAuth App access restrictions/i.test(message2)) return "restricted";
563
563
  }
564
564
  return "unknown";
565
565
  }
@@ -581,8 +581,8 @@ async function createRepository(owner, name, options) {
581
581
  throw new Error(`cannot reach the GitHub API: ${reason2(e)}`, { cause: e });
582
582
  }
583
583
  const body = await res.json().catch(() => void 0);
584
- const message = body !== null && typeof body === "object" ? String(body["message"] ?? "") : "";
585
- return { status: res.status, message };
584
+ const message2 = body !== null && typeof body === "object" ? String(body["message"] ?? "") : "";
585
+ return { status: res.status, message: message2 };
586
586
  };
587
587
  let answer = await post2(`/orgs/${encodeURIComponent(owner)}/repos`);
588
588
  if (answer.status === 404 && options.self !== void 0 && options.self !== null && options.self.toLowerCase() === owner.toLowerCase())
@@ -775,9 +775,9 @@ async function originUrl(dir) {
775
775
  async function init(dir) {
776
776
  await git(["init", "-q", "-b", "main"], dir);
777
777
  }
778
- async function commitAll(dir, message) {
778
+ async function commitAll(dir, message2) {
779
779
  await git(["add", "-A"], dir);
780
- await git(["commit", "-q", "-m", message], dir);
780
+ await git(["commit", "-q", "-m", message2], dir);
781
781
  }
782
782
 
783
783
  // src/core/remote.ts
@@ -812,7 +812,7 @@ function classifyProbe(url, exitCode, stderr) {
812
812
  var STDERR_FLUSH_MS = 200;
813
813
  async function probeRemote(url, timeoutMs = 6e4, credentials) {
814
814
  positional(url, "repository url");
815
- return new Promise((resolve7) => {
815
+ return new Promise((resolve8) => {
816
816
  const auth = withCredentials(credentials);
817
817
  const child = spawn(
818
818
  "git",
@@ -831,7 +831,7 @@ async function probeRemote(url, timeoutMs = 6e4, credentials) {
831
831
  settled = true;
832
832
  clearTimeout(timer);
833
833
  child.stderr?.destroy();
834
- resolve7(probe);
834
+ resolve8(probe);
835
835
  };
836
836
  const timer = setTimeout(() => {
837
837
  child.kill("SIGKILL");
@@ -876,8 +876,8 @@ function isWorkspaceClone(name) {
876
876
  var NAME_RULE = "lowercase letters, digits, '.', '_' and '-', not starting with '-'";
877
877
  var ORG_NAME = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
878
878
  var KEYS = ["contract", "org", "agents", "repos", "scopes"];
879
- function fail(message) {
880
- throw new Error(`rness.json: ${message}`);
879
+ function fail(message2) {
880
+ throw new Error(`rness.json: ${message2}`);
881
881
  }
882
882
  function isRecord(value) {
883
883
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -1198,6 +1198,12 @@ function badge(text, tone = "intro", stream = process.stdout) {
1198
1198
  });
1199
1199
  return painted === padded ? text : painted;
1200
1200
  }
1201
+ function inverse(text, stream = process.stdout) {
1202
+ const plain = `[${text.slice(1, -1)}]`;
1203
+ if (noColor()) return plain;
1204
+ const painted = styleText("inverse", text, { stream, validateStream: true });
1205
+ return painted === text ? plain : painted;
1206
+ }
1201
1207
  function grey(text, stream = process.stdout) {
1202
1208
  if (noColor()) return text;
1203
1209
  return styleText("gray", text, { stream, validateStream: true });
@@ -1213,8 +1219,8 @@ function warn(text) {
1213
1219
  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"];
1214
1220
  var FROM = [63, 185, 80];
1215
1221
  var TO = [57, 197, 207];
1216
- function gradient(line2) {
1217
- const chars = [...line2];
1222
+ function gradient(line3) {
1223
+ const chars = [...line3];
1218
1224
  const last = Math.max(1, chars.length - 1);
1219
1225
  const painted = chars.map((ch, i) => {
1220
1226
  if (ch === " ") return ch;
@@ -1231,7 +1237,7 @@ function banner(version, stream = process.stdout) {
1231
1237
  const coloured = paint("done", "x", stream) !== "x";
1232
1238
  const trueColour = coloured && typeof stream.hasColors === "function" && stream.hasColors(2 ** 24);
1233
1239
  const word = WORD.map(
1234
- (line2) => trueColour ? gradient(line2) : paint("done", line2, stream)
1240
+ (line3) => trueColour ? gradient(line3) : paint("done", line3, stream)
1235
1241
  );
1236
1242
  return [
1237
1243
  "",
@@ -1263,7 +1269,7 @@ async function detectGithubSsh(opts) {
1263
1269
  ...opts.interactive ? [] : ["-o", "BatchMode=yes"],
1264
1270
  "git@github.com"
1265
1271
  ];
1266
- return new Promise((resolve7) => {
1272
+ return new Promise((resolve8) => {
1267
1273
  const child = spawn2(opts.command ?? "ssh", args, {
1268
1274
  stdio: [opts.interactive ? "inherit" : "ignore", "ignore", "pipe"]
1269
1275
  });
@@ -1276,7 +1282,7 @@ async function detectGithubSsh(opts) {
1276
1282
  settled = true;
1277
1283
  clearTimeout(timer);
1278
1284
  child.stderr?.destroy();
1279
- resolve7(access2);
1285
+ resolve8(access2);
1280
1286
  };
1281
1287
  const timer = setTimeout(() => {
1282
1288
  child.kill("SIGKILL");
@@ -1397,15 +1403,15 @@ function reportError(e) {
1397
1403
  process.stderr.write(`${e.stack}
1398
1404
  `);
1399
1405
  } else {
1400
- const message = e instanceof Error ? e.message : String(e);
1401
- process.stderr.write(`${paint("error", message, process.stderr)}
1406
+ const message2 = e instanceof Error ? e.message : String(e);
1407
+ process.stderr.write(`${paint("error", message2, process.stderr)}
1402
1408
  `);
1403
1409
  }
1404
1410
  return 1;
1405
1411
  }
1406
1412
 
1407
1413
  // src/version.ts
1408
- var VERSION = "0.9.2";
1414
+ var VERSION = "0.11.0";
1409
1415
 
1410
1416
  // src/core/block.ts
1411
1417
  import { createHash } from "crypto";
@@ -1457,8 +1463,8 @@ ${rules.join("\n\n")}`;
1457
1463
  const text = [BEGIN, header(scope, hash), body, END].join("\n");
1458
1464
  return { text, hash, bytes: Buffer.byteLength(text, "utf8") };
1459
1465
  }
1460
- function parseHeader(line2) {
1461
- const m = HEADER.exec(line2.trimEnd());
1466
+ function parseHeader(line3) {
1467
+ const m = HEADER.exec(line3.trimEnd());
1462
1468
  if (m === null) return null;
1463
1469
  return { version: m[1] ?? null, scope: m[2] ?? "", hash: m[3] ?? "" };
1464
1470
  }
@@ -1473,8 +1479,8 @@ function isCurrentBlock(blockLines, freshHash) {
1473
1479
  // src/core/frontmatter.ts
1474
1480
  import { parse as parse2 } from "yaml";
1475
1481
  var FRONT_MATTER = /^---\r?\n(?:([\s\S]*?)\r?\n)?---(?:\r?\n|$)/;
1476
- function fail2(message) {
1477
- throw new Error(`front matter: ${message}`);
1482
+ function fail2(message2) {
1483
+ throw new Error(`front matter: ${message2}`);
1478
1484
  }
1479
1485
  function stripBom(source) {
1480
1486
  return source.startsWith("\uFEFF") ? source.slice(1) : source;
@@ -1629,8 +1635,8 @@ import { join as join8 } from "path";
1629
1635
  function findBlock(lines) {
1630
1636
  const begins = [];
1631
1637
  const ends = [];
1632
- lines.forEach((line2, i) => {
1633
- const l = line2.trimEnd();
1638
+ lines.forEach((line3, i) => {
1639
+ const l = line3.trimEnd();
1634
1640
  if (l === BEGIN) begins.push(i);
1635
1641
  if (l === END) ends.push(i);
1636
1642
  });
@@ -1706,6 +1712,88 @@ import { mkdir as mkdir2 } from "fs/promises";
1706
1712
  import { dirname as dirname4, join as join9 } from "path";
1707
1713
 
1708
1714
  // src/core/agents.ts
1715
+ var PINNED_BIN = "node_modules/@rness/cli/dist/bin/rness.js";
1716
+ function hookLine(rness, event) {
1717
+ const run3 = `f="$CLAUDE_PROJECT_DIR/${rness}/${PINNED_BIN}"; if [ -f "$f" ]; then node "$f" hook ${event};`;
1718
+ if (event === "post-tool-use") return `${run3} fi`;
1719
+ const missing = JSON.stringify({
1720
+ systemMessage: `rness: ${rness} is not installed, so the workspace context is not loaded. Clone the workspace, then install its dependencies in .rness.`
1721
+ });
1722
+ return `${run3} else echo '${missing}'; fi`;
1723
+ }
1724
+ function hooks(rness) {
1725
+ return [
1726
+ {
1727
+ path: ["hooks", "SessionStart"],
1728
+ contains: {
1729
+ hooks: [
1730
+ {
1731
+ type: "command",
1732
+ command: hookLine(rness, "session-start"),
1733
+ timeout: 10
1734
+ }
1735
+ ]
1736
+ },
1737
+ label: "the rness session-start hook"
1738
+ },
1739
+ {
1740
+ path: ["hooks", "PostToolUse"],
1741
+ contains: {
1742
+ matcher: "Edit|Write",
1743
+ hooks: [
1744
+ {
1745
+ type: "command",
1746
+ command: hookLine(rness, "post-tool-use"),
1747
+ timeout: 10
1748
+ }
1749
+ ]
1750
+ },
1751
+ label: "the rness post-tool-use hook"
1752
+ }
1753
+ ];
1754
+ }
1755
+ var PLUGIN = ".claude/skills/rness";
1756
+ var PLUGIN_JSON = `${JSON.stringify(
1757
+ {
1758
+ name: "rness",
1759
+ description: "The rness workspace in Claude Code: /rness:status."
1760
+ },
1761
+ null,
1762
+ 2
1763
+ )}
1764
+ `;
1765
+ function statusSkill(rness) {
1766
+ const command = `node "\${CLAUDE_PROJECT_DIR}/${rness}/${PINNED_BIN}" status --cwd "\${CLAUDE_PROJECT_DIR}"`;
1767
+ return `---
1768
+ name: status
1769
+ description: The status of every decision, specification, plan and other tracked document of the rness workspace, one table per directory. Read-only.
1770
+ argument-hint: '[tab]'
1771
+ disable-model-invocation: true
1772
+ allowed-tools: Bash(${command})
1773
+ ---
1774
+
1775
+ !\`${command}\`
1776
+
1777
+ Show the output above to the user as it is: the heading and the tables,
1778
+ nothing added, nothing summarised, no other tool. Arguments: \`$ARGUMENTS\`.
1779
+ When they name a tab, show only that tab's section. If the output says the
1780
+ module cannot be found, say instead that ${rness} is not installed next to
1781
+ this repository.
1782
+
1783
+ End with this line: _For the view with tabs and scrolling: Ctrl+Z, then
1784
+ \`npx @rness/cli status\` (q to close), then \`fg\`._
1785
+ `;
1786
+ }
1787
+ function plugin(rness, at) {
1788
+ return [
1789
+ { file: `${PLUGIN}/.claude-plugin/plugin.json`, at, content: PLUGIN_JSON },
1790
+ {
1791
+ file: `${PLUGIN}/skills/status/SKILL.md`,
1792
+ at,
1793
+ content: statusSkill(rness)
1794
+ }
1795
+ ];
1796
+ }
1709
1797
  var TARGETS = {
1710
1798
  claude: {
1711
1799
  name: "claude",
@@ -1713,6 +1801,7 @@ var TARGETS = {
1713
1801
  files: [
1714
1802
  {
1715
1803
  file: ".claude/settings.json",
1804
+ at: "clones",
1716
1805
  guarantees: [
1717
1806
  // Read the context repository from a repository without a prompt.
1718
1807
  // Resolved against the project directory, and applied once Claude
@@ -1720,11 +1809,13 @@ var TARGETS = {
1720
1809
  {
1721
1810
  path: ["permissions", "additionalDirectories"],
1722
1811
  contains: "../../.rness"
1723
- }
1812
+ },
1813
+ ...hooks("../../.rness")
1724
1814
  ]
1725
1815
  },
1726
1816
  {
1727
1817
  file: ".mcp.json",
1818
+ at: "clones",
1728
1819
  guarantees: [
1729
1820
  // The pinned copy, by a path relative to the repository — the
1730
1821
  // directory Claude Code starts the server in (spec 0014 §6). Not
@@ -1742,7 +1833,16 @@ var TARGETS = {
1742
1833
  }
1743
1834
  }
1744
1835
  ]
1745
- }
1836
+ },
1837
+ ...plugin("../../.rness", "clones"),
1838
+ // A session opened at the workspace root reads this file, one opened
1839
+ // in a clone does not (spec 0011 §1): the hooks only, `.rness` inside.
1840
+ {
1841
+ file: ".claude/settings.json",
1842
+ at: "root",
1843
+ guarantees: hooks(".rness")
1844
+ },
1845
+ ...plugin(".rness", "root")
1746
1846
  ]
1747
1847
  }
1748
1848
  };
@@ -1755,6 +1855,7 @@ function unsupportedMessage(name) {
1755
1855
  }
1756
1856
 
1757
1857
  // src/core/json-guarantees.ts
1858
+ import { isDeepStrictEqual } from "util";
1758
1859
  function isObject(value) {
1759
1860
  return value !== null && typeof value === "object" && !Array.isArray(value);
1760
1861
  }
@@ -1798,14 +1899,15 @@ function apply(root, g, create) {
1798
1899
  }
1799
1900
  if (value === void 0) {
1800
1901
  if (!create) return { holds: false, changed: false };
1801
- parent[last] = [g.contains];
1902
+ parent[last] = [structuredClone(g.contains)];
1802
1903
  return { holds: true, changed: true };
1803
1904
  }
1804
1905
  if (!Array.isArray(value))
1805
1906
  return { invalid: `${g.path.join(".")} is not a list` };
1806
- if (value.includes(g.contains)) return { holds: true, changed: false };
1907
+ if (value.some((entry) => isDeepStrictEqual(entry, g.contains)))
1908
+ return { holds: true, changed: false };
1807
1909
  if (!create) return { holds: false, changed: false };
1808
- value.push(g.contains);
1910
+ value.push(structuredClone(g.contains));
1809
1911
  return { holds: true, changed: true };
1810
1912
  }
1811
1913
  function styleOf(text) {
@@ -1849,21 +1951,37 @@ function missingGuarantees(existing, guarantees) {
1849
1951
  return missing;
1850
1952
  }
1851
1953
  function describeGuarantee(g) {
1852
- return "value" in g ? `${g.path.join(".")} is missing` : `${g.path.join(".")} lacks ${g.contains}`;
1954
+ if ("value" in g) return `${g.path.join(".")} is missing`;
1955
+ const what = g.label ?? (typeof g.contains === "string" ? g.contains : JSON.stringify(g.contains));
1956
+ return `${g.path.join(".")} lacks ${what}`;
1853
1957
  }
1854
1958
 
1855
1959
  // src/core/agent-targets.ts
1960
+ function wholeDiff(existing, content) {
1961
+ if (existing === content) return null;
1962
+ return existing === null ? "missing" : "differs from what rness writes";
1963
+ }
1856
1964
  async function filesOf(root, manifest, targets) {
1857
1965
  const files = [];
1966
+ if (!await exists(join9(root, "org"))) return files;
1967
+ for (const target of targets)
1968
+ for (const owned of target.files)
1969
+ if (owned.at === "root")
1970
+ files.push({
1971
+ ...owned,
1972
+ file: join9(root, ...owned.file.split("/")),
1973
+ label: owned.file
1974
+ });
1858
1975
  for (const repo of Object.keys(manifest.repos)) {
1859
1976
  if (!await exists(join9(root, "org", repo))) continue;
1860
1977
  for (const target of targets)
1861
1978
  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
- });
1979
+ if (owned.at === "clones")
1980
+ files.push({
1981
+ ...owned,
1982
+ file: join9(root, "org", repo, ...owned.file.split("/")),
1983
+ label: `org/${repo}/${owned.file}`
1984
+ });
1867
1985
  }
1868
1986
  return files;
1869
1987
  }
@@ -1876,17 +1994,30 @@ var BLOCK_FILES = ["AGENTS.md", "CLAUDE.md"];
1876
1994
  function writtenFiles(manifest) {
1877
1995
  return [
1878
1996
  ...BLOCK_FILES,
1879
- ...declared(manifest).flatMap((t) => t.files.map((f) => f.file))
1997
+ ...declared(manifest).flatMap(
1998
+ (t) => t.files.filter((f) => f.at === "clones").map((f) => f.file)
1999
+ )
1880
2000
  ];
1881
2001
  }
1882
2002
  async function agentTargets(root, manifest, opts) {
1883
2003
  const outcomes = [];
1884
- for (const { guarantees, file, label } of await filesOf(
1885
- root,
1886
- manifest,
1887
- declared(manifest)
1888
- )) {
2004
+ for (const owned of await filesOf(root, manifest, declared(manifest))) {
2005
+ const { file, label } = owned;
1889
2006
  const existing = await readOrNull(file);
2007
+ if ("content" in owned) {
2008
+ const diff = wholeDiff(existing, owned.content);
2009
+ if (diff === null)
2010
+ outcomes.push({ label, status: "unchanged", detail: null });
2011
+ else if (opts.check)
2012
+ outcomes.push({ label, status: "stale", detail: diff });
2013
+ else {
2014
+ await mkdir2(dirname4(file), { recursive: true });
2015
+ await writeFileAtomic(file, owned.content);
2016
+ outcomes.push({ label, status: "updated", detail: null });
2017
+ }
2018
+ continue;
2019
+ }
2020
+ const { guarantees } = owned;
1890
2021
  if (opts.check) {
1891
2022
  const missing = missingGuarantees(existing, guarantees);
1892
2023
  if ("invalid" in missing)
@@ -1922,15 +2053,16 @@ async function leftoverTargets(root, manifest) {
1922
2053
  (t) => !agents.includes(t.name)
1923
2054
  );
1924
2055
  const leftovers = [];
1925
- for (const { guarantees, file, label } of await filesOf(
1926
- root,
1927
- manifest,
1928
- undeclared
1929
- )) {
1930
- const existing = await readOrNull(file);
2056
+ for (const owned of await filesOf(root, manifest, undeclared)) {
2057
+ const existing = await readOrNull(owned.file);
1931
2058
  if (existing === null) continue;
1932
- const missing = missingGuarantees(existing, guarantees);
1933
- if (!("invalid" in missing) && missing.length === 0) leftovers.push(label);
2059
+ if ("content" in owned) {
2060
+ if (existing === owned.content) leftovers.push(owned.label);
2061
+ continue;
2062
+ }
2063
+ const missing = missingGuarantees(existing, owned.guarantees);
2064
+ if (!("invalid" in missing) && missing.length === 0)
2065
+ leftovers.push(owned.label);
1934
2066
  }
1935
2067
  return leftovers;
1936
2068
  }
@@ -2040,8 +2172,8 @@ var plainUi = {
2040
2172
  { label: options.doing, animate: options.git !== true },
2041
2173
  work
2042
2174
  );
2043
- const line2 = options.quiet === true ? null : done?.(result) ?? null;
2044
- if (line2 !== null) this.line(line2[0], line2[1]);
2175
+ const line3 = options.quiet === true ? null : done?.(result) ?? null;
2176
+ if (line3 !== null) this.line(line3[0], line3[1]);
2045
2177
  return result;
2046
2178
  }
2047
2179
  };
@@ -2064,8 +2196,8 @@ function sessionUi(p) {
2064
2196
  intro: (title) => {
2065
2197
  p.intro(badge(title));
2066
2198
  },
2067
- outro: (message, tone = "done") => {
2068
- p.outro(badge(message, tone));
2199
+ outro: (message2, tone = "done") => {
2200
+ p.outro(badge(message2, tone));
2069
2201
  },
2070
2202
  line: (verb, rest, sentence) => {
2071
2203
  say(verb, sentence ?? `${rest} ${verb}`);
@@ -2074,7 +2206,7 @@ function sessionUi(p) {
2074
2206
  p.log.warn(text);
2075
2207
  },
2076
2208
  note: (title, lines) => {
2077
- p.note(lines.join("\n"), title, { format: (line2) => grey(line2) });
2209
+ p.note(lines.join("\n"), title, { format: (line3) => grey(line3) });
2078
2210
  },
2079
2211
  hint: (text) => {
2080
2212
  p.log.message(paint("idle", text));
@@ -2088,20 +2220,20 @@ function sessionUi(p) {
2088
2220
  async step(options, work, done) {
2089
2221
  const label = options.sentence ?? tidy(options.doing);
2090
2222
  const finish2 = (result) => done?.(result) ?? null;
2091
- const label_ = (line2) => line2[2] ?? `${line2[1]} ${line2[0]}`;
2223
+ const label_ = (line3) => line3[2] ?? `${line3[1]} ${line3[0]}`;
2092
2224
  if (options.git === true && !this.gitIsSilent) {
2093
2225
  p.log.message(paint("idle", `${label}\u2026`));
2094
2226
  const result = await work();
2095
- const line2 = finish2(result);
2096
- if (line2 !== null) say(line2[0], label_(line2));
2227
+ const line3 = finish2(result);
2228
+ if (line3 !== null) say(line3[0], label_(line3));
2097
2229
  return result;
2098
2230
  }
2099
2231
  const spin = p.spinner();
2100
2232
  spin.start(label);
2101
2233
  try {
2102
2234
  const result = await work();
2103
- const line2 = finish2(result);
2104
- spin.stop(line2 === null ? label : label_(line2));
2235
+ const line3 = finish2(result);
2236
+ spin.stop(line3 === null ? label : label_(line3));
2105
2237
  return result;
2106
2238
  } catch (e) {
2107
2239
  spin.error(`${label} \u2014 failed`);
@@ -3119,11 +3251,11 @@ function summariseSync(stdout) {
3119
3251
  const blocks = /* @__PURE__ */ new Map();
3120
3252
  const agentFiles = /* @__PURE__ */ new Map();
3121
3253
  const others = [];
3122
- for (const line2 of stdout.split("\n")) {
3123
- if (line2.trim() === "") continue;
3124
- const m = /^(updated|unchanged|stale)\s+(\S+)/.exec(line2);
3254
+ for (const line3 of stdout.split("\n")) {
3255
+ if (line3.trim() === "") continue;
3256
+ const m = /^(updated|unchanged|stale)\s+(\S+)/.exec(line3);
3125
3257
  if (m === null) {
3126
- others.push(line2);
3258
+ others.push(line3);
3127
3259
  continue;
3128
3260
  }
3129
3261
  const [, verb = "", path = ""] = m;
@@ -3240,13 +3372,13 @@ function scaffoldMessage(subject, version) {
3240
3372
 
3241
3373
  ${SCAFFOLD_TRAILER}: ${version}`;
3242
3374
  }
3243
- async function commitUpgrade(rnessDir, message) {
3375
+ async function commitUpgrade(rnessDir, message2) {
3244
3376
  const add = await run(rnessDir, ["add", "-A"]);
3245
3377
  if (add.code !== 0) return { kind: "refused", output: add.stderr.trim() };
3246
3378
  const staged = await run(rnessDir, ["diff", "--cached", "--quiet"]);
3247
3379
  if (staged.code === 0 && !await mergeInProgress(rnessDir))
3248
3380
  return { kind: "nothing" };
3249
- const commit = await run(rnessDir, ["commit", "-q", "-m", message]);
3381
+ const commit = await run(rnessDir, ["commit", "-q", "-m", message2]);
3250
3382
  if (commit.code !== 0)
3251
3383
  return {
3252
3384
  kind: "refused",
@@ -3353,8 +3485,8 @@ async function mergeScaffold(rnessDir, commit, base) {
3353
3485
  "--name-status",
3354
3486
  "HEAD"
3355
3487
  ]);
3356
- for (const line2 of status2.split("\n")) {
3357
- const [letter = "", path = ""] = line2.split(" ");
3488
+ for (const line3 of status2.split("\n")) {
3489
+ const [letter = "", path = ""] = line3.split(" ");
3358
3490
  if (path === "" || conflicts.includes(path)) continue;
3359
3491
  if (letter === "A") changes.push(["added", path]);
3360
3492
  else if (letter === "D") changes.push(["removed", path]);
@@ -3500,8 +3632,8 @@ async function syncBlocks(ui, root, shown, inPlace, sshWorks) {
3500
3632
  return 1;
3501
3633
  }
3502
3634
  const outcome = syncOutcome(result, { sshWorks: sshWorks === true });
3503
- for (const line2 of outcome.hints) ui.hint(line2);
3504
- for (const line2 of outcome.errors) ui.error(line2);
3635
+ for (const line3 of outcome.hints) ui.hint(line3);
3636
+ for (const line3 of outcome.errors) ui.error(line3);
3505
3637
  return outcome.code;
3506
3638
  }
3507
3639
 
@@ -3606,7 +3738,7 @@ async function createBlank(input) {
3606
3738
  `Workspace \`${name}\` is ready in ${shown}/.`,
3607
3739
  "",
3608
3740
  "Next:",
3609
- ...next.map((line2) => ` ${line2}`),
3741
+ ...next.map((line3) => ` ${line3}`),
3610
3742
  ""
3611
3743
  ].join("\n")
3612
3744
  );
@@ -3767,9 +3899,9 @@ async function cloneSelection(input) {
3767
3899
  manifest = result.manifest;
3768
3900
  ui.line("declared", `scope ${result.name} (org/${result.name})`);
3769
3901
  } catch (e) {
3770
- const message = firstLine4(e instanceof Error ? e.message : String(e));
3771
- ui.error(`${spec}: ${message}`);
3772
- failures.push({ spec, message });
3902
+ const message2 = firstLine4(e instanceof Error ? e.message : String(e));
3903
+ ui.error(`${spec}: ${message2}`);
3904
+ failures.push({ spec, message: message2 });
3773
3905
  }
3774
3906
  }
3775
3907
  return failures;
@@ -4249,7 +4381,7 @@ ${next2.map((l) => ` ${l}`).join("\n")}
4249
4381
  `Workspace for \`${org}\` is ready in ${shown}/.`,
4250
4382
  "",
4251
4383
  "Next:",
4252
- ...next.map((line2) => ` ${line2}`),
4384
+ ...next.map((line3) => ` ${line3}`),
4253
4385
  ""
4254
4386
  ].join("\n")
4255
4387
  );
@@ -4262,36 +4394,89 @@ ${next2.map((l) => ` ${l}`).join("\n")}
4262
4394
  }
4263
4395
  }
4264
4396
 
4265
- // src/commands/logout.ts
4266
- async function logoutCommand() {
4267
- try {
4268
- const stored = await readAuth();
4269
- await clearAuth();
4270
- const undone = await removeGitSetup().catch(() => false);
4271
- if (stored === null && !undone) {
4272
- process.stdout.write("not logged in\n");
4273
- return 0;
4397
+ // src/core/blocks.ts
4398
+ import { join as join20 } from "path";
4399
+ var PROBLEM = /* @__PURE__ */ new Set(["stale", "malformed"]);
4400
+ async function checkBlocks({
4401
+ root,
4402
+ rnessDir,
4403
+ manifest,
4404
+ org
4405
+ }) {
4406
+ const checks = [];
4407
+ const note = (label, status2, message2 = null) => {
4408
+ checks.push({ label, status: status2, message: message2 });
4409
+ };
4410
+ const derive = () => ({
4411
+ problems: checks.filter((c) => PROBLEM.has(c.status) && c.message !== null).map((c) => c.message),
4412
+ warnings: checks.filter((c) => !PROBLEM.has(c.status) && c.message !== null).map((c) => c.message),
4413
+ checks
4414
+ });
4415
+ if (!await exists(join20(root, "org"))) return derive();
4416
+ const targets = [
4417
+ { scope: null, dir: root, depth: 0, file: "AGENTS.md", label: "AGENTS.md" },
4418
+ { scope: null, dir: root, depth: 0, file: "CLAUDE.md", label: "CLAUDE.md" }
4419
+ ];
4420
+ for (const [name, entry] of Object.entries(manifest.scopes)) {
4421
+ const segments = entry.path.split("/");
4422
+ targets.push({
4423
+ scope: name,
4424
+ dir: join20(root, ...segments),
4425
+ depth: segments.length,
4426
+ file: "AGENTS.md",
4427
+ label: `${entry.path}/AGENTS.md`
4428
+ });
4429
+ }
4430
+ for (const t of targets) {
4431
+ if (!await exists(t.dir)) {
4432
+ note(
4433
+ t.label,
4434
+ "missing-dir",
4435
+ `${t.label}: directory not present, block not checked`
4436
+ );
4437
+ continue;
4274
4438
  }
4275
- if (stored !== null)
4276
- process.stdout.write(
4277
- `${status("logged out", `${stored.login} (github.com)`)}
4278
- `
4439
+ const text = await readOrNull(join20(t.dir, t.file));
4440
+ const lines = text === null ? [] : text.split(/\r?\n/);
4441
+ const found = findBlock(lines);
4442
+ if (found.kind === "none") {
4443
+ note(
4444
+ t.label,
4445
+ "no-block",
4446
+ `${t.label}: no rness block yet (run ${rnessCommand()} sync)`
4279
4447
  );
4280
- if (undone)
4281
- process.stdout.write(
4282
- `${status("updated", "git: rness no longer answers for https://github.com")}
4283
- `
4448
+ continue;
4449
+ }
4450
+ if (found.kind === "error") {
4451
+ note(t.label, "malformed", `${t.label}: ${found.message}`);
4452
+ continue;
4453
+ }
4454
+ const context2 = await assembleContext({
4455
+ rnessDir,
4456
+ manifest,
4457
+ scope: t.scope
4458
+ });
4459
+ const fresh2 = renderBlock({
4460
+ scope: t.scope,
4461
+ org,
4462
+ depth: t.depth,
4463
+ context: context2
4464
+ });
4465
+ const pointerLeft = t.file === "CLAUDE.md" && lines.some((l) => l.trim() === "@AGENTS.md");
4466
+ if (isCurrentBlock(lines.slice(found.begin, found.end + 1), fresh2.hash) && !pointerLeft)
4467
+ note(t.label, "current");
4468
+ else
4469
+ note(
4470
+ t.label,
4471
+ "stale",
4472
+ `${t.label}: stale rness block (run ${rnessCommand()} sync)`
4284
4473
  );
4285
- process.stdout.write(`revoke rness on GitHub: ${revokeUrl()}
4286
- `);
4287
- return 0;
4288
- } catch (e) {
4289
- return reportError(e);
4290
4474
  }
4475
+ return derive();
4291
4476
  }
4292
4477
 
4293
4478
  // src/core/contract.ts
4294
- import { join as join20 } from "path";
4479
+ import { join as join21 } from "path";
4295
4480
  var STATUSES = {
4296
4481
  adr: ["Proposed", "Accepted", "Rejected", "Superseded"],
4297
4482
  specs: [
@@ -4310,7 +4495,7 @@ async function checkContract(rnessDir) {
4310
4495
  const problems = [];
4311
4496
  for (const name of COLLECTIONS) {
4312
4497
  const allowed = STATUSES[name];
4313
- const items = await collectMarkdown(join20(rnessDir, name));
4498
+ const items = await collectMarkdown(join21(rnessDir, name));
4314
4499
  for (const item of items) {
4315
4500
  const where = `${name}/${item.rel}`;
4316
4501
  if (SKIP.has(where)) continue;
@@ -4346,11 +4531,39 @@ async function checkContract(rnessDir) {
4346
4531
  return problems;
4347
4532
  }
4348
4533
 
4534
+ // src/commands/logout.ts
4535
+ async function logoutCommand() {
4536
+ try {
4537
+ const stored = await readAuth();
4538
+ await clearAuth();
4539
+ const undone = await removeGitSetup().catch(() => false);
4540
+ if (stored === null && !undone) {
4541
+ process.stdout.write("not logged in\n");
4542
+ return 0;
4543
+ }
4544
+ if (stored !== null)
4545
+ process.stdout.write(
4546
+ `${status("logged out", `${stored.login} (github.com)`)}
4547
+ `
4548
+ );
4549
+ if (undone)
4550
+ process.stdout.write(
4551
+ `${status("updated", "git: rness no longer answers for https://github.com")}
4552
+ `
4553
+ );
4554
+ process.stdout.write(`revoke rness on GitHub: ${revokeUrl()}
4555
+ `);
4556
+ return 0;
4557
+ } catch (e) {
4558
+ return reportError(e);
4559
+ }
4560
+ }
4561
+
4349
4562
  // src/commands/upgrade.ts
4350
4563
  import { execFile as execFile6 } from "child_process";
4351
4564
  import { mkdtemp as mkdtemp3, readFile as readFile9, rm as rm7 } from "fs/promises";
4352
4565
  import { tmpdir as tmpdir3 } from "os";
4353
- import { basename as basename5, join as join21, relative as relative5 } from "path";
4566
+ import { basename as basename5, join as join22, relative as relative5 } from "path";
4354
4567
  import { promisify as promisify6 } from "util";
4355
4568
  var execFileP6 = promisify6(execFile6);
4356
4569
  var NOTES = "https://github.com/rness-dev/rness/tree/main/packages/cli#readme";
@@ -4370,10 +4583,10 @@ async function latestVersion() {
4370
4583
  async function scaffoldFor(version, rnessDir) {
4371
4584
  if (version === VERSION) return { dir: scaffoldDir() };
4372
4585
  const installed = await readPinnedCli(rnessDir);
4373
- const inside = installed === null ? null : join21(installed.dir, "scaffold");
4374
- if (installed?.version === version && inside !== null && await exists(join21(inside, "package.json")))
4375
- return { dir: inside };
4376
- const scratch = await mkdtemp3(join21(tmpdir3(), "rness-pack-"));
4586
+ const inside2 = installed === null ? null : join22(installed.dir, "scaffold");
4587
+ if (installed?.version === version && inside2 !== null && await exists(join22(inside2, "package.json")))
4588
+ return { dir: inside2 };
4589
+ const scratch = await mkdtemp3(join22(tmpdir3(), "rness-pack-"));
4377
4590
  const cleanup = () => rm7(scratch, { recursive: true, force: true });
4378
4591
  try {
4379
4592
  const { stdout } = await execFileP6(
@@ -4388,8 +4601,8 @@ async function scaffoldFor(version, rnessDir) {
4388
4601
  { timeout: 6e4 }
4389
4602
  );
4390
4603
  const tarball = stdout.trim().split("\n").at(-1) ?? "";
4391
- await execFileP6("tar", ["-xzf", join21(scratch, tarball), "-C", scratch]);
4392
- return { dir: join21(scratch, "package", "scaffold"), cleanup };
4604
+ await execFileP6("tar", ["-xzf", join22(scratch, tarball), "-C", scratch]);
4605
+ return { dir: join22(scratch, "package", "scaffold"), cleanup };
4393
4606
  } catch (e) {
4394
4607
  await cleanup();
4395
4608
  throw new Error(
@@ -4401,7 +4614,7 @@ async function scaffoldFor(version, rnessDir) {
4401
4614
  async function declaredManager(rnessDir, pm) {
4402
4615
  try {
4403
4616
  const pkg = JSON.parse(
4404
- await readFile9(join21(rnessDir, "package.json"), "utf8")
4617
+ await readFile9(join22(rnessDir, "package.json"), "utf8")
4405
4618
  );
4406
4619
  if (typeof pkg.packageManager === "string") return pkg.packageManager;
4407
4620
  } catch {
@@ -4412,7 +4625,7 @@ async function clonesToCommit(root, cwd, manifest) {
4412
4625
  const files = writtenFiles(manifest);
4413
4626
  const clones = [];
4414
4627
  for (const repo of Object.keys(manifest.repos)) {
4415
- const dir = join21(root, "org", repo);
4628
+ const dir = join22(root, "org", repo);
4416
4629
  if (!await exists(dir)) continue;
4417
4630
  try {
4418
4631
  const paths = await changedPaths(dir, files);
@@ -4425,14 +4638,14 @@ async function clonesToCommit(root, cwd, manifest) {
4425
4638
  }
4426
4639
  function finish(ui, cwd, rnessDir, label, target, ending) {
4427
4640
  const rel = relative5(cwd, rnessDir) || ".";
4428
- const message = `chore: rness ${target}`;
4641
+ const message2 = `chore: rness ${target}`;
4429
4642
  const next = [];
4430
4643
  if (ending.commit === "committed") next.push(`git -C ${rel} push`);
4431
4644
  if (ending.commit === "refused")
4432
- next.push(`git -C ${rel} commit -m "${message}"`);
4645
+ next.push(`git -C ${rel} commit -m "${message2}"`);
4433
4646
  for (const c of ending.clones)
4434
4647
  next.push(
4435
- `git -C ${c.dir} add ${c.paths.join(" ")} && git -C ${c.dir} commit -m "${message}"`
4648
+ `git -C ${c.dir} add ${c.paths.join(" ")} && git -C ${c.dir} commit -m "${message2}"`
4436
4649
  );
4437
4650
  if (ending.commit !== "nothing")
4438
4651
  next.push("# teammates: git pull \u2014 the next rness command installs it");
@@ -4445,7 +4658,7 @@ function finish(ui, cwd, rnessDir, label, target, ending) {
4445
4658
  "",
4446
4659
  status("upgraded", `${label} to @rness/cli ${target}`),
4447
4660
  "",
4448
- ...next.length > 0 ? ["Next:", ...next.map((line2) => ` ${line2}`), ""] : []
4661
+ ...next.length > 0 ? ["Next:", ...next.map((line3) => ` ${line3}`), ""] : []
4449
4662
  ].join("\n")
4450
4663
  );
4451
4664
  return ending.commit === "refused" ? 1 : 0;
@@ -4605,7 +4818,7 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
4605
4818
  await source.cleanup?.();
4606
4819
  }
4607
4820
  }
4608
- const packageFile = join21(ws.rnessDir, "package.json");
4821
+ const packageFile = join22(ws.rnessDir, "package.json");
4609
4822
  const restore = /* @__PURE__ */ new Map([
4610
4823
  [packageFile, await readFile9(packageFile, "utf8")]
4611
4824
  ]);
@@ -4621,8 +4834,8 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
4621
4834
  () => ["installed", `dependencies with ${pm}`]
4622
4835
  );
4623
4836
  } catch (e) {
4624
- const message2 = e instanceof Error ? e.message : String(e);
4625
- ui.error(message2);
4837
+ const message3 = e instanceof Error ? e.message : String(e);
4838
+ ui.error(message3);
4626
4839
  if (!merged) {
4627
4840
  for (const [file, text] of restore) await writeFileAtomic(file, text);
4628
4841
  ui.hint(
@@ -4635,7 +4848,7 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
4635
4848
  `the scaffold merge is still staged; undo it with: git -C ${relative5(cwd, ws.rnessDir) || "."} merge --abort`,
4636
4849
  "stderr"
4637
4850
  );
4638
- if (/minimum.?release.?age|NO_MATURE/i.test(message2))
4851
+ if (/minimum.?release.?age|NO_MATURE/i.test(message3))
4639
4852
  ui.hint(
4640
4853
  "pnpm refuses versions published less than 24 h ago: add '@rness/cli' under minimumReleaseAgeExclude in .rness/pnpm-workspace.yaml",
4641
4854
  "stderr"
@@ -4654,20 +4867,20 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
4654
4867
  ui.gitIsSilent = true;
4655
4868
  const code = await syncBlocks(ui, ws.root, basename5(ws.root));
4656
4869
  if (code !== 0) return code;
4657
- const message = `chore: rness ${target}`;
4658
- const committed = await commitUpgrade(ws.rnessDir, message);
4870
+ const message2 = `chore: rness ${target}`;
4871
+ const committed = await commitUpgrade(ws.rnessDir, message2);
4659
4872
  if (committed.kind === "committed")
4660
4873
  ui.line(
4661
4874
  "committed",
4662
- `${label} \u2014 ${message}`,
4663
- `Committed ${label}: ${message}`
4875
+ `${label} \u2014 ${message2}`,
4876
+ `Committed ${label}: ${message2}`
4664
4877
  );
4665
4878
  if (committed.kind === "refused") {
4666
4879
  ui.error(
4667
4880
  `the commit of ${label} was refused; everything is staged \u2014 deal with what git said, then commit`
4668
4881
  );
4669
- for (const line2 of committed.output.split("\n"))
4670
- if (line2.trim() !== "") ui.hint(line2, "stderr");
4882
+ for (const line3 of committed.output.split("\n"))
4883
+ if (line3.trim() !== "") ui.hint(line3, "stderr");
4671
4884
  }
4672
4885
  return finish(ui, cwd, ws.rnessDir, label, target, {
4673
4886
  commit: committed.kind,
@@ -4682,85 +4895,28 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
4682
4895
  }
4683
4896
  }
4684
4897
 
4685
- // src/core/blocks.ts
4686
- import { join as join22 } from "path";
4687
- var PROBLEM = /* @__PURE__ */ new Set(["stale", "malformed"]);
4688
- async function checkBlocks({
4689
- root,
4690
- rnessDir,
4691
- manifest,
4692
- org
4693
- }) {
4694
- const checks = [];
4695
- const note = (label, status2, message = null) => {
4696
- checks.push({ label, status: status2, message });
4697
- };
4698
- const derive = () => ({
4699
- problems: checks.filter((c) => PROBLEM.has(c.status) && c.message !== null).map((c) => c.message),
4700
- warnings: checks.filter((c) => !PROBLEM.has(c.status) && c.message !== null).map((c) => c.message),
4701
- checks
4898
+ // src/core/check-workspace.ts
4899
+ async function checkWorkspace(ws, manifest) {
4900
+ for (const s of Object.keys(manifest.scopes)) scopeChain(manifest, s);
4901
+ const problems = await checkContract(ws.rnessDir);
4902
+ const blocks = await checkBlocks({
4903
+ root: ws.root,
4904
+ rnessDir: ws.rnessDir,
4905
+ manifest,
4906
+ org: workspaceName(manifest, ws.root)
4702
4907
  });
4703
- if (!await exists(join22(root, "org"))) return derive();
4704
- const targets = [
4705
- { scope: null, dir: root, depth: 0, file: "AGENTS.md", label: "AGENTS.md" },
4706
- { scope: null, dir: root, depth: 0, file: "CLAUDE.md", label: "CLAUDE.md" }
4707
- ];
4708
- for (const [name, entry] of Object.entries(manifest.scopes)) {
4709
- const segments = entry.path.split("/");
4710
- targets.push({
4711
- scope: name,
4712
- dir: join22(root, ...segments),
4713
- depth: segments.length,
4714
- file: "AGENTS.md",
4715
- label: `${entry.path}/AGENTS.md`
4716
- });
4717
- }
4718
- for (const t of targets) {
4719
- if (!await exists(t.dir)) {
4720
- note(
4721
- t.label,
4722
- "missing-dir",
4723
- `${t.label}: directory not present, block not checked`
4724
- );
4725
- continue;
4726
- }
4727
- const text = await readOrNull(join22(t.dir, t.file));
4728
- const lines = text === null ? [] : text.split(/\r?\n/);
4729
- const found = findBlock(lines);
4730
- if (found.kind === "none") {
4731
- note(
4732
- t.label,
4733
- "no-block",
4734
- `${t.label}: no rness block yet (run ${rnessCommand()} sync)`
4735
- );
4736
- continue;
4737
- }
4738
- if (found.kind === "error") {
4739
- note(t.label, "malformed", `${t.label}: ${found.message}`);
4740
- continue;
4741
- }
4742
- const context2 = await assembleContext({
4743
- rnessDir,
4744
- manifest,
4745
- scope: t.scope
4746
- });
4747
- const fresh2 = renderBlock({
4748
- scope: t.scope,
4749
- org,
4750
- depth: t.depth,
4751
- context: context2
4752
- });
4753
- const pointerLeft = t.file === "CLAUDE.md" && lines.some((l) => l.trim() === "@AGENTS.md");
4754
- if (isCurrentBlock(lines.slice(found.begin, found.end + 1), fresh2.hash) && !pointerLeft)
4755
- note(t.label, "current");
4756
- else
4757
- note(
4758
- t.label,
4759
- "stale",
4760
- `${t.label}: stale rness block (run ${rnessCommand()} sync)`
4908
+ problems.push(...blocks.problems);
4909
+ for (const name of unsupportedAgents(manifest.agents ?? []))
4910
+ problems.push(unsupportedMessage(name));
4911
+ const targets = await agentTargets(ws.root, manifest, { check: true });
4912
+ for (const o of targets)
4913
+ if (o.status === "stale")
4914
+ problems.push(
4915
+ `${o.label}: ${o.detail ?? ""} (run ${rnessCommand()} sync)`
4761
4916
  );
4762
- }
4763
- return derive();
4917
+ else if (o.status === "invalid")
4918
+ problems.push(`${o.label}: ${o.detail ?? "unreadable"}`);
4919
+ return { problems, blocks, targets };
4764
4920
  }
4765
4921
 
4766
4922
  // src/commands/validate.ts
@@ -4769,26 +4925,8 @@ async function validateCommand(opts, deps = {}) {
4769
4925
  try {
4770
4926
  const ws = await findWorkspace(cwd);
4771
4927
  const manifest = await loadManifest(ws.rnessDir);
4772
- for (const s of Object.keys(manifest.scopes)) scopeChain(manifest, s);
4773
4928
  const org = workspaceName(manifest, ws.root);
4774
- const problems = await checkContract(ws.rnessDir);
4775
- const blocks = await checkBlocks({
4776
- root: ws.root,
4777
- rnessDir: ws.rnessDir,
4778
- manifest,
4779
- org
4780
- });
4781
- problems.push(...blocks.problems);
4782
- for (const name of unsupportedAgents(manifest.agents ?? []))
4783
- problems.push(unsupportedMessage(name));
4784
- const targets = await agentTargets(ws.root, manifest, { check: true });
4785
- for (const o of targets)
4786
- if (o.status === "stale")
4787
- problems.push(
4788
- `${o.label}: ${o.detail ?? ""} (run ${rnessCommand()} sync)`
4789
- );
4790
- else if (o.status === "invalid")
4791
- problems.push(`${o.label}: ${o.detail ?? "unreadable"}`);
4929
+ const { problems, blocks, targets } = await checkWorkspace(ws, manifest);
4792
4930
  const scaffoldWarnings = [];
4793
4931
  if (await isRepository2(ws.rnessDir) && !await isShallow(ws.rnessDir)) {
4794
4932
  const pin = await readPin(ws.rnessDir);
@@ -4851,9 +4989,9 @@ async function readStdin() {
4851
4989
  async function gitCredentialCommand(operation, input) {
4852
4990
  if (operation !== "get") return 0;
4853
4991
  const fields = /* @__PURE__ */ new Map();
4854
- for (const line2 of (input ?? await readStdin()).split("\n")) {
4855
- const eq = line2.indexOf("=");
4856
- if (eq > 0) fields.set(line2.slice(0, eq), line2.slice(eq + 1).trimEnd());
4992
+ for (const line3 of (input ?? await readStdin()).split("\n")) {
4993
+ const eq = line3.indexOf("=");
4994
+ if (eq > 0) fields.set(line3.slice(0, eq), line3.slice(eq + 1).trimEnd());
4857
4995
  }
4858
4996
  if (fields.get("protocol") !== "https" || fields.get("host") !== "github.com")
4859
4997
  return 0;
@@ -4865,138 +5003,26 @@ password=${resolved.token}
4865
5003
  return 0;
4866
5004
  }
4867
5005
 
4868
- // src/commands/mcp.ts
4869
- import { createInterface } from "readline";
5006
+ // src/commands/hook.ts
5007
+ import { realpath as realpath4 } from "fs/promises";
5008
+ import { isAbsolute as isAbsolute2, posix as posix6, relative as relative7, resolve as resolve7, sep as sep5 } from "path";
4870
5009
 
4871
- // src/mcp/protocol.ts
4872
- var MODERN_VERSIONS = ["2026-07-28"];
4873
- var LEGACY_VERSIONS = [
4874
- "2025-11-25",
4875
- "2025-06-18",
4876
- "2025-03-26",
4877
- "2024-11-05"
4878
- ];
4879
- var SUPPORTED = [...MODERN_VERSIONS, ...LEGACY_VERSIONS];
4880
- var VERSION_KEY = "io.modelcontextprotocol/protocolVersion";
4881
- var CAPABILITIES_KEY = "io.modelcontextprotocol/clientCapabilities";
4882
- var SERVER_INFO_KEY = "io.modelcontextprotocol/serverInfo";
4883
- function isObject2(value) {
4884
- return value !== null && typeof value === "object" && !Array.isArray(value);
4885
- }
4886
- function errorLine(id, code, message, data) {
4887
- return JSON.stringify({
4888
- jsonrpc: "2.0",
4889
- id,
4890
- error: data === void 0 ? { code, message } : { code, message, data }
4891
- });
4892
- }
4893
- function createProtocol(opts) {
4894
- const serverInfo = { name: opts.name, version: opts.version };
4895
- return {
4896
- async handle(line2) {
4897
- let message;
4898
- try {
4899
- message = JSON.parse(line2);
4900
- } catch {
4901
- return errorLine(null, -32700, "Parse error");
4902
- }
4903
- const rawId = isObject2(message) ? message["id"] : void 0;
4904
- const id = typeof rawId === "string" || typeof rawId === "number" ? rawId : null;
4905
- if (!isObject2(message) || message["jsonrpc"] !== "2.0" || typeof message["method"] !== "string")
4906
- return errorLine(id, -32600, "Invalid Request");
4907
- if (!("id" in message)) return null;
4908
- if (id === null) return errorLine(null, -32600, "Invalid Request");
4909
- const method = message["method"];
4910
- const params = isObject2(message["params"]) ? message["params"] : {};
4911
- const meta = isObject2(params["_meta"]) ? params["_meta"] : null;
4912
- const modern = meta !== null && typeof meta[VERSION_KEY] === "string";
4913
- if (modern) {
4914
- const requested = meta[VERSION_KEY];
4915
- if (!MODERN_VERSIONS.includes(requested))
4916
- return errorLine(id, -32022, "Unsupported protocol version", {
4917
- supported: SUPPORTED,
4918
- requested
4919
- });
4920
- if (!isObject2(meta[CAPABILITIES_KEY]))
4921
- return errorLine(
4922
- id,
4923
- -32602,
4924
- `Invalid params: ${CAPABILITIES_KEY} is required`
4925
- );
4926
- }
4927
- const answer = (result) => JSON.stringify({
4928
- jsonrpc: "2.0",
4929
- id,
4930
- result: modern ? {
4931
- resultType: "complete",
4932
- ...result,
4933
- _meta: { [SERVER_INFO_KEY]: serverInfo }
4934
- } : result
4935
- });
4936
- switch (method) {
4937
- case "server/discover":
4938
- return answer({
4939
- supportedVersions: SUPPORTED,
4940
- capabilities: { tools: {} }
4941
- });
4942
- case "initialize": {
4943
- const asked = params["protocolVersion"];
4944
- const protocolVersion = typeof asked === "string" && LEGACY_VERSIONS.includes(asked) ? asked : LEGACY_VERSIONS[0];
4945
- return JSON.stringify({
4946
- jsonrpc: "2.0",
4947
- id,
4948
- result: {
4949
- protocolVersion,
4950
- capabilities: { tools: {} },
4951
- serverInfo
4952
- }
4953
- });
4954
- }
4955
- case "ping":
4956
- return answer({});
4957
- case "tools/list":
4958
- return answer({
4959
- tools: opts.tools.map((t) => ({
4960
- name: t.name,
4961
- title: t.title,
4962
- description: t.description,
4963
- inputSchema: t.inputSchema
4964
- }))
4965
- });
4966
- case "tools/call": {
4967
- const name = params["name"];
4968
- const tool = opts.tools.find((t) => t.name === name);
4969
- if (tool === void 0)
4970
- return errorLine(id, -32602, `Unknown tool: ${String(name)}`);
4971
- const args = isObject2(params["arguments"]) ? params["arguments"] : {};
4972
- try {
4973
- const r = await tool.call(args);
4974
- return answer({
4975
- content: [{ type: "text", text: r.text }],
4976
- isError: r.isError === true
4977
- });
4978
- } catch (e) {
4979
- return answer({
4980
- content: [
4981
- {
4982
- type: "text",
4983
- text: `${tool.name} failed: ${e instanceof Error ? e.message : String(e)}`
4984
- }
4985
- ],
4986
- isError: true
4987
- });
4988
- }
4989
- }
4990
- default:
4991
- return errorLine(id, -32601, "Method not found");
4992
- }
4993
- }
4994
- };
5010
+ // src/mcp/tools.ts
5011
+ import { readFile as readFile10, realpath as realpath3, stat as stat4 } from "fs/promises";
5012
+ import { isAbsolute, join as join23, posix as posix5, relative as relative6, resolve as resolve6, sep as sep4 } from "path";
5013
+
5014
+ // src/core/documents.ts
5015
+ import { posix as posix4 } from "path";
5016
+ function documentNumber(rel) {
5017
+ return /^(\d{4})-/.exec(posix4.basename(rel))?.[1] ?? null;
5018
+ }
5019
+ function documentTitle(heading, number) {
5020
+ if (number === null) return heading;
5021
+ const rest = new RegExp(`^${number}\\s*[\u2014\u2013:-]\\s*(.+)$`).exec(heading)?.[1];
5022
+ return rest ?? heading;
4995
5023
  }
4996
5024
 
4997
5025
  // src/mcp/tools.ts
4998
- import { readFile as readFile10, realpath as realpath3, stat as stat4 } from "fs/promises";
4999
- import { isAbsolute, join as join23, posix as posix4, relative as relative6, resolve as resolve6, sep as sep4 } from "path";
5000
5026
  var LABELS = {
5001
5027
  standards: "Standards",
5002
5028
  adr: "Decisions",
@@ -5021,11 +5047,6 @@ async function workspaceAt(cwd) {
5021
5047
  const ws = await findWorkspace(cwd);
5022
5048
  return { ws, manifest: await loadManifest(ws.rnessDir) };
5023
5049
  }
5024
- function titleOf(heading, id) {
5025
- if (id === null) return heading;
5026
- const rest = new RegExp(`^${id}\\s*[\u2014\u2013:-]\\s*(.+)$`).exec(heading)?.[1];
5027
- return rest ?? heading;
5028
- }
5029
5050
  async function documents(rnessDir, only) {
5030
5051
  const docs = [];
5031
5052
  for (const collection of COLLECTIONS) {
@@ -5034,14 +5055,14 @@ async function documents(rnessDir, only) {
5034
5055
  const path = `${collection}/${item.rel}`;
5035
5056
  if (path === TEMPLATE) continue;
5036
5057
  const status2 = item.fields?.["status"];
5037
- const id = /^(\d{4})-/.exec(posix4.basename(item.rel))?.[1] ?? null;
5058
+ const id = documentNumber(item.rel);
5038
5059
  docs.push({
5039
5060
  collection,
5040
5061
  path,
5041
5062
  scope: ownerScope(item.rel),
5042
5063
  id,
5043
5064
  status: typeof status2 === "string" ? status2 : null,
5044
- title: titleOf(item.title ?? posix4.basename(item.rel, ".md"), id),
5065
+ title: documentTitle(item.title ?? posix5.basename(item.rel, ".md"), id),
5045
5066
  text: item.body
5046
5067
  });
5047
5068
  }
@@ -5071,23 +5092,33 @@ async function context(cwd, args) {
5071
5092
  } else
5072
5093
  scope = resolveScope(
5073
5094
  manifest,
5074
- relative6(ws.root, cwd).split(sep4).join(posix4.sep)
5095
+ relative6(ws.root, cwd).split(sep4).join(posix5.sep)
5075
5096
  );
5097
+ const summary = await scopeSummary(ws, manifest, scope);
5098
+ return {
5099
+ text: [
5100
+ ...summary.lines,
5101
+ "Read one with rness_read; search with rness_search."
5102
+ ].join("\n")
5103
+ };
5104
+ }
5105
+ async function scopeSummary(ws, manifest, scope) {
5076
5106
  const entry = scope === null ? void 0 : manifest.scopes[scope];
5077
5107
  const where = scope === null || entry === void 0 ? "global scope" : `scope ${scope} (${entry.path}${entry.extends.length > 0 ? `, extends ${entry.extends.join(", ")}` : ""})`;
5078
5108
  const ctx = await assembleContext({ rnessDir: ws.rnessDir, manifest, scope });
5079
5109
  const index = new Map(
5080
5110
  (await documents(ws.rnessDir)).map((d) => [d.path, d])
5081
5111
  );
5082
- const out = [`Workspace ${workspaceName(manifest, ws.root)} \xB7 ${where}`];
5112
+ const lines = [`Workspace ${workspaceName(manifest, ws.root)} \xB7 ${where}`];
5113
+ const counts = {};
5083
5114
  for (const collection of ctx.collections) {
5084
5115
  const docs = collection.files.map((f) => index.get(`${collection.name}/${f.rel}`)).filter((d) => d !== void 0);
5085
5116
  if (docs.length === 0) continue;
5086
- out.push(`${LABELS[collection.name]} (${docs.length}):`);
5087
- for (const d of docs) out.push(line(d));
5117
+ counts[collection.name] = docs.length;
5118
+ lines.push(`${LABELS[collection.name]} (${docs.length}):`);
5119
+ for (const d of docs) lines.push(line(d));
5088
5120
  }
5089
- out.push("Read one with rness_read; search with rness_search.");
5090
- return { text: out.join("\n") };
5121
+ return { where, counts, lines };
5091
5122
  }
5092
5123
  async function list(cwd, args) {
5093
5124
  const { ws } = await workspaceAt(cwd);
@@ -5248,6 +5279,285 @@ function mcpTools(cwd) {
5248
5279
  ];
5249
5280
  }
5250
5281
 
5282
+ // src/commands/hook.ts
5283
+ var NOUNS = {
5284
+ standards: ["standard", "standards"],
5285
+ adr: ["decision", "decisions"],
5286
+ specs: ["specification", "specifications"],
5287
+ plans: ["plan", "plans"],
5288
+ skills: ["skill", "skills"]
5289
+ };
5290
+ var SHOWN_PROBLEMS = 3;
5291
+ async function readInput(input) {
5292
+ if (input.isTTY === true) return {};
5293
+ let text = "";
5294
+ for await (const chunk of input) text += String(chunk);
5295
+ try {
5296
+ const data = JSON.parse(text);
5297
+ return data !== null && typeof data === "object" && !Array.isArray(data) ? data : {};
5298
+ } catch {
5299
+ return {};
5300
+ }
5301
+ }
5302
+ var message = (e) => e instanceof Error ? e.message : String(e);
5303
+ function cwdOf(input, env) {
5304
+ const cwd = input["cwd"];
5305
+ if (typeof cwd === "string" && cwd !== "") return cwd;
5306
+ return env["CLAUDE_PROJECT_DIR"] ?? process.cwd();
5307
+ }
5308
+ var toPosix2 = (p) => p.split(sep5).join(posix6.sep);
5309
+ function count(n, [one, many]) {
5310
+ return `${n} ${n === 1 ? one : many}`;
5311
+ }
5312
+ async function safetyNet(ws, manifest) {
5313
+ const notes = [];
5314
+ const drift = await pinDrift(ws.rnessDir);
5315
+ if (drift !== null)
5316
+ notes.push(
5317
+ `.rness pins @rness/cli ${drift.pin} but ${drift.installed ?? "nothing"} is installed \u2014 run ${await workspacePackageManager(ws.rnessDir)} install in .rness`
5318
+ );
5319
+ try {
5320
+ const { problems } = await checkWorkspace(ws, manifest);
5321
+ if (problems.length > 0)
5322
+ notes.push(
5323
+ `${count(problems.length, ["problem", "problems"])} in the workspace context \u2014 run rness validate`,
5324
+ ...problems.slice(0, SHOWN_PROBLEMS).map((p) => ` ${p}`)
5325
+ );
5326
+ } catch (e) {
5327
+ notes.push(message(e));
5328
+ }
5329
+ return notes;
5330
+ }
5331
+ async function sessionStart(input, env) {
5332
+ const cwd = cwdOf(input, env);
5333
+ let banner2 = null;
5334
+ let context2 = [];
5335
+ let notes;
5336
+ try {
5337
+ const ws = await findWorkspace(cwd);
5338
+ const manifest = await loadManifest(ws.rnessDir);
5339
+ const scope = resolveScope(manifest, toPosix2(relative7(ws.root, cwd)));
5340
+ const summary = await scopeSummary(ws, manifest, scope);
5341
+ const counts = COLLECTIONS.flatMap((c) => {
5342
+ const n = summary.counts[c];
5343
+ return n === void 0 ? [] : [count(n, NOUNS[c])];
5344
+ });
5345
+ banner2 = `rness ${VERSION} \xB7 ${workspaceName(manifest, ws.root)} \xB7 ${scope === null ? "global scope" : `scope ${scope}`} \u2014 ${counts.length === 0 ? "nothing applies yet" : counts.join(", ")}`;
5346
+ const at = toPosix2(relative7(cwd, ws.rnessDir)) || ".rness";
5347
+ context2 = [
5348
+ ...summary.lines,
5349
+ `These documents are in ${at}: read one there, or with rness_read when the rness MCP server is connected.`
5350
+ ];
5351
+ notes = await safetyNet(ws, manifest);
5352
+ } catch (e) {
5353
+ notes = [message(e)];
5354
+ }
5355
+ const warnings = notes.map((n) => `rness: ${n}`);
5356
+ const shown = [
5357
+ ...banner2 !== null && input["source"] !== "compact" ? [banner2] : [],
5358
+ ...warnings
5359
+ ];
5360
+ return {
5361
+ ...shown.length > 0 ? { systemMessage: shown.join("\n") } : {},
5362
+ hookSpecificOutput: {
5363
+ hookEventName: "SessionStart",
5364
+ additionalContext: [...context2, ...warnings].join("\n")
5365
+ }
5366
+ };
5367
+ }
5368
+ function inside(from, to) {
5369
+ const rel = relative7(from, to);
5370
+ return rel === "" || rel.startsWith("..") || isAbsolute2(rel) ? null : toPosix2(rel);
5371
+ }
5372
+ async function postToolUse(input, io) {
5373
+ const toolInput = input["tool_input"];
5374
+ const path = toolInput !== null && typeof toolInput === "object" ? toolInput["file_path"] : void 0;
5375
+ if (typeof path !== "string" || path === "") return 0;
5376
+ const cwd = cwdOf(input, io.env);
5377
+ let ws;
5378
+ try {
5379
+ ws = await findWorkspace(cwd);
5380
+ } catch {
5381
+ return 0;
5382
+ }
5383
+ const file = resolve7(cwd, path);
5384
+ const rel = inside(ws.rnessDir, file) ?? inside(
5385
+ await realpath4(ws.rnessDir).catch(() => ws.rnessDir),
5386
+ await realpath4(file).catch(() => file)
5387
+ );
5388
+ if (rel === null) return 0;
5389
+ const first = rel.split("/")[0] ?? "";
5390
+ let problems = [];
5391
+ if (rel === "rness.json") {
5392
+ try {
5393
+ const manifest = await loadManifest(ws.rnessDir);
5394
+ for (const s of Object.keys(manifest.scopes)) scopeChain(manifest, s);
5395
+ } catch (e) {
5396
+ problems = [message(e)];
5397
+ }
5398
+ } else if (COLLECTIONS.includes(first) && rel.endsWith(".md"))
5399
+ problems = (await checkContract(ws.rnessDir)).filter(
5400
+ (p) => p.startsWith(`${rel}:`)
5401
+ );
5402
+ if (problems.length === 0) return 0;
5403
+ io.error.write(
5404
+ [
5405
+ ...problems.map((p) => `rness: ${p}`),
5406
+ `Fix it in ${rel}; rness validate checks the whole workspace.`,
5407
+ ""
5408
+ ].join("\n")
5409
+ );
5410
+ return 2;
5411
+ }
5412
+ async function hookCommand(event, io = {
5413
+ input: process.stdin,
5414
+ output: process.stdout,
5415
+ error: process.stderr,
5416
+ env: process.env
5417
+ }) {
5418
+ if (event !== "session-start" && event !== "post-tool-use") {
5419
+ io.error.write(
5420
+ `unknown hook event ${JSON.stringify(event)} (known: session-start, post-tool-use)
5421
+ `
5422
+ );
5423
+ return 1;
5424
+ }
5425
+ const input = await readInput(io.input);
5426
+ if (event === "post-tool-use") return postToolUse(input, io);
5427
+ io.output.write(`${JSON.stringify(await sessionStart(input, io.env))}
5428
+ `);
5429
+ return 0;
5430
+ }
5431
+
5432
+ // src/commands/mcp.ts
5433
+ import { createInterface } from "readline";
5434
+
5435
+ // src/mcp/protocol.ts
5436
+ var MODERN_VERSIONS = ["2026-07-28"];
5437
+ var LEGACY_VERSIONS = [
5438
+ "2025-11-25",
5439
+ "2025-06-18",
5440
+ "2025-03-26",
5441
+ "2024-11-05"
5442
+ ];
5443
+ var SUPPORTED = [...MODERN_VERSIONS, ...LEGACY_VERSIONS];
5444
+ var VERSION_KEY = "io.modelcontextprotocol/protocolVersion";
5445
+ var CAPABILITIES_KEY = "io.modelcontextprotocol/clientCapabilities";
5446
+ var SERVER_INFO_KEY = "io.modelcontextprotocol/serverInfo";
5447
+ function isObject2(value) {
5448
+ return value !== null && typeof value === "object" && !Array.isArray(value);
5449
+ }
5450
+ function errorLine(id, code, message2, data) {
5451
+ return JSON.stringify({
5452
+ jsonrpc: "2.0",
5453
+ id,
5454
+ error: data === void 0 ? { code, message: message2 } : { code, message: message2, data }
5455
+ });
5456
+ }
5457
+ function createProtocol(opts) {
5458
+ const serverInfo = { name: opts.name, version: opts.version };
5459
+ return {
5460
+ async handle(line3) {
5461
+ let message2;
5462
+ try {
5463
+ message2 = JSON.parse(line3);
5464
+ } catch {
5465
+ return errorLine(null, -32700, "Parse error");
5466
+ }
5467
+ const rawId = isObject2(message2) ? message2["id"] : void 0;
5468
+ const id = typeof rawId === "string" || typeof rawId === "number" ? rawId : null;
5469
+ if (!isObject2(message2) || message2["jsonrpc"] !== "2.0" || typeof message2["method"] !== "string")
5470
+ return errorLine(id, -32600, "Invalid Request");
5471
+ if (!("id" in message2)) return null;
5472
+ if (id === null) return errorLine(null, -32600, "Invalid Request");
5473
+ const method = message2["method"];
5474
+ const params = isObject2(message2["params"]) ? message2["params"] : {};
5475
+ const meta = isObject2(params["_meta"]) ? params["_meta"] : null;
5476
+ const modern = meta !== null && typeof meta[VERSION_KEY] === "string";
5477
+ if (modern) {
5478
+ const requested = meta[VERSION_KEY];
5479
+ if (!MODERN_VERSIONS.includes(requested))
5480
+ return errorLine(id, -32022, "Unsupported protocol version", {
5481
+ supported: SUPPORTED,
5482
+ requested
5483
+ });
5484
+ if (!isObject2(meta[CAPABILITIES_KEY]))
5485
+ return errorLine(
5486
+ id,
5487
+ -32602,
5488
+ `Invalid params: ${CAPABILITIES_KEY} is required`
5489
+ );
5490
+ }
5491
+ const answer = (result) => JSON.stringify({
5492
+ jsonrpc: "2.0",
5493
+ id,
5494
+ result: modern ? {
5495
+ resultType: "complete",
5496
+ ...result,
5497
+ _meta: { [SERVER_INFO_KEY]: serverInfo }
5498
+ } : result
5499
+ });
5500
+ switch (method) {
5501
+ case "server/discover":
5502
+ return answer({
5503
+ supportedVersions: SUPPORTED,
5504
+ capabilities: { tools: {} }
5505
+ });
5506
+ case "initialize": {
5507
+ const asked = params["protocolVersion"];
5508
+ const protocolVersion = typeof asked === "string" && LEGACY_VERSIONS.includes(asked) ? asked : LEGACY_VERSIONS[0];
5509
+ return JSON.stringify({
5510
+ jsonrpc: "2.0",
5511
+ id,
5512
+ result: {
5513
+ protocolVersion,
5514
+ capabilities: { tools: {} },
5515
+ serverInfo
5516
+ }
5517
+ });
5518
+ }
5519
+ case "ping":
5520
+ return answer({});
5521
+ case "tools/list":
5522
+ return answer({
5523
+ tools: opts.tools.map((t) => ({
5524
+ name: t.name,
5525
+ title: t.title,
5526
+ description: t.description,
5527
+ inputSchema: t.inputSchema
5528
+ }))
5529
+ });
5530
+ case "tools/call": {
5531
+ const name = params["name"];
5532
+ const tool = opts.tools.find((t) => t.name === name);
5533
+ if (tool === void 0)
5534
+ return errorLine(id, -32602, `Unknown tool: ${String(name)}`);
5535
+ const args = isObject2(params["arguments"]) ? params["arguments"] : {};
5536
+ try {
5537
+ const r = await tool.call(args);
5538
+ return answer({
5539
+ content: [{ type: "text", text: r.text }],
5540
+ isError: r.isError === true
5541
+ });
5542
+ } catch (e) {
5543
+ return answer({
5544
+ content: [
5545
+ {
5546
+ type: "text",
5547
+ text: `${tool.name} failed: ${e instanceof Error ? e.message : String(e)}`
5548
+ }
5549
+ ],
5550
+ isError: true
5551
+ });
5552
+ }
5553
+ }
5554
+ default:
5555
+ return errorLine(id, -32601, "Method not found");
5556
+ }
5557
+ }
5558
+ };
5559
+ }
5560
+
5251
5561
  // src/commands/mcp.ts
5252
5562
  async function mcpCommand(opts, io = {
5253
5563
  input: process.stdin,
@@ -5259,15 +5569,349 @@ async function mcpCommand(opts, io = {
5259
5569
  tools: mcpTools(opts.cwd ?? process.cwd())
5260
5570
  });
5261
5571
  const lines = createInterface({ input: io.input, crlfDelay: Infinity });
5262
- for await (const line2 of lines) {
5263
- if (line2.trim() === "") continue;
5264
- const answer = await protocol.handle(line2);
5572
+ for await (const line3 of lines) {
5573
+ if (line3.trim() === "") continue;
5574
+ const answer = await protocol.handle(line3);
5265
5575
  if (answer !== null) io.output.write(`${answer}
5266
5576
  `);
5267
5577
  }
5268
5578
  return 0;
5269
5579
  }
5270
5580
 
5581
+ // src/commands/status.ts
5582
+ import { emitKeypressEvents } from "readline";
5583
+
5584
+ // src/core/status.ts
5585
+ import { readdir as readdir3 } from "fs/promises";
5586
+ import { join as join24, posix as posix7 } from "path";
5587
+ var FIXED = [
5588
+ ["adr", "ADR"],
5589
+ ["specs", "Specs"],
5590
+ ["plans", "Plans"]
5591
+ ];
5592
+ var NOT_DISCOVERED = /* @__PURE__ */ new Set([
5593
+ ...FIXED.map(([name]) => name),
5594
+ "standards",
5595
+ "skills",
5596
+ "node_modules"
5597
+ ]);
5598
+ var TEMPLATE2 = "adr/0000-template.md";
5599
+ function statusOf(item) {
5600
+ const status2 = item.fields?.["status"];
5601
+ return typeof status2 === "string" && status2 !== "" ? status2 : null;
5602
+ }
5603
+ function rowOf(dir, item) {
5604
+ const base = posix7.basename(item.rel, ".md");
5605
+ const date = /^(\d{4}-\d{2}-\d{2})-/.exec(base)?.[1] ?? null;
5606
+ const number = date === null ? documentNumber(item.rel) : null;
5607
+ return {
5608
+ id: date ?? number ?? base,
5609
+ title: documentTitle(item.title ?? base, number),
5610
+ status: statusOf(item),
5611
+ path: `${dir}/${item.rel}`
5612
+ };
5613
+ }
5614
+ function newestFirst(a, b) {
5615
+ const [x, y] = [posix7.basename(a.path), posix7.basename(b.path)];
5616
+ if (x !== y) return x < y ? 1 : -1;
5617
+ return a.path < b.path ? 1 : a.path > b.path ? -1 : 0;
5618
+ }
5619
+ async function statusTabs(rnessDir) {
5620
+ const tabs = [];
5621
+ for (const [name, label] of FIXED) {
5622
+ const rows = (await collectMarkdown(join24(rnessDir, name))).map((item) => rowOf(name, item)).filter((row) => row.path !== TEMPLATE2);
5623
+ tabs.push({ name, label, rows: rows.sort(newestFirst) });
5624
+ }
5625
+ const entries = await readdir3(rnessDir, { withFileTypes: true }).catch(
5626
+ () => []
5627
+ );
5628
+ const discovered = entries.filter(
5629
+ (e) => e.isDirectory() && !e.name.startsWith(".") && !NOT_DISCOVERED.has(e.name)
5630
+ ).map((e) => e.name).sort();
5631
+ for (const name of discovered) {
5632
+ const rows = (await collectMarkdown(join24(rnessDir, name))).filter((item) => statusOf(item) !== null).map((item) => rowOf(name, item));
5633
+ if (rows.length > 0)
5634
+ tabs.push({
5635
+ name,
5636
+ label: `${name.charAt(0).toUpperCase()}${name.slice(1)}`,
5637
+ rows: rows.sort(newestFirst)
5638
+ });
5639
+ }
5640
+ return tabs;
5641
+ }
5642
+ function findTab(tabs, wanted) {
5643
+ const w = wanted.toLowerCase();
5644
+ return tabs.find(
5645
+ (tab) => tab.name.toLowerCase() === w || tab.label.toLowerCase() === w
5646
+ );
5647
+ }
5648
+ var DONE2 = /* @__PURE__ */ new Set([
5649
+ "accepted",
5650
+ "implemented",
5651
+ "completed",
5652
+ "published"
5653
+ ]);
5654
+ var DROPPED = /* @__PURE__ */ new Set([
5655
+ "rejected",
5656
+ "superseded",
5657
+ "abandoned"
5658
+ ]);
5659
+ function statusTone(status2) {
5660
+ if (status2 === null) return "missing";
5661
+ const s = status2.toLowerCase();
5662
+ if (DONE2.has(s)) return "done";
5663
+ if (DROPPED.has(s)) return "dropped";
5664
+ return "active";
5665
+ }
5666
+ var cell = (text) => text.replaceAll("|", "\\|");
5667
+ function statusMarkdown(workspace, tabs) {
5668
+ const out = [`${workspace} \xB7 status`, ""];
5669
+ for (const tab of tabs) {
5670
+ out.push(`## ${tab.label} (${tab.rows.length})`, "");
5671
+ if (tab.rows.length === 0) {
5672
+ out.push("_Nothing yet._", "");
5673
+ continue;
5674
+ }
5675
+ out.push("| Id | Title | Status |", "| --- | --- | --- |");
5676
+ for (const row of tab.rows)
5677
+ out.push(
5678
+ `| ${cell(row.id)} | ${cell(row.title)} | ${cell(row.status ?? "?")} |`
5679
+ );
5680
+ out.push("");
5681
+ }
5682
+ return out.join("\n");
5683
+ }
5684
+
5685
+ // src/core/status-view.ts
5686
+ var FRAME = 5;
5687
+ function pageSize(height) {
5688
+ return Math.max(1, height - FRAME);
5689
+ }
5690
+ function lastTop(tab, page) {
5691
+ return Math.max(0, (tab?.rows.length ?? 0) - page);
5692
+ }
5693
+ function press(view, key, tabs, page) {
5694
+ const n = tabs.length;
5695
+ if (key === "right" || key === "tab")
5696
+ return { tab: (view.tab + 1) % n, top: 0 };
5697
+ if (key === "left" || key === "backtab")
5698
+ return { tab: (view.tab - 1 + n) % n, top: 0 };
5699
+ const moved = {
5700
+ up: view.top - 1,
5701
+ down: view.top + 1,
5702
+ pageup: view.top - page,
5703
+ pagedown: view.top + page,
5704
+ home: 0,
5705
+ end: Number.MAX_SAFE_INTEGER
5706
+ };
5707
+ const top = moved[key] ?? view.top;
5708
+ return {
5709
+ tab: view.tab,
5710
+ top: Math.min(Math.max(0, top), lastTop(tabs[view.tab], page))
5711
+ };
5712
+ }
5713
+ function clampView(view, tabs, page) {
5714
+ return {
5715
+ tab: view.tab,
5716
+ top: Math.min(view.top, lastTop(tabs[view.tab], page))
5717
+ };
5718
+ }
5719
+ var width = (text) => [...text].length;
5720
+ function fit(text, n) {
5721
+ if (n <= 0) return "";
5722
+ const chars = [...text];
5723
+ if (chars.length <= n) return text + " ".repeat(n - chars.length);
5724
+ return `${chars.slice(0, n - 1).join("").trimEnd()}\u2026`.padEnd(n);
5725
+ }
5726
+ function line2(segments, columns) {
5727
+ let left = columns;
5728
+ let out = "";
5729
+ for (const [text, style] of segments) {
5730
+ if (left <= 0) break;
5731
+ const chars = [...text];
5732
+ const kept = chars.length <= left ? text : chars.slice(0, left).join("");
5733
+ left -= width(kept);
5734
+ out += style === void 0 ? kept : style(kept);
5735
+ }
5736
+ return out;
5737
+ }
5738
+ function tabBar(tabs, active, look) {
5739
+ const segments = [[" "]];
5740
+ tabs.forEach((tab, i) => {
5741
+ if (i > 0) segments.push([" "]);
5742
+ const label = ` ${tab.label} (${tab.rows.length}) `;
5743
+ segments.push(i === active ? [label, look.inverse] : [label]);
5744
+ });
5745
+ return segments;
5746
+ }
5747
+ function rowLines(tab, top, page, columns, look) {
5748
+ if (tab === void 0 || tab.rows.length === 0)
5749
+ return [[[" Nothing here yet.", look.dim]]];
5750
+ const idWidth = Math.max(...tab.rows.map((r) => width(r.id)));
5751
+ const statusWidth = Math.max(...tab.rows.map((r) => width(r.status ?? "?")));
5752
+ const titleWidth = Math.max(
5753
+ 1,
5754
+ columns - 1 - (2 + idWidth + 2 + 2 + statusWidth)
5755
+ );
5756
+ return tab.rows.slice(top, top + page).map((row) => [
5757
+ [" "],
5758
+ [row.id.padEnd(idWidth)],
5759
+ [" "],
5760
+ [fit(row.title, titleWidth)],
5761
+ [" "],
5762
+ [
5763
+ (row.status ?? "?").padEnd(statusWidth),
5764
+ (text) => look.tone(statusTone(row.status), text)
5765
+ ]
5766
+ ]);
5767
+ }
5768
+ function footer(above, below) {
5769
+ const parts = [
5770
+ ...above > 0 ? [`\u2191 ${above} above`] : [],
5771
+ ...below > 0 ? [`\u2193 ${below} more`] : [],
5772
+ "\u2190/\u2192 tab \xB7 \u2191/\u2193 scroll \xB7 q close"
5773
+ ];
5774
+ return ` ${parts.join(" \xB7 ")}`;
5775
+ }
5776
+ function renderView(view, tabs, workspace, columns, height, look) {
5777
+ const page = pageSize(height);
5778
+ const tab = tabs[view.tab];
5779
+ const rule = [[` ${"\u2500".repeat(Math.max(0, columns - 2))}`]];
5780
+ const rows = rowLines(tab, view.top, page, columns, look);
5781
+ const shown = Math.min(page, rows.length);
5782
+ const total = tab?.rows.length ?? 0;
5783
+ const lines = [
5784
+ [[` rness \xB7 ${workspace}`, look.bold]],
5785
+ tabBar(tabs, view.tab, look),
5786
+ rule,
5787
+ ...rows,
5788
+ ...Array.from({ length: page - shown }, () => [[""]]),
5789
+ rule,
5790
+ [[footer(view.top, Math.max(0, total - view.top - page)), look.dim]]
5791
+ ];
5792
+ return lines.slice(0, Math.max(0, height)).map((l) => line2(l, columns));
5793
+ }
5794
+
5795
+ // src/commands/status.ts
5796
+ var ENTER = "\x1B[?1049h\x1B[?25l";
5797
+ var LEAVE = "\x1B[?25h\x1B[?1049l";
5798
+ function lookFor(output) {
5799
+ return {
5800
+ bold: (text) => bold(text, output),
5801
+ dim: (text) => grey(text, output),
5802
+ inverse: (text) => inverse(text, output),
5803
+ tone: (tone, text) => tone === "done" ? paint("done", text, output) : tone === "dropped" ? grey(text, output) : tone === "missing" ? paint("error", text, output) : paint("warn", text, output)
5804
+ };
5805
+ }
5806
+ var MOVES = /* @__PURE__ */ new Set([
5807
+ "left",
5808
+ "right",
5809
+ "up",
5810
+ "down",
5811
+ "pageup",
5812
+ "pagedown",
5813
+ "home",
5814
+ "end"
5815
+ ]);
5816
+ function keyOf(key) {
5817
+ const name = key?.name;
5818
+ if (name === void 0) return null;
5819
+ if (name === "q" || name === "escape" || key?.ctrl === true && name === "c")
5820
+ return "close";
5821
+ if (name === "tab") return key?.shift === true ? "backtab" : "tab";
5822
+ return MOVES.has(name) ? name : null;
5823
+ }
5824
+ function showView(tabs, workspace, start, { input, output }) {
5825
+ return new Promise((resolve8, reject) => {
5826
+ const look = lookFor(output);
5827
+ let view = { tab: start, top: 0 };
5828
+ const rows = () => output.rows || 24;
5829
+ const draw = () => {
5830
+ view = clampView(view, tabs, pageSize(rows()));
5831
+ const lines = renderView(
5832
+ view,
5833
+ tabs,
5834
+ workspace,
5835
+ output.columns || 80,
5836
+ rows(),
5837
+ look
5838
+ );
5839
+ output.write(`\x1B[H${lines.map((l) => `${l}\x1B[K`).join("\r\n")}\x1B[J`);
5840
+ };
5841
+ const restore = () => {
5842
+ output.write(LEAVE);
5843
+ };
5844
+ const close = (error) => {
5845
+ input.off("keypress", onKey);
5846
+ output.off("resize", onResize);
5847
+ process.off("exit", restore);
5848
+ input.setRawMode(false);
5849
+ input.pause();
5850
+ restore();
5851
+ if (error === void 0) resolve8();
5852
+ else reject(error);
5853
+ };
5854
+ const guarded = (step2) => {
5855
+ try {
5856
+ step2();
5857
+ } catch (e) {
5858
+ close(e);
5859
+ }
5860
+ };
5861
+ const onKey = (_, key) => {
5862
+ const k = keyOf(key);
5863
+ if (k === "close") close();
5864
+ else if (k !== null)
5865
+ guarded(() => {
5866
+ view = press(view, k, tabs, pageSize(rows()));
5867
+ draw();
5868
+ });
5869
+ };
5870
+ const onResize = () => {
5871
+ guarded(draw);
5872
+ };
5873
+ emitKeypressEvents(input);
5874
+ input.setRawMode(true);
5875
+ input.resume();
5876
+ output.write(ENTER);
5877
+ process.on("exit", restore);
5878
+ input.on("keypress", onKey);
5879
+ output.on("resize", onResize);
5880
+ guarded(draw);
5881
+ });
5882
+ }
5883
+ async function statusCommand(tab, opts, terminal = { input: process.stdin, output: process.stdout }) {
5884
+ try {
5885
+ const ws = await findWorkspace(opts.cwd ?? process.cwd());
5886
+ const manifest = await loadManifest(ws.rnessDir);
5887
+ const workspace = workspaceName(manifest, ws.root);
5888
+ const tabs = await statusTabs(ws.rnessDir);
5889
+ const wanted = tab === void 0 ? void 0 : findTab(tabs, tab);
5890
+ if (tab !== void 0 && wanted === void 0) {
5891
+ process.stderr.write(
5892
+ `unknown tab "${tab}" (tabs: ${tabs.map((t) => t.name).join(", ")})
5893
+ `
5894
+ );
5895
+ return 2;
5896
+ }
5897
+ if (terminal.input.isTTY === true && terminal.output.isTTY === true) {
5898
+ await showView(
5899
+ tabs,
5900
+ workspace,
5901
+ wanted === void 0 ? 0 : tabs.indexOf(wanted),
5902
+ terminal
5903
+ );
5904
+ return 0;
5905
+ }
5906
+ terminal.output.write(
5907
+ statusMarkdown(workspace, wanted === void 0 ? tabs : [wanted])
5908
+ );
5909
+ return 0;
5910
+ } catch (e) {
5911
+ return reportError(e);
5912
+ }
5913
+ }
5914
+
5271
5915
  // src/cli.ts
5272
5916
  function buildProgram(state) {
5273
5917
  const program = new Command().name("rness").description("Configuration-plane CLI for rness workspaces").version(VERSION, "-v, --version", "print the version").exitOverride().showHelpAfterError().configureOutput({
@@ -5295,6 +5939,11 @@ function buildProgram(state) {
5295
5939
  ).option("--json", "print JSON instead of Markdown").addOption(new Option("--cwd <dir>").hideHelp()).action(async (opts) => {
5296
5940
  state.code = await contextCommand(opts);
5297
5941
  });
5942
+ program.command("status").description(
5943
+ "Show the status of every decision, specification and plan, a tab per directory"
5944
+ ).argument("[tab]", "open on this tab: adr, specs, plans, \u2026").addOption(new Option("--cwd <dir>").hideHelp()).action(async (tab, opts) => {
5945
+ state.code = await statusCommand(tab, opts);
5946
+ });
5298
5947
  program.command("validate").description(
5299
5948
  "Check the .rness/ tree against the contract and every generated block"
5300
5949
  ).addOption(new Option("--cwd <dir>").hideHelp()).action(async (opts) => {
@@ -5337,6 +5986,9 @@ function buildProgram(state) {
5337
5986
  program.command("git-credential", { hidden: true }).argument("<operation>").action(async (operation) => {
5338
5987
  state.code = await gitCredentialCommand(operation);
5339
5988
  });
5989
+ program.command("hook", { hidden: true }).argument("<event>", "session-start or post-tool-use").action(async (event) => {
5990
+ state.code = await hookCommand(event);
5991
+ });
5340
5992
  program.command("upgrade").alias("update").description(
5341
5993
  "Move this workspace to another @rness/cli: pin it in .rness/package.json, install, sync"
5342
5994
  ).argument("[version]", "an exact version (default: the latest release)").option("-y, --yes", "do not ask for confirmation").addOption(new Option("--cwd <dir>").hideHelp()).action(async (version, opts) => {
@@ -5414,7 +6066,8 @@ var OWN_COMMANDS = [
5414
6066
  "update",
5415
6067
  "login",
5416
6068
  "logout",
5417
- "git-credential"
6069
+ "git-credential",
6070
+ "hook"
5418
6071
  ];
5419
6072
  var CATCH_UP_DONE = "RNESS_CATCH_UP_DONE";
5420
6073
  async function catchUp(argv, deps = {}) {
@@ -5509,14 +6162,14 @@ export {
5509
6162
  copyScaffold,
5510
6163
  loginCommand,
5511
6164
  createCommand,
5512
- logoutCommand,
6165
+ checkBlocks,
5513
6166
  STATUSES,
5514
6167
  checkContract,
6168
+ logoutCommand,
5515
6169
  upgradeCommand,
5516
- checkBlocks,
5517
6170
  validateCommand,
5518
6171
  run2 as run,
5519
6172
  OWN_COMMANDS,
5520
6173
  catchUp
5521
6174
  };
5522
- //# sourceMappingURL=chunk-JKDF7ROY.js.map
6175
+ //# sourceMappingURL=chunk-LW4S5OLI.js.map