@open-tomato/rafa 0.26.0 → 0.28.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,6 +14,7 @@ import {
14
14
  writeTrackerBlocker
15
15
  } from "./index-p1265xgj.js";
16
16
  import {
17
+ ACROSS_STORES_ORDER,
17
18
  CORE_ADAPTER_REGISTRY,
18
19
  ClaudePlannerError,
19
20
  DEVELOPMENT_MARK,
@@ -33,6 +34,7 @@ import {
33
34
  SKILL_FILE,
34
35
  SQLITE_MIGRATIONS,
35
36
  SQLITE_STORE_FILE_NAME,
37
+ STAMPED_COLUMNS,
36
38
  SessionConflictError,
37
39
  SessionRecordError,
38
40
  SessionStateError,
@@ -40,13 +42,17 @@ import {
40
42
  beginSession,
41
43
  checkUsage,
42
44
  createGhRunner,
45
+ createGitRunner,
43
46
  createLocalTracker,
44
47
  declaredNameIssue,
45
48
  effortStoreDir,
46
49
  errorCode,
47
50
  getClaudeUsagePercent,
48
51
  gitRemoteUrl,
52
+ gitSaid,
53
+ goHome,
49
54
  guardTestProcess,
55
+ hopFilePath,
50
56
  interruptClaudeSessions,
51
57
  isPidAlive,
52
58
  isSessionId,
@@ -56,8 +62,12 @@ import {
56
62
  parseInstinct,
57
63
  planFilePath,
58
64
  planSchema,
65
+ positionAt,
66
+ positionFilePath,
59
67
  readFrontmatter,
60
68
  readFrontmatterDocument,
69
+ readHopRecord,
70
+ readPositionFile,
61
71
  readScopes,
62
72
  readSession,
63
73
  readSessions,
@@ -67,21 +77,24 @@ import {
67
77
  runClaudeCaptured,
68
78
  runsDir,
69
79
  scanLayout,
70
- scopeAt,
71
80
  selectEffortStore,
72
81
  sessionFilePath,
73
82
  setActiveStoreSettings,
74
83
  sqliteCatalogue,
75
84
  sqliteStorePath,
85
+ staleAgainst,
86
+ stampedValues,
76
87
  typeOfLabels,
77
88
  updateSession,
78
89
  version,
79
90
  withSqliteStore,
80
91
  writeFrontmatter,
92
+ writeHopRecord,
81
93
  writeInstinct,
82
94
  writeNames,
95
+ writePositionFile,
83
96
  writeSqliteStore
84
- } from "./index-azptzvp3.js";
97
+ } from "./index-4rwvr6t4.js";
85
98
  import {
86
99
  CHANGE_LEVELS,
87
100
  CONFIG_DEFAULTS,
@@ -967,9 +980,10 @@ var INSERT_FINDING = `
967
980
  INSERT INTO findings (
968
981
  id, session_id, plan_stub, task_line,
969
982
  kind, trigger, what, cause, resolution, artifact, signal,
970
- outcome, tracker_ref, collected_at
983
+ outcome, tracker_ref, collected_at,
984
+ ${STAMPED_COLUMNS}
971
985
  )
972
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?)
986
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ${stampedValues("findings")})
973
987
  ON CONFLICT (session_id, artifact) WHERE artifact IS NOT NULL DO NOTHING
974
988
  ON CONFLICT (session_id, trigger, what) WHERE artifact IS NULL DO NOTHING
975
989
  `;
@@ -1096,8 +1110,8 @@ function writeFindings(repoRoot, write, seams = {}) {
1096
1110
  // src/effort/store/skill-invocations.ts
1097
1111
  var UNKNOWN_SKILL_COUNT = "unknown";
1098
1112
  var INSERT_INVOCATION = `
1099
- INSERT INTO skill_invocations (session_id, name, sidechain, count)
1100
- VALUES (?, ?, ?, ?)
1113
+ INSERT INTO skill_invocations (session_id, name, sidechain, count, ${STAMPED_COLUMNS})
1114
+ VALUES (?, ?, ?, ?, ${stampedValues("skill_invocations")})
1101
1115
  `;
1102
1116
  var SELECT_HELD = "SELECT 1 AS held FROM skill_invocations WHERE session_id = ? LIMIT 1";
1103
1117
  var SELECT_ALL = "SELECT session_id, name, sidechain, count FROM skill_invocations ORDER BY seq";
@@ -3812,8 +3826,8 @@ async function waitForChecks(options) {
3812
3826
  // src/effort/store/plan-ci.ts
3813
3827
  var PLAN_CI_VERDICTS = ["green", "red", "none"];
3814
3828
  var INSERT_PLAN_CI = `
3815
- INSERT INTO plan_ci (plan_stub, pr, head_sha, verdict, failing, read_at)
3816
- VALUES (?, ?, ?, ?, ?, ?)
3829
+ INSERT INTO plan_ci (plan_stub, pr, head_sha, verdict, failing, read_at, ${STAMPED_COLUMNS})
3830
+ VALUES (?, ?, ?, ?, ?, ?, ${stampedValues("plan_ci")})
3817
3831
  ON CONFLICT (pr, head_sha, read_at) DO NOTHING
3818
3832
  `;
3819
3833
  var SELECT_COLUMNS = "SELECT plan_stub, pr, head_sha, verdict, failing, read_at FROM plan_ci";
@@ -3928,9 +3942,10 @@ var INSERT_REPORT = `
3928
3942
  id, session_id, plan_stub, task_line,
3929
3943
  status,
3930
3944
  outcome, collected_at,
3931
- skills_used
3945
+ skills_used,
3946
+ ${STAMPED_COLUMNS}
3932
3947
  )
3933
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
3948
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ${stampedValues("task_reports")})
3934
3949
  ON CONFLICT (session_id) DO NOTHING
3935
3950
  `;
3936
3951
  function refusedWrite4(reason) {
@@ -3995,7 +4010,7 @@ function readTaskReportTallies(repoRoot) {
3995
4010
  var SELECT_REPORTED_SKILLS = `
3996
4011
  SELECT session_id, plan_stub, task_line, outcome, skills_used
3997
4012
  FROM task_reports
3998
- ORDER BY seq
4013
+ ORDER BY ${ACROSS_STORES_ORDER}
3999
4014
  `;
4000
4015
  function parseSkillsUsed(sessionId, json) {
4001
4016
  if (json === null)
@@ -4020,14 +4035,14 @@ function readReportedSkills(repoRoot) {
4020
4035
  }
4021
4036
 
4022
4037
  // src/effort/skill-facts.ts
4023
- var SELECT_OFFERS = "SELECT session_id, resolver, skills_offered, lessons_offered FROM dispatches ORDER BY seq";
4038
+ var SELECT_OFFERS = `SELECT session_id, resolver, skills_offered, lessons_offered FROM dispatches ORDER BY ${ACROSS_STORES_ORDER}`;
4024
4039
  var SELECT_FINDINGS = `
4025
4040
  SELECT session_id, kind, trigger, what, cause, resolution, artifact, signal
4026
4041
  FROM findings
4027
- ORDER BY seq
4042
+ ORDER BY ${ACROSS_STORES_ORDER}
4028
4043
  `;
4029
4044
  function selectEntries(table) {
4030
- return `SELECT session_id, what, artifact FROM ${table} ORDER BY seq`;
4045
+ return `SELECT session_id, what, artifact FROM ${table} ORDER BY ${ACROSS_STORES_ORDER}`;
4031
4046
  }
4032
4047
  function parseNames(column, sessionId, json) {
4033
4048
  if (json === null)
@@ -4329,9 +4344,10 @@ var INSERT_DISPATCH = `
4329
4344
  INSERT INTO dispatches (
4330
4345
  session_id, plan_stub, task_line,
4331
4346
  declaration, agent, model, effort, budget_usd, tools,
4332
- flags, resolver, skills_offered, lessons_offered, collected_at
4347
+ flags, resolver, skills_offered, lessons_offered, collected_at,
4348
+ ${STAMPED_COLUMNS}
4333
4349
  )
4334
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
4350
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ${stampedValues("dispatches")})
4335
4351
  ON CONFLICT (session_id) DO NOTHING
4336
4352
  `;
4337
4353
  function refusedWrite5(reason) {
@@ -4472,9 +4488,10 @@ var INSERT_CHECK = `
4472
4488
  run_id, position,
4473
4489
  tier, kind, item, probe,
4474
4490
  outcome, duration_ms, failure,
4475
- collected_at
4491
+ collected_at,
4492
+ ${STAMPED_COLUMNS}
4476
4493
  )
4477
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
4494
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ${stampedValues("preflight")})
4478
4495
  `;
4479
4496
  function refusedWrite6(reason) {
4480
4497
  return new Error(`effort store: preflight write ${reason}; nothing written`);
@@ -6297,6 +6314,7 @@ var SPEC_FLAG = "--spec";
6297
6314
  var ISSUE_FLAG = "--issue";
6298
6315
  var NEXT_FLAG = "--next";
6299
6316
  var REFRESH_FLAG = "--refresh";
6317
+ var ROADMAP_FLAG = "--roadmap";
6300
6318
  var DRY_RUN_FLAG = "--dry-run";
6301
6319
  var SKIP_REVIEW_FLAG = "--skip-review";
6302
6320
  var NO_COMMENT_FLAG = "--no-comment";
@@ -8627,26 +8645,6 @@ var GLOBAL_FLAGS = Object.freeze([
8627
8645
  ]);
8628
8646
  var SPENDS_LEGEND = `${SPENDS_GLYPH} starts Claude Code sessions, which spend your Claude usage`;
8629
8647
 
8630
- // src/pr/git.ts
8631
- import { spawnSync } from "child_process";
8632
- function createGitRunner(cwd) {
8633
- return (args) => {
8634
- const result = spawnSync("git", [...args], {
8635
- cwd,
8636
- encoding: "utf8",
8637
- env: { ...process.env, LC_ALL: "C" }
8638
- });
8639
- if (result.error !== undefined) {
8640
- return { ok: false, stdout: "", stderr: `could not run git in ${cwd}: ${messageOf(result.error)}` };
8641
- }
8642
- return { ok: result.status === 0, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
8643
- };
8644
- }
8645
- function gitSaid(result) {
8646
- return [result.stderr, result.stdout].map((stream) => stream.trim()).filter((stream) => stream !== "").join(`
8647
- `);
8648
- }
8649
-
8650
8648
  // src/refs/verify.ts
8651
8649
  class RefVerifyError extends Error {
8652
8650
  constructor(problem) {
@@ -9406,9 +9404,81 @@ function dryEpicSentence(epic) {
9406
9404
  const { done, total } = epic.progress;
9407
9405
  return `epic #${String(epic.number)} ${epic.title} has run dry: every line of its checklist and every open member it labels` + ` is done or taken, though the epic is not done (${String(done)}/${String(total)});` + " the walk stops here and does not move on to another epic";
9408
9406
  }
9407
+ function rowOnListing(number, listing, what) {
9408
+ const row = listing.find((issue3) => issue3.number === number);
9409
+ if (row === undefined) {
9410
+ throw new Error(`${PREFIX9}: ${what} #${String(number)} is not on the board listing, so the next ${NOW_HORIZON_LABEL} epic` + " cannot be read; the listing reads the newest issues only, and a line it leaves out is not passed as if it were none");
9411
+ }
9412
+ return row;
9413
+ }
9414
+ async function hasPickableLine(epic, row, readings) {
9415
+ const pick = await pickNextRoadmapLine(epicLines(epic, row).lines, readings);
9416
+ return pick.line !== null;
9417
+ }
9418
+ async function nowEpicOf(line, listing, readings) {
9419
+ if (line.ticked)
9420
+ return null;
9421
+ const row = rowOnListing(line.issue, listing, "checklist line");
9422
+ if (row.type !== "epic" || row.state !== "OPEN" || !isNowEpic(row.labels))
9423
+ return null;
9424
+ const { epic } = epicOnListing(line.issue, listing);
9425
+ if (epic.state === "done")
9426
+ return null;
9427
+ return await hasPickableLine(epic, row, readings) ? epic : null;
9428
+ }
9429
+ async function nextNowEpic(request) {
9430
+ const { after, board, listing, readings } = request;
9431
+ const lines = parseRoadmapBody(rowOnListing(board, listing, "board").body);
9432
+ const at = lines.findIndex((line) => line.issue === after);
9433
+ if (at === -1)
9434
+ return null;
9435
+ for (const line of lines.slice(at + 1)) {
9436
+ const epic = await nowEpicOf(line, listing, readings);
9437
+ if (epic === null)
9438
+ continue;
9439
+ return Object.freeze({
9440
+ number: epic.number,
9441
+ title: epic.title,
9442
+ slug: epic.slug,
9443
+ board,
9444
+ line,
9445
+ progress: epic.progress
9446
+ });
9447
+ }
9448
+ return null;
9449
+ }
9409
9450
 
9410
9451
  // src/board/spec-source-roadmap.ts
9411
9452
  var PREFIX10 = "board spec source roadmap";
9453
+ function hopHeaderLine(away) {
9454
+ const { record, target } = away;
9455
+ return `\uD83E\uDDED Away on a hop: issue #${String(target)}, the blocker of #${String(record.blocked)},` + ` in epic #${String(record.targetEpic)} on board #${String(record.targetBoard)}`;
9456
+ }
9457
+ function hopPickLine(target) {
9458
+ return `\u25B6 Next on the hop: issue #${String(target)}`;
9459
+ }
9460
+ function hopBlockedMessage(target) {
9461
+ return `issue #${String(target)} is the hop's target and still has an open blocker, so nothing is planned;` + " a hop follows one blocker, never a second, and `rafa next --roadmap` goes home from here";
9462
+ }
9463
+ function unfollowedHopNotice(detail) {
9464
+ return `${detail}, so no hop is followed and --next picks as it does without --roadmap`;
9465
+ }
9466
+ function readAwayHop(root) {
9467
+ const hop = readHopRecord(root);
9468
+ if (!hop.set) {
9469
+ return {
9470
+ away: null,
9471
+ notices: hop.reason === "absent" ? [] : [unfollowedHopNotice(hop.detail)]
9472
+ };
9473
+ }
9474
+ const { record } = hop;
9475
+ const placed = readPositionFile(root);
9476
+ if (!placed.set || staleAgainst(record, placed.position))
9477
+ return { away: null, notices: [] };
9478
+ if (record.state !== "away" || record.kind !== "blocker" || record.target === null)
9479
+ return { away: null, notices: [] };
9480
+ return { away: Object.freeze({ record, target: record.target }), notices: [] };
9481
+ }
9412
9482
  function roadmapHeaderLine(roadmap) {
9413
9483
  return `\uD83D\uDDFA Reading the roadmap, issue #${String(roadmap)}, for the next spec...`;
9414
9484
  }
@@ -9487,6 +9557,12 @@ async function walkPlaceEpic(number, listing, readings) {
9487
9557
  }
9488
9558
  async function pickRoadmapIssue(options) {
9489
9559
  const { seams, issues, output } = options;
9560
+ if (options.followHop === true) {
9561
+ const hop = readAwayHop(options.root);
9562
+ hop.notices.forEach((notice) => output.warn(notice));
9563
+ if (hop.away !== null)
9564
+ return await pickHopTarget(hop.away, issues, output);
9565
+ }
9490
9566
  const listing = listOnce2(seams.listing);
9491
9567
  const start = await startingPlace(options, listing);
9492
9568
  start.notices.forEach((notice) => output.warn(notice));
@@ -9515,6 +9591,17 @@ async function pickRoadmapIssue(options) {
9515
9591
  output.info(pickLine(pick.line));
9516
9592
  return settlePick(pick.line, { lines: descent.lines, issues, readings, seams, output });
9517
9593
  }
9594
+ async function pickHopTarget(away, issues, output) {
9595
+ output.info(hopHeaderLine(away));
9596
+ const blocked2 = await readBlockedLine(await issues(away.target), blockerStatesOf(issues));
9597
+ if (blocked2 !== null) {
9598
+ output.info(blockedPickLine(blocked2));
9599
+ output.info(hopBlockedMessage(away.target));
9600
+ return { stop: "blocked" };
9601
+ }
9602
+ output.info(hopPickLine(away.target));
9603
+ return { issue: away.target };
9604
+ }
9518
9605
  function printDescent(walked, output) {
9519
9606
  const { descent, pick } = walked;
9520
9607
  descent.passed.forEach((pass) => output.info(descentPassLine(pass)));
@@ -9565,6 +9652,7 @@ async function settleBlockedPick(options) {
9565
9652
  var PREFIX11 = "board spec source";
9566
9653
  var SOURCE_FLAGS = [SPEC_FLAG, ISSUE_FLAG, NEXT_FLAG];
9567
9654
  var SOURCE_REFUSAL_EXIT = 1;
9655
+ var ROADMAP_REFUSAL_EXIT2 = 2;
9568
9656
  function severalSourcesMessage(given) {
9569
9657
  const named = given.join(" and ");
9570
9658
  return `${named} each name a spec, and a run plans from one; drop all but one of them`;
@@ -9572,6 +9660,9 @@ function severalSourcesMessage(given) {
9572
9660
  function noSourceMessage(specsDir) {
9573
9661
  return `no spec was named: pass ${SPEC_FLAG}=<file>.md, read against the project root` + ` or under specs.dir (${specsDir}), ${ISSUE_FLAG}=<n> to plan from an issue,` + ` or ${NEXT_FLAG} to take the first undone line of the roadmap`;
9574
9662
  }
9663
+ function roadmapWithoutNextMessage() {
9664
+ return `${ROADMAP_FLAG} follows a hop to the spec ${NEXT_FLAG} would plan, and this line gives no ${NEXT_FLAG};` + ` write ${NEXT_FLAG} ${ROADMAP_FLAG}, or drop ${ROADMAP_FLAG}`;
9665
+ }
9575
9666
  function missingValueMessage(flag, wants) {
9576
9667
  return `${flag} was given no value; write ${flag}=${wants}`;
9577
9668
  }
@@ -9607,6 +9698,14 @@ function requestOf(flag, word) {
9607
9698
  }
9608
9699
  return flag === SPEC_FLAG ? { kind: "spec", spec: word.value } : { kind: "issue", issue: issueNumberOf(word.value, ISSUE_FLAG) };
9609
9700
  }
9701
+ function withRoadmap(args, request) {
9702
+ if (!readFlagWord(args, ROADMAP_FLAG).given)
9703
+ return request;
9704
+ if (request?.kind !== "next") {
9705
+ throw new CommandExit(ROADMAP_REFUSAL_EXIT2, roadmapWithoutNextMessage());
9706
+ }
9707
+ return { ...request, followHop: true };
9708
+ }
9610
9709
  function readSpecSourceFlags(args) {
9611
9710
  const words = SOURCE_FLAGS.map((flag) => ({ flag, word: readFlagWord(args, flag) }));
9612
9711
  const given = words.filter((entry) => entry.word.given);
@@ -9614,8 +9713,9 @@ function readSpecSourceFlags(args) {
9614
9713
  throw new CommandExit(SOURCE_REFUSAL_EXIT, severalSourcesMessage(given.map((entry) => entry.flag)));
9615
9714
  }
9616
9715
  const [only] = given;
9716
+ const request = only === undefined ? null : requestOf(only.flag, only.word);
9617
9717
  return Object.freeze({
9618
- request: only === undefined ? null : requestOf(only.flag, only.word),
9718
+ request: withRoadmap(args, request),
9619
9719
  refresh: args.includes(REFRESH_FLAG),
9620
9720
  dryRun: args.includes(DRY_RUN_FLAG)
9621
9721
  });
@@ -9670,7 +9770,8 @@ async function resolveSpecSource(options) {
9670
9770
  seams: requireRoadmapSeams(options.roadmap),
9671
9771
  root: repoRoot,
9672
9772
  issues,
9673
- output
9773
+ output,
9774
+ ...request.followHop === true ? { followHop: true } : {}
9674
9775
  });
9675
9776
  if ("stop" in picked)
9676
9777
  return stopped(picked.stop);
@@ -9954,7 +10055,7 @@ function withOutlineCache(outline, root) {
9954
10055
  import path2 from "path";
9955
10056
  async function coreRoster() {
9956
10057
  const [{ CORE_REGISTRY }, { describeRegistry }] = await Promise.all([
9957
- import("./index-2yd8bber.js"),
10058
+ import("./index-em1vd7kd.js"),
9958
10059
  import("./describe-vbwqxj1f.js")
9959
10060
  ]);
9960
10061
  return describeRegistry(CORE_REGISTRY, version);
@@ -10185,78 +10286,6 @@ function renderDoctorRefs(reading2) {
10185
10286
  return [headLine(reading2), ...reading2.copies.flatMap(copyLine)];
10186
10287
  }
10187
10288
 
10188
- // src/project/position.ts
10189
- import { randomUUID as randomUUID3 } from "crypto";
10190
- import { mkdirSync as mkdirSync8, readFileSync as readFileSync19, renameSync as renameSync4, rmSync as rmSync2, writeFileSync as writeFileSync9 } from "fs";
10191
- import { dirname as dirname9, join as join13 } from "path";
10192
- var POSITION_FILE = "position.json";
10193
- function positionFilePath(root) {
10194
- return join13(scopeAt(root).dir, POSITION_FILE);
10195
- }
10196
- function isRecord(value) {
10197
- return typeof value === "object" && value !== null && !Array.isArray(value);
10198
- }
10199
- function isIssueNumber(value) {
10200
- return typeof value === "number" && Number.isInteger(value) && value > 0;
10201
- }
10202
- function asPlace(value) {
10203
- if (!isRecord(value) || !isIssueNumber(value.board))
10204
- return null;
10205
- if (value.epic !== null && !isIssueNumber(value.epic))
10206
- return null;
10207
- return { board: value.board, epic: value.epic };
10208
- }
10209
- function asPosition(value) {
10210
- if (!isRecord(value))
10211
- return null;
10212
- const current = asPlace(value.current);
10213
- const home = asPlace(value.home);
10214
- const previous = value.previous === null ? null : asPlace(value.previous);
10215
- if (current === null || home === null)
10216
- return null;
10217
- if (previous === null && value.previous !== null)
10218
- return null;
10219
- return { current, previous, home };
10220
- }
10221
- function readPositionFile(root) {
10222
- const file = positionFilePath(root);
10223
- let text;
10224
- try {
10225
- text = readFileSync19(file, "utf8");
10226
- } catch (error) {
10227
- const code = error.code;
10228
- return code === "ENOENT" ? { set: false, reason: "absent", detail: `No position file at ${file}` } : { set: false, reason: "unreadable", detail: `Cannot read ${file}: ${messageOf(error)}` };
10229
- }
10230
- let parsed;
10231
- try {
10232
- parsed = JSON.parse(text);
10233
- } catch (error) {
10234
- return { set: false, reason: "invalid-json", detail: `${file} holds no JSON: ${messageOf(error)}` };
10235
- }
10236
- const position = asPosition(parsed);
10237
- return position === null ? { set: false, reason: "wrong-shape", detail: `${file} does not hold a current, previous and home place` } : { set: true, position };
10238
- }
10239
- function writePositionFile(root, position) {
10240
- const file = positionFilePath(root);
10241
- mkdirSync8(dirname9(file), { recursive: true });
10242
- const temporary = `${file}.${String(process.pid)}.${randomUUID3()}.tmp`;
10243
- const { current, previous, home } = position;
10244
- try {
10245
- writeFileSync9(temporary, `${JSON.stringify({ current, previous, home }, null, 2)}
10246
- `);
10247
- renameSync4(temporary, file);
10248
- } catch (error) {
10249
- rmSync2(temporary, { force: true });
10250
- throw error;
10251
- }
10252
- }
10253
- function rehome(position, place) {
10254
- return { current: place, previous: position.current, home: place };
10255
- }
10256
- function hop(position, place) {
10257
- return { current: place, previous: position.current, home: position.home };
10258
- }
10259
-
10260
10289
  // src/adapters/tracker/resolve.ts
10261
10290
  var PREFIX12 = "tracker chain";
10262
10291
  var FAILURE_SEPARATOR = "; ";
@@ -11636,19 +11665,20 @@ function createIssueListCommand(seams = DEFAULT_ISSUE_SEAMS) {
11636
11665
  var list_default = createIssueListCommand();
11637
11666
 
11638
11667
  // src/commands/release/status.ts
11639
- import { existsSync as existsSync17, readdirSync as readdirSync8, readFileSync as readFileSync21 } from "fs";
11640
- import { join as join14 } from "path";
11668
+ import { existsSync as existsSync17, readdirSync as readdirSync8, readFileSync as readFileSync20 } from "fs";
11669
+ import { join as join13 } from "path";
11641
11670
 
11642
11671
  // src/effort/store/changes.ts
11643
- import { randomUUID as randomUUID4 } from "crypto";
11672
+ import { randomUUID as randomUUID3 } from "crypto";
11644
11673
  import { existsSync as existsSync16 } from "fs";
11645
11674
  var INSERT_CHANGE = `
11646
11675
  INSERT INTO changes (
11647
11676
  id, session_id, plan_stub, task_line,
11648
11677
  level, area, summary,
11649
- collected_at
11678
+ collected_at,
11679
+ ${STAMPED_COLUMNS}
11650
11680
  )
11651
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
11681
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ${stampedValues("changes")})
11652
11682
  ON CONFLICT (session_id, level, ifnull(area, ''), summary) DO NOTHING
11653
11683
  `;
11654
11684
  function optionalText(value) {
@@ -11707,7 +11737,7 @@ function writeChanges(repoRoot, write, seams = {}) {
11707
11737
  rejected.push(rejection);
11708
11738
  }
11709
11739
  const collectedAt = (seams.now ?? (() => new Date))().toISOString();
11710
- const newId = seams.newId ?? randomUUID4;
11740
+ const newId = seams.newId ?? randomUUID3;
11711
11741
  const rows = stored.map((change) => rowValues2(change, write.dispatch, newId(), collectedAt));
11712
11742
  const appended2 = writeSqliteStore(path3, rows.length, 0, (db) => insertRows3(db, rows));
11713
11743
  return { path: path3, appended: appended2, skipped: stored.length - appended2, rejected };
@@ -11716,7 +11746,7 @@ var SELECT_PLAN_CHANGES = `
11716
11746
  SELECT session_id, task_line, level, area, summary, collected_at
11717
11747
  FROM changes
11718
11748
  WHERE plan_stub IS ?
11719
- ORDER BY seq
11749
+ ORDER BY ${ACROSS_STORES_ORDER}
11720
11750
  `;
11721
11751
  function readPlanChanges(repoRoot, planStub2) {
11722
11752
  const path3 = sqliteStorePath(repoRoot);
@@ -11973,7 +12003,7 @@ function remainingFrom(steps, failed2) {
11973
12003
  return at === -1 ? Object.freeze([]) : Object.freeze(steps.slice(at).filter((step) => step.skip === undefined));
11974
12004
  }
11975
12005
  // src/pr/none.ts
11976
- import { spawnSync as spawnSync2 } from "child_process";
12006
+ import { spawnSync } from "child_process";
11977
12007
 
11978
12008
  // src/pr/provider.ts
11979
12009
  var GITHUB_HOST = "github.com";
@@ -12014,7 +12044,7 @@ function requireGhProvider(reading2) {
12014
12044
  // src/pr/none.ts
12015
12045
  var COMPARE_QUERY = "?expand=1";
12016
12046
  function pushBranch(dir, branch) {
12017
- const result = spawnSync2("git", ["push", "--set-upstream", "origin", branch], {
12047
+ const result = spawnSync("git", ["push", "--set-upstream", "origin", branch], {
12018
12048
  cwd: dir,
12019
12049
  encoding: "utf8",
12020
12050
  env: { ...process.env, LC_ALL: "C" }
@@ -12048,6 +12078,8 @@ var SUMMARY_FIELDS = "author,baseRefName,headRefName,isCrossRepository,number,st
12048
12078
  var DETAIL_FIELDS = `${SUMMARY_FIELDS},body,headRefOid,labels,mergeStateStatus,mergeable`;
12049
12079
  var CHECK_FIELDS = "name,state,link";
12050
12080
  var MERGED_FIELDS = "headRefName,headRefOid,mergedAt,number";
12081
+ var FILES_FIELDS = "changedFiles,files";
12082
+ var REVIEW_FIELDS = "reviews";
12051
12083
  var WORKFLOWS_PATH = `repos/${REPO_PATH3}/actions/workflows`;
12052
12084
  var LIST_LIMIT = 30;
12053
12085
  var MERGED_LIMIT = 100;
@@ -12182,6 +12214,33 @@ function readComment2(value, command, where) {
12182
12214
  url: readString2(comment["html_url"], command, `${where}.html_url`)
12183
12215
  };
12184
12216
  }
12217
+ function readCount(value, command, where) {
12218
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
12219
+ refuse3(command, `${where} is ${describeValue(value)}, expected a whole number of zero or more`);
12220
+ }
12221
+ return value;
12222
+ }
12223
+ function readChangedFiles(value, command) {
12224
+ const pull = readMapping(value, command, "its output");
12225
+ const count = readCount(pull["changedFiles"], command, "changedFiles");
12226
+ const paths = readList(pull["files"], command, "files").map((file, index) => {
12227
+ const named = readMapping(file, command, `files[${index}]`);
12228
+ return readString2(named["path"], command, `files[${index}].path`);
12229
+ });
12230
+ if (paths.length !== count) {
12231
+ refuse3(command, `${paths.length} paths for ${count} changed files, so the list is not the whole of it`);
12232
+ }
12233
+ return paths;
12234
+ }
12235
+ function readReview(value, command, where) {
12236
+ const review = readMapping(value, command, where);
12237
+ const author = readMapping(review["author"], command, `${where}.author`);
12238
+ return {
12239
+ login: readString2(author["login"], command, `${where}.author.login`),
12240
+ state: readString2(review["state"], command, `${where}.state`),
12241
+ submittedAt: readString2(review["submittedAt"], command, `${where}.submittedAt`)
12242
+ };
12243
+ }
12185
12244
  function readWorkflowCount(stdout) {
12186
12245
  let payload;
12187
12246
  try {
@@ -12277,6 +12336,20 @@ function createGhPullRequests(options) {
12277
12336
  const rows = parseChecks(result.stdout);
12278
12337
  return { rows, verdict: verdictOf(rows) };
12279
12338
  },
12339
+ changedFiles: async (number) => {
12340
+ const target = pullNumber(number, "changedFiles");
12341
+ const command = `gh pr view ${target} --json ${FILES_FIELDS}`;
12342
+ const stdout = await succeed(["pr", "view", target, "--json", FILES_FIELDS], command);
12343
+ return readChangedFiles(parseJson2(stdout, command), command);
12344
+ },
12345
+ reviews: async (number) => {
12346
+ const target = pullNumber(number, "reviews");
12347
+ const command = `gh pr view ${target} --json ${REVIEW_FIELDS}`;
12348
+ const stdout = await succeed(["pr", "view", target, "--json", REVIEW_FIELDS], command);
12349
+ const pull = readMapping(parseJson2(stdout, command), command, "its output");
12350
+ const rows = readList(pull["reviews"], command, "reviews");
12351
+ return rows.map((row, index) => readReview(row, command, `reviews[${index}]`));
12352
+ },
12280
12353
  browse: async (number) => {
12281
12354
  const target = pullNumber(number, "browse");
12282
12355
  await succeed(["pr", "view", target, "--web"], `gh pr view ${target} --web`);
@@ -12546,7 +12619,7 @@ function resolveReleaseLevel(declared, notes) {
12546
12619
  }
12547
12620
 
12548
12621
  // src/release/version.ts
12549
- import { readFileSync as readFileSync20, writeFileSync as writeFileSync10 } from "fs";
12622
+ import { readFileSync as readFileSync19, writeFileSync as writeFileSync9 } from "fs";
12550
12623
  var RELEASE_REMOTE = "origin";
12551
12624
  var RELEASE_BASE_BRANCH = "main";
12552
12625
  var VERSION_SHAPE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$/;
@@ -12638,7 +12711,7 @@ function writeManifestVersion(path3, next) {
12638
12711
  const unwritten = { path: path3, written: false, previous: null, version: next };
12639
12712
  let text;
12640
12713
  try {
12641
- text = readFileSync20(path3, "utf8");
12714
+ text = readFileSync19(path3, "utf8");
12642
12715
  } catch (error) {
12643
12716
  return { ...unwritten, problem: `${path3} could not be read: ${messageOf(error)}` };
12644
12717
  }
@@ -12652,7 +12725,7 @@ function writeManifestVersion(path3, next) {
12652
12725
  };
12653
12726
  }
12654
12727
  try {
12655
- writeFileSync10(path3, rewritten.text);
12728
+ writeFileSync9(path3, rewritten.text);
12656
12729
  } catch (error) {
12657
12730
  return { ...unwritten, previous: rewritten.previous, problem: `${path3} could not be written: ${messageOf(error)}` };
12658
12731
  }
@@ -12786,13 +12859,13 @@ function untaggedVersions(released, tags) {
12786
12859
  }
12787
12860
  function textOf(path3) {
12788
12861
  try {
12789
- return readFileSync21(path3, "utf8");
12862
+ return readFileSync20(path3, "utf8");
12790
12863
  } catch {
12791
12864
  return null;
12792
12865
  }
12793
12866
  }
12794
12867
  function readVersionFile(root, configured) {
12795
- const resolved = join14(root, configured);
12868
+ const resolved = join13(root, configured);
12796
12869
  const text = textOf(resolved);
12797
12870
  if (text === null) {
12798
12871
  return { path: configured, version: null, problem: `the version file could not be read at ${resolved}` };
@@ -12809,7 +12882,7 @@ function readTags(git) {
12809
12882
  return { tags, latest: tags[0] ?? null, problem: null };
12810
12883
  }
12811
12884
  function readUntagged(root, configured, tags) {
12812
- const resolved = join14(root, configured);
12885
+ const resolved = join13(root, configured);
12813
12886
  const text = textOf(resolved);
12814
12887
  if (text === null) {
12815
12888
  return {
@@ -12828,7 +12901,7 @@ function readBranch(git) {
12828
12901
  return result.ok && branch !== "" && branch !== "HEAD" ? branch : null;
12829
12902
  }
12830
12903
  function planStubsUnder2(root, planDir) {
12831
- const resolved = join14(root, planDir);
12904
+ const resolved = join13(root, planDir);
12832
12905
  if (!existsSync17(resolved))
12833
12906
  return [];
12834
12907
  try {
@@ -13030,23 +13103,23 @@ function createReleaseStatusCommand(seams = DEFAULT_RELEASE_SEAMS) {
13030
13103
  var status_default = createReleaseStatusCommand();
13031
13104
 
13032
13105
  // src/commands/plan/list.ts
13033
- import { existsSync as existsSync18, readdirSync as readdirSync9, readFileSync as readFileSync22 } from "fs";
13034
- import { join as join15 } from "path";
13106
+ import { existsSync as existsSync18, readdirSync as readdirSync9, readFileSync as readFileSync21 } from "fs";
13107
+ import { join as join14 } from "path";
13035
13108
  var USAGE4 = "rafa plan list";
13036
13109
  var OPEN_HINT = "Type it bare: rafa plan list --open";
13037
13110
  function listing(dir, stub) {
13038
- const plan = join15(dir, planFileName(stub, false));
13039
- const trackerPath = join15(dir, planFileName(stub, true));
13111
+ const plan = join14(dir, planFileName(stub, false));
13112
+ const trackerPath = join14(dir, planFileName(stub, true));
13040
13113
  const tracker = isFile(trackerPath) ? trackerPath : null;
13041
- const planModel = parsePlan(readFileSync22(plan, "utf8"));
13042
- const counted2 = tracker === null ? planModel : parsePlan(readFileSync22(tracker, "utf8"));
13114
+ const planModel = parsePlan(readFileSync21(plan, "utf8"));
13115
+ const counted2 = tracker === null ? planModel : parsePlan(readFileSync21(tracker, "utf8"));
13043
13116
  return { stub, plan, tracker, tasks: countTasks(counted2.tasks), issues: planModel.issues.length };
13044
13117
  }
13045
13118
  function listPlans(plans) {
13046
13119
  const dir = plans.path;
13047
13120
  if (!existsSync18(dir))
13048
13121
  return { dir, plans: [] };
13049
- const stubs = readdirSync9(dir).filter((name) => isFile(join15(dir, name))).map((name) => stubOfPlanFile(name)).filter((stub) => stub !== null).sort((a, b) => a.localeCompare(b));
13122
+ const stubs = readdirSync9(dir).filter((name) => isFile(join14(dir, name))).map((name) => stubOfPlanFile(name)).filter((stub) => stub !== null).sort((a, b) => a.localeCompare(b));
13050
13123
  return { dir, plans: stubs.map((stub) => listing(dir, stub)) };
13051
13124
  }
13052
13125
  function isOpen(plan) {
@@ -13415,8 +13488,8 @@ function parseArgs(argv, spec = {}) {
13415
13488
 
13416
13489
  // src/commands/loop/loop-sessions.ts
13417
13490
  import { execFileSync as execFileSync2 } from "child_process";
13418
- import { readFileSync as readFileSync23 } from "fs";
13419
- import { join as join16, relative as relative3 } from "path";
13491
+ import { readFileSync as readFileSync22 } from "fs";
13492
+ import { join as join15, relative as relative3 } from "path";
13420
13493
 
13421
13494
  // src/effort/store/task-finishes.ts
13422
13495
  import { existsSync as existsSync19 } from "fs";
@@ -13560,11 +13633,11 @@ function writeSession(root, record, change) {
13560
13633
  }
13561
13634
  }
13562
13635
  function readSessionChecklist(root, record) {
13563
- const plan = join16(root, record.plan);
13636
+ const plan = join15(root, record.plan);
13564
13637
  const file = [trackerPathFor(plan), plan].find((path3) => isFile(path3));
13565
13638
  if (file === undefined)
13566
13639
  return null;
13567
- const content = readFileSync23(file, "utf8");
13640
+ const content = readFileSync22(file, "utf8");
13568
13641
  return { file, tasks: parsePlan(content).tasks, lines: content.split(`
13569
13642
  `) };
13570
13643
  }
@@ -13655,6 +13728,668 @@ function watchDryEpic() {
13655
13728
  };
13656
13729
  }
13657
13730
 
13731
+ // src/board/epic-board.ts
13732
+ function openBoards(listing2) {
13733
+ return listing2.filter((issue3) => issue3.state === "OPEN" && issue3.labels.includes(ROADMAP_LABEL)).sort((a, b) => a.number - b.number);
13734
+ }
13735
+ function boardListsEpic(number, epic, view) {
13736
+ const row = view.rows.get(number);
13737
+ if (row?.state !== "OPEN")
13738
+ return false;
13739
+ return parseRoadmapBody(row.body).some((line) => line.issue === epic);
13740
+ }
13741
+ async function boardOfEpic(epic, current, view) {
13742
+ if (boardListsEpic(current.board, epic, view))
13743
+ return current.board;
13744
+ const fallback = await view.defaultBoard();
13745
+ if (boardListsEpic(fallback, epic, view))
13746
+ return fallback;
13747
+ return openBoards(view.listing).find((row) => boardListsEpic(row.number, epic, view))?.number ?? fallback;
13748
+ }
13749
+
13750
+ // src/board/blocker-epic.ts
13751
+ var REASON_SENTENCES = Object.freeze({
13752
+ "cross-repository": "is on another repository, which the board listing does not read",
13753
+ "not-on-listing": "is not on the board listing",
13754
+ "no-epic-label": `carries no ${EPIC_LABEL_PREFIX}<slug> label, so no epic holds it`,
13755
+ "same-epic": "is in the home epic, so there is no other epic to hop to",
13756
+ "no-epic": "carries an epic label no epic issue owns",
13757
+ "closed-epic": "is in a closed epic",
13758
+ "no-open-board": "is in an epic no open board lists"
13759
+ });
13760
+ function noHopSentence(answer2) {
13761
+ const name = typeof answer2.blocker === "number" ? `#${String(answer2.blocker)}` : answer2.blocker;
13762
+ const epic = answer2.epic === null ? "" : ` (epic #${String(answer2.epic)})`;
13763
+ return `${name}${epic} ${REASON_SENTENCES[answer2.reason]}`;
13764
+ }
13765
+ function epicOfSlug(slug, listing2) {
13766
+ return listing2.filter((row) => row.type === "epic" && epicSlugsOf(row.labels)[0] === slug).sort((a, b) => a.number - b.number)[0];
13767
+ }
13768
+ function noHop(blocker, reason, epic = null) {
13769
+ return Object.freeze({ kind: "no-hop", blocker, reason, epic });
13770
+ }
13771
+ async function locateBlockerEpic(request) {
13772
+ const { blocker, home, view } = request;
13773
+ if (typeof blocker === "string")
13774
+ return noHop(blocker, "cross-repository");
13775
+ const row = view.rows.get(blocker);
13776
+ if (row === undefined)
13777
+ return noHop(blocker, "not-on-listing");
13778
+ const slugs = epicSlugsOf(row.labels);
13779
+ if (slugs.length === 0)
13780
+ return noHop(blocker, "no-epic-label");
13781
+ const epics = slugs.map((slug2) => ({ slug: slug2, epic: epicOfSlug(slug2, view.listing) }));
13782
+ if (home.epic !== null && epics.some(({ epic: epic2 }) => epic2?.number === home.epic)) {
13783
+ return noHop(blocker, "same-epic", home.epic);
13784
+ }
13785
+ const [first] = epics;
13786
+ if (first?.epic === undefined)
13787
+ return noHop(blocker, "no-epic");
13788
+ const { slug, epic } = first;
13789
+ if (epic.state === "CLOSED")
13790
+ return noHop(blocker, "closed-epic", epic.number);
13791
+ const board = await boardOfEpic(epic.number, home, view);
13792
+ if (!boardListsEpic(board, epic.number, view))
13793
+ return noHop(blocker, "no-open-board", epic.number);
13794
+ return Object.freeze({ kind: "located", blocker, epic: epic.number, slug, board });
13795
+ }
13796
+
13797
+ // src/next/hop-chain.ts
13798
+ function oneHopAllows(from, to, home) {
13799
+ return samePlace(from, home) || samePlace(to, home);
13800
+ }
13801
+ function chainText(chain) {
13802
+ const head = `#${String(chain.blocked)} \u2190 #${String(chain.blocker)}`;
13803
+ return chain.next.length === 0 ? head : `${head} \u2190 ${chain.next.map((id2) => `#${String(id2)}`).join(", ")}`;
13804
+ }
13805
+ function placeText(place) {
13806
+ return place.epic === null ? `board #${String(place.board)}` : `epic #${String(place.epic)} on board #${String(place.board)}`;
13807
+ }
13808
+ function leftText(place) {
13809
+ return place.epic === null ? `board #${String(place.board)}` : `epic #${String(place.epic)}`;
13810
+ }
13811
+ function hopMoveSentence(move) {
13812
+ return `hop from ${leftText(move.from)}: #${String(move.blocked)} blocked by #${String(move.blocker)}, in epic #${String(move.epic)}`;
13813
+ }
13814
+ function haltSentence(halt) {
13815
+ const chain = `halt: ${chainText(halt.chain)}`;
13816
+ if (halt.reason === "both-away") {
13817
+ return `${chain}: the move from ${placeText(halt.from)} to ${placeText(halt.to)} has neither end at home`;
13818
+ }
13819
+ if (halt.fault !== null)
13820
+ return `${chain}: ${halt.fault}`;
13821
+ return halt.chain.mutual ? `${chain}: #${String(halt.chain.blocked)} and #${String(halt.chain.blocker)} block each other` : `${chain}: #${String(halt.chain.blocker)} is blocked in turn`;
13822
+ }
13823
+ function takenText(taken) {
13824
+ return taken.by === "branch" ? `branch ${taken.branch} exists` : `PR #${String(taken.pullRequest)} open`;
13825
+ }
13826
+ function hopDecisionSentence(decision) {
13827
+ if (decision.kind === "hop")
13828
+ return hopMoveSentence(decision);
13829
+ if (decision.kind === "wait") {
13830
+ return `#${String(decision.blocked)} waits on #${String(decision.blocker)}, taken: ${takenText(decision.taken)}`;
13831
+ }
13832
+ if (decision.kind === "halt")
13833
+ return haltSentence(decision);
13834
+ return `#${String(decision.blocked)} stays: ${noHopSentence(decision.located)}`;
13835
+ }
13836
+ async function takenBy(blocker, taken) {
13837
+ const branch = taken.branchFor(blocker);
13838
+ if (branch !== null)
13839
+ return { by: "branch", branch };
13840
+ const pullRequest = await taken.pullRequestFor(blocker);
13841
+ return pullRequest === null ? null : { by: "pull-request", pullRequest };
13842
+ }
13843
+ function blockerLine(blocker, view) {
13844
+ const row = view.rows.get(blocker);
13845
+ if (row === undefined)
13846
+ return Promise.resolve(null);
13847
+ const states = (issue3) => Promise.resolve(view.rows.get(issue3)?.state ?? null);
13848
+ return readBlockedLine({ ...row, author: "" }, states);
13849
+ }
13850
+ function chainOf(blocked2, blocker, next) {
13851
+ return Object.freeze({ blocked: blocked2, blocker, next: Object.freeze([...next]), mutual: next.includes(blocked2) });
13852
+ }
13853
+ function halt(reason, chain, from, to, fault = null) {
13854
+ return Object.freeze({ kind: "halt", reason, chain, from, to, fault });
13855
+ }
13856
+ async function decideHop(request) {
13857
+ const { blocked: blocked2, blocker, position, view } = request;
13858
+ const from = position.current;
13859
+ const located = await locateBlockerEpic({ blocker, home: from, view });
13860
+ if (located.kind === "no-hop")
13861
+ return Object.freeze({ kind: "stay", blocked: blocked2, located });
13862
+ const to = Object.freeze({ board: located.board, epic: located.epic });
13863
+ if (!oneHopAllows(from, to, position.home)) {
13864
+ return halt("both-away", chainOf(blocked2, located.blocker, []), from, to);
13865
+ }
13866
+ const taken = await takenBy(located.blocker, request.taken);
13867
+ if (taken !== null) {
13868
+ return Object.freeze({
13869
+ kind: "wait",
13870
+ blocked: blocked2,
13871
+ blocker: located.blocker,
13872
+ epic: located.epic,
13873
+ board: located.board,
13874
+ taken: Object.freeze(taken)
13875
+ });
13876
+ }
13877
+ const line = await blockerLine(located.blocker, view);
13878
+ if (line !== null) {
13879
+ const held = line.blockers.filter((id2) => line.open.includes(id2) || line.unread.includes(id2));
13880
+ const chain = chainOf(blocked2, located.blocker, held);
13881
+ return halt("blocked-blocker", chain, from, to, line.fault);
13882
+ }
13883
+ return Object.freeze({
13884
+ kind: "hop",
13885
+ blocked: blocked2,
13886
+ blocker: located.blocker,
13887
+ epic: located.epic,
13888
+ slug: located.slug,
13889
+ board: located.board,
13890
+ from,
13891
+ to
13892
+ });
13893
+ }
13894
+
13895
+ // src/board/board-body.ts
13896
+ var OWNER_FIELD = "Owner";
13897
+ var BOARD_OWNS_FIELD = "Owns";
13898
+ var LOGIN = "[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}";
13899
+ var TEAM = "[A-Za-z0-9][A-Za-z0-9_.-]*";
13900
+ var HANDLE = new RegExp(`^@${LOGIN}(?:/${TEAM})?$`, "u");
13901
+ function isOwnerHandle(text) {
13902
+ return HANDLE.test(text);
13903
+ }
13904
+ var OWNS_SEPARATOR2 = /[\s,]+/u;
13905
+ function undressed(text) {
13906
+ return text.replaceAll("`", " ").trim();
13907
+ }
13908
+ function normaliseFolder(folder) {
13909
+ let path3 = folder;
13910
+ while (path3.startsWith("./"))
13911
+ path3 = path3.slice(2);
13912
+ while (path3.endsWith("/"))
13913
+ path3 = path3.slice(0, -1);
13914
+ return path3;
13915
+ }
13916
+ function isRepoRelative(path3) {
13917
+ if (path3 === "" || path3 === "." || path3.startsWith("/"))
13918
+ return false;
13919
+ return !path3.split("/").includes("..");
13920
+ }
13921
+ function ownedFolders2(text) {
13922
+ const named = undressed(text).split(OWNS_SEPARATOR2).filter((folder) => folder !== "");
13923
+ const owns = named.map(normaliseFolder).filter(isRepoRelative);
13924
+ const rejected = named.filter((folder) => !isRepoRelative(normaliseFolder(folder)));
13925
+ return { owns: [...new Set(owns)], rejected };
13926
+ }
13927
+ function readBoardBody(body) {
13928
+ const lines = bodyLines(body);
13929
+ const ownerLine = findField(lines, OWNER_FIELD);
13930
+ const ownsLine = findField(lines, BOARD_OWNS_FIELD);
13931
+ const handle = ownerLine === null ? null : undressed(ownerLine.text);
13932
+ const isOwned = handle !== null && isOwnerHandle(handle);
13933
+ const folders = ownsLine === null ? { owns: [], rejected: [] } : ownedFolders2(ownsLine.text);
13934
+ const problems = [
13935
+ ownerLine === null || isOwned ? null : { kind: "malformed-owner", line: ownerLine.number, text: ownerLine.text, rejected: Object.freeze([]) },
13936
+ ownsLine === null || folders.rejected.length === 0 ? null : { kind: "malformed-owns", line: ownsLine.number, text: ownsLine.text, rejected: Object.freeze(folders.rejected) }
13937
+ ];
13938
+ return Object.freeze({
13939
+ lines: parseRoadmapBody(body),
13940
+ owner: isOwned ? handle : null,
13941
+ owns: Object.freeze(folders.owns),
13942
+ problems: Object.freeze(problems.filter((problem) => problem !== null))
13943
+ });
13944
+ }
13945
+
13946
+ // src/board/codeowners.ts
13947
+ import { readFileSync as readFileSync23, statSync as statSync11 } from "fs";
13948
+ import { join as join16 } from "path";
13949
+ var CODEOWNERS_LOCATIONS = Object.freeze([
13950
+ ".github/CODEOWNERS",
13951
+ "CODEOWNERS",
13952
+ "docs/CODEOWNERS"
13953
+ ]);
13954
+ var HANDLE2 = /^@[^\s@/]+(?:\/[^\s@/]+)?$/u;
13955
+ var EMAIL = /^[^\s@]+@[^\s@]+$/u;
13956
+ var WORDS = /\s+/u;
13957
+ var REGEX_SYNTAX = /[.*+?^${}()|[\]\\/]/gu;
13958
+ function isOwner(owner) {
13959
+ return HANDLE2.test(owner) || EMAIL.test(owner);
13960
+ }
13961
+ function skipReason(pattern, owners) {
13962
+ if (pattern.startsWith("!"))
13963
+ return "negation";
13964
+ if (pattern.includes("[") || pattern.includes("]"))
13965
+ return "character-range";
13966
+ if (pattern.includes("***"))
13967
+ return "triple-asterisk";
13968
+ if (!owners.every(isOwner))
13969
+ return "malformed-owner";
13970
+ return null;
13971
+ }
13972
+ function parseCodeowners(text) {
13973
+ const rules = [];
13974
+ const skipped2 = [];
13975
+ text.split(/\r?\n/u).forEach((raw, index) => {
13976
+ const trimmed = raw.trim();
13977
+ if (trimmed === "" || trimmed.startsWith("#"))
13978
+ return;
13979
+ const words = trimmed.split(WORDS);
13980
+ const comment = words.findIndex((word) => word.startsWith("#"));
13981
+ const [pattern = "", ...owners] = comment === -1 ? words : words.slice(0, comment);
13982
+ const line = index + 1;
13983
+ const reason = skipReason(pattern, owners);
13984
+ if (reason === null) {
13985
+ rules.push(Object.freeze({ pattern, owners: Object.freeze(owners), line }));
13986
+ } else {
13987
+ skipped2.push(Object.freeze({ line, text: trimmed, reason }));
13988
+ }
13989
+ });
13990
+ return { rules: Object.freeze(rules), skipped: Object.freeze(skipped2) };
13991
+ }
13992
+ function isFile2(path3) {
13993
+ try {
13994
+ return statSync11(path3).isFile();
13995
+ } catch {
13996
+ return false;
13997
+ }
13998
+ }
13999
+ function findCodeowners(root) {
14000
+ return CODEOWNERS_LOCATIONS.find((location) => isFile2(join16(root, location))) ?? null;
14001
+ }
14002
+ function readCodeowners(root) {
14003
+ const path3 = findCodeowners(root);
14004
+ if (path3 === null)
14005
+ return null;
14006
+ let text;
14007
+ try {
14008
+ text = readFileSync23(join16(root, path3), "utf8");
14009
+ } catch (error) {
14010
+ const why = error instanceof Error ? error.message : String(error);
14011
+ throw new Error(`Cannot read ${path3}: ${why}`, { cause: error });
14012
+ }
14013
+ return Object.freeze({ path: path3, ...parseCodeowners(text) });
14014
+ }
14015
+ function segmentSource(segment) {
14016
+ let source = "";
14017
+ let isEscaped = false;
14018
+ for (const char of segment) {
14019
+ if (isEscaped) {
14020
+ source += char.replace(REGEX_SYNTAX, "\\$&");
14021
+ isEscaped = false;
14022
+ } else if (char === "\\") {
14023
+ isEscaped = true;
14024
+ } else if (char === "*") {
14025
+ source += "[^/]*";
14026
+ } else if (char === "?") {
14027
+ source += "[^/]";
14028
+ } else {
14029
+ source += char.replace(REGEX_SYNTAX, "\\$&");
14030
+ }
14031
+ }
14032
+ return source;
14033
+ }
14034
+ function patternSegments(pattern) {
14035
+ const split = pattern.split("/");
14036
+ const isSingle = split.length === 1 || split.length === 2 && split[1] === "";
14037
+ const anchored = split[0] === "" ? split.slice(1) : split;
14038
+ const floating = split[0] !== "" && isSingle && split[0] !== "**" ? ["**", ...anchored] : anchored;
14039
+ return floating.length > 1 && floating.at(-1) === "" ? [...floating.slice(0, -1), "**"] : floating;
14040
+ }
14041
+ function globstarSource(index, last) {
14042
+ if (index === 0) {
14043
+ return index === last ? ".+" : "(?:.+/)?";
14044
+ }
14045
+ return index === last ? "/.*" : "(?:/.+)?";
14046
+ }
14047
+ function patternRegex(pattern) {
14048
+ if (pattern === "/")
14049
+ return /^$/u;
14050
+ const segments = patternSegments(pattern);
14051
+ const last = segments.length - 1;
14052
+ let source = "";
14053
+ let needsSlash = false;
14054
+ segments.forEach((segment, index) => {
14055
+ if (segment === "**") {
14056
+ source += globstarSource(index, last);
14057
+ needsSlash = index !== 0;
14058
+ return;
14059
+ }
14060
+ const slash = needsSlash ? "/" : "";
14061
+ if (segment === "*") {
14062
+ source += `${slash}[^/]+`;
14063
+ } else {
14064
+ const under = index === last ? "(?:/.*)?" : "";
14065
+ source += `${slash}${segmentSource(segment)}${under}`;
14066
+ }
14067
+ needsSlash = true;
14068
+ });
14069
+ return new RegExp(`^${source}$`, "u");
14070
+ }
14071
+ function repoRelative(path3) {
14072
+ let relative4 = path3;
14073
+ while (relative4.startsWith("./") || relative4.startsWith("/")) {
14074
+ relative4 = relative4.startsWith("/") ? relative4.slice(1) : relative4.slice(2);
14075
+ }
14076
+ return relative4;
14077
+ }
14078
+ function matchesPattern(pattern, path3) {
14079
+ return pattern !== "" && patternRegex(pattern).test(repoRelative(path3));
14080
+ }
14081
+ function lastMatchingRule(file, path3) {
14082
+ const matching = file.rules.filter((rule) => matchesPattern(rule.pattern, path3));
14083
+ return matching.at(-1) ?? null;
14084
+ }
14085
+ function rulesNaming(file, owner) {
14086
+ const wanted = owner.toLowerCase();
14087
+ return Object.freeze(file.rules.filter((rule) => rule.owners.some((named) => named.toLowerCase() === wanted)));
14088
+ }
14089
+
14090
+ // src/board/board-owns.ts
14091
+ function ownedBoard(issue3) {
14092
+ const { owner, owns } = readBoardBody(issue3.body);
14093
+ return Object.freeze({ number: issue3.number, owner, owns });
14094
+ }
14095
+ function boardFolders(board, codeowners) {
14096
+ if (codeowners !== null && board.owner !== null) {
14097
+ const folders = rulesNaming(codeowners, board.owner).map((rule) => rule.pattern);
14098
+ return Object.freeze({ board: board.number, source: "codeowners", folders: Object.freeze(folders) });
14099
+ }
14100
+ if (codeowners === null && board.owns.length > 0) {
14101
+ return Object.freeze({ board: board.number, source: "owns", folders: Object.freeze([...board.owns]) });
14102
+ }
14103
+ return Object.freeze({ board: board.number, source: "repository", folders: Object.freeze([]) });
14104
+ }
14105
+ function repoPath(path3) {
14106
+ let relative4 = path3;
14107
+ while (relative4.startsWith("./") || relative4.startsWith("/")) {
14108
+ relative4 = relative4.startsWith("/") ? relative4.slice(1) : relative4.slice(2);
14109
+ }
14110
+ while (relative4.endsWith("/"))
14111
+ relative4 = relative4.slice(0, -1);
14112
+ return relative4;
14113
+ }
14114
+ function holds(folder, path3) {
14115
+ return path3 === folder || path3.startsWith(`${folder}/`);
14116
+ }
14117
+ function byNumber2(boards) {
14118
+ return [...boards].sort((a, b) => a.board - b.board);
14119
+ }
14120
+ function codeownersOwner(path3, boards, folders, codeowners) {
14121
+ const rule = lastMatchingRule(codeowners, path3);
14122
+ if (rule === null)
14123
+ return null;
14124
+ const ranked = byNumber2(folders.filter((entry) => entry.source === "codeowners"));
14125
+ const ownerOf = new Map(boards.map((board) => [board.number, board.owner?.toLowerCase() ?? null]));
14126
+ for (const named of rule.owners) {
14127
+ const wanted = named.toLowerCase();
14128
+ const board = ranked.find((entry) => ownerOf.get(entry.board) === wanted);
14129
+ if (board !== undefined)
14130
+ return Object.freeze({ board: board.board, source: "codeowners", match: rule.pattern });
14131
+ }
14132
+ return null;
14133
+ }
14134
+ function ownsOwner(path3, folders) {
14135
+ let best = null;
14136
+ for (const entry of byNumber2(folders.filter((candidate) => candidate.source === "owns"))) {
14137
+ for (const folder of entry.folders) {
14138
+ const depth = folder.split("/").length;
14139
+ if (holds(folder, path3) && (best === null || depth > best.depth)) {
14140
+ best = { board: entry.board, folder, depth };
14141
+ }
14142
+ }
14143
+ }
14144
+ return best === null ? null : Object.freeze({ board: best.board, source: "owns", match: best.folder });
14145
+ }
14146
+ function owningBoard(path3, boards, codeowners) {
14147
+ const relative4 = repoPath(path3);
14148
+ const folders = boards.map((board) => boardFolders(board, codeowners));
14149
+ const specific = codeowners === null ? ownsOwner(relative4, folders) : codeownersOwner(relative4, boards, folders, codeowners);
14150
+ if (specific !== null)
14151
+ return specific;
14152
+ const [whole] = byNumber2(folders.filter((entry) => entry.source === "repository"));
14153
+ return whole === undefined ? null : Object.freeze({ board: whole.board, source: "repository", match: null });
14154
+ }
14155
+
14156
+ // src/board/owner-resolve.ts
14157
+ var NOT_FOUND = "(HTTP 404)";
14158
+ function ownerLookupArgs(handle) {
14159
+ if (!isOwnerHandle(handle)) {
14160
+ return null;
14161
+ }
14162
+ const [account = "", slug] = handle.slice(1).split("/");
14163
+ return slug === undefined ? ["api", `users/${account}`] : ["api", `orgs/${account}/teams/${slug}`];
14164
+ }
14165
+ function failureReason(result, command) {
14166
+ const written = result.stderr.trim() || result.stdout.trim();
14167
+ return written === "" ? `${command} failed and wrote nothing` : `${command} failed: ${written}`;
14168
+ }
14169
+ async function lookUp(gh, handle) {
14170
+ const args = ownerLookupArgs(handle);
14171
+ if (args === null) {
14172
+ return { handle, state: "unknown", reason: `${JSON.stringify(handle)} is not an @login or @org/team handle, so nothing was asked` };
14173
+ }
14174
+ const result = await gh(args);
14175
+ if (result.ok) {
14176
+ return { handle, state: "resolved" };
14177
+ }
14178
+ return result.stderr.includes(NOT_FOUND) ? { handle, state: "unresolved" } : { handle, state: "unknown", reason: failureReason(result, `gh ${args.join(" ")}`) };
14179
+ }
14180
+ function createOwnerResolver(options) {
14181
+ const { gh } = options;
14182
+ const answers = new Map;
14183
+ return (handle) => {
14184
+ const key = handle.toLowerCase();
14185
+ const known = answers.get(key);
14186
+ if (known !== undefined) {
14187
+ return known.then((answer2) => ({ ...answer2, handle }));
14188
+ }
14189
+ const pending = lookUp(gh, handle);
14190
+ answers.set(key, pending);
14191
+ return pending;
14192
+ };
14193
+ }
14194
+
14195
+ // src/pr/owner-approval.ts
14196
+ var APPROVED = "APPROVED";
14197
+ var NOT_FOUND2 = "(HTTP 404)";
14198
+ var SEVERITY = Object.freeze(["unresolved", "unknown", "waiting"]);
14199
+ function messageOf5(error) {
14200
+ return error instanceof Error ? error.message : String(error);
14201
+ }
14202
+ function folded(handle) {
14203
+ return handle === null ? null : handle.toLowerCase();
14204
+ }
14205
+ function teamMembershipArgs(team, login) {
14206
+ const isTeam = isOwnerHandle(team) && team.includes("/");
14207
+ const isLogin = isOwnerHandle(`@${login}`) && !login.includes("/");
14208
+ if (!isTeam || !isLogin)
14209
+ return null;
14210
+ const [org = "", slug = ""] = team.slice(1).split("/");
14211
+ return ["api", `orgs/${org}/teams/${slug}/memberships/${login}`];
14212
+ }
14213
+ function failureReason2(result, command) {
14214
+ const written = result.stderr.trim() || result.stdout.trim();
14215
+ return written === "" ? `${command} failed and wrote nothing` : `${command} failed: ${written}`;
14216
+ }
14217
+ function readMembership(stdout, command) {
14218
+ let parsed;
14219
+ try {
14220
+ parsed = JSON.parse(stdout);
14221
+ } catch {
14222
+ return { state: "unknown", reason: `${command} wrote something that is not JSON` };
14223
+ }
14224
+ const state = typeof parsed === "object" && parsed !== null ? parsed["state"] : undefined;
14225
+ if (state === "active" || state === "pending")
14226
+ return { state };
14227
+ return { state: "unknown", reason: `${command} answered membership state ${JSON.stringify(state) ?? "undefined"}` };
14228
+ }
14229
+ function createGhTeamMembership(options) {
14230
+ const { gh } = options;
14231
+ return async (team, login) => {
14232
+ const args = teamMembershipArgs(team, login);
14233
+ if (args === null) {
14234
+ return {
14235
+ state: "unknown",
14236
+ reason: `${JSON.stringify(login)} in team ${JSON.stringify(team)} is not a login and a team handle, so nothing was asked`
14237
+ };
14238
+ }
14239
+ const command = `gh ${args.join(" ")}`;
14240
+ const result = await gh(args);
14241
+ if (result.ok)
14242
+ return readMembership(result.stdout, command);
14243
+ return result.stderr.includes(NOT_FOUND2) ? { state: "not-member" } : { state: "unknown", reason: failureReason2(result, command) };
14244
+ };
14245
+ }
14246
+ function ref3(number) {
14247
+ return `#${String(number)}`;
14248
+ }
14249
+ function ownerName(owner) {
14250
+ const boards = owner.boards.map(ref3).join(", ");
14251
+ const noun = owner.boards.length === 1 ? "board" : "boards";
14252
+ return owner.handle === null ? `${noun} ${boards}` : `owner ${owner.handle} of ${noun} ${boards}`;
14253
+ }
14254
+ function gatedOwners(paths, request) {
14255
+ const byNumber3 = new Map(request.boards.map((board) => [board.number, board]));
14256
+ const foreign = new Set;
14257
+ for (const path3 of paths) {
14258
+ const owner = owningBoard(path3, request.boards, request.codeowners);
14259
+ if (owner !== null && owner.board !== request.home)
14260
+ foreign.add(owner.board);
14261
+ }
14262
+ if (foreign.size === 0)
14263
+ return [];
14264
+ const home = byNumber3.get(request.home);
14265
+ if (home === undefined)
14266
+ return null;
14267
+ const groups = new Map;
14268
+ for (const number of [...foreign].sort((a, b) => a - b)) {
14269
+ const handle = byNumber3.get(number)?.owner ?? null;
14270
+ if (folded(handle) === folded(home.owner))
14271
+ continue;
14272
+ const key = folded(handle) ?? `board ${String(number)}`;
14273
+ const group = groups.get(key) ?? { handle, boards: [] };
14274
+ groups.set(key, { handle: group.handle, boards: [...group.boards, number] });
14275
+ }
14276
+ return [...groups.values()].map((group) => Object.freeze({ handle: group.handle, boards: Object.freeze(group.boards) }));
14277
+ }
14278
+ function blocked2(state, reason, owners) {
14279
+ return Object.freeze({ state, reason, owners: Object.freeze([...owners]) });
14280
+ }
14281
+ function mostSevere(answers) {
14282
+ const refusals = answers.filter((answer2) => answer2 !== null && answer2.state !== "approved");
14283
+ for (const state of SEVERITY) {
14284
+ const found = refusals.find((refusal3) => refusal3.state === state);
14285
+ if (found !== undefined)
14286
+ return found;
14287
+ }
14288
+ return null;
14289
+ }
14290
+ function latestReviews(reviews) {
14291
+ const ordered = [...reviews].sort((a, b) => {
14292
+ const difference = Date.parse(a.submittedAt) - Date.parse(b.submittedAt);
14293
+ return Number.isNaN(difference) ? 0 : difference;
14294
+ });
14295
+ return new Map(ordered.map((review) => [review.login.toLowerCase(), review]));
14296
+ }
14297
+ async function resolutionRefusal(owner, seams) {
14298
+ const name = ownerName(owner);
14299
+ if (owner.handle === null) {
14300
+ return { state: "unresolved", reason: `${name} names no Owner:, so nobody can approve its paths` };
14301
+ }
14302
+ const resolution = await seams.resolveOwner(owner.handle);
14303
+ if (resolution.state === "unresolved") {
14304
+ return { state: "unresolved", reason: `${name} does not resolve to an account or team GitHub knows` };
14305
+ }
14306
+ return resolution.state === "unknown" ? { state: "unknown", reason: `could not resolve ${name}: ${resolution.reason}` } : null;
14307
+ }
14308
+ async function ownerApproval(owner, latest, membership, pullRequest) {
14309
+ const name = ownerName(owner);
14310
+ if (!owner.handle.includes("/")) {
14311
+ const review = latest.get(owner.handle.slice(1).toLowerCase());
14312
+ return review?.state === APPROVED ? { state: "approved", approvedBy: review.login } : { state: "waiting", reason: `${name} has not approved ${ref3(pullRequest)}` };
14313
+ }
14314
+ const failures = [];
14315
+ for (const review of [...latest.values()].filter((latestReview) => latestReview.state === APPROVED)) {
14316
+ const answer2 = await membership(owner.handle, review.login);
14317
+ if (answer2.state === "active")
14318
+ return { state: "approved", approvedBy: review.login };
14319
+ if (answer2.state === "unknown")
14320
+ failures.push(answer2.reason);
14321
+ }
14322
+ return failures.length === 0 ? { state: "waiting", reason: `no active member of ${name} has approved ${ref3(pullRequest)}` } : { state: "unknown", reason: `could not tell whether ${name} approved ${ref3(pullRequest)}: ${failures.join("; ")}` };
14323
+ }
14324
+ function isHandled(owner) {
14325
+ return owner.handle !== null;
14326
+ }
14327
+ async function readOwnerApproval(request, seams) {
14328
+ const pull = ref3(request.pullRequest);
14329
+ let paths;
14330
+ try {
14331
+ paths = await seams.pullRequests.changedFiles(request.pullRequest);
14332
+ } catch (error) {
14333
+ return blocked2("unknown", `could not read the changed files of ${pull}: ${messageOf5(error)}`, []);
14334
+ }
14335
+ const owners = gatedOwners(paths, request);
14336
+ if (owners === null) {
14337
+ return blocked2("unknown", `home board ${ref3(request.home)} is not among the boards read, so its owner is unknown`, []);
14338
+ }
14339
+ if (owners.length === 0)
14340
+ return Object.freeze({ state: "not-gated" });
14341
+ const unresolved = mostSevere(await Promise.all(owners.map((owner) => resolutionRefusal(owner, seams))));
14342
+ if (unresolved !== null)
14343
+ return blocked2(unresolved.state, unresolved.reason, owners);
14344
+ let latest;
14345
+ try {
14346
+ latest = latestReviews(await seams.pullRequests.reviews(request.pullRequest));
14347
+ } catch (error) {
14348
+ return blocked2("unknown", `could not read the reviews of ${pull}: ${messageOf5(error)}`, owners);
14349
+ }
14350
+ const handled = owners.filter(isHandled);
14351
+ const answers = await Promise.all(handled.map((owner) => ownerApproval(owner, latest, seams.membership, request.pullRequest)));
14352
+ const refused = mostSevere(answers);
14353
+ if (refused !== null)
14354
+ return blocked2(refused.state, refused.reason, owners);
14355
+ const given = handled.flatMap((owner, index) => {
14356
+ const answer2 = answers[index];
14357
+ return answer2?.state === "approved" ? [Object.freeze({ ...owner, approvedBy: answer2.approvedBy })] : [];
14358
+ });
14359
+ return Object.freeze({ state: "approved", owners: Object.freeze(given) });
14360
+ }
14361
+
14362
+ // src/next/owner-gate.ts
14363
+ async function homeBoard(options) {
14364
+ const reading2 = readPositionFile(options.root);
14365
+ if (reading2.set)
14366
+ return reading2.position.home.board;
14367
+ const { gh, configured } = options;
14368
+ const board = await resolveDefaultBoard({
14369
+ configured,
14370
+ listBoards: createGhBoardLister({ gh }),
14371
+ search: createGhRoadmapSearch({ gh })
14372
+ });
14373
+ return board.number;
14374
+ }
14375
+ function gateBoards(listing2, home) {
14376
+ const labelled = openBoards(listing2);
14377
+ const unlabelledHome = labelled.some((board) => board.number === home) ? undefined : listing2.find((issue3) => issue3.number === home && issue3.state === "OPEN");
14378
+ const boards = unlabelledHome === undefined ? labelled : [...labelled, unlabelledHome].sort((a, b) => a.number - b.number);
14379
+ return Object.freeze(boards.map(ownedBoard));
14380
+ }
14381
+ function nextOwnerGate(options) {
14382
+ const { gh, root, pulls } = options;
14383
+ const listing2 = options.listing ?? createGhBoardListing({ gh });
14384
+ const resolveOwner = createOwnerResolver({ gh });
14385
+ const membership = createGhTeamMembership({ gh });
14386
+ return async (pullRequest) => {
14387
+ const home = await homeBoard(options);
14388
+ const boards = gateBoards(await listing2(), home);
14389
+ return readOwnerApproval({ pullRequest, home, boards, codeowners: readCodeowners(root) }, { pullRequests: pulls, resolveOwner, membership });
14390
+ };
14391
+ }
14392
+
13658
14393
  // src/next/sources.ts
13659
14394
  var PREFIX14 = "rafa next";
13660
14395
  var DEFAULT_BASE_BRANCH = "main";
@@ -13681,20 +14416,143 @@ async function cancelledNotices(listing2) {
13681
14416
  return [];
13682
14417
  }
13683
14418
  }
13684
- async function walkEpic(epic, listing2, readings) {
14419
+ var plainPick = async (lines, readings) => {
14420
+ const pick = await pickNextRoadmapLine(lines, readings);
14421
+ return { line: pick.line, passed: pick.skipped.length, waiting: [] };
14422
+ };
14423
+ async function everyBlockerTaken(blocked3, readings) {
14424
+ if (blocked3 === null || blocked3.fault !== null)
14425
+ return null;
14426
+ const held = blocked3.blockers.filter((id2) => blocked3.open.includes(id2) || blocked3.unread.includes(id2));
14427
+ if (held.length === 0)
14428
+ return null;
14429
+ let taken = [];
14430
+ for (const issue3 of held) {
14431
+ const by = await takenBy(issue3, readings);
14432
+ if (by === null)
14433
+ return null;
14434
+ taken = [...taken, Object.freeze({ issue: issue3, taken: Object.freeze(by) })];
14435
+ }
14436
+ return Object.freeze(taken);
14437
+ }
14438
+ function waitingPick(blocking) {
14439
+ const pick = async (lines, readings, before) => {
14440
+ const next = await pickNextRoadmapLine(lines, readings);
14441
+ const passed2 = before.passed + next.skipped.length;
14442
+ if (next.line === null)
14443
+ return { line: null, passed: passed2, waiting: before.waiting };
14444
+ const blockers = await everyBlockerTaken(await blocking(next.line.issue), readings);
14445
+ if (blockers === null)
14446
+ return { line: next.line, passed: passed2, waiting: before.waiting };
14447
+ const waiting = [...before.waiting, Object.freeze({ line: next.line, blockers })];
14448
+ return pick(lines.slice(lines.indexOf(next.line) + 1), readings, { line: null, passed: passed2 + 1, waiting });
14449
+ };
14450
+ return (lines, readings) => pick(lines, readings, { line: null, passed: 0, waiting: [] });
14451
+ }
14452
+ async function walkEpic(epic, listing2, readings, picker) {
13685
14453
  const rows = await listing2();
13686
14454
  const read = readEpics({ issues: rows, claims: new Set, today: new Date(0) }).epics.find((candidate) => candidate.number === epic);
13687
14455
  const row = rows.find((issue3) => issue3.number === epic);
13688
14456
  if (read === undefined || row === undefined) {
13689
14457
  throw new Error(`${PREFIX14}: epic #${String(epic)}, the current place, is not on the board listing, so its members cannot be read`);
13690
14458
  }
13691
- const pick = await pickNextRoadmapLine(epicLines(read, row).lines, readings);
14459
+ const pick = await picker(epicLines(read, row).lines, readings);
13692
14460
  return {
13693
14461
  line: pick.line,
13694
- passed: pick.skipped.length,
13695
- dry: pick.line === null ? { number: read.number, title: read.title } : null
14462
+ passed: pick.passed,
14463
+ dry: pick.line === null ? { number: read.number, title: read.title } : null,
14464
+ epic,
14465
+ waiting: pick.waiting
14466
+ };
14467
+ }
14468
+ function followHopRecord(root) {
14469
+ if (root === undefined)
14470
+ return { record: null, stale: null, position: null, problems: [] };
14471
+ const placed = readPositionFile(root);
14472
+ const position = placed.set ? placed.position : null;
14473
+ const hop = readHopRecord(root);
14474
+ if (!hop.set) {
14475
+ const problems = hop.reason === "absent" ? [] : [`${hop.detail}, so no hop is followed`];
14476
+ return { record: null, stale: null, position, problems };
14477
+ }
14478
+ return position === null || staleAgainst(hop.record, position) ? { record: null, stale: hop.record, position, problems: [] } : { record: hop.record, stale: null, position, problems: [] };
14479
+ }
14480
+ function awayTargetOf(record) {
14481
+ if (record?.state !== "away" || record.kind !== "blocker")
14482
+ return null;
14483
+ return record.target;
14484
+ }
14485
+ async function walkTarget(record, target, readings) {
14486
+ const closed = await readings.isClosed(target);
14487
+ const pullRequest = closed ? null : await readings.pullRequestFor(target);
14488
+ const line = closed || pullRequest !== null ? null : Object.freeze({ issue: target, ticked: false, why: "", lineNumber: 0 });
14489
+ return {
14490
+ walk: { line, passed: 0, dry: null, epic: record.targetEpic, waiting: [] },
14491
+ target: Object.freeze({ issue: target, closed, pullRequest })
14492
+ };
14493
+ }
14494
+ var NOTHING_TAKEN = Object.freeze({
14495
+ branchFor: () => null,
14496
+ pullRequestFor: () => Promise.resolve(null)
14497
+ });
14498
+ async function boardView(seams) {
14499
+ const listing2 = await seams.listing();
14500
+ return {
14501
+ listing: listing2,
14502
+ rows: new Map(listing2.map((row) => [row.number, row])),
14503
+ defaultBoard: () => Promise.resolve(seams.fallback)
14504
+ };
14505
+ }
14506
+ async function decideAtHome(line, places, seams) {
14507
+ const blocked3 = await seams.blocking(line.issue);
14508
+ if (blocked3 === null || blocked3.fault !== null)
14509
+ return null;
14510
+ const held = blocked3.blockers.filter((id2) => blocked3.open.includes(id2) || blocked3.unread.includes(id2));
14511
+ const view = await boardView(seams);
14512
+ let last = null;
14513
+ for (const blocker of held) {
14514
+ last = await decideHop({ blocked: line.issue, blocker, position: places, view, taken: seams.readings });
14515
+ if (last.kind !== "wait")
14516
+ return last;
14517
+ }
14518
+ return last;
14519
+ }
14520
+ async function decideAway(record, target, seams) {
14521
+ if (record.blocked === null || await seams.blocking(target) === null)
14522
+ return null;
14523
+ return decideHop({
14524
+ blocked: record.blocked,
14525
+ blocker: target,
14526
+ position: { current: record.from, home: record.home },
14527
+ view: await boardView(seams),
14528
+ taken: NOTHING_TAKEN
14529
+ });
14530
+ }
14531
+ function placesOf(walk, roadmap, position) {
14532
+ const current = { board: roadmap, epic: walk.epic };
14533
+ const away = position !== null && !samePlace(position.current, position.home);
14534
+ return {
14535
+ current,
14536
+ home: away ? position.home : current
13696
14537
  };
13697
14538
  }
14539
+ async function decisionFor(walk, followed, seams) {
14540
+ if (walk.line === null)
14541
+ return null;
14542
+ const target = awayTargetOf(followed.record);
14543
+ if (followed.record !== null && target !== null)
14544
+ return decideAway(followed.record, target, seams);
14545
+ return decideAtHome(walk.line, placesOf(walk, seams.roadmap, followed.position), seams);
14546
+ }
14547
+ async function readHopAnswer(walk, followed, seams) {
14548
+ try {
14549
+ const decision = await decisionFor(walk, followed, seams);
14550
+ const nextEpic = walk.dry === null ? null : await nextNowEpic({ after: walk.dry.number, board: seams.roadmap, listing: await seams.listing(), readings: seams.readings });
14551
+ return { decision, nextEpic, problems: [] };
14552
+ } catch (error) {
14553
+ return { decision: null, nextEpic: null, problems: [`the hop could not be read, so none is proposed: ${messageOf(error)}`] };
14554
+ }
14555
+ }
13698
14556
  function ghNextBoard(options) {
13699
14557
  const { gh, git, configured, remote, root } = options;
13700
14558
  const listed = options.listing ?? createGhBoardListing({ gh });
@@ -13704,6 +14562,10 @@ function ghNextBoard(options) {
13704
14562
  return listed();
13705
14563
  });
13706
14564
  const issues = memoiseIssues2(createGhSpecIssueReader({ gh }));
14565
+ const board = {
14566
+ blocking: async (issue3) => readBlockedLine(await issues(issue3), blockerStatesOf(issues)),
14567
+ isReady: async (issue3) => hasSpecReadyLabel((await issues(issue3)).labels)
14568
+ };
13707
14569
  return Object.freeze({
13708
14570
  next: async () => {
13709
14571
  const { number: fallback } = await resolveDefaultBoard({
@@ -13720,27 +14582,47 @@ function ghNextBoard(options) {
13720
14582
  branches,
13721
14583
  pullRequests: createGhOpenPullRequests({ gh })
13722
14584
  });
13723
- const walk = epic === null ? await walkBoard(roadmap, { issues, readings, listing: listing2 }) : await walkEpic(epic, listing2, readings);
14585
+ const followed = options.roadmap === true ? followHopRecord(root) : null;
14586
+ const picker = followed === null ? plainPick : waitingPick(board.blocking);
14587
+ const record = followed?.record ?? null;
14588
+ const target = awayTargetOf(record);
14589
+ const away = record !== null && target !== null ? await walkTarget(record, target, readings) : null;
14590
+ const walk = away?.walk ?? (epic === null ? await walkBoard(roadmap, { issues, readings, listing: listing2 }, picker) : await walkEpic(epic, listing2, readings, picker));
14591
+ const answer2 = followed === null ? null : await readHopAnswer(walk, followed, { blocking: board.blocking, readings, listing: listing2, fallback, roadmap });
13724
14592
  const cancelled = options.noticeCancelled === true && asked ? await cancelledNotices(listing2) : [];
13725
14593
  return {
13726
14594
  roadmap,
13727
14595
  line: walk.line,
13728
14596
  passed: walk.passed,
13729
- problems: [...current?.notices ?? [], ...cancelled, ...branches.problems],
13730
- ...walk.dry === null ? {} : { dryEpic: walk.dry }
14597
+ problems: [...current?.notices ?? [], ...cancelled, ...branches.problems, ...followed?.problems ?? [], ...answer2?.problems ?? []],
14598
+ ...walk.dry === null ? {} : { dryEpic: walk.dry },
14599
+ ...followed === null || answer2 === null ? {} : {
14600
+ hop: Object.freeze({
14601
+ record: followed.record,
14602
+ stale: followed.stale,
14603
+ position: followed.position,
14604
+ target: away?.target ?? null,
14605
+ waiting: walk.waiting,
14606
+ decision: answer2.decision,
14607
+ nextEpic: answer2.nextEpic
14608
+ })
14609
+ }
13731
14610
  };
13732
14611
  },
13733
- blocking: async (issue3) => readBlockedLine(await issues(issue3), blockerStatesOf(issues)),
13734
- isReady: async (issue3) => hasSpecReadyLabel((await issues(issue3)).labels)
14612
+ blocking: board.blocking,
14613
+ isReady: board.isReady
13735
14614
  });
13736
14615
  }
13737
- async function walkBoard(roadmap, seams) {
14616
+ async function walkBoard(roadmap, seams, picker) {
13738
14617
  const read = await seams.issues(roadmap);
13739
- const { descent, pick, dry } = await pickDescendedLine(parseRoadmapBody(read.body), seams);
14618
+ const descent = await descendRoadmap(parseRoadmapBody(read.body), seams);
14619
+ const pick = await picker(descent.lines, seams.readings);
13740
14620
  return {
13741
14621
  line: pick.line,
13742
- passed: descent.passed.length + pick.skipped.length,
13743
- dry: dry && descent.epic !== null ? { number: descent.epic.number, title: descent.epic.title } : null
14622
+ passed: descent.passed.length + pick.passed,
14623
+ dry: descent.epic !== null && pick.line === null ? { number: descent.epic.number, title: descent.epic.title } : null,
14624
+ epic: descent.epic?.number ?? null,
14625
+ waiting: pick.waiting
13744
14626
  };
13745
14627
  }
13746
14628
  function nextProject(context) {
@@ -13761,7 +14643,7 @@ function nextConfig(project, warn) {
13761
14643
  `));
13762
14644
  }
13763
14645
  }
13764
- function openNextSources(context, seams = DEFAULT_NEXT_SOURCE_SEAMS) {
14646
+ function openNextSources(context, seams = DEFAULT_NEXT_SOURCE_SEAMS, options = {}) {
13765
14647
  const project = nextProject(context);
13766
14648
  const config = nextConfig(project, (message) => {
13767
14649
  context.output.warn(message);
@@ -13774,13 +14656,23 @@ function openNextSources(context, seams = DEFAULT_NEXT_SOURCE_SEAMS) {
13774
14656
  const git = (seams.openGit ?? createGitRunner)(project.root);
13775
14657
  const gh = (seams.openGh ?? ((root) => createGhRunner({ cwd: root })))(project.root);
13776
14658
  const loop = resolveLoopSeams({ isAlive: seams.isAlive });
13777
- const board = () => ghNextBoard({ gh, git, configured: config.roadmapIssue, root: project.root, noticeCancelled: true });
14659
+ const roadmap = options.roadmap === true;
14660
+ const board = () => ghNextBoard({
14661
+ gh,
14662
+ git,
14663
+ configured: config.roadmapIssue,
14664
+ root: project.root,
14665
+ noticeCancelled: true,
14666
+ ...roadmap ? { roadmap } : {}
14667
+ });
14668
+ const pulls = (seams.pullRequests ?? ghPullRequestsIn)(project.root);
13778
14669
  const held = {
13779
14670
  base: config.prBase ?? DEFAULT_BASE_BRANCH,
13780
14671
  plans: plansDirAt(project.root, config.planDir),
13781
14672
  runs: () => readRecords(project.root, loop),
13782
14673
  git,
13783
- pulls: (seams.pullRequests ?? ghPullRequestsIn)(project.root)
14674
+ pulls,
14675
+ ...roadmap ? { roadmap: Object.freeze({ ownerApproval: nextOwnerGate({ gh, root: project.root, pulls, configured: config.roadmapIssue }) }) } : {}
13784
14676
  };
13785
14677
  return Object.freeze({
13786
14678
  ...held,
@@ -13880,7 +14772,7 @@ function hasRun(records, stub) {
13880
14772
  }
13881
14773
  function hasBranch(refs, stub) {
13882
14774
  const branch = `${BRANCH_PREFIX2}${stub}`;
13883
- return refs.some((ref3) => ref3 === branch || ref3.endsWith(`/${branch}`));
14775
+ return refs.some((ref4) => ref4 === branch || ref4.endsWith(`/${branch}`));
13884
14776
  }
13885
14777
  function openWorld(sources) {
13886
14778
  let problems = [];
@@ -13931,6 +14823,221 @@ function branchLabel(world) {
13931
14823
  return branch === "" ? "this checkout" : `\`${branch}\``;
13932
14824
  }
13933
14825
 
14826
+ // src/next/hop-rows.ts
14827
+ function oneLine2(text) {
14828
+ return text.replace(/\s+/g, " ").trim();
14829
+ }
14830
+ function ref4(issue3) {
14831
+ return `#${String(issue3)}`;
14832
+ }
14833
+ function placeText2(place) {
14834
+ return place.epic === null ? `board ${ref4(place.board)}` : `epic ${ref4(place.epic)} on board ${ref4(place.board)}`;
14835
+ }
14836
+ function dryHopSentence(dry, next) {
14837
+ return `hop from epic ${ref4(dry.number)}: it ran dry, next \`now\` epic ${ref4(next.number)} on board ${ref4(next.board)}`;
14838
+ }
14839
+ async function hopView(world) {
14840
+ if (!onBase(world))
14841
+ return null;
14842
+ const reading2 = await world.roadmap();
14843
+ return reading2.hop === undefined ? null : { reading: reading2, hop: reading2.hop };
14844
+ }
14845
+ function awayRecord(hop) {
14846
+ return hop.record?.state === "away" ? hop.record : null;
14847
+ }
14848
+ function homeOf(view) {
14849
+ return view.hop.position?.home ?? { board: view.reading.roadmap, epic: null };
14850
+ }
14851
+ function homeAnswer(id2, reading2, step, issue3) {
14852
+ return {
14853
+ id: id2,
14854
+ action: "home",
14855
+ reading: reading2,
14856
+ proposal: `go back home to ${placeText2(step.home)}`,
14857
+ ...issue3 === null ? {} : { issue: issue3 },
14858
+ ...step.pullRequest === null ? {} : { pullRequest: step.pullRequest },
14859
+ hop: Object.freeze(step)
14860
+ };
14861
+ }
14862
+ async function readGate(roadmap, pullRequest) {
14863
+ try {
14864
+ return await roadmap.ownerApproval(pullRequest);
14865
+ } catch (error) {
14866
+ return {
14867
+ state: "unknown",
14868
+ reason: `could not read the owner gate of ${ref4(pullRequest)}: ${oneLine2(messageOf(error))}`,
14869
+ owners: []
14870
+ };
14871
+ }
14872
+ }
14873
+ async function waitingIssue(world, pullRequest) {
14874
+ let hop;
14875
+ try {
14876
+ hop = (await world.roadmap()).hop;
14877
+ } catch {
14878
+ return null;
14879
+ }
14880
+ const record = hop?.record ?? null;
14881
+ if (record?.target === null || record === null)
14882
+ return null;
14883
+ const named = record.pullRequest === pullRequest || hop?.target?.pullRequest === pullRequest;
14884
+ return named ? record.target : null;
14885
+ }
14886
+ async function readPrOwnerReview(world) {
14887
+ const { roadmap } = world.sources;
14888
+ if (roadmap === undefined)
14889
+ return null;
14890
+ const open = await world.openPull();
14891
+ if (open === null || open.mergeable !== "mergeable")
14892
+ return null;
14893
+ if (open.verdict !== "green" && open.verdict !== "none")
14894
+ return null;
14895
+ const pullRequest = open.summary.number;
14896
+ const gate = await readGate(roadmap, pullRequest);
14897
+ if (gate.state === "not-gated" || gate.state === "approved")
14898
+ return null;
14899
+ const issue3 = await waitingIssue(world, pullRequest);
14900
+ return {
14901
+ id: "pr-owner-review",
14902
+ action: "none",
14903
+ reading: `waiting on ${ref4(issue3 ?? pullRequest)} (owner review)`,
14904
+ proposal: `leave ${ref4(pullRequest)} to its owner's review, which rafa does not merge past: ${oneLine2(gate.reason)}`,
14905
+ pullRequest,
14906
+ ...issue3 === null ? {} : { issue: issue3 }
14907
+ };
14908
+ }
14909
+ function dryEnded(view, record) {
14910
+ const { dryEpic, line } = view.reading;
14911
+ if (line !== null || dryEpic === undefined)
14912
+ return null;
14913
+ const step = { action: "home", home: record.home, closing: "halted", pullRequest: null };
14914
+ const reading2 = `epic ${ref4(dryEpic.number)}, where the dry hop went, has no line left, and a second hop would leave home behind`;
14915
+ return homeAnswer("away-ended", reading2, step, null);
14916
+ }
14917
+ async function notReadyEnded(world, record, target) {
14918
+ const picked = await world.picked();
14919
+ if (picked === null || picked.line.issue !== target || picked.ready || picked.blocked !== null)
14920
+ return null;
14921
+ const step = { action: "home", home: record.home, closing: "halted", pullRequest: null };
14922
+ const reading2 = `${ref4(target)}, the hop's target in epic ${ref4(record.targetEpic)}, carries no \`${SPEC_READY_LABEL}\`, so its spec is refused as not ready`;
14923
+ return homeAnswer("away-ended", reading2, step, target);
14924
+ }
14925
+ async function readAwayEnded(world) {
14926
+ const view = await hopView(world);
14927
+ const record = view === null ? null : awayRecord(view.hop);
14928
+ if (view === null || record === null)
14929
+ return null;
14930
+ if (record.kind === "dry")
14931
+ return dryEnded(view, record);
14932
+ const target = view.hop.target;
14933
+ if (target === null)
14934
+ return null;
14935
+ const where = `${ref4(target.issue)}, the hop's target in epic ${ref4(record.targetEpic)}`;
14936
+ if (target.closed) {
14937
+ const step = { action: "home", home: record.home, closing: "merged", pullRequest: null };
14938
+ return homeAnswer("away-ended", `${where}, is closed`, step, target.issue);
14939
+ }
14940
+ if (target.pullRequest !== null) {
14941
+ const step = { action: "home", home: record.home, closing: "waiting", pullRequest: target.pullRequest };
14942
+ return homeAnswer("away-ended", `${where}, has pull request ${ref4(target.pullRequest)} open`, step, target.issue);
14943
+ }
14944
+ return notReadyEnded(world, record, target.issue);
14945
+ }
14946
+ function decisionOf(view, kind) {
14947
+ const decision = view?.hop.decision ?? null;
14948
+ return decision?.kind === kind ? decision : null;
14949
+ }
14950
+ async function readHopHalt(world) {
14951
+ const view = await hopView(world);
14952
+ const halt2 = decisionOf(view, "halt");
14953
+ if (view === null || halt2 === null)
14954
+ return null;
14955
+ const home = view.hop.record?.home ?? view.hop.position?.home ?? halt2.from;
14956
+ const step = { action: "home", home, closing: "halted", pullRequest: null };
14957
+ return homeAnswer("hop-halt", hopDecisionSentence(halt2), step, halt2.chain.blocked);
14958
+ }
14959
+ async function readHopBlocked(world) {
14960
+ const view = await hopView(world);
14961
+ const move = decisionOf(view, "hop");
14962
+ if (view === null || move === null || awayRecord(view.hop) !== null)
14963
+ return null;
14964
+ const opening = {
14965
+ kind: "blocker",
14966
+ home: homeOf(view),
14967
+ from: move.from,
14968
+ blocked: move.blocked,
14969
+ target: move.blocker,
14970
+ targetEpic: move.epic,
14971
+ targetBoard: move.board
14972
+ };
14973
+ return {
14974
+ id: "hop-blocked",
14975
+ action: "hop",
14976
+ reading: hopDecisionSentence(move),
14977
+ proposal: `hop to ${placeText2(move.to)} and work ${ref4(move.blocker)}, keeping home`,
14978
+ issue: move.blocker,
14979
+ hop: Object.freeze({ action: "hop", opening: Object.freeze(opening) })
14980
+ };
14981
+ }
14982
+ function dryPlaces(view, dry) {
14983
+ const from = { board: view.reading.roadmap, epic: dry.number };
14984
+ const { position } = view.hop;
14985
+ return {
14986
+ from,
14987
+ home: position === null || samePlace(position.current, position.home) ? from : position.home
14988
+ };
14989
+ }
14990
+ async function readHopDry(world) {
14991
+ const view = await hopView(world);
14992
+ if (view === null || awayRecord(view.hop) !== null)
14993
+ return null;
14994
+ const { dryEpic, line } = view.reading;
14995
+ const next = view.hop.nextEpic;
14996
+ if (line !== null || dryEpic === undefined || next === null)
14997
+ return null;
14998
+ const { from, home } = dryPlaces(view, dryEpic);
14999
+ const to = { board: next.board, epic: next.number };
15000
+ if (!oneHopAllows(from, to, home))
15001
+ return null;
15002
+ const opening = {
15003
+ kind: "dry",
15004
+ home: homeOf(view),
15005
+ from,
15006
+ blocked: null,
15007
+ target: null,
15008
+ targetEpic: next.number,
15009
+ targetBoard: next.board
15010
+ };
15011
+ return {
15012
+ id: "hop-dry",
15013
+ action: "hop",
15014
+ reading: dryHopSentence(dryEpic, next),
15015
+ proposal: `hop to ${placeText2(to)}, keeping home`,
15016
+ hop: Object.freeze({ action: "hop", opening: Object.freeze(opening) })
15017
+ };
15018
+ }
15019
+ function loopEnding(record, target) {
15020
+ const home = (closing, pullRequest) => ({ action: "home", home: record.home, closing, pullRequest });
15021
+ const epic = ref4(record.targetEpic);
15022
+ if (record.kind === "dry" || record.target === null) {
15023
+ return { reading: `the loop has run in epic ${epic}, where the dry hop went`, step: home("halted", null) };
15024
+ }
15025
+ const where = `${ref4(record.target)}, the hop's target in epic ${epic},`;
15026
+ if (target?.closed === true)
15027
+ return { reading: `${where} is closed`, step: home("merged", null) };
15028
+ if (target !== null && target.pullRequest !== null) {
15029
+ return { reading: `${where} has pull request ${ref4(target.pullRequest)} open`, step: home("waiting", target.pullRequest) };
15030
+ }
15031
+ return { reading: `${where} has no pull request open after the loop`, step: home("halted", null) };
15032
+ }
15033
+ function homeAfterLoop(reading2) {
15034
+ const record = reading2.hop === undefined ? null : awayRecord(reading2.hop);
15035
+ if (record === null)
15036
+ return null;
15037
+ const ending = loopEnding(record, reading2.hop?.target ?? null);
15038
+ return homeAnswer("away-ended", ending.reading, ending.step, record.target);
15039
+ }
15040
+
13934
15041
  // src/next/state.ts
13935
15042
  var PREFIX15 = "rafa next";
13936
15043
  var MAX_NAMED_FILES = 5;
@@ -13943,7 +15050,7 @@ function nameFiles(paths) {
13943
15050
  const hidden = paths.length - shown.length;
13944
15051
  return hidden > 0 ? `${shown.join(", ")} and ${hidden} more` : shown.join(", ");
13945
15052
  }
13946
- function oneLine2(text) {
15053
+ function oneLine3(text) {
13947
15054
  return text.replace(/\s+/g, " ").trim();
13948
15055
  }
13949
15056
  function readTreeModified(world) {
@@ -13965,7 +15072,7 @@ async function readPullsUnusable(world) {
13965
15072
  return {
13966
15073
  id: "pulls-unusable",
13967
15074
  action: "none",
13968
- reading: `the \`${world.sources.pulls.kind}\` pull request provider could not be asked: ${oneLine2(messageOf(error))}`,
15075
+ reading: `the \`${world.sources.pulls.kind}\` pull request provider could not be asked: ${oneLine3(messageOf(error))}`,
13969
15076
  proposal: "run `rafa doctor` to see what the provider needs, then read the state again"
13970
15077
  };
13971
15078
  }
@@ -14182,6 +15289,14 @@ var ROWS = Object.freeze([
14182
15289
  { id: "nothing-left", read: readNothingLeft }
14183
15290
  ]);
14184
15291
  var NEXT_STATES = Object.freeze(ROWS.map((row) => row.id));
15292
+ var HOP_ROWS_AHEAD_OF = Object.freeze({
15293
+ "pr-no-checks": [{ id: "pr-owner-review", read: readPrOwnerReview, pulls: true }],
15294
+ "plan-unstarted": [{ id: "away-ended", read: readAwayEnded }],
15295
+ "issue-blocked": [{ id: "hop-halt", read: readHopHalt }, { id: "hop-blocked", read: readHopBlocked }],
15296
+ "nothing-left": [{ id: "hop-dry", read: readHopDry }]
15297
+ });
15298
+ var ROADMAP_ROWS = Object.freeze(ROWS.flatMap((row) => [...HOP_ROWS_AHEAD_OF[row.id] ?? [], row]));
15299
+ var NEXT_ROADMAP_STATES = Object.freeze(ROADMAP_ROWS.map((row) => row.id));
14185
15300
  function answer2(row, problems) {
14186
15301
  return Object.freeze({
14187
15302
  id: row.id,
@@ -14192,15 +15307,17 @@ function answer2(row, problems) {
14192
15307
  issue: row.issue ?? null,
14193
15308
  planStub: row.planStub ?? null,
14194
15309
  planPath: row.planPath ?? null,
14195
- problems: Object.freeze([...problems])
15310
+ problems: Object.freeze([...problems]),
15311
+ ...row.hop === undefined ? {} : { hop: row.hop }
14196
15312
  });
14197
15313
  }
14198
15314
  async function readNextState(sources) {
14199
15315
  const world = openWorld(sources);
15316
+ const rows = sources.roadmap === undefined ? ROWS : ROADMAP_ROWS;
14200
15317
  const modified = readTreeModified(world);
14201
15318
  if (modified !== null)
14202
15319
  return answer2(modified, world.problems());
14203
- for (const row of ROWS) {
15320
+ for (const row of rows) {
14204
15321
  if (row.pulls === true) {
14205
15322
  const unusable = await readPullsUnusable(world);
14206
15323
  if (unusable !== null)
@@ -14212,6 +15329,13 @@ async function readNextState(sources) {
14212
15329
  }
14213
15330
  throw new Error(`${PREFIX15}: no row of the table answered, and the last row answers for every reading`);
14214
15331
  }
15332
+ async function readHomeAfterLoop(sources) {
15333
+ if (sources.roadmap === undefined)
15334
+ return null;
15335
+ const reading2 = await sources.board.next();
15336
+ const found = homeAfterLoop(reading2);
15337
+ return found === null ? null : answer2(found, reading2.problems);
15338
+ }
14215
15339
 
14216
15340
  // src/next/hint.ts
14217
15341
  var HINT_FLAG = "hint";
@@ -14290,8 +15414,108 @@ async function nextStepHint(context, seams = DEFAULT_HINT_SEAMS) {
14290
15414
  }
14291
15415
  }
14292
15416
 
15417
+ // src/next/hop-action.ts
15418
+ import { rmSync as rmSync2 } from "fs";
15419
+ var NO_REHOME = "--no-rehome";
15420
+ function hopLogLine(opening) {
15421
+ const { from, blocked: blocked3, target, targetEpic, targetBoard } = opening;
15422
+ if (opening.kind === "dry" || blocked3 === null || target === null) {
15423
+ return dryHopSentence({ number: from.epic ?? from.board }, { number: targetEpic, board: targetBoard });
15424
+ }
15425
+ return hopMoveSentence({ from, blocked: blocked3, blocker: target, epic: targetEpic });
15426
+ }
15427
+ function homeLogLine(home) {
15428
+ return `back home: ${placeText2(home)}`;
15429
+ }
15430
+ function openedRecord(opening, now) {
15431
+ return {
15432
+ ...opening,
15433
+ state: "away",
15434
+ pullRequest: null,
15435
+ startedAt: now.toISOString()
15436
+ };
15437
+ }
15438
+ function writeRecord(root, record) {
15439
+ try {
15440
+ writeHopRecord(root, record);
15441
+ } catch (error) {
15442
+ throw new CommandExit(1, `\u274C Could not write ${hopFilePath(root)}: ${messageOf(error)}`);
15443
+ }
15444
+ }
15445
+ function restoreRecord(root, before) {
15446
+ if (before.set)
15447
+ writeHopRecord(root, before.record);
15448
+ else
15449
+ rmSync2(hopFilePath(root), { force: true });
15450
+ }
15451
+ async function runHop(world, step) {
15452
+ const { opening } = step;
15453
+ const before = readHopRecord(world.root);
15454
+ const record = openedRecord(opening, world.now());
15455
+ writeRecord(world.root, record);
15456
+ try {
15457
+ await world.switchTo([String(opening.targetEpic), NO_REHOME]);
15458
+ } catch (error) {
15459
+ restoreRecord(world.root, before);
15460
+ throw error;
15461
+ }
15462
+ world.info(hopLogLine(opening));
15463
+ return record;
15464
+ }
15465
+ function writePosition(root, position) {
15466
+ try {
15467
+ writePositionFile(root, position);
15468
+ } catch (error) {
15469
+ throw new CommandExit(1, `\u274C Could not write ${positionFilePath(root)}: ${messageOf(error)}`);
15470
+ }
15471
+ }
15472
+ function movedHome(world, step) {
15473
+ const reading2 = readPositionFile(world.root);
15474
+ if (reading2.set) {
15475
+ if (samePlace(reading2.position.current, reading2.position.home))
15476
+ return { now: reading2.position, written: false };
15477
+ return { now: goHome(reading2.position), written: true };
15478
+ }
15479
+ if (reading2.reason === "absent")
15480
+ return { now: null, written: false };
15481
+ world.warn(`${reading2.detail}, so it is written afresh at home`);
15482
+ return { now: positionAt(step.home), written: true };
15483
+ }
15484
+ function awayToClose(world, position) {
15485
+ const reading2 = readHopRecord(world.root);
15486
+ if (!reading2.set) {
15487
+ if (reading2.reason !== "absent")
15488
+ world.warn(`${reading2.detail}, so no hop record is closed`);
15489
+ return null;
15490
+ }
15491
+ const { record } = reading2;
15492
+ if (record.state !== "away" || position === null)
15493
+ return null;
15494
+ return staleAgainst(record, position) ? null : record;
15495
+ }
15496
+ function runHome(world, step) {
15497
+ const moved = movedHome(world, step);
15498
+ if (moved.written && moved.now !== null)
15499
+ writePosition(world.root, moved.now);
15500
+ const away = awayToClose(world, moved.now);
15501
+ const closed = away === null ? null : { ...away, state: step.closing, pullRequest: step.pullRequest ?? away.pullRequest };
15502
+ if (closed !== null)
15503
+ writeRecord(world.root, closed);
15504
+ const line = homeLogLine(moved.now?.home ?? step.home);
15505
+ world.info(line);
15506
+ return Object.freeze({
15507
+ position: moved.written ? moved.now : null,
15508
+ record: closed,
15509
+ line
15510
+ });
15511
+ }
15512
+
14293
15513
  // src/next/actions.ts
14294
15514
  var PREFIX16 = "rafa next";
15515
+ var NEXT_IN_PROCESS_ACTIONS = Object.freeze(["sync", "hop", "home"]);
15516
+ var SWITCH_COMMAND = Object.freeze({ subject: "switch", action: "switch" });
15517
+ var ROADMAP_WORD = "--roadmap";
15518
+ var ROADMAP_PASSED_ACTIONS = new Set(["plan", "start", "resume"]);
14295
15519
  function needed(state, value, what) {
14296
15520
  if (value === null) {
14297
15521
  throw new Error(`${PREFIX16}: state "${state.id}" proposes "${state.action}" and names no ${what}`);
@@ -14322,14 +15546,15 @@ var NEXT_COMMAND_ACTIONS = Object.freeze(Object.keys(ACTION_COMMANDS));
14322
15546
  function runsCommand(action) {
14323
15547
  return NEXT_COMMAND_ACTIONS.includes(action);
14324
15548
  }
14325
- function actionInvocation(state) {
15549
+ function actionInvocation(state, options = {}) {
14326
15550
  if (!runsCommand(state.action))
14327
15551
  return null;
14328
15552
  const spec = ACTION_COMMANDS[state.action];
15553
+ const passes = options.roadmap === true && ROADMAP_PASSED_ACTIONS.has(state.action);
14329
15554
  return Object.freeze({
14330
15555
  action: state.action,
14331
15556
  command: commandSpelling(spec),
14332
- argv: Object.freeze(spec.argv(state))
15557
+ argv: Object.freeze(passes ? [...spec.argv(state), ROADMAP_WORD] : spec.argv(state))
14333
15558
  });
14334
15559
  }
14335
15560
  function actionContext(caller, command, argv) {
@@ -14341,17 +15566,52 @@ function actionContext(caller, command, argv) {
14341
15566
  argv: Object.freeze([...argv])
14342
15567
  });
14343
15568
  }
14344
- async function runAction(caller, state) {
14345
- const invocation = actionInvocation(state);
15569
+ async function runRegistered(caller, name, argv, step) {
15570
+ const command = caller.registry.find(name.subject, name.action);
15571
+ if (command === undefined) {
15572
+ throw new CommandExit(1, `\u274C ${PREFIX16}: the "${step}" step runs "rafa ${commandSpelling(name)}", which is registered by nothing`);
15573
+ }
15574
+ await command.run(actionContext(caller, command, argv));
15575
+ }
15576
+ function hopWorld(caller, state) {
15577
+ if (caller.project === null) {
15578
+ throw new Error(`${PREFIX16}: the action "${state.action}" of state "${state.id}" runs inside a project, and was handed none`);
15579
+ }
15580
+ return {
15581
+ root: caller.project.root,
15582
+ switchTo: (argv) => runRegistered(caller, SWITCH_COMMAND, argv, state.action),
15583
+ info: (line) => {
15584
+ caller.output.info(line);
15585
+ },
15586
+ warn: (line) => {
15587
+ caller.output.warn(line);
15588
+ },
15589
+ now: () => new Date
15590
+ };
15591
+ }
15592
+ function noStep(state) {
15593
+ return new Error(`${PREFIX16}: state "${state.id}" proposes "${state.action}" and carries no ${state.action} step`);
15594
+ }
15595
+ async function runHopStep(caller, state) {
15596
+ const step = state.hop;
15597
+ if (step?.action !== state.action)
15598
+ throw noStep(state);
15599
+ if (step.action === "hop") {
15600
+ await runHop(hopWorld(caller, state), step);
15601
+ return;
15602
+ }
15603
+ runHome(hopWorld(caller, state), step);
15604
+ }
15605
+ async function runAction(caller, state, options = {}) {
15606
+ if (state.action === "hop" || state.action === "home") {
15607
+ await runHopStep(caller, state);
15608
+ return;
15609
+ }
15610
+ const invocation = actionInvocation(state, options);
14346
15611
  if (invocation === null) {
14347
15612
  throw new Error(`${PREFIX16}: the action "${state.action}" of state "${state.id}" runs no registered command`);
14348
15613
  }
14349
- const spec = ACTION_COMMANDS[invocation.action];
14350
- const command = caller.registry.find(spec.subject, spec.action);
14351
- if (command === undefined) {
14352
- throw new CommandExit(1, `\u274C ${PREFIX16}: the "${invocation.action}" step runs "rafa ${invocation.command}", which is registered by nothing`);
14353
- }
14354
- await command.run(actionContext(caller, command, invocation.argv));
15614
+ await runRegistered(caller, ACTION_COMMANDS[invocation.action], invocation.argv, invocation.action);
14355
15615
  }
14356
15616
 
14357
15617
  // src/next/ending.ts
@@ -14587,8 +15847,8 @@ import { fileURLToPath as fileURLToPath2 } from "url";
14587
15847
  import { homedir as homedir3 } from "os";
14588
15848
 
14589
15849
  // src/notices/notices.ts
14590
- import { mkdirSync as mkdirSync9, readFileSync as readFileSync24, writeFileSync as writeFileSync11 } from "fs";
14591
- import { dirname as dirname10, join as join17 } from "path";
15850
+ import { mkdirSync as mkdirSync8, readFileSync as readFileSync24, writeFileSync as writeFileSync10 } from "fs";
15851
+ import { dirname as dirname9, join as join17 } from "path";
14592
15852
  var NOTICE_IDS = Object.freeze(["alpha", "danger"]);
14593
15853
  var FEEDBACK_URL = "https://github.com/open-tomato/rafa/issues/new/choose";
14594
15854
  var ROADMAP_URL = "https://github.com/open-tomato/rafa/issues/31";
@@ -14620,8 +15880,8 @@ function readDismissed(home) {
14620
15880
  function writeDismissed(home, ids) {
14621
15881
  const all = NOTICE_IDS.filter((id2) => ids.includes(id2) || readDismissed(home).includes(id2));
14622
15882
  const path3 = noticesPath(home);
14623
- mkdirSync9(dirname10(path3), { recursive: true });
14624
- writeFileSync11(path3, `${JSON.stringify({ dismissed: all }, null, 2)}
15883
+ mkdirSync8(dirname9(path3), { recursive: true });
15884
+ writeFileSync10(path3, `${JSON.stringify({ dismissed: all }, null, 2)}
14625
15885
  `);
14626
15886
  }
14627
15887
  function pendingNotices(dismissed) {
@@ -14698,8 +15958,8 @@ async function requireNoticesAnswered() {
14698
15958
  // src/start/branch.ts
14699
15959
  var INDENT4 = " ";
14700
15960
  var DEFAULT_BRANCH_SEAMS = Object.freeze({});
14701
- function refExists(git, ref3) {
14702
- return git(["show-ref", "--verify", "--quiet", ref3]).ok;
15961
+ function refExists(git, ref5) {
15962
+ return git(["show-ref", "--verify", "--quiet", ref5]).ok;
14703
15963
  }
14704
15964
  function terminalReading(seams) {
14705
15965
  return seams.isTerminal ?? (() => process.stdin.isTTY === true);
@@ -14802,7 +16062,7 @@ function markBudgetExit(options) {
14802
16062
  }
14803
16063
 
14804
16064
  // src/utils/commit.ts
14805
- import { spawnSync as spawnSync3 } from "child_process";
16065
+ import { spawnSync as spawnSync2 } from "child_process";
14806
16066
  var DEFAULT_COMMIT_TYPE = "chore";
14807
16067
  var TEST_NOUN_WINDOW = 40;
14808
16068
  var FIX_VERBS = ["fix", "repair", "correct", "resolve"];
@@ -15014,7 +16274,7 @@ function failed2(step, subject, result) {
15014
16274
  }
15015
16275
  function spawnGit(cwd) {
15016
16276
  return (args) => {
15017
- const result = spawnSync3("git", args, {
16277
+ const result = spawnSync2("git", args, {
15018
16278
  cwd,
15019
16279
  encoding: "utf8",
15020
16280
  maxBuffer: MAX_GIT_BUFFER
@@ -15079,24 +16339,24 @@ function indentBlock(text) {
15079
16339
  }
15080
16340
  function holdsOf(reading2) {
15081
16341
  const { report: report2, issues } = reading2;
15082
- const holds = [];
16342
+ const holds2 = [];
15083
16343
  if (report2.status === "blocked")
15084
- holds.push("status: blocked");
16344
+ holds2.push("status: blocked");
15085
16345
  for (const blocker of report2.blockers) {
15086
- holds.push(`blocker: ${blocker.what ?? "an entry with no usable what"}`);
16346
+ holds2.push(`blocker: ${blocker.what ?? "an entry with no usable what"}`);
15087
16347
  }
15088
16348
  for (const issue3 of issues) {
15089
16349
  if (!DROPPED_BLOCKER_FIELD.test(issue3.field))
15090
16350
  continue;
15091
- holds.push(`blocker: ${issue3.field}, an entry the report parser dropped`);
16351
+ holds2.push(`blocker: ${issue3.field}, an entry the report parser dropped`);
15092
16352
  }
15093
- return holds;
16353
+ return holds2;
15094
16354
  }
15095
16355
  function leftNothingBehind(attempt2, reported) {
15096
16356
  return !reported && attempt2.outcome === "nothing-to-commit";
15097
16357
  }
15098
- function holdsAfterCommit(holds, silent) {
15099
- return silent ? [...holds, NO_REPORT_HOLD, NO_COMMIT_HOLD] : holds;
16358
+ function holdsAfterCommit(holds2, silent) {
16359
+ return silent ? [...holds2, NO_REPORT_HOLD, NO_COMMIT_HOLD] : holds2;
15100
16360
  }
15101
16361
  function commitFinishedTask(options) {
15102
16362
  const { taskInfo, trackerPath } = options;
@@ -15117,8 +16377,8 @@ function commitFinishedTask(options) {
15117
16377
  return attempt2;
15118
16378
  }
15119
16379
  const silent = leftNothingBehind(attempt2, reported);
15120
- const holds = holdsAfterCommit(reportHolds, silent);
15121
- const held = holds.length > 0;
16380
+ const holds2 = holdsAfterCommit(reportHolds, silent);
16381
+ const held = holds2.length > 0;
15122
16382
  const marked = silent && writeTrackerBlocker(trackerPath, taskInfo.lineNum, NOTHING_REPORTED_OR_COMMITTED);
15123
16383
  if (!marked) {
15124
16384
  updateTrackerLine(trackerPath, taskInfo.lineNum, held ? "blocked" : "done");
@@ -15127,7 +16387,7 @@ function commitFinishedTask(options) {
15127
16387
  out.error(silent ? `
15128
16388
  \u26D4 Task held: its session left neither a report nor a commit: ${taskText}` : `
15129
16389
  \u26D4 Task blocked by its own report: ${taskText}`);
15130
- for (const hold of holds)
16390
+ for (const hold of holds2)
15131
16391
  out.error(indentBlock(hold));
15132
16392
  } else {
15133
16393
  out.info(`\u2705 Task done: ${taskText}`);
@@ -15149,17 +16409,17 @@ function finishCleanExit(options) {
15149
16409
  const reported = reading2.present;
15150
16410
  const reportHolds = reported ? holdsOf(reading2) : [];
15151
16411
  const attempt2 = commitFinishedTask({ ...task, holds: reportHolds, reported });
15152
- const holds = holdsAfterCommit(reportHolds, leftNothingBehind(attempt2, reported));
15153
- const blocked2 = attempt2.outcome === "failed" || holds.length > 0;
16412
+ const holds2 = holdsAfterCommit(reportHolds, leftNothingBehind(attempt2, reported));
16413
+ const blocked3 = attempt2.outcome === "failed" || holds2.length > 0;
15154
16414
  return {
15155
16415
  attempt: attempt2,
15156
- holds,
15157
- outcome: blocked2 ? "blocked" : "done"
16416
+ holds: holds2,
16417
+ outcome: blocked3 ? "blocked" : "done"
15158
16418
  };
15159
16419
  }
15160
16420
 
15161
16421
  // src/start/dispatch.ts
15162
- import { randomUUID as randomUUID8 } from "crypto";
16422
+ import { randomUUID as randomUUID7 } from "crypto";
15163
16423
 
15164
16424
  // src/report/lessons.ts
15165
16425
  import { createHash as createHash3 } from "crypto";
@@ -15567,7 +16827,7 @@ function reportLessons(record, context, now) {
15567
16827
  }
15568
16828
 
15569
16829
  // src/effort/store/absences.ts
15570
- import { randomUUID as randomUUID5 } from "crypto";
16830
+ import { randomUUID as randomUUID4 } from "crypto";
15571
16831
  import { existsSync as existsSync20 } from "fs";
15572
16832
  var ABSENCE_REASONS = {
15573
16833
  "no-block": true,
@@ -15579,9 +16839,10 @@ var INSERT_ABSENCE = `
15579
16839
  INSERT INTO report_absences (
15580
16840
  id, session_id, plan_stub, task_line,
15581
16841
  reason, detail, block_body,
15582
- outcome, collected_at
16842
+ outcome, collected_at,
16843
+ ${STAMPED_COLUMNS}
15583
16844
  )
15584
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
16845
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ${stampedValues("report_absences")})
15585
16846
  ON CONFLICT (session_id) DO NOTHING
15586
16847
  `;
15587
16848
  function refusedWrite7(reason) {
@@ -15615,7 +16876,7 @@ function writeReportAbsence(repoRoot, write, seams = {}) {
15615
16876
  const { sessionId, planStub: planStub2, taskLine } = write.dispatch;
15616
16877
  const { reason, text, block } = write.absence;
15617
16878
  const collectedAt = (seams.now ?? (() => new Date))().toISOString();
15618
- const id2 = (seams.newId ?? randomUUID5)();
16879
+ const id2 = (seams.newId ?? randomUUID4)();
15619
16880
  const values = [
15620
16881
  id2,
15621
16882
  sessionId,
@@ -15632,7 +16893,7 @@ function writeReportAbsence(repoRoot, write, seams = {}) {
15632
16893
  }
15633
16894
 
15634
16895
  // src/effort/store/triage.ts
15635
- import { randomUUID as randomUUID6 } from "crypto";
16896
+ import { randomUUID as randomUUID5 } from "crypto";
15636
16897
 
15637
16898
  // src/triage/machine-fault.ts
15638
16899
  var BUILD_FAILURE = /\b(?:abort(?:ed|s)?|broke|broken|crash(?:ed|es)?|error|errors|fail(?:ed|ing|s|ure|ures)?|unusable)\b/iu;
@@ -15714,9 +16975,10 @@ var BLOCKERS = {
15714
16975
  INSERT INTO blockers (
15715
16976
  id, session_id, plan_stub, task_line,
15716
16977
  what, artifact,
15717
- outcome, collected_at
16978
+ outcome, collected_at,
16979
+ ${STAMPED_COLUMNS}
15718
16980
  )
15719
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
16981
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ${stampedValues("blockers")})
15720
16982
  ON CONFLICT (session_id, what, ifnull(artifact, '')) DO NOTHING
15721
16983
  `,
15722
16984
  values: ({ what, artifact }) => [what, artifact]
@@ -15731,9 +16993,10 @@ var OUT_OF_SCOPE_BUGS = {
15731
16993
  INSERT INTO out_of_scope_bugs (
15732
16994
  id, session_id, plan_stub, task_line,
15733
16995
  what, artifact, security, scope,
15734
- outcome, collected_at
16996
+ outcome, collected_at,
16997
+ ${STAMPED_COLUMNS}
15735
16998
  )
15736
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
16999
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ${stampedValues("out_of_scope_bugs")})
15737
17000
  ON CONFLICT (session_id, what, ifnull(artifact, ''), ifnull(security, -1)) DO NOTHING
15738
17001
  `,
15739
17002
  values: (entry) => [
@@ -15797,7 +17060,7 @@ function writeTriage(repoRoot, write, seams = {}) {
15797
17060
  const blockers = checkList(BLOCKERS, write.blockers);
15798
17061
  const bugs = checkList(OUT_OF_SCOPE_BUGS, write.outOfScopeBugs);
15799
17062
  const collectedAt = (seams.now ?? (() => new Date))().toISOString();
15800
- const newId = seams.newId ?? randomUUID6;
17063
+ const newId = seams.newId ?? randomUUID5;
15801
17064
  const blockerRows = rowsOf2(BLOCKERS, blockers.stored, write, newId, collectedAt);
15802
17065
  const bugRows = rowsOf2(OUT_OF_SCOPE_BUGS, bugs.stored, write, newId, collectedAt);
15803
17066
  const [blockersAdded, bugsAdded] = writeSqliteStore(path3, blockerRows.length + bugRows.length, [0, 0], (db) => insertBoth(db, blockerRows, bugRows));
@@ -15854,12 +17117,12 @@ function describeTaskReportRecord(record) {
15854
17117
  var SKILLS_HEADING = "## Skills for this task";
15855
17118
  var SKILLS_INSTRUCTION = "Invoke each with the Skill tool before you change anything it covers.";
15856
17119
  var LESSONS_HEADING = "## Lessons from earlier tasks";
15857
- function oneLine3(text) {
17120
+ function oneLine4(text) {
15858
17121
  return text.replace(/\s+/g, " ").trim();
15859
17122
  }
15860
17123
  function skillLine(skill, delivery = SKILL_DELIVERY) {
15861
17124
  const name = `\`${sessionSkillName(skill.name, delivery)}\``;
15862
- const description = oneLine3(skill.description);
17125
+ const description = oneLine4(skill.description);
15863
17126
  return description === "" ? `- ${name}` : `- ${name}: ${description}`;
15864
17127
  }
15865
17128
  function renderSkillsSection(skills, delivery = SKILL_DELIVERY) {
@@ -15869,7 +17132,7 @@ function renderSkillsSection(skills, delivery = SKILL_DELIVERY) {
15869
17132
  `);
15870
17133
  }
15871
17134
  function triggerClause(trigger) {
15872
- return oneLine3(trigger).replace(/^when\b\s*/i, "");
17135
+ return oneLine4(trigger).replace(/^when\b\s*/i, "");
15873
17136
  }
15874
17137
  function lessonTaskCount(lesson) {
15875
17138
  return lesson.sources === undefined ? lesson.usage_count : lesson.sources.length;
@@ -15878,7 +17141,7 @@ function lessonLine(lesson) {
15878
17141
  const count = lessonTaskCount(lesson);
15879
17142
  const tasks = count === 1 ? "1 task" : `${count} tasks`;
15880
17143
  const evidence = `confidence ${lesson.confidence.toFixed(2)}, from ${tasks}, lesson ${lesson.id}`;
15881
- return `- When ${triggerClause(lesson.trigger)}: ${oneLine3(lesson.action)} (${evidence})`;
17144
+ return `- When ${triggerClause(lesson.trigger)}: ${oneLine4(lesson.action)} (${evidence})`;
15882
17145
  }
15883
17146
  function renderLessonsSection(lessons) {
15884
17147
  if (lessons.length === 0)
@@ -15888,7 +17151,7 @@ function renderLessonsSection(lessons) {
15888
17151
  }
15889
17152
 
15890
17153
  // src/utils/progress.ts
15891
- import { existsSync as existsSync21, writeFileSync as writeFileSync12 } from "fs";
17154
+ import { existsSync as existsSync21, writeFileSync as writeFileSync11 } from "fs";
15892
17155
  import { join as join18 } from "path";
15893
17156
  var PROGRESS_FILE_NAME = "progress.txt";
15894
17157
  var PROGRESS_CAP_BYTES = 16000;
@@ -16013,7 +17276,7 @@ function readProgressFindings(repoRoot, planStub2) {
16013
17276
  function writeProgress(repoRoot, planStub2) {
16014
17277
  const render = renderProgressText(readProgressFindings(repoRoot, planStub2));
16015
17278
  const path3 = progressFilePath(repoRoot);
16016
- writeFileSync12(path3, render.text, "utf8");
17279
+ writeFileSync11(path3, render.text, "utf8");
16017
17280
  return { ...render, path: path3, storePath: sqliteStorePath(repoRoot) };
16018
17281
  }
16019
17282
 
@@ -16166,7 +17429,7 @@ var SEPARATOR = " \u2014 ";
16166
17429
  function moreLine(dropped) {
16167
17430
  return `${dropped} more: rafa skill list`;
16168
17431
  }
16169
- function oneLine4(text) {
17432
+ function oneLine5(text) {
16170
17433
  return text.replace(/\s+/g, " ").trim();
16171
17434
  }
16172
17435
  function cutLine(line) {
@@ -16183,9 +17446,9 @@ function isDisabled(row) {
16183
17446
  }
16184
17447
  function skillIndexLine(item) {
16185
17448
  const fields = fieldsOf2(item.winner);
16186
- const tags = (fields.tags ?? []).map(oneLine4).filter((tag) => tag !== "").join(", ");
16187
- const prevents = oneLine4(fields.prevents ?? "");
16188
- const about = prevents === "" ? oneLine4(fields.summary ?? "") : prevents;
17449
+ const tags = (fields.tags ?? []).map(oneLine5).filter((tag) => tag !== "").join(", ");
17450
+ const prevents = oneLine5(fields.prevents ?? "");
17451
+ const about = prevents === "" ? oneLine5(fields.summary ?? "") : prevents;
16189
17452
  const parts = [item.name, tags, about].filter((part) => part !== "");
16190
17453
  return cutLine(parts.join(SEPARATOR));
16191
17454
  }
@@ -16341,14 +17604,14 @@ function taskInputFor(taskInfo, text, declaration, planContent) {
16341
17604
  stageContext: stageContextOf(taskInfo, planContent)
16342
17605
  };
16343
17606
  }
16344
- function messageOf5(error) {
17607
+ function messageOf6(error) {
16345
17608
  return error instanceof Error ? error.message : String(error);
16346
17609
  }
16347
17610
  async function pullBundle(learning, repoRoot) {
16348
17611
  try {
16349
17612
  return await taskLearningAdapter(learning, repoRoot).pullBlessed();
16350
17613
  } catch (error) {
16351
- activeOutput().warn(` Lessons: none handed to this task: the \`${learning.kind}\` learning adapter answered no blessed set: ${messageOf5(error)}`);
17614
+ activeOutput().warn(` Lessons: none handed to this task: the \`${learning.kind}\` learning adapter answered no blessed set: ${messageOf6(error)}`);
16352
17615
  activeOutput().warn(" The task is not failed for it: it is dispatched without a lessons section.");
16353
17616
  return null;
16354
17617
  }
@@ -16368,7 +17631,7 @@ async function handOut(handout, task, resolution, repoRoot) {
16368
17631
  }
16369
17632
 
16370
17633
  // src/start/preflight.ts
16371
- import { randomUUID as randomUUID7 } from "crypto";
17634
+ import { randomUUID as randomUUID6 } from "crypto";
16372
17635
  import { existsSync as existsSync23, readFileSync as readFileSync27 } from "fs";
16373
17636
  import { homedir as homedir4 } from "os";
16374
17637
  import { basename as basename7, join as join19 } from "path";
@@ -16678,10 +17941,10 @@ function storeChecks(options, runId, report2) {
16678
17941
  return messageOf(error);
16679
17942
  }
16680
17943
  }
16681
- function refusalOf(runId, halt, storeProblem) {
16682
- if (halt === null && storeProblem === null)
17944
+ function refusalOf(runId, halt2, storeProblem) {
17945
+ if (halt2 === null && storeProblem === null)
16683
17946
  return null;
16684
- if (halt === null) {
17947
+ if (halt2 === null) {
16685
17948
  return [
16686
17949
  `\u274C The preflight checks of run ${runId} could not be stored: ${storeProblem}`,
16687
17950
  " Nothing was dispatched. Make the store writable, then run again."
@@ -16690,11 +17953,11 @@ function refusalOf(runId, halt, storeProblem) {
16690
17953
  }
16691
17954
  const where = storeProblem === null ? ` Nothing was dispatched. The checks are stored under run ${runId},
16692
17955
  and \`rafa effort report\` lists the halt.` : ` Nothing was dispatched. The checks of run ${runId} were not stored: ${storeProblem}`;
16693
- return `\u274C ${halt}
17956
+ return `\u274C ${halt2}
16694
17957
  ${where}`;
16695
17958
  }
16696
17959
  async function runStartPreflight(options) {
16697
- const runId = (options.newRunId ?? randomUUID7)();
17960
+ const runId = (options.newRunId ?? randomUUID6)();
16698
17961
  refuseUnresolvableAgents(options);
16699
17962
  refuseStoreRuleBreaks(options.planPath);
16700
17963
  const items = await loadItems(options);
@@ -16798,7 +18061,7 @@ async function dispatchTask(options) {
16798
18061
  const handed = await handOut(options.handout, task, resolution, options.repoRoot);
16799
18062
  const prompt = withStamp(buildTaskPrompt(taskText, options.promptContent, injection.text, options.knownMissing, taskInfo.blocker ?? null, { skills: renderSkillsSection(handed.skills), lessons: renderLessonsSection(handed.lessons) }));
16800
18063
  const served = serveForSession(options.serving, resolution);
16801
- const sessionId = (options.newSessionId ?? randomUUID8)();
18064
+ const sessionId = (options.newSessionId ?? randomUUID7)();
16802
18065
  const session = await run(prompt, flags, sessionId, options.settingSources, served);
16803
18066
  return {
16804
18067
  taskText,
@@ -16823,7 +18086,7 @@ function serveForSession(serving, resolution) {
16823
18086
  activeOutput().warn(` ${skipped2.message}`);
16824
18087
  return served.flags;
16825
18088
  }
16826
- function messageOf6(error) {
18089
+ function messageOf7(error) {
16827
18090
  return error instanceof Error ? error.message : String(error);
16828
18091
  }
16829
18092
  function renderProgressForDispatch(repoRoot, planStub2) {
@@ -16836,7 +18099,7 @@ function renderProgressForDispatch(repoRoot, planStub2) {
16836
18099
  return true;
16837
18100
  } catch (error) {
16838
18101
  activeOutput().error(`
16839
- \u274C progress.txt could not be rendered from the findings store: ${messageOf6(error)}`);
18102
+ \u274C progress.txt could not be rendered from the findings store: ${messageOf7(error)}`);
16840
18103
  activeOutput().error(" Nothing was dispatched. Make the store readable, then run again.");
16841
18104
  return false;
16842
18105
  }
@@ -16860,7 +18123,7 @@ async function pushTaskLessons(options, record) {
16860
18123
  const rules = result.decisions.map((decision) => decision.rule).join(", ");
16861
18124
  activeOutput().info(` Pushed ${count} lesson(s) to the \`${learning.kind}\` learning adapter: ${rules}.`);
16862
18125
  } catch (error) {
16863
- activeOutput().warn(` ${count} lesson(s) of session ${dispatch.sessionId} were not pushed to the \`${learning.kind}\` learning adapter: ${messageOf6(error)}`);
18126
+ activeOutput().warn(` ${count} lesson(s) of session ${dispatch.sessionId} were not pushed to the \`${learning.kind}\` learning adapter: ${messageOf7(error)}`);
16864
18127
  activeOutput().warn(" The task is not failed for it: its report is stored, findings and all.");
16865
18128
  }
16866
18129
  }
@@ -16894,7 +18157,7 @@ async function storeTaskReport(options) {
16894
18157
  activeOutput().warn(` ${warning}`);
16895
18158
  } catch (error) {
16896
18159
  activeOutput().error(`
16897
- \u274C The report of session ${dispatch.sessionId} was not stored: ${messageOf6(error)}`);
18160
+ \u274C The report of session ${dispatch.sessionId} was not stored: ${messageOf7(error)}`);
16898
18161
  activeOutput().error(" The session printed it above as it ran.");
16899
18162
  return false;
16900
18163
  }
@@ -17141,14 +18404,14 @@ async function handleRedChecks(io, branch, prNumber, rows) {
17141
18404
  }
17142
18405
 
17143
18406
  // src/release/prepare.ts
17144
- import { readFileSync as readFileSync28, writeFileSync as writeFileSync13 } from "fs";
18407
+ import { readFileSync as readFileSync28, writeFileSync as writeFileSync12 } from "fs";
17145
18408
 
17146
18409
  // src/release/enabled.ts
17147
- import { statSync as statSync11 } from "fs";
18410
+ import { statSync as statSync12 } from "fs";
17148
18411
  import { resolve as resolve13 } from "path";
17149
18412
  function isRegularFile(full) {
17150
18413
  try {
17151
- return statSync11(full).isFile();
18414
+ return statSync12(full).isFile();
17152
18415
  } catch {
17153
18416
  return false;
17154
18417
  }
@@ -17234,7 +18497,7 @@ function textOf2(path3) {
17234
18497
  }
17235
18498
  function writeText(path3, text) {
17236
18499
  try {
17237
- writeFileSync13(path3, text);
18500
+ writeFileSync12(path3, text);
17238
18501
  return null;
17239
18502
  } catch (error) {
17240
18503
  return `${path3} could not be written: ${messageOf(error)}`;
@@ -17340,7 +18603,7 @@ function changelogFailure(failed3, versionEdit, restored) {
17340
18603
  }
17341
18604
 
17342
18605
  // src/release/verify.ts
17343
- import { readFileSync as readFileSync29, writeFileSync as writeFileSync14 } from "fs";
18606
+ import { readFileSync as readFileSync29, writeFileSync as writeFileSync13 } from "fs";
17344
18607
  function changelogInsertionSpan(before, after) {
17345
18608
  const beforeLines = before.split(`
17346
18609
  `);
@@ -17388,7 +18651,7 @@ function changedOutside(current, before, span) {
17388
18651
  }
17389
18652
  function restoreFile(file) {
17390
18653
  try {
17391
- writeFileSync14(file.resolved, file.after);
18654
+ writeFileSync13(file.resolved, file.after);
17392
18655
  return { path: file.path, restored: true, problem: null };
17393
18656
  } catch (error) {
17394
18657
  return {
@@ -18382,7 +19645,7 @@ async function announceRiskTotal(input, seams = {}) {
18382
19645
  }
18383
19646
 
18384
19647
  // src/start/session.ts
18385
- import { randomUUID as randomUUID9 } from "crypto";
19648
+ import { randomUUID as randomUUID8 } from "crypto";
18386
19649
  import { relative as relative5 } from "path";
18387
19650
  var NOTHING_DISPATCHED = " Nothing was checked and nothing was dispatched.";
18388
19651
  function recordLabel(repoRoot, sessionId) {
@@ -18446,16 +19709,37 @@ function sessionHandle(repoRoot, sessionId) {
18446
19709
  })
18447
19710
  });
18448
19711
  }
19712
+ function awayHopOf(options) {
19713
+ if (options.roadmap !== true)
19714
+ return {};
19715
+ const hop = readAwayHopStamp(options.repoRoot);
19716
+ return hop === null ? {} : { hop };
19717
+ }
19718
+ function readAwayHopStamp(repoRoot) {
19719
+ const reading2 = readHopRecord(repoRoot);
19720
+ if (!reading2.set) {
19721
+ if (reading2.reason !== "absent") {
19722
+ activeOutput().warn(`\u26A0\uFE0F --roadmap: no hop is stamped on the session record: ${reading2.detail}`);
19723
+ }
19724
+ return null;
19725
+ }
19726
+ const { record } = reading2;
19727
+ if (record.state !== "away")
19728
+ return null;
19729
+ const placed = readPositionFile(repoRoot);
19730
+ return placed.set && !staleAgainst(record, placed.position) ? record : null;
19731
+ }
18449
19732
  function openRunSession(options) {
18450
19733
  const { repoRoot } = options;
18451
19734
  const seams = options.seams ?? {};
18452
19735
  const draft = {
18453
- sessionId: (seams.newSessionId ?? randomUUID9)(),
19736
+ sessionId: (seams.newSessionId ?? randomUUID8)(),
18454
19737
  planStub: options.planStub,
18455
19738
  plan: relative5(repoRoot, options.planPath),
18456
19739
  branch: options.branch,
18457
19740
  pid: seams.pid ?? process.pid,
18458
- startedAt: (seams.now ?? (() => new Date))().toISOString()
19741
+ startedAt: (seams.now ?? (() => new Date))().toISOString(),
19742
+ ...awayHopOf(options)
18459
19743
  };
18460
19744
  try {
18461
19745
  beginSession(repoRoot, draft, { isAlive: seams.isAlive });
@@ -18543,7 +19827,7 @@ function announcePlanIssues(planContent) {
18543
19827
  }
18544
19828
 
18545
19829
  // src/start/runtime.ts
18546
- import { existsSync as existsSync25, readdirSync as readdirSync10, realpathSync as realpathSync2, statSync as statSync12 } from "fs";
19830
+ import { existsSync as existsSync25, readdirSync as readdirSync10, realpathSync as realpathSync2, statSync as statSync13 } from "fs";
18547
19831
  import { homedir as homedir5 } from "os";
18548
19832
  import { isAbsolute as isAbsolute4, join as join21, relative as relative6, resolve as resolve14, sep } from "path";
18549
19833
  var RUNTIME_FLAG = "--runtime";
@@ -18583,7 +19867,7 @@ function resolveRuntime(value, places) {
18583
19867
  }
18584
19868
  function entryOf2(value, named, isVersion, home) {
18585
19869
  if (isVersion) {
18586
- if (isFile2(named))
19870
+ if (isFile3(named))
18587
19871
  return named;
18588
19872
  const root = join21(home, RUNTIME_SUBDIR);
18589
19873
  const installed = installedVersions(root);
@@ -18592,10 +19876,10 @@ function entryOf2(value, named, isVersion, home) {
18592
19876
  installed.length === 0 ? ` ${root} holds no version.` : ` ${root} holds ${installed.join(", ")}.`
18593
19877
  ]);
18594
19878
  }
18595
- if (isFile2(named))
19879
+ if (isFile3(named))
18596
19880
  return named;
18597
19881
  const inside = join21(named, RUNTIME_ENTRY);
18598
- if (isDirectory(named) && isFile2(inside))
19882
+ if (isDirectory(named) && isFile3(inside))
18599
19883
  return inside;
18600
19884
  throw refusal3([
18601
19885
  `\u274C Refusing to start: \`--runtime=${value}\` names no runtime: ${named} is neither a file nor a directory holding \`${RUNTIME_ENTRY}\`.`,
@@ -18622,21 +19906,21 @@ function isInside2(path3, dir) {
18622
19906
  }
18623
19907
  function installedVersions(root) {
18624
19908
  try {
18625
- return readdirSync10(root).filter((name) => isFile2(join21(root, name, RUNTIME_ENTRY))).sort((a, b) => a.localeCompare(b));
19909
+ return readdirSync10(root).filter((name) => isFile3(join21(root, name, RUNTIME_ENTRY))).sort((a, b) => a.localeCompare(b));
18626
19910
  } catch {
18627
19911
  return [];
18628
19912
  }
18629
19913
  }
18630
- function isFile2(path3) {
19914
+ function isFile3(path3) {
18631
19915
  try {
18632
- return statSync12(path3).isFile();
19916
+ return statSync13(path3).isFile();
18633
19917
  } catch {
18634
19918
  return false;
18635
19919
  }
18636
19920
  }
18637
19921
  function isDirectory(path3) {
18638
19922
  try {
18639
- return statSync12(path3).isDirectory();
19923
+ return statSync13(path3).isDirectory();
18640
19924
  } catch {
18641
19925
  return false;
18642
19926
  }
@@ -18756,7 +20040,7 @@ function eventOf(line) {
18756
20040
  import { basename as basename8, join as join22 } from "path";
18757
20041
 
18758
20042
  // src/effort/store/tracker-refs.ts
18759
- import { randomUUID as randomUUID10 } from "crypto";
20043
+ import { randomUUID as randomUUID9 } from "crypto";
18760
20044
  import { existsSync as existsSync26 } from "fs";
18761
20045
  var SELECT_HELD2 = `
18762
20046
  SELECT seq, tracker_ref FROM findings
@@ -18766,9 +20050,10 @@ var INSERT_REF = `
18766
20050
  INSERT INTO findings (
18767
20051
  id, session_id, plan_stub, task_line,
18768
20052
  artifact,
18769
- outcome, tracker_ref, collected_at
20053
+ outcome, tracker_ref, collected_at,
20054
+ ${STAMPED_COLUMNS}
18770
20055
  )
18771
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
20056
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ${stampedValues("findings")})
18772
20057
  `;
18773
20058
  var ATTACH_REF = "UPDATE findings SET tracker_ref = ? WHERE seq = ? AND tracker_ref IS NULL";
18774
20059
  var SELECT_OLDEST = `
@@ -18823,17 +20108,17 @@ function storedRefOf(value) {
18823
20108
  if (problem !== null)
18824
20109
  return `whose ${field4} ${problem}`;
18825
20110
  }
18826
- const ref3 = {
20111
+ const ref5 = {
18827
20112
  opt: fields.opt,
18828
20113
  kind: fields.kind,
18829
20114
  externalId: fields.externalId,
18830
20115
  url: fields.url
18831
20116
  };
18832
20117
  if (typeof fields.module === "string")
18833
- ref3.module = fields.module;
20118
+ ref5.module = fields.module;
18834
20119
  if (typeof fields.repo === "string")
18835
- ref3.repo = fields.repo;
18836
- return { ref: ref3, json: JSON.stringify(ref3) };
20120
+ ref5.repo = fields.repo;
20121
+ return { ref: ref5, json: JSON.stringify(ref5) };
18837
20122
  }
18838
20123
  function heldRefOf(path3, seq, text) {
18839
20124
  let parsed;
@@ -18869,7 +20154,7 @@ function writeTrackerRef(repoRoot, write, seams = {}) {
18869
20154
  const path3 = sqliteStorePath(repoRoot);
18870
20155
  const { sessionId, planStub: planStub2, taskLine } = write.dispatch;
18871
20156
  const collectedAt = (seams.now ?? (() => new Date))().toISOString();
18872
- const newId = seams.newId ?? randomUUID10;
20157
+ const newId = seams.newId ?? randomUUID9;
18873
20158
  const insertValues = () => [
18874
20159
  newId(),
18875
20160
  sessionId,
@@ -18901,28 +20186,28 @@ var ARTIFACT_REF_KINDS = new Set(["path", "symbol"]);
18901
20186
  var UNREAD_STAMP = "unread";
18902
20187
  var REFS_INTRO = "The paths and symbols the artifact names, each with its target's" + " fingerprint when this bug was filed.";
18903
20188
  function artifactRefs(artifact) {
18904
- return extractRefs(artifact).filter((ref3) => ARTIFACT_REF_KINDS.has(ref3.kind));
20189
+ return extractRefs(artifact).filter((ref5) => ARTIFACT_REF_KINDS.has(ref5.kind));
18905
20190
  }
18906
20191
  function stampWord(live) {
18907
20192
  return live.kind === "unreadable" ? UNREAD_STAMP : fingerprintText(live);
18908
20193
  }
18909
20194
  async function stampArtifactRefs(artifact, verify) {
18910
20195
  const stamps = [];
18911
- for (const ref3 of artifactRefs(artifact)) {
20196
+ for (const ref5 of artifactRefs(artifact)) {
18912
20197
  let stamp;
18913
20198
  try {
18914
- stamp = stampWord(await verify({ kind: ref3.kind, text: ref3.text }));
20199
+ stamp = stampWord(await verify({ kind: ref5.kind, text: ref5.text }));
18915
20200
  } catch {
18916
20201
  stamp = UNREAD_STAMP;
18917
20202
  }
18918
- stamps.push(Object.freeze({ kind: ref3.kind, text: ref3.text, stamp }));
20203
+ stamps.push(Object.freeze({ kind: ref5.kind, text: ref5.text, stamp }));
18919
20204
  }
18920
20205
  return Object.freeze(stamps);
18921
20206
  }
18922
20207
  function refsSection(stamps) {
18923
20208
  if (stamps.length === 0)
18924
20209
  return null;
18925
- const lines = stamps.map((ref3) => `- ${ref3.kind} \`${ref3.text}\`: \`${ref3.stamp}\``);
20210
+ const lines = stamps.map((ref5) => `- ${ref5.kind} \`${ref5.text}\`: \`${ref5.stamp}\``);
18926
20211
  return [`## ${REFS_HEADING}`, REFS_INTRO, lines.join(`
18927
20212
  `)].join(`
18928
20213
 
@@ -18945,14 +20230,14 @@ async function noIssues() {
18945
20230
  }
18946
20231
  function createArtifactRefsVerifier(repoRoot, env) {
18947
20232
  let made = null;
18948
- return async (ref3) => {
20233
+ return async (ref5) => {
18949
20234
  made ??= createRefVerifier({
18950
20235
  issues: noIssues,
18951
20236
  git: createGitRunner(repoRoot),
18952
20237
  outline: tsSymbolsOutliner({ cwd: repoRoot, env }),
18953
20238
  roster: EMPTY_ROSTER
18954
20239
  });
18955
- return made(ref3);
20240
+ return made(ref5);
18956
20241
  };
18957
20242
  }
18958
20243
 
@@ -19027,11 +20312,11 @@ function triageBlockers(options) {
19027
20312
  function artifactOf(bug) {
19028
20313
  return hasText2(bug.artifact) && textProblem(bug.artifact) === null ? bug.artifact : null;
19029
20314
  }
19030
- function oneLine5(text) {
20315
+ function oneLine6(text) {
19031
20316
  return text.replace(/\s+/g, " ").trim();
19032
20317
  }
19033
20318
  function bugKeyOf(trackerPath, artifact) {
19034
- return `${basename8(trackerPath)}${KEY_SEPARATOR}${oneLine5(artifact)}`;
20319
+ return `${basename8(trackerPath)}${KEY_SEPARATOR}${oneLine6(artifact)}`;
19035
20320
  }
19036
20321
  function fenced(value) {
19037
20322
  const runs = Array.from(value.matchAll(/`+/g), (run) => run[0].length);
@@ -19043,7 +20328,7 @@ function fenced(value) {
19043
20328
  ${body}${fence}`;
19044
20329
  }
19045
20330
  function issueTitle2(redactedWhat) {
19046
- const line = oneLine5(redactedWhat);
20331
+ const line = oneLine6(redactedWhat);
19047
20332
  const points = Array.from(line);
19048
20333
  if (points.length <= TITLE_MAX_LENGTH)
19049
20334
  return line;
@@ -19119,23 +20404,23 @@ function resultOf(run, action, fields = {}) {
19119
20404
  ...fields
19120
20405
  };
19121
20406
  }
19122
- async function settle(run, action, ref3, foundBy) {
20407
+ async function settle(run, action, ref5, foundBy) {
19123
20408
  const { store } = run.route;
19124
20409
  const { key } = run.filing;
19125
20410
  if (store === null || key === null)
19126
- return resultOf(run, action, { ref: ref3, foundBy });
19127
- const stored = await step(run, "storing the reference", () => store(key, ref3));
19128
- return stored.ok ? resultOf(run, action, { ref: ref3, foundBy, stored: stored.value }) : resultOf(run, action, { ref: ref3, foundBy, problem: stored.problem });
20411
+ return resultOf(run, action, { ref: ref5, foundBy });
20412
+ const stored = await step(run, "storing the reference", () => store(key, ref5));
20413
+ return stored.ok ? resultOf(run, action, { ref: ref5, foundBy, stored: stored.value }) : resultOf(run, action, { ref: ref5, foundBy, problem: stored.problem });
19129
20414
  }
19130
20415
  async function fileNew(run) {
19131
20416
  const created = await step(run, `the ${run.route.channel} tracker create`, () => run.route.tracker.create(run.filing.draft));
19132
20417
  return created.ok ? settle(run, "filed", created.value, null) : resultOf(run, "failed", { problem: created.problem });
19133
20418
  }
19134
- async function commentOn(run, ref3, foundBy) {
19135
- const commented = await step(run, `the ${run.route.channel} tracker comment`, () => run.route.tracker.comment(ref3, run.filing.comment));
20419
+ async function commentOn(run, ref5, foundBy) {
20420
+ const commented = await step(run, `the ${run.route.channel} tracker comment`, () => run.route.tracker.comment(ref5, run.filing.comment));
19136
20421
  if (!commented.ok)
19137
- return resultOf(run, "failed", { ref: ref3, foundBy, problem: commented.problem });
19138
- return foundBy === "store" ? resultOf(run, "commented", { ref: ref3, foundBy }) : settle(run, "commented", ref3, foundBy);
20422
+ return resultOf(run, "failed", { ref: ref5, foundBy, problem: commented.problem });
20423
+ return foundBy === "store" ? resultOf(run, "commented", { ref: ref5, foundBy }) : settle(run, "commented", ref5, foundBy);
19139
20424
  }
19140
20425
  async function triageBug(run) {
19141
20426
  const { route, filing } = run;
@@ -19183,11 +20468,11 @@ async function triageReport(options) {
19183
20468
  channel: "public",
19184
20469
  tracker: options.tracker,
19185
20470
  readStored: (key) => readTrackerRef(repoRoot, key),
19186
- store: (key, ref3) => writeTrackerRef(repoRoot, {
20471
+ store: (key, ref5) => writeTrackerRef(repoRoot, {
19187
20472
  dispatch: options.dispatch,
19188
20473
  outcome: options.outcome,
19189
20474
  artifact: key,
19190
- ref: ref3
20475
+ ref: ref5
19191
20476
  }, options.seams).action
19192
20477
  },
19193
20478
  private: { channel: "private", tracker: privateTracker, readStored: null, store: null }
@@ -19238,8 +20523,8 @@ function filesPublicly(bug) {
19238
20523
  function hasTriage(report2) {
19239
20524
  return blockerTextOf(report2.blockers) !== null || report2.outOfScopeBugs.length > 0;
19240
20525
  }
19241
- function issueName2(ref3) {
19242
- return ref3.url ?? `${ref3.kind} issue ${ref3.externalId}`;
20526
+ function issueName2(ref5) {
20527
+ return ref5.url ?? `${ref5.kind} issue ${ref5.externalId}`;
19243
20528
  }
19244
20529
  function bugLines(bug) {
19245
20530
  const name = `out_of_scope_bugs[${bug.index}]`;
@@ -19431,7 +20716,7 @@ function changedSince(git, head) {
19431
20716
  const paths = result.stdout.split("\x00").filter((path3) => path3 !== "");
19432
20717
  return { ok: true, head, paths: new Set(paths) };
19433
20718
  }
19434
- function repoPath(path3, repoRoot) {
20719
+ function repoPath2(path3, repoRoot) {
19435
20720
  const inside = isAbsolute5(path3) ? relative7(repoRoot, path3) : path3;
19436
20721
  const normal = posix2.normalize(inside.replaceAll("\\", "/"));
19437
20722
  if (normal === "." || normal === ".." || normal.startsWith("../") || isAbsolute5(normal))
@@ -19441,7 +20726,7 @@ function repoPath(path3, repoRoot) {
19441
20726
  function promotedOutcome(lesson, named, changed, repoRoot) {
19442
20727
  if (!changed.ok)
19443
20728
  return { kind: "unchecked", id: lesson.id, path: named, reason: changed.reason };
19444
- const path3 = repoPath(named, repoRoot);
20729
+ const path3 = repoPath2(named, repoRoot);
19445
20730
  return path3 !== null && changed.paths.has(path3) ? { lesson, path: path3 } : { kind: "unchanged", id: lesson.id, path: named };
19446
20731
  }
19447
20732
  function checkPromoted(lessons, reading2, changed, repoRoot) {
@@ -19584,15 +20869,15 @@ function buildWrapUpPrompt(branch, planContent, openPullRequest = null, release
19584
20869
  ].join(`
19585
20870
  `);
19586
20871
  }
19587
- function oneLine6(text) {
20872
+ function oneLine7(text) {
19588
20873
  return text.trim().replace(/\s+/g, " ");
19589
20874
  }
19590
20875
  function lessonLines(lesson) {
19591
- const artifact = lesson.artifact === undefined || oneLine6(lesson.artifact) === "" ? "none" : oneLine6(lesson.artifact);
20876
+ const artifact = lesson.artifact === undefined || oneLine7(lesson.artifact) === "" ? "none" : oneLine7(lesson.artifact);
19592
20877
  return [
19593
20878
  `- \`${lesson.id}\``,
19594
- ` - trigger: ${oneLine6(lesson.trigger)}`,
19595
- ` - action: ${oneLine6(lesson.action)}`,
20879
+ ` - trigger: ${oneLine7(lesson.trigger)}`,
20880
+ ` - action: ${oneLine7(lesson.action)}`,
19596
20881
  ` - artifact: ${artifact}`
19597
20882
  ];
19598
20883
  }
@@ -19661,7 +20946,7 @@ async function openPullRequestNumber(branch) {
19661
20946
  return null;
19662
20947
  }
19663
20948
  }
19664
- function messageOf7(error) {
20949
+ function messageOf8(error) {
19665
20950
  return error instanceof Error ? error.message : String(error);
19666
20951
  }
19667
20952
  function wrapUpAdapter(learning) {
@@ -19682,7 +20967,7 @@ async function lessonsToPromote(learning) {
19682
20967
  minConfidence: learning.promoteMinConfidence
19683
20968
  });
19684
20969
  } catch (error) {
19685
- activeOutput().warn(` The wrap-up lists no lesson to promote: the \`${learning.kind}\` learning adapter answered no blessed set: ${messageOf7(error)}`);
20970
+ activeOutput().warn(` The wrap-up lists no lesson to promote: the \`${learning.kind}\` learning adapter answered no blessed set: ${messageOf8(error)}`);
19686
20971
  return [];
19687
20972
  }
19688
20973
  }
@@ -19753,6 +21038,7 @@ var interrupted = false;
19753
21038
  var DEFAULT_BRANCHES = ["main", "master"];
19754
21039
  var ANY_BRANCH_FLAG = "--any-branch";
19755
21040
  var CREATE_BRANCH_FLAG = "--create-branch";
21041
+ var ROADMAP_FLAG2 = "--roadmap";
19756
21042
  var UNNAMED_PLAN = "this-plan";
19757
21043
  async function resolveRunBranch(request, seams = DEFAULT_BRANCH_SEAMS) {
19758
21044
  const { args, base } = request;
@@ -19854,7 +21140,7 @@ async function start(args, repoRoot) {
19854
21140
  });
19855
21141
  await requireNoticesAnswered();
19856
21142
  setActivePlanStub(planStub2);
19857
- const session = openRunSession({ repoRoot, planPath, planStub: planStub2, branch });
21143
+ const session = openRunSession({ repoRoot, planPath, planStub: planStub2, branch, roadmap: args.includes(ROADMAP_FLAG2) });
19858
21144
  try {
19859
21145
  const planContent = fs.readFileSync(planPath, "utf8");
19860
21146
  const promptContent = fs.readFileSync(promptPath, "utf8");
@@ -20544,4 +21830,4 @@ async function plan(args, repoRoot, registry = CORE_ADAPTER_REGISTRY) {
20544
21830
  }
20545
21831
  }
20546
21832
 
20547
- export { readConfigFile, loadConfig, SERVE_CLI_VERSION, PROVENANCE_FIELD, SKILL_FILE2 as SKILL_FILE, provenanceBlock, serveVerdict, SKILL_USE_CLI_VERSION, minutesBetween, sessionLogDir, collectSessionRow, collect, AGENT_DEFINITION_DIR, VENDOR_COMMAND, readAgentDirectory, resolveAgentRoster, missingPlanAgents, isStackList, stackPaths, stacksFromEccSkillName, DESCRIPTION_LIMIT, LISTING_LIMIT, SKILL_SIGNALS, countCharacters, toolOf, pathDirectories, checkReferences, checkFile, checkDirectory, bundledAgentsDirectory, sourceItem, byName, skillItem, readTrees, formatCount, verdictOf, failingRows, formatRows, waitForChecks, recordPlanCi, report, MANIFEST_VERSION, FEATURE_TYPES, OUTPUT_CHANNELS, RUNNING_MANIFEST_SEAMS, validateManifest, SPEC_EXTENSION, boardId, notesFileName, ROADMAP_TITLE, ROADMAP_SETTING, parseRoadmapBody, createGhRoadmapSearch, severalRoadmapsMessage, createGhOpenPullRequests, closedIssuesIn, branchClaims, scanClaimBranches, bodyLines, findField, readEpicBody, SPEC_BLOCKED_LABEL, hasSpecBlockedLabel, readBlockedBy, blockedFaultMessage, EPIC_LABEL_PREFIX, epicSlugsOf, isNotPlanned, groupByEpicLabel, localDay, readEpics, BOARD_LISTING_LIMIT, createGhBoardListing, EPIC_TYPE_LABEL, plansDirAt, requireProject, resolvePlansDir, resolveProjectConfig, countTasks, formatCounts, checkbox, planFileName, isFile, issueLine, plural, expectNoArgument, expectOneArgument, expectAtMostOneArgument, readSwitch, validate_default, createGhPermissions, readAuthorTrust, trustRefusalClause, requireTrustedAuthor, RefsBlockError, fingerprintText, readRefsText, readCopyRefs, restampCopyRefs, createGitRunner, gitSaid, RefVerifyError, PREVIOUS_COPY_WARN_ABOVE, previousDir, countPreviousCopies, createGhSpecIssueReader, readRoadmapSetting, ROADMAP_LABEL, BOARD_LABELS, LABEL_LIST_LIMIT, SPEC_TEMPLATE_PATH, TEMPLATES_DIRNAME, boardChanged, listBoardLabels, missingBoardLabels, anythingAt, writeShippedFile, setUpBoard, createGhBoardLister, resolveDefaultBoard, unlabelledRoadmapMessage, createGhIssueBoard, blockedLineSentence, HORIZON_LABEL_PREFIX, readEpicProblems, epicProblemMessage, readDoctorRefs, renderDoctorRefs, POSITION_FILE, positionFilePath, readPositionFile, writePositionFile, rehome, hop, readEpicDependents, REASON_FLAG, renderHorizonComment, renderParkedPullRequestComment, renderMoveComment, renderCloseComment, renderCancelComment, cancelMoveReason, renderDependentComment, renderUnblockNote, readReason, unaskedReasonMessage, blankReasonMessage, HORIZONS, horizonOf, resolveTracker, DEFAULT_ISSUE_SEAMS, lineRefusal, expectTwoArguments, readTextFlag, readNonBlankFlag, readRequiredFlag, readChoice, readChoiceFlag, issueProject, issueSubjectConfig, resolveIssueTracker, issueRef, issueName, urlLines, onTracker, runIssueList, ISSUE_LIST_FLAGS, list_default, firstNowEpic, show_default, samePlace, resolvePlace, createPlanDirNames, hasPlanFor, memoiseVerifier, LEARNED_DIRECTORY, ORIGIN_FIELD, DEMOTION_VERDICTS, WHEN_TO_USE_HEADING, bodySections, classifySkill, firstSentence, convertToInstinct, rankCandidates, readRankCandidate, NO_NEXT_STEP, readSchemaReport, gateMeaning, unknownAdditive, unknownAdditiveWarning, developmentApplied, shellQuote, ghMissingMessage, ghUnauthenticatedMessage, ghHostOf, ghPreflightItems, resolvePrProvider, requireGhProvider, isFirstDispatch, SESSION_ID_FLAG2 as SESSION_ID_FLAG, resolveLoopSeams, sessionIdFlag, refusal2 as refusal, isLive, planLabel, sessionLine, projectRoot, readRecords, pickSession, writeSession, readSessionChecklist, checkboxAt, estimateEta, readSessionFinishes, formatDuration, etaLine, parseWorkingTree, parseWorktrees, readMergeRefusal, cleanUpSteps, commandLine, remainingFrom, createGhPullRequests, ghPullRequestsIn, readManifestVersion, DEFAULT_RELEASE_SEAMS, changelogVersions, readTags, status_default, epicEndLines, watchDryEpic, DEFAULT_BASE_BRANCH, ghNextBoard, openNextSources, createLinePrompter, REMOTE, parseBaseStanding, hasDiverged, list_default2 as list_default1, readBranch2 as readBranch, readPlans, readOpenPull, readBranchPlan, readNextState, commandWords, nextQuestion, NEXT_COMMAND_ACTIONS, actionInvocation, runAction, HINT_FLAG_SPEC, actionOutput, endWithNextStep, endingWith, lazyPrompter, ready_default, DEFAULT_PLAN_FILE, resolvePlanPath, assessPlanRisk, renderRiskText, announceRiskTotal, resolveCreateSpec, TRIAGE_MODULE, namedSecrets, triageReport, unresolvedTracker, resolveReleaseEnabled, DEFAULT_CI_TIMEOUT_MIN, CI_POLL_INTERVAL_MS, DEFAULT_CI_ATTEMPTS, RUNTIME_SUBDIR, RUNTIME_ENTRY, VERSION_SHAPE2 as VERSION_SHAPE, refuseMisplacedRuntime, MECHANICAL_CONFLICT_SENTENCE, start, usage, createPlanRefsVerifier, plan };
21833
+ export { readConfigFile, loadConfig, SERVE_CLI_VERSION, PROVENANCE_FIELD, SKILL_FILE2 as SKILL_FILE, provenanceBlock, serveVerdict, SKILL_USE_CLI_VERSION, minutesBetween, sessionLogDir, collectSessionRow, collect, AGENT_DEFINITION_DIR, VENDOR_COMMAND, readAgentDirectory, resolveAgentRoster, missingPlanAgents, isStackList, stackPaths, stacksFromEccSkillName, DESCRIPTION_LIMIT, LISTING_LIMIT, SKILL_SIGNALS, countCharacters, toolOf, pathDirectories, checkReferences, checkFile, checkDirectory, bundledAgentsDirectory, sourceItem, byName, skillItem, readTrees, formatCount, verdictOf, failingRows, formatRows, waitForChecks, recordPlanCi, report, MANIFEST_VERSION, FEATURE_TYPES, OUTPUT_CHANNELS, RUNNING_MANIFEST_SEAMS, validateManifest, SPEC_EXTENSION, boardId, notesFileName, ROADMAP_TITLE, ROADMAP_SETTING, parseRoadmapBody, createGhRoadmapSearch, severalRoadmapsMessage, createGhOpenPullRequests, closedIssuesIn, branchClaims, scanClaimBranches, readEpicBody, SPEC_BLOCKED_LABEL, hasSpecBlockedLabel, readBlockedBy, blockedFaultMessage, EPIC_LABEL_PREFIX, epicSlugsOf, isNotPlanned, groupByEpicLabel, localDay, readEpics, BOARD_LISTING_LIMIT, createGhBoardListing, EPIC_TYPE_LABEL, plansDirAt, requireProject, resolvePlansDir, resolveProjectConfig, countTasks, formatCounts, checkbox, planFileName, isFile, issueLine, plural, expectNoArgument, expectOneArgument, expectAtMostOneArgument, readSwitch, validate_default, createGhPermissions, readAuthorTrust, trustRefusalClause, requireTrustedAuthor, RefsBlockError, fingerprintText, readRefsText, readCopyRefs, restampCopyRefs, RefVerifyError, PREVIOUS_COPY_WARN_ABOVE, previousDir, countPreviousCopies, createGhSpecIssueReader, readRoadmapSetting, ROADMAP_LABEL, BOARD_LABELS, LABEL_LIST_LIMIT, SPEC_TEMPLATE_PATH, TEMPLATES_DIRNAME, boardChanged, listBoardLabels, missingBoardLabels, anythingAt, writeShippedFile, setUpBoard, createGhBoardLister, resolveDefaultBoard, unlabelledRoadmapMessage, createGhIssueBoard, blockedLineSentence, HORIZON_LABEL_PREFIX, readEpicProblems, epicProblemMessage, readDoctorRefs, renderDoctorRefs, readEpicDependents, REASON_FLAG, renderHorizonComment, renderParkedPullRequestComment, renderMoveComment, renderCloseComment, renderCancelComment, cancelMoveReason, renderDependentComment, renderUnblockNote, readReason, unaskedReasonMessage, blankReasonMessage, HORIZONS, horizonOf, resolveTracker, DEFAULT_ISSUE_SEAMS, lineRefusal, expectTwoArguments, readTextFlag, readNonBlankFlag, readRequiredFlag, readChoice, readChoiceFlag, issueProject, issueSubjectConfig, resolveIssueTracker, issueRef, issueName, urlLines, onTracker, runIssueList, ISSUE_LIST_FLAGS, list_default, firstNowEpic, show_default, samePlace, resolvePlace, createPlanDirNames, hasPlanFor, memoiseVerifier, LEARNED_DIRECTORY, ORIGIN_FIELD, DEMOTION_VERDICTS, WHEN_TO_USE_HEADING, bodySections, classifySkill, firstSentence, convertToInstinct, rankCandidates, readRankCandidate, NO_NEXT_STEP, readSchemaReport, gateMeaning, unknownAdditive, unknownAdditiveWarning, developmentApplied, shellQuote, ghMissingMessage, ghUnauthenticatedMessage, ghHostOf, ghPreflightItems, resolvePrProvider, requireGhProvider, isFirstDispatch, SESSION_ID_FLAG2 as SESSION_ID_FLAG, readBoardBody, openBoards, boardOfEpic, createOwnerResolver, resolveLoopSeams, sessionIdFlag, refusal2 as refusal, isLive, planLabel, sessionLine, projectRoot, readRecords, pickSession, writeSession, readSessionChecklist, checkboxAt, estimateEta, readSessionFinishes, formatDuration, etaLine, parseWorkingTree, parseWorktrees, readMergeRefusal, cleanUpSteps, commandLine, remainingFrom, createGhPullRequests, ghPullRequestsIn, readManifestVersion, DEFAULT_RELEASE_SEAMS, changelogVersions, readTags, status_default, epicEndLines, watchDryEpic, nextOwnerGate, DEFAULT_BASE_BRANCH, ghNextBoard, openNextSources, createLinePrompter, REMOTE, parseBaseStanding, hasDiverged, list_default2 as list_default1, readBranch2 as readBranch, readPlans, readOpenPull, readBranchPlan, readNextState, readHomeAfterLoop, commandWords, nextQuestion, NEXT_COMMAND_ACTIONS, actionInvocation, runAction, HINT_FLAG_SPEC, actionOutput, endWithNextStep, endingWith, lazyPrompter, ready_default, DEFAULT_PLAN_FILE, resolvePlanPath, assessPlanRisk, renderRiskText, announceRiskTotal, resolveCreateSpec, TRIAGE_MODULE, namedSecrets, triageReport, unresolvedTracker, resolveReleaseEnabled, DEFAULT_CI_TIMEOUT_MIN, CI_POLL_INTERVAL_MS, DEFAULT_CI_ATTEMPTS, RUNTIME_SUBDIR, RUNTIME_ENTRY, VERSION_SHAPE2 as VERSION_SHAPE, refuseMisplacedRuntime, MECHANICAL_CONFLICT_SENTENCE, start, usage, createPlanRefsVerifier, plan };