automata-cli 0.6.0-develop.273 → 0.6.0-develop.282

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.
Files changed (2) hide show
  1. package/dist/index.js +453 -5
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -778,6 +778,47 @@ function deleteLocalBranch(branch) {
778
778
  throw new Error(`Failed to delete branch ${branch}: ${result.stderr.trim()}`);
779
779
  }
780
780
  }
781
+ function listLocalBranches() {
782
+ const { stdout, status } = run2("git", ["for-each-ref", "--format=%(refname:short)", "refs/heads"]);
783
+ if (status !== 0) return [];
784
+ return stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
785
+ }
786
+ function listRemoteBranches() {
787
+ const { stdout, status } = run2("git", ["ls-remote", "--heads", "origin"]);
788
+ if (status !== 0) return null;
789
+ const names = [];
790
+ for (const line of stdout.split("\n")) {
791
+ const match = /^[0-9a-f]+\s+refs\/heads\/(.+)$/.exec(line.trim());
792
+ if (match) names.push(match[1]);
793
+ }
794
+ return names;
795
+ }
796
+ function createBranchAtHead(branch) {
797
+ return gitCommand(["checkout", "-b", branch]);
798
+ }
799
+ function stageAllExcept(excludePaths) {
800
+ const args = ["add", "-A"];
801
+ if (excludePaths.length > 0) {
802
+ args.push("--", ".", ...excludePaths.map((path) => `:(exclude)${path}`));
803
+ }
804
+ return gitCommand(args);
805
+ }
806
+ function commitStaged(message) {
807
+ return gitCommand(["commit", "-m", message]);
808
+ }
809
+ function pushSetUpstream(branch) {
810
+ return gitCommand(["push", "-u", "origin", branch]);
811
+ }
812
+ function countCommitsNotIn(baseBranch, branch) {
813
+ const { stdout, status } = run2("git", ["rev-list", "--count", `${baseBranch}..${branch}`]);
814
+ if (status !== 0) return null;
815
+ const trimmed2 = stdout.trim();
816
+ if (!/^\d+$/.test(trimmed2)) return null;
817
+ return Number(trimmed2);
818
+ }
819
+ function forceDeleteLocalBranch(branch) {
820
+ return gitCommand(["branch", "-D", branch]);
821
+ }
781
822
  var REVIEW_THREADS_QUERY = `
782
823
  query($owner:String!,$repo:String!,$prNumber:Int!){
783
824
  repository(owner:$owner,name:$repo){
@@ -2765,6 +2806,68 @@ function deleteMarker(marker) {
2765
2806
  throw new Error(stderr.trim() || `Failed to delete comment ${marker.commentId}.`);
2766
2807
  }
2767
2808
  }
2809
+ function toHeadState(state) {
2810
+ return state === "OPEN" || state === "MERGED" ? state : "CLOSED";
2811
+ }
2812
+ function listPullRequestsForHead(branch) {
2813
+ const raw = ghJson(
2814
+ ["pr", "list", "--head", branch, "--state", "all", "--json", "number,state,url,updatedAt"],
2815
+ `list pull requests for branch ${branch}`
2816
+ );
2817
+ return raw.map((pr) => ({
2818
+ number: pr.number,
2819
+ url: pr.url,
2820
+ state: toHeadState(pr.state),
2821
+ updatedAt: pr.updatedAt
2822
+ })).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
2823
+ }
2824
+ var MISSING_LABEL_PATTERNS = [
2825
+ // gh's own message: `could not add label: 'rescue' not found`.
2826
+ /could not add label/i,
2827
+ // The GraphQL error it wraps, seen directly on some gh versions.
2828
+ /could not resolve to a label/i,
2829
+ /\blabels?\b[^\n]*\b(?:not found|does not exist)\b/i
2830
+ ];
2831
+ function isMissingLabelError(stderr) {
2832
+ return MISSING_LABEL_PATTERNS.some((pattern) => pattern.test(stderr));
2833
+ }
2834
+ function createDraftPullRequest(input) {
2835
+ const base = [
2836
+ "pr",
2837
+ "create",
2838
+ "--draft",
2839
+ "--head",
2840
+ input.head,
2841
+ "--base",
2842
+ input.base,
2843
+ "--title",
2844
+ input.title,
2845
+ "--body",
2846
+ input.body
2847
+ ];
2848
+ if (input.label !== void 0 && input.label.length > 0) {
2849
+ const labelled = run4("gh", [...base, "--label", input.label]);
2850
+ if (labelled.status === 0) return parseCreatedPrUrl(labelled.stdout, input.head);
2851
+ if (!isMissingLabelError(labelled.stderr)) {
2852
+ throw new Error(
2853
+ labelled.stderr.trim() || `Failed to open a draft pull request for ${input.head}.`
2854
+ );
2855
+ }
2856
+ }
2857
+ const { stdout, stderr, status } = run4("gh", base);
2858
+ if (status !== 0) {
2859
+ throw new Error(stderr.trim() || `Failed to open a draft pull request for ${input.head}.`);
2860
+ }
2861
+ return parseCreatedPrUrl(stdout, input.head);
2862
+ }
2863
+ function parseCreatedPrUrl(stdout, head) {
2864
+ const url = stdout.trim().split("\n").pop()?.trim() ?? "";
2865
+ const match = /\/pull\/(\d+)\s*$/.exec(url);
2866
+ if (!match) {
2867
+ throw new Error(`Opened a pull request for ${head} but could not read its number from: ${url}`);
2868
+ }
2869
+ return { number: Number(match[1]), url };
2870
+ }
2768
2871
 
2769
2872
  // src/github/workDetection.ts
2770
2873
  function isAssignedToAgent(assignees, agentUser) {
@@ -3402,6 +3505,283 @@ function preparePrBranch(headRefName) {
3402
3505
  return { ok: true, branch: headRefName };
3403
3506
  }
3404
3507
 
3508
+ // src/git/repoHygiene.ts
3509
+ var RESCUE_PR_LABEL = "rescue";
3510
+ var RESCUE_BRANCH_PREFIX = "rescue/";
3511
+ function utcStamp(now) {
3512
+ return now.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
3513
+ }
3514
+ function flattenBranchName(branch) {
3515
+ return branch.replaceAll("/", "-");
3516
+ }
3517
+ function detectRescueTarget(baseBranch, now) {
3518
+ const current = getCurrentBranch();
3519
+ const detached = current === "HEAD" || current.length === 0;
3520
+ if (!detached && current !== baseBranch) {
3521
+ return { branch: current, createdBranch: false, source: current };
3522
+ }
3523
+ const source = detached ? "detached HEAD" : baseBranch;
3524
+ const label = detached ? "detached" : flattenBranchName(baseBranch);
3525
+ return {
3526
+ branch: `${RESCUE_BRANCH_PREFIX}${label}-${utcStamp(now)}`,
3527
+ createdBranch: true,
3528
+ source
3529
+ };
3530
+ }
3531
+ function rescueBody(source) {
3532
+ return `Opened by \`automata do-work\`'s repository-hygiene pre-flight because this branch carried work that existed only in the local checkout.
3533
+
3534
+ Source: \`${source}\`.
3535
+
3536
+ It is a draft and references no issue: nothing here is claimed to be finished, it is here so it cannot be lost. Review it, fold it into the right pull request, or close it.`;
3537
+ }
3538
+ function findOpenPr(branch) {
3539
+ const prs = listPullRequestsForHead(branch);
3540
+ return prs.find((pr) => pr.state === "OPEN") ?? null;
3541
+ }
3542
+ function rescueUncommittedChanges(options, now) {
3543
+ if (!hasUncommittedChanges([RUN_LOCK_RELATIVE_PATH])) {
3544
+ options.log(" rescue nothing to do; the working tree is clean\n");
3545
+ return { kind: "clean" };
3546
+ }
3547
+ const target = detectRescueTarget(options.baseBranch, now);
3548
+ if (options.dryRun) {
3549
+ options.log(
3550
+ target.createdBranch ? ` rescue would create ${target.branch} off ${target.source}, commit the uncommitted changes, push it and open a draft PR
3551
+ ` : ` rescue would commit the uncommitted changes onto ${target.branch}, push it and open a draft PR if it has none open
3552
+ `
3553
+ );
3554
+ return { kind: "would-rescue", branch: target.branch, createdBranch: target.createdBranch };
3555
+ }
3556
+ if (target.createdBranch) {
3557
+ const created = createBranchAtHead(target.branch);
3558
+ if (!created.ok) {
3559
+ options.log(` rescue FAILED to create ${target.branch}: ${created.stderr}
3560
+ `);
3561
+ return { kind: "failed", step: "branch", detail: created.stderr };
3562
+ }
3563
+ }
3564
+ const staged = stageAllExcept([RUN_LOCK_RELATIVE_PATH]);
3565
+ if (!staged.ok) {
3566
+ options.log(` rescue FAILED to stage the changes: ${staged.stderr}
3567
+ `);
3568
+ return { kind: "failed", step: "stage", detail: staged.stderr };
3569
+ }
3570
+ const committed = commitStaged(`chore(automata): rescue uncommitted work from ${target.source}`);
3571
+ if (!committed.ok) {
3572
+ options.log(` rescue FAILED to commit: ${committed.stderr}
3573
+ `);
3574
+ return { kind: "failed", step: "commit", detail: committed.stderr };
3575
+ }
3576
+ const pushed = pushSetUpstream(target.branch);
3577
+ if (!pushed.ok) {
3578
+ options.log(` rescue FAILED to push ${target.branch}: ${pushed.stderr}
3579
+ `);
3580
+ return { kind: "failed", step: "push", detail: pushed.stderr };
3581
+ }
3582
+ let existing;
3583
+ try {
3584
+ existing = findOpenPr(target.branch);
3585
+ } catch (err) {
3586
+ options.log(
3587
+ ` rescue committed and pushed ${target.branch}, but could not check for an open PR: ${err.message}
3588
+ `
3589
+ );
3590
+ return { kind: "failed", step: "pr", detail: err.message };
3591
+ }
3592
+ if (existing !== null) {
3593
+ options.log(
3594
+ ` rescue committed and pushed ${target.branch}; PR #${String(existing.number)} is already open
3595
+ `
3596
+ );
3597
+ return {
3598
+ kind: "rescued",
3599
+ branch: target.branch,
3600
+ createdBranch: target.createdBranch,
3601
+ pr: existing.number,
3602
+ prUrl: existing.url,
3603
+ prCreated: false
3604
+ };
3605
+ }
3606
+ try {
3607
+ const pr = createDraftPullRequest({
3608
+ head: target.branch,
3609
+ base: options.baseBranch,
3610
+ title: `rescue: uncommitted work from ${target.source}`,
3611
+ body: rescueBody(target.source),
3612
+ label: RESCUE_PR_LABEL
3613
+ });
3614
+ options.log(
3615
+ ` rescue committed and pushed ${target.branch}; opened draft PR #${String(pr.number)}
3616
+ `
3617
+ );
3618
+ return {
3619
+ kind: "rescued",
3620
+ branch: target.branch,
3621
+ createdBranch: target.createdBranch,
3622
+ pr: pr.number,
3623
+ prUrl: pr.url,
3624
+ prCreated: true
3625
+ };
3626
+ } catch (err) {
3627
+ options.log(
3628
+ ` rescue committed and pushed ${target.branch}, but could not open a draft PR: ${err.message}
3629
+ `
3630
+ );
3631
+ return { kind: "failed", step: "pr", detail: err.message };
3632
+ }
3633
+ }
3634
+ function prepareBase(options) {
3635
+ if (options.dryRun) {
3636
+ options.log(` base would check out ${options.baseBranch} and pull --ff-only
3637
+ `);
3638
+ return { ok: true };
3639
+ }
3640
+ const checkout = checkoutBranch(options.baseBranch);
3641
+ if (!checkout.ok) {
3642
+ options.log(` base FAILED to check out ${options.baseBranch}: ${checkout.stderr}
3643
+ `);
3644
+ return { ok: false, step: "checkout", detail: checkout.stderr };
3645
+ }
3646
+ const pull = pullFastForwardOnly();
3647
+ if (!pull.ok) {
3648
+ options.log(` base FAILED to pull ${options.baseBranch}: ${pull.stderr}
3649
+ `);
3650
+ return { ok: false, step: "pull", detail: pull.stderr };
3651
+ }
3652
+ options.log(` base ${options.baseBranch} checked out and up to date
3653
+ `);
3654
+ return { ok: true };
3655
+ }
3656
+ function collectCandidates(options) {
3657
+ const remote = listRemoteBranches();
3658
+ if (remote === null) return null;
3659
+ const remoteNames = new Set(remote);
3660
+ const current = getCurrentBranch();
3661
+ const untouchable = /* @__PURE__ */ new Set([options.baseBranch, current, ...options.protectedBranches]);
3662
+ return listLocalBranches().filter(
3663
+ (branch) => !untouchable.has(branch) && !remoteNames.has(branch)
3664
+ );
3665
+ }
3666
+ function deleteOrReport(branch, options, why) {
3667
+ if (options.dryRun) {
3668
+ options.log(` prune would delete ${branch} (no remote, ${why})
3669
+ `);
3670
+ return { kind: "would-delete", branch };
3671
+ }
3672
+ const deleted = forceDeleteLocalBranch(branch);
3673
+ if (!deleted.ok) {
3674
+ options.log(` prune kept ${branch}: delete failed \u2014 ${deleted.stderr}
3675
+ `);
3676
+ return { kind: "kept", branch, reason: "delete-failed", detail: deleted.stderr };
3677
+ }
3678
+ options.log(` prune deleted ${branch} (${why})
3679
+ `);
3680
+ return { kind: "deleted", branch };
3681
+ }
3682
+ function pruneCandidate(branch, options) {
3683
+ let prs;
3684
+ try {
3685
+ prs = listPullRequestsForHead(branch);
3686
+ } catch (err) {
3687
+ const detail = err.message;
3688
+ options.log(` prune kept ${branch}: could not read its pull requests \u2014 ${detail}
3689
+ `);
3690
+ return { kind: "kept", branch, reason: "lookup-failed", detail };
3691
+ }
3692
+ const openPr = prs.find((pr) => pr.state === "OPEN") ?? null;
3693
+ if (openPr !== null) {
3694
+ options.log(` prune kept ${branch}: PR #${String(openPr.number)} is open
3695
+ `);
3696
+ return {
3697
+ kind: "kept",
3698
+ branch,
3699
+ reason: "open-pr",
3700
+ detail: `PR #${String(openPr.number)} is open`
3701
+ };
3702
+ }
3703
+ const merged = prs.find((pr) => pr.state === "MERGED") ?? null;
3704
+ if (merged !== null) {
3705
+ return deleteOrReport(branch, options, `PR #${String(merged.number)} was merged`);
3706
+ }
3707
+ const closed = prs.find((pr) => pr.state === "CLOSED") ?? null;
3708
+ if (closed !== null) {
3709
+ return deleteOrReport(branch, options, `PR #${String(closed.number)} was closed unmerged`);
3710
+ }
3711
+ const unmerged = countCommitsNotIn(options.baseBranch, branch);
3712
+ if (unmerged === null) {
3713
+ const detail = `could not count commits outside ${options.baseBranch}`;
3714
+ options.log(` prune kept ${branch}: ${detail}
3715
+ `);
3716
+ return { kind: "kept", branch, reason: "lookup-failed", detail };
3717
+ }
3718
+ if (unmerged === 0) {
3719
+ return deleteOrReport(branch, options, `nothing outside ${options.baseBranch}`);
3720
+ }
3721
+ if (options.dryRun) {
3722
+ options.log(
3723
+ ` prune would push ${branch} and open a draft PR (${String(unmerged)} commit(s) outside ${options.baseBranch}), keeping the branch
3724
+ `
3725
+ );
3726
+ return { kind: "would-rescue", branch, unmergedCommits: unmerged };
3727
+ }
3728
+ const pushed = pushSetUpstream(branch);
3729
+ if (!pushed.ok) {
3730
+ options.log(
3731
+ ` prune kept ${branch}: has ${String(unmerged)} commit(s) outside ${options.baseBranch} and could not be pushed \u2014 ${pushed.stderr}
3732
+ `
3733
+ );
3734
+ return { kind: "kept", branch, reason: "push-failed", detail: pushed.stderr };
3735
+ }
3736
+ try {
3737
+ const pr = createDraftPullRequest({
3738
+ head: branch,
3739
+ base: options.baseBranch,
3740
+ title: `rescue: unmerged commits on ${branch}`,
3741
+ body: rescueBody(branch),
3742
+ label: RESCUE_PR_LABEL
3743
+ });
3744
+ options.log(
3745
+ ` prune rescued ${branch}: pushed and opened draft PR #${String(pr.number)}
3746
+ `
3747
+ );
3748
+ return { kind: "rescued", branch, pr: pr.number, prUrl: pr.url };
3749
+ } catch (err) {
3750
+ options.log(
3751
+ ` prune rescued ${branch}: pushed, but could not open a draft PR \u2014 ${err.message}
3752
+ `
3753
+ );
3754
+ return { kind: "rescued", branch, pr: null, prUrl: null };
3755
+ }
3756
+ }
3757
+ function prune(options) {
3758
+ const candidates = collectCandidates(options);
3759
+ if (candidates === null) {
3760
+ options.log(
3761
+ " prune skipped: could not list origin's branches, so no branch can be shown to have no remote\n"
3762
+ );
3763
+ return { outcomes: [], remoteUnreadable: true };
3764
+ }
3765
+ if (candidates.length === 0) {
3766
+ options.log(" prune nothing to do; every local branch exists on origin\n");
3767
+ return { outcomes: [], remoteUnreadable: false };
3768
+ }
3769
+ return {
3770
+ outcomes: candidates.map((branch) => pruneCandidate(branch, options)),
3771
+ remoteUnreadable: false
3772
+ };
3773
+ }
3774
+ function runRepoHygiene(options, now = /* @__PURE__ */ new Date()) {
3775
+ options.log(options.dryRun ? "\nPre-flight (dry run):\n" : "\nPre-flight:\n");
3776
+ const rescue = rescueUncommittedChanges(options, now);
3777
+ const base = prepareBase(options);
3778
+ const { outcomes, remoteUnreadable } = prune(options);
3779
+ const degraded = rescue.kind === "failed" || !base.ok || remoteUnreadable || outcomes.some(
3780
+ (outcome) => outcome.kind === "kept" && outcome.reason !== "open-pr" || outcome.kind === "rescued" && outcome.pr === null
3781
+ );
3782
+ return { rescue, base, prunes: outcomes, degraded };
3783
+ }
3784
+
3405
3785
  // src/commands/doWork.ts
3406
3786
  var inFlightMarker = null;
3407
3787
  function out(message) {
@@ -4148,6 +4528,12 @@ function explainInterruptedMarker() {
4148
4528
  }
4149
4529
  }
4150
4530
  async function runTick(settings, options) {
4531
+ const hygiene = runRepoHygiene({
4532
+ baseBranch: settings.baseBranch,
4533
+ protectedBranches: settings.protectedBranches,
4534
+ dryRun: options.dryRun === true,
4535
+ log: progress
4536
+ });
4151
4537
  const issues = discoverIssues(settings);
4152
4538
  const linkMap = getOpenPrLinkMap();
4153
4539
  const decisions = issues.map(
@@ -4166,7 +4552,7 @@ ${describePlan(decisions)}`;
4166
4552
  out(planText);
4167
4553
  }
4168
4554
  if (options.dryRun) {
4169
- reportDryRun(items, decisions, settings, options);
4555
+ reportDryRun(items, decisions, settings, options, hygiene);
4170
4556
  return 0;
4171
4557
  }
4172
4558
  const reports = [];
@@ -4206,17 +4592,24 @@ ${describePlan(decisions)}`;
4206
4592
  detail: `run cap of ${String(settings.maxRuns)} reached`
4207
4593
  });
4208
4594
  }
4209
- const degraded = reports.some((report) => report.outcome !== "answered");
4595
+ const degraded = hygiene.degraded || reports.some((report) => report.outcome !== "answered");
4210
4596
  const exitCode = degraded ? 2 : 0;
4211
4597
  if (options.json) {
4212
4598
  out(
4213
4599
  JSON.stringify(
4214
- { dryRun: false, plan: decisions.map(toPlanJson), items: reports.map(toItemJson), exitCode },
4600
+ {
4601
+ dryRun: false,
4602
+ preflight: toHygieneJson(hygiene),
4603
+ plan: decisions.map(toPlanJson),
4604
+ items: reports.map(toItemJson),
4605
+ exitCode
4606
+ },
4215
4607
  null,
4216
4608
  2
4217
4609
  ) + "\n"
4218
4610
  );
4219
4611
  } else {
4612
+ summarizeHygiene(hygiene);
4220
4613
  summarize(reports);
4221
4614
  }
4222
4615
  return exitCode;
@@ -4255,7 +4648,7 @@ function toRunJson(entry) {
4255
4648
  prompt: entry.run.prompt
4256
4649
  };
4257
4650
  }
4258
- function reportDryRun(items, decisions, settings, options) {
4651
+ function reportDryRun(items, decisions, settings, options, hygiene) {
4259
4652
  const describable = settings.maxRuns > 0 ? items.slice(0, settings.maxRuns) : items;
4260
4653
  const planned = describable.map((item) => {
4261
4654
  const resolved = resolveItemExecution(item, settings);
@@ -4270,6 +4663,7 @@ function reportDryRun(items, decisions, settings, options) {
4270
4663
  JSON.stringify(
4271
4664
  {
4272
4665
  dryRun: true,
4666
+ preflight: toHygieneJson(hygiene),
4273
4667
  plan: decisions.map(toPlanJson),
4274
4668
  runs: planned.map(toRunJson)
4275
4669
  },
@@ -4279,6 +4673,7 @@ function reportDryRun(items, decisions, settings, options) {
4279
4673
  );
4280
4674
  return;
4281
4675
  }
4676
+ summarizeHygiene(hygiene);
4282
4677
  for (const entry of planned) {
4283
4678
  out(
4284
4679
  "\n" + (entry.kind === "refused" ? describeRefusedRun(entry.item, entry.refusal) : describePlannedRun(entry.item, settings, entry.run, entry.execution))
@@ -4290,7 +4685,60 @@ function reportDryRun(items, decisions, settings, options) {
4290
4685
  (${String(deferred)} further item(s) deferred by the run cap.)
4291
4686
  `);
4292
4687
  }
4293
- out("\nDry run: nothing was assigned, posted, checked out or executed.\n");
4688
+ out(
4689
+ "\nDry run: nothing was rescued, pruned, pulled, assigned, posted, checked out or executed.\n"
4690
+ );
4691
+ }
4692
+ function summarizeHygiene(hygiene) {
4693
+ out("\nPre-flight:\n");
4694
+ out(` rescue ${describeRescue(hygiene.rescue)}
4695
+ `);
4696
+ out(
4697
+ hygiene.base.ok ? " base ready\n" : ` base ${hygiene.base.step} failed \u2014 ${hygiene.base.detail}
4698
+ `
4699
+ );
4700
+ if (hygiene.prunes.length === 0) {
4701
+ out(" prune no candidates\n");
4702
+ } else {
4703
+ for (const outcome of hygiene.prunes) {
4704
+ out(` prune ${describePrune(outcome)}
4705
+ `);
4706
+ }
4707
+ }
4708
+ }
4709
+ function describeRescue(rescue) {
4710
+ switch (rescue.kind) {
4711
+ case "clean":
4712
+ return "nothing to rescue";
4713
+ case "rescued":
4714
+ return rescue.prCreated ? `committed and pushed ${rescue.branch}, opened draft PR #${String(rescue.pr)}` : `committed and pushed ${rescue.branch}, PR #${String(rescue.pr)} already open`;
4715
+ case "would-rescue":
4716
+ return `would rescue onto ${rescue.branch}`;
4717
+ case "failed":
4718
+ return `${rescue.step} failed \u2014 ${rescue.detail}; the tree is still dirty and nothing was discarded`;
4719
+ }
4720
+ }
4721
+ function describePrune(outcome) {
4722
+ switch (outcome.kind) {
4723
+ case "deleted":
4724
+ return `deleted ${outcome.branch}`;
4725
+ case "would-delete":
4726
+ return `would delete ${outcome.branch}`;
4727
+ case "kept":
4728
+ return `kept ${outcome.branch} (${outcome.reason}: ${outcome.detail})`;
4729
+ case "rescued":
4730
+ return outcome.pr === null ? `rescued ${outcome.branch} (pushed; no PR opened)` : `rescued ${outcome.branch} (pushed, draft PR #${String(outcome.pr)})`;
4731
+ case "would-rescue":
4732
+ return `would rescue ${outcome.branch} (${String(outcome.unmergedCommits)} unmerged commit(s))`;
4733
+ }
4734
+ }
4735
+ function toHygieneJson(hygiene) {
4736
+ return {
4737
+ rescue: hygiene.rescue,
4738
+ base: hygiene.base,
4739
+ prunes: hygiene.prunes,
4740
+ degraded: hygiene.degraded
4741
+ };
4294
4742
  }
4295
4743
  function toPlanJson(decision) {
4296
4744
  if (decision.kind === "skip") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "automata-cli",
3
- "version": "0.6.0-develop.273",
3
+ "version": "0.6.0-develop.282",
4
4
  "description": "Automata CLI tool",
5
5
  "type": "module",
6
6
  "engines": {