@rness/cli 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.0";
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,90 @@ 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
+ labels: once(want.flatMap((w) => [...w.labels, ...w.unlabels]))
1408
1646
  };
1409
1647
  }
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
- }))
1648
+ var sessionsOf2 = (row) => row.sessions?.map((e) => e.agent === void 0 ? e.id : `${e.agent} \xB7 ${e.id}`).join(", ") ?? null;
1649
+ function desiredOf(tabs, org, own) {
1650
+ const fields2 = own === void 0 ? fieldsOf(tabs) : /* @__PURE__ */ new Map();
1651
+ const wanted = tabs.flatMap(
1652
+ (tab) => tab.rows.map((row) => {
1653
+ const { values: values2, labels } = own === void 0 ? { values: {}, labels: [] } : valuesOf(
1654
+ own.shape,
1655
+ own.fronts.get(row.path) ?? {},
1656
+ row.path.slice(tab.name.length + 1)
1657
+ );
1658
+ return {
1659
+ path: row.path,
1660
+ title: `${row.id} \u2014 ${row.title}`,
1661
+ body: headerOf(row.path, org),
1662
+ status: statusOf2(row.status),
1663
+ type: tab.label,
1664
+ statusField: fields2.get(tab.label)?.name ?? null,
1665
+ sessions: sessionsOf2(row),
1666
+ values: values2,
1667
+ labels,
1668
+ unlabels: []
1669
+ };
1670
+ })
1422
1671
  );
1672
+ if (own === void 0) return wanted;
1673
+ const made = once([
1674
+ ...wanted.flatMap((w) => w.labels),
1675
+ ...own.shape.labels?.kind === "directory" ? own.directories ?? [] : []
1676
+ ]);
1677
+ return wanted.map((w) => ({
1678
+ ...w,
1679
+ unlabels: made.filter((l) => !w.labels.includes(l))
1680
+ }));
1423
1681
  }
1424
1682
 
1425
1683
  // 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;
1684
+ var lower = (labels) => new Set(labels.map((l) => l.toLowerCase()));
1685
+ function labelsDiffer(item, w) {
1686
+ if (w.labels.length === 0 && w.unlabels.length === 0) return false;
1687
+ const carried = lower(item.issue?.labels ?? []);
1688
+ return w.labels.some((l) => !carried.has(l.toLowerCase())) || w.unlabels.some((l) => carried.has(l.toLowerCase()));
1689
+ }
1690
+ 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(
1691
+ ([name, value]) => (item.values[name] ?? null) !== value
1692
+ ) || labelsDiffer(item, w);
1427
1693
  var rank = (i) => i.issue === null ? 2 : i.issue.open ? 0 : 1;
1428
1694
  var keptFirst = (a, b) => rank(a) - rank(b) || (a.issue?.number ?? 0) - (b.issue?.number ?? 0);
1429
1695
  var gone = (item) => item.issue === null ? { kind: "archive", id: item.id } : { kind: "close", id: item.id };
@@ -1449,7 +1715,7 @@ function unwantedPaths(want, have) {
1449
1715
  function isGone(path, item, seen, opened) {
1450
1716
  return seen.has(path) || item.issue !== null && opened.get(item.issue.number) === path;
1451
1717
  }
1452
- function planSync(want, have, seen = /* @__PURE__ */ new Set(), opened = /* @__PURE__ */ new Map()) {
1718
+ function planSync(want, have, seen = /* @__PURE__ */ new Set(), opened = /* @__PURE__ */ new Map(), elsewhere = () => false) {
1453
1719
  const byPath = /* @__PURE__ */ new Map();
1454
1720
  for (const item of have)
1455
1721
  if (item.path !== null)
@@ -1482,7 +1748,8 @@ function planSync(want, have, seen = /* @__PURE__ */ new Set(), opened = /* @__P
1482
1748
  for (const [path, item] of kept)
1483
1749
  if (!wanted.has(path))
1484
1750
  steps.push(
1485
- isGone(path, item, seen, opened) ? gone(item) : { kind: "unchanged", id: item.id }
1751
+ // Its own project holds it now: off this board, not closed.
1752
+ elsewhere(path) ? { kind: "remove", id: item.id } : isGone(path, item, seen, opened) ? gone(item) : { kind: "unchanged", id: item.id }
1486
1753
  );
1487
1754
  return [...steps, ...surplus];
1488
1755
  }
@@ -1843,11 +2110,11 @@ async function createRepository(owner, name, options) {
1843
2110
  }
1844
2111
 
1845
2112
  // src/core/github-projects.ts
1846
- function isRecord2(v) {
2113
+ function isRecord3(v) {
1847
2114
  return v !== null && typeof v === "object";
1848
2115
  }
1849
- var rateLimited = (body) => isRecord2(body) && Array.isArray(body["errors"]) && body["errors"].some(
1850
- (e) => isRecord2(e) && e["type"] === "RATE_LIMITED"
2116
+ var rateLimited = (body) => isRecord3(body) && Array.isArray(body["errors"]) && body["errors"].some(
2117
+ (e) => isRecord3(e) && e["type"] === "RATE_LIMITED"
1851
2118
  );
1852
2119
  async function graphqlRaw(query, variables, options) {
1853
2120
  const body = await postJson(
@@ -1857,7 +2124,7 @@ async function graphqlRaw(query, variables, options) {
1857
2124
  true,
1858
2125
  rateLimited
1859
2126
  );
1860
- return isRecord2(body) ? body : {};
2127
+ return isRecord3(body) ? body : {};
1861
2128
  }
1862
2129
  function firstError(answer) {
1863
2130
  const first = answer.errors?.[0];
@@ -1933,6 +2200,7 @@ function toField(raw) {
1933
2200
  id: raw.id ?? "",
1934
2201
  databaseId,
1935
2202
  name: raw.name ?? "",
2203
+ dataType: raw.dataType ?? (raw.options === void 0 ? "TEXT" : "SINGLE_SELECT"),
1936
2204
  options: raw.options ?? null
1937
2205
  };
1938
2206
  }
@@ -1962,10 +2230,10 @@ var optionInput = (o, id) => ({
1962
2230
  color: o.color,
1963
2231
  description: ""
1964
2232
  });
1965
- var FIELD_SELECTION = `... on ProjectV2FieldCommon { id databaseId name }
2233
+ var FIELD_SELECTION = `... on ProjectV2FieldCommon { id databaseId name dataType }
1966
2234
  ... on ProjectV2SingleSelectField { options { id name color } }`;
1967
2235
  async function createField(projectId, name, kind, o) {
1968
- const single = kind !== "TEXT";
2236
+ const single = typeof kind !== "string";
1969
2237
  const d = await graphql(
1970
2238
  `mutation($projectId: ID!, $name: String!, $dataType: ProjectV2CustomFieldType!${single ? ", $options: [ProjectV2SingleSelectFieldOptionInput!]" : ""}) {
1971
2239
  createProjectV2Field(input: {
@@ -1975,7 +2243,7 @@ async function createField(projectId, name, kind, o) {
1975
2243
  {
1976
2244
  projectId,
1977
2245
  name,
1978
- dataType: single ? "SINGLE_SELECT" : "TEXT",
2246
+ dataType: single ? "SINGLE_SELECT" : kind,
1979
2247
  ...single ? { options: kind.options.map((n) => optionInput(n)) } : {}
1980
2248
  },
1981
2249
  o
@@ -2062,6 +2330,22 @@ async function listItems(projectId, o, { bodies = true } = {}) {
2062
2330
  }
2063
2331
  }
2064
2332
  }
2333
+ ... on ProjectV2ItemFieldDateValue {
2334
+ date
2335
+ field {
2336
+ ... on ProjectV2FieldCommon {
2337
+ name
2338
+ }
2339
+ }
2340
+ }
2341
+ ... on ProjectV2ItemFieldNumberValue {
2342
+ number
2343
+ field {
2344
+ ... on ProjectV2FieldCommon {
2345
+ name
2346
+ }
2347
+ }
2348
+ }
2065
2349
  }
2066
2350
  }
2067
2351
  }
@@ -2081,7 +2365,7 @@ async function listItems(projectId, o, { bodies = true } = {}) {
2081
2365
  const values2 = {};
2082
2366
  for (const v of n.fieldValues?.nodes ?? []) {
2083
2367
  const field = v.field?.name;
2084
- const value = v.text ?? v.name;
2368
+ const value = v.text ?? v.name ?? v.date ?? (v.number === void 0 ? void 0 : String(v.number));
2085
2369
  if (field !== void 0 && value !== void 0) values2[field] = value;
2086
2370
  }
2087
2371
  const c = n.content;
@@ -2152,7 +2436,7 @@ async function setValue(projectId, itemId, fieldId, value, o) {
2152
2436
  projectId,
2153
2437
  itemId,
2154
2438
  fieldId,
2155
- value: "text" in value ? { text: value.text } : { singleSelectOptionId: value.optionId }
2439
+ value: "optionId" in value ? { singleSelectOptionId: value.optionId } : value
2156
2440
  },
2157
2441
  o
2158
2442
  );
@@ -2351,6 +2635,142 @@ async function renameView(viewId, name, visibleFieldIds, o) {
2351
2635
  o
2352
2636
  );
2353
2637
  }
2638
+ async function deleteItem(projectId, itemId, o) {
2639
+ await graphql(
2640
+ `
2641
+ mutation ($projectId: ID!, $itemId: ID!) {
2642
+ deleteProjectV2Item(input: { projectId: $projectId, itemId: $itemId }) {
2643
+ deletedItemId
2644
+ }
2645
+ }
2646
+ `,
2647
+ { projectId, itemId },
2648
+ o
2649
+ );
2650
+ }
2651
+ async function projectText(projectId, o) {
2652
+ const d = await graphql(
2653
+ `
2654
+ query ($projectId: ID!) {
2655
+ node(id: $projectId) {
2656
+ ... on ProjectV2 {
2657
+ projectReadme: readme
2658
+ projectDescription: shortDescription
2659
+ }
2660
+ }
2661
+ }
2662
+ `,
2663
+ { projectId },
2664
+ o
2665
+ );
2666
+ return {
2667
+ readme: d.node?.projectReadme ?? "",
2668
+ shortDescription: d.node?.projectDescription ?? ""
2669
+ };
2670
+ }
2671
+ async function updateProjectText(projectId, text, o) {
2672
+ await graphql(
2673
+ `
2674
+ mutation ($projectId: ID!, $readme: String, $shortDescription: String) {
2675
+ updateProjectV2(
2676
+ input: {
2677
+ projectId: $projectId
2678
+ readme: $readme
2679
+ shortDescription: $shortDescription
2680
+ }
2681
+ ) {
2682
+ projectV2 {
2683
+ id
2684
+ }
2685
+ }
2686
+ }
2687
+ `,
2688
+ { projectId, ...text },
2689
+ o
2690
+ );
2691
+ }
2692
+ async function statusUpdates(projectId, o) {
2693
+ const d = await graphql(
2694
+ `
2695
+ query ($projectId: ID!) {
2696
+ node(id: $projectId) {
2697
+ ... on ProjectV2 {
2698
+ statusUpdates(first: 100) {
2699
+ nodes {
2700
+ id
2701
+ body
2702
+ status
2703
+ startDate
2704
+ targetDate
2705
+ }
2706
+ }
2707
+ }
2708
+ }
2709
+ }
2710
+ `,
2711
+ { projectId },
2712
+ o
2713
+ );
2714
+ return d.node?.statusUpdates.nodes ?? [];
2715
+ }
2716
+ async function createStatusUpdate(projectId, update, o) {
2717
+ await graphql(
2718
+ `
2719
+ mutation (
2720
+ $projectId: ID!
2721
+ $body: String!
2722
+ $status: ProjectV2StatusUpdateStatus!
2723
+ $startDate: Date
2724
+ $targetDate: Date
2725
+ ) {
2726
+ createProjectV2StatusUpdate(
2727
+ input: {
2728
+ projectId: $projectId
2729
+ body: $body
2730
+ status: $status
2731
+ startDate: $startDate
2732
+ targetDate: $targetDate
2733
+ }
2734
+ ) {
2735
+ statusUpdate {
2736
+ id
2737
+ }
2738
+ }
2739
+ }
2740
+ `,
2741
+ { projectId, ...update },
2742
+ o
2743
+ );
2744
+ }
2745
+ async function updateStatusUpdate(statusUpdateId, update, o) {
2746
+ await graphql(
2747
+ `
2748
+ mutation (
2749
+ $statusUpdateId: ID!
2750
+ $body: String!
2751
+ $status: ProjectV2StatusUpdateStatus!
2752
+ $startDate: Date
2753
+ $targetDate: Date
2754
+ ) {
2755
+ updateProjectV2StatusUpdate(
2756
+ input: {
2757
+ statusUpdateId: $statusUpdateId
2758
+ body: $body
2759
+ status: $status
2760
+ startDate: $startDate
2761
+ targetDate: $targetDate
2762
+ }
2763
+ ) {
2764
+ statusUpdate {
2765
+ id
2766
+ }
2767
+ }
2768
+ }
2769
+ `,
2770
+ { statusUpdateId, ...update },
2771
+ o
2772
+ );
2773
+ }
2354
2774
 
2355
2775
  // src/core/github-issues.ts
2356
2776
  async function repository(owner, name, label, o) {
@@ -2489,7 +2909,7 @@ async function reopenIssue(id, o) {
2489
2909
  o
2490
2910
  );
2491
2911
  }
2492
- async function addLabels(id, labelIds, o) {
2912
+ async function addLabels(id, labelIds2, o) {
2493
2913
  await graphql(
2494
2914
  `
2495
2915
  mutation ($id: ID!, $labelIds: [ID!]!) {
@@ -2498,7 +2918,53 @@ async function addLabels(id, labelIds, o) {
2498
2918
  }
2499
2919
  }
2500
2920
  `,
2501
- { id, labelIds },
2921
+ { id, labelIds: labelIds2 },
2922
+ o
2923
+ );
2924
+ }
2925
+ async function labelIds(owner, name, o) {
2926
+ const ids = /* @__PURE__ */ new Map();
2927
+ let after = null;
2928
+ for (; ; ) {
2929
+ const d = await graphql(
2930
+ `
2931
+ query ($owner: String!, $name: String!, $after: String) {
2932
+ repository(owner: $owner, name: $name) {
2933
+ labelList: labels(first: 100, after: $after) {
2934
+ nodes {
2935
+ id
2936
+ name
2937
+ }
2938
+ pageInfo {
2939
+ hasNextPage
2940
+ endCursor
2941
+ }
2942
+ }
2943
+ }
2944
+ }
2945
+ `,
2946
+ { owner, name, after },
2947
+ o
2948
+ );
2949
+ const list2 = d.repository?.labelList;
2950
+ for (const l of list2?.nodes ?? []) ids.set(l.name, l.id);
2951
+ if (list2?.pageInfo.hasNextPage !== true || list2.pageInfo.endCursor === null)
2952
+ return ids;
2953
+ after = list2.pageInfo.endCursor;
2954
+ }
2955
+ }
2956
+ async function removeLabels(id, labelIds2, o) {
2957
+ await graphql(
2958
+ `
2959
+ mutation ($id: ID!, $labelIds: [ID!]!) {
2960
+ removeLabelsFromLabelable(
2961
+ input: { labelableId: $id, labelIds: $labelIds }
2962
+ ) {
2963
+ clientMutationId
2964
+ }
2965
+ }
2966
+ `,
2967
+ { id, labelIds: labelIds2 },
2502
2968
  o
2503
2969
  );
2504
2970
  }
@@ -2515,9 +2981,19 @@ var FORMERLY = {
2515
2981
  [WORKING_SESSION]: "Session",
2516
2982
  [SESSION_HISTORY]: "Sessions"
2517
2983
  };
2518
- var valueOf = (values2, name) => values2[name] ?? values2[FORMERLY[name] ?? ""] ?? null;
2984
+ var valueOf2 = (values2, name) => values2[name] ?? values2[FORMERLY[name] ?? ""] ?? null;
2519
2985
  var LABEL_COLOR = "5319e7";
2520
2986
  var LABEL_DESCRIPTION = "A document of .rness, on Agent Pulse";
2987
+ var OWN_LABEL_COLOR = "ededed";
2988
+ var OWN_LABEL_DESCRIPTION = "A label a collection of .rness gives its documents";
2989
+ var CALENDAR_VIEW = "Calendar";
2990
+ var markerOf = (path) => `<!-- rness: ${path} -->`;
2991
+ var DATA_TYPE = {
2992
+ text: "TEXT",
2993
+ date: "DATE",
2994
+ number: "NUMBER",
2995
+ select: "SINGLE_SELECT"
2996
+ };
2521
2997
  var labelled = (labels) => labels.some((l) => l.toLowerCase() === LABEL.toLowerCase());
2522
2998
  var STATUS_FIELD = "Status";
2523
2999
  var ALL_VIEW = "All";
@@ -2549,6 +3025,8 @@ var GitHubBoards = class {
2549
3025
  #cache = /* @__PURE__ */ new Map();
2550
3026
  /** Each organization's `.rness`, as first read: its id, its Issues, its label. */
2551
3027
  #memories = /* @__PURE__ */ new Map();
3028
+ /** Each organization's `.rness` labels by name, read when a collection needs one. */
3029
+ #labels = /* @__PURE__ */ new Map();
2552
3030
  constructor(options) {
2553
3031
  this.#o = options;
2554
3032
  }
@@ -2589,10 +3067,10 @@ var GitHubBoards = class {
2589
3067
  );
2590
3068
  }
2591
3069
  /** The project only: the caller declares it before `ensureLayout` builds it. */
2592
- async createBoard(org) {
3070
+ async createBoard(org, title = PROJECT_TITLE) {
2593
3071
  const project = await createProject(
2594
3072
  await orgId(org, this.#o),
2595
- PROJECT_TITLE,
3073
+ title,
2596
3074
  this.#o
2597
3075
  );
2598
3076
  const board = { org, number: project.number, url: project.url };
@@ -2603,7 +3081,7 @@ var GitHubBoards = class {
2603
3081
  const project = await findProject(org, number, this.#o);
2604
3082
  if (project === null) return null;
2605
3083
  const board = { org, number, url: project.url };
2606
- this.#remember(board, project.id);
3084
+ if (!this.#cache.has(this.#key(board))) this.#remember(board, project.id);
2607
3085
  return board;
2608
3086
  }
2609
3087
  async ensureLayout(board, layout) {
@@ -2678,6 +3156,60 @@ var GitHubBoards = class {
2678
3156
  const session = await text(WORKING_SESSION);
2679
3157
  const path = await text("Path");
2680
3158
  const sessions = await text(SESSION_HISTORY);
3159
+ const declared2 = [];
3160
+ for (const d of layout.declared) {
3161
+ const type = DATA_TYPE[d.type];
3162
+ const field = fields2.find((f) => f.name === d.name);
3163
+ if (field !== void 0 && field.dataType !== type)
3164
+ throw new Error(
3165
+ `the project's field ${d.name} is not a ${d.type} field: rename it on the project, or in the collection's README`
3166
+ );
3167
+ if (d.type !== "select") {
3168
+ if (field !== void 0) declared2.push(field);
3169
+ else {
3170
+ added.push(`field ${d.name}`);
3171
+ declared2.push(
3172
+ await createField(
3173
+ cache.projectId,
3174
+ d.name,
3175
+ DATA_TYPE[d.type],
3176
+ this.#o
3177
+ )
3178
+ );
3179
+ }
3180
+ continue;
3181
+ }
3182
+ if (field === void 0 && d.options.length === 0) continue;
3183
+ const options = (names) => names.map((name) => ({
3184
+ name,
3185
+ color: field?.options?.find((o) => o.name === name)?.color ?? "GRAY"
3186
+ }));
3187
+ if (field === void 0) {
3188
+ added.push(`field ${d.name}`);
3189
+ declared2.push(
3190
+ await createField(
3191
+ cache.projectId,
3192
+ d.name,
3193
+ { options: options(d.options) },
3194
+ this.#o
3195
+ )
3196
+ );
3197
+ continue;
3198
+ }
3199
+ const now = namesOf(field);
3200
+ const next = orderedFirst(d.options, [
3201
+ ...now,
3202
+ ...d.options.filter((n) => !now.includes(n))
3203
+ ]);
3204
+ if (next.every((n, i) => n === now[i]) && next.length === now.length)
3205
+ declared2.push(field);
3206
+ else {
3207
+ added.push(
3208
+ ...d.options.filter((n) => !now.includes(n)).map((n) => `option ${n}`)
3209
+ );
3210
+ declared2.push(await setOptions(field, options(next), this.#o));
3211
+ }
3212
+ }
2681
3213
  const current = await views(cache.projectId, this.#o);
2682
3214
  const first = current[0];
2683
3215
  if (first !== void 0 && first.name === DEFAULT_VIEW && !current.some((v) => v.name === ALL_VIEW)) {
@@ -2710,6 +3242,19 @@ var GitHubBoards = class {
2710
3242
  this.#o
2711
3243
  );
2712
3244
  }
3245
+ if (layout.declared.some((d) => d.type === "date") && !current.some((v) => v.name === CALENDAR_VIEW)) {
3246
+ added.push(`view ${CALENDAR_VIEW}`);
3247
+ await createView(
3248
+ board.org,
3249
+ board.number,
3250
+ {
3251
+ name: CALENDAR_VIEW,
3252
+ layout: "roadmap",
3253
+ filter: viewFilter(layout.types[0] ?? "")
3254
+ },
3255
+ this.#o
3256
+ );
3257
+ }
2713
3258
  if (!current.some((v) => v.name === WORKING_VIEW)) {
2714
3259
  added.push(`view ${WORKING_VIEW}`);
2715
3260
  await createView(
@@ -2740,6 +3285,9 @@ var GitHubBoards = class {
2740
3285
  );
2741
3286
  added.push(`label ${LABEL}`);
2742
3287
  }
3288
+ for (const name of layout.labels)
3289
+ if ((await this.#ownLabel(board.org, name)).made)
3290
+ added.push(`label ${name}`);
2743
3291
  const repository2 = `${board.org}/${MEMORY}`;
2744
3292
  const linked = await linkedRepositories(cache.projectId, this.#o);
2745
3293
  if (!linked.some((r) => r.toLowerCase() === repository2.toLowerCase())) {
@@ -2753,10 +3301,33 @@ var GitHubBoards = class {
2753
3301
  agent,
2754
3302
  session,
2755
3303
  path,
2756
- sessions
3304
+ sessions,
3305
+ ...declared2
2757
3306
  ];
2758
3307
  return added;
2759
3308
  }
3309
+ /** A collection's label on `.rness`, made when missing; its id, and whether this made it. */
3310
+ async #ownLabel(org, name) {
3311
+ let ids = this.#labels.get(org);
3312
+ if (ids === void 0) {
3313
+ ids = await labelIds(org, MEMORY, this.#o);
3314
+ this.#labels.set(org, ids);
3315
+ }
3316
+ const known = [...ids].find(([n]) => n.toLowerCase() === name.toLowerCase());
3317
+ if (known !== void 0) return { id: known[1], made: false };
3318
+ const id = await createLabel(
3319
+ org,
3320
+ MEMORY,
3321
+ {
3322
+ name,
3323
+ color: OWN_LABEL_COLOR,
3324
+ description: OWN_LABEL_DESCRIPTION
3325
+ },
3326
+ this.#o
3327
+ );
3328
+ ids.set(name, id);
3329
+ return { id, made: true };
3330
+ }
2760
3331
  async items(board, options = {}) {
2761
3332
  const cache = await this.#cacheOf(board);
2762
3333
  const raw = await listItems(cache.projectId, this.#o, options);
@@ -2772,15 +3343,17 @@ var GitHubBoards = class {
2772
3343
  number: issue.number,
2773
3344
  open: issue.open,
2774
3345
  labelled: labelled(issue.labels),
2775
- body: issue.body
3346
+ body: issue.body,
3347
+ labels: [...issue.labels]
2776
3348
  },
2777
3349
  title: r.title,
2778
3350
  status: r.values[STATUS_FIELD] ?? null,
2779
3351
  collectionStatus: r.values[statusFieldName(r.values[COLLECTION_FIELD] ?? "")] ?? null,
2780
3352
  type: r.values[COLLECTION_FIELD] ?? null,
2781
3353
  agent: r.values["Agent"] ?? null,
2782
- session: valueOf(r.values, WORKING_SESSION),
2783
- sessions: valueOf(r.values, SESSION_HISTORY)
3354
+ session: valueOf2(r.values, WORKING_SESSION),
3355
+ sessions: valueOf2(r.values, SESSION_HISTORY),
3356
+ values: { ...r.values }
2784
3357
  };
2785
3358
  });
2786
3359
  }
@@ -2790,7 +3363,10 @@ var GitHubBoards = class {
2790
3363
  const field = fields2.find((f) => f.name === name) ?? fields2.find((f) => f.name === FORMERLY[name]);
2791
3364
  if (field === void 0) throw new Error(`the board has no field ${name}`);
2792
3365
  let sent = null;
2793
- if (value !== null && field.options === null) sent = { text: value };
3366
+ if (value !== null && field.dataType === "DATE") sent = { date: value };
3367
+ else if (value !== null && field.dataType === "NUMBER")
3368
+ sent = { number: Number(value) };
3369
+ else if (value !== null && field.options === null) sent = { text: value };
2794
3370
  else if (value !== null) {
2795
3371
  const option = field.options?.find((opt) => opt.name === value);
2796
3372
  if (option === void 0)
@@ -2807,6 +3383,11 @@ var GitHubBoards = class {
2807
3383
  if (step2.kind === "unchanged") return null;
2808
3384
  const cache = await this.#cacheOf(board);
2809
3385
  if (step2.kind === "create") return this.#create(board, cache, step2.want);
3386
+ if (step2.kind === "remove") {
3387
+ await deleteItem(cache.projectId, step2.id, this.#o);
3388
+ cache.known.delete(step2.id);
3389
+ return null;
3390
+ }
2810
3391
  const known = await this.#known(board, step2.id);
2811
3392
  if (step2.kind === "archive" || step2.kind === "close") {
2812
3393
  if (step2.kind === "close" && known.issue?.open === true)
@@ -2837,6 +3418,7 @@ var GitHubBoards = class {
2837
3418
  labels: []
2838
3419
  };
2839
3420
  await this.#issue(known, step2.want, labelId);
3421
+ await this.#ownLabels(board.org, known, step2.want);
2840
3422
  await this.#values(board, step2.id, step2.want);
2841
3423
  return { id: step2.id, number: made.number };
2842
3424
  }
@@ -2846,6 +3428,7 @@ var GitHubBoards = class {
2846
3428
  issue.open = true;
2847
3429
  }
2848
3430
  await this.#issue(known, step2.want, labelId);
3431
+ await this.#ownLabels(board.org, known, step2.want);
2849
3432
  await this.#values(board, step2.id, step2.want);
2850
3433
  return null;
2851
3434
  }
@@ -2889,6 +3472,7 @@ var GitHubBoards = class {
2889
3472
  };
2890
3473
  cache.known.set(id2, known);
2891
3474
  await this.#issue(known, want, labelId);
3475
+ await this.#ownLabels(board.org, known, want);
2892
3476
  await this.#values(board, id2, want);
2893
3477
  return { id: id2, number: found.number, adopted: true };
2894
3478
  }
@@ -2912,6 +3496,7 @@ var GitHubBoards = class {
2912
3496
  archived: false,
2913
3497
  values: {}
2914
3498
  });
3499
+ await this.#ownLabels(board.org, cache.known.get(id), want);
2915
3500
  await this.#values(board, id, want);
2916
3501
  return { id, number: made.number };
2917
3502
  }
@@ -2927,18 +3512,45 @@ var GitHubBoards = class {
2927
3512
  known.title = want.title;
2928
3513
  }
2929
3514
  }
3515
+ /** The labels a collection gives the issue, and those it takes off (spec 0025 §4). */
3516
+ async #ownLabels(org, known, want) {
3517
+ if (want.labels.length === 0 && want.unlabels.length === 0) return;
3518
+ const issue = this.#issueOf(known);
3519
+ const carried = (name) => issue.labels.find((l) => l.toLowerCase() === name.toLowerCase());
3520
+ const add = want.labels.filter((l) => carried(l) === void 0);
3521
+ const off = want.unlabels.filter((l) => carried(l) !== void 0);
3522
+ if (add.length > 0) {
3523
+ const ids = [];
3524
+ for (const name of add) ids.push((await this.#ownLabel(org, name)).id);
3525
+ await addLabels(issue.id, ids, this.#o);
3526
+ issue.labels.push(...add);
3527
+ }
3528
+ if (off.length > 0) {
3529
+ const ids = [];
3530
+ for (const name of off) ids.push((await this.#ownLabel(org, name)).id);
3531
+ await removeLabels(issue.id, ids, this.#o);
3532
+ issue.labels = issue.labels.filter(
3533
+ (l) => !off.some((o) => o.toLowerCase() === l.toLowerCase())
3534
+ );
3535
+ }
3536
+ }
2930
3537
  /** Path first — an issue on the board without it is the team's — then the fields, each only when it differs. */
2931
3538
  async #values(board, id, want) {
2932
3539
  const values2 = (await this.#known(board, id)).values;
3540
+ const fields2 = await this.#fields(board);
2933
3541
  const wanted = [
2934
3542
  ["Path", want.path],
2935
3543
  [COLLECTION_FIELD, want.type],
2936
3544
  [STATUS_FIELD, want.status],
2937
3545
  ...want.statusField === null ? [] : [[want.statusField, want.status]],
2938
- [SESSION_HISTORY, want.sessions]
3546
+ [SESSION_HISTORY, want.sessions],
3547
+ // A select with no option yet has no field yet: nothing to write.
3548
+ ...Object.entries(want.values).filter(
3549
+ ([name]) => fields2.some((f) => f.name === name)
3550
+ )
2939
3551
  ];
2940
3552
  for (const [name, value] of wanted)
2941
- if (valueOf(values2, name) !== value)
3553
+ if (valueOf2(values2, name) !== value)
2942
3554
  await this.#set(board, id, name, value);
2943
3555
  }
2944
3556
  async #known(board, id) {
@@ -2967,6 +3579,48 @@ var GitHubBoards = class {
2967
3579
  await this.#set(board, id, WORKING_SESSION, session);
2968
3580
  }
2969
3581
  }
3582
+ async describe(board, text) {
3583
+ const cache = await this.#cacheOf(board);
3584
+ const now = await projectText(cache.projectId, this.#o);
3585
+ const next = {};
3586
+ if (text.readme !== null && now.readme.trimEnd() !== text.readme.trimEnd())
3587
+ next.readme = text.readme.trimEnd();
3588
+ if (text.description !== null && now.shortDescription !== text.description)
3589
+ next.shortDescription = text.description;
3590
+ if (next.readme === void 0 && next.shortDescription === void 0)
3591
+ return [];
3592
+ await updateProjectText(cache.projectId, next, this.#o);
3593
+ return [
3594
+ ...next.readme === void 0 ? [] : ["readme"],
3595
+ ...next.shortDescription === void 0 ? [] : ["short description"]
3596
+ ];
3597
+ }
3598
+ async postUpdates(board, updates) {
3599
+ if (updates.length === 0) return [];
3600
+ const cache = await this.#cacheOf(board);
3601
+ const there = await statusUpdates(cache.projectId, this.#o);
3602
+ const written = [];
3603
+ for (const u of updates) {
3604
+ const marker = markerOf(u.path);
3605
+ const input = {
3606
+ body: `${u.body.trimEnd()}
3607
+
3608
+ ${marker}`,
3609
+ status: u.health.toUpperCase().replace("-", "_"),
3610
+ startDate: u.startDate,
3611
+ targetDate: u.targetDate
3612
+ };
3613
+ const posted = there.find((p) => p.body.includes(marker));
3614
+ if (posted === void 0) {
3615
+ await createStatusUpdate(cache.projectId, input, this.#o);
3616
+ written.push(`posted ${u.path}`);
3617
+ } else if (posted.body.trimEnd() !== input.body || posted.status !== input.status || (posted.startDate ?? null) !== input.startDate || (posted.targetDate ?? null) !== input.targetDate) {
3618
+ await updateStatusUpdate(posted.id, input, this.#o);
3619
+ written.push(`updated ${u.path}`);
3620
+ }
3621
+ }
3622
+ return written;
3623
+ }
2970
3624
  };
2971
3625
 
2972
3626
  // src/core/github-oauth-provider.ts
@@ -3047,8 +3701,8 @@ var GitHubOAuthProvider = class {
3047
3701
  async checkIssues(org) {
3048
3702
  return this.#boards().checkIssues(org);
3049
3703
  }
3050
- async createBoard(org) {
3051
- return this.#boards().createBoard(org);
3704
+ async createBoard(org, title) {
3705
+ return this.#boards().createBoard(org, title);
3052
3706
  }
3053
3707
  async board(org, number) {
3054
3708
  return this.#boards().board(org, number);
@@ -3065,6 +3719,12 @@ var GitHubOAuthProvider = class {
3065
3719
  async mark(board, itemIds, session) {
3066
3720
  return this.#boards().mark(board, itemIds, session);
3067
3721
  }
3722
+ async describe(board, text) {
3723
+ return this.#boards().describe(board, text);
3724
+ }
3725
+ async postUpdates(board, updates) {
3726
+ return this.#boards().postUpdates(board, updates);
3727
+ }
3068
3728
  credentialsFor(url) {
3069
3729
  if (this.#token === null || !url.startsWith(GITHUB_HTTPS)) return null;
3070
3730
  return { username: "x-access-token", password: this.#token.token };
@@ -3079,11 +3739,11 @@ async function githubProvider(apiBase, wait) {
3079
3739
  }
3080
3740
 
3081
3741
  // 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";
3742
+ import { glob as glob2, readFile as readFile6, rm as rm3 } from "fs/promises";
3743
+ import { join as join9, posix as posix5, sep as sep2 } from "path";
3084
3744
  async function addRepository(input) {
3085
3745
  const { name, url } = parseRepoSpec(input.spec, input.org, input.host);
3086
- const dir = join8(input.root, "org", name);
3746
+ const dir = join9(input.root, "org", name);
3087
3747
  const repoPath = `org/${name}`;
3088
3748
  const current = input.manifest.scopes[name];
3089
3749
  if (current !== void 0 && current.path !== repoPath)
@@ -3126,7 +3786,7 @@ async function addRepository(input) {
3126
3786
  }
3127
3787
  const declared2 = [name];
3128
3788
  for (const { path, base } of subs) {
3129
- if (!await exists(join8(input.root, ...path.split("/")))) {
3789
+ if (!await exists(join9(input.root, ...path.split("/")))) {
3130
3790
  if (action === "cloned") await rm3(dir, { recursive: true, force: true });
3131
3791
  throw new Error(`${path} does not exist`);
3132
3792
  }
@@ -3144,7 +3804,7 @@ async function workspaceDirs(repoDir) {
3144
3804
  let patterns = [];
3145
3805
  try {
3146
3806
  const pkg = JSON.parse(
3147
- await readFile5(join8(repoDir, "package.json"), "utf8")
3807
+ await readFile6(join9(repoDir, "package.json"), "utf8")
3148
3808
  );
3149
3809
  const w = pkg.workspaces;
3150
3810
  if (Array.isArray(w))
@@ -3161,7 +3821,7 @@ async function workspaceDirs(repoDir) {
3161
3821
  for (const pattern of patterns) {
3162
3822
  for await (const match of glob2(pattern, { cwd: repoDir })) {
3163
3823
  const rel = match.split(sep2).join(posix5.sep);
3164
- if (await exists(join8(repoDir, ...rel.split(posix5.sep), "package.json")))
3824
+ if (await exists(join9(repoDir, ...rel.split(posix5.sep), "package.json")))
3165
3825
  dirs.add(rel);
3166
3826
  }
3167
3827
  }
@@ -3412,8 +4072,8 @@ function httpsFlagLine(spec) {
3412
4072
  }
3413
4073
 
3414
4074
  // 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";
4075
+ import { readdir as readdir3 } from "fs/promises";
4076
+ import { dirname as dirname3, join as join10, parse as parse2, resolve as resolve2 } from "path";
3417
4077
  async function findWorkspace(startDir) {
3418
4078
  const start = resolve2(startDir);
3419
4079
  const { root: fsRoot } = parse2(start);
@@ -3423,18 +4083,18 @@ async function findWorkspace(startDir) {
3423
4083
  if (dir === fsRoot) break;
3424
4084
  }
3425
4085
  for (const dir of ancestors) {
3426
- if (await exists(join9(dir, ".rness", "rness.json")))
3427
- return { root: dir, rnessDir: join9(dir, ".rness") };
4086
+ if (await exists(join10(dir, ".rness", "rness.json")))
4087
+ return { root: dir, rnessDir: join10(dir, ".rness") };
3428
4088
  }
3429
4089
  for (const dir of ancestors) {
3430
- if (await exists(join9(dir, "rness.json")))
4090
+ if (await exists(join10(dir, "rness.json")))
3431
4091
  return { root: dirname3(dir), rnessDir: dir };
3432
4092
  }
3433
4093
  throw new Error(`no rness workspace found above ${startDir}`);
3434
4094
  }
3435
4095
  async function clonesIn(root) {
3436
4096
  try {
3437
- const entries = await readdir2(join9(root, "org"), { withFileTypes: true });
4097
+ const entries = await readdir3(join10(root, "org"), { withFileTypes: true });
3438
4098
  return new Set(
3439
4099
  entries.filter((e) => e.isDirectory() && isWorkspaceClone(e.name)).map((e) => e.name)
3440
4100
  );
@@ -3458,15 +4118,15 @@ function reportError(e) {
3458
4118
  }
3459
4119
 
3460
4120
  // src/core/delegate.ts
3461
- import { readFile as readFile6 } from "fs/promises";
3462
- import { join as join10 } from "path";
4121
+ import { readFile as readFile7 } from "fs/promises";
4122
+ import { join as join11 } from "path";
3463
4123
  var DEFAULT_BIN = "dist/bin/rness.js";
3464
4124
  var PINNED_BIN = `node_modules/@rness/cli/${DEFAULT_BIN}`;
3465
4125
  async function readPinnedCli(rnessDir) {
3466
- const dir = join10(rnessDir, "node_modules", "@rness", "cli");
4126
+ const dir = join11(rnessDir, "node_modules", "@rness", "cli");
3467
4127
  let pkg;
3468
4128
  try {
3469
- pkg = JSON.parse(await readFile6(join10(dir, "package.json"), "utf8"));
4129
+ pkg = JSON.parse(await readFile7(join11(dir, "package.json"), "utf8"));
3470
4130
  } catch {
3471
4131
  return null;
3472
4132
  }
@@ -3486,7 +4146,7 @@ async function readPinnedCli(rnessDir) {
3486
4146
  async function pinnedBinPath(rnessDir) {
3487
4147
  const pinned = await readPinnedCli(rnessDir);
3488
4148
  if (pinned === null) return null;
3489
- return join10(pinned.dir, ...pinned.bin.split("/"));
4149
+ return join11(pinned.dir, ...pinned.bin.split("/"));
3490
4150
  }
3491
4151
  async function findDelegate(cwd, ownVersion) {
3492
4152
  let root;
@@ -3502,7 +4162,7 @@ async function findDelegate(cwd, ownVersion) {
3502
4162
  if (pinned === null) return null;
3503
4163
  const { version } = pinned;
3504
4164
  if (version === null || version === ownVersion) return null;
3505
- return { entry: join10(pinned.dir, "dist", "index.js"), version, root };
4165
+ return { entry: join11(pinned.dir, "dist", "index.js"), version, root };
3506
4166
  }
3507
4167
 
3508
4168
  // src/core/block.ts
@@ -3569,7 +4229,7 @@ function isCurrentBlock(blockLines, freshHash) {
3569
4229
  }
3570
4230
 
3571
4231
  // src/core/merge.ts
3572
- import { join as join11 } from "path";
4232
+ import { join as join12 } from "path";
3573
4233
  function findBlock(lines) {
3574
4234
  const begins = [];
3575
4235
  const ends = [];
@@ -3628,7 +4288,7 @@ function claudeMdAtRoot(existing, block) {
3628
4288
  return merged.ok ? { ...merged, changed: merged.text !== existing } : merged;
3629
4289
  }
3630
4290
  async function ensureClaudeMd(dir) {
3631
- const file = join11(dir, "CLAUDE.md");
4291
+ const file = join12(dir, "CLAUDE.md");
3632
4292
  if (await isSymlink(file)) return "unchanged";
3633
4293
  const existing = await readOrNull(file);
3634
4294
  if (existing === null) {
@@ -3643,11 +4303,11 @@ async function ensureClaudeMd(dir) {
3643
4303
  }
3644
4304
 
3645
4305
  // src/commands/sync.ts
3646
- import { join as join14 } from "path";
4306
+ import { join as join15 } from "path";
3647
4307
 
3648
4308
  // src/core/agent-targets.ts
3649
4309
  import { mkdir as mkdir2 } from "fs/promises";
3650
- import { dirname as dirname4, join as join13 } from "path";
4310
+ import { dirname as dirname4, join as join14 } from "path";
3651
4311
 
3652
4312
  // src/core/claude-skills.ts
3653
4313
  var PLUGIN = ".claude/skills/rness";
@@ -4092,8 +4752,8 @@ function describeGuarantee(g) {
4092
4752
 
4093
4753
  // src/core/pinned.ts
4094
4754
  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";
4755
+ import { readFile as readFile8 } from "fs/promises";
4756
+ import { basename as basename3, join as join13 } from "path";
4097
4757
  import { promisify as promisify3 } from "util";
4098
4758
  var execFileP3 = promisify3(execFile3);
4099
4759
  var PACKAGE = "@rness/cli";
@@ -4118,7 +4778,7 @@ function compareVersions(a, b) {
4118
4778
  async function readPackageJson(rnessDir) {
4119
4779
  try {
4120
4780
  const parsed = JSON.parse(
4121
- await readFile7(join12(rnessDir, "package.json"), "utf8")
4781
+ await readFile8(join13(rnessDir, "package.json"), "utf8")
4122
4782
  );
4123
4783
  return parsed !== null && typeof parsed === "object" ? parsed : null;
4124
4784
  } catch {
@@ -4137,8 +4797,8 @@ async function readPin(rnessDir) {
4137
4797
  return null;
4138
4798
  }
4139
4799
  async function writePin(rnessDir, from, to) {
4140
- const file = join12(rnessDir, "package.json");
4141
- const text = await readFile7(file, "utf8");
4800
+ const file = join13(rnessDir, "package.json");
4801
+ const text = await readFile8(file, "utf8");
4142
4802
  const entry = new RegExp(
4143
4803
  `("${PACKAGE}"\\s*:\\s*")${from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(")`
4144
4804
  );
@@ -4160,12 +4820,12 @@ async function workspacePackageManager(rnessDir) {
4160
4820
  if (isPackageManager(name)) return name;
4161
4821
  }
4162
4822
  for (const [lockfile, pm] of LOCKFILES)
4163
- if (await exists(join12(rnessDir, lockfile))) return pm;
4823
+ if (await exists(join13(rnessDir, lockfile))) return pm;
4164
4824
  return "npm";
4165
4825
  }
4166
4826
  async function hasLockfile(rnessDir, pm) {
4167
4827
  for (const [lockfile, owner] of LOCKFILES)
4168
- if (owner === pm && await exists(join12(rnessDir, lockfile))) return true;
4828
+ if (owner === pm && await exists(join13(rnessDir, lockfile))) return true;
4169
4829
  return false;
4170
4830
  }
4171
4831
  async function pinDrift(rnessDir) {
@@ -4221,9 +4881,9 @@ async function ensureInstalled(rnessDir, opts = {}) {
4221
4881
  }
4222
4882
  }
4223
4883
  async function runPinnedSync(root) {
4224
- const installed = await readPinnedCli(join12(root, ".rness"));
4884
+ const installed = await readPinnedCli(join13(root, ".rness"));
4225
4885
  if (installed === null) return null;
4226
- const pinned = join12(installed.dir, ...installed.bin.split("/"));
4886
+ const pinned = join13(installed.dir, ...installed.bin.split("/"));
4227
4887
  if (!await exists(pinned))
4228
4888
  return {
4229
4889
  code: 1,
@@ -4298,7 +4958,7 @@ function syncOutcome(result, opts = {}) {
4298
4958
  };
4299
4959
  }
4300
4960
  async function syncPinned(root, shown, fallback) {
4301
- const rnessDir = join12(root, ".rness");
4961
+ const rnessDir = join13(root, ".rness");
4302
4962
  const installed = await readPinnedCli(rnessDir);
4303
4963
  if (installed === null) {
4304
4964
  if (fallback !== void 0) return fallback();
@@ -4308,7 +4968,7 @@ async function syncPinned(root, shown, fallback) {
4308
4968
  );
4309
4969
  return 1;
4310
4970
  }
4311
- const pinned = join12(installed.dir, ...installed.bin.split("/"));
4971
+ const pinned = join13(installed.dir, ...installed.bin.split("/"));
4312
4972
  if (!await exists(pinned)) {
4313
4973
  process.stderr.write(
4314
4974
  `@rness/cli in ${shown}/.rness is not installed correctly (missing ${installed.bin}); reinstall in .rness/
@@ -4348,30 +5008,30 @@ function wholeDiff(existing, content) {
4348
5008
  }
4349
5009
  async function contextOf(root) {
4350
5010
  return {
4351
- packageManager: await workspacePackageManager(join13(root, ".rness"))
5011
+ packageManager: await workspacePackageManager(join14(root, ".rness"))
4352
5012
  };
4353
5013
  }
4354
5014
  var PATHS_ONLY = { packageManager: "npm" };
4355
5015
  async function filesOf(root, manifest, targets) {
4356
5016
  const files = [];
4357
- if (!await exists(join13(root, "org"))) return files;
5017
+ if (!await exists(join14(root, "org"))) return files;
4358
5018
  const ctx = await contextOf(root);
4359
5019
  for (const target2 of targets)
4360
5020
  for (const owned of target2.files(ctx))
4361
5021
  if (owned.at === "root")
4362
5022
  files.push({
4363
5023
  ...owned,
4364
- file: join13(root, ...owned.file.split("/")),
5024
+ file: join14(root, ...owned.file.split("/")),
4365
5025
  label: owned.file
4366
5026
  });
4367
5027
  for (const repo of Object.keys(manifest.repos)) {
4368
- if (!await exists(join13(root, "org", repo))) continue;
5028
+ if (!await exists(join14(root, "org", repo))) continue;
4369
5029
  for (const target2 of targets)
4370
5030
  for (const owned of target2.files(ctx))
4371
5031
  if (owned.at === "clones")
4372
5032
  files.push({
4373
5033
  ...owned,
4374
- file: join13(root, "org", repo, ...owned.file.split("/")),
5034
+ file: join14(root, "org", repo, ...owned.file.split("/")),
4375
5035
  label: `org/${repo}/${owned.file}`
4376
5036
  });
4377
5037
  }
@@ -4695,7 +5355,7 @@ function targetsOf(manifest, root, only) {
4695
5355
  const segments = entry.path.split("/");
4696
5356
  targets.push({
4697
5357
  scope: name,
4698
- dir: join14(root, ...segments),
5358
+ dir: join15(root, ...segments),
4699
5359
  depth: segments.length,
4700
5360
  label: `${entry.path}/AGENTS.md`
4701
5361
  });
@@ -4854,7 +5514,7 @@ async function syncCommand(opts, deps = {}) {
4854
5514
  try {
4855
5515
  const notCloned = /* @__PURE__ */ new Set();
4856
5516
  for (const [name, repo] of Object.entries(manifest.repos)) {
4857
- const dir = join14(ws.root, "org", name);
5517
+ const dir = join15(ws.root, "org", name);
4858
5518
  const label = `org/${name}`;
4859
5519
  if (!await exists(dir)) {
4860
5520
  if (!toClone.has(name)) {
@@ -4906,7 +5566,7 @@ async function syncCommand(opts, deps = {}) {
4906
5566
  lines.push(
4907
5567
  `not in rness.json: ${undeclared.join(", ")} (${rnessCommand()} add <name> declares it)`
4908
5568
  );
4909
- if (!await exists(join14(ws.root, "org"))) {
5569
+ if (!await exists(join15(ws.root, "org"))) {
4910
5570
  lines.push(["skipped", "blocks (no org/ directory here)"]);
4911
5571
  } else {
4912
5572
  let differences = 0;
@@ -4918,7 +5578,7 @@ async function syncCommand(opts, deps = {}) {
4918
5578
  continue;
4919
5579
  }
4920
5580
  const [agentsLinked, claudeLinked] = await Promise.all(
4921
- BLOCK_FILES.map((f) => isSymlink(join14(t.dir, f)))
5581
+ BLOCK_FILES.map((f) => isSymlink(join15(t.dir, f)))
4922
5582
  );
4923
5583
  const atRoot = t.scope === null;
4924
5584
  if (!atRoot && (agentsLinked === true || claudeLinked === true)) {
@@ -4941,7 +5601,7 @@ async function syncCommand(opts, deps = {}) {
4941
5601
  `${t.label}: block is ${Math.round(block.bytes / 1024)} KB (over 32 KB)`
4942
5602
  );
4943
5603
  }
4944
- const agents = agentsLinked === true ? "skipped" : await syncBlockFile(join14(t.dir, "AGENTS.md"), block, check);
5604
+ const agents = agentsLinked === true ? "skipped" : await syncBlockFile(join15(t.dir, "AGENTS.md"), block, check);
4945
5605
  if (typeof agents === "object") {
4946
5606
  problems.push(`${t.label}: ${agents.error}`);
4947
5607
  continue;
@@ -4956,7 +5616,7 @@ async function syncCommand(opts, deps = {}) {
4956
5616
  }
4957
5617
  if (claudeLinked === true) continue;
4958
5618
  const claude = await syncBlockFile(
4959
- join14(t.dir, "CLAUDE.md"),
5619
+ join15(t.dir, "CLAUDE.md"),
4960
5620
  block,
4961
5621
  check,
4962
5622
  true
@@ -5013,7 +5673,7 @@ async function syncCommand(opts, deps = {}) {
5013
5673
  }
5014
5674
 
5015
5675
  // src/commands/add.ts
5016
- import { basename as basename4, join as join15 } from "path";
5676
+ import { basename as basename4, join as join16 } from "path";
5017
5677
  function printDeclared(ui, result, already) {
5018
5678
  for (const name of result.declared) {
5019
5679
  if (already.has(name)) continue;
@@ -5106,7 +5766,7 @@ async function addCommand(spec, opts, deps = {}) {
5106
5766
  }
5107
5767
  parsed = parseRepoSpec(spec, org, host);
5108
5768
  const scopes = (opts.scopes ?? "").split(",").map((s) => s.trim()).filter((s) => s !== "");
5109
- const dir = join15(ws.root, "org", parsed.name);
5769
+ const dir = join16(ws.root, "org", parsed.name);
5110
5770
  const present = await exists(dir);
5111
5771
  if (interactive) {
5112
5772
  const { confirm, isCancel } = await terminal.prompts();
@@ -5234,7 +5894,7 @@ async function contextCommand(opts) {
5234
5894
 
5235
5895
  // src/core/scaffold.ts
5236
5896
  import { statSync } from "fs";
5237
- import { dirname as dirname5, join as join16 } from "path";
5897
+ import { dirname as dirname5, join as join17 } from "path";
5238
5898
  import { fileURLToPath } from "url";
5239
5899
  var SCAFFOLD_FILES = [
5240
5900
  "AGENTS.md",
@@ -5258,9 +5918,9 @@ var SCAFFOLD_FILES = [
5258
5918
  function scaffoldDir() {
5259
5919
  let dir = dirname5(fileURLToPath(import.meta.url));
5260
5920
  for (let i = 0; i < 4; i += 1) {
5261
- const candidate = join16(dir, "scaffold");
5921
+ const candidate = join17(dir, "scaffold");
5262
5922
  try {
5263
- if (statSync(join16(candidate, "rness.json")).isFile()) return candidate;
5923
+ if (statSync(join17(candidate, "rness.json")).isFile()) return candidate;
5264
5924
  } catch {
5265
5925
  }
5266
5926
  dir = dirname5(dir);
@@ -5269,15 +5929,15 @@ function scaffoldDir() {
5269
5929
  }
5270
5930
 
5271
5931
  // 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";
5932
+ import { chmod as chmod2, mkdir as mkdir3, readFile as readFile9, stat as stat2, writeFile as writeFile2 } from "fs/promises";
5933
+ import { dirname as dirname6, join as join18 } from "path";
5274
5934
  var TOKENISED = /* @__PURE__ */ new Set(["package.json"]);
5275
5935
  async function renderScaffold(source, tokens) {
5276
5936
  const files = [];
5277
5937
  for (const rel of SCAFFOLD_FILES) {
5278
5938
  if (rel === "rness.json") continue;
5279
- const from = join17(source, ...rel.split("/"));
5280
- let content = await readFile8(from, "utf8");
5939
+ const from = join18(source, ...rel.split("/"));
5940
+ let content = await readFile9(from, "utf8");
5281
5941
  if (TOKENISED.has(rel)) {
5282
5942
  content = content.replaceAll("__RNESS_VERSION__", tokens.version).replaceAll("__RNESS_PM__", tokens.packageManager);
5283
5943
  }
@@ -5291,7 +5951,7 @@ async function renderScaffold(source, tokens) {
5291
5951
  }
5292
5952
  async function copyScaffold(dest, tokens) {
5293
5953
  for (const file of await renderScaffold(scaffoldDir(), tokens)) {
5294
- const to = join17(dest, ...file.path.split("/"));
5954
+ const to = join18(dest, ...file.path.split("/"));
5295
5955
  await mkdir3(dirname6(to), { recursive: true });
5296
5956
  await writeFile2(to, file.content, "utf8");
5297
5957
  if (file.executable) await chmod2(to, 493);
@@ -5400,7 +6060,7 @@ async function loginCommand(opts, deps = {}) {
5400
6060
  ui.error(unsupportedProviderMessage(manifest.provider));
5401
6061
  return 1;
5402
6062
  }
5403
- const needsProject = opts.project === true || manifest?.pulse != null;
6063
+ const needsProject = opts.project === true || manifest?.projects != null;
5404
6064
  const bin = deps.bin ?? await realpath(process.argv[1] ?? "").catch(() => "");
5405
6065
  const env = envToken();
5406
6066
  if (env !== null)
@@ -5471,7 +6131,7 @@ function revokeUrl() {
5471
6131
  // src/commands/create.ts
5472
6132
  import { mkdir as mkdir6, mkdtemp as mkdtemp2, rename as rename2, rm as rm6 } from "fs/promises";
5473
6133
  import { tmpdir as tmpdir2 } from "os";
5474
- import { join as join21, relative as relative4, resolve as resolve5 } from "path";
6134
+ import { join as join22, relative as relative4, resolve as resolve5 } from "path";
5475
6135
 
5476
6136
  // src/core/ask-provider.ts
5477
6137
  var PROVIDER_QUESTION = "Where does your organization live?";
@@ -5487,14 +6147,14 @@ async function askProvider(p) {
5487
6147
  }
5488
6148
 
5489
6149
  // 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";
6150
+ import { mkdir as mkdir4, readdir as readdir4, rm as rm5, stat as stat3 } from "fs/promises";
6151
+ import { dirname as dirname8, join as join20, relative as relative2 } from "path";
5492
6152
 
5493
6153
  // src/core/scaffold-git.ts
5494
6154
  import { execFile as execFile5 } from "child_process";
5495
6155
  import { mkdtemp, realpath as realpath2, rm as rm4, writeFile as writeFile3 } from "fs/promises";
5496
6156
  import { tmpdir } from "os";
5497
- import { dirname as dirname7, join as join18, resolve as resolve3 } from "path";
6157
+ import { dirname as dirname7, join as join19, resolve as resolve3 } from "path";
5498
6158
  import { promisify as promisify5 } from "util";
5499
6159
  var execFileP5 = promisify5(execFile5);
5500
6160
  var CREATE_SUBJECT = "chore: rness workspace context";
@@ -5582,8 +6242,8 @@ async function findScaffoldBase(rnessDir) {
5582
6242
  };
5583
6243
  }
5584
6244
  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") };
6245
+ const scratch = await mkdtemp(join19(tmpdir(), "rness-scaffold-"));
6246
+ const env = { GIT_INDEX_FILE: join19(scratch, "index") };
5587
6247
  try {
5588
6248
  await must(
5589
6249
  rnessDir,
@@ -5591,7 +6251,7 @@ async function buildScaffoldCommit(rnessDir, base, files, version) {
5591
6251
  env
5592
6252
  );
5593
6253
  for (const [i, file] of files.entries()) {
5594
- const tmp = join18(scratch, `blob-${i}`);
6254
+ const tmp = join19(scratch, `blob-${i}`);
5595
6255
  await writeFile3(tmp, file.content, "utf8");
5596
6256
  const blob = await must(rnessDir, [
5597
6257
  "hash-object",
@@ -5691,7 +6351,7 @@ async function withoutRedundantKeeps(rnessDir, files) {
5691
6351
 
5692
6352
  // src/core/new-workspace.ts
5693
6353
  async function isEmptyDir(dir) {
5694
- return (await readdir3(dir)).length === 0;
6354
+ return (await readdir4(dir)).length === 0;
5695
6355
  }
5696
6356
  async function insideWorkspace(dir) {
5697
6357
  try {
@@ -5706,8 +6366,8 @@ async function targetProblem(cwd, root, pm) {
5706
6366
  if (await insideWorkspace(cwd) || await insideWorkspace(dirname8(root)))
5707
6367
  return "already inside an rness workspace; create runs from outside one";
5708
6368
  if (!await exists(root)) return null;
5709
- if (await exists(join19(root, ".rness", "rness.json"))) {
5710
- if (!await exists(join19(root, ".rness", "node_modules")))
6369
+ if (await exists(join20(root, ".rness", "rness.json"))) {
6370
+ if (!await exists(join20(root, ".rness", "node_modules")))
5711
6371
  return `${shown} holds an rness workspace whose dependencies are not installed; cd ${shown}/.rness && ${pm} install, then ${rnessCommand()} sync`;
5712
6372
  return `${shown} already holds an rness workspace (.rness/); run ${rnessCommand()} add inside it to add repositories`;
5713
6373
  }
@@ -5730,7 +6390,7 @@ async function installContext(input) {
5730
6390
  }
5731
6391
  async function buildContext(input) {
5732
6392
  const { ui, root, shown, pm } = input;
5733
- const rnessDir = join19(root, ".rness");
6393
+ const rnessDir = join20(root, ".rness");
5734
6394
  const pmVersion = await packageManagerVersion(pm);
5735
6395
  await mkdir4(root, { recursive: true });
5736
6396
  try {
@@ -5802,7 +6462,7 @@ async function syncBlocks(ui, root, shown, inPlace, sshWorks) {
5802
6462
 
5803
6463
  // src/commands/create-blank.ts
5804
6464
  import { mkdir as mkdir5 } from "fs/promises";
5805
- import { join as join20, relative as relative3, resolve as resolve4 } from "path";
6465
+ import { join as join21, relative as relative3, resolve as resolve4 } from "path";
5806
6466
  var WORKSPACE_NAME = /^(?!\.{1,2}$)[A-Za-z0-9._][A-Za-z0-9._-]*$/;
5807
6467
  function workspaceNameError(name) {
5808
6468
  return `workspace name "${name}" may only contain letters, digits, '.', '_' and '-', not starting with '-'`;
@@ -5875,7 +6535,7 @@ async function createBlank(input) {
5875
6535
  provider: null,
5876
6536
  org: null,
5877
6537
  agents,
5878
- pulse: null,
6538
+ projects: null,
5879
6539
  repos: {},
5880
6540
  scopes: {}
5881
6541
  },
@@ -5884,8 +6544,8 @@ async function createBlank(input) {
5884
6544
  kind: "blank workspace"
5885
6545
  });
5886
6546
  if (!built) return 1;
5887
- await mkdir5(join20(root, "org"), { recursive: true });
5888
- await commitContext({ ui, rnessDir: join20(root, ".rness"), shown });
6547
+ await mkdir5(join21(root, "org"), { recursive: true });
6548
+ await commitContext({ ui, rnessDir: join21(root, ".rness"), shown });
5889
6549
  const code = await syncBlocks(
5890
6550
  ui,
5891
6551
  root,
@@ -6045,7 +6705,7 @@ async function cloneSelection(input) {
6045
6705
  { doing: `cloning org/${repo}`, git: true },
6046
6706
  () => clone(
6047
6707
  entry.url,
6048
- join21(input.root, "org", repo),
6708
+ join22(input.root, "org", repo),
6049
6709
  input.provider.credentialsFor(entry.url)
6050
6710
  ),
6051
6711
  () => ["cloned", `org/${repo}`]
@@ -6390,13 +7050,13 @@ async function createCommand(opts, deps = {}) {
6390
7050
  provider: chosenProvider,
6391
7051
  org,
6392
7052
  agents: null,
6393
- pulse: null,
7053
+ projects: null,
6394
7054
  repos: {},
6395
7055
  scopes: {}
6396
7056
  };
6397
7057
  const staged = async () => {
6398
- staging ??= await mkdtemp2(join21(tmpdir2(), "rness-join-"));
6399
- return join21(staging, ".rness");
7058
+ staging ??= await mkdtemp2(join22(tmpdir2(), "rness-join-"));
7059
+ return join22(staging, ".rness");
6400
7060
  };
6401
7061
  if (joining) {
6402
7062
  const stagedDir = await staged();
@@ -6458,7 +7118,7 @@ async function createCommand(opts, deps = {}) {
6458
7118
  });
6459
7119
  if (p.isCancel(ok) || ok !== true) return cancelled(ui);
6460
7120
  }
6461
- const rnessDir = join21(root, ".rness");
7121
+ const rnessDir = join22(root, ".rness");
6462
7122
  const install = () => installContext({ ui, pm, rnessDir, skip: opts.skipInstall === true });
6463
7123
  if (joining) {
6464
7124
  await mkdir6(root, { recursive: true });
@@ -6471,7 +7131,7 @@ async function createCommand(opts, deps = {}) {
6471
7131
  ui.line("cloned", `${shown}/.rness (joined ${org})`);
6472
7132
  if (opts.pm === void 0) pm = await workspacePackageManager(rnessDir);
6473
7133
  await install();
6474
- await mkdir6(join21(root, "org"), { recursive: true });
7134
+ await mkdir6(join22(root, "org"), { recursive: true });
6475
7135
  const failures2 = await cloneSelection({
6476
7136
  root,
6477
7137
  rnessDir,
@@ -6524,7 +7184,7 @@ ${next2.map((l) => ` ${l}`).join("\n")}
6524
7184
  kind: "new workspace"
6525
7185
  });
6526
7186
  if (!built) return 1;
6527
- await mkdir6(join21(root, "org"), { recursive: true });
7187
+ await mkdir6(join22(root, "org"), { recursive: true });
6528
7188
  const failures = await cloneSelection({
6529
7189
  root,
6530
7190
  rnessDir,
@@ -6596,7 +7256,7 @@ ${next2.map((l) => ` ${l}`).join("\n")}
6596
7256
  }
6597
7257
 
6598
7258
  // src/core/blocks.ts
6599
- import { join as join22 } from "path";
7259
+ import { join as join23 } from "path";
6600
7260
  var PROBLEM = /* @__PURE__ */ new Set(["stale", "malformed"]);
6601
7261
  async function checkBlocks({
6602
7262
  root,
@@ -6613,7 +7273,7 @@ async function checkBlocks({
6613
7273
  warnings: checks.filter((c) => !PROBLEM.has(c.status) && c.message !== null).map((c) => c.message),
6614
7274
  checks
6615
7275
  });
6616
- if (!await exists(join22(root, "org"))) return derive();
7276
+ if (!await exists(join23(root, "org"))) return derive();
6617
7277
  const targets = [
6618
7278
  { scope: null, dir: root, depth: 0, file: "AGENTS.md", label: "AGENTS.md" },
6619
7279
  { scope: null, dir: root, depth: 0, file: "CLAUDE.md", label: "CLAUDE.md" }
@@ -6622,7 +7282,7 @@ async function checkBlocks({
6622
7282
  const segments = entry.path.split("/");
6623
7283
  targets.push({
6624
7284
  scope: name,
6625
- dir: join22(root, ...segments),
7285
+ dir: join23(root, ...segments),
6626
7286
  depth: segments.length,
6627
7287
  file: "AGENTS.md",
6628
7288
  label: `${entry.path}/AGENTS.md`
@@ -6637,7 +7297,7 @@ async function checkBlocks({
6637
7297
  );
6638
7298
  continue;
6639
7299
  }
6640
- const text = await readOrNull(join22(t.dir, t.file));
7300
+ const text = await readOrNull(join23(t.dir, t.file));
6641
7301
  const lines = text === null ? [] : text.split(/\r?\n/);
6642
7302
  const found = findBlock(lines);
6643
7303
  if (found.kind === "none") {
@@ -6706,9 +7366,9 @@ async function logoutCommand() {
6706
7366
 
6707
7367
  // src/commands/upgrade.ts
6708
7368
  import { execFile as execFile6 } from "child_process";
6709
- import { mkdtemp as mkdtemp3, readFile as readFile9, rm as rm7 } from "fs/promises";
7369
+ import { mkdtemp as mkdtemp3, readFile as readFile10, rm as rm7 } from "fs/promises";
6710
7370
  import { tmpdir as tmpdir3 } from "os";
6711
- import { basename as basename5, join as join23, relative as relative5 } from "path";
7371
+ import { basename as basename5, join as join24, relative as relative5 } from "path";
6712
7372
  import { promisify as promisify6 } from "util";
6713
7373
  var execFileP6 = promisify6(execFile6);
6714
7374
  var NOTES = "https://github.com/rness-dev/rness/tree/main/packages/cli#readme";
@@ -6728,10 +7388,10 @@ async function latestVersion() {
6728
7388
  async function scaffoldFor(version, rnessDir) {
6729
7389
  if (version === VERSION) return { dir: scaffoldDir() };
6730
7390
  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")))
7391
+ const inside2 = installed === null ? null : join24(installed.dir, "scaffold");
7392
+ if (installed?.version === version && inside2 !== null && await exists(join24(inside2, "package.json")))
6733
7393
  return { dir: inside2 };
6734
- const scratch = await mkdtemp3(join23(tmpdir3(), "rness-pack-"));
7394
+ const scratch = await mkdtemp3(join24(tmpdir3(), "rness-pack-"));
6735
7395
  const cleanup = () => rm7(scratch, { recursive: true, force: true });
6736
7396
  try {
6737
7397
  const { stdout } = await execFileP6(
@@ -6746,8 +7406,8 @@ async function scaffoldFor(version, rnessDir) {
6746
7406
  { timeout: 6e4 }
6747
7407
  );
6748
7408
  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 };
7409
+ await execFileP6("tar", ["-xzf", join24(scratch, tarball), "-C", scratch]);
7410
+ return { dir: join24(scratch, "package", "scaffold"), cleanup };
6751
7411
  } catch (e) {
6752
7412
  await cleanup();
6753
7413
  throw new Error(
@@ -6759,7 +7419,7 @@ async function scaffoldFor(version, rnessDir) {
6759
7419
  async function declaredManager(rnessDir, pm) {
6760
7420
  try {
6761
7421
  const pkg = JSON.parse(
6762
- await readFile9(join23(rnessDir, "package.json"), "utf8")
7422
+ await readFile10(join24(rnessDir, "package.json"), "utf8")
6763
7423
  );
6764
7424
  if (typeof pkg.packageManager === "string") return pkg.packageManager;
6765
7425
  } catch {
@@ -6769,7 +7429,7 @@ async function declaredManager(rnessDir, pm) {
6769
7429
  async function clonesToCommit(root, cwd, manifest, files) {
6770
7430
  const clones = [];
6771
7431
  for (const repo of Object.keys(manifest.repos)) {
6772
- const dir = join23(root, "org", repo);
7432
+ const dir = join24(root, "org", repo);
6773
7433
  if (!await exists(dir)) continue;
6774
7434
  try {
6775
7435
  const paths = await changedPaths(dir, files);
@@ -6787,7 +7447,7 @@ async function filesTheSyncWrites(root, rnessDir, manifest) {
6787
7447
  try {
6788
7448
  const { stdout } = await execFileP6(
6789
7449
  process.execPath,
6790
- [join23(pinned.dir, pinned.bin), "written-files", "--cwd", root],
7450
+ [join24(pinned.dir, pinned.bin), "written-files", "--cwd", root],
6791
7451
  {
6792
7452
  cwd: root,
6793
7453
  env: { ...process.env, NO_COLOR: "1", RNESS_NO_INSTALL: "1" },
@@ -6983,9 +7643,9 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
6983
7643
  await source.cleanup?.();
6984
7644
  }
6985
7645
  }
6986
- const packageFile = join23(ws.rnessDir, "package.json");
7646
+ const packageFile = join24(ws.rnessDir, "package.json");
6987
7647
  const restore = /* @__PURE__ */ new Map([
6988
- [packageFile, await readFile9(packageFile, "utf8")]
7648
+ [packageFile, await readFile10(packageFile, "utf8")]
6989
7649
  ]);
6990
7650
  const moved = await readPin(ws.rnessDir);
6991
7651
  if (moved !== null && moved.spec !== target2)
@@ -7179,8 +7839,8 @@ import { realpath as realpath4 } from "fs/promises";
7179
7839
  import { isAbsolute as isAbsolute2, posix as posix8, relative as relative7, resolve as resolve7, sep as sep5 } from "path";
7180
7840
 
7181
7841
  // 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";
7842
+ import { readFile as readFile11, realpath as realpath3, stat as stat4 } from "fs/promises";
7843
+ import { isAbsolute, join as join25, posix as posix7, relative as relative6, resolve as resolve6, sep as sep4 } from "path";
7184
7844
  var LABELS = {
7185
7845
  standards: "Standards",
7186
7846
  adr: "Decisions",
@@ -7209,7 +7869,7 @@ async function documents(rnessDir, only) {
7209
7869
  const docs = [];
7210
7870
  for (const collection of COLLECTIONS) {
7211
7871
  if (only !== void 0 && collection !== only) continue;
7212
- for (const item of await collectMarkdown(join24(rnessDir, collection))) {
7872
+ for (const item of await collectMarkdown(join25(rnessDir, collection))) {
7213
7873
  const path = `${collection}/${item.rel}`;
7214
7874
  if (path === TEMPLATE2) continue;
7215
7875
  const status2 = item.fields?.["status"];
@@ -7331,7 +7991,7 @@ async function read(cwd, args) {
7331
7991
  return refuse(
7332
7992
  `${path}: ${info.size} bytes, over the 256 KiB rness_read returns`
7333
7993
  );
7334
- return { text: await readFile10(real, "utf8") };
7994
+ return { text: await readFile11(real, "utf8") };
7335
7995
  }
7336
7996
  async function search(cwd, args) {
7337
7997
  const { ws } = await workspaceAt(cwd);
@@ -7439,8 +8099,8 @@ function mcpTools(cwd) {
7439
8099
 
7440
8100
  // src/pulse/detached.ts
7441
8101
  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";
8102
+ import { mkdir as mkdir7, open as open2, readFile as readFile12, rm as rm8, stat as stat5, writeFile as writeFile4 } from "fs/promises";
8103
+ import { join as join26 } from "path";
7444
8104
  import { setTimeout as delay2 } from "timers/promises";
7445
8105
  var detached = (args, cwd) => {
7446
8106
  const bin = process.argv[1];
@@ -7454,7 +8114,7 @@ var detached = (args, cwd) => {
7454
8114
  child.on("error", () => void 0);
7455
8115
  child.unref();
7456
8116
  };
7457
- var failureFile = () => join25(configDir(), "pulse.json");
8117
+ var failureFile = () => join26(configDir(), "pulse.json");
7458
8118
  async function recordFailure(reason3) {
7459
8119
  try {
7460
8120
  await mkdir7(configDir(), { recursive: true, mode: 448 });
@@ -7468,7 +8128,7 @@ async function recordFailure(reason3) {
7468
8128
  }
7469
8129
  async function takeFailure() {
7470
8130
  try {
7471
- const data = JSON.parse(await readFile11(failureFile(), "utf8"));
8131
+ const data = JSON.parse(await readFile12(failureFile(), "utf8"));
7472
8132
  await rm8(failureFile(), { force: true });
7473
8133
  const reason3 = data !== null && typeof data === "object" ? data["reason"] : void 0;
7474
8134
  return typeof reason3 === "string" && reason3 !== "" ? reason3 : null;
@@ -7479,7 +8139,7 @@ async function takeFailure() {
7479
8139
  var PULSE_LOCK_STALE_MS = 15 * 6e4;
7480
8140
  async function oneSyncAtATime(key, work, options = {}) {
7481
8141
  await mkdir7(configDir(), { recursive: true, mode: 448 });
7482
- const file = join25(configDir(), `pulse-${key}.lock`);
8142
+ const file = join26(configDir(), `pulse-${key}.lock`);
7483
8143
  for (; ; ) {
7484
8144
  try {
7485
8145
  const handle = await open2(file, "wx");
@@ -7565,7 +8225,7 @@ function sessionOf(input) {
7565
8225
  }
7566
8226
  async function hasPulse(ws) {
7567
8227
  try {
7568
- return (await loadManifest(ws.rnessDir)).pulse !== null;
8228
+ return (await loadManifest(ws.rnessDir)).projects !== null;
7569
8229
  } catch {
7570
8230
  return false;
7571
8231
  }
@@ -7875,12 +8535,12 @@ async function mcpCommand(opts, io = {
7875
8535
  }
7876
8536
 
7877
8537
  // src/commands/pulse.ts
7878
- import { readFile as readFile13, readdir as readdir4 } from "fs/promises";
7879
- import { join as join26 } from "path";
8538
+ import { readFile as readFile14, readdir as readdir5 } from "fs/promises";
8539
+ import { join as join27 } from "path";
7880
8540
  import { setTimeout as delay3 } from "timers/promises";
7881
8541
 
7882
8542
  // src/pulse/opened.ts
7883
- import { mkdir as mkdir8, readFile as readFile12, rename as rename3, rm as rm9, writeFile as writeFile5 } from "fs/promises";
8543
+ import { mkdir as mkdir8, readFile as readFile13, rename as rename3, rm as rm9, writeFile as writeFile5 } from "fs/promises";
7884
8544
  import { dirname as dirname9 } from "path";
7885
8545
  var NAME2 = "rness/opened";
7886
8546
  function openedFile(rnessDir) {
@@ -7892,7 +8552,7 @@ async function readOpened(file) {
7892
8552
  if (file === null) return opened;
7893
8553
  let data;
7894
8554
  try {
7895
- data = JSON.parse(await readFile12(file, "utf8"));
8555
+ data = JSON.parse(await readFile13(file, "utf8"));
7896
8556
  } catch {
7897
8557
  return opened;
7898
8558
  }
@@ -7959,16 +8619,75 @@ async function missingAccess(provider) {
7959
8619
  const scopes = await fromGithub(provider.scopes());
7960
8620
  return scopes?.includes(PROJECT_SCOPE) === true ? null : NEEDS_SCOPE;
7961
8621
  }
7962
- async function declaredBoard(c) {
7963
- if (c.manifest.pulse === null)
8622
+ async function declaredBoards(c) {
8623
+ const projects = c.manifest.projects;
8624
+ if (projects === null)
7964
8625
  throw new Error("no pulse declared \u2014 rness pulse create");
7965
8626
  const missing = await missingAccess(c.provider);
7966
8627
  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;
8628
+ const names = Object.keys(projects).sort(
8629
+ (a, b) => Number(a === PULSE) - Number(b === PULSE)
8630
+ );
8631
+ const declared2 = [];
8632
+ for (const name of names) {
8633
+ const number = projects[name];
8634
+ const board = await fromGithub(c.provider.board(c.org, number));
8635
+ if (board === null)
8636
+ throw new Error(`GitHub has no project ${number} in ${c.org}`);
8637
+ declared2.push({ name, board });
8638
+ }
8639
+ return declared2;
8640
+ }
8641
+ var labelOf = (name) => `${name.charAt(0).toUpperCase()}${name.slice(1)}`;
8642
+ async function directoriesOf(dir) {
8643
+ try {
8644
+ return (await readdir5(dir, { withFileTypes: true })).filter(
8645
+ (e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "updates"
8646
+ ).map((e) => e.name).sort();
8647
+ } catch {
8648
+ return [];
8649
+ }
8650
+ }
8651
+ async function scopeOf(c, name, tabs, owners) {
8652
+ const within = (collection) => (path) => path.startsWith(`${collection}/`);
8653
+ if (name === PULSE) {
8654
+ const shown = tabs.filter((t) => !owners.includes(t.name));
8655
+ const elsewhere = (path) => owners.some((o) => within(o)(path));
8656
+ return {
8657
+ label: "Agent Pulse",
8658
+ want: desiredOf(shown, c.org),
8659
+ layout: layoutOf(shown),
8660
+ elsewhere,
8661
+ mine: (path) => !elsewhere(path),
8662
+ own: null
8663
+ };
8664
+ }
8665
+ const tab = tabs.find((t) => t.name === name) ?? {
8666
+ name,
8667
+ label: labelOf(name),
8668
+ rows: []
8669
+ };
8670
+ const dir = join27(c.rnessDir, name);
8671
+ const shape = await readShape(c.rnessDir, name);
8672
+ const fronts = new Map(
8673
+ (await collectMarkdown(dir)).map((item) => [
8674
+ `${name}/${item.rel}`,
8675
+ item.fields ?? {}
8676
+ ])
8677
+ );
8678
+ const want = desiredOf([tab], c.org, {
8679
+ shape,
8680
+ fronts,
8681
+ directories: await directoriesOf(dir)
8682
+ });
8683
+ return {
8684
+ label: tab.label,
8685
+ want,
8686
+ layout: layoutOf([tab], { shape, want }),
8687
+ elsewhere: () => false,
8688
+ mine: within(name),
8689
+ own: { shape, updates: await readUpdates(c.rnessDir, name) }
8690
+ };
7972
8691
  }
7973
8692
  var plural = (n) => `${n} item${n === 1 ? "" : "s"}`;
7974
8693
  var eachAdded = (ui, added) => {
@@ -7990,8 +8709,8 @@ var createdByKind = (ui, added) => {
7990
8709
  async function filesOf2(rnessDir) {
7991
8710
  const files = /* @__PURE__ */ new Set();
7992
8711
  const walk = async (rel) => {
7993
- const dir = rel === "" ? rnessDir : join26(rnessDir, ...rel.split("/"));
7994
- for (const e of await readdir4(dir, { withFileTypes: true })) {
8712
+ const dir = rel === "" ? rnessDir : join27(rnessDir, ...rel.split("/"));
8713
+ for (const e of await readdir5(dir, { withFileTypes: true })) {
7995
8714
  const path = rel === "" ? e.name : `${rel}/${e.name}`;
7996
8715
  if (e.isDirectory()) {
7997
8716
  if (e.name !== ".git" && e.name !== "node_modules") await walk(path);
@@ -8002,7 +8721,7 @@ async function filesOf2(rnessDir) {
8002
8721
  return files;
8003
8722
  }
8004
8723
  var count2 = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
8005
- function sayDone(ui, org, documents2, steps, writes, adopted) {
8724
+ function sayDone(ui, org, documents2, steps, writes, adopted, on) {
8006
8725
  const n = (kind) => steps.filter((s) => s.kind === kind).length - (kind === "create" ? adopted : 0);
8007
8726
  const reopened = steps.filter((s) => s.kind === "update" && s.reopen).length;
8008
8727
  if (n("create") > 0) ui.line("created", count2(n("create"), "issue"));
@@ -8017,6 +8736,8 @@ function sayDone(ui, org, documents2, steps, writes, adopted) {
8017
8736
  `${count2(n("convert"), "draft")} into issues of ${org}/.rness`
8018
8737
  );
8019
8738
  if (reopened > 0) ui.line("reopened", count2(reopened, "issue"));
8739
+ if (n("remove") > 0)
8740
+ ui.line("moved", `${plural(n("remove"))} to their collection's project`);
8020
8741
  const written = new Set(writes.map((w) => w.id));
8021
8742
  const bodyOnly = steps.filter(
8022
8743
  (s) => s.kind === "unchanged" && written.has(s.id)
@@ -8034,25 +8755,23 @@ function sayDone(ui, org, documents2, steps, writes, adopted) {
8034
8755
  ].filter(([, k]) => k > 0).map(([word, k]) => `${k} ${word}`);
8035
8756
  ui.line(
8036
8757
  "synced",
8037
- `${plural(documents2)}${parts.length > 0 ? `: ${parts.join(", ")}` : ""}`
8758
+ `${plural(documents2)}${on === void 0 ? "" : ` on ${on}`}${parts.length > 0 ? `: ${parts.join(", ")}` : ""}`
8038
8759
  );
8039
8760
  }
8040
- async function syncBoard(c, board, ui, sayLayout) {
8761
+ async function syncBoard(c, board, ui, sayLayout, scope) {
8041
8762
  await fromGithub(c.provider.checkIssues(c.org));
8042
- const tabs = await statusTabs(c.rnessDir);
8043
- const want = desiredOf(tabs, c.org);
8044
- sayLayout(
8045
- ui,
8046
- await fromGithub(c.provider.ensureLayout(board, layoutOf(tabs)))
8047
- );
8763
+ const { want } = scope;
8764
+ sayLayout(ui, await fromGithub(c.provider.ensureLayout(board, scope.layout)));
8048
8765
  const have = await fromGithub(c.provider.items(board));
8049
8766
  const record = await openedFile(c.rnessDir);
8050
- const opened = await readOpened(record);
8767
+ const all = await readOpened(record);
8768
+ const opened = new Map([...all].filter(([, path]) => scope.mine(path)));
8769
+ const others = [...all].filter(([, path]) => !scope.mine(path));
8051
8770
  const seen = await seenPaths(c.rnessDir, [
8052
8771
  ...unwantedPaths(want, have),
8053
8772
  ...opened.values()
8054
8773
  ]);
8055
- const steps = planSync(want, have, seen, opened);
8774
+ const steps = planSync(want, have, seen, opened, scope.elsewhere);
8056
8775
  const changes = steps.filter((s) => s.kind !== "unchanged").length;
8057
8776
  let writes = [];
8058
8777
  let made = 0;
@@ -8076,7 +8795,10 @@ async function syncBoard(c, board, ui, sayLayout) {
8076
8795
  } finally {
8077
8796
  await writeOpened(
8078
8797
  record,
8079
- stillOpened(opened, have, seen, closed, created)
8798
+ new Map([
8799
+ ...others,
8800
+ ...stillOpened(opened, have, seen, closed, created)
8801
+ ])
8080
8802
  );
8081
8803
  }
8082
8804
  const issued = issuedAfter(steps, have, placed);
@@ -8088,7 +8810,7 @@ async function syncBoard(c, board, ui, sayLayout) {
8088
8810
  w.path,
8089
8811
  issueBody({
8090
8812
  path: w.path,
8091
- text: await readFile13(join26(c.rnessDir, ...w.path.split("/")), "utf8"),
8813
+ text: await readFile14(join27(c.rnessDir, ...w.path.split("/")), "utf8"),
8092
8814
  org: c.org,
8093
8815
  numbers,
8094
8816
  files
@@ -8107,7 +8829,43 @@ async function syncBoard(c, board, ui, sayLayout) {
8107
8829
  { cause: e }
8108
8830
  );
8109
8831
  }
8110
- sayDone(ui, c.org, want.length, steps, writes, adopted);
8832
+ sayDone(
8833
+ ui,
8834
+ c.org,
8835
+ want.length,
8836
+ steps,
8837
+ writes,
8838
+ adopted,
8839
+ scope.own === null ? void 0 : scope.label
8840
+ );
8841
+ if (scope.own !== null) {
8842
+ const { shape, updates } = scope.own;
8843
+ for (const what of await fromGithub(
8844
+ c.provider.describe(board, {
8845
+ readme: shape.readme,
8846
+ description: shape.description
8847
+ })
8848
+ ))
8849
+ ui.line("wrote", `${what} of ${scope.label}`);
8850
+ for (const line3 of await fromGithub(
8851
+ c.provider.postUpdates(board, updates)
8852
+ )) {
8853
+ const [verb, ...rest] = line3.split(" ");
8854
+ ui.line(verb ?? "posted", rest.join(" "));
8855
+ }
8856
+ }
8857
+ }
8858
+ async function syncAll(c, declared2, ui, sayLayout) {
8859
+ const tabs = await statusTabs(c.rnessDir);
8860
+ const owners = declared2.map((d) => d.name).filter((n) => n !== PULSE);
8861
+ for (const { name, board } of declared2)
8862
+ await syncBoard(
8863
+ c,
8864
+ board,
8865
+ ui,
8866
+ sayLayout(name),
8867
+ await scopeOf(c, name, tabs, owners)
8868
+ );
8111
8869
  }
8112
8870
  async function pulseCreateCommand(opts, deps = {}) {
8113
8871
  try {
@@ -8115,9 +8873,20 @@ async function pulseCreateCommand(opts, deps = {}) {
8115
8873
  const ui = deps.ui ?? await makeUi(terminal);
8116
8874
  const wait = waitSaid(opts, ui);
8117
8875
  let c = await context2(opts, wait);
8118
- if (c.manifest.pulse !== null)
8876
+ const name = opts.collection ?? PULSE;
8877
+ const declared2 = c.manifest.projects?.[name];
8878
+ if (opts.collection === PULSE)
8879
+ throw new Error('"pulse" names Agent Pulse: rness pulse create');
8880
+ if (declared2 !== void 0)
8119
8881
  throw new Error(
8120
- `already declared: ${boardUrl(c.org, c.manifest.pulse.project)} \u2014 rness pulse sync`
8882
+ `already declared: ${boardUrl(c.org, declared2)} \u2014 rness pulse sync`
8883
+ );
8884
+ const tab = opts.collection === void 0 ? null : (await statusTabs(c.rnessDir)).find(
8885
+ (t) => t.name === opts.collection
8886
+ ) ?? null;
8887
+ if (opts.collection !== void 0 && tab === null)
8888
+ throw new Error(
8889
+ `${opts.collection} is no collection of .rness: none of its documents carries a status`
8121
8890
  );
8122
8891
  const detected = providerOf(c.manifest);
8123
8892
  if (!PROVIDERS[detected].available)
@@ -8155,19 +8924,26 @@ async function pulseCreateCommand(opts, deps = {}) {
8155
8924
  "checked",
8156
8925
  `${detected}, logged in as ${login}, scope ${PROJECT_SCOPE}`
8157
8926
  );
8158
- const board = await fromGithub(c.provider.createBoard(c.org));
8159
- await writeManifest(c.rnessDir, {
8927
+ const board = await fromGithub(c.provider.createBoard(c.org, tab?.label));
8928
+ const manifest = {
8160
8929
  ...c.manifest,
8161
8930
  provider: detected,
8162
- pulse: { project: board.number }
8163
- });
8164
- ui.line("created", `Agent Pulse \u2014 ${board.url}`);
8931
+ projects: { ...c.manifest.projects, [name]: board.number }
8932
+ };
8933
+ await writeManifest(c.rnessDir, manifest);
8934
+ c = { ...c, manifest };
8935
+ ui.line("created", `${tab?.label ?? "Agent Pulse"} \u2014 ${board.url}`);
8165
8936
  try {
8166
- await syncBoard(c, board, ui, createdByKind);
8937
+ await syncAll(
8938
+ c,
8939
+ await declaredBoards(c),
8940
+ ui,
8941
+ (n) => n === name ? createdByKind : eachAdded
8942
+ );
8167
8943
  } finally {
8168
8944
  ui.line(
8169
8945
  "declared",
8170
- 'pulse in .rness/rness.json \u2014 commit it: git -C .rness commit -am "chore: rness pulse"'
8946
+ `${name} in .rness/rness.json \u2014 commit it: git -C .rness commit -am "chore: rness pulse${opts.collection === void 0 ? "" : ` ${opts.collection}`}"`
8171
8947
  );
8172
8948
  }
8173
8949
  return 0;
@@ -8179,7 +8955,7 @@ async function pulseSyncCommand(opts, deps = {}) {
8179
8955
  try {
8180
8956
  const ui = deps.ui ?? await makeUi(deps.terminal ?? defaultTerminal);
8181
8957
  const c = await context2(opts, waitSaid(opts, ui));
8182
- await syncBoard(c, await declaredBoard(c), ui, eachAdded);
8958
+ await syncAll(c, await declaredBoards(c), ui, () => eachAdded);
8183
8959
  return 0;
8184
8960
  } catch (e) {
8185
8961
  return reportError(e);
@@ -8195,43 +8971,47 @@ async function pulseMarkCommand(opts) {
8195
8971
  }
8196
8972
  async function mark(opts) {
8197
8973
  const c = await context2(opts);
8198
- const board = await declaredBoard(c);
8199
- const key = `${c.org}-${board.number}`;
8974
+ const declared2 = await declaredBoards(c);
8975
+ const key = `${c.org}-${declared2.map((d) => d.board.number).join("-")}`;
8200
8976
  const lock = opts.sleep === void 0 ? {} : { sleep: opts.sleep };
8201
- const listed = () => fromGithub(c.provider.items(board, { bodies: false }));
8202
- let items = await listed();
8977
+ const syncEvery = () => syncAll(c, declared2, plainUi, () => eachAdded);
8978
+ const listed = (board) => fromGithub(c.provider.items(board, { bodies: false }));
8203
8979
  if (opts.end === true) {
8204
8980
  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
- });
8981
+ for (const { board } of declared2) {
8982
+ const ids = (await listed(board)).filter(
8983
+ (i) => i.session === opts.session || i.session?.endsWith(` \xB7 ${id}`)
8984
+ ).map((i) => i.id);
8985
+ await fromGithub(c.provider.mark(board, ids, null));
8986
+ }
8987
+ await oneSyncAtATime(key, syncEvery, { ...lock, wait: true });
8213
8988
  return 0;
8214
8989
  }
8215
- const wanted = new Set(opts.paths);
8990
+ const held = (path) => declared2.some((d) => d.name === PULSE || path.startsWith(`${d.name}/`));
8991
+ const wanted = new Set(opts.paths.filter(held));
8216
8992
  const documents2 = new Map(
8217
8993
  (await statusTabs(c.rnessDir)).flatMap(
8218
8994
  (t) => t.rows.map((r) => [r.path, r.status])
8219
8995
  )
8220
8996
  );
8997
+ let items = /* @__PURE__ */ new Map();
8998
+ const listAll = async () => {
8999
+ items = /* @__PURE__ */ new Map();
9000
+ for (const { name, board } of declared2) items.set(name, await listed(board));
9001
+ };
9002
+ await listAll();
8221
9003
  const onBoard = new Map(
8222
- items.flatMap((i) => i.path === null ? [] : [[i.path, i.status]])
9004
+ [...items.values()].flat().flatMap((i) => i.path === null ? [] : [[i.path, i.status]])
8223
9005
  );
8224
9006
  const stale = (path) => documents2.has(path) && (!onBoard.has(path) || onBoard.get(path) !== documents2.get(path));
8225
9007
  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();
9008
+ if (await oneSyncAtATime(key, syncEvery, lock)) await listAll();
9009
+ }
9010
+ for (const { name, board } of declared2) {
9011
+ const ids = (items.get(name) ?? []).filter((i) => i.path !== null && wanted.has(i.path)).map((i) => i.id);
9012
+ if (ids.length > 0)
9013
+ await fromGithub(c.provider.mark(board, ids, opts.session));
8232
9014
  }
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
9015
  return 0;
8236
9016
  }
8237
9017
 
@@ -8533,10 +9313,19 @@ function buildProgram(state) {
8533
9313
  state.code = await syncCommand(opts);
8534
9314
  });
8535
9315
  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);
9316
+ pulse.command("create").description(
9317
+ "Create the organization's Agent Pulse board, or a collection's own project, and sync"
9318
+ ).argument(
9319
+ "[collection]",
9320
+ "a directory of .rness/ whose documents get a project of their own"
9321
+ ).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) => {
9322
+ state.code = await pulseCreateCommand(
9323
+ collection === void 0 ? opts : { ...opts, collection }
9324
+ );
8538
9325
  });
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) => {
9326
+ pulse.command("sync").description(
9327
+ "Bring every declared project up to date with the documents of .rness/"
9328
+ ).addOption(new Option("--github-api <base>").hideHelp()).addOption(new Option("--cwd <dir>").hideHelp()).action(async (opts) => {
8540
9329
  state.code = await pulseSyncCommand(opts);
8541
9330
  });
8542
9331
  pulse.command("mark", { hidden: true }).requiredOption("--session <id>").option(
@@ -8766,4 +9555,4 @@ export {
8766
9555
  OWN_COMMANDS,
8767
9556
  catchUp
8768
9557
  };
8769
- //# sourceMappingURL=chunk-ABCO6D6B.js.map
9558
+ //# sourceMappingURL=chunk-X52SHBRY.js.map