@rness/cli 0.17.3 → 0.18.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.
@@ -508,7 +508,7 @@ function parseRepoSpec(spec, org, host) {
508
508
  }
509
509
 
510
510
  // src/version.ts
511
- var VERSION = "0.17.3";
511
+ var VERSION = "0.18.0";
512
512
 
513
513
  // src/core/pm.ts
514
514
  import { execFile as execFile2 } from "child_process";
@@ -741,6 +741,19 @@ async function assembleContext({
741
741
 
742
742
  // src/core/contract.ts
743
743
  import { join as join5 } from "path";
744
+
745
+ // src/core/documents.ts
746
+ import { posix as posix2 } from "path";
747
+ function documentNumber(rel) {
748
+ return /^(\d{4})-/.exec(posix2.basename(rel))?.[1] ?? null;
749
+ }
750
+ function documentTitle(heading, number) {
751
+ if (number === null) return heading;
752
+ const rest = new RegExp(`^${number}\\s*[\u2014\u2013:-]\\s*(.+)$`).exec(heading)?.[1];
753
+ return rest ?? heading;
754
+ }
755
+
756
+ // src/core/contract.ts
744
757
  var STATUSES = {
745
758
  adr: ["Proposed", "Accepted", "Rejected", "Superseded"],
746
759
  specs: [
@@ -760,6 +773,21 @@ async function checkContract(rnessDir) {
760
773
  for (const name of COLLECTIONS) {
761
774
  const allowed = STATUSES[name];
762
775
  const items = await collectMarkdown(join5(rnessDir, name));
776
+ const byNumber = /* @__PURE__ */ new Map();
777
+ for (const item of items) {
778
+ const where = `${name}/${item.rel}`;
779
+ if (SKIP.has(where)) continue;
780
+ const number = allowed === void 0 ? null : documentNumber(item.rel);
781
+ if (number !== null)
782
+ byNumber.set(number, [...byNumber.get(number) ?? [], where]);
783
+ }
784
+ for (const [number, files] of byNumber) {
785
+ if (files.length < 2) continue;
786
+ for (const where of files)
787
+ problems.push(
788
+ `${where}: the number ${number} is also that of ${files.filter((f) => f !== where).join(", ")}`
789
+ );
790
+ }
763
791
  for (const item of items) {
764
792
  const where = `${name}/${item.rel}`;
765
793
  if (SKIP.has(where)) continue;
@@ -808,6 +836,9 @@ var PULSE_SCOPE = "project";
808
836
  function clientId() {
809
837
  return process.env["RNESS_GITHUB_CLIENT_ID"] ?? GITHUB_CLIENT_ID;
810
838
  }
839
+ function approvalUrl() {
840
+ return `https://github.com/settings/connections/applications/${clientId()}`;
841
+ }
811
842
  function webBase() {
812
843
  return (process.env["RNESS_GITHUB_WEB"] ?? GITHUB_WEB).replace(/\/+$/, "");
813
844
  }
@@ -1063,19 +1094,6 @@ async function resolveToken(deps = {}) {
1063
1094
  // src/core/status.ts
1064
1095
  import { readdir } from "fs/promises";
1065
1096
  import { join as join7, posix as posix3 } from "path";
1066
-
1067
- // src/core/documents.ts
1068
- import { posix as posix2 } from "path";
1069
- function documentNumber(rel) {
1070
- return /^(\d{4})-/.exec(posix2.basename(rel))?.[1] ?? null;
1071
- }
1072
- function documentTitle(heading, number) {
1073
- if (number === null) return heading;
1074
- const rest = new RegExp(`^${number}\\s*[\u2014\u2013:-]\\s*(.+)$`).exec(heading)?.[1];
1075
- return rest ?? heading;
1076
- }
1077
-
1078
- // src/core/status.ts
1079
1097
  var FIXED = [
1080
1098
  ["adr", "ADR"],
1081
1099
  ["specs", "Specs"],
@@ -2052,16 +2070,26 @@ async function getUser(options) {
2052
2070
  throw new Error(`GitHub API answered ${status2} for /user`);
2053
2071
  return { login };
2054
2072
  }
2055
- async function listUserOrganizations(options) {
2073
+ async function listUserMemberships(options) {
2056
2074
  const { status: status2, body } = await getJson(
2057
- `/user/orgs?per_page=${PER_PAGE}`,
2075
+ `/user/memberships/orgs?state=active&per_page=${PER_PAGE}`,
2058
2076
  options
2059
2077
  );
2060
2078
  if (status2 !== 200 || !Array.isArray(body))
2061
- throw new Error(`GitHub API answered ${status2} for /user/orgs`);
2062
- return body.map(
2063
- (o) => o !== null && typeof o === "object" ? o["login"] : void 0
2064
- ).filter((l) => typeof l === "string");
2079
+ throw new Error(`GitHub API answered ${status2} for /user/memberships/orgs`);
2080
+ const memberships = [];
2081
+ for (const m of body) {
2082
+ if (m === null || typeof m !== "object") continue;
2083
+ const entry = m;
2084
+ const org = entry["organization"];
2085
+ const login = org !== null && typeof org === "object" ? org["login"] : void 0;
2086
+ if (typeof login !== "string" || entry["state"] !== "active") continue;
2087
+ memberships.push({
2088
+ login,
2089
+ role: typeof entry["role"] === "string" ? entry["role"] : ""
2090
+ });
2091
+ }
2092
+ return memberships;
2065
2093
  }
2066
2094
  async function getOrgMembership(org, options) {
2067
2095
  const { status: status2, body } = await getJson(
@@ -3663,8 +3691,15 @@ var GitHubOAuthProvider = class {
3663
3691
  }
3664
3692
  async listOrganizations() {
3665
3693
  if (this.#token === null) return [];
3666
- const logins = await listUserOrganizations(this.#api(this.#token.token));
3667
- return logins.map((login) => ({ login }));
3694
+ const memberships = await listUserMemberships(this.#api(this.#token.token));
3695
+ return memberships.map((m) => ({
3696
+ login: m.login,
3697
+ approved: true,
3698
+ canGrant: m.role === "admin"
3699
+ }));
3700
+ }
3701
+ approvalUrl() {
3702
+ return this.#token?.source === "login" ? approvalUrl() : null;
3668
3703
  }
3669
3704
  async organizationAccess(org) {
3670
3705
  if (this.#token === null) return "unknown";
@@ -3986,12 +4021,12 @@ async function detectGithubSsh(opts) {
3986
4021
  child.stderr?.setEncoding("utf8");
3987
4022
  child.stderr?.on("data", (chunk) => chunks.push(chunk));
3988
4023
  let settled = false;
3989
- const settle = (access2) => {
4024
+ const settle = (access3) => {
3990
4025
  if (settled) return;
3991
4026
  settled = true;
3992
4027
  clearTimeout(timer);
3993
4028
  child.stderr?.destroy();
3994
- resolve8(access2);
4029
+ resolve8(access3);
3995
4030
  };
3996
4031
  const timer = setTimeout(() => {
3997
4032
  child.kill("SIGKILL");
@@ -4036,8 +4071,8 @@ async function testGithubSsh(transport, interactive, announce) {
4036
4071
  };
4037
4072
  }
4038
4073
  var loggedInAs = (login) => login === void 0 || login === null ? "" : `, logged in as ${login}`;
4039
- function usingSentence(access2, login) {
4040
- 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)}`;
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)}`;
4041
4076
  }
4042
4077
  function isSshUrl(url, hosts) {
4043
4078
  return url.startsWith(hosts.ssh);
@@ -4048,8 +4083,8 @@ var CHECKING = [
4048
4083
  "ssh access to github.com\u2026",
4049
4084
  "Checking SSH access to github.com \u2014 ssh may ask for your passphrase"
4050
4085
  ];
4051
- function usingRest(access2, login) {
4052
- return access2.ok ? `ssh (github.com as ${access2.login})` : `https (ssh to github.com unavailable: ${access2.reason}${loggedInAs(login)})`;
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)})`;
4053
4088
  }
4054
4089
  function agentHintLines(platform = process.platform) {
4055
4090
  const mac = platform === "darwin";
@@ -4308,7 +4343,7 @@ async function ensureClaudeMd(dir) {
4308
4343
  import { join as join15 } from "path";
4309
4344
 
4310
4345
  // src/core/agent-targets.ts
4311
- import { mkdir as mkdir2 } from "fs/promises";
4346
+ import { mkdir as mkdir2, rm as rm4, rmdir } from "fs/promises";
4312
4347
  import { dirname as dirname4, join as join14 } from "path";
4313
4348
 
4314
4349
  // src/core/claude-skills.ts
@@ -4317,7 +4352,7 @@ var PLUGIN_JSON = `${JSON.stringify(
4317
4352
  {
4318
4353
  $schema: "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
4319
4354
  name: "rness",
4320
- description: "The rness workspace in Claude Code: /rness:status, /rness:adr, /rness:spec, /rness:plan, /rness:done.",
4355
+ description: "The rness workspace in Claude Code: /rness:status, /rness:adr, /rness:spec, /rness:plan.",
4321
4356
  author: { name: "rness-dev", url: "https://rness.dev" },
4322
4357
  homepage: "https://rness.dev",
4323
4358
  repository: "https://github.com/rness-dev/rness",
@@ -4360,53 +4395,68 @@ terminal, from the workspace's \`.rness/\`: \`${localRunner(pm)} rness status\`.
4360
4395
  }
4361
4396
  var STATUS_RULE = `Move a status yourself when the work calls for it, and whenever the
4362
4397
  developer says so: a plan \`In progress\` as you start its tasks,
4363
- \`Completed\` once they pass; a specification \`Implemented\` with its last
4364
- plan; a document \`Superseded\` by the one that replaces it. Accepting a
4365
- decision \u2014 an ADR \`Accepted\`, a specification \`Approved\` \u2014 is the
4366
- developer's, unless they asked you to go ahead with the work.`;
4398
+ \`Completed\` once its proofs pass under \`check\`; a specification
4399
+ \`Implemented\` with its last plan; a document \`Superseded\` by the one that
4400
+ replaces it. Accepting an ADR is the developer's; a specification is
4401
+ \`Approved\` by \`/rness:plan from NNNN\`, which is their word for it.`;
4367
4402
  var COMMIT_RULE = `Commit each file you changed, in its own repository, and only the files
4368
4403
  you alone changed: add them by name, never \`git add -A\` nor \`commit -a\`.
4369
4404
  A file that had uncommitted changes before your first edit (\`git status\`
4370
4405
  tells you), or that someone else changed since, stays uncommitted: say
4371
4406
  which. Never push.`;
4407
+ var SESSION_LINE = `\`{ id: \${CLAUDE_SESSION_ID}, agent: <model> }\` at the end of
4408
+ \`sessions:\` in the front matter, unless that id is already there;
4409
+ \`<model>\` is the model you run as, as Claude Code names it, after
4410
+ \`Claude\`: for example \`Claude Opus 5.5\``;
4411
+ var allocateStep = (allocate, collection) => `Allocate the file: run \`${allocate}\` from the project directory, where
4412
+ Claude Code was opened (pre-approved above as typed; \`CLAUDE_PROJECT_DIR\`
4413
+ is not set in your shell, so the path is relative). It writes
4414
+ \`${collection}/NNNN-untitled.md\` with the next number, the front matter
4415
+ and the opening sections, and prints the path. Rename it to
4416
+ \`NNNN-<slug>.md\`, the slug a few lowercase words of the title joined by
4417
+ hyphens, and write the title as \`# NNNN \u2014 <title>\`. Never choose the
4418
+ number yourself.`;
4419
+ var HOOK_LINE = (rness) => `The rness hook checks every edit under \`${rness}/\`: fix what it reports.`;
4372
4420
  var LIFECYCLE2 = [
4373
4421
  {
4374
4422
  name: "adr",
4375
- description: "Record a decision of the rness workspace as an ADR in .rness/adr/ \u2014 a choice expensive to reverse or that creates a lasting constraint. Use when the developer asks for an ADR, or when the conversation reaches such a decision.",
4376
- hint: "[subject | NNNN]",
4423
+ description: "Record a decision of the rness workspace as an ADR in .rness/adr/ \u2014 a choice expensive to reverse or that creates a lasting constraint \u2014 or reopen one. Use when the developer asks for an ADR, or when the conversation reaches such a decision.",
4424
+ hint: "[create <subject> | open NNNN]",
4377
4425
  tab: "adr",
4378
- body: (rness) => `You record a decision of this workspace as an ADR in \`${rness}/adr/\`. The table
4379
- above lists the existing ADRs, newest first. Arguments: \`$ARGUMENTS\`.
4426
+ body: (rness, allocate) => `You keep the decisions of this workspace: the ADRs in \`${rness}/adr/\`. The table
4427
+ above lists the existing ones, newest first. Arguments: \`$ARGUMENTS\`.
4428
+
4429
+ The first word is the verb, when a reference (\`0010\`, or a path) follows
4430
+ it; a bare reference is \`open\`; any other words are the subject of a
4431
+ \`create\` (\`open source licensing\` is a subject):
4432
+
4433
+ - \`create [subject]\`, or nothing: a new ADR, numbered by rness.
4434
+ - \`open NNNN\`: reopen ADR NNNN. An \`Accepted\` ADR is never edited: offer a
4435
+ new one that supersedes it. \`Rejected\` and \`Superseded\` are history too.
4380
4436
 
4381
- Before writing:
4437
+ Creating:
4382
4438
 
4383
4439
  1. If the developer did not ask for an ADR in so many words, propose one in
4384
4440
  a single line \u2014 its title, and why the choice is hard to reverse \u2014 and
4385
4441
  wait for a yes.
4386
4442
  2. Read \`${rness}/CONVENTIONS.md\` and \`${rness}/adr/0000-template.md\`: they hold the
4387
4443
  rules and the sections; this skill is only the procedure.
4388
- 3. Arguments naming an ADR (\`NNNN\`): reopen that file. An \`Accepted\` ADR is
4389
- never edited: offer a new ADR that supersedes it.
4390
- 4. Otherwise the subject is the arguments, else the decision just
4391
- discussed. Context, alternatives or consequences missing from the
4392
- conversation: ask for them, one question at a time. Never invent them.
4393
-
4394
- Writing:
4395
-
4396
- - \`${rness}/adr/NNNN-<slug>.md\`, NNNN the highest number above plus one.
4397
- - The template's front matter and sections, filled in, its guidance lines
4398
- removed: \`date\` today, \`status: Proposed\`, \`repo\` the repositories under
4399
- \`org/\` it affects.
4400
- - An ADR it replaces is named in Context. That one becomes \`Superseded\`,
4401
- with \`superseded_by:\`, only once the new ADR is \`Accepted\`.
4402
- - Add \`{ id: \${CLAUDE_SESSION_ID}, agent: <model> }\` at the end of
4403
- \`sessions:\` in the front matter, unless that id is already there.
4404
- \`<model>\` is the model you run as, as Claude Code names it, after
4405
- \`Claude\`: for example \`Claude Opus 5.5\`.
4406
- - The rness hook checks every edit under \`${rness}/\`: fix what it reports.
4444
+ 3. The subject is the arguments, else the decision just discussed. Context,
4445
+ alternatives or consequences missing from the conversation: ask for
4446
+ them, one question at a time. Never invent them.
4447
+ 4. ${allocateStep(allocate, "adr")}
4448
+ 5. Fill it: \`repo\` the repositories under \`org/\` it affects; the template's
4449
+ sections, their guidance lines removed; an ADR it replaces named in
4450
+ Context \u2014 that one becomes \`Superseded\`, with \`superseded_by:\`, only once
4451
+ the new ADR is \`Accepted\`. Add ${SESSION_LINE}.
4452
+ 6. ${HOOK_LINE(rness)}
4453
+
4454
+ Opening: read the ADR and amend what the developer asks, in place while it
4455
+ is \`Proposed\`; add the session line above. Nothing of the file's history is
4456
+ rewritten: a decision that changed is a new ADR.
4407
4457
 
4408
- After writing: give the path and the Decision section. The ADR stays
4409
- \`Proposed\` until it is accepted.
4458
+ After writing: give the path and the Decision section. A new ADR stays
4459
+ \`Proposed\` until the developer accepts it.
4410
4460
 
4411
4461
  ${STATUS_RULE}
4412
4462
 
@@ -4415,43 +4465,47 @@ ${COMMIT_RULE}
4415
4465
  },
4416
4466
  {
4417
4467
  name: "spec",
4418
- description: "Write a specification of the rness workspace in .rness/specs/ \u2014 the outcome wanted and its scope, before any plan. Use when the developer asks for a spec, or when agreed work is too large to start without one.",
4419
- hint: "[subject | NNNN]",
4468
+ description: "Write a specification of the rness workspace in .rness/specs/ \u2014 the outcome wanted and its scope, before any plan \u2014 or reopen one. Use when the developer asks for a spec, or when agreed work is too large to start without one.",
4469
+ hint: "[create <subject> | open NNNN]",
4420
4470
  tab: "specs",
4421
- body: (rness) => `You write a specification of this workspace in \`${rness}/specs/\`. The table above
4422
- lists the existing ones, newest first. Arguments: \`$ARGUMENTS\`.
4471
+ body: (rness, allocate) => `You keep the specifications of this workspace, in \`${rness}/specs/\`. The table
4472
+ above lists the existing ones, newest first. Arguments: \`$ARGUMENTS\`.
4423
4473
 
4424
- Before writing:
4474
+ The first word is the verb, when a reference (\`0028\`, or a path) follows
4475
+ it; a bare reference is \`open\`; any other words are the subject of a
4476
+ \`create\`:
4477
+
4478
+ - \`create [subject]\`, or nothing: a new specification, numbered by rness.
4479
+ - \`open NNNN\`: reopen specification NNNN. One that is \`Implemented\`,
4480
+ \`Superseded\` or \`Rejected\` is not edited: offer a new one.
4481
+
4482
+ Creating:
4425
4483
 
4426
4484
  1. If the developer did not ask for a specification in so many words,
4427
4485
  propose one in a single line and wait for a yes.
4428
4486
  2. Read \`${rness}/CONVENTIONS.md\`, then the most recent specification above: match
4429
4487
  its front matter, sections and tone.
4430
- 3. Arguments naming a specification (\`NNNN\`): reopen it. One that is
4431
- \`Implemented\`, \`Superseded\` or \`Rejected\` is not edited: offer a new one.
4432
- 4. Otherwise the subject is the arguments, else the work just discussed.
4433
- The problem, what is in and out of scope, the ADRs it rests on: ask what
4434
- the conversation does not say, one question at a time.
4435
-
4436
- Writing:
4437
-
4438
- - \`${rness}/specs/NNNN-<slug>.md\`, NNNN the highest number above plus one.
4439
- - \`date\` and \`updated\` today, \`status: Draft\`, \`repo\`, and \`adr:\` linking
4440
- the decisions it rests on \u2014 or the rationale in the body when none does.
4441
- - No specification yet to follow: Summary, Scope, Out of scope,
4442
- Alternatives considered, To verify, Tests.
4443
- - What was checked in this session (a document read, a command run) is
4444
- written as verified, with the date; everything else as a proposal or an
4445
- open question.
4446
- - Add \`{ id: \${CLAUDE_SESSION_ID}, agent: <model> }\` at the end of
4447
- \`sessions:\` in the front matter, unless that id is already there.
4448
- \`<model>\` is the model you run as, as Claude Code names it, after
4449
- \`Claude\`: for example \`Claude Opus 5.5\`.
4450
- - The rness hook checks every edit under \`${rness}/\`: fix what it reports.
4488
+ 3. The subject is the arguments, else the work just discussed. The problem,
4489
+ what is in and out of scope, the ADRs it rests on: ask what the
4490
+ conversation does not say, one question at a time.
4491
+ 4. ${allocateStep(allocate, "specs")}
4492
+ 5. Fill it: \`repo\`, and \`adr:\` linking the decisions it rests on \u2014 or the
4493
+ rationale in the body when none does; the sections it opened with
4494
+ (Summary, Scope, Out of scope, Alternatives considered, To verify,
4495
+ Tests), or the most recent specification's when the team's differ. What
4496
+ was checked in this session (a document read, a command run) is written
4497
+ as verified, with the date; everything else as a proposal or an open
4498
+ question. Add ${SESSION_LINE}.
4499
+ 6. ${HOOK_LINE(rness)}
4451
4500
 
4452
- After writing: give the path and the summary. It starts \`Draft\`:
4453
- \`Proposed\` for review, \`Approved\` to plan it. No plan here:
4454
- \`/rness:plan NNNN\` once approved.
4501
+ Opening: read the specification and amend what the developer asks \u2014 a
4502
+ section, a scope, an open question answered \u2014 \`updated\` today, the session
4503
+ line added. A status moves only as the developer says, or by
4504
+ \`/rness:plan from\`.
4505
+
4506
+ After writing: give the path and the summary. A new specification starts
4507
+ \`Draft\`: \`Proposed\` for review; \`/rness:plan from NNNN\` approves it and
4508
+ plans it.
4455
4509
 
4456
4510
  ${STATUS_RULE}
4457
4511
 
@@ -4460,105 +4514,104 @@ ${COMMIT_RULE}
4460
4514
  },
4461
4515
  {
4462
4516
  name: "plan",
4463
- description: "Turn an approved specification of the rness workspace into a plan in .rness/plans/ \u2014 ordered tasks, each with the command that proves it done. Use when the developer asks for a plan, or when a specification has just been approved.",
4464
- hint: "<spec NNNN>",
4517
+ description: "The plans of the rness workspace in .rness/plans/ \u2014 a plan from a specification (from, which approves it), from the conversation (create), reopened (open), or closed on evidence (check). Use when the developer asks for a plan, to approve a specification, or to check the work of a plan.",
4518
+ hint: "[create <subject> | from <spec> | open NNNN | check NNNN]",
4465
4519
  tab: "plans",
4466
- body: (rness) => `You turn a specification into a plan in \`${rness}/plans/\`. The table above lists
4467
- the existing plans, newest first. Arguments, the specification:
4468
- \`$ARGUMENTS\`.
4469
-
4470
- Before writing:
4471
-
4472
- 1. If the developer did not ask for a plan in so many words, propose one in
4473
- a single line and wait for a yes.
4474
- 2. No specification named: ask which. Read it, \`${rness}/CONVENTIONS.md\`, and the
4475
- most recent plan above: match its front matter and sections.
4476
- 3. A specification that is not \`Approved\`: say so, and plan it only if the
4477
- developer confirms. A plan of it that is neither \`Completed\` nor
4478
- \`Abandoned\`: reopen it rather than write a second.
4479
- 4. Read the code and the repositories the specification touches: every
4480
- task names real files and real commands.
4520
+ body: (rness, allocate) => `You keep the plans of this workspace, in \`${rness}/plans/\`. The table above
4521
+ lists the existing ones, newest first. Arguments: \`$ARGUMENTS\`.
4481
4522
 
4482
- Writing:
4483
-
4484
- - \`${rness}/plans/NNNN-<slug>.md\`, NNNN the highest plan number above plus one \u2014
4485
- not the specification's.
4486
- - \`date\` and \`updated\` today, \`status: Draft\`, \`repo\`, \`spec:\` linking the
4487
- specification.
4488
- - Tasks in order, as checkboxes. Each gives the files, what changes, and
4489
- the command that proves it (a test, a build, a run). The last tasks:
4490
- the documentation the work makes inaccurate, then the release when the
4491
- repository has one.
4492
- - Where the plan decides something the specification leaves open, a
4493
- section says so.
4494
- - Add \`{ id: \${CLAUDE_SESSION_ID}, agent: <model> }\` at the end of
4495
- \`sessions:\` in the front matter, unless that id is already there.
4496
- \`<model>\` is the model you run as, as Claude Code names it, after
4497
- \`Claude\`: for example \`Claude Opus 5.5\`.
4498
- - The rness hook checks every edit under \`${rness}/\`: fix what it reports.
4523
+ The first word is the verb, when a reference (\`0037\`, or a path) follows
4524
+ it \u2014 \`check\` alone means the plan \`In progress\`; a bare reference is
4525
+ \`open\`; any other words are the subject of a \`create\`:
4499
4526
 
4500
- After writing: give the path and the list of tasks. It starts \`Draft\`, and
4501
- becomes \`Ready\` once the developer agrees with it or asks you to carry it
4502
- out.
4527
+ - \`from NNNN\`: a plan from specification NNNN, which this approves.
4528
+ - \`create [subject]\`, or nothing: a plan from the conversation, for work
4529
+ too small for a specification; \`spec:\` is left empty.
4530
+ - \`open NNNN\`: reopen plan NNNN.
4531
+ - \`check NNNN\`: run the proofs of plan NNNN; close it when they all pass.
4503
4532
 
4504
- ${STATUS_RULE}
4533
+ \`from\`:
4505
4534
 
4506
- ${COMMIT_RULE}
4507
- `
4508
- },
4509
- {
4510
- name: "done",
4511
- description: "Close a piece of work of the rness workspace \u2014 check each task of its plan on evidence, then move the plan and its specification to their final status and correct the documents the work made inaccurate. Use when the developer says the work is done, or when the last task of a plan passes.",
4512
- hint: "[plan NNNN]",
4513
- tab: "plans",
4514
- body: (rness) => `You close a piece of work: its plan in \`${rness}/plans/\`, its specification, and
4515
- what they made inaccurate. The table above lists the plans, newest first.
4516
- Arguments: \`$ARGUMENTS\`.
4535
+ 1. Read the specification, \`${rness}/CONVENTIONS.md\`, and the most recent plan
4536
+ above: match its front matter and sections. A specification that is
4537
+ \`Implemented\`, \`Superseded\` or \`Rejected\` is refused: say its status,
4538
+ write nothing. A plan of it that is neither \`Completed\` nor \`Abandoned\`:
4539
+ reopen it rather than write a second.
4540
+ 2. The developer typed this command: that is their approval, and the
4541
+ specification moves from \`Draft\` or \`Proposed\` to \`Approved\`, \`updated\`
4542
+ today, in the same commit as the plan \u2014 no confirmation question. If
4543
+ you are proposing it yourself, propose it in a single line and wait for
4544
+ a yes first: the approval is theirs, never yours.
4545
+ 3. Read the code and the repositories the specification touches: every
4546
+ task names real files and real commands.
4547
+ 4. ${allocateStep(allocate, "plans")}
4548
+ 5. Fill it: \`spec:\` linking the specification; tasks in order, as
4549
+ checkboxes, each with the files, what changes, and the command that
4550
+ proves it (a test, a build, a run) \u2014 the last tasks the documentation
4551
+ the work makes inaccurate, then the release when the repository has
4552
+ one; where the plan decides something the specification leaves open, the
4553
+ section it opened with says so. Add ${SESSION_LINE}.
4554
+ 6. ${HOOK_LINE(rness)}
4555
+
4556
+ \`create\`: as \`from\`, without a specification. If the developer did not ask
4557
+ for a plan in so many words, propose one in a single line and wait for a
4558
+ yes. The body says what the work is and why it needs no specification.
4559
+
4560
+ \`open\`: read the plan and its specification; amend what the developer
4561
+ asks \u2014 a task added, split or reworded, a blocker recorded \u2014 \`updated\`
4562
+ today, the session line added; move \`Draft\` to \`Ready\` or \`Blocked\` as they
4563
+ say. A \`Completed\` or \`Abandoned\` plan is not reopened: offer a new plan of
4564
+ the same specification.
4565
+
4566
+ \`check\`:
4517
4567
 
4518
4568
  1. The plan: the one named, else the one \`In progress\`. Several or none:
4519
4569
  ask.
4520
- 2. The developer asked to close it, or its last task has just passed in
4521
- this session: close it. Otherwise propose it in a single line and wait
4522
- for a yes.
4570
+ 2. The developer asked for the check, or the plan's last task has just
4571
+ passed in this session: run it. Otherwise propose it in a single line
4572
+ and wait for a yes.
4523
4573
  3. Read the plan, its specification and \`${rness}/CONVENTIONS.md\`.
4524
- 4. Evidence first. For each task, what shows it done: a test, a build, a
4525
- command's output in this session. A task without evidence: run its
4526
- check now, or report it. Never mark a task done on belief.
4527
- 5. All tasks proven:
4574
+ 4. Evidence first. For each task, what proves it: its command run now, or
4575
+ its result read in this session. Never mark a task done on belief. A
4576
+ check has no cache: run twice, it proves twice.
4577
+ 5. Every task proven:
4528
4578
  - the plan: \`status: Completed\`, \`updated\` today, the checkboxes of the
4529
4579
  proven tasks ticked, and a section \`Verification (YYYY-MM-DD)\` \u2014 what
4530
4580
  ran, what it showed, what could not run and why;
4531
4581
  - the specification: \`Implemented\` once every plan of it is
4532
4582
  \`Completed\`; \`updated\` today;
4533
- - the plan and the specification: add
4534
- \`{ id: \${CLAUDE_SESSION_ID}, agent: <model> }\` at the end of
4535
- \`sessions:\` in their front matter, unless that id is already there;
4536
- \`<model>\` is the model you run as, as Claude Code names it, after
4537
- \`Claude\`: for example \`Claude Opus 5.5\`;
4583
+ - the plan and the specification: add ${SESSION_LINE};
4538
4584
  - the smallest set of documents the work makes inaccurate: \`${rness}/docs/\`,
4539
4585
  the READMEs of the repositories it changed.
4540
- 6. A task not proven: the plan stays \`In progress\` \u2014 or \`Blocked\`, the
4541
- blocker written in it. Say what remains.
4542
- 7. The rness hook checks every edit under \`${rness}/\`: fix what it reports.
4586
+ 6. A task not proven: nothing moves, the plan stays \`In progress\`. Say,
4587
+ task by task, what passed and what remains. A blocker the developer
4588
+ names is a move of its own: \`Blocked\`, the blocker written in the plan.
4589
+ 7. ${HOOK_LINE(rness)}
4590
+
4591
+ After writing: give the path and the list of tasks, or the statuses moved.
4592
+ A new plan starts \`Draft\`, and becomes \`Ready\` once the developer agrees
4593
+ with it or asks you to carry it out.
4543
4594
 
4544
- After: list the files changed and the statuses moved.
4595
+ ${STATUS_RULE}
4545
4596
 
4546
4597
  ${COMMIT_RULE}
4547
4598
  `
4548
4599
  }
4549
4600
  ];
4601
+ var RETIRED_SKILLS = ["done"];
4550
4602
  function lifecycleSkill(rness, s) {
4551
4603
  const command = `node "\${CLAUDE_PROJECT_DIR}/${rness}/${PINNED_BIN}" status ${s.tab} --cwd "\${CLAUDE_PROJECT_DIR}"`;
4604
+ const allocate = `node "${rness}/${PINNED_BIN}" doc new ${s.tab}`;
4552
4605
  return `---
4553
4606
  name: ${s.name}
4554
4607
  description: ${s.description}
4555
4608
  argument-hint: '${s.hint}'
4556
- allowed-tools: Bash(${command})
4609
+ allowed-tools: Bash(${command}), Bash(${allocate})
4557
4610
  ---
4558
4611
 
4559
4612
  !\`${command}\`
4560
4613
 
4561
- ${s.body(rness)}`;
4614
+ ${s.body(rness, allocate)}`;
4562
4615
  }
4563
4616
  function pluginFiles(rness, at, ctx) {
4564
4617
  return [
@@ -4572,6 +4625,11 @@ function pluginFiles(rness, at, ctx) {
4572
4625
  file: `${PLUGIN}/skills/${s.name}/SKILL.md`,
4573
4626
  at,
4574
4627
  content: lifecycleSkill(rness, s)
4628
+ })),
4629
+ ...RETIRED_SKILLS.map((name) => ({
4630
+ file: `${PLUGIN}/skills/${name}/SKILL.md`,
4631
+ at,
4632
+ retired: true
4575
4633
  }))
4576
4634
  ];
4577
4635
  }
@@ -5097,6 +5155,21 @@ async function agentTargets(root, manifest, opts) {
5097
5155
  for (const owned of await filesOf(root, manifest, declared(manifest))) {
5098
5156
  const { file, label } = owned;
5099
5157
  const existing = await readOrNull(file);
5158
+ if ("retired" in owned) {
5159
+ if (existing === null) continue;
5160
+ if (opts.check) {
5161
+ outcomes.push({
5162
+ label,
5163
+ status: "stale",
5164
+ detail: "no longer written by rness; remove it"
5165
+ });
5166
+ continue;
5167
+ }
5168
+ await rm4(file);
5169
+ await rmdir(dirname4(file)).catch(() => void 0);
5170
+ outcomes.push({ label, status: "removed", detail: null });
5171
+ continue;
5172
+ }
5100
5173
  if ("content" in owned) {
5101
5174
  const diff = wholeDiff(existing, owned.content);
5102
5175
  if (diff === null)
@@ -5147,6 +5220,7 @@ async function leftoverTargets(root, manifest) {
5147
5220
  );
5148
5221
  const leftovers = [];
5149
5222
  for (const owned of await filesOf(root, manifest, undeclared)) {
5223
+ if ("retired" in owned) continue;
5150
5224
  const existing = await readOrNull(owned.file);
5151
5225
  if (existing === null) continue;
5152
5226
  if ("content" in owned) {
@@ -5536,15 +5610,15 @@ async function syncCommand(opts, deps = {}) {
5536
5610
  if (!overSsh) ui.gitIsSilent = true;
5537
5611
  if (overSsh) {
5538
5612
  const interactive = opts.yes !== true && terminal.isTty();
5539
- const { access: access2, unattended } = await testGithubSsh(
5613
+ const { access: access3, unattended } = await testGithubSsh(
5540
5614
  transport,
5541
5615
  interactive,
5542
5616
  () => ui.line(...CHECKING)
5543
5617
  );
5544
5618
  if (unattended) ui.gitIsSilent = true;
5545
- if (!access2.ok) {
5619
+ if (!access3.ok) {
5546
5620
  process.stderr.write(
5547
- `${[...sshWorkspaceLines(access2.reason), ...INSTEAD_OF_LINES].join("\n")}
5621
+ `${[...sshWorkspaceLines(access3.reason), ...INSTEAD_OF_LINES].join("\n")}
5548
5622
  `
5549
5623
  );
5550
5624
  return 1;
@@ -5773,20 +5847,20 @@ async function addCommand(spec, opts, deps = {}) {
5773
5847
  if (host === void 0 && opts.https === true) host = transport.hosts.https;
5774
5848
  if (host === void 0 && FULL_URL2.test(spec)) host = transport.hosts.https;
5775
5849
  if (host === void 0) {
5776
- const { access: access2, unattended } = await testGithubSsh(
5850
+ const { access: access3, unattended } = await testGithubSsh(
5777
5851
  transport,
5778
5852
  interactive,
5779
5853
  () => ui.line(...CHECKING)
5780
5854
  );
5781
- if (unattended || !access2.ok) ui.gitIsSilent = true;
5855
+ if (unattended || !access3.ok) ui.gitIsSilent = true;
5782
5856
  const overSsh = Object.values(manifest.repos).some(
5783
5857
  (r) => isSshUrl(r.url, transport.hosts)
5784
5858
  );
5785
- if (access2.ok || !overSsh) {
5786
- ui.line("using", usingRest(access2), usingSentence(access2));
5787
- host = access2.ok ? transport.hosts.ssh : transport.hosts.https;
5859
+ if (access3.ok || !overSsh) {
5860
+ ui.line("using", usingRest(access3), usingSentence(access3));
5861
+ host = access3.ok ? transport.hosts.ssh : transport.hosts.https;
5788
5862
  } else {
5789
- const lines = sshWorkspaceLines(access2.reason);
5863
+ const lines = sshWorkspaceLines(access3.reason);
5790
5864
  if (!interactive) {
5791
5865
  process.stderr.write(
5792
5866
  `${[...lines, httpsFlagLine(spec)].join("\n")}
@@ -6000,9 +6074,23 @@ async function copyScaffold(dest, tokens) {
6000
6074
  }
6001
6075
 
6002
6076
  // src/commands/login.ts
6003
- import { spawn as spawn3 } from "child_process";
6004
6077
  import { realpath } from "fs/promises";
6005
6078
 
6079
+ // src/core/browser.ts
6080
+ import { spawn as spawn3 } from "child_process";
6081
+ function openInBrowser(url) {
6082
+ const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
6083
+ try {
6084
+ const child = spawn3(command, args, { stdio: "ignore", detached: true });
6085
+ child.on("error", () => void 0);
6086
+ child.unref();
6087
+ } catch {
6088
+ }
6089
+ }
6090
+ function canOpenBrowser() {
6091
+ return process.env["SSH_CONNECTION"] === void 0;
6092
+ }
6093
+
6006
6094
  // src/core/setup-git.ts
6007
6095
  import { execFile as execFile4 } from "child_process";
6008
6096
  import { promisify as promisify4 } from "util";
@@ -6047,15 +6135,6 @@ async function removeGitSetup() {
6047
6135
  }
6048
6136
 
6049
6137
  // src/commands/login.ts
6050
- function openInBrowser(url) {
6051
- const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
6052
- try {
6053
- const child = spawn3(command, args, { stdio: "ignore", detached: true });
6054
- child.on("error", () => void 0);
6055
- child.unref();
6056
- } catch {
6057
- }
6058
- }
6059
6138
  async function offerSetupGit(opts, terminal, bin, ui) {
6060
6139
  if (opts.setupGit === false) return;
6061
6140
  if (!isStableInstall(bin)) {
@@ -6132,7 +6211,7 @@ async function loginCommand(opts, deps = {}) {
6132
6211
  ui.line("open", code.verificationUri);
6133
6212
  ui.line("code", code.userCode);
6134
6213
  }
6135
- if (terminal.isTty() && process.env["SSH_CONNECTION"] === void 0)
6214
+ if (terminal.isTty() && canOpenBrowser())
6136
6215
  (deps.open ?? openInBrowser)(code.verificationUri);
6137
6216
  if (!ui.session)
6138
6217
  ui.line("waiting", "for you to approve rness on github.com\u2026");
@@ -6165,35 +6244,115 @@ async function loginCommand(opts, deps = {}) {
6165
6244
  return reportError(e);
6166
6245
  }
6167
6246
  }
6168
- function revokeUrl() {
6169
- return `https://github.com/settings/connections/applications/${clientId()}`;
6170
- }
6171
6247
 
6172
6248
  // src/commands/create.ts
6173
- import { mkdir as mkdir6, mkdtemp as mkdtemp2, rename as rename2, rm as rm6 } from "fs/promises";
6249
+ import { mkdir as mkdir6, mkdtemp as mkdtemp2, rename as rename2, rm as rm7 } from "fs/promises";
6174
6250
  import { tmpdir as tmpdir2 } from "os";
6175
6251
  import { join as join22, relative as relative4, resolve as resolve5 } from "path";
6176
6252
 
6253
+ // src/core/approval.ts
6254
+ var APPROVAL_INTERVAL_MS = 5e3;
6255
+ var APPROVAL_WAIT_MS = 10 * 6e4;
6256
+ function approvalQuestion(org) {
6257
+ return `Open github.com to approve rness for ${org} now?`;
6258
+ }
6259
+ async function checkAccess(input) {
6260
+ const { org, provider, ui } = input;
6261
+ if (!provider.authenticated) return "unknown";
6262
+ const login = (await provider.identity())?.login;
6263
+ const as = login === void 0 ? "" : ` (as ${login})`;
6264
+ 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") {
6267
+ member();
6268
+ return access3;
6269
+ }
6270
+ if (access3 !== "restricted") return access3;
6271
+ const url = provider.approvalUrl();
6272
+ ui.warn(
6273
+ `${org} has not approved rness, so its private repositories are hidden`
6274
+ );
6275
+ if (!input.interactive || url === null) {
6276
+ ui.hint(
6277
+ url === null ? `an owner of ${org} has to approve the app your token belongs to` : `an owner approves it at ${url}`,
6278
+ "stderr"
6279
+ );
6280
+ return "restricted";
6281
+ }
6282
+ const p = await input.prompts();
6283
+ const ok = await p.confirm({
6284
+ message: approvalQuestion(org),
6285
+ initialValue: true
6286
+ });
6287
+ if (p.isCancel(ok)) return "cancelled";
6288
+ if (ok !== true) {
6289
+ ui.hint(`approve it later: ${url}`, "stderr");
6290
+ return "restricted";
6291
+ }
6292
+ ui.line("open", url, `Open ${url} and approve rness for ${org}`);
6293
+ if (canOpenBrowser()) (input.deps?.open ?? openInBrowser)(url);
6294
+ const sleep = input.deps?.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
6295
+ const now = input.deps?.now ?? Date.now;
6296
+ const who = input.canGrant === true ? org : `an owner of ${org}`;
6297
+ const waiting = `for ${who} to approve rness on github.com`;
6298
+ 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
+ );
6316
+ if (outcome === "member") {
6317
+ if (!ui.session) member();
6318
+ return outcome;
6319
+ }
6320
+ if (outcome === "not-member") return outcome;
6321
+ ui.warn(
6322
+ `${org} still has not approved rness; its private repositories stay hidden this time`
6323
+ );
6324
+ ui.hint(
6325
+ `once an owner approves it, add its private repositories with ${rnessCommand()} add <repo>`,
6326
+ "stderr"
6327
+ );
6328
+ return "restricted";
6329
+ }
6330
+
6177
6331
  // src/core/ask-provider.ts
6178
6332
  var PROVIDER_QUESTION = "Where does your organization live?";
6333
+ var BLANK = "blank";
6334
+ var BLANK_LABEL = "No organization yet: a blank local workspace";
6179
6335
  async function askProvider(p) {
6180
6336
  return p.select({
6181
6337
  message: PROVIDER_QUESTION,
6182
- options: Object.values(PROVIDERS).map((entry) => ({
6183
- value: entry.name,
6184
- label: entry.available ? entry.label : `${entry.label} (coming later)`,
6185
- disabled: !entry.available
6186
- }))
6338
+ options: [
6339
+ ...Object.values(PROVIDERS).map((entry) => ({
6340
+ value: entry.name,
6341
+ label: entry.available ? entry.label : `${entry.label} (coming later)`,
6342
+ disabled: !entry.available
6343
+ })),
6344
+ { value: BLANK, label: BLANK_LABEL, disabled: false }
6345
+ ]
6187
6346
  });
6188
6347
  }
6189
6348
 
6190
6349
  // src/core/new-workspace.ts
6191
- import { mkdir as mkdir4, readdir as readdir4, rm as rm5, stat as stat3 } from "fs/promises";
6350
+ import { mkdir as mkdir4, readdir as readdir4, rm as rm6, stat as stat3 } from "fs/promises";
6192
6351
  import { dirname as dirname8, join as join20, relative as relative2 } from "path";
6193
6352
 
6194
6353
  // src/core/scaffold-git.ts
6195
6354
  import { execFile as execFile5 } from "child_process";
6196
- import { mkdtemp, realpath as realpath2, rm as rm4, writeFile as writeFile3 } from "fs/promises";
6355
+ import { mkdtemp, realpath as realpath2, rm as rm5, writeFile as writeFile3 } from "fs/promises";
6197
6356
  import { tmpdir } from "os";
6198
6357
  import { dirname as dirname7, join as join19, resolve as resolve3 } from "path";
6199
6358
  import { promisify as promisify5 } from "util";
@@ -6321,7 +6480,7 @@ async function buildScaffoldCommit(rnessDir, base, files, version) {
6321
6480
  scaffoldMessage(`chore: rness scaffold ${version}`, version)
6322
6481
  ]);
6323
6482
  } finally {
6324
- await rm4(scratch, { recursive: true, force: true });
6483
+ await rm5(scratch, { recursive: true, force: true });
6325
6484
  }
6326
6485
  }
6327
6486
  async function blobAt(rnessDir, rev, path) {
@@ -6445,9 +6604,9 @@ async function buildContext(input) {
6445
6604
  return true;
6446
6605
  } catch (e) {
6447
6606
  reportError(e);
6448
- await rm5(rnessDir, { recursive: true, force: true }).catch(() => void 0);
6607
+ await rm6(rnessDir, { recursive: true, force: true }).catch(() => void 0);
6449
6608
  if (await isEmptyDir(root).catch(() => false))
6450
- await rm5(root, { recursive: true, force: true }).catch(() => void 0);
6609
+ await rm6(root, { recursive: true, force: true }).catch(() => void 0);
6451
6610
  process.stderr.write(
6452
6611
  `removed ${shown}/.rness after the failure; fix it and retry
6453
6612
  `
@@ -6630,22 +6789,6 @@ function cancelled(ui) {
6630
6789
  }
6631
6790
  async function pickRepositories(input) {
6632
6791
  const { org, prompts, provider, ui } = input;
6633
- if (provider.authenticated) {
6634
- const access2 = await provider.organizationAccess(org);
6635
- const login = (await provider.identity())?.login;
6636
- if (access2 === "member")
6637
- ui.line(
6638
- "member",
6639
- `${org}${login === void 0 ? "" : ` (as ${login})`}`,
6640
- `You are a member of ${org}${login === void 0 ? "" : ` (as ${login})`}`
6641
- );
6642
- else if (access2 === "restricted") {
6643
- ui.warn(
6644
- `${org} restricts OAuth apps, so its private repositories are hidden from rness`
6645
- );
6646
- ui.hint(`ask an owner to approve it: ${revokeUrl()}`, "stderr");
6647
- }
6648
- }
6649
6792
  let listed = [];
6650
6793
  let failed = false;
6651
6794
  if (!ui.session) ui.line("listing", `${org} repositories\u2026`);
@@ -6795,17 +6938,6 @@ function blankConflict(opts) {
6795
6938
  if (opts.provider !== void 0) return "--provider";
6796
6939
  return null;
6797
6940
  }
6798
- async function chooseStart(prompts) {
6799
- const answer = await prompts.select({
6800
- message: "How do you want to start?",
6801
- options: [
6802
- { value: "github", label: "From a GitHub organization" },
6803
- { value: "blank", label: "Blank local workspace" }
6804
- ]
6805
- });
6806
- if (prompts.isCancel(answer)) return null;
6807
- return answer === "blank" ? "blank" : "github";
6808
- }
6809
6941
  function providerFlagError(value) {
6810
6942
  if (!isProviderName(value))
6811
6943
  return `unknown provider "${value}" (${Object.keys(PROVIDERS).join(", ")})`;
@@ -6814,27 +6946,37 @@ function providerFlagError(value) {
6814
6946
  return `provider "${value}" is not available yet (available: ${available.join(", ")})`;
6815
6947
  }
6816
6948
  var OTHER = /* @__PURE__ */ Symbol("another organization");
6949
+ var OTHER_LABEL = "an organization not listed here\u2026";
6950
+ var OTHER_HINT = "not a member, or it has not approved rness yet";
6951
+ var NOT_APPROVED = "rness not approved";
6817
6952
  async function chooseOrganization(provider, prompts) {
6818
6953
  if (!provider.authenticated) return OTHER;
6819
- let names;
6954
+ let organizations;
6955
+ let self;
6820
6956
  try {
6821
- const self = (await provider.identity())?.login;
6822
- names = (await provider.listOrganizations()).map((o) => o.login);
6823
- if (self !== void 0) names.push(self);
6957
+ self = (await provider.identity())?.login;
6958
+ organizations = await provider.listOrganizations();
6824
6959
  } catch {
6825
6960
  return OTHER;
6826
6961
  }
6827
- if (names.length === 0) return OTHER;
6962
+ if (organizations.length === 0 && self === void 0) return OTHER;
6963
+ const padlock = unicode();
6964
+ const options = organizations.map((o) => ({
6965
+ value: o.login,
6966
+ label: !o.approved && padlock ? `${o.login} ${PRIVATE_MARK}` : o.login,
6967
+ ...o.approved ? {} : { hint: NOT_APPROVED }
6968
+ }));
6969
+ if (self !== void 0)
6970
+ options.push({ value: self, label: self, hint: "your account" });
6971
+ options.push({ value: "", label: OTHER_LABEL, hint: OTHER_HINT });
6828
6972
  const answer = await prompts.select({
6829
6973
  message: "Which GitHub organization?",
6830
- options: [
6831
- ...names.map((name) => ({ value: name, label: name })),
6832
- { value: "", label: "another one\u2026" }
6833
- ]
6974
+ options
6834
6975
  });
6835
6976
  if (prompts.isCancel(answer)) return null;
6836
6977
  if (typeof answer !== "string" || answer === "") return OTHER;
6837
- return answer;
6978
+ const picked = organizations.find((o) => o.login === answer);
6979
+ return { org: answer, canGrant: picked?.canGrant ?? null };
6838
6980
  }
6839
6981
  async function publishContext(input) {
6840
6982
  const { ui, prompts, provider, org } = input;
@@ -6873,6 +7015,11 @@ async function publishContext(input) {
6873
7015
  async function createCommand(opts, deps = {}) {
6874
7016
  const terminal = deps.terminal ?? defaultTerminal;
6875
7017
  const transport = deps.transport ?? defaultTransport;
7018
+ const clock = {
7019
+ ...deps.open === void 0 ? {} : { open: deps.open },
7020
+ ...deps.sleep === void 0 ? {} : { sleep: deps.sleep },
7021
+ ...deps.now === void 0 ? {} : { now: deps.now }
7022
+ };
6876
7023
  let provider = deps.provider;
6877
7024
  const getProvider = async () => provider ??= await githubProvider(opts.githubApi);
6878
7025
  const cwd = opts.cwd ?? process.cwd();
@@ -6947,12 +7094,12 @@ async function createCommand(opts, deps = {}) {
6947
7094
  if (opts.host !== void 0) return opts.host;
6948
7095
  if (opts.ssh === true) return transport.hosts.ssh;
6949
7096
  if (opts.https === true) return transport.hosts.https;
6950
- const access2 = await testSsh();
6951
- const github = access2.ok ? null : await getProvider();
7097
+ const access3 = await testSsh();
7098
+ const github = access3.ok ? null : await getProvider();
6952
7099
  const login = github?.authenticated ? (await github.identity())?.login : void 0;
6953
- ui.line("using", usingRest(access2, login), usingSentence(access2, login));
6954
- if (!access2.ok) ui.gitIsSilent = true;
6955
- return access2.ok ? transport.hosts.ssh : transport.hosts.https;
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;
6956
7103
  };
6957
7104
  let loaded;
6958
7105
  const prompts = async () => loaded ??= await terminal.prompts();
@@ -6967,10 +7114,14 @@ async function createCommand(opts, deps = {}) {
6967
7114
  return 1;
6968
7115
  }
6969
7116
  let blank = opts.blank === true;
6970
- if (!blank && interactive && opts.org === void 0 && opts.repos === void 0) {
6971
- const start = await chooseStart(await prompts());
6972
- if (start === null) return cancelled(ui);
6973
- blank = start === "blank";
7117
+ let chosenProvider = "github";
7118
+ if (opts.provider !== void 0 && isProviderName(opts.provider))
7119
+ chosenProvider = opts.provider;
7120
+ else if (!blank && interactive && opts.org === void 0 && opts.repos === void 0) {
7121
+ const answer = await askProvider(await prompts());
7122
+ if (typeof answer !== "string") return cancelled(ui);
7123
+ if (answer === BLANK) blank = true;
7124
+ else chosenProvider = answer;
6974
7125
  }
6975
7126
  if (blank)
6976
7127
  return await createBlank({
@@ -6984,14 +7135,6 @@ async function createCommand(opts, deps = {}) {
6984
7135
  ui,
6985
7136
  prompts
6986
7137
  });
6987
- let chosenProvider = "github";
6988
- if (opts.provider !== void 0 && isProviderName(opts.provider))
6989
- chosenProvider = opts.provider;
6990
- else if (interactive) {
6991
- const answer = await askProvider(await prompts());
6992
- if (typeof answer !== "string") return cancelled(ui);
6993
- chosenProvider = answer;
6994
- }
6995
7138
  let prompted = false;
6996
7139
  let org = opts.org;
6997
7140
  if (org === void 0 && !interactive) {
@@ -7001,19 +7144,20 @@ async function createCommand(opts, deps = {}) {
7001
7144
  if (interactive && opts.repos === void 0 && !(await getProvider()).authenticated) {
7002
7145
  const p = await prompts();
7003
7146
  const login = await p.confirm({
7004
- message: "Log in to GitHub to list private repositories?",
7147
+ message: "Log in to GitHub to list your organizations and private repositories?",
7005
7148
  initialValue: true
7006
7149
  });
7007
7150
  if (p.isCancel(login)) return cancelled(ui);
7008
7151
  if (login === true) {
7009
7152
  const code2 = await loginCommand(
7010
7153
  opts.githubApi === void 0 ? {} : { githubApi: opts.githubApi },
7011
- { terminal, ui }
7154
+ { terminal, ui, ...clock }
7012
7155
  );
7013
7156
  if (code2 !== 0) return code2;
7014
7157
  provider = await githubProvider(opts.githubApi);
7015
7158
  }
7016
7159
  }
7160
+ let canGrant = null;
7017
7161
  if (org === void 0) {
7018
7162
  const chosen = await chooseOrganization(
7019
7163
  await getProvider(),
@@ -7021,7 +7165,8 @@ async function createCommand(opts, deps = {}) {
7021
7165
  );
7022
7166
  if (chosen === null) return cancelled(ui);
7023
7167
  if (chosen !== OTHER) {
7024
- org = chosen;
7168
+ org = chosen.org;
7169
+ canGrant = chosen.canGrant;
7025
7170
  prompted = true;
7026
7171
  }
7027
7172
  }
@@ -7053,6 +7198,16 @@ async function createCommand(opts, deps = {}) {
7053
7198
  );
7054
7199
  return 2;
7055
7200
  }
7201
+ const outcome = await checkAccess({
7202
+ org,
7203
+ provider: await getProvider(),
7204
+ ui,
7205
+ interactive,
7206
+ prompts,
7207
+ canGrant,
7208
+ deps: clock
7209
+ });
7210
+ if (outcome === "cancelled") return cancelled(ui);
7056
7211
  const host = await chooseHost();
7057
7212
  const contextUrl = repoUrl(host, org, ".rness");
7058
7213
  let gitProvider = await getProvider();
@@ -7118,10 +7273,10 @@ async function createCommand(opts, deps = {}) {
7118
7273
  (r) => isSshUrl(r.url, transport.hosts)
7119
7274
  );
7120
7275
  if (overSsh && opts.ssh !== true && opts.host === void 0) {
7121
- const access2 = await testSsh();
7122
- if (!access2.ok) {
7276
+ const access3 = await testSsh();
7277
+ if (!access3.ok) {
7123
7278
  process.stderr.write(
7124
- `${[...sshWorkspaceLines(access2.reason), ...INSTEAD_OF_LINES].join("\n")}
7279
+ `${[...sshWorkspaceLines(access3.reason), ...INSTEAD_OF_LINES].join("\n")}
7125
7280
  `
7126
7281
  );
7127
7282
  return 1;
@@ -7292,7 +7447,7 @@ ${next2.map((l) => ` ${l}`).join("\n")}
7292
7447
  return reportError(e);
7293
7448
  } finally {
7294
7449
  if (staging !== void 0)
7295
- await rm6(staging, { recursive: true, force: true }).catch(() => void 0);
7450
+ await rm7(staging, { recursive: true, force: true }).catch(() => void 0);
7296
7451
  }
7297
7452
  }
7298
7453
 
@@ -7397,7 +7552,7 @@ async function logoutCommand() {
7397
7552
  `${status("updated", "git: rness no longer answers for https://github.com")}
7398
7553
  `
7399
7554
  );
7400
- process.stdout.write(`revoke rness on GitHub: ${revokeUrl()}
7555
+ process.stdout.write(`revoke rness on GitHub: ${approvalUrl()}
7401
7556
  `);
7402
7557
  return 0;
7403
7558
  } catch (e) {
@@ -7407,7 +7562,7 @@ async function logoutCommand() {
7407
7562
 
7408
7563
  // src/commands/upgrade.ts
7409
7564
  import { execFile as execFile6 } from "child_process";
7410
- import { mkdtemp as mkdtemp3, readFile as readFile10, rm as rm7 } from "fs/promises";
7565
+ import { mkdtemp as mkdtemp3, readFile as readFile10, rm as rm8 } from "fs/promises";
7411
7566
  import { tmpdir as tmpdir3 } from "os";
7412
7567
  import { basename as basename5, join as join24, relative as relative5 } from "path";
7413
7568
  import { promisify as promisify6 } from "util";
@@ -7433,7 +7588,7 @@ async function scaffoldFor(version, rnessDir) {
7433
7588
  if (installed?.version === version && inside2 !== null && await exists(join24(inside2, "package.json")))
7434
7589
  return { dir: inside2 };
7435
7590
  const scratch = await mkdtemp3(join24(tmpdir3(), "rness-pack-"));
7436
- const cleanup = () => rm7(scratch, { recursive: true, force: true });
7591
+ const cleanup = () => rm8(scratch, { recursive: true, force: true });
7437
7592
  try {
7438
7593
  const { stdout } = await execFileP6(
7439
7594
  "npm",
@@ -7852,6 +8007,101 @@ async function validateCommand(opts, deps = {}) {
7852
8007
  // src/cli.ts
7853
8008
  import { Command, CommanderError, Option } from "commander";
7854
8009
 
8010
+ // src/commands/doc.ts
8011
+ import { access as access2, mkdir as mkdir7, readFile as readFile11, writeFile as writeFile4 } from "fs/promises";
8012
+ import { join as join25 } from "path";
8013
+ var NUMBERED = Object.keys(STATUSES);
8014
+ var SECTIONS = {
8015
+ specs: [
8016
+ "Summary",
8017
+ "Scope",
8018
+ "Out of scope",
8019
+ "Alternatives considered",
8020
+ "To verify",
8021
+ "Tests"
8022
+ ],
8023
+ plans: ["Decided here, where the specification leaves it open", "Tasks"]
8024
+ };
8025
+ function slugOf(title) {
8026
+ const slug = title.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60).replace(/-+$/g, "");
8027
+ return slug === "" ? "untitled" : slug;
8028
+ }
8029
+ function today() {
8030
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
8031
+ }
8032
+ function headingsOf(template) {
8033
+ return [...template.matchAll(/^## (.+)$/gm)].map((m) => (m[1] ?? "").trim());
8034
+ }
8035
+ async function adrSections(rnessDir) {
8036
+ const own = join25(rnessDir, "adr", "0000-template.md");
8037
+ const shipped = join25(scaffoldDir(), "adr", "0000-template.md");
8038
+ for (const file of [own, shipped]) {
8039
+ try {
8040
+ const sections = headingsOf(await readFile11(file, "utf8"));
8041
+ if (sections.length > 0) return sections;
8042
+ } catch {
8043
+ }
8044
+ }
8045
+ return ["Context", "Decision", "Alternatives Considered", "Consequences"];
8046
+ }
8047
+ async function docNewCommand(collection, opts) {
8048
+ if (!NUMBERED.includes(collection)) {
8049
+ process.stderr.write(
8050
+ `${collection} is not a numbered collection of this workspace (${NUMBERED.join(", ")})
8051
+ `
8052
+ );
8053
+ return 2;
8054
+ }
8055
+ const name = collection;
8056
+ try {
8057
+ const ws = await findWorkspace(opts.cwd ?? process.cwd());
8058
+ const dir = join25(ws.rnessDir, name);
8059
+ await mkdir7(dir, { recursive: true });
8060
+ let highest = 0;
8061
+ for (const item of await collectMarkdown(dir)) {
8062
+ const number2 = documentNumber(item.rel);
8063
+ if (number2 !== null) highest = Math.max(highest, Number(number2));
8064
+ }
8065
+ const number = String(highest + 1).padStart(4, "0");
8066
+ const title = opts.title?.trim() || "Untitled";
8067
+ const file = join25(dir, `${number}-${slugOf(title)}.md`);
8068
+ const status2 = STATUSES[name]?.[0] ?? "Draft";
8069
+ const date = today();
8070
+ const front = [
8071
+ "---",
8072
+ `date: ${date}`,
8073
+ `status: ${status2}`,
8074
+ "repo: ''",
8075
+ // An ADR is never updated once accepted: no `updated` (CONVENTIONS.md).
8076
+ ...name === "adr" ? [] : [`updated: ${date}`],
8077
+ "---"
8078
+ ];
8079
+ const sections = name === "adr" ? await adrSections(ws.rnessDir) : SECTIONS[name] ?? [];
8080
+ const body = [
8081
+ `# ${number} \u2014 ${title}`,
8082
+ // The rule under the title, as the ADR and specification templates draw it.
8083
+ ...name === "plans" ? [] : ["", "---"],
8084
+ ...sections.flatMap((s) => ["", `## ${s}`])
8085
+ ];
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}
8098
+ `);
8099
+ return 0;
8100
+ } catch (e) {
8101
+ return reportError(e);
8102
+ }
8103
+ }
8104
+
7855
8105
  // src/commands/git-credential.ts
7856
8106
  async function readStdin() {
7857
8107
  const chunks = [];
@@ -7880,8 +8130,8 @@ import { realpath as realpath4 } from "fs/promises";
7880
8130
  import { isAbsolute as isAbsolute2, posix as posix8, relative as relative7, resolve as resolve7, sep as sep5 } from "path";
7881
8131
 
7882
8132
  // src/mcp/tools.ts
7883
- import { readFile as readFile11, realpath as realpath3, stat as stat4 } from "fs/promises";
7884
- import { isAbsolute, join as join25, posix as posix7, relative as relative6, resolve as resolve6, sep as sep4 } from "path";
8133
+ import { readFile as readFile12, realpath as realpath3, stat as stat4 } from "fs/promises";
8134
+ import { isAbsolute, join as join26, posix as posix7, relative as relative6, resolve as resolve6, sep as sep4 } from "path";
7885
8135
  var LABELS = {
7886
8136
  standards: "Standards",
7887
8137
  adr: "Decisions",
@@ -7910,7 +8160,7 @@ async function documents(rnessDir, only) {
7910
8160
  const docs = [];
7911
8161
  for (const collection of COLLECTIONS) {
7912
8162
  if (only !== void 0 && collection !== only) continue;
7913
- for (const item of await collectMarkdown(join25(rnessDir, collection))) {
8163
+ for (const item of await collectMarkdown(join26(rnessDir, collection))) {
7914
8164
  const path = `${collection}/${item.rel}`;
7915
8165
  if (path === TEMPLATE2) continue;
7916
8166
  const status2 = item.fields?.["status"];
@@ -8032,7 +8282,7 @@ async function read(cwd, args) {
8032
8282
  return refuse(
8033
8283
  `${path}: ${info.size} bytes, over the 256 KiB rness_read returns`
8034
8284
  );
8035
- return { text: await readFile11(real, "utf8") };
8285
+ return { text: await readFile12(real, "utf8") };
8036
8286
  }
8037
8287
  async function search(cwd, args) {
8038
8288
  const { ws } = await workspaceAt(cwd);
@@ -8140,8 +8390,8 @@ function mcpTools(cwd) {
8140
8390
 
8141
8391
  // src/pulse/detached.ts
8142
8392
  import { spawn as nodeSpawn } from "child_process";
8143
- import { mkdir as mkdir7, open as open2, readFile as readFile12, rm as rm8, stat as stat5, writeFile as writeFile4 } from "fs/promises";
8144
- import { join as join26 } from "path";
8393
+ import { mkdir as mkdir8, open as open2, readFile as readFile13, rm as rm9, stat as stat5, writeFile as writeFile5 } from "fs/promises";
8394
+ import { join as join27 } from "path";
8145
8395
  import { setTimeout as delay2 } from "timers/promises";
8146
8396
  var detached = (args, cwd) => {
8147
8397
  const bin = process.argv[1];
@@ -8155,11 +8405,11 @@ var detached = (args, cwd) => {
8155
8405
  child.on("error", () => void 0);
8156
8406
  child.unref();
8157
8407
  };
8158
- var failureFile = () => join26(configDir(), "pulse.json");
8408
+ var failureFile = () => join27(configDir(), "pulse.json");
8159
8409
  async function recordFailure(reason3) {
8160
8410
  try {
8161
- await mkdir7(configDir(), { recursive: true, mode: 448 });
8162
- await writeFile4(
8411
+ await mkdir8(configDir(), { recursive: true, mode: 448 });
8412
+ await writeFile5(
8163
8413
  failureFile(),
8164
8414
  `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), reason: reason3 })}
8165
8415
  `
@@ -8169,8 +8419,8 @@ async function recordFailure(reason3) {
8169
8419
  }
8170
8420
  async function takeFailure() {
8171
8421
  try {
8172
- const data = JSON.parse(await readFile12(failureFile(), "utf8"));
8173
- await rm8(failureFile(), { force: true });
8422
+ const data = JSON.parse(await readFile13(failureFile(), "utf8"));
8423
+ await rm9(failureFile(), { force: true });
8174
8424
  const reason3 = data !== null && typeof data === "object" ? data["reason"] : void 0;
8175
8425
  return typeof reason3 === "string" && reason3 !== "" ? reason3 : null;
8176
8426
  } catch {
@@ -8179,8 +8429,8 @@ async function takeFailure() {
8179
8429
  }
8180
8430
  var PULSE_LOCK_STALE_MS = 15 * 6e4;
8181
8431
  async function oneSyncAtATime(key, work, options = {}) {
8182
- await mkdir7(configDir(), { recursive: true, mode: 448 });
8183
- const file = join26(configDir(), `pulse-${key}.lock`);
8432
+ await mkdir8(configDir(), { recursive: true, mode: 448 });
8433
+ const file = join27(configDir(), `pulse-${key}.lock`);
8184
8434
  for (; ; ) {
8185
8435
  try {
8186
8436
  const handle = await open2(file, "wx");
@@ -8192,7 +8442,7 @@ async function oneSyncAtATime(key, work, options = {}) {
8192
8442
  (s) => Date.now() - s.mtimeMs,
8193
8443
  () => 0
8194
8444
  );
8195
- if (age > PULSE_LOCK_STALE_MS) await rm8(file, { force: true });
8445
+ if (age > PULSE_LOCK_STALE_MS) await rm9(file, { force: true });
8196
8446
  else if (options.wait !== true) return false;
8197
8447
  else await (options.sleep ?? ((ms) => delay2(ms)))(1e3);
8198
8448
  }
@@ -8201,7 +8451,7 @@ async function oneSyncAtATime(key, work, options = {}) {
8201
8451
  await work();
8202
8452
  return true;
8203
8453
  } finally {
8204
- await rm8(file, { force: true });
8454
+ await rm9(file, { force: true });
8205
8455
  }
8206
8456
  }
8207
8457
 
@@ -8576,12 +8826,12 @@ async function mcpCommand(opts, io = {
8576
8826
  }
8577
8827
 
8578
8828
  // src/commands/pulse.ts
8579
- import { readFile as readFile14, readdir as readdir5 } from "fs/promises";
8580
- import { join as join27 } from "path";
8829
+ import { readFile as readFile15, readdir as readdir5 } from "fs/promises";
8830
+ import { join as join28 } from "path";
8581
8831
  import { setTimeout as delay3 } from "timers/promises";
8582
8832
 
8583
8833
  // src/pulse/opened.ts
8584
- import { mkdir as mkdir8, readFile as readFile13, rename as rename3, rm as rm9, writeFile as writeFile5 } from "fs/promises";
8834
+ import { mkdir as mkdir9, readFile as readFile14, rename as rename3, rm as rm10, writeFile as writeFile6 } from "fs/promises";
8585
8835
  import { dirname as dirname9 } from "path";
8586
8836
  var NAME2 = "rness/opened";
8587
8837
  function openedFile(rnessDir) {
@@ -8593,7 +8843,7 @@ async function readOpened(file) {
8593
8843
  if (file === null) return opened;
8594
8844
  let data;
8595
8845
  try {
8596
- data = JSON.parse(await readFile13(file, "utf8"));
8846
+ data = JSON.parse(await readFile14(file, "utf8"));
8597
8847
  } catch {
8598
8848
  return opened;
8599
8849
  }
@@ -8608,13 +8858,13 @@ async function writeOpened(file, opened) {
8608
8858
  if (file === null) return;
8609
8859
  try {
8610
8860
  if (opened.size === 0) {
8611
- await rm9(file, { force: true });
8861
+ await rm10(file, { force: true });
8612
8862
  return;
8613
8863
  }
8614
8864
  const entries = [...opened].sort(([a], [b]) => a - b);
8615
- await mkdir8(dirname9(file), { recursive: true });
8865
+ await mkdir9(dirname9(file), { recursive: true });
8616
8866
  const temporary = `${file}.${process.pid}`;
8617
- await writeFile5(
8867
+ await writeFile6(
8618
8868
  temporary,
8619
8869
  `${JSON.stringify(Object.fromEntries(entries), null, 2)}
8620
8870
  `
@@ -8708,7 +8958,7 @@ async function scopeOf(c, name, tabs, owners) {
8708
8958
  label: labelOf(name),
8709
8959
  rows: []
8710
8960
  };
8711
- const dir = join27(c.rnessDir, name);
8961
+ const dir = join28(c.rnessDir, name);
8712
8962
  const shape = await readShape(c.rnessDir, name);
8713
8963
  const fronts = new Map(
8714
8964
  (await collectMarkdown(dir)).map((item) => [
@@ -8754,7 +9004,7 @@ async function working(ui, doing, count3, work) {
8754
9004
  async function filesOf2(rnessDir) {
8755
9005
  const files = /* @__PURE__ */ new Set();
8756
9006
  const walk = async (rel) => {
8757
- const dir = rel === "" ? rnessDir : join27(rnessDir, ...rel.split("/"));
9007
+ const dir = rel === "" ? rnessDir : join28(rnessDir, ...rel.split("/"));
8758
9008
  for (const e of await readdir5(dir, { withFileTypes: true })) {
8759
9009
  const path = rel === "" ? e.name : `${rel}/${e.name}`;
8760
9010
  if (e.isDirectory()) {
@@ -8866,7 +9116,7 @@ async function syncBoard(c, board, ui, sayLayout, scope) {
8866
9116
  w.path,
8867
9117
  issueBody({
8868
9118
  path: w.path,
8869
- text: await readFile14(join27(c.rnessDir, ...w.path.split("/")), "utf8"),
9119
+ text: await readFile15(join28(c.rnessDir, ...w.path.split("/")), "utf8"),
8870
9120
  org: c.org,
8871
9121
  numbers,
8872
9122
  files
@@ -9358,6 +9608,14 @@ function buildProgram(state) {
9358
9608
  ).argument("[tab]", "open on this tab: adr, specs, plans, \u2026").addOption(new Option("--cwd <dir>").hideHelp()).action(async (tab, opts) => {
9359
9609
  state.code = await statusCommand(tab, opts);
9360
9610
  });
9611
+ const doc = program.command("doc").description("Documents of the workspace: doc new <collection>");
9612
+ doc.command("new").description(
9613
+ "Write the next numbered document of a collection (adr, specs, plans) and print its path"
9614
+ ).argument("<collection>", "adr, specs or plans").option("--title <text>", "the title; the file name takes its slug").addOption(new Option("--cwd <dir>").hideHelp()).action(
9615
+ async (collection, opts) => {
9616
+ state.code = await docNewCommand(collection, opts);
9617
+ }
9618
+ );
9361
9619
  program.command("validate").description(
9362
9620
  "Check the .rness/ tree against the contract and every generated block"
9363
9621
  ).addOption(new Option("--cwd <dir>").hideHelp()).action(async (opts) => {
@@ -9618,4 +9876,4 @@ export {
9618
9876
  OWN_COMMANDS,
9619
9877
  catchUp
9620
9878
  };
9621
- //# sourceMappingURL=chunk-MMPMXYZU.js.map
9879
+ //# sourceMappingURL=chunk-VGBS7HDY.js.map