@rness/cli 0.16.0 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -292,6 +292,7 @@ var KEYS = [
292
292
  "org",
293
293
  "agents",
294
294
  "pulse",
295
+ "projects",
295
296
  "repos",
296
297
  "scopes"
297
298
  ];
@@ -352,7 +353,26 @@ function readPulse(value) {
352
353
  fail(
353
354
  `"pulse" must be { "project": <number> } (got ${JSON.stringify(value)})`
354
355
  );
355
- return { project: value.project };
356
+ return { [PULSE]: value.project };
357
+ }
358
+ var PULSE = "pulse";
359
+ var PROJECT_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
360
+ function readProjects(value) {
361
+ if (value === void 0) return null;
362
+ const entries = isRecord(value) ? Object.entries(value) : [];
363
+ const numbers = entries.map(([, n]) => n);
364
+ if (entries.length === 0 || !entries.every(
365
+ ([name, n]) => PROJECT_NAME.test(name) && typeof n === "number" && Number.isInteger(n) && n >= 1
366
+ ) || new Set(numbers).size !== numbers.length)
367
+ fail(
368
+ `"projects" must map names to distinct project numbers, as { "pulse": 4, "marketing": 5 } (got ${JSON.stringify(value)})`
369
+ );
370
+ return Object.fromEntries(entries);
371
+ }
372
+ function readProjectsOrPulse(data) {
373
+ if (data.pulse !== void 0 && data.projects !== void 0)
374
+ fail('"pulse" and "projects" at once: keep "projects" only');
375
+ return data.projects !== void 0 ? readProjects(data.projects) : readPulse(data.pulse);
356
376
  }
357
377
  function providerOf(manifest) {
358
378
  if (manifest.provider !== null) return manifest.provider;
@@ -421,7 +441,7 @@ async function loadManifest(rnessDir) {
421
441
  provider: readProvider(data.provider),
422
442
  org: readOrg(data.org),
423
443
  agents: readAgents(data.agents),
424
- pulse: readPulse(data.pulse),
444
+ projects: readProjectsOrPulse(data),
425
445
  repos: readRepos(data.repos),
426
446
  scopes: readScopes(data.scopes)
427
447
  };
@@ -443,8 +463,10 @@ async function writeManifest(rnessDir, manifest) {
443
463
  lines.push(
444
464
  ` "agents": [${manifest.agents.map((a) => JSON.stringify(a)).join(", ")}],`
445
465
  );
446
- if (manifest.pulse !== null)
447
- lines.push(` "pulse": { "project": ${manifest.pulse.project} },`);
466
+ if (manifest.projects !== null)
467
+ lines.push(
468
+ ` "projects": { ${Object.entries(manifest.projects).map(([name, n]) => `${JSON.stringify(name)}: ${n}`).join(", ")} },`
469
+ );
448
470
  lines.push(
449
471
  ' "repos": {',
450
472
  repos.join(",\n"),
@@ -486,7 +508,7 @@ function parseRepoSpec(spec, org, host) {
486
508
  }
487
509
 
488
510
  // src/version.ts
489
- var VERSION = "0.16.0";
511
+ var VERSION = "0.17.1";
490
512
 
491
513
  // src/core/pm.ts
492
514
  import { execFile as execFile2 } from "child_process";
@@ -1321,6 +1343,196 @@ ${content}`;
1321
1343
  ${comment(digest(text))}`;
1322
1344
  }
1323
1345
 
1346
+ // src/pulse/collection.ts
1347
+ import { readFile as readFile5, readdir as readdir2 } from "fs/promises";
1348
+ import { join as join8 } from "path";
1349
+ var FIELD_TYPES = ["text", "date", "select", "number"];
1350
+ var TAKEN = new Set(
1351
+ [
1352
+ "Status",
1353
+ "Collection",
1354
+ "Agent",
1355
+ "Working session",
1356
+ "Path",
1357
+ "Session history",
1358
+ "Title",
1359
+ "Assignees",
1360
+ "Labels",
1361
+ "Linked pull requests",
1362
+ "Milestone",
1363
+ "Repository",
1364
+ "Reviewers",
1365
+ "Type",
1366
+ "Parent issue",
1367
+ "Sub-issues progress"
1368
+ ].map((n) => n.toLowerCase())
1369
+ );
1370
+ var isRecord2 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
1371
+ var isNames = (v) => Array.isArray(v) && v.every((s) => typeof s === "string" && s.trim() !== "") && new Set(v).size === v.length;
1372
+ async function readText(path) {
1373
+ try {
1374
+ return await readFile5(path, "utf8");
1375
+ } catch (e) {
1376
+ if (e.code === "ENOENT") return null;
1377
+ throw e;
1378
+ }
1379
+ }
1380
+ function readField(where, name, value) {
1381
+ const key = `fields.${name}`;
1382
+ if (TAKEN.has(name.toLowerCase()))
1383
+ throw new Error(`${where}: "${key}" is a field of rness: name it otherwise`);
1384
+ if (!isRecord2(value))
1385
+ throw new Error(`${where}: "fields" must map names to { type, from }`);
1386
+ const type = FIELD_TYPES.find((t) => t === value["type"]);
1387
+ if (type === void 0)
1388
+ throw new Error(
1389
+ `${where}: "${key}.type" must be one of ${FIELD_TYPES.join(", ")}`
1390
+ );
1391
+ const raw = value["from"];
1392
+ const from = typeof raw === "string" && raw !== "" ? [raw] : raw;
1393
+ if (!isNames(from) || from.length === 0)
1394
+ throw new Error(
1395
+ `${where}: "${key}.from" must be a front-matter key or a list of them`
1396
+ );
1397
+ const options = value["options"] ?? [];
1398
+ if (!isNames(options) && !(Array.isArray(options) && options.length === 0))
1399
+ throw new Error(`${where}: "${key}.options" must be a list of names`);
1400
+ return { name, type, from, options: [...options] };
1401
+ }
1402
+ function readLabels(where, value) {
1403
+ if (value === void 0 || value === null) return null;
1404
+ if (typeof value !== "string" || value.trim() === "")
1405
+ throw new Error(
1406
+ `${where}: "labels" must be directory or a front-matter key`
1407
+ );
1408
+ return value === "directory" ? { kind: "directory" } : { kind: "key", key: value };
1409
+ }
1410
+ async function readShape(rnessDir, collection) {
1411
+ const where = `${collection}/README.md`;
1412
+ const text = await readText(join8(rnessDir, collection, "README.md"));
1413
+ if (text === null)
1414
+ return {
1415
+ readme: null,
1416
+ description: null,
1417
+ statuses: null,
1418
+ fields: [],
1419
+ labels: null
1420
+ };
1421
+ let front;
1422
+ try {
1423
+ front = parseFrontMatter(text);
1424
+ } catch (e) {
1425
+ throw new Error(`${where}: ${e instanceof Error ? e.message : String(e)}`, {
1426
+ cause: e
1427
+ });
1428
+ }
1429
+ const f = front ?? {};
1430
+ const description = f["description"] ?? null;
1431
+ if (description !== null && typeof description !== "string")
1432
+ throw new Error(`${where}: "description" must be text`);
1433
+ const statuses = f["statuses"] ?? null;
1434
+ if (statuses !== null && !isNames(statuses))
1435
+ throw new Error(`${where}: "statuses" must be a list of names`);
1436
+ const fields2 = f["fields"] ?? {};
1437
+ if (!isRecord2(fields2))
1438
+ throw new Error(`${where}: "fields" must map names to { type, from }`);
1439
+ return {
1440
+ readme: stripFrontMatter(text),
1441
+ description: description === null ? null : description.trim(),
1442
+ statuses: statuses === null ? null : [...statuses],
1443
+ fields: Object.entries(fields2).map(
1444
+ ([name, v]) => readField(where, name, v)
1445
+ ),
1446
+ labels: readLabels(where, f["labels"])
1447
+ };
1448
+ }
1449
+ function dayOf(value) {
1450
+ if (value instanceof Date && !Number.isNaN(value.getTime()))
1451
+ return value.toISOString().slice(0, 10);
1452
+ if (typeof value !== "string") return null;
1453
+ const day = /^(\d{4}-\d{2}-\d{2})(?:$|[T ])/.exec(value.trim())?.[1];
1454
+ return day ?? null;
1455
+ }
1456
+ function valueOf(field, fields2) {
1457
+ const key = field.from.find(
1458
+ (k) => fields2[k] !== void 0 && fields2[k] !== null && fields2[k] !== ""
1459
+ );
1460
+ if (key === void 0) return null;
1461
+ const raw = fields2[key];
1462
+ switch (field.type) {
1463
+ case "date":
1464
+ return dayOf(raw);
1465
+ case "number": {
1466
+ const n = typeof raw === "number" ? raw : Number(raw);
1467
+ return typeof raw !== "boolean" && Number.isFinite(n) ? String(n) : null;
1468
+ }
1469
+ default:
1470
+ if (Array.isArray(raw)) return raw.map(String).join(", ");
1471
+ return typeof raw === "object" ? null : String(raw);
1472
+ }
1473
+ }
1474
+ function valuesOf(shape, fields2, rel) {
1475
+ const values2 = Object.fromEntries(
1476
+ shape.fields.map((f) => [f.name, valueOf(f, fields2)])
1477
+ );
1478
+ const labels = [];
1479
+ if (shape.labels?.kind === "directory") {
1480
+ const slash = rel.indexOf("/");
1481
+ if (slash > 0) labels.push(rel.slice(0, slash));
1482
+ } else if (shape.labels?.kind === "key") {
1483
+ const raw = fields2[shape.labels.key];
1484
+ for (const v of Array.isArray(raw) ? raw : [raw])
1485
+ if (typeof v === "string" && v.trim() !== "" && !labels.includes(v))
1486
+ labels.push(v.trim());
1487
+ }
1488
+ return { values: values2, labels };
1489
+ }
1490
+ var HEALTHS = [
1491
+ "on-track",
1492
+ "at-risk",
1493
+ "off-track",
1494
+ "complete",
1495
+ "inactive"
1496
+ ];
1497
+ async function readUpdates(rnessDir, collection) {
1498
+ let names;
1499
+ try {
1500
+ names = (await readdir2(join8(rnessDir, collection, "updates"), {
1501
+ withFileTypes: true
1502
+ })).filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => e.name).sort();
1503
+ } catch (e) {
1504
+ if (e.code === "ENOENT") return [];
1505
+ throw e;
1506
+ }
1507
+ const updates = [];
1508
+ for (const name of names) {
1509
+ const path = `${collection}/updates/${name}`;
1510
+ const text = await readText(join8(rnessDir, collection, "updates", name)) ?? "";
1511
+ let front;
1512
+ try {
1513
+ front = parseFrontMatter(text);
1514
+ } catch (e) {
1515
+ throw new Error(
1516
+ `${path}: ${e instanceof Error ? e.message : String(e)}`,
1517
+ {
1518
+ cause: e
1519
+ }
1520
+ );
1521
+ }
1522
+ const health = HEALTHS.find((h) => h === front?.["health"]);
1523
+ if (health === void 0)
1524
+ throw new Error(`${path}: "health" must be one of ${HEALTHS.join(", ")}`);
1525
+ updates.push({
1526
+ path,
1527
+ health,
1528
+ startDate: dayOf(front?.["start_date"]),
1529
+ targetDate: dayOf(front?.["target_date"]),
1530
+ body: stripFrontMatter(text)
1531
+ });
1532
+ }
1533
+ return updates;
1534
+ }
1535
+
1324
1536
  // src/pulse/layout.ts
1325
1537
  var statusOf2 = (status2) => status2 === null || status2 === "?" ? null : status2;
1326
1538
  var LIFECYCLE = [
@@ -1394,36 +1606,92 @@ function fieldsOf(tabs) {
1394
1606
  }
1395
1607
  return fields2;
1396
1608
  }
1397
- function layoutOf(tabs) {
1398
- const fields2 = fieldsOf(tabs);
1609
+ var once = (values2) => [...new Set(values2)];
1610
+ function layoutOf(tabs, own) {
1611
+ if (own === void 0) {
1612
+ const fields2 = fieldsOf(tabs);
1613
+ return {
1614
+ statuses: inLifecycleOrder(
1615
+ [...fields2.values()].flatMap((f) => f.statuses)
1616
+ ),
1617
+ fields: [...fields2.values()],
1618
+ types: tabs.map((t) => t.label),
1619
+ views: tabs.map((t) => ({
1620
+ name: t.label,
1621
+ type: t.label,
1622
+ field: fields2.get(t.label)?.name ?? null
1623
+ })),
1624
+ declared: [],
1625
+ labels: []
1626
+ };
1627
+ }
1628
+ const { shape, want } = own;
1399
1629
  return {
1400
- statuses: inLifecycleOrder([...fields2.values()].flatMap((f) => f.statuses)),
1401
- fields: [...fields2.values()],
1630
+ statuses: once([
1631
+ ...shape.statuses ?? [],
1632
+ ...tabs.flatMap(collectionStatuses)
1633
+ ]),
1634
+ fields: [],
1402
1635
  types: tabs.map((t) => t.label),
1403
- views: tabs.map((t) => ({
1404
- name: t.label,
1405
- type: t.label,
1406
- field: fields2.get(t.label)?.name ?? null
1407
- }))
1636
+ views: tabs.map((t) => ({ name: t.label, type: t.label, field: null })),
1637
+ declared: shape.fields.map((f) => ({
1638
+ name: f.name,
1639
+ type: f.type,
1640
+ options: f.type === "select" ? once([
1641
+ ...f.options,
1642
+ ...want.map((w) => w.values[f.name]).filter((v) => typeof v === "string")
1643
+ ]) : []
1644
+ })),
1645
+ // Made only when a document carries it: a directory with no card is no
1646
+ // label, though one carrying it once is taken off (`unlabels`).
1647
+ labels: once(want.flatMap((w) => w.labels))
1408
1648
  };
1409
1649
  }
1410
- function desiredOf(tabs, org) {
1411
- const fields2 = fieldsOf(tabs);
1412
- return tabs.flatMap(
1413
- (tab) => tab.rows.map((row) => ({
1414
- path: row.path,
1415
- title: `${row.id} \u2014 ${row.title}`,
1416
- body: headerOf(row.path, org),
1417
- status: statusOf2(row.status),
1418
- type: tab.label,
1419
- statusField: fields2.get(tab.label)?.name ?? null,
1420
- sessions: row.sessions?.map((e) => e.agent === void 0 ? e.id : `${e.agent} \xB7 ${e.id}`).join(", ") ?? null
1421
- }))
1650
+ var sessionsOf2 = (row) => row.sessions?.map((e) => e.agent === void 0 ? e.id : `${e.agent} \xB7 ${e.id}`).join(", ") ?? null;
1651
+ function desiredOf(tabs, org, own) {
1652
+ const fields2 = own === void 0 ? fieldsOf(tabs) : /* @__PURE__ */ new Map();
1653
+ const wanted = tabs.flatMap(
1654
+ (tab) => tab.rows.map((row) => {
1655
+ const { values: values2, labels } = own === void 0 ? { values: {}, labels: [] } : valuesOf(
1656
+ own.shape,
1657
+ own.fronts.get(row.path) ?? {},
1658
+ row.path.slice(tab.name.length + 1)
1659
+ );
1660
+ return {
1661
+ path: row.path,
1662
+ title: `${row.id} \u2014 ${row.title}`,
1663
+ body: headerOf(row.path, org),
1664
+ status: statusOf2(row.status),
1665
+ type: tab.label,
1666
+ statusField: fields2.get(tab.label)?.name ?? null,
1667
+ sessions: sessionsOf2(row),
1668
+ values: values2,
1669
+ labels,
1670
+ unlabels: []
1671
+ };
1672
+ })
1422
1673
  );
1674
+ if (own === void 0) return wanted;
1675
+ const made = once([
1676
+ ...wanted.flatMap((w) => w.labels),
1677
+ ...own.shape.labels?.kind === "directory" ? own.directories ?? [] : []
1678
+ ]);
1679
+ return wanted.map((w) => ({
1680
+ ...w,
1681
+ unlabels: made.filter((l) => !w.labels.includes(l))
1682
+ }));
1423
1683
  }
1424
1684
 
1425
1685
  // src/pulse/plan.ts
1426
- var differs = (item, w) => item.title !== w.title || item.status !== w.status || w.statusField !== null && item.collectionStatus !== w.status || item.type !== w.type || item.sessions !== w.sessions;
1686
+ var lower = (labels) => new Set(labels.map((l) => l.toLowerCase()));
1687
+ function labelsDiffer(item, w) {
1688
+ if (w.labels.length === 0 && w.unlabels.length === 0) return false;
1689
+ const carried = lower(item.issue?.labels ?? []);
1690
+ return w.labels.some((l) => !carried.has(l.toLowerCase())) || w.unlabels.some((l) => carried.has(l.toLowerCase()));
1691
+ }
1692
+ var differs = (item, w) => item.title !== w.title || item.status !== w.status || w.statusField !== null && item.collectionStatus !== w.status || item.type !== w.type || item.sessions !== w.sessions || Object.entries(w.values).some(
1693
+ ([name, value]) => (item.values[name] ?? null) !== value
1694
+ ) || labelsDiffer(item, w);
1427
1695
  var rank = (i) => i.issue === null ? 2 : i.issue.open ? 0 : 1;
1428
1696
  var keptFirst = (a, b) => rank(a) - rank(b) || (a.issue?.number ?? 0) - (b.issue?.number ?? 0);
1429
1697
  var gone = (item) => item.issue === null ? { kind: "archive", id: item.id } : { kind: "close", id: item.id };
@@ -1449,7 +1717,7 @@ function unwantedPaths(want, have) {
1449
1717
  function isGone(path, item, seen, opened) {
1450
1718
  return seen.has(path) || item.issue !== null && opened.get(item.issue.number) === path;
1451
1719
  }
1452
- function planSync(want, have, seen = /* @__PURE__ */ new Set(), opened = /* @__PURE__ */ new Map()) {
1720
+ function planSync(want, have, seen = /* @__PURE__ */ new Set(), opened = /* @__PURE__ */ new Map(), elsewhere = () => false) {
1453
1721
  const byPath = /* @__PURE__ */ new Map();
1454
1722
  for (const item of have)
1455
1723
  if (item.path !== null)
@@ -1482,7 +1750,8 @@ function planSync(want, have, seen = /* @__PURE__ */ new Set(), opened = /* @__P
1482
1750
  for (const [path, item] of kept)
1483
1751
  if (!wanted.has(path))
1484
1752
  steps.push(
1485
- isGone(path, item, seen, opened) ? gone(item) : { kind: "unchanged", id: item.id }
1753
+ // Its own project holds it now: off this board, not closed.
1754
+ elsewhere(path) ? { kind: "remove", id: item.id } : isGone(path, item, seen, opened) ? gone(item) : { kind: "unchanged", id: item.id }
1486
1755
  );
1487
1756
  return [...steps, ...surplus];
1488
1757
  }
@@ -1843,11 +2112,11 @@ async function createRepository(owner, name, options) {
1843
2112
  }
1844
2113
 
1845
2114
  // src/core/github-projects.ts
1846
- function isRecord2(v) {
2115
+ function isRecord3(v) {
1847
2116
  return v !== null && typeof v === "object";
1848
2117
  }
1849
- var rateLimited = (body) => isRecord2(body) && Array.isArray(body["errors"]) && body["errors"].some(
1850
- (e) => isRecord2(e) && e["type"] === "RATE_LIMITED"
2118
+ var rateLimited = (body) => isRecord3(body) && Array.isArray(body["errors"]) && body["errors"].some(
2119
+ (e) => isRecord3(e) && e["type"] === "RATE_LIMITED"
1851
2120
  );
1852
2121
  async function graphqlRaw(query, variables, options) {
1853
2122
  const body = await postJson(
@@ -1857,7 +2126,7 @@ async function graphqlRaw(query, variables, options) {
1857
2126
  true,
1858
2127
  rateLimited
1859
2128
  );
1860
- return isRecord2(body) ? body : {};
2129
+ return isRecord3(body) ? body : {};
1861
2130
  }
1862
2131
  function firstError(answer) {
1863
2132
  const first = answer.errors?.[0];
@@ -1933,6 +2202,7 @@ function toField(raw) {
1933
2202
  id: raw.id ?? "",
1934
2203
  databaseId,
1935
2204
  name: raw.name ?? "",
2205
+ dataType: raw.dataType ?? (raw.options === void 0 ? "TEXT" : "SINGLE_SELECT"),
1936
2206
  options: raw.options ?? null
1937
2207
  };
1938
2208
  }
@@ -1962,10 +2232,10 @@ var optionInput = (o, id) => ({
1962
2232
  color: o.color,
1963
2233
  description: ""
1964
2234
  });
1965
- var FIELD_SELECTION = `... on ProjectV2FieldCommon { id databaseId name }
2235
+ var FIELD_SELECTION = `... on ProjectV2FieldCommon { id databaseId name dataType }
1966
2236
  ... on ProjectV2SingleSelectField { options { id name color } }`;
1967
2237
  async function createField(projectId, name, kind, o) {
1968
- const single = kind !== "TEXT";
2238
+ const single = typeof kind !== "string";
1969
2239
  const d = await graphql(
1970
2240
  `mutation($projectId: ID!, $name: String!, $dataType: ProjectV2CustomFieldType!${single ? ", $options: [ProjectV2SingleSelectFieldOptionInput!]" : ""}) {
1971
2241
  createProjectV2Field(input: {
@@ -1975,7 +2245,7 @@ async function createField(projectId, name, kind, o) {
1975
2245
  {
1976
2246
  projectId,
1977
2247
  name,
1978
- dataType: single ? "SINGLE_SELECT" : "TEXT",
2248
+ dataType: single ? "SINGLE_SELECT" : kind,
1979
2249
  ...single ? { options: kind.options.map((n) => optionInput(n)) } : {}
1980
2250
  },
1981
2251
  o
@@ -2062,6 +2332,22 @@ async function listItems(projectId, o, { bodies = true } = {}) {
2062
2332
  }
2063
2333
  }
2064
2334
  }
2335
+ ... on ProjectV2ItemFieldDateValue {
2336
+ date
2337
+ field {
2338
+ ... on ProjectV2FieldCommon {
2339
+ name
2340
+ }
2341
+ }
2342
+ }
2343
+ ... on ProjectV2ItemFieldNumberValue {
2344
+ number
2345
+ field {
2346
+ ... on ProjectV2FieldCommon {
2347
+ name
2348
+ }
2349
+ }
2350
+ }
2065
2351
  }
2066
2352
  }
2067
2353
  }
@@ -2081,7 +2367,7 @@ async function listItems(projectId, o, { bodies = true } = {}) {
2081
2367
  const values2 = {};
2082
2368
  for (const v of n.fieldValues?.nodes ?? []) {
2083
2369
  const field = v.field?.name;
2084
- const value = v.text ?? v.name;
2370
+ const value = v.text ?? v.name ?? v.date ?? (v.number === void 0 ? void 0 : String(v.number));
2085
2371
  if (field !== void 0 && value !== void 0) values2[field] = value;
2086
2372
  }
2087
2373
  const c = n.content;
@@ -2152,7 +2438,7 @@ async function setValue(projectId, itemId, fieldId, value, o) {
2152
2438
  projectId,
2153
2439
  itemId,
2154
2440
  fieldId,
2155
- value: "text" in value ? { text: value.text } : { singleSelectOptionId: value.optionId }
2441
+ value: "optionId" in value ? { singleSelectOptionId: value.optionId } : value
2156
2442
  },
2157
2443
  o
2158
2444
  );
@@ -2351,6 +2637,142 @@ async function renameView(viewId, name, visibleFieldIds, o) {
2351
2637
  o
2352
2638
  );
2353
2639
  }
2640
+ async function deleteItem(projectId, itemId, o) {
2641
+ await graphql(
2642
+ `
2643
+ mutation ($projectId: ID!, $itemId: ID!) {
2644
+ deleteProjectV2Item(input: { projectId: $projectId, itemId: $itemId }) {
2645
+ deletedItemId
2646
+ }
2647
+ }
2648
+ `,
2649
+ { projectId, itemId },
2650
+ o
2651
+ );
2652
+ }
2653
+ async function projectText(projectId, o) {
2654
+ const d = await graphql(
2655
+ `
2656
+ query ($projectId: ID!) {
2657
+ node(id: $projectId) {
2658
+ ... on ProjectV2 {
2659
+ projectReadme: readme
2660
+ projectDescription: shortDescription
2661
+ }
2662
+ }
2663
+ }
2664
+ `,
2665
+ { projectId },
2666
+ o
2667
+ );
2668
+ return {
2669
+ readme: d.node?.projectReadme ?? "",
2670
+ shortDescription: d.node?.projectDescription ?? ""
2671
+ };
2672
+ }
2673
+ async function updateProjectText(projectId, text, o) {
2674
+ await graphql(
2675
+ `
2676
+ mutation ($projectId: ID!, $readme: String, $shortDescription: String) {
2677
+ updateProjectV2(
2678
+ input: {
2679
+ projectId: $projectId
2680
+ readme: $readme
2681
+ shortDescription: $shortDescription
2682
+ }
2683
+ ) {
2684
+ projectV2 {
2685
+ id
2686
+ }
2687
+ }
2688
+ }
2689
+ `,
2690
+ { projectId, ...text },
2691
+ o
2692
+ );
2693
+ }
2694
+ async function statusUpdates(projectId, o) {
2695
+ const d = await graphql(
2696
+ `
2697
+ query ($projectId: ID!) {
2698
+ node(id: $projectId) {
2699
+ ... on ProjectV2 {
2700
+ statusUpdates(first: 100) {
2701
+ nodes {
2702
+ id
2703
+ body
2704
+ status
2705
+ startDate
2706
+ targetDate
2707
+ }
2708
+ }
2709
+ }
2710
+ }
2711
+ }
2712
+ `,
2713
+ { projectId },
2714
+ o
2715
+ );
2716
+ return d.node?.statusUpdates.nodes ?? [];
2717
+ }
2718
+ async function createStatusUpdate(projectId, update, o) {
2719
+ await graphql(
2720
+ `
2721
+ mutation (
2722
+ $projectId: ID!
2723
+ $body: String!
2724
+ $status: ProjectV2StatusUpdateStatus!
2725
+ $startDate: Date
2726
+ $targetDate: Date
2727
+ ) {
2728
+ createProjectV2StatusUpdate(
2729
+ input: {
2730
+ projectId: $projectId
2731
+ body: $body
2732
+ status: $status
2733
+ startDate: $startDate
2734
+ targetDate: $targetDate
2735
+ }
2736
+ ) {
2737
+ statusUpdate {
2738
+ id
2739
+ }
2740
+ }
2741
+ }
2742
+ `,
2743
+ { projectId, ...update },
2744
+ o
2745
+ );
2746
+ }
2747
+ async function updateStatusUpdate(statusUpdateId, update, o) {
2748
+ await graphql(
2749
+ `
2750
+ mutation (
2751
+ $statusUpdateId: ID!
2752
+ $body: String!
2753
+ $status: ProjectV2StatusUpdateStatus!
2754
+ $startDate: Date
2755
+ $targetDate: Date
2756
+ ) {
2757
+ updateProjectV2StatusUpdate(
2758
+ input: {
2759
+ statusUpdateId: $statusUpdateId
2760
+ body: $body
2761
+ status: $status
2762
+ startDate: $startDate
2763
+ targetDate: $targetDate
2764
+ }
2765
+ ) {
2766
+ statusUpdate {
2767
+ id
2768
+ }
2769
+ }
2770
+ }
2771
+ `,
2772
+ { statusUpdateId, ...update },
2773
+ o
2774
+ );
2775
+ }
2354
2776
 
2355
2777
  // src/core/github-issues.ts
2356
2778
  async function repository(owner, name, label, o) {
@@ -2489,7 +2911,7 @@ async function reopenIssue(id, o) {
2489
2911
  o
2490
2912
  );
2491
2913
  }
2492
- async function addLabels(id, labelIds, o) {
2914
+ async function addLabels(id, labelIds2, o) {
2493
2915
  await graphql(
2494
2916
  `
2495
2917
  mutation ($id: ID!, $labelIds: [ID!]!) {
@@ -2498,7 +2920,53 @@ async function addLabels(id, labelIds, o) {
2498
2920
  }
2499
2921
  }
2500
2922
  `,
2501
- { id, labelIds },
2923
+ { id, labelIds: labelIds2 },
2924
+ o
2925
+ );
2926
+ }
2927
+ async function labelIds(owner, name, o) {
2928
+ const ids = /* @__PURE__ */ new Map();
2929
+ let after = null;
2930
+ for (; ; ) {
2931
+ const d = await graphql(
2932
+ `
2933
+ query ($owner: String!, $name: String!, $after: String) {
2934
+ repository(owner: $owner, name: $name) {
2935
+ labelList: labels(first: 100, after: $after) {
2936
+ nodes {
2937
+ id
2938
+ name
2939
+ }
2940
+ pageInfo {
2941
+ hasNextPage
2942
+ endCursor
2943
+ }
2944
+ }
2945
+ }
2946
+ }
2947
+ `,
2948
+ { owner, name, after },
2949
+ o
2950
+ );
2951
+ const list2 = d.repository?.labelList;
2952
+ for (const l of list2?.nodes ?? []) ids.set(l.name, l.id);
2953
+ if (list2?.pageInfo.hasNextPage !== true || list2.pageInfo.endCursor === null)
2954
+ return ids;
2955
+ after = list2.pageInfo.endCursor;
2956
+ }
2957
+ }
2958
+ async function removeLabels(id, labelIds2, o) {
2959
+ await graphql(
2960
+ `
2961
+ mutation ($id: ID!, $labelIds: [ID!]!) {
2962
+ removeLabelsFromLabelable(
2963
+ input: { labelableId: $id, labelIds: $labelIds }
2964
+ ) {
2965
+ clientMutationId
2966
+ }
2967
+ }
2968
+ `,
2969
+ { id, labelIds: labelIds2 },
2502
2970
  o
2503
2971
  );
2504
2972
  }
@@ -2515,9 +2983,19 @@ var FORMERLY = {
2515
2983
  [WORKING_SESSION]: "Session",
2516
2984
  [SESSION_HISTORY]: "Sessions"
2517
2985
  };
2518
- var valueOf = (values2, name) => values2[name] ?? values2[FORMERLY[name] ?? ""] ?? null;
2986
+ var valueOf2 = (values2, name) => values2[name] ?? values2[FORMERLY[name] ?? ""] ?? null;
2519
2987
  var LABEL_COLOR = "5319e7";
2520
2988
  var LABEL_DESCRIPTION = "A document of .rness, on Agent Pulse";
2989
+ var OWN_LABEL_COLOR = "ededed";
2990
+ var OWN_LABEL_DESCRIPTION = "A label a collection of .rness gives its documents";
2991
+ var CALENDAR_VIEW = "Calendar";
2992
+ var markerOf = (path) => `<!-- rness: ${path} -->`;
2993
+ var DATA_TYPE = {
2994
+ text: "TEXT",
2995
+ date: "DATE",
2996
+ number: "NUMBER",
2997
+ select: "SINGLE_SELECT"
2998
+ };
2521
2999
  var labelled = (labels) => labels.some((l) => l.toLowerCase() === LABEL.toLowerCase());
2522
3000
  var STATUS_FIELD = "Status";
2523
3001
  var ALL_VIEW = "All";
@@ -2549,6 +3027,8 @@ var GitHubBoards = class {
2549
3027
  #cache = /* @__PURE__ */ new Map();
2550
3028
  /** Each organization's `.rness`, as first read: its id, its Issues, its label. */
2551
3029
  #memories = /* @__PURE__ */ new Map();
3030
+ /** Each organization's `.rness` labels by name, read when a collection needs one. */
3031
+ #labels = /* @__PURE__ */ new Map();
2552
3032
  constructor(options) {
2553
3033
  this.#o = options;
2554
3034
  }
@@ -2589,10 +3069,10 @@ var GitHubBoards = class {
2589
3069
  );
2590
3070
  }
2591
3071
  /** The project only: the caller declares it before `ensureLayout` builds it. */
2592
- async createBoard(org) {
3072
+ async createBoard(org, title = PROJECT_TITLE) {
2593
3073
  const project = await createProject(
2594
3074
  await orgId(org, this.#o),
2595
- PROJECT_TITLE,
3075
+ title,
2596
3076
  this.#o
2597
3077
  );
2598
3078
  const board = { org, number: project.number, url: project.url };
@@ -2603,7 +3083,7 @@ var GitHubBoards = class {
2603
3083
  const project = await findProject(org, number, this.#o);
2604
3084
  if (project === null) return null;
2605
3085
  const board = { org, number, url: project.url };
2606
- this.#remember(board, project.id);
3086
+ if (!this.#cache.has(this.#key(board))) this.#remember(board, project.id);
2607
3087
  return board;
2608
3088
  }
2609
3089
  async ensureLayout(board, layout) {
@@ -2678,6 +3158,60 @@ var GitHubBoards = class {
2678
3158
  const session = await text(WORKING_SESSION);
2679
3159
  const path = await text("Path");
2680
3160
  const sessions = await text(SESSION_HISTORY);
3161
+ const declared2 = [];
3162
+ for (const d of layout.declared) {
3163
+ const type = DATA_TYPE[d.type];
3164
+ const field = fields2.find((f) => f.name === d.name);
3165
+ if (field !== void 0 && field.dataType !== type)
3166
+ throw new Error(
3167
+ `the project's field ${d.name} is not a ${d.type} field: rename it on the project, or in the collection's README`
3168
+ );
3169
+ if (d.type !== "select") {
3170
+ if (field !== void 0) declared2.push(field);
3171
+ else {
3172
+ added.push(`field ${d.name}`);
3173
+ declared2.push(
3174
+ await createField(
3175
+ cache.projectId,
3176
+ d.name,
3177
+ DATA_TYPE[d.type],
3178
+ this.#o
3179
+ )
3180
+ );
3181
+ }
3182
+ continue;
3183
+ }
3184
+ if (field === void 0 && d.options.length === 0) continue;
3185
+ const options = (names) => names.map((name) => ({
3186
+ name,
3187
+ color: field?.options?.find((o) => o.name === name)?.color ?? "GRAY"
3188
+ }));
3189
+ if (field === void 0) {
3190
+ added.push(`field ${d.name}`);
3191
+ declared2.push(
3192
+ await createField(
3193
+ cache.projectId,
3194
+ d.name,
3195
+ { options: options(d.options) },
3196
+ this.#o
3197
+ )
3198
+ );
3199
+ continue;
3200
+ }
3201
+ const now = namesOf(field);
3202
+ const next = orderedFirst(d.options, [
3203
+ ...now,
3204
+ ...d.options.filter((n) => !now.includes(n))
3205
+ ]);
3206
+ if (next.every((n, i) => n === now[i]) && next.length === now.length)
3207
+ declared2.push(field);
3208
+ else {
3209
+ added.push(
3210
+ ...d.options.filter((n) => !now.includes(n)).map((n) => `option ${n}`)
3211
+ );
3212
+ declared2.push(await setOptions(field, options(next), this.#o));
3213
+ }
3214
+ }
2681
3215
  const current = await views(cache.projectId, this.#o);
2682
3216
  const first = current[0];
2683
3217
  if (first !== void 0 && first.name === DEFAULT_VIEW && !current.some((v) => v.name === ALL_VIEW)) {
@@ -2710,6 +3244,19 @@ var GitHubBoards = class {
2710
3244
  this.#o
2711
3245
  );
2712
3246
  }
3247
+ if (layout.declared.some((d) => d.type === "date") && !current.some((v) => v.name === CALENDAR_VIEW)) {
3248
+ added.push(`view ${CALENDAR_VIEW}`);
3249
+ await createView(
3250
+ board.org,
3251
+ board.number,
3252
+ {
3253
+ name: CALENDAR_VIEW,
3254
+ layout: "roadmap",
3255
+ filter: viewFilter(layout.types[0] ?? "")
3256
+ },
3257
+ this.#o
3258
+ );
3259
+ }
2713
3260
  if (!current.some((v) => v.name === WORKING_VIEW)) {
2714
3261
  added.push(`view ${WORKING_VIEW}`);
2715
3262
  await createView(
@@ -2740,6 +3287,9 @@ var GitHubBoards = class {
2740
3287
  );
2741
3288
  added.push(`label ${LABEL}`);
2742
3289
  }
3290
+ for (const name of layout.labels)
3291
+ if ((await this.#ownLabel(board.org, name)).made)
3292
+ added.push(`label ${name}`);
2743
3293
  const repository2 = `${board.org}/${MEMORY}`;
2744
3294
  const linked = await linkedRepositories(cache.projectId, this.#o);
2745
3295
  if (!linked.some((r) => r.toLowerCase() === repository2.toLowerCase())) {
@@ -2753,10 +3303,33 @@ var GitHubBoards = class {
2753
3303
  agent,
2754
3304
  session,
2755
3305
  path,
2756
- sessions
3306
+ sessions,
3307
+ ...declared2
2757
3308
  ];
2758
3309
  return added;
2759
3310
  }
3311
+ /** A collection's label on `.rness`, made when missing; its id, and whether this made it. */
3312
+ async #ownLabel(org, name) {
3313
+ let ids = this.#labels.get(org);
3314
+ if (ids === void 0) {
3315
+ ids = await labelIds(org, MEMORY, this.#o);
3316
+ this.#labels.set(org, ids);
3317
+ }
3318
+ const known = [...ids].find(([n]) => n.toLowerCase() === name.toLowerCase());
3319
+ if (known !== void 0) return { id: known[1], made: false };
3320
+ const id = await createLabel(
3321
+ org,
3322
+ MEMORY,
3323
+ {
3324
+ name,
3325
+ color: OWN_LABEL_COLOR,
3326
+ description: OWN_LABEL_DESCRIPTION
3327
+ },
3328
+ this.#o
3329
+ );
3330
+ ids.set(name, id);
3331
+ return { id, made: true };
3332
+ }
2760
3333
  async items(board, options = {}) {
2761
3334
  const cache = await this.#cacheOf(board);
2762
3335
  const raw = await listItems(cache.projectId, this.#o, options);
@@ -2772,15 +3345,17 @@ var GitHubBoards = class {
2772
3345
  number: issue.number,
2773
3346
  open: issue.open,
2774
3347
  labelled: labelled(issue.labels),
2775
- body: issue.body
3348
+ body: issue.body,
3349
+ labels: [...issue.labels]
2776
3350
  },
2777
3351
  title: r.title,
2778
3352
  status: r.values[STATUS_FIELD] ?? null,
2779
3353
  collectionStatus: r.values[statusFieldName(r.values[COLLECTION_FIELD] ?? "")] ?? null,
2780
3354
  type: r.values[COLLECTION_FIELD] ?? null,
2781
3355
  agent: r.values["Agent"] ?? null,
2782
- session: valueOf(r.values, WORKING_SESSION),
2783
- sessions: valueOf(r.values, SESSION_HISTORY)
3356
+ session: valueOf2(r.values, WORKING_SESSION),
3357
+ sessions: valueOf2(r.values, SESSION_HISTORY),
3358
+ values: { ...r.values }
2784
3359
  };
2785
3360
  });
2786
3361
  }
@@ -2790,7 +3365,10 @@ var GitHubBoards = class {
2790
3365
  const field = fields2.find((f) => f.name === name) ?? fields2.find((f) => f.name === FORMERLY[name]);
2791
3366
  if (field === void 0) throw new Error(`the board has no field ${name}`);
2792
3367
  let sent = null;
2793
- if (value !== null && field.options === null) sent = { text: value };
3368
+ if (value !== null && field.dataType === "DATE") sent = { date: value };
3369
+ else if (value !== null && field.dataType === "NUMBER")
3370
+ sent = { number: Number(value) };
3371
+ else if (value !== null && field.options === null) sent = { text: value };
2794
3372
  else if (value !== null) {
2795
3373
  const option = field.options?.find((opt) => opt.name === value);
2796
3374
  if (option === void 0)
@@ -2807,6 +3385,11 @@ var GitHubBoards = class {
2807
3385
  if (step2.kind === "unchanged") return null;
2808
3386
  const cache = await this.#cacheOf(board);
2809
3387
  if (step2.kind === "create") return this.#create(board, cache, step2.want);
3388
+ if (step2.kind === "remove") {
3389
+ await deleteItem(cache.projectId, step2.id, this.#o);
3390
+ cache.known.delete(step2.id);
3391
+ return null;
3392
+ }
2810
3393
  const known = await this.#known(board, step2.id);
2811
3394
  if (step2.kind === "archive" || step2.kind === "close") {
2812
3395
  if (step2.kind === "close" && known.issue?.open === true)
@@ -2837,6 +3420,7 @@ var GitHubBoards = class {
2837
3420
  labels: []
2838
3421
  };
2839
3422
  await this.#issue(known, step2.want, labelId);
3423
+ await this.#ownLabels(board.org, known, step2.want);
2840
3424
  await this.#values(board, step2.id, step2.want);
2841
3425
  return { id: step2.id, number: made.number };
2842
3426
  }
@@ -2846,6 +3430,7 @@ var GitHubBoards = class {
2846
3430
  issue.open = true;
2847
3431
  }
2848
3432
  await this.#issue(known, step2.want, labelId);
3433
+ await this.#ownLabels(board.org, known, step2.want);
2849
3434
  await this.#values(board, step2.id, step2.want);
2850
3435
  return null;
2851
3436
  }
@@ -2889,6 +3474,7 @@ var GitHubBoards = class {
2889
3474
  };
2890
3475
  cache.known.set(id2, known);
2891
3476
  await this.#issue(known, want, labelId);
3477
+ await this.#ownLabels(board.org, known, want);
2892
3478
  await this.#values(board, id2, want);
2893
3479
  return { id: id2, number: found.number, adopted: true };
2894
3480
  }
@@ -2912,6 +3498,7 @@ var GitHubBoards = class {
2912
3498
  archived: false,
2913
3499
  values: {}
2914
3500
  });
3501
+ await this.#ownLabels(board.org, cache.known.get(id), want);
2915
3502
  await this.#values(board, id, want);
2916
3503
  return { id, number: made.number };
2917
3504
  }
@@ -2927,18 +3514,45 @@ var GitHubBoards = class {
2927
3514
  known.title = want.title;
2928
3515
  }
2929
3516
  }
3517
+ /** The labels a collection gives the issue, and those it takes off (spec 0025 §4). */
3518
+ async #ownLabels(org, known, want) {
3519
+ if (want.labels.length === 0 && want.unlabels.length === 0) return;
3520
+ const issue = this.#issueOf(known);
3521
+ const carried = (name) => issue.labels.find((l) => l.toLowerCase() === name.toLowerCase());
3522
+ const add = want.labels.filter((l) => carried(l) === void 0);
3523
+ const off = want.unlabels.filter((l) => carried(l) !== void 0);
3524
+ if (add.length > 0) {
3525
+ const ids = [];
3526
+ for (const name of add) ids.push((await this.#ownLabel(org, name)).id);
3527
+ await addLabels(issue.id, ids, this.#o);
3528
+ issue.labels.push(...add);
3529
+ }
3530
+ if (off.length > 0) {
3531
+ const ids = [];
3532
+ for (const name of off) ids.push((await this.#ownLabel(org, name)).id);
3533
+ await removeLabels(issue.id, ids, this.#o);
3534
+ issue.labels = issue.labels.filter(
3535
+ (l) => !off.some((o) => o.toLowerCase() === l.toLowerCase())
3536
+ );
3537
+ }
3538
+ }
2930
3539
  /** Path first — an issue on the board without it is the team's — then the fields, each only when it differs. */
2931
3540
  async #values(board, id, want) {
2932
3541
  const values2 = (await this.#known(board, id)).values;
3542
+ const fields2 = await this.#fields(board);
2933
3543
  const wanted = [
2934
3544
  ["Path", want.path],
2935
3545
  [COLLECTION_FIELD, want.type],
2936
3546
  [STATUS_FIELD, want.status],
2937
3547
  ...want.statusField === null ? [] : [[want.statusField, want.status]],
2938
- [SESSION_HISTORY, want.sessions]
3548
+ [SESSION_HISTORY, want.sessions],
3549
+ // A select with no option yet has no field yet: nothing to write.
3550
+ ...Object.entries(want.values).filter(
3551
+ ([name]) => fields2.some((f) => f.name === name)
3552
+ )
2939
3553
  ];
2940
3554
  for (const [name, value] of wanted)
2941
- if (valueOf(values2, name) !== value)
3555
+ if (valueOf2(values2, name) !== value)
2942
3556
  await this.#set(board, id, name, value);
2943
3557
  }
2944
3558
  async #known(board, id) {
@@ -2967,6 +3581,48 @@ var GitHubBoards = class {
2967
3581
  await this.#set(board, id, WORKING_SESSION, session);
2968
3582
  }
2969
3583
  }
3584
+ async describe(board, text) {
3585
+ const cache = await this.#cacheOf(board);
3586
+ const now = await projectText(cache.projectId, this.#o);
3587
+ const next = {};
3588
+ if (text.readme !== null && now.readme.trimEnd() !== text.readme.trimEnd())
3589
+ next.readme = text.readme.trimEnd();
3590
+ if (text.description !== null && now.shortDescription !== text.description)
3591
+ next.shortDescription = text.description;
3592
+ if (next.readme === void 0 && next.shortDescription === void 0)
3593
+ return [];
3594
+ await updateProjectText(cache.projectId, next, this.#o);
3595
+ return [
3596
+ ...next.readme === void 0 ? [] : ["readme"],
3597
+ ...next.shortDescription === void 0 ? [] : ["short description"]
3598
+ ];
3599
+ }
3600
+ async postUpdates(board, updates) {
3601
+ if (updates.length === 0) return [];
3602
+ const cache = await this.#cacheOf(board);
3603
+ const there = await statusUpdates(cache.projectId, this.#o);
3604
+ const written = [];
3605
+ for (const u of updates) {
3606
+ const marker = markerOf(u.path);
3607
+ const input = {
3608
+ body: `${u.body.trimEnd()}
3609
+
3610
+ ${marker}`,
3611
+ status: u.health.toUpperCase().replace("-", "_"),
3612
+ startDate: u.startDate,
3613
+ targetDate: u.targetDate
3614
+ };
3615
+ const posted = there.find((p) => p.body.includes(marker));
3616
+ if (posted === void 0) {
3617
+ await createStatusUpdate(cache.projectId, input, this.#o);
3618
+ written.push(`posted ${u.path}`);
3619
+ } else if (posted.body.trimEnd() !== input.body || posted.status !== input.status || (posted.startDate ?? null) !== input.startDate || (posted.targetDate ?? null) !== input.targetDate) {
3620
+ await updateStatusUpdate(posted.id, input, this.#o);
3621
+ written.push(`updated ${u.path}`);
3622
+ }
3623
+ }
3624
+ return written;
3625
+ }
2970
3626
  };
2971
3627
 
2972
3628
  // src/core/github-oauth-provider.ts
@@ -3047,8 +3703,8 @@ var GitHubOAuthProvider = class {
3047
3703
  async checkIssues(org) {
3048
3704
  return this.#boards().checkIssues(org);
3049
3705
  }
3050
- async createBoard(org) {
3051
- return this.#boards().createBoard(org);
3706
+ async createBoard(org, title) {
3707
+ return this.#boards().createBoard(org, title);
3052
3708
  }
3053
3709
  async board(org, number) {
3054
3710
  return this.#boards().board(org, number);
@@ -3065,6 +3721,12 @@ var GitHubOAuthProvider = class {
3065
3721
  async mark(board, itemIds, session) {
3066
3722
  return this.#boards().mark(board, itemIds, session);
3067
3723
  }
3724
+ async describe(board, text) {
3725
+ return this.#boards().describe(board, text);
3726
+ }
3727
+ async postUpdates(board, updates) {
3728
+ return this.#boards().postUpdates(board, updates);
3729
+ }
3068
3730
  credentialsFor(url) {
3069
3731
  if (this.#token === null || !url.startsWith(GITHUB_HTTPS)) return null;
3070
3732
  return { username: "x-access-token", password: this.#token.token };
@@ -3079,11 +3741,11 @@ async function githubProvider(apiBase, wait) {
3079
3741
  }
3080
3742
 
3081
3743
  // src/core/repos.ts
3082
- import { glob as glob2, readFile as readFile5, rm as rm3 } from "fs/promises";
3083
- import { join as join8, posix as posix5, sep as sep2 } from "path";
3744
+ import { glob as glob2, readFile as readFile6, rm as rm3 } from "fs/promises";
3745
+ import { join as join9, posix as posix5, sep as sep2 } from "path";
3084
3746
  async function addRepository(input) {
3085
3747
  const { name, url } = parseRepoSpec(input.spec, input.org, input.host);
3086
- const dir = join8(input.root, "org", name);
3748
+ const dir = join9(input.root, "org", name);
3087
3749
  const repoPath = `org/${name}`;
3088
3750
  const current = input.manifest.scopes[name];
3089
3751
  if (current !== void 0 && current.path !== repoPath)
@@ -3126,7 +3788,7 @@ async function addRepository(input) {
3126
3788
  }
3127
3789
  const declared2 = [name];
3128
3790
  for (const { path, base } of subs) {
3129
- if (!await exists(join8(input.root, ...path.split("/")))) {
3791
+ if (!await exists(join9(input.root, ...path.split("/")))) {
3130
3792
  if (action === "cloned") await rm3(dir, { recursive: true, force: true });
3131
3793
  throw new Error(`${path} does not exist`);
3132
3794
  }
@@ -3144,7 +3806,7 @@ async function workspaceDirs(repoDir) {
3144
3806
  let patterns = [];
3145
3807
  try {
3146
3808
  const pkg = JSON.parse(
3147
- await readFile5(join8(repoDir, "package.json"), "utf8")
3809
+ await readFile6(join9(repoDir, "package.json"), "utf8")
3148
3810
  );
3149
3811
  const w = pkg.workspaces;
3150
3812
  if (Array.isArray(w))
@@ -3161,7 +3823,7 @@ async function workspaceDirs(repoDir) {
3161
3823
  for (const pattern of patterns) {
3162
3824
  for await (const match of glob2(pattern, { cwd: repoDir })) {
3163
3825
  const rel = match.split(sep2).join(posix5.sep);
3164
- if (await exists(join8(repoDir, ...rel.split(posix5.sep), "package.json")))
3826
+ if (await exists(join9(repoDir, ...rel.split(posix5.sep), "package.json")))
3165
3827
  dirs.add(rel);
3166
3828
  }
3167
3829
  }
@@ -3412,8 +4074,8 @@ function httpsFlagLine(spec) {
3412
4074
  }
3413
4075
 
3414
4076
  // src/core/workspace.ts
3415
- import { readdir as readdir2 } from "fs/promises";
3416
- import { dirname as dirname3, join as join9, parse as parse2, resolve as resolve2 } from "path";
4077
+ import { readdir as readdir3 } from "fs/promises";
4078
+ import { dirname as dirname3, join as join10, parse as parse2, resolve as resolve2 } from "path";
3417
4079
  async function findWorkspace(startDir) {
3418
4080
  const start = resolve2(startDir);
3419
4081
  const { root: fsRoot } = parse2(start);
@@ -3423,18 +4085,18 @@ async function findWorkspace(startDir) {
3423
4085
  if (dir === fsRoot) break;
3424
4086
  }
3425
4087
  for (const dir of ancestors) {
3426
- if (await exists(join9(dir, ".rness", "rness.json")))
3427
- return { root: dir, rnessDir: join9(dir, ".rness") };
4088
+ if (await exists(join10(dir, ".rness", "rness.json")))
4089
+ return { root: dir, rnessDir: join10(dir, ".rness") };
3428
4090
  }
3429
4091
  for (const dir of ancestors) {
3430
- if (await exists(join9(dir, "rness.json")))
4092
+ if (await exists(join10(dir, "rness.json")))
3431
4093
  return { root: dirname3(dir), rnessDir: dir };
3432
4094
  }
3433
4095
  throw new Error(`no rness workspace found above ${startDir}`);
3434
4096
  }
3435
4097
  async function clonesIn(root) {
3436
4098
  try {
3437
- const entries = await readdir2(join9(root, "org"), { withFileTypes: true });
4099
+ const entries = await readdir3(join10(root, "org"), { withFileTypes: true });
3438
4100
  return new Set(
3439
4101
  entries.filter((e) => e.isDirectory() && isWorkspaceClone(e.name)).map((e) => e.name)
3440
4102
  );
@@ -3458,15 +4120,15 @@ function reportError(e) {
3458
4120
  }
3459
4121
 
3460
4122
  // src/core/delegate.ts
3461
- import { readFile as readFile6 } from "fs/promises";
3462
- import { join as join10 } from "path";
4123
+ import { readFile as readFile7 } from "fs/promises";
4124
+ import { join as join11 } from "path";
3463
4125
  var DEFAULT_BIN = "dist/bin/rness.js";
3464
4126
  var PINNED_BIN = `node_modules/@rness/cli/${DEFAULT_BIN}`;
3465
4127
  async function readPinnedCli(rnessDir) {
3466
- const dir = join10(rnessDir, "node_modules", "@rness", "cli");
4128
+ const dir = join11(rnessDir, "node_modules", "@rness", "cli");
3467
4129
  let pkg;
3468
4130
  try {
3469
- pkg = JSON.parse(await readFile6(join10(dir, "package.json"), "utf8"));
4131
+ pkg = JSON.parse(await readFile7(join11(dir, "package.json"), "utf8"));
3470
4132
  } catch {
3471
4133
  return null;
3472
4134
  }
@@ -3486,7 +4148,7 @@ async function readPinnedCli(rnessDir) {
3486
4148
  async function pinnedBinPath(rnessDir) {
3487
4149
  const pinned = await readPinnedCli(rnessDir);
3488
4150
  if (pinned === null) return null;
3489
- return join10(pinned.dir, ...pinned.bin.split("/"));
4151
+ return join11(pinned.dir, ...pinned.bin.split("/"));
3490
4152
  }
3491
4153
  async function findDelegate(cwd, ownVersion) {
3492
4154
  let root;
@@ -3502,7 +4164,7 @@ async function findDelegate(cwd, ownVersion) {
3502
4164
  if (pinned === null) return null;
3503
4165
  const { version } = pinned;
3504
4166
  if (version === null || version === ownVersion) return null;
3505
- return { entry: join10(pinned.dir, "dist", "index.js"), version, root };
4167
+ return { entry: join11(pinned.dir, "dist", "index.js"), version, root };
3506
4168
  }
3507
4169
 
3508
4170
  // src/core/block.ts
@@ -3569,7 +4231,7 @@ function isCurrentBlock(blockLines, freshHash) {
3569
4231
  }
3570
4232
 
3571
4233
  // src/core/merge.ts
3572
- import { join as join11 } from "path";
4234
+ import { join as join12 } from "path";
3573
4235
  function findBlock(lines) {
3574
4236
  const begins = [];
3575
4237
  const ends = [];
@@ -3628,7 +4290,7 @@ function claudeMdAtRoot(existing, block) {
3628
4290
  return merged.ok ? { ...merged, changed: merged.text !== existing } : merged;
3629
4291
  }
3630
4292
  async function ensureClaudeMd(dir) {
3631
- const file = join11(dir, "CLAUDE.md");
4293
+ const file = join12(dir, "CLAUDE.md");
3632
4294
  if (await isSymlink(file)) return "unchanged";
3633
4295
  const existing = await readOrNull(file);
3634
4296
  if (existing === null) {
@@ -3643,11 +4305,11 @@ async function ensureClaudeMd(dir) {
3643
4305
  }
3644
4306
 
3645
4307
  // src/commands/sync.ts
3646
- import { join as join14 } from "path";
4308
+ import { join as join15 } from "path";
3647
4309
 
3648
4310
  // src/core/agent-targets.ts
3649
4311
  import { mkdir as mkdir2 } from "fs/promises";
3650
- import { dirname as dirname4, join as join13 } from "path";
4312
+ import { dirname as dirname4, join as join14 } from "path";
3651
4313
 
3652
4314
  // src/core/claude-skills.ts
3653
4315
  var PLUGIN = ".claude/skills/rness";
@@ -4092,8 +4754,8 @@ function describeGuarantee(g) {
4092
4754
 
4093
4755
  // src/core/pinned.ts
4094
4756
  import { execFile as execFile3 } from "child_process";
4095
- import { readFile as readFile7 } from "fs/promises";
4096
- import { basename as basename3, join as join12 } from "path";
4757
+ import { readFile as readFile8 } from "fs/promises";
4758
+ import { basename as basename3, join as join13 } from "path";
4097
4759
  import { promisify as promisify3 } from "util";
4098
4760
  var execFileP3 = promisify3(execFile3);
4099
4761
  var PACKAGE = "@rness/cli";
@@ -4118,7 +4780,7 @@ function compareVersions(a, b) {
4118
4780
  async function readPackageJson(rnessDir) {
4119
4781
  try {
4120
4782
  const parsed = JSON.parse(
4121
- await readFile7(join12(rnessDir, "package.json"), "utf8")
4783
+ await readFile8(join13(rnessDir, "package.json"), "utf8")
4122
4784
  );
4123
4785
  return parsed !== null && typeof parsed === "object" ? parsed : null;
4124
4786
  } catch {
@@ -4137,8 +4799,8 @@ async function readPin(rnessDir) {
4137
4799
  return null;
4138
4800
  }
4139
4801
  async function writePin(rnessDir, from, to) {
4140
- const file = join12(rnessDir, "package.json");
4141
- const text = await readFile7(file, "utf8");
4802
+ const file = join13(rnessDir, "package.json");
4803
+ const text = await readFile8(file, "utf8");
4142
4804
  const entry = new RegExp(
4143
4805
  `("${PACKAGE}"\\s*:\\s*")${from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(")`
4144
4806
  );
@@ -4160,12 +4822,12 @@ async function workspacePackageManager(rnessDir) {
4160
4822
  if (isPackageManager(name)) return name;
4161
4823
  }
4162
4824
  for (const [lockfile, pm] of LOCKFILES)
4163
- if (await exists(join12(rnessDir, lockfile))) return pm;
4825
+ if (await exists(join13(rnessDir, lockfile))) return pm;
4164
4826
  return "npm";
4165
4827
  }
4166
4828
  async function hasLockfile(rnessDir, pm) {
4167
4829
  for (const [lockfile, owner] of LOCKFILES)
4168
- if (owner === pm && await exists(join12(rnessDir, lockfile))) return true;
4830
+ if (owner === pm && await exists(join13(rnessDir, lockfile))) return true;
4169
4831
  return false;
4170
4832
  }
4171
4833
  async function pinDrift(rnessDir) {
@@ -4221,9 +4883,9 @@ async function ensureInstalled(rnessDir, opts = {}) {
4221
4883
  }
4222
4884
  }
4223
4885
  async function runPinnedSync(root) {
4224
- const installed = await readPinnedCli(join12(root, ".rness"));
4886
+ const installed = await readPinnedCli(join13(root, ".rness"));
4225
4887
  if (installed === null) return null;
4226
- const pinned = join12(installed.dir, ...installed.bin.split("/"));
4888
+ const pinned = join13(installed.dir, ...installed.bin.split("/"));
4227
4889
  if (!await exists(pinned))
4228
4890
  return {
4229
4891
  code: 1,
@@ -4298,7 +4960,7 @@ function syncOutcome(result, opts = {}) {
4298
4960
  };
4299
4961
  }
4300
4962
  async function syncPinned(root, shown, fallback) {
4301
- const rnessDir = join12(root, ".rness");
4963
+ const rnessDir = join13(root, ".rness");
4302
4964
  const installed = await readPinnedCli(rnessDir);
4303
4965
  if (installed === null) {
4304
4966
  if (fallback !== void 0) return fallback();
@@ -4308,7 +4970,7 @@ async function syncPinned(root, shown, fallback) {
4308
4970
  );
4309
4971
  return 1;
4310
4972
  }
4311
- const pinned = join12(installed.dir, ...installed.bin.split("/"));
4973
+ const pinned = join13(installed.dir, ...installed.bin.split("/"));
4312
4974
  if (!await exists(pinned)) {
4313
4975
  process.stderr.write(
4314
4976
  `@rness/cli in ${shown}/.rness is not installed correctly (missing ${installed.bin}); reinstall in .rness/
@@ -4348,30 +5010,30 @@ function wholeDiff(existing, content) {
4348
5010
  }
4349
5011
  async function contextOf(root) {
4350
5012
  return {
4351
- packageManager: await workspacePackageManager(join13(root, ".rness"))
5013
+ packageManager: await workspacePackageManager(join14(root, ".rness"))
4352
5014
  };
4353
5015
  }
4354
5016
  var PATHS_ONLY = { packageManager: "npm" };
4355
5017
  async function filesOf(root, manifest, targets) {
4356
5018
  const files = [];
4357
- if (!await exists(join13(root, "org"))) return files;
5019
+ if (!await exists(join14(root, "org"))) return files;
4358
5020
  const ctx = await contextOf(root);
4359
5021
  for (const target2 of targets)
4360
5022
  for (const owned of target2.files(ctx))
4361
5023
  if (owned.at === "root")
4362
5024
  files.push({
4363
5025
  ...owned,
4364
- file: join13(root, ...owned.file.split("/")),
5026
+ file: join14(root, ...owned.file.split("/")),
4365
5027
  label: owned.file
4366
5028
  });
4367
5029
  for (const repo of Object.keys(manifest.repos)) {
4368
- if (!await exists(join13(root, "org", repo))) continue;
5030
+ if (!await exists(join14(root, "org", repo))) continue;
4369
5031
  for (const target2 of targets)
4370
5032
  for (const owned of target2.files(ctx))
4371
5033
  if (owned.at === "clones")
4372
5034
  files.push({
4373
5035
  ...owned,
4374
- file: join13(root, "org", repo, ...owned.file.split("/")),
5036
+ file: join14(root, "org", repo, ...owned.file.split("/")),
4375
5037
  label: `org/${repo}/${owned.file}`
4376
5038
  });
4377
5039
  }
@@ -4695,7 +5357,7 @@ function targetsOf(manifest, root, only) {
4695
5357
  const segments = entry.path.split("/");
4696
5358
  targets.push({
4697
5359
  scope: name,
4698
- dir: join14(root, ...segments),
5360
+ dir: join15(root, ...segments),
4699
5361
  depth: segments.length,
4700
5362
  label: `${entry.path}/AGENTS.md`
4701
5363
  });
@@ -4854,7 +5516,7 @@ async function syncCommand(opts, deps = {}) {
4854
5516
  try {
4855
5517
  const notCloned = /* @__PURE__ */ new Set();
4856
5518
  for (const [name, repo] of Object.entries(manifest.repos)) {
4857
- const dir = join14(ws.root, "org", name);
5519
+ const dir = join15(ws.root, "org", name);
4858
5520
  const label = `org/${name}`;
4859
5521
  if (!await exists(dir)) {
4860
5522
  if (!toClone.has(name)) {
@@ -4906,7 +5568,7 @@ async function syncCommand(opts, deps = {}) {
4906
5568
  lines.push(
4907
5569
  `not in rness.json: ${undeclared.join(", ")} (${rnessCommand()} add <name> declares it)`
4908
5570
  );
4909
- if (!await exists(join14(ws.root, "org"))) {
5571
+ if (!await exists(join15(ws.root, "org"))) {
4910
5572
  lines.push(["skipped", "blocks (no org/ directory here)"]);
4911
5573
  } else {
4912
5574
  let differences = 0;
@@ -4918,7 +5580,7 @@ async function syncCommand(opts, deps = {}) {
4918
5580
  continue;
4919
5581
  }
4920
5582
  const [agentsLinked, claudeLinked] = await Promise.all(
4921
- BLOCK_FILES.map((f) => isSymlink(join14(t.dir, f)))
5583
+ BLOCK_FILES.map((f) => isSymlink(join15(t.dir, f)))
4922
5584
  );
4923
5585
  const atRoot = t.scope === null;
4924
5586
  if (!atRoot && (agentsLinked === true || claudeLinked === true)) {
@@ -4941,7 +5603,7 @@ async function syncCommand(opts, deps = {}) {
4941
5603
  `${t.label}: block is ${Math.round(block.bytes / 1024)} KB (over 32 KB)`
4942
5604
  );
4943
5605
  }
4944
- const agents = agentsLinked === true ? "skipped" : await syncBlockFile(join14(t.dir, "AGENTS.md"), block, check);
5606
+ const agents = agentsLinked === true ? "skipped" : await syncBlockFile(join15(t.dir, "AGENTS.md"), block, check);
4945
5607
  if (typeof agents === "object") {
4946
5608
  problems.push(`${t.label}: ${agents.error}`);
4947
5609
  continue;
@@ -4956,7 +5618,7 @@ async function syncCommand(opts, deps = {}) {
4956
5618
  }
4957
5619
  if (claudeLinked === true) continue;
4958
5620
  const claude = await syncBlockFile(
4959
- join14(t.dir, "CLAUDE.md"),
5621
+ join15(t.dir, "CLAUDE.md"),
4960
5622
  block,
4961
5623
  check,
4962
5624
  true
@@ -5013,7 +5675,7 @@ async function syncCommand(opts, deps = {}) {
5013
5675
  }
5014
5676
 
5015
5677
  // src/commands/add.ts
5016
- import { basename as basename4, join as join15 } from "path";
5678
+ import { basename as basename4, join as join16 } from "path";
5017
5679
  function printDeclared(ui, result, already) {
5018
5680
  for (const name of result.declared) {
5019
5681
  if (already.has(name)) continue;
@@ -5106,7 +5768,7 @@ async function addCommand(spec, opts, deps = {}) {
5106
5768
  }
5107
5769
  parsed = parseRepoSpec(spec, org, host);
5108
5770
  const scopes = (opts.scopes ?? "").split(",").map((s) => s.trim()).filter((s) => s !== "");
5109
- const dir = join15(ws.root, "org", parsed.name);
5771
+ const dir = join16(ws.root, "org", parsed.name);
5110
5772
  const present = await exists(dir);
5111
5773
  if (interactive) {
5112
5774
  const { confirm, isCancel } = await terminal.prompts();
@@ -5234,7 +5896,7 @@ async function contextCommand(opts) {
5234
5896
 
5235
5897
  // src/core/scaffold.ts
5236
5898
  import { statSync } from "fs";
5237
- import { dirname as dirname5, join as join16 } from "path";
5899
+ import { dirname as dirname5, join as join17 } from "path";
5238
5900
  import { fileURLToPath } from "url";
5239
5901
  var SCAFFOLD_FILES = [
5240
5902
  "AGENTS.md",
@@ -5258,9 +5920,9 @@ var SCAFFOLD_FILES = [
5258
5920
  function scaffoldDir() {
5259
5921
  let dir = dirname5(fileURLToPath(import.meta.url));
5260
5922
  for (let i = 0; i < 4; i += 1) {
5261
- const candidate = join16(dir, "scaffold");
5923
+ const candidate = join17(dir, "scaffold");
5262
5924
  try {
5263
- if (statSync(join16(candidate, "rness.json")).isFile()) return candidate;
5925
+ if (statSync(join17(candidate, "rness.json")).isFile()) return candidate;
5264
5926
  } catch {
5265
5927
  }
5266
5928
  dir = dirname5(dir);
@@ -5269,15 +5931,15 @@ function scaffoldDir() {
5269
5931
  }
5270
5932
 
5271
5933
  // src/core/scaffold-copy.ts
5272
- import { chmod as chmod2, mkdir as mkdir3, readFile as readFile8, stat as stat2, writeFile as writeFile2 } from "fs/promises";
5273
- import { dirname as dirname6, join as join17 } from "path";
5934
+ import { chmod as chmod2, mkdir as mkdir3, readFile as readFile9, stat as stat2, writeFile as writeFile2 } from "fs/promises";
5935
+ import { dirname as dirname6, join as join18 } from "path";
5274
5936
  var TOKENISED = /* @__PURE__ */ new Set(["package.json"]);
5275
5937
  async function renderScaffold(source, tokens) {
5276
5938
  const files = [];
5277
5939
  for (const rel of SCAFFOLD_FILES) {
5278
5940
  if (rel === "rness.json") continue;
5279
- const from = join17(source, ...rel.split("/"));
5280
- let content = await readFile8(from, "utf8");
5941
+ const from = join18(source, ...rel.split("/"));
5942
+ let content = await readFile9(from, "utf8");
5281
5943
  if (TOKENISED.has(rel)) {
5282
5944
  content = content.replaceAll("__RNESS_VERSION__", tokens.version).replaceAll("__RNESS_PM__", tokens.packageManager);
5283
5945
  }
@@ -5291,7 +5953,7 @@ async function renderScaffold(source, tokens) {
5291
5953
  }
5292
5954
  async function copyScaffold(dest, tokens) {
5293
5955
  for (const file of await renderScaffold(scaffoldDir(), tokens)) {
5294
- const to = join17(dest, ...file.path.split("/"));
5956
+ const to = join18(dest, ...file.path.split("/"));
5295
5957
  await mkdir3(dirname6(to), { recursive: true });
5296
5958
  await writeFile2(to, file.content, "utf8");
5297
5959
  if (file.executable) await chmod2(to, 493);
@@ -5400,7 +6062,7 @@ async function loginCommand(opts, deps = {}) {
5400
6062
  ui.error(unsupportedProviderMessage(manifest.provider));
5401
6063
  return 1;
5402
6064
  }
5403
- const needsProject = opts.project === true || manifest?.pulse != null;
6065
+ const needsProject = opts.project === true || manifest?.projects != null;
5404
6066
  const bin = deps.bin ?? await realpath(process.argv[1] ?? "").catch(() => "");
5405
6067
  const env = envToken();
5406
6068
  if (env !== null)
@@ -5471,7 +6133,7 @@ function revokeUrl() {
5471
6133
  // src/commands/create.ts
5472
6134
  import { mkdir as mkdir6, mkdtemp as mkdtemp2, rename as rename2, rm as rm6 } from "fs/promises";
5473
6135
  import { tmpdir as tmpdir2 } from "os";
5474
- import { join as join21, relative as relative4, resolve as resolve5 } from "path";
6136
+ import { join as join22, relative as relative4, resolve as resolve5 } from "path";
5475
6137
 
5476
6138
  // src/core/ask-provider.ts
5477
6139
  var PROVIDER_QUESTION = "Where does your organization live?";
@@ -5487,14 +6149,14 @@ async function askProvider(p) {
5487
6149
  }
5488
6150
 
5489
6151
  // src/core/new-workspace.ts
5490
- import { mkdir as mkdir4, readdir as readdir3, rm as rm5, stat as stat3 } from "fs/promises";
5491
- import { dirname as dirname8, join as join19, relative as relative2 } from "path";
6152
+ import { mkdir as mkdir4, readdir as readdir4, rm as rm5, stat as stat3 } from "fs/promises";
6153
+ import { dirname as dirname8, join as join20, relative as relative2 } from "path";
5492
6154
 
5493
6155
  // src/core/scaffold-git.ts
5494
6156
  import { execFile as execFile5 } from "child_process";
5495
6157
  import { mkdtemp, realpath as realpath2, rm as rm4, writeFile as writeFile3 } from "fs/promises";
5496
6158
  import { tmpdir } from "os";
5497
- import { dirname as dirname7, join as join18, resolve as resolve3 } from "path";
6159
+ import { dirname as dirname7, join as join19, resolve as resolve3 } from "path";
5498
6160
  import { promisify as promisify5 } from "util";
5499
6161
  var execFileP5 = promisify5(execFile5);
5500
6162
  var CREATE_SUBJECT = "chore: rness workspace context";
@@ -5582,8 +6244,8 @@ async function findScaffoldBase(rnessDir) {
5582
6244
  };
5583
6245
  }
5584
6246
  async function buildScaffoldCommit(rnessDir, base, files, version) {
5585
- const scratch = await mkdtemp(join18(tmpdir(), "rness-scaffold-"));
5586
- const env = { GIT_INDEX_FILE: join18(scratch, "index") };
6247
+ const scratch = await mkdtemp(join19(tmpdir(), "rness-scaffold-"));
6248
+ const env = { GIT_INDEX_FILE: join19(scratch, "index") };
5587
6249
  try {
5588
6250
  await must(
5589
6251
  rnessDir,
@@ -5591,7 +6253,7 @@ async function buildScaffoldCommit(rnessDir, base, files, version) {
5591
6253
  env
5592
6254
  );
5593
6255
  for (const [i, file] of files.entries()) {
5594
- const tmp = join18(scratch, `blob-${i}`);
6256
+ const tmp = join19(scratch, `blob-${i}`);
5595
6257
  await writeFile3(tmp, file.content, "utf8");
5596
6258
  const blob = await must(rnessDir, [
5597
6259
  "hash-object",
@@ -5691,7 +6353,7 @@ async function withoutRedundantKeeps(rnessDir, files) {
5691
6353
 
5692
6354
  // src/core/new-workspace.ts
5693
6355
  async function isEmptyDir(dir) {
5694
- return (await readdir3(dir)).length === 0;
6356
+ return (await readdir4(dir)).length === 0;
5695
6357
  }
5696
6358
  async function insideWorkspace(dir) {
5697
6359
  try {
@@ -5706,8 +6368,8 @@ async function targetProblem(cwd, root, pm) {
5706
6368
  if (await insideWorkspace(cwd) || await insideWorkspace(dirname8(root)))
5707
6369
  return "already inside an rness workspace; create runs from outside one";
5708
6370
  if (!await exists(root)) return null;
5709
- if (await exists(join19(root, ".rness", "rness.json"))) {
5710
- if (!await exists(join19(root, ".rness", "node_modules")))
6371
+ if (await exists(join20(root, ".rness", "rness.json"))) {
6372
+ if (!await exists(join20(root, ".rness", "node_modules")))
5711
6373
  return `${shown} holds an rness workspace whose dependencies are not installed; cd ${shown}/.rness && ${pm} install, then ${rnessCommand()} sync`;
5712
6374
  return `${shown} already holds an rness workspace (.rness/); run ${rnessCommand()} add inside it to add repositories`;
5713
6375
  }
@@ -5730,7 +6392,7 @@ async function installContext(input) {
5730
6392
  }
5731
6393
  async function buildContext(input) {
5732
6394
  const { ui, root, shown, pm } = input;
5733
- const rnessDir = join19(root, ".rness");
6395
+ const rnessDir = join20(root, ".rness");
5734
6396
  const pmVersion = await packageManagerVersion(pm);
5735
6397
  await mkdir4(root, { recursive: true });
5736
6398
  try {
@@ -5802,7 +6464,7 @@ async function syncBlocks(ui, root, shown, inPlace, sshWorks) {
5802
6464
 
5803
6465
  // src/commands/create-blank.ts
5804
6466
  import { mkdir as mkdir5 } from "fs/promises";
5805
- import { join as join20, relative as relative3, resolve as resolve4 } from "path";
6467
+ import { join as join21, relative as relative3, resolve as resolve4 } from "path";
5806
6468
  var WORKSPACE_NAME = /^(?!\.{1,2}$)[A-Za-z0-9._][A-Za-z0-9._-]*$/;
5807
6469
  function workspaceNameError(name) {
5808
6470
  return `workspace name "${name}" may only contain letters, digits, '.', '_' and '-', not starting with '-'`;
@@ -5875,7 +6537,7 @@ async function createBlank(input) {
5875
6537
  provider: null,
5876
6538
  org: null,
5877
6539
  agents,
5878
- pulse: null,
6540
+ projects: null,
5879
6541
  repos: {},
5880
6542
  scopes: {}
5881
6543
  },
@@ -5884,8 +6546,8 @@ async function createBlank(input) {
5884
6546
  kind: "blank workspace"
5885
6547
  });
5886
6548
  if (!built) return 1;
5887
- await mkdir5(join20(root, "org"), { recursive: true });
5888
- await commitContext({ ui, rnessDir: join20(root, ".rness"), shown });
6549
+ await mkdir5(join21(root, "org"), { recursive: true });
6550
+ await commitContext({ ui, rnessDir: join21(root, ".rness"), shown });
5889
6551
  const code = await syncBlocks(
5890
6552
  ui,
5891
6553
  root,
@@ -6045,7 +6707,7 @@ async function cloneSelection(input) {
6045
6707
  { doing: `cloning org/${repo}`, git: true },
6046
6708
  () => clone(
6047
6709
  entry.url,
6048
- join21(input.root, "org", repo),
6710
+ join22(input.root, "org", repo),
6049
6711
  input.provider.credentialsFor(entry.url)
6050
6712
  ),
6051
6713
  () => ["cloned", `org/${repo}`]
@@ -6390,13 +7052,13 @@ async function createCommand(opts, deps = {}) {
6390
7052
  provider: chosenProvider,
6391
7053
  org,
6392
7054
  agents: null,
6393
- pulse: null,
7055
+ projects: null,
6394
7056
  repos: {},
6395
7057
  scopes: {}
6396
7058
  };
6397
7059
  const staged = async () => {
6398
- staging ??= await mkdtemp2(join21(tmpdir2(), "rness-join-"));
6399
- return join21(staging, ".rness");
7060
+ staging ??= await mkdtemp2(join22(tmpdir2(), "rness-join-"));
7061
+ return join22(staging, ".rness");
6400
7062
  };
6401
7063
  if (joining) {
6402
7064
  const stagedDir = await staged();
@@ -6458,7 +7120,7 @@ async function createCommand(opts, deps = {}) {
6458
7120
  });
6459
7121
  if (p.isCancel(ok) || ok !== true) return cancelled(ui);
6460
7122
  }
6461
- const rnessDir = join21(root, ".rness");
7123
+ const rnessDir = join22(root, ".rness");
6462
7124
  const install = () => installContext({ ui, pm, rnessDir, skip: opts.skipInstall === true });
6463
7125
  if (joining) {
6464
7126
  await mkdir6(root, { recursive: true });
@@ -6471,7 +7133,7 @@ async function createCommand(opts, deps = {}) {
6471
7133
  ui.line("cloned", `${shown}/.rness (joined ${org})`);
6472
7134
  if (opts.pm === void 0) pm = await workspacePackageManager(rnessDir);
6473
7135
  await install();
6474
- await mkdir6(join21(root, "org"), { recursive: true });
7136
+ await mkdir6(join22(root, "org"), { recursive: true });
6475
7137
  const failures2 = await cloneSelection({
6476
7138
  root,
6477
7139
  rnessDir,
@@ -6524,7 +7186,7 @@ ${next2.map((l) => ` ${l}`).join("\n")}
6524
7186
  kind: "new workspace"
6525
7187
  });
6526
7188
  if (!built) return 1;
6527
- await mkdir6(join21(root, "org"), { recursive: true });
7189
+ await mkdir6(join22(root, "org"), { recursive: true });
6528
7190
  const failures = await cloneSelection({
6529
7191
  root,
6530
7192
  rnessDir,
@@ -6596,7 +7258,7 @@ ${next2.map((l) => ` ${l}`).join("\n")}
6596
7258
  }
6597
7259
 
6598
7260
  // src/core/blocks.ts
6599
- import { join as join22 } from "path";
7261
+ import { join as join23 } from "path";
6600
7262
  var PROBLEM = /* @__PURE__ */ new Set(["stale", "malformed"]);
6601
7263
  async function checkBlocks({
6602
7264
  root,
@@ -6613,7 +7275,7 @@ async function checkBlocks({
6613
7275
  warnings: checks.filter((c) => !PROBLEM.has(c.status) && c.message !== null).map((c) => c.message),
6614
7276
  checks
6615
7277
  });
6616
- if (!await exists(join22(root, "org"))) return derive();
7278
+ if (!await exists(join23(root, "org"))) return derive();
6617
7279
  const targets = [
6618
7280
  { scope: null, dir: root, depth: 0, file: "AGENTS.md", label: "AGENTS.md" },
6619
7281
  { scope: null, dir: root, depth: 0, file: "CLAUDE.md", label: "CLAUDE.md" }
@@ -6622,7 +7284,7 @@ async function checkBlocks({
6622
7284
  const segments = entry.path.split("/");
6623
7285
  targets.push({
6624
7286
  scope: name,
6625
- dir: join22(root, ...segments),
7287
+ dir: join23(root, ...segments),
6626
7288
  depth: segments.length,
6627
7289
  file: "AGENTS.md",
6628
7290
  label: `${entry.path}/AGENTS.md`
@@ -6637,7 +7299,7 @@ async function checkBlocks({
6637
7299
  );
6638
7300
  continue;
6639
7301
  }
6640
- const text = await readOrNull(join22(t.dir, t.file));
7302
+ const text = await readOrNull(join23(t.dir, t.file));
6641
7303
  const lines = text === null ? [] : text.split(/\r?\n/);
6642
7304
  const found = findBlock(lines);
6643
7305
  if (found.kind === "none") {
@@ -6706,9 +7368,9 @@ async function logoutCommand() {
6706
7368
 
6707
7369
  // src/commands/upgrade.ts
6708
7370
  import { execFile as execFile6 } from "child_process";
6709
- import { mkdtemp as mkdtemp3, readFile as readFile9, rm as rm7 } from "fs/promises";
7371
+ import { mkdtemp as mkdtemp3, readFile as readFile10, rm as rm7 } from "fs/promises";
6710
7372
  import { tmpdir as tmpdir3 } from "os";
6711
- import { basename as basename5, join as join23, relative as relative5 } from "path";
7373
+ import { basename as basename5, join as join24, relative as relative5 } from "path";
6712
7374
  import { promisify as promisify6 } from "util";
6713
7375
  var execFileP6 = promisify6(execFile6);
6714
7376
  var NOTES = "https://github.com/rness-dev/rness/tree/main/packages/cli#readme";
@@ -6728,10 +7390,10 @@ async function latestVersion() {
6728
7390
  async function scaffoldFor(version, rnessDir) {
6729
7391
  if (version === VERSION) return { dir: scaffoldDir() };
6730
7392
  const installed = await readPinnedCli(rnessDir);
6731
- const inside2 = installed === null ? null : join23(installed.dir, "scaffold");
6732
- if (installed?.version === version && inside2 !== null && await exists(join23(inside2, "package.json")))
7393
+ const inside2 = installed === null ? null : join24(installed.dir, "scaffold");
7394
+ if (installed?.version === version && inside2 !== null && await exists(join24(inside2, "package.json")))
6733
7395
  return { dir: inside2 };
6734
- const scratch = await mkdtemp3(join23(tmpdir3(), "rness-pack-"));
7396
+ const scratch = await mkdtemp3(join24(tmpdir3(), "rness-pack-"));
6735
7397
  const cleanup = () => rm7(scratch, { recursive: true, force: true });
6736
7398
  try {
6737
7399
  const { stdout } = await execFileP6(
@@ -6746,8 +7408,8 @@ async function scaffoldFor(version, rnessDir) {
6746
7408
  { timeout: 6e4 }
6747
7409
  );
6748
7410
  const tarball = stdout.trim().split("\n").at(-1) ?? "";
6749
- await execFileP6("tar", ["-xzf", join23(scratch, tarball), "-C", scratch]);
6750
- return { dir: join23(scratch, "package", "scaffold"), cleanup };
7411
+ await execFileP6("tar", ["-xzf", join24(scratch, tarball), "-C", scratch]);
7412
+ return { dir: join24(scratch, "package", "scaffold"), cleanup };
6751
7413
  } catch (e) {
6752
7414
  await cleanup();
6753
7415
  throw new Error(
@@ -6759,7 +7421,7 @@ async function scaffoldFor(version, rnessDir) {
6759
7421
  async function declaredManager(rnessDir, pm) {
6760
7422
  try {
6761
7423
  const pkg = JSON.parse(
6762
- await readFile9(join23(rnessDir, "package.json"), "utf8")
7424
+ await readFile10(join24(rnessDir, "package.json"), "utf8")
6763
7425
  );
6764
7426
  if (typeof pkg.packageManager === "string") return pkg.packageManager;
6765
7427
  } catch {
@@ -6769,7 +7431,7 @@ async function declaredManager(rnessDir, pm) {
6769
7431
  async function clonesToCommit(root, cwd, manifest, files) {
6770
7432
  const clones = [];
6771
7433
  for (const repo of Object.keys(manifest.repos)) {
6772
- const dir = join23(root, "org", repo);
7434
+ const dir = join24(root, "org", repo);
6773
7435
  if (!await exists(dir)) continue;
6774
7436
  try {
6775
7437
  const paths = await changedPaths(dir, files);
@@ -6787,7 +7449,7 @@ async function filesTheSyncWrites(root, rnessDir, manifest) {
6787
7449
  try {
6788
7450
  const { stdout } = await execFileP6(
6789
7451
  process.execPath,
6790
- [join23(pinned.dir, pinned.bin), "written-files", "--cwd", root],
7452
+ [join24(pinned.dir, pinned.bin), "written-files", "--cwd", root],
6791
7453
  {
6792
7454
  cwd: root,
6793
7455
  env: { ...process.env, NO_COLOR: "1", RNESS_NO_INSTALL: "1" },
@@ -6983,9 +7645,9 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
6983
7645
  await source.cleanup?.();
6984
7646
  }
6985
7647
  }
6986
- const packageFile = join23(ws.rnessDir, "package.json");
7648
+ const packageFile = join24(ws.rnessDir, "package.json");
6987
7649
  const restore = /* @__PURE__ */ new Map([
6988
- [packageFile, await readFile9(packageFile, "utf8")]
7650
+ [packageFile, await readFile10(packageFile, "utf8")]
6989
7651
  ]);
6990
7652
  const moved = await readPin(ws.rnessDir);
6991
7653
  if (moved !== null && moved.spec !== target2)
@@ -7179,8 +7841,8 @@ import { realpath as realpath4 } from "fs/promises";
7179
7841
  import { isAbsolute as isAbsolute2, posix as posix8, relative as relative7, resolve as resolve7, sep as sep5 } from "path";
7180
7842
 
7181
7843
  // src/mcp/tools.ts
7182
- import { readFile as readFile10, realpath as realpath3, stat as stat4 } from "fs/promises";
7183
- import { isAbsolute, join as join24, posix as posix7, relative as relative6, resolve as resolve6, sep as sep4 } from "path";
7844
+ import { readFile as readFile11, realpath as realpath3, stat as stat4 } from "fs/promises";
7845
+ import { isAbsolute, join as join25, posix as posix7, relative as relative6, resolve as resolve6, sep as sep4 } from "path";
7184
7846
  var LABELS = {
7185
7847
  standards: "Standards",
7186
7848
  adr: "Decisions",
@@ -7209,7 +7871,7 @@ async function documents(rnessDir, only) {
7209
7871
  const docs = [];
7210
7872
  for (const collection of COLLECTIONS) {
7211
7873
  if (only !== void 0 && collection !== only) continue;
7212
- for (const item of await collectMarkdown(join24(rnessDir, collection))) {
7874
+ for (const item of await collectMarkdown(join25(rnessDir, collection))) {
7213
7875
  const path = `${collection}/${item.rel}`;
7214
7876
  if (path === TEMPLATE2) continue;
7215
7877
  const status2 = item.fields?.["status"];
@@ -7331,7 +7993,7 @@ async function read(cwd, args) {
7331
7993
  return refuse(
7332
7994
  `${path}: ${info.size} bytes, over the 256 KiB rness_read returns`
7333
7995
  );
7334
- return { text: await readFile10(real, "utf8") };
7996
+ return { text: await readFile11(real, "utf8") };
7335
7997
  }
7336
7998
  async function search(cwd, args) {
7337
7999
  const { ws } = await workspaceAt(cwd);
@@ -7439,8 +8101,8 @@ function mcpTools(cwd) {
7439
8101
 
7440
8102
  // src/pulse/detached.ts
7441
8103
  import { spawn as nodeSpawn } from "child_process";
7442
- import { mkdir as mkdir7, open as open2, readFile as readFile11, rm as rm8, stat as stat5, writeFile as writeFile4 } from "fs/promises";
7443
- import { join as join25 } from "path";
8104
+ import { mkdir as mkdir7, open as open2, readFile as readFile12, rm as rm8, stat as stat5, writeFile as writeFile4 } from "fs/promises";
8105
+ import { join as join26 } from "path";
7444
8106
  import { setTimeout as delay2 } from "timers/promises";
7445
8107
  var detached = (args, cwd) => {
7446
8108
  const bin = process.argv[1];
@@ -7454,7 +8116,7 @@ var detached = (args, cwd) => {
7454
8116
  child.on("error", () => void 0);
7455
8117
  child.unref();
7456
8118
  };
7457
- var failureFile = () => join25(configDir(), "pulse.json");
8119
+ var failureFile = () => join26(configDir(), "pulse.json");
7458
8120
  async function recordFailure(reason3) {
7459
8121
  try {
7460
8122
  await mkdir7(configDir(), { recursive: true, mode: 448 });
@@ -7468,7 +8130,7 @@ async function recordFailure(reason3) {
7468
8130
  }
7469
8131
  async function takeFailure() {
7470
8132
  try {
7471
- const data = JSON.parse(await readFile11(failureFile(), "utf8"));
8133
+ const data = JSON.parse(await readFile12(failureFile(), "utf8"));
7472
8134
  await rm8(failureFile(), { force: true });
7473
8135
  const reason3 = data !== null && typeof data === "object" ? data["reason"] : void 0;
7474
8136
  return typeof reason3 === "string" && reason3 !== "" ? reason3 : null;
@@ -7479,7 +8141,7 @@ async function takeFailure() {
7479
8141
  var PULSE_LOCK_STALE_MS = 15 * 6e4;
7480
8142
  async function oneSyncAtATime(key, work, options = {}) {
7481
8143
  await mkdir7(configDir(), { recursive: true, mode: 448 });
7482
- const file = join25(configDir(), `pulse-${key}.lock`);
8144
+ const file = join26(configDir(), `pulse-${key}.lock`);
7483
8145
  for (; ; ) {
7484
8146
  try {
7485
8147
  const handle = await open2(file, "wx");
@@ -7565,7 +8227,7 @@ function sessionOf(input) {
7565
8227
  }
7566
8228
  async function hasPulse(ws) {
7567
8229
  try {
7568
- return (await loadManifest(ws.rnessDir)).pulse !== null;
8230
+ return (await loadManifest(ws.rnessDir)).projects !== null;
7569
8231
  } catch {
7570
8232
  return false;
7571
8233
  }
@@ -7875,12 +8537,12 @@ async function mcpCommand(opts, io = {
7875
8537
  }
7876
8538
 
7877
8539
  // src/commands/pulse.ts
7878
- import { readFile as readFile13, readdir as readdir4 } from "fs/promises";
7879
- import { join as join26 } from "path";
8540
+ import { readFile as readFile14, readdir as readdir5 } from "fs/promises";
8541
+ import { join as join27 } from "path";
7880
8542
  import { setTimeout as delay3 } from "timers/promises";
7881
8543
 
7882
8544
  // src/pulse/opened.ts
7883
- import { mkdir as mkdir8, readFile as readFile12, rename as rename3, rm as rm9, writeFile as writeFile5 } from "fs/promises";
8545
+ import { mkdir as mkdir8, readFile as readFile13, rename as rename3, rm as rm9, writeFile as writeFile5 } from "fs/promises";
7884
8546
  import { dirname as dirname9 } from "path";
7885
8547
  var NAME2 = "rness/opened";
7886
8548
  function openedFile(rnessDir) {
@@ -7892,7 +8554,7 @@ async function readOpened(file) {
7892
8554
  if (file === null) return opened;
7893
8555
  let data;
7894
8556
  try {
7895
- data = JSON.parse(await readFile12(file, "utf8"));
8557
+ data = JSON.parse(await readFile13(file, "utf8"));
7896
8558
  } catch {
7897
8559
  return opened;
7898
8560
  }
@@ -7959,16 +8621,75 @@ async function missingAccess(provider) {
7959
8621
  const scopes = await fromGithub(provider.scopes());
7960
8622
  return scopes?.includes(PROJECT_SCOPE) === true ? null : NEEDS_SCOPE;
7961
8623
  }
7962
- async function declaredBoard(c) {
7963
- if (c.manifest.pulse === null)
8624
+ async function declaredBoards(c) {
8625
+ const projects = c.manifest.projects;
8626
+ if (projects === null)
7964
8627
  throw new Error("no pulse declared \u2014 rness pulse create");
7965
8628
  const missing = await missingAccess(c.provider);
7966
8629
  if (missing !== null) throw new Error(missing);
7967
- const number = c.manifest.pulse.project;
7968
- const board = await fromGithub(c.provider.board(c.org, number));
7969
- if (board === null)
7970
- throw new Error(`GitHub has no project ${number} in ${c.org}`);
7971
- return board;
8630
+ const names = Object.keys(projects).sort(
8631
+ (a, b) => Number(a === PULSE) - Number(b === PULSE)
8632
+ );
8633
+ const declared2 = [];
8634
+ for (const name of names) {
8635
+ const number = projects[name];
8636
+ const board = await fromGithub(c.provider.board(c.org, number));
8637
+ if (board === null)
8638
+ throw new Error(`GitHub has no project ${number} in ${c.org}`);
8639
+ declared2.push({ name, board });
8640
+ }
8641
+ return declared2;
8642
+ }
8643
+ var labelOf = (name) => `${name.charAt(0).toUpperCase()}${name.slice(1)}`;
8644
+ async function directoriesOf(dir) {
8645
+ try {
8646
+ return (await readdir5(dir, { withFileTypes: true })).filter(
8647
+ (e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "updates"
8648
+ ).map((e) => e.name).sort();
8649
+ } catch {
8650
+ return [];
8651
+ }
8652
+ }
8653
+ async function scopeOf(c, name, tabs, owners) {
8654
+ const within = (collection) => (path) => path.startsWith(`${collection}/`);
8655
+ if (name === PULSE) {
8656
+ const shown = tabs.filter((t) => !owners.includes(t.name));
8657
+ const elsewhere = (path) => owners.some((o) => within(o)(path));
8658
+ return {
8659
+ label: "Agent Pulse",
8660
+ want: desiredOf(shown, c.org),
8661
+ layout: layoutOf(shown),
8662
+ elsewhere,
8663
+ mine: (path) => !elsewhere(path),
8664
+ own: null
8665
+ };
8666
+ }
8667
+ const tab = tabs.find((t) => t.name === name) ?? {
8668
+ name,
8669
+ label: labelOf(name),
8670
+ rows: []
8671
+ };
8672
+ const dir = join27(c.rnessDir, name);
8673
+ const shape = await readShape(c.rnessDir, name);
8674
+ const fronts = new Map(
8675
+ (await collectMarkdown(dir)).map((item) => [
8676
+ `${name}/${item.rel}`,
8677
+ item.fields ?? {}
8678
+ ])
8679
+ );
8680
+ const want = desiredOf([tab], c.org, {
8681
+ shape,
8682
+ fronts,
8683
+ directories: await directoriesOf(dir)
8684
+ });
8685
+ return {
8686
+ label: tab.label,
8687
+ want,
8688
+ layout: layoutOf([tab], { shape, want }),
8689
+ elsewhere: () => false,
8690
+ mine: within(name),
8691
+ own: { shape, updates: await readUpdates(c.rnessDir, name) }
8692
+ };
7972
8693
  }
7973
8694
  var plural = (n) => `${n} item${n === 1 ? "" : "s"}`;
7974
8695
  var eachAdded = (ui, added) => {
@@ -7976,7 +8697,7 @@ var eachAdded = (ui, added) => {
7976
8697
  if (a.startsWith("renamed ")) ui.line("renamed", a.slice("renamed ".length));
7977
8698
  else ui.line("added", a);
7978
8699
  };
7979
- var createdByKind = (ui, added) => {
8700
+ var createdByKind = (ui, added, project) => {
7980
8701
  for (const kind of ["field", "option", "view"]) {
7981
8702
  const names = added.filter((a) => a.startsWith(`${kind} `)).map((a) => a.slice(kind.length + 1));
7982
8703
  if (names.length > 0) ui.line("created", `${kind}s ${names.join(", ")}`);
@@ -7984,14 +8705,18 @@ var createdByKind = (ui, added) => {
7984
8705
  for (const a of added) {
7985
8706
  if (a.startsWith("label ")) ui.line("created", a);
7986
8707
  else if (a.startsWith("link "))
7987
- ui.line("linked", `Agent Pulse to ${a.slice("link ".length)}`);
8708
+ ui.line("linked", `${project} to ${a.slice("link ".length)}`);
7988
8709
  }
7989
8710
  };
8711
+ async function working(ui, doing, count3, work) {
8712
+ if (count3 === 0) return work();
8713
+ await ui.step({ doing, quiet: true }, work, () => null);
8714
+ }
7990
8715
  async function filesOf2(rnessDir) {
7991
8716
  const files = /* @__PURE__ */ new Set();
7992
8717
  const walk = async (rel) => {
7993
- const dir = rel === "" ? rnessDir : join26(rnessDir, ...rel.split("/"));
7994
- for (const e of await readdir4(dir, { withFileTypes: true })) {
8718
+ const dir = rel === "" ? rnessDir : join27(rnessDir, ...rel.split("/"));
8719
+ for (const e of await readdir5(dir, { withFileTypes: true })) {
7995
8720
  const path = rel === "" ? e.name : `${rel}/${e.name}`;
7996
8721
  if (e.isDirectory()) {
7997
8722
  if (e.name !== ".git" && e.name !== "node_modules") await walk(path);
@@ -8002,7 +8727,7 @@ async function filesOf2(rnessDir) {
8002
8727
  return files;
8003
8728
  }
8004
8729
  var count2 = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
8005
- function sayDone(ui, org, documents2, steps, writes, adopted) {
8730
+ function sayDone(ui, org, documents2, steps, writes, adopted, on) {
8006
8731
  const n = (kind) => steps.filter((s) => s.kind === kind).length - (kind === "create" ? adopted : 0);
8007
8732
  const reopened = steps.filter((s) => s.kind === "update" && s.reopen).length;
8008
8733
  if (n("create") > 0) ui.line("created", count2(n("create"), "issue"));
@@ -8017,6 +8742,8 @@ function sayDone(ui, org, documents2, steps, writes, adopted) {
8017
8742
  `${count2(n("convert"), "draft")} into issues of ${org}/.rness`
8018
8743
  );
8019
8744
  if (reopened > 0) ui.line("reopened", count2(reopened, "issue"));
8745
+ if (n("remove") > 0)
8746
+ ui.line("moved", `${plural(n("remove"))} to their collection's project`);
8020
8747
  const written = new Set(writes.map((w) => w.id));
8021
8748
  const bodyOnly = steps.filter(
8022
8749
  (s) => s.kind === "unchanged" && written.has(s.id)
@@ -8034,25 +8761,27 @@ function sayDone(ui, org, documents2, steps, writes, adopted) {
8034
8761
  ].filter(([, k]) => k > 0).map(([word, k]) => `${k} ${word}`);
8035
8762
  ui.line(
8036
8763
  "synced",
8037
- `${plural(documents2)}${parts.length > 0 ? `: ${parts.join(", ")}` : ""}`
8764
+ `${plural(documents2)}${on === void 0 ? "" : ` on ${on}`}${parts.length > 0 ? `: ${parts.join(", ")}` : ""}`
8038
8765
  );
8039
8766
  }
8040
- async function syncBoard(c, board, ui, sayLayout) {
8767
+ async function syncBoard(c, board, ui, sayLayout, scope) {
8041
8768
  await fromGithub(c.provider.checkIssues(c.org));
8042
- const tabs = await statusTabs(c.rnessDir);
8043
- const want = desiredOf(tabs, c.org);
8769
+ const { want } = scope;
8044
8770
  sayLayout(
8045
8771
  ui,
8046
- await fromGithub(c.provider.ensureLayout(board, layoutOf(tabs)))
8772
+ await fromGithub(c.provider.ensureLayout(board, scope.layout)),
8773
+ scope.label
8047
8774
  );
8048
8775
  const have = await fromGithub(c.provider.items(board));
8049
8776
  const record = await openedFile(c.rnessDir);
8050
- const opened = await readOpened(record);
8777
+ const all = await readOpened(record);
8778
+ const opened = new Map([...all].filter(([, path]) => scope.mine(path)));
8779
+ const others = [...all].filter(([, path]) => !scope.mine(path));
8051
8780
  const seen = await seenPaths(c.rnessDir, [
8052
8781
  ...unwantedPaths(want, have),
8053
8782
  ...opened.values()
8054
8783
  ]);
8055
- const steps = planSync(want, have, seen, opened);
8784
+ const steps = planSync(want, have, seen, opened, scope.elsewhere);
8056
8785
  const changes = steps.filter((s) => s.kind !== "unchanged").length;
8057
8786
  let writes = [];
8058
8787
  let made = 0;
@@ -8062,21 +8791,31 @@ async function syncBoard(c, board, ui, sayLayout) {
8062
8791
  const created = /* @__PURE__ */ new Map();
8063
8792
  const closed = /* @__PURE__ */ new Set();
8064
8793
  try {
8065
- for (const step2 of steps) {
8066
- const p = await fromGithub(c.provider.apply(board, step2));
8067
- if (p !== null && (step2.kind === "create" || step2.kind === "convert"))
8068
- placed.set(step2.want.path, p);
8069
- if (p?.adopted === true) adopted++;
8070
- else if (p !== null && step2.kind === "create")
8071
- created.set(step2.want.path, p);
8072
- if (step2.kind === "close" || step2.kind === "archive")
8073
- closed.add(step2.id);
8074
- if (step2.kind !== "unchanged") made++;
8075
- }
8794
+ await working(
8795
+ ui,
8796
+ `writing ${plural(changes)} on ${scope.label}`,
8797
+ changes,
8798
+ async () => {
8799
+ for (const step2 of steps) {
8800
+ const p = await fromGithub(c.provider.apply(board, step2));
8801
+ if (p !== null && (step2.kind === "create" || step2.kind === "convert"))
8802
+ placed.set(step2.want.path, p);
8803
+ if (p?.adopted === true) adopted++;
8804
+ else if (p !== null && step2.kind === "create")
8805
+ created.set(step2.want.path, p);
8806
+ if (step2.kind === "close" || step2.kind === "archive")
8807
+ closed.add(step2.id);
8808
+ if (step2.kind !== "unchanged") made++;
8809
+ }
8810
+ }
8811
+ );
8076
8812
  } finally {
8077
8813
  await writeOpened(
8078
8814
  record,
8079
- stillOpened(opened, have, seen, closed, created)
8815
+ new Map([
8816
+ ...others,
8817
+ ...stillOpened(opened, have, seen, closed, created)
8818
+ ])
8080
8819
  );
8081
8820
  }
8082
8821
  const issued = issuedAfter(steps, have, placed);
@@ -8088,17 +8827,24 @@ async function syncBoard(c, board, ui, sayLayout) {
8088
8827
  w.path,
8089
8828
  issueBody({
8090
8829
  path: w.path,
8091
- text: await readFile13(join26(c.rnessDir, ...w.path.split("/")), "utf8"),
8830
+ text: await readFile14(join27(c.rnessDir, ...w.path.split("/")), "utf8"),
8092
8831
  org: c.org,
8093
8832
  numbers,
8094
8833
  files
8095
8834
  })
8096
8835
  );
8097
8836
  writes = planBodies(bodies, issued);
8098
- for (const step2 of writes) {
8099
- await fromGithub(c.provider.apply(board, step2));
8100
- made++;
8101
- }
8837
+ await working(
8838
+ ui,
8839
+ `writing ${plural(writes.length)} bodies on ${scope.label}`,
8840
+ writes.length,
8841
+ async () => {
8842
+ for (const step2 of writes) {
8843
+ await fromGithub(c.provider.apply(board, step2));
8844
+ made++;
8845
+ }
8846
+ }
8847
+ );
8102
8848
  } catch (e) {
8103
8849
  if (!(e instanceof RateLimitError)) throw e;
8104
8850
  const total = changes + writes.length;
@@ -8107,7 +8853,43 @@ async function syncBoard(c, board, ui, sayLayout) {
8107
8853
  { cause: e }
8108
8854
  );
8109
8855
  }
8110
- sayDone(ui, c.org, want.length, steps, writes, adopted);
8856
+ sayDone(
8857
+ ui,
8858
+ c.org,
8859
+ want.length,
8860
+ steps,
8861
+ writes,
8862
+ adopted,
8863
+ scope.own === null ? void 0 : scope.label
8864
+ );
8865
+ if (scope.own !== null) {
8866
+ const { shape, updates } = scope.own;
8867
+ for (const what of await fromGithub(
8868
+ c.provider.describe(board, {
8869
+ readme: shape.readme,
8870
+ description: shape.description
8871
+ })
8872
+ ))
8873
+ ui.line("wrote", `${what} of ${scope.label}`);
8874
+ for (const line3 of await fromGithub(
8875
+ c.provider.postUpdates(board, updates)
8876
+ )) {
8877
+ const [verb, ...rest] = line3.split(" ");
8878
+ ui.line(verb ?? "posted", rest.join(" "));
8879
+ }
8880
+ }
8881
+ }
8882
+ async function syncAll(c, declared2, ui, sayLayout) {
8883
+ const tabs = await statusTabs(c.rnessDir);
8884
+ const owners = declared2.map((d) => d.name).filter((n) => n !== PULSE);
8885
+ for (const { name, board } of declared2)
8886
+ await syncBoard(
8887
+ c,
8888
+ board,
8889
+ ui,
8890
+ sayLayout(name),
8891
+ await scopeOf(c, name, tabs, owners)
8892
+ );
8111
8893
  }
8112
8894
  async function pulseCreateCommand(opts, deps = {}) {
8113
8895
  try {
@@ -8115,9 +8897,20 @@ async function pulseCreateCommand(opts, deps = {}) {
8115
8897
  const ui = deps.ui ?? await makeUi(terminal);
8116
8898
  const wait = waitSaid(opts, ui);
8117
8899
  let c = await context2(opts, wait);
8118
- if (c.manifest.pulse !== null)
8900
+ const name = opts.collection ?? PULSE;
8901
+ const declared2 = c.manifest.projects?.[name];
8902
+ if (opts.collection === PULSE)
8903
+ throw new Error('"pulse" names Agent Pulse: rness pulse create');
8904
+ if (declared2 !== void 0)
8905
+ throw new Error(
8906
+ `already declared: ${boardUrl(c.org, declared2)} \u2014 rness pulse sync`
8907
+ );
8908
+ const tab = opts.collection === void 0 ? null : (await statusTabs(c.rnessDir)).find(
8909
+ (t) => t.name === opts.collection
8910
+ ) ?? null;
8911
+ if (opts.collection !== void 0 && tab === null)
8119
8912
  throw new Error(
8120
- `already declared: ${boardUrl(c.org, c.manifest.pulse.project)} \u2014 rness pulse sync`
8913
+ `${opts.collection} is no collection of .rness: none of its documents carries a status`
8121
8914
  );
8122
8915
  const detected = providerOf(c.manifest);
8123
8916
  if (!PROVIDERS[detected].available)
@@ -8155,19 +8948,26 @@ async function pulseCreateCommand(opts, deps = {}) {
8155
8948
  "checked",
8156
8949
  `${detected}, logged in as ${login}, scope ${PROJECT_SCOPE}`
8157
8950
  );
8158
- const board = await fromGithub(c.provider.createBoard(c.org));
8159
- await writeManifest(c.rnessDir, {
8951
+ const board = await fromGithub(c.provider.createBoard(c.org, tab?.label));
8952
+ const manifest = {
8160
8953
  ...c.manifest,
8161
8954
  provider: detected,
8162
- pulse: { project: board.number }
8163
- });
8164
- ui.line("created", `Agent Pulse \u2014 ${board.url}`);
8955
+ projects: { ...c.manifest.projects, [name]: board.number }
8956
+ };
8957
+ await writeManifest(c.rnessDir, manifest);
8958
+ c = { ...c, manifest };
8959
+ ui.line("created", `${tab?.label ?? "Agent Pulse"} \u2014 ${board.url}`);
8165
8960
  try {
8166
- await syncBoard(c, board, ui, createdByKind);
8961
+ await syncAll(
8962
+ c,
8963
+ await declaredBoards(c),
8964
+ ui,
8965
+ (n) => n === name ? createdByKind : eachAdded
8966
+ );
8167
8967
  } finally {
8168
8968
  ui.line(
8169
8969
  "declared",
8170
- 'pulse in .rness/rness.json \u2014 commit it: git -C .rness commit -am "chore: rness pulse"'
8970
+ `${name} in .rness/rness.json \u2014 commit it: git -C .rness commit -am "chore: rness pulse${opts.collection === void 0 ? "" : ` ${opts.collection}`}"`
8171
8971
  );
8172
8972
  }
8173
8973
  return 0;
@@ -8179,7 +8979,7 @@ async function pulseSyncCommand(opts, deps = {}) {
8179
8979
  try {
8180
8980
  const ui = deps.ui ?? await makeUi(deps.terminal ?? defaultTerminal);
8181
8981
  const c = await context2(opts, waitSaid(opts, ui));
8182
- await syncBoard(c, await declaredBoard(c), ui, eachAdded);
8982
+ await syncAll(c, await declaredBoards(c), ui, () => eachAdded);
8183
8983
  return 0;
8184
8984
  } catch (e) {
8185
8985
  return reportError(e);
@@ -8195,43 +8995,47 @@ async function pulseMarkCommand(opts) {
8195
8995
  }
8196
8996
  async function mark(opts) {
8197
8997
  const c = await context2(opts);
8198
- const board = await declaredBoard(c);
8199
- const key = `${c.org}-${board.number}`;
8998
+ const declared2 = await declaredBoards(c);
8999
+ const key = `${c.org}-${declared2.map((d) => d.board.number).join("-")}`;
8200
9000
  const lock = opts.sleep === void 0 ? {} : { sleep: opts.sleep };
8201
- const listed = () => fromGithub(c.provider.items(board, { bodies: false }));
8202
- let items = await listed();
9001
+ const syncEvery = () => syncAll(c, declared2, plainUi, () => eachAdded);
9002
+ const listed = (board) => fromGithub(c.provider.items(board, { bodies: false }));
8203
9003
  if (opts.end === true) {
8204
9004
  const id = opts.session.split(" \xB7 ").pop() ?? opts.session;
8205
- const ids2 = items.filter(
8206
- (i) => i.session === opts.session || i.session?.endsWith(` \xB7 ${id}`)
8207
- ).map((i) => i.id);
8208
- await fromGithub(c.provider.mark(board, ids2, null));
8209
- await oneSyncAtATime(key, () => syncBoard(c, board, plainUi, eachAdded), {
8210
- ...lock,
8211
- wait: true
8212
- });
9005
+ for (const { board } of declared2) {
9006
+ const ids = (await listed(board)).filter(
9007
+ (i) => i.session === opts.session || i.session?.endsWith(` \xB7 ${id}`)
9008
+ ).map((i) => i.id);
9009
+ await fromGithub(c.provider.mark(board, ids, null));
9010
+ }
9011
+ await oneSyncAtATime(key, syncEvery, { ...lock, wait: true });
8213
9012
  return 0;
8214
9013
  }
8215
- const wanted = new Set(opts.paths);
9014
+ const held = (path) => declared2.some((d) => d.name === PULSE || path.startsWith(`${d.name}/`));
9015
+ const wanted = new Set(opts.paths.filter(held));
8216
9016
  const documents2 = new Map(
8217
9017
  (await statusTabs(c.rnessDir)).flatMap(
8218
9018
  (t) => t.rows.map((r) => [r.path, r.status])
8219
9019
  )
8220
9020
  );
9021
+ let items = /* @__PURE__ */ new Map();
9022
+ const listAll = async () => {
9023
+ items = /* @__PURE__ */ new Map();
9024
+ for (const { name, board } of declared2) items.set(name, await listed(board));
9025
+ };
9026
+ await listAll();
8221
9027
  const onBoard = new Map(
8222
- items.flatMap((i) => i.path === null ? [] : [[i.path, i.status]])
9028
+ [...items.values()].flat().flatMap((i) => i.path === null ? [] : [[i.path, i.status]])
8223
9029
  );
8224
9030
  const stale = (path) => documents2.has(path) && (!onBoard.has(path) || onBoard.get(path) !== documents2.get(path));
8225
9031
  if ([...wanted].some(stale)) {
8226
- const synced = await oneSyncAtATime(
8227
- key,
8228
- () => syncBoard(c, board, plainUi, eachAdded),
8229
- lock
8230
- );
8231
- if (synced) items = await listed();
9032
+ if (await oneSyncAtATime(key, syncEvery, lock)) await listAll();
9033
+ }
9034
+ for (const { name, board } of declared2) {
9035
+ const ids = (items.get(name) ?? []).filter((i) => i.path !== null && wanted.has(i.path)).map((i) => i.id);
9036
+ if (ids.length > 0)
9037
+ await fromGithub(c.provider.mark(board, ids, opts.session));
8232
9038
  }
8233
- const ids = items.filter((i) => i.path !== null && wanted.has(i.path)).map((i) => i.id);
8234
- await fromGithub(c.provider.mark(board, ids, opts.session));
8235
9039
  return 0;
8236
9040
  }
8237
9041
 
@@ -8533,10 +9337,19 @@ function buildProgram(state) {
8533
9337
  state.code = await syncCommand(opts);
8534
9338
  });
8535
9339
  const pulse = program.command("pulse").description("Show the agent's work on the organization's board");
8536
- pulse.command("create").description("Create the organization's Agent Pulse board and sync it").option("-y, --yes", "do not ask for confirmation").addOption(new Option("--github-api <base>").hideHelp()).addOption(new Option("--cwd <dir>").hideHelp()).action(async (opts) => {
8537
- state.code = await pulseCreateCommand(opts);
9340
+ pulse.command("create").description(
9341
+ "Create the organization's Agent Pulse board, or a collection's own project, and sync"
9342
+ ).argument(
9343
+ "[collection]",
9344
+ "a directory of .rness/ whose documents get a project of their own"
9345
+ ).option("-y, --yes", "do not ask for confirmation").addOption(new Option("--github-api <base>").hideHelp()).addOption(new Option("--cwd <dir>").hideHelp()).action(async (collection, opts) => {
9346
+ state.code = await pulseCreateCommand(
9347
+ collection === void 0 ? opts : { ...opts, collection }
9348
+ );
8538
9349
  });
8539
- pulse.command("sync").description("Bring the board up to date with the documents of .rness/").addOption(new Option("--github-api <base>").hideHelp()).addOption(new Option("--cwd <dir>").hideHelp()).action(async (opts) => {
9350
+ pulse.command("sync").description(
9351
+ "Bring every declared project up to date with the documents of .rness/"
9352
+ ).addOption(new Option("--github-api <base>").hideHelp()).addOption(new Option("--cwd <dir>").hideHelp()).action(async (opts) => {
8540
9353
  state.code = await pulseSyncCommand(opts);
8541
9354
  });
8542
9355
  pulse.command("mark", { hidden: true }).requiredOption("--session <id>").option(
@@ -8766,4 +9579,4 @@ export {
8766
9579
  OWN_COMMANDS,
8767
9580
  catchUp
8768
9581
  };
8769
- //# sourceMappingURL=chunk-ABCO6D6B.js.map
9582
+ //# sourceMappingURL=chunk-GAZDKEOH.js.map