@rness/cli 0.18.1 → 0.20.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.1";
514
+ var VERSION = "0.20.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);
@@ -774,11 +779,25 @@ var STATUSES = {
774
779
  };
775
780
  var SKIP = /* @__PURE__ */ new Set(["adr/0000-template.md"]);
776
781
  var PLACEMENT_KEYS = ["scopes", "scope"];
777
- 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) {
778
797
  const problems = [];
779
798
  for (const name of COLLECTIONS) {
780
799
  const allowed = STATUSES[name];
781
- const items = await collectMarkdown(join5(rnessDir, name));
800
+ const items = await itemsOf(rnessDir, name, override);
782
801
  const byNumber = /* @__PURE__ */ new Map();
783
802
  for (const item of items) {
784
803
  const where = `${name}/${item.rel}`;
@@ -1221,13 +1240,34 @@ function statusMarkdown(workspace, tabs) {
1221
1240
  }
1222
1241
  return out.join("\n");
1223
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
+ }
1224
1258
 
1225
1259
  // src/pulse/body.ts
1226
1260
  import { createHash } from "crypto";
1227
1261
  import { posix as posix4 } from "path";
1228
- var BODY_LIMIT = 65536;
1262
+
1263
+ // src/pulse/urls.ts
1264
+ var boardUrl = (org, project) => `https://github.com/orgs/${org}/projects/${project}`;
1265
+ var itemUrl = (org, project, path) => `${boardUrl(org, project)}?filterQuery=${encodeURIComponent(`path:"${path}"`)}`;
1229
1266
  var blobUrl = (org, path) => `https://github.com/${org}/.rness/blob/main/${encodeURI(path)}`;
1230
1267
  var issueUrl = (org, number) => `https://github.com/${org}/.rness/issues/${number}`;
1268
+
1269
+ // src/pulse/body.ts
1270
+ var BODY_LIMIT = 65536;
1231
1271
  function headerOf(path, org) {
1232
1272
  return `\`${path}\` \xB7 [on GitHub](${blobUrl(org, path)})`;
1233
1273
  }
@@ -2040,9 +2080,9 @@ async function postJson(path, payload, options, explain = false, limited = () =>
2040
2080
  continue;
2041
2081
  }
2042
2082
  if (!ok) {
2043
- const message2 = explain && page.status !== 401 && page.status !== 429 && page.body !== null && typeof page.body === "object" ? page.body.message : void 0;
2044
- if (typeof message2 === "string" && message2 !== "")
2045
- throw new GitHubMessageError(message2, {
2083
+ const message3 = explain && page.status !== 401 && page.status !== 429 && page.body !== null && typeof page.body === "object" ? page.body.message : void 0;
2084
+ if (typeof message3 === "string" && message3 !== "")
2085
+ throw new GitHubMessageError(message3, {
2046
2086
  cause: { status: page.status, path }
2047
2087
  });
2048
2088
  throw httpError(page, path);
@@ -2066,9 +2106,9 @@ async function getScopes(options) {
2066
2106
  const cause = { status: res.status, path: "/user" };
2067
2107
  if (res.status === 401)
2068
2108
  throw new Error("GitHub rejected the token (401)", { cause });
2069
- const message2 = body !== null && typeof body === "object" ? body["message"] : void 0;
2070
- if (typeof message2 === "string" && message2 !== "")
2071
- throw new GitHubMessageError(message2, { cause });
2109
+ const message3 = body !== null && typeof body === "object" ? body["message"] : void 0;
2110
+ if (typeof message3 === "string" && message3 !== "")
2111
+ throw new GitHubMessageError(message3, { cause });
2072
2112
  throw new Error(`GitHub API answered ${res.status} for /user`, { cause });
2073
2113
  }
2074
2114
  const header2 = res.headers.get("x-oauth-scopes");
@@ -2115,8 +2155,8 @@ async function getOrgMembership(org, options) {
2115
2155
  }
2116
2156
  if (status2 === 404) return "not-member";
2117
2157
  if (status2 === 403) {
2118
- const message2 = body !== null && typeof body === "object" ? String(body["message"] ?? "") : "";
2119
- if (/OAuth App access restrictions/i.test(message2)) return "restricted";
2158
+ const message3 = body !== null && typeof body === "object" ? String(body["message"] ?? "") : "";
2159
+ if (/OAuth App access restrictions/i.test(message3)) return "restricted";
2120
2160
  }
2121
2161
  return "unknown";
2122
2162
  }
@@ -2138,8 +2178,8 @@ async function createRepository(owner, name, options) {
2138
2178
  throw new Error(`cannot reach the GitHub API: ${reason2(e)}`, { cause: e });
2139
2179
  }
2140
2180
  const body = await res.json().catch(() => void 0);
2141
- const message2 = body !== null && typeof body === "object" ? String(body["message"] ?? "") : "";
2142
- return { status: res.status, message: message2 };
2181
+ const message3 = body !== null && typeof body === "object" ? String(body["message"] ?? "") : "";
2182
+ return { status: res.status, message: message3 };
2143
2183
  };
2144
2184
  let answer = await post2(`/orgs/${encodeURIComponent(owner)}/repos`);
2145
2185
  if (answer.status === 404 && options.self !== void 0 && options.self !== null && options.self.toLowerCase() === owner.toLowerCase())
@@ -4169,8 +4209,8 @@ function reportError(e) {
4169
4209
  process.stderr.write(`${e.stack}
4170
4210
  `);
4171
4211
  } else {
4172
- const message2 = e instanceof Error ? e.message : String(e);
4173
- process.stderr.write(`${paint("error", message2, process.stderr)}
4212
+ const message3 = e instanceof Error ? e.message : String(e);
4213
+ process.stderr.write(`${paint("error", message3, process.stderr)}
4174
4214
  `);
4175
4215
  }
4176
4216
  return 1;
@@ -4362,13 +4402,16 @@ async function ensureClaudeMd(dir) {
4362
4402
  }
4363
4403
 
4364
4404
  // src/commands/sync.ts
4365
- import { join as join15 } from "path";
4405
+ import { join as join16 } from "path";
4366
4406
 
4367
4407
  // src/core/agent-targets.ts
4368
4408
  import { mkdir as mkdir2, rm as rm4, rmdir } from "fs/promises";
4369
- import { dirname as dirname4, join as join14 } from "path";
4409
+ import { dirname as dirname5, join as join15 } from "path";
4370
4410
 
4371
4411
  // src/core/claude-skills.ts
4412
+ import { readFileSync, statSync } from "fs";
4413
+ import { dirname as dirname4, join as join13 } from "path";
4414
+ import { fileURLToPath } from "url";
4372
4415
  var PLUGIN = ".claude/skills/rness";
4373
4416
  var PLUGIN_JSON = `${JSON.stringify(
4374
4417
  {
@@ -4386,7 +4429,9 @@ var PLUGIN_JSON = `${JSON.stringify(
4386
4429
  "adr",
4387
4430
  "specification",
4388
4431
  "plan"
4389
- ]
4432
+ ],
4433
+ // The contract of the mod's `$.state` (spec 0029 §3.1).
4434
+ types: "./types/index.d.ts"
4390
4435
  },
4391
4436
  null,
4392
4437
  2
@@ -4635,6 +4680,45 @@ allowed-tools: Bash(${command}), Bash(${allocate})
4635
4680
 
4636
4681
  ${s.body(rness, allocate)}`;
4637
4682
  }
4683
+ function modDir() {
4684
+ let dir = dirname4(fileURLToPath(import.meta.url));
4685
+ for (let i = 0; i < 4; i += 1) {
4686
+ const candidate = join13(dir, "mod");
4687
+ try {
4688
+ if (statSync(join13(candidate, "hooks", "hooks.json")).isFile())
4689
+ return candidate;
4690
+ } catch {
4691
+ }
4692
+ dir = dirname4(dir);
4693
+ }
4694
+ throw new Error("mod directory not found next to the @rness/cli package");
4695
+ }
4696
+ var MOD_FILES = [
4697
+ "hooks/hooks.json",
4698
+ "hooks/register.tsx",
4699
+ "types/index.d.ts"
4700
+ ];
4701
+ var RNESS_LINE = "const RNESS = '.rness'";
4702
+ var modSources = null;
4703
+ function modSource(rel) {
4704
+ if (modSources === null) {
4705
+ const dir = modDir();
4706
+ modSources = new Map(
4707
+ MOD_FILES.map((f) => [
4708
+ f,
4709
+ readFileSync(join13(dir, ...f.split("/")), "utf8")
4710
+ ])
4711
+ );
4712
+ }
4713
+ return modSources.get(rel) ?? "";
4714
+ }
4715
+ function modFile(rel, rness) {
4716
+ const text = modSource(rel);
4717
+ if (rel !== "hooks/register.tsx") return text;
4718
+ if (!text.includes(RNESS_LINE))
4719
+ throw new Error(`mod/hooks/register.tsx lacks ${RNESS_LINE}`);
4720
+ return text.replace(RNESS_LINE, `const RNESS = '${rness}'`);
4721
+ }
4638
4722
  function pluginFiles(rness, at, ctx) {
4639
4723
  return [
4640
4724
  { file: `${PLUGIN}/.claude-plugin/plugin.json`, at, content: PLUGIN_JSON },
@@ -4648,6 +4732,11 @@ function pluginFiles(rness, at, ctx) {
4648
4732
  at,
4649
4733
  content: lifecycleSkill(rness, s)
4650
4734
  })),
4735
+ ...MOD_FILES.map((rel) => ({
4736
+ file: `${PLUGIN}/${rel}`,
4737
+ at,
4738
+ content: modFile(rel, rness)
4739
+ })),
4651
4740
  ...RETIRED_SKILLS.map((name) => ({
4652
4741
  file: `${PLUGIN}/skills/${name}/SKILL.md`,
4653
4742
  at,
@@ -4680,6 +4769,22 @@ function hooks(rness) {
4680
4769
  },
4681
4770
  label: "the rness session-start hook"
4682
4771
  },
4772
+ {
4773
+ // Before an edit: refused when it would change what sync generates,
4774
+ // or break the contract of .rness (spec 0029 §4).
4775
+ path: ["hooks", "PreToolUse"],
4776
+ contains: {
4777
+ matcher: "Edit|Write",
4778
+ hooks: [
4779
+ {
4780
+ type: "command",
4781
+ command: hookLine(rness, "pre-tool-use"),
4782
+ timeout: 10
4783
+ }
4784
+ ]
4785
+ },
4786
+ label: "the rness pre-tool-use hook"
4787
+ },
4683
4788
  {
4684
4789
  path: ["hooks", "PostToolUse"],
4685
4790
  contains: {
@@ -4874,7 +4979,7 @@ function describeGuarantee(g) {
4874
4979
  // src/core/pinned.ts
4875
4980
  import { execFile as execFile3 } from "child_process";
4876
4981
  import { readFile as readFile8 } from "fs/promises";
4877
- import { basename as basename3, join as join13 } from "path";
4982
+ import { basename as basename3, join as join14 } from "path";
4878
4983
  import { promisify as promisify3 } from "util";
4879
4984
  var execFileP3 = promisify3(execFile3);
4880
4985
  var PACKAGE = "@rness/cli";
@@ -4899,7 +5004,7 @@ function compareVersions(a, b) {
4899
5004
  async function readPackageJson(rnessDir) {
4900
5005
  try {
4901
5006
  const parsed = JSON.parse(
4902
- await readFile8(join13(rnessDir, "package.json"), "utf8")
5007
+ await readFile8(join14(rnessDir, "package.json"), "utf8")
4903
5008
  );
4904
5009
  return parsed !== null && typeof parsed === "object" ? parsed : null;
4905
5010
  } catch {
@@ -4918,7 +5023,7 @@ async function readPin(rnessDir) {
4918
5023
  return null;
4919
5024
  }
4920
5025
  async function writePin(rnessDir, from, to) {
4921
- const file = join13(rnessDir, "package.json");
5026
+ const file = join14(rnessDir, "package.json");
4922
5027
  const text = await readFile8(file, "utf8");
4923
5028
  const entry = new RegExp(
4924
5029
  `("${PACKAGE}"\\s*:\\s*")${from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(")`
@@ -4941,12 +5046,12 @@ async function workspacePackageManager(rnessDir) {
4941
5046
  if (isPackageManager(name)) return name;
4942
5047
  }
4943
5048
  for (const [lockfile, pm] of LOCKFILES)
4944
- if (await exists(join13(rnessDir, lockfile))) return pm;
5049
+ if (await exists(join14(rnessDir, lockfile))) return pm;
4945
5050
  return "npm";
4946
5051
  }
4947
5052
  async function hasLockfile(rnessDir, pm) {
4948
5053
  for (const [lockfile, owner] of LOCKFILES)
4949
- if (owner === pm && await exists(join13(rnessDir, lockfile))) return true;
5054
+ if (owner === pm && await exists(join14(rnessDir, lockfile))) return true;
4950
5055
  return false;
4951
5056
  }
4952
5057
  async function pinDrift(rnessDir) {
@@ -5002,9 +5107,9 @@ async function ensureInstalled(rnessDir, opts = {}) {
5002
5107
  }
5003
5108
  }
5004
5109
  async function runPinnedSync(root) {
5005
- const installed = await readPinnedCli(join13(root, ".rness"));
5110
+ const installed = await readPinnedCli(join14(root, ".rness"));
5006
5111
  if (installed === null) return null;
5007
- const pinned = join13(installed.dir, ...installed.bin.split("/"));
5112
+ const pinned = join14(installed.dir, ...installed.bin.split("/"));
5008
5113
  if (!await exists(pinned))
5009
5114
  return {
5010
5115
  code: 1,
@@ -5079,7 +5184,7 @@ function syncOutcome(result, opts = {}) {
5079
5184
  };
5080
5185
  }
5081
5186
  async function syncPinned(root, shown, fallback) {
5082
- const rnessDir = join13(root, ".rness");
5187
+ const rnessDir = join14(root, ".rness");
5083
5188
  const installed = await readPinnedCli(rnessDir);
5084
5189
  if (installed === null) {
5085
5190
  if (fallback !== void 0) return fallback();
@@ -5089,7 +5194,7 @@ async function syncPinned(root, shown, fallback) {
5089
5194
  );
5090
5195
  return 1;
5091
5196
  }
5092
- const pinned = join13(installed.dir, ...installed.bin.split("/"));
5197
+ const pinned = join14(installed.dir, ...installed.bin.split("/"));
5093
5198
  if (!await exists(pinned)) {
5094
5199
  process.stderr.write(
5095
5200
  `@rness/cli in ${shown}/.rness is not installed correctly (missing ${installed.bin}); reinstall in .rness/
@@ -5129,30 +5234,30 @@ function wholeDiff(existing, content) {
5129
5234
  }
5130
5235
  async function contextOf(root) {
5131
5236
  return {
5132
- packageManager: await workspacePackageManager(join14(root, ".rness"))
5237
+ packageManager: await workspacePackageManager(join15(root, ".rness"))
5133
5238
  };
5134
5239
  }
5135
5240
  var PATHS_ONLY = { packageManager: "npm" };
5136
5241
  async function filesOf(root, manifest, targets) {
5137
5242
  const files = [];
5138
- if (!await exists(join14(root, "org"))) return files;
5243
+ if (!await exists(join15(root, "org"))) return files;
5139
5244
  const ctx = await contextOf(root);
5140
5245
  for (const target2 of targets)
5141
5246
  for (const owned of target2.files(ctx))
5142
5247
  if (owned.at === "root")
5143
5248
  files.push({
5144
5249
  ...owned,
5145
- file: join14(root, ...owned.file.split("/")),
5250
+ file: join15(root, ...owned.file.split("/")),
5146
5251
  label: owned.file
5147
5252
  });
5148
5253
  for (const repo of Object.keys(manifest.repos)) {
5149
- if (!await exists(join14(root, "org", repo))) continue;
5254
+ if (!await exists(join15(root, "org", repo))) continue;
5150
5255
  for (const target2 of targets)
5151
5256
  for (const owned of target2.files(ctx))
5152
5257
  if (owned.at === "clones")
5153
5258
  files.push({
5154
5259
  ...owned,
5155
- file: join14(root, "org", repo, ...owned.file.split("/")),
5260
+ file: join15(root, "org", repo, ...owned.file.split("/")),
5156
5261
  label: `org/${repo}/${owned.file}`
5157
5262
  });
5158
5263
  }
@@ -5188,7 +5293,7 @@ async function agentTargets(root, manifest, opts) {
5188
5293
  continue;
5189
5294
  }
5190
5295
  await rm4(file);
5191
- await rmdir(dirname4(file)).catch(() => void 0);
5296
+ await rmdir(dirname5(file)).catch(() => void 0);
5192
5297
  outcomes.push({ label, status: "removed", detail: null });
5193
5298
  continue;
5194
5299
  }
@@ -5199,7 +5304,7 @@ async function agentTargets(root, manifest, opts) {
5199
5304
  else if (opts.check)
5200
5305
  outcomes.push({ label, status: "stale", detail: diff });
5201
5306
  else {
5202
- await mkdir2(dirname4(file), { recursive: true });
5307
+ await mkdir2(dirname5(file), { recursive: true });
5203
5308
  await writeFileAtomic(file, owned.content);
5204
5309
  outcomes.push({ label, status: "updated", detail: null });
5205
5310
  }
@@ -5228,7 +5333,7 @@ async function agentTargets(root, manifest, opts) {
5228
5333
  outcomes.push({ label, status: "unchanged", detail: null });
5229
5334
  continue;
5230
5335
  }
5231
- await mkdir2(dirname4(file), { recursive: true });
5336
+ await mkdir2(dirname5(file), { recursive: true });
5232
5337
  await writeFileAtomic(file, ensured.text);
5233
5338
  outcomes.push({ label, status: "updated", detail: null });
5234
5339
  }
@@ -5420,8 +5525,8 @@ function sessionUi(p) {
5420
5525
  intro: (title) => {
5421
5526
  p.intro(badge(title));
5422
5527
  },
5423
- outro: (message2, tone = "done") => {
5424
- p.outro(badge(message2, tone));
5528
+ outro: (message3, tone = "done") => {
5529
+ p.outro(badge(message3, tone));
5425
5530
  },
5426
5531
  line: (verb, rest, sentence) => {
5427
5532
  say(verb, sentence ?? `${rest} ${verb}`);
@@ -5492,7 +5597,7 @@ function targetsOf(manifest, root, only) {
5492
5597
  const segments = entry.path.split("/");
5493
5598
  targets.push({
5494
5599
  scope: name,
5495
- dir: join15(root, ...segments),
5600
+ dir: join16(root, ...segments),
5496
5601
  depth: segments.length,
5497
5602
  label: `${entry.path}/AGENTS.md`
5498
5603
  });
@@ -5652,7 +5757,7 @@ async function syncCommand(opts, deps = {}) {
5652
5757
  try {
5653
5758
  const notCloned = /* @__PURE__ */ new Set();
5654
5759
  for (const [name, repo] of Object.entries(manifest.repos)) {
5655
- const dir = join15(ws.root, "org", name);
5760
+ const dir = join16(ws.root, "org", name);
5656
5761
  const label = `org/${name}`;
5657
5762
  if (!await exists(dir)) {
5658
5763
  if (!toClone.has(name)) {
@@ -5704,7 +5809,7 @@ async function syncCommand(opts, deps = {}) {
5704
5809
  lines.push(
5705
5810
  `not in rness.json: ${undeclared.join(", ")} (${rnessCommand()} add <name> declares it)`
5706
5811
  );
5707
- if (!await exists(join15(ws.root, "org"))) {
5812
+ if (!await exists(join16(ws.root, "org"))) {
5708
5813
  lines.push(["skipped", "blocks (no org/ directory here)"]);
5709
5814
  } else {
5710
5815
  let differences = 0;
@@ -5716,7 +5821,7 @@ async function syncCommand(opts, deps = {}) {
5716
5821
  continue;
5717
5822
  }
5718
5823
  const [agentsLinked, claudeLinked] = await Promise.all(
5719
- BLOCK_FILES.map((f) => isSymlink(join15(t.dir, f)))
5824
+ BLOCK_FILES.map((f) => isSymlink(join16(t.dir, f)))
5720
5825
  );
5721
5826
  const atRoot = t.scope === null;
5722
5827
  if (!atRoot && (agentsLinked === true || claudeLinked === true)) {
@@ -5739,7 +5844,7 @@ async function syncCommand(opts, deps = {}) {
5739
5844
  `${t.label}: block is ${Math.round(block.bytes / 1024)} KB (over 32 KB)`
5740
5845
  );
5741
5846
  }
5742
- const agents = agentsLinked === true ? "skipped" : await syncBlockFile(join15(t.dir, "AGENTS.md"), block, check);
5847
+ const agents = agentsLinked === true ? "skipped" : await syncBlockFile(join16(t.dir, "AGENTS.md"), block, check);
5743
5848
  if (typeof agents === "object") {
5744
5849
  problems.push(`${t.label}: ${agents.error}`);
5745
5850
  continue;
@@ -5754,7 +5859,7 @@ async function syncCommand(opts, deps = {}) {
5754
5859
  }
5755
5860
  if (claudeLinked === true) continue;
5756
5861
  const claude = await syncBlockFile(
5757
- join15(t.dir, "CLAUDE.md"),
5862
+ join16(t.dir, "CLAUDE.md"),
5758
5863
  block,
5759
5864
  check,
5760
5865
  true
@@ -5811,7 +5916,7 @@ async function syncCommand(opts, deps = {}) {
5811
5916
  }
5812
5917
 
5813
5918
  // src/commands/add.ts
5814
- import { basename as basename4, join as join16 } from "path";
5919
+ import { basename as basename4, join as join17 } from "path";
5815
5920
  function printDeclared(ui, result, already) {
5816
5921
  for (const name of result.declared) {
5817
5922
  if (already.has(name)) continue;
@@ -5905,7 +6010,7 @@ async function addCommand(spec, opts, deps = {}) {
5905
6010
  }
5906
6011
  parsed = parseRepoSpec(spec, org, host);
5907
6012
  const scopes = (opts.scopes ?? "").split(",").map((s) => s.trim()).filter((s) => s !== "");
5908
- const dir = join16(ws.root, "org", parsed.name);
6013
+ const dir = join17(ws.root, "org", parsed.name);
5909
6014
  const present = await exists(dir);
5910
6015
  if (interactive) {
5911
6016
  const { confirm, isCancel } = await terminal.prompts();
@@ -6032,9 +6137,9 @@ async function contextCommand(opts) {
6032
6137
  }
6033
6138
 
6034
6139
  // src/core/scaffold.ts
6035
- import { statSync } from "fs";
6036
- import { dirname as dirname5, join as join17 } from "path";
6037
- import { fileURLToPath } from "url";
6140
+ import { statSync as statSync2 } from "fs";
6141
+ import { dirname as dirname6, join as join18 } from "path";
6142
+ import { fileURLToPath as fileURLToPath2 } from "url";
6038
6143
  var SCAFFOLD_FILES = [
6039
6144
  "AGENTS.md",
6040
6145
  "CONVENTIONS.md",
@@ -6055,27 +6160,27 @@ var SCAFFOLD_FILES = [
6055
6160
  "docs/.gitkeep"
6056
6161
  ];
6057
6162
  function scaffoldDir() {
6058
- let dir = dirname5(fileURLToPath(import.meta.url));
6163
+ let dir = dirname6(fileURLToPath2(import.meta.url));
6059
6164
  for (let i = 0; i < 4; i += 1) {
6060
- const candidate = join17(dir, "scaffold");
6165
+ const candidate = join18(dir, "scaffold");
6061
6166
  try {
6062
- if (statSync(join17(candidate, "rness.json")).isFile()) return candidate;
6167
+ if (statSync2(join18(candidate, "rness.json")).isFile()) return candidate;
6063
6168
  } catch {
6064
6169
  }
6065
- dir = dirname5(dir);
6170
+ dir = dirname6(dir);
6066
6171
  }
6067
6172
  throw new Error("scaffold directory not found next to the @rness/cli package");
6068
6173
  }
6069
6174
 
6070
6175
  // src/core/scaffold-copy.ts
6071
6176
  import { chmod as chmod2, mkdir as mkdir3, readFile as readFile9, stat as stat2, writeFile as writeFile2 } from "fs/promises";
6072
- import { dirname as dirname6, join as join18 } from "path";
6177
+ import { dirname as dirname7, join as join19 } from "path";
6073
6178
  var TOKENISED = /* @__PURE__ */ new Set(["package.json"]);
6074
6179
  async function renderScaffold(source, tokens) {
6075
6180
  const files = [];
6076
6181
  for (const rel of SCAFFOLD_FILES) {
6077
6182
  if (rel === "rness.json") continue;
6078
- const from = join18(source, ...rel.split("/"));
6183
+ const from = join19(source, ...rel.split("/"));
6079
6184
  let content = await readFile9(from, "utf8");
6080
6185
  if (TOKENISED.has(rel)) {
6081
6186
  content = content.replaceAll("__RNESS_VERSION__", tokens.version).replaceAll("__RNESS_PM__", tokens.packageManager);
@@ -6090,8 +6195,8 @@ async function renderScaffold(source, tokens) {
6090
6195
  }
6091
6196
  async function copyScaffold(dest, tokens) {
6092
6197
  for (const file of await renderScaffold(scaffoldDir(), tokens)) {
6093
- const to = join18(dest, ...file.path.split("/"));
6094
- await mkdir3(dirname6(to), { recursive: true });
6198
+ const to = join19(dest, ...file.path.split("/"));
6199
+ await mkdir3(dirname7(to), { recursive: true });
6095
6200
  await writeFile2(to, file.content, "utf8");
6096
6201
  if (file.executable) await chmod2(to, 493);
6097
6202
  }
@@ -6272,7 +6377,7 @@ async function loginCommand(opts, deps = {}) {
6272
6377
  // src/commands/create.ts
6273
6378
  import { mkdir as mkdir6, mkdtemp as mkdtemp2, rename as rename2, rm as rm7 } from "fs/promises";
6274
6379
  import { tmpdir as tmpdir2 } from "os";
6275
- import { join as join22, relative as relative4, resolve as resolve5 } from "path";
6380
+ import { join as join23, relative as relative4, resolve as resolve5 } from "path";
6276
6381
 
6277
6382
  // src/core/approval.ts
6278
6383
  var APPROVAL_INTERVAL_MS = 5e3;
@@ -6391,13 +6496,13 @@ async function askProvider(p) {
6391
6496
 
6392
6497
  // src/core/new-workspace.ts
6393
6498
  import { mkdir as mkdir4, readdir as readdir4, rm as rm6, stat as stat3 } from "fs/promises";
6394
- import { dirname as dirname8, join as join20, relative as relative2 } from "path";
6499
+ import { dirname as dirname9, join as join21, relative as relative2 } from "path";
6395
6500
 
6396
6501
  // src/core/scaffold-git.ts
6397
6502
  import { execFile as execFile5 } from "child_process";
6398
6503
  import { mkdtemp, realpath as realpath2, rm as rm5, writeFile as writeFile3 } from "fs/promises";
6399
6504
  import { tmpdir } from "os";
6400
- import { dirname as dirname7, join as join19, resolve as resolve3 } from "path";
6505
+ import { dirname as dirname8, join as join20, resolve as resolve3 } from "path";
6401
6506
  import { promisify as promisify5 } from "util";
6402
6507
  var execFileP5 = promisify5(execFile5);
6403
6508
  var CREATE_SUBJECT = "chore: rness workspace context";
@@ -6411,7 +6516,7 @@ async function run(dir, args, env = {}) {
6411
6516
  ...process.env,
6412
6517
  ...env,
6413
6518
  GIT_TERMINAL_PROMPT: "0",
6414
- GIT_CEILING_DIRECTORIES: dirname7(resolve3(dir))
6519
+ GIT_CEILING_DIRECTORIES: dirname8(resolve3(dir))
6415
6520
  },
6416
6521
  maxBuffer: 16 * 1024 * 1024
6417
6522
  });
@@ -6438,13 +6543,13 @@ function scaffoldMessage(subject, version) {
6438
6543
 
6439
6544
  ${SCAFFOLD_TRAILER}: ${version}`;
6440
6545
  }
6441
- async function commitUpgrade(rnessDir, message2) {
6546
+ async function commitUpgrade(rnessDir, message3) {
6442
6547
  const add = await run(rnessDir, ["add", "-A"]);
6443
6548
  if (add.code !== 0) return { kind: "refused", output: add.stderr.trim() };
6444
6549
  const staged = await run(rnessDir, ["diff", "--cached", "--quiet"]);
6445
6550
  if (staged.code === 0 && !await mergeInProgress(rnessDir))
6446
6551
  return { kind: "nothing" };
6447
- const commit = await run(rnessDir, ["commit", "-q", "-m", message2]);
6552
+ const commit = await run(rnessDir, ["commit", "-q", "-m", message3]);
6448
6553
  if (commit.code !== 0)
6449
6554
  return {
6450
6555
  kind: "refused",
@@ -6485,8 +6590,8 @@ async function findScaffoldBase(rnessDir) {
6485
6590
  };
6486
6591
  }
6487
6592
  async function buildScaffoldCommit(rnessDir, base, files, version) {
6488
- const scratch = await mkdtemp(join19(tmpdir(), "rness-scaffold-"));
6489
- const env = { GIT_INDEX_FILE: join19(scratch, "index") };
6593
+ const scratch = await mkdtemp(join20(tmpdir(), "rness-scaffold-"));
6594
+ const env = { GIT_INDEX_FILE: join20(scratch, "index") };
6490
6595
  try {
6491
6596
  await must(
6492
6597
  rnessDir,
@@ -6494,7 +6599,7 @@ async function buildScaffoldCommit(rnessDir, base, files, version) {
6494
6599
  env
6495
6600
  );
6496
6601
  for (const [i, file] of files.entries()) {
6497
- const tmp = join19(scratch, `blob-${i}`);
6602
+ const tmp = join20(scratch, `blob-${i}`);
6498
6603
  await writeFile3(tmp, file.content, "utf8");
6499
6604
  const blob = await must(rnessDir, [
6500
6605
  "hash-object",
@@ -6606,11 +6711,11 @@ async function insideWorkspace(dir) {
6606
6711
  }
6607
6712
  async function targetProblem(cwd, root, pm) {
6608
6713
  const shown = relative2(cwd, root) || ".";
6609
- if (await insideWorkspace(cwd) || await insideWorkspace(dirname8(root)))
6714
+ if (await insideWorkspace(cwd) || await insideWorkspace(dirname9(root)))
6610
6715
  return "already inside an rness workspace; create runs from outside one";
6611
6716
  if (!await exists(root)) return null;
6612
- if (await exists(join20(root, ".rness", "rness.json"))) {
6613
- if (!await exists(join20(root, ".rness", "node_modules")))
6717
+ if (await exists(join21(root, ".rness", "rness.json"))) {
6718
+ if (!await exists(join21(root, ".rness", "node_modules")))
6614
6719
  return `${shown} holds an rness workspace whose dependencies are not installed; cd ${shown}/.rness && ${pm} install, then ${rnessCommand()} sync`;
6615
6720
  return `${shown} already holds an rness workspace (.rness/); run ${rnessCommand()} add inside it to add repositories`;
6616
6721
  }
@@ -6633,7 +6738,7 @@ async function installContext(input) {
6633
6738
  }
6634
6739
  async function buildContext(input) {
6635
6740
  const { ui, root, shown, pm } = input;
6636
- const rnessDir = join20(root, ".rness");
6741
+ const rnessDir = join21(root, ".rness");
6637
6742
  const pmVersion = await packageManagerVersion(pm);
6638
6743
  await mkdir4(root, { recursive: true });
6639
6744
  try {
@@ -6705,7 +6810,7 @@ async function syncBlocks(ui, root, shown, inPlace, sshWorks) {
6705
6810
 
6706
6811
  // src/commands/create-blank.ts
6707
6812
  import { mkdir as mkdir5 } from "fs/promises";
6708
- import { join as join21, relative as relative3, resolve as resolve4 } from "path";
6813
+ import { join as join22, relative as relative3, resolve as resolve4 } from "path";
6709
6814
  var WORKSPACE_NAME = /^(?!\.{1,2}$)[A-Za-z0-9._][A-Za-z0-9._-]*$/;
6710
6815
  function workspaceNameError(name) {
6711
6816
  return `workspace name "${name}" may only contain letters, digits, '.', '_' and '-', not starting with '-'`;
@@ -6787,8 +6892,8 @@ async function createBlank(input) {
6787
6892
  kind: "blank workspace"
6788
6893
  });
6789
6894
  if (!built) return 1;
6790
- await mkdir5(join21(root, "org"), { recursive: true });
6791
- await commitContext({ ui, rnessDir: join21(root, ".rness"), shown });
6895
+ await mkdir5(join22(root, "org"), { recursive: true });
6896
+ await commitContext({ ui, rnessDir: join22(root, ".rness"), shown });
6792
6897
  const code = await syncBlocks(
6793
6898
  ui,
6794
6899
  root,
@@ -6932,7 +7037,7 @@ async function cloneSelection(input) {
6932
7037
  { doing: `cloning org/${repo}`, git: true },
6933
7038
  () => clone(
6934
7039
  entry.url,
6935
- join22(input.root, "org", repo),
7040
+ join23(input.root, "org", repo),
6936
7041
  input.provider.credentialsFor(entry.url)
6937
7042
  ),
6938
7043
  () => ["cloned", `org/${repo}`]
@@ -6957,9 +7062,9 @@ async function cloneSelection(input) {
6957
7062
  manifest = result.manifest;
6958
7063
  ui.line("declared", `scope ${result.name} (org/${result.name})`);
6959
7064
  } catch (e) {
6960
- const message2 = firstLine5(e instanceof Error ? e.message : String(e));
6961
- ui.error(`${spec}: ${message2}`);
6962
- failures.push({ spec, message: message2 });
7065
+ const message3 = firstLine5(e instanceof Error ? e.message : String(e));
7066
+ ui.error(`${spec}: ${message3}`);
7067
+ failures.push({ spec, message: message3 });
6963
7068
  }
6964
7069
  }
6965
7070
  return failures;
@@ -7295,8 +7400,8 @@ async function createCommand(opts, deps = {}) {
7295
7400
  scopes: {}
7296
7401
  };
7297
7402
  const staged = async () => {
7298
- staging ??= await mkdtemp2(join22(tmpdir2(), "rness-join-"));
7299
- return join22(staging, ".rness");
7403
+ staging ??= await mkdtemp2(join23(tmpdir2(), "rness-join-"));
7404
+ return join23(staging, ".rness");
7300
7405
  };
7301
7406
  if (joining) {
7302
7407
  const stagedDir = await staged();
@@ -7358,7 +7463,7 @@ async function createCommand(opts, deps = {}) {
7358
7463
  });
7359
7464
  if (p.isCancel(ok) || ok !== true) return cancelled(ui);
7360
7465
  }
7361
- const rnessDir = join22(root, ".rness");
7466
+ const rnessDir = join23(root, ".rness");
7362
7467
  const install = () => installContext({ ui, pm, rnessDir, skip: opts.skipInstall === true });
7363
7468
  if (joining) {
7364
7469
  await mkdir6(root, { recursive: true });
@@ -7371,7 +7476,7 @@ async function createCommand(opts, deps = {}) {
7371
7476
  ui.line("cloned", `${shown}/.rness (joined ${org})`);
7372
7477
  if (opts.pm === void 0) pm = await workspacePackageManager(rnessDir);
7373
7478
  await install();
7374
- await mkdir6(join22(root, "org"), { recursive: true });
7479
+ await mkdir6(join23(root, "org"), { recursive: true });
7375
7480
  const failures2 = await cloneSelection({
7376
7481
  root,
7377
7482
  rnessDir,
@@ -7424,7 +7529,7 @@ ${next2.map((l) => ` ${l}`).join("\n")}
7424
7529
  kind: "new workspace"
7425
7530
  });
7426
7531
  if (!built) return 1;
7427
- await mkdir6(join22(root, "org"), { recursive: true });
7532
+ await mkdir6(join23(root, "org"), { recursive: true });
7428
7533
  const failures = await cloneSelection({
7429
7534
  root,
7430
7535
  rnessDir,
@@ -7496,7 +7601,7 @@ ${next2.map((l) => ` ${l}`).join("\n")}
7496
7601
  }
7497
7602
 
7498
7603
  // src/core/blocks.ts
7499
- import { join as join23 } from "path";
7604
+ import { join as join24 } from "path";
7500
7605
  var PROBLEM = /* @__PURE__ */ new Set(["stale", "malformed"]);
7501
7606
  async function checkBlocks({
7502
7607
  root,
@@ -7505,15 +7610,15 @@ async function checkBlocks({
7505
7610
  org
7506
7611
  }) {
7507
7612
  const checks = [];
7508
- const note = (label, status2, message2 = null) => {
7509
- checks.push({ label, status: status2, message: message2 });
7613
+ const note = (label, status2, message3 = null) => {
7614
+ checks.push({ label, status: status2, message: message3 });
7510
7615
  };
7511
7616
  const derive = () => ({
7512
7617
  problems: checks.filter((c) => PROBLEM.has(c.status) && c.message !== null).map((c) => c.message),
7513
7618
  warnings: checks.filter((c) => !PROBLEM.has(c.status) && c.message !== null).map((c) => c.message),
7514
7619
  checks
7515
7620
  });
7516
- if (!await exists(join23(root, "org"))) return derive();
7621
+ if (!await exists(join24(root, "org"))) return derive();
7517
7622
  const targets = [
7518
7623
  { scope: null, dir: root, depth: 0, file: "AGENTS.md", label: "AGENTS.md" },
7519
7624
  { scope: null, dir: root, depth: 0, file: "CLAUDE.md", label: "CLAUDE.md" }
@@ -7522,7 +7627,7 @@ async function checkBlocks({
7522
7627
  const segments = entry.path.split("/");
7523
7628
  targets.push({
7524
7629
  scope: name,
7525
- dir: join23(root, ...segments),
7630
+ dir: join24(root, ...segments),
7526
7631
  depth: segments.length,
7527
7632
  file: "AGENTS.md",
7528
7633
  label: `${entry.path}/AGENTS.md`
@@ -7537,7 +7642,7 @@ async function checkBlocks({
7537
7642
  );
7538
7643
  continue;
7539
7644
  }
7540
- const text = await readOrNull(join23(t.dir, t.file));
7645
+ const text = await readOrNull(join24(t.dir, t.file));
7541
7646
  const lines = text === null ? [] : text.split(/\r?\n/);
7542
7647
  const found = findBlock(lines);
7543
7648
  if (found.kind === "none") {
@@ -7608,7 +7713,7 @@ async function logoutCommand() {
7608
7713
  import { execFile as execFile6 } from "child_process";
7609
7714
  import { mkdtemp as mkdtemp3, readFile as readFile10, rm as rm8 } from "fs/promises";
7610
7715
  import { tmpdir as tmpdir3 } from "os";
7611
- import { basename as basename5, join as join24, relative as relative5 } from "path";
7716
+ import { basename as basename5, join as join25, relative as relative5 } from "path";
7612
7717
  import { promisify as promisify6 } from "util";
7613
7718
  var execFileP6 = promisify6(execFile6);
7614
7719
  var NOTES = "https://github.com/rness-dev/rness/tree/main/packages/cli#readme";
@@ -7628,10 +7733,10 @@ async function latestVersion() {
7628
7733
  async function scaffoldFor(version, rnessDir) {
7629
7734
  if (version === VERSION) return { dir: scaffoldDir() };
7630
7735
  const installed = await readPinnedCli(rnessDir);
7631
- const inside2 = installed === null ? null : join24(installed.dir, "scaffold");
7632
- if (installed?.version === version && inside2 !== null && await exists(join24(inside2, "package.json")))
7736
+ const inside2 = installed === null ? null : join25(installed.dir, "scaffold");
7737
+ if (installed?.version === version && inside2 !== null && await exists(join25(inside2, "package.json")))
7633
7738
  return { dir: inside2 };
7634
- const scratch = await mkdtemp3(join24(tmpdir3(), "rness-pack-"));
7739
+ const scratch = await mkdtemp3(join25(tmpdir3(), "rness-pack-"));
7635
7740
  const cleanup = () => rm8(scratch, { recursive: true, force: true });
7636
7741
  try {
7637
7742
  const { stdout } = await execFileP6(
@@ -7646,8 +7751,8 @@ async function scaffoldFor(version, rnessDir) {
7646
7751
  { timeout: 6e4 }
7647
7752
  );
7648
7753
  const tarball = stdout.trim().split("\n").at(-1) ?? "";
7649
- await execFileP6("tar", ["-xzf", join24(scratch, tarball), "-C", scratch]);
7650
- return { dir: join24(scratch, "package", "scaffold"), cleanup };
7754
+ await execFileP6("tar", ["-xzf", join25(scratch, tarball), "-C", scratch]);
7755
+ return { dir: join25(scratch, "package", "scaffold"), cleanup };
7651
7756
  } catch (e) {
7652
7757
  await cleanup();
7653
7758
  throw new Error(
@@ -7659,7 +7764,7 @@ async function scaffoldFor(version, rnessDir) {
7659
7764
  async function declaredManager(rnessDir, pm) {
7660
7765
  try {
7661
7766
  const pkg = JSON.parse(
7662
- await readFile10(join24(rnessDir, "package.json"), "utf8")
7767
+ await readFile10(join25(rnessDir, "package.json"), "utf8")
7663
7768
  );
7664
7769
  if (typeof pkg.packageManager === "string") return pkg.packageManager;
7665
7770
  } catch {
@@ -7669,7 +7774,7 @@ async function declaredManager(rnessDir, pm) {
7669
7774
  async function clonesToCommit(root, cwd, manifest, files) {
7670
7775
  const clones = [];
7671
7776
  for (const repo of Object.keys(manifest.repos)) {
7672
- const dir = join24(root, "org", repo);
7777
+ const dir = join25(root, "org", repo);
7673
7778
  if (!await exists(dir)) continue;
7674
7779
  try {
7675
7780
  const paths = await changedPaths(dir, files);
@@ -7687,7 +7792,7 @@ async function filesTheSyncWrites(root, rnessDir, manifest) {
7687
7792
  try {
7688
7793
  const { stdout } = await execFileP6(
7689
7794
  process.execPath,
7690
- [join24(pinned.dir, pinned.bin), "written-files", "--cwd", root],
7795
+ [join25(pinned.dir, pinned.bin), "written-files", "--cwd", root],
7691
7796
  {
7692
7797
  cwd: root,
7693
7798
  env: { ...process.env, NO_COLOR: "1", RNESS_NO_INSTALL: "1" },
@@ -7703,14 +7808,14 @@ async function filesTheSyncWrites(root, rnessDir, manifest) {
7703
7808
  }
7704
7809
  function finish(ui, cwd, rnessDir, label, target2, ending) {
7705
7810
  const rel = relative5(cwd, rnessDir) || ".";
7706
- const message2 = `chore: rness ${target2}`;
7811
+ const message3 = `chore: rness ${target2}`;
7707
7812
  const next = [];
7708
7813
  if (ending.commit === "committed") next.push(`git -C ${rel} push`);
7709
7814
  if (ending.commit === "refused")
7710
- next.push(`git -C ${rel} commit -m "${message2}"`);
7815
+ next.push(`git -C ${rel} commit -m "${message3}"`);
7711
7816
  for (const c of ending.clones)
7712
7817
  next.push(
7713
- `git -C ${c.dir} add ${c.paths.join(" ")} && git -C ${c.dir} commit -m "${message2}"`
7818
+ `git -C ${c.dir} add ${c.paths.join(" ")} && git -C ${c.dir} commit -m "${message3}"`
7714
7819
  );
7715
7820
  if (ending.commit !== "nothing")
7716
7821
  next.push("# teammates: git pull \u2014 the next rness command installs it");
@@ -7883,7 +7988,7 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
7883
7988
  await source.cleanup?.();
7884
7989
  }
7885
7990
  }
7886
- const packageFile = join24(ws.rnessDir, "package.json");
7991
+ const packageFile = join25(ws.rnessDir, "package.json");
7887
7992
  const restore = /* @__PURE__ */ new Map([
7888
7993
  [packageFile, await readFile10(packageFile, "utf8")]
7889
7994
  ]);
@@ -7899,8 +8004,8 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
7899
8004
  () => ["installed", `dependencies with ${pm}`]
7900
8005
  );
7901
8006
  } catch (e) {
7902
- const message3 = e instanceof Error ? e.message : String(e);
7903
- ui.error(message3);
8007
+ const message4 = e instanceof Error ? e.message : String(e);
8008
+ ui.error(message4);
7904
8009
  if (!merged) {
7905
8010
  for (const [file, text] of restore) await writeFileAtomic(file, text);
7906
8011
  ui.hint(
@@ -7913,7 +8018,7 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
7913
8018
  `the scaffold merge is still staged; undo it with: git -C ${relative5(cwd, ws.rnessDir) || "."} merge --abort`,
7914
8019
  "stderr"
7915
8020
  );
7916
- if (/minimum.?release.?age|NO_MATURE/i.test(message3))
8021
+ if (/minimum.?release.?age|NO_MATURE/i.test(message4))
7917
8022
  ui.hint(
7918
8023
  "pnpm refuses versions published less than 24 h ago: add '@rness/cli' under minimumReleaseAgeExclude in .rness/pnpm-workspace.yaml",
7919
8024
  "stderr"
@@ -7932,13 +8037,13 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
7932
8037
  ui.gitIsSilent = true;
7933
8038
  const code = await syncBlocks(ui, ws.root, basename5(ws.root));
7934
8039
  if (code !== 0) return code;
7935
- const message2 = `chore: rness ${target2}`;
7936
- const committed = await commitUpgrade(ws.rnessDir, message2);
8040
+ const message3 = `chore: rness ${target2}`;
8041
+ const committed = await commitUpgrade(ws.rnessDir, message3);
7937
8042
  if (committed.kind === "committed")
7938
8043
  ui.line(
7939
8044
  "committed",
7940
- `${label} \u2014 ${message2}`,
7941
- `Committed ${label}: ${message2}`
8045
+ `${label} \u2014 ${message3}`,
8046
+ `Committed ${label}: ${message3}`
7942
8047
  );
7943
8048
  if (committed.kind === "refused") {
7944
8049
  ui.error(
@@ -8053,7 +8158,7 @@ import { Command, CommanderError, Option } from "commander";
8053
8158
 
8054
8159
  // src/commands/doc.ts
8055
8160
  import { mkdir as mkdir7, readFile as readFile11, writeFile as writeFile4 } from "fs/promises";
8056
- import { join as join25 } from "path";
8161
+ import { join as join26 } from "path";
8057
8162
  var NUMBERED = Object.keys(STATUSES);
8058
8163
  var SECTIONS = {
8059
8164
  specs: [
@@ -8077,8 +8182,8 @@ function headingsOf(template) {
8077
8182
  return [...template.matchAll(/^## (.+)$/gm)].map((m) => (m[1] ?? "").trim());
8078
8183
  }
8079
8184
  async function adrSections(rnessDir) {
8080
- const own = join25(rnessDir, "adr", "0000-template.md");
8081
- const shipped = join25(scaffoldDir(), "adr", "0000-template.md");
8185
+ const own = join26(rnessDir, "adr", "0000-template.md");
8186
+ const shipped = join26(scaffoldDir(), "adr", "0000-template.md");
8082
8187
  for (const file of [own, shipped]) {
8083
8188
  try {
8084
8189
  const sections = headingsOf(await readFile11(file, "utf8"));
@@ -8112,7 +8217,7 @@ async function docNewCommand(collection, opts) {
8112
8217
  const name = collection;
8113
8218
  try {
8114
8219
  const ws = await findWorkspace(opts.cwd ?? process.cwd());
8115
- const dir = join25(ws.rnessDir, name);
8220
+ const dir = join26(ws.rnessDir, name);
8116
8221
  await mkdir7(dir, { recursive: true });
8117
8222
  let highest = 0;
8118
8223
  for (const item of await collectMarkdown(dir)) {
@@ -8121,7 +8226,7 @@ async function docNewCommand(collection, opts) {
8121
8226
  }
8122
8227
  const number = String(highest + 1).padStart(4, "0");
8123
8228
  const title = opts.title?.trim() || "Untitled";
8124
- const file = join25(dir, `${number}-${slugOf(title)}.md`);
8229
+ const file = join26(dir, `${number}-${slugOf(title)}.md`);
8125
8230
  const status2 = STATUSES[name]?.[0] ?? "Draft";
8126
8231
  const date = today();
8127
8232
  const front = [
@@ -8174,9 +8279,243 @@ password=${resolved.token}
8174
8279
  import { realpath as realpath4 } from "fs/promises";
8175
8280
  import { isAbsolute as isAbsolute2, posix as posix8, relative as relative7, resolve as resolve7, sep as sep5 } from "path";
8176
8281
 
8282
+ // src/core/claude-code.ts
8283
+ import { mkdir as mkdir8, readFile as readFile12 } from "fs/promises";
8284
+ import { join as join27 } from "path";
8285
+ var MOD_FLOOR = "2.1.280";
8286
+ var AGENT = /^claude-code_(\d+)-(\d+)-(\d+)_[a-z]+$/;
8287
+ function claudeCodeVersion(env) {
8288
+ const m = AGENT.exec(env["AI_AGENT"] ?? "");
8289
+ return m === null ? null : `${m[1]}.${m[2]}.${m[3]}`;
8290
+ }
8291
+ function belowModFloor(version) {
8292
+ const have = version.split(".").map(Number);
8293
+ const floor = MOD_FLOOR.split(".").map(Number);
8294
+ for (let i = 0; i < 3; i++) {
8295
+ const a = have[i] ?? 0;
8296
+ const b = floor[i] ?? 0;
8297
+ if (a !== b) return a < b;
8298
+ }
8299
+ return false;
8300
+ }
8301
+ var noticeFile = () => join27(configDir(), "claude-code.json");
8302
+ async function shownVersions() {
8303
+ try {
8304
+ const data = JSON.parse(await readFile12(noticeFile(), "utf8"));
8305
+ const shown = data !== null && typeof data === "object" ? data["modNoticeShown"] : void 0;
8306
+ return Array.isArray(shown) ? shown.filter((v) => typeof v === "string") : [];
8307
+ } catch {
8308
+ return [];
8309
+ }
8310
+ }
8311
+ async function takeModNotice(env) {
8312
+ const version = claudeCodeVersion(env);
8313
+ if (version === null || !belowModFloor(version)) return null;
8314
+ if ((env["CLAUDE_CODE_ENTRYPOINT"] ?? "").startsWith("sdk-")) return null;
8315
+ const shown = await shownVersions();
8316
+ if (shown.includes(version)) return null;
8317
+ try {
8318
+ await mkdir8(configDir(), { recursive: true, mode: 448 });
8319
+ await writeFileAtomic(
8320
+ noticeFile(),
8321
+ `${JSON.stringify({ modNoticeShown: [...shown, version] })}
8322
+ `
8323
+ );
8324
+ } catch {
8325
+ }
8326
+ return `rness: Claude Code ${version} shows no rness band, status line or pane; ${MOD_FLOOR} or later does \u2014 claude update`;
8327
+ }
8328
+
8329
+ // src/core/edit-guard.ts
8330
+ function occurrences(text, needle) {
8331
+ const spans = [];
8332
+ for (let i = text.indexOf(needle); i !== -1; i = text.indexOf(needle, i + needle.length))
8333
+ spans.push([i, i + needle.length]);
8334
+ return spans;
8335
+ }
8336
+ var QUOTES = {
8337
+ "\u2018": "'",
8338
+ "\u2019": "'",
8339
+ "\u201C": '"',
8340
+ "\u201D": '"'
8341
+ };
8342
+ function loose(text) {
8343
+ let out = "";
8344
+ const at = [];
8345
+ for (let i = 0; i < text.length; i++) {
8346
+ const c = text[i] ?? "";
8347
+ if (c === "\r" && text[i + 1] === "\n") continue;
8348
+ out += QUOTES[c] ?? c;
8349
+ at.push(i);
8350
+ }
8351
+ at.push(text.length);
8352
+ return { text: out, at };
8353
+ }
8354
+ function replayEdit(text, oldString, newString, all) {
8355
+ if (oldString === "") return null;
8356
+ let spans = occurrences(text, oldString);
8357
+ if (spans.length === 0) {
8358
+ const file = loose(text);
8359
+ spans = occurrences(file.text, loose(oldString).text).map(
8360
+ ([s, e]) => [file.at[s] ?? 0, (file.at[e - 1] ?? 0) + 1]
8361
+ );
8362
+ }
8363
+ if (spans.length === 0 || !all && spans.length !== 1) return null;
8364
+ let after = text;
8365
+ for (const [s, e] of [...spans].reverse())
8366
+ after = `${after.slice(0, s)}${newString}${after.slice(e)}`;
8367
+ return { after, spans };
8368
+ }
8369
+ var OWNED = /(?:^|\/)\.claude\/skills\/rness\//;
8370
+ var ownedWhole = (rel) => OWNED.test(rel);
8371
+ function ownedRefusal(label) {
8372
+ return `rness: ${label} is written whole by rness sync, and the next sync overwrites it. Leave it to rness.`;
8373
+ }
8374
+ var MARKER = /^<!-- rness: (\S+) -->$/;
8375
+ function blockOf(text) {
8376
+ const lines = text.split(/\r?\n/);
8377
+ const found = findBlock(lines);
8378
+ if (found.kind !== "one") return null;
8379
+ const starts = [0];
8380
+ for (const m of text.matchAll(/\r?\n/g))
8381
+ starts.push((m.index ?? 0) + m[0].length);
8382
+ const from = starts[found.begin] ?? 0;
8383
+ const to = (starts[found.end] ?? 0) + (lines[found.end] ?? "").length;
8384
+ return { begin: found.begin, end: found.end, from, to, lines };
8385
+ }
8386
+ function lineAt(text, offset) {
8387
+ return text.slice(0, offset).split(/\r?\n/).length - 1;
8388
+ }
8389
+ function sourceOf(block, line3) {
8390
+ for (let i = Math.min(line3, block.end); i > block.begin; i--) {
8391
+ const m = MARKER.exec((block.lines[i] ?? "").trimEnd());
8392
+ if (m !== null) return `.rness/${m[1] ?? ""}`;
8393
+ }
8394
+ return ".rness/ and rness.json";
8395
+ }
8396
+ function blockRefusal(label, block, source) {
8397
+ 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.`;
8398
+ }
8399
+ function editRefusal(label, before, spans) {
8400
+ const block = blockOf(before);
8401
+ if (block === null) return null;
8402
+ const hit = spans.find(([s, e]) => s < block.to && e > block.from);
8403
+ if (hit === void 0) return null;
8404
+ return blockRefusal(
8405
+ label,
8406
+ block,
8407
+ sourceOf(block, lineAt(before, Math.max(hit[0], block.from)))
8408
+ );
8409
+ }
8410
+ function writeRefusal(label, before, after) {
8411
+ const block = blockOf(before);
8412
+ if (block === null) return null;
8413
+ const kept = blockOf(after);
8414
+ const was = before.slice(block.from, block.to);
8415
+ if (kept !== null && after.slice(kept.from, kept.to) === was) return null;
8416
+ const next = kept === null ? [] : kept.lines.slice(kept.begin, kept.end + 1);
8417
+ const old = block.lines.slice(block.begin, block.end + 1);
8418
+ const first = old.findIndex((line3, i) => line3 !== next[i]);
8419
+ return blockRefusal(
8420
+ label,
8421
+ block,
8422
+ kept === null ? ".rness/ and rness.json" : sourceOf(block, block.begin + Math.max(first, 0))
8423
+ );
8424
+ }
8425
+
8426
+ // src/core/safety-net.ts
8427
+ var SHOWN_PROBLEMS = 3;
8428
+ var message = (e) => e instanceof Error ? e.message : String(e);
8429
+ async function safetyNet(ws, manifest) {
8430
+ const notes = [];
8431
+ const drift = await pinDrift(ws.rnessDir);
8432
+ if (drift !== null)
8433
+ notes.push(
8434
+ `.rness pins @rness/cli ${drift.pin} but ${drift.installed ?? "nothing"} is installed \u2014 run ${await workspacePackageManager(ws.rnessDir)} install in .rness`
8435
+ );
8436
+ try {
8437
+ const { problems } = await checkWorkspace(ws, manifest);
8438
+ if (problems.length > 0)
8439
+ notes.push(
8440
+ `${count(problems.length, ["problem", "problems"])} in the workspace context \u2014 run rness validate`,
8441
+ ...problems.slice(0, SHOWN_PROBLEMS).map((p) => ` ${p}`)
8442
+ );
8443
+ } catch (e) {
8444
+ notes.push(message(e));
8445
+ }
8446
+ return notes;
8447
+ }
8448
+
8449
+ // src/pulse/detached.ts
8450
+ import { spawn as nodeSpawn } from "child_process";
8451
+ import { mkdir as mkdir9, open as open2, readFile as readFile13, rm as rm9, stat as stat4, writeFile as writeFile5 } from "fs/promises";
8452
+ import { join as join28 } from "path";
8453
+ import { setTimeout as delay2 } from "timers/promises";
8454
+ var detached = (args, cwd) => {
8455
+ const bin = process.argv[1];
8456
+ if (bin === void 0) return;
8457
+ const child = nodeSpawn(process.execPath, [bin, ...args], {
8458
+ detached: true,
8459
+ stdio: "ignore",
8460
+ cwd,
8461
+ env: { ...process.env, RNESS_NO_DELEGATE: "1" }
8462
+ });
8463
+ child.on("error", () => void 0);
8464
+ child.unref();
8465
+ };
8466
+ var failureFile = () => join28(configDir(), "pulse.json");
8467
+ async function recordFailure(reason3) {
8468
+ try {
8469
+ await mkdir9(configDir(), { recursive: true, mode: 448 });
8470
+ await writeFile5(
8471
+ failureFile(),
8472
+ `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), reason: reason3 })}
8473
+ `
8474
+ );
8475
+ } catch {
8476
+ }
8477
+ }
8478
+ async function takeFailure() {
8479
+ try {
8480
+ const data = JSON.parse(await readFile13(failureFile(), "utf8"));
8481
+ await rm9(failureFile(), { force: true });
8482
+ const reason3 = data !== null && typeof data === "object" ? data["reason"] : void 0;
8483
+ return typeof reason3 === "string" && reason3 !== "" ? reason3 : null;
8484
+ } catch {
8485
+ return null;
8486
+ }
8487
+ }
8488
+ var PULSE_LOCK_STALE_MS = 15 * 6e4;
8489
+ async function oneSyncAtATime(key, work, options = {}) {
8490
+ await mkdir9(configDir(), { recursive: true, mode: 448 });
8491
+ const file = join28(configDir(), `pulse-${key}.lock`);
8492
+ for (; ; ) {
8493
+ try {
8494
+ const handle = await open2(file, "wx");
8495
+ await handle.close();
8496
+ break;
8497
+ } catch (e) {
8498
+ if (e.code !== "EEXIST") throw e;
8499
+ const age = await stat4(file).then(
8500
+ (s) => Date.now() - s.mtimeMs,
8501
+ () => 0
8502
+ );
8503
+ if (age > PULSE_LOCK_STALE_MS) await rm9(file, { force: true });
8504
+ else if (options.wait !== true) return false;
8505
+ else await (options.sleep ?? ((ms) => delay2(ms)))(1e3);
8506
+ }
8507
+ }
8508
+ try {
8509
+ await work();
8510
+ return true;
8511
+ } finally {
8512
+ await rm9(file, { force: true });
8513
+ }
8514
+ }
8515
+
8177
8516
  // src/mcp/tools.ts
8178
- import { readFile as readFile12, realpath as realpath3, stat as stat4 } from "fs/promises";
8179
- import { isAbsolute, join as join26, posix as posix7, relative as relative6, resolve as resolve6, sep as sep4 } from "path";
8517
+ import { readFile as readFile14, realpath as realpath3, stat as stat5 } from "fs/promises";
8518
+ import { isAbsolute, join as join29, posix as posix7, relative as relative6, resolve as resolve6, sep as sep4 } from "path";
8180
8519
  var LABELS = {
8181
8520
  standards: "Standards",
8182
8521
  adr: "Decisions",
@@ -8205,7 +8544,7 @@ async function documents(rnessDir, only) {
8205
8544
  const docs = [];
8206
8545
  for (const collection of COLLECTIONS) {
8207
8546
  if (only !== void 0 && collection !== only) continue;
8208
- for (const item of await collectMarkdown(join26(rnessDir, collection))) {
8547
+ for (const item of await collectMarkdown(join29(rnessDir, collection))) {
8209
8548
  const path = `${collection}/${item.rel}`;
8210
8549
  if (path === TEMPLATE2) continue;
8211
8550
  const status2 = item.fields?.["status"];
@@ -8321,13 +8660,13 @@ async function read(cwd, args) {
8321
8660
  }
8322
8661
  if (!real.startsWith(`${root}${sep4}`))
8323
8662
  return refuse(`${path}: leads outside .rness/`);
8324
- const info = await stat4(real);
8663
+ const info = await stat5(real);
8325
8664
  if (!info.isFile()) return refuse(`${path}: not a file`);
8326
8665
  if (info.size > READ_LIMIT)
8327
8666
  return refuse(
8328
8667
  `${path}: ${info.size} bytes, over the 256 KiB rness_read returns`
8329
8668
  );
8330
- return { text: await readFile12(real, "utf8") };
8669
+ return { text: await readFile14(real, "utf8") };
8331
8670
  }
8332
8671
  async function search(cwd, args) {
8333
8672
  const { ws } = await workspaceAt(cwd);
@@ -8433,74 +8772,7 @@ function mcpTools(cwd) {
8433
8772
  ];
8434
8773
  }
8435
8774
 
8436
- // src/pulse/detached.ts
8437
- import { spawn as nodeSpawn } from "child_process";
8438
- import { mkdir as mkdir8, open as open2, readFile as readFile13, rm as rm9, stat as stat5, writeFile as writeFile5 } from "fs/promises";
8439
- import { join as join27 } from "path";
8440
- import { setTimeout as delay2 } from "timers/promises";
8441
- var detached = (args, cwd) => {
8442
- const bin = process.argv[1];
8443
- if (bin === void 0) return;
8444
- const child = nodeSpawn(process.execPath, [bin, ...args], {
8445
- detached: true,
8446
- stdio: "ignore",
8447
- cwd,
8448
- env: { ...process.env, RNESS_NO_DELEGATE: "1" }
8449
- });
8450
- child.on("error", () => void 0);
8451
- child.unref();
8452
- };
8453
- var failureFile = () => join27(configDir(), "pulse.json");
8454
- async function recordFailure(reason3) {
8455
- try {
8456
- await mkdir8(configDir(), { recursive: true, mode: 448 });
8457
- await writeFile5(
8458
- failureFile(),
8459
- `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), reason: reason3 })}
8460
- `
8461
- );
8462
- } catch {
8463
- }
8464
- }
8465
- async function takeFailure() {
8466
- try {
8467
- const data = JSON.parse(await readFile13(failureFile(), "utf8"));
8468
- await rm9(failureFile(), { force: true });
8469
- const reason3 = data !== null && typeof data === "object" ? data["reason"] : void 0;
8470
- return typeof reason3 === "string" && reason3 !== "" ? reason3 : null;
8471
- } catch {
8472
- return null;
8473
- }
8474
- }
8475
- var PULSE_LOCK_STALE_MS = 15 * 6e4;
8476
- async function oneSyncAtATime(key, work, options = {}) {
8477
- await mkdir8(configDir(), { recursive: true, mode: 448 });
8478
- const file = join27(configDir(), `pulse-${key}.lock`);
8479
- for (; ; ) {
8480
- try {
8481
- const handle = await open2(file, "wx");
8482
- await handle.close();
8483
- break;
8484
- } catch (e) {
8485
- if (e.code !== "EEXIST") throw e;
8486
- const age = await stat5(file).then(
8487
- (s) => Date.now() - s.mtimeMs,
8488
- () => 0
8489
- );
8490
- if (age > PULSE_LOCK_STALE_MS) await rm9(file, { force: true });
8491
- else if (options.wait !== true) return false;
8492
- else await (options.sleep ?? ((ms) => delay2(ms)))(1e3);
8493
- }
8494
- }
8495
- try {
8496
- await work();
8497
- return true;
8498
- } finally {
8499
- await rm9(file, { force: true });
8500
- }
8501
- }
8502
-
8503
- // src/commands/hook.ts
8775
+ // src/commands/snapshot.ts
8504
8776
  var NOUNS = {
8505
8777
  standards: ["standard", "standards"],
8506
8778
  adr: ["decision", "decisions"],
@@ -8508,7 +8780,54 @@ var NOUNS = {
8508
8780
  plans: ["plan", "plans"],
8509
8781
  skills: ["skill", "skills"]
8510
8782
  };
8511
- var SHOWN_PROBLEMS = 3;
8783
+ async function scopeBanner(ws, manifest, scope) {
8784
+ const summary = await scopeSummary(ws, manifest, scope);
8785
+ const counts = COLLECTIONS.flatMap((c) => {
8786
+ const n = summary.counts[c];
8787
+ return n === void 0 ? [] : [count(n, NOUNS[c])];
8788
+ });
8789
+ 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(", ")}`;
8790
+ return { banner: banner2, lines: summary.lines };
8791
+ }
8792
+ var noteLines = (notes) => notes.map((n) => `rness: ${n}`);
8793
+ function statusLine(scope, inProgress, notes) {
8794
+ const acting = notes.filter((n) => !n.startsWith(" ")).length;
8795
+ return `${scope ?? "global"} \xB7 ${inProgress.length} in progress${acting > 0 ? ` \xB7 \u26A0 ${acting}` : ""}`;
8796
+ }
8797
+ function pulseOf(manifest) {
8798
+ const project = manifest.projects?.[PULSE];
8799
+ return manifest.org === null || project === void 0 ? null : { org: manifest.org, project };
8800
+ }
8801
+ function snapshotTabs(tabs, pulse) {
8802
+ return tabs.map((tab) => ({
8803
+ name: tab.name,
8804
+ label: tab.label,
8805
+ rows: tab.rows.map((row) => ({
8806
+ ...row,
8807
+ color: row.status === null ? "red" : optionColor("Status", row.status),
8808
+ link: pulse === null ? null : itemUrl(pulse.org, pulse.project, row.path)
8809
+ }))
8810
+ }));
8811
+ }
8812
+ async function statusSnapshot(ws, manifest, scope, tabs) {
8813
+ const { banner: banner2 } = await scopeBanner(ws, manifest, scope);
8814
+ const inProgress = await plansInProgress(ws, manifest, scope);
8815
+ const notes = await safetyNet(ws, manifest);
8816
+ const pulse = pulseOf(manifest);
8817
+ return {
8818
+ version: VERSION,
8819
+ workspace: workspaceName(manifest, ws.root),
8820
+ scope,
8821
+ banner: banner2,
8822
+ statusLine: statusLine(scope, inProgress, notes),
8823
+ inProgress,
8824
+ notes: noteLines(notes),
8825
+ pulse: pulse === null ? null : boardUrl(pulse.org, pulse.project),
8826
+ tabs: snapshotTabs(tabs, pulse)
8827
+ };
8828
+ }
8829
+
8830
+ // src/commands/hook.ts
8512
8831
  async function readInput(input) {
8513
8832
  if (input.isTTY === true) return {};
8514
8833
  let text = "";
@@ -8520,32 +8839,13 @@ async function readInput(input) {
8520
8839
  return {};
8521
8840
  }
8522
8841
  }
8523
- var message = (e) => e instanceof Error ? e.message : String(e);
8842
+ var message2 = (e) => e instanceof Error ? e.message : String(e);
8524
8843
  function cwdOf(input, env) {
8525
8844
  const cwd = input["cwd"];
8526
8845
  if (typeof cwd === "string" && cwd !== "") return cwd;
8527
8846
  return env["CLAUDE_PROJECT_DIR"] ?? process.cwd();
8528
8847
  }
8529
8848
  var toPosix2 = (p) => p.split(sep5).join(posix8.sep);
8530
- async function safetyNet(ws, manifest) {
8531
- const notes = [];
8532
- const drift = await pinDrift(ws.rnessDir);
8533
- if (drift !== null)
8534
- notes.push(
8535
- `.rness pins @rness/cli ${drift.pin} but ${drift.installed ?? "nothing"} is installed \u2014 run ${await workspacePackageManager(ws.rnessDir)} install in .rness`
8536
- );
8537
- try {
8538
- const { problems } = await checkWorkspace(ws, manifest);
8539
- if (problems.length > 0)
8540
- notes.push(
8541
- `${count(problems.length, ["problem", "problems"])} in the workspace context \u2014 run rness validate`,
8542
- ...problems.slice(0, SHOWN_PROBLEMS).map((p) => ` ${p}`)
8543
- );
8544
- } catch (e) {
8545
- notes.push(message(e));
8546
- }
8547
- return notes;
8548
- }
8549
8849
  function sessionOf(input) {
8550
8850
  const id = input["session_id"];
8551
8851
  if (typeof id !== "string" || id === "") return null;
@@ -8573,21 +8873,6 @@ async function markDetached(ws, input, io, rest) {
8573
8873
  } catch {
8574
8874
  }
8575
8875
  }
8576
- async function plansInProgress(ws, manifest, scope) {
8577
- const context3 = await assembleContext({
8578
- rnessDir: ws.rnessDir,
8579
- manifest,
8580
- scope
8581
- });
8582
- const plans = context3.collections.find((c) => c.name === "plans");
8583
- return (plans?.files ?? []).filter((f) => {
8584
- try {
8585
- return parseFrontMatter(f.body)?.["status"] === "In progress";
8586
- } catch {
8587
- return false;
8588
- }
8589
- }).map((f) => `plans/${f.rel}`);
8590
- }
8591
8876
  async function sessionStart(input, io) {
8592
8877
  const cwd = cwdOf(input, io.env);
8593
8878
  let banner2 = null;
@@ -8597,15 +8882,11 @@ async function sessionStart(input, io) {
8597
8882
  const ws = await findWorkspace(cwd);
8598
8883
  const manifest = await loadManifest(ws.rnessDir);
8599
8884
  const scope = resolveScope(manifest, toPosix2(relative7(ws.root, cwd)));
8600
- const summary = await scopeSummary(ws, manifest, scope);
8601
- const counts = COLLECTIONS.flatMap((c) => {
8602
- const n = summary.counts[c];
8603
- return n === void 0 ? [] : [count(n, NOUNS[c])];
8604
- });
8605
- banner2 = `rness ${VERSION} \xB7 ${workspaceName(manifest, ws.root)} \xB7 ${scope === null ? "global scope" : `scope ${scope}`} \u2014 ${counts.length === 0 ? "nothing applies yet" : counts.join(", ")}`;
8885
+ const view = await scopeBanner(ws, manifest, scope);
8886
+ banner2 = view.banner;
8606
8887
  const at = toPosix2(relative7(cwd, ws.rnessDir)) || ".rness";
8607
8888
  context3 = [
8608
- ...summary.lines,
8889
+ ...view.lines,
8609
8890
  `These documents are in ${at}: read one there, or with rness_read when the rness MCP server is connected.`
8610
8891
  ];
8611
8892
  notes = await safetyNet(ws, manifest);
@@ -8617,14 +8898,17 @@ async function sessionStart(input, io) {
8617
8898
  (session) => paths.length === 0 ? null : ["--session", session, ...paths.flatMap((p) => ["--path", p])]
8618
8899
  );
8619
8900
  } catch (e) {
8620
- notes = [message(e)];
8901
+ notes = [message2(e)];
8621
8902
  }
8622
8903
  const failure = await takeFailure();
8623
8904
  if (failure !== null) notes.push(`pulse not updated \u2014 ${failure}`);
8624
- const warnings = notes.map((n) => `rness: ${n}`);
8905
+ const warnings = noteLines(notes);
8906
+ const compact = input["source"] === "compact";
8907
+ const tooOld = compact ? null : await takeModNotice(io.env);
8625
8908
  const shown = [
8626
- ...banner2 !== null && input["source"] !== "compact" ? [banner2] : [],
8627
- ...warnings
8909
+ ...banner2 !== null && !compact ? [banner2] : [],
8910
+ ...warnings,
8911
+ ...tooOld !== null ? [tooOld] : []
8628
8912
  ];
8629
8913
  return {
8630
8914
  ...shown.length > 0 ? { systemMessage: shown.join("\n") } : {},
@@ -8649,11 +8933,7 @@ async function postToolUse(input, io) {
8649
8933
  } catch {
8650
8934
  return 0;
8651
8935
  }
8652
- const file = resolve7(cwd, path);
8653
- const rel = inside(ws.rnessDir, file) ?? inside(
8654
- await realpath4(ws.rnessDir).catch(() => ws.rnessDir),
8655
- await realpath4(file).catch(() => file)
8656
- );
8936
+ const rel = await within(ws.rnessDir, resolve7(cwd, path));
8657
8937
  if (rel === null) return 0;
8658
8938
  const first = rel.split("/")[0] ?? "";
8659
8939
  let problems = [];
@@ -8662,7 +8942,7 @@ async function postToolUse(input, io) {
8662
8942
  const manifest = await loadManifest(ws.rnessDir);
8663
8943
  for (const s of Object.keys(manifest.scopes)) scopeChain(manifest, s);
8664
8944
  } catch (e) {
8665
- problems = [message(e)];
8945
+ problems = [message2(e)];
8666
8946
  }
8667
8947
  } else if (COLLECTIONS.includes(first) && rel.endsWith(".md"))
8668
8948
  problems = (await checkContract(ws.rnessDir)).filter(
@@ -8685,6 +8965,95 @@ async function postToolUse(input, io) {
8685
8965
  );
8686
8966
  return 2;
8687
8967
  }
8968
+ async function within(base, file) {
8969
+ return inside(base, file) ?? inside(
8970
+ await realpath4(base).catch(() => base),
8971
+ await realpath4(file).catch(() => file)
8972
+ );
8973
+ }
8974
+ function manifestProblems(text) {
8975
+ if (text === null) return [];
8976
+ try {
8977
+ const manifest = parseManifest(text);
8978
+ for (const s of Object.keys(manifest.scopes)) scopeChain(manifest, s);
8979
+ return [];
8980
+ } catch (e) {
8981
+ return [message2(e)];
8982
+ }
8983
+ }
8984
+ async function addedProblems(ws, rel, before, after) {
8985
+ let was;
8986
+ let will;
8987
+ if (rel === "rness.json") {
8988
+ was = manifestProblems(before);
8989
+ will = manifestProblems(after);
8990
+ } else {
8991
+ const first = rel.split("/")[0] ?? "";
8992
+ if (!COLLECTIONS.includes(first) || !rel.endsWith(".md"))
8993
+ return [];
8994
+ const mine = (p) => p.startsWith(`${rel}:`);
8995
+ was = (await checkContract(ws.rnessDir)).filter(mine);
8996
+ will = (await checkContract(ws.rnessDir, { rel, text: after })).filter(mine);
8997
+ }
8998
+ return will.filter((p) => !was.includes(p));
8999
+ }
9000
+ async function preToolUse(input, io) {
9001
+ const toolInput = input["tool_input"];
9002
+ if (toolInput === null || typeof toolInput !== "object") return 0;
9003
+ const args = toolInput;
9004
+ const path = args["file_path"];
9005
+ if (typeof path !== "string" || path === "") return 0;
9006
+ try {
9007
+ const cwd = cwdOf(input, io.env);
9008
+ const ws = await findWorkspace(cwd);
9009
+ const file = resolve7(cwd, path);
9010
+ const label = await within(ws.root, file);
9011
+ if (label === null) return 0;
9012
+ if (ownedWhole(label)) return refuse2(io, [ownedRefusal(label)]);
9013
+ const rel = await within(ws.rnessDir, file);
9014
+ const blocked = BLOCK_FILES.includes(posix8.basename(label));
9015
+ if (rel === null && !blocked) return 0;
9016
+ const before = await readOrNull(file);
9017
+ let after;
9018
+ let spans = null;
9019
+ if (input["tool_name"] === "Write") {
9020
+ const content = args["content"];
9021
+ if (typeof content !== "string") return 0;
9022
+ after = content;
9023
+ } else {
9024
+ const { old_string, new_string, replace_all } = args;
9025
+ if (before === null || typeof old_string !== "string" || typeof new_string !== "string")
9026
+ return 0;
9027
+ const replayed = replayEdit(
9028
+ before,
9029
+ old_string,
9030
+ new_string,
9031
+ replace_all === true
9032
+ );
9033
+ if (replayed === null) return 0;
9034
+ after = replayed.after;
9035
+ spans = replayed.spans;
9036
+ }
9037
+ if (blocked && before !== null) {
9038
+ const reason3 = spans === null ? writeRefusal(label, before, after) : editRefusal(label, before, spans);
9039
+ if (reason3 !== null) return refuse2(io, [reason3]);
9040
+ }
9041
+ if (rel === null) return 0;
9042
+ const added = await addedProblems(ws, rel, before, after);
9043
+ if (added.length === 0) return 0;
9044
+ return refuse2(io, [
9045
+ ...added.map((p) => `rness: ${p}`),
9046
+ `Fix the edit of ${rel}; rness validate checks the whole workspace.`
9047
+ ]);
9048
+ } catch {
9049
+ return 0;
9050
+ }
9051
+ }
9052
+ function refuse2(io, lines) {
9053
+ io.error.write(`${lines.join("\n")}
9054
+ `);
9055
+ return 2;
9056
+ }
8688
9057
  async function sessionEnd(input, io) {
8689
9058
  try {
8690
9059
  const ws = await findWorkspace(cwdOf(input, io.env));
@@ -8703,14 +9072,15 @@ async function hookCommand(event, io = {
8703
9072
  error: process.stderr,
8704
9073
  env: process.env
8705
9074
  }) {
8706
- if (event !== "session-start" && event !== "post-tool-use" && event !== "session-end") {
9075
+ if (event !== "session-start" && event !== "pre-tool-use" && event !== "post-tool-use" && event !== "session-end") {
8707
9076
  io.error.write(
8708
- `unknown hook event ${JSON.stringify(event)} (known: session-start, post-tool-use, session-end)
9077
+ `unknown hook event ${JSON.stringify(event)} (known: session-start, pre-tool-use, post-tool-use, session-end)
8709
9078
  `
8710
9079
  );
8711
9080
  return 1;
8712
9081
  }
8713
9082
  const input = await readInput(io.input);
9083
+ if (event === "pre-tool-use") return preToolUse(input, io);
8714
9084
  if (event === "post-tool-use") return postToolUse(input, io);
8715
9085
  if (event === "session-end") return sessionEnd(input, io);
8716
9086
  io.output.write(`${JSON.stringify(await sessionStart(input, io))}
@@ -8736,31 +9106,31 @@ var SERVER_INFO_KEY = "io.modelcontextprotocol/serverInfo";
8736
9106
  function isObject2(value) {
8737
9107
  return value !== null && typeof value === "object" && !Array.isArray(value);
8738
9108
  }
8739
- function errorLine(id, code, message2, data) {
9109
+ function errorLine(id, code, message3, data) {
8740
9110
  return JSON.stringify({
8741
9111
  jsonrpc: "2.0",
8742
9112
  id,
8743
- error: data === void 0 ? { code, message: message2 } : { code, message: message2, data }
9113
+ error: data === void 0 ? { code, message: message3 } : { code, message: message3, data }
8744
9114
  });
8745
9115
  }
8746
9116
  function createProtocol(opts) {
8747
9117
  const serverInfo = { name: opts.name, version: opts.version };
8748
9118
  return {
8749
9119
  async handle(line3) {
8750
- let message2;
9120
+ let message3;
8751
9121
  try {
8752
- message2 = JSON.parse(line3);
9122
+ message3 = JSON.parse(line3);
8753
9123
  } catch {
8754
9124
  return errorLine(null, -32700, "Parse error");
8755
9125
  }
8756
- const rawId = isObject2(message2) ? message2["id"] : void 0;
9126
+ const rawId = isObject2(message3) ? message3["id"] : void 0;
8757
9127
  const id = typeof rawId === "string" || typeof rawId === "number" ? rawId : null;
8758
- if (!isObject2(message2) || message2["jsonrpc"] !== "2.0" || typeof message2["method"] !== "string")
9128
+ if (!isObject2(message3) || message3["jsonrpc"] !== "2.0" || typeof message3["method"] !== "string")
8759
9129
  return errorLine(id, -32600, "Invalid Request");
8760
- if (!("id" in message2)) return null;
9130
+ if (!("id" in message3)) return null;
8761
9131
  if (id === null) return errorLine(null, -32600, "Invalid Request");
8762
- const method = message2["method"];
8763
- const params = isObject2(message2["params"]) ? message2["params"] : {};
9132
+ const method = message3["method"];
9133
+ const params = isObject2(message3["params"]) ? message3["params"] : {};
8764
9134
  const meta = isObject2(params["_meta"]) ? params["_meta"] : null;
8765
9135
  const modern = meta !== null && typeof meta[VERSION_KEY] === "string";
8766
9136
  if (modern) {
@@ -8868,13 +9238,13 @@ async function mcpCommand(opts, io = {
8868
9238
  }
8869
9239
 
8870
9240
  // src/commands/pulse.ts
8871
- import { readFile as readFile15, readdir as readdir5 } from "fs/promises";
8872
- import { join as join28 } from "path";
9241
+ import { readFile as readFile16, readdir as readdir5 } from "fs/promises";
9242
+ import { join as join30 } from "path";
8873
9243
  import { setTimeout as delay3 } from "timers/promises";
8874
9244
 
8875
9245
  // src/pulse/opened.ts
8876
- import { mkdir as mkdir9, readFile as readFile14, rename as rename3, rm as rm10, writeFile as writeFile6 } from "fs/promises";
8877
- import { dirname as dirname9 } from "path";
9246
+ import { mkdir as mkdir10, readFile as readFile15, rename as rename3, rm as rm10, writeFile as writeFile6 } from "fs/promises";
9247
+ import { dirname as dirname10 } from "path";
8878
9248
  var NAME2 = "rness/opened";
8879
9249
  function openedFile(rnessDir) {
8880
9250
  return gitPath(rnessDir, NAME2);
@@ -8885,7 +9255,7 @@ async function readOpened(file) {
8885
9255
  if (file === null) return opened;
8886
9256
  let data;
8887
9257
  try {
8888
- data = JSON.parse(await readFile14(file, "utf8"));
9258
+ data = JSON.parse(await readFile15(file, "utf8"));
8889
9259
  } catch {
8890
9260
  return opened;
8891
9261
  }
@@ -8904,7 +9274,7 @@ async function writeOpened(file, opened) {
8904
9274
  return;
8905
9275
  }
8906
9276
  const entries = [...opened].sort(([a], [b]) => a - b);
8907
- await mkdir9(dirname9(file), { recursive: true });
9277
+ await mkdir10(dirname10(file), { recursive: true });
8908
9278
  const temporary = `${file}.${process.pid}`;
8909
9279
  await writeFile6(
8910
9280
  temporary,
@@ -8920,7 +9290,6 @@ async function writeOpened(file, opened) {
8920
9290
  var PROJECT_SCOPE = "project";
8921
9291
  var NEEDS_LOGIN = "the pulse needs a GitHub login: run rness login";
8922
9292
  var NEEDS_SCOPE = "the pulse needs the project scope: run rness login";
8923
- var boardUrl = (org, project) => `https://github.com/orgs/${org}/projects/${project}`;
8924
9293
  async function fromGithub(work) {
8925
9294
  try {
8926
9295
  return await work;
@@ -8982,10 +9351,10 @@ async function directoriesOf(dir) {
8982
9351
  }
8983
9352
  }
8984
9353
  async function scopeOf(c, name, tabs, owners) {
8985
- const within = (collection) => (path) => path.startsWith(`${collection}/`);
9354
+ const within2 = (collection) => (path) => path.startsWith(`${collection}/`);
8986
9355
  if (name === PULSE) {
8987
9356
  const shown = tabs.filter((t) => !owners.includes(t.name));
8988
- const elsewhere = (path) => owners.some((o) => within(o)(path));
9357
+ const elsewhere = (path) => owners.some((o) => within2(o)(path));
8989
9358
  return {
8990
9359
  label: "Agent Pulse",
8991
9360
  want: desiredOf(shown, c.org),
@@ -9000,7 +9369,7 @@ async function scopeOf(c, name, tabs, owners) {
9000
9369
  label: labelOf(name),
9001
9370
  rows: []
9002
9371
  };
9003
- const dir = join28(c.rnessDir, name);
9372
+ const dir = join30(c.rnessDir, name);
9004
9373
  const shape = await readShape(c.rnessDir, name);
9005
9374
  const fronts = new Map(
9006
9375
  (await collectMarkdown(dir)).map((item) => [
@@ -9018,7 +9387,7 @@ async function scopeOf(c, name, tabs, owners) {
9018
9387
  want,
9019
9388
  layout: layoutOf([tab], { shape, want }),
9020
9389
  elsewhere: () => false,
9021
- mine: within(name),
9390
+ mine: within2(name),
9022
9391
  own: { shape, updates: await readUpdates(c.rnessDir, name) }
9023
9392
  };
9024
9393
  }
@@ -9046,7 +9415,7 @@ async function working(ui, doing, count3, work) {
9046
9415
  async function filesOf2(rnessDir) {
9047
9416
  const files = /* @__PURE__ */ new Set();
9048
9417
  const walk = async (rel) => {
9049
- const dir = rel === "" ? rnessDir : join28(rnessDir, ...rel.split("/"));
9418
+ const dir = rel === "" ? rnessDir : join30(rnessDir, ...rel.split("/"));
9050
9419
  for (const e of await readdir5(dir, { withFileTypes: true })) {
9051
9420
  const path = rel === "" ? e.name : `${rel}/${e.name}`;
9052
9421
  if (e.isDirectory()) {
@@ -9158,7 +9527,7 @@ async function syncBoard(c, board, ui, sayLayout, scope) {
9158
9527
  w.path,
9159
9528
  issueBody({
9160
9529
  path: w.path,
9161
- text: await readFile15(join28(c.rnessDir, ...w.path.split("/")), "utf8"),
9530
+ text: await readFile16(join30(c.rnessDir, ...w.path.split("/")), "utf8"),
9162
9531
  org: c.org,
9163
9532
  numbers,
9164
9533
  files
@@ -9371,6 +9740,7 @@ async function mark(opts) {
9371
9740
  }
9372
9741
 
9373
9742
  // src/commands/status.ts
9743
+ import { posix as posix9, relative as relative8, sep as sep6 } from "path";
9374
9744
  import { emitKeypressEvents } from "readline";
9375
9745
 
9376
9746
  // src/core/status-view.ts
@@ -9573,7 +9943,8 @@ function showView(tabs, workspace, start, { input, output }) {
9573
9943
  }
9574
9944
  async function statusCommand(tab, opts, terminal = { input: process.stdin, output: process.stdout }) {
9575
9945
  try {
9576
- const ws = await findWorkspace(opts.cwd ?? process.cwd());
9946
+ const cwd = opts.cwd ?? process.cwd();
9947
+ const ws = await findWorkspace(cwd);
9577
9948
  const manifest = await loadManifest(ws.rnessDir);
9578
9949
  const workspace = workspaceName(manifest, ws.root);
9579
9950
  const tabs = await statusTabs(ws.rnessDir);
@@ -9585,6 +9956,18 @@ async function statusCommand(tab, opts, terminal = { input: process.stdin, outpu
9585
9956
  );
9586
9957
  return 2;
9587
9958
  }
9959
+ const shown = wanted === void 0 ? tabs : [wanted];
9960
+ if (opts.json === true) {
9961
+ const scope = resolveScope(
9962
+ manifest,
9963
+ relative8(ws.root, cwd).split(sep6).join(posix9.sep)
9964
+ );
9965
+ terminal.output.write(
9966
+ `${JSON.stringify(await statusSnapshot(ws, manifest, scope, shown))}
9967
+ `
9968
+ );
9969
+ return 0;
9970
+ }
9588
9971
  if (terminal.input.isTTY === true && terminal.output.isTTY === true) {
9589
9972
  await showView(
9590
9973
  tabs,
@@ -9594,9 +9977,7 @@ async function statusCommand(tab, opts, terminal = { input: process.stdin, outpu
9594
9977
  );
9595
9978
  return 0;
9596
9979
  }
9597
- terminal.output.write(
9598
- statusMarkdown(workspace, wanted === void 0 ? tabs : [wanted])
9599
- );
9980
+ terminal.output.write(statusMarkdown(workspace, shown));
9600
9981
  return 0;
9601
9982
  } catch (e) {
9602
9983
  return reportError(e);
@@ -9647,7 +10028,7 @@ function buildProgram(state) {
9647
10028
  });
9648
10029
  program.command("status").description(
9649
10030
  "Show the status of every decision, specification and plan, a tab per directory"
9650
- ).argument("[tab]", "open on this tab: adr, specs, plans, \u2026").addOption(new Option("--cwd <dir>").hideHelp()).action(async (tab, opts) => {
10031
+ ).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) => {
9651
10032
  state.code = await statusCommand(tab, opts);
9652
10033
  });
9653
10034
  const doc = program.command("doc").description("Documents of the workspace: doc new <collection>");
@@ -9729,7 +10110,10 @@ function buildProgram(state) {
9729
10110
  program.command("written-files", { hidden: true }).addOption(new Option("--cwd <dir>").hideHelp()).action(async (opts) => {
9730
10111
  state.code = await writtenFilesCommand(opts);
9731
10112
  });
9732
- program.command("hook", { hidden: true }).argument("<event>", "session-start, post-tool-use or session-end").action(async (event) => {
10113
+ program.command("hook", { hidden: true }).argument(
10114
+ "<event>",
10115
+ "session-start, pre-tool-use, post-tool-use or session-end"
10116
+ ).action(async (event) => {
9733
10117
  state.code = await hookCommand(event);
9734
10118
  });
9735
10119
  program.command("upgrade").alias("update").description(
@@ -9918,4 +10302,4 @@ export {
9918
10302
  OWN_COMMANDS,
9919
10303
  catchUp
9920
10304
  };
9921
- //# sourceMappingURL=chunk-JHU2G7K7.js.map
10305
+ //# sourceMappingURL=chunk-WIZHU4WT.js.map