@rness/cli 0.12.0 → 0.13.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.13.0";
460
490
 
461
491
  // src/core/pm.ts
462
492
  import { execFile as execFile2 } from "child_process";
@@ -599,8 +629,8 @@ async function collectMarkdown(dir) {
599
629
  }
600
630
 
601
631
  // src/core/scope.ts
602
- function isPrefix(prefix, target) {
603
- return prefix === target || target.startsWith(`${prefix}/`);
632
+ function isPrefix(prefix, target2) {
633
+ return prefix === target2 || target2.startsWith(`${prefix}/`);
604
634
  }
605
635
  function resolveScope(manifest, relDir) {
606
636
  let best = null;
@@ -624,7 +654,7 @@ function scopeChain(manifest, scope) {
624
654
  chain.push(name);
625
655
  stack.add(name);
626
656
  const entry = Object.hasOwn(manifest.scopes, name) ? manifest.scopes[name] : void 0;
627
- for (const target of entry?.extends ?? []) visit(target, stack);
657
+ for (const target2 of entry?.extends ?? []) visit(target2, stack);
628
658
  stack.delete(name);
629
659
  };
630
660
  visit(scope, /* @__PURE__ */ new Set());
@@ -1116,6 +1146,158 @@ function statusMarkdown(workspace, tabs) {
1116
1146
  return out.join("\n");
1117
1147
  }
1118
1148
 
1149
+ // src/pulse/body.ts
1150
+ import { createHash } from "crypto";
1151
+ import { posix as posix4 } from "path";
1152
+ var BODY_LIMIT = 65536;
1153
+ var blobUrl = (org, path) => `https://github.com/${org}/.rness/blob/main/${encodeURI(path)}`;
1154
+ var issueUrl = (org, number) => `https://github.com/${org}/.rness/issues/${number}`;
1155
+ function headerOf(path, org) {
1156
+ return `\`${path}\` \xB7 [on GitHub](${blobUrl(org, path)})`;
1157
+ }
1158
+ var digest = (text) => createHash("sha256").update(text, "utf8").digest("hex").slice(0, 12);
1159
+ var comment = (d) => `<!-- rness ${d} -->`;
1160
+ var TAIL = `
1161
+
1162
+ ${comment("0".repeat(12))}`.length;
1163
+ var LAST_LINE = /\n\n<!-- rness ([0-9a-f]{12}) -->\s*$/;
1164
+ function digestOf(body) {
1165
+ const text = body.replace(/\r\n?/g, "\n");
1166
+ const m = LAST_LINE.exec(text);
1167
+ const d = m?.[1];
1168
+ if (m === null || d === void 0) return null;
1169
+ return digest(text.slice(0, m.index)) === d ? d : null;
1170
+ }
1171
+ var OPENER = /^[ \t]*(`{3,}|~{3,})(.*)$/;
1172
+ var CLOSER = /^[ \t]*(`{3,}|~{3,})[ \t]*$/;
1173
+ function fenced(lines) {
1174
+ const code = [];
1175
+ let run3 = null;
1176
+ for (const line3 of lines) {
1177
+ if (run3 === null) {
1178
+ const m = OPENER.exec(line3);
1179
+ const fence2 = m?.[1];
1180
+ if (fence2 !== void 0 && !(fence2.startsWith("`") && (m?.[2] ?? "").includes("`")))
1181
+ run3 = fence2;
1182
+ code.push(run3 !== null);
1183
+ continue;
1184
+ }
1185
+ code.push(true);
1186
+ const fence = CLOSER.exec(line3)?.[1];
1187
+ if (fence !== void 0 && fence[0] === run3[0] && fence.length >= run3.length)
1188
+ run3 = null;
1189
+ }
1190
+ return code;
1191
+ }
1192
+ var TITLE = /^ {0,3}#(?:[ \t]|$)/;
1193
+ function withoutTitle(lines) {
1194
+ const code = fenced(lines);
1195
+ const at = lines.findIndex((line3, i) => code[i] !== true && TITLE.test(line3));
1196
+ if (at < 0) return [...lines];
1197
+ let end = at + 1;
1198
+ while (end < lines.length && (lines[end] ?? "").trim() === "") end++;
1199
+ return [...lines.slice(0, at), ...lines.slice(end)];
1200
+ }
1201
+ var SCHEME = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
1202
+ var IMAGE = /\.(?:png|jpe?g|gif|svg|webp|avif)$/i;
1203
+ var decoded = (path) => {
1204
+ try {
1205
+ return decodeURI(path);
1206
+ } catch {
1207
+ return path;
1208
+ }
1209
+ };
1210
+ function target(dest, s) {
1211
+ const raw = dest.startsWith("<") ? dest.slice(1, -1) : dest;
1212
+ if (raw === "" || SCHEME.test(raw) || raw.startsWith("//") || raw.startsWith("#"))
1213
+ return dest;
1214
+ const hash = raw.indexOf("#");
1215
+ const pathPart = hash < 0 ? raw : raw.slice(0, hash);
1216
+ const anchor = hash < 0 ? "" : raw.slice(hash);
1217
+ const file = decoded(
1218
+ posix4.normalize(
1219
+ pathPart.startsWith("/") ? pathPart.slice(1) : posix4.join(posix4.dirname(s.path), pathPart)
1220
+ )
1221
+ );
1222
+ const number = s.numbers.get(file);
1223
+ if (number !== void 0) return issueUrl(s.org, number);
1224
+ if (!s.files.has(file)) return dest;
1225
+ return `${blobUrl(s.org, file)}${IMAGE.test(file) ? "?raw=true" : ""}${anchor}`;
1226
+ }
1227
+ var PROTECTED = new RegExp(
1228
+ [
1229
+ /(?<!`)(`+)(?!`)[\s\S]*?(?<!`)\1(?!`)/.source,
1230
+ /<!--[\s\S]*?-->/.source,
1231
+ /<[a-zA-Z][a-zA-Z0-9+.-]*:[^\s<>]*>/.source,
1232
+ /^ {0,3}\[(?:[^\]\\\n]|\\.)+\]:[ \t]*(?<def><[^>\n]*>|\S+)/.source,
1233
+ /\]\([ \t]*(?<dest><[^>\n]*>|[^\s()]+)/.source,
1234
+ /\b(?:https?:\/\/|www\.)[^\s<>]*[^\s<>.,:;"')\]]/.source
1235
+ ].join("|"),
1236
+ "gm"
1237
+ );
1238
+ var MENTION = /(^|[^A-Za-z0-9`\\])@([A-Za-z0-9][A-Za-z0-9-]*)/g;
1239
+ var REFERENCE = /(^|[^A-Za-z0-9&#/])#(\d+)(?![A-Za-z0-9])/g;
1240
+ var ZWSP = "\u200B";
1241
+ var neutral = (text) => text.replace(MENTION, "$1\\@$2").replace(REFERENCE, `$1#${ZWSP}$2`);
1242
+ function inline(text, s) {
1243
+ let out = "";
1244
+ let from = 0;
1245
+ for (const m of text.matchAll(PROTECTED)) {
1246
+ const at = m.index ?? 0;
1247
+ out += neutral(text.slice(from, at));
1248
+ const dest = m.groups?.["dest"] ?? m.groups?.["def"];
1249
+ out += dest === void 0 ? m[0] : `${m[0].slice(0, m[0].length - dest.length)}${target(dest, s)}`;
1250
+ from = at + m[0].length;
1251
+ }
1252
+ return out + neutral(text.slice(from));
1253
+ }
1254
+ function rewrite(lines, s) {
1255
+ const code = fenced(lines);
1256
+ const out = [];
1257
+ let paragraph = [];
1258
+ const flush = () => {
1259
+ if (paragraph.length > 0) out.push(inline(paragraph.join("\n"), s));
1260
+ paragraph = [];
1261
+ };
1262
+ lines.forEach((line3, i) => {
1263
+ if (code[i] === true || line3.trim() === "") {
1264
+ flush();
1265
+ out.push(line3);
1266
+ } else paragraph.push(line3);
1267
+ });
1268
+ flush();
1269
+ return out;
1270
+ }
1271
+ function cut(text, rest) {
1272
+ const room = BODY_LIMIT - TAIL - 2 - rest.length;
1273
+ const lines = text.split("\n");
1274
+ const code = fenced(lines);
1275
+ let length = -1;
1276
+ let at = 1;
1277
+ for (let k = 1; k < lines.length; k++) {
1278
+ length += 1 + (lines[k - 1] ?? "").length;
1279
+ if (length > room) break;
1280
+ if (code[k] !== true && (lines[k] ?? "").trim() === "") at = k;
1281
+ }
1282
+ return `${lines.slice(0, at).join("\n").trimEnd()}
1283
+
1284
+ ${rest}`;
1285
+ }
1286
+ function issueBody(s) {
1287
+ const lines = withoutTitle(
1288
+ stripFrontMatter(s.text.replace(/\r\n?/g, "\n")).split("\n")
1289
+ );
1290
+ const content = rewrite(lines, s).join("\n").replace(/^(?:[ \t]*\n)+/, "").trimEnd();
1291
+ const header2 = headerOf(s.path, s.org);
1292
+ const whole = content === "" ? header2 : `${header2}
1293
+
1294
+ ${content}`;
1295
+ const text = whole.length + TAIL <= BODY_LIMIT ? whole : cut(whole, `The rest: ${blobUrl(s.org, s.path)}`);
1296
+ return `${text}
1297
+
1298
+ ${comment(digest(text))}`;
1299
+ }
1300
+
1119
1301
  // src/pulse/layout.ts
1120
1302
  var statusOf2 = (status2) => status2 === null || status2 === "?" ? null : status2;
1121
1303
  var LIFECYCLE = [
@@ -1142,8 +1324,8 @@ var STATUS_COLORS = {
1142
1324
  Accepted: "green",
1143
1325
  Implemented: "green",
1144
1326
  Completed: "green",
1145
- Rejected: "gray",
1146
- Superseded: "gray",
1327
+ Rejected: "red",
1328
+ Superseded: "yellow",
1147
1329
  Abandoned: "gray"
1148
1330
  };
1149
1331
  var COLLECTION_COLORS = {
@@ -1208,9 +1390,7 @@ function desiredOf(tabs, org) {
1208
1390
  (tab) => tab.rows.map((row) => ({
1209
1391
  path: row.path,
1210
1392
  title: `${row.id} \u2014 ${row.title}`,
1211
- body: `${row.path}
1212
-
1213
- https://github.com/${org}/.rness/blob/main/${row.path}`,
1393
+ body: headerOf(row.path, org),
1214
1394
  status: statusOf2(row.status),
1215
1395
  type: tab.label,
1216
1396
  statusField: fields2.get(tab.label)?.name ?? null
@@ -1219,6 +1399,7 @@ https://github.com/${org}/.rness/blob/main/${row.path}`,
1219
1399
  }
1220
1400
 
1221
1401
  // src/core/github.ts
1402
+ import { setTimeout as delay } from "timers/promises";
1222
1403
  var DEFAULT_GITHUB_API = "https://api.github.com";
1223
1404
  var MAX_PAGES = 50;
1224
1405
  var PER_PAGE = 100;
@@ -1353,6 +1534,39 @@ async function listRepositories(owner, options) {
1353
1534
  truncated: true
1354
1535
  };
1355
1536
  }
1537
+ var WAIT_BUDGET_MS = 6e5;
1538
+ var DEFAULT_WAIT_MS = 6e4;
1539
+ var MIN_WAIT_MS = 1e3;
1540
+ var RATE_LIMIT_OUTLASTED = "GitHub's rate limit outlasted the 10 minutes rness waits";
1541
+ function rateWait(sleep = (ms) => delay(ms)) {
1542
+ return { left: WAIT_BUDGET_MS, now: () => Date.now(), sleep };
1543
+ }
1544
+ var PROCESS_WAIT = rateWait();
1545
+ var RateLimitError = class extends Error {
1546
+ };
1547
+ var messageOf = (body) => {
1548
+ const m = body !== null && typeof body === "object" ? body["message"] : void 0;
1549
+ return typeof m === "string" ? m : "";
1550
+ };
1551
+ function rateDelay(page, now, limited = false) {
1552
+ const h = page.headers;
1553
+ const spent = h.get("x-ratelimit-remaining") === "0";
1554
+ const hit = limited || page.status === 429 || page.status === 403 && (h.has("retry-after") || spent || /rate limit/i.test(messageOf(page.body)));
1555
+ if (!hit) return null;
1556
+ const after = (h.get("retry-after") ?? "").trim();
1557
+ if (/^\d+$/.test(after)) return Number(after) * 1e3;
1558
+ const reset = (h.get("x-ratelimit-reset") ?? "").trim();
1559
+ if (spent && /^\d+$/.test(reset))
1560
+ return Math.max(0, Number(reset) * 1e3 - now);
1561
+ return DEFAULT_WAIT_MS;
1562
+ }
1563
+ async function waitOut(options, ms) {
1564
+ const wait = options.wait ?? PROCESS_WAIT;
1565
+ const spent = Math.max(ms, MIN_WAIT_MS);
1566
+ if (spent > wait.left) throw new RateLimitError(RATE_LIMIT_OUTLASTED);
1567
+ wait.left -= spent;
1568
+ await wait.sleep(spent);
1569
+ }
1356
1570
  function apiHeaders(token) {
1357
1571
  return {
1358
1572
  Accept: "application/vnd.github+json",
@@ -1375,23 +1589,34 @@ async function getJson(path, options) {
1375
1589
  const body = await res.json().catch(() => void 0);
1376
1590
  return { status: res.status, body };
1377
1591
  }
1378
- async function postJson(path, payload, options, explain = false) {
1592
+ async function postJson(path, payload, options, explain = false, limited = () => false) {
1379
1593
  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);
1594
+ const clock = options.wait ?? PROCESS_WAIT;
1595
+ for (; ; ) {
1596
+ const page = await getPage(
1597
+ `${base}${path}`,
1598
+ { ...apiHeaders(options.token), "Content-Type": "application/json" },
1599
+ options.timeoutMs ?? 15e3,
1600
+ // A refusal's body says whether it is a rate limit. A timeout throws
1601
+ // above and is never retried: the write may have been made.
1602
+ { method: "POST", body: JSON.stringify(payload), errorBody: true }
1603
+ );
1604
+ const ok = page.status >= 200 && page.status < 300;
1605
+ const wait = rateDelay(page, clock.now(), ok && limited(page.body));
1606
+ if (wait !== null) {
1607
+ await waitOut(options, wait);
1608
+ continue;
1609
+ }
1610
+ if (!ok) {
1611
+ const message2 = explain && page.status !== 401 && page.status !== 429 && page.body !== null && typeof page.body === "object" ? page.body.message : void 0;
1612
+ if (typeof message2 === "string" && message2 !== "")
1613
+ throw new GitHubMessageError(message2, {
1614
+ cause: { status: page.status, path }
1615
+ });
1616
+ throw httpError(page, path);
1617
+ }
1618
+ return page.body;
1393
1619
  }
1394
- return page.body;
1395
1620
  }
1396
1621
  async function getScopes(options) {
1397
1622
  const base = (options.apiBase ?? DEFAULT_GITHUB_API).replace(/\/+$/, "");
@@ -1489,8 +1714,17 @@ async function createRepository(owner, name, options) {
1489
1714
  function isRecord2(v) {
1490
1715
  return v !== null && typeof v === "object";
1491
1716
  }
1717
+ var rateLimited = (body) => isRecord2(body) && Array.isArray(body["errors"]) && body["errors"].some(
1718
+ (e) => isRecord2(e) && e["type"] === "RATE_LIMITED"
1719
+ );
1492
1720
  async function graphqlRaw(query, variables, options) {
1493
- const body = await postJson("/graphql", { query, variables }, options, true);
1721
+ const body = await postJson(
1722
+ "/graphql",
1723
+ { query, variables },
1724
+ options,
1725
+ true,
1726
+ rateLimited
1727
+ );
1494
1728
  return isRecord2(body) ? body : {};
1495
1729
  }
1496
1730
  function firstError(answer) {
@@ -1505,6 +1739,15 @@ async function graphql(query, variables, options) {
1505
1739
  throw new Error("GitHub GraphQL answered without data");
1506
1740
  return answer.data;
1507
1741
  }
1742
+ async function graphqlFound(query, variables, options) {
1743
+ const answer = await graphqlRaw(query, variables, options);
1744
+ const errors = answer.errors ?? [];
1745
+ if (errors.length > 0 && errors.every((e) => e.type === "NOT_FOUND"))
1746
+ return null;
1747
+ const error = firstError(answer);
1748
+ if (error !== null) throw error;
1749
+ return answer.data ?? null;
1750
+ }
1508
1751
  async function orgId(org, o) {
1509
1752
  const d = await graphql(
1510
1753
  "query($login: String!) { organization(login: $login) { id } }",
@@ -1541,7 +1784,9 @@ async function findProject(org, number, o) {
1541
1784
  { login: org, number },
1542
1785
  o
1543
1786
  );
1544
- if (answer.errors?.every((e) => e.type === "NOT_FOUND") === true) return null;
1787
+ const errors = answer.errors ?? [];
1788
+ if (errors.length > 0 && errors.every((e) => e.type === "NOT_FOUND"))
1789
+ return null;
1545
1790
  const error = firstError(answer);
1546
1791
  if (error !== null) throw error;
1547
1792
  return answer.data?.organization?.projectV2 ?? null;
@@ -1621,7 +1866,7 @@ async function setOptions(field, options, o) {
1621
1866
  );
1622
1867
  return toField(d.updateProjectV2Field.projectV2Field);
1623
1868
  }
1624
- async function listItems(projectId, o) {
1869
+ async function listItems(projectId, o, { bodies = true } = {}) {
1625
1870
  const items = [];
1626
1871
  let cursor = null;
1627
1872
  for (; ; ) {
@@ -1639,8 +1884,23 @@ async function listItems(projectId, o) {
1639
1884
  id
1640
1885
  title
1641
1886
  }
1887
+ ... on Issue {
1888
+ id
1889
+ number
1890
+ title
1891
+ state
1892
+ ${bodies ? "body" : ""}
1893
+ repository {
1894
+ nameWithOwner
1895
+ }
1896
+ labels(first: 20) {
1897
+ nodes {
1898
+ name
1899
+ }
1900
+ }
1901
+ }
1642
1902
  }
1643
- fieldValues(first: 20) {
1903
+ fieldValues(first: 50) {
1644
1904
  nodes {
1645
1905
  ... on ProjectV2ItemFieldTextValue {
1646
1906
  text
@@ -1680,10 +1940,20 @@ async function listItems(projectId, o) {
1680
1940
  const value = v.text ?? v.name;
1681
1941
  if (field !== void 0 && value !== void 0) values2[field] = value;
1682
1942
  }
1943
+ const c = n.content;
1944
+ const issue = c?.id !== void 0 && c.number !== void 0 ? {
1945
+ id: c.id,
1946
+ number: c.number,
1947
+ open: c.state === "OPEN",
1948
+ repository: c.repository?.nameWithOwner ?? "",
1949
+ body: bodies ? c.body ?? "" : null,
1950
+ labels: (c.labels?.nodes ?? []).map((l) => l.name ?? "").filter((l) => l !== "")
1951
+ } : null;
1683
1952
  items.push({
1684
1953
  id: n.id,
1685
- draftId: n.content?.id ?? null,
1686
- title: n.content?.title ?? "",
1954
+ draftId: issue === null ? c?.id ?? null : null,
1955
+ issue,
1956
+ title: c?.title ?? "",
1687
1957
  archived: n.isArchived === true,
1688
1958
  values: values2
1689
1959
  });
@@ -1693,41 +1963,6 @@ async function listItems(projectId, o) {
1693
1963
  cursor = endCursor;
1694
1964
  }
1695
1965
  }
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
1966
  async function setValue(projectId, itemId, fieldId, value, o) {
1732
1967
  if (value === null) {
1733
1968
  await graphql(
@@ -1795,6 +2030,90 @@ async function archive(projectId, itemId, o) {
1795
2030
  o
1796
2031
  );
1797
2032
  }
2033
+ async function addItem(projectId, contentId, o) {
2034
+ const d = await graphql(
2035
+ `
2036
+ mutation ($projectId: ID!, $contentId: ID!) {
2037
+ addProjectV2ItemById(
2038
+ input: { projectId: $projectId, contentId: $contentId }
2039
+ ) {
2040
+ item {
2041
+ id
2042
+ }
2043
+ }
2044
+ }
2045
+ `,
2046
+ { projectId, contentId },
2047
+ o
2048
+ );
2049
+ return d.addProjectV2ItemById.item.id;
2050
+ }
2051
+ async function convertDraft(itemId, repositoryId, o) {
2052
+ const d = await graphql(
2053
+ `
2054
+ mutation ($itemId: ID!, $repositoryId: ID!) {
2055
+ convertProjectV2DraftIssueItemToIssue(
2056
+ input: { itemId: $itemId, repositoryId: $repositoryId }
2057
+ ) {
2058
+ item {
2059
+ id
2060
+ content {
2061
+ ... on Issue {
2062
+ id
2063
+ number
2064
+ }
2065
+ }
2066
+ }
2067
+ }
2068
+ }
2069
+ `,
2070
+ { itemId, repositoryId },
2071
+ o
2072
+ );
2073
+ const content = d.convertProjectV2DraftIssueItemToIssue.item.content;
2074
+ const id = content?.id;
2075
+ const number = content?.number;
2076
+ if (id === void 0 || number === void 0)
2077
+ throw new Error(`GitHub converted the item ${itemId} without an issue`);
2078
+ return { id, number };
2079
+ }
2080
+ async function linkedRepositories(projectId, o) {
2081
+ const d = await graphql(
2082
+ `
2083
+ query ($projectId: ID!) {
2084
+ node(id: $projectId) {
2085
+ ... on ProjectV2 {
2086
+ repositories(first: 100) {
2087
+ nodes {
2088
+ nameWithOwner
2089
+ }
2090
+ }
2091
+ }
2092
+ }
2093
+ }
2094
+ `,
2095
+ { projectId },
2096
+ o
2097
+ );
2098
+ return (d.node?.repositories.nodes ?? []).map((r) => r.nameWithOwner);
2099
+ }
2100
+ async function linkRepository(projectId, repositoryId, o) {
2101
+ await graphql(
2102
+ `
2103
+ mutation ($projectId: ID!, $repositoryId: ID!) {
2104
+ linkProjectV2ToRepository(
2105
+ input: { projectId: $projectId, repositoryId: $repositoryId }
2106
+ ) {
2107
+ repository {
2108
+ id
2109
+ }
2110
+ }
2111
+ }
2112
+ `,
2113
+ { projectId, repositoryId },
2114
+ o
2115
+ );
2116
+ }
1798
2117
  async function createView(org, number, view, o) {
1799
2118
  const body = {
1800
2119
  name: view.name,
@@ -1889,10 +2208,127 @@ async function renameView(viewId, name, visibleFieldIds, o) {
1889
2208
  );
1890
2209
  }
1891
2210
 
2211
+ // src/core/github-issues.ts
2212
+ async function repository(owner, name, label, o) {
2213
+ const d = await graphqlFound(
2214
+ `query($owner: String!, $name: String!, $label: String!) {
2215
+ repository(owner: $owner, name: $name) { id hasIssuesEnabled label(name: $label) { id } }
2216
+ }`,
2217
+ { owner, name, label },
2218
+ o
2219
+ );
2220
+ const r = d?.repository ?? null;
2221
+ return r === null ? null : { id: r.id, issues: r.hasIssuesEnabled, labelId: r.label?.id ?? null };
2222
+ }
2223
+ async function createLabel(owner, name, label, o) {
2224
+ const body = await postJson(
2225
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/labels`,
2226
+ label,
2227
+ o,
2228
+ true
2229
+ );
2230
+ const id = body !== null && typeof body === "object" ? body["node_id"] : void 0;
2231
+ if (typeof id !== "string")
2232
+ throw new Error(`GitHub created the label ${label.name} without a node id`);
2233
+ return id;
2234
+ }
2235
+ async function createIssue(repositoryId, issue, o) {
2236
+ const d = await graphql(
2237
+ `
2238
+ mutation (
2239
+ $repositoryId: ID!
2240
+ $title: String!
2241
+ $body: String
2242
+ $labelIds: [ID!]
2243
+ ) {
2244
+ createIssue(
2245
+ input: {
2246
+ repositoryId: $repositoryId
2247
+ title: $title
2248
+ body: $body
2249
+ labelIds: $labelIds
2250
+ }
2251
+ ) {
2252
+ issue {
2253
+ id
2254
+ number
2255
+ }
2256
+ }
2257
+ }
2258
+ `,
2259
+ { repositoryId, ...issue },
2260
+ o
2261
+ );
2262
+ return d.createIssue.issue;
2263
+ }
2264
+ async function updateIssue(id, change, o) {
2265
+ await graphql(
2266
+ `
2267
+ mutation ($id: ID!, $title: String, $body: String) {
2268
+ updateIssue(input: { id: $id, title: $title, body: $body }) {
2269
+ issue {
2270
+ id
2271
+ }
2272
+ }
2273
+ }
2274
+ `,
2275
+ { id, ...change },
2276
+ o
2277
+ );
2278
+ }
2279
+ async function closeIssue(id, o) {
2280
+ await graphql(
2281
+ `
2282
+ mutation ($id: ID!) {
2283
+ closeIssue(input: { issueId: $id, stateReason: NOT_PLANNED }) {
2284
+ issue {
2285
+ id
2286
+ }
2287
+ }
2288
+ }
2289
+ `,
2290
+ { id },
2291
+ o
2292
+ );
2293
+ }
2294
+ async function reopenIssue(id, o) {
2295
+ await graphql(
2296
+ `
2297
+ mutation ($id: ID!) {
2298
+ reopenIssue(input: { issueId: $id }) {
2299
+ issue {
2300
+ id
2301
+ }
2302
+ }
2303
+ }
2304
+ `,
2305
+ { id },
2306
+ o
2307
+ );
2308
+ }
2309
+ async function addLabels(id, labelIds, o) {
2310
+ await graphql(
2311
+ `
2312
+ mutation ($id: ID!, $labelIds: [ID!]!) {
2313
+ addLabelsToLabelable(input: { labelableId: $id, labelIds: $labelIds }) {
2314
+ clientMutationId
2315
+ }
2316
+ }
2317
+ `,
2318
+ { id, labelIds },
2319
+ o
2320
+ );
2321
+ }
2322
+
1892
2323
  // src/core/github-board.ts
1893
2324
  var PROJECT_TITLE = "Agent Pulse";
1894
2325
  var WORKING = "working";
1895
2326
  var WORKING_VIEW = "Working";
2327
+ var MEMORY = ".rness";
2328
+ var LABEL = "rness";
2329
+ var LABEL_COLOR = "5319e7";
2330
+ var LABEL_DESCRIPTION = "A document of .rness, on Agent Pulse";
2331
+ var labelled = (labels) => labels.some((l) => l.toLowerCase() === LABEL.toLowerCase());
1896
2332
  var STATUS_FIELD = "Status";
1897
2333
  var ALL_VIEW = "All";
1898
2334
  var DEFAULT_VIEW = "View 1";
@@ -1921,6 +2357,8 @@ var orderedFirst = (wanted, all) => {
1921
2357
  var GitHubBoards = class {
1922
2358
  #o;
1923
2359
  #cache = /* @__PURE__ */ new Map();
2360
+ /** Each organization's `.rness`, as first read: its id, its Issues, its label. */
2361
+ #memories = /* @__PURE__ */ new Map();
1924
2362
  constructor(options) {
1925
2363
  this.#o = options;
1926
2364
  }
@@ -1943,6 +2381,23 @@ var GitHubBoards = class {
1943
2381
  cache.fields ??= await fields(cache.projectId, this.#o);
1944
2382
  return cache.fields;
1945
2383
  }
2384
+ async #memory(org) {
2385
+ const known = this.#memories.get(org);
2386
+ if (known !== void 0) return known;
2387
+ const found = await repository(org, MEMORY, LABEL, this.#o);
2388
+ if (found === null)
2389
+ throw new Error(
2390
+ `the pulse needs ${org}/${MEMORY} on GitHub: it is not there, or this login cannot see it`
2391
+ );
2392
+ this.#memories.set(org, found);
2393
+ return found;
2394
+ }
2395
+ async checkIssues(org) {
2396
+ if (!(await this.#memory(org)).issues)
2397
+ throw new Error(
2398
+ `the pulse needs Issues on ${org}/${MEMORY}: turn them on in its Settings`
2399
+ );
2400
+ }
1946
2401
  /** The project only: the caller declares it before `ensureLayout` builds it. */
1947
2402
  async createBoard(org) {
1948
2403
  const project = await createProject(
@@ -2079,89 +2534,186 @@ var GitHubBoards = class {
2079
2534
  this.#o
2080
2535
  );
2081
2536
  }
2537
+ const memory = await this.#memory(board.org);
2538
+ if (memory.labelId === null) {
2539
+ memory.labelId = await createLabel(
2540
+ board.org,
2541
+ MEMORY,
2542
+ { name: LABEL, color: LABEL_COLOR, description: LABEL_DESCRIPTION },
2543
+ this.#o
2544
+ );
2545
+ added.push(`label ${LABEL}`);
2546
+ }
2547
+ const repository2 = `${board.org}/${MEMORY}`;
2548
+ const linked = await linkedRepositories(cache.projectId, this.#o);
2549
+ if (!linked.some((r) => r.toLowerCase() === repository2.toLowerCase())) {
2550
+ await linkRepository(cache.projectId, memory.id, this.#o);
2551
+ added.push(`link ${repository2}`);
2552
+ }
2082
2553
  cache.fields = [status2, collection, ...own.values(), agent, session, path];
2083
2554
  return added;
2084
2555
  }
2085
- async items(board) {
2556
+ async items(board, options = {}) {
2086
2557
  const cache = await this.#cacheOf(board);
2087
- const raw = await listItems(cache.projectId, this.#o);
2558
+ const raw = await listItems(cache.projectId, this.#o, options);
2088
2559
  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
- }));
2560
+ const memory = `${board.org}/${MEMORY}`.toLowerCase();
2561
+ return raw.filter((r) => !r.archived).map((r) => {
2562
+ const issue = r.issue !== null && r.issue.repository.toLowerCase() === memory ? r.issue : null;
2563
+ const ours = r.draftId !== null || issue !== null;
2564
+ return {
2565
+ id: r.id,
2566
+ path: ours ? r.values["Path"] ?? null : null,
2567
+ issue: issue === null ? null : {
2568
+ number: issue.number,
2569
+ open: issue.open,
2570
+ labelled: labelled(issue.labels),
2571
+ body: issue.body
2572
+ },
2573
+ title: r.title,
2574
+ status: r.values[STATUS_FIELD] ?? null,
2575
+ collectionStatus: r.values[statusFieldName(r.values[COLLECTION_FIELD] ?? "")] ?? null,
2576
+ type: r.values[COLLECTION_FIELD] ?? null,
2577
+ agent: r.values["Agent"] ?? null,
2578
+ session: r.values["Session"] ?? null
2579
+ };
2580
+ });
2101
2581
  }
2102
2582
  async #set(board, itemId, name, value) {
2103
2583
  const cache = await this.#cacheOf(board);
2104
2584
  const field = (await this.#fields(board)).find((f) => f.name === name);
2105
2585
  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;
2586
+ let sent = null;
2587
+ if (value !== null && field.options === null) sent = { text: value };
2588
+ else if (value !== null) {
2589
+ const option = field.options?.find((opt) => opt.name === value);
2590
+ if (option === void 0)
2591
+ throw new Error(`the field ${name} has no option ${value}`);
2592
+ sent = { optionId: option.id };
2109
2593
  }
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;
2119
- }
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
- );
2594
+ await setValue(cache.projectId, itemId, field.id, sent, this.#o);
2595
+ const known = cache.known.get(itemId);
2596
+ if (known === void 0) return;
2597
+ if (value === null) delete known.values[name];
2598
+ else known.values[name] = value;
2130
2599
  }
2131
2600
  async apply(board, step2) {
2132
- if (step2.kind === "unchanged") return;
2601
+ if (step2.kind === "unchanged") return null;
2133
2602
  const cache = await this.#cacheOf(board);
2134
- if (step2.kind === "archive") {
2603
+ if (step2.kind === "create") return this.#create(board, cache, step2.want);
2604
+ const known = await this.#known(board, step2.id);
2605
+ if (step2.kind === "archive" || step2.kind === "close") {
2606
+ if (step2.kind === "close" && known.issue?.open === true)
2607
+ await closeIssue(known.issue.id, this.#o);
2135
2608
  await archive(cache.projectId, step2.id, this.#o);
2136
2609
  cache.known.delete(step2.id);
2137
- return;
2610
+ return null;
2611
+ }
2612
+ if (step2.kind === "body") {
2613
+ const issue2 = this.#issueOf(known);
2614
+ await updateIssue(issue2.id, { body: step2.body }, this.#o);
2615
+ issue2.body = step2.body;
2616
+ return null;
2617
+ }
2618
+ const labelId = await this.#labelId(board);
2619
+ if (step2.kind === "convert") {
2620
+ const made = await convertDraft(
2621
+ step2.id,
2622
+ (await this.#memory(board.org)).id,
2623
+ this.#o
2624
+ );
2625
+ known.draftId = null;
2626
+ known.issue = {
2627
+ ...made,
2628
+ open: true,
2629
+ repository: `${board.org}/${MEMORY}`,
2630
+ body: "",
2631
+ labels: []
2632
+ };
2633
+ await this.#issue(known, step2.want, labelId);
2634
+ await this.#values(board, step2.id, step2.want);
2635
+ return { id: step2.id, number: made.number };
2636
+ }
2637
+ const issue = this.#issueOf(known);
2638
+ if (step2.reopen) {
2639
+ await reopenIssue(issue.id, this.#o);
2640
+ issue.open = true;
2641
+ }
2642
+ await this.#issue(known, step2.want, labelId);
2643
+ await this.#values(board, step2.id, step2.want);
2644
+ return null;
2645
+ }
2646
+ /** A new document's issue: labelled, the first line as body, added to the board, Path first. */
2647
+ async #create(board, cache, want) {
2648
+ const memory = await this.#memory(board.org);
2649
+ const labelId = await this.#labelId(board);
2650
+ const made = await createIssue(
2651
+ memory.id,
2652
+ { title: want.title, body: want.body, labelIds: [labelId] },
2653
+ this.#o
2654
+ );
2655
+ const id = await addItem(cache.projectId, made.id, this.#o);
2656
+ cache.known.set(id, {
2657
+ id,
2658
+ draftId: null,
2659
+ issue: {
2660
+ ...made,
2661
+ open: true,
2662
+ repository: `${board.org}/${MEMORY}`,
2663
+ body: want.body,
2664
+ labels: [LABEL]
2665
+ },
2666
+ title: want.title,
2667
+ archived: false,
2668
+ values: {}
2669
+ });
2670
+ await this.#values(board, id, want);
2671
+ return { id, number: made.number };
2672
+ }
2673
+ /** The label and the title an issue of the board carries. */
2674
+ async #issue(known, want, labelId) {
2675
+ const issue = this.#issueOf(known);
2676
+ if (!labelled(issue.labels)) {
2677
+ await addLabels(issue.id, [labelId], this.#o);
2678
+ issue.labels.push(LABEL);
2138
2679
  }
2139
- const { want } = step2;
2140
- const values2 = [
2680
+ if (known.title !== want.title) {
2681
+ await updateIssue(issue.id, { title: want.title }, this.#o);
2682
+ known.title = want.title;
2683
+ }
2684
+ }
2685
+ /** Path first — an issue on the board without it is the team's — then the fields, each only when it differs. */
2686
+ async #values(board, id, want) {
2687
+ const values2 = (await this.#known(board, id)).values;
2688
+ const wanted = [
2141
2689
  ["Path", want.path],
2142
2690
  [COLLECTION_FIELD, want.type],
2143
2691
  [STATUS_FIELD, want.status],
2144
2692
  ...want.statusField === null ? [] : [[want.statusField, want.status]]
2145
2693
  ];
2146
- if (step2.kind === "create") {
2147
- const id = await addDraft(
2148
- cache.projectId,
2149
- want.title,
2150
- want.body,
2151
- this.#o
2694
+ for (const [name, value] of wanted)
2695
+ if ((values2[name] ?? null) !== value)
2696
+ await this.#set(board, id, name, value);
2697
+ }
2698
+ async #known(board, id) {
2699
+ const cache = await this.#cacheOf(board);
2700
+ if (!cache.known.has(id)) await this.items(board);
2701
+ const known = cache.known.get(id);
2702
+ if (known === void 0) throw new Error(`the board has no item ${id}`);
2703
+ return known;
2704
+ }
2705
+ #issueOf(known) {
2706
+ if (known.issue === null)
2707
+ throw new Error(`the board's item ${known.id} is no issue`);
2708
+ return known.issue;
2709
+ }
2710
+ async #labelId(board) {
2711
+ const id = (await this.#memory(board.org)).labelId;
2712
+ if (id === null)
2713
+ throw new Error(
2714
+ `${board.org}/${MEMORY} has no label ${LABEL}: run rness pulse sync`
2152
2715
  );
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);
2716
+ return id;
2165
2717
  }
2166
2718
  async mark(board, itemIds, session) {
2167
2719
  for (const id of itemIds) {
@@ -2176,6 +2728,7 @@ var GITHUB_HTTPS = "https://github.com/";
2176
2728
  var GitHubOAuthProvider = class {
2177
2729
  #token;
2178
2730
  #apiBase;
2731
+ #wait;
2179
2732
  #login;
2180
2733
  #boardClient;
2181
2734
  get authenticated() {
@@ -2184,9 +2737,14 @@ var GitHubOAuthProvider = class {
2184
2737
  constructor(options) {
2185
2738
  this.#token = options.token;
2186
2739
  this.#apiBase = options.apiBase;
2740
+ this.#wait = options.wait;
2187
2741
  }
2188
2742
  #api(token) {
2189
- return this.#apiBase === void 0 ? { token } : { token, apiBase: this.#apiBase };
2743
+ return {
2744
+ token,
2745
+ ...this.#apiBase === void 0 ? {} : { apiBase: this.#apiBase },
2746
+ ...this.#wait === void 0 ? {} : { wait: this.#wait }
2747
+ };
2190
2748
  }
2191
2749
  identity() {
2192
2750
  this.#login ??= (async () => {
@@ -2240,6 +2798,9 @@ var GitHubOAuthProvider = class {
2240
2798
  return this.#boardClient;
2241
2799
  }
2242
2800
  // The board methods are async so that a missing login rejects, never throws.
2801
+ async checkIssues(org) {
2802
+ return this.#boards().checkIssues(org);
2803
+ }
2243
2804
  async createBoard(org) {
2244
2805
  return this.#boards().createBoard(org);
2245
2806
  }
@@ -2249,8 +2810,8 @@ var GitHubOAuthProvider = class {
2249
2810
  async ensureLayout(board, layout) {
2250
2811
  return this.#boards().ensureLayout(board, layout);
2251
2812
  }
2252
- async items(board) {
2253
- return this.#boards().items(board);
2813
+ async items(board, options) {
2814
+ return this.#boards().items(board, options);
2254
2815
  }
2255
2816
  async apply(board, step2) {
2256
2817
  return this.#boards().apply(board, step2);
@@ -2263,16 +2824,17 @@ var GitHubOAuthProvider = class {
2263
2824
  return { username: "x-access-token", password: this.#token.token };
2264
2825
  }
2265
2826
  };
2266
- async function githubProvider(apiBase) {
2827
+ async function githubProvider(apiBase, wait) {
2267
2828
  return new GitHubOAuthProvider({
2268
2829
  token: await resolveToken(),
2269
- ...apiBase === void 0 ? {} : { apiBase }
2830
+ ...apiBase === void 0 ? {} : { apiBase },
2831
+ ...wait === void 0 ? {} : { wait }
2270
2832
  });
2271
2833
  }
2272
2834
 
2273
2835
  // src/core/repos.ts
2274
2836
  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";
2837
+ import { join as join8, posix as posix5, sep as sep2 } from "path";
2276
2838
  async function addRepository(input) {
2277
2839
  const { name, url } = parseRepoSpec(input.spec, input.org, input.host);
2278
2840
  const dir = join8(input.root, "org", name);
@@ -2352,8 +2914,8 @@ async function workspaceDirs(repoDir) {
2352
2914
  const dirs = /* @__PURE__ */ new Set();
2353
2915
  for (const pattern of patterns) {
2354
2916
  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")))
2917
+ const rel = match.split(sep2).join(posix5.sep);
2918
+ if (await exists(join8(repoDir, ...rel.split(posix5.sep), "package.json")))
2357
2919
  dirs.add(rel);
2358
2920
  }
2359
2921
  }
@@ -2650,7 +3212,7 @@ function reportError(e) {
2650
3212
  }
2651
3213
 
2652
3214
  // src/core/block.ts
2653
- import { createHash } from "crypto";
3215
+ import { createHash as createHash2 } from "crypto";
2654
3216
  var BEGIN = "<!-- BEGIN rness -->";
2655
3217
  var END = "<!-- END rness -->";
2656
3218
  var BLOCK_SIZE_WARNING = 32 * 1024;
@@ -2659,7 +3221,7 @@ function toLf(s) {
2659
3221
  return s.replace(/\r\n?/g, "\n");
2660
3222
  }
2661
3223
  function blockHash(body) {
2662
- return createHash("sha256").update(toLf(body), "utf8").digest("hex").slice(0, 12);
3224
+ return createHash2("sha256").update(toLf(body), "utf8").digest("hex").slice(0, 12);
2663
3225
  }
2664
3226
  function intro(scope, org, depth) {
2665
3227
  if (scope === null) {
@@ -3059,8 +3621,8 @@ function wholeDiff(existing, content) {
3059
3621
  async function filesOf(root, manifest, targets) {
3060
3622
  const files = [];
3061
3623
  if (!await exists(join11(root, "org"))) return files;
3062
- for (const target of targets)
3063
- for (const owned of target.files)
3624
+ for (const target2 of targets)
3625
+ for (const owned of target2.files)
3064
3626
  if (owned.at === "root")
3065
3627
  files.push({
3066
3628
  ...owned,
@@ -3069,8 +3631,8 @@ async function filesOf(root, manifest, targets) {
3069
3631
  });
3070
3632
  for (const repo of Object.keys(manifest.repos)) {
3071
3633
  if (!await exists(join11(root, "org", repo))) continue;
3072
- for (const target of targets)
3073
- for (const owned of target.files)
3634
+ for (const target2 of targets)
3635
+ for (const owned of target2.files)
3074
3636
  if (owned.at === "clones")
3075
3637
  files.push({
3076
3638
  ...owned,
@@ -3183,7 +3745,7 @@ var PROVIDERS = {
3183
3745
  name: "github",
3184
3746
  label: "GitHub",
3185
3747
  available: true,
3186
- open: ({ apiBase }) => githubProvider(apiBase)
3748
+ open: ({ apiBase, wait }) => githubProvider(apiBase, wait)
3187
3749
  },
3188
3750
  gitlab: { name: "gitlab", label: "GitLab", available: false },
3189
3751
  atlassian: {
@@ -3885,7 +4447,7 @@ async function addCommand(spec, opts, deps = {}) {
3885
4447
  }
3886
4448
 
3887
4449
  // src/commands/context.ts
3888
- import { posix as posix5, relative, sep as sep3 } from "path";
4450
+ import { posix as posix6, relative, sep as sep3 } from "path";
3889
4451
  function renderMarkdown(ctx) {
3890
4452
  const parts = [];
3891
4453
  for (const collection of ctx.collections) {
@@ -3917,7 +4479,7 @@ async function contextCommand(opts) {
3917
4479
  }
3918
4480
  scope = opts.scope;
3919
4481
  } else {
3920
- const rel = relative(ws.root, cwd).split(sep3).join(posix5.sep);
4482
+ const rel = relative(ws.root, cwd).split(sep3).join(posix6.sep);
3921
4483
  scope = resolveScope(manifest, rel);
3922
4484
  }
3923
4485
  const ctx = await assembleContext({
@@ -5782,9 +6344,9 @@ async function clonesToCommit(root, cwd, manifest) {
5782
6344
  }
5783
6345
  return clones;
5784
6346
  }
5785
- function finish(ui, cwd, rnessDir, label, target, ending) {
6347
+ function finish(ui, cwd, rnessDir, label, target2, ending) {
5786
6348
  const rel = relative5(cwd, rnessDir) || ".";
5787
- const message2 = `chore: rness ${target}`;
6349
+ const message2 = `chore: rness ${target2}`;
5788
6350
  const next = [];
5789
6351
  if (ending.commit === "committed") next.push(`git -C ${rel} push`);
5790
6352
  if (ending.commit === "refused")
@@ -5797,12 +6359,12 @@ function finish(ui, cwd, rnessDir, label, target, ending) {
5797
6359
  next.push("# teammates: git pull \u2014 the next rness command installs it");
5798
6360
  if (ui.session) {
5799
6361
  if (next.length > 0) ui.note("Next steps", next);
5800
- ui.outro(`${label} runs @rness/cli ${target}`);
6362
+ ui.outro(`${label} runs @rness/cli ${target2}`);
5801
6363
  } else
5802
6364
  process.stdout.write(
5803
6365
  [
5804
6366
  "",
5805
- status("upgraded", `${label} to @rness/cli ${target}`),
6367
+ status("upgraded", `${label} to @rness/cli ${target2}`),
5806
6368
  "",
5807
6369
  ...next.length > 0 ? ["Next:", ...next.map((line3) => ` ${line3}`), ""] : []
5808
6370
  ].join("\n")
@@ -5838,12 +6400,12 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
5838
6400
  }
5839
6401
  const ui = interactive ? await makeUi(terminal) : { ...plainUi };
5840
6402
  ui.intro(`Upgrade workspace ${basename5(ws.root)}`);
5841
- const target = version ?? await ui.step(
6403
+ const target2 = version ?? await ui.step(
5842
6404
  { doing: "asking the registry for the latest release", quiet: true },
5843
6405
  () => latestVersion(),
5844
6406
  (v) => v === null ? null : ["latest", `@rness/cli ${v}`]
5845
6407
  );
5846
- if (target === null) {
6408
+ if (target2 === null) {
5847
6409
  process.stderr.write(
5848
6410
  `cannot reach the registry; name the version: ${rnessCommand()} upgrade <version>
5849
6411
  `
@@ -5871,44 +6433,44 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
5871
6433
  return 1;
5872
6434
  }
5873
6435
  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}
6436
+ const scaffoldCurrent = base !== null && base.version === target2;
6437
+ if (pin.spec === target2 && installed === target2 && scaffoldCurrent) {
6438
+ if (ui.session) ui.outro(`Already at ${target2}`);
6439
+ else process.stdout.write(`already at ${target2}
5878
6440
  `);
5879
6441
  return 0;
5880
6442
  }
5881
- const downgrading = EXACT_VERSION.test(pin.spec) && compareVersions(target, pin.spec) < 0;
6443
+ const downgrading = EXACT_VERSION.test(pin.spec) && compareVersions(target2, pin.spec) < 0;
5882
6444
  if (downgrading && version === void 0) {
5883
6445
  process.stderr.write(
5884
- `${label} pins @rness/cli ${pin.spec}, newer than the latest release ${target}; to downgrade: ${rnessCommand()} upgrade ${target}
6446
+ `${label} pins @rness/cli ${pin.spec}, newer than the latest release ${target2}; to downgrade: ${rnessCommand()} upgrade ${target2}
5885
6447
  `
5886
6448
  );
5887
6449
  return 1;
5888
6450
  }
5889
6451
  const pm = await workspacePackageManager(ws.rnessDir);
5890
- if (pin.spec === target && installed === target)
6452
+ if (pin.spec === target2 && installed === target2)
5891
6453
  ui.line(
5892
6454
  "scaffold",
5893
- `@rness/cli ${target} \u2014 merging it into ${label}`,
5894
- `Merging the @rness/cli ${target} scaffold into ${label}`
6455
+ `@rness/cli ${target2} \u2014 merging it into ${label}`,
6456
+ `Merging the @rness/cli ${target2} scaffold into ${label}`
5895
6457
  );
5896
- else if (pin.spec === target)
6458
+ else if (pin.spec === target2)
5897
6459
  ui.line(
5898
6460
  "install",
5899
- `@rness/cli ${target} in ${label} (${pm}) \u2014 ${installed ?? "nothing"} is installed`
6461
+ `@rness/cli ${target2} in ${label} (${pm}) \u2014 ${installed ?? "nothing"} is installed`
5900
6462
  );
5901
6463
  else {
5902
6464
  ui.line(
5903
6465
  "upgrade",
5904
- `@rness/cli ${pin.spec} \u2192 ${target} in ${label} (${pm})${downgrading ? " \u2014 downgrading" : ""}`
6466
+ `@rness/cli ${pin.spec} \u2192 ${target2} in ${label} (${pm})${downgrading ? " \u2014 downgrading" : ""}`
5905
6467
  );
5906
6468
  ui.line("notes", NOTES, `Release notes: ${NOTES}`);
5907
6469
  }
5908
6470
  if (interactive) {
5909
6471
  const p = await terminal.prompts();
5910
6472
  const ok = await p.confirm({
5911
- message: `Upgrade @rness/cli to ${target}?`
6473
+ message: `Upgrade @rness/cli to ${target2}?`
5912
6474
  });
5913
6475
  if (p.isCancel(ok) || ok !== true) {
5914
6476
  ui.cancelled();
@@ -5918,31 +6480,31 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
5918
6480
  let merged = false;
5919
6481
  if (!scaffoldCurrent) {
5920
6482
  const source = await (deps.scaffoldFor ?? scaffoldFor)(
5921
- target,
6483
+ target2,
5922
6484
  ws.rnessDir
5923
6485
  );
5924
6486
  try {
5925
6487
  const files = await renderScaffold(source.dir, {
5926
- version: target,
6488
+ version: target2,
5927
6489
  packageManager: await declaredManager(ws.rnessDir, pm)
5928
6490
  });
5929
6491
  if (base === null)
5930
6492
  ui.line(
5931
6493
  "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`
6494
+ `the @rness/cli ${target2} scaffold (no scaffold commit in ${label} yet)`,
6495
+ `Adopting the @rness/cli ${target2} scaffold \u2014 ${label} has no scaffold commit yet`
5934
6496
  );
5935
6497
  else
5936
6498
  ui.line(
5937
6499
  "merging",
5938
- `the @rness/cli ${target} scaffold`,
5939
- `Merging the @rness/cli ${target} scaffold`
6500
+ `the @rness/cli ${target2} scaffold`,
6501
+ `Merging the @rness/cli ${target2} scaffold`
5940
6502
  );
5941
6503
  const commit = await buildScaffoldCommit(
5942
6504
  ws.rnessDir,
5943
6505
  base?.commit ?? null,
5944
6506
  base === null ? await withoutRedundantKeeps(ws.rnessDir, files) : files,
5945
- target
6507
+ target2
5946
6508
  );
5947
6509
  const result = await mergeScaffold(
5948
6510
  ws.rnessDir,
@@ -5969,9 +6531,9 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
5969
6531
  [packageFile, await readFile9(packageFile, "utf8")]
5970
6532
  ]);
5971
6533
  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);
6534
+ if (moved !== null && moved.spec !== target2)
6535
+ await writePin(ws.rnessDir, moved.spec, target2);
6536
+ const mustInstall = !(pin.spec === target2 && installed === target2);
5975
6537
  if (mustInstall) {
5976
6538
  try {
5977
6539
  await ui.step(
@@ -6002,9 +6564,9 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
6002
6564
  return 1;
6003
6565
  }
6004
6566
  const now = (await readPinnedCli(ws.rnessDir))?.version ?? null;
6005
- if (now !== target) {
6567
+ if (now !== target2) {
6006
6568
  process.stderr.write(
6007
- `${pm} install left @rness/cli ${now ?? "not installed"} in ${label}, not ${target}
6569
+ `${pm} install left @rness/cli ${now ?? "not installed"} in ${label}, not ${target2}
6008
6570
  `
6009
6571
  );
6010
6572
  return 1;
@@ -6013,7 +6575,7 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
6013
6575
  ui.gitIsSilent = true;
6014
6576
  const code = await syncBlocks(ui, ws.root, basename5(ws.root));
6015
6577
  if (code !== 0) return code;
6016
- const message2 = `chore: rness ${target}`;
6578
+ const message2 = `chore: rness ${target2}`;
6017
6579
  const committed = await commitUpgrade(ws.rnessDir, message2);
6018
6580
  if (committed.kind === "committed")
6019
6581
  ui.line(
@@ -6028,7 +6590,7 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
6028
6590
  for (const line3 of committed.output.split("\n"))
6029
6591
  if (line3.trim() !== "") ui.hint(line3, "stderr");
6030
6592
  }
6031
- return finish(ui, cwd, ws.rnessDir, label, target, {
6593
+ return finish(ui, cwd, ws.rnessDir, label, target2, {
6032
6594
  commit: committed.kind,
6033
6595
  clones: await clonesToCommit(
6034
6596
  ws.root,
@@ -6153,11 +6715,11 @@ password=${resolved.token}
6153
6715
 
6154
6716
  // src/commands/hook.ts
6155
6717
  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";
6718
+ import { isAbsolute as isAbsolute2, posix as posix8, relative as relative7, resolve as resolve7, sep as sep5 } from "path";
6157
6719
 
6158
6720
  // src/mcp/tools.ts
6159
6721
  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";
6722
+ import { isAbsolute, join as join24, posix as posix7, relative as relative6, resolve as resolve6, sep as sep4 } from "path";
6161
6723
  var LABELS = {
6162
6724
  standards: "Standards",
6163
6725
  adr: "Decisions",
@@ -6197,7 +6759,7 @@ async function documents(rnessDir, only) {
6197
6759
  scope: ownerScope(item.rel),
6198
6760
  id,
6199
6761
  status: typeof status2 === "string" ? status2 : null,
6200
- title: documentTitle(item.title ?? posix6.basename(item.rel, ".md"), id),
6762
+ title: documentTitle(item.title ?? posix7.basename(item.rel, ".md"), id),
6201
6763
  text: item.body
6202
6764
  });
6203
6765
  }
@@ -6227,7 +6789,7 @@ async function context(cwd, args) {
6227
6789
  } else
6228
6790
  scope = resolveScope(
6229
6791
  manifest,
6230
- relative6(ws.root, cwd).split(sep4).join(posix6.sep)
6792
+ relative6(ws.root, cwd).split(sep4).join(posix7.sep)
6231
6793
  );
6232
6794
  const summary = await scopeSummary(ws, manifest, scope);
6233
6795
  return {
@@ -6416,8 +6978,9 @@ function mcpTools(cwd) {
6416
6978
 
6417
6979
  // src/pulse/detached.ts
6418
6980
  import { spawn as nodeSpawn } from "child_process";
6419
- import { mkdir as mkdir7, readFile as readFile11, rm as rm8, writeFile as writeFile4 } from "fs/promises";
6981
+ import { mkdir as mkdir7, open as open2, readFile as readFile11, rm as rm8, stat as stat5, writeFile as writeFile4 } from "fs/promises";
6420
6982
  import { join as join25 } from "path";
6983
+ import { setTimeout as delay2 } from "timers/promises";
6421
6984
  var detached = (args, cwd) => {
6422
6985
  const bin = process.argv[1];
6423
6986
  if (bin === void 0) return;
@@ -6452,6 +7015,33 @@ async function takeFailure() {
6452
7015
  return null;
6453
7016
  }
6454
7017
  }
7018
+ var PULSE_LOCK_STALE_MS = 15 * 6e4;
7019
+ async function oneSyncAtATime(key, work, options = {}) {
7020
+ await mkdir7(configDir(), { recursive: true, mode: 448 });
7021
+ const file = join25(configDir(), `pulse-${key}.lock`);
7022
+ for (; ; ) {
7023
+ try {
7024
+ const handle = await open2(file, "wx");
7025
+ await handle.close();
7026
+ break;
7027
+ } catch (e) {
7028
+ if (e.code !== "EEXIST") throw e;
7029
+ const age = await stat5(file).then(
7030
+ (s) => Date.now() - s.mtimeMs,
7031
+ () => 0
7032
+ );
7033
+ if (age > PULSE_LOCK_STALE_MS) await rm8(file, { force: true });
7034
+ else if (options.wait !== true) return false;
7035
+ else await (options.sleep ?? ((ms) => delay2(ms)))(1e3);
7036
+ }
7037
+ }
7038
+ try {
7039
+ await work();
7040
+ return true;
7041
+ } finally {
7042
+ await rm8(file, { force: true });
7043
+ }
7044
+ }
6455
7045
 
6456
7046
  // src/commands/hook.ts
6457
7047
  var NOUNS = {
@@ -6479,7 +7069,7 @@ function cwdOf(input, env) {
6479
7069
  if (typeof cwd === "string" && cwd !== "") return cwd;
6480
7070
  return env["CLAUDE_PROJECT_DIR"] ?? process.cwd();
6481
7071
  }
6482
- var toPosix2 = (p) => p.split(sep5).join(posix7.sep);
7072
+ var toPosix2 = (p) => p.split(sep5).join(posix8.sep);
6483
7073
  function count(n, [one, many]) {
6484
7074
  return `${n} ${n === 1 ? one : many}`;
6485
7075
  }
@@ -6823,29 +7413,162 @@ async function mcpCommand(opts, io = {
6823
7413
  return 0;
6824
7414
  }
6825
7415
 
7416
+ // src/commands/pulse.ts
7417
+ import { readFile as readFile13, readdir as readdir4 } from "fs/promises";
7418
+ import { join as join26 } from "path";
7419
+ import { setTimeout as delay3 } from "timers/promises";
7420
+
7421
+ // src/pulse/opened.ts
7422
+ import { mkdir as mkdir8, readFile as readFile12, rename as rename3, rm as rm9, writeFile as writeFile5 } from "fs/promises";
7423
+ import { dirname as dirname9 } from "path";
7424
+ var NAME2 = "rness/opened";
7425
+ function openedFile(rnessDir) {
7426
+ return gitPath(rnessDir, NAME2);
7427
+ }
7428
+ var NUMBER = /^[1-9][0-9]*$/;
7429
+ async function readOpened(file) {
7430
+ const opened = /* @__PURE__ */ new Map();
7431
+ if (file === null) return opened;
7432
+ let data;
7433
+ try {
7434
+ data = JSON.parse(await readFile12(file, "utf8"));
7435
+ } catch {
7436
+ return opened;
7437
+ }
7438
+ if (data === null || typeof data !== "object" || Array.isArray(data))
7439
+ return opened;
7440
+ for (const [key, path] of Object.entries(data))
7441
+ if (NUMBER.test(key) && typeof path === "string" && path !== "")
7442
+ opened.set(Number(key), path);
7443
+ return opened;
7444
+ }
7445
+ async function writeOpened(file, opened) {
7446
+ if (file === null) return;
7447
+ try {
7448
+ if (opened.size === 0) {
7449
+ await rm9(file, { force: true });
7450
+ return;
7451
+ }
7452
+ const entries = [...opened].sort(([a], [b]) => a - b);
7453
+ await mkdir8(dirname9(file), { recursive: true });
7454
+ const temporary = `${file}.${process.pid}`;
7455
+ await writeFile5(
7456
+ temporary,
7457
+ `${JSON.stringify(Object.fromEntries(entries), null, 2)}
7458
+ `
7459
+ );
7460
+ await rename3(temporary, file);
7461
+ } catch {
7462
+ }
7463
+ }
7464
+
6826
7465
  // src/pulse/plan.ts
6827
- function planSync(want, have) {
7466
+ var differs = (item, w) => item.title !== w.title || item.status !== w.status || w.statusField !== null && item.collectionStatus !== w.status || item.type !== w.type;
7467
+ var rank = (i) => i.issue === null ? 2 : i.issue.open ? 0 : 1;
7468
+ var keptFirst = (a, b) => rank(a) - rank(b) || (a.issue?.number ?? 0) - (b.issue?.number ?? 0);
7469
+ var gone = (item) => item.issue === null ? { kind: "archive", id: item.id } : { kind: "close", id: item.id };
7470
+ var firstLine5 = (body) => body.split(/\r\n?|\n/, 1)[0] ?? "";
7471
+ function strays(have) {
7472
+ const byLine = /* @__PURE__ */ new Map();
7473
+ for (const item of have) {
7474
+ if (item.path !== null || item.issue === null || item.issue.body === null || !item.issue.labelled)
7475
+ continue;
7476
+ const line3 = firstLine5(item.issue.body);
7477
+ byLine.set(line3, [...byLine.get(line3) ?? [], item]);
7478
+ }
7479
+ for (const items of byLine.values()) items.sort(keptFirst);
7480
+ return byLine;
7481
+ }
7482
+ function unwantedPaths(want, have) {
7483
+ const wanted = new Set(want.map((w) => w.path));
7484
+ const paths = /* @__PURE__ */ new Set();
7485
+ for (const item of have)
7486
+ if (item.path !== null && !wanted.has(item.path)) paths.add(item.path);
7487
+ return [...paths];
7488
+ }
7489
+ function isGone(path, item, seen, opened) {
7490
+ return seen.has(path) || item.issue !== null && opened.get(item.issue.number) === path;
7491
+ }
7492
+ function planSync(want, have, seen = /* @__PURE__ */ new Set(), opened = /* @__PURE__ */ new Map()) {
6828
7493
  const byPath = /* @__PURE__ */ new Map();
6829
- const steps = [];
7494
+ for (const item of have)
7495
+ if (item.path !== null)
7496
+ byPath.set(item.path, [...byPath.get(item.path) ?? [], item]);
7497
+ const kept = /* @__PURE__ */ new Map();
6830
7498
  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);
7499
+ for (const [path, items] of byPath) {
7500
+ const [keep, ...rest] = [...items].sort(keptFirst);
7501
+ if (keep !== void 0) kept.set(path, keep);
7502
+ surplus.push(...rest.map(gone));
6835
7503
  }
7504
+ const left = strays(have);
7505
+ const steps = [];
6836
7506
  const wanted = /* @__PURE__ */ new Set();
6837
7507
  for (const w of want) {
6838
7508
  wanted.add(w.path);
6839
- const item = byPath.get(w.path);
7509
+ const item = kept.get(w.path) ?? left.get(w.body)?.[0];
6840
7510
  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 });
7511
+ else if (item.issue === null)
7512
+ steps.push({ kind: "convert", id: item.id, want: w });
7513
+ else if (item.path === null || !item.issue.open || !item.issue.labelled || differs(item, w))
7514
+ steps.push({
7515
+ kind: "update",
7516
+ id: item.id,
7517
+ want: w,
7518
+ reopen: !item.issue.open
7519
+ });
6843
7520
  else steps.push({ kind: "unchanged", id: item.id });
6844
7521
  }
6845
- for (const [path, item] of byPath)
6846
- if (!wanted.has(path)) steps.push({ kind: "archive", id: item.id });
7522
+ for (const [path, item] of kept)
7523
+ if (!wanted.has(path))
7524
+ steps.push(
7525
+ isGone(path, item, seen, opened) ? gone(item) : { kind: "unchanged", id: item.id }
7526
+ );
6847
7527
  return [...steps, ...surplus];
6848
7528
  }
7529
+ function stillOpened(opened, have, seen, closed, created) {
7530
+ const items = /* @__PURE__ */ new Map();
7531
+ for (const item of have)
7532
+ if (item.issue !== null) items.set(item.issue.number, item.id);
7533
+ const next = /* @__PURE__ */ new Map();
7534
+ for (const [number, path] of opened) {
7535
+ const id = items.get(number);
7536
+ if (id !== void 0 && !closed.has(id) && !seen.has(path))
7537
+ next.set(number, path);
7538
+ }
7539
+ for (const [path, placed] of created) next.set(placed.number, path);
7540
+ return next;
7541
+ }
7542
+ function issuedAfter(steps, have, placed) {
7543
+ const byId = new Map(have.map((i) => [i.id, i]));
7544
+ const issued = /* @__PURE__ */ new Map();
7545
+ for (const step2 of steps) {
7546
+ if (step2.kind === "create" || step2.kind === "convert") {
7547
+ const p = placed.get(step2.want.path);
7548
+ if (p !== void 0) issued.set(step2.want.path, { ...p, body: null });
7549
+ } else if (step2.kind === "update" || step2.kind === "unchanged") {
7550
+ const item = byId.get(step2.id);
7551
+ const path = step2.kind === "update" ? step2.want.path : item?.path;
7552
+ if (item === void 0 || path == null || item.issue === null) continue;
7553
+ issued.set(path, {
7554
+ id: item.id,
7555
+ number: item.issue.number,
7556
+ body: item.issue.body
7557
+ });
7558
+ }
7559
+ }
7560
+ return issued;
7561
+ }
7562
+ function planBodies(bodies, issued) {
7563
+ const steps = [];
7564
+ for (const [path, body] of bodies) {
7565
+ const i = issued.get(path);
7566
+ if (i === void 0) continue;
7567
+ if (i.body === null || digestOf(i.body) !== digestOf(body))
7568
+ steps.push({ kind: "body", id: i.id, body });
7569
+ }
7570
+ return steps;
7571
+ }
6849
7572
 
6850
7573
  // src/commands/pulse.ts
6851
7574
  var PROJECT_SCOPE = "project";
@@ -6861,15 +7584,23 @@ async function fromGithub(work) {
6861
7584
  throw e;
6862
7585
  }
6863
7586
  }
6864
- async function context2(opts) {
7587
+ var apiOf = (opts) => opts.githubApi === void 0 ? {} : { apiBase: opts.githubApi };
7588
+ var providerOptions = (opts, wait) => ({
7589
+ ...apiOf(opts),
7590
+ ...wait === void 0 ? {} : { wait }
7591
+ });
7592
+ async function context2(opts, wait) {
6865
7593
  const ws = await findWorkspace(opts.cwd ?? process.cwd());
6866
7594
  const manifest = await loadManifest(ws.rnessDir);
6867
- const provider = await openProvider(manifest, apiOf(opts));
7595
+ const provider = await openProvider(manifest, providerOptions(opts, wait));
6868
7596
  if (manifest.org === null)
6869
7597
  throw new Error('the pulse needs an organization: rness.json has no "org"');
6870
7598
  return { rnessDir: ws.rnessDir, manifest, org: manifest.org, provider };
6871
7599
  }
6872
- var apiOf = (opts) => opts.githubApi === void 0 ? {} : { apiBase: opts.githubApi };
7600
+ var waitSaid = (opts, ui) => rateWait(async (ms) => {
7601
+ ui.line("waiting", `${Math.round(ms / 1e3)} s \u2014 GitHub's rate limit`);
7602
+ await (opts.sleep ?? ((n) => delay3(n)))(ms);
7603
+ });
6873
7604
  async function missingAccess(provider) {
6874
7605
  if (!provider.authenticated) return NEEDS_LOGIN;
6875
7606
  const scopes = await fromGithub(provider.scopes());
@@ -6895,33 +7626,131 @@ var createdByKind = (ui, added) => {
6895
7626
  const names = added.filter((a) => a.startsWith(`${kind} `)).map((a) => a.slice(kind.length + 1));
6896
7627
  if (names.length > 0) ui.line("created", `${kind}s ${names.join(", ")}`);
6897
7628
  }
7629
+ for (const a of added) {
7630
+ if (a.startsWith("label ")) ui.line("created", a);
7631
+ else if (a.startsWith("link "))
7632
+ ui.line("linked", `Agent Pulse to ${a.slice("link ".length)}`);
7633
+ }
6898
7634
  };
7635
+ async function filesOf2(rnessDir) {
7636
+ const files = /* @__PURE__ */ new Set();
7637
+ const walk = async (rel) => {
7638
+ const dir = rel === "" ? rnessDir : join26(rnessDir, ...rel.split("/"));
7639
+ for (const e of await readdir4(dir, { withFileTypes: true })) {
7640
+ const path = rel === "" ? e.name : `${rel}/${e.name}`;
7641
+ if (e.isDirectory()) {
7642
+ if (e.name !== ".git" && e.name !== "node_modules") await walk(path);
7643
+ } else if (e.isFile()) files.add(path);
7644
+ }
7645
+ };
7646
+ await walk("");
7647
+ return files;
7648
+ }
7649
+ var count2 = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
7650
+ function sayDone(ui, org, documents2, steps, writes) {
7651
+ const n = (kind) => steps.filter((s) => s.kind === kind).length;
7652
+ const reopened = steps.filter((s) => s.kind === "update" && s.reopen).length;
7653
+ if (n("create") > 0) ui.line("created", count2(n("create"), "issue"));
7654
+ if (n("convert") > 0)
7655
+ ui.line(
7656
+ "converted",
7657
+ `${count2(n("convert"), "draft")} into issues of ${org}/.rness`
7658
+ );
7659
+ if (reopened > 0) ui.line("reopened", count2(reopened, "issue"));
7660
+ const written = new Set(writes.map((w) => w.id));
7661
+ const bodyOnly = steps.filter(
7662
+ (s) => s.kind === "unchanged" && written.has(s.id)
7663
+ ).length;
7664
+ const parts = [
7665
+ ["created", n("create")],
7666
+ ["converted", n("convert")],
7667
+ ["updated", n("update") + bodyOnly],
7668
+ [
7669
+ "unchanged",
7670
+ documents2 - n("create") - n("convert") - n("update") - bodyOnly
7671
+ ],
7672
+ ["archived", n("close") + n("archive")]
7673
+ ].filter(([, k]) => k > 0).map(([word, k]) => `${k} ${word}`);
7674
+ ui.line(
7675
+ "synced",
7676
+ `${plural(documents2)}${parts.length > 0 ? `: ${parts.join(", ")}` : ""}`
7677
+ );
7678
+ }
6899
7679
  async function syncBoard(c, board, ui, sayLayout) {
7680
+ await fromGithub(c.provider.checkIssues(c.org));
6900
7681
  const tabs = await statusTabs(c.rnessDir);
6901
7682
  const want = desiredOf(tabs, c.org);
6902
7683
  sayLayout(
6903
7684
  ui,
6904
7685
  await fromGithub(c.provider.ensureLayout(board, layoutOf(tabs)))
6905
7686
  );
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
- );
7687
+ const have = await fromGithub(c.provider.items(board));
7688
+ const record = await openedFile(c.rnessDir);
7689
+ const opened = await readOpened(record);
7690
+ const seen = await seenPaths(c.rnessDir, [
7691
+ ...unwantedPaths(want, have),
7692
+ ...opened.values()
7693
+ ]);
7694
+ const steps = planSync(want, have, seen, opened);
7695
+ const changes = steps.filter((s) => s.kind !== "unchanged").length;
7696
+ let writes = [];
7697
+ let made = 0;
7698
+ try {
7699
+ const placed = /* @__PURE__ */ new Map();
7700
+ const created = /* @__PURE__ */ new Map();
7701
+ const closed = /* @__PURE__ */ new Set();
7702
+ try {
7703
+ for (const step2 of steps) {
7704
+ const p = await fromGithub(c.provider.apply(board, step2));
7705
+ if (p !== null && (step2.kind === "create" || step2.kind === "convert"))
7706
+ placed.set(step2.want.path, p);
7707
+ if (p !== null && step2.kind === "create") created.set(step2.want.path, p);
7708
+ if (step2.kind === "close" || step2.kind === "archive")
7709
+ closed.add(step2.id);
7710
+ if (step2.kind !== "unchanged") made++;
7711
+ }
7712
+ } finally {
7713
+ await writeOpened(
7714
+ record,
7715
+ stillOpened(opened, have, seen, closed, created)
7716
+ );
7717
+ }
7718
+ const issued = issuedAfter(steps, have, placed);
7719
+ const numbers = new Map([...issued].map(([path, i]) => [path, i.number]));
7720
+ const files = await filesOf2(c.rnessDir);
7721
+ const bodies = /* @__PURE__ */ new Map();
7722
+ for (const w of want)
7723
+ bodies.set(
7724
+ w.path,
7725
+ issueBody({
7726
+ path: w.path,
7727
+ text: await readFile13(join26(c.rnessDir, ...w.path.split("/")), "utf8"),
7728
+ org: c.org,
7729
+ numbers,
7730
+ files
7731
+ })
7732
+ );
7733
+ writes = planBodies(bodies, issued);
7734
+ for (const step2 of writes) {
7735
+ await fromGithub(c.provider.apply(board, step2));
7736
+ made++;
7737
+ }
7738
+ } catch (e) {
7739
+ if (!(e instanceof RateLimitError)) throw e;
7740
+ const total = changes + writes.length;
7741
+ throw new Error(
7742
+ `${e.message}: ${total - made} of ${total} changes not made \u2014 the next rness pulse sync makes them`,
7743
+ { cause: e }
7744
+ );
7745
+ }
7746
+ sayDone(ui, c.org, want.length, steps, writes);
6919
7747
  }
6920
7748
  async function pulseCreateCommand(opts, deps = {}) {
6921
7749
  try {
6922
7750
  const terminal = deps.terminal ?? defaultTerminal;
6923
7751
  const ui = deps.ui ?? await makeUi(terminal);
6924
- let c = await context2(opts);
7752
+ const wait = waitSaid(opts, ui);
7753
+ let c = await context2(opts, wait);
6925
7754
  if (c.manifest.pulse !== null)
6926
7755
  throw new Error(
6927
7756
  `already declared: ${boardUrl(c.org, c.manifest.pulse.project)} \u2014 rness pulse sync`
@@ -6949,10 +7778,14 @@ async function pulseCreateCommand(opts, deps = {}) {
6949
7778
  { terminal, ui, ...opts.cwd === void 0 ? {} : { cwd: opts.cwd } }
6950
7779
  );
6951
7780
  if (code !== 0) return code;
6952
- c = { ...c, provider: await openProvider(c.manifest, apiOf(opts)) };
7781
+ c = {
7782
+ ...c,
7783
+ provider: await openProvider(c.manifest, providerOptions(opts, wait))
7784
+ };
6953
7785
  const still = await missingAccess(c.provider);
6954
7786
  if (still !== null) throw new Error(still);
6955
7787
  }
7788
+ await fromGithub(c.provider.checkIssues(c.org));
6956
7789
  const login = (await c.provider.identity())?.login ?? "unknown";
6957
7790
  ui.line(
6958
7791
  "checked",
@@ -6981,7 +7814,7 @@ async function pulseCreateCommand(opts, deps = {}) {
6981
7814
  async function pulseSyncCommand(opts, deps = {}) {
6982
7815
  try {
6983
7816
  const ui = deps.ui ?? await makeUi(deps.terminal ?? defaultTerminal);
6984
- const c = await context2(opts);
7817
+ const c = await context2(opts, waitSaid(opts, ui));
6985
7818
  await syncBoard(c, await declaredBoard(c), ui, eachAdded);
6986
7819
  return 0;
6987
7820
  } catch (e) {
@@ -6999,17 +7832,37 @@ async function pulseMarkCommand(opts) {
6999
7832
  async function mark(opts) {
7000
7833
  const c = await context2(opts);
7001
7834
  const board = await declaredBoard(c);
7002
- const items = await fromGithub(c.provider.items(board));
7835
+ const key = `${c.org}-${board.number}`;
7836
+ const lock = opts.sleep === void 0 ? {} : { sleep: opts.sleep };
7837
+ const listed = () => fromGithub(c.provider.items(board, { bodies: false }));
7838
+ let items = await listed();
7003
7839
  if (opts.end === true) {
7004
7840
  const id = opts.session.split(" \xB7 ").pop() ?? opts.session;
7005
7841
  const ids2 = items.filter(
7006
7842
  (i) => i.session === opts.session || i.session?.endsWith(` \xB7 ${id}`)
7007
7843
  ).map((i) => i.id);
7008
7844
  await fromGithub(c.provider.mark(board, ids2, null));
7009
- await syncBoard(c, board, plainUi, eachAdded);
7845
+ await oneSyncAtATime(key, () => syncBoard(c, board, plainUi, eachAdded), {
7846
+ ...lock,
7847
+ wait: true
7848
+ });
7010
7849
  return 0;
7011
7850
  }
7012
7851
  const wanted = new Set(opts.paths);
7852
+ const documents2 = new Set(
7853
+ (await statusTabs(c.rnessDir)).flatMap((t) => t.rows.map((r) => r.path))
7854
+ );
7855
+ const onBoard = new Set(
7856
+ items.flatMap((i) => i.path === null ? [] : [i.path])
7857
+ );
7858
+ if ([...wanted].some((p) => documents2.has(p) && !onBoard.has(p))) {
7859
+ const synced = await oneSyncAtATime(
7860
+ key,
7861
+ () => syncBoard(c, board, plainUi, eachAdded),
7862
+ lock
7863
+ );
7864
+ if (synced) items = await listed();
7865
+ }
7013
7866
  const ids = items.filter((i) => i.path !== null && wanted.has(i.path)).map((i) => i.id);
7014
7867
  await fromGithub(c.provider.mark(board, ids, opts.session));
7015
7868
  return 0;
@@ -7528,4 +8381,4 @@ export {
7528
8381
  OWN_COMMANDS,
7529
8382
  catchUp
7530
8383
  };
7531
- //# sourceMappingURL=chunk-AEW7ZDFS.js.map
8384
+ //# sourceMappingURL=chunk-4VTYSPZW.js.map