@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.
@@ -14,8 +14,8 @@ import {
14
14
  openNdjsonStore,
15
15
  openSqliteStore,
16
16
  selectEffortStore
17
- } from "../../index-1r2b1052.js";
18
- import"../../index-m0wt4nr1.js";
17
+ } from "../../index-8veaj0t9.js";
18
+ import"../../index-334y39tn.js";
19
19
  import"../../index-d6d0942q.js";
20
20
  import"../../index-sr3kc4mf.js";
21
21
  import {
@@ -175,6 +175,65 @@ var fragmentsDirectory = (raw, at) => {
175
175
  return { value: undefined, problems: [problem], extras: [] };
176
176
  };
177
177
 
178
+ // src/config-schema-board-project.ts
179
+ var BOARD_PROJECT_RETRIES_MIN = 1;
180
+ var BOARD_PROJECT_RETRIES_MAX = 10;
181
+ var BOARD_PROJECT_RETRY_WAIT_MIN_SECONDS = 1;
182
+ var BOARD_PROJECT_RETRY_WAIT_MAX_SECONDS = 60;
183
+ var BOARD_PROJECT_PROGRESS_MIN_SECONDS = 1;
184
+ var BOARD_PROJECT_PROGRESS_MAX_SECONDS = 300;
185
+ var BOARD_PROJECT_WRITE_BATCH_MIN = 1;
186
+ var BOARD_PROJECT_WRITE_BATCH_MAX = 100;
187
+ var BOARD_PROJECT_WRITE_PAUSE_MIN_MS = 0;
188
+ var BOARD_PROJECT_WRITE_PAUSE_MAX_MS = 60000;
189
+ function isWholeBetween(raw, min, max) {
190
+ return typeof raw === "number" && Number.isSafeInteger(raw) && raw >= min && raw <= max;
191
+ }
192
+ function rangeWords(min, max) {
193
+ return `a whole number from ${String(min)} to ${String(max)}`;
194
+ }
195
+ function wholeBetween(min, max) {
196
+ return (raw, at) => isWholeBetween(raw, min, max) ? { value: raw, problems: [], extras: [] } : refused(at, raw, rangeWords(min, max));
197
+ }
198
+ function offOrWholeBetween(min, max) {
199
+ return (raw, at) => raw === false || isWholeBetween(raw, min, max) ? { value: raw, problems: [], extras: [] } : refused(at, raw, `false or ${rangeWords(min, max)}`);
200
+ }
201
+ var boardProjectRetries = offOrWholeBetween(BOARD_PROJECT_RETRIES_MIN, BOARD_PROJECT_RETRIES_MAX);
202
+ var boardProjectRetryWaitSeconds = wholeBetween(BOARD_PROJECT_RETRY_WAIT_MIN_SECONDS, BOARD_PROJECT_RETRY_WAIT_MAX_SECONDS);
203
+ var boardProjectProgressSeconds = offOrWholeBetween(BOARD_PROJECT_PROGRESS_MIN_SECONDS, BOARD_PROJECT_PROGRESS_MAX_SECONDS);
204
+ var boardProjectWriteBatchSize = wholeBetween(BOARD_PROJECT_WRITE_BATCH_MIN, BOARD_PROJECT_WRITE_BATCH_MAX);
205
+ var boardProjectWritePauseMs = wholeBetween(BOARD_PROJECT_WRITE_PAUSE_MIN_MS, BOARD_PROJECT_WRITE_PAUSE_MAX_MS);
206
+ var BOARD_PROJECT_LIMIT_DEFAULTS = Object.freeze({
207
+ boardProjectRetries: 3,
208
+ boardProjectRetryWaitSeconds: 2,
209
+ boardProjectProgressSeconds: 10,
210
+ boardProjectWriteBatchSize: 20,
211
+ boardProjectWritePauseMs: 1000
212
+ });
213
+ var BOARD_PROJECT_LIMIT_SETTINGS = {
214
+ boardProjectRetries: { key: "board.project.retries", read: boardProjectRetries, cli: false },
215
+ boardProjectRetryWaitSeconds: {
216
+ key: "board.project.retryWaitSeconds",
217
+ read: boardProjectRetryWaitSeconds,
218
+ cli: false
219
+ },
220
+ boardProjectProgressSeconds: {
221
+ key: "board.project.progressSeconds",
222
+ read: boardProjectProgressSeconds,
223
+ cli: false
224
+ },
225
+ boardProjectWriteBatchSize: {
226
+ key: "board.project.writeBatchSize",
227
+ read: boardProjectWriteBatchSize,
228
+ cli: false
229
+ },
230
+ boardProjectWritePauseMs: {
231
+ key: "board.project.writePauseMs",
232
+ read: boardProjectWritePauseMs,
233
+ cli: false
234
+ }
235
+ };
236
+
178
237
  // src/config-schema-hub.ts
179
238
  var HUB_TIMEOUT_MIN_SECONDS = 1;
180
239
  var HUB_TIMEOUT_MAX_SECONDS = 30;
@@ -569,6 +628,7 @@ var CONFIG_DEFAULTS = Object.freeze({
569
628
  boardRelationships: "labels",
570
629
  boardProjectTemplate: BOARD_PROJECT_TEMPLATE_DEFAULT,
571
630
  boardProjectNumber: null,
631
+ ...BOARD_PROJECT_LIMIT_DEFAULTS,
572
632
  roadmapIssue: null,
573
633
  claimsStaleAfter: "3d",
574
634
  claimsAhead: "off",
@@ -659,6 +719,7 @@ var SETTINGS = {
659
719
  },
660
720
  boardProjectTemplate: { key: "board.project.template", read: projectUrl, cli: false },
661
721
  boardProjectNumber: { key: "board.project.number", read: projectNumber, cli: false },
722
+ ...BOARD_PROJECT_LIMIT_SETTINGS,
662
723
  roadmapIssue: { key: "roadmap.issue", read: issueNumber, cli: false },
663
724
  claimsStaleAfter: { key: "claims.staleAfter", read: claimsStaleAfter, cli: false },
664
725
  claimsAhead: { key: "claims.ahead", read: claimsAhead, cli: false },
@@ -797,6 +858,11 @@ function readLayer(rawOf, atOf) {
797
858
  boardRelationships: read("boardRelationships"),
798
859
  boardProjectTemplate: read("boardProjectTemplate"),
799
860
  boardProjectNumber: read("boardProjectNumber"),
861
+ boardProjectRetries: read("boardProjectRetries"),
862
+ boardProjectRetryWaitSeconds: read("boardProjectRetryWaitSeconds"),
863
+ boardProjectProgressSeconds: read("boardProjectProgressSeconds"),
864
+ boardProjectWriteBatchSize: read("boardProjectWriteBatchSize"),
865
+ boardProjectWritePauseMs: read("boardProjectWritePauseMs"),
800
866
  roadmapIssue: read("roadmapIssue"),
801
867
  claimsStaleAfter: read("claimsStaleAfter"),
802
868
  claimsAhead: read("claimsAhead"),
@@ -15,7 +15,7 @@ import {
15
15
  parseConfigText,
16
16
  readRafaBlocks,
17
17
  resolveConfig
18
- } from "./index-m0wt4nr1.js";
18
+ } from "./index-334y39tn.js";
19
19
  import {
20
20
  bless,
21
21
  merge
@@ -259,7 +259,7 @@ import { Database as Database2 } from "bun:sqlite";
259
259
 
260
260
  // package.json
261
261
  var name = "@open-tomato/rafa";
262
- var version = "0.39.0";
262
+ var version = "0.40.0";
263
263
 
264
264
  // src/cli/version.ts
265
265
  var RAFA_VERSION = version;
@@ -41,7 +41,6 @@ import {
41
41
  ORIGIN_FIELD,
42
42
  PREVIOUS_COPY_WARN_ABOVE,
43
43
  PROJECT_SCOPE_FIX,
44
- PROJECT_WRITE_PAUSE_MS,
45
44
  PROVENANCE_FIELD,
46
45
  REASON_FLAG,
47
46
  RELEASE_BASE_BRANCH,
@@ -111,6 +110,8 @@ import {
111
110
  closedIssuesIn,
112
111
  collect,
113
112
  collectSessionRow,
113
+ commandProgressFeed,
114
+ commandRetrySeams,
114
115
  commandWords,
115
116
  compareVersions,
116
117
  configuredMoveRelations,
@@ -220,11 +221,14 @@ import {
220
221
  noBoardListsLine,
221
222
  normaliseReason,
222
223
  notFoundWarning,
224
+ notRefreshedWarning,
223
225
  notesFileName,
224
226
  onTracker,
225
227
  openBoards,
226
228
  openNextRelations,
227
229
  openNextSources,
230
+ openPhase,
231
+ openProjectRunner,
228
232
  parseBaseStanding,
229
233
  parseClaimMessage,
230
234
  parseReportArgs,
@@ -361,6 +365,7 @@ import {
361
365
  shellQuote,
362
366
  show_default,
363
367
  skillItem,
368
+ skippedFieldWarning,
364
369
  sourceItem,
365
370
  stackPaths,
366
371
  stacksFromEccSkillName,
@@ -395,7 +400,7 @@ import {
395
400
  worktreesHolding,
396
401
  writeSession,
397
402
  writeShippedFile
398
- } from "./index-nwgwxfkt.js";
403
+ } from "./index-zcbj07aq.js";
399
404
  import {
400
405
  PROBE_TIMEOUT_MS,
401
406
  loadPlanPrerequisites,
@@ -407,7 +412,7 @@ import {
407
412
  runPreflight,
408
413
  splitBlockerComment,
409
414
  trackerPathFor
410
- } from "./index-g4116t9m.js";
415
+ } from "./index-krpmxpkh.js";
411
416
  import {
412
417
  ACTION_HEADING,
413
418
  CAUSE_HEADING,
@@ -518,7 +523,7 @@ import {
518
523
  versionLine,
519
524
  writeFrontmatter,
520
525
  writePositionFile
521
- } from "./index-1r2b1052.js";
526
+ } from "./index-8veaj0t9.js";
522
527
  import {
523
528
  CONFIG_DEFAULTS,
524
529
  CONFIG_FILE,
@@ -537,7 +542,7 @@ import {
537
542
  readItemBytes,
538
543
  readRafaBlocks,
539
544
  resolveTiers
540
- } from "./index-m0wt4nr1.js";
545
+ } from "./index-334y39tn.js";
541
546
  import {
542
547
  promotable
543
548
  } from "./index-d6d0942q.js";
@@ -3309,8 +3314,17 @@ async function addIssues(options, ref, issues) {
3309
3314
  const project = await port.find(ref);
3310
3315
  if (project === null)
3311
3316
  return null;
3312
- for (const number of issues)
3313
- await port.addItem(project.id, { repository, number });
3317
+ const phase = openPhase(options.progress, "adds", issues.length);
3318
+ let added = 0;
3319
+ try {
3320
+ for (const number of issues) {
3321
+ await port.addItem(project.id, { repository, number });
3322
+ added += 1;
3323
+ phase.advance(added);
3324
+ }
3325
+ } finally {
3326
+ phase.end({ done: added, refused: issues.length - added });
3327
+ }
3314
3328
  return issues;
3315
3329
  }
3316
3330
  async function addMissing(options, first, synced) {
@@ -3327,6 +3341,7 @@ async function addMissing(options, first, synced) {
3327
3341
  ...synced,
3328
3342
  changes: [...first.changes, ...second.changes],
3329
3343
  added,
3344
+ refused: [...first.refused, ...second.refused],
3330
3345
  writes: addedWrites(first.writes, second.writes),
3331
3346
  warnings: [...first.warnings, ...second.warnings.filter((line) => !first.warnings.includes(line))]
3332
3347
  };
@@ -3347,6 +3362,7 @@ async function syncProject(options) {
3347
3362
  changes: first.changes,
3348
3363
  missing: first.missing,
3349
3364
  added: [],
3365
+ refused: first.refused,
3350
3366
  writes: first.writes,
3351
3367
  warnings: first.warnings
3352
3368
  });
@@ -3364,17 +3380,21 @@ function counted(count, noun) {
3364
3380
  function changeLine(change) {
3365
3381
  return `#${String(change.issue)} ${change.name}: ${change.from ?? EMPTY_VALUE} \u2192 ${change.to ?? EMPTY_VALUE}`;
3366
3382
  }
3383
+ function refusedPart(result) {
3384
+ return result.refused.length === 0 ? "" : `; ${counted(result.refused.length, "issue")} not refreshed`;
3385
+ }
3367
3386
  function closingLine(result) {
3368
3387
  const project = `project #${String(result.project.number)}`;
3369
3388
  const adds = result.dryRun ? result.missing : result.added;
3370
3389
  const changes = counted(result.changes.length, "change");
3371
3390
  const issues = counted(adds.length, "issue");
3391
+ const inStep = result.changes.length === 0 && adds.length === 0 && result.refused.length === 0;
3372
3392
  if (result.dryRun) {
3373
- 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.`;
3393
+ return inStep ? `Dry run on ${project}: in step, nothing to change.` : `Dry run on ${project}: ${changes} and ${issues} to add${refusedPart(result)}; nothing written.`;
3374
3394
  }
3375
3395
  if (result.rateLimited)
3376
3396
  return `Stopped on ${project}: ${String(result.written)} of ${changes} written; the rate limit refused the rest.`;
3377
- return result.changes.length === 0 && adds.length === 0 ? `Synced ${project}: in step, nothing to change.` : `Synced ${project}: ${changes} written and ${issues} added.`;
3397
+ return inStep ? `Synced ${project}: in step, nothing to change.` : `Synced ${project}: ${changes} written and ${issues} added${refusedPart(result)}.`;
3378
3398
  }
3379
3399
  function syncLines(result) {
3380
3400
  const adds = result.dryRun ? result.missing.map((issue) => `#${String(issue)} is not on the project: would be added`) : result.added.map((issue) => `#${String(issue)} added to the project`);
@@ -3390,6 +3410,7 @@ function syncResultOf(synced) {
3390
3410
  changes: synced.changes.map(plainChange),
3391
3411
  missing: synced.missing,
3392
3412
  added: synced.added,
3413
+ refused: synced.refused.map(({ number, reason }) => ({ issue: number, reason })),
3393
3414
  written: synced.writes.written,
3394
3415
  rateLimited: synced.writes.rateLimited,
3395
3416
  notUpdated: synced.writes.notUpdated,
@@ -3407,9 +3428,10 @@ async function runBoardSync(context, seams) {
3407
3428
  if (config.boardProjectNumber === null) {
3408
3429
  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.`);
3409
3430
  }
3410
- const gh = seams.gh ?? createGhRunner({ cwd: project.root });
3431
+ const gh = openProjectRunner(seams.gh ?? createGhRunner({ cwd: project.root }), config, commandRetrySeams(context.output, context.outputMode, seams.sleep));
3411
3432
  const dryRun = context.flags[DRY_RUN_FLAG] === true;
3412
- const synced = await syncProject(seams.sleep === undefined ? { config, gh, dryRun } : { config, gh, dryRun, sleep: seams.sleep }).catch((error) => {
3433
+ const progress = commandProgressFeed(context.output, context.outputMode, config.boardProjectProgressSeconds, seams.now === undefined ? {} : { now: seams.now });
3434
+ const synced = await syncProject(seams.sleep === undefined ? { config, gh, dryRun, progress } : { config, gh, dryRun, progress, sleep: seams.sleep }).catch((error) => {
3413
3435
  throw new CommandExit(BOARD_SYNC_REFUSAL_EXIT, `\u274C Could not sync the project: ${messageOf(error)}`);
3414
3436
  });
3415
3437
  if (synced.kind !== "synced") {
@@ -3439,7 +3461,7 @@ function createBoardSyncCommand(seams = {}) {
3439
3461
  subject: "board",
3440
3462
  action: "sync",
3441
3463
  summary: "bring the repository's GitHub project in step with its issues",
3442
- 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.",
3464
+ 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.",
3443
3465
  args: [],
3444
3466
  flags: [
3445
3467
  {
@@ -8274,41 +8296,76 @@ async function missingIssues(options, repository, project) {
8274
8296
  const held = heldIssues(await createGhProjectPort(gh).items(project.id), repository);
8275
8297
  const ranked = board.closed.filter((issue) => rankOf(board.ranks, issue) !== null);
8276
8298
  const unread = board.closed.filter((issue) => rankOf(board.ranks, issue) === null && !held.has(issue));
8277
- const facts = await readIssueFacts({ gh, fragments: config.releaseFragments }, unread);
8299
+ const { facts } = await readIssueFacts({ gh, fragments: config.releaseFragments }, unread);
8278
8300
  const inReview = unread.filter((issue) => {
8279
8301
  const read = facts.get(issue);
8280
8302
  return read !== undefined && stageOf(read) === "In review";
8281
8303
  });
8282
- return [...new Set([...board.open, ...ranked, ...inReview])].filter((issue) => !held.has(issue)).sort((a, b) => a - b);
8304
+ const missing = [...new Set([...board.open, ...ranked, ...inReview])].filter((issue) => !held.has(issue)).sort((a, b) => a - b);
8305
+ return { held, missing };
8283
8306
  }
8284
8307
  async function itemsStep(options, repository, project) {
8285
8308
  const sleep = options.sleep ?? ((ms) => Bun.sleep(ms));
8286
- let missing;
8309
+ let wanted;
8287
8310
  try {
8288
- missing = await missingIssues(options, repository, project);
8311
+ wanted = await missingIssues(options, repository, project);
8289
8312
  } catch (error) {
8290
- return partOf("items", ITEMS_PART, "refused", `the issues to add could not be read: ${refusalOf(error)}`);
8313
+ return { part: partOf("items", ITEMS_PART, "refused", `the issues to add could not be read: ${refusalOf(error)}`), holds: null };
8291
8314
  }
8315
+ const { held, missing } = wanted;
8292
8316
  if (missing.length === 0)
8293
- return partOf("items", ITEMS_PART, "present", "the project holds every issue it shows");
8317
+ return { part: partOf("items", ITEMS_PART, "present", "the project holds every issue it shows"), holds: held.size };
8294
8318
  const port = createGhProjectPort(options.gh);
8319
+ const phase = openPhase(options.progress, "adds", missing.length);
8295
8320
  let added = 0;
8296
8321
  try {
8297
8322
  for (const number of missing) {
8298
8323
  if (added > 0)
8299
- await sleep(PROJECT_WRITE_PAUSE_MS);
8324
+ await sleep(options.config.boardProjectWritePauseMs);
8300
8325
  await port.addItem(project.id, { repository, number });
8301
8326
  added += 1;
8327
+ phase.advance(added);
8302
8328
  }
8303
8329
  } catch (error) {
8330
+ phase.end({ done: added, refused: missing.length - added });
8304
8331
  const went = `${issueCount(added)} of ${String(missing.length)} added`;
8305
- return partOf("items", ITEMS_PART, "refused", `${went}, then #${String(missing[added])} was refused: ${refusalOf(error)}; run \`${PROJECT_STEP_FIX}\` to add the rest`);
8332
+ const why = `${went}, then #${String(missing[added])} was refused: ${refusalOf(error)}; run \`${PROJECT_STEP_FIX}\` to add the rest`;
8333
+ return { part: partOf("items", ITEMS_PART, "refused", why), holds: held.size + added };
8306
8334
  }
8307
- return partOf("items", ITEMS_PART, "created", `added ${issueCount(added)} to the project`);
8335
+ phase.end({ done: added, refused: 0 });
8336
+ return { part: partOf("items", ITEMS_PART, "created", `added ${issueCount(added)} to the project`), holds: held.size + added };
8337
+ }
8338
+ function withListed(part, listed2) {
8339
+ return listed2.length === 0 ? part : { ...part, listed: listed2 };
8308
8340
  }
8309
- async function fieldsStep(options, project) {
8341
+ function refusedClause(refused) {
8342
+ return refused === 0 ? "" : `; ${issueCount(refused)} not refreshed`;
8343
+ }
8344
+ function refreshedPart(refreshed, holds) {
8345
+ const { writes, changes, skipped, refused } = refreshed;
8346
+ const listed2 = refused.map(notRefreshedWarning);
8347
+ if (writes.rateLimited)
8348
+ return withListed(partOf("fields", FIELDS_PART, "refused", rateLimitWarning(writes.notUpdated)), listed2);
8349
+ if (changes.length === 0 && refused.length > 0 && (holds === null || holds <= refused.length)) {
8350
+ return withListed(partOf("fields", FIELDS_PART, "refused", `no item was filled: ${issueCount(refused.length)} not refreshed`), listed2);
8351
+ }
8352
+ if (changes.length === 0) {
8353
+ const held = skipped.length === 0 ? "every value is as the rules give it" : "every value written is as the rules give it";
8354
+ return withListed(partOf("fields", FIELDS_PART, "present", `${held}${refusedClause(refused.length)}`), listed2);
8355
+ }
8356
+ const issues = new Set(changes.map(({ issue }) => issue)).size;
8357
+ const values = writes.written === 1 ? "1 value" : `${String(writes.written)} values`;
8358
+ return withListed(partOf("fields", FIELDS_PART, "created", `wrote ${values} to ${issueCount(issues)}${refusedClause(refused.length)}`), listed2);
8359
+ }
8360
+ async function fieldsStep(options, project, holds) {
8310
8361
  const config = { ...options.config, boardProjectNumber: project.number };
8311
- const refreshOptions = options.sleep === undefined ? { config, gh: options.gh } : { config, gh: options.gh, sleep: options.sleep };
8362
+ const { sleep, progress } = options;
8363
+ const refreshOptions = {
8364
+ config,
8365
+ gh: options.gh,
8366
+ ...sleep === undefined ? {} : { sleep },
8367
+ ...progress === undefined ? {} : { progress }
8368
+ };
8312
8369
  let refreshed;
8313
8370
  try {
8314
8371
  refreshed = await refreshProjectItems(refreshOptions, [], { everyItem: true });
@@ -8320,24 +8377,13 @@ async function fieldsStep(options, project) {
8320
8377
  if (refreshed.kind !== "refreshed") {
8321
8378
  return { part: partOf("fields", FIELDS_PART, "refused", refreshed.warnings.join(" ") || "the refresh sent no call"), problems: [] };
8322
8379
  }
8323
- const { writes, changes, skipped } = refreshed;
8324
- const problems = refreshed.warnings.slice(writes.rateLimited ? 1 : 0);
8325
- if (writes.rateLimited) {
8326
- return { part: partOf("fields", FIELDS_PART, "refused", rateLimitWarning(writes.notUpdated)), problems };
8327
- }
8328
- if (changes.length === 0) {
8329
- const held = skipped.length === 0 ? "every value is as the rules give it" : "every value written is as the rules give it";
8330
- return { part: partOf("fields", FIELDS_PART, "present", held), problems };
8331
- }
8332
- const issues = new Set(changes.map(({ issue }) => issue)).size;
8333
- const values = writes.written === 1 ? "1 value" : `${String(writes.written)} values`;
8334
- return { part: partOf("fields", FIELDS_PART, "created", `wrote ${values} to ${issueCount(issues)}`), problems };
8380
+ return { part: refreshedPart(refreshed, holds), problems: refreshed.skipped.map(skippedFieldWarning) };
8335
8381
  }
8336
8382
  async function onProject(options, repository, project) {
8337
8383
  const items = await itemsStep(options, repository, project);
8338
- const fields = await fieldsStep(options, project);
8384
+ const fields = await fieldsStep(options, project, items.holds);
8339
8385
  const setting = writeProjectNumber(options.root, project.number);
8340
- return { parts: [items, fields.part, setting], problems: fields.problems };
8386
+ return { parts: [items.part, fields.part, setting], problems: fields.problems };
8341
8387
  }
8342
8388
  async function setUpProject(options) {
8343
8389
  const { gh, config } = options;
@@ -8369,12 +8415,16 @@ function projectSetupChanged(report2) {
8369
8415
  }
8370
8416
  var PROJECT_HEADING = "GitHub project:";
8371
8417
  var OUTCOME_WIDTH3 = 7;
8418
+ var LISTED_INDENT = " ".repeat(2 + OUTCOME_WIDTH3 + 2);
8372
8419
  function projectPartLine(part) {
8373
8420
  const row = ` ${part.outcome.padEnd(OUTCOME_WIDTH3, " ")} ${part.name}`;
8374
8421
  return part.outcome === "present" ? row : `${row}: ${part.detail}`;
8375
8422
  }
8423
+ function projectPartLines(part) {
8424
+ return [projectPartLine(part), ...(part.listed ?? []).map((line) => `${LISTED_INDENT}${line}`)];
8425
+ }
8376
8426
  function renderProjectSetup(report2) {
8377
- return [PROJECT_HEADING, ...report2.parts.map((part) => projectPartLine(part)), ...report2.problems];
8427
+ return [PROJECT_HEADING, ...report2.parts.flatMap((part) => projectPartLines(part)), ...report2.problems];
8378
8428
  }
8379
8429
  var PROJECT_QUESTION = "Also create a GitHub project with roadmap and kanban views? [y/N] ";
8380
8430
  var 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}.`;
@@ -8394,6 +8444,13 @@ function withWrittenRoadmap(root, config) {
8394
8444
  const written = readRoadmapSetting(root).issue;
8395
8445
  return written === null ? config : { ...config, roadmapIssue: written };
8396
8446
  }
8447
+ function retrySeamsOf(options) {
8448
+ const { sleep, onRetry } = options;
8449
+ return {
8450
+ ...sleep === undefined ? {} : { sleep },
8451
+ ...onRetry === undefined ? {} : { onRetry }
8452
+ };
8453
+ }
8397
8454
  async function runProjectStep(options) {
8398
8455
  const { wanted, board, root, isTerminal, openPrompter } = options;
8399
8456
  if (board.status !== "ran") {
@@ -8403,7 +8460,16 @@ async function runProjectStep(options) {
8403
8460
  return noProject("declined", false);
8404
8461
  const run = async (asked) => {
8405
8462
  const setUp = options.setUp ?? setUpProject;
8406
- const report2 = await setUp({ root, config: withWrittenRoadmap(root, options.config), gh: options.openGh() });
8463
+ const config = withWrittenRoadmap(root, options.config);
8464
+ const gh = openProjectRunner(options.openGh(), config, retrySeamsOf(options));
8465
+ const { sleep, progress } = options;
8466
+ const report2 = await setUp({
8467
+ root,
8468
+ config,
8469
+ gh,
8470
+ ...sleep === undefined ? {} : { sleep },
8471
+ ...progress === undefined ? {} : { progress }
8472
+ });
8407
8473
  return { status: "ran", asked, report: report2, warnings: [] };
8408
8474
  };
8409
8475
  if (wanted === true)
@@ -8481,9 +8547,10 @@ function fieldsRow(project, template) {
8481
8547
  return rowOf2("fields", FIELDS_ROW, "missing", `${sentences} ${fix}`);
8482
8548
  }
8483
8549
  async function readDoctorProject(input) {
8484
- const { gh, number, template } = input;
8485
- if (gh === null || number === null)
8550
+ const { number, template } = input;
8551
+ if (input.gh === null || number === null)
8486
8552
  return null;
8553
+ const gh = openProjectRunner(input.gh, input.config, input.retry);
8487
8554
  const scope = await readScopeRow(gh);
8488
8555
  if (scope.outcome !== "present") {
8489
8556
  const project = notRead("project", `${PROJECT_ROW} ${String(number)}`, scope);
@@ -10614,7 +10681,7 @@ async function runDoctor(context, seams) {
10614
10681
  const modeSet = preflight.resolved.sources.boardRelationships !== "default";
10615
10682
  const board = await readDoctorBoard(gh, project.root, preflight.config.roadmapIssue, preflight.config.boardRelationships, modeSet);
10616
10683
  const { boardProjectNumber: number, boardProjectTemplate: template } = preflight.config;
10617
- const projectRows = await readDoctorProject({ gh, number, template });
10684
+ const projectRows = await readDoctorProject({ gh, number, template, config: preflight.config, retry: commandRetrySeams(context.output, context.outputMode) });
10618
10685
  const readings = { ...board, project: projectRows, cleanup, refs, release, tiers, effortSchema, effortSync };
10619
10686
  writeText(context, renderDoctor(preflight));
10620
10687
  await announceRisk(context, preflight, seams);
@@ -12002,7 +12069,8 @@ async function refreshProjectAfterEpic(context, seams, target) {
12002
12069
  });
12003
12070
  if (config.boardProjectNumber === null)
12004
12071
  return null;
12005
- const openGh = () => seams.gh ?? createGhRunner({ cwd: project.root });
12072
+ const retry = commandRetrySeams(context.output, context.outputMode, seams.sleep);
12073
+ const openGh = () => openProjectRunner(seams.gh ?? createGhRunner({ cwd: project.root }), config, retry);
12006
12074
  warnings = await refreshLines(config, openGh, seams.projectRefresh ?? refreshIssueItems, target, named2);
12007
12075
  } catch (error) {
12008
12076
  warnings = [epicProjectProblemLine(named2, messageOf(error))];
@@ -14846,6 +14914,11 @@ var CONFIG_SETTINGS_LINES = Object.freeze([
14846
14914
  "# project:",
14847
14915
  `# template: ${CONFIG_DEFAULTS.boardProjectTemplate} # the project URL rafa init --board --project copies`,
14848
14916
  "# number: # the project rafa mirrors the board to; unset until rafa init --board --project",
14917
+ `# retries: ${String(CONFIG_DEFAULTS.boardProjectRetries)} # 1..10 | false, tries again after a failed project call`,
14918
+ `# retryWaitSeconds: ${String(CONFIG_DEFAULTS.boardProjectRetryWaitSeconds)} # 1..60, the first wait before a retry; each later one doubles`,
14919
+ `# progressSeconds: ${String(CONFIG_DEFAULTS.boardProjectProgressSeconds)} # 1..300 | false, the most seconds between two progress lines`,
14920
+ `# writeBatchSize: ${String(CONFIG_DEFAULTS.boardProjectWriteBatchSize)} # 1..100, field writes per request`,
14921
+ `# writePauseMs: ${String(CONFIG_DEFAULTS.boardProjectWritePauseMs)} # 0..60000, the pause between two write requests`,
14849
14922
  "# roadmap:",
14850
14923
  "# issue: # the issue plan create --next reads; unset is the one titled Roadmap",
14851
14924
  "# claims:",
@@ -15248,7 +15321,9 @@ async function runInit(context, seams) {
15248
15321
  config: scopes.config,
15249
15322
  openGh: () => seams.gh(scopes.written.root),
15250
15323
  isTerminal: seams.isTerminal,
15251
- openPrompter: seams.openPrompter
15324
+ openPrompter: seams.openPrompter,
15325
+ ...commandRetrySeams(context.output, context.outputMode, seams.sleep),
15326
+ progress: commandProgressFeed(context.output, context.outputMode, scopes.config.boardProjectProgressSeconds, seams.now === undefined ? {} : { now: seams.now })
15252
15327
  });
15253
15328
  const result2 = {
15254
15329
  ...scopes.written,
@@ -15285,7 +15360,7 @@ function createInitCommand(seams = DEFAULT_INIT_SEAMS) {
15285
15360
  subject: "init",
15286
15361
  action: "init",
15287
15362
  summary: "set up a rafa project: its root, .rafa/config.yaml, the tree and the .gitignore entry",
15288
- 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.",
15363
+ 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.",
15289
15364
  args: [],
15290
15365
  flags: [
15291
15366
  {
@@ -15310,7 +15385,7 @@ function createInitCommand(seams = DEFAULT_INIT_SEAMS) {
15310
15385
  },
15311
15386
  {
15312
15387
  name: "project",
15313
- 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.",
15388
+ 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`.",
15314
15389
  type: "boolean"
15315
15390
  },
15316
15391
  {
@@ -16400,7 +16475,7 @@ async function addCreatedToProject(context, seams, ref8) {
16400
16475
  });
16401
16476
  const lines = await addAndRefreshIssue({
16402
16477
  config,
16403
- openGh: () => seams.gh ?? createGhRunner({ cwd: project.root })
16478
+ openGh: () => openProjectRunner(seams.gh ?? createGhRunner({ cwd: project.root }), config, commandRetrySeams(context.output, context.outputMode, seams.sleep))
16404
16479
  }, issue);
16405
16480
  for (const line of lines)
16406
16481
  context.output.warn(line);
@@ -19075,6 +19150,10 @@ function openPrContext(context, seams = DEFAULT_PR_SEAMS) {
19075
19150
  versionGuard: mergeGuardSettings(config),
19076
19151
  projectRefresh: {
19077
19152
  boardProjectNumber: config.boardProjectNumber,
19153
+ boardProjectRetries: config.boardProjectRetries,
19154
+ boardProjectRetryWaitSeconds: config.boardProjectRetryWaitSeconds,
19155
+ boardProjectWriteBatchSize: config.boardProjectWriteBatchSize,
19156
+ boardProjectWritePauseMs: config.boardProjectWritePauseMs,
19078
19157
  boardRelationships: config.boardRelationships,
19079
19158
  roadmapIssue: config.roadmapIssue,
19080
19159
  releaseFragments: config.releaseFragments
@@ -19879,7 +19958,7 @@ async function refreshProjectAfterMerge(options) {
19879
19958
  const closed = [...new Set(closedIssuesIn(options.body))];
19880
19959
  if (config.boardProjectNumber === null || closed.length === 0)
19881
19960
  return null;
19882
- const gh = options.openGh();
19961
+ const gh = openProjectRunner(options.openGh(), config, options.retry);
19883
19962
  const wanted = new Set(closed);
19884
19963
  const warnings = [];
19885
19964
  let listed2 = [];
@@ -20646,7 +20725,14 @@ async function runMerge2(context, seams) {
20646
20725
  const roadmapTicks = await reportTick(context, pr, seams, detail);
20647
20726
  const steps = cleanUpAfterMerge(git, detail, { info: info2, warn: warn3 });
20648
20727
  const board = await reportAfterCleanUp(context, pr, seams, detail);
20649
- await refreshProjectAfterMerge({ body: detail.body, config: pr.projectRefresh, openGh: () => openGh(pr, seams), unblocked: board.unblocked, warn: warn3 });
20728
+ await refreshProjectAfterMerge({
20729
+ body: detail.body,
20730
+ config: pr.projectRefresh,
20731
+ openGh: () => openGh(pr, seams),
20732
+ unblocked: board.unblocked,
20733
+ warn: warn3,
20734
+ retry: commandRetrySeams(context.output, context.outputMode)
20735
+ });
20650
20736
  const followUps = reportFollowUps({
20651
20737
  root: pr.project.root,
20652
20738
  home: pr.project.home,
@@ -23822,7 +23908,7 @@ async function refreshProjectAfterSettle(options) {
23822
23908
  const commits = releasedCommits(options.delivered);
23823
23909
  if (config.boardProjectNumber === null || commits === null || commits.length === 0)
23824
23910
  return null;
23825
- const gh = options.openGh();
23911
+ const gh = openProjectRunner(options.openGh(), config, options.retry);
23826
23912
  let pulls;
23827
23913
  try {
23828
23914
  pulls = await readPulls(gh, commits, branch);
@@ -23998,7 +24084,8 @@ async function runSettle(context, seams = DEFAULT_RELEASE_SETTLE_SEAMS) {
23998
24084
  branch,
23999
24085
  config,
24000
24086
  openGh: () => (seams.gh ?? ((cwd) => createGhRunner({ cwd })))(project.root),
24001
- refresh: seams.refresh
24087
+ refresh: seams.refresh,
24088
+ retry: commandRetrySeams(context.output, context.outputMode)
24002
24089
  });
24003
24090
  return { ...ran.value, project: refreshed };
24004
24091
  }
@@ -3,7 +3,7 @@ import {
3
3
  activeOutput,
4
4
  isStampableStub,
5
5
  readRafaBlocks
6
- } from "./index-m0wt4nr1.js";
6
+ } from "./index-334y39tn.js";
7
7
  import {
8
8
  messageOf,
9
9
  parseTaskDeclaration