@rness/cli 0.18.0 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -133,7 +133,8 @@ the files a given agent needs, for the agents the team declares in
133
133
  by `rness doc new <collection>` — the second pre-approved line of each
134
134
  skill, so that every runtime and a bare terminal get the same number;
135
135
  `rness validate` refuses two documents with one number, the case a merge
136
- of two branches can still produce. Each skill records its session: it adds
136
+ of two branches can still produce, and a document named without one
137
+ (`NNNN-<slug>.md`, never a date). Each skill records its session: it adds
137
138
  `{ id: <session id>, agent: <model> }` to `sessions:` in the front matter
138
139
  of what it writes or closes. The agent is the model it runs as, as Claude
139
140
  Code names it (`Claude Opus 5.5`): no variable gives it to a skill. An
@@ -603,6 +604,28 @@ per repository, the files to commit there.
603
604
  Exit codes: 0 success, 1 failure, 2 usage — or a refusal without a TTY.
604
605
  `RNESS_DEBUG=1` adds stack traces; `RNESS_NO_DELEGATE=1` skips the delegation.
605
606
 
607
+ ## 0.18.1 — a document without a number is caught; small fixes
608
+
609
+ - `rness validate` and the edit hook name a document of `adr/`, `specs/` or
610
+ `plans/` whose file name has no number, with the next one:
611
+ `plans/2026-10-02-x.md: not numbered; name it 0002-<slug>.md, the next
612
+ number of plans`. A plan another agent's skill wrote with a dated name
613
+ is the usual case; rename it, or let the agent do it when the hook
614
+ tells it. A workspace with such a file fails `validate` (and its CI)
615
+ after the upgrade until it is renamed. The scaffold's `CONVENTIONS.md`
616
+ states the rule, and `upgrade` merges it in.
617
+ - A dated file name is no longer read as a number: `rness doc new` after
618
+ `2026-10-02-x.md` gave `2027`, and two dated plans of one year were
619
+ reported as one number twice.
620
+ - `rness doc new` racing another one at the same number says `<file>
621
+ exists; nothing written` (exit 1) instead of a raw `EEXIST`.
622
+ - `1 repository`, not `1 repositories`, in `create` and `sync`.
623
+ - When the SSH test passes only by asking for the key's passphrase,
624
+ `create`, `add` and `sync` say once how to load it into an agent
625
+ (`ssh-add`), since every clone would ask again.
626
+ - Ctrl+C while `create` waits for an organization's approval is a cancel
627
+ in the plain look too: `cancelled`, nothing written, exit 0 (it was 130).
628
+
606
629
  ## 0.18.0 — the wizard asks where first and waits for the approval; the skills on one grammar; Node 22.17
607
630
 
608
631
  - The published CLI runs on **Node 22.17 or later** (ADR 0010); the guard
package/dist/bin/rness.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  findDelegate,
7
7
  reportError,
8
8
  run
9
- } from "../chunk-VGBS7HDY.js";
9
+ } from "../chunk-JHU2G7K7.js";
10
10
 
11
11
  // src/bin/rness.ts
12
12
  import { pathToFileURL } from "url";
@@ -508,7 +508,7 @@ function parseRepoSpec(spec, org, host) {
508
508
  }
509
509
 
510
510
  // src/version.ts
511
- var VERSION = "0.18.0";
511
+ var VERSION = "0.18.1";
512
512
 
513
513
  // src/core/pm.ts
514
514
  import { execFile as execFile2 } from "child_process";
@@ -744,7 +744,13 @@ import { join as join5 } from "path";
744
744
 
745
745
  // src/core/documents.ts
746
746
  import { posix as posix2 } from "path";
747
+ function documentDate(rel) {
748
+ return /^((?:19|20)\d{2}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01]))-/.exec(
749
+ posix2.basename(rel)
750
+ )?.[1] ?? null;
751
+ }
747
752
  function documentNumber(rel) {
753
+ if (documentDate(rel) !== null) return null;
748
754
  return /^(\d{4})-/.exec(posix2.basename(rel))?.[1] ?? null;
749
755
  }
750
756
  function documentTitle(heading, number) {
@@ -788,9 +794,16 @@ async function checkContract(rnessDir) {
788
794
  `${where}: the number ${number} is also that of ${files.filter((f) => f !== where).join(", ")}`
789
795
  );
790
796
  }
797
+ const next = String(
798
+ Math.max(0, ...[...byNumber.keys()].map(Number)) + 1
799
+ ).padStart(4, "0");
791
800
  for (const item of items) {
792
801
  const where = `${name}/${item.rel}`;
793
802
  if (SKIP.has(where)) continue;
803
+ if (allowed !== void 0 && documentNumber(item.rel) === null)
804
+ problems.push(
805
+ `${where}: not numbered; name it ${next}-<slug>.md, the next number of ${name}`
806
+ );
794
807
  if (item.fieldsError !== null) {
795
808
  problems.push(`${where}: invalid front matter (${item.fieldsError})`);
796
809
  continue;
@@ -1127,8 +1140,8 @@ function sessionsOf(item) {
1127
1140
  }
1128
1141
  function rowOf(dir, item) {
1129
1142
  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;
1143
+ const date = documentDate(item.rel);
1144
+ const number = documentNumber(item.rel);
1132
1145
  const sessions = sessionsOf(item);
1133
1146
  return {
1134
1147
  id: date ?? number ?? base,
@@ -3948,6 +3961,9 @@ function inverse(text, stream = process.stdout) {
3948
3961
  const painted = styleText("inverse", text, { stream, validateStream: true });
3949
3962
  return painted === text ? plain : painted;
3950
3963
  }
3964
+ function count(n, [one, many]) {
3965
+ return `${n} ${n === 1 ? one : many}`;
3966
+ }
3951
3967
  function grey(text, stream = process.stdout) {
3952
3968
  if (noColor()) return text;
3953
3969
  return styleText("gray", text, { stream, validateStream: true });
@@ -4021,12 +4037,12 @@ async function detectGithubSsh(opts) {
4021
4037
  child.stderr?.setEncoding("utf8");
4022
4038
  child.stderr?.on("data", (chunk) => chunks.push(chunk));
4023
4039
  let settled = false;
4024
- const settle = (access3) => {
4040
+ const settle = (access2) => {
4025
4041
  if (settled) return;
4026
4042
  settled = true;
4027
4043
  clearTimeout(timer);
4028
4044
  child.stderr?.destroy();
4029
- resolve8(access3);
4045
+ resolve8(access2);
4030
4046
  };
4031
4047
  const timer = setTimeout(() => {
4032
4048
  child.kill("SIGKILL");
@@ -4071,8 +4087,8 @@ async function testGithubSsh(transport, interactive, announce) {
4071
4087
  };
4072
4088
  }
4073
4089
  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)}`;
4090
+ function usingSentence(access2, login) {
4091
+ 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
4092
  }
4077
4093
  function isSshUrl(url, hosts) {
4078
4094
  return url.startsWith(hosts.ssh);
@@ -4083,8 +4099,8 @@ var CHECKING = [
4083
4099
  "ssh access to github.com\u2026",
4084
4100
  "Checking SSH access to github.com \u2014 ssh may ask for your passphrase"
4085
4101
  ];
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)})`;
4102
+ function usingRest(access2, login) {
4103
+ return access2.ok ? `ssh (github.com as ${access2.login})` : `https (ssh to github.com unavailable: ${access2.reason}${loggedInAs(login)})`;
4088
4104
  }
4089
4105
  function agentHintLines(platform = process.platform) {
4090
4106
  const mac = platform === "darwin";
@@ -4094,6 +4110,12 @@ function agentHintLines(platform = process.platform) {
4094
4110
  ` ~/.ssh/config: AddKeysToAgent yes${mac ? ", UseKeychain yes" : ""}`
4095
4111
  ];
4096
4112
  }
4113
+ function passphraseHint(platform = process.platform) {
4114
+ return [
4115
+ "ssh asked for your passphrase, and asks again for every repository unless an agent holds the key:",
4116
+ ...agentHintLines(platform).slice(1)
4117
+ ].join("\n");
4118
+ }
4097
4119
  function sshWorkspaceLines(reason3) {
4098
4120
  return [
4099
4121
  `this workspace clones over SSH (rness.json) but ssh to github.com fails: ${reason3}`,
@@ -5509,7 +5531,7 @@ async function askWhatToSync(input) {
5509
5531
  {
5510
5532
  value: SELECT_ALL,
5511
5533
  label: "Select all",
5512
- hint: `${input.missing.length} repositories from rness.json`
5534
+ hint: `${count(input.missing.length, ["repository", "repositories"])} from rness.json`
5513
5535
  },
5514
5536
  ...input.missing.map((name) => ({ value: name, label: name }))
5515
5537
  ],
@@ -5610,15 +5632,16 @@ async function syncCommand(opts, deps = {}) {
5610
5632
  if (!overSsh) ui.gitIsSilent = true;
5611
5633
  if (overSsh) {
5612
5634
  const interactive = opts.yes !== true && terminal.isTty();
5613
- const { access: access3, unattended } = await testGithubSsh(
5635
+ const { access: access2, unattended } = await testGithubSsh(
5614
5636
  transport,
5615
5637
  interactive,
5616
5638
  () => ui.line(...CHECKING)
5617
5639
  );
5618
5640
  if (unattended) ui.gitIsSilent = true;
5619
- if (!access3.ok) {
5641
+ else if (access2.ok) ui.hint(passphraseHint(), "stderr");
5642
+ if (!access2.ok) {
5620
5643
  process.stderr.write(
5621
- `${[...sshWorkspaceLines(access3.reason), ...INSTEAD_OF_LINES].join("\n")}
5644
+ `${[...sshWorkspaceLines(access2.reason), ...INSTEAD_OF_LINES].join("\n")}
5622
5645
  `
5623
5646
  );
5624
5647
  return 1;
@@ -5847,20 +5870,21 @@ async function addCommand(spec, opts, deps = {}) {
5847
5870
  if (host === void 0 && opts.https === true) host = transport.hosts.https;
5848
5871
  if (host === void 0 && FULL_URL2.test(spec)) host = transport.hosts.https;
5849
5872
  if (host === void 0) {
5850
- const { access: access3, unattended } = await testGithubSsh(
5873
+ const { access: access2, unattended } = await testGithubSsh(
5851
5874
  transport,
5852
5875
  interactive,
5853
5876
  () => ui.line(...CHECKING)
5854
5877
  );
5855
- if (unattended || !access3.ok) ui.gitIsSilent = true;
5878
+ if (unattended || !access2.ok) ui.gitIsSilent = true;
5879
+ else ui.hint(passphraseHint(), "stderr");
5856
5880
  const overSsh = Object.values(manifest.repos).some(
5857
5881
  (r) => isSshUrl(r.url, transport.hosts)
5858
5882
  );
5859
- if (access3.ok || !overSsh) {
5860
- ui.line("using", usingRest(access3), usingSentence(access3));
5861
- host = access3.ok ? transport.hosts.ssh : transport.hosts.https;
5883
+ if (access2.ok || !overSsh) {
5884
+ ui.line("using", usingRest(access2), usingSentence(access2));
5885
+ host = access2.ok ? transport.hosts.ssh : transport.hosts.https;
5862
5886
  } else {
5863
- const lines = sshWorkspaceLines(access3.reason);
5887
+ const lines = sshWorkspaceLines(access2.reason);
5864
5888
  if (!interactive) {
5865
5889
  process.stderr.write(
5866
5890
  `${[...lines, httpsFlagLine(spec)].join("\n")}
@@ -6262,12 +6286,12 @@ async function checkAccess(input) {
6262
6286
  const login = (await provider.identity())?.login;
6263
6287
  const as = login === void 0 ? "" : ` (as ${login})`;
6264
6288
  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") {
6289
+ const access2 = await provider.organizationAccess(org);
6290
+ if (access2 === "member") {
6267
6291
  member();
6268
- return access3;
6292
+ return access2;
6269
6293
  }
6270
- if (access3 !== "restricted") return access3;
6294
+ if (access2 !== "restricted") return access2;
6271
6295
  const url = provider.approvalUrl();
6272
6296
  ui.warn(
6273
6297
  `${org} has not approved rness, so its private repositories are hidden`
@@ -6296,23 +6320,42 @@ async function checkAccess(input) {
6296
6320
  const who = input.canGrant === true ? org : `an owner of ${org}`;
6297
6321
  const waiting = `for ${who} to approve rness on github.com`;
6298
6322
  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
- );
6323
+ let interrupted = false;
6324
+ let wake = () => void 0;
6325
+ const interrupt = () => {
6326
+ interrupted = true;
6327
+ wake();
6328
+ };
6329
+ if (!ui.session) process.once("SIGINT", interrupt);
6330
+ let outcome;
6331
+ try {
6332
+ outcome = await ui.step(
6333
+ {
6334
+ doing: `waiting ${waiting}`,
6335
+ sentence: `Waiting ${waiting}`,
6336
+ quiet: true
6337
+ },
6338
+ async () => {
6339
+ const deadline = now() + APPROVAL_WAIT_MS;
6340
+ for (; ; ) {
6341
+ await Promise.race([
6342
+ sleep(APPROVAL_INTERVAL_MS),
6343
+ new Promise((resolve8) => {
6344
+ wake = resolve8;
6345
+ })
6346
+ ]);
6347
+ if (interrupted) return "cancelled";
6348
+ const again = await provider.organizationAccess(org);
6349
+ if (again === "member" || again === "not-member") return again;
6350
+ if (now() >= deadline) return "timeout";
6351
+ }
6352
+ },
6353
+ (r) => r === "member" ? ["member", `${org}${as}`] : null
6354
+ );
6355
+ } finally {
6356
+ process.off("SIGINT", interrupt);
6357
+ }
6358
+ if (outcome === "cancelled") return outcome;
6316
6359
  if (outcome === "member") {
6317
6360
  if (!ui.session) member();
6318
6361
  return outcome;
@@ -6804,7 +6847,7 @@ async function pickRepositories(input) {
6804
6847
  // list below it must agree (spec 0009).
6805
6848
  (l) => [
6806
6849
  "listed",
6807
- `${l.repositories.filter(isMemberRepo).length} repositories of ${org}`
6850
+ `${count(l.repositories.filter(isMemberRepo).length, ["repository", "repositories"])} of ${org}`
6808
6851
  ]
6809
6852
  );
6810
6853
  listed = listing.repositories;
@@ -7085,6 +7128,7 @@ async function createCommand(opts, deps = {}) {
7085
7128
  () => ui.line(...CHECKING)
7086
7129
  );
7087
7130
  if (sshTest.unattended) ui.gitIsSilent = true;
7131
+ else if (sshTest.access.ok) ui.hint(passphraseHint(), "stderr");
7088
7132
  }
7089
7133
  return sshTest.access;
7090
7134
  };
@@ -7094,12 +7138,12 @@ async function createCommand(opts, deps = {}) {
7094
7138
  if (opts.host !== void 0) return opts.host;
7095
7139
  if (opts.ssh === true) return transport.hosts.ssh;
7096
7140
  if (opts.https === true) return transport.hosts.https;
7097
- const access3 = await testSsh();
7098
- const github = access3.ok ? null : await getProvider();
7141
+ const access2 = await testSsh();
7142
+ const github = access2.ok ? null : await getProvider();
7099
7143
  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;
7144
+ ui.line("using", usingRest(access2, login), usingSentence(access2, login));
7145
+ if (!access2.ok) ui.gitIsSilent = true;
7146
+ return access2.ok ? transport.hosts.ssh : transport.hosts.https;
7103
7147
  };
7104
7148
  let loaded;
7105
7149
  const prompts = async () => loaded ??= await terminal.prompts();
@@ -7273,10 +7317,10 @@ async function createCommand(opts, deps = {}) {
7273
7317
  (r) => isSshUrl(r.url, transport.hosts)
7274
7318
  );
7275
7319
  if (overSsh && opts.ssh !== true && opts.host === void 0) {
7276
- const access3 = await testSsh();
7277
- if (!access3.ok) {
7320
+ const access2 = await testSsh();
7321
+ if (!access2.ok) {
7278
7322
  process.stderr.write(
7279
- `${[...sshWorkspaceLines(access3.reason), ...INSTEAD_OF_LINES].join("\n")}
7323
+ `${[...sshWorkspaceLines(access2.reason), ...INSTEAD_OF_LINES].join("\n")}
7280
7324
  `
7281
7325
  );
7282
7326
  return 1;
@@ -8008,7 +8052,7 @@ async function validateCommand(opts, deps = {}) {
8008
8052
  import { Command, CommanderError, Option } from "commander";
8009
8053
 
8010
8054
  // src/commands/doc.ts
8011
- import { access as access2, mkdir as mkdir7, readFile as readFile11, writeFile as writeFile4 } from "fs/promises";
8055
+ import { mkdir as mkdir7, readFile as readFile11, writeFile as writeFile4 } from "fs/promises";
8012
8056
  import { join as join25 } from "path";
8013
8057
  var NUMBERED = Object.keys(STATUSES);
8014
8058
  var SECTIONS = {
@@ -8044,6 +8088,19 @@ async function adrSections(rnessDir) {
8044
8088
  }
8045
8089
  return ["Context", "Decision", "Alternatives Considered", "Consequences"];
8046
8090
  }
8091
+ async function writeDocument(file, text) {
8092
+ try {
8093
+ await writeFile4(file, text, { flag: "wx" });
8094
+ } catch (e) {
8095
+ if (e.code !== "EEXIST") throw e;
8096
+ process.stderr.write(`${file} exists; nothing written
8097
+ `);
8098
+ return 1;
8099
+ }
8100
+ process.stdout.write(`${file}
8101
+ `);
8102
+ return 0;
8103
+ }
8047
8104
  async function docNewCommand(collection, opts) {
8048
8105
  if (!NUMBERED.includes(collection)) {
8049
8106
  process.stderr.write(
@@ -8083,20 +8140,8 @@ async function docNewCommand(collection, opts) {
8083
8140
  ...name === "plans" ? [] : ["", "---"],
8084
8141
  ...sections.flatMap((s) => ["", `## ${s}`])
8085
8142
  ];
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}
8143
+ return await writeDocument(file, `${[...front, "", ...body].join("\n")}
8098
8144
  `);
8099
- return 0;
8100
8145
  } catch (e) {
8101
8146
  return reportError(e);
8102
8147
  }
@@ -8482,9 +8527,6 @@ function cwdOf(input, env) {
8482
8527
  return env["CLAUDE_PROJECT_DIR"] ?? process.cwd();
8483
8528
  }
8484
8529
  var toPosix2 = (p) => p.split(sep5).join(posix8.sep);
8485
- function count(n, [one, many]) {
8486
- return `${n} ${n === 1 ? one : many}`;
8487
- }
8488
8530
  async function safetyNet(ws, manifest) {
8489
8531
  const notes = [];
8490
8532
  const drift = await pinDrift(ws.rnessDir);
@@ -9876,4 +9918,4 @@ export {
9876
9918
  OWN_COMMANDS,
9877
9919
  catchUp
9878
9920
  };
9879
- //# sourceMappingURL=chunk-VGBS7HDY.js.map
9921
+ //# sourceMappingURL=chunk-JHU2G7K7.js.map