@open-tomato/rafa 0.38.0 → 0.39.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,7 +14,7 @@ import {
14
14
  trackerPathFor,
15
15
  updateTrackerLine,
16
16
  writeTrackerBlocker
17
- } from "./index-zdz0mp63.js";
17
+ } from "./index-g4116t9m.js";
18
18
  import {
19
19
  ACROSS_STORES_ORDER,
20
20
  CORE_ADAPTER_REGISTRY,
@@ -35,6 +35,7 @@ import {
35
35
  NOT_A_REPOSITORY,
36
36
  PORT_VERSIONS,
37
37
  RAFA_VERSION,
38
+ RECORD_EXTENSION,
38
39
  SCOPE_DIR,
39
40
  SKILL_FILE,
40
41
  SQLITE_MIGRATIONS,
@@ -77,6 +78,7 @@ import {
77
78
  oneLine,
78
79
  padKind,
79
80
  parseInstinct,
81
+ parseSessionRecord,
80
82
  planFileName,
81
83
  planFilePath,
82
84
  planSchema,
@@ -93,6 +95,7 @@ import {
93
95
  readScopes,
94
96
  readSession,
95
97
  readSessions,
98
+ readState,
96
99
  readStoreMeta,
97
100
  readStoreSchema,
98
101
  readSwitch,
@@ -126,7 +129,7 @@ import {
126
129
  writeNames,
127
130
  writePositionFile,
128
131
  writeSqliteStore
129
- } from "./index-74btmn1e.js";
132
+ } from "./index-1r2b1052.js";
130
133
  import {
131
134
  CHANGE_LEVELS,
132
135
  CONFIG_DEFAULTS,
@@ -135,6 +138,7 @@ import {
135
138
  FINDING_KINDS,
136
139
  FINDING_SIGNALS,
137
140
  PREREQUISITE_KINDS,
141
+ RELEASE_DEFAULTS,
138
142
  REPORT_STATUSES,
139
143
  SETTINGS,
140
144
  activeOutput,
@@ -154,7 +158,7 @@ import {
154
158
  requiredPrerequisite,
155
159
  resolveTiers,
156
160
  stampPrompt
157
- } from "./index-w9naz372.js";
161
+ } from "./index-m0wt4nr1.js";
158
162
  import {
159
163
  promotable
160
164
  } from "./index-d6d0942q.js";
@@ -5965,8 +5969,8 @@ async function report(args, repoRoot) {
5965
5969
  }
5966
5970
 
5967
5971
  // src/commands/plan/validate.ts
5968
- import { readFileSync as readFileSync8 } from "fs";
5969
- import { basename as basename4, resolve as resolve6 } from "path";
5972
+ import { readFileSync as readFileSync10 } from "fs";
5973
+ import { resolve as resolve8 } from "path";
5970
5974
 
5971
5975
  // src/board/readiness.ts
5972
5976
  var SPEC_READY_LABEL = "spec:ready";
@@ -6311,23 +6315,425 @@ function storeRuleLine(file, problem) {
6311
6315
  return `${file}:${String(problem.line)}: ${problem.kind}: ${problem.text}`;
6312
6316
  }
6313
6317
 
6314
- // src/commands/plan/validate.ts
6315
- var USAGE = "rafa plan validate <file>";
6318
+ // src/commands/plan/store-check.ts
6319
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync9, renameSync as renameSync2 } from "fs";
6320
+ import { basename as basename5, dirname as dirname6, resolve as resolve7 } from "path";
6321
+
6322
+ // src/board/gate.ts
6323
+ import { existsSync as existsSync11, mkdirSync as mkdirSync2, readFileSync as readFileSync8, renameSync, statSync as statSync4, writeFileSync as writeFileSync3 } from "fs";
6324
+ import { basename as basename4, dirname as dirname5, join as join8, resolve as resolve6 } from "path";
6325
+
6326
+ // src/board/flags.ts
6327
+ var SPEC_FLAG = "--spec";
6328
+ var ISSUE_FLAG = "--issue";
6329
+ var NEXT_FLAG = "--next";
6330
+ var REFRESH_FLAG = "--refresh";
6331
+ var ROADMAP_FLAG = "--roadmap";
6332
+ var CLAIM_AHEAD_FLAG = "--claim-ahead";
6333
+ var DRY_RUN_FLAG = "--dry-run";
6334
+ var SKIP_REVIEW_FLAG = "--skip-review";
6335
+ var NO_COMMENT_FLAG = "--no-comment";
6336
+ var ACCEPT_REFS_FLAG = "--accept-refs";
6337
+
6338
+ // src/board/review-comment.ts
6339
+ var SPEC_REVIEW_MARKER = "<!-- rafa:spec-review v1 -->";
6340
+ var SPEC_REVIEW_COMMENT_NAME = "rafa:spec-review";
6341
+ var ONE_LINE = /\s+/gu;
6342
+ var COMMENT_REMEDY = "Close the gaps, label the issue `spec:ready` again, and plan from it again.";
6343
+ var ASSUMED_COMMENT_REMEDY = "Answer these in the spec where an assumption is wrong, and plan from it" + " again. No label changed: none of these gaps blocked planning.";
6344
+ var COMMENT_HISTORY = "This comment is edited in place each time the spec is reviewed.";
6345
+ function counted2(count, noun) {
6346
+ return count === 1 ? `1 ${noun}` : `${String(count)} ${noun}s`;
6347
+ }
6348
+ function gapItem(gap) {
6349
+ return `- **${gap.heading.replace(ONE_LINE, " ").trim()}** \u2014 ${gap.what.replace(ONE_LINE, " ").trim()}`;
6350
+ }
6351
+ function assumedGapItem(gap) {
6352
+ return gap.assumption === null ? gapItem(gap) : `${gapItem(gap)}
6353
+ - Planned under: ${gap.assumption.replace(ONE_LINE, " ").trim()}`;
6354
+ }
6355
+ function requireGaps(gaps) {
6356
+ if (gaps.length === 0) {
6357
+ throw new TypeError("board review comment: no gap was named, and there is no comment to write");
6358
+ }
6359
+ }
6360
+ function commentBody(found, items, remedy) {
6361
+ return [SPEC_REVIEW_MARKER, ...found, "", ...items, "", remedy, "", COMMENT_HISTORY, ""].join(`
6362
+ `);
6363
+ }
6364
+ function specReviewCommentBody(gaps) {
6365
+ requireGaps(gaps);
6366
+ return commentBody([`**rafa reviewed this spec before planning and found ${counted2(gaps.length, "gap")}.**`, "No plan was written."], gaps.map(gapItem), COMMENT_REMEDY);
6367
+ }
6368
+ function assumedReviewCommentBody(gaps) {
6369
+ requireGaps(gaps);
6370
+ return commentBody([
6371
+ `**rafa reviewed this spec before planning and found ${counted2(gaps.length, "gap")}, none of them blocking.**`,
6372
+ "The plan was written under the assumptions below."
6373
+ ], gaps.map(assumedGapItem), ASSUMED_COMMENT_REMEDY);
6374
+ }
6375
+ function specReviewComments(comments) {
6376
+ return [...comments].reverse().filter((comment) => comment.body.includes(SPEC_REVIEW_MARKER));
6377
+ }
6378
+ function ignoredReviewCommentMessage(comment, reading, repo) {
6379
+ const who = `was written by ${reading.login}, ${trustRefusalClause(repo, reading)}`;
6380
+ return `the ${SPEC_REVIEW_COMMENT_NAME} comment ${comment.id} ${who};` + " it was left alone and the gaps went in a comment of their own";
6381
+ }
6382
+ async function readTrustedSpecReviewComment(comments, trust) {
6383
+ const trustOf = memoisedTrust(trust);
6384
+ const ignored = [];
6385
+ for (const comment of specReviewComments(comments)) {
6386
+ const reading = await trustOf(comment.author);
6387
+ if (reading.trusted)
6388
+ return { comment, ignored };
6389
+ ignored.push({
6390
+ id: comment.id,
6391
+ author: comment.author,
6392
+ reason: ignoredReviewCommentMessage(comment, reading, trust.repo)
6393
+ });
6394
+ }
6395
+ return { comment: null, ignored };
6396
+ }
6397
+ async function writeSpecReviewComment(options) {
6398
+ const { body, issue: issue3, board, trust } = options;
6399
+ const found = await readTrustedSpecReviewComment(await board.comments(issue3), trust);
6400
+ const { comment: existing, ignored } = found;
6401
+ if (existing === null) {
6402
+ return { action: "posted", comment: await board.comment(issue3, body), ignored };
6403
+ }
6404
+ return { action: "edited", comment: await board.editComment(existing.id, body), ignored };
6405
+ }
6406
+ function memoisedTrust(trust) {
6407
+ const read = new Map;
6408
+ return (login) => {
6409
+ const taken = read.get(login) ?? readBoardTrust(trust, login);
6410
+ read.set(login, taken);
6411
+ return taken;
6412
+ };
6413
+ }
6414
+
6415
+ // src/board/plan-field.ts
6416
+ var PLAN_BLOCK_KIND = "plan";
6417
+ var FIELD_NAME = /^[a-z][a-z-]*$/u;
6418
+ var ISSUE_FIELD = "issue";
6419
+ function unrecorded(answer, text, note) {
6420
+ return { answer, text, recorded: false, note };
6421
+ }
6422
+ function indentOf(line) {
6423
+ return /^\s*/u.exec(line)?.[0] ?? "";
6424
+ }
6425
+ function carriageOf(line) {
6426
+ return line.endsWith("\r") ? "\r" : "";
6427
+ }
6428
+ function fieldLine(field2) {
6429
+ if (!FIELD_NAME.test(field2)) {
6430
+ throw new TypeError(`board plan field: ${JSON.stringify(field2)} is no rafa:plan key this records`);
6431
+ }
6432
+ return new RegExp(`^(\\s*)${field2}\\s*:(.*)$`, "u");
6433
+ }
6434
+ function stampPlanField(plan, field2, value) {
6435
+ const pattern = fieldLine(field2);
6436
+ const line = `${field2}: ${value}`;
6437
+ const block = readRafaBlocks(plan).find((found) => found.kind === PLAN_BLOCK_KIND);
6438
+ if (block === undefined) {
6439
+ return unrecorded("no-block", plan, "the plan holds no rafa:plan block to record it in");
6440
+ }
6441
+ if (!block.closed) {
6442
+ return unrecorded("unclosed-block", plan, `the rafa:plan block at line ${String(block.span.first)} is never closed, so nothing was recorded in it`);
6443
+ }
6444
+ const lines = plan.split(`
6445
+ `);
6446
+ const opening = lines[block.span.first - 1] ?? "";
6447
+ const indent = indentOf(opening);
6448
+ const written = `${indent}${line}${carriageOf(opening)}`;
6449
+ for (let index = block.span.first;index < block.span.last - 1; index += 1) {
6450
+ const found = pattern.exec(lines[index] ?? "");
6451
+ if (found === null || found[1] !== indent)
6452
+ continue;
6453
+ if ((found[2] ?? "").trim() === value) {
6454
+ return { answer: "unchanged", text: plan, recorded: true, note: `the plan already records ${line}` };
6455
+ }
6456
+ const replaced = [...lines];
6457
+ replaced[index] = written;
6458
+ return {
6459
+ answer: "replaced",
6460
+ text: replaced.join(`
6461
+ `),
6462
+ recorded: true,
6463
+ note: `line ${String(index + 1)} of the plan now records ${line}`
6464
+ };
6465
+ }
6466
+ const at = block.span.last - 1;
6467
+ const stamped = [...lines.slice(0, at), written, ...lines.slice(at)];
6468
+ return {
6469
+ answer: "inserted",
6470
+ text: stamped.join(`
6471
+ `),
6472
+ recorded: true,
6473
+ note: `line ${String(at + 1)} of the plan now records ${line}`
6474
+ };
6475
+ }
6476
+ function issueFieldLine(issue3) {
6477
+ if (!Number.isSafeInteger(issue3) || issue3 < 1) {
6478
+ throw new TypeError(`board plan field: issue ${String(issue3)} is no issue number, expected a positive whole one`);
6479
+ }
6480
+ return `${ISSUE_FIELD}: ${JSON.stringify(String(issue3))}`;
6481
+ }
6482
+ function stampPlanIssue(plan, issue3) {
6483
+ return stampPlanField(plan, ISSUE_FIELD, JSON.stringify(String(issue3)));
6484
+ }
6485
+
6486
+ // src/board/review-stamp.ts
6487
+ var REVIEW_FIELD = "review";
6488
+ var REVIEW_SKIPPED = "skipped";
6489
+ var REVIEW_MISSING = "missing";
6490
+ var REVIEW_ASSUMED = "assumed";
6491
+ var REVIEW_SKIPPED_LINE = `${REVIEW_FIELD}: ${REVIEW_SKIPPED}`;
6492
+ var REVIEW_MISSING_LINE = `${REVIEW_FIELD}: ${REVIEW_MISSING}`;
6493
+ var REVIEW_ASSUMED_LINE = `${REVIEW_FIELD}: ${REVIEW_ASSUMED}`;
6494
+ function stampReviewSkipped(plan) {
6495
+ return stampPlanField(plan, REVIEW_FIELD, REVIEW_SKIPPED);
6496
+ }
6497
+ function stampReviewMissing(plan) {
6498
+ return stampPlanField(plan, REVIEW_FIELD, REVIEW_MISSING);
6499
+ }
6500
+ function stampReviewAssumed(plan) {
6501
+ return stampPlanField(plan, REVIEW_FIELD, REVIEW_ASSUMED);
6502
+ }
6503
+ var ASSUMPTIONS_HEADING = "# Planned under assumptions";
6504
+ var ASSUMPTIONS_PREAMBLE = [
6505
+ "The spec review found gaps, none of them blocking, so this plan was",
6506
+ "written under the assumptions below. Each names the gap it was",
6507
+ "written for; closing that gap in the spec is what replaces the guess."
6508
+ ];
6509
+ function assumptionsHeading(gaps) {
6510
+ const items = gaps.filter((gap) => gap.assumption !== null).map(assumedGapItem);
6511
+ if (items.length === 0) {
6512
+ throw new TypeError("board review stamp: no gap named an assumption, and there is no heading to open a plan with");
6513
+ }
6514
+ return [ASSUMPTIONS_HEADING, "", ...ASSUMPTIONS_PREAMBLE, "", ...items, "", ""].join(`
6515
+ `);
6516
+ }
6517
+
6518
+ // src/board/gate.ts
6519
+ var SPEC_NOT_READY_EXIT = 3;
6520
+ var SPEC_NEEDS_WORK_LABEL = "spec:needs-work";
6521
+ function readGateFlags(args) {
6522
+ return {
6523
+ skipReview: args.includes(SKIP_REVIEW_FLAG),
6524
+ comment: !args.includes(NO_COMMENT_FLAG)
6525
+ };
6526
+ }
6527
+ function describeGap2(gap) {
6528
+ return ` \u2022 "${gap.heading}": ${gap.what}`;
6529
+ }
6530
+ function remedyFor(issue3) {
6531
+ return issue3 === null ? " Close the gaps in the spec, then plan from it again." : ` Close the gaps, label the issue ${SPEC_READY_LABEL} again, and plan from it again.`;
6532
+ }
6533
+ function specNotReadyMessage(source, reading, gaps, issue3 = null) {
6534
+ return [
6535
+ `\u274C ${source} is not ready to plan from: ${reading}`,
6536
+ ...gaps.map(describeGap2),
6537
+ remedyFor(issue3)
6538
+ ].join(`
6539
+ `);
6540
+ }
6541
+ function unreadReviewWarning(reading, planPath) {
6542
+ return `${reading}; ${planPath} reads as written, so the plan stands unreviewed`;
6543
+ }
6544
+ function assumedReviewWarning(reading, planPath) {
6545
+ return `${reading}, none of them blocking; ${planPath} is written under the assumptions the review named`;
6546
+ }
6547
+ function unreadReviewMessage(source, reading, planIssues) {
6548
+ return [
6549
+ `\u274C ${source}: no plan stands \u2014 ${reading}, and the plan the session wrote does not read as written:`,
6550
+ ...planIssues.map((line) => ` ${line}`),
6551
+ " Plan from the spec again."
6552
+ ].join(`
6553
+ `);
6554
+ }
6555
+ function isWrittenFile(repoRoot, path) {
6556
+ const full = resolve6(repoRoot, path);
6557
+ return existsSync11(full) && statSync4(full).isFile();
6558
+ }
6559
+ var NOT_READY_REASON = "the planner judged the spec not ready, so no plan stands.";
6560
+ var UNREAD_REASON = "no review came back and the plan does not read as written, so no plan stands.";
6561
+ var REJECTED_DIR = "rejected";
6562
+ function rejectedPath(path) {
6563
+ const parent = dirname5(path);
6564
+ return parent === "." ? join8(REJECTED_DIR, basename4(path)) : join8(parent, REJECTED_DIR, basename4(path));
6565
+ }
6566
+ function moveWritten(options, output, reason) {
6567
+ for (const path of [options.planPath, options.prerequisitesPath]) {
6568
+ if (!isWrittenFile(options.repoRoot, path))
6569
+ continue;
6570
+ const destination = rejectedPath(path);
6571
+ try {
6572
+ mkdirSync2(resolve6(options.repoRoot, dirname5(destination)), { recursive: true });
6573
+ renameSync(resolve6(options.repoRoot, path), resolve6(options.repoRoot, destination));
6574
+ output.info(`\uD83D\uDDC3 Moved ${path} to ${destination}: ${reason}`);
6575
+ } catch (error) {
6576
+ output.warn(`${path} was written against a refused review and could not be moved: ${messageOf(error)}`);
6577
+ }
6578
+ }
6579
+ }
6580
+ function readWrittenPlan(options) {
6581
+ const { repoRoot, planPath } = options;
6582
+ if (!isWrittenFile(repoRoot, planPath)) {
6583
+ return { reads: false, issues: [`${planPath}: the session wrote no plan there`] };
6584
+ }
6585
+ try {
6586
+ const { issues } = validatePlan(resolve6(repoRoot, planPath));
6587
+ return { reads: issues.length === 0, issues: issues.map((issue3) => issueLine(planPath, issue3)) };
6588
+ } catch (error) {
6589
+ return { reads: false, issues: [`${planPath}: ${messageOf(error)}`] };
6590
+ }
6591
+ }
6592
+ function standOrRefuse(options, review, output) {
6593
+ const plan = readWrittenPlan(options);
6594
+ if (plan.reads) {
6595
+ output.warn(unreadReviewWarning(review.text, options.planPath));
6596
+ return "unread";
6597
+ }
6598
+ moveWritten(options, output, UNREAD_REASON);
6599
+ throw new CommandExit(SPEC_NOT_READY_EXIT, unreadReviewMessage(options.source, review.text, plan.issues));
6600
+ }
6601
+ function recordAssumptions(options, gaps, output) {
6602
+ const { repoRoot, planPath } = options;
6603
+ const unopened = (why) => {
6604
+ output.warn(`${planPath} was not opened under the review's assumptions: ${why}`);
6605
+ };
6606
+ if (!isWrittenFile(repoRoot, planPath)) {
6607
+ unopened("the session wrote no plan there");
6608
+ return;
6609
+ }
6610
+ const file = resolve6(repoRoot, planPath);
6611
+ let stamped;
6612
+ try {
6613
+ stamped = stampReviewAssumed(`${assumptionsHeading(gaps)}${readFileSync8(file, "utf8")}`);
6614
+ writeFileSync3(file, stamped.text, "utf8");
6615
+ } catch (error) {
6616
+ unopened(messageOf(error));
6617
+ return;
6618
+ }
6619
+ if (!stamped.recorded) {
6620
+ output.warn(`${planPath} opens under the review's assumptions and records no ${REVIEW_ASSUMED_LINE}: ${stamped.note}`);
6621
+ return;
6622
+ }
6623
+ output.info(`\uD83E\uDDED ${planPath} opens under the review's assumptions and records ${REVIEW_ASSUMED_LINE}.`);
6624
+ }
6625
+ async function publishGaps(issue3, gaps, body, output) {
6626
+ try {
6627
+ const write = await writeSpecReviewComment({
6628
+ issue: issue3.number,
6629
+ body: body(gaps),
6630
+ board: issue3.board,
6631
+ trust: issue3.trust
6632
+ });
6633
+ for (const passed2 of write.ignored)
6634
+ output.warn(passed2.reason);
6635
+ output.info(`\uD83D\uDCAC ${write.action === "posted" ? "Posted" : "Edited"} the review comment on issue #${String(issue3.number)}.`);
6636
+ } catch (error) {
6637
+ output.warn(`the review comment on issue #${String(issue3.number)} was not written: ${messageOf(error)}`);
6638
+ }
6639
+ }
6640
+ async function swapLabels(issue3, output) {
6641
+ try {
6642
+ await issue3.board.swapLabels(issue3.number, SPEC_READY_LABEL, SPEC_NEEDS_WORK_LABEL);
6643
+ output.info(`\uD83C\uDFF7 Swapped ${SPEC_READY_LABEL} for ${SPEC_NEEDS_WORK_LABEL} on issue #${String(issue3.number)}.`);
6644
+ } catch (error) {
6645
+ output.warn(`${SPEC_READY_LABEL} was not swapped for ${SPEC_NEEDS_WORK_LABEL} on issue` + ` #${String(issue3.number)}: ${messageOf(error)}`);
6646
+ }
6647
+ }
6648
+ function nothingBlocks(review) {
6649
+ return !review.gaps.some((gap) => gap.blocking);
6650
+ }
6651
+ async function planUnderAssumptions(options, review, output) {
6652
+ output.warn(assumedReviewWarning(review.text, options.planPath));
6653
+ recordAssumptions(options, review.gaps, output);
6654
+ const { issue: issue3 } = options;
6655
+ if (issue3 !== null) {
6656
+ if (options.comment) {
6657
+ await publishGaps(issue3, review.gaps, assumedReviewCommentBody, output);
6658
+ } else {
6659
+ output.info(`\uD83D\uDCAC ${NO_COMMENT_FLAG}: the gaps were not posted on issue #${String(issue3.number)}.`);
6660
+ }
6661
+ }
6662
+ return "assumed";
6663
+ }
6664
+ async function enforceSpecReview(options) {
6665
+ const { review, issue: issue3 } = options;
6666
+ if (review === undefined)
6667
+ return "unjudged";
6668
+ if (review.ready)
6669
+ return "ready";
6670
+ const output = options.output ?? activeOutput();
6671
+ if (review.answer !== "not-ready")
6672
+ return standOrRefuse(options, review, output);
6673
+ if (nothingBlocks(review))
6674
+ return planUnderAssumptions(options, review, output);
6675
+ moveWritten(options, output, NOT_READY_REASON);
6676
+ if (issue3 !== null) {
6677
+ if (options.comment) {
6678
+ await publishGaps(issue3, review.gaps, specReviewCommentBody, output);
6679
+ } else {
6680
+ output.info(`\uD83D\uDCAC ${NO_COMMENT_FLAG}: the gaps were not posted on issue #${String(issue3.number)}.`);
6681
+ }
6682
+ await swapLabels(issue3, output);
6683
+ }
6684
+ throw new CommandExit(SPEC_NOT_READY_EXIT, specNotReadyMessage(options.source, review.text, review.gaps, issue3));
6685
+ }
6686
+
6687
+ // src/commands/plan/store-check.ts
6316
6688
  function readFileOrNull(path) {
6317
- return path !== null && isFile(path) ? readFileSync8(path, "utf8") : null;
6689
+ return path !== null && isFile(path) ? readFileSync9(path, "utf8") : null;
6318
6690
  }
6319
6691
  function checkStoreRules(file, markdown) {
6320
6692
  const prerequisitesPath = prerequisitesPathForPlan(file);
6321
6693
  return findStoreRuleProblems({
6322
6694
  plan: markdown,
6323
6695
  prerequisites: readFileOrNull(prerequisitesPath),
6324
- prerequisitesName: basename4(prerequisitesPath ?? "PREREQUISITES-<stub>.md")
6696
+ prerequisitesName: basename5(prerequisitesPath ?? "PREREQUISITES-<stub>.md")
6325
6697
  });
6326
6698
  }
6699
+ function moveRejected(repoRoot, path, output) {
6700
+ const full = resolve7(repoRoot, path);
6701
+ if (!isFile(full))
6702
+ return;
6703
+ const destination = rejectedPath(path);
6704
+ try {
6705
+ mkdirSync3(resolve7(repoRoot, dirname6(destination)), { recursive: true });
6706
+ renameSync2(full, resolve7(repoRoot, destination));
6707
+ output.info(`\uD83D\uDDC3 Moved ${path} to ${destination}: the plan breaks the effort-store rules.`);
6708
+ } catch (error) {
6709
+ output.warn(`${path} breaks the effort-store rules and could not be moved: ${messageOf(error)}`);
6710
+ }
6711
+ }
6712
+ function enforceStoreRules(repoRoot, generated, output = activeOutput()) {
6713
+ const planFile = resolve7(repoRoot, generated.planPath);
6714
+ if (!isFile(planFile))
6715
+ return;
6716
+ const problems = checkStoreRules(planFile, readFileSync9(planFile, "utf8"));
6717
+ if (problems.length === 0)
6718
+ return;
6719
+ const prerequisites = generated.prerequisitesPath ?? prerequisitesPathForPlan(generated.planPath);
6720
+ for (const path of [generated.planPath, prerequisites]) {
6721
+ if (path !== null)
6722
+ moveRejected(repoRoot, path, output);
6723
+ }
6724
+ throw new CommandExit(1, [
6725
+ `\u274C ${generated.planPath}: ${plural(problems.length, "broken effort-store rule")};` + " rafa loop start would refuse this plan, so no plan stands.",
6726
+ ...problems.map((problem) => ` ${storeRuleLine(generated.planPath, problem)}`)
6727
+ ].join(`
6728
+ `));
6729
+ }
6730
+
6731
+ // src/commands/plan/validate.ts
6732
+ var USAGE = "rafa plan validate <file>";
6327
6733
  function validatePlan(file) {
6328
6734
  if (!isFile(file))
6329
6735
  throw new CommandExit(1, `\u274C Plan file not found: ${file}`);
6330
- const model = parsePlan(readFileSync8(file, "utf8"));
6736
+ const model = parsePlan(readFileSync10(file, "utf8"));
6331
6737
  return { file, stages: model.stages.length, tasks: countTasks(model.tasks), issues: model.issues };
6332
6738
  }
6333
6739
  function resolvedConfig(project, warn) {
@@ -6400,10 +6806,10 @@ function createPlanValidateCommand(workingDirectory = () => process.cwd()) {
6400
6806
  outputs: ["text", "json"],
6401
6807
  run: async (context) => {
6402
6808
  const typed = expectOneArgument(context.args, USAGE);
6403
- const file = resolve6(workingDirectory(), typed);
6809
+ const file = resolve8(workingDirectory(), typed);
6404
6810
  const validation = validatePlan(file);
6405
6811
  const { issues, stages, tasks } = validation;
6406
- const markdown = readFileSync8(file, "utf8");
6812
+ const markdown = readFileSync10(file, "utf8");
6407
6813
  const findings = checkRoster(context, markdown);
6408
6814
  const { missingAgents, skillCollisions, unresolvedSkills } = findings;
6409
6815
  const storeProblems = checkStoreRules(file, markdown);
@@ -6440,8 +6846,8 @@ function createPlanValidateCommand(workingDirectory = () => process.cwd()) {
6440
6846
  var validate_default = createPlanValidateCommand();
6441
6847
 
6442
6848
  // src/board/board-cache.ts
6443
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync9, renameSync, writeFileSync as writeFileSync3 } from "fs";
6444
- import { dirname as dirname5, join as join8 } from "path";
6849
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync11, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "fs";
6850
+ import { dirname as dirname7, join as join9 } from "path";
6445
6851
 
6446
6852
  // src/board/board-cache-native.ts
6447
6853
  var NATIVE_ISSUE_SELECTION = [
@@ -6712,7 +7118,7 @@ function createGhBoardListing(options) {
6712
7118
  }
6713
7119
 
6714
7120
  // src/board/board-cache.ts
6715
- var BOARD_CACHE_FILE = join8(SCOPE_DIR, "cache", "board.json");
7121
+ var BOARD_CACHE_FILE = join9(SCOPE_DIR, "cache", "board.json");
6716
7122
  var BOARD_CACHE_VERSION = 1;
6717
7123
  var NATIVE_MODE_KEY = Object.freeze({ mode: "native" });
6718
7124
  var WATERMARK_ARGS = Object.freeze([
@@ -6739,7 +7145,7 @@ function modeOfFile(value) {
6739
7145
  function readCache(path, mode) {
6740
7146
  let payload;
6741
7147
  try {
6742
- payload = JSON.parse(readFileSync9(path, "utf8"));
7148
+ payload = JSON.parse(readFileSync11(path, "utf8"));
6743
7149
  } catch {
6744
7150
  return null;
6745
7151
  }
@@ -6774,10 +7180,10 @@ function fileOf(cache) {
6774
7180
  function writeCache(path, cache) {
6775
7181
  const temporary = `${path}.${String(process.pid)}.tmp`;
6776
7182
  try {
6777
- mkdirSync2(dirname5(path), { recursive: true });
6778
- writeFileSync3(temporary, `${JSON.stringify(fileOf(cache))}
7183
+ mkdirSync4(dirname7(path), { recursive: true });
7184
+ writeFileSync4(temporary, `${JSON.stringify(fileOf(cache))}
6779
7185
  `);
6780
- renameSync(temporary, path);
7186
+ renameSync3(temporary, path);
6781
7187
  return true;
6782
7188
  } catch {
6783
7189
  return false;
@@ -6859,7 +7265,7 @@ function changesNothing(rows, changed) {
6859
7265
  }
6860
7266
  function createCachedBoardListing(options) {
6861
7267
  const { gh, root, refresh = false, limit = BOARD_LISTING_LIMIT, mode = "labels" } = options;
6862
- const path = join8(root, BOARD_CACHE_FILE);
7268
+ const path = join9(root, BOARD_CACHE_FILE);
6863
7269
  const full = createGhBoardListing({ gh, limit, mode });
6864
7270
  return async () => {
6865
7271
  const cache = refresh ? null : readCache(path, mode);
@@ -6881,7 +7287,7 @@ function invalidateRows(root, numbers) {
6881
7287
  const bad = numbers.find((number) => !isIssueNumber(number));
6882
7288
  if (bad !== undefined)
6883
7289
  throw new RangeError(`not an issue number: ${String(bad)}`);
6884
- const path = join8(root, BOARD_CACHE_FILE);
7290
+ const path = join9(root, BOARD_CACHE_FILE);
6885
7291
  const cache = readCache(path, "native");
6886
7292
  if (cache === null)
6887
7293
  return false;
@@ -7031,10 +7437,75 @@ var MAX_LISTED_CHANGES = 10;
7031
7437
  var DEFAULT_REMOTE = "origin";
7032
7438
  var BRANCH_REF_PREFIX = "refs/heads/";
7033
7439
  var INDENT = " ";
7440
+ var UNTRACKED_PREFIX = "?? ";
7034
7441
  function parseWorkingTree(stdout) {
7035
7442
  const entries = stdout.split(`
7036
7443
  `).map((line) => line.replace(/\r$/, "")).filter((line) => line.trim() !== "");
7037
- return { clean: entries.length === 0, entries: Object.freeze(entries) };
7444
+ const tracked = entries.filter((line) => !line.startsWith(UNTRACKED_PREFIX));
7445
+ const untracked = entries.filter((line) => line.startsWith(UNTRACKED_PREFIX)).map((line) => unquoteGitPath(line.slice(UNTRACKED_PREFIX.length)));
7446
+ return {
7447
+ clean: entries.length === 0,
7448
+ entries: Object.freeze(entries),
7449
+ tracked: Object.freeze(tracked),
7450
+ untracked: Object.freeze(untracked)
7451
+ };
7452
+ }
7453
+ var C_ESCAPES = {
7454
+ a: 7,
7455
+ b: 8,
7456
+ f: 12,
7457
+ n: 10,
7458
+ r: 13,
7459
+ t: 9,
7460
+ v: 11,
7461
+ "\\": 92,
7462
+ '"': 34
7463
+ };
7464
+ var OCTAL_ESCAPE = /^[0-7]{3}$/;
7465
+ function unquoteGitPath(raw) {
7466
+ if (raw.length < 2 || !raw.startsWith('"') || !raw.endsWith('"'))
7467
+ return raw;
7468
+ const body = raw.slice(1, -1);
7469
+ const bytes = [];
7470
+ const encoder = new TextEncoder;
7471
+ for (let at = 0;at < body.length; at += 1) {
7472
+ const char = body.charAt(at);
7473
+ if (char !== "\\") {
7474
+ bytes.push(...encoder.encode(char));
7475
+ continue;
7476
+ }
7477
+ const octal = body.slice(at + 1, at + 4);
7478
+ if (OCTAL_ESCAPE.test(octal)) {
7479
+ bytes.push(Number.parseInt(octal, 8));
7480
+ at += 3;
7481
+ continue;
7482
+ }
7483
+ const escaped = C_ESCAPES[body.charAt(at + 1)];
7484
+ if (escaped === undefined) {
7485
+ bytes.push(...encoder.encode(char));
7486
+ continue;
7487
+ }
7488
+ bytes.push(escaped);
7489
+ at += 1;
7490
+ }
7491
+ return new TextDecoder().decode(new Uint8Array(bytes));
7492
+ }
7493
+ function parseTreePaths(stdout) {
7494
+ return Object.freeze(stdout.split(`
7495
+ `).map((line) => line.replace(/\r$/, "")).filter((line) => line.trim() !== "").map(unquoteGitPath));
7496
+ }
7497
+ function untrackedIncoming(untracked, incoming) {
7498
+ const held = new Set(incoming);
7499
+ return Object.freeze(untracked.filter((path2) => path2.endsWith("/") ? incoming.some((file) => file.startsWith(path2)) : held.has(path2)));
7500
+ }
7501
+ function untrackedLeftLine(paths) {
7502
+ if (paths.length === 0)
7503
+ return null;
7504
+ const named = paths.slice(0, MAX_LISTED_CHANGES).map((path2) => shellQuote(path2));
7505
+ const hidden = paths.length - named.length;
7506
+ const more = hidden > 0 ? ` and ${hidden} more` : "";
7507
+ const which = paths.length === 1 ? "path" : "paths";
7508
+ return `Leaving the untracked ${which} the merge does not touch in place: ${named.join(", ")}${more}.`;
7038
7509
  }
7039
7510
  function parseWorktrees(stdout) {
7040
7511
  const entries = [];
@@ -7097,14 +7568,23 @@ function noChecksRefusal(number) {
7097
7568
  `)
7098
7569
  };
7099
7570
  }
7100
- function dirtyTreeRefusal(tree) {
7101
- const count = plural2(tree.entries.length, "change", "changes");
7571
+ function heldUntracked(reading) {
7572
+ return reading.incoming === undefined ? reading.tree.untracked : untrackedIncoming(reading.tree.untracked, reading.incoming);
7573
+ }
7574
+ function dirtyTreeRefusal(tree, held) {
7575
+ const heldSet = new Set(held);
7576
+ const listed = tree.entries.filter((line) => !line.startsWith(UNTRACKED_PREFIX) || heldSet.has(unquoteGitPath(line.slice(UNTRACKED_PREFIX.length))));
7577
+ if (listed.length === 0)
7578
+ return null;
7579
+ const count = plural2(listed.length, "change", "changes");
7580
+ const untrackedNote = held.length === 0 ? [] : ["An untracked path listed here is one the merge brings a file onto."];
7102
7581
  return {
7103
7582
  reason: "dirty-tree",
7104
7583
  message: [
7105
7584
  `${REFUSAL_PREFIX}: the working tree has ${count}.`,
7106
7585
  "Commit or stash them, then run rafa pr merge again.",
7107
- ...listChanges(tree.entries)
7586
+ ...untrackedNote,
7587
+ ...listChanges(listed)
7108
7588
  ].join(`
7109
7589
  `)
7110
7590
  };
@@ -7168,8 +7648,9 @@ function worktreeRefusal(branch, held) {
7168
7648
  };
7169
7649
  }
7170
7650
  function readMergeRefusal(reading) {
7171
- if (!reading.tree.clean)
7172
- return dirtyTreeRefusal(reading.tree);
7651
+ const dirty = dirtyTreeRefusal(reading.tree, heldUntracked(reading));
7652
+ if (dirty !== null)
7653
+ return dirty;
7173
7654
  if (reading.merge.mergeable !== "mergeable")
7174
7655
  return mergeStateRefusal(reading);
7175
7656
  const checks = checksRefusalOf(reading);
@@ -7406,7 +7887,7 @@ function readWholeNumber2(value, command, where) {
7406
7887
  }
7407
7888
  return value;
7408
7889
  }
7409
- function readState(value, command, where) {
7890
+ function readState2(value, command, where) {
7410
7891
  const state = STATES.get(readString3(value, command, where));
7411
7892
  if (state === undefined) {
7412
7893
  refuse5(command, `${where} is ${describeValue(value)}, expected one of: ${[...STATES.keys()].join(", ")}`);
@@ -7435,7 +7916,7 @@ function readSummary(value, command, where) {
7435
7916
  number: readWholeNumber2(pull["number"], command, `${where}.number`),
7436
7917
  title: readString3(pull["title"], command, `${where}.title`),
7437
7918
  url: readString3(pull["url"], command, `${where}.url`),
7438
- state: readState(pull["state"], command, `${where}.state`),
7919
+ state: readState2(pull["state"], command, `${where}.state`),
7439
7920
  headRefName: readString3(pull["headRefName"], command, `${where}.headRefName`),
7440
7921
  baseRefName: readString3(pull["baseRefName"], command, `${where}.baseRefName`),
7441
7922
  author: readAuthor(pull["author"], command, `${where}.author`),
@@ -7715,7 +8196,7 @@ function ghAuthOkIn(cwd) {
7715
8196
  var REMOTE = "origin";
7716
8197
  var BRANCH_PREFIX2 = "feat/";
7717
8198
  var INDENT2 = " ";
7718
- var UNTRACKED_PREFIX = "??";
8199
+ var UNTRACKED_PREFIX2 = "??";
7719
8200
  var MAX_LISTED_CHANGES2 = 10;
7720
8201
  var COUNT = /^\d+$/;
7721
8202
  var DETACHED_HEAD = "HEAD";
@@ -7791,7 +8272,7 @@ function listChanges2(entries) {
7791
8272
  return hidden > 0 ? [...shown, `${INDENT2}... and ${hidden} more`] : shown;
7792
8273
  }
7793
8274
  function trackedChanges(tree) {
7794
- return Object.freeze(tree.entries.filter((entry) => !entry.startsWith(UNTRACKED_PREFIX)));
8275
+ return Object.freeze(tree.entries.filter((entry) => !entry.startsWith(UNTRACKED_PREFIX2)));
7795
8276
  }
7796
8277
  function treeRefusal(tree, plan) {
7797
8278
  const changes = trackedChanges(tree);
@@ -8483,8 +8964,8 @@ function exhaustedMessage(roadmap, skipped2) {
8483
8964
  }
8484
8965
 
8485
8966
  // src/board/setup.ts
8486
- import { existsSync as existsSync13, lstatSync, mkdirSync as mkdirSync6, readFileSync as readFileSync13, statSync as statSync7, writeFileSync as writeFileSync6 } from "fs";
8487
- import { dirname as dirname8, join as join11 } from "path";
8967
+ import { existsSync as existsSync13, lstatSync, mkdirSync as mkdirSync7, readFileSync as readFileSync14, statSync as statSync7, writeFileSync as writeFileSync6 } from "fs";
8968
+ import { dirname as dirname9, join as join11 } from "path";
8488
8969
  import { fileURLToPath } from "url";
8489
8970
 
8490
8971
  // src/board/blocked.ts
@@ -8588,374 +9069,9 @@ function blockedFaultMessage(read) {
8588
9069
  return `#${String(read.issue)} ${faultClause(read)}; ${REMEDY2}`;
8589
9070
  }
8590
9071
 
8591
- // src/board/gate.ts
8592
- import { existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync10, renameSync as renameSync2, statSync as statSync4, writeFileSync as writeFileSync4 } from "fs";
8593
- import { basename as basename5, dirname as dirname6, join as join9, resolve as resolve7 } from "path";
8594
-
8595
- // src/board/flags.ts
8596
- var SPEC_FLAG = "--spec";
8597
- var ISSUE_FLAG = "--issue";
8598
- var NEXT_FLAG = "--next";
8599
- var REFRESH_FLAG = "--refresh";
8600
- var ROADMAP_FLAG = "--roadmap";
8601
- var CLAIM_AHEAD_FLAG = "--claim-ahead";
8602
- var DRY_RUN_FLAG = "--dry-run";
8603
- var SKIP_REVIEW_FLAG = "--skip-review";
8604
- var NO_COMMENT_FLAG = "--no-comment";
8605
- var ACCEPT_REFS_FLAG = "--accept-refs";
8606
-
8607
- // src/board/review-comment.ts
8608
- var SPEC_REVIEW_MARKER = "<!-- rafa:spec-review v1 -->";
8609
- var SPEC_REVIEW_COMMENT_NAME = "rafa:spec-review";
8610
- var ONE_LINE = /\s+/gu;
8611
- var COMMENT_REMEDY = "Close the gaps, label the issue `spec:ready` again, and plan from it again.";
8612
- var ASSUMED_COMMENT_REMEDY = "Answer these in the spec where an assumption is wrong, and plan from it" + " again. No label changed: none of these gaps blocked planning.";
8613
- var COMMENT_HISTORY = "This comment is edited in place each time the spec is reviewed.";
8614
- function counted2(count, noun) {
8615
- return count === 1 ? `1 ${noun}` : `${String(count)} ${noun}s`;
8616
- }
8617
- function gapItem(gap) {
8618
- return `- **${gap.heading.replace(ONE_LINE, " ").trim()}** \u2014 ${gap.what.replace(ONE_LINE, " ").trim()}`;
8619
- }
8620
- function assumedGapItem(gap) {
8621
- return gap.assumption === null ? gapItem(gap) : `${gapItem(gap)}
8622
- - Planned under: ${gap.assumption.replace(ONE_LINE, " ").trim()}`;
8623
- }
8624
- function requireGaps(gaps) {
8625
- if (gaps.length === 0) {
8626
- throw new TypeError("board review comment: no gap was named, and there is no comment to write");
8627
- }
8628
- }
8629
- function commentBody(found, items, remedy) {
8630
- return [SPEC_REVIEW_MARKER, ...found, "", ...items, "", remedy, "", COMMENT_HISTORY, ""].join(`
8631
- `);
8632
- }
8633
- function specReviewCommentBody(gaps) {
8634
- requireGaps(gaps);
8635
- return commentBody([`**rafa reviewed this spec before planning and found ${counted2(gaps.length, "gap")}.**`, "No plan was written."], gaps.map(gapItem), COMMENT_REMEDY);
8636
- }
8637
- function assumedReviewCommentBody(gaps) {
8638
- requireGaps(gaps);
8639
- return commentBody([
8640
- `**rafa reviewed this spec before planning and found ${counted2(gaps.length, "gap")}, none of them blocking.**`,
8641
- "The plan was written under the assumptions below."
8642
- ], gaps.map(assumedGapItem), ASSUMED_COMMENT_REMEDY);
8643
- }
8644
- function specReviewComments(comments) {
8645
- return [...comments].reverse().filter((comment) => comment.body.includes(SPEC_REVIEW_MARKER));
8646
- }
8647
- function ignoredReviewCommentMessage(comment, reading2, repo) {
8648
- const who = `was written by ${reading2.login}, ${trustRefusalClause(repo, reading2)}`;
8649
- return `the ${SPEC_REVIEW_COMMENT_NAME} comment ${comment.id} ${who};` + " it was left alone and the gaps went in a comment of their own";
8650
- }
8651
- async function readTrustedSpecReviewComment(comments, trust) {
8652
- const trustOf = memoisedTrust(trust);
8653
- const ignored = [];
8654
- for (const comment of specReviewComments(comments)) {
8655
- const reading2 = await trustOf(comment.author);
8656
- if (reading2.trusted)
8657
- return { comment, ignored };
8658
- ignored.push({
8659
- id: comment.id,
8660
- author: comment.author,
8661
- reason: ignoredReviewCommentMessage(comment, reading2, trust.repo)
8662
- });
8663
- }
8664
- return { comment: null, ignored };
8665
- }
8666
- async function writeSpecReviewComment(options) {
8667
- const { body, issue: issue3, board, trust } = options;
8668
- const found = await readTrustedSpecReviewComment(await board.comments(issue3), trust);
8669
- const { comment: existing, ignored } = found;
8670
- if (existing === null) {
8671
- return { action: "posted", comment: await board.comment(issue3, body), ignored };
8672
- }
8673
- return { action: "edited", comment: await board.editComment(existing.id, body), ignored };
8674
- }
8675
- function memoisedTrust(trust) {
8676
- const read = new Map;
8677
- return (login) => {
8678
- const taken = read.get(login) ?? readBoardTrust(trust, login);
8679
- read.set(login, taken);
8680
- return taken;
8681
- };
8682
- }
8683
-
8684
- // src/board/plan-field.ts
8685
- var PLAN_BLOCK_KIND = "plan";
8686
- var FIELD_NAME = /^[a-z][a-z-]*$/u;
8687
- var ISSUE_FIELD = "issue";
8688
- function unrecorded(answer, text, note) {
8689
- return { answer, text, recorded: false, note };
8690
- }
8691
- function indentOf(line) {
8692
- return /^\s*/u.exec(line)?.[0] ?? "";
8693
- }
8694
- function carriageOf(line) {
8695
- return line.endsWith("\r") ? "\r" : "";
8696
- }
8697
- function fieldLine(field2) {
8698
- if (!FIELD_NAME.test(field2)) {
8699
- throw new TypeError(`board plan field: ${JSON.stringify(field2)} is no rafa:plan key this records`);
8700
- }
8701
- return new RegExp(`^(\\s*)${field2}\\s*:(.*)$`, "u");
8702
- }
8703
- function stampPlanField(plan, field2, value) {
8704
- const pattern = fieldLine(field2);
8705
- const line = `${field2}: ${value}`;
8706
- const block = readRafaBlocks(plan).find((found) => found.kind === PLAN_BLOCK_KIND);
8707
- if (block === undefined) {
8708
- return unrecorded("no-block", plan, "the plan holds no rafa:plan block to record it in");
8709
- }
8710
- if (!block.closed) {
8711
- return unrecorded("unclosed-block", plan, `the rafa:plan block at line ${String(block.span.first)} is never closed, so nothing was recorded in it`);
8712
- }
8713
- const lines = plan.split(`
8714
- `);
8715
- const opening = lines[block.span.first - 1] ?? "";
8716
- const indent = indentOf(opening);
8717
- const written = `${indent}${line}${carriageOf(opening)}`;
8718
- for (let index = block.span.first;index < block.span.last - 1; index += 1) {
8719
- const found = pattern.exec(lines[index] ?? "");
8720
- if (found === null || found[1] !== indent)
8721
- continue;
8722
- if ((found[2] ?? "").trim() === value) {
8723
- return { answer: "unchanged", text: plan, recorded: true, note: `the plan already records ${line}` };
8724
- }
8725
- const replaced = [...lines];
8726
- replaced[index] = written;
8727
- return {
8728
- answer: "replaced",
8729
- text: replaced.join(`
8730
- `),
8731
- recorded: true,
8732
- note: `line ${String(index + 1)} of the plan now records ${line}`
8733
- };
8734
- }
8735
- const at = block.span.last - 1;
8736
- const stamped = [...lines.slice(0, at), written, ...lines.slice(at)];
8737
- return {
8738
- answer: "inserted",
8739
- text: stamped.join(`
8740
- `),
8741
- recorded: true,
8742
- note: `line ${String(at + 1)} of the plan now records ${line}`
8743
- };
8744
- }
8745
- function issueFieldLine(issue3) {
8746
- if (!Number.isSafeInteger(issue3) || issue3 < 1) {
8747
- throw new TypeError(`board plan field: issue ${String(issue3)} is no issue number, expected a positive whole one`);
8748
- }
8749
- return `${ISSUE_FIELD}: ${JSON.stringify(String(issue3))}`;
8750
- }
8751
- function stampPlanIssue(plan, issue3) {
8752
- return stampPlanField(plan, ISSUE_FIELD, JSON.stringify(String(issue3)));
8753
- }
8754
-
8755
- // src/board/review-stamp.ts
8756
- var REVIEW_FIELD = "review";
8757
- var REVIEW_SKIPPED = "skipped";
8758
- var REVIEW_MISSING = "missing";
8759
- var REVIEW_ASSUMED = "assumed";
8760
- var REVIEW_SKIPPED_LINE = `${REVIEW_FIELD}: ${REVIEW_SKIPPED}`;
8761
- var REVIEW_MISSING_LINE = `${REVIEW_FIELD}: ${REVIEW_MISSING}`;
8762
- var REVIEW_ASSUMED_LINE = `${REVIEW_FIELD}: ${REVIEW_ASSUMED}`;
8763
- function stampReviewSkipped(plan) {
8764
- return stampPlanField(plan, REVIEW_FIELD, REVIEW_SKIPPED);
8765
- }
8766
- function stampReviewMissing(plan) {
8767
- return stampPlanField(plan, REVIEW_FIELD, REVIEW_MISSING);
8768
- }
8769
- function stampReviewAssumed(plan) {
8770
- return stampPlanField(plan, REVIEW_FIELD, REVIEW_ASSUMED);
8771
- }
8772
- var ASSUMPTIONS_HEADING = "# Planned under assumptions";
8773
- var ASSUMPTIONS_PREAMBLE = [
8774
- "The spec review found gaps, none of them blocking, so this plan was",
8775
- "written under the assumptions below. Each names the gap it was",
8776
- "written for; closing that gap in the spec is what replaces the guess."
8777
- ];
8778
- function assumptionsHeading(gaps) {
8779
- const items = gaps.filter((gap) => gap.assumption !== null).map(assumedGapItem);
8780
- if (items.length === 0) {
8781
- throw new TypeError("board review stamp: no gap named an assumption, and there is no heading to open a plan with");
8782
- }
8783
- return [ASSUMPTIONS_HEADING, "", ...ASSUMPTIONS_PREAMBLE, "", ...items, "", ""].join(`
8784
- `);
8785
- }
8786
-
8787
- // src/board/gate.ts
8788
- var SPEC_NOT_READY_EXIT = 3;
8789
- var SPEC_NEEDS_WORK_LABEL = "spec:needs-work";
8790
- function readGateFlags(args) {
8791
- return {
8792
- skipReview: args.includes(SKIP_REVIEW_FLAG),
8793
- comment: !args.includes(NO_COMMENT_FLAG)
8794
- };
8795
- }
8796
- function describeGap2(gap) {
8797
- return ` \u2022 "${gap.heading}": ${gap.what}`;
8798
- }
8799
- function remedyFor(issue3) {
8800
- return issue3 === null ? " Close the gaps in the spec, then plan from it again." : ` Close the gaps, label the issue ${SPEC_READY_LABEL} again, and plan from it again.`;
8801
- }
8802
- function specNotReadyMessage(source, reading2, gaps, issue3 = null) {
8803
- return [
8804
- `\u274C ${source} is not ready to plan from: ${reading2}`,
8805
- ...gaps.map(describeGap2),
8806
- remedyFor(issue3)
8807
- ].join(`
8808
- `);
8809
- }
8810
- function unreadReviewWarning(reading2, planPath) {
8811
- return `${reading2}; ${planPath} reads as written, so the plan stands unreviewed`;
8812
- }
8813
- function assumedReviewWarning(reading2, planPath) {
8814
- return `${reading2}, none of them blocking; ${planPath} is written under the assumptions the review named`;
8815
- }
8816
- function unreadReviewMessage(source, reading2, planIssues) {
8817
- return [
8818
- `\u274C ${source}: no plan stands \u2014 ${reading2}, and the plan the session wrote does not read as written:`,
8819
- ...planIssues.map((line) => ` ${line}`),
8820
- " Plan from the spec again."
8821
- ].join(`
8822
- `);
8823
- }
8824
- function isWrittenFile(repoRoot, path2) {
8825
- const full = resolve7(repoRoot, path2);
8826
- return existsSync11(full) && statSync4(full).isFile();
8827
- }
8828
- var NOT_READY_REASON = "the planner judged the spec not ready, so no plan stands.";
8829
- var UNREAD_REASON = "no review came back and the plan does not read as written, so no plan stands.";
8830
- var REJECTED_DIR = "rejected";
8831
- function rejectedPath(path2) {
8832
- const parent = dirname6(path2);
8833
- return parent === "." ? join9(REJECTED_DIR, basename5(path2)) : join9(parent, REJECTED_DIR, basename5(path2));
8834
- }
8835
- function moveWritten(options, output, reason) {
8836
- for (const path2 of [options.planPath, options.prerequisitesPath]) {
8837
- if (!isWrittenFile(options.repoRoot, path2))
8838
- continue;
8839
- const destination = rejectedPath(path2);
8840
- try {
8841
- mkdirSync3(resolve7(options.repoRoot, dirname6(destination)), { recursive: true });
8842
- renameSync2(resolve7(options.repoRoot, path2), resolve7(options.repoRoot, destination));
8843
- output.info(`\uD83D\uDDC3 Moved ${path2} to ${destination}: ${reason}`);
8844
- } catch (error) {
8845
- output.warn(`${path2} was written against a refused review and could not be moved: ${messageOf(error)}`);
8846
- }
8847
- }
8848
- }
8849
- function readWrittenPlan(options) {
8850
- const { repoRoot, planPath } = options;
8851
- if (!isWrittenFile(repoRoot, planPath)) {
8852
- return { reads: false, issues: [`${planPath}: the session wrote no plan there`] };
8853
- }
8854
- try {
8855
- const { issues } = validatePlan(resolve7(repoRoot, planPath));
8856
- return { reads: issues.length === 0, issues: issues.map((issue3) => issueLine(planPath, issue3)) };
8857
- } catch (error) {
8858
- return { reads: false, issues: [`${planPath}: ${messageOf(error)}`] };
8859
- }
8860
- }
8861
- function standOrRefuse(options, review, output) {
8862
- const plan = readWrittenPlan(options);
8863
- if (plan.reads) {
8864
- output.warn(unreadReviewWarning(review.text, options.planPath));
8865
- return "unread";
8866
- }
8867
- moveWritten(options, output, UNREAD_REASON);
8868
- throw new CommandExit(SPEC_NOT_READY_EXIT, unreadReviewMessage(options.source, review.text, plan.issues));
8869
- }
8870
- function recordAssumptions(options, gaps, output) {
8871
- const { repoRoot, planPath } = options;
8872
- const unopened = (why) => {
8873
- output.warn(`${planPath} was not opened under the review's assumptions: ${why}`);
8874
- };
8875
- if (!isWrittenFile(repoRoot, planPath)) {
8876
- unopened("the session wrote no plan there");
8877
- return;
8878
- }
8879
- const file = resolve7(repoRoot, planPath);
8880
- let stamped;
8881
- try {
8882
- stamped = stampReviewAssumed(`${assumptionsHeading(gaps)}${readFileSync10(file, "utf8")}`);
8883
- writeFileSync4(file, stamped.text, "utf8");
8884
- } catch (error) {
8885
- unopened(messageOf(error));
8886
- return;
8887
- }
8888
- if (!stamped.recorded) {
8889
- output.warn(`${planPath} opens under the review's assumptions and records no ${REVIEW_ASSUMED_LINE}: ${stamped.note}`);
8890
- return;
8891
- }
8892
- output.info(`\uD83E\uDDED ${planPath} opens under the review's assumptions and records ${REVIEW_ASSUMED_LINE}.`);
8893
- }
8894
- async function publishGaps(issue3, gaps, body, output) {
8895
- try {
8896
- const write = await writeSpecReviewComment({
8897
- issue: issue3.number,
8898
- body: body(gaps),
8899
- board: issue3.board,
8900
- trust: issue3.trust
8901
- });
8902
- for (const passed2 of write.ignored)
8903
- output.warn(passed2.reason);
8904
- output.info(`\uD83D\uDCAC ${write.action === "posted" ? "Posted" : "Edited"} the review comment on issue #${String(issue3.number)}.`);
8905
- } catch (error) {
8906
- output.warn(`the review comment on issue #${String(issue3.number)} was not written: ${messageOf(error)}`);
8907
- }
8908
- }
8909
- async function swapLabels(issue3, output) {
8910
- try {
8911
- await issue3.board.swapLabels(issue3.number, SPEC_READY_LABEL, SPEC_NEEDS_WORK_LABEL);
8912
- output.info(`\uD83C\uDFF7 Swapped ${SPEC_READY_LABEL} for ${SPEC_NEEDS_WORK_LABEL} on issue #${String(issue3.number)}.`);
8913
- } catch (error) {
8914
- output.warn(`${SPEC_READY_LABEL} was not swapped for ${SPEC_NEEDS_WORK_LABEL} on issue` + ` #${String(issue3.number)}: ${messageOf(error)}`);
8915
- }
8916
- }
8917
- function nothingBlocks(review) {
8918
- return !review.gaps.some((gap) => gap.blocking);
8919
- }
8920
- async function planUnderAssumptions(options, review, output) {
8921
- output.warn(assumedReviewWarning(review.text, options.planPath));
8922
- recordAssumptions(options, review.gaps, output);
8923
- const { issue: issue3 } = options;
8924
- if (issue3 !== null) {
8925
- if (options.comment) {
8926
- await publishGaps(issue3, review.gaps, assumedReviewCommentBody, output);
8927
- } else {
8928
- output.info(`\uD83D\uDCAC ${NO_COMMENT_FLAG}: the gaps were not posted on issue #${String(issue3.number)}.`);
8929
- }
8930
- }
8931
- return "assumed";
8932
- }
8933
- async function enforceSpecReview(options) {
8934
- const { review, issue: issue3 } = options;
8935
- if (review === undefined)
8936
- return "unjudged";
8937
- if (review.ready)
8938
- return "ready";
8939
- const output = options.output ?? activeOutput();
8940
- if (review.answer !== "not-ready")
8941
- return standOrRefuse(options, review, output);
8942
- if (nothingBlocks(review))
8943
- return planUnderAssumptions(options, review, output);
8944
- moveWritten(options, output, NOT_READY_REASON);
8945
- if (issue3 !== null) {
8946
- if (options.comment) {
8947
- await publishGaps(issue3, review.gaps, specReviewCommentBody, output);
8948
- } else {
8949
- output.info(`\uD83D\uDCAC ${NO_COMMENT_FLAG}: the gaps were not posted on issue #${String(issue3.number)}.`);
8950
- }
8951
- await swapLabels(issue3, output);
8952
- }
8953
- throw new CommandExit(SPEC_NOT_READY_EXIT, specNotReadyMessage(options.source, review.text, review.gaps, issue3));
8954
- }
8955
-
8956
9072
  // src/board/issue.ts
8957
- import { existsSync as existsSync12, mkdirSync as mkdirSync5, readFileSync as readFileSync11, statSync as statSync6, writeFileSync as writeFileSync5 } from "fs";
8958
- import { dirname as dirname7, resolve as resolve9 } from "path";
9073
+ import { existsSync as existsSync12, mkdirSync as mkdirSync6, readFileSync as readFileSync12, statSync as statSync6, writeFileSync as writeFileSync5 } from "fs";
9074
+ import { dirname as dirname8, resolve as resolve10 } from "path";
8959
9075
 
8960
9076
  // src/refs/stamp.ts
8961
9077
  import { createHash } from "crypto";
@@ -9179,7 +9295,8 @@ function readStamp(entry, index) {
9179
9295
  const text = stringField(field2(entry, "text"), `${what}: text`);
9180
9296
  const fingerprint = readFingerprint(entry, what);
9181
9297
  if (!STAMPS_BY_KIND[refKind].includes(fingerprint.kind)) {
9182
- throw new RefsBlockError(`${what}: a ${refKind} is not stamped ${fingerprintText(fingerprint)}`);
9298
+ const article = /^[aeiou]/.test(refKind) ? "an" : "a";
9299
+ throw new RefsBlockError(`${what}: ${article} ${refKind} is not stamped ${fingerprintText(fingerprint)}`);
9183
9300
  }
9184
9301
  return Object.freeze({ kind: refKind, text, fingerprint });
9185
9302
  }
@@ -9224,8 +9341,8 @@ function carryRefsBlock(copy, text) {
9224
9341
  }
9225
9342
 
9226
9343
  // src/board/previous-copy.ts
9227
- import { linkSync, mkdirSync as mkdirSync4, readdirSync as readdirSync6, statSync as statSync5, unlinkSync } from "fs";
9228
- import { basename as basename6, extname, join as join10, resolve as resolve8 } from "path";
9344
+ import { linkSync, mkdirSync as mkdirSync5, readdirSync as readdirSync6, statSync as statSync5, unlinkSync } from "fs";
9345
+ import { basename as basename6, extname, join as join10, resolve as resolve9 } from "path";
9229
9346
  var PREFIX5 = "board previous copy";
9230
9347
  var PREVIOUS_DIR_NAME = "previous";
9231
9348
  var PREVIOUS_COPY_WARN_ABOVE = 50;
@@ -9264,14 +9381,14 @@ function linkUnderFreeName(source, directory, savedName, timestamp) {
9264
9381
  }
9265
9382
  function movePreviousCopy(options) {
9266
9383
  const { repoRoot, specsDir, path: path2 } = options;
9267
- const source = resolve8(repoRoot, path2);
9384
+ const source = resolve9(repoRoot, path2);
9268
9385
  const directory = previousDir(specsDir);
9269
- const absoluteDirectory = resolve8(repoRoot, directory);
9386
+ const absoluteDirectory = resolve9(repoRoot, directory);
9270
9387
  const savedName = basename6(path2);
9271
9388
  let name2;
9272
9389
  try {
9273
9390
  const timestamp = previousTimestamp(statSync5(source).mtime);
9274
- mkdirSync4(absoluteDirectory, { recursive: true });
9391
+ mkdirSync5(absoluteDirectory, { recursive: true });
9275
9392
  name2 = linkUnderFreeName(source, absoluteDirectory, savedName, timestamp);
9276
9393
  } catch (error) {
9277
9394
  throw new Error(`${PREFIX5}: ${path2} could not be moved to ${directory}: ${messageOf(error)}`, { cause: error });
@@ -9293,7 +9410,7 @@ function isMissing(error) {
9293
9410
  function countPreviousCopies(repoRoot, specsDir) {
9294
9411
  const directory = previousDir(specsDir);
9295
9412
  try {
9296
- return readdirSync6(resolve8(repoRoot, directory), { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(COPY_EXTENSION)).length;
9413
+ return readdirSync6(resolve9(repoRoot, directory), { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(COPY_EXTENSION)).length;
9297
9414
  } catch (error) {
9298
9415
  if (isMissing(error))
9299
9416
  return 0;
@@ -9497,13 +9614,13 @@ function readIfFile(file, what) {
9497
9614
  throw new CommandExit(ISSUE_REFUSAL_EXIT, `${what} ${file} is not a file, and a spec is one`);
9498
9615
  }
9499
9616
  try {
9500
- return readFileSync11(file, "utf8");
9617
+ return readFileSync12(file, "utf8");
9501
9618
  } catch (error) {
9502
9619
  throw new CommandExit(ISSUE_REFUSAL_EXIT, `${what} ${file} could not be read: ${messageOf(error)}`);
9503
9620
  }
9504
9621
  }
9505
9622
  function readLocalNotes(repoRoot, specsDir, issue3) {
9506
- return readIfFile(resolve9(repoRoot, notesPath(specsDir, issue3)), "the local notes");
9623
+ return readIfFile(resolve10(repoRoot, notesPath(specsDir, issue3)), "the local notes");
9507
9624
  }
9508
9625
  function snapshotDiffersMessage(path2, issue3) {
9509
9626
  return `${path2} is a snapshot of issue #${String(issue3)} that no longer matches it:` + " the issue body, or the local notes appended to it, changed since the snapshot was written;" + ` pass ${REFRESH_FLAG} to plan from the issue as it reads now, or --spec=${path2} to plan from` + " the snapshot as it stands";
@@ -9519,7 +9636,7 @@ function writeSpecSnapshot(options) {
9519
9636
  throw new CommandExit(ISSUE_REFUSAL_EXIT, notesCollisionMessage(path2, issue3.number));
9520
9637
  }
9521
9638
  const local = readLocalNotes(repoRoot, specsDir, issue3.number);
9522
- const absolute = resolve9(repoRoot, path2);
9639
+ const absolute = resolve10(repoRoot, path2);
9523
9640
  const existing = readIfFile(absolute, "the spec snapshot");
9524
9641
  const text = carryRefsBlock(existing, snapshotText(issue3.body, local));
9525
9642
  const written = {
@@ -9538,7 +9655,7 @@ function writeSpecSnapshot(options) {
9538
9655
  }
9539
9656
  const previous = action === "created" ? null : movePreviousCopy({ repoRoot, specsDir, path: path2 }).path;
9540
9657
  try {
9541
- mkdirSync5(dirname7(absolute), { recursive: true });
9658
+ mkdirSync6(dirname8(absolute), { recursive: true });
9542
9659
  writeFileSync5(absolute, text);
9543
9660
  } catch (error) {
9544
9661
  throw new Error(`${PREFIX6}: the snapshot ${path2} could not be written: ${messageOf(error)}`, { cause: error });
@@ -9557,7 +9674,7 @@ function rewriteAction(existing, body, notes, notesFile, refresh) {
9557
9674
  }
9558
9675
 
9559
9676
  // src/board/setup-config.ts
9560
- import { readFileSync as readFileSync12 } from "fs";
9677
+ import { readFileSync as readFileSync13 } from "fs";
9561
9678
  function roadmapIssueIn(text, path2) {
9562
9679
  return parseConfigText(text, path2).values.roadmapIssue ?? null;
9563
9680
  }
@@ -9565,7 +9682,7 @@ function readRoadmapSetting(root) {
9565
9682
  const path2 = configFilePath(root);
9566
9683
  let text;
9567
9684
  try {
9568
- text = readFileSync12(path2, "utf8");
9685
+ text = readFileSync13(path2, "utf8");
9569
9686
  } catch (error) {
9570
9687
  return { issue: null, text: "", problem: `${path2} could not be read: ${messageOf(error)}` };
9571
9688
  }
@@ -9615,7 +9732,7 @@ function withRoadmapIssue(text, issue3) {
9615
9732
 
9616
9733
  // src/board/setup.ts
9617
9734
  var PREFIX7 = "board setup";
9618
- var MODULE_DIR = dirname8(fileURLToPath(import.meta.url));
9735
+ var MODULE_DIR = dirname9(fileURLToPath(import.meta.url));
9619
9736
  var ROADMAP_LABEL = `${GITHUB_LABELS.typePrefix}roadmap`;
9620
9737
  var BOARD_LABELS = Object.freeze([
9621
9738
  {
@@ -9779,7 +9896,7 @@ function readSpecTemplate(moduleDir = MODULE_DIR) {
9779
9896
  if (!existsSync13(source)) {
9780
9897
  throw new Error(`${PREFIX7}: the spec issue template is missing: no file at ${source}`);
9781
9898
  }
9782
- return readFileSync13(source, "utf8");
9899
+ return readFileSync14(source, "utf8");
9783
9900
  }
9784
9901
  function writeShippedFile(root, file) {
9785
9902
  const path2 = join11(root, file.path);
@@ -9793,7 +9910,7 @@ function writeShippedFile(root, file) {
9793
9910
  }
9794
9911
  try {
9795
9912
  const text = file.read();
9796
- mkdirSync6(dirname8(path2), { recursive: true });
9913
+ mkdirSync7(dirname9(path2), { recursive: true });
9797
9914
  writeFileSync6(path2, text, { encoding: "utf8", flag: "wx" });
9798
9915
  return partOf("template", name2, "created", `written from ${file.source}`);
9799
9916
  } catch (error) {
@@ -11935,14 +12052,14 @@ function withSubIssuesSummary(epic, row, claims, today) {
11935
12052
  import { existsSync as existsSync15, readdirSync as readdirSync8 } from "fs";
11936
12053
 
11937
12054
  // src/commands/doctor-refs.ts
11938
- import { readdirSync as readdirSync7, readFileSync as readFileSync19, statSync as statSync9 } from "fs";
11939
- import { join as join13, resolve as resolve12 } from "path";
12055
+ import { readdirSync as readdirSync7, readFileSync as readFileSync20, statSync as statSync9 } from "fs";
12056
+ import { join as join13, resolve as resolve13 } from "path";
11940
12057
 
11941
12058
  // src/board/refs-gate.ts
11942
- import { readFileSync as readFileSync16 } from "fs";
12059
+ import { readFileSync as readFileSync17 } from "fs";
11943
12060
 
11944
12061
  // src/refs/reading.ts
11945
- import { readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
12062
+ import { readFileSync as readFileSync15, writeFileSync as writeFileSync7 } from "fs";
11946
12063
 
11947
12064
  // src/refs/extract.ts
11948
12065
  var FILE_EXTENSIONS = new Set([
@@ -12218,7 +12335,7 @@ async function restampRefsText(options) {
12218
12335
  return answer2(rows, options.copy, body, Object.freeze(stamps), true);
12219
12336
  }
12220
12337
  async function overFile(options, reading2) {
12221
- const copy = readFileSync14(options.path, "utf8");
12338
+ const copy = readFileSync15(options.path, "utf8");
12222
12339
  const { issue: issue3, verify, blockers } = options;
12223
12340
  const read = await reading2(blockers === undefined ? { copy, issue: issue3, verify } : { copy, issue: issue3, verify, blockers });
12224
12341
  if (read.changed)
@@ -12669,8 +12786,8 @@ function aheadNotClaimedWarning(ahead) {
12669
12786
  }
12670
12787
 
12671
12788
  // src/board/snapshot-settle.ts
12672
- import { existsSync as existsSync14, readFileSync as readFileSync15, statSync as statSync8 } from "fs";
12673
- import { resolve as resolve10 } from "path";
12789
+ import { existsSync as existsSync14, readFileSync as readFileSync16, statSync as statSync8 } from "fs";
12790
+ import { resolve as resolve11 } from "path";
12674
12791
  var PREFIX15 = "board snapshot settle";
12675
12792
  var DATE_LENGTH = 10;
12676
12793
  function savedCopyDate(savedAt) {
@@ -12689,7 +12806,7 @@ function readSavedCopy(absolute, path2) {
12689
12806
  const stat = statSync8(absolute);
12690
12807
  if (!stat.isFile())
12691
12808
  return null;
12692
- return { text: readFileSync15(absolute, "utf8"), savedAt: stat.mtime };
12809
+ return { text: readFileSync16(absolute, "utf8"), savedAt: stat.mtime };
12693
12810
  } catch (error) {
12694
12811
  throw new Error(`${PREFIX15}: the saved copy ${path2} could not be read: ${messageOf(error)}`, { cause: error });
12695
12812
  }
@@ -12707,7 +12824,7 @@ async function settleSpecSnapshot(options) {
12707
12824
  const notes = notesPath(specsDir, issue3.number);
12708
12825
  if (path2 === notes)
12709
12826
  return write(refresh);
12710
- const saved = readSavedCopy(resolve10(repoRoot, path2), path2);
12827
+ const saved = readSavedCopy(resolve11(repoRoot, path2), path2);
12711
12828
  if (saved === null)
12712
12829
  return write(refresh);
12713
12830
  const change = readSnapshotChange({
@@ -13459,7 +13576,7 @@ function memoiseVerifier(verify) {
13459
13576
  }
13460
13577
  async function acceptAll(options, acceptance, output) {
13461
13578
  const verify = memoiseVerifier(options.verify);
13462
- const before = await readRefsText({ copy: readFileSync16(options.path, "utf8"), issue: options.issue, verify });
13579
+ const before = await readRefsText({ copy: readFileSync17(options.path, "utf8"), issue: options.issue, verify });
13463
13580
  const after = await restampCopyRefs({ path: options.path, issue: options.issue, verify });
13464
13581
  const accepted = before.rows.filter(isRefusedRow);
13465
13582
  listPassingRows(before.rows, output);
@@ -13489,12 +13606,12 @@ async function enforceRefsGate(options) {
13489
13606
 
13490
13607
  // src/refs/outline-cache.ts
13491
13608
  import { createHash as createHash2 } from "crypto";
13492
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync17, renameSync as renameSync3, writeFileSync as writeFileSync8 } from "fs";
13493
- import { join as join12, resolve as resolve11 } from "path";
13609
+ import { mkdirSync as mkdirSync8, readFileSync as readFileSync18, renameSync as renameSync4, writeFileSync as writeFileSync8 } from "fs";
13610
+ import { join as join12, resolve as resolve12 } from "path";
13494
13611
  var OUTLINE_CACHE_DIR = join12(SCOPE_DIR, "cache", "outline", "v2");
13495
13612
  function readKept(path2) {
13496
13613
  try {
13497
- const payload = JSON.parse(readFileSync17(path2, "utf8"));
13614
+ const payload = JSON.parse(readFileSync18(path2, "utf8"));
13498
13615
  return Array.isArray(payload) && payload.every((name2) => typeof name2 === "string") ? payload : null;
13499
13616
  } catch {
13500
13617
  return null;
@@ -13503,14 +13620,14 @@ function readKept(path2) {
13503
13620
  function keep(path2, dir, names) {
13504
13621
  const temporary = `${path2}.${String(process.pid)}.tmp`;
13505
13622
  try {
13506
- mkdirSync7(dir, { recursive: true });
13623
+ mkdirSync8(dir, { recursive: true });
13507
13624
  writeFileSync8(temporary, JSON.stringify(names));
13508
- renameSync3(temporary, path2);
13625
+ renameSync4(temporary, path2);
13509
13626
  } catch {}
13510
13627
  }
13511
13628
  function contentKey(root, file) {
13512
13629
  try {
13513
- return createHash2("sha256").update(readFileSync17(resolve11(root, file))).digest("hex");
13630
+ return createHash2("sha256").update(readFileSync18(resolve12(root, file))).digest("hex");
13514
13631
  } catch {
13515
13632
  return null;
13516
13633
  }
@@ -13535,7 +13652,7 @@ function withOutlineCache(outline, root) {
13535
13652
  }
13536
13653
 
13537
13654
  // src/commands/plan/refs-check.ts
13538
- import { readFileSync as readFileSync18 } from "fs";
13655
+ import { readFileSync as readFileSync19 } from "fs";
13539
13656
  import path2 from "path";
13540
13657
  var RAFA_PACKAGE_NAME = "@open-tomato/rafa";
13541
13658
  var RAFA_ENTRY = "src/rafa.ts";
@@ -13543,7 +13660,7 @@ var CHECKOUT_ROSTER_TIMEOUT_MS = 30000;
13543
13660
  var DESCRIBE_LINE = `bun ${RAFA_ENTRY} describe --output=json`;
13544
13661
  async function coreRoster() {
13545
13662
  const [{ CORE_REGISTRY }, { describeRegistry }] = await Promise.all([
13546
- import("./index-mzcz139z.js"),
13663
+ import("./index-psv1sxkv.js"),
13547
13664
  import("./describe-pwhh0vws.js")
13548
13665
  ]);
13549
13666
  return describeRegistry(CORE_REGISTRY, version);
@@ -13592,7 +13709,7 @@ function resultData(stdout) {
13592
13709
  }
13593
13710
  function isRafaPackage(repoRoot) {
13594
13711
  try {
13595
- const manifest = recordOf(JSON.parse(readFileSync18(path2.join(repoRoot, "package.json"), "utf8")));
13712
+ const manifest = recordOf(JSON.parse(readFileSync19(path2.join(repoRoot, "package.json"), "utf8")));
13596
13713
  return manifest?.["name"] === RAFA_PACKAGE_NAME;
13597
13714
  } catch {
13598
13715
  return false;
@@ -13761,7 +13878,7 @@ async function readCopy(file, input, verify) {
13761
13878
  const path3 = join13(input.specsDir, file.name);
13762
13879
  const empty = { issue: file.issue, path: path3, suspect: 0, dangling: 0, unknown: 0 };
13763
13880
  try {
13764
- const copy = readFileSync19(resolve12(input.root, path3), "utf8");
13881
+ const copy = readFileSync20(resolve13(input.root, path3), "utf8");
13765
13882
  const { rows } = await readRefsText({ copy, issue: file.issue, verify: await verify() });
13766
13883
  const states = rows.map((row) => row.state);
13767
13884
  return Object.freeze({
@@ -13781,7 +13898,7 @@ function total(copies, field4) {
13781
13898
  async function readDoctorRefs(input, seams = {}) {
13782
13899
  let files;
13783
13900
  try {
13784
- const listed = listCopies(resolve12(input.root, input.specsDir));
13901
+ const listed = listCopies(resolve13(input.root, input.specsDir));
13785
13902
  const only = input.issues === undefined ? null : new Set(input.issues);
13786
13903
  files = listed.filter((file) => only === null || only.has(file.issue));
13787
13904
  } catch (error) {
@@ -17507,7 +17624,7 @@ function createIssueListCommand(seams = DEFAULT_ISSUE_SEAMS) {
17507
17624
  var list_default = createIssueListCommand();
17508
17625
 
17509
17626
  // src/commands/release/status.ts
17510
- import { existsSync as existsSync17, readdirSync as readdirSync9, readFileSync as readFileSync24 } from "fs";
17627
+ import { existsSync as existsSync17, readdirSync as readdirSync9, readFileSync as readFileSync25 } from "fs";
17511
17628
  import { join as join17 } from "path";
17512
17629
 
17513
17630
  // src/effort/store/changes.ts
@@ -17606,15 +17723,15 @@ function readPlanChanges(repoRoot, planStub2) {
17606
17723
  }
17607
17724
 
17608
17725
  // src/release/audit.ts
17609
- import { readFileSync as readFileSync22 } from "fs";
17726
+ import { readFileSync as readFileSync23 } from "fs";
17610
17727
  import { join as join15 } from "path";
17611
17728
 
17612
17729
  // src/release/receipt.ts
17613
- import { readFileSync as readFileSync21 } from "fs";
17730
+ import { readFileSync as readFileSync22 } from "fs";
17614
17731
  import { join as join14 } from "path";
17615
17732
 
17616
17733
  // src/release/version.ts
17617
- import { readFileSync as readFileSync20, writeFileSync as writeFileSync9 } from "fs";
17734
+ import { readFileSync as readFileSync21, writeFileSync as writeFileSync9 } from "fs";
17618
17735
  var RELEASE_REMOTE = "origin";
17619
17736
  var RELEASE_BASE_BRANCH = "main";
17620
17737
  var VERSION_SHAPE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$/;
@@ -17706,7 +17823,7 @@ function writeManifestVersion(path3, next) {
17706
17823
  const unwritten = { path: path3, written: false, previous: null, version: next };
17707
17824
  let text;
17708
17825
  try {
17709
- text = readFileSync20(path3, "utf8");
17826
+ text = readFileSync21(path3, "utf8");
17710
17827
  } catch (error) {
17711
17828
  return { ...unwritten, problem: `${path3} could not be read: ${messageOf(error)}` };
17712
17829
  }
@@ -17814,7 +17931,7 @@ function receiptProblem(verdict, version2, path3) {
17814
17931
  function readReceiptVerdict(root, configured, version2) {
17815
17932
  let text;
17816
17933
  try {
17817
- text = readFileSync21(join14(root, configured), "utf8");
17934
+ text = readFileSync22(join14(root, configured), "utf8");
17818
17935
  } catch {
17819
17936
  return null;
17820
17937
  }
@@ -17915,7 +18032,7 @@ function auditSentence(finding) {
17915
18032
  function readAudit(root, configured, tagged) {
17916
18033
  let text;
17917
18034
  try {
17918
- text = readFileSync22(join15(root, configured), "utf8");
18035
+ text = readFileSync23(join15(root, configured), "utf8");
17919
18036
  } catch {
17920
18037
  return { path: configured, read: false, sections: 0, findings: [] };
17921
18038
  }
@@ -18286,8 +18403,9 @@ function forecastSettle(input) {
18286
18403
  }
18287
18404
 
18288
18405
  // src/release/settle.ts
18289
- import { readFileSync as readFileSync23, writeFileSync as writeFileSync10 } from "fs";
18406
+ import { readFileSync as readFileSync24, writeFileSync as writeFileSync10 } from "fs";
18290
18407
  import { join as join16 } from "path";
18408
+ var CHANGELOG_TITLE = "# Changelog";
18291
18409
  function releaseCommitSubject(version2) {
18292
18410
  return `chore: release ${version2}`;
18293
18411
  }
@@ -18325,7 +18443,17 @@ function readSettle(git, tree, settings) {
18325
18443
  return { ...basis, outcome: "failed", line: folded.line };
18326
18444
  if (folded.result === null)
18327
18445
  return { ...basis, outcome: "nothing" };
18328
- return { ...basis, outcome: "folded", version: folded.result.version, section: folded.result.section };
18446
+ const changelog = changelogMissingAt(git, commit, settings.releaseChangelog);
18447
+ if (typeof changelog === "object")
18448
+ return { outcome: "unread", strategy: strategy.name, problem: changelog.problem };
18449
+ return { ...basis, outcome: "folded", version: folded.result.version, section: folded.result.section, changelogMissing: changelog };
18450
+ }
18451
+ function changelogMissingAt(git, commit, changelog) {
18452
+ const path3 = gitPathOf(changelog);
18453
+ const listed = git(["ls-tree", "--name-only", commit, "--", path3]);
18454
+ if (!listed.ok)
18455
+ return { problem: `${commit}:${path3} could not be listed: ${gitSaid(listed)}` };
18456
+ return listed.stdout.trim() === "";
18329
18457
  }
18330
18458
  function unbuilt(reading2, problem) {
18331
18459
  return { ...reading2, outcome: "unbuilt", problem };
@@ -18342,18 +18470,24 @@ function writeVersion(root, reading2, versionFile) {
18342
18470
  function writeChangelog(root, reading2, changelog) {
18343
18471
  const path3 = join16(root, gitPathOf(changelog));
18344
18472
  let text;
18473
+ let created = false;
18345
18474
  try {
18346
- text = readFileSync23(path3, "utf8");
18475
+ text = readFileSync24(path3, "utf8");
18347
18476
  } catch (error) {
18348
- return { problem: `${changelog} could not be read: ${messageOf(error)}` };
18477
+ if (errorCode(error) !== "ENOENT")
18478
+ return { problem: `${changelog} could not be read: ${messageOf(error)}` };
18479
+ text = `${CHANGELOG_TITLE}
18480
+ `;
18481
+ created = true;
18349
18482
  }
18350
18483
  const inserted = insertChangelogEntry(text, reading2.section);
18484
+ const verb = created ? "created" : "written";
18351
18485
  try {
18352
18486
  writeFileSync10(path3, inserted.text);
18353
18487
  } catch (error) {
18354
- return { problem: `${changelog} could not be written: ${messageOf(error)}` };
18488
+ return { problem: `${changelog} could not be ${verb}: ${messageOf(error)}` };
18355
18489
  }
18356
- return { point: inserted.point };
18490
+ return { point: inserted.point, created };
18357
18491
  }
18358
18492
  function commitRelease(git, reading2, settings) {
18359
18493
  const files = [gitPathOf(settings.releaseVersionFile), gitPathOf(settings.releaseChangelog)];
@@ -18392,7 +18526,8 @@ function buildSettle(worktree, settings) {
18392
18526
  outcome: "built",
18393
18527
  release: committed.release,
18394
18528
  deleted: reading2.fragments.map((each) => each.path),
18395
- insertPoint: changelog.point
18529
+ insertPoint: changelog.point,
18530
+ createdChangelog: changelog.created ? settings.releaseChangelog : null
18396
18531
  };
18397
18532
  }
18398
18533
 
@@ -18485,6 +18620,7 @@ var CODE_FENCE4 = /^ {0,3}(?:```|~~~)/;
18485
18620
  var RELEASE_TAG = /^v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)$/;
18486
18621
  var NUMERIC_IDENTIFIER = /^(?:0|[1-9]\d*)$/;
18487
18622
  var DEFAULT_RELEASE_SEAMS = Object.freeze({});
18623
+ var MISSING_CHANGELOG_CELL = "missing; rafa release settle creates it";
18488
18624
  function compareIdentifiers(left, right) {
18489
18625
  const leftNumeric = NUMERIC_IDENTIFIER.test(left);
18490
18626
  const rightNumeric = NUMERIC_IDENTIFIER.test(right);
@@ -18574,7 +18710,7 @@ function untaggedVersions(released, tags) {
18574
18710
  }
18575
18711
  function textOf(path3) {
18576
18712
  try {
18577
- return readFileSync24(path3, "utf8");
18713
+ return readFileSync25(path3, "utf8");
18578
18714
  } catch {
18579
18715
  return null;
18580
18716
  }
@@ -18604,11 +18740,12 @@ function readUntagged(root, configured, tags) {
18604
18740
  path: configured,
18605
18741
  released: [],
18606
18742
  versions: [],
18743
+ missing: !existsSync17(resolved),
18607
18744
  problem: `the changelog could not be read at ${resolved}`
18608
18745
  };
18609
18746
  }
18610
18747
  const released = changelogVersions(text);
18611
- return { path: configured, released, versions: untaggedVersions(released, tags), problem: null };
18748
+ return { path: configured, released, versions: untaggedVersions(released, tags), missing: false, problem: null };
18612
18749
  }
18613
18750
  function readBranch(git) {
18614
18751
  const result = git(["rev-parse", "--abbrev-ref", "HEAD"]);
@@ -18676,6 +18813,8 @@ function tagCell(reading2) {
18676
18813
  return reading2.latest === null ? `${NOTHING}, ${held}` : `${reading2.latest.tag}, ${held}`;
18677
18814
  }
18678
18815
  function untaggedCell(reading2) {
18816
+ if (reading2.missing)
18817
+ return MISSING_CHANGELOG_CELL;
18679
18818
  if (reading2.problem !== null)
18680
18819
  return UNREADABLE2;
18681
18820
  if (reading2.released.length === 0)
@@ -18700,7 +18839,7 @@ function problemsOf(reading2) {
18700
18839
  return [
18701
18840
  reading2.versionFile.problem,
18702
18841
  reading2.tags.problem,
18703
- reading2.untagged.problem,
18842
+ reading2.untagged.missing ? null : reading2.untagged.problem,
18704
18843
  reading2.plan.problem,
18705
18844
  ...reading2.waiting.problems
18706
18845
  ].filter((problem) => problem !== null);
@@ -18714,7 +18853,7 @@ function renderStatus(reading2) {
18714
18853
  untaggedCell(reading2.untagged),
18715
18854
  pendingCell(reading2.plan),
18716
18855
  waitingCell(reading2.waiting) ?? UNREADABLE2,
18717
- auditCell(reading2.audit) ?? UNREADABLE2
18856
+ auditCell(reading2.audit) ?? (reading2.untagged.missing ? MISSING_CHANGELOG_CELL : UNREADABLE2)
18718
18857
  ];
18719
18858
  const [versionLine, tagLine, untaggedLine, pendingLine, waitingLine, auditLine] = LABELS.map((label, index) => `${INDENT3}${label.padEnd(width)}${GAP}${cells[index] ?? ""}`);
18720
18859
  const under = (line) => `${INDENT3}${" ".repeat(width)}${GAP}${line}`;
@@ -18834,7 +18973,7 @@ function createReleaseStatusCommand(seams = DEFAULT_RELEASE_SEAMS) {
18834
18973
  var status_default = createReleaseStatusCommand();
18835
18974
 
18836
18975
  // src/project/bin-path.ts
18837
- import { delimiter as delimiter2, isAbsolute as isAbsolute3, join as join18, resolve as resolve13 } from "path";
18976
+ import { delimiter as delimiter2, isAbsolute as isAbsolute3, join as join18, resolve as resolve14 } from "path";
18838
18977
  var RAFA_BIN_DIR = join18(SCOPE_DIR, "bin");
18839
18978
  var BUN_BIN_DIR = join18(".bun", "bin");
18840
18979
  function exportLine(rafaBin) {
@@ -18853,7 +18992,7 @@ function canonical(path3, fs) {
18853
18992
  try {
18854
18993
  return fs.realpath(path3);
18855
18994
  } catch {
18856
- return resolve13(path3);
18995
+ return resolve14(path3);
18857
18996
  }
18858
18997
  }
18859
18998
  function indexOf(entries, dir, fs) {
@@ -18883,21 +19022,21 @@ import {
18883
19022
  chmodSync,
18884
19023
  copyFileSync,
18885
19024
  existsSync as existsSync19,
18886
- mkdirSync as mkdirSync8,
19025
+ mkdirSync as mkdirSync9,
18887
19026
  readdirSync as readdirSync11,
18888
- readFileSync as readFileSync25,
19027
+ readFileSync as readFileSync26,
18889
19028
  realpathSync as realpathSync2,
18890
- renameSync as renameSync4,
19029
+ renameSync as renameSync5,
18891
19030
  rmSync as rmSync2,
18892
19031
  statSync as statSync11,
18893
19032
  symlinkSync
18894
19033
  } from "fs";
18895
- import { basename as basename7, delimiter as delimiter3, dirname as dirname9, join as join20, relative as relative6, resolve as resolve15 } from "path";
19034
+ import { basename as basename7, delimiter as delimiter3, dirname as dirname10, join as join20, relative as relative6, resolve as resolve16 } from "path";
18896
19035
 
18897
19036
  // src/start/runtime.ts
18898
19037
  import { existsSync as existsSync18, readdirSync as readdirSync10, realpathSync, statSync as statSync10 } from "fs";
18899
19038
  import { homedir as homedir5 } from "os";
18900
- import { isAbsolute as isAbsolute4, join as join19, relative as relative5, resolve as resolve14, sep } from "path";
19039
+ import { isAbsolute as isAbsolute4, join as join19, relative as relative5, resolve as resolve15, sep } from "path";
18901
19040
 
18902
19041
  // src/start/session.ts
18903
19042
  import { randomUUID as randomUUID4 } from "crypto";
@@ -19119,7 +19258,7 @@ function resolveRuntime(value, places) {
19119
19258
  throw refusal2([`\u274C Refusing to start: \`--runtime\` names no runtime. Spell it ${SPELLING}.`]);
19120
19259
  }
19121
19260
  const isVersion = VERSION_SHAPE2.test(value);
19122
- const named3 = isVersion ? join19(places.home, RUNTIME_SUBDIR, value, RUNTIME_ENTRY) : resolve14(places.cwd, value);
19261
+ const named3 = isVersion ? join19(places.home, RUNTIME_SUBDIR, value, RUNTIME_ENTRY) : resolve15(places.cwd, value);
19123
19262
  refuseInsideSource(value, named3, places);
19124
19263
  const entry = entryOf2(value, named3, isVersion, places.home);
19125
19264
  const real = realpathSync(entry);
@@ -19212,7 +19351,7 @@ function realOrResolved(path3) {
19212
19351
  try {
19213
19352
  return realpathSync(path3);
19214
19353
  } catch {
19215
- return resolve14(path3);
19354
+ return resolve15(path3);
19216
19355
  }
19217
19356
  }
19218
19357
  async function runRuntime(entry, words, run) {
@@ -19360,17 +19499,17 @@ function runtimeRefusalLines(runtimeDir, version2) {
19360
19499
  ];
19361
19500
  }
19362
19501
  function listTrackers(repoRoot, planDir) {
19363
- const dir = resolve15(repoRoot, planDir);
19502
+ const dir = resolve16(repoRoot, planDir);
19364
19503
  return entryNames(dir).filter((entry) => TRACKER_NAME.test(entry)).sort((a, b) => a.localeCompare(b)).map((entry) => join20(dir, entry)).filter((path3) => statSync11(path3).isFile()).map((path3) => relative6(repoRoot, path3));
19365
19504
  }
19366
19505
  function openTrackers(repoRoot, planDir) {
19367
19506
  return listTrackers(repoRoot, planDir).flatMap((path3) => {
19368
- const task = findNextTask(readFileSync25(join20(repoRoot, path3), "utf8"));
19507
+ const task = findNextTask(readFileSync26(join20(repoRoot, path3), "utf8"));
19369
19508
  return task ? [{ path: path3, task }] : [];
19370
19509
  });
19371
19510
  }
19372
19511
  function readManifestVersion2(repoRoot) {
19373
- const manifest = JSON.parse(readFileSync25(join20(repoRoot, "package.json"), "utf8"));
19512
+ const manifest = JSON.parse(readFileSync26(join20(repoRoot, "package.json"), "utf8"));
19374
19513
  const record = typeof manifest === "object" && manifest !== null ? manifest : {};
19375
19514
  if (record["name"] !== name) {
19376
19515
  throw new Error(`${join20(repoRoot, "package.json")} names ${JSON.stringify(record["name"])}, not ${name}: this is no rafa checkout`);
@@ -19382,7 +19521,7 @@ function readManifestVersion2(repoRoot) {
19382
19521
  return version2;
19383
19522
  }
19384
19523
  function copyTree(from, to) {
19385
- mkdirSync8(to, { recursive: true });
19524
+ mkdirSync9(to, { recursive: true });
19386
19525
  let copied = 0;
19387
19526
  const entries = readdirSync11(from, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
19388
19527
  for (const entry of entries) {
@@ -19413,27 +19552,27 @@ function replaceTree(from, to) {
19413
19552
  }
19414
19553
  function swapIn(staging, to) {
19415
19554
  if (!existsSync19(to)) {
19416
- renameSync4(staging, to);
19555
+ renameSync5(staging, to);
19417
19556
  return;
19418
19557
  }
19419
19558
  const outgoing = `${temporaryName(to)}.outgoing`;
19420
19559
  rmSync2(outgoing, { recursive: true, force: true });
19421
- renameSync4(to, outgoing);
19560
+ renameSync5(to, outgoing);
19422
19561
  try {
19423
- renameSync4(staging, to);
19562
+ renameSync5(staging, to);
19424
19563
  } catch (err) {
19425
- renameSync4(outgoing, to);
19564
+ renameSync5(outgoing, to);
19426
19565
  throw err;
19427
19566
  }
19428
19567
  rmSync2(outgoing, { recursive: true, force: true });
19429
19568
  }
19430
19569
  function relink(linkPath, target) {
19431
- mkdirSync8(dirname9(linkPath), { recursive: true });
19570
+ mkdirSync9(dirname10(linkPath), { recursive: true });
19432
19571
  const temporary = temporaryName(linkPath);
19433
19572
  rmSync2(temporary, { force: true });
19434
19573
  try {
19435
19574
  symlinkSync(target, temporary);
19436
- renameSync4(temporary, linkPath);
19575
+ renameSync5(temporary, linkPath);
19437
19576
  } catch (err) {
19438
19577
  rmSync2(temporary, { force: true });
19439
19578
  throw err;
@@ -19451,7 +19590,7 @@ function runBuild(repoRoot, lines) {
19451
19590
  cwd: repoRoot,
19452
19591
  env: {
19453
19592
  ...process.env,
19454
- PATH: [dirname9(process.execPath), process.env["PATH"] ?? ""].join(delimiter3)
19593
+ PATH: [dirname10(process.execPath), process.env["PATH"] ?? ""].join(delimiter3)
19455
19594
  },
19456
19595
  stdin: "ignore",
19457
19596
  stdout: stdio,
@@ -19488,7 +19627,7 @@ function runInstall(seams, options) {
19488
19627
  log(`runtime root: ${runtimeRoot}`);
19489
19628
  log(`link: ${linkPath}`);
19490
19629
  const version2 = inStage("manifest", () => readManifestVersion2(repoRoot));
19491
- const planDir = inStage("config", () => resolve15(repoRoot, loadConfig({ root: repoRoot, home }, {}, seams.warn).config.planDir));
19630
+ const planDir = inStage("config", () => resolve16(repoRoot, loadConfig({ root: repoRoot, home }, {}, seams.warn).config.planDir));
19492
19631
  log(`plan dir: ${planDir}`);
19493
19632
  const open = inStage("trackers", () => openTrackers(repoRoot, planDir));
19494
19633
  if (open.length > 0)
@@ -19536,14 +19675,14 @@ function replaceFile(source, target) {
19536
19675
  try {
19537
19676
  copyFileSync(source, temporary);
19538
19677
  chmodSync(temporary, statSync11(source).mode & 4095);
19539
- renameSync4(temporary, target);
19678
+ renameSync5(temporary, target);
19540
19679
  } catch (err) {
19541
19680
  rmSync2(temporary, { force: true });
19542
19681
  throw err;
19543
19682
  }
19544
19683
  }
19545
19684
  function temporaryName(path3) {
19546
- return join20(dirname9(path3), `.${basename7(path3)}.snapshot-${process.pid}`);
19685
+ return join20(dirname10(path3), `.${basename7(path3)}.snapshot-${process.pid}`);
19547
19686
  }
19548
19687
 
19549
19688
  // src/commands/self-update.ts
@@ -19651,7 +19790,7 @@ function createSelfUpdateCommand(seams = DEFAULT_SELF_UPDATE_SEAMS) {
19651
19790
  var self_update_default = createSelfUpdateCommand();
19652
19791
 
19653
19792
  // src/commands/plan/list.ts
19654
- import { existsSync as existsSync20, readdirSync as readdirSync12, readFileSync as readFileSync26 } from "fs";
19793
+ import { existsSync as existsSync20, readdirSync as readdirSync12, readFileSync as readFileSync27 } from "fs";
19655
19794
  import { join as join21 } from "path";
19656
19795
  var USAGE5 = "rafa plan list";
19657
19796
  var OPEN_HINT = "Type it bare: rafa plan list --open";
@@ -19659,8 +19798,8 @@ function listing(dir, stub) {
19659
19798
  const plan = join21(dir, planFileName(stub, false));
19660
19799
  const trackerPath = join21(dir, planFileName(stub, true));
19661
19800
  const tracker = isFile(trackerPath) ? trackerPath : null;
19662
- const planModel = parsePlan(readFileSync26(plan, "utf8"));
19663
- const counted4 = tracker === null ? planModel : parsePlan(readFileSync26(tracker, "utf8"));
19801
+ const planModel = parsePlan(readFileSync27(plan, "utf8"));
19802
+ const counted4 = tracker === null ? planModel : parsePlan(readFileSync27(tracker, "utf8"));
19664
19803
  return { stub, plan, tracker, tasks: countTasks(counted4.tasks), issues: planModel.issues.length };
19665
19804
  }
19666
19805
  function listPlans(plans) {
@@ -19849,7 +19988,7 @@ async function openNextRelations(context, seams) {
19849
19988
 
19850
19989
  // src/commands/loop/loop-sessions.ts
19851
19990
  import { execFileSync } from "child_process";
19852
- import { readFileSync as readFileSync27 } from "fs";
19991
+ import { readFileSync as readFileSync28 } from "fs";
19853
19992
  import { join as join22, relative as relative7 } from "path";
19854
19993
 
19855
19994
  // src/effort/store/task-finishes.ts
@@ -20006,15 +20145,20 @@ function readSessionChecklist(root, record) {
20006
20145
  const file = [trackerPathFor(plan), plan].find((path3) => isFile(path3));
20007
20146
  if (file === undefined)
20008
20147
  return null;
20009
- const content = readFileSync27(file, "utf8");
20148
+ const content = readFileSync28(file, "utf8");
20010
20149
  return { file, tasks: parsePlan(content).tasks, lines: content.split(`
20011
20150
  `) };
20012
20151
  }
20013
- function phaseNote(record) {
20014
- return `(phase ${sessionPhase(record)})`;
20152
+ function phaseNote(record, counts = null) {
20153
+ return `(phase ${notedPhase(record, counts)})`;
20154
+ }
20155
+ function notedPhase(record, counts) {
20156
+ const phase = sessionPhase(record);
20157
+ const everyTaskTicked = counts !== null && counts.total > 0 && counts.done === counts.total;
20158
+ return phase === "task" && record.state === "running" && record.task === null && everyTaskTicked ? "wrap-up" : phase;
20015
20159
  }
20016
20160
  function phasedCounts(counts, record) {
20017
- return `${counts.done}/${counts.total} done ${phaseNote(record)}, ${counts.blocked} blocked, ${counts.open} open`;
20161
+ return `${counts.done}/${counts.total} done ${phaseNote(record, counts)}, ${counts.blocked} blocked, ${counts.open} open`;
20018
20162
  }
20019
20163
  function checkboxAt(checklist, line) {
20020
20164
  return checklist?.tasks.find((task) => task.lineNum === line - 1)?.status ?? null;
@@ -20155,7 +20299,7 @@ function readBoardBody(body) {
20155
20299
  }
20156
20300
 
20157
20301
  // src/board/codeowners.ts
20158
- import { readFileSync as readFileSync28, statSync as statSync12 } from "fs";
20302
+ import { readFileSync as readFileSync29, statSync as statSync12 } from "fs";
20159
20303
  import { join as join23 } from "path";
20160
20304
  var CODEOWNERS_LOCATIONS = Object.freeze([
20161
20305
  ".github/CODEOWNERS",
@@ -20216,7 +20360,7 @@ function readCodeowners(root) {
20216
20360
  return null;
20217
20361
  let text;
20218
20362
  try {
20219
- text = readFileSync28(join23(root, path3), "utf8");
20363
+ text = readFileSync29(join23(root, path3), "utf8");
20220
20364
  } catch (error) {
20221
20365
  const why = error instanceof Error ? error.message : String(error);
20222
20366
  throw new Error(`Cannot read ${path3}: ${why}`, { cause: error });
@@ -20990,12 +21134,12 @@ function pickerFor(followed, seams) {
20990
21134
  }
20991
21135
 
20992
21136
  // src/commands/pr/merge-cleanup.ts
20993
- import { existsSync as existsSync22, readFileSync as readFileSync29 } from "fs";
21137
+ import { existsSync as existsSync22, readFileSync as readFileSync30 } from "fs";
20994
21138
  import { join as join24 } from "path";
20995
21139
 
20996
21140
  // src/release/enabled.ts
20997
21141
  import { statSync as statSync13 } from "fs";
20998
- import { resolve as resolve16 } from "path";
21142
+ import { resolve as resolve17 } from "path";
20999
21143
  function isRegularFile(full) {
21000
21144
  try {
21001
21145
  return statSync13(full).isFile();
@@ -21004,7 +21148,7 @@ function isRegularFile(full) {
21004
21148
  }
21005
21149
  }
21006
21150
  function readFile(repoRoot, path3) {
21007
- const resolved = resolve16(repoRoot, path3);
21151
+ const resolved = resolve17(repoRoot, path3);
21008
21152
  return { path: path3, resolved, present: isRegularFile(resolved) };
21009
21153
  }
21010
21154
  function resolveReleaseEnabled(settings, repoRoot) {
@@ -21155,7 +21299,7 @@ function cleanUpAfterMerge(git, merged, report2) {
21155
21299
  }
21156
21300
  function textOf2(path3) {
21157
21301
  try {
21158
- return readFileSync29(path3, "utf8");
21302
+ return readFileSync30(path3, "utf8");
21159
21303
  } catch {
21160
21304
  return "";
21161
21305
  }
@@ -22588,8 +22732,8 @@ function wantsHint(flags) {
22588
22732
  }
22589
22733
  var DEFAULT_HINT_SEAMS = Object.freeze({});
22590
22734
  function afterDelay(ms) {
22591
- return new Promise((resolve17) => {
22592
- setTimeout(resolve17, ms).unref();
22735
+ return new Promise((resolve18) => {
22736
+ setTimeout(resolve18, ms).unref();
22593
22737
  });
22594
22738
  }
22595
22739
  function silenced() {
@@ -23131,8 +23275,8 @@ import { fileURLToPath as fileURLToPath2 } from "url";
23131
23275
  import { homedir as homedir6 } from "os";
23132
23276
 
23133
23277
  // src/notices/notices.ts
23134
- import { mkdirSync as mkdirSync9, readFileSync as readFileSync30, writeFileSync as writeFileSync11 } from "fs";
23135
- import { dirname as dirname10, join as join25 } from "path";
23278
+ import { mkdirSync as mkdirSync10, readFileSync as readFileSync31, writeFileSync as writeFileSync11 } from "fs";
23279
+ import { dirname as dirname11, join as join25 } from "path";
23136
23280
  var NOTICE_IDS = Object.freeze(["alpha", "danger"]);
23137
23281
  var FEEDBACK_URL = "https://github.com/open-tomato/rafa/issues/new/choose";
23138
23282
  var ROADMAP_URL = "https://github.com/open-tomato/rafa/issues/31";
@@ -23145,7 +23289,7 @@ function isNoticeId(value) {
23145
23289
  function readDismissed(home) {
23146
23290
  let raw;
23147
23291
  try {
23148
- raw = readFileSync30(noticesPath(home), "utf8");
23292
+ raw = readFileSync31(noticesPath(home), "utf8");
23149
23293
  } catch {
23150
23294
  return [];
23151
23295
  }
@@ -23164,7 +23308,7 @@ function readDismissed(home) {
23164
23308
  function writeDismissed(home, ids) {
23165
23309
  const all = NOTICE_IDS.filter((id2) => ids.includes(id2) || readDismissed(home).includes(id2));
23166
23310
  const path3 = noticesPath(home);
23167
- mkdirSync9(dirname10(path3), { recursive: true });
23311
+ mkdirSync10(dirname11(path3), { recursive: true });
23168
23312
  writeFileSync11(path3, `${JSON.stringify({ dismissed: all }, null, 2)}
23169
23313
  `);
23170
23314
  }
@@ -23450,8 +23594,8 @@ function haltIfWrapUpMoved(options, seams = {}) {
23450
23594
 
23451
23595
  // src/project/roots.ts
23452
23596
  import { spawnSync as spawnSync2 } from "child_process";
23453
- import { accessSync, constants, readFileSync as readFileSync31, statSync as statSync14 } from "fs";
23454
- import { isAbsolute as isAbsolute5, join as join26, resolve as resolve17 } from "path";
23597
+ import { accessSync, constants, readFileSync as readFileSync32, statSync as statSync14 } from "fs";
23598
+ import { isAbsolute as isAbsolute5, join as join26, resolve as resolve18 } from "path";
23455
23599
  var REFUSED_ROOTS = Object.freeze([
23456
23600
  { path: "/", why: "is the filesystem root" },
23457
23601
  { path: "/var", why: "holds system state" },
@@ -23470,7 +23614,7 @@ var MONOREPO_MARKER_FILES = Object.freeze([
23470
23614
  var PACKAGE_JSON = "package.json";
23471
23615
  var DISK_ROOTS_FILE_SYSTEM = Object.freeze({
23472
23616
  ...DISK_FILE_SYSTEM,
23473
- readFile: (path3) => readFileSync31(path3, "utf8"),
23617
+ readFile: (path3) => readFileSync32(path3, "utf8"),
23474
23618
  isDirectory: (path3) => statSync14(path3).isDirectory(),
23475
23619
  assertWritable: (path3) => accessSync(path3, constants.W_OK)
23476
23620
  });
@@ -23513,7 +23657,7 @@ function realOrSpelled(path3, fs) {
23513
23657
  try {
23514
23658
  return fs.realpath(path3);
23515
23659
  } catch {
23516
- return resolve17(path3);
23660
+ return resolve18(path3);
23517
23661
  }
23518
23662
  }
23519
23663
  function namedRoots(home, fs) {
@@ -23555,7 +23699,7 @@ function writeRefusal(given, real, fs) {
23555
23699
  }
23556
23700
  function refusalOf(given, named3, fs) {
23557
23701
  const resolved = resolveReal(given, fs);
23558
- const byName2 = namedRefusal(given, resolved.real ?? resolve17(given), named3);
23702
+ const byName2 = namedRefusal(given, resolved.real ?? resolve18(given), named3);
23559
23703
  if (byName2 !== null)
23560
23704
  return byName2;
23561
23705
  if (resolved.real === null) {
@@ -23907,9 +24051,46 @@ function commitTaskWork(options) {
23907
24051
  };
23908
24052
  }
23909
24053
 
24054
+ // src/start/background-wait.ts
24055
+ var WAIT_LINE_CAP = 200;
24056
+ var FENCE_LINE5 = /^\s*(?:```|~~~)/;
24057
+ var WAIT_PHRASES = [
24058
+ /\b(?:running|runs|run)\s+in\s+(?:the\s+)?background\b/i,
24059
+ /\bwait(?:ing)?\s+(?:for|on)\s+(?:the|a|its)\s+(?:completion\s+)?notification\b/i,
24060
+ /\brun_in_background\b/
24061
+ ];
24062
+ function unfencedLines(output) {
24063
+ const lines = [];
24064
+ let fenced = false;
24065
+ for (const line of output.split(`
24066
+ `)) {
24067
+ if (FENCE_LINE5.test(line)) {
24068
+ fenced = !fenced;
24069
+ continue;
24070
+ }
24071
+ if (!fenced)
24072
+ lines.push(line);
24073
+ }
24074
+ return lines;
24075
+ }
24076
+ function capped(line) {
24077
+ const trimmed = line.trim();
24078
+ return trimmed.length > WAIT_LINE_CAP ? `${trimmed.slice(0, WAIT_LINE_CAP - 1)}\u2026` : trimmed;
24079
+ }
24080
+ function readBackgroundWait(output) {
24081
+ if (output.trim().length === 0)
24082
+ return null;
24083
+ const reading2 = parseReport(output);
24084
+ if (reading2.present || reading2.reason !== "no-block")
24085
+ return null;
24086
+ const waiting = unfencedLines(output).find((line) => WAIT_PHRASES.some((phrase) => phrase.test(line)));
24087
+ return waiting === undefined ? null : capped(waiting);
24088
+ }
24089
+
23910
24090
  // src/start/commit.ts
23911
24091
  var DROPPED_BLOCKER_FIELD = /^blockers\[\d+\]$/;
23912
24092
  var NO_REPORT_HOLD = "no report: the session wrote no rafa:report block the loop could read";
24093
+ var BACKGROUND_WAIT_HOLD = "no report: the session ended its turn waiting on a command left running in the background:";
23913
24094
  var NO_COMMIT_HOLD = "no commit: the task changed no tracked file";
23914
24095
  var NOTHING_REPORTED_OR_COMMITTED = "the session wrote no report block and changed no tracked file";
23915
24096
  function indentBlock(text) {
@@ -23935,8 +24116,12 @@ function holdsOf(reading2) {
23935
24116
  function leftNothingBehind(attempt2, reported) {
23936
24117
  return !reported && attempt2.outcome === "nothing-to-commit";
23937
24118
  }
23938
- function holdsAfterCommit(holds4, silent) {
23939
- return silent ? [...holds4, NO_REPORT_HOLD, NO_COMMIT_HOLD] : holds4;
24119
+ function noReportHold(output) {
24120
+ const waiting = output === undefined ? null : readBackgroundWait(output);
24121
+ return waiting === null ? NO_REPORT_HOLD : `${BACKGROUND_WAIT_HOLD} ${waiting}`;
24122
+ }
24123
+ function holdsAfterCommit(holds4, silent, output) {
24124
+ return silent ? [...holds4, noReportHold(output), NO_COMMIT_HOLD] : holds4;
23940
24125
  }
23941
24126
  function commitFinishedTask(options) {
23942
24127
  const { taskInfo, trackerPath } = options;
@@ -23957,7 +24142,7 @@ function commitFinishedTask(options) {
23957
24142
  return attempt2;
23958
24143
  }
23959
24144
  const silent = leftNothingBehind(attempt2, reported);
23960
- const holds4 = holdsAfterCommit(reportHolds, silent);
24145
+ const holds4 = holdsAfterCommit(reportHolds, silent, options.output);
23961
24146
  const held = holds4.length > 0;
23962
24147
  const marked = silent && writeTrackerBlocker(trackerPath, taskInfo.lineNum, NOTHING_REPORTED_OR_COMMITTED);
23963
24148
  if (!marked) {
@@ -23988,8 +24173,8 @@ function finishCleanExit(options) {
23988
24173
  const reading2 = parseReport(output);
23989
24174
  const reported = reading2.present;
23990
24175
  const reportHolds = reported ? holdsOf(reading2) : [];
23991
- const attempt2 = commitFinishedTask({ ...task, holds: reportHolds, reported });
23992
- const holds4 = holdsAfterCommit(reportHolds, leftNothingBehind(attempt2, reported));
24176
+ const attempt2 = commitFinishedTask({ ...task, holds: reportHolds, reported, output });
24177
+ const holds4 = holdsAfterCommit(reportHolds, leftNothingBehind(attempt2, reported), output);
23993
24178
  const blocked3 = attempt2.outcome === "failed" || holds4.length > 0;
23994
24179
  return {
23995
24180
  attempt: attempt2,
@@ -24038,7 +24223,7 @@ var SCRIPT_EXTENSIONS = [
24038
24223
  ".cjs",
24039
24224
  ".ts"
24040
24225
  ];
24041
- var FENCE_LINE5 = /^ {0,3}(?:```|~~~)/;
24226
+ var FENCE_LINE6 = /^ {0,3}(?:```|~~~)/;
24042
24227
  var HEADING_LINE2 = /^ {0,3}(#{1,6})\s+(.*)$/;
24043
24228
  var NUMBERED_ITEM = /^ {0,3}\d+[.)]\s+\S/;
24044
24229
  var CONTINUATION = /^ {2,}\S/;
@@ -24072,7 +24257,7 @@ function bodySections(body) {
24072
24257
  const headings = [];
24073
24258
  let fenced = false;
24074
24259
  lines.forEach((line, index) => {
24075
- if (FENCE_LINE5.test(line)) {
24260
+ if (FENCE_LINE6.test(line)) {
24076
24261
  fenced = !fenced;
24077
24262
  return;
24078
24263
  }
@@ -24111,7 +24296,7 @@ function numberedRuns(body) {
24111
24296
  steps = 0;
24112
24297
  };
24113
24298
  bodyLines2(body).forEach((text, index) => {
24114
- if (FENCE_LINE5.test(text)) {
24299
+ if (FENCE_LINE6.test(text)) {
24115
24300
  fenced = !fenced;
24116
24301
  return;
24117
24302
  }
@@ -24199,7 +24384,7 @@ function withoutFences(text) {
24199
24384
  const kept = [];
24200
24385
  let fenced = false;
24201
24386
  for (const line of bodyLines2(text)) {
24202
- if (FENCE_LINE5.test(line)) {
24387
+ if (FENCE_LINE6.test(line)) {
24203
24388
  fenced = !fenced;
24204
24389
  continue;
24205
24390
  }
@@ -24258,7 +24443,7 @@ function extractedDate(body) {
24258
24443
  function errorString(body) {
24259
24444
  let fenced = false;
24260
24445
  for (const line of bodyLines2(body)) {
24261
- if (FENCE_LINE5.test(line)) {
24446
+ if (FENCE_LINE6.test(line)) {
24262
24447
  fenced = !fenced;
24263
24448
  continue;
24264
24449
  }
@@ -24861,7 +25046,7 @@ function writeProgress(repoRoot, planStub2, dir = repoRoot) {
24861
25046
  }
24862
25047
 
24863
25048
  // src/inventory/search/rank.ts
24864
- import { readFileSync as readFileSync32 } from "fs";
25049
+ import { readFileSync as readFileSync33 } from "fs";
24865
25050
  var SEARCH_LIMIT = 12;
24866
25051
  var MIN_WORD_LENGTH = 2;
24867
25052
  var MIN_PREFIX_LENGTH = 4;
@@ -24983,7 +25168,7 @@ function rankCandidates(question, candidates, limit = SEARCH_LIMIT) {
24983
25168
  return [];
24984
25169
  return candidates.map((candidate) => scoreCandidate(candidate, words)).filter((ranked) => ranked.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
24985
25170
  }
24986
- function readRankCandidate(record, read = (path3) => readFileSync32(path3, "utf8")) {
25171
+ function readRankCandidate(record, read = (path3) => readFileSync33(path3, "utf8")) {
24987
25172
  let text;
24988
25173
  try {
24989
25174
  text = read(record.path);
@@ -25232,7 +25417,7 @@ function renderInheritedSection(failures) {
25232
25417
 
25233
25418
  // src/start/preflight.ts
25234
25419
  import { randomUUID as randomUUID7 } from "crypto";
25235
- import { existsSync as existsSync28, readFileSync as readFileSync34 } from "fs";
25420
+ import { existsSync as existsSync28, readFileSync as readFileSync35 } from "fs";
25236
25421
  import { homedir as homedir7 } from "os";
25237
25422
  import { basename as basename8, join as join28 } from "path";
25238
25423
 
@@ -25350,11 +25535,11 @@ function developmentApplied(report2) {
25350
25535
  }
25351
25536
 
25352
25537
  // src/preflight/first-dispatch.ts
25353
- import { readFileSync as readFileSync33 } from "fs";
25538
+ import { readFileSync as readFileSync34 } from "fs";
25354
25539
  var TICKED_TASK = /^- \[[xX]\] (.+)/;
25355
25540
  function readIfReadable(path3) {
25356
25541
  try {
25357
- return readFileSync33(path3, "utf8");
25542
+ return readFileSync34(path3, "utf8");
25358
25543
  } catch {
25359
25544
  return null;
25360
25545
  }
@@ -25696,7 +25881,7 @@ function driftLines(report2) {
25696
25881
 
25697
25882
  // src/start/preflight-drift.ts
25698
25883
  var DRIFT_EVERY = 2;
25699
- var RECORD_EXTENSION = ".json";
25884
+ var RECORD_EXTENSION2 = ".json";
25700
25885
  function countRunRecords(root) {
25701
25886
  let names2;
25702
25887
  try {
@@ -25706,7 +25891,7 @@ function countRunRecords(root) {
25706
25891
  return 0;
25707
25892
  throw error;
25708
25893
  }
25709
- return names2.filter((name2) => name2.endsWith(RECORD_EXTENSION) && isSessionId(name2.slice(0, -RECORD_EXTENSION.length))).length;
25894
+ return names2.filter((name2) => name2.endsWith(RECORD_EXTENSION2) && isSessionId(name2.slice(0, -RECORD_EXTENSION2.length))).length;
25710
25895
  }
25711
25896
  function isDriftRun(count) {
25712
25897
  return count > 0 && count % DRIFT_EVERY === 0;
@@ -25819,7 +26004,7 @@ function agentSourcePath(planPath) {
25819
26004
  }
25820
26005
  function readIfReadable2(path3) {
25821
26006
  try {
25822
- return readFileSync34(path3, "utf8");
26007
+ return readFileSync35(path3, "utf8");
25823
26008
  } catch {
25824
26009
  return null;
25825
26010
  }
@@ -26075,6 +26260,7 @@ function runTaskSession(prompt, flags, sessionId, settingSources, served = [], c
26075
26260
  const options = cwd === undefined ? {} : { cwd };
26076
26261
  return runClaudeCaptured(prompt, settingSources, [SESSION_ID_FLAG2, sessionId, ...flags], spawn, served, options);
26077
26262
  }
26263
+ var HALTED_EXIT_CODE = 1;
26078
26264
  var BLOCKER_PROMPT_PREFIX = "An earlier run of this task left it blocked on: ";
26079
26265
  function blockerLines(blocker) {
26080
26266
  return blocker === null || blocker.trim().length === 0 ? [] : [`${BLOCKER_PROMPT_PREFIX}${escapeBlockerText(blocker)}`];
@@ -26135,22 +26321,28 @@ async function dispatchTask(options) {
26135
26321
  const handed = await handOut(options.handout, task, resolution, options.repoRoot);
26136
26322
  const prompt = withStamp(buildTaskPrompt(taskText, options.promptContent, injection.text, options.knownMissing, taskInfo.blocker ?? null, { skills: renderSkillsSection(handed.skills), lessons: renderLessonsSection(handed.lessons) }, options.base, options.inherited, options.alwaysRun));
26137
26323
  const served = serveForSession(options.serving, resolution);
26138
- const sessionId = (options.newSessionId ?? randomUUID8)();
26139
- const session = await run(prompt, flags, sessionId, options.settingSources, served, options.checkout);
26140
- return {
26324
+ const record = {
26141
26325
  taskText,
26142
26326
  prompt,
26143
26327
  flags,
26144
26328
  served,
26145
26329
  declaration,
26146
26330
  injection,
26147
- sessionId,
26148
- exitCode: session.exitCode,
26149
- output: session.stdout,
26150
26331
  resolver: handed.resolver,
26151
26332
  skillsOffered: handed.skills,
26152
26333
  lessonsOffered: handed.lessons
26153
26334
  };
26335
+ if (haltBeforeSpawn(options.guard ?? null, taskInfo)) {
26336
+ return { ...record, sessionId: "", exitCode: HALTED_EXIT_CODE, output: "", halted: true };
26337
+ }
26338
+ const sessionId = (options.newSessionId ?? randomUUID8)();
26339
+ const session = await run(prompt, flags, sessionId, options.settingSources, served, options.checkout);
26340
+ return { ...record, sessionId, exitCode: session.exitCode, output: session.stdout, halted: false };
26341
+ }
26342
+ function haltBeforeSpawn(guard, taskInfo) {
26343
+ if (guard === null)
26344
+ return false;
26345
+ return haltIfCheckoutMoved({ expected: guard.expected, trackerPath: guard.trackerPath, taskInfo }, guard.seams);
26154
26346
  }
26155
26347
  function serveForSession(serving, resolution) {
26156
26348
  if (serving === null)
@@ -26240,7 +26432,7 @@ async function storeTaskReport(options) {
26240
26432
  }
26241
26433
 
26242
26434
  // src/start/loop-events.ts
26243
- import { appendFileSync, mkdirSync as mkdirSync10 } from "fs";
26435
+ import { appendFileSync, mkdirSync as mkdirSync11 } from "fs";
26244
26436
  import { join as join29 } from "path";
26245
26437
  var MINUTE_MS = 60000;
26246
26438
  var TOKENS_PER_K = 1000;
@@ -26292,7 +26484,7 @@ function bindEventsFile(root, sessionId, warn2 = (line) => {
26292
26484
  }) {
26293
26485
  const file = eventsFilePath(root, sessionId);
26294
26486
  try {
26295
- mkdirSync10(runsDir(root), { recursive: true });
26487
+ mkdirSync11(runsDir(root), { recursive: true });
26296
26488
  } catch {}
26297
26489
  bound = { file, warn: warn2, warned: false };
26298
26490
  return file;
@@ -26386,20 +26578,20 @@ async function holdWhilePaused(options) {
26386
26578
 
26387
26579
  // src/start/plan-path.ts
26388
26580
  import { existsSync as existsSync29 } from "fs";
26389
- import { resolve as resolve18 } from "path";
26581
+ import { resolve as resolve19 } from "path";
26390
26582
  var DEFAULT_PLAN_FILE = "PLAN.md";
26391
26583
  function resolvePlanPath(repoRoot, planDir, planArg) {
26392
26584
  if (planArg)
26393
- return resolve18(repoRoot, planArg);
26394
- const inPlanDir = resolve18(repoRoot, planDir, DEFAULT_PLAN_FILE);
26395
- return existsSync29(inPlanDir) ? inPlanDir : resolve18(repoRoot, DEFAULT_PLAN_FILE);
26585
+ return resolve19(repoRoot, planArg);
26586
+ const inPlanDir = resolve19(repoRoot, planDir, DEFAULT_PLAN_FILE);
26587
+ return existsSync29(inPlanDir) ? inPlanDir : resolve19(repoRoot, DEFAULT_PLAN_FILE);
26396
26588
  }
26397
26589
 
26398
26590
  // src/start/risk-total.ts
26399
- import { readFileSync as readFileSync36 } from "fs";
26591
+ import { readFileSync as readFileSync37 } from "fs";
26400
26592
 
26401
26593
  // src/plan/risk.ts
26402
- import { readFileSync as readFileSync35 } from "fs";
26594
+ import { readFileSync as readFileSync36 } from "fs";
26403
26595
  import { isAbsolute as isAbsolute6, join as join30, relative as relative8 } from "path";
26404
26596
 
26405
26597
  // src/plan/risk/accounts.ts
@@ -26959,7 +27151,7 @@ function located(finding2, file, task) {
26959
27151
  }
26960
27152
  function readText5(path3) {
26961
27153
  try {
26962
- return readFileSync35(path3, "utf8");
27154
+ return readFileSync36(path3, "utf8");
26963
27155
  } catch {
26964
27156
  return null;
26965
27157
  }
@@ -27061,7 +27253,7 @@ function spawningRunners(root) {
27061
27253
  }
27062
27254
  async function readRiskTotalLine(input, seams = {}) {
27063
27255
  const makeRunners = seams.runners ?? spawningRunners;
27064
- const report2 = await assessPlanRisk({ path: input.planPath, text: readFileSync36(input.planPath, "utf8") }, {
27256
+ const report2 = await assessPlanRisk({ path: input.planPath, text: readFileSync37(input.planPath, "utf8") }, {
27065
27257
  repoRoot: input.repoRoot,
27066
27258
  home: input.home,
27067
27259
  settingSources: input.config.settingSources,
@@ -27621,7 +27813,7 @@ function readRunArgs(args) {
27621
27813
 
27622
27814
  // src/start/worktree.ts
27623
27815
  import { realpathSync as realpathSync4 } from "fs";
27624
- import { join as join31, resolve as resolve20 } from "path";
27816
+ import { join as join31, resolve as resolve21 } from "path";
27625
27817
 
27626
27818
  // src/start/claim-catch-up.ts
27627
27819
  var OWN_COMMIT_FORMAT = "--format=%H%x09%P%x09%s";
@@ -27686,9 +27878,9 @@ function readClaimCatchUp(git, branch, base) {
27686
27878
  }
27687
27879
 
27688
27880
  // src/start/worktree-dir.ts
27689
- import { resolve as resolve19 } from "path";
27881
+ import { resolve as resolve20 } from "path";
27690
27882
  function worktreeDirAt(projectRoot3, worktreeDir) {
27691
- return resolve19(projectRoot3, worktreeDir);
27883
+ return resolve20(projectRoot3, worktreeDir);
27692
27884
  }
27693
27885
 
27694
27886
  // src/start/worktree.ts
@@ -27752,7 +27944,7 @@ function canonicalPath(path3) {
27752
27944
  try {
27753
27945
  return realpathSync4(path3);
27754
27946
  } catch {
27755
- return resolve20(path3);
27947
+ return resolve21(path3);
27756
27948
  }
27757
27949
  }
27758
27950
  function readExisting(listing2, branch, path3) {
@@ -27925,13 +28117,13 @@ async function settleRunCheckout(request, seams = {}) {
27925
28117
  }
27926
28118
 
27927
28119
  // src/start/suite-step.ts
27928
- import { existsSync as existsSync35, mkdirSync as mkdirSync13, readFileSync as readFileSync43, renameSync as renameSync6, writeFileSync as writeFileSync16 } from "fs";
28120
+ import { existsSync as existsSync36, mkdirSync as mkdirSync14, readFileSync as readFileSync44, renameSync as renameSync7, writeFileSync as writeFileSync17 } from "fs";
27929
28121
  import { constants as constants2 } from "os";
27930
- import { basename as basename11, dirname as dirname13, join as join36 } from "path";
28122
+ import { basename as basename11, dirname as dirname15, join as join37 } from "path";
27931
28123
 
27932
28124
  // src/suite/baseline.ts
27933
- import { existsSync as existsSync30, mkdirSync as mkdirSync11, readFileSync as readFileSync37, renameSync as renameSync5, writeFileSync as writeFileSync13 } from "fs";
27934
- import { basename as basename9, dirname as dirname11, join as join32 } from "path";
28125
+ import { existsSync as existsSync30, mkdirSync as mkdirSync12, readFileSync as readFileSync38, renameSync as renameSync6, writeFileSync as writeFileSync13 } from "fs";
28126
+ import { basename as basename9, dirname as dirname12, join as join32 } from "path";
27935
28127
  var BASELINE_VERSION = 1;
27936
28128
  var BASELINE_FILE_PREFIX = "SUITE_BASELINE";
27937
28129
  var PLAN_FILE_NAME2 = /^PLAN(?:_TRACKER)?(-.+)?\.md$/;
@@ -27941,7 +28133,7 @@ function baselinePathFor(trackerPath) {
27941
28133
  if (match === null) {
27942
28134
  throw new RangeError(`baselinePathFor: ${trackerPath} is not a PLAN or PLAN_TRACKER file`);
27943
28135
  }
27944
- return join32(dirname11(trackerPath), `${BASELINE_FILE_PREFIX}${match[1] ?? ""}.json`);
28136
+ return join32(dirname12(trackerPath), `${BASELINE_FILE_PREFIX}${match[1] ?? ""}.json`);
27945
28137
  }
27946
28138
  function copyFailure(failure3) {
27947
28139
  return failure3.message === undefined ? { file: failure3.file, name: failure3.name } : { file: failure3.file, name: failure3.name, message: failure3.message };
@@ -27960,11 +28152,11 @@ function baselineOf(result, recordedAt, commit) {
27960
28152
  };
27961
28153
  }
27962
28154
  function writeBaseline(path3, baseline) {
27963
- mkdirSync11(dirname11(path3), { recursive: true });
28155
+ mkdirSync12(dirname12(path3), { recursive: true });
27964
28156
  const temporary = `${path3}.${process.pid}.tmp`;
27965
28157
  writeFileSync13(temporary, `${JSON.stringify(baseline, null, 2)}
27966
28158
  `, "utf8");
27967
- renameSync5(temporary, path3);
28159
+ renameSync6(temporary, path3);
27968
28160
  }
27969
28161
  function isStringOrNull(value) {
27970
28162
  return value === null || typeof value === "string";
@@ -28008,7 +28200,7 @@ function readBaseline(path3) {
28008
28200
  return { state: "missing" };
28009
28201
  let text;
28010
28202
  try {
28011
- text = readFileSync37(path3, "utf8");
28203
+ text = readFileSync38(path3, "utf8");
28012
28204
  } catch (error) {
28013
28205
  const reason = error instanceof Error ? error.message : String(error);
28014
28206
  return { state: "unreadable", reason };
@@ -28103,8 +28295,8 @@ async function readPlanOwns(options) {
28103
28295
  }
28104
28296
 
28105
28297
  // src/suite/run.ts
28106
- import { existsSync as existsSync31, mkdirSync as mkdirSync12, readFileSync as readFileSync38, rmSync as rmSync4, writeFileSync as writeFileSync14 } from "fs";
28107
- import { basename as basename10, dirname as dirname12, extname as extname2, isAbsolute as isAbsolute7, join as join33 } from "path";
28298
+ import { existsSync as existsSync31, mkdirSync as mkdirSync13, readFileSync as readFileSync39, rmSync as rmSync4, writeFileSync as writeFileSync14 } from "fs";
28299
+ import { basename as basename10, dirname as dirname13, extname as extname2, isAbsolute as isAbsolute7, join as join33 } from "path";
28108
28300
 
28109
28301
  // src/suite/unhandled.ts
28110
28302
  var UNHANDLED_HEADING = "# Unhandled error between tests";
@@ -28214,7 +28406,7 @@ var OUTPUT_SUFFIX = ".output.txt";
28214
28406
  function outputFileFor(junitFile) {
28215
28407
  const name2 = basename10(junitFile);
28216
28408
  const stem = name2.endsWith(JUNIT_SUFFIX) ? name2.slice(0, -JUNIT_SUFFIX.length) : name2.slice(0, name2.length - extname2(name2).length);
28217
- return join33(dirname12(junitFile), `${stem}${OUTPUT_SUFFIX}`);
28409
+ return join33(dirname13(junitFile), `${stem}${OUTPUT_SUFFIX}`);
28218
28410
  }
28219
28411
  function suiteEnv(base) {
28220
28412
  return Object.fromEntries(Object.entries(base).filter(([key]) => key !== CLAUDE_CODE_ENV));
@@ -28322,7 +28514,7 @@ function readJunit(path3) {
28322
28514
  return { junit: "missing", failures: [] };
28323
28515
  let text;
28324
28516
  try {
28325
- text = readFileSync38(path3, "utf8");
28517
+ text = readFileSync39(path3, "utf8");
28326
28518
  } catch {
28327
28519
  return { junit: "unreadable", failures: [] };
28328
28520
  }
@@ -28346,7 +28538,7 @@ async function runSuite(options) {
28346
28538
  }
28347
28539
  const command = suiteCommand(options.junitFile, options.paths, options.changedSince);
28348
28540
  const outputFile = outputFileFor(options.junitFile);
28349
- mkdirSync12(dirname12(options.junitFile), { recursive: true });
28541
+ mkdirSync13(dirname13(options.junitFile), { recursive: true });
28350
28542
  rmSync4(options.junitFile, { force: true });
28351
28543
  rmSync4(outputFile, { force: true });
28352
28544
  const spawn = options.spawn ?? spawnSuite;
@@ -28361,7 +28553,7 @@ async function runSuite(options) {
28361
28553
  }
28362
28554
 
28363
28555
  // src/suite/scope.ts
28364
- import { existsSync as existsSync32, readdirSync as readdirSync14, readFileSync as readFileSync39 } from "fs";
28556
+ import { existsSync as existsSync32, readdirSync as readdirSync14, readFileSync as readFileSync40 } from "fs";
28365
28557
  import { isAbsolute as isAbsolute8, join as join34, relative as relative9, sep as sep2 } from "path";
28366
28558
  var BUNFIG_FILE = "bunfig.toml";
28367
28559
  var TEST_FILE_NAME = /[._](?:test|spec)\.(?:tsx?|jsx?|[cm][jt]s)$/;
@@ -28449,7 +28641,7 @@ function readPreloadFiles(root) {
28449
28641
  if (!existsSync32(path3))
28450
28642
  return { state: "missing", files: [] };
28451
28643
  try {
28452
- return { state: "read", files: preloadFilesOf(readFileSync39(path3, "utf8"), root) };
28644
+ return { state: "read", files: preloadFilesOf(readFileSync40(path3, "utf8"), root) };
28453
28645
  } catch (error) {
28454
28646
  const reason = error instanceof Error ? error.message : String(error);
28455
28647
  return { state: "unreadable", files: [], reason };
@@ -28605,7 +28797,7 @@ async function runLintStep(input) {
28605
28797
  }
28606
28798
 
28607
28799
  // src/start/suite-blocker.ts
28608
- import { readFileSync as readFileSync40, writeFileSync as writeFileSync15 } from "fs";
28800
+ import { readFileSync as readFileSync41, writeFileSync as writeFileSync15 } from "fs";
28609
28801
  function failingFiles(failures) {
28610
28802
  const counts = new Map;
28611
28803
  for (const failure3 of failures)
@@ -28689,7 +28881,7 @@ function blockRepairAgain(trackerPath, content, line, blocker) {
28689
28881
  return writeTrackerBlocker(trackerPath, line, blocker) ? { line, inserted: false } : null;
28690
28882
  }
28691
28883
  function writeRepairTask(trackerPath, blocker, site) {
28692
- const content = readFileSync40(trackerPath, "utf8");
28884
+ const content = readFileSync41(trackerPath, "utf8");
28693
28885
  const repaired = existingRepairOf(content, site);
28694
28886
  if (repaired !== null)
28695
28887
  return blockRepairAgain(trackerPath, content, repaired, blocker);
@@ -28699,7 +28891,7 @@ function writeRepairTask(trackerPath, blocker, site) {
28699
28891
  }
28700
28892
 
28701
28893
  // src/start/sweep-timing.ts
28702
- import { existsSync as existsSync34, readFileSync as readFileSync41 } from "fs";
28894
+ import { existsSync as existsSync34, readFileSync as readFileSync42 } from "fs";
28703
28895
  var SLOW_SWEEP_SECONDS = 10;
28704
28896
  var SUITE_TAG = /<(\/?)testsuite\b([^>]*?)(\/?)>/g;
28705
28897
  function fileTimeOf(attributes) {
@@ -28742,7 +28934,7 @@ function readFileTimes(path3) {
28742
28934
  if (!existsSync34(path3))
28743
28935
  return [];
28744
28936
  try {
28745
- return parseJunitFileTimes(readFileSync41(path3, "utf8")) ?? [];
28937
+ return parseJunitFileTimes(readFileSync42(path3, "utf8")) ?? [];
28746
28938
  } catch {
28747
28939
  return [];
28748
28940
  }
@@ -28816,8 +29008,300 @@ function foldResults(first, second, stopCode) {
28816
29008
  };
28817
29009
  return first.noTestFiles === true && second.noTestFiles === true ? { ...folded2, noTestFiles: true } : folded2;
28818
29010
  }
29011
+
29012
+ // src/start/type-step.ts
29013
+ import { existsSync as existsSync35, mkdtempSync, realpathSync as realpathSync5, rmSync as rmSync5, symlinkSync as symlinkSync2, writeFileSync as writeFileSync16 } from "fs";
29014
+ import { tmpdir } from "os";
29015
+ import { dirname as dirname14, join as join36, relative as relative11, resolve as resolve22, sep as sep3 } from "path";
29016
+ var TEST_FILE_SUFFIX = ".test.ts";
29017
+ var TSC_FLAGS = ["--noEmit", "--pretty", "false"];
29018
+ var ERRORS_LISTED2 = 10;
29019
+ var NODE_MODULES = "node_modules";
29020
+ var TSC_BIN = [".bin", "tsc"];
29021
+ var SCRATCH_NAME = "tsconfig.json";
29022
+ var FILE_ERROR = /^(.+)\((\d+),(\d+)\): error (TS\d+): (.*)$/;
29023
+ var GLOBAL_ERROR = /^error (TS\d+): (.*)$/;
29024
+ var NOTHING_RAN2 = { ran: false, red: false, interrupted: false, blocker: null };
29025
+ var GREEN = { ran: true, red: false, interrupted: false, blocker: null };
29026
+ var INTERRUPTED = { ran: true, red: false, interrupted: true, blocker: null };
29027
+ async function runTsc(options) {
29028
+ const proc = Bun.spawn([...options.argv], {
29029
+ cwd: options.cwd,
29030
+ stdin: "ignore",
29031
+ stdout: "pipe",
29032
+ stderr: "pipe"
29033
+ });
29034
+ const [stdout, stderr, exitCode] = await Promise.all([
29035
+ new Response(proc.stdout).text(),
29036
+ new Response(proc.stderr).text(),
29037
+ proc.exited
29038
+ ]);
29039
+ return { exitCode, stdout, stderr };
29040
+ }
29041
+ function typeLabel(task) {
29042
+ return `type step after "${task}"`;
29043
+ }
29044
+ function pathsOf(status) {
29045
+ return /^[RC]/.test(status) ? 2 : 1;
29046
+ }
29047
+ function readTypeFiles(git, base) {
29048
+ const result = git(["diff", "--name-status", "-z", "--find-renames", "--diff-filter=d", base, "HEAD"]);
29049
+ if (!result.ok) {
29050
+ activeOutput().warn(`\u26A0\uFE0F git diff from ${base} did not answer (${gitSaid(result) || "nothing said"}); the type step runs nothing.`);
29051
+ return null;
29052
+ }
29053
+ const fields = result.stdout.split("\x00").filter((field4) => field4 !== "");
29054
+ const files = [];
29055
+ for (let index = 0;index < fields.length; ) {
29056
+ const status = fields[index] ?? "";
29057
+ const count = pathsOf(status);
29058
+ const paths = fields.slice(index + 1, index + 1 + count);
29059
+ index += 1 + count;
29060
+ const head = paths[paths.length - 1];
29061
+ if (head === undefined || !head.endsWith(TEST_FILE_SUFFIX))
29062
+ continue;
29063
+ files.push({ head, base: status.startsWith("A") ? null : paths[0] ?? null });
29064
+ }
29065
+ return files;
29066
+ }
29067
+ function realPath(path3) {
29068
+ try {
29069
+ return realpathSync5(path3);
29070
+ } catch {
29071
+ return resolve22(path3);
29072
+ }
29073
+ }
29074
+ function walkStop(git) {
29075
+ const result = git(["rev-parse", "--path-format=absolute", "--git-common-dir"]);
29076
+ const common = result.stdout.trim();
29077
+ return result.ok && common !== "" ? dirname14(realPath(common)) : null;
29078
+ }
29079
+ function isUnder(dir, root) {
29080
+ return dir === root || dir.startsWith(root.endsWith(sep3) ? root : `${root}${sep3}`);
29081
+ }
29082
+ function findNodeModules(checkout, git) {
29083
+ const start = realPath(checkout);
29084
+ const stop = walkStop(git);
29085
+ const top = stop !== null && isUnder(start, stop) ? stop : start;
29086
+ for (let dir = start;; dir = dirname14(dir)) {
29087
+ const modules = join36(dir, NODE_MODULES);
29088
+ if (existsSync35(join36(modules, ...TSC_BIN)))
29089
+ return modules;
29090
+ if (dir === top || dirname14(dir) === dir)
29091
+ return null;
29092
+ }
29093
+ }
29094
+ function scratchTsconfig(tree, files, modules) {
29095
+ const root = resolve22(tree);
29096
+ return `${JSON.stringify({
29097
+ extends: join36(root, "tsconfig.json"),
29098
+ compilerOptions: { typeRoots: [join36(resolve22(modules), "@types")] },
29099
+ files: files.map((file) => join36(root, file)),
29100
+ include: []
29101
+ }, null, 2)}
29102
+ `;
29103
+ }
29104
+ function parseTscOutput(stdout, cwd) {
29105
+ const errors = [];
29106
+ for (const line of stdout.split(`
29107
+ `)) {
29108
+ const atFile = FILE_ERROR.exec(line);
29109
+ const global = atFile === null ? GLOBAL_ERROR.exec(line) : null;
29110
+ const last = errors[errors.length - 1];
29111
+ if (atFile !== null) {
29112
+ const [, file = "", row = "0", column = "0", code = "", message = ""] = atFile;
29113
+ errors.push({ file: relative11(cwd, resolve22(cwd, file)), line: Number(row), column: Number(column), code, message });
29114
+ } else if (global !== null) {
29115
+ errors.push({ file: null, line: 0, column: 0, code: global[1] ?? "", message: global[2] ?? "" });
29116
+ } else if (line.startsWith(" ") && last !== undefined) {
29117
+ errors[errors.length - 1] = { ...last, message: `${last.message}
29118
+ ${line.trimEnd()}` };
29119
+ }
29120
+ }
29121
+ return errors;
29122
+ }
29123
+ function identity(error) {
29124
+ return `${error.code} ${error.message}`;
29125
+ }
29126
+ function newErrors(head, base) {
29127
+ const left = new Map;
29128
+ for (const error of base) {
29129
+ const key = `${error.file ?? ""}\x00${identity(error)}`;
29130
+ left.set(key, (left.get(key) ?? 0) + 1);
29131
+ }
29132
+ return head.filter((error) => {
29133
+ const key = `${error.file ?? ""}\x00${identity(error)}`;
29134
+ const count = left.get(key) ?? 0;
29135
+ if (count === 0)
29136
+ return true;
29137
+ left.set(key, count - 1);
29138
+ return false;
29139
+ });
29140
+ }
29141
+ function errorLine2(error) {
29142
+ const first = error.message.split(`
29143
+ `)[0] ?? "";
29144
+ return `${error.file ?? "(no file)"}:${error.line}:${error.column} ${error.code} ${first}`;
29145
+ }
29146
+ function listed(errors) {
29147
+ const shown2 = errors.slice(0, ERRORS_LISTED2).map(errorLine2);
29148
+ return errors.length > ERRORS_LISTED2 ? [...shown2, `...and ${errors.length - ERRORS_LISTED2} more`] : shown2;
29149
+ }
29150
+ function typeBlockerText(label, base, errors) {
29151
+ const files = [...new Set(errors.map((error) => error.file ?? ""))];
29152
+ const shown2 = listed(errors).join("; ").replace(/\.$/u, "");
29153
+ return `The runner's ${label} found type errors in the task's test files that ${base} did not hold. ` + `New errors: ${shown2}. ` + `Fix them, then check ${files.join(" ")} with tsc ${TSC_FLAGS.join(" ")} -p over a tsconfig outside the checkout ` + "that extends its tsconfig.json by absolute path and lists them under files.";
29154
+ }
29155
+ function brokenBlockerText(label, run, said) {
29156
+ return `The runner's ${label} could not type-check the task's test files: tsc exited ${run.exitCode}${said === undefined ? " and printed no error." : `: ${said}`}`;
29157
+ }
29158
+ function configError(errors, scratch, cwd) {
29159
+ const config = relative11(cwd, scratch);
29160
+ return errors.find((error) => error.file === null || error.file === config);
29161
+ }
29162
+ function firstLine7(run) {
29163
+ return `${run.stdout}
29164
+ ${run.stderr}`.split(`
29165
+ `).map((line) => line.trim()).find((line) => line !== "");
29166
+ }
29167
+ async function checkTree(input, modules, tree, files) {
29168
+ const scratchDir = mkdtempSync(join36(tmpdir(), "rafa-type-step-"));
29169
+ const scratch = join36(scratchDir, SCRATCH_NAME);
29170
+ try {
29171
+ writeFileSync16(scratch, scratchTsconfig(tree, files, modules), "utf8");
29172
+ const tsc = join36(modules, ...TSC_BIN);
29173
+ let run;
29174
+ try {
29175
+ run = await (input.runTypes ?? runTsc)({ cwd: tree, argv: [tsc, "-p", scratch, ...TSC_FLAGS] });
29176
+ } catch (error) {
29177
+ return { kind: "threw", why: messageOf(error) };
29178
+ }
29179
+ if (run.exitCode === input.stopCode || input.isInterrupted?.() === true)
29180
+ return { kind: "interrupted" };
29181
+ const errors = parseTscOutput(run.stdout, tree);
29182
+ const config = configError(errors, scratch, tree);
29183
+ if (config !== undefined)
29184
+ return { kind: "broken", run, said: `error ${config.code}: ${config.message.split(`
29185
+ `)[0] ?? ""}` };
29186
+ if (run.exitCode !== 0 && errors.length === 0)
29187
+ return { kind: "broken", run, said: firstLine7(run) };
29188
+ const wanted = new Set(files);
29189
+ return { kind: "read", errors: errors.filter((error) => error.file !== null && wanted.has(error.file)) };
29190
+ } finally {
29191
+ rmSync5(scratchDir, { recursive: true, force: true });
29192
+ }
29193
+ }
29194
+ async function inBaseTree(input, label, modules, check) {
29195
+ const parent = mkdtempSync(join36(tmpdir(), "rafa-type-base-"));
29196
+ const tree = join36(parent, "tree");
29197
+ try {
29198
+ const added = input.git(["worktree", "add", "--detach", tree, input.base]);
29199
+ if (!added.ok) {
29200
+ activeOutput().warn(`\u26A0\uFE0F The ${label} could not check out ${input.base} (${gitSaid(added) || "nothing said"}); it reads no error as new.`);
29201
+ return null;
29202
+ }
29203
+ if (existsSync35(modules) && !existsSync35(join36(tree, NODE_MODULES)))
29204
+ symlinkSync2(modules, join36(tree, NODE_MODULES));
29205
+ if (!existsSync35(join36(tree, "tsconfig.json")))
29206
+ return { kind: "read", errors: [] };
29207
+ return await check(tree);
29208
+ } finally {
29209
+ if (existsSync35(tree)) {
29210
+ const removed = input.git(["worktree", "remove", "--force", tree]);
29211
+ if (!removed.ok)
29212
+ activeOutput().warn(`\u26A0\uFE0F The ${label} could not remove its worktree ${tree} (${gitSaid(removed) || "nothing said"}).`);
29213
+ }
29214
+ rmSync5(parent, { recursive: true, force: true });
29215
+ }
29216
+ }
29217
+ async function baseErrors(input, label, modules, files) {
29218
+ const pairs = files.filter((file) => file.base !== null);
29219
+ if (pairs.length === 0)
29220
+ return { errors: [] };
29221
+ const run = await inBaseTree(input, label, modules, (tree) => checkTree(input, modules, tree, pairs.map((file) => file.base)));
29222
+ if (run === null)
29223
+ return { outcome: GREEN };
29224
+ if (run.kind === "interrupted")
29225
+ return { outcome: interrupted(label) };
29226
+ if (run.kind !== "read") {
29227
+ const why = run.kind === "threw" ? run.why : brokenBlockerText(label, run.run, run.said);
29228
+ activeOutput().warn(`\u26A0\uFE0F The ${label} could not check the files at ${input.base} (${why}); it reads no error as new.`);
29229
+ return { outcome: GREEN };
29230
+ }
29231
+ const headOf = new Map(pairs.map((file) => [file.base, file.head]));
29232
+ return { errors: run.errors.map((error) => ({ ...error, file: headOf.get(error.file ?? "") ?? error.file })) };
29233
+ }
29234
+ function interrupted(label) {
29235
+ activeOutput().info(`\u23F9 The ${label} was interrupted by SIGINT: read as a stop, not as errors.`);
29236
+ return INTERRUPTED;
29237
+ }
29238
+ function filesToCheck(input, label) {
29239
+ if (!existsSync35(join36(input.checkout, "tsconfig.json"))) {
29240
+ activeOutput().info(`\uD83D\uDD0E ${label}: no tsconfig.json at the checkout root; nothing to type-check.`);
29241
+ return null;
29242
+ }
29243
+ const files = readTypeFiles(input.git, input.base);
29244
+ if (files === null)
29245
+ return null;
29246
+ if (files.length > 0)
29247
+ return files;
29248
+ activeOutput().info(`\uD83D\uDD0E ${label}: the task changed no ${TEST_FILE_SUFFIX} file; nothing to type-check.`);
29249
+ return null;
29250
+ }
29251
+ async function runTypeStep(input) {
29252
+ const label = typeLabel(input.task);
29253
+ const files = filesToCheck(input, label);
29254
+ if (files === null)
29255
+ return NOTHING_RAN2;
29256
+ const modules = findNodeModules(input.checkout, input.git) ?? join36(resolve22(input.checkout), NODE_MODULES);
29257
+ const head = await checkTree(input, modules, input.checkout, files.map((file) => file.head));
29258
+ if (head.kind === "threw") {
29259
+ activeOutput().warn(`\u26A0\uFE0F The ${label} could not run tsc (${head.why}); the run goes on without it.`);
29260
+ return NOTHING_RAN2;
29261
+ }
29262
+ if (head.kind === "interrupted")
29263
+ return interrupted(label);
29264
+ if (head.kind === "broken") {
29265
+ const blocker = brokenBlockerText(label, head.run, head.said);
29266
+ activeOutput().info(`\uD83D\uDD0E ${label}: ${blocker}`);
29267
+ return { ran: true, red: true, interrupted: false, blocker };
29268
+ }
29269
+ const ran = `\uD83D\uDD0E ${label}: tsc over ${files.length} test file(s)`;
29270
+ if (head.errors.length === 0) {
29271
+ activeOutput().info(`${ran}: no type error.`);
29272
+ return GREEN;
29273
+ }
29274
+ const base = await baseErrors(input, label, modules, files.filter((file) => head.errors.some((error) => error.file === file.head)));
29275
+ if ("outcome" in base)
29276
+ return base.outcome;
29277
+ const added = newErrors(head.errors, base.errors);
29278
+ activeOutput().info(`${ran}: ${head.errors.length} error(s), ${head.errors.length - added.length} already held at ${input.base}, ${added.length} not held.`);
29279
+ for (const line of listed(added))
29280
+ activeOutput().info(` ${line}`);
29281
+ if (added.length === 0)
29282
+ return GREEN;
29283
+ return { ran: true, red: true, interrupted: false, blocker: typeBlockerText(label, input.base, added) };
29284
+ }
29285
+
29286
+ // src/start/task-step-checks.ts
29287
+ function lintTask(context, seams, input) {
29288
+ const isInterrupted = context.isInterrupted ?? (() => false);
29289
+ return runLintStep({ checkout: context.checkout, base: input.base, task: input.task, git: seams.git, runLint: seams.runLint, stopCode: SIGINT_EXIT_CODE, isInterrupted });
29290
+ }
29291
+ function typeCheckTask(context, seams, input) {
29292
+ const isInterrupted = context.isInterrupted ?? (() => false);
29293
+ return runTypeStep({ checkout: context.checkout, base: input.base, task: input.task, git: seams.git, runTypes: seams.runTypes, stopCode: SIGINT_EXIT_CODE, isInterrupted });
29294
+ }
29295
+ async function taskDiffChecks(context, seams, input, result) {
29296
+ if (isStepInterrupted(context, result))
29297
+ return {};
29298
+ const lint = await lintTask(context, seams, input);
29299
+ if (lint.interrupted)
29300
+ return { lint };
29301
+ return { lint, types: await typeCheckTask(context, seams, input) };
29302
+ }
28819
29303
  // src/start/suite-stage-step.ts
28820
- import { readFileSync as readFileSync42 } from "fs";
29304
+ import { readFileSync as readFileSync43 } from "fs";
28821
29305
  function stageSince(trackerPath, baseline) {
28822
29306
  const taken = readStageLedger(stageLedgerPathFor(trackerPath)).filter((entry) => entry.commit !== null);
28823
29307
  return taken.at(-1)?.commit ?? baseline?.commit ?? null;
@@ -28873,7 +29357,7 @@ async function runStageStep(context, stage, baseline) {
28873
29357
  }
28874
29358
  async function runDueStageSteps(context, baseline) {
28875
29359
  const ledger = readStageLedger(stageLedgerPathFor(context.trackerPath));
28876
- const due = dueStages(readFileSync42(context.trackerPath, "utf8"), ledger);
29360
+ const due = dueStages(readFileSync43(context.trackerPath, "utf8"), ledger);
28877
29361
  const outcomes = [];
28878
29362
  for (const stage of due) {
28879
29363
  const outcome2 = await runStageStep(context, stage, baseline);
@@ -28900,19 +29384,20 @@ function seamsOf(context) {
28900
29384
  listTestFiles: seams.listTestFiles ?? listTestFiles,
28901
29385
  readPreloadFiles: seams.readPreloadFiles ?? readPreloadFiles,
28902
29386
  now: seams.now ?? (() => new Date),
28903
- runLint: seams.runLint ?? runEslint
29387
+ runLint: seams.runLint ?? runEslint,
29388
+ runTypes: seams.runTypes ?? runTsc
28904
29389
  };
28905
29390
  }
28906
29391
  function junitFileFor(repoRoot, sessionId, kind) {
28907
- return join36(runsDir(repoRoot), sessionId, "suite", `${kind}.junit.xml`);
29392
+ return join37(runsDir(repoRoot), sessionId, "suite", `${kind}.junit.xml`);
28908
29393
  }
28909
29394
  function alwaysRunJunitFileFor(repoRoot, sessionId, kind = "task") {
28910
- return join36(runsDir(repoRoot), sessionId, "suite", `${kind}-always-run.junit.xml`);
29395
+ return join37(runsDir(repoRoot), sessionId, "suite", `${kind}-always-run.junit.xml`);
28911
29396
  }
28912
29397
  function stageLedgerPathFor(trackerPath) {
28913
29398
  const baselinePath = baselinePathFor(trackerPath);
28914
29399
  const name2 = basename11(baselinePath).replace(BASELINE_FILE_PREFIX, STAGE_LEDGER_PREFIX);
28915
- return join36(dirname13(baselinePath), name2);
29400
+ return join37(dirname15(baselinePath), name2);
28916
29401
  }
28917
29402
  function isLedgerEntry(value) {
28918
29403
  if (typeof value !== "object" || value === null)
@@ -28936,11 +29421,11 @@ function parseLedger(text) {
28936
29421
  return stages;
28937
29422
  }
28938
29423
  function readStageLedger(path3) {
28939
- if (!existsSync35(path3))
29424
+ if (!existsSync36(path3))
28940
29425
  return [];
28941
29426
  let parsed2;
28942
29427
  try {
28943
- parsed2 = parseLedger(readFileSync43(path3, "utf8"));
29428
+ parsed2 = parseLedger(readFileSync44(path3, "utf8"));
28944
29429
  } catch (error) {
28945
29430
  parsed2 = messageOf(error);
28946
29431
  }
@@ -28950,11 +29435,11 @@ function readStageLedger(path3) {
28950
29435
  return [];
28951
29436
  }
28952
29437
  function writeStageLedger(path3, entries) {
28953
- mkdirSync13(dirname13(path3), { recursive: true });
29438
+ mkdirSync14(dirname15(path3), { recursive: true });
28954
29439
  const temporary = `${path3}.${process.pid}.tmp`;
28955
- writeFileSync16(temporary, `${JSON.stringify({ version: STAGE_LEDGER_VERSION, stages: entries }, null, 2)}
29440
+ writeFileSync17(temporary, `${JSON.stringify({ version: STAGE_LEDGER_VERSION, stages: entries }, null, 2)}
28956
29441
  `, "utf8");
28957
- renameSync6(temporary, path3);
29442
+ renameSync7(temporary, path3);
28958
29443
  }
28959
29444
  function addToLedger(trackerPath, added) {
28960
29445
  if (added.length === 0)
@@ -28998,9 +29483,9 @@ function readDiff(git, from) {
28998
29483
  }
28999
29484
  function verdictOf3(result, baseline) {
29000
29485
  const { fresh, known } = splitFailures(result.failures, baseline);
29001
- const newErrors = Math.max(0, (result.errors ?? 0) - (baseline?.errors ?? 0));
29486
+ const newErrors2 = Math.max(0, (result.errors ?? 0) - (baseline?.errors ?? 0));
29002
29487
  const unreported2 = result.exitCode !== 0 && result.summary === null && result.noTestFiles !== true;
29003
- return { fresh, known, newErrors, unreported: unreported2 };
29488
+ return { fresh, known, newErrors: newErrors2, unreported: unreported2 };
29004
29489
  }
29005
29490
  function isStepInterrupted(context, result) {
29006
29491
  return result.exitCode === SIGINT_EXIT_CODE || context.isInterrupted?.() === true;
@@ -29038,6 +29523,15 @@ function announce2(label, result, known) {
29038
29523
  if (known.length > KNOWN_LISTED)
29039
29524
  activeOutput().info(` ...and ${known.length - KNOWN_LISTED} more.`);
29040
29525
  }
29526
+ function checksOf(settling) {
29527
+ return [settling.lint, settling.types].filter((check) => check !== undefined);
29528
+ }
29529
+ function isCheckInterrupted(settling) {
29530
+ return checksOf(settling).some((check) => check.interrupted);
29531
+ }
29532
+ function checkBlockers(settling) {
29533
+ return checksOf(settling).flatMap((check) => check.blocker === null ? [] : [check.blocker]);
29534
+ }
29041
29535
  function settleInterrupted(seams, settling) {
29042
29536
  const { kind, label, result } = settling;
29043
29537
  const step = { ...stepOf(settling, result, []), interrupted: true };
@@ -29048,32 +29542,32 @@ function settleInterrupted(seams, settling) {
29048
29542
  }
29049
29543
  function settleStep(context, seams, settling) {
29050
29544
  const { kind, label, result, baseline } = settling;
29051
- if (isStepInterrupted(context, result) || settling.lint?.interrupted === true)
29545
+ if (isStepInterrupted(context, result) || isCheckInterrupted(settling))
29052
29546
  return settleInterrupted(seams, settling);
29053
29547
  const verdict = verdictOf3(result, baseline);
29054
29548
  const step = stepOf(settling, result, verdict.fresh);
29055
29549
  recordStep(seams, step);
29056
29550
  announce2(label, result, verdict.known);
29057
- const lintBlocker = settling.lint?.blocker ?? null;
29058
- if (!isRed(verdict) && lintBlocker === null)
29551
+ const checked = checkBlockers(settling);
29552
+ if (!isRed(verdict) && checked.length === 0)
29059
29553
  return { kind, step, red: false, interrupted: false, blocker: null, blockedLine: null, repairInserted: false };
29060
29554
  const tested = isRed(verdict) ? [blockerText(label, result, verdict)] : [];
29061
- const blocker = [...tested, ...lintBlocker === null ? [] : [lintBlocker]].join(" ");
29555
+ const blocker = [...tested, ...checked].join(" ");
29062
29556
  activeOutput().error(`\u274C ${blocker}`);
29063
29557
  const written4 = settling.repair === null ? null : writeRepairTask(context.trackerPath, blocker, { ...settling.repair, commit: readHead(seams.git) });
29064
29558
  if (written4 !== null)
29065
29559
  activeOutput().error(written4.inserted ? ` A repair task (line ${written4.line + 1}) is inserted, blocked on it.` : ` The repair task (line ${written4.line + 1}) is marked blocked on it again.`);
29066
29560
  return { kind, step, red: true, interrupted: false, blocker, blockedLine: written4?.line ?? null, repairInserted: written4?.inserted ?? false };
29067
29561
  }
29068
- function isErrorsOnly(verdict, lint) {
29069
- return verdict.newErrors > 0 && verdict.fresh.length === 0 && !verdict.unreported && (lint?.blocker ?? null) === null;
29562
+ function isErrorsOnly(verdict, settling) {
29563
+ return verdict.newErrors > 0 && verdict.fresh.length === 0 && !verdict.unreported && checkBlockers(settling).length === 0;
29070
29564
  }
29071
29565
  async function retakeOnErrors(context, seams, settling, rerun) {
29072
29566
  const { label, result, baseline } = settling;
29073
- if (isStepInterrupted(context, result) || settling.lint?.interrupted === true)
29567
+ if (isStepInterrupted(context, result) || isCheckInterrupted(settling))
29074
29568
  return settling;
29075
29569
  const verdict = verdictOf3(result, baseline);
29076
- if (!isErrorsOnly(verdict, settling.lint))
29570
+ if (!isErrorsOnly(verdict, settling))
29077
29571
  return settling;
29078
29572
  recordStep(seams, stepOf(settling, result, verdict.fresh));
29079
29573
  announce2(label, result, verdict.known);
@@ -29116,7 +29610,7 @@ async function ensureBaseline(context) {
29116
29610
  activeOutput().warn(`\u26A0\uFE0F Could not write ${basename11(path3)} (${messageOf(error)}); the next run records the baseline again.`);
29117
29611
  }
29118
29612
  const outcome2 = settleStep(context, seams, settling);
29119
- const covered = completeStages(readFileSync43(context.trackerPath, "utf8"));
29613
+ const covered = completeStages(readFileSync44(context.trackerPath, "utf8"));
29120
29614
  addToLedger(context.trackerPath, covered.map((stage) => ({ ...stage, commit, via: "baseline" })));
29121
29615
  return { baseline, step: outcome2.step, interrupted: false };
29122
29616
  }
@@ -29159,10 +29653,6 @@ async function runWithAlwaysRun(context, seams, kind, runs) {
29159
29653
  const second = await runOne(context, seams, kind, { paths: runs.alwaysRun }, junitFile);
29160
29654
  return foldResults(first, second, SIGINT_EXIT_CODE);
29161
29655
  }
29162
- function lintTask(context, seams, input) {
29163
- const isInterrupted = context.isInterrupted ?? (() => false);
29164
- return runLintStep({ checkout: context.checkout, base: input.base, task: input.task, git: seams.git, runLint: seams.runLint, stopCode: SIGINT_EXIT_CODE, isInterrupted });
29165
- }
29166
29656
  async function runTaskStep(context, input) {
29167
29657
  const seams = seamsOf(context);
29168
29658
  const scope = await taskScopeOf(context, seams, input);
@@ -29170,8 +29660,8 @@ async function runTaskStep(context, input) {
29170
29660
  const recorded = scope?.scope ?? "full";
29171
29661
  const result = await runWithAlwaysRun(context, seams, "task", runs);
29172
29662
  const label = `task step after "${input.task}"`;
29173
- const lint = isStepInterrupted(context, result) ? {} : { lint: await lintTask(context, seams, input) };
29174
- const settling = { kind: "task", scope: recorded, reason: taskStepReason(scope), label, result, baseline: input.baseline, repair: { kind: "task", task: input.task }, ...lint };
29663
+ const checks = await taskDiffChecks(context, seams, input, result);
29664
+ const settling = { kind: "task", scope: recorded, reason: taskStepReason(scope), label, result, baseline: input.baseline, repair: { kind: "task", task: input.task }, ...checks };
29175
29665
  const outcome2 = settleStep(context, seams, await retakeOnErrors(context, seams, settling, () => runWithAlwaysRun(context, seams, "task", runs)));
29176
29666
  const timedIn = runs.alwaysRun.length > 0 ? alwaysRunJunitFileFor(context.repoRoot, context.sessionId) : junitFileFor(context.repoRoot, context.sessionId, "task");
29177
29667
  if (!outcome2.interrupted)
@@ -29485,13 +29975,13 @@ function similarCommentOpening(score, threshold) {
29485
29975
  }
29486
29976
  function possibleDuplicatesSection(duplicates2, redact) {
29487
29977
  const intro = `No open bug scored at or above the similarity threshold of ${String(duplicates2.threshold)}.`;
29488
- const listed = duplicates2.nearest.length === 0 ? "No open bug shares a word with this one." : duplicates2.nearest.map(({ issue: issue3, score }) => `- ${issueNameOf(issue3.ref)} (score ${scoreText(score)})`).join(`
29978
+ const listed2 = duplicates2.nearest.length === 0 ? "No open bug shares a word with this one." : duplicates2.nearest.map(({ issue: issue3, score }) => `- ${issueNameOf(issue3.ref)} (score ${scoreText(score)})`).join(`
29489
29979
  `);
29490
29980
  return `## Possible duplicates
29491
29981
 
29492
29982
  ${redact(`${intro}
29493
29983
 
29494
- ${listed}`)}`;
29984
+ ${listed2}`)}`;
29495
29985
  }
29496
29986
  function issueNameOf(ref8) {
29497
29987
  return ref8.url ?? `${ref8.kind} issue ${ref8.externalId}`;
@@ -29527,8 +30017,8 @@ function issueText(opening, values, redact) {
29527
30017
  }
29528
30018
 
29529
30019
  // src/triage/local-paths.ts
29530
- import { realpathSync as realpathSync5 } from "fs";
29531
- import { resolve as resolve21 } from "path";
30020
+ import { realpathSync as realpathSync6 } from "fs";
30021
+ import { resolve as resolve23 } from "path";
29532
30022
  var HOME_MARKER = "[redacted: HOME]";
29533
30023
  var SEGMENT = "[^/\\s'\"`()<>:,;]+";
29534
30024
  var HOME_PATTERN = `(?:/${SEGMENT})*?/(?:Users|home)/${SEGMENT}`;
@@ -29539,9 +30029,9 @@ function literalPattern(text) {
29539
30029
  return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
29540
30030
  }
29541
30031
  function forms(path3) {
29542
- const given = resolve21(path3);
30032
+ const given = resolve23(path3);
29543
30033
  try {
29544
- const real = realpathSync5(given);
30034
+ const real = realpathSync6(given);
29545
30035
  return real === given ? [given] : [given, real];
29546
30036
  } catch {
29547
30037
  return [given];
@@ -29572,11 +30062,11 @@ function localPathRedactor(repoRoot, home) {
29572
30062
 
29573
30063
  // src/triage/triage.ts
29574
30064
  import { homedir as homedir8 } from "os";
29575
- import { join as join37 } from "path";
30065
+ import { join as join38 } from "path";
29576
30066
 
29577
30067
  // src/effort/store/tracker-refs.ts
29578
30068
  import { randomUUID as randomUUID9 } from "crypto";
29579
- import { existsSync as existsSync36 } from "fs";
30069
+ import { existsSync as existsSync37 } from "fs";
29580
30070
  var SELECT_HELD2 = `
29581
30071
  SELECT seq, tracker_ref FROM findings
29582
30072
  WHERE session_id = ? AND artifact = ?
@@ -29725,7 +30215,7 @@ function writeTrackerRef(repoRoot, write, seams = {}) {
29725
30215
  wanted.json,
29726
30216
  collectedAt
29727
30217
  ];
29728
- const [action, stored] = withSqliteStore(path3, "write", !existsSync36(path3), (db) => {
30218
+ const [action, stored] = withSqliteStore(path3, "write", !existsSync37(path3), (db) => {
29729
30219
  const place = db.transaction(() => placeRef(db, path3, write, wanted, superseded, insertValues));
29730
30220
  return place.immediate();
29731
30221
  });
@@ -29734,7 +30224,7 @@ function writeTrackerRef(repoRoot, write, seams = {}) {
29734
30224
  function readTrackerRef(repoRoot, artifact) {
29735
30225
  checkArtifact("read", artifact);
29736
30226
  const path3 = sqliteStorePath(repoRoot);
29737
- if (!existsSync36(path3))
30227
+ if (!existsSync37(path3))
29738
30228
  return null;
29739
30229
  const newest = withSqliteStore(path3, "read", false, (db) => db.query(SELECT_NEWEST).get(artifact));
29740
30230
  return newest === null ? null : heldRefOf(path3, newest.seq, newest.tracker_ref).ref;
@@ -29804,7 +30294,7 @@ function createArtifactRefsVerifier(repoRoot, env) {
29804
30294
  // src/triage/similarity.ts
29805
30295
  import { basename as basename14 } from "path";
29806
30296
  var NON_WORD2 = /\W+/;
29807
- var FENCE_LINE6 = /^`{3,}$/;
30297
+ var FENCE_LINE7 = /^`{3,}$/;
29808
30298
  var HEADING_LINE3 = /^## (.+)$/;
29809
30299
  function wordSetOf(text) {
29810
30300
  const words = strippedText(text).toLowerCase().split(NON_WORD2).filter((word) => word.length > 0);
@@ -29824,7 +30314,7 @@ function fencedValueAt(lines, start) {
29824
30314
  if (open === -1)
29825
30315
  return null;
29826
30316
  const fence = lines[start + open];
29827
- if (!FENCE_LINE6.test(fence))
30317
+ if (!FENCE_LINE7.test(fence))
29828
30318
  return null;
29829
30319
  const body = lines.slice(start + open + 1);
29830
30320
  const close = body.indexOf(fence);
@@ -29863,12 +30353,12 @@ function nearestOpenBugs(report2, openBugs) {
29863
30353
  }
29864
30354
 
29865
30355
  // src/triage/triage.ts
29866
- var PRIVATE_TRIAGE_DIR = join37(".rafa", "triage", "private");
30356
+ var PRIVATE_TRIAGE_DIR = join38(".rafa", "triage", "private");
29867
30357
  var SECRET_ENV_NAMES = ["GITHUB_TOKEN", "ANTHROPIC_API_KEY", "LINEAR_API_KEY"];
29868
30358
  var TRIAGE_MODULE = "unassigned";
29869
30359
  var BLOCKER_SEPARATOR2 = "; ";
29870
30360
  function privateTriageDir(repoRoot) {
29871
- return join37(repoRoot, PRIVATE_TRIAGE_DIR);
30361
+ return join38(repoRoot, PRIVATE_TRIAGE_DIR);
29872
30362
  }
29873
30363
  function createPrivateTriageTracker(repoRoot, options = {}) {
29874
30364
  return createLocalTracker({
@@ -30047,10 +30537,10 @@ async function answerMiss(run) {
30047
30537
  const list = tracker.openIssues;
30048
30538
  if (similarity === null || list === undefined)
30049
30539
  return fileNew(run);
30050
- const listed = await step(run, `the ${run.route.channel} tracker openIssues`, () => list.call(tracker, "bug"));
30051
- if (!listed.ok)
30052
- return resultOf(run, "failed", { problem: listed.problem });
30053
- const nearest = nearestOpenBugs(run.filing.compared, listed.value);
30540
+ const listed2 = await step(run, `the ${run.route.channel} tracker openIssues`, () => list.call(tracker, "bug"));
30541
+ if (!listed2.ok)
30542
+ return resultOf(run, "failed", { problem: listed2.problem });
30543
+ const nearest = nearestOpenBugs(run.filing.compared, listed2.value);
30054
30544
  const [first] = nearest;
30055
30545
  return first !== undefined && first.score >= similarity.threshold ? commentOnNearest(run, first, similarity.threshold) : fileNew(run, { duplicates: { threshold: similarity.threshold, nearest: nearest.slice(0, similarity.candidates) } });
30056
30546
  }
@@ -30230,9 +30720,9 @@ function describeStartTriage(result, inherited = new Map) {
30230
30720
  };
30231
30721
  }
30232
30722
  async function resolvePublicTracker(options, redact) {
30233
- const resolve22 = options.resolve ?? resolveTracker;
30723
+ const resolve24 = options.resolve ?? resolveTracker;
30234
30724
  try {
30235
- const { tracker, attempts } = await resolve22({
30725
+ const { tracker, attempts } = await resolve24({
30236
30726
  config: options.config,
30237
30727
  context: { repoRoot: options.repoRoot }
30238
30728
  });
@@ -30419,8 +30909,128 @@ function aheadOf(track) {
30419
30909
  return Number(count);
30420
30910
  }
30421
30911
 
30912
+ // src/cleanup/groups.ts
30913
+ import { basename as basename15 } from "path";
30914
+
30915
+ // src/cleanup/past-head.ts
30916
+ import { posix as posix4 } from "path";
30917
+ var PATH_TERMINATOR = "\x00";
30918
+ var FIELD_SEPARATOR = "\t";
30919
+ function unread3(command, result) {
30920
+ const said = gitSaid(result);
30921
+ return {
30922
+ kind: "unread",
30923
+ detail: said === "" ? `git ${command} failed` : `git ${command} failed: ${said}`
30924
+ };
30925
+ }
30926
+ function terminated(output) {
30927
+ return output.split(PATH_TERMINATOR).filter((entry) => entry !== "");
30928
+ }
30929
+ function isAncestor(git, head, tip) {
30930
+ if (!git(["rev-parse", "--verify", "--quiet", `${head}^{commit}`]).ok) {
30931
+ return false;
30932
+ }
30933
+ const result = git(["merge-base", "--is-ancestor", head, tip]);
30934
+ if (result.ok) {
30935
+ return true;
30936
+ }
30937
+ return result.stderr.trim() === "" ? false : unread3("merge-base --is-ancestor", result);
30938
+ }
30939
+ function receiptIds(git, baseRef, changelog) {
30940
+ const path3 = posix4.normalize(changelog.replace(/\\/g, "/"));
30941
+ const listed2 = git(["ls-tree", "-z", "--name-only", baseRef, "--", path3]);
30942
+ if (!listed2.ok) {
30943
+ return unread3("ls-tree", listed2);
30944
+ }
30945
+ if (!terminated(listed2.stdout).includes(path3)) {
30946
+ return new Set;
30947
+ }
30948
+ const shown2 = git(["cat-file", "blob", `${baseRef}:${path3}`]);
30949
+ if (!shown2.ok) {
30950
+ return unread3("cat-file", shown2);
30951
+ }
30952
+ return new Set(changelogSections(shown2.stdout).flatMap((section2) => section2.receipt ?? []));
30953
+ }
30954
+ function readBaseHolding(git, baseRef, settings) {
30955
+ const prefix = directoryPrefix(settings.fragments);
30956
+ const listed2 = git(["ls-tree", "-r", "-z", "--name-only", baseRef, "--", prefix === "" ? "." : prefix]);
30957
+ if (!listed2.ok) {
30958
+ return unread3("ls-tree", listed2);
30959
+ }
30960
+ const ids = receiptIds(git, baseRef, settings.changelog);
30961
+ if ("kind" in ids) {
30962
+ return ids;
30963
+ }
30964
+ return { paths: new Set(terminated(listed2.stdout)), receiptIds: ids };
30965
+ }
30966
+ function isHeldFragment(path3, prefix, holding) {
30967
+ if (!path3.startsWith(prefix)) {
30968
+ return false;
30969
+ }
30970
+ const name2 = path3.slice(prefix.length);
30971
+ const id2 = name2.includes("/") ? null : fragmentIdOf(name2);
30972
+ if (id2 === null) {
30973
+ return false;
30974
+ }
30975
+ return holding.paths.has(path3) || holding.receiptIds.has(id2);
30976
+ }
30977
+ function commitsIn(git, range) {
30978
+ const logged = git(["log", "--reverse", `--format=%H${FIELD_SEPARATOR}%s`, range, "--"]);
30979
+ if (!logged.ok) {
30980
+ return unread3("log", logged);
30981
+ }
30982
+ return logged.stdout.split(`
30983
+ `).filter((line) => line !== "").map((line) => {
30984
+ const at = line.indexOf(FIELD_SEPARATOR);
30985
+ return at === -1 ? { hash: line, subject: "" } : { hash: line.slice(0, at), subject: line.slice(at + 1) };
30986
+ });
30987
+ }
30988
+ function readPastHead(git, branch, pullRequest, settings) {
30989
+ const tip = `refs/heads/${branch}`;
30990
+ const descends = isAncestor(git, pullRequest.headRefOid, tip);
30991
+ if (descends === false) {
30992
+ return { kind: "not-descended" };
30993
+ }
30994
+ if (descends !== true) {
30995
+ return descends;
30996
+ }
30997
+ const listed2 = commitsIn(git, `${pullRequest.headRefOid}..${tip}`);
30998
+ if ("kind" in listed2) {
30999
+ return listed2;
31000
+ }
31001
+ if (listed2.length === 0) {
31002
+ return { kind: "past-head", pullRequest, count: 0, commits: [], held: true };
31003
+ }
31004
+ const holding = readBaseHolding(git, `refs/heads/${settings.base}`, settings);
31005
+ if ("kind" in holding) {
31006
+ return holding;
31007
+ }
31008
+ const prefix = directoryPrefix(settings.fragments);
31009
+ const commits = [];
31010
+ for (const { hash, subject } of listed2) {
31011
+ const touched = git(["diff-tree", "-r", "-m", "--no-commit-id", "--name-only", "-z", hash]);
31012
+ if (!touched.ok) {
31013
+ return unread3("diff-tree", touched);
31014
+ }
31015
+ const paths = [...new Set(terminated(touched.stdout))];
31016
+ const held = paths.every((path3) => isHeldFragment(path3, prefix, holding));
31017
+ commits.push({ hash, subject, paths, held });
31018
+ }
31019
+ return {
31020
+ kind: "past-head",
31021
+ pullRequest,
31022
+ count: commits.length,
31023
+ commits,
31024
+ held: commits.every((each) => each.held)
31025
+ };
31026
+ }
31027
+
30422
31028
  // src/cleanup/groups.ts
30423
31029
  var MERGED_STATE_UNKNOWN = "merged state unknown";
31030
+ var DEFAULT_RELEASE_PATHS = Object.freeze({
31031
+ fragments: RELEASE_DEFAULTS.releaseFragments,
31032
+ changelog: RELEASE_DEFAULTS.releaseChangelog
31033
+ });
30424
31034
  var MS_PER_DAY2 = 24 * 60 * 60 * 1000;
30425
31035
  function unreachableNote(detail) {
30426
31036
  return `pull request provider unreachable (${detail}); merged pull requests not read, grouped from git alone`;
@@ -30428,6 +31038,13 @@ function unreachableNote(detail) {
30428
31038
  function baseUnreadNote(base, detail) {
30429
31039
  return `could not read which branches are merged into ${base} (${detail}); none is listed as merged into it`;
30430
31040
  }
31041
+ function pastHeadUnreadNote(branch, pullRequest, detail) {
31042
+ return `could not read the commits ${branch} holds past #${String(pullRequest)}'s head (${detail}); its delete is not forced`;
31043
+ }
31044
+ function pastHeadReason(reading2) {
31045
+ const subjects = reading2.commits.map((commit) => commit.subject).join(", ");
31046
+ return `${plural4(reading2.count, "commit")} past #${String(reading2.pullRequest.number)}'s head: ${subjects}`;
31047
+ }
30431
31048
  async function readProviderMerges(pulls) {
30432
31049
  if (pulls === null) {
30433
31050
  return { state: "none" };
@@ -30435,8 +31052,25 @@ async function readProviderMerges(pulls) {
30435
31052
  try {
30436
31053
  return { state: "read", pullRequests: await pulls.listMerged() };
30437
31054
  } catch (error) {
30438
- return { state: "unreachable", detail: firstLine7(messageOf(error)) };
31055
+ return { state: "unreachable", detail: firstLine8(messageOf(error)) };
31056
+ }
31057
+ }
31058
+ function heldReason(holder) {
31059
+ return `checked out in ${holder.name} (${holder.blockers.join(", ")})`;
31060
+ }
31061
+ function holdBranchRows(rows, worktrees) {
31062
+ const holders = new Map;
31063
+ for (const row of worktrees) {
31064
+ if (row.tickable || row.branch === null) {
31065
+ continue;
31066
+ }
31067
+ const blockers = [...new Set(row.blockers.map((blocker) => blocker.kind))];
31068
+ holders.set(row.branch, { path: row.path, name: basename15(row.path), blockers });
30439
31069
  }
31070
+ return rows.map((row) => {
31071
+ const holder = holders.get(row.branch.name);
31072
+ return holder === undefined ? row : { ...row, ticked: false, heldBy: holder, reason: `${row.reason}; ${heldReason(holder)}` };
31073
+ });
30440
31074
  }
30441
31075
  function classifyBranches(git, read, provider, settings) {
30442
31076
  const notes = provider.state === "unreachable" ? [unreachableNote(provider.detail)] : [];
@@ -30458,12 +31092,20 @@ function classifyBranches(git, read, provider, settings) {
30458
31092
  unknown: provider.state === "none",
30459
31093
  exclude: mergedRead.ok ? [baseRef] : []
30460
31094
  };
31095
+ const unreached = {
31096
+ git,
31097
+ base: read.base,
31098
+ baseRead: mergedRead.ok,
31099
+ release: settings.release ?? DEFAULT_RELEASE_PATHS,
31100
+ notes
31101
+ };
30461
31102
  for (const branch of read.branches) {
30462
31103
  const pulls = named3.get(branch.name) ?? [];
30463
31104
  const atTip = pulls.find((pull) => pull.headRefOid === tips.get(branch.name)) ?? null;
30464
31105
  const mergedBy = mergedReadings(branch, reachable.has(branch.name), atTip);
30465
31106
  if (mergedBy.length > 0) {
30466
- merged.push(mergedRow(branch, mergedBy, atTip, read.base));
31107
+ const row = mergedRow(branch, mergedBy, atTip, read.base);
31108
+ merged.push(mergedBy.includes("base") || atTip !== null ? row : unreachedRow(unreached, row, pulls));
30467
31109
  continue;
30468
31110
  }
30469
31111
  const suffix = rowSuffix(context, pulls[0] ?? null);
@@ -30481,13 +31123,36 @@ function classifyBranches(git, read, provider, settings) {
30481
31123
  stale.push({ group: "stale", branch, ticked: false, idleDays, reason: `no commit in ${plural4(idleDays, "day")}${suffix}` });
30482
31124
  }
30483
31125
  }
30484
- return { ok: true, base: read.base, merged, stale, notPushed, notes };
31126
+ const worktrees = settings.worktrees ?? [];
31127
+ return {
31128
+ ok: true,
31129
+ base: read.base,
31130
+ merged: holdBranchRows(merged, worktrees),
31131
+ stale: holdBranchRows(stale, worktrees),
31132
+ notPushed: holdBranchRows(notPushed, worktrees),
31133
+ notes
31134
+ };
31135
+ }
31136
+ function unreachedRow(context, row, pulls) {
31137
+ const settings = { base: context.base, ...context.release };
31138
+ for (const pull of pulls) {
31139
+ const reading2 = readPastHead(context.git, row.branch.name, pull, settings);
31140
+ if (reading2.kind === "past-head") {
31141
+ return { ...row, ticked: false, pastHead: reading2, reason: `${row.reason}; ${pastHeadReason(reading2)}` };
31142
+ }
31143
+ if (reading2.kind === "unread") {
31144
+ context.notes.push(pastHeadUnreadNote(row.branch.name, pull.number, firstLine8(reading2.detail)));
31145
+ break;
31146
+ }
31147
+ }
31148
+ const why = context.baseRead ? `${context.base} does not reach its tip` : `whether ${context.base} reaches its tip could not be read`;
31149
+ return { ...row, ticked: false, reason: `${row.reason}; ${why}` };
30485
31150
  }
30486
31151
  function namedPullRequests(pullRequests, branches) {
30487
- const listed = new Set(branches.map((branch) => branch.name));
31152
+ const listed2 = new Set(branches.map((branch) => branch.name));
30488
31153
  const named3 = new Map;
30489
31154
  for (const pull of pullRequests) {
30490
- if (listed.has(pull.headRefName)) {
31155
+ if (listed2.has(pull.headRefName)) {
30491
31156
  named3.set(pull.headRefName, [...named3.get(pull.headRefName) ?? [], pull]);
30492
31157
  }
30493
31158
  }
@@ -30556,7 +31221,7 @@ function countLost(git, name2, exclude) {
30556
31221
  return count;
30557
31222
  }
30558
31223
  function failure3(what, result) {
30559
- const said = firstLine7(gitSaid(result));
31224
+ const said = firstLine8(gitSaid(result));
30560
31225
  return said === "" ? `${what} failed` : `${what} failed: ${said}`;
30561
31226
  }
30562
31227
  function plural4(count, noun) {
@@ -30566,26 +31231,87 @@ function lines(text) {
30566
31231
  return text.split(`
30567
31232
  `).filter((line) => line !== "");
30568
31233
  }
30569
- function firstLine7(text) {
31234
+ function firstLine8(text) {
30570
31235
  return text.split(`
30571
31236
  `).map((line) => line.trim()).find((line) => line !== "") ?? "";
30572
31237
  }
30573
31238
 
31239
+ // src/cleanup/runs.ts
31240
+ import { existsSync as existsSync38, readdirSync as readdirSync15, readFileSync as readFileSync45 } from "fs";
31241
+ import { join as join39 } from "path";
31242
+ var LIVE_STATES = new Set(["running", "paused"]);
31243
+ function readRunRecords(root, seams = {}) {
31244
+ const dir = runsDir(root);
31245
+ const isAlive = seams.isAlive ?? isPidAlive;
31246
+ let names2;
31247
+ try {
31248
+ names2 = readdirSync15(dir);
31249
+ } catch (error) {
31250
+ if (errorCode(error) === "ENOENT")
31251
+ return { runs: [], notes: [] };
31252
+ return { runs: [], notes: [`run records: cannot list ${dir}: ${messageOf(error)}`] };
31253
+ }
31254
+ const notes = [];
31255
+ const finished = [];
31256
+ for (const name2 of names2.filter((entry) => entry.endsWith(RECORD_EXTENSION)).sort()) {
31257
+ const file = join39(dir, name2);
31258
+ const read = readRecord(file);
31259
+ if (typeof read === "string") {
31260
+ notes.push(read);
31261
+ continue;
31262
+ }
31263
+ if (!LIVE_STATES.has(readState(read, isAlive)))
31264
+ finished.push({ file, record: read });
31265
+ }
31266
+ const sorted = [...finished].sort((a, b) => byStart(a.record, b.record));
31267
+ const newest = new Map;
31268
+ for (const entry of sorted)
31269
+ newest.set(planKey(entry.record), entry);
31270
+ const kept = new Set(newest.values());
31271
+ const runs = sorted.filter((entry) => !kept.has(entry)).map((entry) => runRow(dir, entry));
31272
+ return { runs, notes };
31273
+ }
31274
+ function readRecord(file) {
31275
+ try {
31276
+ return parseSessionRecord(readFileSync45(file, "utf8"), file);
31277
+ } catch (error) {
31278
+ return `run records: skipped ${file}: ${messageOf(error)}`;
31279
+ }
31280
+ }
31281
+ function planKey(record) {
31282
+ return record.planStub === null ? `plan:${record.plan}` : `stub:${record.planStub}`;
31283
+ }
31284
+ function byStart(a, b) {
31285
+ return Date.parse(a.startedAt) - Date.parse(b.startedAt) || a.sessionId.localeCompare(b.sessionId);
31286
+ }
31287
+ function runRow(dir, entry) {
31288
+ const { record } = entry;
31289
+ const events = join39(dir, `${record.sessionId}${EVENTS_EXTENSION}`);
31290
+ return {
31291
+ sessionId: record.sessionId,
31292
+ path: entry.file,
31293
+ eventsPath: existsSync38(events) ? events : null,
31294
+ plan: record.planStub ?? record.plan,
31295
+ startedAt: record.startedAt,
31296
+ ticked: true
31297
+ };
31298
+ }
31299
+
30574
31300
  // src/cleanup/worktrees.ts
30575
- import { realpathSync as realpathSync7, statSync as statSync15 } from "fs";
30576
- import { join as join39, sep as sep3 } from "path";
31301
+ import { realpathSync as realpathSync8, statSync as statSync15 } from "fs";
31302
+ import { join as join41, sep as sep4 } from "path";
30577
31303
 
30578
31304
  // src/pr/worktree.ts
30579
- import { existsSync as existsSync37, realpathSync as realpathSync6 } from "fs";
30580
- import { join as join38 } from "path";
30581
- var WORKTREES_SUBDIR = join38(".rafa", "worktrees");
31305
+ import { existsSync as existsSync39, realpathSync as realpathSync7 } from "fs";
31306
+ import { join as join40 } from "path";
31307
+ var WORKTREES_SUBDIR = join40(".rafa", "worktrees");
30582
31308
  var WORKTREE_PREFIX = "pr-";
30583
31309
  var DEFAULT_REMOTE3 = "origin";
30584
31310
  var REFUSAL_PREFIX2 = "rafa pr triage --resolve refuses";
30585
31311
  var FORCING_FLAG = /^(?:-f|--force(?:-with-lease|-if-includes)?(?:=.*)?|--mirror)$/;
30586
31312
  var FORCING_REFSPEC_MARK = "+";
30587
31313
  function resolveWorktreePath(home, number) {
30588
- return join38(home, WORKTREES_SUBDIR, `${WORKTREE_PREFIX}${number}`);
31314
+ return join40(home, WORKTREES_SUBDIR, `${WORKTREE_PREFIX}${number}`);
30589
31315
  }
30590
31316
  function crossRepositoryRefusal(pr) {
30591
31317
  if (!pr.isCrossRepository)
@@ -30652,20 +31378,20 @@ function pushResolved(git, branch, remote = DEFAULT_REMOTE3) {
30652
31378
  return { id: step2.id, ok: false, command: worktreeCommandLine(step2), said: refusal4 };
30653
31379
  }
30654
31380
  function pathForms(path3) {
30655
- if (!existsSync37(path3))
31381
+ if (!existsSync39(path3))
30656
31382
  return [path3];
30657
- return [path3, realpathSync6(path3)];
31383
+ return [path3, realpathSync7(path3)];
30658
31384
  }
30659
31385
  function worktreeAt(git, path3) {
30660
- const listed = git(["worktree", "list", "--porcelain"]);
30661
- if (!listed.ok)
31386
+ const listed2 = git(["worktree", "list", "--porcelain"]);
31387
+ if (!listed2.ok)
30662
31388
  return null;
30663
31389
  const forms2 = pathForms(path3);
30664
- return parseWorktrees(listed.stdout).find((entry) => forms2.includes(entry.path)) ?? null;
31390
+ return parseWorktrees(listed2.stdout).find((entry) => forms2.includes(entry.path)) ?? null;
30665
31391
  }
30666
31392
 
30667
31393
  // src/cleanup/worktrees.ts
30668
- var CLAUDE_WORKTREES_SUBDIR = join39(".claude", "worktrees");
31394
+ var CLAUDE_WORKTREES_SUBDIR = join41(".claude", "worktrees");
30669
31395
  var WORKTREE_LIST = Object.freeze(["worktree", "list", "--porcelain"]);
30670
31396
  var GIT_DIR = Object.freeze(["rev-parse", "--absolute-git-dir"]);
30671
31397
  var WORKTREE_STATUS = Object.freeze([
@@ -30674,11 +31400,11 @@ var WORKTREE_STATUS = Object.freeze([
30674
31400
  "--porcelain=v1",
30675
31401
  "--untracked-files=all"
30676
31402
  ]);
30677
- var ADMIN_FILES = Object.freeze(["HEAD", join39("logs", "HEAD"), "index"]);
31403
+ var ADMIN_FILES = Object.freeze(["HEAD", join41("logs", "HEAD"), "index"]);
30678
31404
  var MS_PER_DAY3 = 24 * 60 * 60 * 1000;
30679
31405
  var UNTRACKED_MARK = "?? ";
30680
31406
  var LOCKED_LINE = "locked";
30681
- var LIVE_STATES = new Set(["running", "paused"]);
31407
+ var LIVE_STATES2 = new Set(["running", "paused"]);
30682
31408
  function defaultWorktreeSeams(cwd) {
30683
31409
  return {
30684
31410
  git: createGitRunner(cwd),
@@ -30693,7 +31419,7 @@ function defaultWorktreeSeams(cwd) {
30693
31419
  },
30694
31420
  realPath: (path3) => {
30695
31421
  try {
30696
- return realpathSync7(path3);
31422
+ return realpathSync8(path3);
30697
31423
  } catch {
30698
31424
  return path3;
30699
31425
  }
@@ -30701,21 +31427,21 @@ function defaultWorktreeSeams(cwd) {
30701
31427
  };
30702
31428
  }
30703
31429
  function readWorktrees(seams, read) {
30704
- const listed = seams.git(WORKTREE_LIST);
30705
- if (!listed.ok) {
30706
- return { ok: false, detail: failure4("git worktree list", listed) };
31430
+ const listed2 = seams.git(WORKTREE_LIST);
31431
+ if (!listed2.ok) {
31432
+ return { ok: false, detail: failure4("git worktree list", listed2) };
30707
31433
  }
30708
- const entries = parseWorktrees(listed.stdout);
31434
+ const entries = parseWorktrees(listed2.stdout);
30709
31435
  const main = entries[0]?.path;
30710
31436
  if (main === undefined) {
30711
31437
  return { ok: true, worktrees: [] };
30712
31438
  }
30713
31439
  const roots = [
30714
- ...pathForms2(seams, join39(main, CLAUDE_WORKTREES_SUBDIR)),
30715
- ...pathForms2(seams, join39(read.home, WORKTREES_SUBDIR)),
31440
+ ...pathForms2(seams, join41(main, CLAUDE_WORKTREES_SUBDIR)),
31441
+ ...pathForms2(seams, join41(read.home, WORKTREES_SUBDIR)),
30716
31442
  ...pathForms2(seams, worktreeDirAt(read.projectRoot, read.worktreeDir))
30717
31443
  ];
30718
- const locks = parseLocks(listed.stdout);
31444
+ const locks = parseLocks(listed2.stdout);
30719
31445
  const context = {
30720
31446
  seams,
30721
31447
  read,
@@ -30783,7 +31509,7 @@ function lastModified(seams, git, path3) {
30783
31509
  if (!gitDir.ok || adminDir === "") {
30784
31510
  return failure4("git rev-parse --absolute-git-dir", gitDir);
30785
31511
  }
30786
- const times = [path3, ...ADMIN_FILES.map((file) => join39(adminDir, file))].map((file) => seams.modifiedAt(file)).filter((time) => time !== null).map((time) => time.getTime());
31512
+ const times = [path3, ...ADMIN_FILES.map((file) => join41(adminDir, file))].map((file) => seams.modifiedAt(file)).filter((time) => time !== null).map((time) => time.getTime());
30787
31513
  if (times.length === 0) {
30788
31514
  return `no modification time could be read under ${path3} or ${adminDir}`;
30789
31515
  }
@@ -30835,7 +31561,7 @@ function recentBlockers(modified, read) {
30835
31561
  }
30836
31562
  function readLive(seams, root) {
30837
31563
  try {
30838
- return seams.sessions(root).filter((record) => LIVE_STATES.has(record.state));
31564
+ return seams.sessions(root).filter((record) => LIVE_STATES2.has(record.state));
30839
31565
  } catch (error) {
30840
31566
  return `the session records under ${runsDir(root)} could not be read: ${messageOf(error)}`;
30841
31567
  }
@@ -30858,7 +31584,7 @@ function pathForms2(seams, path3) {
30858
31584
  return resolved === path3 ? [path3] : [path3, resolved];
30859
31585
  }
30860
31586
  function isInside3(path3, dir) {
30861
- const prefix = dir.endsWith(sep3) ? dir : `${dir}${sep3}`;
31587
+ const prefix = dir.endsWith(sep4) ? dir : `${dir}${sep4}`;
30862
31588
  return path3.startsWith(prefix);
30863
31589
  }
30864
31590
  function failure4(what, result) {
@@ -30870,7 +31596,14 @@ function plural5(count, noun) {
30870
31596
  return count === 1 ? `1 ${noun}` : `${String(count)} ${noun}s`;
30871
31597
  }
30872
31598
  // src/cleanup/steps.ts
31599
+ import { rmSync as rmSync6 } from "fs";
31600
+ var SHORT_HASH_LENGTH2 = 7;
30873
31601
  var FORCING_FLAG2 = /^(?:--force(?:=.*)?|-[^-]*f[^-]*)$/;
31602
+ var DEFAULT_CLEANUP_FILES = Object.freeze({
31603
+ remove: (path3) => {
31604
+ rmSync6(path3);
31605
+ }
31606
+ });
30874
31607
  function frozenStep2(step2) {
30875
31608
  return Object.freeze({ ...step2, argv: Object.freeze([...step2.argv]) });
30876
31609
  }
@@ -30891,30 +31624,72 @@ function deleteBranchStep(branch, forced, after = null) {
30891
31624
  after
30892
31625
  });
30893
31626
  }
31627
+ function removeRunStep(row) {
31628
+ const events = row.eventsPath === null ? [] : [row.eventsPath];
31629
+ return frozenStep2({
31630
+ kind: "remove-run",
31631
+ subject: row.path,
31632
+ argv: ["rm", row.path, ...events],
31633
+ after: null
31634
+ });
31635
+ }
30894
31636
  function needsForcedDelete(row) {
30895
- return !row.mergedBy.includes("base") && row.mergedBy.includes("pull-request") && row.pullRequest !== null;
31637
+ if (row.mergedBy.includes("base")) {
31638
+ return false;
31639
+ }
31640
+ if (row.mergedBy.includes("pull-request") && row.pullRequest !== null) {
31641
+ return true;
31642
+ }
31643
+ return row.pastHead?.held === true;
31644
+ }
31645
+ function unheldPastHeadReason(reading2) {
31646
+ const unheld = reading2.commits.filter((commit) => !commit.held);
31647
+ const named3 = unheld.map((commit) => `${commit.hash.slice(0, SHORT_HASH_LENGTH2)} ${commit.subject}`).join(", ");
31648
+ const head = `#${String(reading2.pullRequest.number)}'s head`;
31649
+ const counted5 = unheld.length === 1 ? `1 commit past ${head} is not a release fragment` : `${String(unheld.length)} commits past ${head} are not release fragments`;
31650
+ return `${counted5} the base holds: ${named3}; deleting it would lose them`;
30896
31651
  }
30897
31652
  function branchPicks(selection) {
30898
31653
  return [
30899
- ...selection.merged.map((row) => ({ name: row.branch.name, merged: true, forced: needsForcedDelete(row) })),
30900
- ...selection.stale.map((row) => ({ name: row.branch.name, merged: false, forced: true })),
30901
- ...selection.notPushed.map((row) => ({ name: row.branch.name, merged: false, forced: true }))
31654
+ ...selection.merged.map((row) => ({
31655
+ name: row.branch.name,
31656
+ merged: true,
31657
+ forced: needsForcedDelete(row),
31658
+ holder: row.heldBy ?? null,
31659
+ unheld: row.pastHead === undefined || row.pastHead.held ? null : row.pastHead
31660
+ })),
31661
+ ...[...selection.stale, ...selection.notPushed].map((row) => ({
31662
+ name: row.branch.name,
31663
+ merged: false,
31664
+ forced: true,
31665
+ holder: row.heldBy ?? null,
31666
+ unheld: null
31667
+ }))
30902
31668
  ];
30903
31669
  }
30904
- function heldReason(path3) {
31670
+ function heldReason2(path3) {
30905
31671
  return `held by the worktree at ${path3}; only a Merged branch is deleted with its worktree`;
30906
31672
  }
30907
31673
  function blockedHolderReason(path3) {
30908
31674
  return `held by the worktree at ${path3}, which is not removed`;
30909
31675
  }
31676
+ function untickableHolderReason(holder) {
31677
+ return `${heldReason(holder)}, which is not removed`;
31678
+ }
30910
31679
  function branchStep(pick, removed, blocked3) {
31680
+ if (pick.holder !== null) {
31681
+ return { kind: "delete-branch", subject: pick.name, reason: untickableHolderReason(pick.holder) };
31682
+ }
31683
+ if (pick.unheld !== null) {
31684
+ return { kind: "delete-branch", subject: pick.name, reason: unheldPastHeadReason(pick.unheld) };
31685
+ }
30911
31686
  const blockedPath = blocked3.get(pick.name);
30912
31687
  if (blockedPath !== undefined) {
30913
31688
  return { kind: "delete-branch", subject: pick.name, reason: blockedHolderReason(blockedPath) };
30914
31689
  }
30915
31690
  const path3 = removed.get(pick.name) ?? null;
30916
31691
  if (path3 !== null && !pick.merged) {
30917
- return { kind: "delete-branch", subject: pick.name, reason: heldReason(path3) };
31692
+ return { kind: "delete-branch", subject: pick.name, reason: heldReason2(path3) };
30918
31693
  }
30919
31694
  return deleteBranchStep(pick.name, pick.forced, path3);
30920
31695
  }
@@ -30931,7 +31706,8 @@ function cleanupSteps(selection) {
30931
31706
  return Object.freeze({
30932
31707
  steps: Object.freeze([
30933
31708
  ...removable.map((row) => removeWorktreeStep2(row.path)),
30934
- ...branchEntries.filter(isStep)
31709
+ ...branchEntries.filter(isStep),
31710
+ ...(selection.runs ?? []).filter((row) => row.ticked).map((row) => removeRunStep(row))
30935
31711
  ]),
30936
31712
  withheld: Object.freeze([
30937
31713
  ...untickable.map((row) => ({ kind: "remove-worktree", subject: row.path, reason: row.reason })),
@@ -30961,19 +31737,32 @@ function afterFailedReason(path3) {
30961
31737
  function notRun(step2, said) {
30962
31738
  return { step: step2, ran: false, ok: false, command: cleanupCommandLine(step2), said };
30963
31739
  }
30964
- function runStep2(git, step2, removed) {
31740
+ function removeRun(files, step2) {
31741
+ const command = cleanupCommandLine(step2);
31742
+ for (const path3 of step2.argv.slice(1)) {
31743
+ try {
31744
+ files.remove(path3);
31745
+ } catch (error) {
31746
+ return { step: step2, ran: true, ok: false, command, said: `cannot remove ${path3}: ${messageOf(error)}` };
31747
+ }
31748
+ }
31749
+ return { step: step2, ran: true, ok: true, command, said: "" };
31750
+ }
31751
+ function runStep2(git, files, step2, removed) {
30965
31752
  const refusal4 = forcedFlagRefusal(step2.argv);
30966
31753
  if (refusal4 !== null)
30967
31754
  return notRun(step2, refusal4);
30968
31755
  if (step2.after !== null && !removed.has(step2.after))
30969
31756
  return notRun(step2, afterFailedReason(step2.after));
31757
+ if (step2.kind === "remove-run")
31758
+ return removeRun(files, step2);
30970
31759
  const result = git(step2.argv.slice(1));
30971
31760
  return { step: step2, ran: true, ok: result.ok, command: cleanupCommandLine(step2), said: gitSaid(result) };
30972
31761
  }
30973
- function runCleanupSteps(git, plan) {
31762
+ function runCleanupSteps(git, plan, files = DEFAULT_CLEANUP_FILES) {
30974
31763
  const removed = new Set;
30975
31764
  return plan.steps.map((step2) => {
30976
- const outcome2 = runStep2(git, step2, removed);
31765
+ const outcome2 = runStep2(git, files, step2, removed);
30977
31766
  if (outcome2.ok && step2.kind === "remove-worktree")
30978
31767
  removed.add(step2.subject);
30979
31768
  return outcome2;
@@ -30995,7 +31784,7 @@ async function readCleanup(seams, settings) {
30995
31784
  const result = seams.git(FETCH_PRUNE);
30996
31785
  fetched = result.ok;
30997
31786
  if (!result.ok) {
30998
- notes.push(fetchFailedNote(firstLine8(gitSaid(result)) || "git said nothing"));
31787
+ notes.push(fetchFailedNote(firstLine9(gitSaid(result)) || "git said nothing"));
30999
31788
  }
31000
31789
  }
31001
31790
  const branches = readBranches(seams.git, { base: settings.base, keep: settings.keep });
@@ -31005,7 +31794,8 @@ async function readCleanup(seams, settings) {
31005
31794
  const provider = await readProviderMerges(seams.pulls);
31006
31795
  const groups = classifyBranches(seams.git, branches, provider, {
31007
31796
  staleDays: settings.staleDays,
31008
- now: settings.now
31797
+ now: settings.now,
31798
+ ...settings.release === undefined ? {} : { release: settings.release }
31009
31799
  });
31010
31800
  if (!groups.ok) {
31011
31801
  return groups;
@@ -31022,15 +31812,17 @@ async function readCleanup(seams, settings) {
31022
31812
  if (!worktrees.ok) {
31023
31813
  return worktrees;
31024
31814
  }
31815
+ const runs = readRunRecords(settings.projectRoot, seams);
31025
31816
  return {
31026
31817
  ok: true,
31027
31818
  base: groups.base,
31028
31819
  fetched,
31029
- merged: groups.merged,
31030
- stale: groups.stale,
31031
- notPushed: groups.notPushed,
31820
+ merged: holdBranchRows(groups.merged, worktrees.worktrees),
31821
+ stale: holdBranchRows(groups.stale, worktrees.worktrees),
31822
+ notPushed: holdBranchRows(groups.notPushed, worktrees.worktrees),
31032
31823
  worktrees: worktrees.worktrees,
31033
- notes: [...notes, ...groups.notes]
31824
+ runs: runs.runs,
31825
+ notes: [...notes, ...groups.notes, ...runs.notes]
31034
31826
  };
31035
31827
  }
31036
31828
  function cleanupCounts(read) {
@@ -31041,7 +31833,7 @@ function cleanupCounts(read) {
31041
31833
  worktrees: read.worktrees.length
31042
31834
  };
31043
31835
  }
31044
- function firstLine8(text) {
31836
+ function firstLine9(text) {
31045
31837
  return text.split(`
31046
31838
  `).map((line) => line.trim()).find((line) => line !== "") ?? "";
31047
31839
  }
@@ -31068,9 +31860,76 @@ async function retargetPullRequest(pull, base, seams) {
31068
31860
  return { kind: "retargeted", from, to: base };
31069
31861
  }
31070
31862
 
31863
+ // src/start/release-body.ts
31864
+ var RELEASE_BODY_SEAMS = {
31865
+ pulls: ghPullRequestsIn,
31866
+ readProvider: (repoRoot) => resolvePrProvider({ configured: null, dir: repoRoot })
31867
+ };
31868
+ function unwritten(number, problem) {
31869
+ return { number, carried: false, already: false, problem };
31870
+ }
31871
+ async function carryIntoBody(pulls, branch, write) {
31872
+ try {
31873
+ const found = await pulls.findOpen(branch);
31874
+ if (found === null) {
31875
+ return unwritten(null, `no open pull request was found for ${branch} to write it to`);
31876
+ }
31877
+ const detail = await pulls.get(found.number);
31878
+ if (detail === null) {
31879
+ return unwritten(found.number, `pull request #${found.number} could not be read back`);
31880
+ }
31881
+ const next = bodyWithRelease(detail.body, write.sentence, write.block);
31882
+ if (next === detail.body) {
31883
+ return { number: found.number, carried: true, already: true, problem: null };
31884
+ }
31885
+ await pulls.editBody(found.number, next);
31886
+ return { number: found.number, carried: true, already: false, problem: null };
31887
+ } catch (error) {
31888
+ return unwritten(null, `the pull request body could not be written: ${messageOf(error)}`);
31889
+ }
31890
+ }
31891
+ function noProviderProblem(reading2) {
31892
+ const origin = reading2.remote === null ? "origin is not set" : `origin is ${reading2.remote}`;
31893
+ const because = reading2.source === "config" ? "pr.provider says so" : origin;
31894
+ return `this repository resolves to pr.provider: ${reading2.provider}, because ${because},` + " so there is no pull request to write it to";
31895
+ }
31896
+ function linesSubject(count) {
31897
+ return count === 1 ? { noun: "That line", verb: "is" } : { noun: "Those lines", verb: "are" };
31898
+ }
31899
+ function announceBody(write, count) {
31900
+ const out = activeOutput();
31901
+ const { noun, verb } = linesSubject(count);
31902
+ if (write.problem !== null) {
31903
+ out.error(` ${noun} ${verb} not in the pull request body: ${write.problem}`);
31904
+ return;
31905
+ }
31906
+ if (write.already) {
31907
+ out.info(` Pull request #${write.number} already carries ${noun.toLowerCase()}.`);
31908
+ return;
31909
+ }
31910
+ out.info(` ${noun} ${verb} now in the body of pull request #${write.number}.`);
31911
+ }
31912
+ async function writeBody(seams, target, write) {
31913
+ const reading2 = seams.readProvider(target.repoRoot);
31914
+ if (reading2.provider !== "gh") {
31915
+ const problem = noProviderProblem(reading2);
31916
+ activeOutput().info(` No pull request body carries ${JSON.stringify(write.lines.join(" "))}: ${problem}.`);
31917
+ return unwritten(null, problem);
31918
+ }
31919
+ const body = await carryIntoBody(seams.pulls(target.repoRoot), target.branch, write);
31920
+ announceBody(body, write.lines.length);
31921
+ return body;
31922
+ }
31923
+ async function carryReleaseIntoPullRequest(finish, target, seams = {}) {
31924
+ const { written: written4, body } = finish;
31925
+ if (written4 === null || body === null || body.number !== null)
31926
+ return null;
31927
+ return writeBody({ ...RELEASE_BODY_SEAMS, ...seams }, target, written4);
31928
+ }
31929
+
31071
31930
  // src/release/prepare.ts
31072
- import { mkdirSync as mkdirSync14, readFileSync as readFileSync44, writeFileSync as writeFileSync17 } from "fs";
31073
- import { dirname as dirname14, join as join40, posix as posix4 } from "path";
31931
+ import { mkdirSync as mkdirSync15, readFileSync as readFileSync46, writeFileSync as writeFileSync18 } from "fs";
31932
+ import { dirname as dirname16, join as join42, posix as posix5 } from "path";
31074
31933
  var NO_FRAGMENT = "no release fragment";
31075
31934
  var WHITESPACE3 = /\s+/g;
31076
31935
  function levelFields(reading2) {
@@ -31128,18 +31987,18 @@ function readBase(input) {
31128
31987
  }
31129
31988
  function textOf3(path3) {
31130
31989
  try {
31131
- return readFileSync44(path3, "utf8");
31990
+ return readFileSync46(path3, "utf8");
31132
31991
  } catch {
31133
31992
  return null;
31134
31993
  }
31135
31994
  }
31136
31995
  function nameOf2(path3) {
31137
- return posix4.basename(path3);
31996
+ return posix5.basename(path3);
31138
31997
  }
31139
31998
  function writeFragment(path3, resolved, text) {
31140
31999
  try {
31141
- mkdirSync14(dirname14(resolved), { recursive: true });
31142
- writeFileSync17(resolved, text);
32000
+ mkdirSync15(dirname16(resolved), { recursive: true });
32001
+ writeFileSync18(resolved, text);
31143
32002
  return null;
31144
32003
  } catch (error) {
31145
32004
  return `${path3} could not be written: ${messageOf(error)}`;
@@ -31162,8 +32021,8 @@ function prepareRelease(input) {
31162
32021
  const lines2 = fragmentNotes(input, level, title);
31163
32022
  const fragment = { plan: input.plan, title, level, notes: lines2.notes };
31164
32023
  const name2 = allocateFragmentName(input.plan, read.base.waiting.map((waiting) => nameOf2(waiting.path)));
31165
- const path3 = posix4.join(input.settings.releaseFragments.replace(/\\/g, "/"), name2);
31166
- const resolved = join40(input.repoRoot, path3);
32024
+ const path3 = posix5.join(input.settings.releaseFragments.replace(/\\/g, "/"), name2);
32025
+ const resolved = join42(input.repoRoot, path3);
31167
32026
  const before = textOf3(resolved);
31168
32027
  const after = serializeFragment(fragment);
31169
32028
  const problems = lines2.problem === null ? read.problems : [...read.problems, lines2.problem];
@@ -31183,18 +32042,18 @@ function prepareRelease(input) {
31183
32042
  }
31184
32043
 
31185
32044
  // src/release/verify.ts
31186
- import { readFileSync as readFileSync45, writeFileSync as writeFileSync18 } from "fs";
31187
- import { posix as posix5 } from "path";
32045
+ import { readFileSync as readFileSync47, writeFileSync as writeFileSync19 } from "fs";
32046
+ import { posix as posix6 } from "path";
31188
32047
  var STATUS_PREFIX_LENGTH = 3;
31189
32048
  function textOf4(path3) {
31190
32049
  try {
31191
- return readFileSync45(path3, "utf8");
32050
+ return readFileSync47(path3, "utf8");
31192
32051
  } catch {
31193
32052
  return null;
31194
32053
  }
31195
32054
  }
31196
32055
  function repoPath2(path3) {
31197
- const normal = posix5.normalize(path3.replace(/\\/g, "/"));
32056
+ const normal = posix6.normalize(path3.replace(/\\/g, "/"));
31198
32057
  return normal.endsWith("/") && normal.length > 1 ? normal.slice(0, -1) : normal;
31199
32058
  }
31200
32059
  function listOf3(names2) {
@@ -31204,7 +32063,7 @@ function listOf3(names2) {
31204
32063
  }
31205
32064
  function restoreFragment(file) {
31206
32065
  try {
31207
- writeFileSync18(file.resolved, file.after);
32066
+ writeFileSync19(file.resolved, file.after);
31208
32067
  return { path: file.path, restored: true, problem: null };
31209
32068
  } catch (error) {
31210
32069
  return {
@@ -31307,8 +32166,7 @@ var RELEASE_STAGE_SEAMS = {
31307
32166
  readNotes: readPlanChanges,
31308
32167
  git: createGitRunner,
31309
32168
  push: pushBranch,
31310
- pulls: ghPullRequestsIn,
31311
- readProvider: (repoRoot) => resolvePrProvider({ configured: null, dir: repoRoot }),
32169
+ ...RELEASE_BODY_SEAMS,
31312
32170
  now: () => new Date
31313
32171
  };
31314
32172
  var PLAN_HEADING = /^# +(.+)$/;
@@ -31317,14 +32175,10 @@ function releaseTitle(input) {
31317
32175
  return planTitleIn(input.planContent, input.planStub);
31318
32176
  }
31319
32177
  function planTitleIn(planContent, planStub2) {
31320
- for (const line of planContent.split(`
31321
- `)) {
31322
- const heading = PLAN_HEADING.exec(line);
31323
- if (heading === null)
31324
- continue;
31325
- return (heading[1] ?? "").replace(PLAN_LABEL, "").trim();
31326
- }
31327
- return planStub2 ?? "";
32178
+ const headings = planContent.split(`
32179
+ `).filter((line) => line.trim() !== ASSUMPTIONS_HEADING).map((line) => (PLAN_HEADING.exec(line)?.[1] ?? "").trim()).filter((heading) => heading !== "");
32180
+ const chosen = headings.find((heading) => PLAN_LABEL.test(heading)) ?? headings[0];
32181
+ return chosen === undefined ? planStub2 ?? "" : chosen.replace(PLAN_LABEL, "").trim();
31328
32182
  }
31329
32183
  function announcePreparation(preparation) {
31330
32184
  const out = activeOutput();
@@ -31407,61 +32261,6 @@ function bodyWithSentence(body, sentence) {
31407
32261
 
31408
32262
  ${sentence}`;
31409
32263
  }
31410
- function unwritten(number, problem) {
31411
- return { number, carried: false, already: false, problem };
31412
- }
31413
- async function carryIntoBody(pulls, branch, write) {
31414
- try {
31415
- const found = await pulls.findOpen(branch);
31416
- if (found === null) {
31417
- return unwritten(null, `no open pull request was found for ${branch} to write it to`);
31418
- }
31419
- const detail = await pulls.get(found.number);
31420
- if (detail === null) {
31421
- return unwritten(found.number, `pull request #${found.number} could not be read back`);
31422
- }
31423
- const next = bodyWithRelease(detail.body, write.sentence, write.block);
31424
- if (next === detail.body) {
31425
- return { number: found.number, carried: true, already: true, problem: null };
31426
- }
31427
- await pulls.editBody(found.number, next);
31428
- return { number: found.number, carried: true, already: false, problem: null };
31429
- } catch (error) {
31430
- return unwritten(null, `the pull request body could not be written: ${messageOf(error)}`);
31431
- }
31432
- }
31433
- function noProviderProblem(reading2) {
31434
- const origin = reading2.remote === null ? "origin is not set" : `origin is ${reading2.remote}`;
31435
- const because = reading2.source === "config" ? "pr.provider says so" : origin;
31436
- return `this repository resolves to pr.provider: ${reading2.provider}, because ${because},` + " so there is no pull request to write it to";
31437
- }
31438
- function linesSubject(count) {
31439
- return count === 1 ? { noun: "That line", verb: "is" } : { noun: "Those lines", verb: "are" };
31440
- }
31441
- function announceBody(write, count) {
31442
- const out = activeOutput();
31443
- const { noun, verb } = linesSubject(count);
31444
- if (write.problem !== null) {
31445
- out.error(` ${noun} ${verb} not in the pull request body: ${write.problem}`);
31446
- return;
31447
- }
31448
- if (write.already) {
31449
- out.info(` Pull request #${write.number} already carries ${noun.toLowerCase()}.`);
31450
- return;
31451
- }
31452
- out.info(` ${noun} ${verb} now in the body of pull request #${write.number}.`);
31453
- }
31454
- async function writeBody(io, input, write) {
31455
- const reading2 = io.readProvider(input.repoRoot);
31456
- if (reading2.provider !== "gh") {
31457
- const problem = noProviderProblem(reading2);
31458
- activeOutput().info(` No pull request body carries ${JSON.stringify(write.lines.join(" "))}: ${problem}.`);
31459
- return unwritten(null, problem);
31460
- }
31461
- const body = await carryIntoBody(io.pulls(input.repoRoot), input.branch, write);
31462
- announceBody(body, write.lines.length);
31463
- return body;
31464
- }
31465
32264
  async function reportFailure(io, input, outcome2, sentence, facts) {
31466
32265
  const out = activeOutput();
31467
32266
  if (outcome2 === "skipped") {
@@ -31473,8 +32272,9 @@ async function reportFailure(io, input, outcome2, sentence, facts) {
31473
32272
  }
31474
32273
  const parts = { forecast: null, levelReport: facts.levelReport };
31475
32274
  const block = releaseBodyBlock(parts);
31476
- const body = await writeBody(io, input, { sentence, block, lines: [sentence, ...releaseBodyLines(parts)] });
31477
- return { outcome: outcome2, sentence, forecast: null, body, ...facts };
32275
+ const written4 = { sentence, block, lines: [sentence, ...releaseBodyLines(parts)] };
32276
+ const body = await writeBody(io, input, written4);
32277
+ return { outcome: outcome2, sentence, forecast: null, written: written4, body, ...facts };
31478
32278
  }
31479
32279
  var NO_RELEASE = {
31480
32280
  outcome: "none",
@@ -31484,6 +32284,7 @@ var NO_RELEASE = {
31484
32284
  sentence: null,
31485
32285
  forecast: null,
31486
32286
  levelReport: null,
32287
+ written: null,
31487
32288
  body: null
31488
32289
  };
31489
32290
  async function reportForecast(io, input, forecast, facts) {
@@ -31493,8 +32294,9 @@ async function reportForecast(io, input, forecast, facts) {
31493
32294
  out.warn(` \u26A0\uFE0F ${problem}`);
31494
32295
  const parts = { forecast, levelReport: facts.levelReport };
31495
32296
  const block = releaseBodyBlock(parts);
31496
- const body = await writeBody(io, input, { sentence: null, block, lines: releaseBodyLines(parts) });
31497
- return { outcome: "released", sentence: null, forecast, body, ...facts };
32297
+ const written4 = { sentence: null, block, lines: releaseBodyLines(parts) };
32298
+ const body = await writeBody(io, input, written4);
32299
+ return { outcome: "released", sentence: null, forecast, written: written4, body, ...facts };
31498
32300
  }
31499
32301
  async function finishRelease(input, seams = {}) {
31500
32302
  const io = { ...RELEASE_STAGE_SEAMS, ...seams };
@@ -31539,7 +32341,7 @@ async function finishRelease(input, seams = {}) {
31539
32341
  }
31540
32342
 
31541
32343
  // src/start/runner-pr.ts
31542
- import { readFileSync as readFileSync46 } from "fs";
32344
+ import { readFileSync as readFileSync48 } from "fs";
31543
32345
  import path3 from "path";
31544
32346
  var RELEASE_NOTES_HEADING = "## Release notes";
31545
32347
  var UNFINISHED_WRAP_UP_LINE = "> \u26A0\uFE0F The wrap-up did not finish: no wrap-up session opened this pull request, so the loop opened it. Check that the base is merged in, the release notes were rewritten and the lessons were promoted before merging.";
@@ -31572,7 +32374,7 @@ function fragmentNotesIn(checkout, fragment) {
31572
32374
  return [];
31573
32375
  let text;
31574
32376
  try {
31575
- text = readFileSync46(path3.resolve(checkout, fragment), "utf8");
32377
+ text = readFileSync48(path3.resolve(checkout, fragment), "utf8");
31576
32378
  } catch {
31577
32379
  return [];
31578
32380
  }
@@ -31634,7 +32436,7 @@ async function openRunnerPullRequest(input, seams) {
31634
32436
  }
31635
32437
 
31636
32438
  // src/start/wrap-up.ts
31637
- import { posix as posix7 } from "path";
32439
+ import { posix as posix8 } from "path";
31638
32440
 
31639
32441
  // src/pr/conflict-sentence.ts
31640
32442
  var MECHANICAL_CONFLICT_SENTENCE = "Resolve MECHANICAL conflicts yourself and do not stop for them: dependency version bumps (take the base's version unless this branch deliberately pinned it, and say which in the commit), lockfiles, generated artifacts, and complementary additions where both sides appended different material to the same file (keep BOTH). Stop only for a genuine semantic conflict \u2014 two sides changing the same behaviour incompatibly. In that case commit nothing, leave the branch as it is, and report the conflicting paths and both sides' intent, so a human decides.";
@@ -31643,7 +32445,7 @@ function mechanicalConflictBullet() {
31643
32445
  }
31644
32446
 
31645
32447
  // src/start/promoted-check.ts
31646
- import { isAbsolute as isAbsolute9, posix as posix6, relative as relative11 } from "path";
32448
+ import { isAbsolute as isAbsolute9, posix as posix7, relative as relative12 } from "path";
31647
32449
 
31648
32450
  // src/start/promoted.ts
31649
32451
  var PROMOTED_BLOCK_KIND = "promoted";
@@ -31688,7 +32490,7 @@ function parsePromoted(output) {
31688
32490
  return { status: "unclosed", answers: [], unreadable: [] };
31689
32491
  const answers = [];
31690
32492
  const unreadable = [];
31691
- const firstLine9 = new Map;
32493
+ const firstLine10 = new Map;
31692
32494
  block.body.split(`
31693
32495
  `).forEach((text, index) => {
31694
32496
  if (text.trim() === "")
@@ -31700,13 +32502,13 @@ function parsePromoted(output) {
31700
32502
  return;
31701
32503
  }
31702
32504
  const { id: id2 } = reading2.answer;
31703
- const earlier = firstLine9.get(id2);
32505
+ const earlier = firstLine10.get(id2);
31704
32506
  if (earlier !== undefined) {
31705
32507
  const reason = `\`${id2}\` was already answered at line ${String(earlier)}`;
31706
32508
  unreadable.push({ line, text, reason });
31707
32509
  return;
31708
32510
  }
31709
- firstLine9.set(id2, line);
32511
+ firstLine10.set(id2, line);
31710
32512
  answers.push(reading2.answer);
31711
32513
  });
31712
32514
  return { status: "read", answers, unreadable };
@@ -31734,8 +32536,8 @@ function changedSince(git, head) {
31734
32536
  return { ok: true, head, paths: new Set(paths) };
31735
32537
  }
31736
32538
  function repoPath3(path4, repoRoot) {
31737
- const inside = isAbsolute9(path4) ? relative11(repoRoot, path4) : path4;
31738
- const normal = posix6.normalize(inside.replaceAll("\\", "/"));
32539
+ const inside = isAbsolute9(path4) ? relative12(repoRoot, path4) : path4;
32540
+ const normal = posix7.normalize(inside.replaceAll("\\", "/"));
31739
32541
  if (normal === "." || normal === ".." || normal.startsWith("../") || isAbsolute9(normal))
31740
32542
  return null;
31741
32543
  return normal.replace(/^(\.\/)+/, "");
@@ -31748,7 +32550,7 @@ function promotedOutcome(lesson, named3, changed, repoRoot) {
31748
32550
  }
31749
32551
  function checkPromoted(lessons, reading2, changed, repoRoot) {
31750
32552
  const answers = new Map(reading2.answers.map((answer4) => [answer4.id, answer4]));
31751
- const listed = new Set(lessons.map((lesson) => lesson.id));
32553
+ const listed2 = new Set(lessons.map((lesson) => lesson.id));
31752
32554
  const promoted = [];
31753
32555
  const skipped2 = [];
31754
32556
  const unpromoted = [];
@@ -31766,7 +32568,7 @@ function checkPromoted(lessons, reading2, changed, repoRoot) {
31766
32568
  unpromoted.push(outcome2);
31767
32569
  }
31768
32570
  }
31769
- const unlisted = reading2.answers.map((answer4) => answer4.id).filter((id2) => !listed.has(id2));
32571
+ const unlisted = reading2.answers.map((answer4) => answer4.id).filter((id2) => !listed2.has(id2));
31770
32572
  return { promoted, skipped: skipped2, unpromoted, unlisted };
31771
32573
  }
31772
32574
  function unpromotedPhrase(lesson, head) {
@@ -31934,7 +32736,7 @@ function releaseBullets(release) {
31934
32736
  ];
31935
32737
  }
31936
32738
  function fragmentDirectory(prepared) {
31937
- return posix7.dirname(prepared.file.path);
32739
+ return posix8.dirname(prepared.file.path);
31938
32740
  }
31939
32741
  function fragmentRewriteBullet(prepared) {
31940
32742
  const { plan, level } = prepared.fragment;
@@ -31982,9 +32784,10 @@ async function lessonsToPromote(learning) {
31982
32784
  return [];
31983
32785
  }
31984
32786
  }
31985
- async function preserveProgress(planContent, settingSources, release, serving, learning, base, checkout) {
32787
+ async function preserveProgress(planContent, settingSources, release, serving, learning, base, checkout, seams = {}) {
32788
+ const lookup = seams.lookup ?? openPullRequestNumber;
31986
32789
  const branch = getCurrentBranch(checkout);
31987
- const openPullRequest = await openPullRequestNumber(checkout, branch);
32790
+ const openPullRequest = await lookup(checkout, branch);
31988
32791
  return runWrapUpSession({
31989
32792
  buildPrompt: (lessons) => buildWrapUpPrompt(branch, base, planContent, openPullRequest, release, lessons),
31990
32793
  settingSources,
@@ -31992,10 +32795,15 @@ async function preserveProgress(planContent, settingSources, release, serving, l
31992
32795
  learning,
31993
32796
  checkout,
31994
32797
  branch,
31995
- succeeded: `
31996
- \u2705 Progress preserved; PR opened or updated on this branch.`
32798
+ succeeded: async () => progressPreservedLine(await lookup(checkout, branch)),
32799
+ ...seams.spawn === undefined ? {} : { spawn: seams.spawn }
31997
32800
  });
31998
32801
  }
32802
+ function progressPreservedLine(openPullRequest) {
32803
+ return openPullRequest === null ? `
32804
+ \u2705 Progress preserved; no pull request is open on this branch yet \u2014 the loop retries the wrap-up or opens one itself.` : `
32805
+ \u2705 Progress preserved; pull request #${openPullRequest} is open on this branch.`;
32806
+ }
31999
32807
  async function runWrapUpSession(run) {
32000
32808
  const { learning, checkout } = run;
32001
32809
  const lessons = await lessonsToPromote(learning);
@@ -32011,7 +32819,7 @@ async function runWrapUpSession(run) {
32011
32819
  \u274C Failed to preserve progress (exit ${session.exitCode}). Please try again.`);
32012
32820
  return session.stdout;
32013
32821
  }
32014
- activeOutput().info(run.succeeded);
32822
+ activeOutput().info(typeof run.succeeded === "string" ? run.succeeded : await run.succeeded());
32015
32823
  if (learning === null || git === null)
32016
32824
  return session.stdout;
32017
32825
  await checkWrapUpAnswer({
@@ -32085,7 +32893,7 @@ async function runWrapUp(input) {
32085
32893
  session.wrapUpStarted();
32086
32894
  activeOutput().info(`
32087
32895
  \u2705 All tasks completed!`);
32088
- activeOutput().info("\uD83E\uDDF9 Wrap-up session starting: promote progress.txt findings, sync with main, then commit, push and open the PR.");
32896
+ activeOutput().info("\uD83E\uDDF9 Wrap-up session starting: promote the listed lessons, sync with main, then commit, push and open the PR.");
32089
32897
  activeOutput().info(" This is one full Claude session with no intermediate output \u2014 expect several quiet minutes. Interrupting it skips the push and PR; if that happens, run again to retry just this stage.");
32090
32898
  emitLoopEvent({ kind: "wrap-up", phase: "fragment" });
32091
32899
  const release = prepareReleaseStage({
@@ -32103,9 +32911,8 @@ async function runWrapUp(input) {
32103
32911
  dir: checkout
32104
32912
  });
32105
32913
  const lookup = () => readProvider().provider === "none" ? Promise.resolve(NO_PROVIDER_REASON) : openPullRequestNumber(checkout, expected2.branch);
32106
- await emitPullRequestEvent("session", expected2.branch, null, lookup);
32107
32914
  if (haltIfWrapUpMoved({ expected: expectWrapUpCommits(expected2), before: "release" })) {
32108
- await emitPullRequestEvent("delivery", expected2.branch, null, lookup);
32915
+ await emitPullRequestEvent(expected2.branch, null, lookup);
32109
32916
  emitLoopEvent({ kind: "halt", reason: "checkout moved" });
32110
32917
  return;
32111
32918
  }
@@ -32113,14 +32920,15 @@ async function runWrapUp(input) {
32113
32920
  const finish = await finishRelease({ repoRoot: checkout, branch: expected2.branch, settings, preparation: release }, { readProvider });
32114
32921
  if (readProvider().provider !== "none") {
32115
32922
  const delivery = await deliverPullRequest({ branch: expected2.branch, retries: settings.loopWrapUpRetries, previousMessage: finalMessage }, deliverySeamsIn({ ...input, base, fragment: finish.fragment }));
32116
- await emitPullRequestEvent("delivery", expected2.branch, delivery.kind === "delivered" ? delivery.pull.number : null, lookup);
32923
+ await emitPullRequestEvent(expected2.branch, delivery.kind === "delivered" ? delivery.pull.number : null, lookup);
32117
32924
  if (delivery.kind === "interrupted")
32118
32925
  return;
32119
32926
  if (delivery.kind === "blocked")
32120
32927
  throw new CommandExit(1, delivery.message);
32928
+ await carryReleaseIntoPullRequest(finish, { repoRoot: checkout, branch: expected2.branch }, { readProvider });
32121
32929
  await retargetPullRequest(delivery.pull, base, { pulls: ghPullRequestsIn(checkout), output: activeOutput() });
32122
32930
  } else {
32123
- await emitPullRequestEvent("delivery", expected2.branch, null, lookup);
32931
+ await emitPullRequestEvent(expected2.branch, null, lookup);
32124
32932
  }
32125
32933
  if (ciWait) {
32126
32934
  emitLoopEvent({ kind: "wrap-up", phase: "ci" });
@@ -32134,11 +32942,7 @@ async function runWrapUp(input) {
32134
32942
  session.finished();
32135
32943
  }
32136
32944
  var NO_PROVIDER_REASON = "no pull request provider is configured";
32137
- async function emitPullRequestEvent(place, branch, known, lookup) {
32138
- const textPlace = "delivery";
32139
- const ownPlace = activeOutputMode() === "text" ? textPlace : "session";
32140
- if (place !== ownPlace)
32141
- return;
32945
+ async function emitPullRequestEvent(branch, known, lookup) {
32142
32946
  const number = known ?? await lookup();
32143
32947
  if (typeof number === "string") {
32144
32948
  emitLoopEvent({ kind: "no-pr", reason: number });
@@ -32275,8 +33079,8 @@ function msUntil(startAt, now = new Date) {
32275
33079
  return target.getTime() - now.getTime();
32276
33080
  }
32277
33081
  function sleepFor(ms) {
32278
- return new Promise((resolve22) => {
32279
- setTimeout(resolve22, ms);
33082
+ return new Promise((resolve24) => {
33083
+ setTimeout(resolve24, ms);
32280
33084
  });
32281
33085
  }
32282
33086
  async function deferUntil(startAt, now = new Date, sleep = sleepFor) {
@@ -32287,7 +33091,7 @@ async function deferUntil(startAt, now = new Date, sleep = sleepFor) {
32287
33091
 
32288
33092
  // src/start.ts
32289
33093
  var __dirname2 = path4.dirname(fileURLToPath2(import.meta.url));
32290
- var interrupted = false;
33094
+ var interrupted2 = false;
32291
33095
  async function start(args, repoRoot) {
32292
33096
  refuseDetachedRun(args);
32293
33097
  refuseWorktreeBesideCreateBranch(args);
@@ -32391,7 +33195,7 @@ async function start(args, repoRoot) {
32391
33195
  sessionId: session.id,
32392
33196
  settings: runConfig.config,
32393
33197
  planContent,
32394
- isInterrupted: () => interrupted
33198
+ isInterrupted: () => interrupted2
32395
33199
  });
32396
33200
  if (!fs.existsSync(trackerPath)) {
32397
33201
  activeOutput().info(`\uD83D\uDCCB Creating new plan tracker at ${path4.basename(trackerPath)}...`);
@@ -32400,14 +33204,14 @@ async function start(args, repoRoot) {
32400
33204
  activeOutput().info(`\uD83D\uDCCB Resuming from existing ${path4.basename(trackerPath)}...`);
32401
33205
  }
32402
33206
  process.on("SIGINT", () => {
32403
- interrupted = true;
33207
+ interrupted2 = true;
32404
33208
  interruptClaudeSessions();
32405
33209
  });
32406
33210
  while (true) {
32407
- if (interrupted)
33211
+ if (interrupted2)
32408
33212
  break;
32409
- await holdWhilePaused({ repoRoot, sessionId: session.id, isInterrupted: () => interrupted });
32410
- if (interrupted)
33213
+ await holdWhilePaused({ repoRoot, sessionId: session.id, isInterrupted: () => interrupted2 });
33214
+ if (interrupted2)
32411
33215
  break;
32412
33216
  const trackerContent = fs.readFileSync(trackerPath, "utf8");
32413
33217
  const taskInfo = findNextTask(trackerContent);
@@ -32423,7 +33227,7 @@ async function start(args, repoRoot) {
32423
33227
  emitLoopEvent({ kind: "halt", reason: "suite step red" });
32424
33228
  return;
32425
33229
  }
32426
- if (interrupted)
33230
+ if (interrupted2)
32427
33231
  break;
32428
33232
  if (suiteGate === "repair")
32429
33233
  continue;
@@ -32444,7 +33248,7 @@ async function start(args, repoRoot) {
32444
33248
  ciWait,
32445
33249
  ciTimeoutMin,
32446
33250
  ciAttempts,
32447
- isInterrupted: () => interrupted
33251
+ isInterrupted: () => interrupted2
32448
33252
  });
32449
33253
  break;
32450
33254
  }
@@ -32467,16 +33271,22 @@ async function start(args, repoRoot) {
32467
33271
  alwaysRun: readAlwaysRunFiles(createGitRunner(checkout), runConfig.config.testsAlwaysRun),
32468
33272
  serving,
32469
33273
  handout,
32470
- base
33274
+ base,
33275
+ guard: { expected: expected2, trackerPath }
32471
33276
  });
32472
33277
  const { exitCode } = dispatch;
33278
+ if (dispatch.halted) {
33279
+ emitLoopEvent({ kind: "task-blocked", position, reason: "checkout moved" });
33280
+ emitLoopEvent({ kind: "halt", reason: "checkout moved" });
33281
+ return;
33282
+ }
32473
33283
  const storeReport = async (outcome2) => {
32474
33284
  const stored2 = await storeTaskReport({ repoRoot, planStub: planStub2, dispatch, outcome: outcome2, learning });
32475
33285
  await triageTask({ trackerPath, lineNum: taskInfo.lineNum, planStub: planStub2, dispatch, outcome: outcome2 });
32476
33286
  await hubContact.pushThenPull();
32477
33287
  return stored2;
32478
33288
  };
32479
- if (interrupted) {
33289
+ if (interrupted2) {
32480
33290
  updateTrackerLine(trackerPath, taskInfo.lineNum, "blocked");
32481
33291
  activeOutput().info(`
32482
33292
  \u26A0\uFE0F Interrupted. Task marked as blocked. Run again to resume.`);
@@ -32609,11 +33419,11 @@ function claimOnRemote(attempt3, ahead, context) {
32609
33419
  const answer4 = keepLocally(attempt3, git, "offline", fetched.reason);
32610
33420
  return ahead === null ? answer4 : withAhead(answer4, notTried(ahead, `${REMOTE} could not be reached`));
32611
33421
  }
32612
- const listed = git(["for-each-ref", "--format=%(refname:lstrip=3)", `refs/remotes/${REMOTE}/${BRANCH_PREFIX2}`]);
32613
- if (!listed.ok) {
32614
- return refused2(attempt3, attempt3.branch, null, `the ${REMOTE} branches of #${String(attempt3.issue)} could not be listed: ${gitSaid(listed)}`);
33422
+ const listed2 = git(["for-each-ref", "--format=%(refname:lstrip=3)", `refs/remotes/${REMOTE}/${BRANCH_PREFIX2}`]);
33423
+ if (!listed2.ok) {
33424
+ return refused2(attempt3, attempt3.branch, null, `the ${REMOTE} branches of #${String(attempt3.issue)} could not be listed: ${gitSaid(listed2)}`);
32615
33425
  }
32616
- const names2 = listed.stdout.split(`
33426
+ const names2 = listed2.stdout.split(`
32617
33427
  `).map((line) => line.trim());
32618
33428
  const move = readMove(attempt3, names2, context);
32619
33429
  if (ahead === null || move.kind === "answer")
@@ -33151,11 +33961,11 @@ async function resolveCreateSpec(options, seams = DEFAULT_ROUTE_SEAMS) {
33151
33961
  const source = readSpecSourceFlags(options.args);
33152
33962
  if (source.request === null)
33153
33963
  throw new CommandExit(SOURCE_REFUSAL_EXIT, usageRefusal(specsDir));
33154
- const resolve22 = seams.resolve ?? resolvePlanSpec;
33964
+ const resolve24 = seams.resolve ?? resolvePlanSpec;
33155
33965
  const makeReadyOffer = seams.makeReadyOffer ?? createPlanReadyOffer;
33156
33966
  const makeAlternativeOffer = seams.makeAlternativeOffer ?? createPlanBlockedOffer;
33157
33967
  const makeRefreshOffer = seams.makeRefreshOffer ?? createPlanRefreshOffer;
33158
- return await resolve22({
33968
+ return await resolve24({
33159
33969
  request: source.request,
33160
33970
  refresh: source.refresh,
33161
33971
  dryRun: source.dryRun,
@@ -33184,7 +33994,7 @@ function formatProgressSection(progressContent) {
33184
33994
  const trimmed = progressContent?.trim();
33185
33995
  if (!trimmed)
33186
33996
  return "";
33187
- const capped = trimmed.length > PROGRESS_CAP_CHARS ? `\u2026(older findings truncated)\u2026
33997
+ const capped2 = trimmed.length > PROGRESS_CAP_CHARS ? `\u2026(older findings truncated)\u2026
33188
33998
  ${trimmed.slice(-PROGRESS_CAP_CHARS)}` : trimmed;
33189
33999
  return [
33190
34000
  "## Findings from previous runs (progress.txt)",
@@ -33195,7 +34005,7 @@ ${trimmed.slice(-PROGRESS_CAP_CHARS)}` : trimmed;
33195
34005
  "not requirements, they may be stale, and they must NOT be copied into",
33196
34006
  "the plan.",
33197
34007
  "",
33198
- capped,
34008
+ capped2,
33199
34009
  ""
33200
34010
  ].join(`
33201
34011
  `);
@@ -33421,6 +34231,7 @@ async function plan(args, repoRoot, registry = CORE_ADAPTER_REGISTRY) {
33421
34231
  await requireNoticesAnswered();
33422
34232
  activeOutput().info(`\uD83D\uDCDD Generating ${planFile} from ${path8.basename(specPath2)}...`);
33423
34233
  const generated = await generateOrExit(planner, { specPath: specRequest, stub }, gate, flags.skipReview);
34234
+ enforceStoreRules(repoRoot, generated);
33424
34235
  await settleReview(gate, generated, flags.skipReview);
33425
34236
  if (resolved.spec.issue !== null) {
33426
34237
  recordPlanIssue(repoRoot, generated.planPath, resolved.spec.issue);
@@ -33437,4 +34248,4 @@ async function plan(args, repoRoot, registry = CORE_ADAPTER_REGISTRY) {
33437
34248
  }
33438
34249
  }
33439
34250
 
33440
- export { SERVE_CLI_VERSION, PROVENANCE_FIELD, SKILL_FILE2 as SKILL_FILE, provenanceBlock, serveVerdict, SKILL_USE_CLI_VERSION, sessionLogDir, MANIFEST_VERSION, FEATURE_TYPES, OUTPUT_CHANNELS, RUNNING_MANIFEST_SEAMS, validateManifest, loadModules, moduleSettings, pullBeforeRead, 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, parseReportArgs, formatCount, alignRows, verdictOf, failingRows, formatRows, waitForChecks, recordPlanCi, formatTokens, formatTrendSegment, formatLoopsSegment, readTrendInputs, trendReportOf, buildReport, readSkillsReport, report, SPEC_EXTENSION, boardId, notesFileName, branchName, shellQuote, ghMissingMessage, ghUnauthenticatedMessage, ghHostOf, ghPreflightItems, parseWorkingTree, parseWorktrees, worktreesHolding, readMergeRefusal, resolvePrProvider, requireGhProvider, createGhPullRequests, ghPullRequestsIn, REMOTE, BRANCH_PREFIX2 as BRANCH_PREFIX, parseBaseStanding, hasDiverged, parseClaimMessage, claimBranchIssue, makeOwnershipCommit, fetchClaimBranches, readClaimBranch, pushOwnershipCommit, CLAIMED_LABEL, IN_DEVELOPMENT_LABEL, readClaimState, idleText, ROADMAP_TITLE, ROADMAP_SETTING, parseRoadmapBody, createGhRoadmapSearch, severalRoadmapsMessage, createGhOpenPullRequests, closedIssuesIn, branchClaims, scanClaimBranches, readEpicBody, SPEC_BLOCKED_LABEL, BLOCKED_BY_FIELD, hasSpecBlockedLabel, readBlockedBy, EPIC_LABEL_PREFIX, epicSlugsOf, isNotPlanned, groupByEpicLabel, localDay, readEpics, BOARD_LISTING_LIMIT, createGhBoardListing, SPEC_READY_LABEL, findReadinessGaps, requireCompleteSpec, validate_default, RefsBlockError, fingerprintText, PREVIOUS_COPY_WARN_ABOVE, previousDir, countPreviousCopies, createGhSpecIssueReader, hasSpecLabel, readRoadmapSetting, ROADMAP_LABEL, BOARD_LABELS, LABEL_LIST_LIMIT, SPEC_TEMPLATE_PATH, TEMPLATES_DIRNAME, boardChanged, listBoardLabels, missingBoardLabels, setUpLabels, anythingAt, writeShippedFile, setUpBoard, resolveDefaultBoard, unlabelledRoadmapMessage, createGhRoadmapBody, tickSentence, appendLine, tickLine, editChecklist, noBoardListsLine, epicTickSentence, tickRoadmapAfterMerge, createGhIssueBoard, PROJECT_WRITE_PAUSE_MS, matchProjectFields, PROJECT_SCOPE_FIX, BOARD_SYNC_FIX, isMissingProjectScope, scopeWarning, rateLimitWarning, notFoundWarning, isWaiting, blockerArg, createNativeRelations, selectBoardRelations, resolveTracker, DEFAULT_ISSUE_SEAMS, lineRefusal, expectTwoArguments, readTextFlag, readNonBlankFlag, readRequiredFlag, readChoice, readChoiceFlag, issueProject, issueSubjectConfig, resolveIssueTracker, issueRef, issueName, urlLines, onTracker, NATIVE_MODE, NATIVE_RETRY_HINT, nativeEpicLeft, nativeAlreadyMessage, nativeParentLine, readBoardRepository, configuredMoveRelations, FRAGMENT_PLAN_ID_PATTERN, serializeFragment, allocateFragmentName, directoryPrefix, HORIZON_LABEL_PREFIX, readEpicProblems, epicProblemMessage, readEpicDependents, REASON_FLAG, normaliseReason, renderHorizonComment, renderParkedPullRequestComment, renderMoveComment, renderCloseComment, renderCancelComment, cancelMoveReason, renderDependentComment, renderUnblockNote, readReason, unaskedReasonMessage, blankReasonMessage, readRefsText, readCopyRefs, restampCopyRefs, RefVerifyError, readConfiguredRelations, requireNoLeak, blockedLineSentence, boardRepoLabel, 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, isFirstDispatch, STAGE_LABELS, unlabelReleased, createStartPreflightClaim, invalidateRows, checkDrift, driftLines, readDoctorEffortSync, renderDoctorEffortSync, effortSyncRefusal, SESSION_ID_FLAG2 as SESSION_ID_FLAG, readBoardBody, openBoards, boardOfEpic, createOwnerResolver, runIssueList, ISSUE_LIST_FLAGS, list_default, firstNowEpic, show_default, samePlace, resolvePlace, createGhProjectPort, resolveLoopSeams, sessionIdFlag, lineRefusal2 as lineRefusal1, refusal3 as refusal, NoSessionRefusal, isLive, planLabel, sessionLine, projectRoot2 as projectRoot, readRecords, pickSession, writeSession, readSessionChecklist, phaseNote, phasedCounts, checkboxAt, estimateEta, readSessionFinishes, formatDuration, etaLine, RELEASE_REMOTE, RELEASE_BASE_BRANCH, parseSemanticVersion, readManifestVersion, replaceManifestVersion, gitPathOf, receiptProblem, readReceiptVerdict, releaseCommitSubject, readSettle, buildSettle, waitingSettingsOf, readWaiting, forecastPhrase, DEFAULT_RELEASE_SEAMS, compareVersions, changelogVersions, readTags, status_default, epicEndLines, watchDryEpic, nextOwnerGate, resolveReleaseEnabled, refuseMisplacedRuntime, readBinPath, self_update_default, versionTag, INDENT4 as INDENT, cleanUpAfterMerge, reportFollowUps, RELEASE_BLOCK_OPEN, RELEASE_BLOCK_CLOSE, localPlanNotes, readGuard, guardLines, mergeGuardSettings, readMergeGuard, guardReaction, followsMerge, readSettleAfterMerge, DEFAULT_BASE_BRANCH, ghNextBoard, openNextSources, WORKTREES_SUBDIR, resolveWorktreePath, crossRepositoryRefusal, addResolveWorktree, removeResolveWorktree, pushResolved, worktreeAt, worktreeDirAt, WORKTREE_STATUS, countChanges, cleanupSteps, dryRunLines, runCleanupSteps, defaultCleanupSeams, readCleanup, cleanupCounts, createLinePrompter, openNextRelations, list_default2 as list_default1, readBranch2 as readBranch, readPlans, readOpenPull, readBranchPlan, readNextState, readHomeAfterLoop, commandWords, nextQuestion, NEXT_COMMAND_ACTIONS, LABELS_ONLY_ACTIONS, actionInvocation, runAction, HINT_FLAG_SPEC, actionOutput, endWithNextStep, endingWith, lazyPrompter2 as lazyPrompter, ready_default, DEFAULT_PLAN_FILE, resolvePlanPath, assessPlanRisk, renderRiskText, announceRiskTotal, readBlockedIssues, renderBlockedIssues, blockerToken, addBlockerToBody, resolveCreateSpec, TRIAGE_MODULE, namedSecrets, triageReport, eventsFilePath, unresolvedTracker, DISK_ROOTS_FILE_SYSTEM, gitToplevel, rootRefusal, rootCandidates, DEFAULT_CI_TIMEOUT_MIN, CI_POLL_INTERVAL_MS, DEFAULT_CI_ATTEMPTS, MECHANICAL_CONFLICT_SENTENCE, start, createPlanRefsVerifier, readDoctorRefs, renderDoctorRefs, createPlanDirNames, hasPlanFor, HORIZONS, horizonOf, readListedEpics, stageOf, rankOf, readIssueFacts, readRefreshBoard, refreshProjectItems, refreshIssueItems, refreshFailedWarning, unblock_default, unblockAfterMerge, labelsLineBlockersOf, LABELS_READS, createLabelsRelations, EPIC_TYPE_LABEL, plan };
34251
+ export { SERVE_CLI_VERSION, PROVENANCE_FIELD, SKILL_FILE2 as SKILL_FILE, provenanceBlock, serveVerdict, SKILL_USE_CLI_VERSION, sessionLogDir, MANIFEST_VERSION, FEATURE_TYPES, OUTPUT_CHANNELS, RUNNING_MANIFEST_SEAMS, validateManifest, loadModules, moduleSettings, pullBeforeRead, 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, parseReportArgs, formatCount, alignRows, verdictOf, failingRows, formatRows, waitForChecks, recordPlanCi, formatTokens, formatTrendSegment, formatLoopsSegment, readTrendInputs, trendReportOf, buildReport, readSkillsReport, report, SPEC_EXTENSION, boardId, notesFileName, branchName, shellQuote, ghMissingMessage, ghUnauthenticatedMessage, ghHostOf, ghPreflightItems, parseWorkingTree, parseTreePaths, untrackedLeftLine, parseWorktrees, worktreesHolding, readMergeRefusal, resolvePrProvider, requireGhProvider, createGhPullRequests, ghPullRequestsIn, REMOTE, BRANCH_PREFIX2 as BRANCH_PREFIX, parseBaseStanding, hasDiverged, parseClaimMessage, claimBranchIssue, makeOwnershipCommit, fetchClaimBranches, readClaimBranch, pushOwnershipCommit, CLAIMED_LABEL, IN_DEVELOPMENT_LABEL, readClaimState, idleText, ROADMAP_TITLE, ROADMAP_SETTING, parseRoadmapBody, createGhRoadmapSearch, severalRoadmapsMessage, createGhOpenPullRequests, closedIssuesIn, branchClaims, scanClaimBranches, readEpicBody, SPEC_BLOCKED_LABEL, BLOCKED_BY_FIELD, hasSpecBlockedLabel, readBlockedBy, EPIC_LABEL_PREFIX, epicSlugsOf, isNotPlanned, groupByEpicLabel, localDay, readEpics, BOARD_LISTING_LIMIT, createGhBoardListing, SPEC_READY_LABEL, findReadinessGaps, requireCompleteSpec, validate_default, RefsBlockError, fingerprintText, PREVIOUS_COPY_WARN_ABOVE, previousDir, countPreviousCopies, createGhSpecIssueReader, hasSpecLabel, readRoadmapSetting, ROADMAP_LABEL, BOARD_LABELS, LABEL_LIST_LIMIT, SPEC_TEMPLATE_PATH, TEMPLATES_DIRNAME, boardChanged, listBoardLabels, missingBoardLabels, setUpLabels, anythingAt, writeShippedFile, setUpBoard, resolveDefaultBoard, unlabelledRoadmapMessage, createGhRoadmapBody, tickSentence, appendLine, tickLine, editChecklist, noBoardListsLine, epicTickSentence, tickRoadmapAfterMerge, createGhIssueBoard, PROJECT_WRITE_PAUSE_MS, matchProjectFields, PROJECT_SCOPE_FIX, BOARD_SYNC_FIX, isMissingProjectScope, scopeWarning, rateLimitWarning, notFoundWarning, isWaiting, blockerArg, createNativeRelations, selectBoardRelations, resolveTracker, DEFAULT_ISSUE_SEAMS, lineRefusal, expectTwoArguments, readTextFlag, readNonBlankFlag, readRequiredFlag, readChoice, readChoiceFlag, issueProject, issueSubjectConfig, resolveIssueTracker, issueRef, issueName, urlLines, onTracker, NATIVE_MODE, NATIVE_RETRY_HINT, nativeEpicLeft, nativeAlreadyMessage, nativeParentLine, readBoardRepository, configuredMoveRelations, FRAGMENT_PLAN_ID_PATTERN, serializeFragment, allocateFragmentName, directoryPrefix, HORIZON_LABEL_PREFIX, readEpicProblems, epicProblemMessage, readEpicDependents, REASON_FLAG, normaliseReason, renderHorizonComment, renderParkedPullRequestComment, renderMoveComment, renderCloseComment, renderCancelComment, cancelMoveReason, renderDependentComment, renderUnblockNote, readReason, unaskedReasonMessage, blankReasonMessage, readRefsText, readCopyRefs, restampCopyRefs, RefVerifyError, readConfiguredRelations, requireNoLeak, blockedLineSentence, boardRepoLabel, 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, isFirstDispatch, STAGE_LABELS, unlabelReleased, createStartPreflightClaim, invalidateRows, checkDrift, driftLines, readDoctorEffortSync, renderDoctorEffortSync, effortSyncRefusal, SESSION_ID_FLAG2 as SESSION_ID_FLAG, readBoardBody, openBoards, boardOfEpic, createOwnerResolver, runIssueList, ISSUE_LIST_FLAGS, list_default, firstNowEpic, show_default, samePlace, resolvePlace, createGhProjectPort, resolveLoopSeams, sessionIdFlag, lineRefusal2 as lineRefusal1, refusal3 as refusal, NoSessionRefusal, isLive, planLabel, sessionLine, projectRoot2 as projectRoot, readRecords, pickSession, writeSession, readSessionChecklist, phaseNote, phasedCounts, checkboxAt, estimateEta, readSessionFinishes, formatDuration, etaLine, RELEASE_REMOTE, RELEASE_BASE_BRANCH, parseSemanticVersion, readManifestVersion, replaceManifestVersion, gitPathOf, receiptProblem, readReceiptVerdict, CHANGELOG_TITLE, releaseCommitSubject, readSettle, buildSettle, waitingSettingsOf, readWaiting, forecastPhrase, DEFAULT_RELEASE_SEAMS, compareVersions, changelogVersions, readTags, status_default, epicEndLines, watchDryEpic, nextOwnerGate, resolveReleaseEnabled, refuseMisplacedRuntime, readBinPath, self_update_default, versionTag, INDENT4 as INDENT, cleanUpAfterMerge, reportFollowUps, RELEASE_BLOCK_OPEN, RELEASE_BLOCK_CLOSE, localPlanNotes, readGuard, guardLines, mergeGuardSettings, readMergeGuard, guardReaction, followsMerge, readSettleAfterMerge, DEFAULT_BASE_BRANCH, ghNextBoard, openNextSources, readRemoteHead, eventsFilePath, WORKTREES_SUBDIR, resolveWorktreePath, crossRepositoryRefusal, addResolveWorktree, removeResolveWorktree, pushResolved, worktreeAt, worktreeDirAt, WORKTREE_STATUS, countChanges, cleanupSteps, dryRunLines, runCleanupSteps, defaultCleanupSeams, readCleanup, cleanupCounts, createLinePrompter, openNextRelations, list_default2 as list_default1, readBranch2 as readBranch, readPlans, readOpenPull, readBranchPlan, readNextState, readHomeAfterLoop, commandWords, nextQuestion, NEXT_COMMAND_ACTIONS, LABELS_ONLY_ACTIONS, actionInvocation, runAction, HINT_FLAG_SPEC, actionOutput, endWithNextStep, endingWith, lazyPrompter2 as lazyPrompter, ready_default, DEFAULT_PLAN_FILE, resolvePlanPath, assessPlanRisk, renderRiskText, announceRiskTotal, readBlockedIssues, renderBlockedIssues, blockerToken, addBlockerToBody, resolveCreateSpec, TRIAGE_MODULE, namedSecrets, triageReport, unresolvedTracker, DISK_ROOTS_FILE_SYSTEM, gitToplevel, rootRefusal, rootCandidates, DEFAULT_CI_TIMEOUT_MIN, CI_POLL_INTERVAL_MS, DEFAULT_CI_ATTEMPTS, retargetedLine, MECHANICAL_CONFLICT_SENTENCE, start, createPlanRefsVerifier, readDoctorRefs, renderDoctorRefs, createPlanDirNames, hasPlanFor, HORIZONS, horizonOf, readListedEpics, stageOf, rankOf, readIssueFacts, readRefreshBoard, refreshProjectItems, refreshIssueItems, refreshFailedWarning, unblock_default, unblockAfterMerge, labelsLineBlockersOf, LABELS_READS, createLabelsRelations, EPIC_TYPE_LABEL, plan };