automata-cli 0.7.0 → 0.8.0-develop.346

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 +161 -16
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -230,6 +230,7 @@ import { Command as Command2 } from "commander";
230
230
 
231
231
  // src/git/gitService.ts
232
232
  import { spawnSync as spawnSync2 } from "child_process";
233
+ import { existsSync } from "fs";
233
234
 
234
235
  // src/config/azdoService.ts
235
236
  import { spawnSync } from "child_process";
@@ -752,7 +753,7 @@ function checkoutAndPull(targetBranch) {
752
753
  if (checkout.status !== 0) {
753
754
  throw new Error(`Failed to checkout ${targetBranch}: ${checkout.stderr.trim()}`);
754
755
  }
755
- const pull = run2("git", ["pull"]);
756
+ const pull = run2("git", ["pull", "--ff-only"]);
756
757
  if (pull.status !== 0) {
757
758
  throw new Error(`Failed to pull ${targetBranch}: ${pull.stderr.trim()}`);
758
759
  }
@@ -785,6 +786,41 @@ function isAncestorCommit(maybeAncestor, descendant) {
785
786
  function resetHardTo(ref) {
786
787
  return gitCommand(["reset", "--hard", ref]);
787
788
  }
789
+ var CHERRY_LINE = /^([+-]) ([0-9a-f]+)$/;
790
+ function describeDivergence(upstream, head) {
791
+ const cherry = run2("git", ["cherry", upstream, head]);
792
+ if (cherry.status !== 0) return null;
793
+ const commits = [];
794
+ for (const line of cherry.stdout.split("\n")) {
795
+ const trimmed2 = line.trim();
796
+ if (trimmed2.length === 0) continue;
797
+ const match = CHERRY_LINE.exec(trimmed2);
798
+ if (match === null) return null;
799
+ commits.push({ sha: match[2], alreadyUpstream: match[1] === "-" });
800
+ }
801
+ const merges = run2("git", ["rev-list", "--count", "--merges", `${upstream}..${head}`]);
802
+ if (merges.status !== 0) return null;
803
+ const count = Number.parseInt(merges.stdout.trim(), 10);
804
+ if (!Number.isInteger(count)) return null;
805
+ return { commits, merges: count };
806
+ }
807
+ function rebaseOnto(ref) {
808
+ const { stdout, stderr, status } = run2("git", ["rebase", "--no-reapply-cherry-picks", ref]);
809
+ const detail = [stdout.trim(), stderr.trim()].filter((part) => part.length > 0).join("\n");
810
+ return { ok: status === 0, stderr: detail };
811
+ }
812
+ function abortRebase() {
813
+ return gitCommand(["rebase", "--abort"]);
814
+ }
815
+ var REBASE_STATE_PATHS = ["rebase-merge", "rebase-apply/rebasing"];
816
+ function isRebaseInProgress() {
817
+ return REBASE_STATE_PATHS.some((name) => {
818
+ const { stdout, status } = run2("git", ["rev-parse", "--git-path", name]);
819
+ if (status !== 0) return false;
820
+ const path = stdout.trim();
821
+ return path.length > 0 && existsSync(path);
822
+ });
823
+ }
788
824
  function fetchPrune() {
789
825
  const result = run2("git", ["fetch", "--prune"]);
790
826
  if (result.status !== 0) {
@@ -2035,7 +2071,7 @@ function parseCreatedPrUrl(stdout, head) {
2035
2071
  // src/claude/claudeService.ts
2036
2072
  import { spawn, spawnSync as spawnSync5 } from "child_process";
2037
2073
  import { createInterface } from "readline";
2038
- import { existsSync } from "fs";
2074
+ import { existsSync as existsSync2 } from "fs";
2039
2075
  import { delimiter, join } from "path";
2040
2076
 
2041
2077
  // src/cli/spawnUtils.ts
@@ -2129,7 +2165,7 @@ function resolveCommand(name) {
2129
2165
  const pathDirs = (process.env["PATH"] ?? "").split(delimiter);
2130
2166
  for (const dir of pathDirs) {
2131
2167
  const candidate = join(dir, name);
2132
- if (existsSync(candidate)) return candidate;
2168
+ if (existsSync2(candidate)) return candidate;
2133
2169
  }
2134
2170
  return name;
2135
2171
  }
@@ -3668,7 +3704,7 @@ function prepareBaseBranch(baseBranch) {
3668
3704
  if (!pull.ok) {
3669
3705
  return { ok: false, reason: "pull-failed", detail: pull.stderr };
3670
3706
  }
3671
- return { ok: true, branch: baseBranch };
3707
+ return { ok: true, branch: baseBranch, strategy: "fast-forward" };
3672
3708
  }
3673
3709
  function resetToForcePushedRemote(headRefName, previousRemoteSha) {
3674
3710
  const localSha = revParse(`refs/heads/${headRefName}`);
@@ -3678,7 +3714,81 @@ function resetToForcePushedRemote(headRefName, previousRemoteSha) {
3678
3714
  if (!reset.ok) {
3679
3715
  return { ok: false, reason: "pull-failed", detail: reset.stderr };
3680
3716
  }
3681
- return { ok: true, branch: headRefName };
3717
+ return { ok: true, branch: headRefName, strategy: "reset-to-remote" };
3718
+ }
3719
+ function rebaseOntoAlreadyAppliedRemote(headRefName) {
3720
+ const upstream = `refs/remotes/origin/${headRefName}`;
3721
+ const divergence = describeDivergence(upstream, `refs/heads/${headRefName}`);
3722
+ if (divergence === null) return null;
3723
+ if (divergence.merges > 0) return null;
3724
+ if (divergence.commits.length === 0) return null;
3725
+ if (divergence.commits.some((commit) => !commit.alreadyUpstream)) return null;
3726
+ if (isRebaseInProgress()) {
3727
+ return {
3728
+ ok: false,
3729
+ reason: "pull-failed",
3730
+ detail: `a rebase is already in progress in this checkout and automata did not start it, so ${headRefName} was left untouched; finish it with \`git rebase --continue\` or drop it with \`git rebase --abort\``
3731
+ };
3732
+ }
3733
+ const rebase = rebaseOnto(upstream);
3734
+ if (!rebase.ok) {
3735
+ if (!isRebaseInProgress()) {
3736
+ return {
3737
+ ok: false,
3738
+ reason: "pull-failed",
3739
+ detail: `${rebase.stderr} \u2014 the rebase never started, so ${headRefName} is untouched`
3740
+ };
3741
+ }
3742
+ const aborted = abortRebase();
3743
+ const restored = aborted.ok ? `the rebase was aborted, so ${headRefName} is back where it was` : `the rebase could NOT be aborted (${aborted.stderr}) \u2014 run \`git rebase --abort\` in the checkout`;
3744
+ return {
3745
+ ok: false,
3746
+ reason: "rebase-conflict",
3747
+ detail: `${rebase.stderr} \u2014 ${restored}`
3748
+ };
3749
+ }
3750
+ const local = revParse(`refs/heads/${headRefName}`);
3751
+ const remote = revParse(upstream);
3752
+ if (local === null || remote === null || local !== remote) {
3753
+ return {
3754
+ ok: false,
3755
+ reason: "pull-failed",
3756
+ detail: `the rebase of ${headRefName} onto origin/${headRefName} reported success but did not land on the remote tip, so the branch is still out of sync; compare them with \`git log origin/${headRefName}..${headRefName}\``
3757
+ };
3758
+ }
3759
+ return { ok: true, branch: headRefName, strategy: "rebase" };
3760
+ }
3761
+ function divergenceRefusal(headRefName, pullError) {
3762
+ const divergence = describeDivergence(
3763
+ `refs/remotes/origin/${headRefName}`,
3764
+ `refs/heads/${headRefName}`
3765
+ );
3766
+ if (divergence === null) {
3767
+ return {
3768
+ ok: false,
3769
+ reason: "pull-failed",
3770
+ detail: `${pullError} \u2014 how the local ${headRefName} relates to origin/${headRefName} could not be established, so nothing was changed and ${headRefName} is untouched; inspect it with \`git log --oneline origin/${headRefName}...${headRefName}\` before deciding what to do`
3771
+ };
3772
+ }
3773
+ const unpushedCount = divergence.commits.filter((c) => !c.alreadyUpstream).length + divergence.merges;
3774
+ if (unpushedCount === 0) {
3775
+ return {
3776
+ ok: false,
3777
+ reason: "pull-failed",
3778
+ detail: `${pullError} \u2014 no commit of the local ${headRefName} is missing from origin/${headRefName}, so this is not a divergence; ${headRefName} is untouched and git's own error above is the cause to fix`
3779
+ };
3780
+ }
3781
+ let unpushed;
3782
+ if (unpushedCount === 1) {
3783
+ unpushed = `1 of its commits is not on origin/${headRefName}`;
3784
+ } else {
3785
+ unpushed = `${String(unpushedCount)} of its commits are not on origin/${headRefName}`;
3786
+ }
3787
+ return {
3788
+ ok: false,
3789
+ reason: "pull-failed",
3790
+ detail: `${pullError} \u2014 the local ${headRefName} has diverged from origin/${headRefName} and ${unpushed}, so it was not synchronized automatically; inspect them with \`git log origin/${headRefName}..${headRefName}\` and \`git reset --hard origin/${headRefName}\` yourself once they are safe to lose`
3791
+ };
3682
3792
  }
3683
3793
  function preparePrBranch(headRefName) {
3684
3794
  if (hasUncommittedChanges([RUN_LOCK_RELATIVE_PATH])) return dirtyTree();
@@ -3687,25 +3797,28 @@ function preparePrBranch(headRefName) {
3687
3797
  if (!fetched.ok) {
3688
3798
  return { ok: false, reason: "checkout-failed", detail: fetched.stderr };
3689
3799
  }
3800
+ const localExisted = revParse(`refs/heads/${headRefName}`) !== null;
3690
3801
  const checkout = checkoutBranch(headRefName);
3691
3802
  if (!checkout.ok) {
3692
3803
  const created = createTrackingBranch(headRefName);
3693
3804
  if (!created.ok) {
3694
3805
  return { ok: false, reason: "checkout-failed", detail: created.stderr };
3695
3806
  }
3696
- return { ok: true, branch: headRefName };
3807
+ return { ok: true, branch: headRefName, strategy: "tracking-branch" };
3697
3808
  }
3698
3809
  const pull = pullFastForwardOnly(headRefName);
3699
3810
  if (!pull.ok) {
3700
3811
  const reset = resetToForcePushedRemote(headRefName, previousRemoteSha);
3701
3812
  if (reset !== null) return reset;
3702
- return {
3703
- ok: false,
3704
- reason: "pull-failed",
3705
- detail: `${pull.stderr} \u2014 the local ${headRefName} has commits origin/${headRefName} does not, so it was not reset automatically; inspect them and \`git reset --hard origin/${headRefName}\` yourself`
3706
- };
3813
+ const rebased = rebaseOntoAlreadyAppliedRemote(headRefName);
3814
+ if (rebased !== null) return rebased;
3815
+ return divergenceRefusal(headRefName, pull.stderr);
3707
3816
  }
3708
- return { ok: true, branch: headRefName };
3817
+ return {
3818
+ ok: true,
3819
+ branch: headRefName,
3820
+ strategy: localExisted ? "fast-forward" : "tracking-branch"
3821
+ };
3709
3822
  }
3710
3823
 
3711
3824
  // src/git/repoHygiene.ts
@@ -4073,6 +4186,18 @@ function repoField(repo) {
4073
4186
  const flat = oneLine(repo);
4074
4187
  return flat.length === 0 ? "-" : flat;
4075
4188
  }
4189
+ var QUIET_SYNC = /* @__PURE__ */ new Set(["fast-forward", "tracking-branch"]);
4190
+ function summarizeSync(items) {
4191
+ const counts = /* @__PURE__ */ new Map();
4192
+ for (const item of items) {
4193
+ if (item.sync === void 0) continue;
4194
+ const key = oneLine(item.sync);
4195
+ if (key.length === 0 || QUIET_SYNC.has(key)) continue;
4196
+ counts.set(key, (counts.get(key) ?? 0) + 1);
4197
+ }
4198
+ if (counts.size === 0) return null;
4199
+ return [...counts].map(([strategy, count]) => `${strategy}:${String(count)}`).join(",");
4200
+ }
4076
4201
  function formatExecutionLine(tick) {
4077
4202
  const counts = {
4078
4203
  answered: 0,
@@ -4096,6 +4221,8 @@ function formatExecutionLine(tick) {
4096
4221
  `exit=${String(tick.exitCode)}`,
4097
4222
  `dur=${(tick.durationMs / 1e3).toFixed(1)}s`
4098
4223
  ];
4224
+ const sync = summarizeSync(tick.items);
4225
+ if (sync !== null) fields.push(`sync=${sync}`);
4099
4226
  if (tick.note !== void 0 && tick.note.length > 0) fields.push(`note=${tick.note}`);
4100
4227
  return fields.join(" ") + "\n";
4101
4228
  }
@@ -4112,13 +4239,18 @@ function describeItemExecution(item) {
4112
4239
  const effort = item.effort === void 0 ? "" : ` effort=${oneLine(item.effort)}`;
4113
4240
  return ` [${oneLine(item.executor)}${model}${effort}]`;
4114
4241
  }
4242
+ function describeItemSync(item) {
4243
+ if (item.sync === void 0) return "";
4244
+ const flat = oneLine(item.sync);
4245
+ return flat.length === 0 ? "" : ` sync=${flat}`;
4246
+ }
4115
4247
  function formatWorkRecord(tick) {
4116
4248
  const ran = tick.items.filter((item) => item.ranExecutor);
4117
4249
  if (ran.length === 0) return null;
4118
4250
  const header = `=== ${tick.timestamp.toISOString()} ${repoField(tick.repo)} ===
4119
4251
  `;
4120
4252
  const lines = ran.map(
4121
- (item) => `${oneLine(item.subject)} ${item.turn === null ? "-" : oneLine(item.turn)} ${item.outcome}${describeItemExecution(item)} \u2014 ${briefDetail(item.detail)}
4253
+ (item) => `${oneLine(item.subject)} ${item.turn === null ? "-" : oneLine(item.turn)} ${item.outcome}${describeItemExecution(item)}${describeItemSync(item)} \u2014 ${briefDetail(item.detail)}
4122
4254
  `
4123
4255
  );
4124
4256
  return header + lines.join("") + "\n";
@@ -4202,6 +4334,7 @@ function recordTick(tick, dir = operationLogDirectory()) {
4202
4334
 
4203
4335
  // src/commands/doWork.ts
4204
4336
  var inFlightMarker = null;
4337
+ var inFlightSync = null;
4205
4338
  function subjectLabel(issue, pr) {
4206
4339
  if (issue !== null) return `#${String(issue)}`;
4207
4340
  return pr === null ? "#?" : `PR #${String(pr)}`;
@@ -4840,6 +4973,7 @@ async function processItem(planned, settings, silent, preflightCauses) {
4840
4973
  progress(`
4841
4974
  ${itemLabel(planned)} ${planned.turn}: ${planned.reason}
4842
4975
  `);
4976
+ inFlightSync = null;
4843
4977
  const refreshed = refreshItem(planned, settings);
4844
4978
  if (refreshed.kind === "skip") {
4845
4979
  progress(` skipped: ${refreshed.detail}
@@ -4872,9 +5006,18 @@ ${itemLabel(planned)} ${planned.turn}: ${planned.reason}
4872
5006
  return {
4873
5007
  ...base,
4874
5008
  outcome: "skipped",
4875
- detail: `${prepared.reason}: ${prepared.detail}${why}`
5009
+ detail: `${prepared.reason}: ${prepared.detail}${why}`,
5010
+ sync: prepared.reason
4876
5011
  };
4877
5012
  }
5013
+ base.sync = prepared.strategy;
5014
+ inFlightSync = prepared.strategy;
5015
+ if (prepared.strategy !== "fast-forward" && prepared.strategy !== "tracking-branch") {
5016
+ progress(
5017
+ ` ${prepared.branch} had diverged from origin; synchronized by ${prepared.strategy}.
5018
+ `
5019
+ );
5020
+ }
4878
5021
  claimIssue(item, settings);
4879
5022
  if (item.turn === "pr-work" && item.pr !== null && item.prNeedsAssignment) {
4880
5023
  claimPr(item.pr.number, settings.participants.agentUser);
@@ -5077,7 +5220,8 @@ function toTickLogItem(report) {
5077
5220
  ranExecutor: report.ranExecutor ?? false,
5078
5221
  executor: report.execution?.executor,
5079
5222
  model: report.execution?.model,
5080
- effort: report.execution?.effort
5223
+ effort: report.execution?.effort,
5224
+ sync: report.sync
5081
5225
  };
5082
5226
  }
5083
5227
  function logTick(reports, exitCode, startedAt, note) {
@@ -5183,7 +5327,8 @@ ${describePlan(decisions)}`;
5183
5327
  title: itemTitle(item),
5184
5328
  turn: item.turn,
5185
5329
  outcome: "failed",
5186
- detail: err.message
5330
+ detail: err.message,
5331
+ sync: inFlightSync ?? void 0
5187
5332
  };
5188
5333
  }
5189
5334
  if (report.ranExecutor === true) runsUsed++;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "automata-cli",
3
- "version": "0.7.0",
3
+ "version": "0.8.0-develop.346",
4
4
  "description": "Automata CLI tool",
5
5
  "type": "module",
6
6
  "engines": {
@@ -37,14 +37,14 @@
37
37
  },
38
38
  "devDependencies": {
39
39
  "@eslint/js": "^10.0.1",
40
- "@types/node": "^25.5.0",
40
+ "@types/node": "^26.6.1",
41
41
  "@types/react": "^19.3.0",
42
42
  "eslint": "^10.10.0",
43
43
  "ink-testing-library": "^4.0.0",
44
- "prettier": "^3.9.6",
44
+ "prettier": "^3.9.7",
45
45
  "tsup": "^8.5.1",
46
46
  "typescript": "^5.9.3",
47
47
  "typescript-eslint": "^8.70.0",
48
- "vitest": "^5.0.0"
48
+ "vitest": "^5.0.1"
49
49
  }
50
50
  }