@open-tomato/rafa 0.39.0 → 0.40.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.
package/dist/cli.js CHANGED
@@ -747,6 +747,59 @@ var init_config_readers = __esm(() => {
747
747
  FRAGMENTS_EXPECTED = `a relative directory path not under ${UNTRACKED_DIRECTORY}/`;
748
748
  });
749
749
 
750
+ // src/config-schema-board-project.ts
751
+ function isWholeBetween(raw, min, max) {
752
+ return typeof raw === "number" && Number.isSafeInteger(raw) && raw >= min && raw <= max;
753
+ }
754
+ function rangeWords(min, max) {
755
+ return `a whole number from ${String(min)} to ${String(max)}`;
756
+ }
757
+ function wholeBetween(min, max) {
758
+ return (raw, at) => isWholeBetween(raw, min, max) ? { value: raw, problems: [], extras: [] } : refused(at, raw, rangeWords(min, max));
759
+ }
760
+ function offOrWholeBetween(min, max) {
761
+ return (raw, at) => raw === false || isWholeBetween(raw, min, max) ? { value: raw, problems: [], extras: [] } : refused(at, raw, `false or ${rangeWords(min, max)}`);
762
+ }
763
+ var BOARD_PROJECT_RETRIES_MIN = 1, BOARD_PROJECT_RETRIES_MAX = 10, BOARD_PROJECT_RETRY_WAIT_MIN_SECONDS = 1, BOARD_PROJECT_RETRY_WAIT_MAX_SECONDS = 60, BOARD_PROJECT_PROGRESS_MIN_SECONDS = 1, BOARD_PROJECT_PROGRESS_MAX_SECONDS = 300, BOARD_PROJECT_WRITE_BATCH_MIN = 1, BOARD_PROJECT_WRITE_BATCH_MAX = 100, BOARD_PROJECT_WRITE_PAUSE_MIN_MS = 0, BOARD_PROJECT_WRITE_PAUSE_MAX_MS = 60000, boardProjectRetries, boardProjectRetryWaitSeconds, boardProjectProgressSeconds, boardProjectWriteBatchSize, boardProjectWritePauseMs, BOARD_PROJECT_LIMIT_DEFAULTS, BOARD_PROJECT_LIMIT_SETTINGS;
764
+ var init_config_schema_board_project = __esm(() => {
765
+ init_config_sections();
766
+ boardProjectRetries = offOrWholeBetween(BOARD_PROJECT_RETRIES_MIN, BOARD_PROJECT_RETRIES_MAX);
767
+ boardProjectRetryWaitSeconds = wholeBetween(BOARD_PROJECT_RETRY_WAIT_MIN_SECONDS, BOARD_PROJECT_RETRY_WAIT_MAX_SECONDS);
768
+ boardProjectProgressSeconds = offOrWholeBetween(BOARD_PROJECT_PROGRESS_MIN_SECONDS, BOARD_PROJECT_PROGRESS_MAX_SECONDS);
769
+ boardProjectWriteBatchSize = wholeBetween(BOARD_PROJECT_WRITE_BATCH_MIN, BOARD_PROJECT_WRITE_BATCH_MAX);
770
+ boardProjectWritePauseMs = wholeBetween(BOARD_PROJECT_WRITE_PAUSE_MIN_MS, BOARD_PROJECT_WRITE_PAUSE_MAX_MS);
771
+ BOARD_PROJECT_LIMIT_DEFAULTS = Object.freeze({
772
+ boardProjectRetries: 3,
773
+ boardProjectRetryWaitSeconds: 2,
774
+ boardProjectProgressSeconds: 10,
775
+ boardProjectWriteBatchSize: 20,
776
+ boardProjectWritePauseMs: 1000
777
+ });
778
+ BOARD_PROJECT_LIMIT_SETTINGS = {
779
+ boardProjectRetries: { key: "board.project.retries", read: boardProjectRetries, cli: false },
780
+ boardProjectRetryWaitSeconds: {
781
+ key: "board.project.retryWaitSeconds",
782
+ read: boardProjectRetryWaitSeconds,
783
+ cli: false
784
+ },
785
+ boardProjectProgressSeconds: {
786
+ key: "board.project.progressSeconds",
787
+ read: boardProjectProgressSeconds,
788
+ cli: false
789
+ },
790
+ boardProjectWriteBatchSize: {
791
+ key: "board.project.writeBatchSize",
792
+ read: boardProjectWriteBatchSize,
793
+ cli: false
794
+ },
795
+ boardProjectWritePauseMs: {
796
+ key: "board.project.writePauseMs",
797
+ read: boardProjectWritePauseMs,
798
+ cli: false
799
+ }
800
+ };
801
+ });
802
+
750
803
  // src/config-schema-release.ts
751
804
  var PR_DEFAULTS, RELEASE_DEFAULTS, DANGEROUS_RELEASE_DEFAULTS, PR_SETTINGS, RELEASE_SETTINGS, DANGEROUS_RELEASE_SETTINGS;
752
805
  var init_config_schema_release = __esm(() => {
@@ -1093,6 +1146,7 @@ var CONFIG_FILE, CONFIG_DEFAULTS, SETTINGS, SETTING_NAMES, SETTING_BY_KEY, SECTI
1093
1146
  var init_config_schema = __esm(() => {
1094
1147
  init_config_items();
1095
1148
  init_config_readers();
1149
+ init_config_schema_board_project();
1096
1150
  init_config_schema_hub();
1097
1151
  init_config_schema_release();
1098
1152
  init_config_schema_tests();
@@ -1132,6 +1186,7 @@ var init_config_schema = __esm(() => {
1132
1186
  boardRelationships: "labels",
1133
1187
  boardProjectTemplate: BOARD_PROJECT_TEMPLATE_DEFAULT,
1134
1188
  boardProjectNumber: null,
1189
+ ...BOARD_PROJECT_LIMIT_DEFAULTS,
1135
1190
  roadmapIssue: null,
1136
1191
  claimsStaleAfter: "3d",
1137
1192
  claimsAhead: "off",
@@ -1222,6 +1277,7 @@ var init_config_schema = __esm(() => {
1222
1277
  },
1223
1278
  boardProjectTemplate: { key: "board.project.template", read: projectUrl, cli: false },
1224
1279
  boardProjectNumber: { key: "board.project.number", read: projectNumber, cli: false },
1280
+ ...BOARD_PROJECT_LIMIT_SETTINGS,
1225
1281
  roadmapIssue: { key: "roadmap.issue", read: issueNumber, cli: false },
1226
1282
  claimsStaleAfter: { key: "claims.staleAfter", read: claimsStaleAfter, cli: false },
1227
1283
  claimsAhead: { key: "claims.ahead", read: claimsAhead, cli: false },
@@ -1334,6 +1390,11 @@ function readLayer(rawOf, atOf) {
1334
1390
  boardRelationships: read("boardRelationships"),
1335
1391
  boardProjectTemplate: read("boardProjectTemplate"),
1336
1392
  boardProjectNumber: read("boardProjectNumber"),
1393
+ boardProjectRetries: read("boardProjectRetries"),
1394
+ boardProjectRetryWaitSeconds: read("boardProjectRetryWaitSeconds"),
1395
+ boardProjectProgressSeconds: read("boardProjectProgressSeconds"),
1396
+ boardProjectWriteBatchSize: read("boardProjectWriteBatchSize"),
1397
+ boardProjectWritePauseMs: read("boardProjectWritePauseMs"),
1337
1398
  roadmapIssue: read("roadmapIssue"),
1338
1399
  claimsStaleAfter: read("claimsStaleAfter"),
1339
1400
  claimsAhead: read("claimsAhead"),
@@ -2243,7 +2304,7 @@ function restoreRunningCommand(previous) {
2243
2304
  var running = null;
2244
2305
 
2245
2306
  // package.json
2246
- var name = "@open-tomato/rafa", version = "0.39.0";
2307
+ var name = "@open-tomato/rafa", version = "0.40.0";
2247
2308
  var init_package = () => {};
2248
2309
 
2249
2310
  // src/cli/version.ts
@@ -21408,6 +21469,153 @@ var init_issue_board = __esm(() => {
21408
21469
  CLOSE_REASONS = Object.freeze(["completed", "not planned"]);
21409
21470
  });
21410
21471
 
21472
+ // src/board/project/retry.ts
21473
+ function firstMatch2(patterns, text2) {
21474
+ for (const pattern of patterns) {
21475
+ const match = pattern.exec(text2);
21476
+ if (match !== null)
21477
+ return match[0];
21478
+ }
21479
+ return;
21480
+ }
21481
+ function lastLine(text2) {
21482
+ const lines = text2.split(`
21483
+ `).map((line) => line.trim()).filter((line) => line !== "");
21484
+ return lines.at(-1) ?? UNKNOWN_REASON;
21485
+ }
21486
+ function classifyFailure(stderr) {
21487
+ const permanent = firstMatch2(NEVER_RETRIED, stderr);
21488
+ if (permanent !== undefined)
21489
+ return { kind: "permanent", reason: permanent };
21490
+ const retried = firstMatch2(RETRIED, stderr);
21491
+ if (retried !== undefined)
21492
+ return { kind: "retried", reason: retried };
21493
+ return { kind: "permanent", reason: lastLine(stderr) };
21494
+ }
21495
+ function callNumber(args) {
21496
+ for (let index = 0;index + 1 < args.length; index += 1) {
21497
+ const value = args[index + 1] ?? "";
21498
+ if (args[index] !== "-F" || !value.startsWith("number="))
21499
+ continue;
21500
+ const number = Number(value.slice("number=".length));
21501
+ if (Number.isSafeInteger(number))
21502
+ return number;
21503
+ }
21504
+ return;
21505
+ }
21506
+ function callSubject(args) {
21507
+ const number = callNumber(args);
21508
+ if (number !== undefined)
21509
+ return `#${String(number)}`;
21510
+ const query = args.find((arg) => arg.startsWith("query="));
21511
+ const field4 = query === undefined ? undefined : FIRST_FIELD.exec(query)?.[1];
21512
+ return field4 ?? args.slice(0, 2).join(" ");
21513
+ }
21514
+ function isWholeFrom(value, min) {
21515
+ return Number.isSafeInteger(value) && value >= min;
21516
+ }
21517
+ function checkOptions(options) {
21518
+ if (options.retries !== false && !isWholeFrom(options.retries, 1)) {
21519
+ throw new RangeError(`board.project.retries must be false or a whole number from 1, not ${String(options.retries)}`);
21520
+ }
21521
+ if (!isWholeFrom(options.retryWaitSeconds, 1)) {
21522
+ throw new RangeError(`board.project.retryWaitSeconds must be a whole number from 1, not ${String(options.retryWaitSeconds)}`);
21523
+ }
21524
+ }
21525
+ function retryingGhRunner(gh, options) {
21526
+ checkOptions(options);
21527
+ const { retries, retryWaitSeconds, onRetry } = options;
21528
+ if (retries === false)
21529
+ return gh;
21530
+ const sleep = options.sleep ?? ((ms) => Bun.sleep(ms));
21531
+ return async (args, stdin) => {
21532
+ let result = await gh(args, stdin);
21533
+ for (let attempt = 1;attempt <= retries && !result.ok; attempt += 1) {
21534
+ const reading2 = classifyFailure(result.stderr);
21535
+ if (reading2.kind === "permanent")
21536
+ return result;
21537
+ const waitMs = retryWaitSeconds * MS_PER_SECOND2 * 2 ** (attempt - 1);
21538
+ onRetry?.({
21539
+ attempt,
21540
+ of: retries,
21541
+ reason: reading2.reason,
21542
+ subject: callSubject(args),
21543
+ number: callNumber(args),
21544
+ waitMs,
21545
+ args
21546
+ });
21547
+ await sleep(waitMs);
21548
+ result = await gh(args, stdin);
21549
+ }
21550
+ return result;
21551
+ };
21552
+ }
21553
+ var NEVER_RETRIED, RETRIED, UNKNOWN_REASON = "unknown error", FIRST_FIELD, MS_PER_SECOND2 = 1000;
21554
+ var init_retry = __esm(() => {
21555
+ NEVER_RETRIED = Object.freeze([
21556
+ /RATE_LIMITED|(?:secondary )?rate limit/iu,
21557
+ /INSUFFICIENT_SCOPES|not been granted the required scopes|needs the "[^"]*" scope/u,
21558
+ /NOT_FOUND|Could not resolve to [^\n]*/u
21559
+ ]);
21560
+ RETRIED = Object.freeze([
21561
+ /operation timed out|i\/o timeout|TLS handshake timeout/u,
21562
+ /connection reset|\bEOF\b/u,
21563
+ /\bHTTP 50[234]\b/u
21564
+ ]);
21565
+ FIRST_FIELD = /\{\s*([A-Za-z_]\w*)/u;
21566
+ });
21567
+
21568
+ // src/board/project/project-runner.ts
21569
+ function retryLine(notice) {
21570
+ return `retrying ${notice.subject} (${String(notice.attempt)} of ${String(notice.of)}): ${notice.reason}`;
21571
+ }
21572
+ function retryEvent(notice, now) {
21573
+ return {
21574
+ type: "event",
21575
+ name: RETRY_EVENT,
21576
+ summary: retryLine(notice),
21577
+ data: {
21578
+ attempt: notice.attempt,
21579
+ of: notice.of,
21580
+ reason: notice.reason,
21581
+ subject: notice.subject,
21582
+ number: notice.number ?? null,
21583
+ waitMs: notice.waitMs
21584
+ },
21585
+ ts: now.toISOString()
21586
+ };
21587
+ }
21588
+ function retryReporter(output, mode, now = () => new Date) {
21589
+ return (notice) => {
21590
+ if (mode === "json")
21591
+ output.emit(retryEvent(notice, now()));
21592
+ else
21593
+ output.info(retryLine(notice));
21594
+ };
21595
+ }
21596
+ function commandRetrySeams(output, mode, sleep) {
21597
+ const onRetry = retryReporter(output, mode);
21598
+ return sleep === undefined ? { onRetry } : { onRetry, sleep };
21599
+ }
21600
+ function openProjectRunner(gh, config, seams = {}) {
21601
+ if (OPENED.has(gh))
21602
+ return gh;
21603
+ const onRetry = seams.onRetry ?? activeRetryReport;
21604
+ const base = { retries: config.boardProjectRetries, retryWaitSeconds: config.boardProjectRetryWaitSeconds, onRetry };
21605
+ const opened = retryingGhRunner(gh, seams.sleep === undefined ? base : { ...base, sleep: seams.sleep });
21606
+ if (opened !== gh)
21607
+ OPENED.add(opened);
21608
+ return opened;
21609
+ }
21610
+ var RETRY_EVENT = "retry", OPENED, activeRetryReport = (notice) => {
21611
+ retryReporter(activeOutput(), activeOutputMode())(notice);
21612
+ };
21613
+ var init_project_runner = __esm(() => {
21614
+ init_active();
21615
+ init_retry();
21616
+ OPENED = new WeakSet;
21617
+ });
21618
+
21411
21619
  // src/board/project/options.ts
21412
21620
  var STAGE_OPTIONS, HORIZON_OPTIONS;
21413
21621
  var init_options = __esm(() => {
@@ -21471,7 +21679,7 @@ function matchProjectFields(project) {
21471
21679
  mismatched: read.filter((entry) => ("problem" in entry))
21472
21680
  };
21473
21681
  }
21474
- var PROJECT_PAGE_SIZE = 100, PROJECT_WRITE_BATCH_SIZE = 20, PROJECT_WRITE_PAUSE_MS = 1000, PROJECT_PORT_PREFIX = "board project", FIELD_DATA_TYPES, PROJECT_FIELDS, ProjectPortError;
21682
+ var PROJECT_PAGE_SIZE = 100, PROJECT_PORT_PREFIX = "board project", FIELD_DATA_TYPES, PROJECT_FIELDS, ProjectPortError;
21475
21683
  var init_port = __esm(() => {
21476
21684
  init_options();
21477
21685
  FIELD_DATA_TYPES = Object.freeze({
@@ -21515,6 +21723,9 @@ function notFoundWarning(ref) {
21515
21723
  function skippedFieldWarning(mismatch) {
21516
21724
  return `The project's field "${mismatch.template.name}" was skipped and the rest were written: ${mismatch.sentence} Run \`${DOCTOR_FIX}\`.`;
21517
21725
  }
21726
+ function notRefreshedWarning(refusal3) {
21727
+ return `#${String(refusal3.number)} not refreshed: ${refusal3.reason}`;
21728
+ }
21518
21729
  var PROJECT_SCOPE_FIX = "gh auth refresh -s project", BOARD_SYNC_FIX = "rafa board sync", INIT_BOARD_FIX = "rafa init --board", DOCTOR_FIX = "rafa doctor", SCOPE_REFUSAL, PROJECT_SCOPE;
21519
21730
  var init_refresh_warnings = __esm(() => {
21520
21731
  init_port();
@@ -21967,10 +22178,10 @@ function appendLine(body, issue4, why = "") {
21967
22178
  if (lineIndicesOf(body, issue4).length > 0)
21968
22179
  return body;
21969
22180
  const parts = partsOf(body);
21970
- const lastLine = parseRoadmapBody(body).at(-1);
21971
- const anchorIndex = lastLine === undefined ? lastTextIndex(parts) : lastLine.lineNumber - 1;
22181
+ const lastLine2 = parseRoadmapBody(body).at(-1);
22182
+ const anchorIndex = lastLine2 === undefined ? lastTextIndex(parts) : lastLine2.lineNumber - 1;
21972
22183
  const anchor = parts[anchorIndex];
21973
- const bullet = lastLine === undefined ? DEFAULT_BULLET : BULLET.exec(anchor?.text ?? "")?.[0] ?? DEFAULT_BULLET;
22184
+ const bullet = lastLine2 === undefined ? DEFAULT_BULLET : BULLET.exec(anchor?.text ?? "")?.[0] ?? DEFAULT_BULLET;
21974
22185
  const trimmedWhy = why.trim();
21975
22186
  const text2 = trimmedWhy === "" ? `${bullet}[ ] #${String(issue4)}` : `${bullet}[ ] #${String(issue4)} ${trimmedWhy}`;
21976
22187
  if (anchor === undefined)
@@ -24570,6 +24781,10 @@ var init_fragment_tree = __esm(() => {
24570
24781
  });
24571
24782
 
24572
24783
  // src/board/project/facts-query.ts
24784
+ function listSelection(list, after) {
24785
+ const from = after === null ? "" : `, after: $${after}`;
24786
+ return `${list}(first: ${String(PAGE_SIZE)}${from}${LIST_ARGUMENTS[list]})` + ` { pageInfo { hasNextPage endCursor } nodes { ${LIST_NODES[list]} } }`;
24787
+ }
24573
24788
  function checkNumber(number) {
24574
24789
  if (!Number.isSafeInteger(number) || number < 1)
24575
24790
  throw new RangeError(`not an issue number: ${String(number)}`);
@@ -24591,6 +24806,9 @@ function graphqlArgs(query, variables) {
24591
24806
  function issueAlias(number) {
24592
24807
  return `i${checkNumber(number)}`;
24593
24808
  }
24809
+ function listAlias(number, list) {
24810
+ return `${LIST_CODES[list]}${checkNumber(number)}`;
24811
+ }
24594
24812
  function pullAlias(number) {
24595
24813
  return `p${checkNumber(number)}`;
24596
24814
  }
@@ -24602,6 +24820,18 @@ function issuesArgs(numbers) {
24602
24820
  const query = `query($owner: String!, $repo: String!) { repository(owner: $owner, name: $repo) { nameWithOwner${issues} } }` + ` fragment facts on Issue { ${ISSUE_SELECTION} } fragment pull on PullRequest { ${PULL_FIELDS} }`;
24603
24821
  return graphqlArgs(query, []);
24604
24822
  }
24823
+ function listPagesArgs(pages) {
24824
+ const aliases = pages.map(({ number, list }) => listAlias(number, list));
24825
+ const declared = aliases.map((alias) => `, $${alias}: String!`).join("");
24826
+ const issues = pages.map(({ number, list }, index) => {
24827
+ const alias = aliases[index] ?? listAlias(number, list);
24828
+ return ` ${alias}: issue(number: ${String(number)}) { ${listSelection(list, alias)} }`;
24829
+ }).join("");
24830
+ const usesPull = pages.some(({ list }) => list !== "labels");
24831
+ const fragment = usesPull ? ` fragment pull on PullRequest { ${PULL_FIELDS} }` : "";
24832
+ const query = `query($owner: String!, $repo: String!${declared}) { repository(owner: $owner, name: $repo) {${issues} } }${fragment}`;
24833
+ return graphqlArgs(query, pages.map(({ cursor }, index) => [aliases[index] ?? "", cursor]));
24834
+ }
24605
24835
  function filesArgs(pages) {
24606
24836
  const cursors = pages.map(({ number }) => `, $c${checkNumber(number)}: String`).join("");
24607
24837
  const pulls = pages.map(({ number }) => ` ${pullAlias(number)}: pullRequest(number: ${String(number)})` + ` { files(first: ${String(PAGE_SIZE)}, after: $c${String(number)})` + " { pageInfo { hasNextPage endCursor } nodes { path changeType } } }").join("");
@@ -24621,17 +24851,25 @@ function blobsArgs(lookups) {
24621
24851
  function batchesOf(items, size) {
24622
24852
  return Array.from({ length: Math.ceil(items.length / size) }, (_, index) => items.slice(index * size, (index + 1) * size));
24623
24853
  }
24624
- var PAGE_SIZE = 100, ISSUE_BATCH = 20, PULL_BATCH = 20, FRAGMENT_BATCH = 20, PULL_FIELDS = "number state baseRefName headRefOid", ISSUE_SELECTION;
24854
+ var PAGE_SIZE = 100, ISSUE_BATCH = 20, LIST_BATCH = 20, PULL_BATCH = 20, FRAGMENT_BATCH = 20, PULL_FIELDS = "number state baseRefName headRefOid", ISSUE_LISTS, LIST_CODES, LIST_ARGUMENTS, LIST_NODES, ISSUE_SELECTION;
24625
24855
  var init_facts_query = __esm(() => {
24626
- ISSUE_SELECTION = [
24627
- "number state stateReason",
24628
- ` labels(first: ${String(PAGE_SIZE)}) { pageInfo { hasNextPage } nodes { name } }`,
24629
- ` closedByPullRequestsReferences(first: ${String(PAGE_SIZE)}, includeClosedPrs: true)`,
24630
- " { pageInfo { hasNextPage } nodes { ...pull } }",
24631
- ` timelineItems(first: ${String(PAGE_SIZE)}, itemTypes: [CROSS_REFERENCED_EVENT])`,
24632
- " { pageInfo { hasNextPage } nodes { ... on CrossReferencedEvent",
24633
- " { isCrossRepository source { ... on PullRequest { ...pull body } } } } }"
24634
- ].join("");
24856
+ ISSUE_LISTS = Object.freeze(["labels", "closedByPullRequestsReferences", "timelineItems"]);
24857
+ LIST_CODES = Object.freeze({
24858
+ labels: "la",
24859
+ closedByPullRequestsReferences: "cb",
24860
+ timelineItems: "tl"
24861
+ });
24862
+ LIST_ARGUMENTS = Object.freeze({
24863
+ labels: "",
24864
+ closedByPullRequestsReferences: ", includeClosedPrs: true",
24865
+ timelineItems: ", itemTypes: [CROSS_REFERENCED_EVENT]"
24866
+ });
24867
+ LIST_NODES = Object.freeze({
24868
+ labels: "name",
24869
+ closedByPullRequestsReferences: "...pull",
24870
+ timelineItems: "... on CrossReferencedEvent { isCrossRepository source { ... on PullRequest { ...pull body } } }"
24871
+ });
24872
+ ISSUE_SELECTION = ["number state stateReason", ...ISSUE_LISTS.map((list) => listSelection(list, null))].join(" ");
24635
24873
  });
24636
24874
 
24637
24875
  // src/board/epic-dependents.ts
@@ -28783,6 +29021,17 @@ var init_rules = __esm(() => {
28783
29021
  function refuse8(problem) {
28784
29022
  throw new Error(`${PREFIX29}: ${problem}`);
28785
29023
  }
29024
+ function reasonOf2(error) {
29025
+ const message = error instanceof Error ? error.message : String(error);
29026
+ return message.startsWith(`${PREFIX29}: `) ? message.slice(PREFIX29.length + 2) : message;
29027
+ }
29028
+ function settle2(item, read) {
29029
+ try {
29030
+ return { item, ok: true, value: read() };
29031
+ } catch (error) {
29032
+ return { item, ok: false, reason: reasonOf2(error) };
29033
+ }
29034
+ }
28786
29035
  function readMapping3(value, where) {
28787
29036
  if (!isMapping(value))
28788
29037
  refuse8(`${where} is ${describeValue(value)}, expected a mapping`);
@@ -28804,12 +29053,16 @@ function readList2(value, where) {
28804
29053
  refuse8(`${where} is ${describeValue(value)}, expected a list`);
28805
29054
  return value;
28806
29055
  }
28807
- function readOnePage(value, where) {
29056
+ function readPage2(value, where) {
28808
29057
  const connection = readMapping3(value, where);
28809
29058
  const pageInfo = readMapping3(connection["pageInfo"], `${where}.pageInfo`);
28810
- if (pageInfo["hasNextPage"] !== false)
28811
- refuse8(`${where} holds more than ${String(PAGE_SIZE)} entries, one page`);
28812
- return readList2(connection["nodes"], `${where}.nodes`);
29059
+ const nodes = readList2(connection["nodes"], `${where}.nodes`);
29060
+ const more = pageInfo["hasNextPage"];
29061
+ if (more === false)
29062
+ return { nodes, next: null };
29063
+ if (more !== true)
29064
+ refuse8(`${where}.pageInfo.hasNextPage is ${describeValue(more)}, expected true or false`);
29065
+ return { nodes, next: readString4(pageInfo["endCursor"], `${where}.pageInfo.endCursor`) };
28813
29066
  }
28814
29067
  async function readRepository(gh, args) {
28815
29068
  const result = await gh(args);
@@ -28824,6 +29077,21 @@ async function readRepository(gh, args) {
28824
29077
  const data = readMapping3(readMapping3(parsed, "the answer")["data"], "data");
28825
29078
  return readMapping3(data["repository"], "data.repository");
28826
29079
  }
29080
+ async function readEach(gh, items, how) {
29081
+ const read = [];
29082
+ for (const batch of batchesOf(items, how.size)) {
29083
+ let answer3;
29084
+ try {
29085
+ answer3 = await readRepository(gh, how.argsOf(batch));
29086
+ } catch (error) {
29087
+ const alone = batch.length === 1 ? batch.map((item) => ({ item, ok: false, reason: reasonOf2(error) })) : await readEach(gh, batch, { ...how, size: 1 });
29088
+ read.push(...alone);
29089
+ continue;
29090
+ }
29091
+ read.push(...batch.map((item, index) => settle2(item, () => how.readOne(answer3, item, index))));
29092
+ }
29093
+ return read;
29094
+ }
28827
29095
  function readPull(value, where) {
28828
29096
  const pull = readMapping3(value, where);
28829
29097
  const state = pull["state"];
@@ -28853,35 +29121,100 @@ function crossReferencedPull(value, where, issue4, repository) {
28853
29121
  refuse8(`${where}.source.body is ${describeValue(body)}, expected a string`);
28854
29122
  return bodyCloses(body, issue4, repository) ? readPull(source, `${where}.source`) : null;
28855
29123
  }
28856
- function readIssue(repositoryAnswer, number, repository) {
29124
+ function listWhere(number, list) {
29125
+ return `issue #${String(number)}.${list}`;
29126
+ }
29127
+ function readIssueFirstPages(answer3, number, repository) {
28857
29128
  const where = `issue #${String(number)}`;
28858
- const issue4 = readMapping3(repositoryAnswer[issueAlias(number)], where);
29129
+ const issue4 = readMapping3(answer3[issueAlias(number)], where);
28859
29130
  const state = issue4["state"];
28860
29131
  if (state !== "OPEN" && state !== "CLOSED")
28861
29132
  refuse8(`${where}.state is ${describeValue(state)}, expected OPEN or CLOSED`);
28862
29133
  const reason = issue4["stateReason"];
28863
29134
  if (reason !== null && typeof reason !== "string")
28864
29135
  refuse8(`${where}.stateReason is ${describeValue(reason)}, expected a string or null`);
28865
- const labels = readOnePage(issue4["labels"], `${where}.labels`).map((label, index) => readString4(readMapping3(label, `${where}.labels[${String(index)}]`)["name"], `${where}.labels[${String(index)}].name`));
28866
- const linked = readOnePage(issue4["closedByPullRequestsReferences"], `${where}.closedByPullRequestsReferences`).map((pull, index) => readPull(pull, `${where}.closedByPullRequestsReferences[${String(index)}]`));
28867
- const mentioned = readOnePage(issue4["timelineItems"], `${where}.timelineItems`).map((event, index) => crossReferencedPull(event, `${where}.timelineItems[${String(index)}]`, number, repository));
29136
+ const pages = Object.fromEntries(ISSUE_LISTS.map((list) => [list, readPage2(issue4[list], listWhere(number, list))]));
29137
+ return { head: { number, state, stateReason: reason }, repository, pages };
29138
+ }
29139
+ function listKey(number, list) {
29140
+ return `${String(number)}:${list}`;
29141
+ }
29142
+ function readListPage(answer3, { number, list }) {
29143
+ return readPage2(readMapping3(answer3[listAlias(number, list)], `issue #${String(number)}`)[list], listWhere(number, list));
29144
+ }
29145
+ async function readRemainingPages(gh, firsts, refused3) {
29146
+ const nodes = new Map;
29147
+ const seen = new Map;
29148
+ let pending = firsts.flatMap(({ head: { number }, pages }) => ISSUE_LISTS.flatMap((list) => {
29149
+ const page2 = pages[list];
29150
+ nodes.set(listKey(number, list), [...page2.nodes]);
29151
+ seen.set(listKey(number, list), new Set(page2.next === null ? [] : [page2.next]));
29152
+ return page2.next === null ? [] : [{ number, list, cursor: page2.next }];
29153
+ }));
29154
+ while (pending.length > 0) {
29155
+ const next = [];
29156
+ for (const settled of await readEach(gh, pending, LIST_PAGES)) {
29157
+ const { number, list } = settled.item;
29158
+ if (refused3.has(number))
29159
+ continue;
29160
+ if (!settled.ok) {
29161
+ refused3.set(number, settled.reason);
29162
+ continue;
29163
+ }
29164
+ nodes.get(listKey(number, list))?.push(...settled.value.nodes);
29165
+ const cursor = settled.value.next;
29166
+ if (cursor === null)
29167
+ continue;
29168
+ const cursors = seen.get(listKey(number, list));
29169
+ if (cursors?.has(cursor) === true) {
29170
+ refused3.set(number, `${listWhere(number, list)} answered the cursor ${JSON.stringify(cursor)} a second time, so its next page would repeat one read before`);
29171
+ continue;
29172
+ }
29173
+ cursors?.add(cursor);
29174
+ next.push({ number, list, cursor });
29175
+ }
29176
+ pending = next.filter(({ number }) => !refused3.has(number));
29177
+ }
29178
+ return nodes;
29179
+ }
29180
+ function readIssue(head, lists, repository) {
29181
+ const where = `issue #${String(head.number)}`;
29182
+ const labels = lists.labels.map((label, index) => readString4(readMapping3(label, `${where}.labels[${String(index)}]`)["name"], `${where}.labels[${String(index)}].name`));
29183
+ const linked = lists.closedByPullRequestsReferences.map((pull, index) => readPull(pull, `${where}.closedByPullRequestsReferences[${String(index)}]`));
29184
+ const mentioned = lists.timelineItems.map((event, index) => crossReferencedPull(event, `${where}.timelineItems[${String(index)}]`, head.number, repository));
28868
29185
  const pulls = new Map([...linked, ...mentioned].flatMap((pull) => pull === null ? [] : [[pull.number, pull]]));
28869
29186
  return {
28870
- number,
28871
- state,
28872
- stateReason: reason,
29187
+ ...head,
28873
29188
  labels,
28874
29189
  pulls: [...pulls.values()].sort((left, right) => left.number - right.number)
28875
29190
  };
28876
29191
  }
28877
- async function readIssues(gh, numbers) {
28878
- const read = [];
28879
- for (const batch of batchesOf(numbers, ISSUE_BATCH)) {
28880
- const answer3 = await readRepository(gh, issuesArgs(batch));
28881
- const repository = readString4(answer3["nameWithOwner"], "data.repository.nameWithOwner");
28882
- read.push(...batch.map((number) => readIssue(answer3, number, repository)));
29192
+ function readFirstPagesOf(answer3, number) {
29193
+ return readIssueFirstPages(answer3, number, readString4(answer3["nameWithOwner"], "data.repository.nameWithOwner"));
29194
+ }
29195
+ async function readIssues(gh, numbers, refused3, onRead) {
29196
+ const settledFirsts = [];
29197
+ for (const batch of batchesOf(numbers, ISSUES.size)) {
29198
+ settledFirsts.push(...await readEach(gh, batch, ISSUES));
29199
+ onRead(settledFirsts.length);
28883
29200
  }
28884
- return read;
29201
+ const firsts = settledFirsts.flatMap((settled) => {
29202
+ if (settled.ok)
29203
+ return [settled.value];
29204
+ refused3.set(settled.item, settled.reason);
29205
+ return [];
29206
+ });
29207
+ const nodes = await readRemainingPages(gh, firsts, refused3);
29208
+ return firsts.flatMap(({ head, repository }) => {
29209
+ if (refused3.has(head.number))
29210
+ return [];
29211
+ const lists = Object.fromEntries(ISSUE_LISTS.map((list) => [list, nodes.get(listKey(head.number, list)) ?? []]));
29212
+ const issue4 = settle2(head.number, () => readIssue(head, lists, repository));
29213
+ if (issue4.ok)
29214
+ return [issue4.value];
29215
+ refused3.set(head.number, issue4.reason);
29216
+ return [];
29217
+ });
28885
29218
  }
28886
29219
  function readFilesPage(value, where, prefix) {
28887
29220
  const files = readMapping3(readMapping3(value, where)["files"], `${where}.files`);
@@ -28898,40 +29231,39 @@ function readFilesPage(value, where, prefix) {
28898
29231
  next: pageInfo["hasNextPage"] === true ? readString4(pageInfo["endCursor"], `${where}.files.pageInfo.endCursor`) : null
28899
29232
  };
28900
29233
  }
28901
- async function readAddedFragments(gh, numbers, prefix) {
29234
+ async function readAddedFragments(gh, numbers, prefix, failed2) {
28902
29235
  const added = new Map(numbers.map((number) => [number, []]));
29236
+ const how = {
29237
+ size: PULL_BATCH,
29238
+ argsOf: filesArgs,
29239
+ readOne: (answer3, { number }) => readFilesPage(answer3[pullAlias(number)], `pull request #${String(number)}`, prefix)
29240
+ };
28903
29241
  let pending = numbers.map((number) => ({ number, cursor: null }));
28904
29242
  while (pending.length > 0) {
28905
29243
  const next = [];
28906
- for (const batch of batchesOf(pending, PULL_BATCH)) {
28907
- const answer3 = await readRepository(gh, filesArgs(batch));
28908
- for (const { number } of batch) {
28909
- const page2 = readFilesPage(answer3[pullAlias(number)], `pull request #${String(number)}`, prefix);
28910
- added.get(number)?.push(...page2.paths);
28911
- if (page2.next !== null)
28912
- next.push({ number, cursor: page2.next });
29244
+ for (const settled of await readEach(gh, pending, how)) {
29245
+ const { number } = settled.item;
29246
+ if (!settled.ok) {
29247
+ failed2.set(number, settled.reason);
29248
+ continue;
28913
29249
  }
29250
+ added.get(number)?.push(...settled.value.paths);
29251
+ if (settled.value.next !== null)
29252
+ next.push({ number, cursor: settled.value.next });
28914
29253
  }
28915
29254
  pending = next;
28916
29255
  }
28917
29256
  return added;
28918
29257
  }
28919
- async function readBlobs(gh, lookups) {
28920
- const read = [];
28921
- for (const batch of batchesOf(lookups, FRAGMENT_BATCH)) {
28922
- const answer3 = await readRepository(gh, blobsArgs(batch));
28923
- read.push(...batch.map(({ path: path3 }, index) => {
28924
- const { head, base } = blobAliases(index);
28925
- if (!(head in answer3) || !(base in answer3))
28926
- refuse8(`the blob read answered no ${head} or ${base} for ${path3}`);
28927
- const headBlob = answer3[head];
28928
- const text2 = headBlob === null ? null : readMapping3(headBlob, `${path3} at the head`)["text"];
28929
- if (text2 !== null && typeof text2 !== "string")
28930
- refuse8(`${path3} at the head has text ${describeValue(text2)}, expected a string`);
28931
- return { text: text2, onBase: answer3[base] !== null };
28932
- }));
28933
- }
28934
- return read;
29258
+ function readBlobsOf(answer3, { path: path3 }, index) {
29259
+ const { head, base } = blobAliases(index);
29260
+ if (!(head in answer3) || !(base in answer3))
29261
+ refuse8(`the blob read answered no ${head} or ${base} for ${path3}`);
29262
+ const headBlob = answer3[head];
29263
+ const text2 = headBlob === null ? null : readMapping3(headBlob, `${path3} at the head`)["text"];
29264
+ if (text2 !== null && typeof text2 !== "string")
29265
+ refuse8(`${path3} at the head has text ${describeValue(text2)}, expected a string`);
29266
+ return { text: text2, onBase: answer3[base] !== null };
28935
29267
  }
28936
29268
  function fragmentOf(path3, blobs) {
28937
29269
  if (blobs.text === null) {
@@ -28946,22 +29278,47 @@ function decidingFragment(fragments) {
28946
29278
  function blobKey(pull, path3) {
28947
29279
  return `${String(pull)}:${path3}`;
28948
29280
  }
29281
+ async function readFragments(gh, pulls, prefix, failed2) {
29282
+ const added = await readAddedFragments(gh, pulls.map(({ number }) => number), prefix, failed2);
29283
+ const lookups = pulls.filter(({ number }) => !failed2.has(number)).flatMap((pull) => (added.get(pull.number) ?? []).map((path3) => ({ pull: pull.number, path: path3, headRefOid: pull.headRefOid, baseRefName: pull.baseRefName })));
29284
+ const fragments = new Map((await readEach(gh, lookups, BLOBS)).flatMap((settled) => {
29285
+ if (settled.ok)
29286
+ return [[blobKey(settled.item.pull, settled.item.path), fragmentOf(settled.item.path, settled.value)]];
29287
+ failed2.set(settled.item.pull, settled.reason);
29288
+ return [];
29289
+ }));
29290
+ return { added, fragments };
29291
+ }
28949
29292
  async function readIssueFacts(options, numbers) {
28950
29293
  const { gh } = options;
28951
- const prefix = directoryPrefix(options.fragments);
28952
- const issues = await readIssues(gh, [...new Set(numbers)]);
29294
+ const asked = [...new Set(numbers)];
29295
+ const refused3 = new Map;
29296
+ const failed2 = new Map;
29297
+ const issues = await readIssues(gh, asked, refused3, options.onRead ?? (() => {
29298
+ return;
29299
+ }));
28953
29300
  const pulls = [...new Map(issues.flatMap(({ pulls: read }) => read.map((pull) => [pull.number, pull]))).values()];
28954
- const added = await readAddedFragments(gh, pulls.map(({ number }) => number), prefix);
28955
- const lookups = pulls.flatMap((pull) => (added.get(pull.number) ?? []).map((path3) => ({ pull: pull.number, path: path3, headRefOid: pull.headRefOid, baseRefName: pull.baseRefName })));
28956
- const blobs = await readBlobs(gh, lookups);
28957
- const fragments = new Map(lookups.map(({ pull, path: path3 }, index) => [blobKey(pull, path3), fragmentOf(path3, blobs[index] ?? { text: null, onBase: false })]));
29301
+ const { added, fragments } = await readFragments(gh, pulls, directoryPrefix(options.fragments), failed2);
28958
29302
  const factsOf = (pull) => {
28959
29303
  const own = [...added.get(pull.number) ?? []].sort().flatMap((path3) => fragments.get(blobKey(pull.number, path3)) ?? []);
28960
29304
  return { number: pull.number, state: pull.state, baseRefName: pull.baseRefName, fragments: own, fragment: decidingFragment(own) };
28961
29305
  };
28962
- return new Map(issues.map(({ pulls: read, ...issue4 }) => [issue4.number, { ...issue4, pullRequests: read.map(factsOf) }]));
29306
+ const facts = new Map(issues.flatMap(({ pulls: read, ...issue4 }) => {
29307
+ const unread3 = read.find(({ number }) => failed2.has(number));
29308
+ if (unread3 === undefined)
29309
+ return [[issue4.number, { ...issue4, pullRequests: read.map(factsOf) }]];
29310
+ refused3.set(issue4.number, `pull request #${String(unread3.number)} could not be read: ${failed2.get(unread3.number) ?? ""}`);
29311
+ return [];
29312
+ }));
29313
+ return {
29314
+ facts,
29315
+ refused: asked.flatMap((number) => {
29316
+ const reason = refused3.get(number);
29317
+ return reason === undefined ? [] : [{ number, reason }];
29318
+ })
29319
+ };
28963
29320
  }
28964
- var UNREAD_LEVEL = "patch", PREFIX29 = "board project facts", ADDED = "ADDED", CLOSING_REFERENCE;
29321
+ var UNREAD_LEVEL = "patch", PREFIX29 = "board project facts", ADDED = "ADDED", CLOSING_REFERENCE, LIST_PAGES, ISSUES, BLOBS;
28965
29322
  var init_facts = __esm(() => {
28966
29323
  init_config_sections();
28967
29324
  init_fragment_tree();
@@ -28970,6 +29327,9 @@ var init_facts = __esm(() => {
28970
29327
  init_rules();
28971
29328
  init_facts_query();
28972
29329
  CLOSING_REFERENCE = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?):?\s+(?:([\w.-]+\/[\w.-]+))?#(\d+)\b/giu;
29330
+ LIST_PAGES = { size: LIST_BATCH, argsOf: listPagesArgs, readOne: readListPage };
29331
+ ISSUES = { size: ISSUE_BATCH, argsOf: issuesArgs, readOne: readFirstPagesOf };
29332
+ BLOBS = { size: FRAGMENT_BATCH, argsOf: blobsArgs, readOne: readBlobsOf };
28973
29333
  });
28974
29334
 
28975
29335
  // src/board/project/gh.ts
@@ -29002,7 +29362,7 @@ function readList3(value, where) {
29002
29362
  refuse9(`${where} is ${describeValue(value)}, expected a list`);
29003
29363
  return value;
29004
29364
  }
29005
- function readOnePage2(value, where) {
29365
+ function readOnePage(value, where) {
29006
29366
  const connection = readMapping4(value, where);
29007
29367
  if (readMapping4(connection["pageInfo"], `${where}.pageInfo`)["hasNextPage"] !== false) {
29008
29368
  refuse9(`${where} holds more than ${String(PROJECT_PAGE_SIZE)} entries, one page`);
@@ -29061,7 +29421,7 @@ function readProject(owner, value, where) {
29061
29421
  url: readString5(project["url"], `${where}.url`),
29062
29422
  closed: readBoolean2(project["closed"], `${where}.closed`),
29063
29423
  public: readBoolean2(project["public"], `${where}.public`),
29064
- fields: readOnePage2(project["fields"], `${where}.fields`).map((field5, index) => readField(field5, `${where}.fields.nodes[${String(index)}]`))
29424
+ fields: readOnePage(project["fields"], `${where}.fields`).map((field5, index) => readField(field5, `${where}.fields.nodes[${String(index)}]`))
29065
29425
  };
29066
29426
  }
29067
29427
  function findArgs(ref6) {
@@ -29159,7 +29519,7 @@ function readContent(item, where) {
29159
29519
  }
29160
29520
  function readItem2(value, where) {
29161
29521
  const item = readMapping4(value, where);
29162
- const values = readOnePage2(item["fieldValues"], `${where}.fieldValues`).map((node, index) => readValue(node, `${where}.fieldValues.nodes[${String(index)}]`));
29522
+ const values = readOnePage(item["fieldValues"], `${where}.fieldValues`).map((node, index) => readValue(node, `${where}.fieldValues.nodes[${String(index)}]`));
29163
29523
  return {
29164
29524
  id: readString5(item["id"], `${where}.id`),
29165
29525
  archived: readBoolean2(item["isArchived"], `${where}.isArchived`),
@@ -29254,6 +29614,128 @@ var init_gh2 = __esm(() => {
29254
29614
  NUMBERED_TYPES = Object.freeze({ ISSUE: "issue", PULL_REQUEST: "pull-request" });
29255
29615
  });
29256
29616
 
29617
+ // src/board/project/progress.ts
29618
+ function formatElapsed(elapsedMs) {
29619
+ const seconds = Math.floor(Math.max(0, elapsedMs) / MS_PER_SECOND3);
29620
+ const hours = Math.floor(seconds / SECONDS_PER_HOUR);
29621
+ const minutes = Math.floor(seconds % SECONDS_PER_HOUR / SECONDS_PER_MINUTE);
29622
+ const rest = seconds % SECONDS_PER_MINUTE;
29623
+ if (hours > 0)
29624
+ return `${String(hours)}h ${String(minutes)}m ${String(rest)}s`;
29625
+ if (minutes > 0)
29626
+ return `${String(minutes)}m ${String(rest)}s`;
29627
+ return `${String(rest)}s`;
29628
+ }
29629
+ function waitLine(waitMs) {
29630
+ return `waiting ${String(Math.ceil(Math.max(0, waitMs) / MS_PER_SECOND3))} s for GitHub's write limit`;
29631
+ }
29632
+ function progressEvent(line, now) {
29633
+ return {
29634
+ type: "event",
29635
+ name: PROGRESS_EVENT,
29636
+ summary: line.text,
29637
+ data: { ...line.data },
29638
+ ts: now.toISOString()
29639
+ };
29640
+ }
29641
+ function phaseReporter(options) {
29642
+ const label = PROGRESS_PHASES[options.phase];
29643
+ const total2 = String(options.total);
29644
+ const gapMs = options.progressSeconds === false ? null : options.progressSeconds * MS_PER_SECOND3;
29645
+ let startedAt = options.now();
29646
+ let lastLineAt = startedAt;
29647
+ let doneSoFar = 0;
29648
+ const base = (step, done, at) => ({
29649
+ phase: options.phase,
29650
+ step,
29651
+ done,
29652
+ total: options.total,
29653
+ elapsedMs: at - startedAt
29654
+ });
29655
+ return {
29656
+ start: () => {
29657
+ startedAt = options.now();
29658
+ lastLineAt = startedAt;
29659
+ return { text: `${label}: ${total2}`, data: base("start", 0, startedAt) };
29660
+ },
29661
+ advance: (done) => {
29662
+ doneSoFar = done;
29663
+ if (gapMs === null)
29664
+ return;
29665
+ const at = options.now();
29666
+ if (at - lastLineAt < gapMs)
29667
+ return;
29668
+ lastLineAt = at;
29669
+ const data = base("progress", done, at);
29670
+ return { text: `${label}: ${String(done)}/${total2}, ${formatElapsed(data.elapsedMs)}`, data };
29671
+ },
29672
+ end: ({ done, refused: refused3 }) => {
29673
+ const data = { ...base("end", done, options.now()), refused: refused3 };
29674
+ return {
29675
+ text: `${label}: ${String(done)}/${total2}, ${String(refused3)} refused, ${formatElapsed(data.elapsedMs)}`,
29676
+ data
29677
+ };
29678
+ },
29679
+ wait: (waitMs) => {
29680
+ const data = { ...base("wait", doneSoFar, options.now()), waitMs };
29681
+ return { text: waitLine(waitMs), data };
29682
+ }
29683
+ };
29684
+ }
29685
+ function openPhase(feed, phase, total2) {
29686
+ if (feed === undefined)
29687
+ return SILENT_PHASE;
29688
+ const reporter = phaseReporter({ phase, total: total2, now: feed.now, progressSeconds: feed.progressSeconds });
29689
+ feed.sink(reporter.start());
29690
+ return {
29691
+ advance: (done) => {
29692
+ const line = reporter.advance(done);
29693
+ if (line !== undefined)
29694
+ feed.sink(line);
29695
+ },
29696
+ wait: (waitMs) => {
29697
+ feed.sink(reporter.wait(waitMs));
29698
+ },
29699
+ end: (result) => {
29700
+ feed.sink(reporter.end(result));
29701
+ }
29702
+ };
29703
+ }
29704
+ function progressSink(output, mode, stamp2 = () => new Date) {
29705
+ return (line) => {
29706
+ if (mode === "json")
29707
+ output.emit(progressEvent(line, stamp2()));
29708
+ else
29709
+ output.info(line.text);
29710
+ };
29711
+ }
29712
+ function commandProgressFeed(output, mode, progressSeconds, clocks = {}) {
29713
+ return {
29714
+ sink: progressSink(output, mode, clocks.stamp),
29715
+ now: clocks.now ?? (() => Date.now()),
29716
+ progressSeconds
29717
+ };
29718
+ }
29719
+ var PROGRESS_EVENT = "progress", PROGRESS_PHASES, SILENT_PHASE, MS_PER_SECOND3 = 1000, SECONDS_PER_MINUTE = 60, SECONDS_PER_HOUR = 3600;
29720
+ var init_progress2 = __esm(() => {
29721
+ PROGRESS_PHASES = Object.freeze({
29722
+ adds: "adding issues",
29723
+ facts: "reading facts",
29724
+ writes: "writing fields"
29725
+ });
29726
+ SILENT_PHASE = Object.freeze({
29727
+ advance: () => {
29728
+ return;
29729
+ },
29730
+ wait: () => {
29731
+ return;
29732
+ },
29733
+ end: () => {
29734
+ return;
29735
+ }
29736
+ });
29737
+ });
29738
+
29257
29739
  // src/board/project/refresh-values.ts
29258
29740
  function projectValuesOf(facts, board) {
29259
29741
  return {
@@ -29359,8 +29841,16 @@ function writeBatchArgs(projectId2, batch) {
29359
29841
  const query = `mutation(${declarations}) { ${parts.map((part) => part.mutation).join(" ")} }`;
29360
29842
  return graphqlArgs2(query, [["-f", "project", projectId2], ...parts.flatMap((part) => part.fields)]);
29361
29843
  }
29362
- function batchesOf2(writes) {
29363
- return Array.from({ length: Math.ceil(writes.length / PROJECT_WRITE_BATCH_SIZE) }, (_, index) => writes.slice(index * PROJECT_WRITE_BATCH_SIZE, (index + 1) * PROJECT_WRITE_BATCH_SIZE));
29844
+ function requirePace(batchSize, pauseMs) {
29845
+ if (!Number.isSafeInteger(batchSize) || batchSize < 1) {
29846
+ throw new RangeError(`not a batch size of writes: ${String(batchSize)}, expected a whole number from 1`);
29847
+ }
29848
+ if (!Number.isSafeInteger(pauseMs) || pauseMs < 0) {
29849
+ throw new RangeError(`not a pause between write requests: ${String(pauseMs)}, expected a whole number from 0`);
29850
+ }
29851
+ }
29852
+ function batchesOf2(writes, batchSize) {
29853
+ return Array.from({ length: Math.ceil(writes.length / batchSize) }, (_, index) => writes.slice(index * batchSize, (index + 1) * batchSize));
29364
29854
  }
29365
29855
  function isRateLimited(stdout, stderr) {
29366
29856
  const answer3 = parsed(stdout);
@@ -29397,21 +29887,31 @@ async function sendBatch(gh, args, size) {
29397
29887
  function itemCount(writes) {
29398
29888
  return new Set(writes.map((write) => write.itemId)).size;
29399
29889
  }
29400
- async function writeProjectFields(gh, projectId2, writes, seams = {}) {
29401
- const sleep = seams.sleep ?? ((ms) => Bun.sleep(ms));
29402
- const batches = batchesOf2(writes);
29890
+ async function writeProjectFields(gh, projectId2, writes, options) {
29891
+ const { batchSize, pauseMs } = options;
29892
+ requirePace(batchSize, pauseMs);
29893
+ const sleep = options.sleep ?? ((ms) => Bun.sleep(ms));
29894
+ const batches = batchesOf2(writes, batchSize);
29403
29895
  const requests = batches.map((batch) => writeBatchArgs(projectId2, batch));
29896
+ const phase = openPhase(options.progress, "writes", writes.length);
29404
29897
  let sent = 0;
29405
29898
  for (const [index, batch] of batches.entries()) {
29406
- if (index > 0)
29407
- await sleep(PROJECT_WRITE_PAUSE_MS);
29899
+ if (index > 0) {
29900
+ if (pauseMs > 0)
29901
+ phase.wait(pauseMs);
29902
+ await sleep(pauseMs);
29903
+ }
29408
29904
  const outcome2 = await sendBatch(gh, requests[index] ?? [], batch.length);
29409
29905
  if (outcome2.rateLimited) {
29410
29906
  const unwritten = [...batch.filter((_, at) => !outcome2.landed.has(at)), ...batches.slice(index + 1).flat()];
29411
- return { written: sent + outcome2.landed.size, notUpdated: itemCount(unwritten), rateLimited: true, detail: outcome2.detail };
29907
+ const written4 = sent + outcome2.landed.size;
29908
+ phase.end({ done: written4, refused: writes.length - written4 });
29909
+ return { written: written4, notUpdated: itemCount(unwritten), rateLimited: true, detail: outcome2.detail };
29412
29910
  }
29413
29911
  sent += batch.length;
29912
+ phase.advance(sent);
29414
29913
  }
29914
+ phase.end({ done: sent, refused: 0 });
29415
29915
  return { written: sent, notUpdated: 0, rateLimited: false, detail: "" };
29416
29916
  }
29417
29917
  var RATE_LIMITED = "RATE_LIMITED", RATE_LIMIT_TEXT, MUTATIONS, VALUE_INPUTS;
@@ -29419,6 +29919,7 @@ var init_writes = __esm(() => {
29419
29919
  init_config_sections();
29420
29920
  init_gh2();
29421
29921
  init_port();
29922
+ init_progress2();
29422
29923
  RATE_LIMIT_TEXT = /\brate limit/iu;
29423
29924
  MUTATIONS = Object.freeze({ set: "updateProjectV2ItemFieldValue", clear: "clearProjectV2ItemFieldValue" });
29424
29925
  VALUE_INPUTS = Object.freeze({
@@ -29429,6 +29930,15 @@ var init_writes = __esm(() => {
29429
29930
  });
29430
29931
 
29431
29932
  // src/board/project/refresh.ts
29933
+ function writesOptionsOf(options) {
29934
+ const { sleep, progress } = options;
29935
+ return {
29936
+ batchSize: options.config.boardProjectWriteBatchSize,
29937
+ pauseMs: options.config.boardProjectWritePauseMs,
29938
+ ...sleep === undefined ? {} : { sleep },
29939
+ ...progress === undefined ? {} : { progress }
29940
+ };
29941
+ }
29432
29942
  function issueNumbers2(issues) {
29433
29943
  const bad = issues.find((issue4) => !Number.isSafeInteger(issue4) || issue4 < 1);
29434
29944
  if (bad !== undefined)
@@ -29476,16 +29986,17 @@ async function readRefreshBoard(config, gh, repository) {
29476
29986
  async function changesFor(options, repository, project, present, read) {
29477
29987
  const { config, gh } = options;
29478
29988
  const numbers = [...present.keys()];
29479
- const facts = await readIssueFacts({ gh, fragments: config.releaseFragments }, numbers);
29989
+ const phase = openPhase(options.progress, "facts", numbers.length);
29990
+ const { facts, refused: refused3 } = await readIssueFacts({ gh, fragments: config.releaseFragments, onRead: phase.advance }, numbers);
29991
+ phase.end({ done: facts.size, refused: refused3.length });
29480
29992
  const board = read ?? await readRefreshBoard(config, gh, repository);
29481
29993
  const { matched } = matchProjectFields(project);
29482
- return numbers.flatMap((issue4) => {
29483
- const read2 = facts.get(issue4);
29994
+ const changes = numbers.flatMap((issue4) => {
29995
+ const issueFacts = facts.get(issue4);
29484
29996
  const item = present.get(issue4);
29485
- if (read2 === undefined || item === undefined)
29486
- throw new Error(`${PREFIX30}: no facts were read for #${String(issue4)}`);
29487
- return projectChangesOf(issue4, item, projectValuesOf(read2, board), matched);
29997
+ return issueFacts === undefined || item === undefined ? [] : projectChangesOf(issue4, item, projectValuesOf(issueFacts, board), matched);
29488
29998
  });
29999
+ return { changes, refused: refused3 };
29489
30000
  }
29490
30001
  function rankShifted(issue4, item, rankField, rank) {
29491
30002
  const values = { stage: null, horizon: null, rank, blockedBy: null, progress: null };
@@ -29517,13 +30028,14 @@ async function refreshOn(options, repository, ref6, asked, widening) {
29517
30028
  }));
29518
30029
  const missing2 = numbers.filter((issue4) => !present.has(issue4));
29519
30030
  const { mismatched: skipped2 } = matchProjectFields(project);
29520
- const changes = present.size === 0 ? [] : await changesFor(options, repository, project, present, board);
29521
- const writes = changes.length === 0 || options.dryRun === true ? NOTHING_WRITTEN : await writeProjectFields(gh, project.id, changes.map(({ write }) => write), options.sleep === undefined ? {} : { sleep: options.sleep });
30031
+ const { changes, refused: refused3 } = present.size === 0 ? NOTHING_READ : await changesFor(options, repository, project, present, board);
30032
+ const writes = changes.length === 0 || options.dryRun === true ? NOTHING_WRITTEN : await writeProjectFields(gh, project.id, changes.map(({ write }) => write), writesOptionsOf(options));
29522
30033
  const warnings = [
30034
+ ...refused3.map(notRefreshedWarning),
29523
30035
  ...writes.rateLimited ? [rateLimitWarning(writes.notUpdated)] : [],
29524
30036
  ...skipped2.map(skippedFieldWarning)
29525
30037
  ];
29526
- return { kind: "refreshed", project: ref6, changes, writes, missing: missing2, skipped: skipped2, warnings };
30038
+ return { kind: "refreshed", project: ref6, changes, writes, missing: missing2, skipped: skipped2, refused: refused3, warnings };
29527
30039
  }
29528
30040
  async function refreshProjectItems(options, issues, widening = {}) {
29529
30041
  const { config, gh } = options;
@@ -29549,7 +30061,7 @@ async function refreshProjectItems(options, issues, widening = {}) {
29549
30061
  throw error;
29550
30062
  }
29551
30063
  }
29552
- var PREFIX30 = "board project refresh", NOTHING_WRITTEN, NO_UNLABELLED_SEARCH = () => Promise.resolve([]);
30064
+ var PREFIX30 = "board project refresh", NOTHING_READ, NOTHING_WRITTEN, NO_UNLABELLED_SEARCH = () => Promise.resolve([]);
29553
30065
  var init_refresh = __esm(() => {
29554
30066
  init_move_native();
29555
30067
  init_boards();
@@ -29560,16 +30072,24 @@ var init_refresh = __esm(() => {
29560
30072
  init_facts();
29561
30073
  init_gh2();
29562
30074
  init_port();
30075
+ init_progress2();
29563
30076
  init_refresh_values();
29564
30077
  init_refresh_warnings();
29565
30078
  init_rules();
29566
30079
  init_writes();
30080
+ NOTHING_READ = Object.freeze({ changes: [], refused: [] });
29567
30081
  NOTHING_WRITTEN = Object.freeze({ written: 0, notUpdated: 0, rateLimited: false, detail: "" });
29568
30082
  });
29569
30083
 
29570
30084
  // src/board/project/issue-board-refresh.ts
29571
30085
  function refreshIssueItems(options, issues, widening = {}) {
29572
- return refreshProjectItems(options, issues, widening);
30086
+ return refreshProjectItems(retrying(options), issues, widening);
30087
+ }
30088
+ function retrying(options) {
30089
+ if (options.config.boardProjectNumber === null)
30090
+ return options;
30091
+ const seams = options.sleep === undefined ? {} : { sleep: options.sleep };
30092
+ return { ...options, gh: openProjectRunner(options.gh, options.config, seams) };
29573
30093
  }
29574
30094
  function refreshFailedWarning(issue4, error) {
29575
30095
  return `The project was not updated for #${String(issue4)}: ${messageOf(error)}. Run \`${BOARD_SYNC_FIX}\` to catch up.`;
@@ -29609,7 +30129,7 @@ function createRefreshingGhIssueBoard(options) {
29609
30129
  });
29610
30130
  const refreshOptions = sleep === undefined ? { config, gh } : { config, gh, sleep };
29611
30131
  return withProjectRefresh(createGhIssueBoard({ gh }), {
29612
- refresh: (issues) => refreshProjectItems(refreshOptions, issues),
30132
+ refresh: (issues) => refreshProjectItems(retrying(refreshOptions), issues),
29613
30133
  warn
29614
30134
  });
29615
30135
  }
@@ -29617,6 +30137,7 @@ var init_issue_board_refresh = __esm(() => {
29617
30137
  init_active();
29618
30138
  init_config_sections();
29619
30139
  init_issue_board();
30140
+ init_project_runner();
29620
30141
  init_refresh_warnings();
29621
30142
  init_refresh();
29622
30143
  });
@@ -31941,8 +32462,17 @@ async function addIssues(options, ref6, issues) {
31941
32462
  const project = await port.find(ref6);
31942
32463
  if (project === null)
31943
32464
  return null;
31944
- for (const number of issues)
31945
- await port.addItem(project.id, { repository, number });
32465
+ const phase = openPhase(options.progress, "adds", issues.length);
32466
+ let added = 0;
32467
+ try {
32468
+ for (const number of issues) {
32469
+ await port.addItem(project.id, { repository, number });
32470
+ added += 1;
32471
+ phase.advance(added);
32472
+ }
32473
+ } finally {
32474
+ phase.end({ done: added, refused: issues.length - added });
32475
+ }
31946
32476
  return issues;
31947
32477
  }
31948
32478
  async function addMissing(options, first, synced) {
@@ -31959,6 +32489,7 @@ async function addMissing(options, first, synced) {
31959
32489
  ...synced,
31960
32490
  changes: [...first.changes, ...second.changes],
31961
32491
  added,
32492
+ refused: [...first.refused, ...second.refused],
31962
32493
  writes: addedWrites(first.writes, second.writes),
31963
32494
  warnings: [...first.warnings, ...second.warnings.filter((line) => !first.warnings.includes(line))]
31964
32495
  };
@@ -31979,6 +32510,7 @@ async function syncProject(options) {
31979
32510
  changes: first.changes,
31980
32511
  missing: first.missing,
31981
32512
  added: [],
32513
+ refused: first.refused,
31982
32514
  writes: first.writes,
31983
32515
  warnings: first.warnings
31984
32516
  });
@@ -31987,6 +32519,7 @@ var WHOLE_PROJECT;
31987
32519
  var init_sync = __esm(() => {
31988
32520
  init_move_native();
31989
32521
  init_gh2();
32522
+ init_progress2();
31990
32523
  init_refresh_warnings();
31991
32524
  init_refresh();
31992
32525
  WHOLE_PROJECT = Object.freeze({ everyItem: true, openIssues: true });
@@ -31999,17 +32532,21 @@ function counted4(count2, noun) {
31999
32532
  function changeLine(change) {
32000
32533
  return `#${String(change.issue)} ${change.name}: ${change.from ?? EMPTY_VALUE} \u2192 ${change.to ?? EMPTY_VALUE}`;
32001
32534
  }
32535
+ function refusedPart(result) {
32536
+ return result.refused.length === 0 ? "" : `; ${counted4(result.refused.length, "issue")} not refreshed`;
32537
+ }
32002
32538
  function closingLine(result) {
32003
32539
  const project = `project #${String(result.project.number)}`;
32004
32540
  const adds = result.dryRun ? result.missing : result.added;
32005
32541
  const changes = counted4(result.changes.length, "change");
32006
32542
  const issues = counted4(adds.length, "issue");
32543
+ const inStep = result.changes.length === 0 && adds.length === 0 && result.refused.length === 0;
32007
32544
  if (result.dryRun) {
32008
- return result.changes.length === 0 && adds.length === 0 ? `Dry run on ${project}: in step, nothing to change.` : `Dry run on ${project}: ${changes} and ${issues} to add; nothing written.`;
32545
+ return inStep ? `Dry run on ${project}: in step, nothing to change.` : `Dry run on ${project}: ${changes} and ${issues} to add${refusedPart(result)}; nothing written.`;
32009
32546
  }
32010
32547
  if (result.rateLimited)
32011
32548
  return `Stopped on ${project}: ${String(result.written)} of ${changes} written; the rate limit refused the rest.`;
32012
- return result.changes.length === 0 && adds.length === 0 ? `Synced ${project}: in step, nothing to change.` : `Synced ${project}: ${changes} written and ${issues} added.`;
32549
+ return inStep ? `Synced ${project}: in step, nothing to change.` : `Synced ${project}: ${changes} written and ${issues} added${refusedPart(result)}.`;
32013
32550
  }
32014
32551
  function syncLines(result) {
32015
32552
  const adds = result.dryRun ? result.missing.map((issue4) => `#${String(issue4)} is not on the project: would be added`) : result.added.map((issue4) => `#${String(issue4)} added to the project`);
@@ -32025,6 +32562,7 @@ function syncResultOf(synced) {
32025
32562
  changes: synced.changes.map(plainChange),
32026
32563
  missing: synced.missing,
32027
32564
  added: synced.added,
32565
+ refused: synced.refused.map(({ number, reason }) => ({ issue: number, reason })),
32028
32566
  written: synced.writes.written,
32029
32567
  rateLimited: synced.writes.rateLimited,
32030
32568
  notUpdated: synced.writes.notUpdated,
@@ -32042,9 +32580,10 @@ async function runBoardSync(context, seams) {
32042
32580
  if (config.boardProjectNumber === null) {
32043
32581
  throw new CommandExit(1, `\u274C board.project.number is not set, so there is no project to sync. Run \`${PROJECT_INIT_FIX}\` to create one.`);
32044
32582
  }
32045
- const gh = seams.gh ?? createGhRunner({ cwd: project.root });
32583
+ const gh = openProjectRunner(seams.gh ?? createGhRunner({ cwd: project.root }), config, commandRetrySeams(context.output, context.outputMode, seams.sleep));
32046
32584
  const dryRun = context.flags[DRY_RUN_FLAG3] === true;
32047
- const synced = await syncProject(seams.sleep === undefined ? { config, gh, dryRun } : { config, gh, dryRun, sleep: seams.sleep }).catch((error) => {
32585
+ const progress = commandProgressFeed(context.output, context.outputMode, config.boardProjectProgressSeconds, seams.now === undefined ? {} : { now: seams.now });
32586
+ const synced = await syncProject(seams.sleep === undefined ? { config, gh, dryRun, progress } : { config, gh, dryRun, progress, sleep: seams.sleep }).catch((error) => {
32048
32587
  throw new CommandExit(BOARD_SYNC_REFUSAL_EXIT, `\u274C Could not sync the project: ${messageOf(error)}`);
32049
32588
  });
32050
32589
  if (synced.kind !== "synced") {
@@ -32074,7 +32613,7 @@ function createBoardSyncCommand(seams = {}) {
32074
32613
  subject: "board",
32075
32614
  action: "sync",
32076
32615
  summary: "bring the repository's GitHub project in step with its issues",
32077
- description: "Refreshes every item of this repository on the GitHub project board.project.number names, writing" + " only the Stage, Horizon, Rank, Blocked by and Progress values that differ from what rafa reads of the" + " issues, then adds every open issue missing from the project and fills its fields. It repairs what changed" + " outside rafa: a label edited in the web UI, an issue closed by hand, the Roadmap issue's order edited in" + " its body. Prints one line per change and per issue added, then a closing count. With `--dry-run` it" + " prints the same lines and writes nothing. With `--output=json` the changes are the data of the terminal" + " result event. Refused with exit code 1 when board.project.number is unset, and with exit code 2 when the" + " token lacks the project scope, the number names no project, or the rate limit stopped the writes.",
32616
+ description: "Refreshes every item of this repository on the GitHub project board.project.number names, writing" + " only the Stage, Horizon, Rank, Blocked by and Progress values that differ from what rafa reads of the" + " issues, then adds every open issue missing from the project and fills its fields. It repairs what changed" + " outside rafa: a label edited in the web UI, an issue closed by hand, the Roadmap issue's order edited in" + " its body. Prints one line per change and per issue added, then a closing count. With `--dry-run` it" + " prints the same lines and writes nothing. With `--output=json` the changes are the data of the terminal" + " result event. An issue whose facts could not be read is named in a warning, `#<n> not refreshed:" + " <reason>`, while the others are synced; the exit code stays 0 and the next run reads it again. Adding" + " the issues, reading their facts and writing their fields each print a start line with the total, a" + " progress line at most every board.project.progressSeconds, as `reading facts: 146/283, 6m 40s`, and an" + " end line; with `--output=json` each is a `progress` event. Refused" + " with exit code 1 when board.project.number is unset, and with exit code 2 when the token lacks the" + " project scope, the number names no project, or the rate limit stopped the writes.",
32078
32617
  args: [],
32079
32618
  flags: [
32080
32619
  {
@@ -32107,6 +32646,8 @@ function createBoardSyncCommand(seams = {}) {
32107
32646
  var USAGE10 = "rafa board sync [--dry-run]", DRY_RUN_FLAG3 = "dry-run", BOARD_SYNC_REFUSAL_EXIT = 2, PROJECT_INIT_FIX = "rafa init --board --project", EMPTY_VALUE = "(empty)", sync_default;
32108
32647
  var init_sync2 = __esm(() => {
32109
32648
  init_github();
32649
+ init_progress2();
32650
+ init_project_runner();
32110
32651
  init_refresh_warnings();
32111
32652
  init_sync();
32112
32653
  init_command();
@@ -36398,7 +36939,7 @@ function settleWhy(waiting) {
36398
36939
  return `${count2} ${wait} on ${waiting.base} and ${fold} into ${waiting.version}`;
36399
36940
  }
36400
36941
  function readFollowUps(reading2) {
36401
- const { liveLoopBranches, rafaCheckout, runtimeInstalled, settle: settle2, version: version2 } = reading2;
36942
+ const { liveLoopBranches, rafaCheckout, runtimeInstalled, settle: settle3, version: version2 } = reading2;
36402
36943
  const followUps = [];
36403
36944
  if (version2 !== null && rafaCheckout && !runtimeInstalled) {
36404
36945
  followUps.push({
@@ -36407,8 +36948,8 @@ function readFollowUps(reading2) {
36407
36948
  why: selfUpdateWhy(version2, liveLoopBranches)
36408
36949
  });
36409
36950
  }
36410
- if (settle2 !== null) {
36411
- followUps.push({ id: "release-settle", command: RELEASE_SETTLE_COMMAND, why: settleWhy(settle2) });
36951
+ if (settle3 !== null) {
36952
+ followUps.push({ id: "release-settle", command: RELEASE_SETTLE_COMMAND, why: settleWhy(settle3) });
36412
36953
  }
36413
36954
  return Object.freeze(followUps.map((followUp) => Object.freeze(followUp)));
36414
36955
  }
@@ -37296,7 +37837,7 @@ function parseBranchLine(line) {
37296
37837
  if (unix === "" || !Number.isInteger(seconds)) {
37297
37838
  return `git for-each-ref wrote no commit date for ${name2}: ${JSON.stringify(unix)}`;
37298
37839
  }
37299
- const lastCommit = new Date(seconds * MS_PER_SECOND2);
37840
+ const lastCommit = new Date(seconds * MS_PER_SECOND4);
37300
37841
  const current = head === "*";
37301
37842
  if (upstreamField === "") {
37302
37843
  return { current, branch: { name: name2, upstream: null, gone: false, ahead: null, lastCommit } };
@@ -37313,7 +37854,7 @@ function aheadOf(track) {
37313
37854
  }
37314
37855
  return Number(count2);
37315
37856
  }
37316
- var BRANCH_FORMAT, REMOTE3 = "origin", FIELD_COUNT = 5, MS_PER_SECOND2 = 1000, GONE_TRACK = "gone";
37857
+ var BRANCH_FORMAT, REMOTE3 = "origin", FIELD_COUNT = 5, MS_PER_SECOND4 = 1000, GONE_TRACK = "gone";
37317
37858
  var init_branches = __esm(() => {
37318
37859
  init_sources();
37319
37860
  init_git();
@@ -44773,41 +45314,76 @@ async function missingIssues(options, repository, project) {
44773
45314
  const held = heldIssues(await createGhProjectPort(gh).items(project.id), repository);
44774
45315
  const ranked = board.closed.filter((issue4) => rankOf(board.ranks, issue4) !== null);
44775
45316
  const unread4 = board.closed.filter((issue4) => rankOf(board.ranks, issue4) === null && !held.has(issue4));
44776
- const facts = await readIssueFacts({ gh, fragments: config.releaseFragments }, unread4);
45317
+ const { facts } = await readIssueFacts({ gh, fragments: config.releaseFragments }, unread4);
44777
45318
  const inReview = unread4.filter((issue4) => {
44778
45319
  const read = facts.get(issue4);
44779
45320
  return read !== undefined && stageOf(read) === "In review";
44780
45321
  });
44781
- return [...new Set([...board.open, ...ranked, ...inReview])].filter((issue4) => !held.has(issue4)).sort((a, b) => a - b);
45322
+ const missing2 = [...new Set([...board.open, ...ranked, ...inReview])].filter((issue4) => !held.has(issue4)).sort((a, b) => a - b);
45323
+ return { held, missing: missing2 };
44782
45324
  }
44783
45325
  async function itemsStep(options, repository, project) {
44784
45326
  const sleep = options.sleep ?? ((ms) => Bun.sleep(ms));
44785
- let missing2;
45327
+ let wanted;
44786
45328
  try {
44787
- missing2 = await missingIssues(options, repository, project);
45329
+ wanted = await missingIssues(options, repository, project);
44788
45330
  } catch (error) {
44789
- return partOf2("items", ITEMS_PART, "refused", `the issues to add could not be read: ${refusalOf2(error)}`);
45331
+ return { part: partOf2("items", ITEMS_PART, "refused", `the issues to add could not be read: ${refusalOf2(error)}`), holds: null };
44790
45332
  }
45333
+ const { held, missing: missing2 } = wanted;
44791
45334
  if (missing2.length === 0)
44792
- return partOf2("items", ITEMS_PART, "present", "the project holds every issue it shows");
45335
+ return { part: partOf2("items", ITEMS_PART, "present", "the project holds every issue it shows"), holds: held.size };
44793
45336
  const port = createGhProjectPort(options.gh);
45337
+ const phase = openPhase(options.progress, "adds", missing2.length);
44794
45338
  let added = 0;
44795
45339
  try {
44796
45340
  for (const number of missing2) {
44797
45341
  if (added > 0)
44798
- await sleep(PROJECT_WRITE_PAUSE_MS);
45342
+ await sleep(options.config.boardProjectWritePauseMs);
44799
45343
  await port.addItem(project.id, { repository, number });
44800
45344
  added += 1;
45345
+ phase.advance(added);
44801
45346
  }
44802
45347
  } catch (error) {
45348
+ phase.end({ done: added, refused: missing2.length - added });
44803
45349
  const went = `${issueCount(added)} of ${String(missing2.length)} added`;
44804
- return partOf2("items", ITEMS_PART, "refused", `${went}, then #${String(missing2[added])} was refused: ${refusalOf2(error)}; run \`${PROJECT_STEP_FIX}\` to add the rest`);
45350
+ const why = `${went}, then #${String(missing2[added])} was refused: ${refusalOf2(error)}; run \`${PROJECT_STEP_FIX}\` to add the rest`;
45351
+ return { part: partOf2("items", ITEMS_PART, "refused", why), holds: held.size + added };
45352
+ }
45353
+ phase.end({ done: added, refused: 0 });
45354
+ return { part: partOf2("items", ITEMS_PART, "created", `added ${issueCount(added)} to the project`), holds: held.size + added };
45355
+ }
45356
+ function withListed(part, listed4) {
45357
+ return listed4.length === 0 ? part : { ...part, listed: listed4 };
45358
+ }
45359
+ function refusedClause(refused3) {
45360
+ return refused3 === 0 ? "" : `; ${issueCount(refused3)} not refreshed`;
45361
+ }
45362
+ function refreshedPart(refreshed, holds4) {
45363
+ const { writes, changes, skipped: skipped2, refused: refused3 } = refreshed;
45364
+ const listed4 = refused3.map(notRefreshedWarning);
45365
+ if (writes.rateLimited)
45366
+ return withListed(partOf2("fields", FIELDS_PART, "refused", rateLimitWarning(writes.notUpdated)), listed4);
45367
+ if (changes.length === 0 && refused3.length > 0 && (holds4 === null || holds4 <= refused3.length)) {
45368
+ return withListed(partOf2("fields", FIELDS_PART, "refused", `no item was filled: ${issueCount(refused3.length)} not refreshed`), listed4);
44805
45369
  }
44806
- return partOf2("items", ITEMS_PART, "created", `added ${issueCount(added)} to the project`);
45370
+ if (changes.length === 0) {
45371
+ const held = skipped2.length === 0 ? "every value is as the rules give it" : "every value written is as the rules give it";
45372
+ return withListed(partOf2("fields", FIELDS_PART, "present", `${held}${refusedClause(refused3.length)}`), listed4);
45373
+ }
45374
+ const issues = new Set(changes.map(({ issue: issue4 }) => issue4)).size;
45375
+ const values = writes.written === 1 ? "1 value" : `${String(writes.written)} values`;
45376
+ return withListed(partOf2("fields", FIELDS_PART, "created", `wrote ${values} to ${issueCount(issues)}${refusedClause(refused3.length)}`), listed4);
44807
45377
  }
44808
- async function fieldsStep(options, project) {
45378
+ async function fieldsStep(options, project, holds4) {
44809
45379
  const config = { ...options.config, boardProjectNumber: project.number };
44810
- const refreshOptions = options.sleep === undefined ? { config, gh: options.gh } : { config, gh: options.gh, sleep: options.sleep };
45380
+ const { sleep, progress } = options;
45381
+ const refreshOptions = {
45382
+ config,
45383
+ gh: options.gh,
45384
+ ...sleep === undefined ? {} : { sleep },
45385
+ ...progress === undefined ? {} : { progress }
45386
+ };
44811
45387
  let refreshed;
44812
45388
  try {
44813
45389
  refreshed = await refreshProjectItems(refreshOptions, [], { everyItem: true });
@@ -44819,24 +45395,13 @@ async function fieldsStep(options, project) {
44819
45395
  if (refreshed.kind !== "refreshed") {
44820
45396
  return { part: partOf2("fields", FIELDS_PART, "refused", refreshed.warnings.join(" ") || "the refresh sent no call"), problems: [] };
44821
45397
  }
44822
- const { writes, changes, skipped: skipped2 } = refreshed;
44823
- const problems = refreshed.warnings.slice(writes.rateLimited ? 1 : 0);
44824
- if (writes.rateLimited) {
44825
- return { part: partOf2("fields", FIELDS_PART, "refused", rateLimitWarning(writes.notUpdated)), problems };
44826
- }
44827
- if (changes.length === 0) {
44828
- const held = skipped2.length === 0 ? "every value is as the rules give it" : "every value written is as the rules give it";
44829
- return { part: partOf2("fields", FIELDS_PART, "present", held), problems };
44830
- }
44831
- const issues = new Set(changes.map(({ issue: issue4 }) => issue4)).size;
44832
- const values = writes.written === 1 ? "1 value" : `${String(writes.written)} values`;
44833
- return { part: partOf2("fields", FIELDS_PART, "created", `wrote ${values} to ${issueCount(issues)}`), problems };
45398
+ return { part: refreshedPart(refreshed, holds4), problems: refreshed.skipped.map(skippedFieldWarning) };
44834
45399
  }
44835
45400
  async function onProject(options, repository, project) {
44836
45401
  const items2 = await itemsStep(options, repository, project);
44837
- const fields = await fieldsStep(options, project);
45402
+ const fields = await fieldsStep(options, project, items2.holds);
44838
45403
  const setting = writeProjectNumber(options.root, project.number);
44839
- return { parts: [items2, fields.part, setting], problems: fields.problems };
45404
+ return { parts: [items2.part, fields.part, setting], problems: fields.problems };
44840
45405
  }
44841
45406
  async function setUpProject(options) {
44842
45407
  const { gh, config } = options;
@@ -44870,8 +45435,11 @@ function projectPartLine(part) {
44870
45435
  const row2 = ` ${part.outcome.padEnd(OUTCOME_WIDTH3, " ")} ${part.name}`;
44871
45436
  return part.outcome === "present" ? row2 : `${row2}: ${part.detail}`;
44872
45437
  }
45438
+ function projectPartLines(part) {
45439
+ return [projectPartLine(part), ...(part.listed ?? []).map((line) => `${LISTED_INDENT}${line}`)];
45440
+ }
44873
45441
  function renderProjectSetup(report) {
44874
- return [PROJECT_HEADING, ...report.parts.map((part) => projectPartLine(part)), ...report.problems];
45442
+ return [PROJECT_HEADING, ...report.parts.flatMap((part) => projectPartLines(part)), ...report.problems];
44875
45443
  }
44876
45444
  function noProject(status, asked, warnings = []) {
44877
45445
  return { status, asked, report: null, warnings: Object.freeze([...warnings]) };
@@ -44889,6 +45457,13 @@ function withWrittenRoadmap(root, config) {
44889
45457
  const written4 = readRoadmapSetting(root).issue;
44890
45458
  return written4 === null ? config : { ...config, roadmapIssue: written4 };
44891
45459
  }
45460
+ function retrySeamsOf(options) {
45461
+ const { sleep, onRetry } = options;
45462
+ return {
45463
+ ...sleep === undefined ? {} : { sleep },
45464
+ ...onRetry === undefined ? {} : { onRetry }
45465
+ };
45466
+ }
44892
45467
  async function runProjectStep(options) {
44893
45468
  const { wanted, board, root, isTerminal, openPrompter } = options;
44894
45469
  if (board.status !== "ran") {
@@ -44898,7 +45473,16 @@ async function runProjectStep(options) {
44898
45473
  return noProject("declined", false);
44899
45474
  const run = async (asked) => {
44900
45475
  const setUp = options.setUp ?? setUpProject;
44901
- const report = await setUp({ root, config: withWrittenRoadmap(root, options.config), gh: options.openGh() });
45476
+ const config = withWrittenRoadmap(root, options.config);
45477
+ const gh = openProjectRunner(options.openGh(), config, retrySeamsOf(options));
45478
+ const { sleep, progress } = options;
45479
+ const report = await setUp({
45480
+ root,
45481
+ config,
45482
+ gh,
45483
+ ...sleep === undefined ? {} : { sleep },
45484
+ ...progress === undefined ? {} : { progress }
45485
+ });
44902
45486
  return { status: "ran", asked, report, warnings: [] };
44903
45487
  };
44904
45488
  if (wanted === true)
@@ -44919,11 +45503,12 @@ function renderProjectStep(result2) {
44919
45503
  return [`The GitHub project question needs a terminal; run ${PROJECT_STEP_FIX} to create it.`];
44920
45504
  return [];
44921
45505
  }
44922
- var PROJECT_STEP_FIX = "rafa init --board --project", SCOPE_PART = "project scope", COPY_PART = "project", ITEMS_PART = "project issues", FIELDS_PART = "project fields", PROJECT_SCOPE2 = "project", SCOPE_ARGS, LOGGED_IN = "success", PROJECT_HEADING = "GitHub project:", OUTCOME_WIDTH3 = 7, PROJECT_QUESTION = "Also create a GitHub project with roadmap and kanban views? [y/N] ", PROJECT_NO_BOARD_WARNING;
45506
+ var PROJECT_STEP_FIX = "rafa init --board --project", SCOPE_PART = "project scope", COPY_PART = "project", ITEMS_PART = "project issues", FIELDS_PART = "project fields", PROJECT_SCOPE2 = "project", SCOPE_ARGS, LOGGED_IN = "success", PROJECT_HEADING = "GitHub project:", OUTCOME_WIDTH3 = 7, LISTED_INDENT, PROJECT_QUESTION = "Also create a GitHub project with roadmap and kanban views? [y/N] ", PROJECT_NO_BOARD_WARNING;
44923
45507
  var init_init_board_project = __esm(() => {
44924
45508
  init_facts();
44925
45509
  init_gh2();
44926
- init_port();
45510
+ init_progress2();
45511
+ init_project_runner();
44927
45512
  init_refresh_warnings();
44928
45513
  init_refresh();
44929
45514
  init_rules();
@@ -44933,6 +45518,7 @@ var init_init_board_project = __esm(() => {
44933
45518
  init_init_board_project_setting();
44934
45519
  init_init_board();
44935
45520
  SCOPE_ARGS = Object.freeze(["auth", "status", "--active", "--json", "hosts"]);
45521
+ LISTED_INDENT = " ".repeat(2 + OUTCOME_WIDTH3 + 2);
44936
45522
  PROJECT_NO_BOARD_WARNING = "--project creates the GitHub project with the GitHub board," + ` and the board step did not run, so no project was created; run ${PROJECT_STEP_FIX}.`;
44937
45523
  });
44938
45524
 
@@ -44987,9 +45573,10 @@ function fieldsRow(project, template) {
44987
45573
  return rowOf8("fields", FIELDS_ROW, "missing", `${sentences} ${fix}`);
44988
45574
  }
44989
45575
  async function readDoctorProject(input) {
44990
- const { gh, number, template } = input;
44991
- if (gh === null || number === null)
45576
+ const { number, template } = input;
45577
+ if (input.gh === null || number === null)
44992
45578
  return null;
45579
+ const gh = openProjectRunner(input.gh, input.config, input.retry);
44993
45580
  const scope = await readScopeRow(gh);
44994
45581
  if (scope.outcome !== "present") {
44995
45582
  const project = notRead("project", `${PROJECT_ROW} ${String(number)}`, scope);
@@ -45016,6 +45603,7 @@ var SCOPE_ROW = "project scope", PROJECT_ROW = "project", FIELDS_ROW = "project
45016
45603
  var init_doctor_project = __esm(() => {
45017
45604
  init_gh2();
45018
45605
  init_port();
45606
+ init_project_runner();
45019
45607
  init_refresh_warnings();
45020
45608
  init_config_sections();
45021
45609
  init_move_native();
@@ -46214,7 +46802,7 @@ function failure6(why) {
46214
46802
  function short2(sha) {
46215
46803
  return sha.slice(0, SHORT_SHA);
46216
46804
  }
46217
- function reasonOf2(data, field5, summary) {
46805
+ function reasonOf3(data, field5, summary) {
46218
46806
  const value = data[field5];
46219
46807
  return typeof value === "string" && value.trim() !== "" ? value.trim() : summary;
46220
46808
  }
@@ -46236,8 +46824,8 @@ function readLoopEnding(root, sessionId) {
46236
46824
  return typeof number === "number" && Number.isSafeInteger(number) && number > 0 ? { kind: "pr", number } : { kind: "none", reason: `its pr event names no pull request number: ${describeValue(number)}` };
46237
46825
  }
46238
46826
  if (event.name === "error")
46239
- return { kind: "error", reason: reasonOf2(event.data, "message", event.summary) };
46240
- return { kind: event.name === "halt" ? "halt" : "no-pr", reason: reasonOf2(event.data, "reason", event.summary) };
46827
+ return { kind: "error", reason: reasonOf3(event.data, "message", event.summary) };
46828
+ return { kind: event.name === "halt" ? "halt" : "no-pr", reason: reasonOf3(event.data, "reason", event.summary) };
46241
46829
  }
46242
46830
  function noPullRequestLine(issue4, ending) {
46243
46831
  const how = ending.kind === "none" ? "ended without a pull request" : `ended on ${ending.kind}`;
@@ -47190,7 +47778,7 @@ async function runDoctor(context, seams) {
47190
47778
  const modeSet = preflight.resolved.sources.boardRelationships !== "default";
47191
47779
  const board = await readDoctorBoard(gh, project.root, preflight.config.roadmapIssue, preflight.config.boardRelationships, modeSet);
47192
47780
  const { boardProjectNumber: number, boardProjectTemplate: template } = preflight.config;
47193
- const projectRows = await readDoctorProject({ gh, number, template });
47781
+ const projectRows = await readDoctorProject({ gh, number, template, config: preflight.config, retry: commandRetrySeams(context.output, context.outputMode) });
47194
47782
  const readings = { ...board, project: projectRows, cleanup, refs, release, tiers, effortSchema, effortSync };
47195
47783
  writeText(context, renderDoctor(preflight));
47196
47784
  await announceRisk(context, preflight, seams);
@@ -47257,6 +47845,7 @@ function createDoctorCommand(seams = DEFAULT_DOCTOR_SEAMS) {
47257
47845
  }
47258
47846
  var DEFAULT_DOCTOR_SEAMS, NOTHING_CHECKED = "Nothing was checked.", NO_ITEMS3, NO_START_TIER, doctor_default;
47259
47847
  var init_doctor = __esm(() => {
47848
+ init_project_runner();
47260
47849
  init_command();
47261
47850
  init_version();
47262
47851
  init_config_load();
@@ -47984,13 +48573,13 @@ function estimateFrom(secondsPerTask, left) {
47984
48573
  }
47985
48574
  function byTaskEstimate(row3, counts) {
47986
48575
  const minutes2 = row3?.perTask.minutes.avg ?? null;
47987
- const estimate = estimateFrom(minutes2 === null ? null : minutes2 * SECONDS_PER_MINUTE, tasksLeft(counts));
48576
+ const estimate = estimateFrom(minutes2 === null ? null : minutes2 * SECONDS_PER_MINUTE2, tasksLeft(counts));
47988
48577
  return estimate === null || row3 === undefined ? null : { ...estimate, sessions: row3.tasks };
47989
48578
  }
47990
48579
  function byProgressEstimate(planStartedAt, counts, now) {
47991
48580
  if (counts.done === 0)
47992
48581
  return null;
47993
- const elapsedSeconds = Math.max(0, (now.getTime() - Date.parse(planStartedAt)) / MS_PER_SECOND3);
48582
+ const elapsedSeconds = Math.max(0, (now.getTime() - Date.parse(planStartedAt)) / MS_PER_SECOND5);
47994
48583
  const estimate = estimateFrom(elapsedSeconds / counts.done, tasksLeft(counts));
47995
48584
  return estimate === null ? null : { ...estimate, planStartedAt, elapsedSeconds: Math.round(elapsedSeconds) };
47996
48585
  }
@@ -48046,7 +48635,7 @@ function readStatusWidget(root, inputs, isAlive) {
48046
48635
  const seams = isAlive === undefined ? {} : { isAlive };
48047
48636
  return statusWidget(root, readSessions(root, seams), inputs);
48048
48637
  }
48049
- var MS_PER_SECOND3 = 1000, SECONDS_PER_MINUTE = 60;
48638
+ var MS_PER_SECOND5 = 1000, SECONDS_PER_MINUTE2 = 60;
48050
48639
  var init_dashboard_status = __esm(() => {
48051
48640
  init_loop_sessions();
48052
48641
  init_plan_files();
@@ -50497,7 +51086,8 @@ async function refreshProjectAfterEpic(context, seams, target) {
50497
51086
  });
50498
51087
  if (config.boardProjectNumber === null)
50499
51088
  return null;
50500
- const openGh = () => seams.gh ?? createGhRunner({ cwd: project.root });
51089
+ const retry = commandRetrySeams(context.output, context.outputMode, seams.sleep);
51090
+ const openGh = () => openProjectRunner(seams.gh ?? createGhRunner({ cwd: project.root }), config, retry);
50501
51091
  warnings = await refreshLines(config, openGh, seams.projectRefresh ?? refreshIssueItems, target, named4);
50502
51092
  } catch (error) {
50503
51093
  warnings = [epicProjectProblemLine(named4, messageOf(error))];
@@ -50510,6 +51100,7 @@ var init_epic_project = __esm(() => {
50510
51100
  init_github();
50511
51101
  init_add_issue();
50512
51102
  init_issue_board_refresh();
51103
+ init_project_runner();
50513
51104
  init_refresh_warnings();
50514
51105
  init_config_sections();
50515
51106
  init_issue_tracker();
@@ -51741,7 +52332,7 @@ function summariseEpicCost(rows2, members) {
51741
52332
  }
51742
52333
  return {
51743
52334
  runs: seen.size,
51744
- wallSeconds: Math.round(minutes2 * SECONDS_PER_MINUTE2),
52335
+ wallSeconds: Math.round(minutes2 * SECONDS_PER_MINUTE3),
51745
52336
  runsWithoutTime,
51746
52337
  tokens: tokens2
51747
52338
  };
@@ -51761,7 +52352,7 @@ function renderEpicCost(cost, estimate) {
51761
52352
  MEMBERSHIP_NOTE
51762
52353
  ];
51763
52354
  }
51764
- var MEMBERSHIP_NOTE, NO_ESTIMATE = "none written", SECONDS_PER_MINUTE2 = 60;
52355
+ var MEMBERSHIP_NOTE, NO_ESTIMATE = "none written", SECONDS_PER_MINUTE3 = 60;
51765
52356
  var init_epic_cost = __esm(() => {
51766
52357
  init_naming();
51767
52358
  init_loop_sessions();
@@ -52954,7 +53545,7 @@ function resultOf5(run, action, fields = {}) {
52954
53545
  ...fields
52955
53546
  };
52956
53547
  }
52957
- async function settle2(run, action, ref14, foundBy, supersedes) {
53548
+ async function settle3(run, action, ref14, foundBy, supersedes) {
52958
53549
  const { store } = run.route;
52959
53550
  const { key } = run.filing;
52960
53551
  if (store === null || key === null)
@@ -52966,13 +53557,13 @@ async function fileNew(run, extras = {}) {
52966
53557
  const { filing } = run;
52967
53558
  const draft = extras.supersedes === undefined && extras.duplicates === undefined ? filing.draft : { ...filing.draft, body: issueText(ISSUE_OPENING, { ...filing.values, ...extras }, run.redact) };
52968
53559
  const created = await step(run, `the ${run.route.channel} tracker create`, () => run.route.tracker.create(draft));
52969
- return created.ok ? settle2(run, "filed", created.value, null, extras.supersedes) : resultOf5(run, "failed", { problem: created.problem });
53560
+ return created.ok ? settle3(run, "filed", created.value, null, extras.supersedes) : resultOf5(run, "failed", { problem: created.problem });
52970
53561
  }
52971
53562
  async function commentOn2(run, ref14, foundBy, action = "commented", comment = run.filing.comment) {
52972
53563
  const commented = await step(run, `the ${run.route.channel} tracker comment`, () => run.route.tracker.comment(ref14, comment));
52973
53564
  if (!commented.ok)
52974
53565
  return resultOf5(run, "failed", { ref: ref14, foundBy, problem: commented.problem });
52975
- return foundBy === "store" ? resultOf5(run, action, { ref: ref14, foundBy }) : settle2(run, action, ref14, foundBy);
53566
+ return foundBy === "store" ? resultOf5(run, action, { ref: ref14, foundBy }) : settle3(run, action, ref14, foundBy);
52976
53567
  }
52977
53568
  async function lookUp2(run, key, searchText) {
52978
53569
  const { route, filing } = run;
@@ -54902,6 +55493,11 @@ var init_scaffold = __esm(() => {
54902
55493
  "# project:",
54903
55494
  `# template: ${CONFIG_DEFAULTS.boardProjectTemplate} # the project URL rafa init --board --project copies`,
54904
55495
  "# number: # the project rafa mirrors the board to; unset until rafa init --board --project",
55496
+ `# retries: ${String(CONFIG_DEFAULTS.boardProjectRetries)} # 1..10 | false, tries again after a failed project call`,
55497
+ `# retryWaitSeconds: ${String(CONFIG_DEFAULTS.boardProjectRetryWaitSeconds)} # 1..60, the first wait before a retry; each later one doubles`,
55498
+ `# progressSeconds: ${String(CONFIG_DEFAULTS.boardProjectProgressSeconds)} # 1..300 | false, the most seconds between two progress lines`,
55499
+ `# writeBatchSize: ${String(CONFIG_DEFAULTS.boardProjectWriteBatchSize)} # 1..100, field writes per request`,
55500
+ `# writePauseMs: ${String(CONFIG_DEFAULTS.boardProjectWritePauseMs)} # 0..60000, the pause between two write requests`,
54905
55501
  "# roadmap:",
54906
55502
  "# issue: # the issue plan create --next reads; unset is the one titled Roadmap",
54907
55503
  "# claims:",
@@ -55217,7 +55813,9 @@ async function runInit(context, seams) {
55217
55813
  config: scopes.config,
55218
55814
  openGh: () => seams.gh(scopes.written.root),
55219
55815
  isTerminal: seams.isTerminal,
55220
- openPrompter: seams.openPrompter
55816
+ openPrompter: seams.openPrompter,
55817
+ ...commandRetrySeams(context.output, context.outputMode, seams.sleep),
55818
+ progress: commandProgressFeed(context.output, context.outputMode, scopes.config.boardProjectProgressSeconds, seams.now === undefined ? {} : { now: seams.now })
55221
55819
  });
55222
55820
  const result2 = {
55223
55821
  ...scopes.written,
@@ -55254,7 +55852,7 @@ function createInitCommand(seams = DEFAULT_INIT_SEAMS) {
55254
55852
  subject: "init",
55255
55853
  action: "init",
55256
55854
  summary: "set up a rafa project: its root, .rafa/config.yaml, the tree and the .gitignore entry",
55257
- description: "Sets up a rafa project. On a terminal it lists the root candidates, the git toplevel or" + " the working directory outside a repository and then the outermost monorepo root above it, each" + " refused one with its reason, and reads a candidate number or a path typed freely. `--root=<path>`" + " names the root instead, absolute or relative to the working directory, and `--yes` takes the" + " first candidate; without a terminal one of the two is needed. Under the root it writes" + " `.rafa/config.yaml` with every setting commented out at its default, the tree `specs/`, `plans/`," + " `runs/`, `effort/` and `instincts/` under `.rafa/`, and the rafa block in `.gitignore` from the" + " `tracking` settings; under the home it writes `~/.rafa/config.yaml` and `~/.rafa/instincts/` when" + " they are missing. Only what is missing or stale is written, so a rerun changes no byte and says" + " so. It warns when `~/.rafa/bin` is not on PATH ahead of `~/.bun/bin`, and when a plan under" + " `plan.dir` routes to an agent that resolves only in `~/.claude/agents` and rafa does not ship," + " naming `rafa agent vendor <name>`; it writes nothing under `.claude/` itself. A repository whose pull request provider is `gh` ends with one" + " question about setting up the GitHub board, which `--board` and `--no-board` answer for a" + " script, and then, once the board has run, one question about installing the epic guard" + " workflow `.github/workflows/epic-guard.yml`, which removes a second `epic:` label from an issue" + " and comments why; `--epic-guard` and `--no-epic-guard` answer that one. When `board.relationships`" + " is set, the board is then read for relationships held in the other mode: every write that moves" + " them into the configured one is printed and a terminal is asked once, nothing is written on a no," + " and once every write went through a second question asks whether to remove the old marks. Last, once the" + " board has run, one question asks whether to copy the GitHub project template to the repository's" + " owner, link it, add the issues and fill their fields; `--project` and `--no-project` answer that one." + " On a terminal it also asks once whether every pull request bumps the version and gains" + " a changelog entry, and writes the answer as `release.enabled`; `--release` and `--no-release`" + " answer that one, and `--yes` leaves it unset. With `--output=json` the" + " root and every path checked are the data of the terminal result event.",
55855
+ description: "Sets up a rafa project. On a terminal it lists the root candidates, the git toplevel or" + " the working directory outside a repository and then the outermost monorepo root above it, each" + " refused one with its reason, and reads a candidate number or a path typed freely. `--root=<path>`" + " names the root instead, absolute or relative to the working directory, and `--yes` takes the" + " first candidate; without a terminal one of the two is needed. Under the root it writes" + " `.rafa/config.yaml` with every setting commented out at its default, the tree `specs/`, `plans/`," + " `runs/`, `effort/` and `instincts/` under `.rafa/`, and the rafa block in `.gitignore` from the" + " `tracking` settings; under the home it writes `~/.rafa/config.yaml` and `~/.rafa/instincts/` when" + " they are missing. Only what is missing or stale is written, so a rerun changes no byte and says" + " so. It warns when `~/.rafa/bin` is not on PATH ahead of `~/.bun/bin`, and when a plan under" + " `plan.dir` routes to an agent that resolves only in `~/.claude/agents` and rafa does not ship," + " naming `rafa agent vendor <name>`; it writes nothing under `.claude/` itself. A repository whose pull request provider is `gh` ends with one" + " question about setting up the GitHub board, which `--board` and `--no-board` answer for a" + " script, and then, once the board has run, one question about installing the epic guard" + " workflow `.github/workflows/epic-guard.yml`, which removes a second `epic:` label from an issue" + " and comments why; `--epic-guard` and `--no-epic-guard` answer that one. When `board.relationships`" + " is set, the board is then read for relationships held in the other mode: every write that moves" + " them into the configured one is printed and a terminal is asked once, nothing is written on a no," + " and once every write went through a second question asks whether to remove the old marks. Last, once the" + " board has run, one question asks whether to copy the GitHub project template to the repository's" + " owner, link it, add the issues and fill their fields; `--project` and `--no-project` answer that one." + " Adding the issues, reading their facts and writing their fields each print a start line with the" + " total, a progress line at most every board.project.progressSeconds and an end line, one `progress`" + " event each with `--output=json`." + " On a terminal it also asks once whether every pull request bumps the version and gains" + " a changelog entry, and writes the answer as `release.enabled`; `--release` and `--no-release`" + " answer that one, and `--yes` leaves it unset. With `--output=json` the" + " root and every path checked are the data of the terminal result event.",
55258
55856
  args: [],
55259
55857
  flags: [
55260
55858
  {
@@ -55279,7 +55877,7 @@ function createInitCommand(seams = DEFAULT_INIT_SEAMS) {
55279
55877
  },
55280
55878
  {
55281
55879
  name: "project",
55282
- description: "Create the GitHub project without asking, once the board has run: copy" + " `board.project.template` to the repository's owner, link the repository, add its issues, fill" + " their fields and save `board.project.number`. `--no-project` leaves it out. Without either, a" + " terminal is asked once and a run with no terminal leaves it out.",
55880
+ description: "Create the GitHub project without asking, once the board has run: copy" + " `board.project.template` to the repository's owner, link the repository, add its issues, fill" + " their fields and save `board.project.number`. `--no-project` leaves it out. Without either, a" + " terminal is asked once and a run with no terminal leaves it out. The adds, the facts reads and the" + " field writes print their progress, as `adding issues: 146/283, 6m 40s`.",
55283
55881
  type: "boolean"
55284
55882
  },
55285
55883
  {
@@ -55328,6 +55926,8 @@ var DEFAULT_INIT_SEAMS, NOTHING_WRITTEN3 = "Nothing was written.", SOURCE_PHRASE
55328
55926
  var init_init = __esm(() => {
55329
55927
  init_github();
55330
55928
  init_vendorable();
55929
+ init_progress2();
55930
+ init_project_runner();
55331
55931
  init_command();
55332
55932
  init_confirm();
55333
55933
  init_config_load();
@@ -56454,7 +57054,7 @@ async function addCreatedToProject(context, seams, ref15) {
56454
57054
  });
56455
57055
  const lines2 = await addAndRefreshIssue({
56456
57056
  config,
56457
- openGh: () => seams.gh ?? createGhRunner({ cwd: project.root })
57057
+ openGh: () => openProjectRunner(seams.gh ?? createGhRunner({ cwd: project.root }), config, commandRetrySeams(context.output, context.outputMode, seams.sleep))
56458
57058
  }, issue4);
56459
57059
  for (const line of lines2)
56460
57060
  context.output.warn(line);
@@ -56536,6 +57136,7 @@ var init_create = __esm(() => {
56536
57136
  init_github();
56537
57137
  init_issue_values();
56538
57138
  init_add_issue();
57139
+ init_project_runner();
56539
57140
  init_config_sections();
56540
57141
  init_triage2();
56541
57142
  init_plan_files();
@@ -63448,10 +64049,10 @@ async function runNextChain(options) {
63448
64049
  const homeStop = home === null ? null : await turn(home, null);
63449
64050
  return homeStop === null ? stopOn("loop-started", state) : stopOn(homeStop, home ?? state);
63450
64051
  }
63451
- const settle3 = followsMerge(state.action) ? options.afterMerge?.() ?? null : null;
63452
- const settleStop = settle3 === null ? null : await turn(settle3, null);
64052
+ const settle4 = followsMerge(state.action) ? options.afterMerge?.() ?? null : null;
64053
+ const settleStop = settle4 === null ? null : await turn(settle4, null);
63453
64054
  if (settleStop !== null)
63454
- return stopOn(settleStop, settle3 ?? state);
64055
+ return stopOn(settleStop, settle4 ?? state);
63455
64056
  if (steps.length >= MAX_ACTIONS)
63456
64057
  return stopOn("capped", state);
63457
64058
  previous = state;
@@ -64882,6 +65483,10 @@ function openPrContext(context, seams = DEFAULT_PR_SEAMS) {
64882
65483
  versionGuard: mergeGuardSettings(config),
64883
65484
  projectRefresh: {
64884
65485
  boardProjectNumber: config.boardProjectNumber,
65486
+ boardProjectRetries: config.boardProjectRetries,
65487
+ boardProjectRetryWaitSeconds: config.boardProjectRetryWaitSeconds,
65488
+ boardProjectWriteBatchSize: config.boardProjectWriteBatchSize,
65489
+ boardProjectWritePauseMs: config.boardProjectWritePauseMs,
64885
65490
  boardRelationships: config.boardRelationships,
64886
65491
  roadmapIssue: config.roadmapIssue,
64887
65492
  releaseFragments: config.releaseFragments
@@ -65748,7 +66353,7 @@ async function refreshProjectAfterMerge(options) {
65748
66353
  const closed = [...new Set(closedIssuesIn(options.body))];
65749
66354
  if (config.boardProjectNumber === null || closed.length === 0)
65750
66355
  return null;
65751
- const gh = options.openGh();
66356
+ const gh = openProjectRunner(options.openGh(), config, options.retry);
65752
66357
  const wanted = new Set(closed);
65753
66358
  const warnings = [];
65754
66359
  let listed5 = [];
@@ -65770,6 +66375,7 @@ async function refreshProjectAfterMerge(options) {
65770
66375
  }
65771
66376
  var init_merge_project = __esm(() => {
65772
66377
  init_issue_board_refresh();
66378
+ init_project_runner();
65773
66379
  init_refresh_warnings();
65774
66380
  init_select3();
65775
66381
  init_roadmap_board();
@@ -66537,7 +67143,14 @@ async function runMerge2(context, seams) {
66537
67143
  const roadmapTicks = await reportTick(context, pr, seams, detail);
66538
67144
  const steps = cleanUpAfterMerge(git, detail, { info: info2, warn: warn4 });
66539
67145
  const board = await reportAfterCleanUp(context, pr, seams, detail);
66540
- await refreshProjectAfterMerge({ body: detail.body, config: pr.projectRefresh, openGh: () => openGh(pr, seams), unblocked: board.unblocked, warn: warn4 });
67146
+ await refreshProjectAfterMerge({
67147
+ body: detail.body,
67148
+ config: pr.projectRefresh,
67149
+ openGh: () => openGh(pr, seams),
67150
+ unblocked: board.unblocked,
67151
+ warn: warn4,
67152
+ retry: commandRetrySeams(context.output, context.outputMode)
67153
+ });
66541
67154
  const followUps = reportFollowUps({
66542
67155
  root: pr.project.root,
66543
67156
  home: pr.project.home,
@@ -66622,6 +67235,7 @@ function createPrMergeCommand(seams = DEFAULT_MERGE_SEAMS) {
66622
67235
  var USAGE43, YES_ANSWERS6, DEFAULT_MERGE_SEAMS, merge_default2;
66623
67236
  var init_merge4 = __esm(() => {
66624
67237
  init_github();
67238
+ init_project_runner();
66625
67239
  init_roadmap_tick();
66626
67240
  init_roadmap();
66627
67241
  init_command();
@@ -69467,7 +70081,7 @@ function pollWord(polls) {
69467
70081
  return polls === 1 ? "1 poll" : `${polls} polls`;
69468
70082
  }
69469
70083
  function secondsWord(elapsedMs) {
69470
- return `${Math.round(elapsedMs / MS_PER_SECOND4)}s`;
70084
+ return `${Math.round(elapsedMs / MS_PER_SECOND6)}s`;
69471
70085
  }
69472
70086
  function verdictWord(verdict) {
69473
70087
  return verdict === "timeout" || verdict === "pending" ? "still running" : verdict;
@@ -69579,7 +70193,7 @@ function createPrWaitCommand(seams = DEFAULT_WAIT_SEAMS) {
69579
70193
  };
69580
70194
  return Object.freeze(command);
69581
70195
  }
69582
- var USAGE49, TIMEOUT_FLAG2 = "timeout", DEFAULT_WAIT_TIMEOUT_MIN, MS_PER_MIN = 60000, MS_PER_SECOND4 = 1000, WAIT_EXIT_CODES, DEFAULT_WAIT_SEAMS, wait_default2;
70196
+ var USAGE49, TIMEOUT_FLAG2 = "timeout", DEFAULT_WAIT_TIMEOUT_MIN, MS_PER_MIN = 60000, MS_PER_SECOND6 = 1000, WAIT_EXIT_CODES, DEFAULT_WAIT_SEAMS, wait_default2;
69583
70197
  var init_wait2 = __esm(() => {
69584
70198
  init_command();
69585
70199
  init_ending();
@@ -69782,7 +70396,7 @@ function readString7(value, where) {
69782
70396
  refuse19(`${where} is ${describeValue(value)}, expected a string`);
69783
70397
  return value;
69784
70398
  }
69785
- function readOnePage3(value, where) {
70399
+ function readOnePage2(value, where) {
69786
70400
  const connection = readMapping6(value, where);
69787
70401
  if (readMapping6(connection["pageInfo"], `${where}.pageInfo`)["hasNextPage"] !== false) {
69788
70402
  refuse19(`${where} holds more entries than one page`);
@@ -69794,7 +70408,7 @@ function readPull2(value, where, repository) {
69794
70408
  if (pull2["state"] !== "MERGED")
69795
70409
  return null;
69796
70410
  const merge2 = pull2["mergeCommit"];
69797
- const linked = readOnePage3(pull2["closingIssuesReferences"], `${where}.closingIssuesReferences`).flatMap((node, index) => {
70411
+ const linked = readOnePage2(pull2["closingIssuesReferences"], `${where}.closingIssuesReferences`).flatMap((node, index) => {
69798
70412
  const at = `${where}.closingIssuesReferences[${String(index)}]`;
69799
70413
  const issue4 = readMapping6(node, at);
69800
70414
  const owner = readString7(readMapping6(issue4["repository"], `${at}.repository`)["nameWithOwner"], `${at}.repository.nameWithOwner`);
@@ -69829,7 +70443,7 @@ async function readBatch(gh, commits, branch) {
69829
70443
  const object = answer5[commitAlias(index)];
69830
70444
  if (object === null || object === undefined)
69831
70445
  return [];
69832
- const pulls = readOnePage3(readMapping6(object, where)["associatedPullRequests"], `${where}.associatedPullRequests`).map((pull2, at) => readPull2(pull2, `${where}.associatedPullRequests[${String(at)}]`, repository)).filter((pull2) => pull2 !== null);
70446
+ const pulls = readOnePage2(readMapping6(object, where)["associatedPullRequests"], `${where}.associatedPullRequests`).map((pull2, at) => readPull2(pull2, `${where}.associatedPullRequests[${String(at)}]`, repository)).filter((pull2) => pull2 !== null);
69833
70447
  return broughtBy(pulls, commit, branch);
69834
70448
  });
69835
70449
  }
@@ -69851,7 +70465,7 @@ async function refreshProjectAfterSettle(options) {
69851
70465
  const commits = releasedCommits(options.delivered);
69852
70466
  if (config.boardProjectNumber === null || commits === null || commits.length === 0)
69853
70467
  return null;
69854
- const gh = options.openGh();
70468
+ const gh = openProjectRunner(options.openGh(), config, options.retry);
69855
70469
  let pulls;
69856
70470
  try {
69857
70471
  pulls = await readPulls(gh, commits, branch);
@@ -69871,6 +70485,7 @@ async function refreshProjectAfterSettle(options) {
69871
70485
  var COMMIT_BATCH = 20, COMMIT_PULL_LIMIT = 10, CLOSING_LIMIT = 100, PREFIX42 = "release settle project";
69872
70486
  var init_settle_project = __esm(() => {
69873
70487
  init_issue_board_refresh();
70488
+ init_project_runner();
69874
70489
  init_refresh_warnings();
69875
70490
  init_roadmap();
69876
70491
  init_config_sections();
@@ -70028,7 +70643,8 @@ async function runSettle(context, seams = DEFAULT_RELEASE_SETTLE_SEAMS) {
70028
70643
  branch,
70029
70644
  config,
70030
70645
  openGh: () => (seams.gh ?? ((cwd) => createGhRunner({ cwd })))(project.root),
70031
- refresh: seams.refresh
70646
+ refresh: seams.refresh,
70647
+ retry: commandRetrySeams(context.output, context.outputMode)
70032
70648
  });
70033
70649
  return { ...ran.value, project: refreshed };
70034
70650
  }
@@ -70087,6 +70703,7 @@ function createReleaseSettleCommand(seams = DEFAULT_RELEASE_SETTLE_SEAMS) {
70087
70703
  var RELEASE_SETTLE_USAGE = "rafa release settle [--dry-run]", COMMAND_NAME17 = "rafa release settle", DRY_RUN_FLAG9 = "dry-run", INDENT17 = " ", SHORT_HASH4 = 12, DEFAULT_RELEASE_SETTLE_SEAMS, settle_default;
70088
70704
  var init_settle2 = __esm(() => {
70089
70705
  init_github();
70706
+ init_project_runner();
70090
70707
  init_command();
70091
70708
  init_pr();
70092
70709
  init_settle_pr();
@@ -71759,7 +72376,7 @@ function actionLines(actions) {
71759
72376
  return marker === null ? [] : [`${marker} ${action.path}: ${action.detail}`];
71760
72377
  });
71761
72378
  }
71762
- function checkOptions(context, projectRoot3) {
72379
+ function checkOptions2(context, projectRoot3) {
71763
72380
  return { projectRoot: projectRoot3, pathDirs: pathDirectories(context.env["PATH"]) };
71764
72381
  }
71765
72382
  function report5(context, result2, lines2) {
@@ -71771,7 +72388,7 @@ function report5(context, result2, lines2) {
71771
72388
  context.output.info(line);
71772
72389
  }
71773
72390
  function runPlan(context, scope, backfillDir, projectRoot3) {
71774
- const plan2 = planDerivation(scope.dir, checkOptions(context, projectRoot3));
72391
+ const plan2 = planDerivation(scope.dir, checkOptions2(context, projectRoot3));
71775
72392
  const candidates = selectProposals(scope.dir).length;
71776
72393
  const actions = plan2.actions.map(actionData);
71777
72394
  const result2 = {
@@ -71840,7 +72457,7 @@ function rowPaths(files, root) {
71840
72457
  return files.flatMap((file) => file.status !== REVIEWED_FILE ? [] : file.rows.filter((row3) => row3.status === ANSWERED_ROW).map((row3) => join109(root, ...row3.path.split("/"))));
71841
72458
  }
71842
72459
  function runApply(context, scope, backfillDir, projectRoot3) {
71843
- const options = checkOptions(context, projectRoot3);
72460
+ const options = checkOptions2(context, projectRoot3);
71844
72461
  const backupDir = backupDirectory(scope.base);
71845
72462
  const repoRoot = projectOf13(context).root;
71846
72463
  const backups = [];