@rness/cli 0.18.0 → 0.19.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.
@@ -186,9 +186,9 @@ async function originUrl(dir) {
186
186
  async function init(dir) {
187
187
  await git(["init", "-q", "-b", "main"], dir);
188
188
  }
189
- async function commitAll(dir, message2) {
189
+ async function commitAll(dir, message3) {
190
190
  await git(["add", "-A"], dir);
191
- await git(["commit", "-q", "-m", message2], dir);
191
+ await git(["commit", "-q", "-m", message3], dir);
192
192
  }
193
193
 
194
194
  // src/core/remote.ts
@@ -296,8 +296,8 @@ var KEYS = [
296
296
  "repos",
297
297
  "scopes"
298
298
  ];
299
- function fail(message2) {
300
- throw new Error(`rness.json: ${message2}`);
299
+ function fail(message3) {
300
+ throw new Error(`rness.json: ${message3}`);
301
301
  }
302
302
  function isRecord(value) {
303
303
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -424,6 +424,9 @@ async function loadManifest(rnessDir) {
424
424
  } catch {
425
425
  fail("not found");
426
426
  }
427
+ return parseManifest(raw);
428
+ }
429
+ function parseManifest(raw) {
427
430
  let data;
428
431
  try {
429
432
  data = JSON.parse(raw);
@@ -508,7 +511,7 @@ function parseRepoSpec(spec, org, host) {
508
511
  }
509
512
 
510
513
  // src/version.ts
511
- var VERSION = "0.18.0";
514
+ var VERSION = "0.19.0";
512
515
 
513
516
  // src/core/pm.ts
514
517
  import { execFile as execFile2 } from "child_process";
@@ -588,8 +591,8 @@ async function installDependencies(pm, dir, opts = {}) {
588
591
  // src/core/frontmatter.ts
589
592
  import { parse } from "yaml";
590
593
  var FRONT_MATTER = /^---\r?\n(?:([\s\S]*?)\r?\n)?---(?:\r?\n|$)/;
591
- function fail2(message2) {
592
- throw new Error(`front matter: ${message2}`);
594
+ function fail2(message3) {
595
+ throw new Error(`front matter: ${message3}`);
593
596
  }
594
597
  function stripBom(source) {
595
598
  return source.startsWith("\uFEFF") ? source.slice(1) : source;
@@ -628,29 +631,31 @@ import { join as join3, posix, sep } from "path";
628
631
  function toPosix(rel) {
629
632
  return rel.split(sep).join(posix.sep);
630
633
  }
634
+ function markdownItem(path, rel, body) {
635
+ let fields2 = null;
636
+ let fieldsError = null;
637
+ try {
638
+ fields2 = parseFrontMatter(body);
639
+ } catch (e) {
640
+ fieldsError = e instanceof Error ? e.message : String(e);
641
+ }
642
+ return {
643
+ path,
644
+ rel,
645
+ fields: fields2,
646
+ fieldsError,
647
+ title: extractTitle(body),
648
+ body,
649
+ content: stripFrontMatter(body)
650
+ };
651
+ }
631
652
  async function collectMarkdown(dir) {
632
653
  const rels = [];
633
654
  for await (const rel of glob("**/*.md", { cwd: dir })) rels.push(toPosix(rel));
634
655
  const items = await Promise.all(
635
656
  rels.map(async (rel) => {
636
657
  const path = join3(dir, ...rel.split(posix.sep));
637
- const body = await readFile3(path, "utf8");
638
- let fields2 = null;
639
- let fieldsError = null;
640
- try {
641
- fields2 = parseFrontMatter(body);
642
- } catch (e) {
643
- fieldsError = e instanceof Error ? e.message : String(e);
644
- }
645
- return {
646
- path,
647
- rel,
648
- fields: fields2,
649
- fieldsError,
650
- title: extractTitle(body),
651
- body,
652
- content: stripFrontMatter(body)
653
- };
658
+ return markdownItem(path, rel, await readFile3(path, "utf8"));
654
659
  })
655
660
  );
656
661
  return items.sort((a, b) => a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0);
@@ -744,7 +749,13 @@ import { join as join5 } from "path";
744
749
 
745
750
  // src/core/documents.ts
746
751
  import { posix as posix2 } from "path";
752
+ function documentDate(rel) {
753
+ return /^((?:19|20)\d{2}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01]))-/.exec(
754
+ posix2.basename(rel)
755
+ )?.[1] ?? null;
756
+ }
747
757
  function documentNumber(rel) {
758
+ if (documentDate(rel) !== null) return null;
748
759
  return /^(\d{4})-/.exec(posix2.basename(rel))?.[1] ?? null;
749
760
  }
750
761
  function documentTitle(heading, number) {
@@ -768,11 +779,25 @@ var STATUSES = {
768
779
  };
769
780
  var SKIP = /* @__PURE__ */ new Set(["adr/0000-template.md"]);
770
781
  var PLACEMENT_KEYS = ["scopes", "scope"];
771
- async function checkContract(rnessDir) {
782
+ async function itemsOf(rnessDir, name, override) {
783
+ const items = await collectMarkdown(join5(rnessDir, name));
784
+ const prefix = `${name}/`;
785
+ if (override === void 0 || !override.rel.startsWith(prefix)) return items;
786
+ const rel = override.rel.slice(prefix.length);
787
+ const item = markdownItem(
788
+ join5(rnessDir, ...override.rel.split("/")),
789
+ rel,
790
+ override.text
791
+ );
792
+ return [...items.filter((i) => i.rel !== rel), item].sort(
793
+ (a, b) => a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0
794
+ );
795
+ }
796
+ async function checkContract(rnessDir, override) {
772
797
  const problems = [];
773
798
  for (const name of COLLECTIONS) {
774
799
  const allowed = STATUSES[name];
775
- const items = await collectMarkdown(join5(rnessDir, name));
800
+ const items = await itemsOf(rnessDir, name, override);
776
801
  const byNumber = /* @__PURE__ */ new Map();
777
802
  for (const item of items) {
778
803
  const where = `${name}/${item.rel}`;
@@ -788,9 +813,16 @@ async function checkContract(rnessDir) {
788
813
  `${where}: the number ${number} is also that of ${files.filter((f) => f !== where).join(", ")}`
789
814
  );
790
815
  }
816
+ const next = String(
817
+ Math.max(0, ...[...byNumber.keys()].map(Number)) + 1
818
+ ).padStart(4, "0");
791
819
  for (const item of items) {
792
820
  const where = `${name}/${item.rel}`;
793
821
  if (SKIP.has(where)) continue;
822
+ if (allowed !== void 0 && documentNumber(item.rel) === null)
823
+ problems.push(
824
+ `${where}: not numbered; name it ${next}-<slug>.md, the next number of ${name}`
825
+ );
794
826
  if (item.fieldsError !== null) {
795
827
  problems.push(`${where}: invalid front matter (${item.fieldsError})`);
796
828
  continue;
@@ -1127,8 +1159,8 @@ function sessionsOf(item) {
1127
1159
  }
1128
1160
  function rowOf(dir, item) {
1129
1161
  const base = posix3.basename(item.rel, ".md");
1130
- const date = /^(\d{4}-\d{2}-\d{2})-/.exec(base)?.[1] ?? null;
1131
- const number = date === null ? documentNumber(item.rel) : null;
1162
+ const date = documentDate(item.rel);
1163
+ const number = documentNumber(item.rel);
1132
1164
  const sessions = sessionsOf(item);
1133
1165
  return {
1134
1166
  id: date ?? number ?? base,
@@ -1208,6 +1240,21 @@ function statusMarkdown(workspace, tabs) {
1208
1240
  }
1209
1241
  return out.join("\n");
1210
1242
  }
1243
+ async function plansInProgress(ws, manifest, scope) {
1244
+ const context3 = await assembleContext({
1245
+ rnessDir: ws.rnessDir,
1246
+ manifest,
1247
+ scope
1248
+ });
1249
+ const plans = context3.collections.find((c) => c.name === "plans");
1250
+ return (plans?.files ?? []).filter((f) => {
1251
+ try {
1252
+ return parseFrontMatter(f.body)?.["status"] === "In progress";
1253
+ } catch {
1254
+ return false;
1255
+ }
1256
+ }).map((f) => `plans/${f.rel}`);
1257
+ }
1211
1258
 
1212
1259
  // src/pulse/body.ts
1213
1260
  import { createHash } from "crypto";
@@ -2027,9 +2074,9 @@ async function postJson(path, payload, options, explain = false, limited = () =>
2027
2074
  continue;
2028
2075
  }
2029
2076
  if (!ok) {
2030
- const message2 = explain && page.status !== 401 && page.status !== 429 && page.body !== null && typeof page.body === "object" ? page.body.message : void 0;
2031
- if (typeof message2 === "string" && message2 !== "")
2032
- throw new GitHubMessageError(message2, {
2077
+ const message3 = explain && page.status !== 401 && page.status !== 429 && page.body !== null && typeof page.body === "object" ? page.body.message : void 0;
2078
+ if (typeof message3 === "string" && message3 !== "")
2079
+ throw new GitHubMessageError(message3, {
2033
2080
  cause: { status: page.status, path }
2034
2081
  });
2035
2082
  throw httpError(page, path);
@@ -2053,9 +2100,9 @@ async function getScopes(options) {
2053
2100
  const cause = { status: res.status, path: "/user" };
2054
2101
  if (res.status === 401)
2055
2102
  throw new Error("GitHub rejected the token (401)", { cause });
2056
- const message2 = body !== null && typeof body === "object" ? body["message"] : void 0;
2057
- if (typeof message2 === "string" && message2 !== "")
2058
- throw new GitHubMessageError(message2, { cause });
2103
+ const message3 = body !== null && typeof body === "object" ? body["message"] : void 0;
2104
+ if (typeof message3 === "string" && message3 !== "")
2105
+ throw new GitHubMessageError(message3, { cause });
2059
2106
  throw new Error(`GitHub API answered ${res.status} for /user`, { cause });
2060
2107
  }
2061
2108
  const header2 = res.headers.get("x-oauth-scopes");
@@ -2102,8 +2149,8 @@ async function getOrgMembership(org, options) {
2102
2149
  }
2103
2150
  if (status2 === 404) return "not-member";
2104
2151
  if (status2 === 403) {
2105
- const message2 = body !== null && typeof body === "object" ? String(body["message"] ?? "") : "";
2106
- if (/OAuth App access restrictions/i.test(message2)) return "restricted";
2152
+ const message3 = body !== null && typeof body === "object" ? String(body["message"] ?? "") : "";
2153
+ if (/OAuth App access restrictions/i.test(message3)) return "restricted";
2107
2154
  }
2108
2155
  return "unknown";
2109
2156
  }
@@ -2125,8 +2172,8 @@ async function createRepository(owner, name, options) {
2125
2172
  throw new Error(`cannot reach the GitHub API: ${reason2(e)}`, { cause: e });
2126
2173
  }
2127
2174
  const body = await res.json().catch(() => void 0);
2128
- const message2 = body !== null && typeof body === "object" ? String(body["message"] ?? "") : "";
2129
- return { status: res.status, message: message2 };
2175
+ const message3 = body !== null && typeof body === "object" ? String(body["message"] ?? "") : "";
2176
+ return { status: res.status, message: message3 };
2130
2177
  };
2131
2178
  let answer = await post2(`/orgs/${encodeURIComponent(owner)}/repos`);
2132
2179
  if (answer.status === 404 && options.self !== void 0 && options.self !== null && options.self.toLowerCase() === owner.toLowerCase())
@@ -3948,6 +3995,9 @@ function inverse(text, stream = process.stdout) {
3948
3995
  const painted = styleText("inverse", text, { stream, validateStream: true });
3949
3996
  return painted === text ? plain : painted;
3950
3997
  }
3998
+ function count(n, [one, many]) {
3999
+ return `${n} ${n === 1 ? one : many}`;
4000
+ }
3951
4001
  function grey(text, stream = process.stdout) {
3952
4002
  if (noColor()) return text;
3953
4003
  return styleText("gray", text, { stream, validateStream: true });
@@ -4021,12 +4071,12 @@ async function detectGithubSsh(opts) {
4021
4071
  child.stderr?.setEncoding("utf8");
4022
4072
  child.stderr?.on("data", (chunk) => chunks.push(chunk));
4023
4073
  let settled = false;
4024
- const settle = (access3) => {
4074
+ const settle = (access2) => {
4025
4075
  if (settled) return;
4026
4076
  settled = true;
4027
4077
  clearTimeout(timer);
4028
4078
  child.stderr?.destroy();
4029
- resolve8(access3);
4079
+ resolve8(access2);
4030
4080
  };
4031
4081
  const timer = setTimeout(() => {
4032
4082
  child.kill("SIGKILL");
@@ -4071,8 +4121,8 @@ async function testGithubSsh(transport, interactive, announce) {
4071
4121
  };
4072
4122
  }
4073
4123
  var loggedInAs = (login) => login === void 0 || login === null ? "" : `, logged in as ${login}`;
4074
- function usingSentence(access3, login) {
4075
- return access3.ok ? `SSH works for github.com (as ${access3.login}) \u2014 repositories are cloned over SSH` : `No SSH access to github.com (${access3.reason}) \u2014 repositories are cloned over HTTPS${loggedInAs(login)}`;
4124
+ function usingSentence(access2, login) {
4125
+ return access2.ok ? `SSH works for github.com (as ${access2.login}) \u2014 repositories are cloned over SSH` : `No SSH access to github.com (${access2.reason}) \u2014 repositories are cloned over HTTPS${loggedInAs(login)}`;
4076
4126
  }
4077
4127
  function isSshUrl(url, hosts) {
4078
4128
  return url.startsWith(hosts.ssh);
@@ -4083,8 +4133,8 @@ var CHECKING = [
4083
4133
  "ssh access to github.com\u2026",
4084
4134
  "Checking SSH access to github.com \u2014 ssh may ask for your passphrase"
4085
4135
  ];
4086
- function usingRest(access3, login) {
4087
- return access3.ok ? `ssh (github.com as ${access3.login})` : `https (ssh to github.com unavailable: ${access3.reason}${loggedInAs(login)})`;
4136
+ function usingRest(access2, login) {
4137
+ return access2.ok ? `ssh (github.com as ${access2.login})` : `https (ssh to github.com unavailable: ${access2.reason}${loggedInAs(login)})`;
4088
4138
  }
4089
4139
  function agentHintLines(platform = process.platform) {
4090
4140
  const mac = platform === "darwin";
@@ -4094,6 +4144,12 @@ function agentHintLines(platform = process.platform) {
4094
4144
  ` ~/.ssh/config: AddKeysToAgent yes${mac ? ", UseKeychain yes" : ""}`
4095
4145
  ];
4096
4146
  }
4147
+ function passphraseHint(platform = process.platform) {
4148
+ return [
4149
+ "ssh asked for your passphrase, and asks again for every repository unless an agent holds the key:",
4150
+ ...agentHintLines(platform).slice(1)
4151
+ ].join("\n");
4152
+ }
4097
4153
  function sshWorkspaceLines(reason3) {
4098
4154
  return [
4099
4155
  `this workspace clones over SSH (rness.json) but ssh to github.com fails: ${reason3}`,
@@ -4147,8 +4203,8 @@ function reportError(e) {
4147
4203
  process.stderr.write(`${e.stack}
4148
4204
  `);
4149
4205
  } else {
4150
- const message2 = e instanceof Error ? e.message : String(e);
4151
- process.stderr.write(`${paint("error", message2, process.stderr)}
4206
+ const message3 = e instanceof Error ? e.message : String(e);
4207
+ process.stderr.write(`${paint("error", message3, process.stderr)}
4152
4208
  `);
4153
4209
  }
4154
4210
  return 1;
@@ -4340,13 +4396,16 @@ async function ensureClaudeMd(dir) {
4340
4396
  }
4341
4397
 
4342
4398
  // src/commands/sync.ts
4343
- import { join as join15 } from "path";
4399
+ import { join as join16 } from "path";
4344
4400
 
4345
4401
  // src/core/agent-targets.ts
4346
4402
  import { mkdir as mkdir2, rm as rm4, rmdir } from "fs/promises";
4347
- import { dirname as dirname4, join as join14 } from "path";
4403
+ import { dirname as dirname5, join as join15 } from "path";
4348
4404
 
4349
4405
  // src/core/claude-skills.ts
4406
+ import { readFileSync, statSync } from "fs";
4407
+ import { dirname as dirname4, join as join13 } from "path";
4408
+ import { fileURLToPath } from "url";
4350
4409
  var PLUGIN = ".claude/skills/rness";
4351
4410
  var PLUGIN_JSON = `${JSON.stringify(
4352
4411
  {
@@ -4364,7 +4423,9 @@ var PLUGIN_JSON = `${JSON.stringify(
4364
4423
  "adr",
4365
4424
  "specification",
4366
4425
  "plan"
4367
- ]
4426
+ ],
4427
+ // The contract of the mod's `$.state` (spec 0029 §3.1).
4428
+ types: "./types/index.d.ts"
4368
4429
  },
4369
4430
  null,
4370
4431
  2
@@ -4613,6 +4674,45 @@ allowed-tools: Bash(${command}), Bash(${allocate})
4613
4674
 
4614
4675
  ${s.body(rness, allocate)}`;
4615
4676
  }
4677
+ function modDir() {
4678
+ let dir = dirname4(fileURLToPath(import.meta.url));
4679
+ for (let i = 0; i < 4; i += 1) {
4680
+ const candidate = join13(dir, "mod");
4681
+ try {
4682
+ if (statSync(join13(candidate, "hooks", "hooks.json")).isFile())
4683
+ return candidate;
4684
+ } catch {
4685
+ }
4686
+ dir = dirname4(dir);
4687
+ }
4688
+ throw new Error("mod directory not found next to the @rness/cli package");
4689
+ }
4690
+ var MOD_FILES = [
4691
+ "hooks/hooks.json",
4692
+ "hooks/register.tsx",
4693
+ "types/index.d.ts"
4694
+ ];
4695
+ var RNESS_LINE = "const RNESS = '.rness'";
4696
+ var modSources = null;
4697
+ function modSource(rel) {
4698
+ if (modSources === null) {
4699
+ const dir = modDir();
4700
+ modSources = new Map(
4701
+ MOD_FILES.map((f) => [
4702
+ f,
4703
+ readFileSync(join13(dir, ...f.split("/")), "utf8")
4704
+ ])
4705
+ );
4706
+ }
4707
+ return modSources.get(rel) ?? "";
4708
+ }
4709
+ function modFile(rel, rness) {
4710
+ const text = modSource(rel);
4711
+ if (rel !== "hooks/register.tsx") return text;
4712
+ if (!text.includes(RNESS_LINE))
4713
+ throw new Error(`mod/hooks/register.tsx lacks ${RNESS_LINE}`);
4714
+ return text.replace(RNESS_LINE, `const RNESS = '${rness}'`);
4715
+ }
4616
4716
  function pluginFiles(rness, at, ctx) {
4617
4717
  return [
4618
4718
  { file: `${PLUGIN}/.claude-plugin/plugin.json`, at, content: PLUGIN_JSON },
@@ -4626,6 +4726,11 @@ function pluginFiles(rness, at, ctx) {
4626
4726
  at,
4627
4727
  content: lifecycleSkill(rness, s)
4628
4728
  })),
4729
+ ...MOD_FILES.map((rel) => ({
4730
+ file: `${PLUGIN}/${rel}`,
4731
+ at,
4732
+ content: modFile(rel, rness)
4733
+ })),
4629
4734
  ...RETIRED_SKILLS.map((name) => ({
4630
4735
  file: `${PLUGIN}/skills/${name}/SKILL.md`,
4631
4736
  at,
@@ -4658,6 +4763,22 @@ function hooks(rness) {
4658
4763
  },
4659
4764
  label: "the rness session-start hook"
4660
4765
  },
4766
+ {
4767
+ // Before an edit: refused when it would change what sync generates,
4768
+ // or break the contract of .rness (spec 0029 §4).
4769
+ path: ["hooks", "PreToolUse"],
4770
+ contains: {
4771
+ matcher: "Edit|Write",
4772
+ hooks: [
4773
+ {
4774
+ type: "command",
4775
+ command: hookLine(rness, "pre-tool-use"),
4776
+ timeout: 10
4777
+ }
4778
+ ]
4779
+ },
4780
+ label: "the rness pre-tool-use hook"
4781
+ },
4661
4782
  {
4662
4783
  path: ["hooks", "PostToolUse"],
4663
4784
  contains: {
@@ -4852,7 +4973,7 @@ function describeGuarantee(g) {
4852
4973
  // src/core/pinned.ts
4853
4974
  import { execFile as execFile3 } from "child_process";
4854
4975
  import { readFile as readFile8 } from "fs/promises";
4855
- import { basename as basename3, join as join13 } from "path";
4976
+ import { basename as basename3, join as join14 } from "path";
4856
4977
  import { promisify as promisify3 } from "util";
4857
4978
  var execFileP3 = promisify3(execFile3);
4858
4979
  var PACKAGE = "@rness/cli";
@@ -4877,7 +4998,7 @@ function compareVersions(a, b) {
4877
4998
  async function readPackageJson(rnessDir) {
4878
4999
  try {
4879
5000
  const parsed = JSON.parse(
4880
- await readFile8(join13(rnessDir, "package.json"), "utf8")
5001
+ await readFile8(join14(rnessDir, "package.json"), "utf8")
4881
5002
  );
4882
5003
  return parsed !== null && typeof parsed === "object" ? parsed : null;
4883
5004
  } catch {
@@ -4896,7 +5017,7 @@ async function readPin(rnessDir) {
4896
5017
  return null;
4897
5018
  }
4898
5019
  async function writePin(rnessDir, from, to) {
4899
- const file = join13(rnessDir, "package.json");
5020
+ const file = join14(rnessDir, "package.json");
4900
5021
  const text = await readFile8(file, "utf8");
4901
5022
  const entry = new RegExp(
4902
5023
  `("${PACKAGE}"\\s*:\\s*")${from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(")`
@@ -4919,12 +5040,12 @@ async function workspacePackageManager(rnessDir) {
4919
5040
  if (isPackageManager(name)) return name;
4920
5041
  }
4921
5042
  for (const [lockfile, pm] of LOCKFILES)
4922
- if (await exists(join13(rnessDir, lockfile))) return pm;
5043
+ if (await exists(join14(rnessDir, lockfile))) return pm;
4923
5044
  return "npm";
4924
5045
  }
4925
5046
  async function hasLockfile(rnessDir, pm) {
4926
5047
  for (const [lockfile, owner] of LOCKFILES)
4927
- if (owner === pm && await exists(join13(rnessDir, lockfile))) return true;
5048
+ if (owner === pm && await exists(join14(rnessDir, lockfile))) return true;
4928
5049
  return false;
4929
5050
  }
4930
5051
  async function pinDrift(rnessDir) {
@@ -4980,9 +5101,9 @@ async function ensureInstalled(rnessDir, opts = {}) {
4980
5101
  }
4981
5102
  }
4982
5103
  async function runPinnedSync(root) {
4983
- const installed = await readPinnedCli(join13(root, ".rness"));
5104
+ const installed = await readPinnedCli(join14(root, ".rness"));
4984
5105
  if (installed === null) return null;
4985
- const pinned = join13(installed.dir, ...installed.bin.split("/"));
5106
+ const pinned = join14(installed.dir, ...installed.bin.split("/"));
4986
5107
  if (!await exists(pinned))
4987
5108
  return {
4988
5109
  code: 1,
@@ -5057,7 +5178,7 @@ function syncOutcome(result, opts = {}) {
5057
5178
  };
5058
5179
  }
5059
5180
  async function syncPinned(root, shown, fallback) {
5060
- const rnessDir = join13(root, ".rness");
5181
+ const rnessDir = join14(root, ".rness");
5061
5182
  const installed = await readPinnedCli(rnessDir);
5062
5183
  if (installed === null) {
5063
5184
  if (fallback !== void 0) return fallback();
@@ -5067,7 +5188,7 @@ async function syncPinned(root, shown, fallback) {
5067
5188
  );
5068
5189
  return 1;
5069
5190
  }
5070
- const pinned = join13(installed.dir, ...installed.bin.split("/"));
5191
+ const pinned = join14(installed.dir, ...installed.bin.split("/"));
5071
5192
  if (!await exists(pinned)) {
5072
5193
  process.stderr.write(
5073
5194
  `@rness/cli in ${shown}/.rness is not installed correctly (missing ${installed.bin}); reinstall in .rness/
@@ -5107,30 +5228,30 @@ function wholeDiff(existing, content) {
5107
5228
  }
5108
5229
  async function contextOf(root) {
5109
5230
  return {
5110
- packageManager: await workspacePackageManager(join14(root, ".rness"))
5231
+ packageManager: await workspacePackageManager(join15(root, ".rness"))
5111
5232
  };
5112
5233
  }
5113
5234
  var PATHS_ONLY = { packageManager: "npm" };
5114
5235
  async function filesOf(root, manifest, targets) {
5115
5236
  const files = [];
5116
- if (!await exists(join14(root, "org"))) return files;
5237
+ if (!await exists(join15(root, "org"))) return files;
5117
5238
  const ctx = await contextOf(root);
5118
5239
  for (const target2 of targets)
5119
5240
  for (const owned of target2.files(ctx))
5120
5241
  if (owned.at === "root")
5121
5242
  files.push({
5122
5243
  ...owned,
5123
- file: join14(root, ...owned.file.split("/")),
5244
+ file: join15(root, ...owned.file.split("/")),
5124
5245
  label: owned.file
5125
5246
  });
5126
5247
  for (const repo of Object.keys(manifest.repos)) {
5127
- if (!await exists(join14(root, "org", repo))) continue;
5248
+ if (!await exists(join15(root, "org", repo))) continue;
5128
5249
  for (const target2 of targets)
5129
5250
  for (const owned of target2.files(ctx))
5130
5251
  if (owned.at === "clones")
5131
5252
  files.push({
5132
5253
  ...owned,
5133
- file: join14(root, "org", repo, ...owned.file.split("/")),
5254
+ file: join15(root, "org", repo, ...owned.file.split("/")),
5134
5255
  label: `org/${repo}/${owned.file}`
5135
5256
  });
5136
5257
  }
@@ -5166,7 +5287,7 @@ async function agentTargets(root, manifest, opts) {
5166
5287
  continue;
5167
5288
  }
5168
5289
  await rm4(file);
5169
- await rmdir(dirname4(file)).catch(() => void 0);
5290
+ await rmdir(dirname5(file)).catch(() => void 0);
5170
5291
  outcomes.push({ label, status: "removed", detail: null });
5171
5292
  continue;
5172
5293
  }
@@ -5177,7 +5298,7 @@ async function agentTargets(root, manifest, opts) {
5177
5298
  else if (opts.check)
5178
5299
  outcomes.push({ label, status: "stale", detail: diff });
5179
5300
  else {
5180
- await mkdir2(dirname4(file), { recursive: true });
5301
+ await mkdir2(dirname5(file), { recursive: true });
5181
5302
  await writeFileAtomic(file, owned.content);
5182
5303
  outcomes.push({ label, status: "updated", detail: null });
5183
5304
  }
@@ -5206,7 +5327,7 @@ async function agentTargets(root, manifest, opts) {
5206
5327
  outcomes.push({ label, status: "unchanged", detail: null });
5207
5328
  continue;
5208
5329
  }
5209
- await mkdir2(dirname4(file), { recursive: true });
5330
+ await mkdir2(dirname5(file), { recursive: true });
5210
5331
  await writeFileAtomic(file, ensured.text);
5211
5332
  outcomes.push({ label, status: "updated", detail: null });
5212
5333
  }
@@ -5398,8 +5519,8 @@ function sessionUi(p) {
5398
5519
  intro: (title) => {
5399
5520
  p.intro(badge(title));
5400
5521
  },
5401
- outro: (message2, tone = "done") => {
5402
- p.outro(badge(message2, tone));
5522
+ outro: (message3, tone = "done") => {
5523
+ p.outro(badge(message3, tone));
5403
5524
  },
5404
5525
  line: (verb, rest, sentence) => {
5405
5526
  say(verb, sentence ?? `${rest} ${verb}`);
@@ -5470,7 +5591,7 @@ function targetsOf(manifest, root, only) {
5470
5591
  const segments = entry.path.split("/");
5471
5592
  targets.push({
5472
5593
  scope: name,
5473
- dir: join15(root, ...segments),
5594
+ dir: join16(root, ...segments),
5474
5595
  depth: segments.length,
5475
5596
  label: `${entry.path}/AGENTS.md`
5476
5597
  });
@@ -5509,7 +5630,7 @@ async function askWhatToSync(input) {
5509
5630
  {
5510
5631
  value: SELECT_ALL,
5511
5632
  label: "Select all",
5512
- hint: `${input.missing.length} repositories from rness.json`
5633
+ hint: `${count(input.missing.length, ["repository", "repositories"])} from rness.json`
5513
5634
  },
5514
5635
  ...input.missing.map((name) => ({ value: name, label: name }))
5515
5636
  ],
@@ -5610,15 +5731,16 @@ async function syncCommand(opts, deps = {}) {
5610
5731
  if (!overSsh) ui.gitIsSilent = true;
5611
5732
  if (overSsh) {
5612
5733
  const interactive = opts.yes !== true && terminal.isTty();
5613
- const { access: access3, unattended } = await testGithubSsh(
5734
+ const { access: access2, unattended } = await testGithubSsh(
5614
5735
  transport,
5615
5736
  interactive,
5616
5737
  () => ui.line(...CHECKING)
5617
5738
  );
5618
5739
  if (unattended) ui.gitIsSilent = true;
5619
- if (!access3.ok) {
5740
+ else if (access2.ok) ui.hint(passphraseHint(), "stderr");
5741
+ if (!access2.ok) {
5620
5742
  process.stderr.write(
5621
- `${[...sshWorkspaceLines(access3.reason), ...INSTEAD_OF_LINES].join("\n")}
5743
+ `${[...sshWorkspaceLines(access2.reason), ...INSTEAD_OF_LINES].join("\n")}
5622
5744
  `
5623
5745
  );
5624
5746
  return 1;
@@ -5629,7 +5751,7 @@ async function syncCommand(opts, deps = {}) {
5629
5751
  try {
5630
5752
  const notCloned = /* @__PURE__ */ new Set();
5631
5753
  for (const [name, repo] of Object.entries(manifest.repos)) {
5632
- const dir = join15(ws.root, "org", name);
5754
+ const dir = join16(ws.root, "org", name);
5633
5755
  const label = `org/${name}`;
5634
5756
  if (!await exists(dir)) {
5635
5757
  if (!toClone.has(name)) {
@@ -5681,7 +5803,7 @@ async function syncCommand(opts, deps = {}) {
5681
5803
  lines.push(
5682
5804
  `not in rness.json: ${undeclared.join(", ")} (${rnessCommand()} add <name> declares it)`
5683
5805
  );
5684
- if (!await exists(join15(ws.root, "org"))) {
5806
+ if (!await exists(join16(ws.root, "org"))) {
5685
5807
  lines.push(["skipped", "blocks (no org/ directory here)"]);
5686
5808
  } else {
5687
5809
  let differences = 0;
@@ -5693,7 +5815,7 @@ async function syncCommand(opts, deps = {}) {
5693
5815
  continue;
5694
5816
  }
5695
5817
  const [agentsLinked, claudeLinked] = await Promise.all(
5696
- BLOCK_FILES.map((f) => isSymlink(join15(t.dir, f)))
5818
+ BLOCK_FILES.map((f) => isSymlink(join16(t.dir, f)))
5697
5819
  );
5698
5820
  const atRoot = t.scope === null;
5699
5821
  if (!atRoot && (agentsLinked === true || claudeLinked === true)) {
@@ -5716,7 +5838,7 @@ async function syncCommand(opts, deps = {}) {
5716
5838
  `${t.label}: block is ${Math.round(block.bytes / 1024)} KB (over 32 KB)`
5717
5839
  );
5718
5840
  }
5719
- const agents = agentsLinked === true ? "skipped" : await syncBlockFile(join15(t.dir, "AGENTS.md"), block, check);
5841
+ const agents = agentsLinked === true ? "skipped" : await syncBlockFile(join16(t.dir, "AGENTS.md"), block, check);
5720
5842
  if (typeof agents === "object") {
5721
5843
  problems.push(`${t.label}: ${agents.error}`);
5722
5844
  continue;
@@ -5731,7 +5853,7 @@ async function syncCommand(opts, deps = {}) {
5731
5853
  }
5732
5854
  if (claudeLinked === true) continue;
5733
5855
  const claude = await syncBlockFile(
5734
- join15(t.dir, "CLAUDE.md"),
5856
+ join16(t.dir, "CLAUDE.md"),
5735
5857
  block,
5736
5858
  check,
5737
5859
  true
@@ -5788,7 +5910,7 @@ async function syncCommand(opts, deps = {}) {
5788
5910
  }
5789
5911
 
5790
5912
  // src/commands/add.ts
5791
- import { basename as basename4, join as join16 } from "path";
5913
+ import { basename as basename4, join as join17 } from "path";
5792
5914
  function printDeclared(ui, result, already) {
5793
5915
  for (const name of result.declared) {
5794
5916
  if (already.has(name)) continue;
@@ -5847,20 +5969,21 @@ async function addCommand(spec, opts, deps = {}) {
5847
5969
  if (host === void 0 && opts.https === true) host = transport.hosts.https;
5848
5970
  if (host === void 0 && FULL_URL2.test(spec)) host = transport.hosts.https;
5849
5971
  if (host === void 0) {
5850
- const { access: access3, unattended } = await testGithubSsh(
5972
+ const { access: access2, unattended } = await testGithubSsh(
5851
5973
  transport,
5852
5974
  interactive,
5853
5975
  () => ui.line(...CHECKING)
5854
5976
  );
5855
- if (unattended || !access3.ok) ui.gitIsSilent = true;
5977
+ if (unattended || !access2.ok) ui.gitIsSilent = true;
5978
+ else ui.hint(passphraseHint(), "stderr");
5856
5979
  const overSsh = Object.values(manifest.repos).some(
5857
5980
  (r) => isSshUrl(r.url, transport.hosts)
5858
5981
  );
5859
- if (access3.ok || !overSsh) {
5860
- ui.line("using", usingRest(access3), usingSentence(access3));
5861
- host = access3.ok ? transport.hosts.ssh : transport.hosts.https;
5982
+ if (access2.ok || !overSsh) {
5983
+ ui.line("using", usingRest(access2), usingSentence(access2));
5984
+ host = access2.ok ? transport.hosts.ssh : transport.hosts.https;
5862
5985
  } else {
5863
- const lines = sshWorkspaceLines(access3.reason);
5986
+ const lines = sshWorkspaceLines(access2.reason);
5864
5987
  if (!interactive) {
5865
5988
  process.stderr.write(
5866
5989
  `${[...lines, httpsFlagLine(spec)].join("\n")}
@@ -5881,7 +6004,7 @@ async function addCommand(spec, opts, deps = {}) {
5881
6004
  }
5882
6005
  parsed = parseRepoSpec(spec, org, host);
5883
6006
  const scopes = (opts.scopes ?? "").split(",").map((s) => s.trim()).filter((s) => s !== "");
5884
- const dir = join16(ws.root, "org", parsed.name);
6007
+ const dir = join17(ws.root, "org", parsed.name);
5885
6008
  const present = await exists(dir);
5886
6009
  if (interactive) {
5887
6010
  const { confirm, isCancel } = await terminal.prompts();
@@ -6008,9 +6131,9 @@ async function contextCommand(opts) {
6008
6131
  }
6009
6132
 
6010
6133
  // src/core/scaffold.ts
6011
- import { statSync } from "fs";
6012
- import { dirname as dirname5, join as join17 } from "path";
6013
- import { fileURLToPath } from "url";
6134
+ import { statSync as statSync2 } from "fs";
6135
+ import { dirname as dirname6, join as join18 } from "path";
6136
+ import { fileURLToPath as fileURLToPath2 } from "url";
6014
6137
  var SCAFFOLD_FILES = [
6015
6138
  "AGENTS.md",
6016
6139
  "CONVENTIONS.md",
@@ -6031,27 +6154,27 @@ var SCAFFOLD_FILES = [
6031
6154
  "docs/.gitkeep"
6032
6155
  ];
6033
6156
  function scaffoldDir() {
6034
- let dir = dirname5(fileURLToPath(import.meta.url));
6157
+ let dir = dirname6(fileURLToPath2(import.meta.url));
6035
6158
  for (let i = 0; i < 4; i += 1) {
6036
- const candidate = join17(dir, "scaffold");
6159
+ const candidate = join18(dir, "scaffold");
6037
6160
  try {
6038
- if (statSync(join17(candidate, "rness.json")).isFile()) return candidate;
6161
+ if (statSync2(join18(candidate, "rness.json")).isFile()) return candidate;
6039
6162
  } catch {
6040
6163
  }
6041
- dir = dirname5(dir);
6164
+ dir = dirname6(dir);
6042
6165
  }
6043
6166
  throw new Error("scaffold directory not found next to the @rness/cli package");
6044
6167
  }
6045
6168
 
6046
6169
  // src/core/scaffold-copy.ts
6047
6170
  import { chmod as chmod2, mkdir as mkdir3, readFile as readFile9, stat as stat2, writeFile as writeFile2 } from "fs/promises";
6048
- import { dirname as dirname6, join as join18 } from "path";
6171
+ import { dirname as dirname7, join as join19 } from "path";
6049
6172
  var TOKENISED = /* @__PURE__ */ new Set(["package.json"]);
6050
6173
  async function renderScaffold(source, tokens) {
6051
6174
  const files = [];
6052
6175
  for (const rel of SCAFFOLD_FILES) {
6053
6176
  if (rel === "rness.json") continue;
6054
- const from = join18(source, ...rel.split("/"));
6177
+ const from = join19(source, ...rel.split("/"));
6055
6178
  let content = await readFile9(from, "utf8");
6056
6179
  if (TOKENISED.has(rel)) {
6057
6180
  content = content.replaceAll("__RNESS_VERSION__", tokens.version).replaceAll("__RNESS_PM__", tokens.packageManager);
@@ -6066,8 +6189,8 @@ async function renderScaffold(source, tokens) {
6066
6189
  }
6067
6190
  async function copyScaffold(dest, tokens) {
6068
6191
  for (const file of await renderScaffold(scaffoldDir(), tokens)) {
6069
- const to = join18(dest, ...file.path.split("/"));
6070
- await mkdir3(dirname6(to), { recursive: true });
6192
+ const to = join19(dest, ...file.path.split("/"));
6193
+ await mkdir3(dirname7(to), { recursive: true });
6071
6194
  await writeFile2(to, file.content, "utf8");
6072
6195
  if (file.executable) await chmod2(to, 493);
6073
6196
  }
@@ -6248,7 +6371,7 @@ async function loginCommand(opts, deps = {}) {
6248
6371
  // src/commands/create.ts
6249
6372
  import { mkdir as mkdir6, mkdtemp as mkdtemp2, rename as rename2, rm as rm7 } from "fs/promises";
6250
6373
  import { tmpdir as tmpdir2 } from "os";
6251
- import { join as join22, relative as relative4, resolve as resolve5 } from "path";
6374
+ import { join as join23, relative as relative4, resolve as resolve5 } from "path";
6252
6375
 
6253
6376
  // src/core/approval.ts
6254
6377
  var APPROVAL_INTERVAL_MS = 5e3;
@@ -6262,12 +6385,12 @@ async function checkAccess(input) {
6262
6385
  const login = (await provider.identity())?.login;
6263
6386
  const as = login === void 0 ? "" : ` (as ${login})`;
6264
6387
  const member = () => ui.line("member", `${org}${as}`, `You are a member of ${org}${as}`);
6265
- const access3 = await provider.organizationAccess(org);
6266
- if (access3 === "member") {
6388
+ const access2 = await provider.organizationAccess(org);
6389
+ if (access2 === "member") {
6267
6390
  member();
6268
- return access3;
6391
+ return access2;
6269
6392
  }
6270
- if (access3 !== "restricted") return access3;
6393
+ if (access2 !== "restricted") return access2;
6271
6394
  const url = provider.approvalUrl();
6272
6395
  ui.warn(
6273
6396
  `${org} has not approved rness, so its private repositories are hidden`
@@ -6296,23 +6419,42 @@ async function checkAccess(input) {
6296
6419
  const who = input.canGrant === true ? org : `an owner of ${org}`;
6297
6420
  const waiting = `for ${who} to approve rness on github.com`;
6298
6421
  if (!ui.session) ui.line("waiting", `${waiting}\u2026`);
6299
- const outcome = await ui.step(
6300
- {
6301
- doing: `waiting ${waiting}`,
6302
- sentence: `Waiting ${waiting}`,
6303
- quiet: true
6304
- },
6305
- async () => {
6306
- const deadline = now() + APPROVAL_WAIT_MS;
6307
- for (; ; ) {
6308
- await sleep(APPROVAL_INTERVAL_MS);
6309
- const again = await provider.organizationAccess(org);
6310
- if (again === "member" || again === "not-member") return again;
6311
- if (now() >= deadline) return "timeout";
6312
- }
6313
- },
6314
- (r) => r === "member" ? ["member", `${org}${as}`] : null
6315
- );
6422
+ let interrupted = false;
6423
+ let wake = () => void 0;
6424
+ const interrupt = () => {
6425
+ interrupted = true;
6426
+ wake();
6427
+ };
6428
+ if (!ui.session) process.once("SIGINT", interrupt);
6429
+ let outcome;
6430
+ try {
6431
+ outcome = await ui.step(
6432
+ {
6433
+ doing: `waiting ${waiting}`,
6434
+ sentence: `Waiting ${waiting}`,
6435
+ quiet: true
6436
+ },
6437
+ async () => {
6438
+ const deadline = now() + APPROVAL_WAIT_MS;
6439
+ for (; ; ) {
6440
+ await Promise.race([
6441
+ sleep(APPROVAL_INTERVAL_MS),
6442
+ new Promise((resolve8) => {
6443
+ wake = resolve8;
6444
+ })
6445
+ ]);
6446
+ if (interrupted) return "cancelled";
6447
+ const again = await provider.organizationAccess(org);
6448
+ if (again === "member" || again === "not-member") return again;
6449
+ if (now() >= deadline) return "timeout";
6450
+ }
6451
+ },
6452
+ (r) => r === "member" ? ["member", `${org}${as}`] : null
6453
+ );
6454
+ } finally {
6455
+ process.off("SIGINT", interrupt);
6456
+ }
6457
+ if (outcome === "cancelled") return outcome;
6316
6458
  if (outcome === "member") {
6317
6459
  if (!ui.session) member();
6318
6460
  return outcome;
@@ -6348,13 +6490,13 @@ async function askProvider(p) {
6348
6490
 
6349
6491
  // src/core/new-workspace.ts
6350
6492
  import { mkdir as mkdir4, readdir as readdir4, rm as rm6, stat as stat3 } from "fs/promises";
6351
- import { dirname as dirname8, join as join20, relative as relative2 } from "path";
6493
+ import { dirname as dirname9, join as join21, relative as relative2 } from "path";
6352
6494
 
6353
6495
  // src/core/scaffold-git.ts
6354
6496
  import { execFile as execFile5 } from "child_process";
6355
6497
  import { mkdtemp, realpath as realpath2, rm as rm5, writeFile as writeFile3 } from "fs/promises";
6356
6498
  import { tmpdir } from "os";
6357
- import { dirname as dirname7, join as join19, resolve as resolve3 } from "path";
6499
+ import { dirname as dirname8, join as join20, resolve as resolve3 } from "path";
6358
6500
  import { promisify as promisify5 } from "util";
6359
6501
  var execFileP5 = promisify5(execFile5);
6360
6502
  var CREATE_SUBJECT = "chore: rness workspace context";
@@ -6368,7 +6510,7 @@ async function run(dir, args, env = {}) {
6368
6510
  ...process.env,
6369
6511
  ...env,
6370
6512
  GIT_TERMINAL_PROMPT: "0",
6371
- GIT_CEILING_DIRECTORIES: dirname7(resolve3(dir))
6513
+ GIT_CEILING_DIRECTORIES: dirname8(resolve3(dir))
6372
6514
  },
6373
6515
  maxBuffer: 16 * 1024 * 1024
6374
6516
  });
@@ -6395,13 +6537,13 @@ function scaffoldMessage(subject, version) {
6395
6537
 
6396
6538
  ${SCAFFOLD_TRAILER}: ${version}`;
6397
6539
  }
6398
- async function commitUpgrade(rnessDir, message2) {
6540
+ async function commitUpgrade(rnessDir, message3) {
6399
6541
  const add = await run(rnessDir, ["add", "-A"]);
6400
6542
  if (add.code !== 0) return { kind: "refused", output: add.stderr.trim() };
6401
6543
  const staged = await run(rnessDir, ["diff", "--cached", "--quiet"]);
6402
6544
  if (staged.code === 0 && !await mergeInProgress(rnessDir))
6403
6545
  return { kind: "nothing" };
6404
- const commit = await run(rnessDir, ["commit", "-q", "-m", message2]);
6546
+ const commit = await run(rnessDir, ["commit", "-q", "-m", message3]);
6405
6547
  if (commit.code !== 0)
6406
6548
  return {
6407
6549
  kind: "refused",
@@ -6442,8 +6584,8 @@ async function findScaffoldBase(rnessDir) {
6442
6584
  };
6443
6585
  }
6444
6586
  async function buildScaffoldCommit(rnessDir, base, files, version) {
6445
- const scratch = await mkdtemp(join19(tmpdir(), "rness-scaffold-"));
6446
- const env = { GIT_INDEX_FILE: join19(scratch, "index") };
6587
+ const scratch = await mkdtemp(join20(tmpdir(), "rness-scaffold-"));
6588
+ const env = { GIT_INDEX_FILE: join20(scratch, "index") };
6447
6589
  try {
6448
6590
  await must(
6449
6591
  rnessDir,
@@ -6451,7 +6593,7 @@ async function buildScaffoldCommit(rnessDir, base, files, version) {
6451
6593
  env
6452
6594
  );
6453
6595
  for (const [i, file] of files.entries()) {
6454
- const tmp = join19(scratch, `blob-${i}`);
6596
+ const tmp = join20(scratch, `blob-${i}`);
6455
6597
  await writeFile3(tmp, file.content, "utf8");
6456
6598
  const blob = await must(rnessDir, [
6457
6599
  "hash-object",
@@ -6563,11 +6705,11 @@ async function insideWorkspace(dir) {
6563
6705
  }
6564
6706
  async function targetProblem(cwd, root, pm) {
6565
6707
  const shown = relative2(cwd, root) || ".";
6566
- if (await insideWorkspace(cwd) || await insideWorkspace(dirname8(root)))
6708
+ if (await insideWorkspace(cwd) || await insideWorkspace(dirname9(root)))
6567
6709
  return "already inside an rness workspace; create runs from outside one";
6568
6710
  if (!await exists(root)) return null;
6569
- if (await exists(join20(root, ".rness", "rness.json"))) {
6570
- if (!await exists(join20(root, ".rness", "node_modules")))
6711
+ if (await exists(join21(root, ".rness", "rness.json"))) {
6712
+ if (!await exists(join21(root, ".rness", "node_modules")))
6571
6713
  return `${shown} holds an rness workspace whose dependencies are not installed; cd ${shown}/.rness && ${pm} install, then ${rnessCommand()} sync`;
6572
6714
  return `${shown} already holds an rness workspace (.rness/); run ${rnessCommand()} add inside it to add repositories`;
6573
6715
  }
@@ -6590,7 +6732,7 @@ async function installContext(input) {
6590
6732
  }
6591
6733
  async function buildContext(input) {
6592
6734
  const { ui, root, shown, pm } = input;
6593
- const rnessDir = join20(root, ".rness");
6735
+ const rnessDir = join21(root, ".rness");
6594
6736
  const pmVersion = await packageManagerVersion(pm);
6595
6737
  await mkdir4(root, { recursive: true });
6596
6738
  try {
@@ -6662,7 +6804,7 @@ async function syncBlocks(ui, root, shown, inPlace, sshWorks) {
6662
6804
 
6663
6805
  // src/commands/create-blank.ts
6664
6806
  import { mkdir as mkdir5 } from "fs/promises";
6665
- import { join as join21, relative as relative3, resolve as resolve4 } from "path";
6807
+ import { join as join22, relative as relative3, resolve as resolve4 } from "path";
6666
6808
  var WORKSPACE_NAME = /^(?!\.{1,2}$)[A-Za-z0-9._][A-Za-z0-9._-]*$/;
6667
6809
  function workspaceNameError(name) {
6668
6810
  return `workspace name "${name}" may only contain letters, digits, '.', '_' and '-', not starting with '-'`;
@@ -6744,8 +6886,8 @@ async function createBlank(input) {
6744
6886
  kind: "blank workspace"
6745
6887
  });
6746
6888
  if (!built) return 1;
6747
- await mkdir5(join21(root, "org"), { recursive: true });
6748
- await commitContext({ ui, rnessDir: join21(root, ".rness"), shown });
6889
+ await mkdir5(join22(root, "org"), { recursive: true });
6890
+ await commitContext({ ui, rnessDir: join22(root, ".rness"), shown });
6749
6891
  const code = await syncBlocks(
6750
6892
  ui,
6751
6893
  root,
@@ -6804,7 +6946,7 @@ async function pickRepositories(input) {
6804
6946
  // list below it must agree (spec 0009).
6805
6947
  (l) => [
6806
6948
  "listed",
6807
- `${l.repositories.filter(isMemberRepo).length} repositories of ${org}`
6949
+ `${count(l.repositories.filter(isMemberRepo).length, ["repository", "repositories"])} of ${org}`
6808
6950
  ]
6809
6951
  );
6810
6952
  listed = listing.repositories;
@@ -6889,7 +7031,7 @@ async function cloneSelection(input) {
6889
7031
  { doing: `cloning org/${repo}`, git: true },
6890
7032
  () => clone(
6891
7033
  entry.url,
6892
- join22(input.root, "org", repo),
7034
+ join23(input.root, "org", repo),
6893
7035
  input.provider.credentialsFor(entry.url)
6894
7036
  ),
6895
7037
  () => ["cloned", `org/${repo}`]
@@ -6914,9 +7056,9 @@ async function cloneSelection(input) {
6914
7056
  manifest = result.manifest;
6915
7057
  ui.line("declared", `scope ${result.name} (org/${result.name})`);
6916
7058
  } catch (e) {
6917
- const message2 = firstLine5(e instanceof Error ? e.message : String(e));
6918
- ui.error(`${spec}: ${message2}`);
6919
- failures.push({ spec, message: message2 });
7059
+ const message3 = firstLine5(e instanceof Error ? e.message : String(e));
7060
+ ui.error(`${spec}: ${message3}`);
7061
+ failures.push({ spec, message: message3 });
6920
7062
  }
6921
7063
  }
6922
7064
  return failures;
@@ -7085,6 +7227,7 @@ async function createCommand(opts, deps = {}) {
7085
7227
  () => ui.line(...CHECKING)
7086
7228
  );
7087
7229
  if (sshTest.unattended) ui.gitIsSilent = true;
7230
+ else if (sshTest.access.ok) ui.hint(passphraseHint(), "stderr");
7088
7231
  }
7089
7232
  return sshTest.access;
7090
7233
  };
@@ -7094,12 +7237,12 @@ async function createCommand(opts, deps = {}) {
7094
7237
  if (opts.host !== void 0) return opts.host;
7095
7238
  if (opts.ssh === true) return transport.hosts.ssh;
7096
7239
  if (opts.https === true) return transport.hosts.https;
7097
- const access3 = await testSsh();
7098
- const github = access3.ok ? null : await getProvider();
7240
+ const access2 = await testSsh();
7241
+ const github = access2.ok ? null : await getProvider();
7099
7242
  const login = github?.authenticated ? (await github.identity())?.login : void 0;
7100
- ui.line("using", usingRest(access3, login), usingSentence(access3, login));
7101
- if (!access3.ok) ui.gitIsSilent = true;
7102
- return access3.ok ? transport.hosts.ssh : transport.hosts.https;
7243
+ ui.line("using", usingRest(access2, login), usingSentence(access2, login));
7244
+ if (!access2.ok) ui.gitIsSilent = true;
7245
+ return access2.ok ? transport.hosts.ssh : transport.hosts.https;
7103
7246
  };
7104
7247
  let loaded;
7105
7248
  const prompts = async () => loaded ??= await terminal.prompts();
@@ -7251,8 +7394,8 @@ async function createCommand(opts, deps = {}) {
7251
7394
  scopes: {}
7252
7395
  };
7253
7396
  const staged = async () => {
7254
- staging ??= await mkdtemp2(join22(tmpdir2(), "rness-join-"));
7255
- return join22(staging, ".rness");
7397
+ staging ??= await mkdtemp2(join23(tmpdir2(), "rness-join-"));
7398
+ return join23(staging, ".rness");
7256
7399
  };
7257
7400
  if (joining) {
7258
7401
  const stagedDir = await staged();
@@ -7273,10 +7416,10 @@ async function createCommand(opts, deps = {}) {
7273
7416
  (r) => isSshUrl(r.url, transport.hosts)
7274
7417
  );
7275
7418
  if (overSsh && opts.ssh !== true && opts.host === void 0) {
7276
- const access3 = await testSsh();
7277
- if (!access3.ok) {
7419
+ const access2 = await testSsh();
7420
+ if (!access2.ok) {
7278
7421
  process.stderr.write(
7279
- `${[...sshWorkspaceLines(access3.reason), ...INSTEAD_OF_LINES].join("\n")}
7422
+ `${[...sshWorkspaceLines(access2.reason), ...INSTEAD_OF_LINES].join("\n")}
7280
7423
  `
7281
7424
  );
7282
7425
  return 1;
@@ -7314,7 +7457,7 @@ async function createCommand(opts, deps = {}) {
7314
7457
  });
7315
7458
  if (p.isCancel(ok) || ok !== true) return cancelled(ui);
7316
7459
  }
7317
- const rnessDir = join22(root, ".rness");
7460
+ const rnessDir = join23(root, ".rness");
7318
7461
  const install = () => installContext({ ui, pm, rnessDir, skip: opts.skipInstall === true });
7319
7462
  if (joining) {
7320
7463
  await mkdir6(root, { recursive: true });
@@ -7327,7 +7470,7 @@ async function createCommand(opts, deps = {}) {
7327
7470
  ui.line("cloned", `${shown}/.rness (joined ${org})`);
7328
7471
  if (opts.pm === void 0) pm = await workspacePackageManager(rnessDir);
7329
7472
  await install();
7330
- await mkdir6(join22(root, "org"), { recursive: true });
7473
+ await mkdir6(join23(root, "org"), { recursive: true });
7331
7474
  const failures2 = await cloneSelection({
7332
7475
  root,
7333
7476
  rnessDir,
@@ -7380,7 +7523,7 @@ ${next2.map((l) => ` ${l}`).join("\n")}
7380
7523
  kind: "new workspace"
7381
7524
  });
7382
7525
  if (!built) return 1;
7383
- await mkdir6(join22(root, "org"), { recursive: true });
7526
+ await mkdir6(join23(root, "org"), { recursive: true });
7384
7527
  const failures = await cloneSelection({
7385
7528
  root,
7386
7529
  rnessDir,
@@ -7452,7 +7595,7 @@ ${next2.map((l) => ` ${l}`).join("\n")}
7452
7595
  }
7453
7596
 
7454
7597
  // src/core/blocks.ts
7455
- import { join as join23 } from "path";
7598
+ import { join as join24 } from "path";
7456
7599
  var PROBLEM = /* @__PURE__ */ new Set(["stale", "malformed"]);
7457
7600
  async function checkBlocks({
7458
7601
  root,
@@ -7461,15 +7604,15 @@ async function checkBlocks({
7461
7604
  org
7462
7605
  }) {
7463
7606
  const checks = [];
7464
- const note = (label, status2, message2 = null) => {
7465
- checks.push({ label, status: status2, message: message2 });
7607
+ const note = (label, status2, message3 = null) => {
7608
+ checks.push({ label, status: status2, message: message3 });
7466
7609
  };
7467
7610
  const derive = () => ({
7468
7611
  problems: checks.filter((c) => PROBLEM.has(c.status) && c.message !== null).map((c) => c.message),
7469
7612
  warnings: checks.filter((c) => !PROBLEM.has(c.status) && c.message !== null).map((c) => c.message),
7470
7613
  checks
7471
7614
  });
7472
- if (!await exists(join23(root, "org"))) return derive();
7615
+ if (!await exists(join24(root, "org"))) return derive();
7473
7616
  const targets = [
7474
7617
  { scope: null, dir: root, depth: 0, file: "AGENTS.md", label: "AGENTS.md" },
7475
7618
  { scope: null, dir: root, depth: 0, file: "CLAUDE.md", label: "CLAUDE.md" }
@@ -7478,7 +7621,7 @@ async function checkBlocks({
7478
7621
  const segments = entry.path.split("/");
7479
7622
  targets.push({
7480
7623
  scope: name,
7481
- dir: join23(root, ...segments),
7624
+ dir: join24(root, ...segments),
7482
7625
  depth: segments.length,
7483
7626
  file: "AGENTS.md",
7484
7627
  label: `${entry.path}/AGENTS.md`
@@ -7493,7 +7636,7 @@ async function checkBlocks({
7493
7636
  );
7494
7637
  continue;
7495
7638
  }
7496
- const text = await readOrNull(join23(t.dir, t.file));
7639
+ const text = await readOrNull(join24(t.dir, t.file));
7497
7640
  const lines = text === null ? [] : text.split(/\r?\n/);
7498
7641
  const found = findBlock(lines);
7499
7642
  if (found.kind === "none") {
@@ -7564,7 +7707,7 @@ async function logoutCommand() {
7564
7707
  import { execFile as execFile6 } from "child_process";
7565
7708
  import { mkdtemp as mkdtemp3, readFile as readFile10, rm as rm8 } from "fs/promises";
7566
7709
  import { tmpdir as tmpdir3 } from "os";
7567
- import { basename as basename5, join as join24, relative as relative5 } from "path";
7710
+ import { basename as basename5, join as join25, relative as relative5 } from "path";
7568
7711
  import { promisify as promisify6 } from "util";
7569
7712
  var execFileP6 = promisify6(execFile6);
7570
7713
  var NOTES = "https://github.com/rness-dev/rness/tree/main/packages/cli#readme";
@@ -7584,10 +7727,10 @@ async function latestVersion() {
7584
7727
  async function scaffoldFor(version, rnessDir) {
7585
7728
  if (version === VERSION) return { dir: scaffoldDir() };
7586
7729
  const installed = await readPinnedCli(rnessDir);
7587
- const inside2 = installed === null ? null : join24(installed.dir, "scaffold");
7588
- if (installed?.version === version && inside2 !== null && await exists(join24(inside2, "package.json")))
7730
+ const inside2 = installed === null ? null : join25(installed.dir, "scaffold");
7731
+ if (installed?.version === version && inside2 !== null && await exists(join25(inside2, "package.json")))
7589
7732
  return { dir: inside2 };
7590
- const scratch = await mkdtemp3(join24(tmpdir3(), "rness-pack-"));
7733
+ const scratch = await mkdtemp3(join25(tmpdir3(), "rness-pack-"));
7591
7734
  const cleanup = () => rm8(scratch, { recursive: true, force: true });
7592
7735
  try {
7593
7736
  const { stdout } = await execFileP6(
@@ -7602,8 +7745,8 @@ async function scaffoldFor(version, rnessDir) {
7602
7745
  { timeout: 6e4 }
7603
7746
  );
7604
7747
  const tarball = stdout.trim().split("\n").at(-1) ?? "";
7605
- await execFileP6("tar", ["-xzf", join24(scratch, tarball), "-C", scratch]);
7606
- return { dir: join24(scratch, "package", "scaffold"), cleanup };
7748
+ await execFileP6("tar", ["-xzf", join25(scratch, tarball), "-C", scratch]);
7749
+ return { dir: join25(scratch, "package", "scaffold"), cleanup };
7607
7750
  } catch (e) {
7608
7751
  await cleanup();
7609
7752
  throw new Error(
@@ -7615,7 +7758,7 @@ async function scaffoldFor(version, rnessDir) {
7615
7758
  async function declaredManager(rnessDir, pm) {
7616
7759
  try {
7617
7760
  const pkg = JSON.parse(
7618
- await readFile10(join24(rnessDir, "package.json"), "utf8")
7761
+ await readFile10(join25(rnessDir, "package.json"), "utf8")
7619
7762
  );
7620
7763
  if (typeof pkg.packageManager === "string") return pkg.packageManager;
7621
7764
  } catch {
@@ -7625,7 +7768,7 @@ async function declaredManager(rnessDir, pm) {
7625
7768
  async function clonesToCommit(root, cwd, manifest, files) {
7626
7769
  const clones = [];
7627
7770
  for (const repo of Object.keys(manifest.repos)) {
7628
- const dir = join24(root, "org", repo);
7771
+ const dir = join25(root, "org", repo);
7629
7772
  if (!await exists(dir)) continue;
7630
7773
  try {
7631
7774
  const paths = await changedPaths(dir, files);
@@ -7643,7 +7786,7 @@ async function filesTheSyncWrites(root, rnessDir, manifest) {
7643
7786
  try {
7644
7787
  const { stdout } = await execFileP6(
7645
7788
  process.execPath,
7646
- [join24(pinned.dir, pinned.bin), "written-files", "--cwd", root],
7789
+ [join25(pinned.dir, pinned.bin), "written-files", "--cwd", root],
7647
7790
  {
7648
7791
  cwd: root,
7649
7792
  env: { ...process.env, NO_COLOR: "1", RNESS_NO_INSTALL: "1" },
@@ -7659,14 +7802,14 @@ async function filesTheSyncWrites(root, rnessDir, manifest) {
7659
7802
  }
7660
7803
  function finish(ui, cwd, rnessDir, label, target2, ending) {
7661
7804
  const rel = relative5(cwd, rnessDir) || ".";
7662
- const message2 = `chore: rness ${target2}`;
7805
+ const message3 = `chore: rness ${target2}`;
7663
7806
  const next = [];
7664
7807
  if (ending.commit === "committed") next.push(`git -C ${rel} push`);
7665
7808
  if (ending.commit === "refused")
7666
- next.push(`git -C ${rel} commit -m "${message2}"`);
7809
+ next.push(`git -C ${rel} commit -m "${message3}"`);
7667
7810
  for (const c of ending.clones)
7668
7811
  next.push(
7669
- `git -C ${c.dir} add ${c.paths.join(" ")} && git -C ${c.dir} commit -m "${message2}"`
7812
+ `git -C ${c.dir} add ${c.paths.join(" ")} && git -C ${c.dir} commit -m "${message3}"`
7670
7813
  );
7671
7814
  if (ending.commit !== "nothing")
7672
7815
  next.push("# teammates: git pull \u2014 the next rness command installs it");
@@ -7839,7 +7982,7 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
7839
7982
  await source.cleanup?.();
7840
7983
  }
7841
7984
  }
7842
- const packageFile = join24(ws.rnessDir, "package.json");
7985
+ const packageFile = join25(ws.rnessDir, "package.json");
7843
7986
  const restore = /* @__PURE__ */ new Map([
7844
7987
  [packageFile, await readFile10(packageFile, "utf8")]
7845
7988
  ]);
@@ -7855,8 +7998,8 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
7855
7998
  () => ["installed", `dependencies with ${pm}`]
7856
7999
  );
7857
8000
  } catch (e) {
7858
- const message3 = e instanceof Error ? e.message : String(e);
7859
- ui.error(message3);
8001
+ const message4 = e instanceof Error ? e.message : String(e);
8002
+ ui.error(message4);
7860
8003
  if (!merged) {
7861
8004
  for (const [file, text] of restore) await writeFileAtomic(file, text);
7862
8005
  ui.hint(
@@ -7869,7 +8012,7 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
7869
8012
  `the scaffold merge is still staged; undo it with: git -C ${relative5(cwd, ws.rnessDir) || "."} merge --abort`,
7870
8013
  "stderr"
7871
8014
  );
7872
- if (/minimum.?release.?age|NO_MATURE/i.test(message3))
8015
+ if (/minimum.?release.?age|NO_MATURE/i.test(message4))
7873
8016
  ui.hint(
7874
8017
  "pnpm refuses versions published less than 24 h ago: add '@rness/cli' under minimumReleaseAgeExclude in .rness/pnpm-workspace.yaml",
7875
8018
  "stderr"
@@ -7888,13 +8031,13 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
7888
8031
  ui.gitIsSilent = true;
7889
8032
  const code = await syncBlocks(ui, ws.root, basename5(ws.root));
7890
8033
  if (code !== 0) return code;
7891
- const message2 = `chore: rness ${target2}`;
7892
- const committed = await commitUpgrade(ws.rnessDir, message2);
8034
+ const message3 = `chore: rness ${target2}`;
8035
+ const committed = await commitUpgrade(ws.rnessDir, message3);
7893
8036
  if (committed.kind === "committed")
7894
8037
  ui.line(
7895
8038
  "committed",
7896
- `${label} \u2014 ${message2}`,
7897
- `Committed ${label}: ${message2}`
8039
+ `${label} \u2014 ${message3}`,
8040
+ `Committed ${label}: ${message3}`
7898
8041
  );
7899
8042
  if (committed.kind === "refused") {
7900
8043
  ui.error(
@@ -8008,8 +8151,8 @@ async function validateCommand(opts, deps = {}) {
8008
8151
  import { Command, CommanderError, Option } from "commander";
8009
8152
 
8010
8153
  // src/commands/doc.ts
8011
- import { access as access2, mkdir as mkdir7, readFile as readFile11, writeFile as writeFile4 } from "fs/promises";
8012
- import { join as join25 } from "path";
8154
+ import { mkdir as mkdir7, readFile as readFile11, writeFile as writeFile4 } from "fs/promises";
8155
+ import { join as join26 } from "path";
8013
8156
  var NUMBERED = Object.keys(STATUSES);
8014
8157
  var SECTIONS = {
8015
8158
  specs: [
@@ -8033,8 +8176,8 @@ function headingsOf(template) {
8033
8176
  return [...template.matchAll(/^## (.+)$/gm)].map((m) => (m[1] ?? "").trim());
8034
8177
  }
8035
8178
  async function adrSections(rnessDir) {
8036
- const own = join25(rnessDir, "adr", "0000-template.md");
8037
- const shipped = join25(scaffoldDir(), "adr", "0000-template.md");
8179
+ const own = join26(rnessDir, "adr", "0000-template.md");
8180
+ const shipped = join26(scaffoldDir(), "adr", "0000-template.md");
8038
8181
  for (const file of [own, shipped]) {
8039
8182
  try {
8040
8183
  const sections = headingsOf(await readFile11(file, "utf8"));
@@ -8044,6 +8187,19 @@ async function adrSections(rnessDir) {
8044
8187
  }
8045
8188
  return ["Context", "Decision", "Alternatives Considered", "Consequences"];
8046
8189
  }
8190
+ async function writeDocument(file, text) {
8191
+ try {
8192
+ await writeFile4(file, text, { flag: "wx" });
8193
+ } catch (e) {
8194
+ if (e.code !== "EEXIST") throw e;
8195
+ process.stderr.write(`${file} exists; nothing written
8196
+ `);
8197
+ return 1;
8198
+ }
8199
+ process.stdout.write(`${file}
8200
+ `);
8201
+ return 0;
8202
+ }
8047
8203
  async function docNewCommand(collection, opts) {
8048
8204
  if (!NUMBERED.includes(collection)) {
8049
8205
  process.stderr.write(
@@ -8055,7 +8211,7 @@ async function docNewCommand(collection, opts) {
8055
8211
  const name = collection;
8056
8212
  try {
8057
8213
  const ws = await findWorkspace(opts.cwd ?? process.cwd());
8058
- const dir = join25(ws.rnessDir, name);
8214
+ const dir = join26(ws.rnessDir, name);
8059
8215
  await mkdir7(dir, { recursive: true });
8060
8216
  let highest = 0;
8061
8217
  for (const item of await collectMarkdown(dir)) {
@@ -8064,7 +8220,7 @@ async function docNewCommand(collection, opts) {
8064
8220
  }
8065
8221
  const number = String(highest + 1).padStart(4, "0");
8066
8222
  const title = opts.title?.trim() || "Untitled";
8067
- const file = join25(dir, `${number}-${slugOf(title)}.md`);
8223
+ const file = join26(dir, `${number}-${slugOf(title)}.md`);
8068
8224
  const status2 = STATUSES[name]?.[0] ?? "Draft";
8069
8225
  const date = today();
8070
8226
  const front = [
@@ -8083,20 +8239,8 @@ async function docNewCommand(collection, opts) {
8083
8239
  ...name === "plans" ? [] : ["", "---"],
8084
8240
  ...sections.flatMap((s) => ["", `## ${s}`])
8085
8241
  ];
8086
- try {
8087
- await access2(file);
8088
- process.stderr.write(`${file} exists; nothing written
8089
- `);
8090
- return 1;
8091
- } catch {
8092
- }
8093
- await writeFile4(file, `${[...front, "", ...body].join("\n")}
8094
- `, {
8095
- flag: "wx"
8096
- });
8097
- process.stdout.write(`${file}
8242
+ return await writeDocument(file, `${[...front, "", ...body].join("\n")}
8098
8243
  `);
8099
- return 0;
8100
8244
  } catch (e) {
8101
8245
  return reportError(e);
8102
8246
  }
@@ -8129,9 +8273,243 @@ password=${resolved.token}
8129
8273
  import { realpath as realpath4 } from "fs/promises";
8130
8274
  import { isAbsolute as isAbsolute2, posix as posix8, relative as relative7, resolve as resolve7, sep as sep5 } from "path";
8131
8275
 
8276
+ // src/core/claude-code.ts
8277
+ import { mkdir as mkdir8, readFile as readFile12 } from "fs/promises";
8278
+ import { join as join27 } from "path";
8279
+ var MOD_FLOOR = "2.1.280";
8280
+ var AGENT = /^claude-code_(\d+)-(\d+)-(\d+)_[a-z]+$/;
8281
+ function claudeCodeVersion(env) {
8282
+ const m = AGENT.exec(env["AI_AGENT"] ?? "");
8283
+ return m === null ? null : `${m[1]}.${m[2]}.${m[3]}`;
8284
+ }
8285
+ function belowModFloor(version) {
8286
+ const have = version.split(".").map(Number);
8287
+ const floor = MOD_FLOOR.split(".").map(Number);
8288
+ for (let i = 0; i < 3; i++) {
8289
+ const a = have[i] ?? 0;
8290
+ const b = floor[i] ?? 0;
8291
+ if (a !== b) return a < b;
8292
+ }
8293
+ return false;
8294
+ }
8295
+ var noticeFile = () => join27(configDir(), "claude-code.json");
8296
+ async function shownVersions() {
8297
+ try {
8298
+ const data = JSON.parse(await readFile12(noticeFile(), "utf8"));
8299
+ const shown = data !== null && typeof data === "object" ? data["modNoticeShown"] : void 0;
8300
+ return Array.isArray(shown) ? shown.filter((v) => typeof v === "string") : [];
8301
+ } catch {
8302
+ return [];
8303
+ }
8304
+ }
8305
+ async function takeModNotice(env) {
8306
+ const version = claudeCodeVersion(env);
8307
+ if (version === null || !belowModFloor(version)) return null;
8308
+ if ((env["CLAUDE_CODE_ENTRYPOINT"] ?? "").startsWith("sdk-")) return null;
8309
+ const shown = await shownVersions();
8310
+ if (shown.includes(version)) return null;
8311
+ try {
8312
+ await mkdir8(configDir(), { recursive: true, mode: 448 });
8313
+ await writeFileAtomic(
8314
+ noticeFile(),
8315
+ `${JSON.stringify({ modNoticeShown: [...shown, version] })}
8316
+ `
8317
+ );
8318
+ } catch {
8319
+ }
8320
+ return `rness: Claude Code ${version} shows no rness band, status line or pane; ${MOD_FLOOR} or later does \u2014 claude update`;
8321
+ }
8322
+
8323
+ // src/core/edit-guard.ts
8324
+ function occurrences(text, needle) {
8325
+ const spans = [];
8326
+ for (let i = text.indexOf(needle); i !== -1; i = text.indexOf(needle, i + needle.length))
8327
+ spans.push([i, i + needle.length]);
8328
+ return spans;
8329
+ }
8330
+ var QUOTES = {
8331
+ "\u2018": "'",
8332
+ "\u2019": "'",
8333
+ "\u201C": '"',
8334
+ "\u201D": '"'
8335
+ };
8336
+ function loose(text) {
8337
+ let out = "";
8338
+ const at = [];
8339
+ for (let i = 0; i < text.length; i++) {
8340
+ const c = text[i] ?? "";
8341
+ if (c === "\r" && text[i + 1] === "\n") continue;
8342
+ out += QUOTES[c] ?? c;
8343
+ at.push(i);
8344
+ }
8345
+ at.push(text.length);
8346
+ return { text: out, at };
8347
+ }
8348
+ function replayEdit(text, oldString, newString, all) {
8349
+ if (oldString === "") return null;
8350
+ let spans = occurrences(text, oldString);
8351
+ if (spans.length === 0) {
8352
+ const file = loose(text);
8353
+ spans = occurrences(file.text, loose(oldString).text).map(
8354
+ ([s, e]) => [file.at[s] ?? 0, (file.at[e - 1] ?? 0) + 1]
8355
+ );
8356
+ }
8357
+ if (spans.length === 0 || !all && spans.length !== 1) return null;
8358
+ let after = text;
8359
+ for (const [s, e] of [...spans].reverse())
8360
+ after = `${after.slice(0, s)}${newString}${after.slice(e)}`;
8361
+ return { after, spans };
8362
+ }
8363
+ var OWNED = /(?:^|\/)\.claude\/skills\/rness\//;
8364
+ var ownedWhole = (rel) => OWNED.test(rel);
8365
+ function ownedRefusal(label) {
8366
+ return `rness: ${label} is written whole by rness sync, and the next sync overwrites it. Leave it to rness.`;
8367
+ }
8368
+ var MARKER = /^<!-- rness: (\S+) -->$/;
8369
+ function blockOf(text) {
8370
+ const lines = text.split(/\r?\n/);
8371
+ const found = findBlock(lines);
8372
+ if (found.kind !== "one") return null;
8373
+ const starts = [0];
8374
+ for (const m of text.matchAll(/\r?\n/g))
8375
+ starts.push((m.index ?? 0) + m[0].length);
8376
+ const from = starts[found.begin] ?? 0;
8377
+ const to = (starts[found.end] ?? 0) + (lines[found.end] ?? "").length;
8378
+ return { begin: found.begin, end: found.end, from, to, lines };
8379
+ }
8380
+ function lineAt(text, offset) {
8381
+ return text.slice(0, offset).split(/\r?\n/).length - 1;
8382
+ }
8383
+ function sourceOf(block, line3) {
8384
+ for (let i = Math.min(line3, block.end); i > block.begin; i--) {
8385
+ const m = MARKER.exec((block.lines[i] ?? "").trimEnd());
8386
+ if (m !== null) return `.rness/${m[1] ?? ""}`;
8387
+ }
8388
+ return ".rness/ and rness.json";
8389
+ }
8390
+ function blockRefusal(label, block, source) {
8391
+ return `rness: lines ${block.begin + 1}\u2013${block.end + 1} of ${label} are generated by rness sync from ${source}. Edit that file, then run rness sync.`;
8392
+ }
8393
+ function editRefusal(label, before, spans) {
8394
+ const block = blockOf(before);
8395
+ if (block === null) return null;
8396
+ const hit = spans.find(([s, e]) => s < block.to && e > block.from);
8397
+ if (hit === void 0) return null;
8398
+ return blockRefusal(
8399
+ label,
8400
+ block,
8401
+ sourceOf(block, lineAt(before, Math.max(hit[0], block.from)))
8402
+ );
8403
+ }
8404
+ function writeRefusal(label, before, after) {
8405
+ const block = blockOf(before);
8406
+ if (block === null) return null;
8407
+ const kept = blockOf(after);
8408
+ const was = before.slice(block.from, block.to);
8409
+ if (kept !== null && after.slice(kept.from, kept.to) === was) return null;
8410
+ const next = kept === null ? [] : kept.lines.slice(kept.begin, kept.end + 1);
8411
+ const old = block.lines.slice(block.begin, block.end + 1);
8412
+ const first = old.findIndex((line3, i) => line3 !== next[i]);
8413
+ return blockRefusal(
8414
+ label,
8415
+ block,
8416
+ kept === null ? ".rness/ and rness.json" : sourceOf(block, block.begin + Math.max(first, 0))
8417
+ );
8418
+ }
8419
+
8420
+ // src/core/safety-net.ts
8421
+ var SHOWN_PROBLEMS = 3;
8422
+ var message = (e) => e instanceof Error ? e.message : String(e);
8423
+ async function safetyNet(ws, manifest) {
8424
+ const notes = [];
8425
+ const drift = await pinDrift(ws.rnessDir);
8426
+ if (drift !== null)
8427
+ notes.push(
8428
+ `.rness pins @rness/cli ${drift.pin} but ${drift.installed ?? "nothing"} is installed \u2014 run ${await workspacePackageManager(ws.rnessDir)} install in .rness`
8429
+ );
8430
+ try {
8431
+ const { problems } = await checkWorkspace(ws, manifest);
8432
+ if (problems.length > 0)
8433
+ notes.push(
8434
+ `${count(problems.length, ["problem", "problems"])} in the workspace context \u2014 run rness validate`,
8435
+ ...problems.slice(0, SHOWN_PROBLEMS).map((p) => ` ${p}`)
8436
+ );
8437
+ } catch (e) {
8438
+ notes.push(message(e));
8439
+ }
8440
+ return notes;
8441
+ }
8442
+
8443
+ // src/pulse/detached.ts
8444
+ import { spawn as nodeSpawn } from "child_process";
8445
+ import { mkdir as mkdir9, open as open2, readFile as readFile13, rm as rm9, stat as stat4, writeFile as writeFile5 } from "fs/promises";
8446
+ import { join as join28 } from "path";
8447
+ import { setTimeout as delay2 } from "timers/promises";
8448
+ var detached = (args, cwd) => {
8449
+ const bin = process.argv[1];
8450
+ if (bin === void 0) return;
8451
+ const child = nodeSpawn(process.execPath, [bin, ...args], {
8452
+ detached: true,
8453
+ stdio: "ignore",
8454
+ cwd,
8455
+ env: { ...process.env, RNESS_NO_DELEGATE: "1" }
8456
+ });
8457
+ child.on("error", () => void 0);
8458
+ child.unref();
8459
+ };
8460
+ var failureFile = () => join28(configDir(), "pulse.json");
8461
+ async function recordFailure(reason3) {
8462
+ try {
8463
+ await mkdir9(configDir(), { recursive: true, mode: 448 });
8464
+ await writeFile5(
8465
+ failureFile(),
8466
+ `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), reason: reason3 })}
8467
+ `
8468
+ );
8469
+ } catch {
8470
+ }
8471
+ }
8472
+ async function takeFailure() {
8473
+ try {
8474
+ const data = JSON.parse(await readFile13(failureFile(), "utf8"));
8475
+ await rm9(failureFile(), { force: true });
8476
+ const reason3 = data !== null && typeof data === "object" ? data["reason"] : void 0;
8477
+ return typeof reason3 === "string" && reason3 !== "" ? reason3 : null;
8478
+ } catch {
8479
+ return null;
8480
+ }
8481
+ }
8482
+ var PULSE_LOCK_STALE_MS = 15 * 6e4;
8483
+ async function oneSyncAtATime(key, work, options = {}) {
8484
+ await mkdir9(configDir(), { recursive: true, mode: 448 });
8485
+ const file = join28(configDir(), `pulse-${key}.lock`);
8486
+ for (; ; ) {
8487
+ try {
8488
+ const handle = await open2(file, "wx");
8489
+ await handle.close();
8490
+ break;
8491
+ } catch (e) {
8492
+ if (e.code !== "EEXIST") throw e;
8493
+ const age = await stat4(file).then(
8494
+ (s) => Date.now() - s.mtimeMs,
8495
+ () => 0
8496
+ );
8497
+ if (age > PULSE_LOCK_STALE_MS) await rm9(file, { force: true });
8498
+ else if (options.wait !== true) return false;
8499
+ else await (options.sleep ?? ((ms) => delay2(ms)))(1e3);
8500
+ }
8501
+ }
8502
+ try {
8503
+ await work();
8504
+ return true;
8505
+ } finally {
8506
+ await rm9(file, { force: true });
8507
+ }
8508
+ }
8509
+
8132
8510
  // src/mcp/tools.ts
8133
- import { readFile as readFile12, realpath as realpath3, stat as stat4 } from "fs/promises";
8134
- import { isAbsolute, join as join26, posix as posix7, relative as relative6, resolve as resolve6, sep as sep4 } from "path";
8511
+ import { readFile as readFile14, realpath as realpath3, stat as stat5 } from "fs/promises";
8512
+ import { isAbsolute, join as join29, posix as posix7, relative as relative6, resolve as resolve6, sep as sep4 } from "path";
8135
8513
  var LABELS = {
8136
8514
  standards: "Standards",
8137
8515
  adr: "Decisions",
@@ -8160,7 +8538,7 @@ async function documents(rnessDir, only) {
8160
8538
  const docs = [];
8161
8539
  for (const collection of COLLECTIONS) {
8162
8540
  if (only !== void 0 && collection !== only) continue;
8163
- for (const item of await collectMarkdown(join26(rnessDir, collection))) {
8541
+ for (const item of await collectMarkdown(join29(rnessDir, collection))) {
8164
8542
  const path = `${collection}/${item.rel}`;
8165
8543
  if (path === TEMPLATE2) continue;
8166
8544
  const status2 = item.fields?.["status"];
@@ -8276,13 +8654,13 @@ async function read(cwd, args) {
8276
8654
  }
8277
8655
  if (!real.startsWith(`${root}${sep4}`))
8278
8656
  return refuse(`${path}: leads outside .rness/`);
8279
- const info = await stat4(real);
8657
+ const info = await stat5(real);
8280
8658
  if (!info.isFile()) return refuse(`${path}: not a file`);
8281
8659
  if (info.size > READ_LIMIT)
8282
8660
  return refuse(
8283
8661
  `${path}: ${info.size} bytes, over the 256 KiB rness_read returns`
8284
8662
  );
8285
- return { text: await readFile12(real, "utf8") };
8663
+ return { text: await readFile14(real, "utf8") };
8286
8664
  }
8287
8665
  async function search(cwd, args) {
8288
8666
  const { ws } = await workspaceAt(cwd);
@@ -8388,74 +8766,7 @@ function mcpTools(cwd) {
8388
8766
  ];
8389
8767
  }
8390
8768
 
8391
- // src/pulse/detached.ts
8392
- import { spawn as nodeSpawn } from "child_process";
8393
- import { mkdir as mkdir8, open as open2, readFile as readFile13, rm as rm9, stat as stat5, writeFile as writeFile5 } from "fs/promises";
8394
- import { join as join27 } from "path";
8395
- import { setTimeout as delay2 } from "timers/promises";
8396
- var detached = (args, cwd) => {
8397
- const bin = process.argv[1];
8398
- if (bin === void 0) return;
8399
- const child = nodeSpawn(process.execPath, [bin, ...args], {
8400
- detached: true,
8401
- stdio: "ignore",
8402
- cwd,
8403
- env: { ...process.env, RNESS_NO_DELEGATE: "1" }
8404
- });
8405
- child.on("error", () => void 0);
8406
- child.unref();
8407
- };
8408
- var failureFile = () => join27(configDir(), "pulse.json");
8409
- async function recordFailure(reason3) {
8410
- try {
8411
- await mkdir8(configDir(), { recursive: true, mode: 448 });
8412
- await writeFile5(
8413
- failureFile(),
8414
- `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), reason: reason3 })}
8415
- `
8416
- );
8417
- } catch {
8418
- }
8419
- }
8420
- async function takeFailure() {
8421
- try {
8422
- const data = JSON.parse(await readFile13(failureFile(), "utf8"));
8423
- await rm9(failureFile(), { force: true });
8424
- const reason3 = data !== null && typeof data === "object" ? data["reason"] : void 0;
8425
- return typeof reason3 === "string" && reason3 !== "" ? reason3 : null;
8426
- } catch {
8427
- return null;
8428
- }
8429
- }
8430
- var PULSE_LOCK_STALE_MS = 15 * 6e4;
8431
- async function oneSyncAtATime(key, work, options = {}) {
8432
- await mkdir8(configDir(), { recursive: true, mode: 448 });
8433
- const file = join27(configDir(), `pulse-${key}.lock`);
8434
- for (; ; ) {
8435
- try {
8436
- const handle = await open2(file, "wx");
8437
- await handle.close();
8438
- break;
8439
- } catch (e) {
8440
- if (e.code !== "EEXIST") throw e;
8441
- const age = await stat5(file).then(
8442
- (s) => Date.now() - s.mtimeMs,
8443
- () => 0
8444
- );
8445
- if (age > PULSE_LOCK_STALE_MS) await rm9(file, { force: true });
8446
- else if (options.wait !== true) return false;
8447
- else await (options.sleep ?? ((ms) => delay2(ms)))(1e3);
8448
- }
8449
- }
8450
- try {
8451
- await work();
8452
- return true;
8453
- } finally {
8454
- await rm9(file, { force: true });
8455
- }
8456
- }
8457
-
8458
- // src/commands/hook.ts
8769
+ // src/commands/snapshot.ts
8459
8770
  var NOUNS = {
8460
8771
  standards: ["standard", "standards"],
8461
8772
  adr: ["decision", "decisions"],
@@ -8463,7 +8774,37 @@ var NOUNS = {
8463
8774
  plans: ["plan", "plans"],
8464
8775
  skills: ["skill", "skills"]
8465
8776
  };
8466
- var SHOWN_PROBLEMS = 3;
8777
+ async function scopeBanner(ws, manifest, scope) {
8778
+ const summary = await scopeSummary(ws, manifest, scope);
8779
+ const counts = COLLECTIONS.flatMap((c) => {
8780
+ const n = summary.counts[c];
8781
+ return n === void 0 ? [] : [count(n, NOUNS[c])];
8782
+ });
8783
+ const banner2 = `rness ${VERSION} \xB7 ${workspaceName(manifest, ws.root)} \xB7 ${scope === null ? "global scope" : `scope ${scope}`} \u2014 ${counts.length === 0 ? "nothing applies yet" : counts.join(", ")}`;
8784
+ return { banner: banner2, lines: summary.lines };
8785
+ }
8786
+ var noteLines = (notes) => notes.map((n) => `rness: ${n}`);
8787
+ function statusLine(scope, inProgress, notes) {
8788
+ const acting = notes.filter((n) => !n.startsWith(" ")).length;
8789
+ return `${scope ?? "global"} \xB7 ${inProgress.length} in progress${acting > 0 ? ` \xB7 \u26A0 ${acting}` : ""}`;
8790
+ }
8791
+ async function statusSnapshot(ws, manifest, scope, tabs) {
8792
+ const { banner: banner2 } = await scopeBanner(ws, manifest, scope);
8793
+ const inProgress = await plansInProgress(ws, manifest, scope);
8794
+ const notes = await safetyNet(ws, manifest);
8795
+ return {
8796
+ version: VERSION,
8797
+ workspace: workspaceName(manifest, ws.root),
8798
+ scope,
8799
+ banner: banner2,
8800
+ statusLine: statusLine(scope, inProgress, notes),
8801
+ inProgress,
8802
+ notes: noteLines(notes),
8803
+ tabs
8804
+ };
8805
+ }
8806
+
8807
+ // src/commands/hook.ts
8467
8808
  async function readInput(input) {
8468
8809
  if (input.isTTY === true) return {};
8469
8810
  let text = "";
@@ -8475,35 +8816,13 @@ async function readInput(input) {
8475
8816
  return {};
8476
8817
  }
8477
8818
  }
8478
- var message = (e) => e instanceof Error ? e.message : String(e);
8819
+ var message2 = (e) => e instanceof Error ? e.message : String(e);
8479
8820
  function cwdOf(input, env) {
8480
8821
  const cwd = input["cwd"];
8481
8822
  if (typeof cwd === "string" && cwd !== "") return cwd;
8482
8823
  return env["CLAUDE_PROJECT_DIR"] ?? process.cwd();
8483
8824
  }
8484
8825
  var toPosix2 = (p) => p.split(sep5).join(posix8.sep);
8485
- function count(n, [one, many]) {
8486
- return `${n} ${n === 1 ? one : many}`;
8487
- }
8488
- async function safetyNet(ws, manifest) {
8489
- const notes = [];
8490
- const drift = await pinDrift(ws.rnessDir);
8491
- if (drift !== null)
8492
- notes.push(
8493
- `.rness pins @rness/cli ${drift.pin} but ${drift.installed ?? "nothing"} is installed \u2014 run ${await workspacePackageManager(ws.rnessDir)} install in .rness`
8494
- );
8495
- try {
8496
- const { problems } = await checkWorkspace(ws, manifest);
8497
- if (problems.length > 0)
8498
- notes.push(
8499
- `${count(problems.length, ["problem", "problems"])} in the workspace context \u2014 run rness validate`,
8500
- ...problems.slice(0, SHOWN_PROBLEMS).map((p) => ` ${p}`)
8501
- );
8502
- } catch (e) {
8503
- notes.push(message(e));
8504
- }
8505
- return notes;
8506
- }
8507
8826
  function sessionOf(input) {
8508
8827
  const id = input["session_id"];
8509
8828
  if (typeof id !== "string" || id === "") return null;
@@ -8531,21 +8850,6 @@ async function markDetached(ws, input, io, rest) {
8531
8850
  } catch {
8532
8851
  }
8533
8852
  }
8534
- async function plansInProgress(ws, manifest, scope) {
8535
- const context3 = await assembleContext({
8536
- rnessDir: ws.rnessDir,
8537
- manifest,
8538
- scope
8539
- });
8540
- const plans = context3.collections.find((c) => c.name === "plans");
8541
- return (plans?.files ?? []).filter((f) => {
8542
- try {
8543
- return parseFrontMatter(f.body)?.["status"] === "In progress";
8544
- } catch {
8545
- return false;
8546
- }
8547
- }).map((f) => `plans/${f.rel}`);
8548
- }
8549
8853
  async function sessionStart(input, io) {
8550
8854
  const cwd = cwdOf(input, io.env);
8551
8855
  let banner2 = null;
@@ -8555,15 +8859,11 @@ async function sessionStart(input, io) {
8555
8859
  const ws = await findWorkspace(cwd);
8556
8860
  const manifest = await loadManifest(ws.rnessDir);
8557
8861
  const scope = resolveScope(manifest, toPosix2(relative7(ws.root, cwd)));
8558
- const summary = await scopeSummary(ws, manifest, scope);
8559
- const counts = COLLECTIONS.flatMap((c) => {
8560
- const n = summary.counts[c];
8561
- return n === void 0 ? [] : [count(n, NOUNS[c])];
8562
- });
8563
- banner2 = `rness ${VERSION} \xB7 ${workspaceName(manifest, ws.root)} \xB7 ${scope === null ? "global scope" : `scope ${scope}`} \u2014 ${counts.length === 0 ? "nothing applies yet" : counts.join(", ")}`;
8862
+ const view = await scopeBanner(ws, manifest, scope);
8863
+ banner2 = view.banner;
8564
8864
  const at = toPosix2(relative7(cwd, ws.rnessDir)) || ".rness";
8565
8865
  context3 = [
8566
- ...summary.lines,
8866
+ ...view.lines,
8567
8867
  `These documents are in ${at}: read one there, or with rness_read when the rness MCP server is connected.`
8568
8868
  ];
8569
8869
  notes = await safetyNet(ws, manifest);
@@ -8575,14 +8875,17 @@ async function sessionStart(input, io) {
8575
8875
  (session) => paths.length === 0 ? null : ["--session", session, ...paths.flatMap((p) => ["--path", p])]
8576
8876
  );
8577
8877
  } catch (e) {
8578
- notes = [message(e)];
8878
+ notes = [message2(e)];
8579
8879
  }
8580
8880
  const failure = await takeFailure();
8581
8881
  if (failure !== null) notes.push(`pulse not updated \u2014 ${failure}`);
8582
- const warnings = notes.map((n) => `rness: ${n}`);
8882
+ const warnings = noteLines(notes);
8883
+ const compact = input["source"] === "compact";
8884
+ const tooOld = compact ? null : await takeModNotice(io.env);
8583
8885
  const shown = [
8584
- ...banner2 !== null && input["source"] !== "compact" ? [banner2] : [],
8585
- ...warnings
8886
+ ...banner2 !== null && !compact ? [banner2] : [],
8887
+ ...warnings,
8888
+ ...tooOld !== null ? [tooOld] : []
8586
8889
  ];
8587
8890
  return {
8588
8891
  ...shown.length > 0 ? { systemMessage: shown.join("\n") } : {},
@@ -8607,11 +8910,7 @@ async function postToolUse(input, io) {
8607
8910
  } catch {
8608
8911
  return 0;
8609
8912
  }
8610
- const file = resolve7(cwd, path);
8611
- const rel = inside(ws.rnessDir, file) ?? inside(
8612
- await realpath4(ws.rnessDir).catch(() => ws.rnessDir),
8613
- await realpath4(file).catch(() => file)
8614
- );
8913
+ const rel = await within(ws.rnessDir, resolve7(cwd, path));
8615
8914
  if (rel === null) return 0;
8616
8915
  const first = rel.split("/")[0] ?? "";
8617
8916
  let problems = [];
@@ -8620,7 +8919,7 @@ async function postToolUse(input, io) {
8620
8919
  const manifest = await loadManifest(ws.rnessDir);
8621
8920
  for (const s of Object.keys(manifest.scopes)) scopeChain(manifest, s);
8622
8921
  } catch (e) {
8623
- problems = [message(e)];
8922
+ problems = [message2(e)];
8624
8923
  }
8625
8924
  } else if (COLLECTIONS.includes(first) && rel.endsWith(".md"))
8626
8925
  problems = (await checkContract(ws.rnessDir)).filter(
@@ -8643,6 +8942,95 @@ async function postToolUse(input, io) {
8643
8942
  );
8644
8943
  return 2;
8645
8944
  }
8945
+ async function within(base, file) {
8946
+ return inside(base, file) ?? inside(
8947
+ await realpath4(base).catch(() => base),
8948
+ await realpath4(file).catch(() => file)
8949
+ );
8950
+ }
8951
+ function manifestProblems(text) {
8952
+ if (text === null) return [];
8953
+ try {
8954
+ const manifest = parseManifest(text);
8955
+ for (const s of Object.keys(manifest.scopes)) scopeChain(manifest, s);
8956
+ return [];
8957
+ } catch (e) {
8958
+ return [message2(e)];
8959
+ }
8960
+ }
8961
+ async function addedProblems(ws, rel, before, after) {
8962
+ let was;
8963
+ let will;
8964
+ if (rel === "rness.json") {
8965
+ was = manifestProblems(before);
8966
+ will = manifestProblems(after);
8967
+ } else {
8968
+ const first = rel.split("/")[0] ?? "";
8969
+ if (!COLLECTIONS.includes(first) || !rel.endsWith(".md"))
8970
+ return [];
8971
+ const mine = (p) => p.startsWith(`${rel}:`);
8972
+ was = (await checkContract(ws.rnessDir)).filter(mine);
8973
+ will = (await checkContract(ws.rnessDir, { rel, text: after })).filter(mine);
8974
+ }
8975
+ return will.filter((p) => !was.includes(p));
8976
+ }
8977
+ async function preToolUse(input, io) {
8978
+ const toolInput = input["tool_input"];
8979
+ if (toolInput === null || typeof toolInput !== "object") return 0;
8980
+ const args = toolInput;
8981
+ const path = args["file_path"];
8982
+ if (typeof path !== "string" || path === "") return 0;
8983
+ try {
8984
+ const cwd = cwdOf(input, io.env);
8985
+ const ws = await findWorkspace(cwd);
8986
+ const file = resolve7(cwd, path);
8987
+ const label = await within(ws.root, file);
8988
+ if (label === null) return 0;
8989
+ if (ownedWhole(label)) return refuse2(io, [ownedRefusal(label)]);
8990
+ const rel = await within(ws.rnessDir, file);
8991
+ const blocked = BLOCK_FILES.includes(posix8.basename(label));
8992
+ if (rel === null && !blocked) return 0;
8993
+ const before = await readOrNull(file);
8994
+ let after;
8995
+ let spans = null;
8996
+ if (input["tool_name"] === "Write") {
8997
+ const content = args["content"];
8998
+ if (typeof content !== "string") return 0;
8999
+ after = content;
9000
+ } else {
9001
+ const { old_string, new_string, replace_all } = args;
9002
+ if (before === null || typeof old_string !== "string" || typeof new_string !== "string")
9003
+ return 0;
9004
+ const replayed = replayEdit(
9005
+ before,
9006
+ old_string,
9007
+ new_string,
9008
+ replace_all === true
9009
+ );
9010
+ if (replayed === null) return 0;
9011
+ after = replayed.after;
9012
+ spans = replayed.spans;
9013
+ }
9014
+ if (blocked && before !== null) {
9015
+ const reason3 = spans === null ? writeRefusal(label, before, after) : editRefusal(label, before, spans);
9016
+ if (reason3 !== null) return refuse2(io, [reason3]);
9017
+ }
9018
+ if (rel === null) return 0;
9019
+ const added = await addedProblems(ws, rel, before, after);
9020
+ if (added.length === 0) return 0;
9021
+ return refuse2(io, [
9022
+ ...added.map((p) => `rness: ${p}`),
9023
+ `Fix the edit of ${rel}; rness validate checks the whole workspace.`
9024
+ ]);
9025
+ } catch {
9026
+ return 0;
9027
+ }
9028
+ }
9029
+ function refuse2(io, lines) {
9030
+ io.error.write(`${lines.join("\n")}
9031
+ `);
9032
+ return 2;
9033
+ }
8646
9034
  async function sessionEnd(input, io) {
8647
9035
  try {
8648
9036
  const ws = await findWorkspace(cwdOf(input, io.env));
@@ -8661,14 +9049,15 @@ async function hookCommand(event, io = {
8661
9049
  error: process.stderr,
8662
9050
  env: process.env
8663
9051
  }) {
8664
- if (event !== "session-start" && event !== "post-tool-use" && event !== "session-end") {
9052
+ if (event !== "session-start" && event !== "pre-tool-use" && event !== "post-tool-use" && event !== "session-end") {
8665
9053
  io.error.write(
8666
- `unknown hook event ${JSON.stringify(event)} (known: session-start, post-tool-use, session-end)
9054
+ `unknown hook event ${JSON.stringify(event)} (known: session-start, pre-tool-use, post-tool-use, session-end)
8667
9055
  `
8668
9056
  );
8669
9057
  return 1;
8670
9058
  }
8671
9059
  const input = await readInput(io.input);
9060
+ if (event === "pre-tool-use") return preToolUse(input, io);
8672
9061
  if (event === "post-tool-use") return postToolUse(input, io);
8673
9062
  if (event === "session-end") return sessionEnd(input, io);
8674
9063
  io.output.write(`${JSON.stringify(await sessionStart(input, io))}
@@ -8694,31 +9083,31 @@ var SERVER_INFO_KEY = "io.modelcontextprotocol/serverInfo";
8694
9083
  function isObject2(value) {
8695
9084
  return value !== null && typeof value === "object" && !Array.isArray(value);
8696
9085
  }
8697
- function errorLine(id, code, message2, data) {
9086
+ function errorLine(id, code, message3, data) {
8698
9087
  return JSON.stringify({
8699
9088
  jsonrpc: "2.0",
8700
9089
  id,
8701
- error: data === void 0 ? { code, message: message2 } : { code, message: message2, data }
9090
+ error: data === void 0 ? { code, message: message3 } : { code, message: message3, data }
8702
9091
  });
8703
9092
  }
8704
9093
  function createProtocol(opts) {
8705
9094
  const serverInfo = { name: opts.name, version: opts.version };
8706
9095
  return {
8707
9096
  async handle(line3) {
8708
- let message2;
9097
+ let message3;
8709
9098
  try {
8710
- message2 = JSON.parse(line3);
9099
+ message3 = JSON.parse(line3);
8711
9100
  } catch {
8712
9101
  return errorLine(null, -32700, "Parse error");
8713
9102
  }
8714
- const rawId = isObject2(message2) ? message2["id"] : void 0;
9103
+ const rawId = isObject2(message3) ? message3["id"] : void 0;
8715
9104
  const id = typeof rawId === "string" || typeof rawId === "number" ? rawId : null;
8716
- if (!isObject2(message2) || message2["jsonrpc"] !== "2.0" || typeof message2["method"] !== "string")
9105
+ if (!isObject2(message3) || message3["jsonrpc"] !== "2.0" || typeof message3["method"] !== "string")
8717
9106
  return errorLine(id, -32600, "Invalid Request");
8718
- if (!("id" in message2)) return null;
9107
+ if (!("id" in message3)) return null;
8719
9108
  if (id === null) return errorLine(null, -32600, "Invalid Request");
8720
- const method = message2["method"];
8721
- const params = isObject2(message2["params"]) ? message2["params"] : {};
9109
+ const method = message3["method"];
9110
+ const params = isObject2(message3["params"]) ? message3["params"] : {};
8722
9111
  const meta = isObject2(params["_meta"]) ? params["_meta"] : null;
8723
9112
  const modern = meta !== null && typeof meta[VERSION_KEY] === "string";
8724
9113
  if (modern) {
@@ -8826,13 +9215,13 @@ async function mcpCommand(opts, io = {
8826
9215
  }
8827
9216
 
8828
9217
  // src/commands/pulse.ts
8829
- import { readFile as readFile15, readdir as readdir5 } from "fs/promises";
8830
- import { join as join28 } from "path";
9218
+ import { readFile as readFile16, readdir as readdir5 } from "fs/promises";
9219
+ import { join as join30 } from "path";
8831
9220
  import { setTimeout as delay3 } from "timers/promises";
8832
9221
 
8833
9222
  // src/pulse/opened.ts
8834
- import { mkdir as mkdir9, readFile as readFile14, rename as rename3, rm as rm10, writeFile as writeFile6 } from "fs/promises";
8835
- import { dirname as dirname9 } from "path";
9223
+ import { mkdir as mkdir10, readFile as readFile15, rename as rename3, rm as rm10, writeFile as writeFile6 } from "fs/promises";
9224
+ import { dirname as dirname10 } from "path";
8836
9225
  var NAME2 = "rness/opened";
8837
9226
  function openedFile(rnessDir) {
8838
9227
  return gitPath(rnessDir, NAME2);
@@ -8843,7 +9232,7 @@ async function readOpened(file) {
8843
9232
  if (file === null) return opened;
8844
9233
  let data;
8845
9234
  try {
8846
- data = JSON.parse(await readFile14(file, "utf8"));
9235
+ data = JSON.parse(await readFile15(file, "utf8"));
8847
9236
  } catch {
8848
9237
  return opened;
8849
9238
  }
@@ -8862,7 +9251,7 @@ async function writeOpened(file, opened) {
8862
9251
  return;
8863
9252
  }
8864
9253
  const entries = [...opened].sort(([a], [b]) => a - b);
8865
- await mkdir9(dirname9(file), { recursive: true });
9254
+ await mkdir10(dirname10(file), { recursive: true });
8866
9255
  const temporary = `${file}.${process.pid}`;
8867
9256
  await writeFile6(
8868
9257
  temporary,
@@ -8940,10 +9329,10 @@ async function directoriesOf(dir) {
8940
9329
  }
8941
9330
  }
8942
9331
  async function scopeOf(c, name, tabs, owners) {
8943
- const within = (collection) => (path) => path.startsWith(`${collection}/`);
9332
+ const within2 = (collection) => (path) => path.startsWith(`${collection}/`);
8944
9333
  if (name === PULSE) {
8945
9334
  const shown = tabs.filter((t) => !owners.includes(t.name));
8946
- const elsewhere = (path) => owners.some((o) => within(o)(path));
9335
+ const elsewhere = (path) => owners.some((o) => within2(o)(path));
8947
9336
  return {
8948
9337
  label: "Agent Pulse",
8949
9338
  want: desiredOf(shown, c.org),
@@ -8958,7 +9347,7 @@ async function scopeOf(c, name, tabs, owners) {
8958
9347
  label: labelOf(name),
8959
9348
  rows: []
8960
9349
  };
8961
- const dir = join28(c.rnessDir, name);
9350
+ const dir = join30(c.rnessDir, name);
8962
9351
  const shape = await readShape(c.rnessDir, name);
8963
9352
  const fronts = new Map(
8964
9353
  (await collectMarkdown(dir)).map((item) => [
@@ -8976,7 +9365,7 @@ async function scopeOf(c, name, tabs, owners) {
8976
9365
  want,
8977
9366
  layout: layoutOf([tab], { shape, want }),
8978
9367
  elsewhere: () => false,
8979
- mine: within(name),
9368
+ mine: within2(name),
8980
9369
  own: { shape, updates: await readUpdates(c.rnessDir, name) }
8981
9370
  };
8982
9371
  }
@@ -9004,7 +9393,7 @@ async function working(ui, doing, count3, work) {
9004
9393
  async function filesOf2(rnessDir) {
9005
9394
  const files = /* @__PURE__ */ new Set();
9006
9395
  const walk = async (rel) => {
9007
- const dir = rel === "" ? rnessDir : join28(rnessDir, ...rel.split("/"));
9396
+ const dir = rel === "" ? rnessDir : join30(rnessDir, ...rel.split("/"));
9008
9397
  for (const e of await readdir5(dir, { withFileTypes: true })) {
9009
9398
  const path = rel === "" ? e.name : `${rel}/${e.name}`;
9010
9399
  if (e.isDirectory()) {
@@ -9116,7 +9505,7 @@ async function syncBoard(c, board, ui, sayLayout, scope) {
9116
9505
  w.path,
9117
9506
  issueBody({
9118
9507
  path: w.path,
9119
- text: await readFile15(join28(c.rnessDir, ...w.path.split("/")), "utf8"),
9508
+ text: await readFile16(join30(c.rnessDir, ...w.path.split("/")), "utf8"),
9120
9509
  org: c.org,
9121
9510
  numbers,
9122
9511
  files
@@ -9329,6 +9718,7 @@ async function mark(opts) {
9329
9718
  }
9330
9719
 
9331
9720
  // src/commands/status.ts
9721
+ import { posix as posix9, relative as relative8, sep as sep6 } from "path";
9332
9722
  import { emitKeypressEvents } from "readline";
9333
9723
 
9334
9724
  // src/core/status-view.ts
@@ -9531,7 +9921,8 @@ function showView(tabs, workspace, start, { input, output }) {
9531
9921
  }
9532
9922
  async function statusCommand(tab, opts, terminal = { input: process.stdin, output: process.stdout }) {
9533
9923
  try {
9534
- const ws = await findWorkspace(opts.cwd ?? process.cwd());
9924
+ const cwd = opts.cwd ?? process.cwd();
9925
+ const ws = await findWorkspace(cwd);
9535
9926
  const manifest = await loadManifest(ws.rnessDir);
9536
9927
  const workspace = workspaceName(manifest, ws.root);
9537
9928
  const tabs = await statusTabs(ws.rnessDir);
@@ -9543,6 +9934,18 @@ async function statusCommand(tab, opts, terminal = { input: process.stdin, outpu
9543
9934
  );
9544
9935
  return 2;
9545
9936
  }
9937
+ const shown = wanted === void 0 ? tabs : [wanted];
9938
+ if (opts.json === true) {
9939
+ const scope = resolveScope(
9940
+ manifest,
9941
+ relative8(ws.root, cwd).split(sep6).join(posix9.sep)
9942
+ );
9943
+ terminal.output.write(
9944
+ `${JSON.stringify(await statusSnapshot(ws, manifest, scope, shown))}
9945
+ `
9946
+ );
9947
+ return 0;
9948
+ }
9546
9949
  if (terminal.input.isTTY === true && terminal.output.isTTY === true) {
9547
9950
  await showView(
9548
9951
  tabs,
@@ -9552,9 +9955,7 @@ async function statusCommand(tab, opts, terminal = { input: process.stdin, outpu
9552
9955
  );
9553
9956
  return 0;
9554
9957
  }
9555
- terminal.output.write(
9556
- statusMarkdown(workspace, wanted === void 0 ? tabs : [wanted])
9557
- );
9958
+ terminal.output.write(statusMarkdown(workspace, shown));
9558
9959
  return 0;
9559
9960
  } catch (e) {
9560
9961
  return reportError(e);
@@ -9605,7 +10006,7 @@ function buildProgram(state) {
9605
10006
  });
9606
10007
  program.command("status").description(
9607
10008
  "Show the status of every decision, specification and plan, a tab per directory"
9608
- ).argument("[tab]", "open on this tab: adr, specs, plans, \u2026").addOption(new Option("--cwd <dir>").hideHelp()).action(async (tab, opts) => {
10009
+ ).argument("[tab]", "open on this tab: adr, specs, plans, \u2026").option("--json", "print JSON instead: what the Claude Code plugin draws").addOption(new Option("--cwd <dir>").hideHelp()).action(async (tab, opts) => {
9609
10010
  state.code = await statusCommand(tab, opts);
9610
10011
  });
9611
10012
  const doc = program.command("doc").description("Documents of the workspace: doc new <collection>");
@@ -9687,7 +10088,10 @@ function buildProgram(state) {
9687
10088
  program.command("written-files", { hidden: true }).addOption(new Option("--cwd <dir>").hideHelp()).action(async (opts) => {
9688
10089
  state.code = await writtenFilesCommand(opts);
9689
10090
  });
9690
- program.command("hook", { hidden: true }).argument("<event>", "session-start, post-tool-use or session-end").action(async (event) => {
10091
+ program.command("hook", { hidden: true }).argument(
10092
+ "<event>",
10093
+ "session-start, pre-tool-use, post-tool-use or session-end"
10094
+ ).action(async (event) => {
9691
10095
  state.code = await hookCommand(event);
9692
10096
  });
9693
10097
  program.command("upgrade").alias("update").description(
@@ -9876,4 +10280,4 @@ export {
9876
10280
  OWN_COMMANDS,
9877
10281
  catchUp
9878
10282
  };
9879
- //# sourceMappingURL=chunk-VGBS7HDY.js.map
10283
+ //# sourceMappingURL=chunk-U5SBNWVD.js.map