@rness/cli 0.12.0 → 0.14.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.
@@ -139,6 +139,36 @@ async function changedPaths(dir, paths) {
139
139
  }
140
140
  return changed;
141
141
  }
142
+ async function seenPaths(dir, paths) {
143
+ const seen = /* @__PURE__ */ new Set();
144
+ for (const path of new Set(paths)) {
145
+ try {
146
+ const commit = await git(
147
+ [
148
+ "log",
149
+ "-1",
150
+ "--full-history",
151
+ "--format=%H",
152
+ "HEAD",
153
+ "--",
154
+ `:(literal)${path}`
155
+ ],
156
+ dir
157
+ );
158
+ if (commit.trim() !== "") seen.add(path);
159
+ } catch {
160
+ }
161
+ }
162
+ return seen;
163
+ }
164
+ async function gitPath(dir, name) {
165
+ try {
166
+ const path = (await git(["rev-parse", "--git-path", name], dir)).trim();
167
+ return path === "" ? null : resolve(dir, path);
168
+ } catch {
169
+ return null;
170
+ }
171
+ }
142
172
  async function pullFastForward(dir, credentials) {
143
173
  await git(["pull", "--ff-only", "--quiet"], dir, credentials);
144
174
  }
@@ -360,9 +390,9 @@ function readScopes(value) {
360
390
  scopes[name] = { path, extends: ext };
361
391
  }
362
392
  for (const [name, entry] of Object.entries(scopes)) {
363
- for (const target of entry.extends) {
364
- if (!Object.hasOwn(scopes, target))
365
- fail(`scope "${name}" extends unknown scope "${target}"`);
393
+ for (const target2 of entry.extends) {
394
+ if (!Object.hasOwn(scopes, target2))
395
+ fail(`scope "${name}" extends unknown scope "${target2}"`);
366
396
  }
367
397
  }
368
398
  return scopes;
@@ -456,7 +486,7 @@ function parseRepoSpec(spec, org, host) {
456
486
  }
457
487
 
458
488
  // src/version.ts
459
- var VERSION = "0.12.0";
489
+ var VERSION = "0.14.0";
460
490
 
461
491
  // src/core/pm.ts
462
492
  import { execFile as execFile2 } from "child_process";
@@ -493,6 +523,12 @@ function rnessRunner(pm, version) {
493
523
  return "yarn dlx @rness/cli";
494
524
  return "npx @rness/cli";
495
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
+ }
496
532
  function rnessCommand(env = process.env) {
497
533
  const agent = env["npm_config_user_agent"] ?? "";
498
534
  if (agent === "") return "rness";
@@ -599,8 +635,8 @@ async function collectMarkdown(dir) {
599
635
  }
600
636
 
601
637
  // src/core/scope.ts
602
- function isPrefix(prefix, target) {
603
- return prefix === target || target.startsWith(`${prefix}/`);
638
+ function isPrefix(prefix, target2) {
639
+ return prefix === target2 || target2.startsWith(`${prefix}/`);
604
640
  }
605
641
  function resolveScope(manifest, relDir) {
606
642
  let best = null;
@@ -624,7 +660,7 @@ function scopeChain(manifest, scope) {
624
660
  chain.push(name);
625
661
  stack.add(name);
626
662
  const entry = Object.hasOwn(manifest.scopes, name) ? manifest.scopes[name] : void 0;
627
- for (const target of entry?.extends ?? []) visit(target, stack);
663
+ for (const target2 of entry?.extends ?? []) visit(target2, stack);
628
664
  stack.delete(name);
629
665
  };
630
666
  visit(scope, /* @__PURE__ */ new Set());
@@ -1116,6 +1152,158 @@ function statusMarkdown(workspace, tabs) {
1116
1152
  return out.join("\n");
1117
1153
  }
1118
1154
 
1155
+ // src/pulse/body.ts
1156
+ import { createHash } from "crypto";
1157
+ import { posix as posix4 } from "path";
1158
+ var BODY_LIMIT = 65536;
1159
+ var blobUrl = (org, path) => `https://github.com/${org}/.rness/blob/main/${encodeURI(path)}`;
1160
+ var issueUrl = (org, number) => `https://github.com/${org}/.rness/issues/${number}`;
1161
+ function headerOf(path, org) {
1162
+ return `\`${path}\` \xB7 [on GitHub](${blobUrl(org, path)})`;
1163
+ }
1164
+ var digest = (text) => createHash("sha256").update(text, "utf8").digest("hex").slice(0, 12);
1165
+ var comment = (d) => `<!-- rness ${d} -->`;
1166
+ var TAIL = `
1167
+
1168
+ ${comment("0".repeat(12))}`.length;
1169
+ var LAST_LINE = /\n\n<!-- rness ([0-9a-f]{12}) -->\s*$/;
1170
+ function digestOf(body) {
1171
+ const text = body.replace(/\r\n?/g, "\n");
1172
+ const m = LAST_LINE.exec(text);
1173
+ const d = m?.[1];
1174
+ if (m === null || d === void 0) return null;
1175
+ return digest(text.slice(0, m.index)) === d ? d : null;
1176
+ }
1177
+ var OPENER = /^[ \t]*(`{3,}|~{3,})(.*)$/;
1178
+ var CLOSER = /^[ \t]*(`{3,}|~{3,})[ \t]*$/;
1179
+ function fenced(lines) {
1180
+ const code = [];
1181
+ let run3 = null;
1182
+ for (const line3 of lines) {
1183
+ if (run3 === null) {
1184
+ const m = OPENER.exec(line3);
1185
+ const fence2 = m?.[1];
1186
+ if (fence2 !== void 0 && !(fence2.startsWith("`") && (m?.[2] ?? "").includes("`")))
1187
+ run3 = fence2;
1188
+ code.push(run3 !== null);
1189
+ continue;
1190
+ }
1191
+ code.push(true);
1192
+ const fence = CLOSER.exec(line3)?.[1];
1193
+ if (fence !== void 0 && fence[0] === run3[0] && fence.length >= run3.length)
1194
+ run3 = null;
1195
+ }
1196
+ return code;
1197
+ }
1198
+ var TITLE = /^ {0,3}#(?:[ \t]|$)/;
1199
+ function withoutTitle(lines) {
1200
+ const code = fenced(lines);
1201
+ const at = lines.findIndex((line3, i) => code[i] !== true && TITLE.test(line3));
1202
+ if (at < 0) return [...lines];
1203
+ let end = at + 1;
1204
+ while (end < lines.length && (lines[end] ?? "").trim() === "") end++;
1205
+ return [...lines.slice(0, at), ...lines.slice(end)];
1206
+ }
1207
+ var SCHEME = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
1208
+ var IMAGE = /\.(?:png|jpe?g|gif|svg|webp|avif)$/i;
1209
+ var decoded = (path) => {
1210
+ try {
1211
+ return decodeURI(path);
1212
+ } catch {
1213
+ return path;
1214
+ }
1215
+ };
1216
+ function target(dest, s) {
1217
+ const raw = dest.startsWith("<") ? dest.slice(1, -1) : dest;
1218
+ if (raw === "" || SCHEME.test(raw) || raw.startsWith("//") || raw.startsWith("#"))
1219
+ return dest;
1220
+ const hash = raw.indexOf("#");
1221
+ const pathPart = hash < 0 ? raw : raw.slice(0, hash);
1222
+ const anchor = hash < 0 ? "" : raw.slice(hash);
1223
+ const file = decoded(
1224
+ posix4.normalize(
1225
+ pathPart.startsWith("/") ? pathPart.slice(1) : posix4.join(posix4.dirname(s.path), pathPart)
1226
+ )
1227
+ );
1228
+ const number = s.numbers.get(file);
1229
+ if (number !== void 0) return issueUrl(s.org, number);
1230
+ if (!s.files.has(file)) return dest;
1231
+ return `${blobUrl(s.org, file)}${IMAGE.test(file) ? "?raw=true" : ""}${anchor}`;
1232
+ }
1233
+ var PROTECTED = new RegExp(
1234
+ [
1235
+ /(?<!`)(`+)(?!`)[\s\S]*?(?<!`)\1(?!`)/.source,
1236
+ /<!--[\s\S]*?-->/.source,
1237
+ /<[a-zA-Z][a-zA-Z0-9+.-]*:[^\s<>]*>/.source,
1238
+ /^ {0,3}\[(?:[^\]\\\n]|\\.)+\]:[ \t]*(?<def><[^>\n]*>|\S+)/.source,
1239
+ /\]\([ \t]*(?<dest><[^>\n]*>|[^\s()]+)/.source,
1240
+ /\b(?:https?:\/\/|www\.)[^\s<>]*[^\s<>.,:;"')\]]/.source
1241
+ ].join("|"),
1242
+ "gm"
1243
+ );
1244
+ var MENTION = /(^|[^A-Za-z0-9`\\])@([A-Za-z0-9][A-Za-z0-9-]*)/g;
1245
+ var REFERENCE = /(^|[^A-Za-z0-9&#/])#(\d+)(?![A-Za-z0-9])/g;
1246
+ var ZWSP = "\u200B";
1247
+ var neutral = (text) => text.replace(MENTION, "$1\\@$2").replace(REFERENCE, `$1#${ZWSP}$2`);
1248
+ function inline(text, s) {
1249
+ let out = "";
1250
+ let from = 0;
1251
+ for (const m of text.matchAll(PROTECTED)) {
1252
+ const at = m.index ?? 0;
1253
+ out += neutral(text.slice(from, at));
1254
+ const dest = m.groups?.["dest"] ?? m.groups?.["def"];
1255
+ out += dest === void 0 ? m[0] : `${m[0].slice(0, m[0].length - dest.length)}${target(dest, s)}`;
1256
+ from = at + m[0].length;
1257
+ }
1258
+ return out + neutral(text.slice(from));
1259
+ }
1260
+ function rewrite(lines, s) {
1261
+ const code = fenced(lines);
1262
+ const out = [];
1263
+ let paragraph = [];
1264
+ const flush = () => {
1265
+ if (paragraph.length > 0) out.push(inline(paragraph.join("\n"), s));
1266
+ paragraph = [];
1267
+ };
1268
+ lines.forEach((line3, i) => {
1269
+ if (code[i] === true || line3.trim() === "") {
1270
+ flush();
1271
+ out.push(line3);
1272
+ } else paragraph.push(line3);
1273
+ });
1274
+ flush();
1275
+ return out;
1276
+ }
1277
+ function cut(text, rest) {
1278
+ const room = BODY_LIMIT - TAIL - 2 - rest.length;
1279
+ const lines = text.split("\n");
1280
+ const code = fenced(lines);
1281
+ let length = -1;
1282
+ let at = 1;
1283
+ for (let k = 1; k < lines.length; k++) {
1284
+ length += 1 + (lines[k - 1] ?? "").length;
1285
+ if (length > room) break;
1286
+ if (code[k] !== true && (lines[k] ?? "").trim() === "") at = k;
1287
+ }
1288
+ return `${lines.slice(0, at).join("\n").trimEnd()}
1289
+
1290
+ ${rest}`;
1291
+ }
1292
+ function issueBody(s) {
1293
+ const lines = withoutTitle(
1294
+ stripFrontMatter(s.text.replace(/\r\n?/g, "\n")).split("\n")
1295
+ );
1296
+ const content = rewrite(lines, s).join("\n").replace(/^(?:[ \t]*\n)+/, "").trimEnd();
1297
+ const header2 = headerOf(s.path, s.org);
1298
+ const whole = content === "" ? header2 : `${header2}
1299
+
1300
+ ${content}`;
1301
+ const text = whole.length + TAIL <= BODY_LIMIT ? whole : cut(whole, `The rest: ${blobUrl(s.org, s.path)}`);
1302
+ return `${text}
1303
+
1304
+ ${comment(digest(text))}`;
1305
+ }
1306
+
1119
1307
  // src/pulse/layout.ts
1120
1308
  var statusOf2 = (status2) => status2 === null || status2 === "?" ? null : status2;
1121
1309
  var LIFECYCLE = [
@@ -1142,8 +1330,8 @@ var STATUS_COLORS = {
1142
1330
  Accepted: "green",
1143
1331
  Implemented: "green",
1144
1332
  Completed: "green",
1145
- Rejected: "gray",
1146
- Superseded: "gray",
1333
+ Rejected: "red",
1334
+ Superseded: "yellow",
1147
1335
  Abandoned: "gray"
1148
1336
  };
1149
1337
  var COLLECTION_COLORS = {
@@ -1208,9 +1396,7 @@ function desiredOf(tabs, org) {
1208
1396
  (tab) => tab.rows.map((row) => ({
1209
1397
  path: row.path,
1210
1398
  title: `${row.id} \u2014 ${row.title}`,
1211
- body: `${row.path}
1212
-
1213
- https://github.com/${org}/.rness/blob/main/${row.path}`,
1399
+ body: headerOf(row.path, org),
1214
1400
  status: statusOf2(row.status),
1215
1401
  type: tab.label,
1216
1402
  statusField: fields2.get(tab.label)?.name ?? null
@@ -1219,6 +1405,7 @@ https://github.com/${org}/.rness/blob/main/${row.path}`,
1219
1405
  }
1220
1406
 
1221
1407
  // src/core/github.ts
1408
+ import { setTimeout as delay } from "timers/promises";
1222
1409
  var DEFAULT_GITHUB_API = "https://api.github.com";
1223
1410
  var MAX_PAGES = 50;
1224
1411
  var PER_PAGE = 100;
@@ -1353,6 +1540,39 @@ async function listRepositories(owner, options) {
1353
1540
  truncated: true
1354
1541
  };
1355
1542
  }
1543
+ var WAIT_BUDGET_MS = 6e5;
1544
+ var DEFAULT_WAIT_MS = 6e4;
1545
+ var MIN_WAIT_MS = 1e3;
1546
+ var RATE_LIMIT_OUTLASTED = "GitHub's rate limit outlasted the 10 minutes rness waits";
1547
+ function rateWait(sleep = (ms) => delay(ms)) {
1548
+ return { left: WAIT_BUDGET_MS, now: () => Date.now(), sleep };
1549
+ }
1550
+ var PROCESS_WAIT = rateWait();
1551
+ var RateLimitError = class extends Error {
1552
+ };
1553
+ var messageOf = (body) => {
1554
+ const m = body !== null && typeof body === "object" ? body["message"] : void 0;
1555
+ return typeof m === "string" ? m : "";
1556
+ };
1557
+ function rateDelay(page, now, limited = false) {
1558
+ const h = page.headers;
1559
+ const spent = h.get("x-ratelimit-remaining") === "0";
1560
+ const hit = limited || page.status === 429 || page.status === 403 && (h.has("retry-after") || spent || /rate limit/i.test(messageOf(page.body)));
1561
+ if (!hit) return null;
1562
+ const after = (h.get("retry-after") ?? "").trim();
1563
+ if (/^\d+$/.test(after)) return Number(after) * 1e3;
1564
+ const reset = (h.get("x-ratelimit-reset") ?? "").trim();
1565
+ if (spent && /^\d+$/.test(reset))
1566
+ return Math.max(0, Number(reset) * 1e3 - now);
1567
+ return DEFAULT_WAIT_MS;
1568
+ }
1569
+ async function waitOut(options, ms) {
1570
+ const wait = options.wait ?? PROCESS_WAIT;
1571
+ const spent = Math.max(ms, MIN_WAIT_MS);
1572
+ if (spent > wait.left) throw new RateLimitError(RATE_LIMIT_OUTLASTED);
1573
+ wait.left -= spent;
1574
+ await wait.sleep(spent);
1575
+ }
1356
1576
  function apiHeaders(token) {
1357
1577
  return {
1358
1578
  Accept: "application/vnd.github+json",
@@ -1375,23 +1595,34 @@ async function getJson(path, options) {
1375
1595
  const body = await res.json().catch(() => void 0);
1376
1596
  return { status: res.status, body };
1377
1597
  }
1378
- async function postJson(path, payload, options, explain = false) {
1598
+ async function postJson(path, payload, options, explain = false, limited = () => false) {
1379
1599
  const base = (options.apiBase ?? DEFAULT_GITHUB_API).replace(/\/+$/, "");
1380
- const page = await getPage(
1381
- `${base}${path}`,
1382
- { ...apiHeaders(options.token), "Content-Type": "application/json" },
1383
- options.timeoutMs ?? 15e3,
1384
- { method: "POST", body: JSON.stringify(payload), errorBody: explain }
1385
- );
1386
- if (page.status < 200 || page.status >= 300) {
1387
- const message2 = explain && page.status !== 401 && page.status !== 429 && page.body !== null && typeof page.body === "object" ? page.body.message : void 0;
1388
- if (typeof message2 === "string" && message2 !== "")
1389
- throw new GitHubMessageError(message2, {
1390
- cause: { status: page.status, path }
1391
- });
1392
- throw httpError(page, path);
1600
+ const clock = options.wait ?? PROCESS_WAIT;
1601
+ for (; ; ) {
1602
+ const page = await getPage(
1603
+ `${base}${path}`,
1604
+ { ...apiHeaders(options.token), "Content-Type": "application/json" },
1605
+ options.timeoutMs ?? 15e3,
1606
+ // A refusal's body says whether it is a rate limit. A timeout throws
1607
+ // above and is never retried: the write may have been made.
1608
+ { method: "POST", body: JSON.stringify(payload), errorBody: true }
1609
+ );
1610
+ const ok = page.status >= 200 && page.status < 300;
1611
+ const wait = rateDelay(page, clock.now(), ok && limited(page.body));
1612
+ if (wait !== null) {
1613
+ await waitOut(options, wait);
1614
+ continue;
1615
+ }
1616
+ if (!ok) {
1617
+ const message2 = explain && page.status !== 401 && page.status !== 429 && page.body !== null && typeof page.body === "object" ? page.body.message : void 0;
1618
+ if (typeof message2 === "string" && message2 !== "")
1619
+ throw new GitHubMessageError(message2, {
1620
+ cause: { status: page.status, path }
1621
+ });
1622
+ throw httpError(page, path);
1623
+ }
1624
+ return page.body;
1393
1625
  }
1394
- return page.body;
1395
1626
  }
1396
1627
  async function getScopes(options) {
1397
1628
  const base = (options.apiBase ?? DEFAULT_GITHUB_API).replace(/\/+$/, "");
@@ -1489,8 +1720,17 @@ async function createRepository(owner, name, options) {
1489
1720
  function isRecord2(v) {
1490
1721
  return v !== null && typeof v === "object";
1491
1722
  }
1723
+ var rateLimited = (body) => isRecord2(body) && Array.isArray(body["errors"]) && body["errors"].some(
1724
+ (e) => isRecord2(e) && e["type"] === "RATE_LIMITED"
1725
+ );
1492
1726
  async function graphqlRaw(query, variables, options) {
1493
- const body = await postJson("/graphql", { query, variables }, options, true);
1727
+ const body = await postJson(
1728
+ "/graphql",
1729
+ { query, variables },
1730
+ options,
1731
+ true,
1732
+ rateLimited
1733
+ );
1494
1734
  return isRecord2(body) ? body : {};
1495
1735
  }
1496
1736
  function firstError(answer) {
@@ -1505,6 +1745,15 @@ async function graphql(query, variables, options) {
1505
1745
  throw new Error("GitHub GraphQL answered without data");
1506
1746
  return answer.data;
1507
1747
  }
1748
+ async function graphqlFound(query, variables, options) {
1749
+ const answer = await graphqlRaw(query, variables, options);
1750
+ const errors = answer.errors ?? [];
1751
+ if (errors.length > 0 && errors.every((e) => e.type === "NOT_FOUND"))
1752
+ return null;
1753
+ const error = firstError(answer);
1754
+ if (error !== null) throw error;
1755
+ return answer.data ?? null;
1756
+ }
1508
1757
  async function orgId(org, o) {
1509
1758
  const d = await graphql(
1510
1759
  "query($login: String!) { organization(login: $login) { id } }",
@@ -1541,7 +1790,9 @@ async function findProject(org, number, o) {
1541
1790
  { login: org, number },
1542
1791
  o
1543
1792
  );
1544
- if (answer.errors?.every((e) => e.type === "NOT_FOUND") === true) return null;
1793
+ const errors = answer.errors ?? [];
1794
+ if (errors.length > 0 && errors.every((e) => e.type === "NOT_FOUND"))
1795
+ return null;
1545
1796
  const error = firstError(answer);
1546
1797
  if (error !== null) throw error;
1547
1798
  return answer.data?.organization?.projectV2 ?? null;
@@ -1621,7 +1872,7 @@ async function setOptions(field, options, o) {
1621
1872
  );
1622
1873
  return toField(d.updateProjectV2Field.projectV2Field);
1623
1874
  }
1624
- async function listItems(projectId, o) {
1875
+ async function listItems(projectId, o, { bodies = true } = {}) {
1625
1876
  const items = [];
1626
1877
  let cursor = null;
1627
1878
  for (; ; ) {
@@ -1639,8 +1890,23 @@ async function listItems(projectId, o) {
1639
1890
  id
1640
1891
  title
1641
1892
  }
1893
+ ... on Issue {
1894
+ id
1895
+ number
1896
+ title
1897
+ state
1898
+ ${bodies ? "body" : ""}
1899
+ repository {
1900
+ nameWithOwner
1901
+ }
1902
+ labels(first: 20) {
1903
+ nodes {
1904
+ name
1905
+ }
1906
+ }
1907
+ }
1642
1908
  }
1643
- fieldValues(first: 20) {
1909
+ fieldValues(first: 50) {
1644
1910
  nodes {
1645
1911
  ... on ProjectV2ItemFieldTextValue {
1646
1912
  text
@@ -1680,10 +1946,20 @@ async function listItems(projectId, o) {
1680
1946
  const value = v.text ?? v.name;
1681
1947
  if (field !== void 0 && value !== void 0) values2[field] = value;
1682
1948
  }
1949
+ const c = n.content;
1950
+ const issue = c?.id !== void 0 && c.number !== void 0 ? {
1951
+ id: c.id,
1952
+ number: c.number,
1953
+ open: c.state === "OPEN",
1954
+ repository: c.repository?.nameWithOwner ?? "",
1955
+ body: bodies ? c.body ?? "" : null,
1956
+ labels: (c.labels?.nodes ?? []).map((l) => l.name ?? "").filter((l) => l !== "")
1957
+ } : null;
1683
1958
  items.push({
1684
1959
  id: n.id,
1685
- draftId: n.content?.id ?? null,
1686
- title: n.content?.title ?? "",
1960
+ draftId: issue === null ? c?.id ?? null : null,
1961
+ issue,
1962
+ title: c?.title ?? "",
1687
1963
  archived: n.isArchived === true,
1688
1964
  values: values2
1689
1965
  });
@@ -1693,41 +1969,6 @@ async function listItems(projectId, o) {
1693
1969
  cursor = endCursor;
1694
1970
  }
1695
1971
  }
1696
- async function addDraft(projectId, title, body, o) {
1697
- const d = await graphql(
1698
- `
1699
- mutation ($projectId: ID!, $title: String!, $body: String) {
1700
- addProjectV2DraftIssue(
1701
- input: { projectId: $projectId, title: $title, body: $body }
1702
- ) {
1703
- projectItem {
1704
- id
1705
- }
1706
- }
1707
- }
1708
- `,
1709
- { projectId, title, body },
1710
- o
1711
- );
1712
- return d.addProjectV2DraftIssue.projectItem.id;
1713
- }
1714
- async function editDraft(draftId, title, body, o) {
1715
- await graphql(
1716
- `
1717
- mutation ($draftIssueId: ID!, $title: String!, $body: String) {
1718
- updateProjectV2DraftIssue(
1719
- input: { draftIssueId: $draftIssueId, title: $title, body: $body }
1720
- ) {
1721
- draftIssue {
1722
- id
1723
- }
1724
- }
1725
- }
1726
- `,
1727
- { draftIssueId: draftId, title, body },
1728
- o
1729
- );
1730
- }
1731
1972
  async function setValue(projectId, itemId, fieldId, value, o) {
1732
1973
  if (value === null) {
1733
1974
  await graphql(
@@ -1795,6 +2036,90 @@ async function archive(projectId, itemId, o) {
1795
2036
  o
1796
2037
  );
1797
2038
  }
2039
+ async function addItem(projectId, contentId, o) {
2040
+ const d = await graphql(
2041
+ `
2042
+ mutation ($projectId: ID!, $contentId: ID!) {
2043
+ addProjectV2ItemById(
2044
+ input: { projectId: $projectId, contentId: $contentId }
2045
+ ) {
2046
+ item {
2047
+ id
2048
+ }
2049
+ }
2050
+ }
2051
+ `,
2052
+ { projectId, contentId },
2053
+ o
2054
+ );
2055
+ return d.addProjectV2ItemById.item.id;
2056
+ }
2057
+ async function convertDraft(itemId, repositoryId, o) {
2058
+ const d = await graphql(
2059
+ `
2060
+ mutation ($itemId: ID!, $repositoryId: ID!) {
2061
+ convertProjectV2DraftIssueItemToIssue(
2062
+ input: { itemId: $itemId, repositoryId: $repositoryId }
2063
+ ) {
2064
+ item {
2065
+ id
2066
+ content {
2067
+ ... on Issue {
2068
+ id
2069
+ number
2070
+ }
2071
+ }
2072
+ }
2073
+ }
2074
+ }
2075
+ `,
2076
+ { itemId, repositoryId },
2077
+ o
2078
+ );
2079
+ const content = d.convertProjectV2DraftIssueItemToIssue.item.content;
2080
+ const id = content?.id;
2081
+ const number = content?.number;
2082
+ if (id === void 0 || number === void 0)
2083
+ throw new Error(`GitHub converted the item ${itemId} without an issue`);
2084
+ return { id, number };
2085
+ }
2086
+ async function linkedRepositories(projectId, o) {
2087
+ const d = await graphql(
2088
+ `
2089
+ query ($projectId: ID!) {
2090
+ node(id: $projectId) {
2091
+ ... on ProjectV2 {
2092
+ repositories(first: 100) {
2093
+ nodes {
2094
+ nameWithOwner
2095
+ }
2096
+ }
2097
+ }
2098
+ }
2099
+ }
2100
+ `,
2101
+ { projectId },
2102
+ o
2103
+ );
2104
+ return (d.node?.repositories.nodes ?? []).map((r) => r.nameWithOwner);
2105
+ }
2106
+ async function linkRepository(projectId, repositoryId, o) {
2107
+ await graphql(
2108
+ `
2109
+ mutation ($projectId: ID!, $repositoryId: ID!) {
2110
+ linkProjectV2ToRepository(
2111
+ input: { projectId: $projectId, repositoryId: $repositoryId }
2112
+ ) {
2113
+ repository {
2114
+ id
2115
+ }
2116
+ }
2117
+ }
2118
+ `,
2119
+ { projectId, repositoryId },
2120
+ o
2121
+ );
2122
+ }
1798
2123
  async function createView(org, number, view, o) {
1799
2124
  const body = {
1800
2125
  name: view.name,
@@ -1889,10 +2214,127 @@ async function renameView(viewId, name, visibleFieldIds, o) {
1889
2214
  );
1890
2215
  }
1891
2216
 
2217
+ // src/core/github-issues.ts
2218
+ async function repository(owner, name, label, o) {
2219
+ const d = await graphqlFound(
2220
+ `query($owner: String!, $name: String!, $label: String!) {
2221
+ repository(owner: $owner, name: $name) { id hasIssuesEnabled label(name: $label) { id } }
2222
+ }`,
2223
+ { owner, name, label },
2224
+ o
2225
+ );
2226
+ const r = d?.repository ?? null;
2227
+ return r === null ? null : { id: r.id, issues: r.hasIssuesEnabled, labelId: r.label?.id ?? null };
2228
+ }
2229
+ async function createLabel(owner, name, label, o) {
2230
+ const body = await postJson(
2231
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/labels`,
2232
+ label,
2233
+ o,
2234
+ true
2235
+ );
2236
+ const id = body !== null && typeof body === "object" ? body["node_id"] : void 0;
2237
+ if (typeof id !== "string")
2238
+ throw new Error(`GitHub created the label ${label.name} without a node id`);
2239
+ return id;
2240
+ }
2241
+ async function createIssue(repositoryId, issue, o) {
2242
+ const d = await graphql(
2243
+ `
2244
+ mutation (
2245
+ $repositoryId: ID!
2246
+ $title: String!
2247
+ $body: String
2248
+ $labelIds: [ID!]
2249
+ ) {
2250
+ createIssue(
2251
+ input: {
2252
+ repositoryId: $repositoryId
2253
+ title: $title
2254
+ body: $body
2255
+ labelIds: $labelIds
2256
+ }
2257
+ ) {
2258
+ issue {
2259
+ id
2260
+ number
2261
+ }
2262
+ }
2263
+ }
2264
+ `,
2265
+ { repositoryId, ...issue },
2266
+ o
2267
+ );
2268
+ return d.createIssue.issue;
2269
+ }
2270
+ async function updateIssue(id, change, o) {
2271
+ await graphql(
2272
+ `
2273
+ mutation ($id: ID!, $title: String, $body: String) {
2274
+ updateIssue(input: { id: $id, title: $title, body: $body }) {
2275
+ issue {
2276
+ id
2277
+ }
2278
+ }
2279
+ }
2280
+ `,
2281
+ { id, ...change },
2282
+ o
2283
+ );
2284
+ }
2285
+ async function closeIssue(id, o) {
2286
+ await graphql(
2287
+ `
2288
+ mutation ($id: ID!) {
2289
+ closeIssue(input: { issueId: $id, stateReason: NOT_PLANNED }) {
2290
+ issue {
2291
+ id
2292
+ }
2293
+ }
2294
+ }
2295
+ `,
2296
+ { id },
2297
+ o
2298
+ );
2299
+ }
2300
+ async function reopenIssue(id, o) {
2301
+ await graphql(
2302
+ `
2303
+ mutation ($id: ID!) {
2304
+ reopenIssue(input: { issueId: $id }) {
2305
+ issue {
2306
+ id
2307
+ }
2308
+ }
2309
+ }
2310
+ `,
2311
+ { id },
2312
+ o
2313
+ );
2314
+ }
2315
+ async function addLabels(id, labelIds, o) {
2316
+ await graphql(
2317
+ `
2318
+ mutation ($id: ID!, $labelIds: [ID!]!) {
2319
+ addLabelsToLabelable(input: { labelableId: $id, labelIds: $labelIds }) {
2320
+ clientMutationId
2321
+ }
2322
+ }
2323
+ `,
2324
+ { id, labelIds },
2325
+ o
2326
+ );
2327
+ }
2328
+
1892
2329
  // src/core/github-board.ts
1893
2330
  var PROJECT_TITLE = "Agent Pulse";
1894
2331
  var WORKING = "working";
1895
2332
  var WORKING_VIEW = "Working";
2333
+ var MEMORY = ".rness";
2334
+ var LABEL = "rness";
2335
+ var LABEL_COLOR = "5319e7";
2336
+ var LABEL_DESCRIPTION = "A document of .rness, on Agent Pulse";
2337
+ var labelled = (labels) => labels.some((l) => l.toLowerCase() === LABEL.toLowerCase());
1896
2338
  var STATUS_FIELD = "Status";
1897
2339
  var ALL_VIEW = "All";
1898
2340
  var DEFAULT_VIEW = "View 1";
@@ -1921,6 +2363,8 @@ var orderedFirst = (wanted, all) => {
1921
2363
  var GitHubBoards = class {
1922
2364
  #o;
1923
2365
  #cache = /* @__PURE__ */ new Map();
2366
+ /** Each organization's `.rness`, as first read: its id, its Issues, its label. */
2367
+ #memories = /* @__PURE__ */ new Map();
1924
2368
  constructor(options) {
1925
2369
  this.#o = options;
1926
2370
  }
@@ -1943,6 +2387,23 @@ var GitHubBoards = class {
1943
2387
  cache.fields ??= await fields(cache.projectId, this.#o);
1944
2388
  return cache.fields;
1945
2389
  }
2390
+ async #memory(org) {
2391
+ const known = this.#memories.get(org);
2392
+ if (known !== void 0) return known;
2393
+ const found = await repository(org, MEMORY, LABEL, this.#o);
2394
+ if (found === null)
2395
+ throw new Error(
2396
+ `the pulse needs ${org}/${MEMORY} on GitHub: it is not there, or this login cannot see it`
2397
+ );
2398
+ this.#memories.set(org, found);
2399
+ return found;
2400
+ }
2401
+ async checkIssues(org) {
2402
+ if (!(await this.#memory(org)).issues)
2403
+ throw new Error(
2404
+ `the pulse needs Issues on ${org}/${MEMORY}: turn them on in its Settings`
2405
+ );
2406
+ }
1946
2407
  /** The project only: the caller declares it before `ensureLayout` builds it. */
1947
2408
  async createBoard(org) {
1948
2409
  const project = await createProject(
@@ -2079,89 +2540,186 @@ var GitHubBoards = class {
2079
2540
  this.#o
2080
2541
  );
2081
2542
  }
2543
+ const memory = await this.#memory(board.org);
2544
+ if (memory.labelId === null) {
2545
+ memory.labelId = await createLabel(
2546
+ board.org,
2547
+ MEMORY,
2548
+ { name: LABEL, color: LABEL_COLOR, description: LABEL_DESCRIPTION },
2549
+ this.#o
2550
+ );
2551
+ added.push(`label ${LABEL}`);
2552
+ }
2553
+ const repository2 = `${board.org}/${MEMORY}`;
2554
+ const linked = await linkedRepositories(cache.projectId, this.#o);
2555
+ if (!linked.some((r) => r.toLowerCase() === repository2.toLowerCase())) {
2556
+ await linkRepository(cache.projectId, memory.id, this.#o);
2557
+ added.push(`link ${repository2}`);
2558
+ }
2082
2559
  cache.fields = [status2, collection, ...own.values(), agent, session, path];
2083
2560
  return added;
2084
2561
  }
2085
- async items(board) {
2562
+ async items(board, options = {}) {
2086
2563
  const cache = await this.#cacheOf(board);
2087
- const raw = await listItems(cache.projectId, this.#o);
2564
+ const raw = await listItems(cache.projectId, this.#o, options);
2088
2565
  cache.known = new Map(raw.map((r) => [r.id, r]));
2089
- return raw.filter((r) => !r.archived).map((r) => ({
2090
- id: r.id,
2091
- // No longer a draft issue (converted by hand): the team's, like an
2092
- // item without Path — the plan leaves it alone (spec 0017 §4).
2093
- path: r.draftId === null ? null : r.values["Path"] ?? null,
2094
- title: r.title,
2095
- status: r.values[STATUS_FIELD] ?? null,
2096
- collectionStatus: r.values[statusFieldName(r.values[COLLECTION_FIELD] ?? "")] ?? null,
2097
- type: r.values[COLLECTION_FIELD] ?? null,
2098
- agent: r.values["Agent"] ?? null,
2099
- session: r.values["Session"] ?? null
2100
- }));
2566
+ const memory = `${board.org}/${MEMORY}`.toLowerCase();
2567
+ return raw.filter((r) => !r.archived).map((r) => {
2568
+ const issue = r.issue !== null && r.issue.repository.toLowerCase() === memory ? r.issue : null;
2569
+ const ours = r.draftId !== null || issue !== null;
2570
+ return {
2571
+ id: r.id,
2572
+ path: ours ? r.values["Path"] ?? null : null,
2573
+ issue: issue === null ? null : {
2574
+ number: issue.number,
2575
+ open: issue.open,
2576
+ labelled: labelled(issue.labels),
2577
+ body: issue.body
2578
+ },
2579
+ title: r.title,
2580
+ status: r.values[STATUS_FIELD] ?? null,
2581
+ collectionStatus: r.values[statusFieldName(r.values[COLLECTION_FIELD] ?? "")] ?? null,
2582
+ type: r.values[COLLECTION_FIELD] ?? null,
2583
+ agent: r.values["Agent"] ?? null,
2584
+ session: r.values["Session"] ?? null
2585
+ };
2586
+ });
2101
2587
  }
2102
2588
  async #set(board, itemId, name, value) {
2103
2589
  const cache = await this.#cacheOf(board);
2104
2590
  const field = (await this.#fields(board)).find((f) => f.name === name);
2105
2591
  if (field === void 0) throw new Error(`the board has no field ${name}`);
2106
- if (value === null) {
2107
- await setValue(cache.projectId, itemId, field.id, null, this.#o);
2108
- return;
2109
- }
2110
- if (field.options === null) {
2111
- await setValue(
2112
- cache.projectId,
2113
- itemId,
2114
- field.id,
2115
- { text: value },
2116
- this.#o
2117
- );
2118
- return;
2592
+ let sent = null;
2593
+ if (value !== null && field.options === null) sent = { text: value };
2594
+ else if (value !== null) {
2595
+ const option = field.options?.find((opt) => opt.name === value);
2596
+ if (option === void 0)
2597
+ throw new Error(`the field ${name} has no option ${value}`);
2598
+ sent = { optionId: option.id };
2119
2599
  }
2120
- const option = field.options.find((opt) => opt.name === value);
2121
- if (option === void 0)
2122
- throw new Error(`the field ${name} has no option ${value}`);
2123
- await setValue(
2124
- cache.projectId,
2125
- itemId,
2126
- field.id,
2127
- { optionId: option.id },
2128
- this.#o
2129
- );
2600
+ await setValue(cache.projectId, itemId, field.id, sent, this.#o);
2601
+ const known = cache.known.get(itemId);
2602
+ if (known === void 0) return;
2603
+ if (value === null) delete known.values[name];
2604
+ else known.values[name] = value;
2130
2605
  }
2131
2606
  async apply(board, step2) {
2132
- if (step2.kind === "unchanged") return;
2607
+ if (step2.kind === "unchanged") return null;
2133
2608
  const cache = await this.#cacheOf(board);
2134
- if (step2.kind === "archive") {
2609
+ if (step2.kind === "create") return this.#create(board, cache, step2.want);
2610
+ const known = await this.#known(board, step2.id);
2611
+ if (step2.kind === "archive" || step2.kind === "close") {
2612
+ if (step2.kind === "close" && known.issue?.open === true)
2613
+ await closeIssue(known.issue.id, this.#o);
2135
2614
  await archive(cache.projectId, step2.id, this.#o);
2136
2615
  cache.known.delete(step2.id);
2137
- return;
2616
+ return null;
2617
+ }
2618
+ if (step2.kind === "body") {
2619
+ const issue2 = this.#issueOf(known);
2620
+ await updateIssue(issue2.id, { body: step2.body }, this.#o);
2621
+ issue2.body = step2.body;
2622
+ return null;
2623
+ }
2624
+ const labelId = await this.#labelId(board);
2625
+ if (step2.kind === "convert") {
2626
+ const made = await convertDraft(
2627
+ step2.id,
2628
+ (await this.#memory(board.org)).id,
2629
+ this.#o
2630
+ );
2631
+ known.draftId = null;
2632
+ known.issue = {
2633
+ ...made,
2634
+ open: true,
2635
+ repository: `${board.org}/${MEMORY}`,
2636
+ body: "",
2637
+ labels: []
2638
+ };
2639
+ await this.#issue(known, step2.want, labelId);
2640
+ await this.#values(board, step2.id, step2.want);
2641
+ return { id: step2.id, number: made.number };
2642
+ }
2643
+ const issue = this.#issueOf(known);
2644
+ if (step2.reopen) {
2645
+ await reopenIssue(issue.id, this.#o);
2646
+ issue.open = true;
2647
+ }
2648
+ await this.#issue(known, step2.want, labelId);
2649
+ await this.#values(board, step2.id, step2.want);
2650
+ return null;
2651
+ }
2652
+ /** A new document's issue: labelled, the first line as body, added to the board, Path first. */
2653
+ async #create(board, cache, want) {
2654
+ const memory = await this.#memory(board.org);
2655
+ const labelId = await this.#labelId(board);
2656
+ const made = await createIssue(
2657
+ memory.id,
2658
+ { title: want.title, body: want.body, labelIds: [labelId] },
2659
+ this.#o
2660
+ );
2661
+ const id = await addItem(cache.projectId, made.id, this.#o);
2662
+ cache.known.set(id, {
2663
+ id,
2664
+ draftId: null,
2665
+ issue: {
2666
+ ...made,
2667
+ open: true,
2668
+ repository: `${board.org}/${MEMORY}`,
2669
+ body: want.body,
2670
+ labels: [LABEL]
2671
+ },
2672
+ title: want.title,
2673
+ archived: false,
2674
+ values: {}
2675
+ });
2676
+ await this.#values(board, id, want);
2677
+ return { id, number: made.number };
2678
+ }
2679
+ /** The label and the title an issue of the board carries. */
2680
+ async #issue(known, want, labelId) {
2681
+ const issue = this.#issueOf(known);
2682
+ if (!labelled(issue.labels)) {
2683
+ await addLabels(issue.id, [labelId], this.#o);
2684
+ issue.labels.push(LABEL);
2685
+ }
2686
+ if (known.title !== want.title) {
2687
+ await updateIssue(issue.id, { title: want.title }, this.#o);
2688
+ known.title = want.title;
2138
2689
  }
2139
- const { want } = step2;
2140
- const values2 = [
2690
+ }
2691
+ /** Path first — an issue on the board without it is the team's — then the fields, each only when it differs. */
2692
+ async #values(board, id, want) {
2693
+ const values2 = (await this.#known(board, id)).values;
2694
+ const wanted = [
2141
2695
  ["Path", want.path],
2142
2696
  [COLLECTION_FIELD, want.type],
2143
2697
  [STATUS_FIELD, want.status],
2144
2698
  ...want.statusField === null ? [] : [[want.statusField, want.status]]
2145
2699
  ];
2146
- if (step2.kind === "create") {
2147
- const id = await addDraft(
2148
- cache.projectId,
2149
- want.title,
2150
- want.body,
2151
- this.#o
2700
+ for (const [name, value] of wanted)
2701
+ if ((values2[name] ?? null) !== value)
2702
+ await this.#set(board, id, name, value);
2703
+ }
2704
+ async #known(board, id) {
2705
+ const cache = await this.#cacheOf(board);
2706
+ if (!cache.known.has(id)) await this.items(board);
2707
+ const known = cache.known.get(id);
2708
+ if (known === void 0) throw new Error(`the board has no item ${id}`);
2709
+ return known;
2710
+ }
2711
+ #issueOf(known) {
2712
+ if (known.issue === null)
2713
+ throw new Error(`the board's item ${known.id} is no issue`);
2714
+ return known.issue;
2715
+ }
2716
+ async #labelId(board) {
2717
+ const id = (await this.#memory(board.org)).labelId;
2718
+ if (id === null)
2719
+ throw new Error(
2720
+ `${board.org}/${MEMORY} has no label ${LABEL}: run rness pulse sync`
2152
2721
  );
2153
- for (const [name, value] of values2)
2154
- if (value !== null) await this.#set(board, id, name, value);
2155
- return;
2156
- }
2157
- if (!cache.known.has(step2.id)) await this.items(board);
2158
- const known = cache.known.get(step2.id);
2159
- if (known?.draftId == null)
2160
- throw new Error(`the board has no draft item ${step2.id}`);
2161
- await editDraft(known.draftId, want.title, want.body, this.#o);
2162
- for (const [name, value] of values2)
2163
- if ((known.values[name] ?? null) !== value)
2164
- await this.#set(board, step2.id, name, value);
2722
+ return id;
2165
2723
  }
2166
2724
  async mark(board, itemIds, session) {
2167
2725
  for (const id of itemIds) {
@@ -2176,6 +2734,7 @@ var GITHUB_HTTPS = "https://github.com/";
2176
2734
  var GitHubOAuthProvider = class {
2177
2735
  #token;
2178
2736
  #apiBase;
2737
+ #wait;
2179
2738
  #login;
2180
2739
  #boardClient;
2181
2740
  get authenticated() {
@@ -2184,11 +2743,16 @@ var GitHubOAuthProvider = class {
2184
2743
  constructor(options) {
2185
2744
  this.#token = options.token;
2186
2745
  this.#apiBase = options.apiBase;
2746
+ this.#wait = options.wait;
2187
2747
  }
2188
2748
  #api(token) {
2189
- return this.#apiBase === void 0 ? { token } : { token, apiBase: this.#apiBase };
2190
- }
2191
- identity() {
2749
+ return {
2750
+ token,
2751
+ ...this.#apiBase === void 0 ? {} : { apiBase: this.#apiBase },
2752
+ ...this.#wait === void 0 ? {} : { wait: this.#wait }
2753
+ };
2754
+ }
2755
+ identity() {
2192
2756
  this.#login ??= (async () => {
2193
2757
  const t = this.#token;
2194
2758
  if (t === null) return null;
@@ -2240,6 +2804,9 @@ var GitHubOAuthProvider = class {
2240
2804
  return this.#boardClient;
2241
2805
  }
2242
2806
  // The board methods are async so that a missing login rejects, never throws.
2807
+ async checkIssues(org) {
2808
+ return this.#boards().checkIssues(org);
2809
+ }
2243
2810
  async createBoard(org) {
2244
2811
  return this.#boards().createBoard(org);
2245
2812
  }
@@ -2249,8 +2816,8 @@ var GitHubOAuthProvider = class {
2249
2816
  async ensureLayout(board, layout) {
2250
2817
  return this.#boards().ensureLayout(board, layout);
2251
2818
  }
2252
- async items(board) {
2253
- return this.#boards().items(board);
2819
+ async items(board, options) {
2820
+ return this.#boards().items(board, options);
2254
2821
  }
2255
2822
  async apply(board, step2) {
2256
2823
  return this.#boards().apply(board, step2);
@@ -2263,16 +2830,17 @@ var GitHubOAuthProvider = class {
2263
2830
  return { username: "x-access-token", password: this.#token.token };
2264
2831
  }
2265
2832
  };
2266
- async function githubProvider(apiBase) {
2833
+ async function githubProvider(apiBase, wait) {
2267
2834
  return new GitHubOAuthProvider({
2268
2835
  token: await resolveToken(),
2269
- ...apiBase === void 0 ? {} : { apiBase }
2836
+ ...apiBase === void 0 ? {} : { apiBase },
2837
+ ...wait === void 0 ? {} : { wait }
2270
2838
  });
2271
2839
  }
2272
2840
 
2273
2841
  // src/core/repos.ts
2274
2842
  import { glob as glob2, readFile as readFile5, rm as rm3 } from "fs/promises";
2275
- import { join as join8, posix as posix4, sep as sep2 } from "path";
2843
+ import { join as join8, posix as posix5, sep as sep2 } from "path";
2276
2844
  async function addRepository(input) {
2277
2845
  const { name, url } = parseRepoSpec(input.spec, input.org, input.host);
2278
2846
  const dir = join8(input.root, "org", name);
@@ -2352,8 +2920,8 @@ async function workspaceDirs(repoDir) {
2352
2920
  const dirs = /* @__PURE__ */ new Set();
2353
2921
  for (const pattern of patterns) {
2354
2922
  for await (const match of glob2(pattern, { cwd: repoDir })) {
2355
- const rel = match.split(sep2).join(posix4.sep);
2356
- if (await exists(join8(repoDir, ...rel.split(posix4.sep), "package.json")))
2923
+ const rel = match.split(sep2).join(posix5.sep);
2924
+ if (await exists(join8(repoDir, ...rel.split(posix5.sep), "package.json")))
2357
2925
  dirs.add(rel);
2358
2926
  }
2359
2927
  }
@@ -2649,8 +3217,56 @@ function reportError(e) {
2649
3217
  return 1;
2650
3218
  }
2651
3219
 
3220
+ // src/core/delegate.ts
3221
+ import { readFile as readFile6 } from "fs/promises";
3222
+ import { join as join10 } from "path";
3223
+ var DEFAULT_BIN = "dist/bin/rness.js";
3224
+ var PINNED_BIN = `node_modules/@rness/cli/${DEFAULT_BIN}`;
3225
+ async function readPinnedCli(rnessDir) {
3226
+ const dir = join10(rnessDir, "node_modules", "@rness", "cli");
3227
+ let pkg;
3228
+ try {
3229
+ pkg = JSON.parse(await readFile6(join10(dir, "package.json"), "utf8"));
3230
+ } catch {
3231
+ return null;
3232
+ }
3233
+ const declared2 = pkg.bin;
3234
+ let bin = DEFAULT_BIN;
3235
+ if (typeof declared2 === "string") bin = declared2;
3236
+ else if (declared2 !== null && typeof declared2 === "object") {
3237
+ const named = declared2["rness"];
3238
+ if (typeof named === "string") bin = named;
3239
+ }
3240
+ return {
3241
+ dir,
3242
+ bin,
3243
+ version: typeof pkg.version === "string" ? pkg.version : null
3244
+ };
3245
+ }
3246
+ async function pinnedBinPath(rnessDir) {
3247
+ const pinned = await readPinnedCli(rnessDir);
3248
+ if (pinned === null) return null;
3249
+ return join10(pinned.dir, ...pinned.bin.split("/"));
3250
+ }
3251
+ async function findDelegate(cwd, ownVersion) {
3252
+ let root;
3253
+ let rnessDir;
3254
+ try {
3255
+ const ws = await findWorkspace(cwd);
3256
+ root = ws.root;
3257
+ rnessDir = ws.rnessDir;
3258
+ } catch {
3259
+ return null;
3260
+ }
3261
+ const pinned = await readPinnedCli(rnessDir);
3262
+ if (pinned === null) return null;
3263
+ const { version } = pinned;
3264
+ if (version === null || version === ownVersion) return null;
3265
+ return { entry: join10(pinned.dir, "dist", "index.js"), version, root };
3266
+ }
3267
+
2652
3268
  // src/core/block.ts
2653
- import { createHash } from "crypto";
3269
+ import { createHash as createHash2 } from "crypto";
2654
3270
  var BEGIN = "<!-- BEGIN rness -->";
2655
3271
  var END = "<!-- END rness -->";
2656
3272
  var BLOCK_SIZE_WARNING = 32 * 1024;
@@ -2659,7 +3275,7 @@ function toLf(s) {
2659
3275
  return s.replace(/\r\n?/g, "\n");
2660
3276
  }
2661
3277
  function blockHash(body) {
2662
- return createHash("sha256").update(toLf(body), "utf8").digest("hex").slice(0, 12);
3278
+ return createHash2("sha256").update(toLf(body), "utf8").digest("hex").slice(0, 12);
2663
3279
  }
2664
3280
  function intro(scope, org, depth) {
2665
3281
  if (scope === null) {
@@ -2713,7 +3329,7 @@ function isCurrentBlock(blockLines, freshHash) {
2713
3329
  }
2714
3330
 
2715
3331
  // src/core/merge.ts
2716
- import { join as join10 } from "path";
3332
+ import { join as join11 } from "path";
2717
3333
  function findBlock(lines) {
2718
3334
  const begins = [];
2719
3335
  const ends = [];
@@ -2772,7 +3388,7 @@ function claudeMdAtRoot(existing, block) {
2772
3388
  return merged.ok ? { ...merged, changed: merged.text !== existing } : merged;
2773
3389
  }
2774
3390
  async function ensureClaudeMd(dir) {
2775
- const file = join10(dir, "CLAUDE.md");
3391
+ const file = join11(dir, "CLAUDE.md");
2776
3392
  if (await isSymlink(file)) return "unchanged";
2777
3393
  const existing = await readOrNull(file);
2778
3394
  if (existing === null) {
@@ -2787,14 +3403,222 @@ async function ensureClaudeMd(dir) {
2787
3403
  }
2788
3404
 
2789
3405
  // src/commands/sync.ts
2790
- import { join as join12 } from "path";
3406
+ import { join as join14 } from "path";
2791
3407
 
2792
3408
  // src/core/agent-targets.ts
2793
3409
  import { mkdir as mkdir2 } from "fs/promises";
2794
- import { dirname as dirname4, join as join11 } from "path";
3410
+ import { dirname as dirname4, join as join13 } from "path";
3411
+
3412
+ // src/core/claude-skills.ts
3413
+ var PLUGIN = ".claude/skills/rness";
3414
+ var PLUGIN_JSON = `${JSON.stringify(
3415
+ {
3416
+ name: "rness",
3417
+ description: "The rness workspace in Claude Code: /rness:status, /rness:adr, /rness:spec, /rness:plan, /rness:done."
3418
+ },
3419
+ null,
3420
+ 2
3421
+ )}
3422
+ `;
3423
+ function statusSkill(rness, pm) {
3424
+ const command = `node "\${CLAUDE_PROJECT_DIR}/${rness}/${PINNED_BIN}" status --cwd "\${CLAUDE_PROJECT_DIR}"`;
3425
+ return `---
3426
+ name: status
3427
+ description: The status of every decision, specification, plan and other tracked document of the rness workspace, one table per directory. Read-only.
3428
+ argument-hint: '[tab]'
3429
+ disable-model-invocation: true
3430
+ allowed-tools: Bash(${command})
3431
+ ---
3432
+
3433
+ !\`${command}\`
3434
+
3435
+ Show the output above to the user as it is: the heading and the tables,
3436
+ nothing added, nothing summarised, no other tool. Arguments: \`$ARGUMENTS\`.
3437
+ When they name a tab, show only that tab's section. If the output says the
3438
+ module cannot be found, say instead that ${rness} is not installed next to
3439
+ this repository.
3440
+
3441
+ End with this line: _For the view with tabs and scrolling: here, Ctrl+Z,
3442
+ then \`npx @rness/cli status\` (q to close), then \`fg\`; or in another
3443
+ terminal, from the workspace's \`.rness/\`: \`${localRunner(pm)} rness status\`._
3444
+ `;
3445
+ }
3446
+ var LIFECYCLE2 = [
3447
+ {
3448
+ name: "adr",
3449
+ 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.",
3450
+ hint: "[subject | NNNN]",
3451
+ tab: "adr",
3452
+ body: (rness) => `You record a decision of this workspace as an ADR in \`${rness}/adr/\`. The table
3453
+ above lists the existing ADRs, newest first. Arguments: \`$ARGUMENTS\`.
3454
+
3455
+ Before writing:
3456
+
3457
+ 1. If the developer did not ask for an ADR in so many words, propose one in
3458
+ a single line \u2014 its title, and why the choice is hard to reverse \u2014 and
3459
+ wait for a yes.
3460
+ 2. Read \`${rness}/CONVENTIONS.md\` and \`${rness}/adr/0000-template.md\`: they hold the
3461
+ rules and the sections; this skill is only the procedure.
3462
+ 3. Arguments naming an ADR (\`NNNN\`): reopen that file. An \`Accepted\` ADR is
3463
+ never edited: offer a new ADR that supersedes it.
3464
+ 4. Otherwise the subject is the arguments, else the decision just
3465
+ discussed. Context, alternatives or consequences missing from the
3466
+ conversation: ask for them, one question at a time. Never invent them.
3467
+
3468
+ Writing:
3469
+
3470
+ - \`${rness}/adr/NNNN-<slug>.md\`, NNNN the highest number above plus one.
3471
+ - The template's front matter and sections, filled in, its guidance lines
3472
+ removed: \`date\` today, \`status: Proposed\`, \`repo\` the repositories under
3473
+ \`org/\` it affects.
3474
+ - An ADR it replaces is named in Context. That one becomes \`Superseded\`,
3475
+ with \`superseded_by:\`, only once the new ADR is \`Accepted\`.
3476
+ - The rness hook checks every edit under \`${rness}/\`: fix what it reports.
3477
+
3478
+ After writing: give the path and the Decision section, and say the ADR
3479
+ stays \`Proposed\` until the developer accepts it. Change a status only when
3480
+ the developer says so. Do not commit.
3481
+ `
3482
+ },
3483
+ {
3484
+ name: "spec",
3485
+ 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.",
3486
+ hint: "[subject | NNNN]",
3487
+ tab: "specs",
3488
+ body: (rness) => `You write a specification of this workspace in \`${rness}/specs/\`. The table above
3489
+ lists the existing ones, newest first. Arguments: \`$ARGUMENTS\`.
3490
+
3491
+ Before writing:
3492
+
3493
+ 1. If the developer did not ask for a specification in so many words,
3494
+ propose one in a single line and wait for a yes.
3495
+ 2. Read \`${rness}/CONVENTIONS.md\`, then the most recent specification above: match
3496
+ its front matter, sections and tone.
3497
+ 3. Arguments naming a specification (\`NNNN\`): reopen it. One that is
3498
+ \`Implemented\`, \`Superseded\` or \`Rejected\` is not edited: offer a new one.
3499
+ 4. Otherwise the subject is the arguments, else the work just discussed.
3500
+ The problem, what is in and out of scope, the ADRs it rests on: ask what
3501
+ the conversation does not say, one question at a time.
3502
+
3503
+ Writing:
3504
+
3505
+ - \`${rness}/specs/NNNN-<slug>.md\`, NNNN the highest number above plus one.
3506
+ - \`date\` and \`updated\` today, \`status: Draft\`, \`repo\`, and \`adr:\` linking
3507
+ the decisions it rests on \u2014 or the rationale in the body when none does.
3508
+ - No specification yet to follow: Summary, Scope, Out of scope,
3509
+ Alternatives considered, To verify, Tests.
3510
+ - What was checked in this session (a document read, a command run) is
3511
+ written as verified, with the date; everything else as a proposal or an
3512
+ open question.
3513
+ - The rness hook checks every edit under \`${rness}/\`: fix what it reports.
3514
+
3515
+ After writing: give the path and the summary. It stays \`Draft\` until the
3516
+ developer moves it (\`Proposed\` for review, \`Approved\` to plan it). No plan
3517
+ here: \`/rness:plan NNNN\` once approved. Do not commit.
3518
+ `
3519
+ },
3520
+ {
3521
+ name: "plan",
3522
+ 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.",
3523
+ hint: "<spec NNNN>",
3524
+ tab: "plans",
3525
+ body: (rness) => `You turn a specification into a plan in \`${rness}/plans/\`. The table above lists
3526
+ the existing plans, newest first. Arguments, the specification:
3527
+ \`$ARGUMENTS\`.
3528
+
3529
+ Before writing:
3530
+
3531
+ 1. If the developer did not ask for a plan in so many words, propose one in
3532
+ a single line and wait for a yes.
3533
+ 2. No specification named: ask which. Read it, \`${rness}/CONVENTIONS.md\`, and the
3534
+ most recent plan above: match its front matter and sections.
3535
+ 3. A specification that is not \`Approved\`: say so, and plan it only if the
3536
+ developer confirms. A plan of it that is neither \`Completed\` nor
3537
+ \`Abandoned\`: reopen it rather than write a second.
3538
+ 4. Read the code and the repositories the specification touches: every
3539
+ task names real files and real commands.
3540
+
3541
+ Writing:
3542
+
3543
+ - \`${rness}/plans/NNNN-<slug>.md\`, NNNN the highest plan number above plus one \u2014
3544
+ not the specification's.
3545
+ - \`date\` and \`updated\` today, \`status: Draft\`, \`repo\`, \`spec:\` linking the
3546
+ specification.
3547
+ - Tasks in order, as checkboxes. Each gives the files, what changes, and
3548
+ the command that proves it (a test, a build, a run). The last tasks:
3549
+ the documentation the work makes inaccurate, then the release when the
3550
+ repository has one.
3551
+ - Where the plan decides something the specification leaves open, a
3552
+ section says so.
3553
+ - The rness hook checks every edit under \`${rness}/\`: fix what it reports.
3554
+
3555
+ After writing: give the path and the list of tasks. It becomes \`Ready\` when
3556
+ the developer agrees. Do not commit.
3557
+ `
3558
+ },
3559
+ {
3560
+ name: "done",
3561
+ 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.",
3562
+ hint: "[plan NNNN]",
3563
+ tab: "plans",
3564
+ body: (rness) => `You close a piece of work: its plan in \`${rness}/plans/\`, its specification, and
3565
+ what they made inaccurate. The table above lists the plans, newest first.
3566
+ Arguments: \`$ARGUMENTS\`.
3567
+
3568
+ 1. The plan: the one named, else the one \`In progress\`. Several or none:
3569
+ ask.
3570
+ 2. If the developer did not ask to close it in so many words, propose it in
3571
+ a single line and wait for a yes.
3572
+ 3. Read the plan, its specification and \`${rness}/CONVENTIONS.md\`.
3573
+ 4. Evidence first. For each task, what shows it done: a test, a build, a
3574
+ command's output in this session. A task without evidence: run its
3575
+ check now, or report it. Never mark a task done on belief.
3576
+ 5. All tasks proven:
3577
+ - the plan: \`status: Completed\`, \`updated\` today, the checkboxes of the
3578
+ proven tasks ticked, and a section \`Verification (YYYY-MM-DD)\` \u2014 what
3579
+ ran, what it showed, what could not run and why;
3580
+ - the specification: \`Implemented\` once every plan of it is
3581
+ \`Completed\`; \`updated\` today;
3582
+ - the smallest set of documents the work makes inaccurate: \`${rness}/docs/\`,
3583
+ the READMEs of the repositories it changed.
3584
+ 6. A task not proven: the plan stays \`In progress\` \u2014 or \`Blocked\`, the
3585
+ blocker written in it. Say what remains.
3586
+ 7. The rness hook checks every edit under \`${rness}/\`: fix what it reports.
3587
+
3588
+ After: list the files changed and the statuses moved. Do not commit.
3589
+ `
3590
+ }
3591
+ ];
3592
+ function lifecycleSkill(rness, s) {
3593
+ const command = `node "\${CLAUDE_PROJECT_DIR}/${rness}/${PINNED_BIN}" status ${s.tab} --cwd "\${CLAUDE_PROJECT_DIR}"`;
3594
+ return `---
3595
+ name: ${s.name}
3596
+ description: ${s.description}
3597
+ argument-hint: '${s.hint}'
3598
+ allowed-tools: Bash(${command})
3599
+ ---
3600
+
3601
+ !\`${command}\`
3602
+
3603
+ ${s.body(rness)}`;
3604
+ }
3605
+ function pluginFiles(rness, at, ctx) {
3606
+ return [
3607
+ { file: `${PLUGIN}/.claude-plugin/plugin.json`, at, content: PLUGIN_JSON },
3608
+ {
3609
+ file: `${PLUGIN}/skills/status/SKILL.md`,
3610
+ at,
3611
+ content: statusSkill(rness, ctx.packageManager)
3612
+ },
3613
+ ...LIFECYCLE2.map((s) => ({
3614
+ file: `${PLUGIN}/skills/${s.name}/SKILL.md`,
3615
+ at,
3616
+ content: lifecycleSkill(rness, s)
3617
+ }))
3618
+ ];
3619
+ }
2795
3620
 
2796
3621
  // src/core/agents.ts
2797
- var PINNED_BIN = "node_modules/@rness/cli/dist/bin/rness.js";
2798
3622
  function hookLine(rness, event) {
2799
3623
  const run3 = `f="$CLAUDE_PROJECT_DIR/${rness}/${PINNED_BIN}"; if [ -f "$f" ]; then node "$f" hook ${event};`;
2800
3624
  if (event !== "session-start") return `${run3} fi`;
@@ -2847,53 +3671,11 @@ function hooks(rness) {
2847
3671
  }
2848
3672
  ];
2849
3673
  }
2850
- var PLUGIN = ".claude/skills/rness";
2851
- var PLUGIN_JSON = `${JSON.stringify(
2852
- {
2853
- name: "rness",
2854
- description: "The rness workspace in Claude Code: /rness:status."
2855
- },
2856
- null,
2857
- 2
2858
- )}
2859
- `;
2860
- function statusSkill(rness) {
2861
- const command = `node "\${CLAUDE_PROJECT_DIR}/${rness}/${PINNED_BIN}" status --cwd "\${CLAUDE_PROJECT_DIR}"`;
2862
- return `---
2863
- name: status
2864
- description: The status of every decision, specification, plan and other tracked document of the rness workspace, one table per directory. Read-only.
2865
- argument-hint: '[tab]'
2866
- disable-model-invocation: true
2867
- allowed-tools: Bash(${command})
2868
- ---
2869
-
2870
- !\`${command}\`
2871
-
2872
- Show the output above to the user as it is: the heading and the tables,
2873
- nothing added, nothing summarised, no other tool. Arguments: \`$ARGUMENTS\`.
2874
- When they name a tab, show only that tab's section. If the output says the
2875
- module cannot be found, say instead that ${rness} is not installed next to
2876
- this repository.
2877
-
2878
- End with this line: _For the view with tabs and scrolling: Ctrl+Z, then
2879
- \`npx @rness/cli status\` (q to close), then \`fg\`._
2880
- `;
2881
- }
2882
- function plugin(rness, at) {
2883
- return [
2884
- { file: `${PLUGIN}/.claude-plugin/plugin.json`, at, content: PLUGIN_JSON },
2885
- {
2886
- file: `${PLUGIN}/skills/status/SKILL.md`,
2887
- at,
2888
- content: statusSkill(rness)
2889
- }
2890
- ];
2891
- }
2892
3674
  var TARGETS = {
2893
3675
  claude: {
2894
3676
  name: "claude",
2895
3677
  label: "Claude Code",
2896
- files: [
3678
+ files: (ctx) => [
2897
3679
  {
2898
3680
  file: ".claude/settings.json",
2899
3681
  at: "clones",
@@ -2929,7 +3711,7 @@ var TARGETS = {
2929
3711
  }
2930
3712
  ]
2931
3713
  },
2932
- ...plugin("../../.rness", "clones"),
3714
+ ...pluginFiles("../../.rness", "clones", ctx),
2933
3715
  // A session opened at the workspace root reads this file, one opened
2934
3716
  // in a clone does not (spec 0011 §1): the hooks only, `.rness` inside.
2935
3717
  {
@@ -2937,7 +3719,7 @@ var TARGETS = {
2937
3719
  at: "root",
2938
3720
  guarantees: hooks(".rness")
2939
3721
  },
2940
- ...plugin(".rness", "root")
3722
+ ...pluginFiles(".rness", "root", ctx)
2941
3723
  ]
2942
3724
  }
2943
3725
  };
@@ -3051,52 +3833,310 @@ function describeGuarantee(g) {
3051
3833
  return `${g.path.join(".")} lacks ${what}`;
3052
3834
  }
3053
3835
 
3054
- // src/core/agent-targets.ts
3055
- function wholeDiff(existing, content) {
3056
- if (existing === content) return null;
3057
- return existing === null ? "missing" : "differs from what rness writes";
3836
+ // src/core/pinned.ts
3837
+ import { execFile as execFile3 } from "child_process";
3838
+ import { readFile as readFile7 } from "fs/promises";
3839
+ import { basename as basename3, join as join12 } from "path";
3840
+ import { promisify as promisify3 } from "util";
3841
+ var execFileP3 = promisify3(execFile3);
3842
+ var PACKAGE = "@rness/cli";
3843
+ var EXACT_VERSION = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/;
3844
+ function compareVersions(a, b) {
3845
+ const split = (v) => {
3846
+ const dash = v.indexOf("-");
3847
+ const core = dash === -1 ? v : v.slice(0, dash);
3848
+ return [core.split(".").map(Number), dash === -1 ? null : v.slice(dash + 1)];
3849
+ };
3850
+ const [coreA, preA] = split(a);
3851
+ const [coreB, preB] = split(b);
3852
+ for (let i = 0; i < 3; i += 1) {
3853
+ const diff = (coreA[i] ?? 0) - (coreB[i] ?? 0);
3854
+ if (diff !== 0) return diff;
3855
+ }
3856
+ if (preA === preB) return 0;
3857
+ if (preA === null) return 1;
3858
+ if (preB === null) return -1;
3859
+ return preA.localeCompare(preB, "en", { numeric: true });
3058
3860
  }
3059
- async function filesOf(root, manifest, targets) {
3060
- const files = [];
3061
- if (!await exists(join11(root, "org"))) return files;
3062
- for (const target of targets)
3063
- for (const owned of target.files)
3064
- if (owned.at === "root")
3065
- files.push({
3066
- ...owned,
3067
- file: join11(root, ...owned.file.split("/")),
3068
- label: owned.file
3069
- });
3070
- for (const repo of Object.keys(manifest.repos)) {
3071
- if (!await exists(join11(root, "org", repo))) continue;
3072
- for (const target of targets)
3073
- for (const owned of target.files)
3074
- if (owned.at === "clones")
3075
- files.push({
3076
- ...owned,
3077
- file: join11(root, "org", repo, ...owned.file.split("/")),
3078
- label: `org/${repo}/${owned.file}`
3079
- });
3861
+ async function readPackageJson(rnessDir) {
3862
+ try {
3863
+ const parsed = JSON.parse(
3864
+ await readFile7(join12(rnessDir, "package.json"), "utf8")
3865
+ );
3866
+ return parsed !== null && typeof parsed === "object" ? parsed : null;
3867
+ } catch {
3868
+ return null;
3080
3869
  }
3081
- return files;
3082
3870
  }
3083
- function declared(manifest) {
3084
- return (manifest.agents ?? []).flatMap(
3085
- (name) => Object.hasOwn(TARGETS, name) ? [TARGETS[name]] : []
3871
+ async function readPin(rnessDir) {
3872
+ const pkg = await readPackageJson(rnessDir);
3873
+ if (pkg === null) return null;
3874
+ for (const field of ["devDependencies", "dependencies"]) {
3875
+ const deps = pkg[field];
3876
+ if (deps === null || typeof deps !== "object") continue;
3877
+ const spec = deps[PACKAGE];
3878
+ if (typeof spec === "string") return { spec, field };
3879
+ }
3880
+ return null;
3881
+ }
3882
+ async function writePin(rnessDir, from, to) {
3883
+ const file = join12(rnessDir, "package.json");
3884
+ const text = await readFile7(file, "utf8");
3885
+ const entry = new RegExp(
3886
+ `("${PACKAGE}"\\s*:\\s*")${from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(")`
3086
3887
  );
3888
+ if (!entry.test(text))
3889
+ throw new Error(`"${PACKAGE}": "${from}" not found in package.json`);
3890
+ await writeFileAtomic(file, text.replace(entry, `$1${to}$2`));
3087
3891
  }
3088
- var BLOCK_FILES = ["AGENTS.md", "CLAUDE.md"];
3089
- function writtenFiles(manifest) {
3090
- return [
3091
- ...BLOCK_FILES,
3092
- ...declared(manifest).flatMap(
3093
- (t) => t.files.filter((f) => f.at === "clones").map((f) => f.file)
3094
- )
3095
- ];
3892
+ var LOCKFILES = [
3893
+ ["pnpm-lock.yaml", "pnpm"],
3894
+ ["yarn.lock", "yarn"],
3895
+ ["bun.lock", "bun"],
3896
+ ["bun.lockb", "bun"],
3897
+ ["package-lock.json", "npm"]
3898
+ ];
3899
+ async function workspacePackageManager(rnessDir) {
3900
+ const declared2 = (await readPackageJson(rnessDir))?.["packageManager"];
3901
+ if (typeof declared2 === "string") {
3902
+ const name = declared2.split("@")[0] ?? "";
3903
+ if (isPackageManager(name)) return name;
3904
+ }
3905
+ for (const [lockfile, pm] of LOCKFILES)
3906
+ if (await exists(join12(rnessDir, lockfile))) return pm;
3907
+ return "npm";
3096
3908
  }
3097
- async function agentTargets(root, manifest, opts) {
3098
- const outcomes = [];
3099
- for (const owned of await filesOf(root, manifest, declared(manifest))) {
3909
+ async function hasLockfile(rnessDir, pm) {
3910
+ for (const [lockfile, owner] of LOCKFILES)
3911
+ if (owner === pm && await exists(join12(rnessDir, lockfile))) return true;
3912
+ return false;
3913
+ }
3914
+ async function pinDrift(rnessDir) {
3915
+ const pin = await readPin(rnessDir);
3916
+ if (pin === null || !EXACT_VERSION.test(pin.spec)) return null;
3917
+ const installed = (await readPinnedCli(rnessDir))?.version ?? null;
3918
+ return installed === pin.spec ? null : { pin: pin.spec, installed };
3919
+ }
3920
+ async function driftWarning(cwd) {
3921
+ let root;
3922
+ let rnessDir;
3923
+ try {
3924
+ const ws = await findWorkspace(cwd);
3925
+ root = ws.root;
3926
+ rnessDir = ws.rnessDir;
3927
+ } catch {
3928
+ return null;
3929
+ }
3930
+ const drift = await pinDrift(rnessDir);
3931
+ if (drift === null) return null;
3932
+ const pm = await workspacePackageManager(rnessDir);
3933
+ return warn(
3934
+ `${basename3(root)}/.rness pins @rness/cli ${drift.pin} but ${drift.installed ?? "nothing"} is installed \u2014 run ${pm} install in .rness`
3935
+ );
3936
+ }
3937
+ var CHILD_DEADLINE_MS = 12e4;
3938
+ function childEnv(env = process.env) {
3939
+ return {
3940
+ ...env,
3941
+ GIT_SSH_COMMAND: `${env["GIT_SSH_COMMAND"] ?? "ssh"} -o BatchMode=yes`,
3942
+ GIT_TERMINAL_PROMPT: "0"
3943
+ };
3944
+ }
3945
+ async function ensureInstalled(rnessDir, opts = {}) {
3946
+ const drift = await pinDrift(rnessDir);
3947
+ if (drift === null) return null;
3948
+ const env = opts.env ?? process.env;
3949
+ if (env["RNESS_NO_INSTALL"] === "1")
3950
+ return { installed: false, pin: drift.pin, reason: "RNESS_NO_INSTALL=1" };
3951
+ const pm = await workspacePackageManager(rnessDir);
3952
+ const frozen = await hasLockfile(rnessDir, pm);
3953
+ const install = opts.install ?? installDependencies;
3954
+ opts.announce?.(drift.pin);
3955
+ try {
3956
+ await install(pm, rnessDir, { frozen });
3957
+ return { installed: true, pin: drift.pin, reason: null };
3958
+ } catch (e) {
3959
+ return {
3960
+ installed: false,
3961
+ pin: drift.pin,
3962
+ reason: e instanceof Error ? e.message : String(e)
3963
+ };
3964
+ }
3965
+ }
3966
+ async function runPinnedSync(root) {
3967
+ const installed = await readPinnedCli(join12(root, ".rness"));
3968
+ if (installed === null) return null;
3969
+ const pinned = join12(installed.dir, ...installed.bin.split("/"));
3970
+ if (!await exists(pinned))
3971
+ return {
3972
+ code: 1,
3973
+ stdout: "",
3974
+ stderr: `@rness/cli in .rness is not installed correctly (missing ${installed.bin}); reinstall in .rness/
3975
+ `
3976
+ };
3977
+ try {
3978
+ const { stdout, stderr } = await execFileP3(
3979
+ process.execPath,
3980
+ [pinned, "sync", "--yes"],
3981
+ {
3982
+ cwd: root,
3983
+ maxBuffer: 16 * 1024 * 1024,
3984
+ env: childEnv(),
3985
+ timeout: CHILD_DEADLINE_MS,
3986
+ killSignal: "SIGKILL"
3987
+ }
3988
+ );
3989
+ return { code: 0, stdout, stderr };
3990
+ } catch (e) {
3991
+ const err = e;
3992
+ const deadline = err.killed === true ? `rness sync in .rness did not finish within ${CHILD_DEADLINE_MS / 1e3}s and was stopped
3993
+ ` : "";
3994
+ return {
3995
+ code: typeof err.code === "number" ? err.code : 1,
3996
+ stdout: err.stdout ?? "",
3997
+ stderr: `${err.stderr ?? ""}${deadline}`
3998
+ };
3999
+ }
4000
+ }
4001
+ var SYNC_VERBS = ["updated", "unchanged", "stale"];
4002
+ function counted(noun, counts) {
4003
+ const total = [...counts.values()].reduce((a, b) => a + b, 0);
4004
+ const detail = SYNC_VERBS.filter((verb) => counts.has(verb)).map((verb) => `${counts.get(verb) ?? 0} ${verb}`).join(", ");
4005
+ return `${total} ${noun}${total === 1 ? "" : "s"}${detail === "" ? "" : `: ${detail}`}`;
4006
+ }
4007
+ function summariseSync(stdout) {
4008
+ const blocks = /* @__PURE__ */ new Map();
4009
+ const agentFiles = /* @__PURE__ */ new Map();
4010
+ const others = [];
4011
+ for (const line3 of stdout.split("\n")) {
4012
+ if (line3.trim() === "") continue;
4013
+ const m = /^(updated|unchanged|stale)\s+(\S+)/.exec(line3);
4014
+ if (m === null) {
4015
+ others.push(line3);
4016
+ continue;
4017
+ }
4018
+ const [, verb = "", path = ""] = m;
4019
+ const counts = /(^|\/)(AGENTS|CLAUDE)\.md$/.test(path) ? blocks : agentFiles;
4020
+ counts.set(verb, (counts.get(verb) ?? 0) + 1);
4021
+ }
4022
+ return {
4023
+ summary: [
4024
+ counted("block", blocks),
4025
+ ...agentFiles.size > 0 ? [counted("agent file", agentFiles)] : []
4026
+ ].join("; "),
4027
+ others
4028
+ };
4029
+ }
4030
+ function syncOutcome(result, opts = {}) {
4031
+ const { others } = summariseSync(result.stdout);
4032
+ const wrote = /^(updated|unchanged|stale)\s/m.test(result.stdout);
4033
+ const stderr = result.stderr.split("\n").map((l) => l.trimEnd()).filter((l) => l.trim() !== "");
4034
+ const hints = wrote ? [...others, ...stderr] : others;
4035
+ if (opts.sshWorks === true && /Permission denied \(publickey\)/.test(result.stderr))
4036
+ hints.push(...agentHintLines());
4037
+ return {
4038
+ code: wrote ? 0 : result.code,
4039
+ hints,
4040
+ errors: wrote ? [] : stderr
4041
+ };
4042
+ }
4043
+ async function syncPinned(root, shown, fallback) {
4044
+ const rnessDir = join12(root, ".rness");
4045
+ const installed = await readPinnedCli(rnessDir);
4046
+ if (installed === null) {
4047
+ if (fallback !== void 0) return fallback();
4048
+ process.stderr.write(
4049
+ `@rness/cli is not installed in ${shown}/.rness; install in .rness/, then ${rnessCommand()} sync
4050
+ `
4051
+ );
4052
+ return 1;
4053
+ }
4054
+ const pinned = join12(installed.dir, ...installed.bin.split("/"));
4055
+ if (!await exists(pinned)) {
4056
+ process.stderr.write(
4057
+ `@rness/cli in ${shown}/.rness is not installed correctly (missing ${installed.bin}); reinstall in .rness/
4058
+ `
4059
+ );
4060
+ return 1;
4061
+ }
4062
+ try {
4063
+ const { stdout, stderr } = await execFileP3(
4064
+ process.execPath,
4065
+ [pinned, "sync", "--yes"],
4066
+ {
4067
+ cwd: root,
4068
+ maxBuffer: 16 * 1024 * 1024,
4069
+ env: childEnv(),
4070
+ timeout: CHILD_DEADLINE_MS,
4071
+ killSignal: "SIGKILL"
4072
+ }
4073
+ );
4074
+ process.stdout.write(stdout);
4075
+ if (stderr !== "") process.stderr.write(stderr);
4076
+ return 0;
4077
+ } catch (e) {
4078
+ const err = e;
4079
+ if (err.stdout !== void 0 && err.stdout !== "")
4080
+ process.stdout.write(err.stdout);
4081
+ if (err.stderr !== void 0 && err.stderr !== "")
4082
+ process.stderr.write(err.stderr);
4083
+ return typeof err.code === "number" ? err.code : 1;
4084
+ }
4085
+ }
4086
+
4087
+ // src/core/agent-targets.ts
4088
+ function wholeDiff(existing, content) {
4089
+ if (existing === content) return null;
4090
+ return existing === null ? "missing" : "differs from what rness writes";
4091
+ }
4092
+ async function contextOf(root) {
4093
+ return {
4094
+ packageManager: await workspacePackageManager(join13(root, ".rness"))
4095
+ };
4096
+ }
4097
+ var PATHS_ONLY = { packageManager: "npm" };
4098
+ async function filesOf(root, manifest, targets) {
4099
+ const files = [];
4100
+ if (!await exists(join13(root, "org"))) return files;
4101
+ const ctx = await contextOf(root);
4102
+ for (const target2 of targets)
4103
+ for (const owned of target2.files(ctx))
4104
+ if (owned.at === "root")
4105
+ files.push({
4106
+ ...owned,
4107
+ file: join13(root, ...owned.file.split("/")),
4108
+ label: owned.file
4109
+ });
4110
+ for (const repo of Object.keys(manifest.repos)) {
4111
+ if (!await exists(join13(root, "org", repo))) continue;
4112
+ for (const target2 of targets)
4113
+ for (const owned of target2.files(ctx))
4114
+ if (owned.at === "clones")
4115
+ files.push({
4116
+ ...owned,
4117
+ file: join13(root, "org", repo, ...owned.file.split("/")),
4118
+ label: `org/${repo}/${owned.file}`
4119
+ });
4120
+ }
4121
+ return files;
4122
+ }
4123
+ function declared(manifest) {
4124
+ return (manifest.agents ?? []).flatMap(
4125
+ (name) => Object.hasOwn(TARGETS, name) ? [TARGETS[name]] : []
4126
+ );
4127
+ }
4128
+ var BLOCK_FILES = ["AGENTS.md", "CLAUDE.md"];
4129
+ function writtenFiles(manifest) {
4130
+ return [
4131
+ ...BLOCK_FILES,
4132
+ ...declared(manifest).flatMap(
4133
+ (t) => t.files(PATHS_ONLY).filter((f) => f.at === "clones").map((f) => f.file)
4134
+ )
4135
+ ];
4136
+ }
4137
+ async function agentTargets(root, manifest, opts) {
4138
+ const outcomes = [];
4139
+ for (const owned of await filesOf(root, manifest, declared(manifest))) {
3100
4140
  const { file, label } = owned;
3101
4141
  const existing = await readOrNull(file);
3102
4142
  if ("content" in owned) {
@@ -3183,7 +4223,7 @@ var PROVIDERS = {
3183
4223
  name: "github",
3184
4224
  label: "GitHub",
3185
4225
  available: true,
3186
- open: ({ apiBase }) => githubProvider(apiBase)
4226
+ open: ({ apiBase, wait }) => githubProvider(apiBase, wait)
3187
4227
  },
3188
4228
  gitlab: { name: "gitlab", label: "GitLab", available: false },
3189
4229
  atlassian: {
@@ -3398,7 +4438,7 @@ function targetsOf(manifest, root, only) {
3398
4438
  const segments = entry.path.split("/");
3399
4439
  targets.push({
3400
4440
  scope: name,
3401
- dir: join12(root, ...segments),
4441
+ dir: join14(root, ...segments),
3402
4442
  depth: segments.length,
3403
4443
  label: `${entry.path}/AGENTS.md`
3404
4444
  });
@@ -3557,7 +4597,7 @@ async function syncCommand(opts, deps = {}) {
3557
4597
  try {
3558
4598
  const notCloned = /* @__PURE__ */ new Set();
3559
4599
  for (const [name, repo] of Object.entries(manifest.repos)) {
3560
- const dir = join12(ws.root, "org", name);
4600
+ const dir = join14(ws.root, "org", name);
3561
4601
  const label = `org/${name}`;
3562
4602
  if (!await exists(dir)) {
3563
4603
  if (!toClone.has(name)) {
@@ -3609,7 +4649,7 @@ async function syncCommand(opts, deps = {}) {
3609
4649
  lines.push(
3610
4650
  `not in rness.json: ${undeclared.join(", ")} (${rnessCommand()} add <name> declares it)`
3611
4651
  );
3612
- if (!await exists(join12(ws.root, "org"))) {
4652
+ if (!await exists(join14(ws.root, "org"))) {
3613
4653
  lines.push(["skipped", "blocks (no org/ directory here)"]);
3614
4654
  } else {
3615
4655
  let differences = 0;
@@ -3621,7 +4661,7 @@ async function syncCommand(opts, deps = {}) {
3621
4661
  continue;
3622
4662
  }
3623
4663
  const [agentsLinked, claudeLinked] = await Promise.all(
3624
- BLOCK_FILES.map((f) => isSymlink(join12(t.dir, f)))
4664
+ BLOCK_FILES.map((f) => isSymlink(join14(t.dir, f)))
3625
4665
  );
3626
4666
  const atRoot = t.scope === null;
3627
4667
  if (!atRoot && (agentsLinked === true || claudeLinked === true)) {
@@ -3644,7 +4684,7 @@ async function syncCommand(opts, deps = {}) {
3644
4684
  `${t.label}: block is ${Math.round(block.bytes / 1024)} KB (over 32 KB)`
3645
4685
  );
3646
4686
  }
3647
- const agents = agentsLinked === true ? "skipped" : await syncBlockFile(join12(t.dir, "AGENTS.md"), block, check);
4687
+ const agents = agentsLinked === true ? "skipped" : await syncBlockFile(join14(t.dir, "AGENTS.md"), block, check);
3648
4688
  if (typeof agents === "object") {
3649
4689
  problems.push(`${t.label}: ${agents.error}`);
3650
4690
  continue;
@@ -3659,7 +4699,7 @@ async function syncCommand(opts, deps = {}) {
3659
4699
  }
3660
4700
  if (claudeLinked === true) continue;
3661
4701
  const claude = await syncBlockFile(
3662
- join12(t.dir, "CLAUDE.md"),
4702
+ join14(t.dir, "CLAUDE.md"),
3663
4703
  block,
3664
4704
  check,
3665
4705
  true
@@ -3716,7 +4756,7 @@ async function syncCommand(opts, deps = {}) {
3716
4756
  }
3717
4757
 
3718
4758
  // src/commands/add.ts
3719
- import { basename as basename3, join as join13 } from "path";
4759
+ import { basename as basename4, join as join15 } from "path";
3720
4760
  function printDeclared(ui, result, already) {
3721
4761
  for (const name of result.declared) {
3722
4762
  if (already.has(name)) continue;
@@ -3751,7 +4791,7 @@ async function addCommand(spec, opts, deps = {}) {
3751
4791
  }
3752
4792
  if (parsed.name === CONTEXT_REPO) {
3753
4793
  process.stderr.write(
3754
- `${CONTEXT_REPO} is the workspace context, not a member repository \u2014 it is already cloned at ${basename3(ws.root)}/${CONTEXT_REPO}
4794
+ `${CONTEXT_REPO} is the workspace context, not a member repository \u2014 it is already cloned at ${basename4(ws.root)}/${CONTEXT_REPO}
3755
4795
  `
3756
4796
  );
3757
4797
  return 2;
@@ -3809,7 +4849,7 @@ async function addCommand(spec, opts, deps = {}) {
3809
4849
  }
3810
4850
  parsed = parseRepoSpec(spec, org, host);
3811
4851
  const scopes = (opts.scopes ?? "").split(",").map((s) => s.trim()).filter((s) => s !== "");
3812
- const dir = join13(ws.root, "org", parsed.name);
4852
+ const dir = join15(ws.root, "org", parsed.name);
3813
4853
  const present = await exists(dir);
3814
4854
  if (interactive) {
3815
4855
  const { confirm, isCancel } = await terminal.prompts();
@@ -3885,7 +4925,7 @@ async function addCommand(spec, opts, deps = {}) {
3885
4925
  }
3886
4926
 
3887
4927
  // src/commands/context.ts
3888
- import { posix as posix5, relative, sep as sep3 } from "path";
4928
+ import { posix as posix6, relative, sep as sep3 } from "path";
3889
4929
  function renderMarkdown(ctx) {
3890
4930
  const parts = [];
3891
4931
  for (const collection of ctx.collections) {
@@ -3917,7 +4957,7 @@ async function contextCommand(opts) {
3917
4957
  }
3918
4958
  scope = opts.scope;
3919
4959
  } else {
3920
- const rel = relative(ws.root, cwd).split(sep3).join(posix5.sep);
4960
+ const rel = relative(ws.root, cwd).split(sep3).join(posix6.sep);
3921
4961
  scope = resolveScope(manifest, rel);
3922
4962
  }
3923
4963
  const ctx = await assembleContext({
@@ -3935,56 +4975,9 @@ async function contextCommand(opts) {
3935
4975
  }
3936
4976
  }
3937
4977
 
3938
- // src/core/delegate.ts
3939
- import { readFile as readFile6 } from "fs/promises";
3940
- import { join as join14 } from "path";
3941
- var DEFAULT_BIN = "dist/bin/rness.js";
3942
- async function readPinnedCli(rnessDir) {
3943
- const dir = join14(rnessDir, "node_modules", "@rness", "cli");
3944
- let pkg;
3945
- try {
3946
- pkg = JSON.parse(await readFile6(join14(dir, "package.json"), "utf8"));
3947
- } catch {
3948
- return null;
3949
- }
3950
- const declared2 = pkg.bin;
3951
- let bin = DEFAULT_BIN;
3952
- if (typeof declared2 === "string") bin = declared2;
3953
- else if (declared2 !== null && typeof declared2 === "object") {
3954
- const named = declared2["rness"];
3955
- if (typeof named === "string") bin = named;
3956
- }
3957
- return {
3958
- dir,
3959
- bin,
3960
- version: typeof pkg.version === "string" ? pkg.version : null
3961
- };
3962
- }
3963
- async function pinnedBinPath(rnessDir) {
3964
- const pinned = await readPinnedCli(rnessDir);
3965
- if (pinned === null) return null;
3966
- return join14(pinned.dir, ...pinned.bin.split("/"));
3967
- }
3968
- async function findDelegate(cwd, ownVersion) {
3969
- let root;
3970
- let rnessDir;
3971
- try {
3972
- const ws = await findWorkspace(cwd);
3973
- root = ws.root;
3974
- rnessDir = ws.rnessDir;
3975
- } catch {
3976
- return null;
3977
- }
3978
- const pinned = await readPinnedCli(rnessDir);
3979
- if (pinned === null) return null;
3980
- const { version } = pinned;
3981
- if (version === null || version === ownVersion) return null;
3982
- return { entry: join14(pinned.dir, "dist", "index.js"), version, root };
3983
- }
3984
-
3985
4978
  // src/core/scaffold.ts
3986
4979
  import { statSync } from "fs";
3987
- import { dirname as dirname5, join as join15 } from "path";
4980
+ import { dirname as dirname5, join as join16 } from "path";
3988
4981
  import { fileURLToPath } from "url";
3989
4982
  var SCAFFOLD_FILES = [
3990
4983
  "AGENTS.md",
@@ -4008,9 +5001,9 @@ var SCAFFOLD_FILES = [
4008
5001
  function scaffoldDir() {
4009
5002
  let dir = dirname5(fileURLToPath(import.meta.url));
4010
5003
  for (let i = 0; i < 4; i += 1) {
4011
- const candidate = join15(dir, "scaffold");
5004
+ const candidate = join16(dir, "scaffold");
4012
5005
  try {
4013
- if (statSync(join15(candidate, "rness.json")).isFile()) return candidate;
5006
+ if (statSync(join16(candidate, "rness.json")).isFile()) return candidate;
4014
5007
  } catch {
4015
5008
  }
4016
5009
  dir = dirname5(dir);
@@ -4019,15 +5012,15 @@ function scaffoldDir() {
4019
5012
  }
4020
5013
 
4021
5014
  // src/core/scaffold-copy.ts
4022
- import { chmod as chmod2, mkdir as mkdir3, readFile as readFile7, stat as stat2, writeFile as writeFile2 } from "fs/promises";
4023
- import { dirname as dirname6, join as join16 } from "path";
5015
+ import { chmod as chmod2, mkdir as mkdir3, readFile as readFile8, stat as stat2, writeFile as writeFile2 } from "fs/promises";
5016
+ import { dirname as dirname6, join as join17 } from "path";
4024
5017
  var TOKENISED = /* @__PURE__ */ new Set(["package.json"]);
4025
5018
  async function renderScaffold(source, tokens) {
4026
5019
  const files = [];
4027
5020
  for (const rel of SCAFFOLD_FILES) {
4028
5021
  if (rel === "rness.json") continue;
4029
- const from = join16(source, ...rel.split("/"));
4030
- let content = await readFile7(from, "utf8");
5022
+ const from = join17(source, ...rel.split("/"));
5023
+ let content = await readFile8(from, "utf8");
4031
5024
  if (TOKENISED.has(rel)) {
4032
5025
  content = content.replaceAll("__RNESS_VERSION__", tokens.version).replaceAll("__RNESS_PM__", tokens.packageManager);
4033
5026
  }
@@ -4041,7 +5034,7 @@ async function renderScaffold(source, tokens) {
4041
5034
  }
4042
5035
  async function copyScaffold(dest, tokens) {
4043
5036
  for (const file of await renderScaffold(scaffoldDir(), tokens)) {
4044
- const to = join16(dest, ...file.path.split("/"));
5037
+ const to = join17(dest, ...file.path.split("/"));
4045
5038
  await mkdir3(dirname6(to), { recursive: true });
4046
5039
  await writeFile2(to, file.content, "utf8");
4047
5040
  if (file.executable) await chmod2(to, 493);
@@ -4053,16 +5046,16 @@ import { spawn as spawn3 } from "child_process";
4053
5046
  import { realpath } from "fs/promises";
4054
5047
 
4055
5048
  // src/core/setup-git.ts
4056
- import { execFile as execFile3 } from "child_process";
4057
- import { promisify as promisify3 } from "util";
4058
- var execFileP3 = promisify3(execFile3);
5049
+ import { execFile as execFile4 } from "child_process";
5050
+ import { promisify as promisify4 } from "util";
5051
+ var execFileP4 = promisify4(execFile4);
4059
5052
  var KEY = "credential.https://github.com.helper";
4060
5053
  function helperValue(node, bin) {
4061
5054
  return `!'${node}' '${bin}' git-credential`;
4062
5055
  }
4063
5056
  var isOurs = (value) => /^!'[^']+' '[^']*rness[^']*' git-credential$/.test(value);
4064
5057
  async function config(args) {
4065
- const { stdout } = await execFileP3("git", ["config", "--global", ...args]);
5058
+ const { stdout } = await execFileP4("git", ["config", "--global", ...args]);
4066
5059
  return stdout;
4067
5060
  }
4068
5061
  async function values() {
@@ -4096,400 +5089,149 @@ async function removeGitSetup() {
4096
5089
  }
4097
5090
 
4098
5091
  // src/commands/login.ts
4099
- function openInBrowser(url) {
4100
- const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
4101
- try {
4102
- const child = spawn3(command, args, { stdio: "ignore", detached: true });
4103
- child.on("error", () => void 0);
4104
- child.unref();
4105
- } catch {
4106
- }
4107
- }
4108
- async function offerSetupGit(opts, terminal, bin, ui) {
4109
- if (opts.setupGit === false) return;
4110
- if (!isStableInstall(bin)) {
4111
- ui.hint(
4112
- "install rness globally to let git use your login: npm i -g @rness/cli"
4113
- );
4114
- return;
4115
- }
4116
- if (opts.setupGit !== true) {
4117
- if (!terminal.isTty()) return;
4118
- const p = await terminal.prompts();
4119
- const ok = await p.confirm({
4120
- message: "Use rness to authenticate git over HTTPS for github.com?",
4121
- initialValue: true
4122
- });
4123
- if (p.isCancel(ok) || ok !== true) return;
4124
- }
4125
- const changed = await setupGit(process.execPath, bin);
4126
- ui.line(
4127
- changed ? "updated" : "unchanged",
4128
- `git: rness answers for https://github.com (${rnessCommand()} logout undoes it)`,
4129
- `git now asks rness for your github.com login over HTTPS (${rnessCommand()} logout undoes it)`
4130
- );
4131
- }
4132
- async function manifestAround(cwd) {
4133
- try {
4134
- return await loadManifest((await findWorkspace(cwd)).rnessDir);
4135
- } catch {
4136
- return null;
4137
- }
4138
- }
4139
- function splitScopes(scopes) {
4140
- return scopes.split(/[\s,]+/).filter((s) => s !== "");
4141
- }
4142
- async function loginCommand(opts, deps = {}) {
4143
- const terminal = deps.terminal ?? defaultTerminal;
4144
- const own = deps.ui === void 0;
4145
- const ui = deps.ui ?? await makeUi(terminal);
4146
- if (own) ui.intro("Log in to GitHub");
4147
- try {
4148
- const manifest = await manifestAround(deps.cwd ?? process.cwd());
4149
- if (manifest?.provider != null && !PROVIDERS[manifest.provider].available) {
4150
- ui.error(unsupportedProviderMessage(manifest.provider));
4151
- return 1;
4152
- }
4153
- const needsProject = opts.project === true || manifest?.pulse != null;
4154
- const bin = deps.bin ?? await realpath(process.argv[1] ?? "").catch(() => "");
4155
- const env = envToken();
4156
- if (env !== null)
4157
- ui.hint(`${env.source} is set and wins over a stored login`);
4158
- const stored = await readAuth();
4159
- const resolved = stored === null ? null : await resolveToken();
4160
- const lacksProject = needsProject && resolved?.source === "login" && !splitScopes(stored?.tokens.scopes ?? "").includes(PULSE_SCOPE);
4161
- if (stored !== null && resolved !== null && !lacksProject) {
4162
- ui.line(
4163
- "logged in",
4164
- `as ${stored.login} (github.com); ${rnessCommand()} logout to switch`
4165
- );
4166
- await offerSetupGit(opts, terminal, bin, ui);
4167
- if (own) ui.outro("Nothing to do", "idle");
4168
- return 0;
4169
- }
4170
- if (lacksProject) ui.line("login", "logging in again for the project scope");
4171
- const code = await requestDeviceCode(
4172
- needsProject ? `${SCOPES} ${PULSE_SCOPE}` : SCOPES
4173
- );
4174
- if (ui.session)
4175
- ui.line(
4176
- "open",
4177
- code.verificationUri,
4178
- `Open ${code.verificationUri} and enter the code ${code.userCode}`
4179
- );
4180
- else {
4181
- ui.line("open", code.verificationUri);
4182
- ui.line("code", code.userCode);
4183
- }
4184
- if (terminal.isTty() && process.env["SSH_CONNECTION"] === void 0)
4185
- (deps.open ?? openInBrowser)(code.verificationUri);
4186
- if (!ui.session)
4187
- ui.line("waiting", "for you to approve rness on github.com\u2026");
4188
- const login = await ui.step(
4189
- {
4190
- doing: "waiting for you to approve rness on github.com",
4191
- sentence: "Waiting for your approval on github.com",
4192
- quiet: true
4193
- },
4194
- async () => {
4195
- const tokens = await pollForToken(code, deps);
4196
- const user = await getUser({
4197
- token: tokens.accessToken,
4198
- apiBase: opts.githubApi ?? DEFAULT_GITHUB_API
4199
- });
4200
- await writeAuth({ login: user.login, tokens });
4201
- return user.login;
4202
- },
4203
- (name) => ["logged in", `as ${name} (github.com)`]
4204
- );
4205
- if (!ui.session) ui.line("logged in", `as ${login} (github.com)`);
4206
- await offerSetupGit(opts, terminal, bin, ui);
4207
- if (own) ui.outro(`You are logged in as ${login}`);
4208
- return 0;
4209
- } catch (e) {
4210
- if (e instanceof LoginError) {
4211
- ui.error(e.message);
4212
- return 1;
4213
- }
4214
- return reportError(e);
4215
- }
4216
- }
4217
- function revokeUrl() {
4218
- return `https://github.com/settings/connections/applications/${clientId()}`;
4219
- }
4220
-
4221
- // src/commands/create.ts
4222
- import { mkdir as mkdir6, mkdtemp as mkdtemp2, rename as rename2, rm as rm6 } from "fs/promises";
4223
- import { tmpdir as tmpdir2 } from "os";
4224
- import { join as join21, relative as relative4, resolve as resolve5 } from "path";
4225
-
4226
- // src/core/ask-provider.ts
4227
- var PROVIDER_QUESTION = "Where does your organization live?";
4228
- async function askProvider(p) {
4229
- return p.select({
4230
- message: PROVIDER_QUESTION,
4231
- options: Object.values(PROVIDERS).map((entry) => ({
4232
- value: entry.name,
4233
- label: entry.available ? entry.label : `${entry.label} (coming later)`,
4234
- disabled: !entry.available
4235
- }))
4236
- });
4237
- }
4238
-
4239
- // src/core/new-workspace.ts
4240
- import { mkdir as mkdir4, readdir as readdir3, rm as rm5, stat as stat3 } from "fs/promises";
4241
- import { dirname as dirname8, join as join19, relative as relative2 } from "path";
4242
-
4243
- // src/core/pinned.ts
4244
- import { execFile as execFile4 } from "child_process";
4245
- import { readFile as readFile8 } from "fs/promises";
4246
- import { basename as basename4, join as join17 } from "path";
4247
- import { promisify as promisify4 } from "util";
4248
- var execFileP4 = promisify4(execFile4);
4249
- var PACKAGE = "@rness/cli";
4250
- var EXACT_VERSION = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/;
4251
- function compareVersions(a, b) {
4252
- const split = (v) => {
4253
- const dash = v.indexOf("-");
4254
- const core = dash === -1 ? v : v.slice(0, dash);
4255
- return [core.split(".").map(Number), dash === -1 ? null : v.slice(dash + 1)];
4256
- };
4257
- const [coreA, preA] = split(a);
4258
- const [coreB, preB] = split(b);
4259
- for (let i = 0; i < 3; i += 1) {
4260
- const diff = (coreA[i] ?? 0) - (coreB[i] ?? 0);
4261
- if (diff !== 0) return diff;
4262
- }
4263
- if (preA === preB) return 0;
4264
- if (preA === null) return 1;
4265
- if (preB === null) return -1;
4266
- return preA.localeCompare(preB, "en", { numeric: true });
4267
- }
4268
- async function readPackageJson(rnessDir) {
4269
- try {
4270
- const parsed = JSON.parse(
4271
- await readFile8(join17(rnessDir, "package.json"), "utf8")
4272
- );
4273
- return parsed !== null && typeof parsed === "object" ? parsed : null;
4274
- } catch {
4275
- return null;
4276
- }
4277
- }
4278
- async function readPin(rnessDir) {
4279
- const pkg = await readPackageJson(rnessDir);
4280
- if (pkg === null) return null;
4281
- for (const field of ["devDependencies", "dependencies"]) {
4282
- const deps = pkg[field];
4283
- if (deps === null || typeof deps !== "object") continue;
4284
- const spec = deps[PACKAGE];
4285
- if (typeof spec === "string") return { spec, field };
4286
- }
4287
- return null;
4288
- }
4289
- async function writePin(rnessDir, from, to) {
4290
- const file = join17(rnessDir, "package.json");
4291
- const text = await readFile8(file, "utf8");
4292
- const entry = new RegExp(
4293
- `("${PACKAGE}"\\s*:\\s*")${from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(")`
4294
- );
4295
- if (!entry.test(text))
4296
- throw new Error(`"${PACKAGE}": "${from}" not found in package.json`);
4297
- await writeFileAtomic(file, text.replace(entry, `$1${to}$2`));
4298
- }
4299
- var LOCKFILES = [
4300
- ["pnpm-lock.yaml", "pnpm"],
4301
- ["yarn.lock", "yarn"],
4302
- ["bun.lock", "bun"],
4303
- ["bun.lockb", "bun"],
4304
- ["package-lock.json", "npm"]
4305
- ];
4306
- async function workspacePackageManager(rnessDir) {
4307
- const declared2 = (await readPackageJson(rnessDir))?.["packageManager"];
4308
- if (typeof declared2 === "string") {
4309
- const name = declared2.split("@")[0] ?? "";
4310
- if (isPackageManager(name)) return name;
4311
- }
4312
- for (const [lockfile, pm] of LOCKFILES)
4313
- if (await exists(join17(rnessDir, lockfile))) return pm;
4314
- return "npm";
4315
- }
4316
- async function hasLockfile(rnessDir, pm) {
4317
- for (const [lockfile, owner] of LOCKFILES)
4318
- if (owner === pm && await exists(join17(rnessDir, lockfile))) return true;
4319
- return false;
4320
- }
4321
- async function pinDrift(rnessDir) {
4322
- const pin = await readPin(rnessDir);
4323
- if (pin === null || !EXACT_VERSION.test(pin.spec)) return null;
4324
- const installed = (await readPinnedCli(rnessDir))?.version ?? null;
4325
- return installed === pin.spec ? null : { pin: pin.spec, installed };
4326
- }
4327
- async function driftWarning(cwd) {
4328
- let root;
4329
- let rnessDir;
4330
- try {
4331
- const ws = await findWorkspace(cwd);
4332
- root = ws.root;
4333
- rnessDir = ws.rnessDir;
4334
- } catch {
4335
- return null;
4336
- }
4337
- const drift = await pinDrift(rnessDir);
4338
- if (drift === null) return null;
4339
- const pm = await workspacePackageManager(rnessDir);
4340
- return warn(
4341
- `${basename4(root)}/.rness pins @rness/cli ${drift.pin} but ${drift.installed ?? "nothing"} is installed \u2014 run ${pm} install in .rness`
4342
- );
4343
- }
4344
- var CHILD_DEADLINE_MS = 12e4;
4345
- function childEnv(env = process.env) {
4346
- return {
4347
- ...env,
4348
- GIT_SSH_COMMAND: `${env["GIT_SSH_COMMAND"] ?? "ssh"} -o BatchMode=yes`,
4349
- GIT_TERMINAL_PROMPT: "0"
4350
- };
4351
- }
4352
- async function ensureInstalled(rnessDir, opts = {}) {
4353
- const drift = await pinDrift(rnessDir);
4354
- if (drift === null) return null;
4355
- const env = opts.env ?? process.env;
4356
- if (env["RNESS_NO_INSTALL"] === "1")
4357
- return { installed: false, pin: drift.pin, reason: "RNESS_NO_INSTALL=1" };
4358
- const pm = await workspacePackageManager(rnessDir);
4359
- const frozen = await hasLockfile(rnessDir, pm);
4360
- const install = opts.install ?? installDependencies;
4361
- opts.announce?.(drift.pin);
4362
- try {
4363
- await install(pm, rnessDir, { frozen });
4364
- return { installed: true, pin: drift.pin, reason: null };
4365
- } catch (e) {
4366
- return {
4367
- installed: false,
4368
- pin: drift.pin,
4369
- reason: e instanceof Error ? e.message : String(e)
4370
- };
4371
- }
4372
- }
4373
- async function runPinnedSync(root) {
4374
- const installed = await readPinnedCli(join17(root, ".rness"));
4375
- if (installed === null) return null;
4376
- const pinned = join17(installed.dir, ...installed.bin.split("/"));
4377
- if (!await exists(pinned))
4378
- return {
4379
- code: 1,
4380
- stdout: "",
4381
- stderr: `@rness/cli in .rness is not installed correctly (missing ${installed.bin}); reinstall in .rness/
4382
- `
4383
- };
4384
- try {
4385
- const { stdout, stderr } = await execFileP4(
4386
- process.execPath,
4387
- [pinned, "sync", "--yes"],
4388
- {
4389
- cwd: root,
4390
- maxBuffer: 16 * 1024 * 1024,
4391
- env: childEnv(),
4392
- timeout: CHILD_DEADLINE_MS,
4393
- killSignal: "SIGKILL"
4394
- }
4395
- );
4396
- return { code: 0, stdout, stderr };
4397
- } catch (e) {
4398
- const err = e;
4399
- const deadline = err.killed === true ? `rness sync in .rness did not finish within ${CHILD_DEADLINE_MS / 1e3}s and was stopped
4400
- ` : "";
4401
- return {
4402
- code: typeof err.code === "number" ? err.code : 1,
4403
- stdout: err.stdout ?? "",
4404
- stderr: `${err.stderr ?? ""}${deadline}`
4405
- };
4406
- }
4407
- }
4408
- var SYNC_VERBS = ["updated", "unchanged", "stale"];
4409
- function counted(noun, counts) {
4410
- const total = [...counts.values()].reduce((a, b) => a + b, 0);
4411
- const detail = SYNC_VERBS.filter((verb) => counts.has(verb)).map((verb) => `${counts.get(verb) ?? 0} ${verb}`).join(", ");
4412
- return `${total} ${noun}${total === 1 ? "" : "s"}${detail === "" ? "" : `: ${detail}`}`;
4413
- }
4414
- function summariseSync(stdout) {
4415
- const blocks = /* @__PURE__ */ new Map();
4416
- const agentFiles = /* @__PURE__ */ new Map();
4417
- const others = [];
4418
- for (const line3 of stdout.split("\n")) {
4419
- if (line3.trim() === "") continue;
4420
- const m = /^(updated|unchanged|stale)\s+(\S+)/.exec(line3);
4421
- if (m === null) {
4422
- others.push(line3);
4423
- continue;
4424
- }
4425
- const [, verb = "", path = ""] = m;
4426
- const counts = /(^|\/)(AGENTS|CLAUDE)\.md$/.test(path) ? blocks : agentFiles;
4427
- counts.set(verb, (counts.get(verb) ?? 0) + 1);
4428
- }
4429
- return {
4430
- summary: [
4431
- counted("block", blocks),
4432
- ...agentFiles.size > 0 ? [counted("agent file", agentFiles)] : []
4433
- ].join("; "),
4434
- others
4435
- };
4436
- }
4437
- function syncOutcome(result, opts = {}) {
4438
- const { others } = summariseSync(result.stdout);
4439
- const wrote = /^(updated|unchanged|stale)\s/m.test(result.stdout);
4440
- const stderr = result.stderr.split("\n").map((l) => l.trimEnd()).filter((l) => l.trim() !== "");
4441
- const hints = wrote ? [...others, ...stderr] : others;
4442
- if (opts.sshWorks === true && /Permission denied \(publickey\)/.test(result.stderr))
4443
- hints.push(...agentHintLines());
4444
- return {
4445
- code: wrote ? 0 : result.code,
4446
- hints,
4447
- errors: wrote ? [] : stderr
4448
- };
5092
+ function openInBrowser(url) {
5093
+ const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
5094
+ try {
5095
+ const child = spawn3(command, args, { stdio: "ignore", detached: true });
5096
+ child.on("error", () => void 0);
5097
+ child.unref();
5098
+ } catch {
5099
+ }
4449
5100
  }
4450
- async function syncPinned(root, shown, fallback) {
4451
- const rnessDir = join17(root, ".rness");
4452
- const installed = await readPinnedCli(rnessDir);
4453
- if (installed === null) {
4454
- if (fallback !== void 0) return fallback();
4455
- process.stderr.write(
4456
- `@rness/cli is not installed in ${shown}/.rness; install in .rness/, then ${rnessCommand()} sync
4457
- `
5101
+ async function offerSetupGit(opts, terminal, bin, ui) {
5102
+ if (opts.setupGit === false) return;
5103
+ if (!isStableInstall(bin)) {
5104
+ ui.hint(
5105
+ "install rness globally to let git use your login: npm i -g @rness/cli"
4458
5106
  );
4459
- return 1;
5107
+ return;
4460
5108
  }
4461
- const pinned = join17(installed.dir, ...installed.bin.split("/"));
4462
- if (!await exists(pinned)) {
4463
- process.stderr.write(
4464
- `@rness/cli in ${shown}/.rness is not installed correctly (missing ${installed.bin}); reinstall in .rness/
4465
- `
4466
- );
4467
- return 1;
5109
+ if (opts.setupGit !== true) {
5110
+ if (!terminal.isTty()) return;
5111
+ const p = await terminal.prompts();
5112
+ const ok = await p.confirm({
5113
+ message: "Use rness to authenticate git over HTTPS for github.com?",
5114
+ initialValue: true
5115
+ });
5116
+ if (p.isCancel(ok) || ok !== true) return;
4468
5117
  }
5118
+ const changed = await setupGit(process.execPath, bin);
5119
+ ui.line(
5120
+ changed ? "updated" : "unchanged",
5121
+ `git: rness answers for https://github.com (${rnessCommand()} logout undoes it)`,
5122
+ `git now asks rness for your github.com login over HTTPS (${rnessCommand()} logout undoes it)`
5123
+ );
5124
+ }
5125
+ async function manifestAround(cwd) {
4469
5126
  try {
4470
- const { stdout, stderr } = await execFileP4(
4471
- process.execPath,
4472
- [pinned, "sync", "--yes"],
5127
+ return await loadManifest((await findWorkspace(cwd)).rnessDir);
5128
+ } catch {
5129
+ return null;
5130
+ }
5131
+ }
5132
+ function splitScopes(scopes) {
5133
+ return scopes.split(/[\s,]+/).filter((s) => s !== "");
5134
+ }
5135
+ async function loginCommand(opts, deps = {}) {
5136
+ const terminal = deps.terminal ?? defaultTerminal;
5137
+ const own = deps.ui === void 0;
5138
+ const ui = deps.ui ?? await makeUi(terminal);
5139
+ if (own) ui.intro("Log in to GitHub");
5140
+ try {
5141
+ const manifest = await manifestAround(deps.cwd ?? process.cwd());
5142
+ if (manifest?.provider != null && !PROVIDERS[manifest.provider].available) {
5143
+ ui.error(unsupportedProviderMessage(manifest.provider));
5144
+ return 1;
5145
+ }
5146
+ const needsProject = opts.project === true || manifest?.pulse != null;
5147
+ const bin = deps.bin ?? await realpath(process.argv[1] ?? "").catch(() => "");
5148
+ const env = envToken();
5149
+ if (env !== null)
5150
+ ui.hint(`${env.source} is set and wins over a stored login`);
5151
+ const stored = await readAuth();
5152
+ const resolved = stored === null ? null : await resolveToken();
5153
+ const lacksProject = needsProject && resolved?.source === "login" && !splitScopes(stored?.tokens.scopes ?? "").includes(PULSE_SCOPE);
5154
+ if (stored !== null && resolved !== null && !lacksProject) {
5155
+ ui.line(
5156
+ "logged in",
5157
+ `as ${stored.login} (github.com); ${rnessCommand()} logout to switch`
5158
+ );
5159
+ await offerSetupGit(opts, terminal, bin, ui);
5160
+ if (own) ui.outro("Nothing to do", "idle");
5161
+ return 0;
5162
+ }
5163
+ if (lacksProject) ui.line("login", "logging in again for the project scope");
5164
+ const code = await requestDeviceCode(
5165
+ needsProject ? `${SCOPES} ${PULSE_SCOPE}` : SCOPES
5166
+ );
5167
+ if (ui.session)
5168
+ ui.line(
5169
+ "open",
5170
+ code.verificationUri,
5171
+ `Open ${code.verificationUri} and enter the code ${code.userCode}`
5172
+ );
5173
+ else {
5174
+ ui.line("open", code.verificationUri);
5175
+ ui.line("code", code.userCode);
5176
+ }
5177
+ if (terminal.isTty() && process.env["SSH_CONNECTION"] === void 0)
5178
+ (deps.open ?? openInBrowser)(code.verificationUri);
5179
+ if (!ui.session)
5180
+ ui.line("waiting", "for you to approve rness on github.com\u2026");
5181
+ const login = await ui.step(
4473
5182
  {
4474
- cwd: root,
4475
- maxBuffer: 16 * 1024 * 1024,
4476
- env: childEnv(),
4477
- timeout: CHILD_DEADLINE_MS,
4478
- killSignal: "SIGKILL"
4479
- }
5183
+ doing: "waiting for you to approve rness on github.com",
5184
+ sentence: "Waiting for your approval on github.com",
5185
+ quiet: true
5186
+ },
5187
+ async () => {
5188
+ const tokens = await pollForToken(code, deps);
5189
+ const user = await getUser({
5190
+ token: tokens.accessToken,
5191
+ apiBase: opts.githubApi ?? DEFAULT_GITHUB_API
5192
+ });
5193
+ await writeAuth({ login: user.login, tokens });
5194
+ return user.login;
5195
+ },
5196
+ (name) => ["logged in", `as ${name} (github.com)`]
4480
5197
  );
4481
- process.stdout.write(stdout);
4482
- if (stderr !== "") process.stderr.write(stderr);
5198
+ if (!ui.session) ui.line("logged in", `as ${login} (github.com)`);
5199
+ await offerSetupGit(opts, terminal, bin, ui);
5200
+ if (own) ui.outro(`You are logged in as ${login}`);
4483
5201
  return 0;
4484
5202
  } catch (e) {
4485
- const err = e;
4486
- if (err.stdout !== void 0 && err.stdout !== "")
4487
- process.stdout.write(err.stdout);
4488
- if (err.stderr !== void 0 && err.stderr !== "")
4489
- process.stderr.write(err.stderr);
4490
- return typeof err.code === "number" ? err.code : 1;
5203
+ if (e instanceof LoginError) {
5204
+ ui.error(e.message);
5205
+ return 1;
5206
+ }
5207
+ return reportError(e);
4491
5208
  }
4492
5209
  }
5210
+ function revokeUrl() {
5211
+ return `https://github.com/settings/connections/applications/${clientId()}`;
5212
+ }
5213
+
5214
+ // src/commands/create.ts
5215
+ import { mkdir as mkdir6, mkdtemp as mkdtemp2, rename as rename2, rm as rm6 } from "fs/promises";
5216
+ import { tmpdir as tmpdir2 } from "os";
5217
+ import { join as join21, relative as relative4, resolve as resolve5 } from "path";
5218
+
5219
+ // src/core/ask-provider.ts
5220
+ var PROVIDER_QUESTION = "Where does your organization live?";
5221
+ async function askProvider(p) {
5222
+ return p.select({
5223
+ message: PROVIDER_QUESTION,
5224
+ options: Object.values(PROVIDERS).map((entry) => ({
5225
+ value: entry.name,
5226
+ label: entry.available ? entry.label : `${entry.label} (coming later)`,
5227
+ disabled: !entry.available
5228
+ }))
5229
+ });
5230
+ }
5231
+
5232
+ // src/core/new-workspace.ts
5233
+ import { mkdir as mkdir4, readdir as readdir3, rm as rm5, stat as stat3 } from "fs/promises";
5234
+ import { dirname as dirname8, join as join19, relative as relative2 } from "path";
4493
5235
 
4494
5236
  // src/core/scaffold-git.ts
4495
5237
  import { execFile as execFile5 } from "child_process";
@@ -5782,9 +6524,9 @@ async function clonesToCommit(root, cwd, manifest) {
5782
6524
  }
5783
6525
  return clones;
5784
6526
  }
5785
- function finish(ui, cwd, rnessDir, label, target, ending) {
6527
+ function finish(ui, cwd, rnessDir, label, target2, ending) {
5786
6528
  const rel = relative5(cwd, rnessDir) || ".";
5787
- const message2 = `chore: rness ${target}`;
6529
+ const message2 = `chore: rness ${target2}`;
5788
6530
  const next = [];
5789
6531
  if (ending.commit === "committed") next.push(`git -C ${rel} push`);
5790
6532
  if (ending.commit === "refused")
@@ -5797,12 +6539,12 @@ function finish(ui, cwd, rnessDir, label, target, ending) {
5797
6539
  next.push("# teammates: git pull \u2014 the next rness command installs it");
5798
6540
  if (ui.session) {
5799
6541
  if (next.length > 0) ui.note("Next steps", next);
5800
- ui.outro(`${label} runs @rness/cli ${target}`);
6542
+ ui.outro(`${label} runs @rness/cli ${target2}`);
5801
6543
  } else
5802
6544
  process.stdout.write(
5803
6545
  [
5804
6546
  "",
5805
- status("upgraded", `${label} to @rness/cli ${target}`),
6547
+ status("upgraded", `${label} to @rness/cli ${target2}`),
5806
6548
  "",
5807
6549
  ...next.length > 0 ? ["Next:", ...next.map((line3) => ` ${line3}`), ""] : []
5808
6550
  ].join("\n")
@@ -5838,12 +6580,12 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
5838
6580
  }
5839
6581
  const ui = interactive ? await makeUi(terminal) : { ...plainUi };
5840
6582
  ui.intro(`Upgrade workspace ${basename5(ws.root)}`);
5841
- const target = version ?? await ui.step(
6583
+ const target2 = version ?? await ui.step(
5842
6584
  { doing: "asking the registry for the latest release", quiet: true },
5843
6585
  () => latestVersion(),
5844
6586
  (v) => v === null ? null : ["latest", `@rness/cli ${v}`]
5845
6587
  );
5846
- if (target === null) {
6588
+ if (target2 === null) {
5847
6589
  process.stderr.write(
5848
6590
  `cannot reach the registry; name the version: ${rnessCommand()} upgrade <version>
5849
6591
  `
@@ -5871,44 +6613,44 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
5871
6613
  return 1;
5872
6614
  }
5873
6615
  const base = await findScaffoldBase(ws.rnessDir);
5874
- const scaffoldCurrent = base !== null && base.version === target;
5875
- if (pin.spec === target && installed === target && scaffoldCurrent) {
5876
- if (ui.session) ui.outro(`Already at ${target}`);
5877
- else process.stdout.write(`already at ${target}
6616
+ const scaffoldCurrent = base !== null && base.version === target2;
6617
+ if (pin.spec === target2 && installed === target2 && scaffoldCurrent) {
6618
+ if (ui.session) ui.outro(`Already at ${target2}`);
6619
+ else process.stdout.write(`already at ${target2}
5878
6620
  `);
5879
6621
  return 0;
5880
6622
  }
5881
- const downgrading = EXACT_VERSION.test(pin.spec) && compareVersions(target, pin.spec) < 0;
6623
+ const downgrading = EXACT_VERSION.test(pin.spec) && compareVersions(target2, pin.spec) < 0;
5882
6624
  if (downgrading && version === void 0) {
5883
6625
  process.stderr.write(
5884
- `${label} pins @rness/cli ${pin.spec}, newer than the latest release ${target}; to downgrade: ${rnessCommand()} upgrade ${target}
6626
+ `${label} pins @rness/cli ${pin.spec}, newer than the latest release ${target2}; to downgrade: ${rnessCommand()} upgrade ${target2}
5885
6627
  `
5886
6628
  );
5887
6629
  return 1;
5888
6630
  }
5889
6631
  const pm = await workspacePackageManager(ws.rnessDir);
5890
- if (pin.spec === target && installed === target)
6632
+ if (pin.spec === target2 && installed === target2)
5891
6633
  ui.line(
5892
6634
  "scaffold",
5893
- `@rness/cli ${target} \u2014 merging it into ${label}`,
5894
- `Merging the @rness/cli ${target} scaffold into ${label}`
6635
+ `@rness/cli ${target2} \u2014 merging it into ${label}`,
6636
+ `Merging the @rness/cli ${target2} scaffold into ${label}`
5895
6637
  );
5896
- else if (pin.spec === target)
6638
+ else if (pin.spec === target2)
5897
6639
  ui.line(
5898
6640
  "install",
5899
- `@rness/cli ${target} in ${label} (${pm}) \u2014 ${installed ?? "nothing"} is installed`
6641
+ `@rness/cli ${target2} in ${label} (${pm}) \u2014 ${installed ?? "nothing"} is installed`
5900
6642
  );
5901
6643
  else {
5902
6644
  ui.line(
5903
6645
  "upgrade",
5904
- `@rness/cli ${pin.spec} \u2192 ${target} in ${label} (${pm})${downgrading ? " \u2014 downgrading" : ""}`
6646
+ `@rness/cli ${pin.spec} \u2192 ${target2} in ${label} (${pm})${downgrading ? " \u2014 downgrading" : ""}`
5905
6647
  );
5906
6648
  ui.line("notes", NOTES, `Release notes: ${NOTES}`);
5907
6649
  }
5908
6650
  if (interactive) {
5909
6651
  const p = await terminal.prompts();
5910
6652
  const ok = await p.confirm({
5911
- message: `Upgrade @rness/cli to ${target}?`
6653
+ message: `Upgrade @rness/cli to ${target2}?`
5912
6654
  });
5913
6655
  if (p.isCancel(ok) || ok !== true) {
5914
6656
  ui.cancelled();
@@ -5918,31 +6660,31 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
5918
6660
  let merged = false;
5919
6661
  if (!scaffoldCurrent) {
5920
6662
  const source = await (deps.scaffoldFor ?? scaffoldFor)(
5921
- target,
6663
+ target2,
5922
6664
  ws.rnessDir
5923
6665
  );
5924
6666
  try {
5925
6667
  const files = await renderScaffold(source.dir, {
5926
- version: target,
6668
+ version: target2,
5927
6669
  packageManager: await declaredManager(ws.rnessDir, pm)
5928
6670
  });
5929
6671
  if (base === null)
5930
6672
  ui.line(
5931
6673
  "adopting",
5932
- `the @rness/cli ${target} scaffold (no scaffold commit in ${label} yet)`,
5933
- `Adopting the @rness/cli ${target} scaffold \u2014 ${label} has no scaffold commit yet`
6674
+ `the @rness/cli ${target2} scaffold (no scaffold commit in ${label} yet)`,
6675
+ `Adopting the @rness/cli ${target2} scaffold \u2014 ${label} has no scaffold commit yet`
5934
6676
  );
5935
6677
  else
5936
6678
  ui.line(
5937
6679
  "merging",
5938
- `the @rness/cli ${target} scaffold`,
5939
- `Merging the @rness/cli ${target} scaffold`
6680
+ `the @rness/cli ${target2} scaffold`,
6681
+ `Merging the @rness/cli ${target2} scaffold`
5940
6682
  );
5941
6683
  const commit = await buildScaffoldCommit(
5942
6684
  ws.rnessDir,
5943
6685
  base?.commit ?? null,
5944
6686
  base === null ? await withoutRedundantKeeps(ws.rnessDir, files) : files,
5945
- target
6687
+ target2
5946
6688
  );
5947
6689
  const result = await mergeScaffold(
5948
6690
  ws.rnessDir,
@@ -5969,9 +6711,9 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
5969
6711
  [packageFile, await readFile9(packageFile, "utf8")]
5970
6712
  ]);
5971
6713
  const moved = await readPin(ws.rnessDir);
5972
- if (moved !== null && moved.spec !== target)
5973
- await writePin(ws.rnessDir, moved.spec, target);
5974
- const mustInstall = !(pin.spec === target && installed === target);
6714
+ if (moved !== null && moved.spec !== target2)
6715
+ await writePin(ws.rnessDir, moved.spec, target2);
6716
+ const mustInstall = !(pin.spec === target2 && installed === target2);
5975
6717
  if (mustInstall) {
5976
6718
  try {
5977
6719
  await ui.step(
@@ -6002,9 +6744,9 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
6002
6744
  return 1;
6003
6745
  }
6004
6746
  const now = (await readPinnedCli(ws.rnessDir))?.version ?? null;
6005
- if (now !== target) {
6747
+ if (now !== target2) {
6006
6748
  process.stderr.write(
6007
- `${pm} install left @rness/cli ${now ?? "not installed"} in ${label}, not ${target}
6749
+ `${pm} install left @rness/cli ${now ?? "not installed"} in ${label}, not ${target2}
6008
6750
  `
6009
6751
  );
6010
6752
  return 1;
@@ -6013,7 +6755,7 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
6013
6755
  ui.gitIsSilent = true;
6014
6756
  const code = await syncBlocks(ui, ws.root, basename5(ws.root));
6015
6757
  if (code !== 0) return code;
6016
- const message2 = `chore: rness ${target}`;
6758
+ const message2 = `chore: rness ${target2}`;
6017
6759
  const committed = await commitUpgrade(ws.rnessDir, message2);
6018
6760
  if (committed.kind === "committed")
6019
6761
  ui.line(
@@ -6028,7 +6770,7 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
6028
6770
  for (const line3 of committed.output.split("\n"))
6029
6771
  if (line3.trim() !== "") ui.hint(line3, "stderr");
6030
6772
  }
6031
- return finish(ui, cwd, ws.rnessDir, label, target, {
6773
+ return finish(ui, cwd, ws.rnessDir, label, target2, {
6032
6774
  commit: committed.kind,
6033
6775
  clones: await clonesToCommit(
6034
6776
  ws.root,
@@ -6153,11 +6895,11 @@ password=${resolved.token}
6153
6895
 
6154
6896
  // src/commands/hook.ts
6155
6897
  import { realpath as realpath4 } from "fs/promises";
6156
- import { isAbsolute as isAbsolute2, posix as posix7, relative as relative7, resolve as resolve7, sep as sep5 } from "path";
6898
+ import { isAbsolute as isAbsolute2, posix as posix8, relative as relative7, resolve as resolve7, sep as sep5 } from "path";
6157
6899
 
6158
6900
  // src/mcp/tools.ts
6159
6901
  import { readFile as readFile10, realpath as realpath3, stat as stat4 } from "fs/promises";
6160
- import { isAbsolute, join as join24, posix as posix6, relative as relative6, resolve as resolve6, sep as sep4 } from "path";
6902
+ import { isAbsolute, join as join24, posix as posix7, relative as relative6, resolve as resolve6, sep as sep4 } from "path";
6161
6903
  var LABELS = {
6162
6904
  standards: "Standards",
6163
6905
  adr: "Decisions",
@@ -6197,7 +6939,7 @@ async function documents(rnessDir, only) {
6197
6939
  scope: ownerScope(item.rel),
6198
6940
  id,
6199
6941
  status: typeof status2 === "string" ? status2 : null,
6200
- title: documentTitle(item.title ?? posix6.basename(item.rel, ".md"), id),
6942
+ title: documentTitle(item.title ?? posix7.basename(item.rel, ".md"), id),
6201
6943
  text: item.body
6202
6944
  });
6203
6945
  }
@@ -6227,7 +6969,7 @@ async function context(cwd, args) {
6227
6969
  } else
6228
6970
  scope = resolveScope(
6229
6971
  manifest,
6230
- relative6(ws.root, cwd).split(sep4).join(posix6.sep)
6972
+ relative6(ws.root, cwd).split(sep4).join(posix7.sep)
6231
6973
  );
6232
6974
  const summary = await scopeSummary(ws, manifest, scope);
6233
6975
  return {
@@ -6416,8 +7158,9 @@ function mcpTools(cwd) {
6416
7158
 
6417
7159
  // src/pulse/detached.ts
6418
7160
  import { spawn as nodeSpawn } from "child_process";
6419
- import { mkdir as mkdir7, readFile as readFile11, rm as rm8, writeFile as writeFile4 } from "fs/promises";
7161
+ import { mkdir as mkdir7, open as open2, readFile as readFile11, rm as rm8, stat as stat5, writeFile as writeFile4 } from "fs/promises";
6420
7162
  import { join as join25 } from "path";
7163
+ import { setTimeout as delay2 } from "timers/promises";
6421
7164
  var detached = (args, cwd) => {
6422
7165
  const bin = process.argv[1];
6423
7166
  if (bin === void 0) return;
@@ -6452,6 +7195,33 @@ async function takeFailure() {
6452
7195
  return null;
6453
7196
  }
6454
7197
  }
7198
+ var PULSE_LOCK_STALE_MS = 15 * 6e4;
7199
+ async function oneSyncAtATime(key, work, options = {}) {
7200
+ await mkdir7(configDir(), { recursive: true, mode: 448 });
7201
+ const file = join25(configDir(), `pulse-${key}.lock`);
7202
+ for (; ; ) {
7203
+ try {
7204
+ const handle = await open2(file, "wx");
7205
+ await handle.close();
7206
+ break;
7207
+ } catch (e) {
7208
+ if (e.code !== "EEXIST") throw e;
7209
+ const age = await stat5(file).then(
7210
+ (s) => Date.now() - s.mtimeMs,
7211
+ () => 0
7212
+ );
7213
+ if (age > PULSE_LOCK_STALE_MS) await rm8(file, { force: true });
7214
+ else if (options.wait !== true) return false;
7215
+ else await (options.sleep ?? ((ms) => delay2(ms)))(1e3);
7216
+ }
7217
+ }
7218
+ try {
7219
+ await work();
7220
+ return true;
7221
+ } finally {
7222
+ await rm8(file, { force: true });
7223
+ }
7224
+ }
6455
7225
 
6456
7226
  // src/commands/hook.ts
6457
7227
  var NOUNS = {
@@ -6479,7 +7249,7 @@ function cwdOf(input, env) {
6479
7249
  if (typeof cwd === "string" && cwd !== "") return cwd;
6480
7250
  return env["CLAUDE_PROJECT_DIR"] ?? process.cwd();
6481
7251
  }
6482
- var toPosix2 = (p) => p.split(sep5).join(posix7.sep);
7252
+ var toPosix2 = (p) => p.split(sep5).join(posix8.sep);
6483
7253
  function count(n, [one, many]) {
6484
7254
  return `${n} ${n === 1 ? one : many}`;
6485
7255
  }
@@ -6823,29 +7593,162 @@ async function mcpCommand(opts, io = {
6823
7593
  return 0;
6824
7594
  }
6825
7595
 
7596
+ // src/commands/pulse.ts
7597
+ import { readFile as readFile13, readdir as readdir4 } from "fs/promises";
7598
+ import { join as join26 } from "path";
7599
+ import { setTimeout as delay3 } from "timers/promises";
7600
+
7601
+ // src/pulse/opened.ts
7602
+ import { mkdir as mkdir8, readFile as readFile12, rename as rename3, rm as rm9, writeFile as writeFile5 } from "fs/promises";
7603
+ import { dirname as dirname9 } from "path";
7604
+ var NAME2 = "rness/opened";
7605
+ function openedFile(rnessDir) {
7606
+ return gitPath(rnessDir, NAME2);
7607
+ }
7608
+ var NUMBER = /^[1-9][0-9]*$/;
7609
+ async function readOpened(file) {
7610
+ const opened = /* @__PURE__ */ new Map();
7611
+ if (file === null) return opened;
7612
+ let data;
7613
+ try {
7614
+ data = JSON.parse(await readFile12(file, "utf8"));
7615
+ } catch {
7616
+ return opened;
7617
+ }
7618
+ if (data === null || typeof data !== "object" || Array.isArray(data))
7619
+ return opened;
7620
+ for (const [key, path] of Object.entries(data))
7621
+ if (NUMBER.test(key) && typeof path === "string" && path !== "")
7622
+ opened.set(Number(key), path);
7623
+ return opened;
7624
+ }
7625
+ async function writeOpened(file, opened) {
7626
+ if (file === null) return;
7627
+ try {
7628
+ if (opened.size === 0) {
7629
+ await rm9(file, { force: true });
7630
+ return;
7631
+ }
7632
+ const entries = [...opened].sort(([a], [b]) => a - b);
7633
+ await mkdir8(dirname9(file), { recursive: true });
7634
+ const temporary = `${file}.${process.pid}`;
7635
+ await writeFile5(
7636
+ temporary,
7637
+ `${JSON.stringify(Object.fromEntries(entries), null, 2)}
7638
+ `
7639
+ );
7640
+ await rename3(temporary, file);
7641
+ } catch {
7642
+ }
7643
+ }
7644
+
6826
7645
  // src/pulse/plan.ts
6827
- function planSync(want, have) {
7646
+ var differs = (item, w) => item.title !== w.title || item.status !== w.status || w.statusField !== null && item.collectionStatus !== w.status || item.type !== w.type;
7647
+ var rank = (i) => i.issue === null ? 2 : i.issue.open ? 0 : 1;
7648
+ var keptFirst = (a, b) => rank(a) - rank(b) || (a.issue?.number ?? 0) - (b.issue?.number ?? 0);
7649
+ var gone = (item) => item.issue === null ? { kind: "archive", id: item.id } : { kind: "close", id: item.id };
7650
+ var firstLine5 = (body) => body.split(/\r\n?|\n/, 1)[0] ?? "";
7651
+ function strays(have) {
7652
+ const byLine = /* @__PURE__ */ new Map();
7653
+ for (const item of have) {
7654
+ if (item.path !== null || item.issue === null || item.issue.body === null || !item.issue.labelled)
7655
+ continue;
7656
+ const line3 = firstLine5(item.issue.body);
7657
+ byLine.set(line3, [...byLine.get(line3) ?? [], item]);
7658
+ }
7659
+ for (const items of byLine.values()) items.sort(keptFirst);
7660
+ return byLine;
7661
+ }
7662
+ function unwantedPaths(want, have) {
7663
+ const wanted = new Set(want.map((w) => w.path));
7664
+ const paths = /* @__PURE__ */ new Set();
7665
+ for (const item of have)
7666
+ if (item.path !== null && !wanted.has(item.path)) paths.add(item.path);
7667
+ return [...paths];
7668
+ }
7669
+ function isGone(path, item, seen, opened) {
7670
+ return seen.has(path) || item.issue !== null && opened.get(item.issue.number) === path;
7671
+ }
7672
+ function planSync(want, have, seen = /* @__PURE__ */ new Set(), opened = /* @__PURE__ */ new Map()) {
6828
7673
  const byPath = /* @__PURE__ */ new Map();
6829
- const steps = [];
7674
+ for (const item of have)
7675
+ if (item.path !== null)
7676
+ byPath.set(item.path, [...byPath.get(item.path) ?? [], item]);
7677
+ const kept = /* @__PURE__ */ new Map();
6830
7678
  const surplus = [];
6831
- for (const item of have) {
6832
- if (item.path === null) continue;
6833
- if (byPath.has(item.path)) surplus.push({ kind: "archive", id: item.id });
6834
- else byPath.set(item.path, item);
7679
+ for (const [path, items] of byPath) {
7680
+ const [keep, ...rest] = [...items].sort(keptFirst);
7681
+ if (keep !== void 0) kept.set(path, keep);
7682
+ surplus.push(...rest.map(gone));
6835
7683
  }
7684
+ const left = strays(have);
7685
+ const steps = [];
6836
7686
  const wanted = /* @__PURE__ */ new Set();
6837
7687
  for (const w of want) {
6838
7688
  wanted.add(w.path);
6839
- const item = byPath.get(w.path);
7689
+ const item = kept.get(w.path) ?? left.get(w.body)?.[0];
6840
7690
  if (item === void 0) steps.push({ kind: "create", want: w });
6841
- else if (item.title !== w.title || item.status !== w.status || w.statusField !== null && item.collectionStatus !== w.status || item.type !== w.type)
6842
- steps.push({ kind: "update", id: item.id, want: w });
7691
+ else if (item.issue === null)
7692
+ steps.push({ kind: "convert", id: item.id, want: w });
7693
+ else if (item.path === null || !item.issue.open || !item.issue.labelled || differs(item, w))
7694
+ steps.push({
7695
+ kind: "update",
7696
+ id: item.id,
7697
+ want: w,
7698
+ reopen: !item.issue.open
7699
+ });
6843
7700
  else steps.push({ kind: "unchanged", id: item.id });
6844
7701
  }
6845
- for (const [path, item] of byPath)
6846
- if (!wanted.has(path)) steps.push({ kind: "archive", id: item.id });
7702
+ for (const [path, item] of kept)
7703
+ if (!wanted.has(path))
7704
+ steps.push(
7705
+ isGone(path, item, seen, opened) ? gone(item) : { kind: "unchanged", id: item.id }
7706
+ );
6847
7707
  return [...steps, ...surplus];
6848
7708
  }
7709
+ function stillOpened(opened, have, seen, closed, created) {
7710
+ const items = /* @__PURE__ */ new Map();
7711
+ for (const item of have)
7712
+ if (item.issue !== null) items.set(item.issue.number, item.id);
7713
+ const next = /* @__PURE__ */ new Map();
7714
+ for (const [number, path] of opened) {
7715
+ const id = items.get(number);
7716
+ if (id !== void 0 && !closed.has(id) && !seen.has(path))
7717
+ next.set(number, path);
7718
+ }
7719
+ for (const [path, placed] of created) next.set(placed.number, path);
7720
+ return next;
7721
+ }
7722
+ function issuedAfter(steps, have, placed) {
7723
+ const byId = new Map(have.map((i) => [i.id, i]));
7724
+ const issued = /* @__PURE__ */ new Map();
7725
+ for (const step2 of steps) {
7726
+ if (step2.kind === "create" || step2.kind === "convert") {
7727
+ const p = placed.get(step2.want.path);
7728
+ if (p !== void 0) issued.set(step2.want.path, { ...p, body: null });
7729
+ } else if (step2.kind === "update" || step2.kind === "unchanged") {
7730
+ const item = byId.get(step2.id);
7731
+ const path = step2.kind === "update" ? step2.want.path : item?.path;
7732
+ if (item === void 0 || path == null || item.issue === null) continue;
7733
+ issued.set(path, {
7734
+ id: item.id,
7735
+ number: item.issue.number,
7736
+ body: item.issue.body
7737
+ });
7738
+ }
7739
+ }
7740
+ return issued;
7741
+ }
7742
+ function planBodies(bodies, issued) {
7743
+ const steps = [];
7744
+ for (const [path, body] of bodies) {
7745
+ const i = issued.get(path);
7746
+ if (i === void 0) continue;
7747
+ if (i.body === null || digestOf(i.body) !== digestOf(body))
7748
+ steps.push({ kind: "body", id: i.id, body });
7749
+ }
7750
+ return steps;
7751
+ }
6849
7752
 
6850
7753
  // src/commands/pulse.ts
6851
7754
  var PROJECT_SCOPE = "project";
@@ -6861,15 +7764,23 @@ async function fromGithub(work) {
6861
7764
  throw e;
6862
7765
  }
6863
7766
  }
6864
- async function context2(opts) {
7767
+ var apiOf = (opts) => opts.githubApi === void 0 ? {} : { apiBase: opts.githubApi };
7768
+ var providerOptions = (opts, wait) => ({
7769
+ ...apiOf(opts),
7770
+ ...wait === void 0 ? {} : { wait }
7771
+ });
7772
+ async function context2(opts, wait) {
6865
7773
  const ws = await findWorkspace(opts.cwd ?? process.cwd());
6866
7774
  const manifest = await loadManifest(ws.rnessDir);
6867
- const provider = await openProvider(manifest, apiOf(opts));
7775
+ const provider = await openProvider(manifest, providerOptions(opts, wait));
6868
7776
  if (manifest.org === null)
6869
7777
  throw new Error('the pulse needs an organization: rness.json has no "org"');
6870
7778
  return { rnessDir: ws.rnessDir, manifest, org: manifest.org, provider };
6871
7779
  }
6872
- var apiOf = (opts) => opts.githubApi === void 0 ? {} : { apiBase: opts.githubApi };
7780
+ var waitSaid = (opts, ui) => rateWait(async (ms) => {
7781
+ ui.line("waiting", `${Math.round(ms / 1e3)} s \u2014 GitHub's rate limit`);
7782
+ await (opts.sleep ?? ((n) => delay3(n)))(ms);
7783
+ });
6873
7784
  async function missingAccess(provider) {
6874
7785
  if (!provider.authenticated) return NEEDS_LOGIN;
6875
7786
  const scopes = await fromGithub(provider.scopes());
@@ -6895,33 +7806,131 @@ var createdByKind = (ui, added) => {
6895
7806
  const names = added.filter((a) => a.startsWith(`${kind} `)).map((a) => a.slice(kind.length + 1));
6896
7807
  if (names.length > 0) ui.line("created", `${kind}s ${names.join(", ")}`);
6897
7808
  }
7809
+ for (const a of added) {
7810
+ if (a.startsWith("label ")) ui.line("created", a);
7811
+ else if (a.startsWith("link "))
7812
+ ui.line("linked", `Agent Pulse to ${a.slice("link ".length)}`);
7813
+ }
6898
7814
  };
7815
+ async function filesOf2(rnessDir) {
7816
+ const files = /* @__PURE__ */ new Set();
7817
+ const walk = async (rel) => {
7818
+ const dir = rel === "" ? rnessDir : join26(rnessDir, ...rel.split("/"));
7819
+ for (const e of await readdir4(dir, { withFileTypes: true })) {
7820
+ const path = rel === "" ? e.name : `${rel}/${e.name}`;
7821
+ if (e.isDirectory()) {
7822
+ if (e.name !== ".git" && e.name !== "node_modules") await walk(path);
7823
+ } else if (e.isFile()) files.add(path);
7824
+ }
7825
+ };
7826
+ await walk("");
7827
+ return files;
7828
+ }
7829
+ var count2 = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
7830
+ function sayDone(ui, org, documents2, steps, writes) {
7831
+ const n = (kind) => steps.filter((s) => s.kind === kind).length;
7832
+ const reopened = steps.filter((s) => s.kind === "update" && s.reopen).length;
7833
+ if (n("create") > 0) ui.line("created", count2(n("create"), "issue"));
7834
+ if (n("convert") > 0)
7835
+ ui.line(
7836
+ "converted",
7837
+ `${count2(n("convert"), "draft")} into issues of ${org}/.rness`
7838
+ );
7839
+ if (reopened > 0) ui.line("reopened", count2(reopened, "issue"));
7840
+ const written = new Set(writes.map((w) => w.id));
7841
+ const bodyOnly = steps.filter(
7842
+ (s) => s.kind === "unchanged" && written.has(s.id)
7843
+ ).length;
7844
+ const parts = [
7845
+ ["created", n("create")],
7846
+ ["converted", n("convert")],
7847
+ ["updated", n("update") + bodyOnly],
7848
+ [
7849
+ "unchanged",
7850
+ documents2 - n("create") - n("convert") - n("update") - bodyOnly
7851
+ ],
7852
+ ["archived", n("close") + n("archive")]
7853
+ ].filter(([, k]) => k > 0).map(([word, k]) => `${k} ${word}`);
7854
+ ui.line(
7855
+ "synced",
7856
+ `${plural(documents2)}${parts.length > 0 ? `: ${parts.join(", ")}` : ""}`
7857
+ );
7858
+ }
6899
7859
  async function syncBoard(c, board, ui, sayLayout) {
7860
+ await fromGithub(c.provider.checkIssues(c.org));
6900
7861
  const tabs = await statusTabs(c.rnessDir);
6901
7862
  const want = desiredOf(tabs, c.org);
6902
7863
  sayLayout(
6903
7864
  ui,
6904
7865
  await fromGithub(c.provider.ensureLayout(board, layoutOf(tabs)))
6905
7866
  );
6906
- const steps = planSync(want, await fromGithub(c.provider.items(board)));
6907
- for (const step2 of steps) await fromGithub(c.provider.apply(board, step2));
6908
- const count2 = (kind) => steps.filter((s) => s.kind === kind).length;
6909
- const parts = [
6910
- ["created", "create"],
6911
- ["updated", "update"],
6912
- ["unchanged", "unchanged"],
6913
- ["archived", "archive"]
6914
- ].map(([word, kind]) => [word, count2(kind)]).filter(([, n]) => n > 0).map(([word, n]) => `${n} ${word}`);
6915
- ui.line(
6916
- "synced",
6917
- `${plural(want.length)}${parts.length > 0 ? `: ${parts.join(", ")}` : ""}`
6918
- );
7867
+ const have = await fromGithub(c.provider.items(board));
7868
+ const record = await openedFile(c.rnessDir);
7869
+ const opened = await readOpened(record);
7870
+ const seen = await seenPaths(c.rnessDir, [
7871
+ ...unwantedPaths(want, have),
7872
+ ...opened.values()
7873
+ ]);
7874
+ const steps = planSync(want, have, seen, opened);
7875
+ const changes = steps.filter((s) => s.kind !== "unchanged").length;
7876
+ let writes = [];
7877
+ let made = 0;
7878
+ try {
7879
+ const placed = /* @__PURE__ */ new Map();
7880
+ const created = /* @__PURE__ */ new Map();
7881
+ const closed = /* @__PURE__ */ new Set();
7882
+ try {
7883
+ for (const step2 of steps) {
7884
+ const p = await fromGithub(c.provider.apply(board, step2));
7885
+ if (p !== null && (step2.kind === "create" || step2.kind === "convert"))
7886
+ placed.set(step2.want.path, p);
7887
+ if (p !== null && step2.kind === "create") created.set(step2.want.path, p);
7888
+ if (step2.kind === "close" || step2.kind === "archive")
7889
+ closed.add(step2.id);
7890
+ if (step2.kind !== "unchanged") made++;
7891
+ }
7892
+ } finally {
7893
+ await writeOpened(
7894
+ record,
7895
+ stillOpened(opened, have, seen, closed, created)
7896
+ );
7897
+ }
7898
+ const issued = issuedAfter(steps, have, placed);
7899
+ const numbers = new Map([...issued].map(([path, i]) => [path, i.number]));
7900
+ const files = await filesOf2(c.rnessDir);
7901
+ const bodies = /* @__PURE__ */ new Map();
7902
+ for (const w of want)
7903
+ bodies.set(
7904
+ w.path,
7905
+ issueBody({
7906
+ path: w.path,
7907
+ text: await readFile13(join26(c.rnessDir, ...w.path.split("/")), "utf8"),
7908
+ org: c.org,
7909
+ numbers,
7910
+ files
7911
+ })
7912
+ );
7913
+ writes = planBodies(bodies, issued);
7914
+ for (const step2 of writes) {
7915
+ await fromGithub(c.provider.apply(board, step2));
7916
+ made++;
7917
+ }
7918
+ } catch (e) {
7919
+ if (!(e instanceof RateLimitError)) throw e;
7920
+ const total = changes + writes.length;
7921
+ throw new Error(
7922
+ `${e.message}: ${total - made} of ${total} changes not made \u2014 the next rness pulse sync makes them`,
7923
+ { cause: e }
7924
+ );
7925
+ }
7926
+ sayDone(ui, c.org, want.length, steps, writes);
6919
7927
  }
6920
7928
  async function pulseCreateCommand(opts, deps = {}) {
6921
7929
  try {
6922
7930
  const terminal = deps.terminal ?? defaultTerminal;
6923
7931
  const ui = deps.ui ?? await makeUi(terminal);
6924
- let c = await context2(opts);
7932
+ const wait = waitSaid(opts, ui);
7933
+ let c = await context2(opts, wait);
6925
7934
  if (c.manifest.pulse !== null)
6926
7935
  throw new Error(
6927
7936
  `already declared: ${boardUrl(c.org, c.manifest.pulse.project)} \u2014 rness pulse sync`
@@ -6949,10 +7958,14 @@ async function pulseCreateCommand(opts, deps = {}) {
6949
7958
  { terminal, ui, ...opts.cwd === void 0 ? {} : { cwd: opts.cwd } }
6950
7959
  );
6951
7960
  if (code !== 0) return code;
6952
- c = { ...c, provider: await openProvider(c.manifest, apiOf(opts)) };
7961
+ c = {
7962
+ ...c,
7963
+ provider: await openProvider(c.manifest, providerOptions(opts, wait))
7964
+ };
6953
7965
  const still = await missingAccess(c.provider);
6954
7966
  if (still !== null) throw new Error(still);
6955
7967
  }
7968
+ await fromGithub(c.provider.checkIssues(c.org));
6956
7969
  const login = (await c.provider.identity())?.login ?? "unknown";
6957
7970
  ui.line(
6958
7971
  "checked",
@@ -6981,7 +7994,7 @@ async function pulseCreateCommand(opts, deps = {}) {
6981
7994
  async function pulseSyncCommand(opts, deps = {}) {
6982
7995
  try {
6983
7996
  const ui = deps.ui ?? await makeUi(deps.terminal ?? defaultTerminal);
6984
- const c = await context2(opts);
7997
+ const c = await context2(opts, waitSaid(opts, ui));
6985
7998
  await syncBoard(c, await declaredBoard(c), ui, eachAdded);
6986
7999
  return 0;
6987
8000
  } catch (e) {
@@ -6999,17 +8012,37 @@ async function pulseMarkCommand(opts) {
6999
8012
  async function mark(opts) {
7000
8013
  const c = await context2(opts);
7001
8014
  const board = await declaredBoard(c);
7002
- const items = await fromGithub(c.provider.items(board));
8015
+ const key = `${c.org}-${board.number}`;
8016
+ const lock = opts.sleep === void 0 ? {} : { sleep: opts.sleep };
8017
+ const listed = () => fromGithub(c.provider.items(board, { bodies: false }));
8018
+ let items = await listed();
7003
8019
  if (opts.end === true) {
7004
8020
  const id = opts.session.split(" \xB7 ").pop() ?? opts.session;
7005
8021
  const ids2 = items.filter(
7006
8022
  (i) => i.session === opts.session || i.session?.endsWith(` \xB7 ${id}`)
7007
8023
  ).map((i) => i.id);
7008
8024
  await fromGithub(c.provider.mark(board, ids2, null));
7009
- await syncBoard(c, board, plainUi, eachAdded);
8025
+ await oneSyncAtATime(key, () => syncBoard(c, board, plainUi, eachAdded), {
8026
+ ...lock,
8027
+ wait: true
8028
+ });
7010
8029
  return 0;
7011
8030
  }
7012
8031
  const wanted = new Set(opts.paths);
8032
+ const documents2 = new Set(
8033
+ (await statusTabs(c.rnessDir)).flatMap((t) => t.rows.map((r) => r.path))
8034
+ );
8035
+ const onBoard = new Set(
8036
+ items.flatMap((i) => i.path === null ? [] : [i.path])
8037
+ );
8038
+ if ([...wanted].some((p) => documents2.has(p) && !onBoard.has(p))) {
8039
+ const synced = await oneSyncAtATime(
8040
+ key,
8041
+ () => syncBoard(c, board, plainUi, eachAdded),
8042
+ lock
8043
+ );
8044
+ if (synced) items = await listed();
8045
+ }
7013
8046
  const ids = items.filter((i) => i.path !== null && wanted.has(i.path)).map((i) => i.id);
7014
8047
  await fromGithub(c.provider.mark(board, ids, opts.session));
7015
8048
  return 0;
@@ -7500,6 +8533,7 @@ export {
7500
8533
  defaultTransport,
7501
8534
  findWorkspace,
7502
8535
  reportError,
8536
+ findDelegate,
7503
8537
  BEGIN,
7504
8538
  END,
7505
8539
  BLOCK_SIZE_WARNING,
@@ -7514,7 +8548,6 @@ export {
7514
8548
  addCommand,
7515
8549
  renderMarkdown,
7516
8550
  contextCommand,
7517
- findDelegate,
7518
8551
  SCAFFOLD_FILES,
7519
8552
  scaffoldDir,
7520
8553
  copyScaffold,
@@ -7528,4 +8561,4 @@ export {
7528
8561
  OWN_COMMANDS,
7529
8562
  catchUp
7530
8563
  };
7531
- //# sourceMappingURL=chunk-AEW7ZDFS.js.map
8564
+ //# sourceMappingURL=chunk-AXTFDXN2.js.map