@rness/cli 0.9.1 → 0.10.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);
@@ -1263,7 +1263,7 @@ async function detectGithubSsh(opts) {
1263
1263
  ...opts.interactive ? [] : ["-o", "BatchMode=yes"],
1264
1264
  "git@github.com"
1265
1265
  ];
1266
- return new Promise((resolve7) => {
1266
+ return new Promise((resolve8) => {
1267
1267
  const child = spawn2(opts.command ?? "ssh", args, {
1268
1268
  stdio: [opts.interactive ? "inherit" : "ignore", "ignore", "pipe"]
1269
1269
  });
@@ -1276,7 +1276,7 @@ async function detectGithubSsh(opts) {
1276
1276
  settled = true;
1277
1277
  clearTimeout(timer);
1278
1278
  child.stderr?.destroy();
1279
- resolve7(access2);
1279
+ resolve8(access2);
1280
1280
  };
1281
1281
  const timer = setTimeout(() => {
1282
1282
  child.kill("SIGKILL");
@@ -1397,15 +1397,15 @@ function reportError(e) {
1397
1397
  process.stderr.write(`${e.stack}
1398
1398
  `);
1399
1399
  } else {
1400
- const message = e instanceof Error ? e.message : String(e);
1401
- process.stderr.write(`${paint("error", message, process.stderr)}
1400
+ const message2 = e instanceof Error ? e.message : String(e);
1401
+ process.stderr.write(`${paint("error", message2, process.stderr)}
1402
1402
  `);
1403
1403
  }
1404
1404
  return 1;
1405
1405
  }
1406
1406
 
1407
1407
  // src/version.ts
1408
- var VERSION = "0.9.1";
1408
+ var VERSION = "0.10.0";
1409
1409
 
1410
1410
  // src/core/block.ts
1411
1411
  import { createHash } from "crypto";
@@ -1473,8 +1473,8 @@ function isCurrentBlock(blockLines, freshHash) {
1473
1473
  // src/core/frontmatter.ts
1474
1474
  import { parse as parse2 } from "yaml";
1475
1475
  var FRONT_MATTER = /^---\r?\n(?:([\s\S]*?)\r?\n)?---(?:\r?\n|$)/;
1476
- function fail2(message) {
1477
- throw new Error(`front matter: ${message}`);
1476
+ function fail2(message2) {
1477
+ throw new Error(`front matter: ${message2}`);
1478
1478
  }
1479
1479
  function stripBom(source) {
1480
1480
  return source.startsWith("\uFEFF") ? source.slice(1) : source;
@@ -1706,6 +1706,46 @@ import { mkdir as mkdir2 } from "fs/promises";
1706
1706
  import { dirname as dirname4, join as join9 } from "path";
1707
1707
 
1708
1708
  // src/core/agents.ts
1709
+ var PINNED_BIN = "node_modules/@rness/cli/dist/bin/rness.js";
1710
+ function hookLine(rness, event) {
1711
+ const run3 = `f="$CLAUDE_PROJECT_DIR/${rness}/${PINNED_BIN}"; if [ -f "$f" ]; then node "$f" hook ${event};`;
1712
+ if (event === "post-tool-use") return `${run3} fi`;
1713
+ const missing = JSON.stringify({
1714
+ systemMessage: `rness: ${rness} is not installed, so the workspace context is not loaded. Clone the workspace, then install its dependencies in .rness.`
1715
+ });
1716
+ return `${run3} else echo '${missing}'; fi`;
1717
+ }
1718
+ function hooks(rness) {
1719
+ return [
1720
+ {
1721
+ path: ["hooks", "SessionStart"],
1722
+ contains: {
1723
+ hooks: [
1724
+ {
1725
+ type: "command",
1726
+ command: hookLine(rness, "session-start"),
1727
+ timeout: 10
1728
+ }
1729
+ ]
1730
+ },
1731
+ label: "the rness session-start hook"
1732
+ },
1733
+ {
1734
+ path: ["hooks", "PostToolUse"],
1735
+ contains: {
1736
+ matcher: "Edit|Write",
1737
+ hooks: [
1738
+ {
1739
+ type: "command",
1740
+ command: hookLine(rness, "post-tool-use"),
1741
+ timeout: 10
1742
+ }
1743
+ ]
1744
+ },
1745
+ label: "the rness post-tool-use hook"
1746
+ }
1747
+ ];
1748
+ }
1709
1749
  var TARGETS = {
1710
1750
  claude: {
1711
1751
  name: "claude",
@@ -1713,6 +1753,7 @@ var TARGETS = {
1713
1753
  files: [
1714
1754
  {
1715
1755
  file: ".claude/settings.json",
1756
+ at: "clones",
1716
1757
  guarantees: [
1717
1758
  // Read the context repository from a repository without a prompt.
1718
1759
  // Resolved against the project directory, and applied once Claude
@@ -1720,11 +1761,13 @@ var TARGETS = {
1720
1761
  {
1721
1762
  path: ["permissions", "additionalDirectories"],
1722
1763
  contains: "../../.rness"
1723
- }
1764
+ },
1765
+ ...hooks("../../.rness")
1724
1766
  ]
1725
1767
  },
1726
1768
  {
1727
1769
  file: ".mcp.json",
1770
+ at: "clones",
1728
1771
  guarantees: [
1729
1772
  // The pinned copy, by a path relative to the repository — the
1730
1773
  // directory Claude Code starts the server in (spec 0014 §6). Not
@@ -1742,6 +1785,13 @@ var TARGETS = {
1742
1785
  }
1743
1786
  }
1744
1787
  ]
1788
+ },
1789
+ // A session opened at the workspace root reads this file, one opened
1790
+ // in a clone does not (spec 0011 §1): the hooks only, `.rness` inside.
1791
+ {
1792
+ file: ".claude/settings.json",
1793
+ at: "root",
1794
+ guarantees: hooks(".rness")
1745
1795
  }
1746
1796
  ]
1747
1797
  }
@@ -1755,6 +1805,7 @@ function unsupportedMessage(name) {
1755
1805
  }
1756
1806
 
1757
1807
  // src/core/json-guarantees.ts
1808
+ import { isDeepStrictEqual } from "util";
1758
1809
  function isObject(value) {
1759
1810
  return value !== null && typeof value === "object" && !Array.isArray(value);
1760
1811
  }
@@ -1798,14 +1849,15 @@ function apply(root, g, create) {
1798
1849
  }
1799
1850
  if (value === void 0) {
1800
1851
  if (!create) return { holds: false, changed: false };
1801
- parent[last] = [g.contains];
1852
+ parent[last] = [structuredClone(g.contains)];
1802
1853
  return { holds: true, changed: true };
1803
1854
  }
1804
1855
  if (!Array.isArray(value))
1805
1856
  return { invalid: `${g.path.join(".")} is not a list` };
1806
- if (value.includes(g.contains)) return { holds: true, changed: false };
1857
+ if (value.some((entry) => isDeepStrictEqual(entry, g.contains)))
1858
+ return { holds: true, changed: false };
1807
1859
  if (!create) return { holds: false, changed: false };
1808
- value.push(g.contains);
1860
+ value.push(structuredClone(g.contains));
1809
1861
  return { holds: true, changed: true };
1810
1862
  }
1811
1863
  function styleOf(text) {
@@ -1849,21 +1901,33 @@ function missingGuarantees(existing, guarantees) {
1849
1901
  return missing;
1850
1902
  }
1851
1903
  function describeGuarantee(g) {
1852
- return "value" in g ? `${g.path.join(".")} is missing` : `${g.path.join(".")} lacks ${g.contains}`;
1904
+ if ("value" in g) return `${g.path.join(".")} is missing`;
1905
+ const what = g.label ?? (typeof g.contains === "string" ? g.contains : JSON.stringify(g.contains));
1906
+ return `${g.path.join(".")} lacks ${what}`;
1853
1907
  }
1854
1908
 
1855
1909
  // src/core/agent-targets.ts
1856
1910
  async function filesOf(root, manifest, targets) {
1857
1911
  const files = [];
1912
+ if (!await exists(join9(root, "org"))) return files;
1913
+ for (const target of targets)
1914
+ for (const owned of target.files)
1915
+ if (owned.at === "root")
1916
+ files.push({
1917
+ guarantees: owned.guarantees,
1918
+ file: join9(root, ...owned.file.split("/")),
1919
+ label: owned.file
1920
+ });
1858
1921
  for (const repo of Object.keys(manifest.repos)) {
1859
1922
  if (!await exists(join9(root, "org", repo))) continue;
1860
1923
  for (const target of targets)
1861
1924
  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
- });
1925
+ if (owned.at === "clones")
1926
+ files.push({
1927
+ guarantees: owned.guarantees,
1928
+ file: join9(root, "org", repo, ...owned.file.split("/")),
1929
+ label: `org/${repo}/${owned.file}`
1930
+ });
1867
1931
  }
1868
1932
  return files;
1869
1933
  }
@@ -1876,7 +1940,9 @@ var BLOCK_FILES = ["AGENTS.md", "CLAUDE.md"];
1876
1940
  function writtenFiles(manifest) {
1877
1941
  return [
1878
1942
  ...BLOCK_FILES,
1879
- ...declared(manifest).flatMap((t) => t.files.map((f) => f.file))
1943
+ ...declared(manifest).flatMap(
1944
+ (t) => t.files.filter((f) => f.at === "clones").map((f) => f.file)
1945
+ )
1880
1946
  ];
1881
1947
  }
1882
1948
  async function agentTargets(root, manifest, opts) {
@@ -2064,8 +2130,8 @@ function sessionUi(p) {
2064
2130
  intro: (title) => {
2065
2131
  p.intro(badge(title));
2066
2132
  },
2067
- outro: (message, tone = "done") => {
2068
- p.outro(badge(message, tone));
2133
+ outro: (message2, tone = "done") => {
2134
+ p.outro(badge(message2, tone));
2069
2135
  },
2070
2136
  line: (verb, rest, sentence) => {
2071
2137
  say(verb, sentence ?? `${rest} ${verb}`);
@@ -2088,7 +2154,7 @@ function sessionUi(p) {
2088
2154
  async step(options, work, done) {
2089
2155
  const label = options.sentence ?? tidy(options.doing);
2090
2156
  const finish2 = (result) => done?.(result) ?? null;
2091
- const label_ = (line2) => `${line2[1]} ${line2[0]}`;
2157
+ const label_ = (line2) => line2[2] ?? `${line2[1]} ${line2[0]}`;
2092
2158
  if (options.git === true && !this.gitIsSilent) {
2093
2159
  p.log.message(paint("idle", `${label}\u2026`));
2094
2160
  const result = await work();
@@ -3109,19 +3175,32 @@ async function runPinnedSync(root) {
3109
3175
  };
3110
3176
  }
3111
3177
  }
3178
+ var SYNC_VERBS = ["updated", "unchanged", "stale"];
3179
+ function counted(noun, counts) {
3180
+ const total = [...counts.values()].reduce((a, b) => a + b, 0);
3181
+ const detail = SYNC_VERBS.filter((verb) => counts.has(verb)).map((verb) => `${counts.get(verb) ?? 0} ${verb}`).join(", ");
3182
+ return `${total} ${noun}${total === 1 ? "" : "s"}${detail === "" ? "" : `: ${detail}`}`;
3183
+ }
3112
3184
  function summariseSync(stdout) {
3113
- const counts = /* @__PURE__ */ new Map();
3185
+ const blocks = /* @__PURE__ */ new Map();
3186
+ const agentFiles = /* @__PURE__ */ new Map();
3114
3187
  const others = [];
3115
3188
  for (const line2 of stdout.split("\n")) {
3116
3189
  if (line2.trim() === "") continue;
3117
- const verb = /^(updated|unchanged|stale)\s/.exec(line2)?.[1];
3118
- if (verb === void 0) others.push(line2);
3119
- else counts.set(verb, (counts.get(verb) ?? 0) + 1);
3190
+ const m = /^(updated|unchanged|stale)\s+(\S+)/.exec(line2);
3191
+ if (m === null) {
3192
+ others.push(line2);
3193
+ continue;
3194
+ }
3195
+ const [, verb = "", path = ""] = m;
3196
+ const counts = /(^|\/)(AGENTS|CLAUDE)\.md$/.test(path) ? blocks : agentFiles;
3197
+ counts.set(verb, (counts.get(verb) ?? 0) + 1);
3120
3198
  }
3121
- const total = [...counts.values()].reduce((a, b) => a + b, 0);
3122
- const detail = [...counts].map(([verb, n]) => `${n} ${verb}`).join(", ");
3123
3199
  return {
3124
- summary: `${total} block${total === 1 ? "" : "s"}${detail === "" ? "" : `: ${detail}`}`,
3200
+ summary: [
3201
+ counted("block", blocks),
3202
+ ...agentFiles.size > 0 ? [counted("agent file", agentFiles)] : []
3203
+ ].join("; "),
3125
3204
  others
3126
3205
  };
3127
3206
  }
@@ -3227,13 +3306,13 @@ function scaffoldMessage(subject, version) {
3227
3306
 
3228
3307
  ${SCAFFOLD_TRAILER}: ${version}`;
3229
3308
  }
3230
- async function commitUpgrade(rnessDir, message) {
3309
+ async function commitUpgrade(rnessDir, message2) {
3231
3310
  const add = await run(rnessDir, ["add", "-A"]);
3232
3311
  if (add.code !== 0) return { kind: "refused", output: add.stderr.trim() };
3233
3312
  const staged = await run(rnessDir, ["diff", "--cached", "--quiet"]);
3234
3313
  if (staged.code === 0 && !await mergeInProgress(rnessDir))
3235
3314
  return { kind: "nothing" };
3236
- const commit = await run(rnessDir, ["commit", "-q", "-m", message]);
3315
+ const commit = await run(rnessDir, ["commit", "-q", "-m", message2]);
3237
3316
  if (commit.code !== 0)
3238
3317
  return {
3239
3318
  kind: "refused",
@@ -3474,7 +3553,10 @@ async function syncBlocks(ui, root, shown, inPlace, sshWorks) {
3474
3553
  sentence: "Writing the rness block into every AGENTS.md"
3475
3554
  },
3476
3555
  () => runPinnedSync(root),
3477
- (r) => r === null || r.code !== 0 ? null : ["synced", summariseSync(r.stdout).summary]
3556
+ (r) => r === null || r.code !== 0 ? null : (() => {
3557
+ const { summary } = summariseSync(r.stdout);
3558
+ return ["synced", summary, `Synced ${summary}`];
3559
+ })()
3478
3560
  );
3479
3561
  if (result === null) {
3480
3562
  if (inPlace !== void 0) return inPlace();
@@ -3751,9 +3833,9 @@ async function cloneSelection(input) {
3751
3833
  manifest = result.manifest;
3752
3834
  ui.line("declared", `scope ${result.name} (org/${result.name})`);
3753
3835
  } catch (e) {
3754
- const message = firstLine4(e instanceof Error ? e.message : String(e));
3755
- ui.error(`${spec}: ${message}`);
3756
- failures.push({ spec, message });
3836
+ const message2 = firstLine4(e instanceof Error ? e.message : String(e));
3837
+ ui.error(`${spec}: ${message2}`);
3838
+ failures.push({ spec, message: message2 });
3757
3839
  }
3758
3840
  }
3759
3841
  return failures;
@@ -4246,36 +4328,89 @@ ${next2.map((l) => ` ${l}`).join("\n")}
4246
4328
  }
4247
4329
  }
4248
4330
 
4249
- // src/commands/logout.ts
4250
- async function logoutCommand() {
4251
- try {
4252
- const stored = await readAuth();
4253
- await clearAuth();
4254
- const undone = await removeGitSetup().catch(() => false);
4255
- if (stored === null && !undone) {
4256
- process.stdout.write("not logged in\n");
4257
- return 0;
4331
+ // src/core/blocks.ts
4332
+ import { join as join20 } from "path";
4333
+ var PROBLEM = /* @__PURE__ */ new Set(["stale", "malformed"]);
4334
+ async function checkBlocks({
4335
+ root,
4336
+ rnessDir,
4337
+ manifest,
4338
+ org
4339
+ }) {
4340
+ const checks = [];
4341
+ const note = (label, status2, message2 = null) => {
4342
+ checks.push({ label, status: status2, message: message2 });
4343
+ };
4344
+ const derive = () => ({
4345
+ problems: checks.filter((c) => PROBLEM.has(c.status) && c.message !== null).map((c) => c.message),
4346
+ warnings: checks.filter((c) => !PROBLEM.has(c.status) && c.message !== null).map((c) => c.message),
4347
+ checks
4348
+ });
4349
+ if (!await exists(join20(root, "org"))) return derive();
4350
+ const targets = [
4351
+ { scope: null, dir: root, depth: 0, file: "AGENTS.md", label: "AGENTS.md" },
4352
+ { scope: null, dir: root, depth: 0, file: "CLAUDE.md", label: "CLAUDE.md" }
4353
+ ];
4354
+ for (const [name, entry] of Object.entries(manifest.scopes)) {
4355
+ const segments = entry.path.split("/");
4356
+ targets.push({
4357
+ scope: name,
4358
+ dir: join20(root, ...segments),
4359
+ depth: segments.length,
4360
+ file: "AGENTS.md",
4361
+ label: `${entry.path}/AGENTS.md`
4362
+ });
4363
+ }
4364
+ for (const t of targets) {
4365
+ if (!await exists(t.dir)) {
4366
+ note(
4367
+ t.label,
4368
+ "missing-dir",
4369
+ `${t.label}: directory not present, block not checked`
4370
+ );
4371
+ continue;
4258
4372
  }
4259
- if (stored !== null)
4260
- process.stdout.write(
4261
- `${status("logged out", `${stored.login} (github.com)`)}
4262
- `
4373
+ const text = await readOrNull(join20(t.dir, t.file));
4374
+ const lines = text === null ? [] : text.split(/\r?\n/);
4375
+ const found = findBlock(lines);
4376
+ if (found.kind === "none") {
4377
+ note(
4378
+ t.label,
4379
+ "no-block",
4380
+ `${t.label}: no rness block yet (run ${rnessCommand()} sync)`
4263
4381
  );
4264
- if (undone)
4265
- process.stdout.write(
4266
- `${status("updated", "git: rness no longer answers for https://github.com")}
4267
- `
4382
+ continue;
4383
+ }
4384
+ if (found.kind === "error") {
4385
+ note(t.label, "malformed", `${t.label}: ${found.message}`);
4386
+ continue;
4387
+ }
4388
+ const context2 = await assembleContext({
4389
+ rnessDir,
4390
+ manifest,
4391
+ scope: t.scope
4392
+ });
4393
+ const fresh2 = renderBlock({
4394
+ scope: t.scope,
4395
+ org,
4396
+ depth: t.depth,
4397
+ context: context2
4398
+ });
4399
+ const pointerLeft = t.file === "CLAUDE.md" && lines.some((l) => l.trim() === "@AGENTS.md");
4400
+ if (isCurrentBlock(lines.slice(found.begin, found.end + 1), fresh2.hash) && !pointerLeft)
4401
+ note(t.label, "current");
4402
+ else
4403
+ note(
4404
+ t.label,
4405
+ "stale",
4406
+ `${t.label}: stale rness block (run ${rnessCommand()} sync)`
4268
4407
  );
4269
- process.stdout.write(`revoke rness on GitHub: ${revokeUrl()}
4270
- `);
4271
- return 0;
4272
- } catch (e) {
4273
- return reportError(e);
4274
4408
  }
4409
+ return derive();
4275
4410
  }
4276
4411
 
4277
4412
  // src/core/contract.ts
4278
- import { join as join20 } from "path";
4413
+ import { join as join21 } from "path";
4279
4414
  var STATUSES = {
4280
4415
  adr: ["Proposed", "Accepted", "Rejected", "Superseded"],
4281
4416
  specs: [
@@ -4294,7 +4429,7 @@ async function checkContract(rnessDir) {
4294
4429
  const problems = [];
4295
4430
  for (const name of COLLECTIONS) {
4296
4431
  const allowed = STATUSES[name];
4297
- const items = await collectMarkdown(join20(rnessDir, name));
4432
+ const items = await collectMarkdown(join21(rnessDir, name));
4298
4433
  for (const item of items) {
4299
4434
  const where = `${name}/${item.rel}`;
4300
4435
  if (SKIP.has(where)) continue;
@@ -4330,11 +4465,39 @@ async function checkContract(rnessDir) {
4330
4465
  return problems;
4331
4466
  }
4332
4467
 
4468
+ // src/commands/logout.ts
4469
+ async function logoutCommand() {
4470
+ try {
4471
+ const stored = await readAuth();
4472
+ await clearAuth();
4473
+ const undone = await removeGitSetup().catch(() => false);
4474
+ if (stored === null && !undone) {
4475
+ process.stdout.write("not logged in\n");
4476
+ return 0;
4477
+ }
4478
+ if (stored !== null)
4479
+ process.stdout.write(
4480
+ `${status("logged out", `${stored.login} (github.com)`)}
4481
+ `
4482
+ );
4483
+ if (undone)
4484
+ process.stdout.write(
4485
+ `${status("updated", "git: rness no longer answers for https://github.com")}
4486
+ `
4487
+ );
4488
+ process.stdout.write(`revoke rness on GitHub: ${revokeUrl()}
4489
+ `);
4490
+ return 0;
4491
+ } catch (e) {
4492
+ return reportError(e);
4493
+ }
4494
+ }
4495
+
4333
4496
  // src/commands/upgrade.ts
4334
4497
  import { execFile as execFile6 } from "child_process";
4335
4498
  import { mkdtemp as mkdtemp3, readFile as readFile9, rm as rm7 } from "fs/promises";
4336
4499
  import { tmpdir as tmpdir3 } from "os";
4337
- import { basename as basename5, join as join21, relative as relative5 } from "path";
4500
+ import { basename as basename5, join as join22, relative as relative5 } from "path";
4338
4501
  import { promisify as promisify6 } from "util";
4339
4502
  var execFileP6 = promisify6(execFile6);
4340
4503
  var NOTES = "https://github.com/rness-dev/rness/tree/main/packages/cli#readme";
@@ -4354,10 +4517,10 @@ async function latestVersion() {
4354
4517
  async function scaffoldFor(version, rnessDir) {
4355
4518
  if (version === VERSION) return { dir: scaffoldDir() };
4356
4519
  const installed = await readPinnedCli(rnessDir);
4357
- const inside = installed === null ? null : join21(installed.dir, "scaffold");
4358
- if (installed?.version === version && inside !== null && await exists(join21(inside, "package.json")))
4359
- return { dir: inside };
4360
- const scratch = await mkdtemp3(join21(tmpdir3(), "rness-pack-"));
4520
+ const inside2 = installed === null ? null : join22(installed.dir, "scaffold");
4521
+ if (installed?.version === version && inside2 !== null && await exists(join22(inside2, "package.json")))
4522
+ return { dir: inside2 };
4523
+ const scratch = await mkdtemp3(join22(tmpdir3(), "rness-pack-"));
4361
4524
  const cleanup = () => rm7(scratch, { recursive: true, force: true });
4362
4525
  try {
4363
4526
  const { stdout } = await execFileP6(
@@ -4372,8 +4535,8 @@ async function scaffoldFor(version, rnessDir) {
4372
4535
  { timeout: 6e4 }
4373
4536
  );
4374
4537
  const tarball = stdout.trim().split("\n").at(-1) ?? "";
4375
- await execFileP6("tar", ["-xzf", join21(scratch, tarball), "-C", scratch]);
4376
- return { dir: join21(scratch, "package", "scaffold"), cleanup };
4538
+ await execFileP6("tar", ["-xzf", join22(scratch, tarball), "-C", scratch]);
4539
+ return { dir: join22(scratch, "package", "scaffold"), cleanup };
4377
4540
  } catch (e) {
4378
4541
  await cleanup();
4379
4542
  throw new Error(
@@ -4385,7 +4548,7 @@ async function scaffoldFor(version, rnessDir) {
4385
4548
  async function declaredManager(rnessDir, pm) {
4386
4549
  try {
4387
4550
  const pkg = JSON.parse(
4388
- await readFile9(join21(rnessDir, "package.json"), "utf8")
4551
+ await readFile9(join22(rnessDir, "package.json"), "utf8")
4389
4552
  );
4390
4553
  if (typeof pkg.packageManager === "string") return pkg.packageManager;
4391
4554
  } catch {
@@ -4396,7 +4559,7 @@ async function clonesToCommit(root, cwd, manifest) {
4396
4559
  const files = writtenFiles(manifest);
4397
4560
  const clones = [];
4398
4561
  for (const repo of Object.keys(manifest.repos)) {
4399
- const dir = join21(root, "org", repo);
4562
+ const dir = join22(root, "org", repo);
4400
4563
  if (!await exists(dir)) continue;
4401
4564
  try {
4402
4565
  const paths = await changedPaths(dir, files);
@@ -4409,14 +4572,14 @@ async function clonesToCommit(root, cwd, manifest) {
4409
4572
  }
4410
4573
  function finish(ui, cwd, rnessDir, label, target, ending) {
4411
4574
  const rel = relative5(cwd, rnessDir) || ".";
4412
- const message = `chore: rness ${target}`;
4575
+ const message2 = `chore: rness ${target}`;
4413
4576
  const next = [];
4414
4577
  if (ending.commit === "committed") next.push(`git -C ${rel} push`);
4415
4578
  if (ending.commit === "refused")
4416
- next.push(`git -C ${rel} commit -m "${message}"`);
4579
+ next.push(`git -C ${rel} commit -m "${message2}"`);
4417
4580
  for (const c of ending.clones)
4418
4581
  next.push(
4419
- `git -C ${c.dir} add ${c.paths.join(" ")} && git -C ${c.dir} commit -m "${message}"`
4582
+ `git -C ${c.dir} add ${c.paths.join(" ")} && git -C ${c.dir} commit -m "${message2}"`
4420
4583
  );
4421
4584
  if (ending.commit !== "nothing")
4422
4585
  next.push("# teammates: git pull \u2014 the next rness command installs it");
@@ -4589,7 +4752,7 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
4589
4752
  await source.cleanup?.();
4590
4753
  }
4591
4754
  }
4592
- const packageFile = join21(ws.rnessDir, "package.json");
4755
+ const packageFile = join22(ws.rnessDir, "package.json");
4593
4756
  const restore = /* @__PURE__ */ new Map([
4594
4757
  [packageFile, await readFile9(packageFile, "utf8")]
4595
4758
  ]);
@@ -4605,8 +4768,8 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
4605
4768
  () => ["installed", `dependencies with ${pm}`]
4606
4769
  );
4607
4770
  } catch (e) {
4608
- const message2 = e instanceof Error ? e.message : String(e);
4609
- ui.error(message2);
4771
+ const message3 = e instanceof Error ? e.message : String(e);
4772
+ ui.error(message3);
4610
4773
  if (!merged) {
4611
4774
  for (const [file, text] of restore) await writeFileAtomic(file, text);
4612
4775
  ui.hint(
@@ -4619,7 +4782,7 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
4619
4782
  `the scaffold merge is still staged; undo it with: git -C ${relative5(cwd, ws.rnessDir) || "."} merge --abort`,
4620
4783
  "stderr"
4621
4784
  );
4622
- if (/minimum.?release.?age|NO_MATURE/i.test(message2))
4785
+ if (/minimum.?release.?age|NO_MATURE/i.test(message3))
4623
4786
  ui.hint(
4624
4787
  "pnpm refuses versions published less than 24 h ago: add '@rness/cli' under minimumReleaseAgeExclude in .rness/pnpm-workspace.yaml",
4625
4788
  "stderr"
@@ -4638,13 +4801,13 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
4638
4801
  ui.gitIsSilent = true;
4639
4802
  const code = await syncBlocks(ui, ws.root, basename5(ws.root));
4640
4803
  if (code !== 0) return code;
4641
- const message = `chore: rness ${target}`;
4642
- const committed = await commitUpgrade(ws.rnessDir, message);
4804
+ const message2 = `chore: rness ${target}`;
4805
+ const committed = await commitUpgrade(ws.rnessDir, message2);
4643
4806
  if (committed.kind === "committed")
4644
4807
  ui.line(
4645
4808
  "committed",
4646
- `${label} \u2014 ${message}`,
4647
- `Committed ${label}: ${message}`
4809
+ `${label} \u2014 ${message2}`,
4810
+ `Committed ${label}: ${message2}`
4648
4811
  );
4649
4812
  if (committed.kind === "refused") {
4650
4813
  ui.error(
@@ -4666,85 +4829,28 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
4666
4829
  }
4667
4830
  }
4668
4831
 
4669
- // src/core/blocks.ts
4670
- import { join as join22 } from "path";
4671
- var PROBLEM = /* @__PURE__ */ new Set(["stale", "malformed"]);
4672
- async function checkBlocks({
4673
- root,
4674
- rnessDir,
4675
- manifest,
4676
- org
4677
- }) {
4678
- const checks = [];
4679
- const note = (label, status2, message = null) => {
4680
- checks.push({ label, status: status2, message });
4681
- };
4682
- const derive = () => ({
4683
- problems: checks.filter((c) => PROBLEM.has(c.status) && c.message !== null).map((c) => c.message),
4684
- warnings: checks.filter((c) => !PROBLEM.has(c.status) && c.message !== null).map((c) => c.message),
4685
- checks
4832
+ // src/core/check-workspace.ts
4833
+ async function checkWorkspace(ws, manifest) {
4834
+ for (const s of Object.keys(manifest.scopes)) scopeChain(manifest, s);
4835
+ const problems = await checkContract(ws.rnessDir);
4836
+ const blocks = await checkBlocks({
4837
+ root: ws.root,
4838
+ rnessDir: ws.rnessDir,
4839
+ manifest,
4840
+ org: workspaceName(manifest, ws.root)
4686
4841
  });
4687
- if (!await exists(join22(root, "org"))) return derive();
4688
- const targets = [
4689
- { scope: null, dir: root, depth: 0, file: "AGENTS.md", label: "AGENTS.md" },
4690
- { scope: null, dir: root, depth: 0, file: "CLAUDE.md", label: "CLAUDE.md" }
4691
- ];
4692
- for (const [name, entry] of Object.entries(manifest.scopes)) {
4693
- const segments = entry.path.split("/");
4694
- targets.push({
4695
- scope: name,
4696
- dir: join22(root, ...segments),
4697
- depth: segments.length,
4698
- file: "AGENTS.md",
4699
- label: `${entry.path}/AGENTS.md`
4700
- });
4701
- }
4702
- for (const t of targets) {
4703
- if (!await exists(t.dir)) {
4704
- note(
4705
- t.label,
4706
- "missing-dir",
4707
- `${t.label}: directory not present, block not checked`
4708
- );
4709
- continue;
4710
- }
4711
- const text = await readOrNull(join22(t.dir, t.file));
4712
- const lines = text === null ? [] : text.split(/\r?\n/);
4713
- const found = findBlock(lines);
4714
- if (found.kind === "none") {
4715
- note(
4716
- t.label,
4717
- "no-block",
4718
- `${t.label}: no rness block yet (run ${rnessCommand()} sync)`
4719
- );
4720
- continue;
4721
- }
4722
- if (found.kind === "error") {
4723
- note(t.label, "malformed", `${t.label}: ${found.message}`);
4724
- continue;
4725
- }
4726
- const context2 = await assembleContext({
4727
- rnessDir,
4728
- manifest,
4729
- scope: t.scope
4730
- });
4731
- const fresh2 = renderBlock({
4732
- scope: t.scope,
4733
- org,
4734
- depth: t.depth,
4735
- context: context2
4736
- });
4737
- const pointerLeft = t.file === "CLAUDE.md" && lines.some((l) => l.trim() === "@AGENTS.md");
4738
- if (isCurrentBlock(lines.slice(found.begin, found.end + 1), fresh2.hash) && !pointerLeft)
4739
- note(t.label, "current");
4740
- else
4741
- note(
4742
- t.label,
4743
- "stale",
4744
- `${t.label}: stale rness block (run ${rnessCommand()} sync)`
4842
+ problems.push(...blocks.problems);
4843
+ for (const name of unsupportedAgents(manifest.agents ?? []))
4844
+ problems.push(unsupportedMessage(name));
4845
+ const targets = await agentTargets(ws.root, manifest, { check: true });
4846
+ for (const o of targets)
4847
+ if (o.status === "stale")
4848
+ problems.push(
4849
+ `${o.label}: ${o.detail ?? ""} (run ${rnessCommand()} sync)`
4745
4850
  );
4746
- }
4747
- return derive();
4851
+ else if (o.status === "invalid")
4852
+ problems.push(`${o.label}: ${o.detail ?? "unreadable"}`);
4853
+ return { problems, blocks, targets };
4748
4854
  }
4749
4855
 
4750
4856
  // src/commands/validate.ts
@@ -4753,26 +4859,8 @@ async function validateCommand(opts, deps = {}) {
4753
4859
  try {
4754
4860
  const ws = await findWorkspace(cwd);
4755
4861
  const manifest = await loadManifest(ws.rnessDir);
4756
- for (const s of Object.keys(manifest.scopes)) scopeChain(manifest, s);
4757
4862
  const org = workspaceName(manifest, ws.root);
4758
- const problems = await checkContract(ws.rnessDir);
4759
- const blocks = await checkBlocks({
4760
- root: ws.root,
4761
- rnessDir: ws.rnessDir,
4762
- manifest,
4763
- org
4764
- });
4765
- problems.push(...blocks.problems);
4766
- for (const name of unsupportedAgents(manifest.agents ?? []))
4767
- problems.push(unsupportedMessage(name));
4768
- const targets = await agentTargets(ws.root, manifest, { check: true });
4769
- for (const o of targets)
4770
- if (o.status === "stale")
4771
- problems.push(
4772
- `${o.label}: ${o.detail ?? ""} (run ${rnessCommand()} sync)`
4773
- );
4774
- else if (o.status === "invalid")
4775
- problems.push(`${o.label}: ${o.detail ?? "unreadable"}`);
4863
+ const { problems, blocks, targets } = await checkWorkspace(ws, manifest);
4776
4864
  const scaffoldWarnings = [];
4777
4865
  if (await isRepository2(ws.rnessDir) && !await isShallow(ws.rnessDir)) {
4778
4866
  const pin = await readPin(ws.rnessDir);
@@ -4849,134 +4937,9 @@ password=${resolved.token}
4849
4937
  return 0;
4850
4938
  }
4851
4939
 
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
- }
4940
+ // src/commands/hook.ts
4941
+ import { realpath as realpath4 } from "fs/promises";
4942
+ import { isAbsolute as isAbsolute2, posix as posix5, relative as relative7, resolve as resolve7, sep as sep5 } from "path";
4980
4943
 
4981
4944
  // src/mcp/tools.ts
4982
4945
  import { readFile as readFile10, realpath as realpath3, stat as stat4 } from "fs/promises";
@@ -5057,21 +5020,31 @@ async function context(cwd, args) {
5057
5020
  manifest,
5058
5021
  relative6(ws.root, cwd).split(sep4).join(posix4.sep)
5059
5022
  );
5023
+ const summary = await scopeSummary(ws, manifest, scope);
5024
+ return {
5025
+ text: [
5026
+ ...summary.lines,
5027
+ "Read one with rness_read; search with rness_search."
5028
+ ].join("\n")
5029
+ };
5030
+ }
5031
+ async function scopeSummary(ws, manifest, scope) {
5060
5032
  const entry = scope === null ? void 0 : manifest.scopes[scope];
5061
5033
  const where = scope === null || entry === void 0 ? "global scope" : `scope ${scope} (${entry.path}${entry.extends.length > 0 ? `, extends ${entry.extends.join(", ")}` : ""})`;
5062
5034
  const ctx = await assembleContext({ rnessDir: ws.rnessDir, manifest, scope });
5063
5035
  const index = new Map(
5064
5036
  (await documents(ws.rnessDir)).map((d) => [d.path, d])
5065
5037
  );
5066
- const out = [`Workspace ${workspaceName(manifest, ws.root)} \xB7 ${where}`];
5038
+ const lines = [`Workspace ${workspaceName(manifest, ws.root)} \xB7 ${where}`];
5039
+ const counts = {};
5067
5040
  for (const collection of ctx.collections) {
5068
5041
  const docs = collection.files.map((f) => index.get(`${collection.name}/${f.rel}`)).filter((d) => d !== void 0);
5069
5042
  if (docs.length === 0) continue;
5070
- out.push(`${LABELS[collection.name]} (${docs.length}):`);
5071
- for (const d of docs) out.push(line(d));
5043
+ counts[collection.name] = docs.length;
5044
+ lines.push(`${LABELS[collection.name]} (${docs.length}):`);
5045
+ for (const d of docs) lines.push(line(d));
5072
5046
  }
5073
- out.push("Read one with rness_read; search with rness_search.");
5074
- return { text: out.join("\n") };
5047
+ return { where, counts, lines };
5075
5048
  }
5076
5049
  async function list(cwd, args) {
5077
5050
  const { ws } = await workspaceAt(cwd);
@@ -5232,6 +5205,285 @@ function mcpTools(cwd) {
5232
5205
  ];
5233
5206
  }
5234
5207
 
5208
+ // src/commands/hook.ts
5209
+ var NOUNS = {
5210
+ standards: ["standard", "standards"],
5211
+ adr: ["decision", "decisions"],
5212
+ specs: ["specification", "specifications"],
5213
+ plans: ["plan", "plans"],
5214
+ skills: ["skill", "skills"]
5215
+ };
5216
+ var SHOWN_PROBLEMS = 3;
5217
+ async function readInput(input) {
5218
+ if (input.isTTY === true) return {};
5219
+ let text = "";
5220
+ for await (const chunk of input) text += String(chunk);
5221
+ try {
5222
+ const data = JSON.parse(text);
5223
+ return data !== null && typeof data === "object" && !Array.isArray(data) ? data : {};
5224
+ } catch {
5225
+ return {};
5226
+ }
5227
+ }
5228
+ var message = (e) => e instanceof Error ? e.message : String(e);
5229
+ function cwdOf(input, env) {
5230
+ const cwd = input["cwd"];
5231
+ if (typeof cwd === "string" && cwd !== "") return cwd;
5232
+ return env["CLAUDE_PROJECT_DIR"] ?? process.cwd();
5233
+ }
5234
+ var toPosix2 = (p) => p.split(sep5).join(posix5.sep);
5235
+ function count(n, [one, many]) {
5236
+ return `${n} ${n === 1 ? one : many}`;
5237
+ }
5238
+ async function safetyNet(ws, manifest) {
5239
+ const notes = [];
5240
+ const drift = await pinDrift(ws.rnessDir);
5241
+ if (drift !== null)
5242
+ notes.push(
5243
+ `.rness pins @rness/cli ${drift.pin} but ${drift.installed ?? "nothing"} is installed \u2014 run ${await workspacePackageManager(ws.rnessDir)} install in .rness`
5244
+ );
5245
+ try {
5246
+ const { problems } = await checkWorkspace(ws, manifest);
5247
+ if (problems.length > 0)
5248
+ notes.push(
5249
+ `${count(problems.length, ["problem", "problems"])} in the workspace context \u2014 run rness validate`,
5250
+ ...problems.slice(0, SHOWN_PROBLEMS).map((p) => ` ${p}`)
5251
+ );
5252
+ } catch (e) {
5253
+ notes.push(message(e));
5254
+ }
5255
+ return notes;
5256
+ }
5257
+ async function sessionStart(input, env) {
5258
+ const cwd = cwdOf(input, env);
5259
+ let banner2 = null;
5260
+ let context2 = [];
5261
+ let notes;
5262
+ try {
5263
+ const ws = await findWorkspace(cwd);
5264
+ const manifest = await loadManifest(ws.rnessDir);
5265
+ const scope = resolveScope(manifest, toPosix2(relative7(ws.root, cwd)));
5266
+ const summary = await scopeSummary(ws, manifest, scope);
5267
+ const counts = COLLECTIONS.flatMap((c) => {
5268
+ const n = summary.counts[c];
5269
+ return n === void 0 ? [] : [count(n, NOUNS[c])];
5270
+ });
5271
+ banner2 = `rness ${VERSION} \xB7 ${workspaceName(manifest, ws.root)} \xB7 ${scope === null ? "global scope" : `scope ${scope}`} \u2014 ${counts.length === 0 ? "nothing applies yet" : counts.join(", ")}`;
5272
+ const at = toPosix2(relative7(cwd, ws.rnessDir)) || ".rness";
5273
+ context2 = [
5274
+ ...summary.lines,
5275
+ `These documents are in ${at}: read one there, or with rness_read when the rness MCP server is connected.`
5276
+ ];
5277
+ notes = await safetyNet(ws, manifest);
5278
+ } catch (e) {
5279
+ notes = [message(e)];
5280
+ }
5281
+ const warnings = notes.map((n) => `rness: ${n}`);
5282
+ const shown = [
5283
+ ...banner2 !== null && input["source"] !== "compact" ? [banner2] : [],
5284
+ ...warnings
5285
+ ];
5286
+ return {
5287
+ ...shown.length > 0 ? { systemMessage: shown.join("\n") } : {},
5288
+ hookSpecificOutput: {
5289
+ hookEventName: "SessionStart",
5290
+ additionalContext: [...context2, ...warnings].join("\n")
5291
+ }
5292
+ };
5293
+ }
5294
+ function inside(from, to) {
5295
+ const rel = relative7(from, to);
5296
+ return rel === "" || rel.startsWith("..") || isAbsolute2(rel) ? null : toPosix2(rel);
5297
+ }
5298
+ async function postToolUse(input, io) {
5299
+ const toolInput = input["tool_input"];
5300
+ const path = toolInput !== null && typeof toolInput === "object" ? toolInput["file_path"] : void 0;
5301
+ if (typeof path !== "string" || path === "") return 0;
5302
+ const cwd = cwdOf(input, io.env);
5303
+ let ws;
5304
+ try {
5305
+ ws = await findWorkspace(cwd);
5306
+ } catch {
5307
+ return 0;
5308
+ }
5309
+ const file = resolve7(cwd, path);
5310
+ const rel = inside(ws.rnessDir, file) ?? inside(
5311
+ await realpath4(ws.rnessDir).catch(() => ws.rnessDir),
5312
+ await realpath4(file).catch(() => file)
5313
+ );
5314
+ if (rel === null) return 0;
5315
+ const first = rel.split("/")[0] ?? "";
5316
+ let problems = [];
5317
+ if (rel === "rness.json") {
5318
+ try {
5319
+ const manifest = await loadManifest(ws.rnessDir);
5320
+ for (const s of Object.keys(manifest.scopes)) scopeChain(manifest, s);
5321
+ } catch (e) {
5322
+ problems = [message(e)];
5323
+ }
5324
+ } else if (COLLECTIONS.includes(first) && rel.endsWith(".md"))
5325
+ problems = (await checkContract(ws.rnessDir)).filter(
5326
+ (p) => p.startsWith(`${rel}:`)
5327
+ );
5328
+ if (problems.length === 0) return 0;
5329
+ io.error.write(
5330
+ [
5331
+ ...problems.map((p) => `rness: ${p}`),
5332
+ `Fix it in ${rel}; rness validate checks the whole workspace.`,
5333
+ ""
5334
+ ].join("\n")
5335
+ );
5336
+ return 2;
5337
+ }
5338
+ async function hookCommand(event, io = {
5339
+ input: process.stdin,
5340
+ output: process.stdout,
5341
+ error: process.stderr,
5342
+ env: process.env
5343
+ }) {
5344
+ if (event !== "session-start" && event !== "post-tool-use") {
5345
+ io.error.write(
5346
+ `unknown hook event ${JSON.stringify(event)} (known: session-start, post-tool-use)
5347
+ `
5348
+ );
5349
+ return 1;
5350
+ }
5351
+ const input = await readInput(io.input);
5352
+ if (event === "post-tool-use") return postToolUse(input, io);
5353
+ io.output.write(`${JSON.stringify(await sessionStart(input, io.env))}
5354
+ `);
5355
+ return 0;
5356
+ }
5357
+
5358
+ // src/commands/mcp.ts
5359
+ import { createInterface } from "readline";
5360
+
5361
+ // src/mcp/protocol.ts
5362
+ var MODERN_VERSIONS = ["2026-07-28"];
5363
+ var LEGACY_VERSIONS = [
5364
+ "2025-11-25",
5365
+ "2025-06-18",
5366
+ "2025-03-26",
5367
+ "2024-11-05"
5368
+ ];
5369
+ var SUPPORTED = [...MODERN_VERSIONS, ...LEGACY_VERSIONS];
5370
+ var VERSION_KEY = "io.modelcontextprotocol/protocolVersion";
5371
+ var CAPABILITIES_KEY = "io.modelcontextprotocol/clientCapabilities";
5372
+ var SERVER_INFO_KEY = "io.modelcontextprotocol/serverInfo";
5373
+ function isObject2(value) {
5374
+ return value !== null && typeof value === "object" && !Array.isArray(value);
5375
+ }
5376
+ function errorLine(id, code, message2, data) {
5377
+ return JSON.stringify({
5378
+ jsonrpc: "2.0",
5379
+ id,
5380
+ error: data === void 0 ? { code, message: message2 } : { code, message: message2, data }
5381
+ });
5382
+ }
5383
+ function createProtocol(opts) {
5384
+ const serverInfo = { name: opts.name, version: opts.version };
5385
+ return {
5386
+ async handle(line2) {
5387
+ let message2;
5388
+ try {
5389
+ message2 = JSON.parse(line2);
5390
+ } catch {
5391
+ return errorLine(null, -32700, "Parse error");
5392
+ }
5393
+ const rawId = isObject2(message2) ? message2["id"] : void 0;
5394
+ const id = typeof rawId === "string" || typeof rawId === "number" ? rawId : null;
5395
+ if (!isObject2(message2) || message2["jsonrpc"] !== "2.0" || typeof message2["method"] !== "string")
5396
+ return errorLine(id, -32600, "Invalid Request");
5397
+ if (!("id" in message2)) return null;
5398
+ if (id === null) return errorLine(null, -32600, "Invalid Request");
5399
+ const method = message2["method"];
5400
+ const params = isObject2(message2["params"]) ? message2["params"] : {};
5401
+ const meta = isObject2(params["_meta"]) ? params["_meta"] : null;
5402
+ const modern = meta !== null && typeof meta[VERSION_KEY] === "string";
5403
+ if (modern) {
5404
+ const requested = meta[VERSION_KEY];
5405
+ if (!MODERN_VERSIONS.includes(requested))
5406
+ return errorLine(id, -32022, "Unsupported protocol version", {
5407
+ supported: SUPPORTED,
5408
+ requested
5409
+ });
5410
+ if (!isObject2(meta[CAPABILITIES_KEY]))
5411
+ return errorLine(
5412
+ id,
5413
+ -32602,
5414
+ `Invalid params: ${CAPABILITIES_KEY} is required`
5415
+ );
5416
+ }
5417
+ const answer = (result) => JSON.stringify({
5418
+ jsonrpc: "2.0",
5419
+ id,
5420
+ result: modern ? {
5421
+ resultType: "complete",
5422
+ ...result,
5423
+ _meta: { [SERVER_INFO_KEY]: serverInfo }
5424
+ } : result
5425
+ });
5426
+ switch (method) {
5427
+ case "server/discover":
5428
+ return answer({
5429
+ supportedVersions: SUPPORTED,
5430
+ capabilities: { tools: {} }
5431
+ });
5432
+ case "initialize": {
5433
+ const asked = params["protocolVersion"];
5434
+ const protocolVersion = typeof asked === "string" && LEGACY_VERSIONS.includes(asked) ? asked : LEGACY_VERSIONS[0];
5435
+ return JSON.stringify({
5436
+ jsonrpc: "2.0",
5437
+ id,
5438
+ result: {
5439
+ protocolVersion,
5440
+ capabilities: { tools: {} },
5441
+ serverInfo
5442
+ }
5443
+ });
5444
+ }
5445
+ case "ping":
5446
+ return answer({});
5447
+ case "tools/list":
5448
+ return answer({
5449
+ tools: opts.tools.map((t) => ({
5450
+ name: t.name,
5451
+ title: t.title,
5452
+ description: t.description,
5453
+ inputSchema: t.inputSchema
5454
+ }))
5455
+ });
5456
+ case "tools/call": {
5457
+ const name = params["name"];
5458
+ const tool = opts.tools.find((t) => t.name === name);
5459
+ if (tool === void 0)
5460
+ return errorLine(id, -32602, `Unknown tool: ${String(name)}`);
5461
+ const args = isObject2(params["arguments"]) ? params["arguments"] : {};
5462
+ try {
5463
+ const r = await tool.call(args);
5464
+ return answer({
5465
+ content: [{ type: "text", text: r.text }],
5466
+ isError: r.isError === true
5467
+ });
5468
+ } catch (e) {
5469
+ return answer({
5470
+ content: [
5471
+ {
5472
+ type: "text",
5473
+ text: `${tool.name} failed: ${e instanceof Error ? e.message : String(e)}`
5474
+ }
5475
+ ],
5476
+ isError: true
5477
+ });
5478
+ }
5479
+ }
5480
+ default:
5481
+ return errorLine(id, -32601, "Method not found");
5482
+ }
5483
+ }
5484
+ };
5485
+ }
5486
+
5235
5487
  // src/commands/mcp.ts
5236
5488
  async function mcpCommand(opts, io = {
5237
5489
  input: process.stdin,
@@ -5321,6 +5573,9 @@ function buildProgram(state) {
5321
5573
  program.command("git-credential", { hidden: true }).argument("<operation>").action(async (operation) => {
5322
5574
  state.code = await gitCredentialCommand(operation);
5323
5575
  });
5576
+ program.command("hook", { hidden: true }).argument("<event>", "session-start or post-tool-use").action(async (event) => {
5577
+ state.code = await hookCommand(event);
5578
+ });
5324
5579
  program.command("upgrade").alias("update").description(
5325
5580
  "Move this workspace to another @rness/cli: pin it in .rness/package.json, install, sync"
5326
5581
  ).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) => {
@@ -5398,7 +5653,8 @@ var OWN_COMMANDS = [
5398
5653
  "update",
5399
5654
  "login",
5400
5655
  "logout",
5401
- "git-credential"
5656
+ "git-credential",
5657
+ "hook"
5402
5658
  ];
5403
5659
  var CATCH_UP_DONE = "RNESS_CATCH_UP_DONE";
5404
5660
  async function catchUp(argv, deps = {}) {
@@ -5493,14 +5749,14 @@ export {
5493
5749
  copyScaffold,
5494
5750
  loginCommand,
5495
5751
  createCommand,
5496
- logoutCommand,
5752
+ checkBlocks,
5497
5753
  STATUSES,
5498
5754
  checkContract,
5755
+ logoutCommand,
5499
5756
  upgradeCommand,
5500
- checkBlocks,
5501
5757
  validateCommand,
5502
5758
  run2 as run,
5503
5759
  OWN_COMMANDS,
5504
5760
  catchUp
5505
5761
  };
5506
- //# sourceMappingURL=chunk-N3F6N6GA.js.map
5762
+ //# sourceMappingURL=chunk-R4BRAAC2.js.map