@rness/cli 0.15.1 → 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.15.1";
511
+ var VERSION = "0.17.0";
490
512
 
491
513
  // src/core/pm.ts
492
514
  import { execFile as execFile2 } from "child_process";
@@ -1073,10 +1095,17 @@ function statusOf(item) {
1073
1095
  function sessionsOf(item) {
1074
1096
  const listed = item.fields?.["sessions"];
1075
1097
  if (!Array.isArray(listed)) return [];
1076
- const ids = listed.filter(
1077
- (s) => typeof s === "string" && s !== ""
1078
- );
1079
- return [...new Set(ids)];
1098
+ const entries = /* @__PURE__ */ new Map();
1099
+ for (const entry of listed) {
1100
+ const fields2 = typeof entry === "string" ? { id: entry } : entry !== null && typeof entry === "object" ? entry : {};
1101
+ const { id, agent } = fields2;
1102
+ if (typeof id !== "string" || id === "" || entries.has(id)) continue;
1103
+ entries.set(
1104
+ id,
1105
+ typeof agent === "string" && agent !== "" ? { id, agent } : { id }
1106
+ );
1107
+ }
1108
+ return [...entries.values()];
1080
1109
  }
1081
1110
  function rowOf(dir, item) {
1082
1111
  const base = posix3.basename(item.rel, ".md");
@@ -1314,6 +1343,196 @@ ${content}`;
1314
1343
  ${comment(digest(text))}`;
1315
1344
  }
1316
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
+
1317
1536
  // src/pulse/layout.ts
1318
1537
  var statusOf2 = (status2) => status2 === null || status2 === "?" ? null : status2;
1319
1538
  var LIFECYCLE = [
@@ -1387,36 +1606,90 @@ function fieldsOf(tabs) {
1387
1606
  }
1388
1607
  return fields2;
1389
1608
  }
1390
- function layoutOf(tabs) {
1391
- 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;
1392
1629
  return {
1393
- statuses: inLifecycleOrder([...fields2.values()].flatMap((f) => f.statuses)),
1394
- fields: [...fields2.values()],
1630
+ statuses: once([
1631
+ ...shape.statuses ?? [],
1632
+ ...tabs.flatMap(collectionStatuses)
1633
+ ]),
1634
+ fields: [],
1395
1635
  types: tabs.map((t) => t.label),
1396
- views: tabs.map((t) => ({
1397
- name: t.label,
1398
- type: t.label,
1399
- field: fields2.get(t.label)?.name ?? null
1400
- }))
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]))
1401
1646
  };
1402
1647
  }
1403
- function desiredOf(tabs, org) {
1404
- const fields2 = fieldsOf(tabs);
1405
- return tabs.flatMap(
1406
- (tab) => tab.rows.map((row) => ({
1407
- path: row.path,
1408
- title: `${row.id} \u2014 ${row.title}`,
1409
- body: headerOf(row.path, org),
1410
- status: statusOf2(row.status),
1411
- type: tab.label,
1412
- statusField: fields2.get(tab.label)?.name ?? null,
1413
- sessions: row.sessions?.join(", ") ?? null
1414
- }))
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
+ })
1415
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
+ }));
1416
1681
  }
1417
1682
 
1418
1683
  // src/pulse/plan.ts
1419
- 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);
1420
1693
  var rank = (i) => i.issue === null ? 2 : i.issue.open ? 0 : 1;
1421
1694
  var keptFirst = (a, b) => rank(a) - rank(b) || (a.issue?.number ?? 0) - (b.issue?.number ?? 0);
1422
1695
  var gone = (item) => item.issue === null ? { kind: "archive", id: item.id } : { kind: "close", id: item.id };
@@ -1442,7 +1715,7 @@ function unwantedPaths(want, have) {
1442
1715
  function isGone(path, item, seen, opened) {
1443
1716
  return seen.has(path) || item.issue !== null && opened.get(item.issue.number) === path;
1444
1717
  }
1445
- 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) {
1446
1719
  const byPath = /* @__PURE__ */ new Map();
1447
1720
  for (const item of have)
1448
1721
  if (item.path !== null)
@@ -1475,7 +1748,8 @@ function planSync(want, have, seen = /* @__PURE__ */ new Set(), opened = /* @__P
1475
1748
  for (const [path, item] of kept)
1476
1749
  if (!wanted.has(path))
1477
1750
  steps.push(
1478
- 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 }
1479
1753
  );
1480
1754
  return [...steps, ...surplus];
1481
1755
  }
@@ -1836,11 +2110,11 @@ async function createRepository(owner, name, options) {
1836
2110
  }
1837
2111
 
1838
2112
  // src/core/github-projects.ts
1839
- function isRecord2(v) {
2113
+ function isRecord3(v) {
1840
2114
  return v !== null && typeof v === "object";
1841
2115
  }
1842
- var rateLimited = (body) => isRecord2(body) && Array.isArray(body["errors"]) && body["errors"].some(
1843
- (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"
1844
2118
  );
1845
2119
  async function graphqlRaw(query, variables, options) {
1846
2120
  const body = await postJson(
@@ -1850,7 +2124,7 @@ async function graphqlRaw(query, variables, options) {
1850
2124
  true,
1851
2125
  rateLimited
1852
2126
  );
1853
- return isRecord2(body) ? body : {};
2127
+ return isRecord3(body) ? body : {};
1854
2128
  }
1855
2129
  function firstError(answer) {
1856
2130
  const first = answer.errors?.[0];
@@ -1926,6 +2200,7 @@ function toField(raw) {
1926
2200
  id: raw.id ?? "",
1927
2201
  databaseId,
1928
2202
  name: raw.name ?? "",
2203
+ dataType: raw.dataType ?? (raw.options === void 0 ? "TEXT" : "SINGLE_SELECT"),
1929
2204
  options: raw.options ?? null
1930
2205
  };
1931
2206
  }
@@ -1955,10 +2230,10 @@ var optionInput = (o, id) => ({
1955
2230
  color: o.color,
1956
2231
  description: ""
1957
2232
  });
1958
- var FIELD_SELECTION = `... on ProjectV2FieldCommon { id databaseId name }
2233
+ var FIELD_SELECTION = `... on ProjectV2FieldCommon { id databaseId name dataType }
1959
2234
  ... on ProjectV2SingleSelectField { options { id name color } }`;
1960
2235
  async function createField(projectId, name, kind, o) {
1961
- const single = kind !== "TEXT";
2236
+ const single = typeof kind !== "string";
1962
2237
  const d = await graphql(
1963
2238
  `mutation($projectId: ID!, $name: String!, $dataType: ProjectV2CustomFieldType!${single ? ", $options: [ProjectV2SingleSelectFieldOptionInput!]" : ""}) {
1964
2239
  createProjectV2Field(input: {
@@ -1968,7 +2243,7 @@ async function createField(projectId, name, kind, o) {
1968
2243
  {
1969
2244
  projectId,
1970
2245
  name,
1971
- dataType: single ? "SINGLE_SELECT" : "TEXT",
2246
+ dataType: single ? "SINGLE_SELECT" : kind,
1972
2247
  ...single ? { options: kind.options.map((n) => optionInput(n)) } : {}
1973
2248
  },
1974
2249
  o
@@ -1991,6 +2266,18 @@ async function setOptions(field, options, o) {
1991
2266
  );
1992
2267
  return toField(d.updateProjectV2Field.projectV2Field);
1993
2268
  }
2269
+ async function renameField(fieldId, name, o) {
2270
+ const d = await graphql(
2271
+ `mutation($fieldId: ID!, $name: String!) {
2272
+ updateProjectV2Field(input: { fieldId: $fieldId, name: $name }) {
2273
+ projectV2Field { ${FIELD_SELECTION} }
2274
+ }
2275
+ }`,
2276
+ { fieldId, name },
2277
+ o
2278
+ );
2279
+ return toField(d.updateProjectV2Field.projectV2Field);
2280
+ }
1994
2281
  async function listItems(projectId, o, { bodies = true } = {}) {
1995
2282
  const items = [];
1996
2283
  let cursor = null;
@@ -2043,6 +2330,22 @@ async function listItems(projectId, o, { bodies = true } = {}) {
2043
2330
  }
2044
2331
  }
2045
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
+ }
2046
2349
  }
2047
2350
  }
2048
2351
  }
@@ -2062,7 +2365,7 @@ async function listItems(projectId, o, { bodies = true } = {}) {
2062
2365
  const values2 = {};
2063
2366
  for (const v of n.fieldValues?.nodes ?? []) {
2064
2367
  const field = v.field?.name;
2065
- const value = v.text ?? v.name;
2368
+ const value = v.text ?? v.name ?? v.date ?? (v.number === void 0 ? void 0 : String(v.number));
2066
2369
  if (field !== void 0 && value !== void 0) values2[field] = value;
2067
2370
  }
2068
2371
  const c = n.content;
@@ -2133,7 +2436,7 @@ async function setValue(projectId, itemId, fieldId, value, o) {
2133
2436
  projectId,
2134
2437
  itemId,
2135
2438
  fieldId,
2136
- value: "text" in value ? { text: value.text } : { singleSelectOptionId: value.optionId }
2439
+ value: "optionId" in value ? { singleSelectOptionId: value.optionId } : value
2137
2440
  },
2138
2441
  o
2139
2442
  );
@@ -2332,6 +2635,142 @@ async function renameView(viewId, name, visibleFieldIds, o) {
2332
2635
  o
2333
2636
  );
2334
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
+ }
2335
2774
 
2336
2775
  // src/core/github-issues.ts
2337
2776
  async function repository(owner, name, label, o) {
@@ -2470,7 +2909,7 @@ async function reopenIssue(id, o) {
2470
2909
  o
2471
2910
  );
2472
2911
  }
2473
- async function addLabels(id, labelIds, o) {
2912
+ async function addLabels(id, labelIds2, o) {
2474
2913
  await graphql(
2475
2914
  `
2476
2915
  mutation ($id: ID!, $labelIds: [ID!]!) {
@@ -2479,7 +2918,53 @@ async function addLabels(id, labelIds, o) {
2479
2918
  }
2480
2919
  }
2481
2920
  `,
2482
- { 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 },
2483
2968
  o
2484
2969
  );
2485
2970
  }
@@ -2490,8 +2975,25 @@ var WORKING = "working";
2490
2975
  var WORKING_VIEW = "Working";
2491
2976
  var MEMORY = ".rness";
2492
2977
  var LABEL = "rness";
2978
+ var WORKING_SESSION = "Working session";
2979
+ var SESSION_HISTORY = "Session history";
2980
+ var FORMERLY = {
2981
+ [WORKING_SESSION]: "Session",
2982
+ [SESSION_HISTORY]: "Sessions"
2983
+ };
2984
+ var valueOf2 = (values2, name) => values2[name] ?? values2[FORMERLY[name] ?? ""] ?? null;
2493
2985
  var LABEL_COLOR = "5319e7";
2494
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
+ };
2495
2997
  var labelled = (labels) => labels.some((l) => l.toLowerCase() === LABEL.toLowerCase());
2496
2998
  var STATUS_FIELD = "Status";
2497
2999
  var ALL_VIEW = "All";
@@ -2523,6 +3025,8 @@ var GitHubBoards = class {
2523
3025
  #cache = /* @__PURE__ */ new Map();
2524
3026
  /** Each organization's `.rness`, as first read: its id, its Issues, its label. */
2525
3027
  #memories = /* @__PURE__ */ new Map();
3028
+ /** Each organization's `.rness` labels by name, read when a collection needs one. */
3029
+ #labels = /* @__PURE__ */ new Map();
2526
3030
  constructor(options) {
2527
3031
  this.#o = options;
2528
3032
  }
@@ -2563,10 +3067,10 @@ var GitHubBoards = class {
2563
3067
  );
2564
3068
  }
2565
3069
  /** The project only: the caller declares it before `ensureLayout` builds it. */
2566
- async createBoard(org) {
3070
+ async createBoard(org, title = PROJECT_TITLE) {
2567
3071
  const project = await createProject(
2568
3072
  await orgId(org, this.#o),
2569
- PROJECT_TITLE,
3073
+ title,
2570
3074
  this.#o
2571
3075
  );
2572
3076
  const board = { org, number: project.number, url: project.url };
@@ -2577,7 +3081,7 @@ var GitHubBoards = class {
2577
3081
  const project = await findProject(org, number, this.#o);
2578
3082
  if (project === null) return null;
2579
3083
  const board = { org, number, url: project.url };
2580
- this.#remember(board, project.id);
3084
+ if (!this.#cache.has(this.#key(board))) this.#remember(board, project.id);
2581
3085
  return board;
2582
3086
  }
2583
3087
  async ensureLayout(board, layout) {
@@ -2624,6 +3128,11 @@ var GitHubBoards = class {
2624
3128
  const text = async (name) => {
2625
3129
  const field = fields2.find((f) => f.name === name);
2626
3130
  if (field !== void 0) return field;
3131
+ const was = fields2.find((f) => f.name === FORMERLY[name]);
3132
+ if (was !== void 0) {
3133
+ added.push(`renamed field ${was.name} \u2192 ${name}`);
3134
+ return renameField(was.id, name, this.#o);
3135
+ }
2627
3136
  added.push(`field ${name}`);
2628
3137
  return createField(cache.projectId, name, "TEXT", this.#o);
2629
3138
  };
@@ -2644,9 +3153,63 @@ var GitHubBoards = class {
2644
3153
  for (const f of layout.fields)
2645
3154
  own.set(f.name, await single(f.name, "Status", f.statuses, true));
2646
3155
  const agent = await single("Agent", "Agent", [WORKING]);
2647
- const session = await text("Session");
3156
+ const session = await text(WORKING_SESSION);
2648
3157
  const path = await text("Path");
2649
- const sessions = await text("Sessions");
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
+ }
2650
3213
  const current = await views(cache.projectId, this.#o);
2651
3214
  const first = current[0];
2652
3215
  if (first !== void 0 && first.name === DEFAULT_VIEW && !current.some((v) => v.name === ALL_VIEW)) {
@@ -2679,6 +3242,19 @@ var GitHubBoards = class {
2679
3242
  this.#o
2680
3243
  );
2681
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
+ }
2682
3258
  if (!current.some((v) => v.name === WORKING_VIEW)) {
2683
3259
  added.push(`view ${WORKING_VIEW}`);
2684
3260
  await createView(
@@ -2709,6 +3285,9 @@ var GitHubBoards = class {
2709
3285
  );
2710
3286
  added.push(`label ${LABEL}`);
2711
3287
  }
3288
+ for (const name of layout.labels)
3289
+ if ((await this.#ownLabel(board.org, name)).made)
3290
+ added.push(`label ${name}`);
2712
3291
  const repository2 = `${board.org}/${MEMORY}`;
2713
3292
  const linked = await linkedRepositories(cache.projectId, this.#o);
2714
3293
  if (!linked.some((r) => r.toLowerCase() === repository2.toLowerCase())) {
@@ -2722,10 +3301,33 @@ var GitHubBoards = class {
2722
3301
  agent,
2723
3302
  session,
2724
3303
  path,
2725
- sessions
3304
+ sessions,
3305
+ ...declared2
2726
3306
  ];
2727
3307
  return added;
2728
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
+ }
2729
3331
  async items(board, options = {}) {
2730
3332
  const cache = await this.#cacheOf(board);
2731
3333
  const raw = await listItems(cache.projectId, this.#o, options);
@@ -2741,24 +3343,30 @@ var GitHubBoards = class {
2741
3343
  number: issue.number,
2742
3344
  open: issue.open,
2743
3345
  labelled: labelled(issue.labels),
2744
- body: issue.body
3346
+ body: issue.body,
3347
+ labels: [...issue.labels]
2745
3348
  },
2746
3349
  title: r.title,
2747
3350
  status: r.values[STATUS_FIELD] ?? null,
2748
3351
  collectionStatus: r.values[statusFieldName(r.values[COLLECTION_FIELD] ?? "")] ?? null,
2749
3352
  type: r.values[COLLECTION_FIELD] ?? null,
2750
3353
  agent: r.values["Agent"] ?? null,
2751
- session: r.values["Session"] ?? null,
2752
- sessions: r.values["Sessions"] ?? null
3354
+ session: valueOf2(r.values, WORKING_SESSION),
3355
+ sessions: valueOf2(r.values, SESSION_HISTORY),
3356
+ values: { ...r.values }
2753
3357
  };
2754
3358
  });
2755
3359
  }
2756
3360
  async #set(board, itemId, name, value) {
2757
3361
  const cache = await this.#cacheOf(board);
2758
- const field = (await this.#fields(board)).find((f) => f.name === name);
3362
+ const fields2 = await this.#fields(board);
3363
+ const field = fields2.find((f) => f.name === name) ?? fields2.find((f) => f.name === FORMERLY[name]);
2759
3364
  if (field === void 0) throw new Error(`the board has no field ${name}`);
2760
3365
  let sent = null;
2761
- 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 };
2762
3370
  else if (value !== null) {
2763
3371
  const option = field.options?.find((opt) => opt.name === value);
2764
3372
  if (option === void 0)
@@ -2768,13 +3376,18 @@ var GitHubBoards = class {
2768
3376
  await setValue(cache.projectId, itemId, field.id, sent, this.#o);
2769
3377
  const known = cache.known.get(itemId);
2770
3378
  if (known === void 0) return;
2771
- if (value === null) delete known.values[name];
2772
- else known.values[name] = value;
3379
+ if (value === null) delete known.values[field.name];
3380
+ else known.values[field.name] = value;
2773
3381
  }
2774
3382
  async apply(board, step2) {
2775
3383
  if (step2.kind === "unchanged") return null;
2776
3384
  const cache = await this.#cacheOf(board);
2777
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
+ }
2778
3391
  const known = await this.#known(board, step2.id);
2779
3392
  if (step2.kind === "archive" || step2.kind === "close") {
2780
3393
  if (step2.kind === "close" && known.issue?.open === true)
@@ -2805,6 +3418,7 @@ var GitHubBoards = class {
2805
3418
  labels: []
2806
3419
  };
2807
3420
  await this.#issue(known, step2.want, labelId);
3421
+ await this.#ownLabels(board.org, known, step2.want);
2808
3422
  await this.#values(board, step2.id, step2.want);
2809
3423
  return { id: step2.id, number: made.number };
2810
3424
  }
@@ -2814,6 +3428,7 @@ var GitHubBoards = class {
2814
3428
  issue.open = true;
2815
3429
  }
2816
3430
  await this.#issue(known, step2.want, labelId);
3431
+ await this.#ownLabels(board.org, known, step2.want);
2817
3432
  await this.#values(board, step2.id, step2.want);
2818
3433
  return null;
2819
3434
  }
@@ -2857,6 +3472,7 @@ var GitHubBoards = class {
2857
3472
  };
2858
3473
  cache.known.set(id2, known);
2859
3474
  await this.#issue(known, want, labelId);
3475
+ await this.#ownLabels(board.org, known, want);
2860
3476
  await this.#values(board, id2, want);
2861
3477
  return { id: id2, number: found.number, adopted: true };
2862
3478
  }
@@ -2880,6 +3496,7 @@ var GitHubBoards = class {
2880
3496
  archived: false,
2881
3497
  values: {}
2882
3498
  });
3499
+ await this.#ownLabels(board.org, cache.known.get(id), want);
2883
3500
  await this.#values(board, id, want);
2884
3501
  return { id, number: made.number };
2885
3502
  }
@@ -2895,18 +3512,45 @@ var GitHubBoards = class {
2895
3512
  known.title = want.title;
2896
3513
  }
2897
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
+ }
2898
3537
  /** Path first — an issue on the board without it is the team's — then the fields, each only when it differs. */
2899
3538
  async #values(board, id, want) {
2900
3539
  const values2 = (await this.#known(board, id)).values;
3540
+ const fields2 = await this.#fields(board);
2901
3541
  const wanted = [
2902
3542
  ["Path", want.path],
2903
3543
  [COLLECTION_FIELD, want.type],
2904
3544
  [STATUS_FIELD, want.status],
2905
3545
  ...want.statusField === null ? [] : [[want.statusField, want.status]],
2906
- ["Sessions", 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
+ )
2907
3551
  ];
2908
3552
  for (const [name, value] of wanted)
2909
- if ((values2[name] ?? null) !== value)
3553
+ if (valueOf2(values2, name) !== value)
2910
3554
  await this.#set(board, id, name, value);
2911
3555
  }
2912
3556
  async #known(board, id) {
@@ -2932,9 +3576,51 @@ var GitHubBoards = class {
2932
3576
  async mark(board, itemIds, session) {
2933
3577
  for (const id of itemIds) {
2934
3578
  await this.#set(board, id, "Agent", session === null ? null : WORKING);
2935
- await this.#set(board, id, "Session", session);
3579
+ await this.#set(board, id, WORKING_SESSION, session);
2936
3580
  }
2937
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
+ }
2938
3624
  };
2939
3625
 
2940
3626
  // src/core/github-oauth-provider.ts
@@ -3015,8 +3701,8 @@ var GitHubOAuthProvider = class {
3015
3701
  async checkIssues(org) {
3016
3702
  return this.#boards().checkIssues(org);
3017
3703
  }
3018
- async createBoard(org) {
3019
- return this.#boards().createBoard(org);
3704
+ async createBoard(org, title) {
3705
+ return this.#boards().createBoard(org, title);
3020
3706
  }
3021
3707
  async board(org, number) {
3022
3708
  return this.#boards().board(org, number);
@@ -3033,6 +3719,12 @@ var GitHubOAuthProvider = class {
3033
3719
  async mark(board, itemIds, session) {
3034
3720
  return this.#boards().mark(board, itemIds, session);
3035
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
+ }
3036
3728
  credentialsFor(url) {
3037
3729
  if (this.#token === null || !url.startsWith(GITHUB_HTTPS)) return null;
3038
3730
  return { username: "x-access-token", password: this.#token.token };
@@ -3047,11 +3739,11 @@ async function githubProvider(apiBase, wait) {
3047
3739
  }
3048
3740
 
3049
3741
  // src/core/repos.ts
3050
- import { glob as glob2, readFile as readFile5, rm as rm3 } from "fs/promises";
3051
- 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";
3052
3744
  async function addRepository(input) {
3053
3745
  const { name, url } = parseRepoSpec(input.spec, input.org, input.host);
3054
- const dir = join8(input.root, "org", name);
3746
+ const dir = join9(input.root, "org", name);
3055
3747
  const repoPath = `org/${name}`;
3056
3748
  const current = input.manifest.scopes[name];
3057
3749
  if (current !== void 0 && current.path !== repoPath)
@@ -3094,7 +3786,7 @@ async function addRepository(input) {
3094
3786
  }
3095
3787
  const declared2 = [name];
3096
3788
  for (const { path, base } of subs) {
3097
- if (!await exists(join8(input.root, ...path.split("/")))) {
3789
+ if (!await exists(join9(input.root, ...path.split("/")))) {
3098
3790
  if (action === "cloned") await rm3(dir, { recursive: true, force: true });
3099
3791
  throw new Error(`${path} does not exist`);
3100
3792
  }
@@ -3112,7 +3804,7 @@ async function workspaceDirs(repoDir) {
3112
3804
  let patterns = [];
3113
3805
  try {
3114
3806
  const pkg = JSON.parse(
3115
- await readFile5(join8(repoDir, "package.json"), "utf8")
3807
+ await readFile6(join9(repoDir, "package.json"), "utf8")
3116
3808
  );
3117
3809
  const w = pkg.workspaces;
3118
3810
  if (Array.isArray(w))
@@ -3129,7 +3821,7 @@ async function workspaceDirs(repoDir) {
3129
3821
  for (const pattern of patterns) {
3130
3822
  for await (const match of glob2(pattern, { cwd: repoDir })) {
3131
3823
  const rel = match.split(sep2).join(posix5.sep);
3132
- if (await exists(join8(repoDir, ...rel.split(posix5.sep), "package.json")))
3824
+ if (await exists(join9(repoDir, ...rel.split(posix5.sep), "package.json")))
3133
3825
  dirs.add(rel);
3134
3826
  }
3135
3827
  }
@@ -3380,8 +4072,8 @@ function httpsFlagLine(spec) {
3380
4072
  }
3381
4073
 
3382
4074
  // src/core/workspace.ts
3383
- import { readdir as readdir2 } from "fs/promises";
3384
- 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";
3385
4077
  async function findWorkspace(startDir) {
3386
4078
  const start = resolve2(startDir);
3387
4079
  const { root: fsRoot } = parse2(start);
@@ -3391,18 +4083,18 @@ async function findWorkspace(startDir) {
3391
4083
  if (dir === fsRoot) break;
3392
4084
  }
3393
4085
  for (const dir of ancestors) {
3394
- if (await exists(join9(dir, ".rness", "rness.json")))
3395
- return { root: dir, rnessDir: join9(dir, ".rness") };
4086
+ if (await exists(join10(dir, ".rness", "rness.json")))
4087
+ return { root: dir, rnessDir: join10(dir, ".rness") };
3396
4088
  }
3397
4089
  for (const dir of ancestors) {
3398
- if (await exists(join9(dir, "rness.json")))
4090
+ if (await exists(join10(dir, "rness.json")))
3399
4091
  return { root: dirname3(dir), rnessDir: dir };
3400
4092
  }
3401
4093
  throw new Error(`no rness workspace found above ${startDir}`);
3402
4094
  }
3403
4095
  async function clonesIn(root) {
3404
4096
  try {
3405
- const entries = await readdir2(join9(root, "org"), { withFileTypes: true });
4097
+ const entries = await readdir3(join10(root, "org"), { withFileTypes: true });
3406
4098
  return new Set(
3407
4099
  entries.filter((e) => e.isDirectory() && isWorkspaceClone(e.name)).map((e) => e.name)
3408
4100
  );
@@ -3426,15 +4118,15 @@ function reportError(e) {
3426
4118
  }
3427
4119
 
3428
4120
  // src/core/delegate.ts
3429
- import { readFile as readFile6 } from "fs/promises";
3430
- import { join as join10 } from "path";
4121
+ import { readFile as readFile7 } from "fs/promises";
4122
+ import { join as join11 } from "path";
3431
4123
  var DEFAULT_BIN = "dist/bin/rness.js";
3432
4124
  var PINNED_BIN = `node_modules/@rness/cli/${DEFAULT_BIN}`;
3433
4125
  async function readPinnedCli(rnessDir) {
3434
- const dir = join10(rnessDir, "node_modules", "@rness", "cli");
4126
+ const dir = join11(rnessDir, "node_modules", "@rness", "cli");
3435
4127
  let pkg;
3436
4128
  try {
3437
- pkg = JSON.parse(await readFile6(join10(dir, "package.json"), "utf8"));
4129
+ pkg = JSON.parse(await readFile7(join11(dir, "package.json"), "utf8"));
3438
4130
  } catch {
3439
4131
  return null;
3440
4132
  }
@@ -3454,7 +4146,7 @@ async function readPinnedCli(rnessDir) {
3454
4146
  async function pinnedBinPath(rnessDir) {
3455
4147
  const pinned = await readPinnedCli(rnessDir);
3456
4148
  if (pinned === null) return null;
3457
- return join10(pinned.dir, ...pinned.bin.split("/"));
4149
+ return join11(pinned.dir, ...pinned.bin.split("/"));
3458
4150
  }
3459
4151
  async function findDelegate(cwd, ownVersion) {
3460
4152
  let root;
@@ -3470,7 +4162,7 @@ async function findDelegate(cwd, ownVersion) {
3470
4162
  if (pinned === null) return null;
3471
4163
  const { version } = pinned;
3472
4164
  if (version === null || version === ownVersion) return null;
3473
- return { entry: join10(pinned.dir, "dist", "index.js"), version, root };
4165
+ return { entry: join11(pinned.dir, "dist", "index.js"), version, root };
3474
4166
  }
3475
4167
 
3476
4168
  // src/core/block.ts
@@ -3537,7 +4229,7 @@ function isCurrentBlock(blockLines, freshHash) {
3537
4229
  }
3538
4230
 
3539
4231
  // src/core/merge.ts
3540
- import { join as join11 } from "path";
4232
+ import { join as join12 } from "path";
3541
4233
  function findBlock(lines) {
3542
4234
  const begins = [];
3543
4235
  const ends = [];
@@ -3596,7 +4288,7 @@ function claudeMdAtRoot(existing, block) {
3596
4288
  return merged.ok ? { ...merged, changed: merged.text !== existing } : merged;
3597
4289
  }
3598
4290
  async function ensureClaudeMd(dir) {
3599
- const file = join11(dir, "CLAUDE.md");
4291
+ const file = join12(dir, "CLAUDE.md");
3600
4292
  if (await isSymlink(file)) return "unchanged";
3601
4293
  const existing = await readOrNull(file);
3602
4294
  if (existing === null) {
@@ -3611,11 +4303,11 @@ async function ensureClaudeMd(dir) {
3611
4303
  }
3612
4304
 
3613
4305
  // src/commands/sync.ts
3614
- import { join as join14 } from "path";
4306
+ import { join as join15 } from "path";
3615
4307
 
3616
4308
  // src/core/agent-targets.ts
3617
4309
  import { mkdir as mkdir2 } from "fs/promises";
3618
- import { dirname as dirname4, join as join13 } from "path";
4310
+ import { dirname as dirname4, join as join14 } from "path";
3619
4311
 
3620
4312
  // src/core/claude-skills.ts
3621
4313
  var PLUGIN = ".claude/skills/rness";
@@ -3681,8 +4373,10 @@ Writing:
3681
4373
  \`org/\` it affects.
3682
4374
  - An ADR it replaces is named in Context. That one becomes \`Superseded\`,
3683
4375
  with \`superseded_by:\`, only once the new ADR is \`Accepted\`.
3684
- - Add \`\${CLAUDE_SESSION_ID}\` at the end of \`sessions:\` in the front matter,
3685
- unless it is already there.
4376
+ - Add \`{ id: \${CLAUDE_SESSION_ID}, agent: <model> }\` at the end of
4377
+ \`sessions:\` in the front matter, unless that id is already there.
4378
+ \`<model>\` is the model you run as, as Claude Code names it, after
4379
+ \`Claude\`: for example \`Claude Opus 5.5\`.
3686
4380
  - The rness hook checks every edit under \`${rness}/\`: fix what it reports.
3687
4381
 
3688
4382
  After writing: give the path and the Decision section, and say the ADR
@@ -3720,8 +4414,10 @@ Writing:
3720
4414
  - What was checked in this session (a document read, a command run) is
3721
4415
  written as verified, with the date; everything else as a proposal or an
3722
4416
  open question.
3723
- - Add \`\${CLAUDE_SESSION_ID}\` at the end of \`sessions:\` in the front matter,
3724
- unless it is already there.
4417
+ - Add \`{ id: \${CLAUDE_SESSION_ID}, agent: <model> }\` at the end of
4418
+ \`sessions:\` in the front matter, unless that id is already there.
4419
+ \`<model>\` is the model you run as, as Claude Code names it, after
4420
+ \`Claude\`: for example \`Claude Opus 5.5\`.
3725
4421
  - The rness hook checks every edit under \`${rness}/\`: fix what it reports.
3726
4422
 
3727
4423
  After writing: give the path and the summary. It stays \`Draft\` until the
@@ -3762,8 +4458,10 @@ Writing:
3762
4458
  repository has one.
3763
4459
  - Where the plan decides something the specification leaves open, a
3764
4460
  section says so.
3765
- - Add \`\${CLAUDE_SESSION_ID}\` at the end of \`sessions:\` in the front matter,
3766
- unless it is already there.
4461
+ - Add \`{ id: \${CLAUDE_SESSION_ID}, agent: <model> }\` at the end of
4462
+ \`sessions:\` in the front matter, unless that id is already there.
4463
+ \`<model>\` is the model you run as, as Claude Code names it, after
4464
+ \`Claude\`: for example \`Claude Opus 5.5\`.
3767
4465
  - The rness hook checks every edit under \`${rness}/\`: fix what it reports.
3768
4466
 
3769
4467
  After writing: give the path and the list of tasks. It becomes \`Ready\` when
@@ -3793,8 +4491,11 @@ Arguments: \`$ARGUMENTS\`.
3793
4491
  ran, what it showed, what could not run and why;
3794
4492
  - the specification: \`Implemented\` once every plan of it is
3795
4493
  \`Completed\`; \`updated\` today;
3796
- - the plan and the specification: add \`\${CLAUDE_SESSION_ID}\` at the end
3797
- of \`sessions:\` in their front matter, unless it is already there;
4494
+ - the plan and the specification: add
4495
+ \`{ id: \${CLAUDE_SESSION_ID}, agent: <model> }\` at the end of
4496
+ \`sessions:\` in their front matter, unless that id is already there;
4497
+ \`<model>\` is the model you run as, as Claude Code names it, after
4498
+ \`Claude\`: for example \`Claude Opus 5.5\`;
3798
4499
  - the smallest set of documents the work makes inaccurate: \`${rness}/docs/\`,
3799
4500
  the READMEs of the repositories it changed.
3800
4501
  6. A task not proven: the plan stays \`In progress\` \u2014 or \`Blocked\`, the
@@ -4051,8 +4752,8 @@ function describeGuarantee(g) {
4051
4752
 
4052
4753
  // src/core/pinned.ts
4053
4754
  import { execFile as execFile3 } from "child_process";
4054
- import { readFile as readFile7 } from "fs/promises";
4055
- 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";
4056
4757
  import { promisify as promisify3 } from "util";
4057
4758
  var execFileP3 = promisify3(execFile3);
4058
4759
  var PACKAGE = "@rness/cli";
@@ -4077,7 +4778,7 @@ function compareVersions(a, b) {
4077
4778
  async function readPackageJson(rnessDir) {
4078
4779
  try {
4079
4780
  const parsed = JSON.parse(
4080
- await readFile7(join12(rnessDir, "package.json"), "utf8")
4781
+ await readFile8(join13(rnessDir, "package.json"), "utf8")
4081
4782
  );
4082
4783
  return parsed !== null && typeof parsed === "object" ? parsed : null;
4083
4784
  } catch {
@@ -4096,8 +4797,8 @@ async function readPin(rnessDir) {
4096
4797
  return null;
4097
4798
  }
4098
4799
  async function writePin(rnessDir, from, to) {
4099
- const file = join12(rnessDir, "package.json");
4100
- const text = await readFile7(file, "utf8");
4800
+ const file = join13(rnessDir, "package.json");
4801
+ const text = await readFile8(file, "utf8");
4101
4802
  const entry = new RegExp(
4102
4803
  `("${PACKAGE}"\\s*:\\s*")${from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(")`
4103
4804
  );
@@ -4119,12 +4820,12 @@ async function workspacePackageManager(rnessDir) {
4119
4820
  if (isPackageManager(name)) return name;
4120
4821
  }
4121
4822
  for (const [lockfile, pm] of LOCKFILES)
4122
- if (await exists(join12(rnessDir, lockfile))) return pm;
4823
+ if (await exists(join13(rnessDir, lockfile))) return pm;
4123
4824
  return "npm";
4124
4825
  }
4125
4826
  async function hasLockfile(rnessDir, pm) {
4126
4827
  for (const [lockfile, owner] of LOCKFILES)
4127
- if (owner === pm && await exists(join12(rnessDir, lockfile))) return true;
4828
+ if (owner === pm && await exists(join13(rnessDir, lockfile))) return true;
4128
4829
  return false;
4129
4830
  }
4130
4831
  async function pinDrift(rnessDir) {
@@ -4180,9 +4881,9 @@ async function ensureInstalled(rnessDir, opts = {}) {
4180
4881
  }
4181
4882
  }
4182
4883
  async function runPinnedSync(root) {
4183
- const installed = await readPinnedCli(join12(root, ".rness"));
4884
+ const installed = await readPinnedCli(join13(root, ".rness"));
4184
4885
  if (installed === null) return null;
4185
- const pinned = join12(installed.dir, ...installed.bin.split("/"));
4886
+ const pinned = join13(installed.dir, ...installed.bin.split("/"));
4186
4887
  if (!await exists(pinned))
4187
4888
  return {
4188
4889
  code: 1,
@@ -4257,7 +4958,7 @@ function syncOutcome(result, opts = {}) {
4257
4958
  };
4258
4959
  }
4259
4960
  async function syncPinned(root, shown, fallback) {
4260
- const rnessDir = join12(root, ".rness");
4961
+ const rnessDir = join13(root, ".rness");
4261
4962
  const installed = await readPinnedCli(rnessDir);
4262
4963
  if (installed === null) {
4263
4964
  if (fallback !== void 0) return fallback();
@@ -4267,7 +4968,7 @@ async function syncPinned(root, shown, fallback) {
4267
4968
  );
4268
4969
  return 1;
4269
4970
  }
4270
- const pinned = join12(installed.dir, ...installed.bin.split("/"));
4971
+ const pinned = join13(installed.dir, ...installed.bin.split("/"));
4271
4972
  if (!await exists(pinned)) {
4272
4973
  process.stderr.write(
4273
4974
  `@rness/cli in ${shown}/.rness is not installed correctly (missing ${installed.bin}); reinstall in .rness/
@@ -4307,30 +5008,30 @@ function wholeDiff(existing, content) {
4307
5008
  }
4308
5009
  async function contextOf(root) {
4309
5010
  return {
4310
- packageManager: await workspacePackageManager(join13(root, ".rness"))
5011
+ packageManager: await workspacePackageManager(join14(root, ".rness"))
4311
5012
  };
4312
5013
  }
4313
5014
  var PATHS_ONLY = { packageManager: "npm" };
4314
5015
  async function filesOf(root, manifest, targets) {
4315
5016
  const files = [];
4316
- if (!await exists(join13(root, "org"))) return files;
5017
+ if (!await exists(join14(root, "org"))) return files;
4317
5018
  const ctx = await contextOf(root);
4318
5019
  for (const target2 of targets)
4319
5020
  for (const owned of target2.files(ctx))
4320
5021
  if (owned.at === "root")
4321
5022
  files.push({
4322
5023
  ...owned,
4323
- file: join13(root, ...owned.file.split("/")),
5024
+ file: join14(root, ...owned.file.split("/")),
4324
5025
  label: owned.file
4325
5026
  });
4326
5027
  for (const repo of Object.keys(manifest.repos)) {
4327
- if (!await exists(join13(root, "org", repo))) continue;
5028
+ if (!await exists(join14(root, "org", repo))) continue;
4328
5029
  for (const target2 of targets)
4329
5030
  for (const owned of target2.files(ctx))
4330
5031
  if (owned.at === "clones")
4331
5032
  files.push({
4332
5033
  ...owned,
4333
- file: join13(root, "org", repo, ...owned.file.split("/")),
5034
+ file: join14(root, "org", repo, ...owned.file.split("/")),
4334
5035
  label: `org/${repo}/${owned.file}`
4335
5036
  });
4336
5037
  }
@@ -4654,7 +5355,7 @@ function targetsOf(manifest, root, only) {
4654
5355
  const segments = entry.path.split("/");
4655
5356
  targets.push({
4656
5357
  scope: name,
4657
- dir: join14(root, ...segments),
5358
+ dir: join15(root, ...segments),
4658
5359
  depth: segments.length,
4659
5360
  label: `${entry.path}/AGENTS.md`
4660
5361
  });
@@ -4813,7 +5514,7 @@ async function syncCommand(opts, deps = {}) {
4813
5514
  try {
4814
5515
  const notCloned = /* @__PURE__ */ new Set();
4815
5516
  for (const [name, repo] of Object.entries(manifest.repos)) {
4816
- const dir = join14(ws.root, "org", name);
5517
+ const dir = join15(ws.root, "org", name);
4817
5518
  const label = `org/${name}`;
4818
5519
  if (!await exists(dir)) {
4819
5520
  if (!toClone.has(name)) {
@@ -4865,7 +5566,7 @@ async function syncCommand(opts, deps = {}) {
4865
5566
  lines.push(
4866
5567
  `not in rness.json: ${undeclared.join(", ")} (${rnessCommand()} add <name> declares it)`
4867
5568
  );
4868
- if (!await exists(join14(ws.root, "org"))) {
5569
+ if (!await exists(join15(ws.root, "org"))) {
4869
5570
  lines.push(["skipped", "blocks (no org/ directory here)"]);
4870
5571
  } else {
4871
5572
  let differences = 0;
@@ -4877,7 +5578,7 @@ async function syncCommand(opts, deps = {}) {
4877
5578
  continue;
4878
5579
  }
4879
5580
  const [agentsLinked, claudeLinked] = await Promise.all(
4880
- BLOCK_FILES.map((f) => isSymlink(join14(t.dir, f)))
5581
+ BLOCK_FILES.map((f) => isSymlink(join15(t.dir, f)))
4881
5582
  );
4882
5583
  const atRoot = t.scope === null;
4883
5584
  if (!atRoot && (agentsLinked === true || claudeLinked === true)) {
@@ -4900,7 +5601,7 @@ async function syncCommand(opts, deps = {}) {
4900
5601
  `${t.label}: block is ${Math.round(block.bytes / 1024)} KB (over 32 KB)`
4901
5602
  );
4902
5603
  }
4903
- 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);
4904
5605
  if (typeof agents === "object") {
4905
5606
  problems.push(`${t.label}: ${agents.error}`);
4906
5607
  continue;
@@ -4915,7 +5616,7 @@ async function syncCommand(opts, deps = {}) {
4915
5616
  }
4916
5617
  if (claudeLinked === true) continue;
4917
5618
  const claude = await syncBlockFile(
4918
- join14(t.dir, "CLAUDE.md"),
5619
+ join15(t.dir, "CLAUDE.md"),
4919
5620
  block,
4920
5621
  check,
4921
5622
  true
@@ -4972,7 +5673,7 @@ async function syncCommand(opts, deps = {}) {
4972
5673
  }
4973
5674
 
4974
5675
  // src/commands/add.ts
4975
- import { basename as basename4, join as join15 } from "path";
5676
+ import { basename as basename4, join as join16 } from "path";
4976
5677
  function printDeclared(ui, result, already) {
4977
5678
  for (const name of result.declared) {
4978
5679
  if (already.has(name)) continue;
@@ -5065,7 +5766,7 @@ async function addCommand(spec, opts, deps = {}) {
5065
5766
  }
5066
5767
  parsed = parseRepoSpec(spec, org, host);
5067
5768
  const scopes = (opts.scopes ?? "").split(",").map((s) => s.trim()).filter((s) => s !== "");
5068
- const dir = join15(ws.root, "org", parsed.name);
5769
+ const dir = join16(ws.root, "org", parsed.name);
5069
5770
  const present = await exists(dir);
5070
5771
  if (interactive) {
5071
5772
  const { confirm, isCancel } = await terminal.prompts();
@@ -5193,7 +5894,7 @@ async function contextCommand(opts) {
5193
5894
 
5194
5895
  // src/core/scaffold.ts
5195
5896
  import { statSync } from "fs";
5196
- import { dirname as dirname5, join as join16 } from "path";
5897
+ import { dirname as dirname5, join as join17 } from "path";
5197
5898
  import { fileURLToPath } from "url";
5198
5899
  var SCAFFOLD_FILES = [
5199
5900
  "AGENTS.md",
@@ -5217,9 +5918,9 @@ var SCAFFOLD_FILES = [
5217
5918
  function scaffoldDir() {
5218
5919
  let dir = dirname5(fileURLToPath(import.meta.url));
5219
5920
  for (let i = 0; i < 4; i += 1) {
5220
- const candidate = join16(dir, "scaffold");
5921
+ const candidate = join17(dir, "scaffold");
5221
5922
  try {
5222
- if (statSync(join16(candidate, "rness.json")).isFile()) return candidate;
5923
+ if (statSync(join17(candidate, "rness.json")).isFile()) return candidate;
5223
5924
  } catch {
5224
5925
  }
5225
5926
  dir = dirname5(dir);
@@ -5228,15 +5929,15 @@ function scaffoldDir() {
5228
5929
  }
5229
5930
 
5230
5931
  // src/core/scaffold-copy.ts
5231
- import { chmod as chmod2, mkdir as mkdir3, readFile as readFile8, stat as stat2, writeFile as writeFile2 } from "fs/promises";
5232
- 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";
5233
5934
  var TOKENISED = /* @__PURE__ */ new Set(["package.json"]);
5234
5935
  async function renderScaffold(source, tokens) {
5235
5936
  const files = [];
5236
5937
  for (const rel of SCAFFOLD_FILES) {
5237
5938
  if (rel === "rness.json") continue;
5238
- const from = join17(source, ...rel.split("/"));
5239
- let content = await readFile8(from, "utf8");
5939
+ const from = join18(source, ...rel.split("/"));
5940
+ let content = await readFile9(from, "utf8");
5240
5941
  if (TOKENISED.has(rel)) {
5241
5942
  content = content.replaceAll("__RNESS_VERSION__", tokens.version).replaceAll("__RNESS_PM__", tokens.packageManager);
5242
5943
  }
@@ -5250,7 +5951,7 @@ async function renderScaffold(source, tokens) {
5250
5951
  }
5251
5952
  async function copyScaffold(dest, tokens) {
5252
5953
  for (const file of await renderScaffold(scaffoldDir(), tokens)) {
5253
- const to = join17(dest, ...file.path.split("/"));
5954
+ const to = join18(dest, ...file.path.split("/"));
5254
5955
  await mkdir3(dirname6(to), { recursive: true });
5255
5956
  await writeFile2(to, file.content, "utf8");
5256
5957
  if (file.executable) await chmod2(to, 493);
@@ -5359,7 +6060,7 @@ async function loginCommand(opts, deps = {}) {
5359
6060
  ui.error(unsupportedProviderMessage(manifest.provider));
5360
6061
  return 1;
5361
6062
  }
5362
- const needsProject = opts.project === true || manifest?.pulse != null;
6063
+ const needsProject = opts.project === true || manifest?.projects != null;
5363
6064
  const bin = deps.bin ?? await realpath(process.argv[1] ?? "").catch(() => "");
5364
6065
  const env = envToken();
5365
6066
  if (env !== null)
@@ -5430,7 +6131,7 @@ function revokeUrl() {
5430
6131
  // src/commands/create.ts
5431
6132
  import { mkdir as mkdir6, mkdtemp as mkdtemp2, rename as rename2, rm as rm6 } from "fs/promises";
5432
6133
  import { tmpdir as tmpdir2 } from "os";
5433
- 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";
5434
6135
 
5435
6136
  // src/core/ask-provider.ts
5436
6137
  var PROVIDER_QUESTION = "Where does your organization live?";
@@ -5446,14 +6147,14 @@ async function askProvider(p) {
5446
6147
  }
5447
6148
 
5448
6149
  // src/core/new-workspace.ts
5449
- import { mkdir as mkdir4, readdir as readdir3, rm as rm5, stat as stat3 } from "fs/promises";
5450
- 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";
5451
6152
 
5452
6153
  // src/core/scaffold-git.ts
5453
6154
  import { execFile as execFile5 } from "child_process";
5454
6155
  import { mkdtemp, realpath as realpath2, rm as rm4, writeFile as writeFile3 } from "fs/promises";
5455
6156
  import { tmpdir } from "os";
5456
- 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";
5457
6158
  import { promisify as promisify5 } from "util";
5458
6159
  var execFileP5 = promisify5(execFile5);
5459
6160
  var CREATE_SUBJECT = "chore: rness workspace context";
@@ -5541,8 +6242,8 @@ async function findScaffoldBase(rnessDir) {
5541
6242
  };
5542
6243
  }
5543
6244
  async function buildScaffoldCommit(rnessDir, base, files, version) {
5544
- const scratch = await mkdtemp(join18(tmpdir(), "rness-scaffold-"));
5545
- 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") };
5546
6247
  try {
5547
6248
  await must(
5548
6249
  rnessDir,
@@ -5550,7 +6251,7 @@ async function buildScaffoldCommit(rnessDir, base, files, version) {
5550
6251
  env
5551
6252
  );
5552
6253
  for (const [i, file] of files.entries()) {
5553
- const tmp = join18(scratch, `blob-${i}`);
6254
+ const tmp = join19(scratch, `blob-${i}`);
5554
6255
  await writeFile3(tmp, file.content, "utf8");
5555
6256
  const blob = await must(rnessDir, [
5556
6257
  "hash-object",
@@ -5650,7 +6351,7 @@ async function withoutRedundantKeeps(rnessDir, files) {
5650
6351
 
5651
6352
  // src/core/new-workspace.ts
5652
6353
  async function isEmptyDir(dir) {
5653
- return (await readdir3(dir)).length === 0;
6354
+ return (await readdir4(dir)).length === 0;
5654
6355
  }
5655
6356
  async function insideWorkspace(dir) {
5656
6357
  try {
@@ -5665,8 +6366,8 @@ async function targetProblem(cwd, root, pm) {
5665
6366
  if (await insideWorkspace(cwd) || await insideWorkspace(dirname8(root)))
5666
6367
  return "already inside an rness workspace; create runs from outside one";
5667
6368
  if (!await exists(root)) return null;
5668
- if (await exists(join19(root, ".rness", "rness.json"))) {
5669
- 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")))
5670
6371
  return `${shown} holds an rness workspace whose dependencies are not installed; cd ${shown}/.rness && ${pm} install, then ${rnessCommand()} sync`;
5671
6372
  return `${shown} already holds an rness workspace (.rness/); run ${rnessCommand()} add inside it to add repositories`;
5672
6373
  }
@@ -5689,7 +6390,7 @@ async function installContext(input) {
5689
6390
  }
5690
6391
  async function buildContext(input) {
5691
6392
  const { ui, root, shown, pm } = input;
5692
- const rnessDir = join19(root, ".rness");
6393
+ const rnessDir = join20(root, ".rness");
5693
6394
  const pmVersion = await packageManagerVersion(pm);
5694
6395
  await mkdir4(root, { recursive: true });
5695
6396
  try {
@@ -5761,7 +6462,7 @@ async function syncBlocks(ui, root, shown, inPlace, sshWorks) {
5761
6462
 
5762
6463
  // src/commands/create-blank.ts
5763
6464
  import { mkdir as mkdir5 } from "fs/promises";
5764
- 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";
5765
6466
  var WORKSPACE_NAME = /^(?!\.{1,2}$)[A-Za-z0-9._][A-Za-z0-9._-]*$/;
5766
6467
  function workspaceNameError(name) {
5767
6468
  return `workspace name "${name}" may only contain letters, digits, '.', '_' and '-', not starting with '-'`;
@@ -5834,7 +6535,7 @@ async function createBlank(input) {
5834
6535
  provider: null,
5835
6536
  org: null,
5836
6537
  agents,
5837
- pulse: null,
6538
+ projects: null,
5838
6539
  repos: {},
5839
6540
  scopes: {}
5840
6541
  },
@@ -5843,8 +6544,8 @@ async function createBlank(input) {
5843
6544
  kind: "blank workspace"
5844
6545
  });
5845
6546
  if (!built) return 1;
5846
- await mkdir5(join20(root, "org"), { recursive: true });
5847
- 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 });
5848
6549
  const code = await syncBlocks(
5849
6550
  ui,
5850
6551
  root,
@@ -6004,7 +6705,7 @@ async function cloneSelection(input) {
6004
6705
  { doing: `cloning org/${repo}`, git: true },
6005
6706
  () => clone(
6006
6707
  entry.url,
6007
- join21(input.root, "org", repo),
6708
+ join22(input.root, "org", repo),
6008
6709
  input.provider.credentialsFor(entry.url)
6009
6710
  ),
6010
6711
  () => ["cloned", `org/${repo}`]
@@ -6349,13 +7050,13 @@ async function createCommand(opts, deps = {}) {
6349
7050
  provider: chosenProvider,
6350
7051
  org,
6351
7052
  agents: null,
6352
- pulse: null,
7053
+ projects: null,
6353
7054
  repos: {},
6354
7055
  scopes: {}
6355
7056
  };
6356
7057
  const staged = async () => {
6357
- staging ??= await mkdtemp2(join21(tmpdir2(), "rness-join-"));
6358
- return join21(staging, ".rness");
7058
+ staging ??= await mkdtemp2(join22(tmpdir2(), "rness-join-"));
7059
+ return join22(staging, ".rness");
6359
7060
  };
6360
7061
  if (joining) {
6361
7062
  const stagedDir = await staged();
@@ -6417,7 +7118,7 @@ async function createCommand(opts, deps = {}) {
6417
7118
  });
6418
7119
  if (p.isCancel(ok) || ok !== true) return cancelled(ui);
6419
7120
  }
6420
- const rnessDir = join21(root, ".rness");
7121
+ const rnessDir = join22(root, ".rness");
6421
7122
  const install = () => installContext({ ui, pm, rnessDir, skip: opts.skipInstall === true });
6422
7123
  if (joining) {
6423
7124
  await mkdir6(root, { recursive: true });
@@ -6430,7 +7131,7 @@ async function createCommand(opts, deps = {}) {
6430
7131
  ui.line("cloned", `${shown}/.rness (joined ${org})`);
6431
7132
  if (opts.pm === void 0) pm = await workspacePackageManager(rnessDir);
6432
7133
  await install();
6433
- await mkdir6(join21(root, "org"), { recursive: true });
7134
+ await mkdir6(join22(root, "org"), { recursive: true });
6434
7135
  const failures2 = await cloneSelection({
6435
7136
  root,
6436
7137
  rnessDir,
@@ -6483,7 +7184,7 @@ ${next2.map((l) => ` ${l}`).join("\n")}
6483
7184
  kind: "new workspace"
6484
7185
  });
6485
7186
  if (!built) return 1;
6486
- await mkdir6(join21(root, "org"), { recursive: true });
7187
+ await mkdir6(join22(root, "org"), { recursive: true });
6487
7188
  const failures = await cloneSelection({
6488
7189
  root,
6489
7190
  rnessDir,
@@ -6555,7 +7256,7 @@ ${next2.map((l) => ` ${l}`).join("\n")}
6555
7256
  }
6556
7257
 
6557
7258
  // src/core/blocks.ts
6558
- import { join as join22 } from "path";
7259
+ import { join as join23 } from "path";
6559
7260
  var PROBLEM = /* @__PURE__ */ new Set(["stale", "malformed"]);
6560
7261
  async function checkBlocks({
6561
7262
  root,
@@ -6572,7 +7273,7 @@ async function checkBlocks({
6572
7273
  warnings: checks.filter((c) => !PROBLEM.has(c.status) && c.message !== null).map((c) => c.message),
6573
7274
  checks
6574
7275
  });
6575
- if (!await exists(join22(root, "org"))) return derive();
7276
+ if (!await exists(join23(root, "org"))) return derive();
6576
7277
  const targets = [
6577
7278
  { scope: null, dir: root, depth: 0, file: "AGENTS.md", label: "AGENTS.md" },
6578
7279
  { scope: null, dir: root, depth: 0, file: "CLAUDE.md", label: "CLAUDE.md" }
@@ -6581,7 +7282,7 @@ async function checkBlocks({
6581
7282
  const segments = entry.path.split("/");
6582
7283
  targets.push({
6583
7284
  scope: name,
6584
- dir: join22(root, ...segments),
7285
+ dir: join23(root, ...segments),
6585
7286
  depth: segments.length,
6586
7287
  file: "AGENTS.md",
6587
7288
  label: `${entry.path}/AGENTS.md`
@@ -6596,7 +7297,7 @@ async function checkBlocks({
6596
7297
  );
6597
7298
  continue;
6598
7299
  }
6599
- const text = await readOrNull(join22(t.dir, t.file));
7300
+ const text = await readOrNull(join23(t.dir, t.file));
6600
7301
  const lines = text === null ? [] : text.split(/\r?\n/);
6601
7302
  const found = findBlock(lines);
6602
7303
  if (found.kind === "none") {
@@ -6665,9 +7366,9 @@ async function logoutCommand() {
6665
7366
 
6666
7367
  // src/commands/upgrade.ts
6667
7368
  import { execFile as execFile6 } from "child_process";
6668
- 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";
6669
7370
  import { tmpdir as tmpdir3 } from "os";
6670
- 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";
6671
7372
  import { promisify as promisify6 } from "util";
6672
7373
  var execFileP6 = promisify6(execFile6);
6673
7374
  var NOTES = "https://github.com/rness-dev/rness/tree/main/packages/cli#readme";
@@ -6687,10 +7388,10 @@ async function latestVersion() {
6687
7388
  async function scaffoldFor(version, rnessDir) {
6688
7389
  if (version === VERSION) return { dir: scaffoldDir() };
6689
7390
  const installed = await readPinnedCli(rnessDir);
6690
- const inside2 = installed === null ? null : join23(installed.dir, "scaffold");
6691
- 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")))
6692
7393
  return { dir: inside2 };
6693
- const scratch = await mkdtemp3(join23(tmpdir3(), "rness-pack-"));
7394
+ const scratch = await mkdtemp3(join24(tmpdir3(), "rness-pack-"));
6694
7395
  const cleanup = () => rm7(scratch, { recursive: true, force: true });
6695
7396
  try {
6696
7397
  const { stdout } = await execFileP6(
@@ -6705,8 +7406,8 @@ async function scaffoldFor(version, rnessDir) {
6705
7406
  { timeout: 6e4 }
6706
7407
  );
6707
7408
  const tarball = stdout.trim().split("\n").at(-1) ?? "";
6708
- await execFileP6("tar", ["-xzf", join23(scratch, tarball), "-C", scratch]);
6709
- 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 };
6710
7411
  } catch (e) {
6711
7412
  await cleanup();
6712
7413
  throw new Error(
@@ -6718,7 +7419,7 @@ async function scaffoldFor(version, rnessDir) {
6718
7419
  async function declaredManager(rnessDir, pm) {
6719
7420
  try {
6720
7421
  const pkg = JSON.parse(
6721
- await readFile9(join23(rnessDir, "package.json"), "utf8")
7422
+ await readFile10(join24(rnessDir, "package.json"), "utf8")
6722
7423
  );
6723
7424
  if (typeof pkg.packageManager === "string") return pkg.packageManager;
6724
7425
  } catch {
@@ -6728,7 +7429,7 @@ async function declaredManager(rnessDir, pm) {
6728
7429
  async function clonesToCommit(root, cwd, manifest, files) {
6729
7430
  const clones = [];
6730
7431
  for (const repo of Object.keys(manifest.repos)) {
6731
- const dir = join23(root, "org", repo);
7432
+ const dir = join24(root, "org", repo);
6732
7433
  if (!await exists(dir)) continue;
6733
7434
  try {
6734
7435
  const paths = await changedPaths(dir, files);
@@ -6746,7 +7447,7 @@ async function filesTheSyncWrites(root, rnessDir, manifest) {
6746
7447
  try {
6747
7448
  const { stdout } = await execFileP6(
6748
7449
  process.execPath,
6749
- [join23(pinned.dir, pinned.bin), "written-files", "--cwd", root],
7450
+ [join24(pinned.dir, pinned.bin), "written-files", "--cwd", root],
6750
7451
  {
6751
7452
  cwd: root,
6752
7453
  env: { ...process.env, NO_COLOR: "1", RNESS_NO_INSTALL: "1" },
@@ -6942,9 +7643,9 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
6942
7643
  await source.cleanup?.();
6943
7644
  }
6944
7645
  }
6945
- const packageFile = join23(ws.rnessDir, "package.json");
7646
+ const packageFile = join24(ws.rnessDir, "package.json");
6946
7647
  const restore = /* @__PURE__ */ new Map([
6947
- [packageFile, await readFile9(packageFile, "utf8")]
7648
+ [packageFile, await readFile10(packageFile, "utf8")]
6948
7649
  ]);
6949
7650
  const moved = await readPin(ws.rnessDir);
6950
7651
  if (moved !== null && moved.spec !== target2)
@@ -7138,8 +7839,8 @@ import { realpath as realpath4 } from "fs/promises";
7138
7839
  import { isAbsolute as isAbsolute2, posix as posix8, relative as relative7, resolve as resolve7, sep as sep5 } from "path";
7139
7840
 
7140
7841
  // src/mcp/tools.ts
7141
- import { readFile as readFile10, realpath as realpath3, stat as stat4 } from "fs/promises";
7142
- 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";
7143
7844
  var LABELS = {
7144
7845
  standards: "Standards",
7145
7846
  adr: "Decisions",
@@ -7168,7 +7869,7 @@ async function documents(rnessDir, only) {
7168
7869
  const docs = [];
7169
7870
  for (const collection of COLLECTIONS) {
7170
7871
  if (only !== void 0 && collection !== only) continue;
7171
- for (const item of await collectMarkdown(join24(rnessDir, collection))) {
7872
+ for (const item of await collectMarkdown(join25(rnessDir, collection))) {
7172
7873
  const path = `${collection}/${item.rel}`;
7173
7874
  if (path === TEMPLATE2) continue;
7174
7875
  const status2 = item.fields?.["status"];
@@ -7290,7 +7991,7 @@ async function read(cwd, args) {
7290
7991
  return refuse(
7291
7992
  `${path}: ${info.size} bytes, over the 256 KiB rness_read returns`
7292
7993
  );
7293
- return { text: await readFile10(real, "utf8") };
7994
+ return { text: await readFile11(real, "utf8") };
7294
7995
  }
7295
7996
  async function search(cwd, args) {
7296
7997
  const { ws } = await workspaceAt(cwd);
@@ -7398,8 +8099,8 @@ function mcpTools(cwd) {
7398
8099
 
7399
8100
  // src/pulse/detached.ts
7400
8101
  import { spawn as nodeSpawn } from "child_process";
7401
- import { mkdir as mkdir7, open as open2, readFile as readFile11, rm as rm8, stat as stat5, writeFile as writeFile4 } from "fs/promises";
7402
- 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";
7403
8104
  import { setTimeout as delay2 } from "timers/promises";
7404
8105
  var detached = (args, cwd) => {
7405
8106
  const bin = process.argv[1];
@@ -7413,7 +8114,7 @@ var detached = (args, cwd) => {
7413
8114
  child.on("error", () => void 0);
7414
8115
  child.unref();
7415
8116
  };
7416
- var failureFile = () => join25(configDir(), "pulse.json");
8117
+ var failureFile = () => join26(configDir(), "pulse.json");
7417
8118
  async function recordFailure(reason3) {
7418
8119
  try {
7419
8120
  await mkdir7(configDir(), { recursive: true, mode: 448 });
@@ -7427,7 +8128,7 @@ async function recordFailure(reason3) {
7427
8128
  }
7428
8129
  async function takeFailure() {
7429
8130
  try {
7430
- const data = JSON.parse(await readFile11(failureFile(), "utf8"));
8131
+ const data = JSON.parse(await readFile12(failureFile(), "utf8"));
7431
8132
  await rm8(failureFile(), { force: true });
7432
8133
  const reason3 = data !== null && typeof data === "object" ? data["reason"] : void 0;
7433
8134
  return typeof reason3 === "string" && reason3 !== "" ? reason3 : null;
@@ -7438,7 +8139,7 @@ async function takeFailure() {
7438
8139
  var PULSE_LOCK_STALE_MS = 15 * 6e4;
7439
8140
  async function oneSyncAtATime(key, work, options = {}) {
7440
8141
  await mkdir7(configDir(), { recursive: true, mode: 448 });
7441
- const file = join25(configDir(), `pulse-${key}.lock`);
8142
+ const file = join26(configDir(), `pulse-${key}.lock`);
7442
8143
  for (; ; ) {
7443
8144
  try {
7444
8145
  const handle = await open2(file, "wx");
@@ -7524,7 +8225,7 @@ function sessionOf(input) {
7524
8225
  }
7525
8226
  async function hasPulse(ws) {
7526
8227
  try {
7527
- return (await loadManifest(ws.rnessDir)).pulse !== null;
8228
+ return (await loadManifest(ws.rnessDir)).projects !== null;
7528
8229
  } catch {
7529
8230
  return false;
7530
8231
  }
@@ -7834,12 +8535,12 @@ async function mcpCommand(opts, io = {
7834
8535
  }
7835
8536
 
7836
8537
  // src/commands/pulse.ts
7837
- import { readFile as readFile13, readdir as readdir4 } from "fs/promises";
7838
- import { join as join26 } from "path";
8538
+ import { readFile as readFile14, readdir as readdir5 } from "fs/promises";
8539
+ import { join as join27 } from "path";
7839
8540
  import { setTimeout as delay3 } from "timers/promises";
7840
8541
 
7841
8542
  // src/pulse/opened.ts
7842
- 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";
7843
8544
  import { dirname as dirname9 } from "path";
7844
8545
  var NAME2 = "rness/opened";
7845
8546
  function openedFile(rnessDir) {
@@ -7851,7 +8552,7 @@ async function readOpened(file) {
7851
8552
  if (file === null) return opened;
7852
8553
  let data;
7853
8554
  try {
7854
- data = JSON.parse(await readFile12(file, "utf8"));
8555
+ data = JSON.parse(await readFile13(file, "utf8"));
7855
8556
  } catch {
7856
8557
  return opened;
7857
8558
  }
@@ -7918,20 +8619,81 @@ async function missingAccess(provider) {
7918
8619
  const scopes = await fromGithub(provider.scopes());
7919
8620
  return scopes?.includes(PROJECT_SCOPE) === true ? null : NEEDS_SCOPE;
7920
8621
  }
7921
- async function declaredBoard(c) {
7922
- if (c.manifest.pulse === null)
8622
+ async function declaredBoards(c) {
8623
+ const projects = c.manifest.projects;
8624
+ if (projects === null)
7923
8625
  throw new Error("no pulse declared \u2014 rness pulse create");
7924
8626
  const missing = await missingAccess(c.provider);
7925
8627
  if (missing !== null) throw new Error(missing);
7926
- const number = c.manifest.pulse.project;
7927
- const board = await fromGithub(c.provider.board(c.org, number));
7928
- if (board === null)
7929
- throw new Error(`GitHub has no project ${number} in ${c.org}`);
7930
- 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
+ };
7931
8691
  }
7932
8692
  var plural = (n) => `${n} item${n === 1 ? "" : "s"}`;
7933
8693
  var eachAdded = (ui, added) => {
7934
- for (const a of added) ui.line("added", a);
8694
+ for (const a of added)
8695
+ if (a.startsWith("renamed ")) ui.line("renamed", a.slice("renamed ".length));
8696
+ else ui.line("added", a);
7935
8697
  };
7936
8698
  var createdByKind = (ui, added) => {
7937
8699
  for (const kind of ["field", "option", "view"]) {
@@ -7947,8 +8709,8 @@ var createdByKind = (ui, added) => {
7947
8709
  async function filesOf2(rnessDir) {
7948
8710
  const files = /* @__PURE__ */ new Set();
7949
8711
  const walk = async (rel) => {
7950
- const dir = rel === "" ? rnessDir : join26(rnessDir, ...rel.split("/"));
7951
- 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 })) {
7952
8714
  const path = rel === "" ? e.name : `${rel}/${e.name}`;
7953
8715
  if (e.isDirectory()) {
7954
8716
  if (e.name !== ".git" && e.name !== "node_modules") await walk(path);
@@ -7959,7 +8721,7 @@ async function filesOf2(rnessDir) {
7959
8721
  return files;
7960
8722
  }
7961
8723
  var count2 = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
7962
- function sayDone(ui, org, documents2, steps, writes, adopted) {
8724
+ function sayDone(ui, org, documents2, steps, writes, adopted, on) {
7963
8725
  const n = (kind) => steps.filter((s) => s.kind === kind).length - (kind === "create" ? adopted : 0);
7964
8726
  const reopened = steps.filter((s) => s.kind === "update" && s.reopen).length;
7965
8727
  if (n("create") > 0) ui.line("created", count2(n("create"), "issue"));
@@ -7974,6 +8736,8 @@ function sayDone(ui, org, documents2, steps, writes, adopted) {
7974
8736
  `${count2(n("convert"), "draft")} into issues of ${org}/.rness`
7975
8737
  );
7976
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`);
7977
8741
  const written = new Set(writes.map((w) => w.id));
7978
8742
  const bodyOnly = steps.filter(
7979
8743
  (s) => s.kind === "unchanged" && written.has(s.id)
@@ -7991,25 +8755,23 @@ function sayDone(ui, org, documents2, steps, writes, adopted) {
7991
8755
  ].filter(([, k]) => k > 0).map(([word, k]) => `${k} ${word}`);
7992
8756
  ui.line(
7993
8757
  "synced",
7994
- `${plural(documents2)}${parts.length > 0 ? `: ${parts.join(", ")}` : ""}`
8758
+ `${plural(documents2)}${on === void 0 ? "" : ` on ${on}`}${parts.length > 0 ? `: ${parts.join(", ")}` : ""}`
7995
8759
  );
7996
8760
  }
7997
- async function syncBoard(c, board, ui, sayLayout) {
8761
+ async function syncBoard(c, board, ui, sayLayout, scope) {
7998
8762
  await fromGithub(c.provider.checkIssues(c.org));
7999
- const tabs = await statusTabs(c.rnessDir);
8000
- const want = desiredOf(tabs, c.org);
8001
- sayLayout(
8002
- ui,
8003
- await fromGithub(c.provider.ensureLayout(board, layoutOf(tabs)))
8004
- );
8763
+ const { want } = scope;
8764
+ sayLayout(ui, await fromGithub(c.provider.ensureLayout(board, scope.layout)));
8005
8765
  const have = await fromGithub(c.provider.items(board));
8006
8766
  const record = await openedFile(c.rnessDir);
8007
- 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));
8008
8770
  const seen = await seenPaths(c.rnessDir, [
8009
8771
  ...unwantedPaths(want, have),
8010
8772
  ...opened.values()
8011
8773
  ]);
8012
- const steps = planSync(want, have, seen, opened);
8774
+ const steps = planSync(want, have, seen, opened, scope.elsewhere);
8013
8775
  const changes = steps.filter((s) => s.kind !== "unchanged").length;
8014
8776
  let writes = [];
8015
8777
  let made = 0;
@@ -8033,7 +8795,10 @@ async function syncBoard(c, board, ui, sayLayout) {
8033
8795
  } finally {
8034
8796
  await writeOpened(
8035
8797
  record,
8036
- stillOpened(opened, have, seen, closed, created)
8798
+ new Map([
8799
+ ...others,
8800
+ ...stillOpened(opened, have, seen, closed, created)
8801
+ ])
8037
8802
  );
8038
8803
  }
8039
8804
  const issued = issuedAfter(steps, have, placed);
@@ -8045,7 +8810,7 @@ async function syncBoard(c, board, ui, sayLayout) {
8045
8810
  w.path,
8046
8811
  issueBody({
8047
8812
  path: w.path,
8048
- text: await readFile13(join26(c.rnessDir, ...w.path.split("/")), "utf8"),
8813
+ text: await readFile14(join27(c.rnessDir, ...w.path.split("/")), "utf8"),
8049
8814
  org: c.org,
8050
8815
  numbers,
8051
8816
  files
@@ -8064,7 +8829,43 @@ async function syncBoard(c, board, ui, sayLayout) {
8064
8829
  { cause: e }
8065
8830
  );
8066
8831
  }
8067
- 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
+ );
8068
8869
  }
8069
8870
  async function pulseCreateCommand(opts, deps = {}) {
8070
8871
  try {
@@ -8072,9 +8873,20 @@ async function pulseCreateCommand(opts, deps = {}) {
8072
8873
  const ui = deps.ui ?? await makeUi(terminal);
8073
8874
  const wait = waitSaid(opts, ui);
8074
8875
  let c = await context2(opts, wait);
8075
- 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)
8881
+ throw new Error(
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)
8076
8888
  throw new Error(
8077
- `already declared: ${boardUrl(c.org, c.manifest.pulse.project)} \u2014 rness pulse sync`
8889
+ `${opts.collection} is no collection of .rness: none of its documents carries a status`
8078
8890
  );
8079
8891
  const detected = providerOf(c.manifest);
8080
8892
  if (!PROVIDERS[detected].available)
@@ -8112,19 +8924,26 @@ async function pulseCreateCommand(opts, deps = {}) {
8112
8924
  "checked",
8113
8925
  `${detected}, logged in as ${login}, scope ${PROJECT_SCOPE}`
8114
8926
  );
8115
- const board = await fromGithub(c.provider.createBoard(c.org));
8116
- await writeManifest(c.rnessDir, {
8927
+ const board = await fromGithub(c.provider.createBoard(c.org, tab?.label));
8928
+ const manifest = {
8117
8929
  ...c.manifest,
8118
8930
  provider: detected,
8119
- pulse: { project: board.number }
8120
- });
8121
- 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}`);
8122
8936
  try {
8123
- await syncBoard(c, board, ui, createdByKind);
8937
+ await syncAll(
8938
+ c,
8939
+ await declaredBoards(c),
8940
+ ui,
8941
+ (n) => n === name ? createdByKind : eachAdded
8942
+ );
8124
8943
  } finally {
8125
8944
  ui.line(
8126
8945
  "declared",
8127
- '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}`}"`
8128
8947
  );
8129
8948
  }
8130
8949
  return 0;
@@ -8136,7 +8955,7 @@ async function pulseSyncCommand(opts, deps = {}) {
8136
8955
  try {
8137
8956
  const ui = deps.ui ?? await makeUi(deps.terminal ?? defaultTerminal);
8138
8957
  const c = await context2(opts, waitSaid(opts, ui));
8139
- await syncBoard(c, await declaredBoard(c), ui, eachAdded);
8958
+ await syncAll(c, await declaredBoards(c), ui, () => eachAdded);
8140
8959
  return 0;
8141
8960
  } catch (e) {
8142
8961
  return reportError(e);
@@ -8152,43 +8971,47 @@ async function pulseMarkCommand(opts) {
8152
8971
  }
8153
8972
  async function mark(opts) {
8154
8973
  const c = await context2(opts);
8155
- const board = await declaredBoard(c);
8156
- const key = `${c.org}-${board.number}`;
8974
+ const declared2 = await declaredBoards(c);
8975
+ const key = `${c.org}-${declared2.map((d) => d.board.number).join("-")}`;
8157
8976
  const lock = opts.sleep === void 0 ? {} : { sleep: opts.sleep };
8158
- const listed = () => fromGithub(c.provider.items(board, { bodies: false }));
8159
- let items = await listed();
8977
+ const syncEvery = () => syncAll(c, declared2, plainUi, () => eachAdded);
8978
+ const listed = (board) => fromGithub(c.provider.items(board, { bodies: false }));
8160
8979
  if (opts.end === true) {
8161
8980
  const id = opts.session.split(" \xB7 ").pop() ?? opts.session;
8162
- const ids2 = items.filter(
8163
- (i) => i.session === opts.session || i.session?.endsWith(` \xB7 ${id}`)
8164
- ).map((i) => i.id);
8165
- await fromGithub(c.provider.mark(board, ids2, null));
8166
- await oneSyncAtATime(key, () => syncBoard(c, board, plainUi, eachAdded), {
8167
- ...lock,
8168
- wait: true
8169
- });
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 });
8170
8988
  return 0;
8171
8989
  }
8172
- 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));
8173
8992
  const documents2 = new Map(
8174
8993
  (await statusTabs(c.rnessDir)).flatMap(
8175
8994
  (t) => t.rows.map((r) => [r.path, r.status])
8176
8995
  )
8177
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();
8178
9003
  const onBoard = new Map(
8179
- items.flatMap((i) => i.path === null ? [] : [[i.path, i.status]])
9004
+ [...items.values()].flat().flatMap((i) => i.path === null ? [] : [[i.path, i.status]])
8180
9005
  );
8181
9006
  const stale = (path) => documents2.has(path) && (!onBoard.has(path) || onBoard.get(path) !== documents2.get(path));
8182
9007
  if ([...wanted].some(stale)) {
8183
- const synced = await oneSyncAtATime(
8184
- key,
8185
- () => syncBoard(c, board, plainUi, eachAdded),
8186
- lock
8187
- );
8188
- 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));
8189
9014
  }
8190
- const ids = items.filter((i) => i.path !== null && wanted.has(i.path)).map((i) => i.id);
8191
- await fromGithub(c.provider.mark(board, ids, opts.session));
8192
9015
  return 0;
8193
9016
  }
8194
9017
 
@@ -8490,10 +9313,19 @@ function buildProgram(state) {
8490
9313
  state.code = await syncCommand(opts);
8491
9314
  });
8492
9315
  const pulse = program.command("pulse").description("Show the agent's work on the organization's board");
8493
- 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) => {
8494
- 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
+ );
8495
9325
  });
8496
- 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) => {
8497
9329
  state.code = await pulseSyncCommand(opts);
8498
9330
  });
8499
9331
  pulse.command("mark", { hidden: true }).requiredOption("--session <id>").option(
@@ -8723,4 +9555,4 @@ export {
8723
9555
  OWN_COMMANDS,
8724
9556
  catchUp
8725
9557
  };
8726
- //# sourceMappingURL=chunk-7IVHL744.js.map
9558
+ //# sourceMappingURL=chunk-X52SHBRY.js.map