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.
- package/dist/index.js +161 -16
- 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 (
|
|
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
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
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 {
|
|
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.
|
|
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": "^
|
|
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.
|
|
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.
|
|
48
|
+
"vitest": "^5.0.1"
|
|
49
49
|
}
|
|
50
50
|
}
|