@open-tomato/rafa 0.24.0 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1787,7 +1787,7 @@ function restoreRunningCommand(previous) {
1787
1787
  var running = null;
1788
1788
 
1789
1789
  // package.json
1790
- var name = "@open-tomato/rafa", version = "0.24.0";
1790
+ var name = "@open-tomato/rafa", version = "0.24.1";
1791
1791
  var init_package = () => {};
1792
1792
 
1793
1793
  // src/cli/version.ts
@@ -21780,7 +21780,7 @@ function readMergeRefusal(reading2) {
21780
21780
  return held.length === 0 ? null : worktreeRefusal(reading2.branch, held);
21781
21781
  }
21782
21782
  function cleanUpSteps(plan) {
21783
- const { base, branch, remote = DEFAULT_REMOTE2, remoteBranchPresent } = plan;
21783
+ const { base, branch, remote = DEFAULT_REMOTE2, localBranchPresent, remoteBranchPresent } = plan;
21784
21784
  const steps = [
21785
21785
  { id: "switch-base", label: `switch to ${base}`, argv: ["git", "switch", base] },
21786
21786
  {
@@ -21791,7 +21791,8 @@ function cleanUpSteps(plan) {
21791
21791
  {
21792
21792
  id: "delete-local",
21793
21793
  label: `delete the local branch ${branch}`,
21794
- argv: ["git", "branch", "-D", branch]
21794
+ argv: ["git", "branch", "-D", branch],
21795
+ ...localBranchPresent ? {} : { skip: `no local branch ${branch}; nothing to delete` }
21795
21796
  }
21796
21797
  ];
21797
21798
  if (remoteBranchPresent) {
@@ -21816,7 +21817,7 @@ function commandLine(step) {
21816
21817
  }
21817
21818
  function remainingFrom(steps, failed2) {
21818
21819
  const at = steps.findIndex((step) => step.id === failed2);
21819
- return at === -1 ? Object.freeze([]) : Object.freeze(steps.slice(at));
21820
+ return at === -1 ? Object.freeze([]) : Object.freeze(steps.slice(at).filter((step) => step.skip === undefined));
21820
21821
  }
21821
21822
  var REFUSAL_PREFIX = "rafa pr merge refuses", MAX_LISTED_CHANGES = 10, DEFAULT_REMOTE2 = "origin", BRANCH_REF_PREFIX = "refs/heads/", INDENT2 = " ";
21822
21823
  var init_merge2 = __esm(() => {
@@ -42118,6 +42119,16 @@ function remoteHoldsBranch(git, branch, warn3) {
42118
42119
  warn3(`${REMOTE3} could not be asked whether it still holds ${branch}, so it is left alone: ${gitSaid(probe2)}`);
42119
42120
  return false;
42120
42121
  }
42122
+ function localHoldsBranch(git, branch, warn3) {
42123
+ const probe2 = git(["show-ref", "--verify", "--quiet", `refs/heads/${branch}`]);
42124
+ if (probe2.ok)
42125
+ return true;
42126
+ const said = gitSaid(probe2);
42127
+ if (said === "")
42128
+ return false;
42129
+ warn3(`could not read whether this checkout holds a local branch ${branch}, so its delete runs: ${said}`);
42130
+ return true;
42131
+ }
42121
42132
  function textOf4(path7) {
42122
42133
  try {
42123
42134
  return readFileSync52(path7, "utf8");
@@ -42195,25 +42206,34 @@ async function reportTick(context, pr, seams, detail) {
42195
42206
  }
42196
42207
  return tick;
42197
42208
  }
42198
- function readyLine(base, branch, remoteBranchPresent) {
42199
- const remote = remoteBranchPresent ? `and ${branch} is gone locally and on ${REMOTE3}.` : `and ${branch} is gone locally; ${REMOTE3} had already deleted it.`;
42200
- return `${base} is checked out and pulled, ${remote}`;
42209
+ function readyLine(base, branch, present) {
42210
+ const pulled = `${base} is checked out and pulled`;
42211
+ if (present.local) {
42212
+ return present.remote ? `${pulled}, and ${branch} is gone locally and on ${REMOTE3}.` : `${pulled}, and ${branch} is gone locally; ${REMOTE3} had already deleted it.`;
42213
+ }
42214
+ return present.remote ? `${pulled}, and ${branch} is gone on ${REMOTE3}; there was no local branch to delete.` : `${pulled}; ${branch} had no local branch, and ${REMOTE3} had already deleted it.`;
42201
42215
  }
42202
42216
  function quotedLines3(said) {
42203
42217
  return said === "" ? [] : said.split(`
42204
42218
  `).map((line) => `${INDENT10}${line}`);
42205
42219
  }
42206
- function runCleanUp(context, git, detail, remoteBranchPresent) {
42220
+ function runCleanUp(context, git, detail, present) {
42207
42221
  const steps = cleanUpSteps({
42208
42222
  branch: detail.headRefName,
42209
42223
  base: detail.baseRefName,
42210
42224
  remote: REMOTE3,
42211
- remoteBranchPresent
42225
+ localBranchPresent: present.local,
42226
+ remoteBranchPresent: present.remote
42212
42227
  });
42213
42228
  const reports = [];
42214
42229
  for (const step2 of steps) {
42230
+ if (step2.skip !== undefined) {
42231
+ reports.push({ id: step2.id, label: step2.label, command: commandLine(step2), ok: true, skipped: true });
42232
+ context.output.info(`${step2.label}: skipped \u2014 ${step2.skip}`);
42233
+ continue;
42234
+ }
42215
42235
  const result = git(step2.argv.slice(1));
42216
- reports.push({ id: step2.id, label: step2.label, command: commandLine(step2), ok: result.ok });
42236
+ reports.push({ id: step2.id, label: step2.label, command: commandLine(step2), ok: result.ok, skipped: false });
42217
42237
  if (result.ok) {
42218
42238
  context.output.info(`${step2.label}: done`);
42219
42239
  continue;
@@ -42309,11 +42329,15 @@ async function runMerge(context, seams) {
42309
42329
  context.output.info(`Merged #${detail.number} into ${detail.baseRefName} (${method}).`);
42310
42330
  const commentUrl = await commentIfUnchecked(context, pr, detail.number, answer4.unchecked);
42311
42331
  const roadmapTicks = await reportTick(context, pr, seams, detail);
42312
- const remoteBranchPresent = remoteHoldsBranch(git, detail.headRefName, (message) => {
42332
+ const warn3 = (message) => {
42313
42333
  context.output.warn(message);
42314
- });
42315
- const steps = runCleanUp(context, git, detail, remoteBranchPresent);
42316
- context.output.info(readyLine(detail.baseRefName, detail.headRefName, remoteBranchPresent));
42334
+ };
42335
+ const present = {
42336
+ remote: remoteHoldsBranch(git, detail.headRefName, warn3),
42337
+ local: localHoldsBranch(git, detail.headRefName, warn3)
42338
+ };
42339
+ const steps = runCleanUp(context, git, detail, present);
42340
+ context.output.info(readyLine(detail.baseRefName, detail.headRefName, present));
42317
42341
  const followUps = followUpsFor(pr, git);
42318
42342
  if (followUps.length > 0) {
42319
42343
  context.output.info("Follow-ups:");
@@ -42340,7 +42364,7 @@ function createPrMergeCommand(seams = DEFAULT_MERGE_SEAMS) {
42340
42364
  subject: "pr",
42341
42365
  action: "merge",
42342
42366
  summary: "merge a pull request and clean up both branches",
42343
- description: "Merges one pull request through the GitHub CLI and then, in code, switches to the base branch," + " pulls it fast-forward only, deletes the head branch locally and on the remote where it is still there," + " and prunes, reporting each step. Refuses before it asks anything on a dirty working tree, on a pull" + " request that is not green or does not merge, and on a head branch checked out in another worktree. It" + " shows the pull request, its branches and the method and asks `Merge? [y/N]`; `--yes` skips the question," + " and without a terminal and without `--yes` it refuses. A pull request that reports no checks at all is" + " refused unless `--skip-checks` is given, which is refused on any pull request that does report checks;" + " with it the command reads how many workflows the repository defines, prints a warning for that case, asks" + " `Merge #<n> with no checks? [y/N]` (which `--yes` answers only where the repository defines no workflow)," + " and after the merge posts one comment on the pull request saying so. A step that fails never undoes the merge: it" + " prints what is left as commands to paste and exits 1. After the merge it ticks the `Closes #<n>` line of" + " every issue the pull request closes on every open board whose checklist lists it, or on the roadmap issue while no issue carries `type:roadmap`, warning rather than failing when that write" + ' does not land. It ends by reading every open issue whose "Blocked by:" line names an issue this pull' + " request closes, asking whether to remove `spec:blocked` from each one whose blockers have all closed;" + " `--yes` does not answer that question, and every failure of that reading is a warning. With" + " `--output=json` the pull request, the method, the steps that ran, the follow-ups, the roadmap tick and" + " the unblock reading are the data of the terminal result event. Refuses with exit code 2 where" + " `pr.provider` is not `gh`.",
42367
+ description: "Merges one pull request through the GitHub CLI and then, in code, switches to the base branch," + " pulls it fast-forward only, deletes the head branch locally and on the remote where it is still there," + " and prunes, reporting each step; a head branch this checkout has no local branch for is reported as a" + " skipped local delete, and the rest still run. Refuses before it asks anything on a dirty working tree, on a pull" + " request that is not green or does not merge, and on a head branch checked out in another worktree. It" + " shows the pull request, its branches and the method and asks `Merge? [y/N]`; `--yes` skips the question," + " and without a terminal and without `--yes` it refuses. A pull request that reports no checks at all is" + " refused unless `--skip-checks` is given, which is refused on any pull request that does report checks;" + " with it the command reads how many workflows the repository defines, prints a warning for that case, asks" + " `Merge #<n> with no checks? [y/N]` (which `--yes` answers only where the repository defines no workflow)," + " and after the merge posts one comment on the pull request saying so. A step that fails never undoes the merge: it" + " prints what is left as commands to paste and exits 1. After the merge it ticks the `Closes #<n>` line of" + " every issue the pull request closes on every open board whose checklist lists it, or on the roadmap issue while no issue carries `type:roadmap`, warning rather than failing when that write" + ' does not land. It ends by reading every open issue whose "Blocked by:" line names an issue this pull' + " request closes, asking whether to remove `spec:blocked` from each one whose blockers have all closed;" + " `--yes` does not answer that question, and every failure of that reading is a warning. With" + " `--output=json` the pull request, the method, the steps that ran, the follow-ups, the roadmap tick and" + " the unblock reading are the data of the terminal result event. Refuses with exit code 2 where" + " `pr.provider` is not `gh`.",
42344
42368
  args: [
42345
42369
  {
42346
42370
  name: "n",
@@ -44923,6 +44947,46 @@ var init_wait = __esm(() => {
44923
44947
  wait_default = createPrWaitCommand();
44924
44948
  });
44925
44949
 
44950
+ // src/commands/release/release-commit.ts
44951
+ function unread2(reason, message) {
44952
+ return { commit: null, ahead: 0, problem: { reason, message } };
44953
+ }
44954
+ function versionAt(git, commit, path7) {
44955
+ const shown2 = git(["show", `${commit}:./${path7}`]);
44956
+ return shown2.ok ? readManifestVersion(shown2.stdout) : null;
44957
+ }
44958
+ function readReleaseCommit(git, path7, version2) {
44959
+ const log = git(["log", "--first-parent", "--format=%H", "HEAD", "--", `./${path7}`]);
44960
+ if (!log.ok)
44961
+ return unread2("git", `the history of ${path7} could not be read: ${gitSaid(log)}`);
44962
+ const changed = log.stdout.split(`
44963
+ `).map((line) => line.trim()).filter((line) => line !== "");
44964
+ if (changed.length === 0) {
44965
+ return unread2("version", `no commit on this branch holds ${path7}; commit it before tagging`);
44966
+ }
44967
+ let release = null;
44968
+ let newest;
44969
+ for (const commit of changed) {
44970
+ const declared = versionAt(git, commit, path7);
44971
+ if (newest === undefined)
44972
+ newest = declared;
44973
+ if (declared !== version2)
44974
+ break;
44975
+ release = commit;
44976
+ }
44977
+ if (release === null) {
44978
+ return unread2("version", `${path7} says ${version2}, and the last commit on this branch that changed it` + ` says ${newest ?? "no version"}; commit the version before tagging it`);
44979
+ }
44980
+ const count2 = git(["rev-list", "--count", "--first-parent", `${release}..HEAD`]);
44981
+ if (!count2.ok)
44982
+ return unread2("git", `the commits past ${release} could not be counted: ${gitSaid(count2)}`);
44983
+ return { commit: release, ahead: Number.parseInt(count2.stdout.trim(), 10), problem: null };
44984
+ }
44985
+ var init_release_commit = __esm(() => {
44986
+ init_pr();
44987
+ init_version2();
44988
+ });
44989
+
44926
44990
  // src/commands/release/tag.ts
44927
44991
  import { readFileSync as readFileSync54 } from "fs";
44928
44992
  import { join as join63 } from "path";
@@ -44996,18 +45060,21 @@ function readPublishTarget(text2) {
44996
45060
  isPrivate: fields["private"] === true
44997
45061
  };
44998
45062
  }
44999
- function followUpsFor2(tag, version2, target) {
45063
+ function followUpsFor2(tag, version2, target, place2 = ON_HEAD) {
45000
45064
  const push = {
45001
45065
  command: `git push ${RELEASE_REMOTE2} ${tag}`,
45002
45066
  why: `the tag is local until ${RELEASE_REMOTE2} has it`
45003
45067
  };
45004
45068
  if (target.name === null || target.isPrivate)
45005
45069
  return [push];
45070
+ const publishes = `publishes ${target.name}@${version2} to ${target.registry}`;
45071
+ if (place2.ahead === 0)
45072
+ return [push, { command: `${target.manager} publish`, why: publishes }];
45006
45073
  return [
45007
45074
  push,
45008
45075
  {
45009
- command: `${target.manager} publish`,
45010
- why: `publishes ${target.name}@${version2} to ${target.registry}`
45076
+ command: `git switch --detach ${tag} && ${target.manager} publish && git switch ${place2.branch}`,
45077
+ why: `${publishes} from the tagged commit, not HEAD; skip it if ${target.registry} has that version already`
45011
45078
  }
45012
45079
  ];
45013
45080
  }
@@ -45047,7 +45114,14 @@ function decideTag(inputs) {
45047
45114
  const disagrees = changelogRefusal(inputs, version2);
45048
45115
  if (disagrees !== null)
45049
45116
  return disagrees;
45050
- return { kind: "ready", version: version2, tag: versionTag(version2) };
45117
+ const release = inputs.release;
45118
+ if (release === null)
45119
+ return refuse7("version", `${inputs.version.path} was not looked up in the history`);
45120
+ if (release.problem !== null)
45121
+ return refuse7(release.problem.reason, release.problem.message);
45122
+ if (release.commit === null)
45123
+ return refuse7("git", `no commit could be named for ${version2}`);
45124
+ return { kind: "ready", version: version2, tag: versionTag(version2), commit: release.commit, ahead: release.ahead };
45051
45125
  }
45052
45126
  function projectOf10(context) {
45053
45127
  if (context.project === null)
@@ -45070,23 +45144,34 @@ function tagConfig(project, warn3) {
45070
45144
  }
45071
45145
  }
45072
45146
  function readTagInputs(root, git, config) {
45147
+ const branch = readBranch3(git);
45148
+ const version2 = readVersion(root, config.versionFile);
45149
+ const walkable = version2.version !== null && branch.branch === config.releaseBranch;
45073
45150
  return {
45074
45151
  releaseBranch: config.releaseBranch,
45075
- branch: readBranch3(git),
45076
- version: readVersion(root, config.versionFile),
45152
+ branch,
45153
+ version: version2,
45077
45154
  changelog: readNewestRelease(root, config.changelog),
45078
- tags: readTags(git)
45155
+ tags: readTags(git),
45156
+ release: walkable && version2.version !== null ? readReleaseCommit(git, config.versionFile, version2.version) : null
45079
45157
  };
45080
45158
  }
45081
- function writeTag(git, tag) {
45082
- const result = git(["tag", tag, "HEAD"]);
45159
+ function writeTag(git, tag, commit) {
45160
+ const result = git(["tag", tag, commit]);
45083
45161
  if (result.ok)
45084
45162
  return;
45085
45163
  throw new CommandExit(1, `\u274C ${tag} could not be written: ${gitSaid(result)}`);
45086
45164
  }
45165
+ function pastReleaseWarning(written, branch) {
45166
+ if (written.ahead === 0)
45167
+ return null;
45168
+ const commits = written.ahead === 1 ? "1 commit" : `${String(written.ahead)} commits`;
45169
+ return `HEAD of ${branch} is ${commits} past ${written.commit.slice(0, SHORT_COMMIT)}, where ${written.version}` + ` was set; they are not in ${written.tag}.`;
45170
+ }
45087
45171
  function renderTagged(written, branch, followUps) {
45172
+ const where = written.ahead === 0 ? `the HEAD of ${branch}` : `${written.commit.slice(0, SHORT_COMMIT)}, the commit of ${branch} that set ${written.version}`;
45088
45173
  return [
45089
- `\u2705 Tagged ${written.tag} at the HEAD of ${branch}.`,
45174
+ `\u2705 Tagged ${written.tag} at ${where}.`,
45090
45175
  "Next:",
45091
45176
  ...followUps.map((followUp2) => `${INDENT14}${followUp2.command} \u2014 ${followUp2.why}`)
45092
45177
  ];
@@ -45104,8 +45189,9 @@ function runTag(context, seams = DEFAULT_RELEASE_SEAMS) {
45104
45189
  throw new CommandExit(1, `\u274C ${decision.message}
45105
45190
  Usage: ${RELEASE_TAG_USAGE}`);
45106
45191
  }
45107
- writeTag(git, decision.tag);
45108
- const followUps = followUpsFor2(decision.tag, decision.version, readPublishTarget(inputs.version.text));
45192
+ writeTag(git, decision.tag, decision.commit);
45193
+ const place2 = { ahead: decision.ahead, branch: config.releaseBranch };
45194
+ const followUps = followUpsFor2(decision.tag, decision.version, readPublishTarget(inputs.version.text), place2);
45109
45195
  return { inputs, written: decision, followUps };
45110
45196
  }
45111
45197
  function createReleaseTagCommand(seams = DEFAULT_RELEASE_SEAMS) {
@@ -45113,8 +45199,8 @@ function createReleaseTagCommand(seams = DEFAULT_RELEASE_SEAMS) {
45113
45199
  name: "release tag",
45114
45200
  subject: "release",
45115
45201
  action: "tag",
45116
- summary: "tag the release branch's HEAD with the version both release files agree on",
45117
- description: "Writes `v<version>` on the HEAD of the release branch \u2014 `pr.base`, or `main` \u2014 when the" + " version `release.versionFile` declares is the one the newest section of `release.changelog` names." + " It refuses, and writes nothing, when another branch is checked out, when a tag already names that" + " version, when the two files disagree, and when either the version file or the tag list could not" + " be read. After the tag it prints what to run next: the push that puts the tag on the remote, and" + " the publish line for the registry the version file configures, which it does not run. With" + " `--output=json` the readings, the decision and the follow-ups are the data of the terminal result" + " event.",
45202
+ summary: "tag the commit that set the version both release files agree on",
45203
+ description: "Writes `v<version>` on the release branch \u2014 `pr.base`, or `main` \u2014 when the" + " version `release.versionFile` declares is the one the newest section of `release.changelog` names." + " The tag goes on the commit that set that version: HEAD, or, where later commits landed on the branch," + " the earlier commit, with a warning naming how many commits HEAD is past it." + " It refuses, and writes nothing, when another branch is checked out, when a tag already names that" + " version, when the two files disagree, when no commit holds the version yet, and when the version" + " file, the tag list or the history could not be read. After the tag it prints what to run next: the" + " push that puts the tag on the remote, and the publish line for the registry the version file" + " configures, spelled to publish from the tag when HEAD is past it, which it does not run. With" + " `--output=json` the readings, the decision and the follow-ups are the data of the terminal result" + " event.",
45118
45204
  args: [],
45119
45205
  flags: [],
45120
45206
  examples: [
@@ -45130,6 +45216,9 @@ function createReleaseTagCommand(seams = DEFAULT_RELEASE_SEAMS) {
45130
45216
  outputs: ["text", "json"],
45131
45217
  run: async (context) => {
45132
45218
  const result = runTag(context, seams);
45219
+ const past = pastReleaseWarning(result.written, result.inputs.releaseBranch);
45220
+ if (past !== null)
45221
+ context.output.warn(past);
45133
45222
  if (context.outputMode === "json") {
45134
45223
  context.output.result(result);
45135
45224
  return;
@@ -45141,7 +45230,7 @@ function createReleaseTagCommand(seams = DEFAULT_RELEASE_SEAMS) {
45141
45230
  };
45142
45231
  return Object.freeze(command);
45143
45232
  }
45144
- var RELEASE_TAG_USAGE = "rafa release tag", DEFAULT_RELEASE_BRANCH = "main", DEFAULT_REGISTRY = "https://registry.npmjs.org", RELEASE_REMOTE2 = "origin", INDENT14 = " ", NO_PACKAGE2, tag_default;
45233
+ var RELEASE_TAG_USAGE = "rafa release tag", DEFAULT_RELEASE_BRANCH = "main", DEFAULT_REGISTRY = "https://registry.npmjs.org", RELEASE_REMOTE2 = "origin", INDENT14 = " ", SHORT_COMMIT = 7, NO_PACKAGE2, ON_HEAD, tag_default;
45145
45234
  var init_tag = __esm(() => {
45146
45235
  init_command();
45147
45236
  init_config_load();
@@ -45150,6 +45239,7 @@ var init_tag = __esm(() => {
45150
45239
  init_version2();
45151
45240
  init_plan_files();
45152
45241
  init_merge_followups();
45242
+ init_release_commit();
45153
45243
  init_status();
45154
45244
  NO_PACKAGE2 = Object.freeze({
45155
45245
  manager: "npm",
@@ -45157,6 +45247,7 @@ var init_tag = __esm(() => {
45157
45247
  name: null,
45158
45248
  isPrivate: false
45159
45249
  });
45250
+ ON_HEAD = Object.freeze({ ahead: 0, branch: DEFAULT_RELEASE_BRANCH });
45160
45251
  tag_default = createReleaseTagCommand();
45161
45252
  });
45162
45253
 
@@ -47477,7 +47568,7 @@ import { existsSync as existsSync47 } from "fs";
47477
47568
  function isIdleWorktree(row3) {
47478
47569
  return !row3.blockers.some((blocker) => blocker.kind === "recent");
47479
47570
  }
47480
- function unread2(problem2) {
47571
+ function unread3(problem2) {
47481
47572
  return { read: false, problem: problem2 };
47482
47573
  }
47483
47574
  function read2(reading2) {
@@ -47488,7 +47579,7 @@ function localSection(work) {
47488
47579
  const reading2 = work();
47489
47580
  return isUnread(reading2) ? reading2 : read2(reading2);
47490
47581
  } catch (error) {
47491
- return unread2(messageOf(error));
47582
+ return unread3(messageOf(error));
47492
47583
  }
47493
47584
  }
47494
47585
  function isUnread(value) {
@@ -47521,13 +47612,13 @@ async function networkSection(what, deadline, work) {
47521
47612
  let timer;
47522
47613
  const expired = new Promise((resolve30) => {
47523
47614
  timer = setTimeout(() => {
47524
- resolve30(unread2(`the ${what} was not read within the ${String(deadline.ms)}ms network deadline`));
47615
+ resolve30(unread3(`the ${what} was not read within the ${String(deadline.ms)}ms network deadline`));
47525
47616
  }, Math.max(0, deadline.left()));
47526
47617
  });
47527
47618
  try {
47528
47619
  return await Promise.race([work().then((reading2) => read2(reading2)), expired]);
47529
47620
  } catch (error) {
47530
- return unread2(messageOf(error));
47621
+ return unread3(messageOf(error));
47531
47622
  } finally {
47532
47623
  clearTimeout(timer);
47533
47624
  }
@@ -47543,7 +47634,7 @@ function readBranchSection(sources) {
47543
47634
  };
47544
47635
  const branch = readBranch2(sources, note);
47545
47636
  if (branch === "") {
47546
- return unread2(notes.join("; ") || "the branch at the project root could not be read");
47637
+ return unread3(notes.join("; ") || "the branch at the project root could not be read");
47547
47638
  }
47548
47639
  const plans = readPlans(sources, note);
47549
47640
  return { branch, plan: readBranchPlan(branch, plans), notes };
@@ -47570,9 +47661,9 @@ function readLoopsSection(root, sources) {
47570
47661
  }
47571
47662
  function pullSection(sources, branch, provider, deadline) {
47572
47663
  if (provider.provider !== "gh")
47573
- return Promise.resolve(unread2(notGhProblem("pull request", provider)));
47664
+ return Promise.resolve(unread3(notGhProblem("pull request", provider)));
47574
47665
  if (!branch.read) {
47575
- return Promise.resolve(unread2(`no pull request was looked for, as the branch was not read: ${branch.problem}`));
47666
+ return Promise.resolve(unread3(`no pull request was looked for, as the branch was not read: ${branch.problem}`));
47576
47667
  }
47577
47668
  return networkSection("pull request", deadline, () => readPull(sources, branch.branch));
47578
47669
  }
@@ -47656,11 +47747,11 @@ async function readHousekeeping(input, cleanup, now) {
47656
47747
  const settings = doctorCleanupSettings({ root: input.root, home: input.home, config: input.config, gh: null }, now);
47657
47748
  const reading2 = await readCleanup(cleanup, settings);
47658
47749
  if (!reading2.ok)
47659
- return unread2(reading2.detail);
47750
+ return unread3(reading2.detail);
47660
47751
  const idle = reading2.worktrees.filter((row3) => isIdleWorktree(row3));
47661
47752
  return read2({ counts: cleanupCounts(reading2), idleWorktrees: idle.length, notes: reading2.notes });
47662
47753
  } catch (error) {
47663
- return unread2(messageOf(error));
47754
+ return unread3(messageOf(error));
47664
47755
  }
47665
47756
  }
47666
47757
  async function readStatusSections(input, seams = {}) {
@@ -47689,7 +47780,7 @@ async function readStatusSections(input, seams = {}) {
47689
47780
  const blockedIssues = seams.blockedIssues ?? ((runner) => readBlockedIssues({ gh: runner }));
47690
47781
  deadline.start();
47691
47782
  const pull = pullSection(sources, branch, provider, deadline);
47692
- const boardSection = isGh ? networkSection("board", deadline, () => readBoard(root, board, listing3, blockedIssues(gh))) : Promise.resolve(unread2(notGhProblem("board", provider)));
47783
+ const boardSection = isGh ? networkSection("board", deadline, () => readBoard(root, board, listing3, blockedIssues(gh))) : Promise.resolve(unread3(notGhProblem("board", provider)));
47693
47784
  const housekeeping = readHousekeeping(input, cleanup, now);
47694
47785
  return {
47695
47786
  branch,
@@ -47917,7 +48008,7 @@ var init_commands2 = __esm(() => {
47917
48008
  { name: "agent", summary: "copy an agent definition into the project; list what a session sees" },
47918
48009
  { name: "skill", summary: "check a skills directory; list each tier; demote and backfill it" },
47919
48010
  { name: "instinct", summary: "check an instincts directory; list, show, flag and promote its records" },
47920
- { name: "release", summary: "read the release state of the project; tag the release branch's HEAD" },
48011
+ { name: "release", summary: "read the release state of the project; tag the commit that set the version" },
47921
48012
  { name: "board", summary: "list the boards with their owner, epic count, and which is current and home" },
47922
48013
  { name: "epic", summary: "show one epic's issues as the Roadmap table; create an epic; defer or promote it; move an issue to it; close it through the gate or cancel it" }
47923
48014
  ]);
@@ -283,7 +283,7 @@ import {
283
283
  writePositionFile,
284
284
  writeSession,
285
285
  writeShippedFile
286
- } from "./index-gt9acb1y.js";
286
+ } from "./index-h369efkh.js";
287
287
  import {
288
288
  PROBE_TIMEOUT_MS,
289
289
  findNextTask,
@@ -15149,6 +15149,16 @@ function remoteHoldsBranch(git, branch, warn3) {
15149
15149
  warn3(`${REMOTE3} could not be asked whether it still holds ${branch}, so it is left alone: ${gitSaid(probe2)}`);
15150
15150
  return false;
15151
15151
  }
15152
+ function localHoldsBranch(git, branch, warn3) {
15153
+ const probe2 = git(["show-ref", "--verify", "--quiet", `refs/heads/${branch}`]);
15154
+ if (probe2.ok)
15155
+ return true;
15156
+ const said = gitSaid(probe2);
15157
+ if (said === "")
15158
+ return false;
15159
+ warn3(`could not read whether this checkout holds a local branch ${branch}, so its delete runs: ${said}`);
15160
+ return true;
15161
+ }
15152
15162
  function textOf(path) {
15153
15163
  try {
15154
15164
  return readFileSync19(path, "utf8");
@@ -15226,25 +15236,34 @@ async function reportTick(context, pr, seams, detail) {
15226
15236
  }
15227
15237
  return tick;
15228
15238
  }
15229
- function readyLine(base, branch, remoteBranchPresent) {
15230
- const remote = remoteBranchPresent ? `and ${branch} is gone locally and on ${REMOTE3}.` : `and ${branch} is gone locally; ${REMOTE3} had already deleted it.`;
15231
- return `${base} is checked out and pulled, ${remote}`;
15239
+ function readyLine(base, branch, present) {
15240
+ const pulled = `${base} is checked out and pulled`;
15241
+ if (present.local) {
15242
+ return present.remote ? `${pulled}, and ${branch} is gone locally and on ${REMOTE3}.` : `${pulled}, and ${branch} is gone locally; ${REMOTE3} had already deleted it.`;
15243
+ }
15244
+ return present.remote ? `${pulled}, and ${branch} is gone on ${REMOTE3}; there was no local branch to delete.` : `${pulled}; ${branch} had no local branch, and ${REMOTE3} had already deleted it.`;
15232
15245
  }
15233
15246
  function quotedLines2(said) {
15234
15247
  return said === "" ? [] : said.split(`
15235
15248
  `).map((line) => `${INDENT4}${line}`);
15236
15249
  }
15237
- function runCleanUp(context, git, detail, remoteBranchPresent) {
15250
+ function runCleanUp(context, git, detail, present) {
15238
15251
  const steps = cleanUpSteps({
15239
15252
  branch: detail.headRefName,
15240
15253
  base: detail.baseRefName,
15241
15254
  remote: REMOTE3,
15242
- remoteBranchPresent
15255
+ localBranchPresent: present.local,
15256
+ remoteBranchPresent: present.remote
15243
15257
  });
15244
15258
  const reports = [];
15245
15259
  for (const step of steps) {
15260
+ if (step.skip !== undefined) {
15261
+ reports.push({ id: step.id, label: step.label, command: commandLine(step), ok: true, skipped: true });
15262
+ context.output.info(`${step.label}: skipped \u2014 ${step.skip}`);
15263
+ continue;
15264
+ }
15246
15265
  const result = git(step.argv.slice(1));
15247
- reports.push({ id: step.id, label: step.label, command: commandLine(step), ok: result.ok });
15266
+ reports.push({ id: step.id, label: step.label, command: commandLine(step), ok: result.ok, skipped: false });
15248
15267
  if (result.ok) {
15249
15268
  context.output.info(`${step.label}: done`);
15250
15269
  continue;
@@ -15340,11 +15359,15 @@ async function runMerge(context, seams) {
15340
15359
  context.output.info(`Merged #${detail.number} into ${detail.baseRefName} (${method}).`);
15341
15360
  const commentUrl = await commentIfUnchecked(context, pr, detail.number, answer2.unchecked);
15342
15361
  const roadmapTicks = await reportTick(context, pr, seams, detail);
15343
- const remoteBranchPresent = remoteHoldsBranch(git, detail.headRefName, (message) => {
15362
+ const warn3 = (message) => {
15344
15363
  context.output.warn(message);
15345
- });
15346
- const steps = runCleanUp(context, git, detail, remoteBranchPresent);
15347
- context.output.info(readyLine(detail.baseRefName, detail.headRefName, remoteBranchPresent));
15364
+ };
15365
+ const present = {
15366
+ remote: remoteHoldsBranch(git, detail.headRefName, warn3),
15367
+ local: localHoldsBranch(git, detail.headRefName, warn3)
15368
+ };
15369
+ const steps = runCleanUp(context, git, detail, present);
15370
+ context.output.info(readyLine(detail.baseRefName, detail.headRefName, present));
15348
15371
  const followUps = followUpsFor(pr, git);
15349
15372
  if (followUps.length > 0) {
15350
15373
  context.output.info("Follow-ups:");
@@ -15371,7 +15394,7 @@ function createPrMergeCommand(seams = DEFAULT_MERGE_SEAMS) {
15371
15394
  subject: "pr",
15372
15395
  action: "merge",
15373
15396
  summary: "merge a pull request and clean up both branches",
15374
- description: "Merges one pull request through the GitHub CLI and then, in code, switches to the base branch," + " pulls it fast-forward only, deletes the head branch locally and on the remote where it is still there," + " and prunes, reporting each step. Refuses before it asks anything on a dirty working tree, on a pull" + " request that is not green or does not merge, and on a head branch checked out in another worktree. It" + " shows the pull request, its branches and the method and asks `Merge? [y/N]`; `--yes` skips the question," + " and without a terminal and without `--yes` it refuses. A pull request that reports no checks at all is" + " refused unless `--skip-checks` is given, which is refused on any pull request that does report checks;" + " with it the command reads how many workflows the repository defines, prints a warning for that case, asks" + " `Merge #<n> with no checks? [y/N]` (which `--yes` answers only where the repository defines no workflow)," + " and after the merge posts one comment on the pull request saying so. A step that fails never undoes the merge: it" + " prints what is left as commands to paste and exits 1. After the merge it ticks the `Closes #<n>` line of" + " every issue the pull request closes on every open board whose checklist lists it, or on the roadmap issue while no issue carries `type:roadmap`, warning rather than failing when that write" + ' does not land. It ends by reading every open issue whose "Blocked by:" line names an issue this pull' + " request closes, asking whether to remove `spec:blocked` from each one whose blockers have all closed;" + " `--yes` does not answer that question, and every failure of that reading is a warning. With" + " `--output=json` the pull request, the method, the steps that ran, the follow-ups, the roadmap tick and" + " the unblock reading are the data of the terminal result event. Refuses with exit code 2 where" + " `pr.provider` is not `gh`.",
15397
+ description: "Merges one pull request through the GitHub CLI and then, in code, switches to the base branch," + " pulls it fast-forward only, deletes the head branch locally and on the remote where it is still there," + " and prunes, reporting each step; a head branch this checkout has no local branch for is reported as a" + " skipped local delete, and the rest still run. Refuses before it asks anything on a dirty working tree, on a pull" + " request that is not green or does not merge, and on a head branch checked out in another worktree. It" + " shows the pull request, its branches and the method and asks `Merge? [y/N]`; `--yes` skips the question," + " and without a terminal and without `--yes` it refuses. A pull request that reports no checks at all is" + " refused unless `--skip-checks` is given, which is refused on any pull request that does report checks;" + " with it the command reads how many workflows the repository defines, prints a warning for that case, asks" + " `Merge #<n> with no checks? [y/N]` (which `--yes` answers only where the repository defines no workflow)," + " and after the merge posts one comment on the pull request saying so. A step that fails never undoes the merge: it" + " prints what is left as commands to paste and exits 1. After the merge it ticks the `Closes #<n>` line of" + " every issue the pull request closes on every open board whose checklist lists it, or on the roadmap issue while no issue carries `type:roadmap`, warning rather than failing when that write" + ' does not land. It ends by reading every open issue whose "Blocked by:" line names an issue this pull' + " request closes, asking whether to remove `spec:blocked` from each one whose blockers have all closed;" + " `--yes` does not answer that question, and every failure of that reading is a warning. With" + " `--output=json` the pull request, the method, the steps that ran, the follow-ups, the roadmap tick and" + " the unblock reading are the data of the terminal result event. Refuses with exit code 2 where" + " `pr.provider` is not `gh`.",
15375
15398
  args: [
15376
15399
  {
15377
15400
  name: "n",
@@ -17858,11 +17881,50 @@ var wait_default = createPrWaitCommand();
17858
17881
  // src/commands/release/tag.ts
17859
17882
  import { readFileSync as readFileSync21 } from "fs";
17860
17883
  import { join as join32 } from "path";
17884
+
17885
+ // src/commands/release/release-commit.ts
17886
+ function unread2(reason, message) {
17887
+ return { commit: null, ahead: 0, problem: { reason, message } };
17888
+ }
17889
+ function versionAt(git, commit, path) {
17890
+ const shown = git(["show", `${commit}:./${path}`]);
17891
+ return shown.ok ? readManifestVersion(shown.stdout) : null;
17892
+ }
17893
+ function readReleaseCommit(git, path, version2) {
17894
+ const log = git(["log", "--first-parent", "--format=%H", "HEAD", "--", `./${path}`]);
17895
+ if (!log.ok)
17896
+ return unread2("git", `the history of ${path} could not be read: ${gitSaid(log)}`);
17897
+ const changed = log.stdout.split(`
17898
+ `).map((line) => line.trim()).filter((line) => line !== "");
17899
+ if (changed.length === 0) {
17900
+ return unread2("version", `no commit on this branch holds ${path}; commit it before tagging`);
17901
+ }
17902
+ let release = null;
17903
+ let newest;
17904
+ for (const commit of changed) {
17905
+ const declared = versionAt(git, commit, path);
17906
+ if (newest === undefined)
17907
+ newest = declared;
17908
+ if (declared !== version2)
17909
+ break;
17910
+ release = commit;
17911
+ }
17912
+ if (release === null) {
17913
+ return unread2("version", `${path} says ${version2}, and the last commit on this branch that changed it` + ` says ${newest ?? "no version"}; commit the version before tagging it`);
17914
+ }
17915
+ const count2 = git(["rev-list", "--count", "--first-parent", `${release}..HEAD`]);
17916
+ if (!count2.ok)
17917
+ return unread2("git", `the commits past ${release} could not be counted: ${gitSaid(count2)}`);
17918
+ return { commit: release, ahead: Number.parseInt(count2.stdout.trim(), 10), problem: null };
17919
+ }
17920
+
17921
+ // src/commands/release/tag.ts
17861
17922
  var RELEASE_TAG_USAGE = "rafa release tag";
17862
17923
  var DEFAULT_RELEASE_BRANCH = "main";
17863
17924
  var DEFAULT_REGISTRY = "https://registry.npmjs.org";
17864
17925
  var RELEASE_REMOTE = "origin";
17865
17926
  var INDENT8 = " ";
17927
+ var SHORT_COMMIT = 7;
17866
17928
  function readBranch2(git) {
17867
17929
  const result = git(["rev-parse", "--abbrev-ref", "HEAD"]);
17868
17930
  if (!result.ok) {
@@ -17939,18 +18001,22 @@ function readPublishTarget(text) {
17939
18001
  isPrivate: fields["private"] === true
17940
18002
  };
17941
18003
  }
17942
- function followUpsFor2(tag, version2, target) {
18004
+ var ON_HEAD = Object.freeze({ ahead: 0, branch: DEFAULT_RELEASE_BRANCH });
18005
+ function followUpsFor2(tag, version2, target, place2 = ON_HEAD) {
17943
18006
  const push = {
17944
18007
  command: `git push ${RELEASE_REMOTE} ${tag}`,
17945
18008
  why: `the tag is local until ${RELEASE_REMOTE} has it`
17946
18009
  };
17947
18010
  if (target.name === null || target.isPrivate)
17948
18011
  return [push];
18012
+ const publishes = `publishes ${target.name}@${version2} to ${target.registry}`;
18013
+ if (place2.ahead === 0)
18014
+ return [push, { command: `${target.manager} publish`, why: publishes }];
17949
18015
  return [
17950
18016
  push,
17951
18017
  {
17952
- command: `${target.manager} publish`,
17953
- why: `publishes ${target.name}@${version2} to ${target.registry}`
18018
+ command: `git switch --detach ${tag} && ${target.manager} publish && git switch ${place2.branch}`,
18019
+ why: `${publishes} from the tagged commit, not HEAD; skip it if ${target.registry} has that version already`
17954
18020
  }
17955
18021
  ];
17956
18022
  }
@@ -17990,7 +18056,14 @@ function decideTag(inputs) {
17990
18056
  const disagrees = changelogRefusal(inputs, version2);
17991
18057
  if (disagrees !== null)
17992
18058
  return disagrees;
17993
- return { kind: "ready", version: version2, tag: versionTag(version2) };
18059
+ const release = inputs.release;
18060
+ if (release === null)
18061
+ return refuse2("version", `${inputs.version.path} was not looked up in the history`);
18062
+ if (release.problem !== null)
18063
+ return refuse2(release.problem.reason, release.problem.message);
18064
+ if (release.commit === null)
18065
+ return refuse2("git", `no commit could be named for ${version2}`);
18066
+ return { kind: "ready", version: version2, tag: versionTag(version2), commit: release.commit, ahead: release.ahead };
17994
18067
  }
17995
18068
  function projectOf9(context) {
17996
18069
  if (context.project === null)
@@ -18013,23 +18086,34 @@ function tagConfig(project, warn3) {
18013
18086
  }
18014
18087
  }
18015
18088
  function readTagInputs(root, git, config) {
18089
+ const branch = readBranch2(git);
18090
+ const version2 = readVersion(root, config.versionFile);
18091
+ const walkable = version2.version !== null && branch.branch === config.releaseBranch;
18016
18092
  return {
18017
18093
  releaseBranch: config.releaseBranch,
18018
- branch: readBranch2(git),
18019
- version: readVersion(root, config.versionFile),
18094
+ branch,
18095
+ version: version2,
18020
18096
  changelog: readNewestRelease(root, config.changelog),
18021
- tags: readTags(git)
18097
+ tags: readTags(git),
18098
+ release: walkable && version2.version !== null ? readReleaseCommit(git, config.versionFile, version2.version) : null
18022
18099
  };
18023
18100
  }
18024
- function writeTag(git, tag) {
18025
- const result = git(["tag", tag, "HEAD"]);
18101
+ function writeTag(git, tag, commit) {
18102
+ const result = git(["tag", tag, commit]);
18026
18103
  if (result.ok)
18027
18104
  return;
18028
18105
  throw new CommandExit(1, `\u274C ${tag} could not be written: ${gitSaid(result)}`);
18029
18106
  }
18107
+ function pastReleaseWarning(written, branch) {
18108
+ if (written.ahead === 0)
18109
+ return null;
18110
+ const commits = written.ahead === 1 ? "1 commit" : `${String(written.ahead)} commits`;
18111
+ return `HEAD of ${branch} is ${commits} past ${written.commit.slice(0, SHORT_COMMIT)}, where ${written.version}` + ` was set; they are not in ${written.tag}.`;
18112
+ }
18030
18113
  function renderTagged(written, branch, followUps) {
18114
+ const where = written.ahead === 0 ? `the HEAD of ${branch}` : `${written.commit.slice(0, SHORT_COMMIT)}, the commit of ${branch} that set ${written.version}`;
18031
18115
  return [
18032
- `\u2705 Tagged ${written.tag} at the HEAD of ${branch}.`,
18116
+ `\u2705 Tagged ${written.tag} at ${where}.`,
18033
18117
  "Next:",
18034
18118
  ...followUps.map((followUp2) => `${INDENT8}${followUp2.command} \u2014 ${followUp2.why}`)
18035
18119
  ];
@@ -18047,8 +18131,9 @@ function runTag(context, seams = DEFAULT_RELEASE_SEAMS) {
18047
18131
  throw new CommandExit(1, `\u274C ${decision.message}
18048
18132
  Usage: ${RELEASE_TAG_USAGE}`);
18049
18133
  }
18050
- writeTag(git, decision.tag);
18051
- const followUps = followUpsFor2(decision.tag, decision.version, readPublishTarget(inputs.version.text));
18134
+ writeTag(git, decision.tag, decision.commit);
18135
+ const place2 = { ahead: decision.ahead, branch: config.releaseBranch };
18136
+ const followUps = followUpsFor2(decision.tag, decision.version, readPublishTarget(inputs.version.text), place2);
18052
18137
  return { inputs, written: decision, followUps };
18053
18138
  }
18054
18139
  function createReleaseTagCommand(seams = DEFAULT_RELEASE_SEAMS) {
@@ -18056,8 +18141,8 @@ function createReleaseTagCommand(seams = DEFAULT_RELEASE_SEAMS) {
18056
18141
  name: "release tag",
18057
18142
  subject: "release",
18058
18143
  action: "tag",
18059
- summary: "tag the release branch's HEAD with the version both release files agree on",
18060
- description: "Writes `v<version>` on the HEAD of the release branch \u2014 `pr.base`, or `main` \u2014 when the" + " version `release.versionFile` declares is the one the newest section of `release.changelog` names." + " It refuses, and writes nothing, when another branch is checked out, when a tag already names that" + " version, when the two files disagree, and when either the version file or the tag list could not" + " be read. After the tag it prints what to run next: the push that puts the tag on the remote, and" + " the publish line for the registry the version file configures, which it does not run. With" + " `--output=json` the readings, the decision and the follow-ups are the data of the terminal result" + " event.",
18144
+ summary: "tag the commit that set the version both release files agree on",
18145
+ description: "Writes `v<version>` on the release branch \u2014 `pr.base`, or `main` \u2014 when the" + " version `release.versionFile` declares is the one the newest section of `release.changelog` names." + " The tag goes on the commit that set that version: HEAD, or, where later commits landed on the branch," + " the earlier commit, with a warning naming how many commits HEAD is past it." + " It refuses, and writes nothing, when another branch is checked out, when a tag already names that" + " version, when the two files disagree, when no commit holds the version yet, and when the version" + " file, the tag list or the history could not be read. After the tag it prints what to run next: the" + " push that puts the tag on the remote, and the publish line for the registry the version file" + " configures, spelled to publish from the tag when HEAD is past it, which it does not run. With" + " `--output=json` the readings, the decision and the follow-ups are the data of the terminal result" + " event.",
18061
18146
  args: [],
18062
18147
  flags: [],
18063
18148
  examples: [
@@ -18073,6 +18158,9 @@ function createReleaseTagCommand(seams = DEFAULT_RELEASE_SEAMS) {
18073
18158
  outputs: ["text", "json"],
18074
18159
  run: async (context) => {
18075
18160
  const result = runTag(context, seams);
18161
+ const past = pastReleaseWarning(result.written, result.inputs.releaseBranch);
18162
+ if (past !== null)
18163
+ context.output.warn(past);
18076
18164
  if (context.outputMode === "json") {
18077
18165
  context.output.result(result);
18078
18166
  return;
@@ -20328,7 +20416,7 @@ var STATUS_NETWORK_TIMEOUT_MS = 5000;
20328
20416
  function isIdleWorktree(row2) {
20329
20417
  return !row2.blockers.some((blocker) => blocker.kind === "recent");
20330
20418
  }
20331
- function unread2(problem2) {
20419
+ function unread3(problem2) {
20332
20420
  return { read: false, problem: problem2 };
20333
20421
  }
20334
20422
  function read2(reading) {
@@ -20339,7 +20427,7 @@ function localSection(work) {
20339
20427
  const reading = work();
20340
20428
  return isUnread(reading) ? reading : read2(reading);
20341
20429
  } catch (error) {
20342
- return unread2(messageOf(error));
20430
+ return unread3(messageOf(error));
20343
20431
  }
20344
20432
  }
20345
20433
  function isUnread(value) {
@@ -20372,13 +20460,13 @@ async function networkSection(what, deadline, work) {
20372
20460
  let timer;
20373
20461
  const expired = new Promise((resolve16) => {
20374
20462
  timer = setTimeout(() => {
20375
- resolve16(unread2(`the ${what} was not read within the ${String(deadline.ms)}ms network deadline`));
20463
+ resolve16(unread3(`the ${what} was not read within the ${String(deadline.ms)}ms network deadline`));
20376
20464
  }, Math.max(0, deadline.left()));
20377
20465
  });
20378
20466
  try {
20379
20467
  return await Promise.race([work().then((reading) => read2(reading)), expired]);
20380
20468
  } catch (error) {
20381
- return unread2(messageOf(error));
20469
+ return unread3(messageOf(error));
20382
20470
  } finally {
20383
20471
  clearTimeout(timer);
20384
20472
  }
@@ -20394,7 +20482,7 @@ function readBranchSection(sources) {
20394
20482
  };
20395
20483
  const branch = readBranch(sources, note);
20396
20484
  if (branch === "") {
20397
- return unread2(notes.join("; ") || "the branch at the project root could not be read");
20485
+ return unread3(notes.join("; ") || "the branch at the project root could not be read");
20398
20486
  }
20399
20487
  const plans = readPlans(sources, note);
20400
20488
  return { branch, plan: readBranchPlan(branch, plans), notes };
@@ -20421,9 +20509,9 @@ function readLoopsSection(root, sources) {
20421
20509
  }
20422
20510
  function pullSection(sources, branch, provider, deadline) {
20423
20511
  if (provider.provider !== "gh")
20424
- return Promise.resolve(unread2(notGhProblem("pull request", provider)));
20512
+ return Promise.resolve(unread3(notGhProblem("pull request", provider)));
20425
20513
  if (!branch.read) {
20426
- return Promise.resolve(unread2(`no pull request was looked for, as the branch was not read: ${branch.problem}`));
20514
+ return Promise.resolve(unread3(`no pull request was looked for, as the branch was not read: ${branch.problem}`));
20427
20515
  }
20428
20516
  return networkSection("pull request", deadline, () => readPull(sources, branch.branch));
20429
20517
  }
@@ -20507,11 +20595,11 @@ async function readHousekeeping(input, cleanup, now) {
20507
20595
  const settings = doctorCleanupSettings({ root: input.root, home: input.home, config: input.config, gh: null }, now);
20508
20596
  const reading = await readCleanup(cleanup, settings);
20509
20597
  if (!reading.ok)
20510
- return unread2(reading.detail);
20598
+ return unread3(reading.detail);
20511
20599
  const idle = reading.worktrees.filter((row2) => isIdleWorktree(row2));
20512
20600
  return read2({ counts: cleanupCounts(reading), idleWorktrees: idle.length, notes: reading.notes });
20513
20601
  } catch (error) {
20514
- return unread2(messageOf(error));
20602
+ return unread3(messageOf(error));
20515
20603
  }
20516
20604
  }
20517
20605
  async function readStatusSections(input, seams = {}) {
@@ -20540,7 +20628,7 @@ async function readStatusSections(input, seams = {}) {
20540
20628
  const blockedIssues = seams.blockedIssues ?? ((runner) => readBlockedIssues({ gh: runner }));
20541
20629
  deadline.start();
20542
20630
  const pull = pullSection(sources, branch, provider, deadline);
20543
- const boardSection = isGh ? networkSection("board", deadline, () => readBoard(root, board, listing2, blockedIssues(gh))) : Promise.resolve(unread2(notGhProblem("board", provider)));
20631
+ const boardSection = isGh ? networkSection("board", deadline, () => readBoard(root, board, listing2, blockedIssues(gh))) : Promise.resolve(unread3(notGhProblem("board", provider)));
20544
20632
  const housekeeping = readHousekeeping(input, cleanup, now);
20545
20633
  return {
20546
20634
  branch,
@@ -20641,7 +20729,7 @@ var CORE_SUBJECTS = Object.freeze([
20641
20729
  { name: "agent", summary: "copy an agent definition into the project; list what a session sees" },
20642
20730
  { name: "skill", summary: "check a skills directory; list each tier; demote and backfill it" },
20643
20731
  { name: "instinct", summary: "check an instincts directory; list, show, flag and promote its records" },
20644
- { name: "release", summary: "read the release state of the project; tag the release branch's HEAD" },
20732
+ { name: "release", summary: "read the release state of the project; tag the commit that set the version" },
20645
20733
  { name: "board", summary: "list the boards with their owner, epic count, and which is current and home" },
20646
20734
  { name: "epic", summary: "show one epic's issues as the Roadmap table; create an epic; defer or promote it; move an issue to it; close it through the gate or cancel it" }
20647
20735
  ]);
@@ -5132,7 +5132,7 @@ async function report(args, repoRoot) {
5132
5132
 
5133
5133
  // package.json
5134
5134
  var name = "@open-tomato/rafa";
5135
- var version = "0.24.0";
5135
+ var version = "0.24.1";
5136
5136
 
5137
5137
  // src/modules/manifest.ts
5138
5138
  var MANIFEST_VERSION = 1;
@@ -9930,7 +9930,7 @@ async function enforceRefsGate(options) {
9930
9930
  import path2 from "path";
9931
9931
  async function coreRoster() {
9932
9932
  const [{ CORE_REGISTRY }, { describeRegistry }] = await Promise.all([
9933
- import("./index-zzvc0gkb.js"),
9933
+ import("./index-amyd4dse.js"),
9934
9934
  import("./describe-66gth329.js")
9935
9935
  ]);
9936
9936
  return describeRegistry(CORE_REGISTRY, version);
@@ -11496,7 +11496,7 @@ function readMergeRefusal(reading2) {
11496
11496
  return held.length === 0 ? null : worktreeRefusal(reading2.branch, held);
11497
11497
  }
11498
11498
  function cleanUpSteps(plan) {
11499
- const { base, branch, remote = DEFAULT_REMOTE2, remoteBranchPresent } = plan;
11499
+ const { base, branch, remote = DEFAULT_REMOTE2, localBranchPresent, remoteBranchPresent } = plan;
11500
11500
  const steps = [
11501
11501
  { id: "switch-base", label: `switch to ${base}`, argv: ["git", "switch", base] },
11502
11502
  {
@@ -11507,7 +11507,8 @@ function cleanUpSteps(plan) {
11507
11507
  {
11508
11508
  id: "delete-local",
11509
11509
  label: `delete the local branch ${branch}`,
11510
- argv: ["git", "branch", "-D", branch]
11510
+ argv: ["git", "branch", "-D", branch],
11511
+ ...localBranchPresent ? {} : { skip: `no local branch ${branch}; nothing to delete` }
11511
11512
  }
11512
11513
  ];
11513
11514
  if (remoteBranchPresent) {
@@ -11532,7 +11533,7 @@ function commandLine(step) {
11532
11533
  }
11533
11534
  function remainingFrom(steps, failed2) {
11534
11535
  const at = steps.findIndex((step) => step.id === failed2);
11535
- return at === -1 ? Object.freeze([]) : Object.freeze(steps.slice(at));
11536
+ return at === -1 ? Object.freeze([]) : Object.freeze(steps.slice(at).filter((step) => step.skip === undefined));
11536
11537
  }
11537
11538
  // src/pr/none.ts
11538
11539
  import { spawnSync as spawnSync2 } from "child_process";
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  start,
21
21
  usage,
22
22
  validateManifest
23
- } from "./index-gt9acb1y.js";
23
+ } from "./index-h369efkh.js";
24
24
  import {
25
25
  PLAN_BLOCK_KINDS,
26
26
  PLAN_HEADER_FIELDS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-tomato/rafa",
3
- "version": "0.24.0",
3
+ "version": "0.24.1",
4
4
  "description": "Alpha. Runs a plan through the ralph loop: one Claude Code session per task, one commit per task, then a pull request and a CI wait.",
5
5
  "private": false,
6
6
  "publishConfig": {