automata-cli 0.8.0-develop.346 → 0.8.0-develop.358
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/README.md +2 -2
- package/dist/{ConfigWizard-D6NYL2E6.js → ConfigWizard-GULICHML.js} +19 -1
- package/dist/index.js +1287 -119
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -214,12 +214,23 @@ var configSetDoWorkPrompt = new Command("do-work-prompt").description("Set the t
|
|
|
214
214
|
process.stdout.write(`do-work ${turnKind} prompt set.
|
|
215
215
|
`);
|
|
216
216
|
});
|
|
217
|
-
var
|
|
217
|
+
var configSetGitTrunkBranch = new Command("git-trunk-branch").description("Pin the branch `publish-release` releases to, instead of detecting it").argument("<value>", "Branch name (unset = detect it from the remote)").action((value) => {
|
|
218
|
+
const branch = value.trim();
|
|
219
|
+
if (branch.length === 0) {
|
|
220
|
+
process.stderr.write("Error: git-trunk-branch requires a non-empty branch name.\n");
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
const current = readRawConfig();
|
|
224
|
+
writeConfig({ ...current, git: { ...current.git, trunkBranch: branch } });
|
|
225
|
+
process.stdout.write(`git trunk branch set to: ${branch}
|
|
226
|
+
`);
|
|
227
|
+
});
|
|
228
|
+
var configSet = new Command("set").description("Set a configuration value").addCommand(configSetType).addCommand(configSetIssueDiscoveryTechnique).addCommand(configSetIssueDiscoveryValue).addCommand(configSetClaudeSystemPrompt).addCommand(configSetAllowedUsers).addCommand(configSetAgentUser).addCommand(configSetDoWorkBaseBranch).addCommand(configSetDoWorkProtectedBranches).addCommand(configSetDoWorkExecutor).addCommand(configSetDoWorkModel).addCommand(configSetDoWorkEffort).addCommand(configSetDoWorkMaxRuns).addCommand(configSetDoWorkLockStaleMinutes).addCommand(configSetDoWorkPrompt).addCommand(configSetGitTrunkBranch);
|
|
218
229
|
var configCommand = new Command("config").description("Configure automata settings").addCommand(configSet).action(async () => {
|
|
219
230
|
const [{ render }, React, { ConfigWizard }] = await Promise.all([
|
|
220
231
|
import("ink"),
|
|
221
232
|
import("react"),
|
|
222
|
-
import("./ConfigWizard-
|
|
233
|
+
import("./ConfigWizard-GULICHML.js")
|
|
223
234
|
]);
|
|
224
235
|
const { waitUntilExit } = render(React.createElement(ConfigWizard));
|
|
225
236
|
await waitUntilExit();
|
|
@@ -280,6 +291,44 @@ function getPrInfo() {
|
|
|
280
291
|
};
|
|
281
292
|
}
|
|
282
293
|
|
|
294
|
+
// src/git/trunkDetection.ts
|
|
295
|
+
var TRUNK_CANDIDATES = ["main", "master"];
|
|
296
|
+
var TRUNK_BRANCH_CONFIG_KEY = "git.trunkBranch";
|
|
297
|
+
var ORIGIN_HEAD_PREFIX = "refs/remotes/origin/";
|
|
298
|
+
var SYMREF_PREFIX = "ref: refs/heads/";
|
|
299
|
+
function parseOriginHeadRef(stdout) {
|
|
300
|
+
const ref = stdout.trim();
|
|
301
|
+
if (!ref.startsWith(ORIGIN_HEAD_PREFIX)) return null;
|
|
302
|
+
const branch = ref.slice(ORIGIN_HEAD_PREFIX.length);
|
|
303
|
+
return branch.length > 0 ? branch : null;
|
|
304
|
+
}
|
|
305
|
+
function parseLsRemoteSymref(stdout) {
|
|
306
|
+
for (const line of stdout.split("\n")) {
|
|
307
|
+
const trimmed2 = line.trim();
|
|
308
|
+
if (!trimmed2.startsWith(SYMREF_PREFIX)) continue;
|
|
309
|
+
const branch = trimmed2.slice(SYMREF_PREFIX.length).split(/\s+/)[0];
|
|
310
|
+
if (branch.length > 0) return branch;
|
|
311
|
+
}
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
function describeTrunkSource(source, branch) {
|
|
315
|
+
switch (source) {
|
|
316
|
+
case "config":
|
|
317
|
+
return `configured as ${TRUNK_BRANCH_CONFIG_KEY}`;
|
|
318
|
+
case "origin-head":
|
|
319
|
+
return "from origin/HEAD";
|
|
320
|
+
case "ls-remote":
|
|
321
|
+
return "from the remote's advertised HEAD";
|
|
322
|
+
case "probe":
|
|
323
|
+
return `probed as origin/${branch}`;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function unresolvedTrunkMessage(attempted) {
|
|
327
|
+
return `Could not determine the trunk branch of 'origin'.
|
|
328
|
+
Tried: ${attempted.join(", ")}.
|
|
329
|
+
Set it explicitly with: automata config set git-trunk-branch <branch>`;
|
|
330
|
+
}
|
|
331
|
+
|
|
283
332
|
// src/git/gitService.ts
|
|
284
333
|
function run2(cmd, args) {
|
|
285
334
|
const result = spawnSync2(cmd, args, { encoding: "utf8" });
|
|
@@ -963,7 +1012,63 @@ function resolveCurrentBranchComments() {
|
|
|
963
1012
|
return { ok: true, branch, comments: raw };
|
|
964
1013
|
}
|
|
965
1014
|
var SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)$/;
|
|
966
|
-
function
|
|
1015
|
+
function remoteBranchExists(branch) {
|
|
1016
|
+
return !isUpstreamGone(branch);
|
|
1017
|
+
}
|
|
1018
|
+
function resolveTrunkBranch() {
|
|
1019
|
+
const configured = readRawConfig().git?.trunkBranch?.trim();
|
|
1020
|
+
if (configured) {
|
|
1021
|
+
return { ok: true, branch: configured, source: "config" };
|
|
1022
|
+
}
|
|
1023
|
+
const attempted = [];
|
|
1024
|
+
attempted.push("origin/HEAD");
|
|
1025
|
+
const head = run2("git", ["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"]);
|
|
1026
|
+
if (head.status === 0) {
|
|
1027
|
+
const branch = parseOriginHeadRef(head.stdout);
|
|
1028
|
+
if (branch !== null) return { ok: true, branch, source: "origin-head" };
|
|
1029
|
+
}
|
|
1030
|
+
attempted.push("git ls-remote --symref origin HEAD");
|
|
1031
|
+
const symref = run2("git", ["ls-remote", "--symref", "origin", "HEAD"]);
|
|
1032
|
+
if (symref.status === 0) {
|
|
1033
|
+
const branch = parseLsRemoteSymref(symref.stdout);
|
|
1034
|
+
if (branch !== null) return { ok: true, branch, source: "ls-remote" };
|
|
1035
|
+
}
|
|
1036
|
+
for (const candidate of TRUNK_CANDIDATES) {
|
|
1037
|
+
attempted.push(`origin/${candidate}`);
|
|
1038
|
+
if (remoteBranchExists(candidate)) {
|
|
1039
|
+
return { ok: true, branch: candidate, source: "probe" };
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
return { ok: false, attempted };
|
|
1043
|
+
}
|
|
1044
|
+
function fetchTrunkAndTags(trunk) {
|
|
1045
|
+
const { status, stderr } = run2("git", [
|
|
1046
|
+
"fetch",
|
|
1047
|
+
"--tags",
|
|
1048
|
+
"origin",
|
|
1049
|
+
`+refs/heads/${trunk}:refs/remotes/origin/${trunk}`
|
|
1050
|
+
]);
|
|
1051
|
+
if (status !== 0) {
|
|
1052
|
+
return { ok: false, message: stderr.trim() || `git fetch --tags origin ${trunk} failed.` };
|
|
1053
|
+
}
|
|
1054
|
+
return { ok: true };
|
|
1055
|
+
}
|
|
1056
|
+
function localBranchExists(branch) {
|
|
1057
|
+
const { status } = run2("git", ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`]);
|
|
1058
|
+
return status === 0;
|
|
1059
|
+
}
|
|
1060
|
+
function trunkBehindCount(trunk) {
|
|
1061
|
+
if (!localBranchExists(trunk)) return 0;
|
|
1062
|
+
const { stdout, status } = run2("git", [
|
|
1063
|
+
"rev-list",
|
|
1064
|
+
"--count",
|
|
1065
|
+
`refs/heads/${trunk}..refs/remotes/origin/${trunk}`
|
|
1066
|
+
]);
|
|
1067
|
+
if (status !== 0) return 0;
|
|
1068
|
+
const count = Number.parseInt(stdout.trim(), 10);
|
|
1069
|
+
return Number.isNaN(count) ? 0 : count;
|
|
1070
|
+
}
|
|
1071
|
+
function getLatestTagOnTrunk(ref) {
|
|
967
1072
|
const { stdout, status } = run2("git", [
|
|
968
1073
|
"describe",
|
|
969
1074
|
"--tags",
|
|
@@ -972,7 +1077,7 @@ function getLatestTagOnMaster() {
|
|
|
972
1077
|
"[0-9]*.[0-9]*.[0-9]*",
|
|
973
1078
|
"--match",
|
|
974
1079
|
"v[0-9]*.[0-9]*.[0-9]*",
|
|
975
|
-
|
|
1080
|
+
ref
|
|
976
1081
|
]);
|
|
977
1082
|
if (status !== 0) return null;
|
|
978
1083
|
const tag = stdout.trim();
|
|
@@ -993,17 +1098,45 @@ ${stderr.trim()}`);
|
|
|
993
1098
|
}
|
|
994
1099
|
return stdout.trim().length > 0;
|
|
995
1100
|
}
|
|
996
|
-
function
|
|
1101
|
+
function checkReleasePreconditions() {
|
|
1102
|
+
let branch;
|
|
1103
|
+
try {
|
|
1104
|
+
branch = getCurrentBranch();
|
|
1105
|
+
} catch (err) {
|
|
1106
|
+
return { ok: false, message: err.message };
|
|
1107
|
+
}
|
|
1108
|
+
if (branch !== "develop") {
|
|
1109
|
+
return {
|
|
1110
|
+
ok: false,
|
|
1111
|
+
message: `publish-release must be run from the 'develop' branch (currently on '${branch}').`
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
if (hasUncommittedChanges()) {
|
|
1115
|
+
return {
|
|
1116
|
+
ok: false,
|
|
1117
|
+
message: "You have uncommitted changes. Commit or stash them before publishing a release."
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
return { ok: true };
|
|
1121
|
+
}
|
|
1122
|
+
function publishRelease(version2, dryRun, trunk) {
|
|
997
1123
|
const releaseBranch = `release/${version2}`;
|
|
1124
|
+
const checkoutTrunk = localBranchExists(trunk) ? { args: ["checkout", trunk], desc: `git checkout ${trunk}` } : {
|
|
1125
|
+
args: ["checkout", "-b", trunk, `origin/${trunk}`],
|
|
1126
|
+
desc: `git checkout -b ${trunk} origin/${trunk}`
|
|
1127
|
+
};
|
|
998
1128
|
const steps = [
|
|
999
1129
|
{ args: ["checkout", "-b", releaseBranch], desc: `git checkout -b ${releaseBranch}` },
|
|
1000
|
-
|
|
1130
|
+
checkoutTrunk,
|
|
1001
1131
|
{ args: ["merge", "--no-ff", releaseBranch], desc: `git merge --no-ff ${releaseBranch}` },
|
|
1002
1132
|
{ args: ["tag", version2], desc: `git tag ${version2}` },
|
|
1003
1133
|
{ args: ["checkout", "develop"], desc: `git checkout develop` },
|
|
1004
1134
|
{ args: ["merge", "--no-ff", releaseBranch], desc: `git merge --no-ff ${releaseBranch}` },
|
|
1005
1135
|
{ args: ["branch", "-d", releaseBranch], desc: `git branch -d ${releaseBranch}` },
|
|
1006
|
-
{
|
|
1136
|
+
{
|
|
1137
|
+
args: ["push", "origin", "develop", trunk, version2],
|
|
1138
|
+
desc: `git push origin develop ${trunk} ${version2}`
|
|
1139
|
+
}
|
|
1007
1140
|
];
|
|
1008
1141
|
for (const step of steps) {
|
|
1009
1142
|
if (dryRun) {
|
|
@@ -1019,6 +1152,29 @@ ${stderr.trim()}`);
|
|
|
1019
1152
|
}
|
|
1020
1153
|
}
|
|
1021
1154
|
|
|
1155
|
+
// src/git/releaseVersion.ts
|
|
1156
|
+
var SEMVER_ARG_RE = /^\d+\.\d+\.\d+$/;
|
|
1157
|
+
function resolveReleaseVersion(requested, trunkRef, latestTag) {
|
|
1158
|
+
if (requested !== void 0) {
|
|
1159
|
+
if (!SEMVER_ARG_RE.test(requested)) {
|
|
1160
|
+
return {
|
|
1161
|
+
ok: false,
|
|
1162
|
+
message: `Version '${requested}' is not valid semver. Use X.Y.Z format (e.g. 1.2.0).`
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
return { ok: true, version: requested, notice: null };
|
|
1166
|
+
}
|
|
1167
|
+
const latest = latestTag();
|
|
1168
|
+
if (latest === null) {
|
|
1169
|
+
return {
|
|
1170
|
+
ok: false,
|
|
1171
|
+
message: `No semver tag found on ${trunkRef}. Pass a version explicitly: automata git publish-release <X.Y.Z>`
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
const version2 = bumpMinorVersion(latest);
|
|
1175
|
+
return { ok: true, version: version2, notice: `Auto-detected version: ${latest} \u2192 ${version2}` };
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1022
1178
|
// src/commands/git.ts
|
|
1023
1179
|
var FAIL_CONCLUSIONS = /* @__PURE__ */ new Set(["FAILURE", "TIMED_OUT", "ACTION_REQUIRED", "CANCELLED"]);
|
|
1024
1180
|
var SKIP_CONCLUSIONS = /* @__PURE__ */ new Set(["SKIPPED", "NEUTRAL"]);
|
|
@@ -1383,59 +1539,58 @@ var finishFeatureCmd = new Command2("finish-feature").description("Clean up a me
|
|
|
1383
1539
|
process.exit(1);
|
|
1384
1540
|
}
|
|
1385
1541
|
});
|
|
1386
|
-
var
|
|
1387
|
-
var publishReleaseCmd = new Command2("publish-release").description("Execute the full GitFlow release sequence and push to origin").argument("[version]", "Release version in X.Y.Z format (auto-detected from master tag if omitted)").option("--dry-run", "Print git commands without executing them").addHelpText(
|
|
1542
|
+
var publishReleaseCmd = new Command2("publish-release").description("Execute the full GitFlow release sequence and push to origin").argument("[version]", "Release version in X.Y.Z format (auto-detected from the trunk tag if omitted)").option("--dry-run", "Print git commands without executing them").addHelpText(
|
|
1388
1543
|
"after",
|
|
1389
1544
|
`
|
|
1390
1545
|
Release sequence:
|
|
1391
1546
|
1. git checkout -b release/<version>
|
|
1392
|
-
2. git checkout
|
|
1547
|
+
2. git checkout <trunk> && git merge --no-ff release/<version>
|
|
1393
1548
|
3. git tag <version>
|
|
1394
1549
|
4. git checkout develop && git merge --no-ff release/<version>
|
|
1395
1550
|
5. git branch -d release/<version>
|
|
1396
|
-
6. git push origin develop
|
|
1551
|
+
6. git push origin develop <trunk> <version>
|
|
1552
|
+
|
|
1553
|
+
<trunk> is resolved from origin \u2014 git.trunkBranch in .automata/config.json if
|
|
1554
|
+
set, else origin/HEAD, the remote's advertised HEAD, or a probe of main/master.
|
|
1555
|
+
Tags are fetched from origin first, in --dry-run too, so the version a dry run
|
|
1556
|
+
prints is the one a real run would use.
|
|
1397
1557
|
|
|
1398
|
-
When [version] is omitted the latest semver tag on
|
|
1399
|
-
minor segment is incremented (e.g. 1.2.0 \u2192 1.3.0).`
|
|
1558
|
+
When [version] is omitted the latest semver tag on origin/<trunk> is detected
|
|
1559
|
+
and the minor segment is incremented (e.g. 1.2.0 \u2192 1.3.0).`
|
|
1400
1560
|
).action((version2, options) => {
|
|
1401
1561
|
const dryRun = options.dryRun ?? false;
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
} catch (err) {
|
|
1406
|
-
process.stderr.write(`Error: ${err.message}
|
|
1562
|
+
const preconditions = checkReleasePreconditions();
|
|
1563
|
+
if (!preconditions.ok) {
|
|
1564
|
+
process.stderr.write(`Error: ${preconditions.message}
|
|
1407
1565
|
`);
|
|
1408
1566
|
process.exit(1);
|
|
1409
1567
|
}
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
`
|
|
1414
|
-
);
|
|
1568
|
+
const trunk = resolveTrunkBranch();
|
|
1569
|
+
if (!trunk.ok) {
|
|
1570
|
+
process.stderr.write(`Error: ${unresolvedTrunkMessage(trunk.attempted)}
|
|
1571
|
+
`);
|
|
1415
1572
|
process.exit(1);
|
|
1416
1573
|
}
|
|
1417
|
-
|
|
1418
|
-
|
|
1574
|
+
const trunkBranch = trunk.branch;
|
|
1575
|
+
const trunkRef = `origin/${trunkBranch}`;
|
|
1576
|
+
process.stdout.write(`Trunk branch: ${trunkBranch} (${describeTrunkSource(trunk.source, trunkBranch)})
|
|
1577
|
+
`);
|
|
1578
|
+
const fetched = fetchTrunkAndTags(trunkBranch);
|
|
1579
|
+
if (!fetched.ok) {
|
|
1580
|
+
process.stderr.write(`Error: Failed to fetch ${trunkBranch} and tags from origin.
|
|
1581
|
+
${fetched.message}
|
|
1582
|
+
`);
|
|
1419
1583
|
process.exit(1);
|
|
1420
1584
|
}
|
|
1421
|
-
|
|
1422
|
-
if (
|
|
1423
|
-
|
|
1424
|
-
process.stderr.write(`Error: Version '${version2}' is not valid semver. Use X.Y.Z format (e.g. 1.2.0).
|
|
1585
|
+
const versionResult = resolveReleaseVersion(version2, trunkRef, () => getLatestTagOnTrunk(trunkRef));
|
|
1586
|
+
if (!versionResult.ok) {
|
|
1587
|
+
process.stderr.write(`Error: ${versionResult.message}
|
|
1425
1588
|
`);
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
if (latest === null) {
|
|
1432
|
-
process.stderr.write(
|
|
1433
|
-
"Error: No semver tag found on master. Pass a version explicitly: automata git publish-release <X.Y.Z>\n"
|
|
1434
|
-
);
|
|
1435
|
-
process.exit(1);
|
|
1436
|
-
}
|
|
1437
|
-
resolvedVersion = bumpMinorVersion(latest);
|
|
1438
|
-
process.stdout.write(`Auto-detected version: ${latest} \u2192 ${resolvedVersion}
|
|
1589
|
+
process.exit(1);
|
|
1590
|
+
}
|
|
1591
|
+
const resolvedVersion = versionResult.version;
|
|
1592
|
+
if (versionResult.notice !== null) {
|
|
1593
|
+
process.stdout.write(`${versionResult.notice}
|
|
1439
1594
|
`);
|
|
1440
1595
|
}
|
|
1441
1596
|
if (tagExists(resolvedVersion)) {
|
|
@@ -1443,6 +1598,14 @@ minor segment is incremented (e.g. 1.2.0 \u2192 1.3.0).`
|
|
|
1443
1598
|
`);
|
|
1444
1599
|
process.exit(1);
|
|
1445
1600
|
}
|
|
1601
|
+
const behind = trunkBehindCount(trunkBranch);
|
|
1602
|
+
if (behind > 0) {
|
|
1603
|
+
process.stderr.write(
|
|
1604
|
+
`Error: Local branch '${trunkBranch}' is ${String(behind)} commit(s) behind ${trunkRef}. Update it (git checkout ${trunkBranch} && git merge --ff-only ${trunkRef}) or delete it, then re-run.
|
|
1605
|
+
`
|
|
1606
|
+
);
|
|
1607
|
+
process.exit(1);
|
|
1608
|
+
}
|
|
1446
1609
|
if (dryRun) {
|
|
1447
1610
|
process.stdout.write(`Dry-run: release ${resolvedVersion}
|
|
1448
1611
|
`);
|
|
@@ -1451,7 +1614,7 @@ minor segment is incremented (e.g. 1.2.0 \u2192 1.3.0).`
|
|
|
1451
1614
|
`);
|
|
1452
1615
|
}
|
|
1453
1616
|
try {
|
|
1454
|
-
publishRelease(resolvedVersion, dryRun);
|
|
1617
|
+
publishRelease(resolvedVersion, dryRun, trunkBranch);
|
|
1455
1618
|
} catch (err) {
|
|
1456
1619
|
process.stderr.write(`Error: ${err.message}
|
|
1457
1620
|
`);
|
|
@@ -1668,10 +1831,16 @@ function getRepoSlug() {
|
|
|
1668
1831
|
return { owner: match[1], repo: match[2] };
|
|
1669
1832
|
}
|
|
1670
1833
|
function getAuthenticatedLogin() {
|
|
1671
|
-
const
|
|
1672
|
-
|
|
1834
|
+
const identity = getAuthenticatedIdentity();
|
|
1835
|
+
return identity.kind === "login" ? identity.login : null;
|
|
1836
|
+
}
|
|
1837
|
+
function getAuthenticatedIdentity() {
|
|
1838
|
+
const { stdout, stderr, status } = run4("gh", ["api", "user", "--jq", ".login"]);
|
|
1839
|
+
if (status !== 0) {
|
|
1840
|
+
return { kind: "unavailable", detail: stderr.trim() || `gh api user exited ${String(status)}` };
|
|
1841
|
+
}
|
|
1673
1842
|
const login2 = stdout.trim();
|
|
1674
|
-
return login2.length > 0 ? login2 :
|
|
1843
|
+
return login2.length > 0 ? { kind: "login", login: login2 } : { kind: "no-user" };
|
|
1675
1844
|
}
|
|
1676
1845
|
function listCandidateIssues(technique, value, limit) {
|
|
1677
1846
|
const args = [
|
|
@@ -2071,10 +2240,27 @@ function parseCreatedPrUrl(stdout, head) {
|
|
|
2071
2240
|
// src/claude/claudeService.ts
|
|
2072
2241
|
import { spawn, spawnSync as spawnSync5 } from "child_process";
|
|
2073
2242
|
import { createInterface } from "readline";
|
|
2074
|
-
import { existsSync as existsSync2 } from "fs";
|
|
2075
|
-
import { delimiter, join } from "path";
|
|
2076
2243
|
|
|
2077
2244
|
// src/cli/spawnUtils.ts
|
|
2245
|
+
import { accessSync, constants, statSync } from "fs";
|
|
2246
|
+
import { delimiter, join } from "path";
|
|
2247
|
+
function resolveCommand(name) {
|
|
2248
|
+
const pathDirs = (process.env["PATH"] ?? "").split(delimiter);
|
|
2249
|
+
for (const dir of pathDirs) {
|
|
2250
|
+
const candidate = join(dir, name);
|
|
2251
|
+
if (isLaunchable(candidate)) return candidate;
|
|
2252
|
+
}
|
|
2253
|
+
return name;
|
|
2254
|
+
}
|
|
2255
|
+
function isLaunchable(candidate) {
|
|
2256
|
+
try {
|
|
2257
|
+
if (!statSync(candidate).isFile()) return false;
|
|
2258
|
+
accessSync(candidate, constants.X_OK);
|
|
2259
|
+
return true;
|
|
2260
|
+
} catch {
|
|
2261
|
+
return false;
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2078
2264
|
var SHELL_SAFE = /^[A-Za-z0-9_@%+=:,./-]+$/;
|
|
2079
2265
|
var ESCAPED_QUOTE = String.raw`'\''`;
|
|
2080
2266
|
function shellQuote(arg) {
|
|
@@ -2098,8 +2284,10 @@ function handleSpawnError(error, toolName) {
|
|
|
2098
2284
|
}
|
|
2099
2285
|
function handleExitCode(status, toolName) {
|
|
2100
2286
|
if (status === null) {
|
|
2101
|
-
process.stderr.write(
|
|
2102
|
-
`)
|
|
2287
|
+
process.stderr.write(
|
|
2288
|
+
`Error: ${toolName} terminated abnormally (exit code is null, likely due to a signal).
|
|
2289
|
+
`
|
|
2290
|
+
);
|
|
2103
2291
|
process.exit(1);
|
|
2104
2292
|
}
|
|
2105
2293
|
if (status !== 0) {
|
|
@@ -2108,14 +2296,20 @@ function handleExitCode(status, toolName) {
|
|
|
2108
2296
|
process.exit(status);
|
|
2109
2297
|
}
|
|
2110
2298
|
}
|
|
2111
|
-
function
|
|
2112
|
-
if (value === void 0) return void 0;
|
|
2299
|
+
function normalizeEffortOption(value) {
|
|
2300
|
+
if (value === void 0) return { ok: true, value: void 0 };
|
|
2113
2301
|
const trimmed2 = value.trim();
|
|
2114
|
-
if (trimmed2.length === 0) {
|
|
2115
|
-
|
|
2302
|
+
if (trimmed2.length === 0) return { ok: false, error: "--effort must be a non-empty level." };
|
|
2303
|
+
return { ok: true, value: trimmed2 };
|
|
2304
|
+
}
|
|
2305
|
+
function resolveEffortOption(value) {
|
|
2306
|
+
const result = normalizeEffortOption(value);
|
|
2307
|
+
if (!result.ok) {
|
|
2308
|
+
process.stderr.write(`Error: ${result.error}
|
|
2309
|
+
`);
|
|
2116
2310
|
process.exit(1);
|
|
2117
2311
|
}
|
|
2118
|
-
return
|
|
2312
|
+
return result.value;
|
|
2119
2313
|
}
|
|
2120
2314
|
|
|
2121
2315
|
// src/cli/childRegistry.ts
|
|
@@ -2161,14 +2355,6 @@ async function terminateTrackedChildren(timeoutMs = 1e4, killGraceMs = 5e3) {
|
|
|
2161
2355
|
}
|
|
2162
2356
|
|
|
2163
2357
|
// src/claude/claudeService.ts
|
|
2164
|
-
function resolveCommand(name) {
|
|
2165
|
-
const pathDirs = (process.env["PATH"] ?? "").split(delimiter);
|
|
2166
|
-
for (const dir of pathDirs) {
|
|
2167
|
-
const candidate = join(dir, name);
|
|
2168
|
-
if (existsSync2(candidate)) return candidate;
|
|
2169
|
-
}
|
|
2170
|
-
return name;
|
|
2171
|
-
}
|
|
2172
2358
|
function buildClaudeArgs(prompt, options = {}) {
|
|
2173
2359
|
const args = [];
|
|
2174
2360
|
if (options.yolo) args.push("--dangerously-skip-permissions");
|
|
@@ -2970,6 +3156,16 @@ var executePromptCommand = new Command5("execute-prompt").description("Execute a
|
|
|
2970
3156
|
// src/commands/doWork.ts
|
|
2971
3157
|
import { Command as Command6 } from "commander";
|
|
2972
3158
|
|
|
3159
|
+
// src/github/identity.ts
|
|
3160
|
+
function identityProblemFor(login2, agentUser, allowedUsers) {
|
|
3161
|
+
if (login2 === null) return null;
|
|
3162
|
+
if (login2.toLowerCase() === agentUser.toLowerCase()) return null;
|
|
3163
|
+
if (allowedUsers.some((user) => user.toLowerCase() === login2.toLowerCase())) {
|
|
3164
|
+
return `\`gh\` is authenticated as "${login2}", which is listed in allowedUsers. Everything do-work posts would be attributed to an account that is allowed to instruct the agent, so its own marker comment would look like a new instruction and each tick would answer the previous tick forever. Authenticate \`gh\` as the agent account (${agentUser}) in this environment, or correct \`agentUser\`.`;
|
|
3165
|
+
}
|
|
3166
|
+
return `\`gh\` is authenticated as "${login2}" but agentUser is "${agentUser}". Comments posted under that identity are neither the agent's nor an authorized user's, so they are filtered out of the conversation: the answer boundary would never advance and the same message would start a run on every tick. Authenticate \`gh\` as the agent account (${agentUser}) in this environment, or correct \`agentUser\`.`;
|
|
3167
|
+
}
|
|
3168
|
+
|
|
2973
3169
|
// src/github/workDetection.ts
|
|
2974
3170
|
var NO_ISSUE_MESSAGES = {
|
|
2975
3171
|
messages: [],
|
|
@@ -3433,7 +3629,7 @@ function analyseAnswer(messages, p, marker, watermark) {
|
|
|
3433
3629
|
}
|
|
3434
3630
|
|
|
3435
3631
|
// src/run/runLock.ts
|
|
3436
|
-
import { writeFileSync, readFileSync as readFileSync3, unlinkSync, mkdirSync, renameSync, linkSync, statSync } from "fs";
|
|
3632
|
+
import { writeFileSync, readFileSync as readFileSync3, unlinkSync, mkdirSync, renameSync, linkSync, statSync as statSync2 } from "fs";
|
|
3437
3633
|
import { randomUUID } from "crypto";
|
|
3438
3634
|
import { hostname } from "os";
|
|
3439
3635
|
import { join as join2 } from "path";
|
|
@@ -3461,6 +3657,14 @@ function isAlive(pid) {
|
|
|
3461
3657
|
return err.code === "EPERM";
|
|
3462
3658
|
}
|
|
3463
3659
|
}
|
|
3660
|
+
function readOwnerError(path) {
|
|
3661
|
+
try {
|
|
3662
|
+
readFileSync3(path, "utf8");
|
|
3663
|
+
return null;
|
|
3664
|
+
} catch (err) {
|
|
3665
|
+
return err.message;
|
|
3666
|
+
}
|
|
3667
|
+
}
|
|
3464
3668
|
function readOwner(path) {
|
|
3465
3669
|
try {
|
|
3466
3670
|
const parsed = JSON.parse(readFileSync3(path, "utf8"));
|
|
@@ -3648,7 +3852,7 @@ function claimIsAbandoned(claimPath) {
|
|
|
3648
3852
|
}
|
|
3649
3853
|
if (Number.isNaN(at)) {
|
|
3650
3854
|
try {
|
|
3651
|
-
at =
|
|
3855
|
+
at = statSync2(claimPath).mtimeMs;
|
|
3652
3856
|
} catch {
|
|
3653
3857
|
return true;
|
|
3654
3858
|
}
|
|
@@ -3685,6 +3889,28 @@ function reclaim(path, command, token, expected) {
|
|
|
3685
3889
|
}
|
|
3686
3890
|
}
|
|
3687
3891
|
}
|
|
3892
|
+
function heldForMs(owner, now) {
|
|
3893
|
+
const startedAt = Date.parse(owner.startedAt);
|
|
3894
|
+
return Number.isNaN(startedAt) ? null : now - startedAt;
|
|
3895
|
+
}
|
|
3896
|
+
function inspectRunLock(staleMinutes, now = Date.now()) {
|
|
3897
|
+
const path = lockPath();
|
|
3898
|
+
try {
|
|
3899
|
+
statSync2(path);
|
|
3900
|
+
} catch (err) {
|
|
3901
|
+
if (err.code === "ENOENT") return { kind: "free" };
|
|
3902
|
+
return { kind: "unreadable", detail: err.message };
|
|
3903
|
+
}
|
|
3904
|
+
const readError = readOwnerError(path);
|
|
3905
|
+
if (readError !== null) return { kind: "unreadable", detail: readError };
|
|
3906
|
+
const owner = readOwner(path);
|
|
3907
|
+
if (isStale(owner, staleMinutes)) {
|
|
3908
|
+
return { kind: "stale", owner, heldForMs: owner === null ? null : heldForMs(owner, now) };
|
|
3909
|
+
}
|
|
3910
|
+
const held = owner;
|
|
3911
|
+
const kind = heldTooLong(held, staleMinutes) ? "suspect" : "held";
|
|
3912
|
+
return { kind, owner: held, heldForMs: heldForMs(held, now) };
|
|
3913
|
+
}
|
|
3688
3914
|
|
|
3689
3915
|
// src/git/workspaceService.ts
|
|
3690
3916
|
function dirtyTree() {
|
|
@@ -3718,11 +3944,11 @@ function resetToForcePushedRemote(headRefName, previousRemoteSha) {
|
|
|
3718
3944
|
}
|
|
3719
3945
|
function rebaseOntoAlreadyAppliedRemote(headRefName) {
|
|
3720
3946
|
const upstream = `refs/remotes/origin/${headRefName}`;
|
|
3721
|
-
const
|
|
3722
|
-
if (
|
|
3723
|
-
if (
|
|
3724
|
-
if (
|
|
3725
|
-
if (
|
|
3947
|
+
const divergence2 = describeDivergence(upstream, `refs/heads/${headRefName}`);
|
|
3948
|
+
if (divergence2 === null) return null;
|
|
3949
|
+
if (divergence2.merges > 0) return null;
|
|
3950
|
+
if (divergence2.commits.length === 0) return null;
|
|
3951
|
+
if (divergence2.commits.some((commit) => !commit.alreadyUpstream)) return null;
|
|
3726
3952
|
if (isRebaseInProgress()) {
|
|
3727
3953
|
return {
|
|
3728
3954
|
ok: false,
|
|
@@ -3759,18 +3985,18 @@ function rebaseOntoAlreadyAppliedRemote(headRefName) {
|
|
|
3759
3985
|
return { ok: true, branch: headRefName, strategy: "rebase" };
|
|
3760
3986
|
}
|
|
3761
3987
|
function divergenceRefusal(headRefName, pullError) {
|
|
3762
|
-
const
|
|
3988
|
+
const divergence2 = describeDivergence(
|
|
3763
3989
|
`refs/remotes/origin/${headRefName}`,
|
|
3764
3990
|
`refs/heads/${headRefName}`
|
|
3765
3991
|
);
|
|
3766
|
-
if (
|
|
3992
|
+
if (divergence2 === null) {
|
|
3767
3993
|
return {
|
|
3768
3994
|
ok: false,
|
|
3769
3995
|
reason: "pull-failed",
|
|
3770
3996
|
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
3997
|
};
|
|
3772
3998
|
}
|
|
3773
|
-
const unpushedCount =
|
|
3999
|
+
const unpushedCount = divergence2.commits.filter((c) => !c.alreadyUpstream).length + divergence2.merges;
|
|
3774
4000
|
if (unpushedCount === 0) {
|
|
3775
4001
|
return {
|
|
3776
4002
|
ok: false,
|
|
@@ -4156,9 +4382,9 @@ function describeRescueRemains(rescue) {
|
|
|
4156
4382
|
|
|
4157
4383
|
// src/run/operationLog.ts
|
|
4158
4384
|
import {
|
|
4159
|
-
accessSync,
|
|
4385
|
+
accessSync as accessSync2,
|
|
4160
4386
|
appendFileSync,
|
|
4161
|
-
constants,
|
|
4387
|
+
constants as constants2,
|
|
4162
4388
|
readFileSync as readFileSync4,
|
|
4163
4389
|
renameSync as renameSync2,
|
|
4164
4390
|
unlinkSync as unlinkSync2,
|
|
@@ -4305,7 +4531,7 @@ function appendWithRetention(dir, file, content, retain) {
|
|
|
4305
4531
|
}
|
|
4306
4532
|
function recordTick(tick, dir = operationLogDirectory()) {
|
|
4307
4533
|
try {
|
|
4308
|
-
|
|
4534
|
+
accessSync2(dir, constants2.W_OK);
|
|
4309
4535
|
} catch {
|
|
4310
4536
|
return;
|
|
4311
4537
|
}
|
|
@@ -4331,6 +4557,696 @@ function recordTick(tick, dir = operationLogDirectory()) {
|
|
|
4331
4557
|
} catch {
|
|
4332
4558
|
}
|
|
4333
4559
|
}
|
|
4560
|
+
function isOutcome(value) {
|
|
4561
|
+
return OUTCOMES.includes(value);
|
|
4562
|
+
}
|
|
4563
|
+
function readLogFile(path) {
|
|
4564
|
+
try {
|
|
4565
|
+
return { content: readFileSync4(path, "utf8"), error: null };
|
|
4566
|
+
} catch (err) {
|
|
4567
|
+
if (err.code === "ENOENT") return { content: null, error: null };
|
|
4568
|
+
return { content: null, error: err.message };
|
|
4569
|
+
}
|
|
4570
|
+
}
|
|
4571
|
+
function emptyResult(path, error, filtered) {
|
|
4572
|
+
return { entries: [], present: false, error, skipped: 0, otherRepos: 0, filtered, path };
|
|
4573
|
+
}
|
|
4574
|
+
function isFiltered(want) {
|
|
4575
|
+
return want !== void 0 && want !== null;
|
|
4576
|
+
}
|
|
4577
|
+
function matchesRepo(entryRepo, want) {
|
|
4578
|
+
if (want === void 0 || want === null) return true;
|
|
4579
|
+
return entryRepo === null || entryRepo === want;
|
|
4580
|
+
}
|
|
4581
|
+
function readInt(value) {
|
|
4582
|
+
if (value === void 0) return 0;
|
|
4583
|
+
if (!/^-?\d+$/.test(value)) return null;
|
|
4584
|
+
const parsed = Number(value);
|
|
4585
|
+
return Number.isSafeInteger(parsed) ? parsed : null;
|
|
4586
|
+
}
|
|
4587
|
+
function readDurationSeconds(value) {
|
|
4588
|
+
if (value === void 0) return 0;
|
|
4589
|
+
if (!/^\d+(?:\.\d+)?s?$/.test(value)) return null;
|
|
4590
|
+
const parsed = Number.parseFloat(value);
|
|
4591
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
4592
|
+
}
|
|
4593
|
+
function parseExecutionLine(line) {
|
|
4594
|
+
const fields = line.trim().split(" ").filter((field) => field.length > 0);
|
|
4595
|
+
if (fields.length < 2) return null;
|
|
4596
|
+
const timestamp = new Date(fields[0]);
|
|
4597
|
+
if (Number.isNaN(timestamp.getTime())) return null;
|
|
4598
|
+
const pairs = /* @__PURE__ */ new Map();
|
|
4599
|
+
for (const field of fields.slice(2)) {
|
|
4600
|
+
const eq = field.indexOf("=");
|
|
4601
|
+
if (eq <= 0) continue;
|
|
4602
|
+
pairs.set(field.slice(0, eq), field.slice(eq + 1));
|
|
4603
|
+
}
|
|
4604
|
+
const exit = readInt(pairs.get("exit"));
|
|
4605
|
+
if (exit === null) return null;
|
|
4606
|
+
const counts = {};
|
|
4607
|
+
for (const outcome of OUTCOMES) {
|
|
4608
|
+
const count = readInt(pairs.get(outcome));
|
|
4609
|
+
if (count === null) return null;
|
|
4610
|
+
counts[outcome] = count;
|
|
4611
|
+
}
|
|
4612
|
+
const items = readInt(pairs.get("items"));
|
|
4613
|
+
const runs = readInt(pairs.get("runs"));
|
|
4614
|
+
const durationSeconds = readDurationSeconds(pairs.get("dur"));
|
|
4615
|
+
if (items === null || runs === null || durationSeconds === null) return null;
|
|
4616
|
+
const repo = pairs.get("repo");
|
|
4617
|
+
const note = pairs.get("note");
|
|
4618
|
+
return {
|
|
4619
|
+
timestamp,
|
|
4620
|
+
command: fields[1],
|
|
4621
|
+
repo: repo === void 0 || repo === "-" ? null : repo,
|
|
4622
|
+
items,
|
|
4623
|
+
counts,
|
|
4624
|
+
runs,
|
|
4625
|
+
exitCode: exit,
|
|
4626
|
+
durationSeconds,
|
|
4627
|
+
note: note === void 0 || note.length === 0 ? null : note
|
|
4628
|
+
};
|
|
4629
|
+
}
|
|
4630
|
+
function readExecutionTicks(options = {}) {
|
|
4631
|
+
const path = join3(options.dir ?? operationLogDirectory(), EXECUTION_LOG_FILE);
|
|
4632
|
+
const { content, error } = readLogFile(path);
|
|
4633
|
+
if (content === null) return emptyResult(path, error, isFiltered(options.repo));
|
|
4634
|
+
const entries = [];
|
|
4635
|
+
let skipped = 0;
|
|
4636
|
+
let otherRepos = 0;
|
|
4637
|
+
for (const line of content.split("\n")) {
|
|
4638
|
+
if (line.trim().length === 0) continue;
|
|
4639
|
+
const tick = parseExecutionLine(line);
|
|
4640
|
+
if (tick === null) {
|
|
4641
|
+
skipped++;
|
|
4642
|
+
continue;
|
|
4643
|
+
}
|
|
4644
|
+
if (!matchesRepo(tick.repo, options.repo)) {
|
|
4645
|
+
otherRepos++;
|
|
4646
|
+
continue;
|
|
4647
|
+
}
|
|
4648
|
+
entries.push(tick);
|
|
4649
|
+
}
|
|
4650
|
+
entries.reverse();
|
|
4651
|
+
return {
|
|
4652
|
+
entries: options.limit === void 0 ? entries : entries.slice(0, options.limit),
|
|
4653
|
+
present: true,
|
|
4654
|
+
error: null,
|
|
4655
|
+
skipped,
|
|
4656
|
+
otherRepos,
|
|
4657
|
+
filtered: isFiltered(options.repo),
|
|
4658
|
+
path
|
|
4659
|
+
};
|
|
4660
|
+
}
|
|
4661
|
+
var WORK_ITEM_LINE = /^(#\S+|PR #\S+) (\S+) (answered-no-reply|answered|skipped|failed|deferred)(?: \[([^\]]*)\])?(?: sync=(.*?))? — (.*)$/;
|
|
4662
|
+
function parseExecution(bracket) {
|
|
4663
|
+
if (bracket === void 0) return { executor: null, model: null, effort: null };
|
|
4664
|
+
const parts = bracket.split(" ").filter((part) => part.length > 0);
|
|
4665
|
+
const executor = parts[0] ?? null;
|
|
4666
|
+
const find = (key) => {
|
|
4667
|
+
const hit = parts.find((part) => part.startsWith(`${key}=`));
|
|
4668
|
+
return hit === void 0 ? null : hit.slice(key.length + 1);
|
|
4669
|
+
};
|
|
4670
|
+
return { executor, model: find("model"), effort: find("effort") };
|
|
4671
|
+
}
|
|
4672
|
+
function parseWorkHeader(line) {
|
|
4673
|
+
const header = /^=== (\S+) (\S+) ===$/.exec(line);
|
|
4674
|
+
if (header === null) return "not-a-header";
|
|
4675
|
+
const timestamp = new Date(header[1]);
|
|
4676
|
+
if (Number.isNaN(timestamp.getTime())) return null;
|
|
4677
|
+
return { timestamp, repo: header[2] === "-" ? null : header[2], items: [] };
|
|
4678
|
+
}
|
|
4679
|
+
function parseWorkItem(line) {
|
|
4680
|
+
const item = WORK_ITEM_LINE.exec(line);
|
|
4681
|
+
if (item === null) return null;
|
|
4682
|
+
return {
|
|
4683
|
+
subject: item[1],
|
|
4684
|
+
turn: item[2] === "-" ? null : item[2],
|
|
4685
|
+
// The alternation in the pattern admits nothing else.
|
|
4686
|
+
outcome: isOutcome(item[3]) ? item[3] : "skipped",
|
|
4687
|
+
...parseExecution(item[4]),
|
|
4688
|
+
sync: item[5] === void 0 || item[5].length === 0 ? null : item[5],
|
|
4689
|
+
detail: item[6]
|
|
4690
|
+
};
|
|
4691
|
+
}
|
|
4692
|
+
function readWorkRecords(options = {}) {
|
|
4693
|
+
const path = join3(options.dir ?? operationLogDirectory(), WORK_LOG_FILE);
|
|
4694
|
+
const { content, error } = readLogFile(path);
|
|
4695
|
+
if (content === null) return emptyResult(path, error, isFiltered(options.repo));
|
|
4696
|
+
const entries = [];
|
|
4697
|
+
let skipped = 0;
|
|
4698
|
+
let otherRepos = 0;
|
|
4699
|
+
let current = null;
|
|
4700
|
+
const close = () => {
|
|
4701
|
+
if (current === null) return;
|
|
4702
|
+
if (matchesRepo(current.repo, options.repo)) {
|
|
4703
|
+
entries.push(current);
|
|
4704
|
+
} else {
|
|
4705
|
+
otherRepos++;
|
|
4706
|
+
}
|
|
4707
|
+
current = null;
|
|
4708
|
+
};
|
|
4709
|
+
for (const line of content.split("\n")) {
|
|
4710
|
+
if (line.trim().length === 0) continue;
|
|
4711
|
+
const header = parseWorkHeader(line);
|
|
4712
|
+
if (header === null) {
|
|
4713
|
+
skipped++;
|
|
4714
|
+
continue;
|
|
4715
|
+
}
|
|
4716
|
+
if (header !== "not-a-header") {
|
|
4717
|
+
close();
|
|
4718
|
+
current = header;
|
|
4719
|
+
continue;
|
|
4720
|
+
}
|
|
4721
|
+
const item = parseWorkItem(line);
|
|
4722
|
+
if (item === null || current === null) {
|
|
4723
|
+
skipped++;
|
|
4724
|
+
continue;
|
|
4725
|
+
}
|
|
4726
|
+
current.items.push(item);
|
|
4727
|
+
}
|
|
4728
|
+
close();
|
|
4729
|
+
entries.reverse();
|
|
4730
|
+
return {
|
|
4731
|
+
entries: options.limit === void 0 ? entries : entries.slice(0, options.limit),
|
|
4732
|
+
present: true,
|
|
4733
|
+
error: null,
|
|
4734
|
+
skipped,
|
|
4735
|
+
otherRepos,
|
|
4736
|
+
filtered: isFiltered(options.repo),
|
|
4737
|
+
path
|
|
4738
|
+
};
|
|
4739
|
+
}
|
|
4740
|
+
|
|
4741
|
+
// src/git/repoStatus.ts
|
|
4742
|
+
import { spawnSync as spawnSync7 } from "child_process";
|
|
4743
|
+
var GIT_BIN = resolveCommand("git");
|
|
4744
|
+
function git(args) {
|
|
4745
|
+
const result = spawnSync7(GIT_BIN, args, { encoding: "utf8" });
|
|
4746
|
+
return {
|
|
4747
|
+
stdout: result.stdout ?? "",
|
|
4748
|
+
stderr: result.stderr ?? "",
|
|
4749
|
+
status: result.status ?? 1
|
|
4750
|
+
};
|
|
4751
|
+
}
|
|
4752
|
+
function notARepo(baseBranch, detail) {
|
|
4753
|
+
return {
|
|
4754
|
+
branch: null,
|
|
4755
|
+
head: null,
|
|
4756
|
+
dirtyPaths: [],
|
|
4757
|
+
statusError: null,
|
|
4758
|
+
baseBranch,
|
|
4759
|
+
baseLocal: false,
|
|
4760
|
+
upstream: null,
|
|
4761
|
+
upstreamTracked: false,
|
|
4762
|
+
ahead: null,
|
|
4763
|
+
behind: null,
|
|
4764
|
+
refreshed: false,
|
|
4765
|
+
fetchError: null,
|
|
4766
|
+
error: detail
|
|
4767
|
+
};
|
|
4768
|
+
}
|
|
4769
|
+
function isOwnLockFile(porcelainEntry) {
|
|
4770
|
+
return porcelainEntry.slice(3).trim().endsWith(RUN_LOCK_RELATIVE_PATH);
|
|
4771
|
+
}
|
|
4772
|
+
function divergence(upstream, branch) {
|
|
4773
|
+
const { stdout, status } = git([
|
|
4774
|
+
"rev-list",
|
|
4775
|
+
"--left-right",
|
|
4776
|
+
"--count",
|
|
4777
|
+
`${upstream}...${branch}`
|
|
4778
|
+
]);
|
|
4779
|
+
if (status !== 0) return null;
|
|
4780
|
+
const parts = stdout.trim().split(/\s+/);
|
|
4781
|
+
if (parts.length < 2) return null;
|
|
4782
|
+
const behind = Number.parseInt(parts[0], 10);
|
|
4783
|
+
const ahead = Number.parseInt(parts[1], 10);
|
|
4784
|
+
if (Number.isNaN(behind) || Number.isNaN(ahead)) return null;
|
|
4785
|
+
return { ahead, behind };
|
|
4786
|
+
}
|
|
4787
|
+
function readHead() {
|
|
4788
|
+
const head = git(["rev-parse", "--short", "HEAD"]);
|
|
4789
|
+
if (head.status === 0) return { head: head.stdout.trim(), fatal: null };
|
|
4790
|
+
if (git(["rev-parse", "--is-inside-work-tree"]).status === 0) return { head: null, fatal: null };
|
|
4791
|
+
return { head: null, fatal: head.stderr.trim() || "not a git repository" };
|
|
4792
|
+
}
|
|
4793
|
+
function readDirtyPaths() {
|
|
4794
|
+
const porcelain = git(["status", "--porcelain"]);
|
|
4795
|
+
if (porcelain.status !== 0) {
|
|
4796
|
+
return { paths: [], error: porcelain.stderr.trim() || "git status --porcelain failed" };
|
|
4797
|
+
}
|
|
4798
|
+
return {
|
|
4799
|
+
paths: porcelain.stdout.split("\n").filter((line) => line.trim().length > 0).filter((line) => !isOwnLockFile(line)),
|
|
4800
|
+
error: null
|
|
4801
|
+
};
|
|
4802
|
+
}
|
|
4803
|
+
function refreshBase(baseBranch) {
|
|
4804
|
+
const fetched = git([
|
|
4805
|
+
"fetch",
|
|
4806
|
+
"origin",
|
|
4807
|
+
`+refs/heads/${baseBranch}:refs/remotes/origin/${baseBranch}`
|
|
4808
|
+
]);
|
|
4809
|
+
if (fetched.status === 0) return { refreshed: true, fetchError: null };
|
|
4810
|
+
return { refreshed: false, fetchError: fetched.stderr.trim() || "git fetch failed" };
|
|
4811
|
+
}
|
|
4812
|
+
function resolveUpstream(baseBranch, baseLocal) {
|
|
4813
|
+
if (baseLocal) {
|
|
4814
|
+
const configured = git([
|
|
4815
|
+
"rev-parse",
|
|
4816
|
+
"--abbrev-ref",
|
|
4817
|
+
"--symbolic-full-name",
|
|
4818
|
+
`${baseBranch}@{u}`
|
|
4819
|
+
]);
|
|
4820
|
+
if (configured.status === 0) return { upstream: configured.stdout.trim(), tracked: true };
|
|
4821
|
+
}
|
|
4822
|
+
if (git(["rev-parse", "--verify", "--quiet", `refs/remotes/origin/${baseBranch}`]).status === 0) {
|
|
4823
|
+
return { upstream: `origin/${baseBranch}`, tracked: false };
|
|
4824
|
+
}
|
|
4825
|
+
return { upstream: null, tracked: false };
|
|
4826
|
+
}
|
|
4827
|
+
function inspectRepoStatus(options) {
|
|
4828
|
+
const { baseBranch } = options;
|
|
4829
|
+
const { head, fatal } = readHead();
|
|
4830
|
+
if (fatal !== null) return notARepo(baseBranch, fatal);
|
|
4831
|
+
const symbolic = git(["symbolic-ref", "--quiet", "--short", "HEAD"]);
|
|
4832
|
+
const branch = symbolic.status === 0 ? symbolic.stdout.trim() : null;
|
|
4833
|
+
const { paths: dirtyPaths, error: statusError } = readDirtyPaths();
|
|
4834
|
+
const baseLocal = git(["rev-parse", "--verify", "--quiet", `refs/heads/${baseBranch}`]).status === 0;
|
|
4835
|
+
const { refreshed, fetchError } = options.fetch ? refreshBase(baseBranch) : { refreshed: false, fetchError: null };
|
|
4836
|
+
const { upstream, tracked } = resolveUpstream(baseBranch, baseLocal);
|
|
4837
|
+
const counts = baseLocal && upstream !== null ? divergence(upstream, baseBranch) : null;
|
|
4838
|
+
return {
|
|
4839
|
+
branch,
|
|
4840
|
+
head,
|
|
4841
|
+
dirtyPaths,
|
|
4842
|
+
statusError,
|
|
4843
|
+
baseBranch,
|
|
4844
|
+
baseLocal,
|
|
4845
|
+
upstream,
|
|
4846
|
+
upstreamTracked: tracked,
|
|
4847
|
+
ahead: counts?.ahead ?? null,
|
|
4848
|
+
behind: counts?.behind ?? null,
|
|
4849
|
+
refreshed,
|
|
4850
|
+
fetchError,
|
|
4851
|
+
error: null
|
|
4852
|
+
};
|
|
4853
|
+
}
|
|
4854
|
+
|
|
4855
|
+
// src/run/checkReport.ts
|
|
4856
|
+
var SECTION_ORDER = [
|
|
4857
|
+
"lock",
|
|
4858
|
+
"ticks",
|
|
4859
|
+
"work",
|
|
4860
|
+
"git",
|
|
4861
|
+
"selection",
|
|
4862
|
+
"environment"
|
|
4863
|
+
];
|
|
4864
|
+
var SECTION_TITLES = {
|
|
4865
|
+
lock: "Run lock",
|
|
4866
|
+
ticks: "Recent ticks",
|
|
4867
|
+
work: "Last work",
|
|
4868
|
+
git: "Repository",
|
|
4869
|
+
selection: "Selection",
|
|
4870
|
+
environment: "Environment"
|
|
4871
|
+
};
|
|
4872
|
+
function sectionTitle(id) {
|
|
4873
|
+
return SECTION_TITLES[id];
|
|
4874
|
+
}
|
|
4875
|
+
var TICK_HISTORY = 20;
|
|
4876
|
+
var WORK_HISTORY = 3;
|
|
4877
|
+
var SILENCE_MULTIPLIER = 3;
|
|
4878
|
+
var MIN_INTERVALS = 3;
|
|
4879
|
+
function median(values) {
|
|
4880
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
4881
|
+
const middle = Math.floor(sorted.length / 2);
|
|
4882
|
+
return sorted.length % 2 === 1 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
|
|
4883
|
+
}
|
|
4884
|
+
function tickCadence(ticks, now) {
|
|
4885
|
+
if (ticks.length === 0) return { medianIntervalMs: null, sinceNewestMs: null, silent: false };
|
|
4886
|
+
const sinceNewestMs = now.getTime() - ticks[0].timestamp.getTime();
|
|
4887
|
+
const intervals = [];
|
|
4888
|
+
for (let i = 0; i + 1 < ticks.length; i++) {
|
|
4889
|
+
const gap = ticks[i].timestamp.getTime() - ticks[i + 1].timestamp.getTime();
|
|
4890
|
+
if (gap > 0) intervals.push(gap);
|
|
4891
|
+
}
|
|
4892
|
+
if (intervals.length < MIN_INTERVALS)
|
|
4893
|
+
return { medianIntervalMs: null, sinceNewestMs, silent: false };
|
|
4894
|
+
const medianIntervalMs = median(intervals);
|
|
4895
|
+
return {
|
|
4896
|
+
medianIntervalMs,
|
|
4897
|
+
sinceNewestMs,
|
|
4898
|
+
silent: sinceNewestMs > SILENCE_MULTIPLIER * medianIntervalMs
|
|
4899
|
+
};
|
|
4900
|
+
}
|
|
4901
|
+
function describeDuration(ms) {
|
|
4902
|
+
if (ms < 0) return "in the future";
|
|
4903
|
+
const seconds = Math.floor(ms / 1e3);
|
|
4904
|
+
if (seconds < 60) return `${String(seconds)}s`;
|
|
4905
|
+
const minutes = Math.floor(seconds / 60);
|
|
4906
|
+
if (minutes < 60) return `${String(minutes)}m`;
|
|
4907
|
+
const hours = Math.floor(minutes / 60);
|
|
4908
|
+
if (hours < 24) return `${String(hours)}h ${String(minutes % 60)}m`;
|
|
4909
|
+
const days = Math.floor(hours / 24);
|
|
4910
|
+
return `${String(days)}d ${String(hours % 24)}h`;
|
|
4911
|
+
}
|
|
4912
|
+
function build(id, lines, problems, data) {
|
|
4913
|
+
return {
|
|
4914
|
+
id,
|
|
4915
|
+
title: SECTION_TITLES[id],
|
|
4916
|
+
lines,
|
|
4917
|
+
problems: problems.map((summary) => ({ section: id, summary })),
|
|
4918
|
+
data
|
|
4919
|
+
};
|
|
4920
|
+
}
|
|
4921
|
+
function describeOwner(owner) {
|
|
4922
|
+
return `pid ${String(owner.pid)} on ${owner.host}, \`${owner.command}\`, since ${owner.startedAt}`;
|
|
4923
|
+
}
|
|
4924
|
+
function lockSection(status, staleMinutes) {
|
|
4925
|
+
const data = { status: status.kind, staleMinutes };
|
|
4926
|
+
switch (status.kind) {
|
|
4927
|
+
case "free":
|
|
4928
|
+
return build("lock", ["no tick is running in this checkout"], [], data);
|
|
4929
|
+
case "held": {
|
|
4930
|
+
const held = status.heldForMs === null ? "" : ` (${describeDuration(status.heldForMs)} so far)`;
|
|
4931
|
+
return build("lock", [`a tick is running: ${describeOwner(status.owner)}${held}`], [], {
|
|
4932
|
+
...data,
|
|
4933
|
+
owner: status.owner,
|
|
4934
|
+
heldForMs: status.heldForMs
|
|
4935
|
+
});
|
|
4936
|
+
}
|
|
4937
|
+
case "suspect": {
|
|
4938
|
+
const held = status.heldForMs === null ? "unknown" : describeDuration(status.heldForMs);
|
|
4939
|
+
return build(
|
|
4940
|
+
"lock",
|
|
4941
|
+
[`a tick has held the lock for ${held}: ${describeOwner(status.owner)}`],
|
|
4942
|
+
[
|
|
4943
|
+
`the run lock has been held longer than ${String(staleMinutes)} minutes by a process whose identity cannot be verified; if no executor is running, kill the holder or delete \`.automata/automata.lock\``
|
|
4944
|
+
],
|
|
4945
|
+
{ ...data, owner: status.owner, heldForMs: status.heldForMs }
|
|
4946
|
+
);
|
|
4947
|
+
}
|
|
4948
|
+
case "stale":
|
|
4949
|
+
return build(
|
|
4950
|
+
"lock",
|
|
4951
|
+
[
|
|
4952
|
+
status.owner === null ? "a run lock exists but could not be parsed" : `a stale run lock is present: ${describeOwner(status.owner)}`
|
|
4953
|
+
],
|
|
4954
|
+
[
|
|
4955
|
+
"a stale run lock is present; the next tick reclaims it automatically, so no action is needed unless ticks keep being turned away"
|
|
4956
|
+
],
|
|
4957
|
+
{ ...data, owner: status.owner, heldForMs: status.heldForMs }
|
|
4958
|
+
);
|
|
4959
|
+
case "unreadable":
|
|
4960
|
+
return build(
|
|
4961
|
+
"lock",
|
|
4962
|
+
[`the run lock could not be read: ${status.detail}`],
|
|
4963
|
+
[`the run lock at \`.automata/automata.lock\` could not be read: ${status.detail}`],
|
|
4964
|
+
{ ...data, detail: status.detail }
|
|
4965
|
+
);
|
|
4966
|
+
}
|
|
4967
|
+
}
|
|
4968
|
+
function describeTick(tick, now) {
|
|
4969
|
+
const counts = [
|
|
4970
|
+
`answered=${String(tick.counts.answered)}`,
|
|
4971
|
+
`no-reply=${String(tick.counts["answered-no-reply"])}`,
|
|
4972
|
+
`skipped=${String(tick.counts.skipped)}`,
|
|
4973
|
+
`failed=${String(tick.counts.failed)}`,
|
|
4974
|
+
`deferred=${String(tick.counts.deferred)}`,
|
|
4975
|
+
`runs=${String(tick.runs)}`
|
|
4976
|
+
].join(" ");
|
|
4977
|
+
const note = tick.note === null ? "" : ` note=${tick.note}`;
|
|
4978
|
+
const age = describeDuration(now.getTime() - tick.timestamp.getTime());
|
|
4979
|
+
return `${tick.timestamp.toISOString()} (${age} ago) exit=${String(tick.exitCode)} ${counts}${note}`;
|
|
4980
|
+
}
|
|
4981
|
+
function describeMissingTicks(read) {
|
|
4982
|
+
if (read.error !== null) {
|
|
4983
|
+
return {
|
|
4984
|
+
line: `the execution log could not be read: ${read.error}`,
|
|
4985
|
+
problem: `the execution log \`${read.path}\` could not be read: ${read.error}`
|
|
4986
|
+
};
|
|
4987
|
+
}
|
|
4988
|
+
if (!read.present) {
|
|
4989
|
+
return {
|
|
4990
|
+
line: `no execution log at ${read.path}`,
|
|
4991
|
+
problem: `no execution log at \`${read.path}\` \u2014 no tick has ever run here, or automata cannot write to the workspace root; check that the scheduler runs \`do-work\` from inside the checkout`
|
|
4992
|
+
};
|
|
4993
|
+
}
|
|
4994
|
+
if (read.entries.length === 0) {
|
|
4995
|
+
return {
|
|
4996
|
+
line: "the execution log holds no tick for this repository",
|
|
4997
|
+
problem: "the execution log holds no tick for this repository \u2014 the scheduler has never successfully run `do-work` here"
|
|
4998
|
+
};
|
|
4999
|
+
}
|
|
5000
|
+
return null;
|
|
5001
|
+
}
|
|
5002
|
+
function describeTickHistory(ticks, cadence, now, lines, problems) {
|
|
5003
|
+
lines.push(`last tick: ${describeTick(ticks[0], now)}`);
|
|
5004
|
+
if (ticks[0].exitCode !== 0) {
|
|
5005
|
+
problems.push(
|
|
5006
|
+
`the last tick exited ${String(ticks[0].exitCode)} \u2014 see \`Last work\` below and the work log for the item that failed`
|
|
5007
|
+
);
|
|
5008
|
+
}
|
|
5009
|
+
const lockHeld = ticks.filter((tick) => tick.note === "lock-held").length;
|
|
5010
|
+
const withRuns = ticks.filter((tick) => tick.runs > 0).length;
|
|
5011
|
+
lines.push(
|
|
5012
|
+
`history: ${String(ticks.length)} tick(s), ${String(withRuns)} invoked the executor, ${String(lockHeld)} were turned away by a held lock`
|
|
5013
|
+
);
|
|
5014
|
+
if (lockHeld === ticks.length && ticks.length > 1) {
|
|
5015
|
+
problems.push(
|
|
5016
|
+
"every recorded tick was turned away by a held run lock \u2014 a previous tick is wedged; see `Run lock` above"
|
|
5017
|
+
);
|
|
5018
|
+
}
|
|
5019
|
+
if (cadence.medianIntervalMs === null) {
|
|
5020
|
+
lines.push("cadence: not enough history to judge whether the scheduler is still firing");
|
|
5021
|
+
return;
|
|
5022
|
+
}
|
|
5023
|
+
lines.push(`cadence: about one tick every ${describeDuration(cadence.medianIntervalMs)}`);
|
|
5024
|
+
if (cadence.silent) {
|
|
5025
|
+
problems.push(
|
|
5026
|
+
`no tick for ${describeDuration(cadence.sinceNewestMs ?? 0)}, against a usual interval of ${describeDuration(cadence.medianIntervalMs)} \u2014 the scheduler appears to have stopped firing (automata does not manage the scheduler; check it on this host)`
|
|
5027
|
+
);
|
|
5028
|
+
}
|
|
5029
|
+
}
|
|
5030
|
+
function unfilteredLine(unit) {
|
|
5031
|
+
return `not filtered by repository: the slug could not be resolved, so ${unit} from other checkouts may be shown`;
|
|
5032
|
+
}
|
|
5033
|
+
function tickSection(read, now) {
|
|
5034
|
+
const lines = [];
|
|
5035
|
+
const problems = [];
|
|
5036
|
+
const ticks = read.entries;
|
|
5037
|
+
const cadence = tickCadence(ticks, now);
|
|
5038
|
+
const missing = describeMissingTicks(read);
|
|
5039
|
+
if (missing !== null) {
|
|
5040
|
+
lines.push(missing.line);
|
|
5041
|
+
problems.push(missing.problem);
|
|
5042
|
+
} else {
|
|
5043
|
+
describeTickHistory(ticks, cadence, now, lines, problems);
|
|
5044
|
+
}
|
|
5045
|
+
if (!read.filtered && read.present) lines.push(unfilteredLine("line(s)"));
|
|
5046
|
+
if (read.skipped > 0)
|
|
5047
|
+
lines.push(`${String(read.skipped)} log line(s) could not be parsed and were ignored`);
|
|
5048
|
+
if (read.otherRepos > 0)
|
|
5049
|
+
lines.push(`${String(read.otherRepos)} line(s) belonged to another repository`);
|
|
5050
|
+
return build("ticks", lines, problems, {
|
|
5051
|
+
newest: ticks[0] ?? null,
|
|
5052
|
+
history: ticks,
|
|
5053
|
+
lockHeldCount: ticks.filter((tick) => tick.note === "lock-held").length,
|
|
5054
|
+
medianIntervalMs: cadence.medianIntervalMs,
|
|
5055
|
+
sinceNewestMs: cadence.sinceNewestMs,
|
|
5056
|
+
silent: cadence.silent,
|
|
5057
|
+
skipped: read.skipped,
|
|
5058
|
+
otherRepos: read.otherRepos,
|
|
5059
|
+
filtered: read.filtered,
|
|
5060
|
+
logPath: read.path,
|
|
5061
|
+
logPresent: read.present
|
|
5062
|
+
});
|
|
5063
|
+
}
|
|
5064
|
+
function describeWorkItem(item) {
|
|
5065
|
+
const how = item.executor === null ? "" : ` [${[item.executor, item.model, item.effort].filter((part) => part !== null).join(" ")}]`;
|
|
5066
|
+
const sync = item.sync === null ? "" : ` sync=${item.sync}`;
|
|
5067
|
+
return ` ${item.subject} ${item.turn ?? "-"} ${item.outcome}${how}${sync} \u2014 ${item.detail}`;
|
|
5068
|
+
}
|
|
5069
|
+
function describeWorkRecords(records, now) {
|
|
5070
|
+
return records.flatMap((record) => [
|
|
5071
|
+
`${record.timestamp.toISOString()} (${describeDuration(now.getTime() - record.timestamp.getTime())} ago)`,
|
|
5072
|
+
...record.items.map(describeWorkItem)
|
|
5073
|
+
]);
|
|
5074
|
+
}
|
|
5075
|
+
function workSection(read, now) {
|
|
5076
|
+
const lines = [];
|
|
5077
|
+
const problems = [];
|
|
5078
|
+
const records = read.entries;
|
|
5079
|
+
if (read.error !== null) {
|
|
5080
|
+
lines.push(`the work log could not be read: ${read.error}`);
|
|
5081
|
+
problems.push(`the work log \`${read.path}\` could not be read: ${read.error}`);
|
|
5082
|
+
} else if (!read.present || records.length === 0) {
|
|
5083
|
+
lines.push("no tick has invoked the executor in the retained window");
|
|
5084
|
+
} else {
|
|
5085
|
+
lines.push(...describeWorkRecords(records, now));
|
|
5086
|
+
}
|
|
5087
|
+
if (!read.filtered && read.present) lines.push(unfilteredLine("record(s)"));
|
|
5088
|
+
if (read.skipped > 0)
|
|
5089
|
+
lines.push(`${String(read.skipped)} log line(s) could not be parsed and were ignored`);
|
|
5090
|
+
if (read.otherRepos > 0)
|
|
5091
|
+
lines.push(`${String(read.otherRepos)} record(s) belonged to another repository`);
|
|
5092
|
+
return build("work", lines, problems, {
|
|
5093
|
+
records,
|
|
5094
|
+
skipped: read.skipped,
|
|
5095
|
+
otherRepos: read.otherRepos,
|
|
5096
|
+
filtered: read.filtered,
|
|
5097
|
+
logPath: read.path,
|
|
5098
|
+
logPresent: read.present
|
|
5099
|
+
});
|
|
5100
|
+
}
|
|
5101
|
+
function describeCheckout(status, lines, problems) {
|
|
5102
|
+
if (status.branch === null) {
|
|
5103
|
+
lines.push(`HEAD is detached at ${status.head ?? "an unknown commit"}`);
|
|
5104
|
+
problems.push("HEAD is detached; the pre-flight expects a branch, so check one out");
|
|
5105
|
+
} else {
|
|
5106
|
+
const at = status.head === null ? "" : ` at ${status.head}`;
|
|
5107
|
+
lines.push(`on ${status.branch}${at}`);
|
|
5108
|
+
}
|
|
5109
|
+
if (status.statusError !== null) {
|
|
5110
|
+
lines.push(`the working tree could not be inspected: ${status.statusError}`);
|
|
5111
|
+
problems.push(
|
|
5112
|
+
`\`git status\` failed (${status.statusError}), so whether the working tree is clean is unknown; the pre-flight runs the same command and stops every item when it cannot answer`
|
|
5113
|
+
);
|
|
5114
|
+
return;
|
|
5115
|
+
}
|
|
5116
|
+
if (status.dirtyPaths.length === 0) {
|
|
5117
|
+
lines.push("working tree is clean");
|
|
5118
|
+
return;
|
|
5119
|
+
}
|
|
5120
|
+
lines.push(`working tree has ${String(status.dirtyPaths.length)} uncommitted change(s):`);
|
|
5121
|
+
for (const path of status.dirtyPaths) lines.push(` ${path}`);
|
|
5122
|
+
problems.push(
|
|
5123
|
+
`the working tree has ${String(status.dirtyPaths.length)} uncommitted change(s); the pre-flight will try to rescue them onto a branch, and every item skips as \`dirty-tree\` if that fails`
|
|
5124
|
+
);
|
|
5125
|
+
}
|
|
5126
|
+
function describeBaseBranch(status, lines, problems) {
|
|
5127
|
+
if (!status.baseLocal) {
|
|
5128
|
+
lines.push(`base branch ${status.baseBranch} does not exist in this checkout`);
|
|
5129
|
+
problems.push(
|
|
5130
|
+
`the base branch \`${status.baseBranch}\` does not exist locally; either check it out or correct \`doWork.baseBranch\` with \`automata config set do-work-base-branch <branch>\``
|
|
5131
|
+
);
|
|
5132
|
+
return;
|
|
5133
|
+
}
|
|
5134
|
+
if (status.upstream === null) {
|
|
5135
|
+
lines.push(`base branch ${status.baseBranch} has no upstream`);
|
|
5136
|
+
problems.push(
|
|
5137
|
+
`the base branch \`${status.baseBranch}\` has no upstream, so the pre-flight cannot fast-forward it`
|
|
5138
|
+
);
|
|
5139
|
+
return;
|
|
5140
|
+
}
|
|
5141
|
+
if (!status.upstreamTracked) {
|
|
5142
|
+
lines.push(
|
|
5143
|
+
`base branch ${status.baseBranch} has no tracking configuration; counted against ${status.upstream}`
|
|
5144
|
+
);
|
|
5145
|
+
problems.push(
|
|
5146
|
+
`the base branch \`${status.baseBranch}\` has no upstream configured, so the pre-flight's \`git pull --ff-only\` fails even though ${status.upstream} exists; set it with \`git branch --set-upstream-to=${status.upstream} ${status.baseBranch}\``
|
|
5147
|
+
);
|
|
5148
|
+
}
|
|
5149
|
+
const freshness = status.refreshed ? "" : " (not refreshed)";
|
|
5150
|
+
const ahead = status.ahead === null ? "?" : String(status.ahead);
|
|
5151
|
+
const behind = status.behind === null ? "?" : String(status.behind);
|
|
5152
|
+
lines.push(
|
|
5153
|
+
`base branch ${status.baseBranch} vs ${status.upstream}: ahead ${ahead}, behind ${behind}${freshness}`
|
|
5154
|
+
);
|
|
5155
|
+
if (status.ahead === null || status.behind === null) {
|
|
5156
|
+
problems.push(
|
|
5157
|
+
`the divergence of \`${status.baseBranch}\` from ${status.upstream} could not be read, so whether the pre-flight's fast-forward pull will succeed is unknown; try \`git rev-list --left-right --count ${status.upstream}...${status.baseBranch}\` to see git's own error`
|
|
5158
|
+
);
|
|
5159
|
+
return;
|
|
5160
|
+
}
|
|
5161
|
+
if (status.ahead === 0) return;
|
|
5162
|
+
if (status.behind > 0) {
|
|
5163
|
+
problems.push(
|
|
5164
|
+
`the base branch \`${status.baseBranch}\` has diverged from ${status.upstream} (${String(status.ahead)} ahead, ${String(status.behind)} behind); the pre-flight's fast-forward pull will fail until that is resolved by hand`
|
|
5165
|
+
);
|
|
5166
|
+
return;
|
|
5167
|
+
}
|
|
5168
|
+
lines.push(` ${String(status.ahead)} local commit(s) not on ${status.upstream}`);
|
|
5169
|
+
}
|
|
5170
|
+
function gitSection(status) {
|
|
5171
|
+
const lines = [];
|
|
5172
|
+
const problems = [];
|
|
5173
|
+
if (status.error !== null) {
|
|
5174
|
+
return build(
|
|
5175
|
+
"git",
|
|
5176
|
+
[`not a usable git repository: ${status.error}`],
|
|
5177
|
+
[`not a usable git repository: ${status.error}`],
|
|
5178
|
+
{
|
|
5179
|
+
...status
|
|
5180
|
+
}
|
|
5181
|
+
);
|
|
5182
|
+
}
|
|
5183
|
+
describeCheckout(status, lines, problems);
|
|
5184
|
+
describeBaseBranch(status, lines, problems);
|
|
5185
|
+
if (status.fetchError !== null) {
|
|
5186
|
+
lines.push(`fetch failed: ${status.fetchError}`);
|
|
5187
|
+
problems.push(
|
|
5188
|
+
`\`git fetch\` failed (${status.fetchError}); the ahead/behind figures above are from the last successful fetch and may be out of date`
|
|
5189
|
+
);
|
|
5190
|
+
}
|
|
5191
|
+
return build("git", lines, problems, { ...status });
|
|
5192
|
+
}
|
|
5193
|
+
function assembleReport(input) {
|
|
5194
|
+
const byId = new Map(input.sections.map((section) => [section.id, section]));
|
|
5195
|
+
const ordered = SECTION_ORDER.flatMap((id) => {
|
|
5196
|
+
const section = byId.get(id);
|
|
5197
|
+
return section === void 0 ? [] : [section];
|
|
5198
|
+
});
|
|
5199
|
+
const problems = ordered.flatMap((section) => section.problems);
|
|
5200
|
+
return {
|
|
5201
|
+
generatedAt: input.generatedAt,
|
|
5202
|
+
repo: input.repo,
|
|
5203
|
+
offline: input.offline,
|
|
5204
|
+
sections: ordered,
|
|
5205
|
+
problems,
|
|
5206
|
+
exitCode: problems.length === 0 ? 0 : 1
|
|
5207
|
+
};
|
|
5208
|
+
}
|
|
5209
|
+
function renderText(report) {
|
|
5210
|
+
const head = `automata do-work --check \u2014 ${report.repo ?? "unknown repository"} \u2014 ${report.generatedAt.toISOString()}`;
|
|
5211
|
+
const parts = [head, ""];
|
|
5212
|
+
for (const section of report.sections) {
|
|
5213
|
+
parts.push(section.title);
|
|
5214
|
+
if (section.lines.length === 0) {
|
|
5215
|
+
parts.push(" (nothing to report)");
|
|
5216
|
+
} else {
|
|
5217
|
+
for (const line of section.lines) parts.push(` ${line}`);
|
|
5218
|
+
}
|
|
5219
|
+
parts.push("");
|
|
5220
|
+
}
|
|
5221
|
+
if (report.problems.length > 0) {
|
|
5222
|
+
parts.push(`Problems (${String(report.problems.length)})`);
|
|
5223
|
+
for (const problem of report.problems) parts.push(` \xB7 ${problem.section}: ${problem.summary}`);
|
|
5224
|
+
parts.push("");
|
|
5225
|
+
}
|
|
5226
|
+
parts.push(
|
|
5227
|
+
report.problems.length === 0 ? "RESULT: healthy" : `RESULT: ${String(report.problems.length)} problem(s) found`
|
|
5228
|
+
);
|
|
5229
|
+
return parts.join("\n") + "\n";
|
|
5230
|
+
}
|
|
5231
|
+
function toJson(report) {
|
|
5232
|
+
const sections = {};
|
|
5233
|
+
for (const section of report.sections) {
|
|
5234
|
+
sections[section.id] = {
|
|
5235
|
+
title: section.title,
|
|
5236
|
+
lines: section.lines,
|
|
5237
|
+
problems: section.problems,
|
|
5238
|
+
data: section.data
|
|
5239
|
+
};
|
|
5240
|
+
}
|
|
5241
|
+
return {
|
|
5242
|
+
generatedAt: report.generatedAt.toISOString(),
|
|
5243
|
+
repo: report.repo,
|
|
5244
|
+
offline: report.offline,
|
|
5245
|
+
exitCode: report.exitCode,
|
|
5246
|
+
problems: report.problems,
|
|
5247
|
+
sections
|
|
5248
|
+
};
|
|
5249
|
+
}
|
|
4334
5250
|
|
|
4335
5251
|
// src/commands/doWork.ts
|
|
4336
5252
|
var inFlightMarker = null;
|
|
@@ -4365,55 +5281,89 @@ function fail(message) {
|
|
|
4365
5281
|
}
|
|
4366
5282
|
process.exit(1);
|
|
4367
5283
|
}
|
|
5284
|
+
var SettingsError = class extends Error {
|
|
5285
|
+
};
|
|
5286
|
+
function failSettings(message) {
|
|
5287
|
+
throw new SettingsError(message);
|
|
5288
|
+
}
|
|
4368
5289
|
function parsePositiveInt(value, label) {
|
|
4369
5290
|
const trimmed2 = value.trim();
|
|
4370
5291
|
if (!/^\d+$/.test(trimmed2)) {
|
|
4371
|
-
|
|
5292
|
+
failSettings(`${label} must be a positive integer (got "${value}").`);
|
|
4372
5293
|
}
|
|
4373
5294
|
const parsed = Number(trimmed2);
|
|
4374
5295
|
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
4375
|
-
|
|
5296
|
+
failSettings(`${label} must be a positive integer within the safe range (got "${value}").`);
|
|
4376
5297
|
}
|
|
4377
5298
|
return parsed;
|
|
4378
5299
|
}
|
|
4379
|
-
function
|
|
4380
|
-
let config;
|
|
5300
|
+
function resolveSettingsResult(options, verifyIdentity) {
|
|
4381
5301
|
try {
|
|
4382
|
-
|
|
5302
|
+
return { ok: true, settings: buildSettings(options, verifyIdentity) };
|
|
4383
5303
|
} catch (err) {
|
|
4384
|
-
|
|
5304
|
+
if (err instanceof SettingsError) return { ok: false, error: err.message };
|
|
5305
|
+
throw err;
|
|
4385
5306
|
}
|
|
5307
|
+
}
|
|
5308
|
+
function resolveSettings(options) {
|
|
5309
|
+
const result = resolveSettingsResult(options, options.dryRun !== true);
|
|
5310
|
+
if (!result.ok) fail(result.error);
|
|
5311
|
+
return result.settings;
|
|
5312
|
+
}
|
|
5313
|
+
function requireParticipants(config) {
|
|
4386
5314
|
if (config.remoteType !== "gh") {
|
|
4387
|
-
|
|
5315
|
+
failSettings(
|
|
4388
5316
|
"do-work is only supported for GitHub remotes. Set it with `automata config set type gh`. Azure DevOps lacks the issue conversation APIs this needs \u2014 see docs/azdo-gap.md."
|
|
4389
5317
|
);
|
|
4390
5318
|
}
|
|
4391
5319
|
if (!config.issueDiscoveryTechnique) {
|
|
4392
|
-
|
|
5320
|
+
failSettings("No issue discovery technique configured. Run `automata config set issue-discovery-technique <value>`.");
|
|
4393
5321
|
}
|
|
4394
5322
|
if (!config.issueDiscoveryValue) {
|
|
4395
|
-
|
|
5323
|
+
failSettings("No issue discovery value configured. Run `automata config set issue-discovery-value <value>`.");
|
|
4396
5324
|
}
|
|
4397
5325
|
const allowedUsers = (config.allowedUsers ?? []).filter((user) => user.trim().length > 0);
|
|
4398
5326
|
if (allowedUsers.length === 0) {
|
|
4399
|
-
|
|
5327
|
+
failSettings("No allowed users configured. Run `automata config set allowed-users <user1,user2>`.");
|
|
4400
5328
|
}
|
|
4401
5329
|
const agentUser = (config.agentUser ?? "").trim();
|
|
4402
5330
|
if (agentUser.length === 0) {
|
|
4403
|
-
|
|
5331
|
+
failSettings("No agent user configured. Run `automata config set agent-user <login>`.");
|
|
5332
|
+
}
|
|
5333
|
+
return {
|
|
5334
|
+
allowedUsers,
|
|
5335
|
+
agentUser,
|
|
5336
|
+
technique: config.issueDiscoveryTechnique,
|
|
5337
|
+
discoveryValue: config.issueDiscoveryValue
|
|
5338
|
+
};
|
|
5339
|
+
}
|
|
5340
|
+
function resolveWithOption(value) {
|
|
5341
|
+
if (value === void 0) return void 0;
|
|
5342
|
+
const requested = value.toLowerCase();
|
|
5343
|
+
if (requested !== "claude" && requested !== "codex") {
|
|
5344
|
+
failSettings(`--with must be 'claude' or 'codex', got '${value}'.`);
|
|
5345
|
+
}
|
|
5346
|
+
return requested;
|
|
5347
|
+
}
|
|
5348
|
+
function resolveEffort(value) {
|
|
5349
|
+
const result = normalizeEffortOption(value);
|
|
5350
|
+
if (!result.ok) failSettings(result.error);
|
|
5351
|
+
return result.value;
|
|
5352
|
+
}
|
|
5353
|
+
function buildSettings(options, verifyIdentity) {
|
|
5354
|
+
let config;
|
|
5355
|
+
try {
|
|
5356
|
+
config = readConfig();
|
|
5357
|
+
} catch (err) {
|
|
5358
|
+
failSettings(err.message);
|
|
4404
5359
|
}
|
|
5360
|
+
const { allowedUsers, agentUser, technique, discoveryValue } = requireParticipants(config);
|
|
4405
5361
|
validateDoWorkConfig(config.doWork);
|
|
4406
5362
|
const doWork = config.doWork ?? {};
|
|
4407
|
-
|
|
4408
|
-
if (
|
|
4409
|
-
const
|
|
4410
|
-
if (
|
|
4411
|
-
fail(`--with must be 'claude' or 'codex', got '${options.with}'.`);
|
|
4412
|
-
}
|
|
4413
|
-
withOption = requested;
|
|
4414
|
-
}
|
|
4415
|
-
if (options.dryRun !== true) {
|
|
4416
|
-
checkAuthenticatedIdentity(agentUser, allowedUsers);
|
|
5363
|
+
const withOption = resolveWithOption(options.with);
|
|
5364
|
+
if (verifyIdentity) {
|
|
5365
|
+
const problem = describeIdentityProblem(agentUser, allowedUsers);
|
|
5366
|
+
if (problem !== null) failSettings(problem);
|
|
4417
5367
|
}
|
|
4418
5368
|
return {
|
|
4419
5369
|
baseBranch: doWork.baseBranch ?? DEFAULT_DO_WORK.baseBranch,
|
|
@@ -4425,8 +5375,10 @@ function resolveSettings(options) {
|
|
|
4425
5375
|
// Rejected here rather than per item: an empty `--effort` is an operator
|
|
4426
5376
|
// mistake on this invocation, not a property of any one work item. The
|
|
4427
5377
|
// configured per-executor defaults are trimmed inside `resolveExecution`,
|
|
4428
|
-
// which is where the executor in use is finally known.
|
|
4429
|
-
|
|
5378
|
+
// which is where the executor in use is finally known. Routed through
|
|
5379
|
+
// `failSettings` rather than `resolveEffortOption`, whose rejection exits the
|
|
5380
|
+
// process — which would take `--check` down before it printed a section.
|
|
5381
|
+
effortOption: resolveEffort(options.effort),
|
|
4430
5382
|
configEfforts: doWork.effort,
|
|
4431
5383
|
maxRuns: options.maxRuns !== void 0 ? parsePositiveInt(options.maxRuns, "--max-runs") : doWork.maxRunsPerTick ?? DEFAULT_DO_WORK.maxRunsPerTick,
|
|
4432
5384
|
lockStaleMinutes: doWork.lockStaleMinutes ?? DEFAULT_DO_WORK.lockStaleMinutes,
|
|
@@ -4437,30 +5389,22 @@ function resolveSettings(options) {
|
|
|
4437
5389
|
"pr-work": doWork.prompts?.prWork ?? DEFAULT_DO_WORK_PR_WORK_PROMPT,
|
|
4438
5390
|
"pr-orphan": doWork.prompts?.prOrphan ?? DEFAULT_DO_WORK_PR_ORPHAN_PROMPT
|
|
4439
5391
|
},
|
|
4440
|
-
technique
|
|
4441
|
-
discoveryValue
|
|
5392
|
+
technique,
|
|
5393
|
+
discoveryValue,
|
|
4442
5394
|
onlyIssue: options.issue === void 0 ? void 0 : parsePositiveInt(options.issue, "--issue"),
|
|
4443
5395
|
onlyPr: options.pr === void 0 ? void 0 : parsePositiveInt(options.pr, "--pr")
|
|
4444
5396
|
};
|
|
4445
5397
|
}
|
|
4446
|
-
function
|
|
5398
|
+
function describeIdentityProblem(agentUser, allowedUsers) {
|
|
4447
5399
|
const login2 = getAuthenticatedLogin();
|
|
4448
5400
|
if (login2 === null) {
|
|
4449
5401
|
progress(
|
|
4450
5402
|
`Warning: could not determine which account \`gh\` is authenticated as; assuming it is the agent (${agentUser}).
|
|
4451
5403
|
`
|
|
4452
5404
|
);
|
|
4453
|
-
return;
|
|
4454
|
-
}
|
|
4455
|
-
if (login2.toLowerCase() === agentUser.toLowerCase()) return;
|
|
4456
|
-
if (allowedUsers.some((user) => user.toLowerCase() === login2.toLowerCase())) {
|
|
4457
|
-
fail(
|
|
4458
|
-
`\`gh\` is authenticated as "${login2}", which is listed in allowedUsers. Everything do-work posts would be attributed to an account that is allowed to instruct the agent, so its own marker comment would look like a new instruction and each tick would answer the previous tick forever. Authenticate \`gh\` as the agent account (${agentUser}) in this environment, or correct \`agentUser\`.`
|
|
4459
|
-
);
|
|
5405
|
+
return null;
|
|
4460
5406
|
}
|
|
4461
|
-
|
|
4462
|
-
`\`gh\` is authenticated as "${login2}" but agentUser is "${agentUser}". Comments posted under that identity are neither the agent's nor an authorized user's, so they are filtered out of the conversation: the answer boundary would never advance and the same message would start a run on every tick. Authenticate \`gh\` as the agent account (${agentUser}) in this environment, or correct \`agentUser\`.`
|
|
4463
|
-
);
|
|
5407
|
+
return identityProblemFor(login2, agentUser, allowedUsers);
|
|
4464
5408
|
}
|
|
4465
5409
|
function resolveItemExecution(item, settings) {
|
|
4466
5410
|
const trigger = triggeringMessage(item);
|
|
@@ -4577,24 +5521,24 @@ function validateOptionalString(container, key, path) {
|
|
|
4577
5521
|
const value = container[key];
|
|
4578
5522
|
if (value === void 0 || value === null) return;
|
|
4579
5523
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
4580
|
-
|
|
5524
|
+
failSettings(`${path} must be a non-empty string.`);
|
|
4581
5525
|
}
|
|
4582
5526
|
}
|
|
4583
5527
|
function validateOptionalInt(container, key, path, min, hint) {
|
|
4584
5528
|
const value = container[key];
|
|
4585
5529
|
if (value === void 0 || value === null) return;
|
|
4586
5530
|
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < min) {
|
|
4587
|
-
|
|
5531
|
+
failSettings(`${path} must be ${hint}, got ${JSON.stringify(value)}.`);
|
|
4588
5532
|
}
|
|
4589
5533
|
}
|
|
4590
5534
|
function validateDoWorkConfig(section) {
|
|
4591
5535
|
if (section === void 0 || section === null) return;
|
|
4592
5536
|
if (!isPlainObject(section)) {
|
|
4593
|
-
|
|
5537
|
+
failSettings(`doWork must be an object, got ${JSON.stringify(section)}.`);
|
|
4594
5538
|
}
|
|
4595
5539
|
const executor = section["executor"];
|
|
4596
5540
|
if (executor !== void 0 && executor !== "claude" && executor !== "codex") {
|
|
4597
|
-
|
|
5541
|
+
failSettings(`doWork.executor must be 'claude' or 'codex', got ${JSON.stringify(executor)}.`);
|
|
4598
5542
|
}
|
|
4599
5543
|
validateOptionalString(section, "baseBranch", "doWork.baseBranch");
|
|
4600
5544
|
validateOptionalInt(section, "maxRunsPerTick", "doWork.maxRunsPerTick", 0, "a non-negative integer (0 = unlimited)");
|
|
@@ -4608,20 +5552,20 @@ function validateProtectedBranches(value) {
|
|
|
4608
5552
|
if (value === void 0 || value === null) return;
|
|
4609
5553
|
const isNonEmptyString = (b) => typeof b === "string" && b.trim().length > 0;
|
|
4610
5554
|
if (!Array.isArray(value) || !value.every(isNonEmptyString)) {
|
|
4611
|
-
|
|
5555
|
+
failSettings("doWork.protectedBranches must be an array of non-empty strings.");
|
|
4612
5556
|
}
|
|
4613
5557
|
}
|
|
4614
5558
|
function validateSettingContainer(value, container, keys) {
|
|
4615
5559
|
if (value === void 0 || value === null) return;
|
|
4616
5560
|
if (!isPlainObject(value)) {
|
|
4617
|
-
|
|
5561
|
+
failSettings(`doWork.${container} must be an object, got ${JSON.stringify(value)}.`);
|
|
4618
5562
|
}
|
|
4619
5563
|
for (const key of keys) {
|
|
4620
5564
|
validateOptionalString(value, key, `doWork.${container}.${key}`);
|
|
4621
5565
|
}
|
|
4622
5566
|
for (const key of Object.keys(value)) {
|
|
4623
5567
|
if (!keys.includes(key)) {
|
|
4624
|
-
|
|
5568
|
+
failSettings(`doWork.${container}.${key} is not a recognised setting; expected one of: ${keys.join(", ")}.`);
|
|
4625
5569
|
}
|
|
4626
5570
|
}
|
|
4627
5571
|
}
|
|
@@ -5156,8 +6100,25 @@ var doWorkCommand = new Command6("do-work").description(
|
|
|
5156
6100
|
).option("--limit <n>", "Maximum number of issues to fetch", "10").option("--max-runs <n>", "Maximum number of model runs this tick").option(
|
|
5157
6101
|
"--dry-run",
|
|
5158
6102
|
"Print the work plan, plus a summary and the exact command that would be launched for each item, and exit without changing anything"
|
|
6103
|
+
).option(
|
|
6104
|
+
"--check",
|
|
6105
|
+
"Print a read-only health report for the loop \u2014 run lock, tick history, last work, repository state, per-candidate selection and environment \u2014 and exit 0 when it found no problem, 1 when it did"
|
|
6106
|
+
).option(
|
|
6107
|
+
"--no-fetch",
|
|
6108
|
+
"With --check: make no network call at all \u2014 no `git fetch` and no `gh` query. Ahead/behind is reported from the last fetch and the selection section does not run"
|
|
5159
6109
|
).option("--json", "Emit the work plan and outcomes as JSON on stdout").option("--silent", "Suppress step-by-step Claude output; show only the final summary").action(async (options) => {
|
|
5160
6110
|
const startedAt = Date.now();
|
|
6111
|
+
if (options.check === true) {
|
|
6112
|
+
if (options.dryRun === true) {
|
|
6113
|
+
process.stderr.write(
|
|
6114
|
+
"Error: --check and --dry-run are two different read-only reports; run one or the other.\n"
|
|
6115
|
+
);
|
|
6116
|
+
process.exit(1);
|
|
6117
|
+
}
|
|
6118
|
+
const exitCode2 = runCheck(options);
|
|
6119
|
+
if (exitCode2 !== 0) process.exit(exitCode2);
|
|
6120
|
+
return;
|
|
6121
|
+
}
|
|
5161
6122
|
if (options.dryRun !== true) loggableInvocation = { startedAt };
|
|
5162
6123
|
const settings = resolveSettings(options);
|
|
5163
6124
|
if (options.dryRun === true) {
|
|
@@ -5211,6 +6172,213 @@ var doWorkCommand = new Command6("do-work").description(
|
|
|
5211
6172
|
logTick(reports, exitCode, startedAt);
|
|
5212
6173
|
if (exitCode !== 0) process.exit(exitCode);
|
|
5213
6174
|
});
|
|
6175
|
+
function resolveRepoSlug() {
|
|
6176
|
+
try {
|
|
6177
|
+
const slug = getRepoSlug();
|
|
6178
|
+
return `${slug.owner}/${slug.repo}`;
|
|
6179
|
+
} catch {
|
|
6180
|
+
return null;
|
|
6181
|
+
}
|
|
6182
|
+
}
|
|
6183
|
+
function collectSelection(settings) {
|
|
6184
|
+
const issues = discoverIssues(settings);
|
|
6185
|
+
const linkMap = getOpenPrLinkMap();
|
|
6186
|
+
const policy = {
|
|
6187
|
+
baseBranch: settings.baseBranch,
|
|
6188
|
+
defaultBranch: linkMap.defaultBranch,
|
|
6189
|
+
protectedBranches: settings.protectedBranches
|
|
6190
|
+
};
|
|
6191
|
+
return skipDuplicateHeadBranches([
|
|
6192
|
+
...issues.map((issue) => decideWork(buildIssueState(issue, linkMap), settings.participants, policy)),
|
|
6193
|
+
...discoverOrphanPrs(settings, linkMap).map(
|
|
6194
|
+
(candidate) => decideOrphanPrWork({ prSurface: getPrSurface(candidate.pr.number) }, settings.participants, policy)
|
|
6195
|
+
)
|
|
6196
|
+
]);
|
|
6197
|
+
}
|
|
6198
|
+
function selectionSection(settings, offline) {
|
|
6199
|
+
const section = (lines2, problems, data) => ({
|
|
6200
|
+
id: "selection",
|
|
6201
|
+
title: sectionTitle("selection"),
|
|
6202
|
+
lines: lines2,
|
|
6203
|
+
problems: problems.map((summary) => ({ section: "selection", summary })),
|
|
6204
|
+
data
|
|
6205
|
+
});
|
|
6206
|
+
if (offline) {
|
|
6207
|
+
return section(
|
|
6208
|
+
["not run: --no-fetch makes no network call, and the selection needs live GitHub data"],
|
|
6209
|
+
[],
|
|
6210
|
+
{ ran: false, detail: "offline", plan: [] }
|
|
6211
|
+
);
|
|
6212
|
+
}
|
|
6213
|
+
if (settings === null) {
|
|
6214
|
+
return section(
|
|
6215
|
+
["not run: the configuration could not be resolved \u2014 see Environment below"],
|
|
6216
|
+
[],
|
|
6217
|
+
{ ran: false, detail: "configuration invalid", plan: [] }
|
|
6218
|
+
);
|
|
6219
|
+
}
|
|
6220
|
+
let decisions;
|
|
6221
|
+
try {
|
|
6222
|
+
decisions = collectSelection(settings);
|
|
6223
|
+
} catch (err) {
|
|
6224
|
+
const detail = err.message;
|
|
6225
|
+
return section(
|
|
6226
|
+
[`could not be computed: ${detail}`],
|
|
6227
|
+
[`the GitHub selection could not be computed: ${detail}`],
|
|
6228
|
+
{ ran: false, detail, plan: [] }
|
|
6229
|
+
);
|
|
6230
|
+
}
|
|
6231
|
+
const work = decisions.filter((decision) => decision.kind === "work").length;
|
|
6232
|
+
const lines = [
|
|
6233
|
+
`${String(work)} of ${String(decisions.length)} candidate(s) would be picked up`,
|
|
6234
|
+
...describePlan(decisions).trimEnd().split("\n")
|
|
6235
|
+
];
|
|
6236
|
+
return section(lines, [], { ran: true, detail: null, plan: decisions.map(toPlanJson) });
|
|
6237
|
+
}
|
|
6238
|
+
function describeExecutor(settings, lines, problems) {
|
|
6239
|
+
const executor = settings.withOption ?? settings.configExecutor ?? "claude";
|
|
6240
|
+
const command = executor === "codex" ? "codex" : "claude";
|
|
6241
|
+
const resolvedPath = resolveCommand(command);
|
|
6242
|
+
const onPath = resolvedPath !== command;
|
|
6243
|
+
lines.push(`default executor: ${executor} (${onPath ? resolvedPath : "not found on PATH"})`);
|
|
6244
|
+
if (!onPath) {
|
|
6245
|
+
problems.push(
|
|
6246
|
+
`the \`${command}\` command is not on PATH, so every run this tick would fail. Under cron the PATH is not your login shell's \u2014 set it in the crontab or use an absolute path`
|
|
6247
|
+
);
|
|
6248
|
+
}
|
|
6249
|
+
return { executor, command, onPath };
|
|
6250
|
+
}
|
|
6251
|
+
function describeGitHubIdentity(options, settings, lines, problems) {
|
|
6252
|
+
if (options.fetch === false) {
|
|
6253
|
+
lines.push("`gh` authentication not checked: --no-fetch");
|
|
6254
|
+
return { ghAvailable: null, ghLogin: null, identityProblem: null };
|
|
6255
|
+
}
|
|
6256
|
+
let identity;
|
|
6257
|
+
try {
|
|
6258
|
+
identity = getAuthenticatedIdentity();
|
|
6259
|
+
} catch (err) {
|
|
6260
|
+
const detail = err.message;
|
|
6261
|
+
lines.push(`\`gh\` could not be queried: ${detail}`);
|
|
6262
|
+
problems.push(`\`gh\` could not be queried (${detail}); install it and run \`gh auth login\``);
|
|
6263
|
+
return { ghAvailable: false, ghLogin: null, identityProblem: null };
|
|
6264
|
+
}
|
|
6265
|
+
if (identity.kind === "unavailable") {
|
|
6266
|
+
lines.push(`\`gh\` could not name an account: ${identity.detail}`);
|
|
6267
|
+
problems.push(
|
|
6268
|
+
`\`gh api user\` failed (${identity.detail}), so \`gh\` is not authenticated here and every GitHub call a tick makes would fail; run \`gh auth login\`, or set \`GH_TOKEN\` in the scheduler's environment`
|
|
6269
|
+
);
|
|
6270
|
+
return { ghAvailable: false, ghLogin: null, identityProblem: null };
|
|
6271
|
+
}
|
|
6272
|
+
const ghLogin = identity.kind === "login" ? identity.login : null;
|
|
6273
|
+
lines.push(
|
|
6274
|
+
ghLogin === null ? "`gh` is available but its account could not be determined (an app installation token has none)" : `\`gh\` is authenticated as ${ghLogin}`
|
|
6275
|
+
);
|
|
6276
|
+
const identityProblem = identityProblemFor(
|
|
6277
|
+
ghLogin,
|
|
6278
|
+
settings.participants.agentUser,
|
|
6279
|
+
settings.participants.allowedUsers
|
|
6280
|
+
);
|
|
6281
|
+
if (identityProblem !== null) problems.push(identityProblem);
|
|
6282
|
+
return { ghAvailable: true, ghLogin, identityProblem };
|
|
6283
|
+
}
|
|
6284
|
+
function readRemoteType() {
|
|
6285
|
+
try {
|
|
6286
|
+
return readConfig().remoteType ?? null;
|
|
6287
|
+
} catch {
|
|
6288
|
+
return null;
|
|
6289
|
+
}
|
|
6290
|
+
}
|
|
6291
|
+
function environmentSection(options, resolved, repo) {
|
|
6292
|
+
const lines = [`automata ${version}`];
|
|
6293
|
+
const problems = [];
|
|
6294
|
+
const remoteType = readRemoteType();
|
|
6295
|
+
const data = {
|
|
6296
|
+
version,
|
|
6297
|
+
repo,
|
|
6298
|
+
remoteType,
|
|
6299
|
+
configValid: resolved.ok,
|
|
6300
|
+
configError: resolved.ok ? null : resolved.error
|
|
6301
|
+
};
|
|
6302
|
+
if (repo === null) {
|
|
6303
|
+
lines.push("repository slug could not be resolved from `origin`");
|
|
6304
|
+
problems.push(
|
|
6305
|
+
"the repository slug could not be resolved; `gh` needs an `origin` remote pointing at GitHub to read issues, and the operation logs cannot attribute their lines without it"
|
|
6306
|
+
);
|
|
6307
|
+
} else {
|
|
6308
|
+
lines.push(`repository ${repo}`);
|
|
6309
|
+
}
|
|
6310
|
+
if (!resolved.ok) {
|
|
6311
|
+
lines.push(`configuration is not usable: ${resolved.error}`);
|
|
6312
|
+
problems.push(`the configuration is not usable: ${resolved.error}`);
|
|
6313
|
+
return {
|
|
6314
|
+
id: "environment",
|
|
6315
|
+
title: sectionTitle("environment"),
|
|
6316
|
+
lines,
|
|
6317
|
+
problems: problems.map((summary) => ({ section: "environment", summary })),
|
|
6318
|
+
// `ghAvailable` is null rather than false: nothing was asked of `gh`,
|
|
6319
|
+
// which is not the same as having asked and been refused.
|
|
6320
|
+
data: { ...data, ghAvailable: null, ghLogin: null, identityProblem: null }
|
|
6321
|
+
};
|
|
6322
|
+
}
|
|
6323
|
+
const settings = resolved.settings;
|
|
6324
|
+
const runCap = settings.maxRuns === 0 ? "unlimited" : String(settings.maxRuns);
|
|
6325
|
+
lines.push(
|
|
6326
|
+
"configuration parses and validates",
|
|
6327
|
+
`discovery: ${settings.technique} = ${settings.discoveryValue}`,
|
|
6328
|
+
`base branch: ${settings.baseBranch}`,
|
|
6329
|
+
`run cap: ${runCap}; lock stale after ${String(settings.lockStaleMinutes)} minutes`
|
|
6330
|
+
);
|
|
6331
|
+
const { executor, command, onPath } = describeExecutor(settings, lines, problems);
|
|
6332
|
+
const { ghAvailable, ghLogin, identityProblem } = describeGitHubIdentity(
|
|
6333
|
+
options,
|
|
6334
|
+
settings,
|
|
6335
|
+
lines,
|
|
6336
|
+
problems
|
|
6337
|
+
);
|
|
6338
|
+
return {
|
|
6339
|
+
id: "environment",
|
|
6340
|
+
title: sectionTitle("environment"),
|
|
6341
|
+
lines,
|
|
6342
|
+
problems: problems.map((summary) => ({ section: "environment", summary })),
|
|
6343
|
+
data: {
|
|
6344
|
+
...data,
|
|
6345
|
+
discovery: { technique: settings.technique, value: settings.discoveryValue },
|
|
6346
|
+
baseBranch: settings.baseBranch,
|
|
6347
|
+
maxRuns: settings.maxRuns,
|
|
6348
|
+
lockStaleMinutes: settings.lockStaleMinutes,
|
|
6349
|
+
executor,
|
|
6350
|
+
executorCommand: command,
|
|
6351
|
+
executorOnPath: onPath,
|
|
6352
|
+
ghAvailable,
|
|
6353
|
+
ghLogin,
|
|
6354
|
+
identityProblem
|
|
6355
|
+
}
|
|
6356
|
+
};
|
|
6357
|
+
}
|
|
6358
|
+
function runCheck(options) {
|
|
6359
|
+
const now = /* @__PURE__ */ new Date();
|
|
6360
|
+
const offline = options.fetch === false;
|
|
6361
|
+
const repo = resolveRepoSlug();
|
|
6362
|
+
const resolved = resolveSettingsResult(options, false);
|
|
6363
|
+
const settings = resolved.ok ? resolved.settings : null;
|
|
6364
|
+
const staleMinutes = settings?.lockStaleMinutes ?? DEFAULT_DO_WORK.lockStaleMinutes;
|
|
6365
|
+
const baseBranch = settings?.baseBranch ?? DEFAULT_DO_WORK.baseBranch;
|
|
6366
|
+
const report = assembleReport({
|
|
6367
|
+
generatedAt: now,
|
|
6368
|
+
repo,
|
|
6369
|
+
offline,
|
|
6370
|
+
sections: [
|
|
6371
|
+
lockSection(inspectRunLock(staleMinutes), staleMinutes),
|
|
6372
|
+
tickSection(readExecutionTicks({ repo, limit: TICK_HISTORY }), now),
|
|
6373
|
+
workSection(readWorkRecords({ repo, limit: WORK_HISTORY }), now),
|
|
6374
|
+
gitSection(inspectRepoStatus({ baseBranch, fetch: !offline })),
|
|
6375
|
+
selectionSection(settings, offline),
|
|
6376
|
+
environmentSection(options, resolved, repo)
|
|
6377
|
+
]
|
|
6378
|
+
});
|
|
6379
|
+
out(options.json === true ? JSON.stringify(toJson(report), null, 2) + "\n" : renderText(report));
|
|
6380
|
+
return report.exitCode;
|
|
6381
|
+
}
|
|
5214
6382
|
function toTickLogItem(report) {
|
|
5215
6383
|
return {
|
|
5216
6384
|
subject: reportLabel(report),
|