@rness/cli 0.13.0 → 0.15.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.
@@ -486,7 +486,7 @@ function parseRepoSpec(spec, org, host) {
486
486
  }
487
487
 
488
488
  // src/version.ts
489
- var VERSION = "0.13.0";
489
+ var VERSION = "0.15.0";
490
490
 
491
491
  // src/core/pm.ts
492
492
  import { execFile as execFile2 } from "child_process";
@@ -523,6 +523,12 @@ function rnessRunner(pm, version) {
523
523
  return "yarn dlx @rness/cli";
524
524
  return "npx @rness/cli";
525
525
  }
526
+ function localRunner(pm) {
527
+ if (pm === "pnpm") return "pnpm";
528
+ if (pm === "yarn") return "yarn";
529
+ if (pm === "bun") return "bunx";
530
+ return "npx";
531
+ }
526
532
  function rnessCommand(env = process.env) {
527
533
  const agent = env["npm_config_user_agent"] ?? "";
528
534
  if (agent === "") return "rness";
@@ -1064,15 +1070,25 @@ function statusOf(item) {
1064
1070
  const status2 = item.fields?.["status"];
1065
1071
  return typeof status2 === "string" && status2 !== "" ? status2 : null;
1066
1072
  }
1073
+ function sessionsOf(item) {
1074
+ const listed = item.fields?.["sessions"];
1075
+ if (!Array.isArray(listed)) return [];
1076
+ const ids = listed.filter(
1077
+ (s) => typeof s === "string" && s !== ""
1078
+ );
1079
+ return [...new Set(ids)];
1080
+ }
1067
1081
  function rowOf(dir, item) {
1068
1082
  const base = posix3.basename(item.rel, ".md");
1069
1083
  const date = /^(\d{4}-\d{2}-\d{2})-/.exec(base)?.[1] ?? null;
1070
1084
  const number = date === null ? documentNumber(item.rel) : null;
1085
+ const sessions = sessionsOf(item);
1071
1086
  return {
1072
1087
  id: date ?? number ?? base,
1073
1088
  title: documentTitle(item.title ?? base, number),
1074
1089
  status: statusOf(item),
1075
- path: `${dir}/${item.rel}`
1090
+ path: `${dir}/${item.rel}`,
1091
+ ...sessions.length > 0 ? { sessions } : {}
1076
1092
  };
1077
1093
  }
1078
1094
  function newestFirst(a, b) {
@@ -1393,7 +1409,8 @@ function desiredOf(tabs, org) {
1393
1409
  body: headerOf(row.path, org),
1394
1410
  status: statusOf2(row.status),
1395
1411
  type: tab.label,
1396
- statusField: fields2.get(tab.label)?.name ?? null
1412
+ statusField: fields2.get(tab.label)?.name ?? null,
1413
+ sessions: row.sessions?.join(", ") ?? null
1397
1414
  }))
1398
1415
  );
1399
1416
  }
@@ -2482,6 +2499,7 @@ var GitHubBoards = class {
2482
2499
  const agent = await single("Agent", "Agent", [WORKING]);
2483
2500
  const session = await text("Session");
2484
2501
  const path = await text("Path");
2502
+ const sessions = await text("Sessions");
2485
2503
  const current = await views(cache.projectId, this.#o);
2486
2504
  const first = current[0];
2487
2505
  if (first !== void 0 && first.name === DEFAULT_VIEW && !current.some((v) => v.name === ALL_VIEW)) {
@@ -2550,7 +2568,15 @@ var GitHubBoards = class {
2550
2568
  await linkRepository(cache.projectId, memory.id, this.#o);
2551
2569
  added.push(`link ${repository2}`);
2552
2570
  }
2553
- cache.fields = [status2, collection, ...own.values(), agent, session, path];
2571
+ cache.fields = [
2572
+ status2,
2573
+ collection,
2574
+ ...own.values(),
2575
+ agent,
2576
+ session,
2577
+ path,
2578
+ sessions
2579
+ ];
2554
2580
  return added;
2555
2581
  }
2556
2582
  async items(board, options = {}) {
@@ -2575,7 +2601,8 @@ var GitHubBoards = class {
2575
2601
  collectionStatus: r.values[statusFieldName(r.values[COLLECTION_FIELD] ?? "")] ?? null,
2576
2602
  type: r.values[COLLECTION_FIELD] ?? null,
2577
2603
  agent: r.values["Agent"] ?? null,
2578
- session: r.values["Session"] ?? null
2604
+ session: r.values["Session"] ?? null,
2605
+ sessions: r.values["Sessions"] ?? null
2579
2606
  };
2580
2607
  });
2581
2608
  }
@@ -2689,7 +2716,8 @@ var GitHubBoards = class {
2689
2716
  ["Path", want.path],
2690
2717
  [COLLECTION_FIELD, want.type],
2691
2718
  [STATUS_FIELD, want.status],
2692
- ...want.statusField === null ? [] : [[want.statusField, want.status]]
2719
+ ...want.statusField === null ? [] : [[want.statusField, want.status]],
2720
+ ["Sessions", want.sessions]
2693
2721
  ];
2694
2722
  for (const [name, value] of wanted)
2695
2723
  if ((values2[name] ?? null) !== value)
@@ -3211,6 +3239,54 @@ function reportError(e) {
3211
3239
  return 1;
3212
3240
  }
3213
3241
 
3242
+ // src/core/delegate.ts
3243
+ import { readFile as readFile6 } from "fs/promises";
3244
+ import { join as join10 } from "path";
3245
+ var DEFAULT_BIN = "dist/bin/rness.js";
3246
+ var PINNED_BIN = `node_modules/@rness/cli/${DEFAULT_BIN}`;
3247
+ async function readPinnedCli(rnessDir) {
3248
+ const dir = join10(rnessDir, "node_modules", "@rness", "cli");
3249
+ let pkg;
3250
+ try {
3251
+ pkg = JSON.parse(await readFile6(join10(dir, "package.json"), "utf8"));
3252
+ } catch {
3253
+ return null;
3254
+ }
3255
+ const declared2 = pkg.bin;
3256
+ let bin = DEFAULT_BIN;
3257
+ if (typeof declared2 === "string") bin = declared2;
3258
+ else if (declared2 !== null && typeof declared2 === "object") {
3259
+ const named = declared2["rness"];
3260
+ if (typeof named === "string") bin = named;
3261
+ }
3262
+ return {
3263
+ dir,
3264
+ bin,
3265
+ version: typeof pkg.version === "string" ? pkg.version : null
3266
+ };
3267
+ }
3268
+ async function pinnedBinPath(rnessDir) {
3269
+ const pinned = await readPinnedCli(rnessDir);
3270
+ if (pinned === null) return null;
3271
+ return join10(pinned.dir, ...pinned.bin.split("/"));
3272
+ }
3273
+ async function findDelegate(cwd, ownVersion) {
3274
+ let root;
3275
+ let rnessDir;
3276
+ try {
3277
+ const ws = await findWorkspace(cwd);
3278
+ root = ws.root;
3279
+ rnessDir = ws.rnessDir;
3280
+ } catch {
3281
+ return null;
3282
+ }
3283
+ const pinned = await readPinnedCli(rnessDir);
3284
+ if (pinned === null) return null;
3285
+ const { version } = pinned;
3286
+ if (version === null || version === ownVersion) return null;
3287
+ return { entry: join10(pinned.dir, "dist", "index.js"), version, root };
3288
+ }
3289
+
3214
3290
  // src/core/block.ts
3215
3291
  import { createHash as createHash2 } from "crypto";
3216
3292
  var BEGIN = "<!-- BEGIN rness -->";
@@ -3275,7 +3351,7 @@ function isCurrentBlock(blockLines, freshHash) {
3275
3351
  }
3276
3352
 
3277
3353
  // src/core/merge.ts
3278
- import { join as join10 } from "path";
3354
+ import { join as join11 } from "path";
3279
3355
  function findBlock(lines) {
3280
3356
  const begins = [];
3281
3357
  const ends = [];
@@ -3334,7 +3410,7 @@ function claudeMdAtRoot(existing, block) {
3334
3410
  return merged.ok ? { ...merged, changed: merged.text !== existing } : merged;
3335
3411
  }
3336
3412
  async function ensureClaudeMd(dir) {
3337
- const file = join10(dir, "CLAUDE.md");
3413
+ const file = join11(dir, "CLAUDE.md");
3338
3414
  if (await isSymlink(file)) return "unchanged";
3339
3415
  const existing = await readOrNull(file);
3340
3416
  if (existing === null) {
@@ -3349,14 +3425,230 @@ async function ensureClaudeMd(dir) {
3349
3425
  }
3350
3426
 
3351
3427
  // src/commands/sync.ts
3352
- import { join as join12 } from "path";
3428
+ import { join as join14 } from "path";
3353
3429
 
3354
3430
  // src/core/agent-targets.ts
3355
3431
  import { mkdir as mkdir2 } from "fs/promises";
3356
- import { dirname as dirname4, join as join11 } from "path";
3432
+ import { dirname as dirname4, join as join13 } from "path";
3433
+
3434
+ // src/core/claude-skills.ts
3435
+ var PLUGIN = ".claude/skills/rness";
3436
+ var PLUGIN_JSON = `${JSON.stringify(
3437
+ {
3438
+ name: "rness",
3439
+ description: "The rness workspace in Claude Code: /rness:status, /rness:adr, /rness:spec, /rness:plan, /rness:done."
3440
+ },
3441
+ null,
3442
+ 2
3443
+ )}
3444
+ `;
3445
+ function statusSkill(rness, pm) {
3446
+ const command = `node "\${CLAUDE_PROJECT_DIR}/${rness}/${PINNED_BIN}" status --cwd "\${CLAUDE_PROJECT_DIR}"`;
3447
+ return `---
3448
+ name: status
3449
+ description: The status of every decision, specification, plan and other tracked document of the rness workspace, one table per directory. Read-only.
3450
+ argument-hint: '[tab]'
3451
+ disable-model-invocation: true
3452
+ allowed-tools: Bash(${command})
3453
+ ---
3454
+
3455
+ !\`${command}\`
3456
+
3457
+ Show the output above to the user as it is: the heading and the tables,
3458
+ nothing added, nothing summarised, no other tool. Arguments: \`$ARGUMENTS\`.
3459
+ When they name a tab, show only that tab's section. If the output says the
3460
+ module cannot be found, say instead that ${rness} is not installed next to
3461
+ this repository.
3462
+
3463
+ End with this line: _For the view with tabs and scrolling: here, Ctrl+Z,
3464
+ then \`npx @rness/cli status\` (q to close), then \`fg\`; or in another
3465
+ terminal, from the workspace's \`.rness/\`: \`${localRunner(pm)} rness status\`._
3466
+ `;
3467
+ }
3468
+ var LIFECYCLE2 = [
3469
+ {
3470
+ name: "adr",
3471
+ 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.",
3472
+ hint: "[subject | NNNN]",
3473
+ tab: "adr",
3474
+ body: (rness) => `You record a decision of this workspace as an ADR in \`${rness}/adr/\`. The table
3475
+ above lists the existing ADRs, newest first. Arguments: \`$ARGUMENTS\`.
3476
+
3477
+ Before writing:
3478
+
3479
+ 1. If the developer did not ask for an ADR in so many words, propose one in
3480
+ a single line \u2014 its title, and why the choice is hard to reverse \u2014 and
3481
+ wait for a yes.
3482
+ 2. Read \`${rness}/CONVENTIONS.md\` and \`${rness}/adr/0000-template.md\`: they hold the
3483
+ rules and the sections; this skill is only the procedure.
3484
+ 3. Arguments naming an ADR (\`NNNN\`): reopen that file. An \`Accepted\` ADR is
3485
+ never edited: offer a new ADR that supersedes it.
3486
+ 4. Otherwise the subject is the arguments, else the decision just
3487
+ discussed. Context, alternatives or consequences missing from the
3488
+ conversation: ask for them, one question at a time. Never invent them.
3489
+
3490
+ Writing:
3491
+
3492
+ - \`${rness}/adr/NNNN-<slug>.md\`, NNNN the highest number above plus one.
3493
+ - The template's front matter and sections, filled in, its guidance lines
3494
+ removed: \`date\` today, \`status: Proposed\`, \`repo\` the repositories under
3495
+ \`org/\` it affects.
3496
+ - An ADR it replaces is named in Context. That one becomes \`Superseded\`,
3497
+ with \`superseded_by:\`, only once the new ADR is \`Accepted\`.
3498
+ - Add \`\${CLAUDE_SESSION_ID}\` at the end of \`sessions:\` in the front matter,
3499
+ unless it is already there.
3500
+ - The rness hook checks every edit under \`${rness}/\`: fix what it reports.
3501
+
3502
+ After writing: give the path and the Decision section, and say the ADR
3503
+ stays \`Proposed\` until the developer accepts it. Change a status only when
3504
+ the developer says so. Do not commit.
3505
+ `
3506
+ },
3507
+ {
3508
+ name: "spec",
3509
+ 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.",
3510
+ hint: "[subject | NNNN]",
3511
+ tab: "specs",
3512
+ body: (rness) => `You write a specification of this workspace in \`${rness}/specs/\`. The table above
3513
+ lists the existing ones, newest first. Arguments: \`$ARGUMENTS\`.
3514
+
3515
+ Before writing:
3516
+
3517
+ 1. If the developer did not ask for a specification in so many words,
3518
+ propose one in a single line and wait for a yes.
3519
+ 2. Read \`${rness}/CONVENTIONS.md\`, then the most recent specification above: match
3520
+ its front matter, sections and tone.
3521
+ 3. Arguments naming a specification (\`NNNN\`): reopen it. One that is
3522
+ \`Implemented\`, \`Superseded\` or \`Rejected\` is not edited: offer a new one.
3523
+ 4. Otherwise the subject is the arguments, else the work just discussed.
3524
+ The problem, what is in and out of scope, the ADRs it rests on: ask what
3525
+ the conversation does not say, one question at a time.
3526
+
3527
+ Writing:
3528
+
3529
+ - \`${rness}/specs/NNNN-<slug>.md\`, NNNN the highest number above plus one.
3530
+ - \`date\` and \`updated\` today, \`status: Draft\`, \`repo\`, and \`adr:\` linking
3531
+ the decisions it rests on \u2014 or the rationale in the body when none does.
3532
+ - No specification yet to follow: Summary, Scope, Out of scope,
3533
+ Alternatives considered, To verify, Tests.
3534
+ - What was checked in this session (a document read, a command run) is
3535
+ written as verified, with the date; everything else as a proposal or an
3536
+ open question.
3537
+ - Add \`\${CLAUDE_SESSION_ID}\` at the end of \`sessions:\` in the front matter,
3538
+ unless it is already there.
3539
+ - The rness hook checks every edit under \`${rness}/\`: fix what it reports.
3540
+
3541
+ After writing: give the path and the summary. It stays \`Draft\` until the
3542
+ developer moves it (\`Proposed\` for review, \`Approved\` to plan it). No plan
3543
+ here: \`/rness:plan NNNN\` once approved. Do not commit.
3544
+ `
3545
+ },
3546
+ {
3547
+ name: "plan",
3548
+ 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.",
3549
+ hint: "<spec NNNN>",
3550
+ tab: "plans",
3551
+ body: (rness) => `You turn a specification into a plan in \`${rness}/plans/\`. The table above lists
3552
+ the existing plans, newest first. Arguments, the specification:
3553
+ \`$ARGUMENTS\`.
3554
+
3555
+ Before writing:
3556
+
3557
+ 1. If the developer did not ask for a plan in so many words, propose one in
3558
+ a single line and wait for a yes.
3559
+ 2. No specification named: ask which. Read it, \`${rness}/CONVENTIONS.md\`, and the
3560
+ most recent plan above: match its front matter and sections.
3561
+ 3. A specification that is not \`Approved\`: say so, and plan it only if the
3562
+ developer confirms. A plan of it that is neither \`Completed\` nor
3563
+ \`Abandoned\`: reopen it rather than write a second.
3564
+ 4. Read the code and the repositories the specification touches: every
3565
+ task names real files and real commands.
3566
+
3567
+ Writing:
3568
+
3569
+ - \`${rness}/plans/NNNN-<slug>.md\`, NNNN the highest plan number above plus one \u2014
3570
+ not the specification's.
3571
+ - \`date\` and \`updated\` today, \`status: Draft\`, \`repo\`, \`spec:\` linking the
3572
+ specification.
3573
+ - Tasks in order, as checkboxes. Each gives the files, what changes, and
3574
+ the command that proves it (a test, a build, a run). The last tasks:
3575
+ the documentation the work makes inaccurate, then the release when the
3576
+ repository has one.
3577
+ - Where the plan decides something the specification leaves open, a
3578
+ section says so.
3579
+ - Add \`\${CLAUDE_SESSION_ID}\` at the end of \`sessions:\` in the front matter,
3580
+ unless it is already there.
3581
+ - The rness hook checks every edit under \`${rness}/\`: fix what it reports.
3582
+
3583
+ After writing: give the path and the list of tasks. It becomes \`Ready\` when
3584
+ the developer agrees. Do not commit.
3585
+ `
3586
+ },
3587
+ {
3588
+ name: "done",
3589
+ 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.",
3590
+ hint: "[plan NNNN]",
3591
+ tab: "plans",
3592
+ body: (rness) => `You close a piece of work: its plan in \`${rness}/plans/\`, its specification, and
3593
+ what they made inaccurate. The table above lists the plans, newest first.
3594
+ Arguments: \`$ARGUMENTS\`.
3595
+
3596
+ 1. The plan: the one named, else the one \`In progress\`. Several or none:
3597
+ ask.
3598
+ 2. If the developer did not ask to close it in so many words, propose it in
3599
+ a single line and wait for a yes.
3600
+ 3. Read the plan, its specification and \`${rness}/CONVENTIONS.md\`.
3601
+ 4. Evidence first. For each task, what shows it done: a test, a build, a
3602
+ command's output in this session. A task without evidence: run its
3603
+ check now, or report it. Never mark a task done on belief.
3604
+ 5. All tasks proven:
3605
+ - the plan: \`status: Completed\`, \`updated\` today, the checkboxes of the
3606
+ proven tasks ticked, and a section \`Verification (YYYY-MM-DD)\` \u2014 what
3607
+ ran, what it showed, what could not run and why;
3608
+ - the specification: \`Implemented\` once every plan of it is
3609
+ \`Completed\`; \`updated\` today;
3610
+ - the plan and the specification: add \`\${CLAUDE_SESSION_ID}\` at the end
3611
+ of \`sessions:\` in their front matter, unless it is already there;
3612
+ - the smallest set of documents the work makes inaccurate: \`${rness}/docs/\`,
3613
+ the READMEs of the repositories it changed.
3614
+ 6. A task not proven: the plan stays \`In progress\` \u2014 or \`Blocked\`, the
3615
+ blocker written in it. Say what remains.
3616
+ 7. The rness hook checks every edit under \`${rness}/\`: fix what it reports.
3617
+
3618
+ After: list the files changed and the statuses moved. Do not commit.
3619
+ `
3620
+ }
3621
+ ];
3622
+ function lifecycleSkill(rness, s) {
3623
+ const command = `node "\${CLAUDE_PROJECT_DIR}/${rness}/${PINNED_BIN}" status ${s.tab} --cwd "\${CLAUDE_PROJECT_DIR}"`;
3624
+ return `---
3625
+ name: ${s.name}
3626
+ description: ${s.description}
3627
+ argument-hint: '${s.hint}'
3628
+ allowed-tools: Bash(${command})
3629
+ ---
3630
+
3631
+ !\`${command}\`
3632
+
3633
+ ${s.body(rness)}`;
3634
+ }
3635
+ function pluginFiles(rness, at, ctx) {
3636
+ return [
3637
+ { file: `${PLUGIN}/.claude-plugin/plugin.json`, at, content: PLUGIN_JSON },
3638
+ {
3639
+ file: `${PLUGIN}/skills/status/SKILL.md`,
3640
+ at,
3641
+ content: statusSkill(rness, ctx.packageManager)
3642
+ },
3643
+ ...LIFECYCLE2.map((s) => ({
3644
+ file: `${PLUGIN}/skills/${s.name}/SKILL.md`,
3645
+ at,
3646
+ content: lifecycleSkill(rness, s)
3647
+ }))
3648
+ ];
3649
+ }
3357
3650
 
3358
3651
  // src/core/agents.ts
3359
- var PINNED_BIN = "node_modules/@rness/cli/dist/bin/rness.js";
3360
3652
  function hookLine(rness, event) {
3361
3653
  const run3 = `f="$CLAUDE_PROJECT_DIR/${rness}/${PINNED_BIN}"; if [ -f "$f" ]; then node "$f" hook ${event};`;
3362
3654
  if (event !== "session-start") return `${run3} fi`;
@@ -3409,53 +3701,11 @@ function hooks(rness) {
3409
3701
  }
3410
3702
  ];
3411
3703
  }
3412
- var PLUGIN = ".claude/skills/rness";
3413
- var PLUGIN_JSON = `${JSON.stringify(
3414
- {
3415
- name: "rness",
3416
- description: "The rness workspace in Claude Code: /rness:status."
3417
- },
3418
- null,
3419
- 2
3420
- )}
3421
- `;
3422
- function statusSkill(rness) {
3423
- const command = `node "\${CLAUDE_PROJECT_DIR}/${rness}/${PINNED_BIN}" status --cwd "\${CLAUDE_PROJECT_DIR}"`;
3424
- return `---
3425
- name: status
3426
- description: The status of every decision, specification, plan and other tracked document of the rness workspace, one table per directory. Read-only.
3427
- argument-hint: '[tab]'
3428
- disable-model-invocation: true
3429
- allowed-tools: Bash(${command})
3430
- ---
3431
-
3432
- !\`${command}\`
3433
-
3434
- Show the output above to the user as it is: the heading and the tables,
3435
- nothing added, nothing summarised, no other tool. Arguments: \`$ARGUMENTS\`.
3436
- When they name a tab, show only that tab's section. If the output says the
3437
- module cannot be found, say instead that ${rness} is not installed next to
3438
- this repository.
3439
-
3440
- End with this line: _For the view with tabs and scrolling: Ctrl+Z, then
3441
- \`npx @rness/cli status\` (q to close), then \`fg\`._
3442
- `;
3443
- }
3444
- function plugin(rness, at) {
3445
- return [
3446
- { file: `${PLUGIN}/.claude-plugin/plugin.json`, at, content: PLUGIN_JSON },
3447
- {
3448
- file: `${PLUGIN}/skills/status/SKILL.md`,
3449
- at,
3450
- content: statusSkill(rness)
3451
- }
3452
- ];
3453
- }
3454
3704
  var TARGETS = {
3455
3705
  claude: {
3456
3706
  name: "claude",
3457
3707
  label: "Claude Code",
3458
- files: [
3708
+ files: (ctx) => [
3459
3709
  {
3460
3710
  file: ".claude/settings.json",
3461
3711
  at: "clones",
@@ -3491,7 +3741,7 @@ var TARGETS = {
3491
3741
  }
3492
3742
  ]
3493
3743
  },
3494
- ...plugin("../../.rness", "clones"),
3744
+ ...pluginFiles("../../.rness", "clones", ctx),
3495
3745
  // A session opened at the workspace root reads this file, one opened
3496
3746
  // in a clone does not (spec 0011 §1): the hooks only, `.rness` inside.
3497
3747
  {
@@ -3499,7 +3749,7 @@ var TARGETS = {
3499
3749
  at: "root",
3500
3750
  guarantees: hooks(".rness")
3501
3751
  },
3502
- ...plugin(".rness", "root")
3752
+ ...pluginFiles(".rness", "root", ctx)
3503
3753
  ]
3504
3754
  }
3505
3755
  };
@@ -3613,30 +3863,288 @@ function describeGuarantee(g) {
3613
3863
  return `${g.path.join(".")} lacks ${what}`;
3614
3864
  }
3615
3865
 
3866
+ // src/core/pinned.ts
3867
+ import { execFile as execFile3 } from "child_process";
3868
+ import { readFile as readFile7 } from "fs/promises";
3869
+ import { basename as basename3, join as join12 } from "path";
3870
+ import { promisify as promisify3 } from "util";
3871
+ var execFileP3 = promisify3(execFile3);
3872
+ var PACKAGE = "@rness/cli";
3873
+ var EXACT_VERSION = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/;
3874
+ function compareVersions(a, b) {
3875
+ const split = (v) => {
3876
+ const dash = v.indexOf("-");
3877
+ const core = dash === -1 ? v : v.slice(0, dash);
3878
+ return [core.split(".").map(Number), dash === -1 ? null : v.slice(dash + 1)];
3879
+ };
3880
+ const [coreA, preA] = split(a);
3881
+ const [coreB, preB] = split(b);
3882
+ for (let i = 0; i < 3; i += 1) {
3883
+ const diff = (coreA[i] ?? 0) - (coreB[i] ?? 0);
3884
+ if (diff !== 0) return diff;
3885
+ }
3886
+ if (preA === preB) return 0;
3887
+ if (preA === null) return 1;
3888
+ if (preB === null) return -1;
3889
+ return preA.localeCompare(preB, "en", { numeric: true });
3890
+ }
3891
+ async function readPackageJson(rnessDir) {
3892
+ try {
3893
+ const parsed = JSON.parse(
3894
+ await readFile7(join12(rnessDir, "package.json"), "utf8")
3895
+ );
3896
+ return parsed !== null && typeof parsed === "object" ? parsed : null;
3897
+ } catch {
3898
+ return null;
3899
+ }
3900
+ }
3901
+ async function readPin(rnessDir) {
3902
+ const pkg = await readPackageJson(rnessDir);
3903
+ if (pkg === null) return null;
3904
+ for (const field of ["devDependencies", "dependencies"]) {
3905
+ const deps = pkg[field];
3906
+ if (deps === null || typeof deps !== "object") continue;
3907
+ const spec = deps[PACKAGE];
3908
+ if (typeof spec === "string") return { spec, field };
3909
+ }
3910
+ return null;
3911
+ }
3912
+ async function writePin(rnessDir, from, to) {
3913
+ const file = join12(rnessDir, "package.json");
3914
+ const text = await readFile7(file, "utf8");
3915
+ const entry = new RegExp(
3916
+ `("${PACKAGE}"\\s*:\\s*")${from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(")`
3917
+ );
3918
+ if (!entry.test(text))
3919
+ throw new Error(`"${PACKAGE}": "${from}" not found in package.json`);
3920
+ await writeFileAtomic(file, text.replace(entry, `$1${to}$2`));
3921
+ }
3922
+ var LOCKFILES = [
3923
+ ["pnpm-lock.yaml", "pnpm"],
3924
+ ["yarn.lock", "yarn"],
3925
+ ["bun.lock", "bun"],
3926
+ ["bun.lockb", "bun"],
3927
+ ["package-lock.json", "npm"]
3928
+ ];
3929
+ async function workspacePackageManager(rnessDir) {
3930
+ const declared2 = (await readPackageJson(rnessDir))?.["packageManager"];
3931
+ if (typeof declared2 === "string") {
3932
+ const name = declared2.split("@")[0] ?? "";
3933
+ if (isPackageManager(name)) return name;
3934
+ }
3935
+ for (const [lockfile, pm] of LOCKFILES)
3936
+ if (await exists(join12(rnessDir, lockfile))) return pm;
3937
+ return "npm";
3938
+ }
3939
+ async function hasLockfile(rnessDir, pm) {
3940
+ for (const [lockfile, owner] of LOCKFILES)
3941
+ if (owner === pm && await exists(join12(rnessDir, lockfile))) return true;
3942
+ return false;
3943
+ }
3944
+ async function pinDrift(rnessDir) {
3945
+ const pin = await readPin(rnessDir);
3946
+ if (pin === null || !EXACT_VERSION.test(pin.spec)) return null;
3947
+ const installed = (await readPinnedCli(rnessDir))?.version ?? null;
3948
+ return installed === pin.spec ? null : { pin: pin.spec, installed };
3949
+ }
3950
+ async function driftWarning(cwd) {
3951
+ let root;
3952
+ let rnessDir;
3953
+ try {
3954
+ const ws = await findWorkspace(cwd);
3955
+ root = ws.root;
3956
+ rnessDir = ws.rnessDir;
3957
+ } catch {
3958
+ return null;
3959
+ }
3960
+ const drift = await pinDrift(rnessDir);
3961
+ if (drift === null) return null;
3962
+ const pm = await workspacePackageManager(rnessDir);
3963
+ return warn(
3964
+ `${basename3(root)}/.rness pins @rness/cli ${drift.pin} but ${drift.installed ?? "nothing"} is installed \u2014 run ${pm} install in .rness`
3965
+ );
3966
+ }
3967
+ var CHILD_DEADLINE_MS = 12e4;
3968
+ function childEnv(env = process.env) {
3969
+ return {
3970
+ ...env,
3971
+ GIT_SSH_COMMAND: `${env["GIT_SSH_COMMAND"] ?? "ssh"} -o BatchMode=yes`,
3972
+ GIT_TERMINAL_PROMPT: "0"
3973
+ };
3974
+ }
3975
+ async function ensureInstalled(rnessDir, opts = {}) {
3976
+ const drift = await pinDrift(rnessDir);
3977
+ if (drift === null) return null;
3978
+ const env = opts.env ?? process.env;
3979
+ if (env["RNESS_NO_INSTALL"] === "1")
3980
+ return { installed: false, pin: drift.pin, reason: "RNESS_NO_INSTALL=1" };
3981
+ const pm = await workspacePackageManager(rnessDir);
3982
+ const frozen = await hasLockfile(rnessDir, pm);
3983
+ const install = opts.install ?? installDependencies;
3984
+ opts.announce?.(drift.pin);
3985
+ try {
3986
+ await install(pm, rnessDir, { frozen });
3987
+ return { installed: true, pin: drift.pin, reason: null };
3988
+ } catch (e) {
3989
+ return {
3990
+ installed: false,
3991
+ pin: drift.pin,
3992
+ reason: e instanceof Error ? e.message : String(e)
3993
+ };
3994
+ }
3995
+ }
3996
+ async function runPinnedSync(root) {
3997
+ const installed = await readPinnedCli(join12(root, ".rness"));
3998
+ if (installed === null) return null;
3999
+ const pinned = join12(installed.dir, ...installed.bin.split("/"));
4000
+ if (!await exists(pinned))
4001
+ return {
4002
+ code: 1,
4003
+ stdout: "",
4004
+ stderr: `@rness/cli in .rness is not installed correctly (missing ${installed.bin}); reinstall in .rness/
4005
+ `
4006
+ };
4007
+ try {
4008
+ const { stdout, stderr } = await execFileP3(
4009
+ process.execPath,
4010
+ [pinned, "sync", "--yes"],
4011
+ {
4012
+ cwd: root,
4013
+ maxBuffer: 16 * 1024 * 1024,
4014
+ env: childEnv(),
4015
+ timeout: CHILD_DEADLINE_MS,
4016
+ killSignal: "SIGKILL"
4017
+ }
4018
+ );
4019
+ return { code: 0, stdout, stderr };
4020
+ } catch (e) {
4021
+ const err = e;
4022
+ const deadline = err.killed === true ? `rness sync in .rness did not finish within ${CHILD_DEADLINE_MS / 1e3}s and was stopped
4023
+ ` : "";
4024
+ return {
4025
+ code: typeof err.code === "number" ? err.code : 1,
4026
+ stdout: err.stdout ?? "",
4027
+ stderr: `${err.stderr ?? ""}${deadline}`
4028
+ };
4029
+ }
4030
+ }
4031
+ var SYNC_VERBS = ["updated", "unchanged", "stale"];
4032
+ function counted(noun, counts) {
4033
+ const total = [...counts.values()].reduce((a, b) => a + b, 0);
4034
+ const detail = SYNC_VERBS.filter((verb) => counts.has(verb)).map((verb) => `${counts.get(verb) ?? 0} ${verb}`).join(", ");
4035
+ return `${total} ${noun}${total === 1 ? "" : "s"}${detail === "" ? "" : `: ${detail}`}`;
4036
+ }
4037
+ function summariseSync(stdout) {
4038
+ const blocks = /* @__PURE__ */ new Map();
4039
+ const agentFiles = /* @__PURE__ */ new Map();
4040
+ const others = [];
4041
+ for (const line3 of stdout.split("\n")) {
4042
+ if (line3.trim() === "") continue;
4043
+ const m = /^(updated|unchanged|stale)\s+(\S+)/.exec(line3);
4044
+ if (m === null) {
4045
+ others.push(line3);
4046
+ continue;
4047
+ }
4048
+ const [, verb = "", path = ""] = m;
4049
+ const counts = /(^|\/)(AGENTS|CLAUDE)\.md$/.test(path) ? blocks : agentFiles;
4050
+ counts.set(verb, (counts.get(verb) ?? 0) + 1);
4051
+ }
4052
+ return {
4053
+ summary: [
4054
+ counted("block", blocks),
4055
+ ...agentFiles.size > 0 ? [counted("agent file", agentFiles)] : []
4056
+ ].join("; "),
4057
+ others
4058
+ };
4059
+ }
4060
+ function syncOutcome(result, opts = {}) {
4061
+ const { others } = summariseSync(result.stdout);
4062
+ const wrote = /^(updated|unchanged|stale)\s/m.test(result.stdout);
4063
+ const stderr = result.stderr.split("\n").map((l) => l.trimEnd()).filter((l) => l.trim() !== "");
4064
+ const hints = wrote ? [...others, ...stderr] : others;
4065
+ if (opts.sshWorks === true && /Permission denied \(publickey\)/.test(result.stderr))
4066
+ hints.push(...agentHintLines());
4067
+ return {
4068
+ code: wrote ? 0 : result.code,
4069
+ hints,
4070
+ errors: wrote ? [] : stderr
4071
+ };
4072
+ }
4073
+ async function syncPinned(root, shown, fallback) {
4074
+ const rnessDir = join12(root, ".rness");
4075
+ const installed = await readPinnedCli(rnessDir);
4076
+ if (installed === null) {
4077
+ if (fallback !== void 0) return fallback();
4078
+ process.stderr.write(
4079
+ `@rness/cli is not installed in ${shown}/.rness; install in .rness/, then ${rnessCommand()} sync
4080
+ `
4081
+ );
4082
+ return 1;
4083
+ }
4084
+ const pinned = join12(installed.dir, ...installed.bin.split("/"));
4085
+ if (!await exists(pinned)) {
4086
+ process.stderr.write(
4087
+ `@rness/cli in ${shown}/.rness is not installed correctly (missing ${installed.bin}); reinstall in .rness/
4088
+ `
4089
+ );
4090
+ return 1;
4091
+ }
4092
+ try {
4093
+ const { stdout, stderr } = await execFileP3(
4094
+ process.execPath,
4095
+ [pinned, "sync", "--yes"],
4096
+ {
4097
+ cwd: root,
4098
+ maxBuffer: 16 * 1024 * 1024,
4099
+ env: childEnv(),
4100
+ timeout: CHILD_DEADLINE_MS,
4101
+ killSignal: "SIGKILL"
4102
+ }
4103
+ );
4104
+ process.stdout.write(stdout);
4105
+ if (stderr !== "") process.stderr.write(stderr);
4106
+ return 0;
4107
+ } catch (e) {
4108
+ const err = e;
4109
+ if (err.stdout !== void 0 && err.stdout !== "")
4110
+ process.stdout.write(err.stdout);
4111
+ if (err.stderr !== void 0 && err.stderr !== "")
4112
+ process.stderr.write(err.stderr);
4113
+ return typeof err.code === "number" ? err.code : 1;
4114
+ }
4115
+ }
4116
+
3616
4117
  // src/core/agent-targets.ts
3617
4118
  function wholeDiff(existing, content) {
3618
4119
  if (existing === content) return null;
3619
4120
  return existing === null ? "missing" : "differs from what rness writes";
3620
4121
  }
4122
+ async function contextOf(root) {
4123
+ return {
4124
+ packageManager: await workspacePackageManager(join13(root, ".rness"))
4125
+ };
4126
+ }
4127
+ var PATHS_ONLY = { packageManager: "npm" };
3621
4128
  async function filesOf(root, manifest, targets) {
3622
4129
  const files = [];
3623
- if (!await exists(join11(root, "org"))) return files;
4130
+ if (!await exists(join13(root, "org"))) return files;
4131
+ const ctx = await contextOf(root);
3624
4132
  for (const target2 of targets)
3625
- for (const owned of target2.files)
4133
+ for (const owned of target2.files(ctx))
3626
4134
  if (owned.at === "root")
3627
4135
  files.push({
3628
4136
  ...owned,
3629
- file: join11(root, ...owned.file.split("/")),
4137
+ file: join13(root, ...owned.file.split("/")),
3630
4138
  label: owned.file
3631
4139
  });
3632
4140
  for (const repo of Object.keys(manifest.repos)) {
3633
- if (!await exists(join11(root, "org", repo))) continue;
4141
+ if (!await exists(join13(root, "org", repo))) continue;
3634
4142
  for (const target2 of targets)
3635
- for (const owned of target2.files)
4143
+ for (const owned of target2.files(ctx))
3636
4144
  if (owned.at === "clones")
3637
4145
  files.push({
3638
4146
  ...owned,
3639
- file: join11(root, "org", repo, ...owned.file.split("/")),
4147
+ file: join13(root, "org", repo, ...owned.file.split("/")),
3640
4148
  label: `org/${repo}/${owned.file}`
3641
4149
  });
3642
4150
  }
@@ -3652,7 +4160,7 @@ function writtenFiles(manifest) {
3652
4160
  return [
3653
4161
  ...BLOCK_FILES,
3654
4162
  ...declared(manifest).flatMap(
3655
- (t) => t.files.filter((f) => f.at === "clones").map((f) => f.file)
4163
+ (t) => t.files(PATHS_ONLY).filter((f) => f.at === "clones").map((f) => f.file)
3656
4164
  )
3657
4165
  ];
3658
4166
  }
@@ -3960,7 +4468,7 @@ function targetsOf(manifest, root, only) {
3960
4468
  const segments = entry.path.split("/");
3961
4469
  targets.push({
3962
4470
  scope: name,
3963
- dir: join12(root, ...segments),
4471
+ dir: join14(root, ...segments),
3964
4472
  depth: segments.length,
3965
4473
  label: `${entry.path}/AGENTS.md`
3966
4474
  });
@@ -4119,7 +4627,7 @@ async function syncCommand(opts, deps = {}) {
4119
4627
  try {
4120
4628
  const notCloned = /* @__PURE__ */ new Set();
4121
4629
  for (const [name, repo] of Object.entries(manifest.repos)) {
4122
- const dir = join12(ws.root, "org", name);
4630
+ const dir = join14(ws.root, "org", name);
4123
4631
  const label = `org/${name}`;
4124
4632
  if (!await exists(dir)) {
4125
4633
  if (!toClone.has(name)) {
@@ -4171,7 +4679,7 @@ async function syncCommand(opts, deps = {}) {
4171
4679
  lines.push(
4172
4680
  `not in rness.json: ${undeclared.join(", ")} (${rnessCommand()} add <name> declares it)`
4173
4681
  );
4174
- if (!await exists(join12(ws.root, "org"))) {
4682
+ if (!await exists(join14(ws.root, "org"))) {
4175
4683
  lines.push(["skipped", "blocks (no org/ directory here)"]);
4176
4684
  } else {
4177
4685
  let differences = 0;
@@ -4183,7 +4691,7 @@ async function syncCommand(opts, deps = {}) {
4183
4691
  continue;
4184
4692
  }
4185
4693
  const [agentsLinked, claudeLinked] = await Promise.all(
4186
- BLOCK_FILES.map((f) => isSymlink(join12(t.dir, f)))
4694
+ BLOCK_FILES.map((f) => isSymlink(join14(t.dir, f)))
4187
4695
  );
4188
4696
  const atRoot = t.scope === null;
4189
4697
  if (!atRoot && (agentsLinked === true || claudeLinked === true)) {
@@ -4206,7 +4714,7 @@ async function syncCommand(opts, deps = {}) {
4206
4714
  `${t.label}: block is ${Math.round(block.bytes / 1024)} KB (over 32 KB)`
4207
4715
  );
4208
4716
  }
4209
- const agents = agentsLinked === true ? "skipped" : await syncBlockFile(join12(t.dir, "AGENTS.md"), block, check);
4717
+ const agents = agentsLinked === true ? "skipped" : await syncBlockFile(join14(t.dir, "AGENTS.md"), block, check);
4210
4718
  if (typeof agents === "object") {
4211
4719
  problems.push(`${t.label}: ${agents.error}`);
4212
4720
  continue;
@@ -4221,7 +4729,7 @@ async function syncCommand(opts, deps = {}) {
4221
4729
  }
4222
4730
  if (claudeLinked === true) continue;
4223
4731
  const claude = await syncBlockFile(
4224
- join12(t.dir, "CLAUDE.md"),
4732
+ join14(t.dir, "CLAUDE.md"),
4225
4733
  block,
4226
4734
  check,
4227
4735
  true
@@ -4278,7 +4786,7 @@ async function syncCommand(opts, deps = {}) {
4278
4786
  }
4279
4787
 
4280
4788
  // src/commands/add.ts
4281
- import { basename as basename3, join as join13 } from "path";
4789
+ import { basename as basename4, join as join15 } from "path";
4282
4790
  function printDeclared(ui, result, already) {
4283
4791
  for (const name of result.declared) {
4284
4792
  if (already.has(name)) continue;
@@ -4313,7 +4821,7 @@ async function addCommand(spec, opts, deps = {}) {
4313
4821
  }
4314
4822
  if (parsed.name === CONTEXT_REPO) {
4315
4823
  process.stderr.write(
4316
- `${CONTEXT_REPO} is the workspace context, not a member repository \u2014 it is already cloned at ${basename3(ws.root)}/${CONTEXT_REPO}
4824
+ `${CONTEXT_REPO} is the workspace context, not a member repository \u2014 it is already cloned at ${basename4(ws.root)}/${CONTEXT_REPO}
4317
4825
  `
4318
4826
  );
4319
4827
  return 2;
@@ -4371,7 +4879,7 @@ async function addCommand(spec, opts, deps = {}) {
4371
4879
  }
4372
4880
  parsed = parseRepoSpec(spec, org, host);
4373
4881
  const scopes = (opts.scopes ?? "").split(",").map((s) => s.trim()).filter((s) => s !== "");
4374
- const dir = join13(ws.root, "org", parsed.name);
4882
+ const dir = join15(ws.root, "org", parsed.name);
4375
4883
  const present = await exists(dir);
4376
4884
  if (interactive) {
4377
4885
  const { confirm, isCancel } = await terminal.prompts();
@@ -4488,65 +4996,18 @@ async function contextCommand(opts) {
4488
4996
  scope
4489
4997
  });
4490
4998
  process.stdout.write(
4491
- opts.json ? `${JSON.stringify(ctx, null, 2)}
4492
- ` : renderMarkdown(ctx)
4493
- );
4494
- return 0;
4495
- } catch (e) {
4496
- return reportError(e);
4497
- }
4498
- }
4499
-
4500
- // src/core/delegate.ts
4501
- import { readFile as readFile6 } from "fs/promises";
4502
- import { join as join14 } from "path";
4503
- var DEFAULT_BIN = "dist/bin/rness.js";
4504
- async function readPinnedCli(rnessDir) {
4505
- const dir = join14(rnessDir, "node_modules", "@rness", "cli");
4506
- let pkg;
4507
- try {
4508
- pkg = JSON.parse(await readFile6(join14(dir, "package.json"), "utf8"));
4509
- } catch {
4510
- return null;
4511
- }
4512
- const declared2 = pkg.bin;
4513
- let bin = DEFAULT_BIN;
4514
- if (typeof declared2 === "string") bin = declared2;
4515
- else if (declared2 !== null && typeof declared2 === "object") {
4516
- const named = declared2["rness"];
4517
- if (typeof named === "string") bin = named;
4518
- }
4519
- return {
4520
- dir,
4521
- bin,
4522
- version: typeof pkg.version === "string" ? pkg.version : null
4523
- };
4524
- }
4525
- async function pinnedBinPath(rnessDir) {
4526
- const pinned = await readPinnedCli(rnessDir);
4527
- if (pinned === null) return null;
4528
- return join14(pinned.dir, ...pinned.bin.split("/"));
4529
- }
4530
- async function findDelegate(cwd, ownVersion) {
4531
- let root;
4532
- let rnessDir;
4533
- try {
4534
- const ws = await findWorkspace(cwd);
4535
- root = ws.root;
4536
- rnessDir = ws.rnessDir;
4537
- } catch {
4538
- return null;
4999
+ opts.json ? `${JSON.stringify(ctx, null, 2)}
5000
+ ` : renderMarkdown(ctx)
5001
+ );
5002
+ return 0;
5003
+ } catch (e) {
5004
+ return reportError(e);
4539
5005
  }
4540
- const pinned = await readPinnedCli(rnessDir);
4541
- if (pinned === null) return null;
4542
- const { version } = pinned;
4543
- if (version === null || version === ownVersion) return null;
4544
- return { entry: join14(pinned.dir, "dist", "index.js"), version, root };
4545
5006
  }
4546
5007
 
4547
5008
  // src/core/scaffold.ts
4548
5009
  import { statSync } from "fs";
4549
- import { dirname as dirname5, join as join15 } from "path";
5010
+ import { dirname as dirname5, join as join16 } from "path";
4550
5011
  import { fileURLToPath } from "url";
4551
5012
  var SCAFFOLD_FILES = [
4552
5013
  "AGENTS.md",
@@ -4570,9 +5031,9 @@ var SCAFFOLD_FILES = [
4570
5031
  function scaffoldDir() {
4571
5032
  let dir = dirname5(fileURLToPath(import.meta.url));
4572
5033
  for (let i = 0; i < 4; i += 1) {
4573
- const candidate = join15(dir, "scaffold");
5034
+ const candidate = join16(dir, "scaffold");
4574
5035
  try {
4575
- if (statSync(join15(candidate, "rness.json")).isFile()) return candidate;
5036
+ if (statSync(join16(candidate, "rness.json")).isFile()) return candidate;
4576
5037
  } catch {
4577
5038
  }
4578
5039
  dir = dirname5(dir);
@@ -4581,15 +5042,15 @@ function scaffoldDir() {
4581
5042
  }
4582
5043
 
4583
5044
  // src/core/scaffold-copy.ts
4584
- import { chmod as chmod2, mkdir as mkdir3, readFile as readFile7, stat as stat2, writeFile as writeFile2 } from "fs/promises";
4585
- import { dirname as dirname6, join as join16 } from "path";
5045
+ import { chmod as chmod2, mkdir as mkdir3, readFile as readFile8, stat as stat2, writeFile as writeFile2 } from "fs/promises";
5046
+ import { dirname as dirname6, join as join17 } from "path";
4586
5047
  var TOKENISED = /* @__PURE__ */ new Set(["package.json"]);
4587
5048
  async function renderScaffold(source, tokens) {
4588
5049
  const files = [];
4589
5050
  for (const rel of SCAFFOLD_FILES) {
4590
5051
  if (rel === "rness.json") continue;
4591
- const from = join16(source, ...rel.split("/"));
4592
- let content = await readFile7(from, "utf8");
5052
+ const from = join17(source, ...rel.split("/"));
5053
+ let content = await readFile8(from, "utf8");
4593
5054
  if (TOKENISED.has(rel)) {
4594
5055
  content = content.replaceAll("__RNESS_VERSION__", tokens.version).replaceAll("__RNESS_PM__", tokens.packageManager);
4595
5056
  }
@@ -4603,7 +5064,7 @@ async function renderScaffold(source, tokens) {
4603
5064
  }
4604
5065
  async function copyScaffold(dest, tokens) {
4605
5066
  for (const file of await renderScaffold(scaffoldDir(), tokens)) {
4606
- const to = join16(dest, ...file.path.split("/"));
5067
+ const to = join17(dest, ...file.path.split("/"));
4607
5068
  await mkdir3(dirname6(to), { recursive: true });
4608
5069
  await writeFile2(to, file.content, "utf8");
4609
5070
  if (file.executable) await chmod2(to, 493);
@@ -4615,16 +5076,16 @@ import { spawn as spawn3 } from "child_process";
4615
5076
  import { realpath } from "fs/promises";
4616
5077
 
4617
5078
  // src/core/setup-git.ts
4618
- import { execFile as execFile3 } from "child_process";
4619
- import { promisify as promisify3 } from "util";
4620
- var execFileP3 = promisify3(execFile3);
5079
+ import { execFile as execFile4 } from "child_process";
5080
+ import { promisify as promisify4 } from "util";
5081
+ var execFileP4 = promisify4(execFile4);
4621
5082
  var KEY = "credential.https://github.com.helper";
4622
5083
  function helperValue(node, bin) {
4623
5084
  return `!'${node}' '${bin}' git-credential`;
4624
5085
  }
4625
5086
  var isOurs = (value) => /^!'[^']+' '[^']*rness[^']*' git-credential$/.test(value);
4626
5087
  async function config(args) {
4627
- const { stdout } = await execFileP3("git", ["config", "--global", ...args]);
5088
+ const { stdout } = await execFileP4("git", ["config", "--global", ...args]);
4628
5089
  return stdout;
4629
5090
  }
4630
5091
  async function values() {
@@ -4802,257 +5263,6 @@ async function askProvider(p) {
4802
5263
  import { mkdir as mkdir4, readdir as readdir3, rm as rm5, stat as stat3 } from "fs/promises";
4803
5264
  import { dirname as dirname8, join as join19, relative as relative2 } from "path";
4804
5265
 
4805
- // src/core/pinned.ts
4806
- import { execFile as execFile4 } from "child_process";
4807
- import { readFile as readFile8 } from "fs/promises";
4808
- import { basename as basename4, join as join17 } from "path";
4809
- import { promisify as promisify4 } from "util";
4810
- var execFileP4 = promisify4(execFile4);
4811
- var PACKAGE = "@rness/cli";
4812
- var EXACT_VERSION = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/;
4813
- function compareVersions(a, b) {
4814
- const split = (v) => {
4815
- const dash = v.indexOf("-");
4816
- const core = dash === -1 ? v : v.slice(0, dash);
4817
- return [core.split(".").map(Number), dash === -1 ? null : v.slice(dash + 1)];
4818
- };
4819
- const [coreA, preA] = split(a);
4820
- const [coreB, preB] = split(b);
4821
- for (let i = 0; i < 3; i += 1) {
4822
- const diff = (coreA[i] ?? 0) - (coreB[i] ?? 0);
4823
- if (diff !== 0) return diff;
4824
- }
4825
- if (preA === preB) return 0;
4826
- if (preA === null) return 1;
4827
- if (preB === null) return -1;
4828
- return preA.localeCompare(preB, "en", { numeric: true });
4829
- }
4830
- async function readPackageJson(rnessDir) {
4831
- try {
4832
- const parsed = JSON.parse(
4833
- await readFile8(join17(rnessDir, "package.json"), "utf8")
4834
- );
4835
- return parsed !== null && typeof parsed === "object" ? parsed : null;
4836
- } catch {
4837
- return null;
4838
- }
4839
- }
4840
- async function readPin(rnessDir) {
4841
- const pkg = await readPackageJson(rnessDir);
4842
- if (pkg === null) return null;
4843
- for (const field of ["devDependencies", "dependencies"]) {
4844
- const deps = pkg[field];
4845
- if (deps === null || typeof deps !== "object") continue;
4846
- const spec = deps[PACKAGE];
4847
- if (typeof spec === "string") return { spec, field };
4848
- }
4849
- return null;
4850
- }
4851
- async function writePin(rnessDir, from, to) {
4852
- const file = join17(rnessDir, "package.json");
4853
- const text = await readFile8(file, "utf8");
4854
- const entry = new RegExp(
4855
- `("${PACKAGE}"\\s*:\\s*")${from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(")`
4856
- );
4857
- if (!entry.test(text))
4858
- throw new Error(`"${PACKAGE}": "${from}" not found in package.json`);
4859
- await writeFileAtomic(file, text.replace(entry, `$1${to}$2`));
4860
- }
4861
- var LOCKFILES = [
4862
- ["pnpm-lock.yaml", "pnpm"],
4863
- ["yarn.lock", "yarn"],
4864
- ["bun.lock", "bun"],
4865
- ["bun.lockb", "bun"],
4866
- ["package-lock.json", "npm"]
4867
- ];
4868
- async function workspacePackageManager(rnessDir) {
4869
- const declared2 = (await readPackageJson(rnessDir))?.["packageManager"];
4870
- if (typeof declared2 === "string") {
4871
- const name = declared2.split("@")[0] ?? "";
4872
- if (isPackageManager(name)) return name;
4873
- }
4874
- for (const [lockfile, pm] of LOCKFILES)
4875
- if (await exists(join17(rnessDir, lockfile))) return pm;
4876
- return "npm";
4877
- }
4878
- async function hasLockfile(rnessDir, pm) {
4879
- for (const [lockfile, owner] of LOCKFILES)
4880
- if (owner === pm && await exists(join17(rnessDir, lockfile))) return true;
4881
- return false;
4882
- }
4883
- async function pinDrift(rnessDir) {
4884
- const pin = await readPin(rnessDir);
4885
- if (pin === null || !EXACT_VERSION.test(pin.spec)) return null;
4886
- const installed = (await readPinnedCli(rnessDir))?.version ?? null;
4887
- return installed === pin.spec ? null : { pin: pin.spec, installed };
4888
- }
4889
- async function driftWarning(cwd) {
4890
- let root;
4891
- let rnessDir;
4892
- try {
4893
- const ws = await findWorkspace(cwd);
4894
- root = ws.root;
4895
- rnessDir = ws.rnessDir;
4896
- } catch {
4897
- return null;
4898
- }
4899
- const drift = await pinDrift(rnessDir);
4900
- if (drift === null) return null;
4901
- const pm = await workspacePackageManager(rnessDir);
4902
- return warn(
4903
- `${basename4(root)}/.rness pins @rness/cli ${drift.pin} but ${drift.installed ?? "nothing"} is installed \u2014 run ${pm} install in .rness`
4904
- );
4905
- }
4906
- var CHILD_DEADLINE_MS = 12e4;
4907
- function childEnv(env = process.env) {
4908
- return {
4909
- ...env,
4910
- GIT_SSH_COMMAND: `${env["GIT_SSH_COMMAND"] ?? "ssh"} -o BatchMode=yes`,
4911
- GIT_TERMINAL_PROMPT: "0"
4912
- };
4913
- }
4914
- async function ensureInstalled(rnessDir, opts = {}) {
4915
- const drift = await pinDrift(rnessDir);
4916
- if (drift === null) return null;
4917
- const env = opts.env ?? process.env;
4918
- if (env["RNESS_NO_INSTALL"] === "1")
4919
- return { installed: false, pin: drift.pin, reason: "RNESS_NO_INSTALL=1" };
4920
- const pm = await workspacePackageManager(rnessDir);
4921
- const frozen = await hasLockfile(rnessDir, pm);
4922
- const install = opts.install ?? installDependencies;
4923
- opts.announce?.(drift.pin);
4924
- try {
4925
- await install(pm, rnessDir, { frozen });
4926
- return { installed: true, pin: drift.pin, reason: null };
4927
- } catch (e) {
4928
- return {
4929
- installed: false,
4930
- pin: drift.pin,
4931
- reason: e instanceof Error ? e.message : String(e)
4932
- };
4933
- }
4934
- }
4935
- async function runPinnedSync(root) {
4936
- const installed = await readPinnedCli(join17(root, ".rness"));
4937
- if (installed === null) return null;
4938
- const pinned = join17(installed.dir, ...installed.bin.split("/"));
4939
- if (!await exists(pinned))
4940
- return {
4941
- code: 1,
4942
- stdout: "",
4943
- stderr: `@rness/cli in .rness is not installed correctly (missing ${installed.bin}); reinstall in .rness/
4944
- `
4945
- };
4946
- try {
4947
- const { stdout, stderr } = await execFileP4(
4948
- process.execPath,
4949
- [pinned, "sync", "--yes"],
4950
- {
4951
- cwd: root,
4952
- maxBuffer: 16 * 1024 * 1024,
4953
- env: childEnv(),
4954
- timeout: CHILD_DEADLINE_MS,
4955
- killSignal: "SIGKILL"
4956
- }
4957
- );
4958
- return { code: 0, stdout, stderr };
4959
- } catch (e) {
4960
- const err = e;
4961
- const deadline = err.killed === true ? `rness sync in .rness did not finish within ${CHILD_DEADLINE_MS / 1e3}s and was stopped
4962
- ` : "";
4963
- return {
4964
- code: typeof err.code === "number" ? err.code : 1,
4965
- stdout: err.stdout ?? "",
4966
- stderr: `${err.stderr ?? ""}${deadline}`
4967
- };
4968
- }
4969
- }
4970
- var SYNC_VERBS = ["updated", "unchanged", "stale"];
4971
- function counted(noun, counts) {
4972
- const total = [...counts.values()].reduce((a, b) => a + b, 0);
4973
- const detail = SYNC_VERBS.filter((verb) => counts.has(verb)).map((verb) => `${counts.get(verb) ?? 0} ${verb}`).join(", ");
4974
- return `${total} ${noun}${total === 1 ? "" : "s"}${detail === "" ? "" : `: ${detail}`}`;
4975
- }
4976
- function summariseSync(stdout) {
4977
- const blocks = /* @__PURE__ */ new Map();
4978
- const agentFiles = /* @__PURE__ */ new Map();
4979
- const others = [];
4980
- for (const line3 of stdout.split("\n")) {
4981
- if (line3.trim() === "") continue;
4982
- const m = /^(updated|unchanged|stale)\s+(\S+)/.exec(line3);
4983
- if (m === null) {
4984
- others.push(line3);
4985
- continue;
4986
- }
4987
- const [, verb = "", path = ""] = m;
4988
- const counts = /(^|\/)(AGENTS|CLAUDE)\.md$/.test(path) ? blocks : agentFiles;
4989
- counts.set(verb, (counts.get(verb) ?? 0) + 1);
4990
- }
4991
- return {
4992
- summary: [
4993
- counted("block", blocks),
4994
- ...agentFiles.size > 0 ? [counted("agent file", agentFiles)] : []
4995
- ].join("; "),
4996
- others
4997
- };
4998
- }
4999
- function syncOutcome(result, opts = {}) {
5000
- const { others } = summariseSync(result.stdout);
5001
- const wrote = /^(updated|unchanged|stale)\s/m.test(result.stdout);
5002
- const stderr = result.stderr.split("\n").map((l) => l.trimEnd()).filter((l) => l.trim() !== "");
5003
- const hints = wrote ? [...others, ...stderr] : others;
5004
- if (opts.sshWorks === true && /Permission denied \(publickey\)/.test(result.stderr))
5005
- hints.push(...agentHintLines());
5006
- return {
5007
- code: wrote ? 0 : result.code,
5008
- hints,
5009
- errors: wrote ? [] : stderr
5010
- };
5011
- }
5012
- async function syncPinned(root, shown, fallback) {
5013
- const rnessDir = join17(root, ".rness");
5014
- const installed = await readPinnedCli(rnessDir);
5015
- if (installed === null) {
5016
- if (fallback !== void 0) return fallback();
5017
- process.stderr.write(
5018
- `@rness/cli is not installed in ${shown}/.rness; install in .rness/, then ${rnessCommand()} sync
5019
- `
5020
- );
5021
- return 1;
5022
- }
5023
- const pinned = join17(installed.dir, ...installed.bin.split("/"));
5024
- if (!await exists(pinned)) {
5025
- process.stderr.write(
5026
- `@rness/cli in ${shown}/.rness is not installed correctly (missing ${installed.bin}); reinstall in .rness/
5027
- `
5028
- );
5029
- return 1;
5030
- }
5031
- try {
5032
- const { stdout, stderr } = await execFileP4(
5033
- process.execPath,
5034
- [pinned, "sync", "--yes"],
5035
- {
5036
- cwd: root,
5037
- maxBuffer: 16 * 1024 * 1024,
5038
- env: childEnv(),
5039
- timeout: CHILD_DEADLINE_MS,
5040
- killSignal: "SIGKILL"
5041
- }
5042
- );
5043
- process.stdout.write(stdout);
5044
- if (stderr !== "") process.stderr.write(stderr);
5045
- return 0;
5046
- } catch (e) {
5047
- const err = e;
5048
- if (err.stdout !== void 0 && err.stdout !== "")
5049
- process.stdout.write(err.stdout);
5050
- if (err.stderr !== void 0 && err.stderr !== "")
5051
- process.stderr.write(err.stderr);
5052
- return typeof err.code === "number" ? err.code : 1;
5053
- }
5054
- }
5055
-
5056
5266
  // src/core/scaffold-git.ts
5057
5267
  import { execFile as execFile5 } from "child_process";
5058
5268
  import { mkdtemp, realpath as realpath2, rm as rm4, writeFile as writeFile3 } from "fs/promises";
@@ -7463,7 +7673,7 @@ async function writeOpened(file, opened) {
7463
7673
  }
7464
7674
 
7465
7675
  // src/pulse/plan.ts
7466
- var differs = (item, w) => item.title !== w.title || item.status !== w.status || w.statusField !== null && item.collectionStatus !== w.status || item.type !== w.type;
7676
+ var differs = (item, w) => item.title !== w.title || item.status !== w.status || w.statusField !== null && item.collectionStatus !== w.status || item.type !== w.type || item.sessions !== w.sessions;
7467
7677
  var rank = (i) => i.issue === null ? 2 : i.issue.open ? 0 : 1;
7468
7678
  var keptFirst = (a, b) => rank(a) - rank(b) || (a.issue?.number ?? 0) - (b.issue?.number ?? 0);
7469
7679
  var gone = (item) => item.issue === null ? { kind: "archive", id: item.id } : { kind: "close", id: item.id };
@@ -7849,13 +8059,16 @@ async function mark(opts) {
7849
8059
  return 0;
7850
8060
  }
7851
8061
  const wanted = new Set(opts.paths);
7852
- const documents2 = new Set(
7853
- (await statusTabs(c.rnessDir)).flatMap((t) => t.rows.map((r) => r.path))
8062
+ const documents2 = new Map(
8063
+ (await statusTabs(c.rnessDir)).flatMap(
8064
+ (t) => t.rows.map((r) => [r.path, r.status])
8065
+ )
7854
8066
  );
7855
- const onBoard = new Set(
7856
- items.flatMap((i) => i.path === null ? [] : [i.path])
8067
+ const onBoard = new Map(
8068
+ items.flatMap((i) => i.path === null ? [] : [[i.path, i.status]])
7857
8069
  );
7858
- if ([...wanted].some((p) => documents2.has(p) && !onBoard.has(p))) {
8070
+ const stale = (path) => documents2.has(path) && (!onBoard.has(path) || onBoard.get(path) !== documents2.get(path));
8071
+ if ([...wanted].some(stale)) {
7859
8072
  const synced = await oneSyncAtATime(
7860
8073
  key,
7861
8074
  () => syncBoard(c, board, plainUi, eachAdded),
@@ -8353,6 +8566,7 @@ export {
8353
8566
  defaultTransport,
8354
8567
  findWorkspace,
8355
8568
  reportError,
8569
+ findDelegate,
8356
8570
  BEGIN,
8357
8571
  END,
8358
8572
  BLOCK_SIZE_WARNING,
@@ -8367,7 +8581,6 @@ export {
8367
8581
  addCommand,
8368
8582
  renderMarkdown,
8369
8583
  contextCommand,
8370
- findDelegate,
8371
8584
  SCAFFOLD_FILES,
8372
8585
  scaffoldDir,
8373
8586
  copyScaffold,
@@ -8381,4 +8594,4 @@ export {
8381
8594
  OWN_COMMANDS,
8382
8595
  catchUp
8383
8596
  };
8384
- //# sourceMappingURL=chunk-4VTYSPZW.js.map
8597
+ //# sourceMappingURL=chunk-HGT7BIWK.js.map