automata-cli 0.8.1 → 0.9.0-develop.387
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
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
readConfig,
|
|
12
12
|
readRawConfig,
|
|
13
13
|
writeConfig
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-42OV4UI4.js";
|
|
15
15
|
|
|
16
16
|
// src/index.ts
|
|
17
17
|
import { Command as Command7 } from "commander";
|
|
@@ -26,6 +26,60 @@ var version = packageJson.version;
|
|
|
26
26
|
|
|
27
27
|
// src/commands/config.ts
|
|
28
28
|
import { Command } from "commander";
|
|
29
|
+
|
|
30
|
+
// src/git/releaseFlow.ts
|
|
31
|
+
var RELEASE_FLOWS = ["gitflow", "trunk"];
|
|
32
|
+
var RELEASE_FLOW_CONFIG_KEY = "git.releaseFlow";
|
|
33
|
+
function isReleaseFlow(value) {
|
|
34
|
+
return typeof value === "string" && RELEASE_FLOWS.includes(value);
|
|
35
|
+
}
|
|
36
|
+
function describeReleaseFlowSource(source) {
|
|
37
|
+
switch (source) {
|
|
38
|
+
case "config":
|
|
39
|
+
return `configured as ${RELEASE_FLOW_CONFIG_KEY}`;
|
|
40
|
+
case "develop-present":
|
|
41
|
+
return "detected: origin/develop exists";
|
|
42
|
+
case "develop-absent":
|
|
43
|
+
return "detected: origin/develop does not exist";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function invalidReleaseFlowMessage(value) {
|
|
47
|
+
return `Invalid ${RELEASE_FLOW_CONFIG_KEY} ${JSON.stringify(value)} in .automata/config.json; expected one of: ${RELEASE_FLOWS.join(", ")}.
|
|
48
|
+
Fix it with: automata config set git-release-flow <gitflow|trunk>`;
|
|
49
|
+
}
|
|
50
|
+
function planRelease(flow, version2, trunk, trunkIsLocal) {
|
|
51
|
+
if (flow === "trunk") {
|
|
52
|
+
const message = `chore(release): ${version2}`;
|
|
53
|
+
return [
|
|
54
|
+
{ args: ["commit", "--allow-empty", "-m", message], desc: `git commit --allow-empty -m "${message}"` },
|
|
55
|
+
{ args: ["tag", version2], desc: `git tag ${version2}` },
|
|
56
|
+
{
|
|
57
|
+
args: ["push", "--atomic", "origin", trunk, version2],
|
|
58
|
+
desc: `git push --atomic origin ${trunk} ${version2}`
|
|
59
|
+
}
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
const releaseBranch = `release/${version2}`;
|
|
63
|
+
const checkoutTrunk = trunkIsLocal ? { args: ["checkout", trunk], desc: `git checkout ${trunk}` } : {
|
|
64
|
+
args: ["checkout", "-b", trunk, `origin/${trunk}`],
|
|
65
|
+
desc: `git checkout -b ${trunk} origin/${trunk}`
|
|
66
|
+
};
|
|
67
|
+
return [
|
|
68
|
+
{ args: ["checkout", "-b", releaseBranch], desc: `git checkout -b ${releaseBranch}` },
|
|
69
|
+
checkoutTrunk,
|
|
70
|
+
{ args: ["merge", "--no-ff", releaseBranch], desc: `git merge --no-ff ${releaseBranch}` },
|
|
71
|
+
{ args: ["tag", version2], desc: `git tag ${version2}` },
|
|
72
|
+
{ args: ["checkout", "develop"], desc: `git checkout develop` },
|
|
73
|
+
{ args: ["merge", "--no-ff", releaseBranch], desc: `git merge --no-ff ${releaseBranch}` },
|
|
74
|
+
{ args: ["branch", "-d", releaseBranch], desc: `git branch -d ${releaseBranch}` },
|
|
75
|
+
{
|
|
76
|
+
args: ["push", "origin", "develop", trunk, version2],
|
|
77
|
+
desc: `git push origin develop ${trunk} ${version2}`
|
|
78
|
+
}
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/commands/config.ts
|
|
29
83
|
var VALID_TYPES = ["gh", "azdo"];
|
|
30
84
|
var VALID_TECHNIQUES = ["label", "assignee", "title-contains"];
|
|
31
85
|
var VALID_EXECUTORS = ["claude", "codex"];
|
|
@@ -184,6 +238,20 @@ var configSetDoWorkLockStaleMinutes = new Command("do-work-lock-stale-minutes").
|
|
|
184
238
|
process.stdout.write(`do-work lock staleness set to: ${String(minutes)} minutes
|
|
185
239
|
`);
|
|
186
240
|
});
|
|
241
|
+
var configSetDoWorkDumpOnBlock = new Command("do-work-dump-on-block").description(
|
|
242
|
+
"Set whether `do-work` prints the full health report when a tick exits having done nothing"
|
|
243
|
+
).argument("<value>", `true or false (default: ${String(DEFAULT_DO_WORK.dumpOnBlock)})`).action((value) => {
|
|
244
|
+
const normalized = value.trim().toLowerCase();
|
|
245
|
+
if (normalized !== "true" && normalized !== "false") {
|
|
246
|
+
process.stderr.write(`Error: do-work-dump-on-block must be true or false (got "${value}").
|
|
247
|
+
`);
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
const dumpOnBlock = normalized === "true";
|
|
251
|
+
writeDoWork({ dumpOnBlock });
|
|
252
|
+
process.stdout.write(`do-work dump on block set to: ${String(dumpOnBlock)}
|
|
253
|
+
`);
|
|
254
|
+
});
|
|
187
255
|
var configSetDoWorkPrompt = new Command("do-work-prompt").description("Set the turn instructions for a `do-work` turn kind (prompt text or a .md filename)").argument("<turn-kind>", `Turn kind: ${VALID_TURN_KINDS.join(", ")}`).argument("<value>", "Prompt text, or a plain .md filename inside .automata/").action((turnKind, value) => {
|
|
188
256
|
if (!VALID_TURN_KINDS.includes(turnKind)) {
|
|
189
257
|
process.stderr.write(
|
|
@@ -225,12 +293,24 @@ var configSetGitTrunkBranch = new Command("git-trunk-branch").description("Pin t
|
|
|
225
293
|
process.stdout.write(`git trunk branch set to: ${branch}
|
|
226
294
|
`);
|
|
227
295
|
});
|
|
228
|
-
var
|
|
296
|
+
var configSetGitReleaseFlow = new Command("git-release-flow").description("Pin the release procedure `publish-release` runs, instead of detecting it").argument("<value>", `Release flow: ${RELEASE_FLOWS.join(" | ")} (when the key is absent, the flow is detected from origin/develop)`).action((value) => {
|
|
297
|
+
const flow = value.trim();
|
|
298
|
+
if (!isReleaseFlow(flow)) {
|
|
299
|
+
process.stderr.write(`Error: invalid release flow "${value}". Must be one of: ${RELEASE_FLOWS.join(", ")}
|
|
300
|
+
`);
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
const current = readRawConfig();
|
|
304
|
+
writeConfig({ ...current, git: { ...current.git, releaseFlow: flow } });
|
|
305
|
+
process.stdout.write(`git release flow set to: ${flow}
|
|
306
|
+
`);
|
|
307
|
+
});
|
|
308
|
+
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(configSetDoWorkDumpOnBlock).addCommand(configSetDoWorkPrompt).addCommand(configSetGitTrunkBranch).addCommand(configSetGitReleaseFlow);
|
|
229
309
|
var configCommand = new Command("config").description("Configure automata settings").addCommand(configSet).action(async () => {
|
|
230
310
|
const [{ render }, React, { ConfigWizard }] = await Promise.all([
|
|
231
311
|
import("ink"),
|
|
232
312
|
import("react"),
|
|
233
|
-
import("./ConfigWizard-
|
|
313
|
+
import("./ConfigWizard-RGXIC7OS.js")
|
|
234
314
|
]);
|
|
235
315
|
const { waitUntilExit } = render(React.createElement(ConfigWizard));
|
|
236
316
|
await waitUntilExit();
|
|
@@ -291,6 +371,38 @@ function getPrInfo() {
|
|
|
291
371
|
};
|
|
292
372
|
}
|
|
293
373
|
|
|
374
|
+
// src/run/commandTrace.ts
|
|
375
|
+
var sink = null;
|
|
376
|
+
function startCommandTrace() {
|
|
377
|
+
sink = [];
|
|
378
|
+
}
|
|
379
|
+
function stopCommandTrace() {
|
|
380
|
+
sink = null;
|
|
381
|
+
}
|
|
382
|
+
function takeCommandTrace() {
|
|
383
|
+
const taken = sink;
|
|
384
|
+
sink = null;
|
|
385
|
+
return taken;
|
|
386
|
+
}
|
|
387
|
+
function recordCommand(command, args, startedAt, exitCode) {
|
|
388
|
+
if (sink === null) return;
|
|
389
|
+
sink.push({
|
|
390
|
+
command,
|
|
391
|
+
args: [...args],
|
|
392
|
+
// A clock that went backwards must not produce a negative duration in a
|
|
393
|
+
// report an operator is reading for anomalies.
|
|
394
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
395
|
+
exitCode
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
function describeTracedCommand(traced) {
|
|
399
|
+
const rendered = [traced.command, ...traced.args].join(" ");
|
|
400
|
+
const flat = rendered.replace(/\s+/g, " ").trim();
|
|
401
|
+
const shown = flat.length <= MAX_COMMAND_LENGTH ? flat : `${flat.slice(0, MAX_COMMAND_LENGTH)}\u2026`;
|
|
402
|
+
return `${shown} \u2014 ${String(traced.durationMs)}ms exit ${String(traced.exitCode)}`;
|
|
403
|
+
}
|
|
404
|
+
var MAX_COMMAND_LENGTH = 300;
|
|
405
|
+
|
|
294
406
|
// src/git/trunkDetection.ts
|
|
295
407
|
var TRUNK_CANDIDATES = ["main", "master"];
|
|
296
408
|
var TRUNK_BRANCH_CONFIG_KEY = "git.trunkBranch";
|
|
@@ -331,11 +443,14 @@ Set it explicitly with: automata config set git-trunk-branch <branch>`;
|
|
|
331
443
|
|
|
332
444
|
// src/git/gitService.ts
|
|
333
445
|
function run2(cmd, args) {
|
|
446
|
+
const startedAt = Date.now();
|
|
334
447
|
const result = spawnSync2(cmd, args, { encoding: "utf8" });
|
|
448
|
+
const status = result.status ?? 1;
|
|
449
|
+
recordCommand(cmd, args, startedAt, result.error ? -1 : status);
|
|
335
450
|
return {
|
|
336
451
|
stdout: result.stdout ?? "",
|
|
337
452
|
stderr: result.stderr ?? "",
|
|
338
|
-
status
|
|
453
|
+
status
|
|
339
454
|
};
|
|
340
455
|
}
|
|
341
456
|
function getCurrentBranch() {
|
|
@@ -1098,17 +1213,44 @@ ${stderr.trim()}`);
|
|
|
1098
1213
|
}
|
|
1099
1214
|
return stdout.trim().length > 0;
|
|
1100
1215
|
}
|
|
1101
|
-
function
|
|
1216
|
+
function probeRemoteBranch(branch) {
|
|
1217
|
+
const ref = `refs/heads/${branch}`;
|
|
1218
|
+
const { status, stderr } = run2("git", ["ls-remote", "--exit-code", "origin", ref]);
|
|
1219
|
+
if (status === 0) return { ok: true, exists: true };
|
|
1220
|
+
if (status === 2) return { ok: true, exists: false };
|
|
1221
|
+
return {
|
|
1222
|
+
ok: false,
|
|
1223
|
+
message: stderr.trim() || `git ls-remote --exit-code origin ${ref} failed.`
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
function resolveReleaseFlow() {
|
|
1227
|
+
const configured = readRawConfig().git?.releaseFlow;
|
|
1228
|
+
if (configured !== void 0) {
|
|
1229
|
+
if (!isReleaseFlow(configured)) return { ok: false, message: invalidReleaseFlowMessage(configured) };
|
|
1230
|
+
return { ok: true, flow: configured, source: "config" };
|
|
1231
|
+
}
|
|
1232
|
+
const develop = probeRemoteBranch("develop");
|
|
1233
|
+
if (!develop.ok) {
|
|
1234
|
+
return {
|
|
1235
|
+
ok: false,
|
|
1236
|
+
message: `Could not check whether origin has a 'develop' branch to choose the release flow.
|
|
1237
|
+
${develop.message}
|
|
1238
|
+
Set it explicitly with: automata config set git-release-flow <gitflow|trunk>`
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
return develop.exists ? { ok: true, flow: "gitflow", source: "develop-present" } : { ok: true, flow: "trunk", source: "develop-absent" };
|
|
1242
|
+
}
|
|
1243
|
+
function checkReleasePreconditions(expectedBranch) {
|
|
1102
1244
|
let branch;
|
|
1103
1245
|
try {
|
|
1104
1246
|
branch = getCurrentBranch();
|
|
1105
1247
|
} catch (err) {
|
|
1106
1248
|
return { ok: false, message: err.message };
|
|
1107
1249
|
}
|
|
1108
|
-
if (branch !==
|
|
1250
|
+
if (branch !== expectedBranch) {
|
|
1109
1251
|
return {
|
|
1110
1252
|
ok: false,
|
|
1111
|
-
message: `publish-release must be run from the '
|
|
1253
|
+
message: `publish-release must be run from the '${expectedBranch}' branch (currently on '${branch}').`
|
|
1112
1254
|
};
|
|
1113
1255
|
}
|
|
1114
1256
|
if (hasUncommittedChanges()) {
|
|
@@ -1119,25 +1261,8 @@ function checkReleasePreconditions() {
|
|
|
1119
1261
|
}
|
|
1120
1262
|
return { ok: true };
|
|
1121
1263
|
}
|
|
1122
|
-
function publishRelease(version2, dryRun, trunk) {
|
|
1123
|
-
const
|
|
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
|
-
};
|
|
1128
|
-
const steps = [
|
|
1129
|
-
{ args: ["checkout", "-b", releaseBranch], desc: `git checkout -b ${releaseBranch}` },
|
|
1130
|
-
checkoutTrunk,
|
|
1131
|
-
{ args: ["merge", "--no-ff", releaseBranch], desc: `git merge --no-ff ${releaseBranch}` },
|
|
1132
|
-
{ args: ["tag", version2], desc: `git tag ${version2}` },
|
|
1133
|
-
{ args: ["checkout", "develop"], desc: `git checkout develop` },
|
|
1134
|
-
{ args: ["merge", "--no-ff", releaseBranch], desc: `git merge --no-ff ${releaseBranch}` },
|
|
1135
|
-
{ args: ["branch", "-d", releaseBranch], desc: `git branch -d ${releaseBranch}` },
|
|
1136
|
-
{
|
|
1137
|
-
args: ["push", "origin", "develop", trunk, version2],
|
|
1138
|
-
desc: `git push origin develop ${trunk} ${version2}`
|
|
1139
|
-
}
|
|
1140
|
-
];
|
|
1264
|
+
function publishRelease(version2, dryRun, trunk, flow) {
|
|
1265
|
+
const steps = planRelease(flow, version2, trunk, flow === "gitflow" && localBranchExists(trunk));
|
|
1141
1266
|
for (const step of steps) {
|
|
1142
1267
|
if (dryRun) {
|
|
1143
1268
|
process.stdout.write(`[dry-run] ${step.desc}
|
|
@@ -1565,10 +1690,13 @@ var finishFeatureCmd = new Command2("finish-feature").description("Clean up a me
|
|
|
1565
1690
|
process.exit(1);
|
|
1566
1691
|
}
|
|
1567
1692
|
});
|
|
1568
|
-
var publishReleaseCmd = new Command2("publish-release").description("Execute the
|
|
1693
|
+
var publishReleaseCmd = new Command2("publish-release").description("Execute the release sequence (GitFlow or trunk-based) 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(
|
|
1569
1694
|
"after",
|
|
1570
1695
|
`
|
|
1571
|
-
Release
|
|
1696
|
+
Release flow: git.releaseFlow in .automata/config.json if set, else detected \u2014
|
|
1697
|
+
gitflow when origin has a 'develop' branch, trunk when it does not.
|
|
1698
|
+
|
|
1699
|
+
GitFlow sequence (run from develop):
|
|
1572
1700
|
1. git checkout -b release/<version>
|
|
1573
1701
|
2. git checkout <trunk> && git merge --no-ff release/<version>
|
|
1574
1702
|
3. git tag <version>
|
|
@@ -1576,6 +1704,11 @@ Release sequence:
|
|
|
1576
1704
|
5. git branch -d release/<version>
|
|
1577
1705
|
6. git push origin develop <trunk> <version>
|
|
1578
1706
|
|
|
1707
|
+
Trunk sequence (run from <trunk>; it may be ahead of origin, not behind):
|
|
1708
|
+
1. git commit --allow-empty -m "chore(release): <version>"
|
|
1709
|
+
2. git tag <version>
|
|
1710
|
+
3. git push --atomic origin <trunk> <version>
|
|
1711
|
+
|
|
1579
1712
|
<trunk> is resolved from origin \u2014 git.trunkBranch in .automata/config.json if
|
|
1580
1713
|
set, else origin/HEAD, the remote's advertised HEAD, or a probe of main/master.
|
|
1581
1714
|
Tags are fetched from origin first, in --dry-run too, so the version a dry run
|
|
@@ -1586,16 +1719,10 @@ and the minor segment is incremented (e.g. 1.2.0 \u2192 1.3.0).
|
|
|
1586
1719
|
|
|
1587
1720
|
CHANGELOG.md must already carry a '## [<version>] - YYYY-MM-DD' section for the
|
|
1588
1721
|
version being released; the release is refused otherwise, in --dry-run too, and
|
|
1589
|
-
before any branch, merge or tag is created. A repository with no
|
|
1590
|
-
not subject to the check.`
|
|
1722
|
+
before any commit, branch, merge or tag is created. A repository with no
|
|
1723
|
+
CHANGELOG.md is not subject to the check.`
|
|
1591
1724
|
).action((version2, options) => {
|
|
1592
1725
|
const dryRun = options.dryRun ?? false;
|
|
1593
|
-
const preconditions = checkReleasePreconditions();
|
|
1594
|
-
if (!preconditions.ok) {
|
|
1595
|
-
process.stderr.write(`Error: ${preconditions.message}
|
|
1596
|
-
`);
|
|
1597
|
-
process.exit(1);
|
|
1598
|
-
}
|
|
1599
1726
|
const trunk = resolveTrunkBranch();
|
|
1600
1727
|
if (!trunk.ok) {
|
|
1601
1728
|
process.stderr.write(`Error: ${unresolvedTrunkMessage(trunk.attempted)}
|
|
@@ -1604,7 +1731,22 @@ not subject to the check.`
|
|
|
1604
1731
|
}
|
|
1605
1732
|
const trunkBranch = trunk.branch;
|
|
1606
1733
|
const trunkRef = `origin/${trunkBranch}`;
|
|
1734
|
+
const flow = resolveReleaseFlow();
|
|
1735
|
+
if (!flow.ok) {
|
|
1736
|
+
process.stderr.write(`Error: ${flow.message}
|
|
1737
|
+
`);
|
|
1738
|
+
process.exit(1);
|
|
1739
|
+
}
|
|
1740
|
+
const releaseFlow = flow.flow;
|
|
1741
|
+
const preconditions = checkReleasePreconditions(releaseFlow === "gitflow" ? "develop" : trunkBranch);
|
|
1742
|
+
if (!preconditions.ok) {
|
|
1743
|
+
process.stderr.write(`Error: ${preconditions.message}
|
|
1744
|
+
`);
|
|
1745
|
+
process.exit(1);
|
|
1746
|
+
}
|
|
1607
1747
|
process.stdout.write(`Trunk branch: ${trunkBranch} (${describeTrunkSource(trunk.source, trunkBranch)})
|
|
1748
|
+
`);
|
|
1749
|
+
process.stdout.write(`Release flow: ${releaseFlow} (${describeReleaseFlowSource(flow.source)})
|
|
1608
1750
|
`);
|
|
1609
1751
|
const fetched = fetchTrunkAndTags(trunkBranch);
|
|
1610
1752
|
if (!fetched.ok) {
|
|
@@ -1651,7 +1793,7 @@ ${fetched.message}
|
|
|
1651
1793
|
`);
|
|
1652
1794
|
}
|
|
1653
1795
|
try {
|
|
1654
|
-
publishRelease(resolvedVersion, dryRun, trunkBranch);
|
|
1796
|
+
publishRelease(resolvedVersion, dryRun, trunkBranch, releaseFlow);
|
|
1655
1797
|
} catch (err) {
|
|
1656
1798
|
process.stderr.write(`Error: ${err.message}
|
|
1657
1799
|
`);
|
|
@@ -1672,7 +1814,9 @@ import { Command as Command3 } from "commander";
|
|
|
1672
1814
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
1673
1815
|
var GITHUB_ISSUE_COMMENT_URL_RE = /github\.com\/([^/]+\/[^/]+)\/issues\/\d+#issuecomment-(\d+)/;
|
|
1674
1816
|
function run3(cmd, args) {
|
|
1817
|
+
const startedAt = Date.now();
|
|
1675
1818
|
const result = spawnSync3(cmd, args, { encoding: "utf8" });
|
|
1819
|
+
recordCommand(cmd, args, startedAt, result.error ? -1 : result.status ?? 1);
|
|
1676
1820
|
if (result.error) {
|
|
1677
1821
|
const err = result.error;
|
|
1678
1822
|
if (err.code === "ENOENT") {
|
|
@@ -1828,7 +1972,9 @@ function addCopilotReviewer(prNumber) {
|
|
|
1828
1972
|
// src/github/ghWorkService.ts
|
|
1829
1973
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
1830
1974
|
function run4(cmd, args) {
|
|
1975
|
+
const startedAt = Date.now();
|
|
1831
1976
|
const result = spawnSync4(cmd, args, { encoding: "utf8" });
|
|
1977
|
+
recordCommand(cmd, args, startedAt, result.error ? -1 : result.status ?? 1);
|
|
1832
1978
|
if (result.error) {
|
|
1833
1979
|
const err = result.error;
|
|
1834
1980
|
if (err.code === "ENOENT") {
|
|
@@ -3666,19 +3812,108 @@ function analyseAnswer(messages, p, marker, watermark) {
|
|
|
3666
3812
|
}
|
|
3667
3813
|
|
|
3668
3814
|
// src/run/runLock.ts
|
|
3669
|
-
import { writeFileSync, readFileSync as
|
|
3815
|
+
import { writeFileSync as writeFileSync2, readFileSync as readFileSync5, unlinkSync as unlinkSync2, mkdirSync, renameSync, linkSync, statSync as statSync2 } from "fs";
|
|
3670
3816
|
import { randomUUID } from "crypto";
|
|
3671
3817
|
import { hostname } from "os";
|
|
3818
|
+
import { join as join4 } from "path";
|
|
3819
|
+
|
|
3820
|
+
// src/run/heartbeat.ts
|
|
3821
|
+
import { unlinkSync, readFileSync as readFileSync4, writeFileSync } from "fs";
|
|
3672
3822
|
import { join as join3 } from "path";
|
|
3823
|
+
var HEARTBEAT_DIR = ".automata";
|
|
3824
|
+
var HEARTBEAT_FILE = "automata-heartbeat.json";
|
|
3825
|
+
var HEARTBEAT_RELATIVE_PATH = `${HEARTBEAT_DIR}/${HEARTBEAT_FILE}`;
|
|
3826
|
+
var PHASES = ["pre-flight", "discovery", "item", "summary"];
|
|
3827
|
+
function isPhase(value) {
|
|
3828
|
+
return typeof value === "string" && PHASES.includes(value);
|
|
3829
|
+
}
|
|
3830
|
+
function heartbeatPath(cwd = process.cwd()) {
|
|
3831
|
+
return join3(cwd, HEARTBEAT_DIR, HEARTBEAT_FILE);
|
|
3832
|
+
}
|
|
3833
|
+
function buildHeartbeat(token, update, now = /* @__PURE__ */ new Date()) {
|
|
3834
|
+
return {
|
|
3835
|
+
token,
|
|
3836
|
+
updatedAt: now.toISOString(),
|
|
3837
|
+
phase: update.phase,
|
|
3838
|
+
item: update.item ?? null,
|
|
3839
|
+
executor: update.executor ?? null
|
|
3840
|
+
};
|
|
3841
|
+
}
|
|
3842
|
+
function formatHeartbeat(heartbeat) {
|
|
3843
|
+
return JSON.stringify(heartbeat, null, 2) + "\n";
|
|
3844
|
+
}
|
|
3845
|
+
function parseHeartbeat(content) {
|
|
3846
|
+
let parsed;
|
|
3847
|
+
try {
|
|
3848
|
+
parsed = JSON.parse(content);
|
|
3849
|
+
} catch {
|
|
3850
|
+
return null;
|
|
3851
|
+
}
|
|
3852
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
3853
|
+
const raw = parsed;
|
|
3854
|
+
if (typeof raw.token !== "string" || raw.token.length === 0) return null;
|
|
3855
|
+
if (typeof raw.updatedAt !== "string") return null;
|
|
3856
|
+
if (!isPhase(raw.phase)) return null;
|
|
3857
|
+
return {
|
|
3858
|
+
token: raw.token,
|
|
3859
|
+
updatedAt: raw.updatedAt,
|
|
3860
|
+
phase: raw.phase,
|
|
3861
|
+
item: parseItem(raw.item),
|
|
3862
|
+
executor: parseExecutor(raw.executor)
|
|
3863
|
+
};
|
|
3864
|
+
}
|
|
3865
|
+
function parseItem(value) {
|
|
3866
|
+
if (typeof value !== "object" || value === null) return null;
|
|
3867
|
+
const raw = value;
|
|
3868
|
+
if (typeof raw.index !== "number" || typeof raw.total !== "number") return null;
|
|
3869
|
+
if (typeof raw.subject !== "string") return null;
|
|
3870
|
+
return { index: raw.index, total: raw.total, subject: raw.subject };
|
|
3871
|
+
}
|
|
3872
|
+
function parseExecutor(value) {
|
|
3873
|
+
if (typeof value !== "object" || value === null) return null;
|
|
3874
|
+
const raw = value;
|
|
3875
|
+
if (typeof raw.command !== "string" || typeof raw.startedAt !== "string") return null;
|
|
3876
|
+
return { command: raw.command, startedAt: raw.startedAt };
|
|
3877
|
+
}
|
|
3878
|
+
function writeHeartbeat(token, update, cwd = process.cwd(), now = /* @__PURE__ */ new Date()) {
|
|
3879
|
+
try {
|
|
3880
|
+
writeFileSync(heartbeatPath(cwd), formatHeartbeat(buildHeartbeat(token, update, now)), "utf8");
|
|
3881
|
+
} catch {
|
|
3882
|
+
}
|
|
3883
|
+
}
|
|
3884
|
+
function clearHeartbeat(token, cwd = process.cwd()) {
|
|
3885
|
+
if (readHeartbeat(token, cwd) === null) return;
|
|
3886
|
+
try {
|
|
3887
|
+
unlinkSync(heartbeatPath(cwd));
|
|
3888
|
+
} catch {
|
|
3889
|
+
}
|
|
3890
|
+
}
|
|
3891
|
+
function readHeartbeat(token, cwd = process.cwd()) {
|
|
3892
|
+
let content;
|
|
3893
|
+
try {
|
|
3894
|
+
content = readFileSync4(heartbeatPath(cwd), "utf8");
|
|
3895
|
+
} catch {
|
|
3896
|
+
return null;
|
|
3897
|
+
}
|
|
3898
|
+
const heartbeat = parseHeartbeat(content);
|
|
3899
|
+
if (heartbeat?.token !== token) return null;
|
|
3900
|
+
return heartbeat;
|
|
3901
|
+
}
|
|
3902
|
+
|
|
3903
|
+
// src/run/runLock.ts
|
|
3673
3904
|
var LOCK_DIR = ".automata";
|
|
3674
3905
|
var LOCK_FILE = "automata.lock";
|
|
3675
3906
|
var RUN_LOCK_RELATIVE_PATH = `${LOCK_DIR}/${LOCK_FILE}`;
|
|
3907
|
+
var AUTOMATA_OWN_PATHS = [
|
|
3908
|
+
RUN_LOCK_RELATIVE_PATH,
|
|
3909
|
+
HEARTBEAT_RELATIVE_PATH
|
|
3910
|
+
];
|
|
3676
3911
|
function lockPath() {
|
|
3677
|
-
return
|
|
3912
|
+
return join4(process.cwd(), LOCK_DIR, LOCK_FILE);
|
|
3678
3913
|
}
|
|
3679
3914
|
function processStartedAt(pid) {
|
|
3680
3915
|
try {
|
|
3681
|
-
const stat =
|
|
3916
|
+
const stat = readFileSync5(`/proc/${String(pid)}/stat`, "utf8");
|
|
3682
3917
|
const afterComm = stat.slice(stat.lastIndexOf(")") + 2);
|
|
3683
3918
|
const field = afterComm.split(" ")[19];
|
|
3684
3919
|
return field === void 0 || field.length === 0 ? null : field;
|
|
@@ -3696,7 +3931,7 @@ function isAlive(pid) {
|
|
|
3696
3931
|
}
|
|
3697
3932
|
function readOwnerError(path) {
|
|
3698
3933
|
try {
|
|
3699
|
-
|
|
3934
|
+
readFileSync5(path, "utf8");
|
|
3700
3935
|
return null;
|
|
3701
3936
|
} catch (err) {
|
|
3702
3937
|
return err.message;
|
|
@@ -3704,7 +3939,7 @@ function readOwnerError(path) {
|
|
|
3704
3939
|
}
|
|
3705
3940
|
function readOwner(path) {
|
|
3706
3941
|
try {
|
|
3707
|
-
const parsed = JSON.parse(
|
|
3942
|
+
const parsed = JSON.parse(readFileSync5(path, "utf8"));
|
|
3708
3943
|
if (typeof parsed.pid !== "number" || typeof parsed.startedAt !== "string") return null;
|
|
3709
3944
|
return {
|
|
3710
3945
|
pid: parsed.pid,
|
|
@@ -3712,7 +3947,8 @@ function readOwner(path) {
|
|
|
3712
3947
|
host: parsed.host ?? "unknown",
|
|
3713
3948
|
command: parsed.command ?? "unknown",
|
|
3714
3949
|
token: parsed.token ?? "",
|
|
3715
|
-
pidStartedAt: parsed.pidStartedAt
|
|
3950
|
+
pidStartedAt: parsed.pidStartedAt,
|
|
3951
|
+
cwd: parsed.cwd
|
|
3716
3952
|
};
|
|
3717
3953
|
} catch {
|
|
3718
3954
|
return null;
|
|
@@ -3741,10 +3977,16 @@ function heldTooLong(owner, staleMinutes) {
|
|
|
3741
3977
|
}
|
|
3742
3978
|
function makeHandle(path, token) {
|
|
3743
3979
|
let released = false;
|
|
3980
|
+
const cwd = process.cwd();
|
|
3744
3981
|
return {
|
|
3982
|
+
heartbeat(update) {
|
|
3983
|
+
if (released) return;
|
|
3984
|
+
writeHeartbeat(token, update, cwd);
|
|
3985
|
+
},
|
|
3745
3986
|
release() {
|
|
3746
3987
|
if (released) return;
|
|
3747
3988
|
released = true;
|
|
3989
|
+
clearHeartbeat(token, cwd);
|
|
3748
3990
|
const current = readOwner(path);
|
|
3749
3991
|
if (current !== null && current.token !== token) {
|
|
3750
3992
|
return;
|
|
@@ -3758,7 +4000,7 @@ function makeHandle(path, token) {
|
|
|
3758
4000
|
const owner = readOwner(takenAway);
|
|
3759
4001
|
if (owner === null || owner.token === token) {
|
|
3760
4002
|
try {
|
|
3761
|
-
|
|
4003
|
+
unlinkSync2(takenAway);
|
|
3762
4004
|
} catch {
|
|
3763
4005
|
}
|
|
3764
4006
|
return;
|
|
@@ -3768,7 +4010,7 @@ function makeHandle(path, token) {
|
|
|
3768
4010
|
} catch {
|
|
3769
4011
|
}
|
|
3770
4012
|
try {
|
|
3771
|
-
|
|
4013
|
+
unlinkSync2(takenAway);
|
|
3772
4014
|
} catch {
|
|
3773
4015
|
}
|
|
3774
4016
|
}
|
|
@@ -3781,11 +4023,12 @@ function publishLock(path, command, token) {
|
|
|
3781
4023
|
host: hostname(),
|
|
3782
4024
|
command,
|
|
3783
4025
|
token,
|
|
3784
|
-
pidStartedAt: processStartedAt(process.pid) ?? void 0
|
|
4026
|
+
pidStartedAt: processStartedAt(process.pid) ?? void 0,
|
|
4027
|
+
cwd: process.cwd()
|
|
3785
4028
|
};
|
|
3786
4029
|
const staging = `${path}.staging.${token}`;
|
|
3787
4030
|
try {
|
|
3788
|
-
|
|
4031
|
+
writeFileSync2(staging, JSON.stringify(owner, null, 2) + "\n", { encoding: "utf8", flag: "wx" });
|
|
3789
4032
|
} catch {
|
|
3790
4033
|
return false;
|
|
3791
4034
|
}
|
|
@@ -3797,7 +4040,7 @@ function publishLock(path, command, token) {
|
|
|
3797
4040
|
return false;
|
|
3798
4041
|
} finally {
|
|
3799
4042
|
try {
|
|
3800
|
-
|
|
4043
|
+
unlinkSync2(staging);
|
|
3801
4044
|
} catch {
|
|
3802
4045
|
}
|
|
3803
4046
|
}
|
|
@@ -3805,7 +4048,7 @@ function publishLock(path, command, token) {
|
|
|
3805
4048
|
function acquireRunLock(command, staleMinutes) {
|
|
3806
4049
|
const path = lockPath();
|
|
3807
4050
|
const token = randomUUID();
|
|
3808
|
-
mkdirSync(
|
|
4051
|
+
mkdirSync(join4(process.cwd(), LOCK_DIR), { recursive: true });
|
|
3809
4052
|
if (publishLock(path, command, token)) {
|
|
3810
4053
|
return { ok: true, handle: makeHandle(path, token) };
|
|
3811
4054
|
}
|
|
@@ -3840,7 +4083,7 @@ function claimStaleLock(path, token, expected) {
|
|
|
3840
4083
|
} catch {
|
|
3841
4084
|
}
|
|
3842
4085
|
try {
|
|
3843
|
-
|
|
4086
|
+
unlinkSync2(claimed);
|
|
3844
4087
|
} catch {
|
|
3845
4088
|
}
|
|
3846
4089
|
return false;
|
|
@@ -3852,7 +4095,7 @@ var CLAIM_STALE_MS = 6e4;
|
|
|
3852
4095
|
function acquireClaim(path) {
|
|
3853
4096
|
const claimPath = `${path}.claim`;
|
|
3854
4097
|
try {
|
|
3855
|
-
|
|
4098
|
+
writeFileSync2(claimPath, JSON.stringify({ pid: process.pid, at: (/* @__PURE__ */ new Date()).toISOString() }), {
|
|
3856
4099
|
encoding: "utf8",
|
|
3857
4100
|
flag: "wx"
|
|
3858
4101
|
});
|
|
@@ -3867,11 +4110,11 @@ function acquireClaim(path) {
|
|
|
3867
4110
|
return null;
|
|
3868
4111
|
}
|
|
3869
4112
|
try {
|
|
3870
|
-
|
|
4113
|
+
unlinkSync2(`${claimPath}.abandoned.${String(process.pid)}`);
|
|
3871
4114
|
} catch {
|
|
3872
4115
|
}
|
|
3873
4116
|
try {
|
|
3874
|
-
|
|
4117
|
+
writeFileSync2(claimPath, JSON.stringify({ pid: process.pid, at: (/* @__PURE__ */ new Date()).toISOString() }), {
|
|
3875
4118
|
encoding: "utf8",
|
|
3876
4119
|
flag: "wx"
|
|
3877
4120
|
});
|
|
@@ -3883,7 +4126,7 @@ function acquireClaim(path) {
|
|
|
3883
4126
|
function claimIsAbandoned(claimPath) {
|
|
3884
4127
|
let at = Number.NaN;
|
|
3885
4128
|
try {
|
|
3886
|
-
const raw = JSON.parse(
|
|
4129
|
+
const raw = JSON.parse(readFileSync5(claimPath, "utf8"));
|
|
3887
4130
|
if (raw.at !== void 0) at = Date.parse(raw.at);
|
|
3888
4131
|
} catch {
|
|
3889
4132
|
}
|
|
@@ -3915,13 +4158,13 @@ function reclaim(path, command, token, expected) {
|
|
|
3915
4158
|
return { ok: false, heldBy: readOwner(path) ?? UNKNOWN_OWNER, suspect: false };
|
|
3916
4159
|
} finally {
|
|
3917
4160
|
try {
|
|
3918
|
-
|
|
4161
|
+
unlinkSync2(claimed);
|
|
3919
4162
|
} catch {
|
|
3920
4163
|
}
|
|
3921
4164
|
}
|
|
3922
4165
|
} finally {
|
|
3923
4166
|
try {
|
|
3924
|
-
|
|
4167
|
+
unlinkSync2(claimPath);
|
|
3925
4168
|
} catch {
|
|
3926
4169
|
}
|
|
3927
4170
|
}
|
|
@@ -3946,7 +4189,12 @@ function inspectRunLock(staleMinutes, now = Date.now()) {
|
|
|
3946
4189
|
}
|
|
3947
4190
|
const held = owner;
|
|
3948
4191
|
const kind = heldTooLong(held, staleMinutes) ? "suspect" : "held";
|
|
3949
|
-
return {
|
|
4192
|
+
return {
|
|
4193
|
+
kind,
|
|
4194
|
+
owner: held,
|
|
4195
|
+
heldForMs: heldForMs(held, now),
|
|
4196
|
+
heartbeat: readHeartbeat(held.token, held.cwd ?? process.cwd())
|
|
4197
|
+
};
|
|
3950
4198
|
}
|
|
3951
4199
|
|
|
3952
4200
|
// src/git/workspaceService.ts
|
|
@@ -3958,7 +4206,7 @@ function dirtyTree() {
|
|
|
3958
4206
|
};
|
|
3959
4207
|
}
|
|
3960
4208
|
function prepareBaseBranch(baseBranch) {
|
|
3961
|
-
if (hasUncommittedChanges([
|
|
4209
|
+
if (hasUncommittedChanges([...AUTOMATA_OWN_PATHS])) return dirtyTree();
|
|
3962
4210
|
const checkout = checkoutBranch(baseBranch);
|
|
3963
4211
|
if (!checkout.ok) {
|
|
3964
4212
|
return { ok: false, reason: "checkout-failed", detail: checkout.stderr };
|
|
@@ -4054,7 +4302,7 @@ function divergenceRefusal(headRefName, pullError) {
|
|
|
4054
4302
|
};
|
|
4055
4303
|
}
|
|
4056
4304
|
function preparePrBranch(headRefName) {
|
|
4057
|
-
if (hasUncommittedChanges([
|
|
4305
|
+
if (hasUncommittedChanges([...AUTOMATA_OWN_PATHS])) return dirtyTree();
|
|
4058
4306
|
const previousRemoteSha = revParse(`refs/remotes/origin/${headRefName}`);
|
|
4059
4307
|
const fetched = fetchBranch(headRefName);
|
|
4060
4308
|
if (!fetched.ok) {
|
|
@@ -4130,7 +4378,7 @@ function findOpenPr(branch) {
|
|
|
4130
4378
|
return prs.find((pr) => pr.state === "OPEN") ?? null;
|
|
4131
4379
|
}
|
|
4132
4380
|
function rescueUncommittedChanges(options, now) {
|
|
4133
|
-
if (!hasUncommittedChanges([
|
|
4381
|
+
if (!hasUncommittedChanges([...AUTOMATA_OWN_PATHS])) {
|
|
4134
4382
|
options.log(" rescue nothing to do; the working tree is clean\n");
|
|
4135
4383
|
return { kind: "clean" };
|
|
4136
4384
|
}
|
|
@@ -4143,7 +4391,7 @@ function rescueUncommittedChanges(options, now) {
|
|
|
4143
4391
|
);
|
|
4144
4392
|
return { kind: "would-rescue", branch: target.branch, createdBranch: target.createdBranch };
|
|
4145
4393
|
}
|
|
4146
|
-
const staged = stageAllExcept([
|
|
4394
|
+
const staged = stageAllExcept([...AUTOMATA_OWN_PATHS]);
|
|
4147
4395
|
if (!staged.ok) {
|
|
4148
4396
|
options.log(` rescue FAILED to stage the changes: ${staged.stderr}
|
|
4149
4397
|
`);
|
|
@@ -4422,13 +4670,13 @@ import {
|
|
|
4422
4670
|
accessSync as accessSync2,
|
|
4423
4671
|
appendFileSync,
|
|
4424
4672
|
constants as constants2,
|
|
4425
|
-
readFileSync as
|
|
4673
|
+
readFileSync as readFileSync6,
|
|
4426
4674
|
renameSync as renameSync2,
|
|
4427
|
-
unlinkSync as
|
|
4428
|
-
writeFileSync as
|
|
4675
|
+
unlinkSync as unlinkSync3,
|
|
4676
|
+
writeFileSync as writeFileSync3
|
|
4429
4677
|
} from "fs";
|
|
4430
4678
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
4431
|
-
import { dirname as dirname2, join as
|
|
4679
|
+
import { dirname as dirname2, join as join5 } from "path";
|
|
4432
4680
|
var EXECUTION_LOG_FILE = "automata-execution.log";
|
|
4433
4681
|
var WORK_LOG_FILE = "automata-work.log";
|
|
4434
4682
|
var MAX_EXECUTION_LINES = 1e3;
|
|
@@ -4444,6 +4692,14 @@ var OUTCOMES = [
|
|
|
4444
4692
|
function operationLogDirectory() {
|
|
4445
4693
|
return dirname2(process.cwd());
|
|
4446
4694
|
}
|
|
4695
|
+
function inspectLogDirectory(dir = operationLogDirectory(), cwd = process.cwd()) {
|
|
4696
|
+
try {
|
|
4697
|
+
accessSync2(dir, constants2.W_OK);
|
|
4698
|
+
return { dir, cwd, writable: true, detail: null };
|
|
4699
|
+
} catch (err) {
|
|
4700
|
+
return { dir, cwd, writable: false, detail: err.message };
|
|
4701
|
+
}
|
|
4702
|
+
}
|
|
4447
4703
|
function repoField(repo) {
|
|
4448
4704
|
if (repo === null) return "-";
|
|
4449
4705
|
const flat = oneLine(repo);
|
|
@@ -4550,18 +4806,18 @@ function pruneOldRecords(content, now, maxAgeMs) {
|
|
|
4550
4806
|
}).map((group) => group.lines.join("\n") + "\n").join("");
|
|
4551
4807
|
}
|
|
4552
4808
|
function appendWithRetention(dir, file, content, retain) {
|
|
4553
|
-
const target =
|
|
4809
|
+
const target = join5(dir, file);
|
|
4554
4810
|
appendFileSync(target, content, "utf8");
|
|
4555
|
-
const existing =
|
|
4811
|
+
const existing = readFileSync6(target, "utf8");
|
|
4556
4812
|
const retained = retain(existing);
|
|
4557
4813
|
if (retained === existing) return;
|
|
4558
4814
|
const temp = `${target}.${randomUUID2()}.tmp`;
|
|
4559
4815
|
try {
|
|
4560
|
-
|
|
4816
|
+
writeFileSync3(temp, retained, "utf8");
|
|
4561
4817
|
renameSync2(temp, target);
|
|
4562
4818
|
} finally {
|
|
4563
4819
|
try {
|
|
4564
|
-
|
|
4820
|
+
unlinkSync3(temp);
|
|
4565
4821
|
} catch {
|
|
4566
4822
|
}
|
|
4567
4823
|
}
|
|
@@ -4599,7 +4855,7 @@ function isOutcome(value) {
|
|
|
4599
4855
|
}
|
|
4600
4856
|
function readLogFile(path) {
|
|
4601
4857
|
try {
|
|
4602
|
-
return { content:
|
|
4858
|
+
return { content: readFileSync6(path, "utf8"), error: null };
|
|
4603
4859
|
} catch (err) {
|
|
4604
4860
|
if (err.code === "ENOENT") return { content: null, error: null };
|
|
4605
4861
|
return { content: null, error: err.message };
|
|
@@ -4665,7 +4921,7 @@ function parseExecutionLine(line) {
|
|
|
4665
4921
|
};
|
|
4666
4922
|
}
|
|
4667
4923
|
function readExecutionTicks(options = {}) {
|
|
4668
|
-
const path =
|
|
4924
|
+
const path = join5(options.dir ?? operationLogDirectory(), EXECUTION_LOG_FILE);
|
|
4669
4925
|
const { content, error } = readLogFile(path);
|
|
4670
4926
|
if (content === null) return emptyResult(path, error, isFiltered(options.repo));
|
|
4671
4927
|
const entries = [];
|
|
@@ -4727,7 +4983,7 @@ function parseWorkItem(line) {
|
|
|
4727
4983
|
};
|
|
4728
4984
|
}
|
|
4729
4985
|
function readWorkRecords(options = {}) {
|
|
4730
|
-
const path =
|
|
4986
|
+
const path = join5(options.dir ?? operationLogDirectory(), WORK_LOG_FILE);
|
|
4731
4987
|
const { content, error } = readLogFile(path);
|
|
4732
4988
|
if (content === null) return emptyResult(path, error, isFiltered(options.repo));
|
|
4733
4989
|
const entries = [];
|
|
@@ -4779,11 +5035,14 @@ function readWorkRecords(options = {}) {
|
|
|
4779
5035
|
import { spawnSync as spawnSync7 } from "child_process";
|
|
4780
5036
|
var GIT_BIN = resolveCommand("git");
|
|
4781
5037
|
function git(args) {
|
|
5038
|
+
const startedAt = Date.now();
|
|
4782
5039
|
const result = spawnSync7(GIT_BIN, args, { encoding: "utf8" });
|
|
5040
|
+
const status = result.status ?? 1;
|
|
5041
|
+
recordCommand(GIT_BIN, args, startedAt, result.error ? -1 : status);
|
|
4783
5042
|
return {
|
|
4784
5043
|
stdout: result.stdout ?? "",
|
|
4785
5044
|
stderr: result.stderr ?? "",
|
|
4786
|
-
status
|
|
5045
|
+
status
|
|
4787
5046
|
};
|
|
4788
5047
|
}
|
|
4789
5048
|
function notARepo(baseBranch, detail) {
|
|
@@ -4804,7 +5063,8 @@ function notARepo(baseBranch, detail) {
|
|
|
4804
5063
|
};
|
|
4805
5064
|
}
|
|
4806
5065
|
function isOwnLockFile(porcelainEntry) {
|
|
4807
|
-
|
|
5066
|
+
const path = porcelainEntry.slice(3).trim();
|
|
5067
|
+
return AUTOMATA_OWN_PATHS.some((own) => path.endsWith(own));
|
|
4808
5068
|
}
|
|
4809
5069
|
function divergence(upstream, branch) {
|
|
4810
5070
|
const { stdout, status } = git([
|
|
@@ -4890,6 +5150,9 @@ function inspectRepoStatus(options) {
|
|
|
4890
5150
|
}
|
|
4891
5151
|
|
|
4892
5152
|
// src/run/checkReport.ts
|
|
5153
|
+
function finding(summary, command) {
|
|
5154
|
+
return { summary, command };
|
|
5155
|
+
}
|
|
4893
5156
|
var SECTION_ORDER = [
|
|
4894
5157
|
"lock",
|
|
4895
5158
|
"ticks",
|
|
@@ -4951,36 +5214,84 @@ function build(id, lines, problems, data) {
|
|
|
4951
5214
|
id,
|
|
4952
5215
|
title: SECTION_TITLES[id],
|
|
4953
5216
|
lines,
|
|
4954
|
-
problems: problems.map((
|
|
5217
|
+
problems: problems.map((item) => ({ section: id, ...item })),
|
|
4955
5218
|
data
|
|
4956
5219
|
};
|
|
4957
5220
|
}
|
|
4958
5221
|
function describeOwner(owner) {
|
|
4959
5222
|
return `pid ${String(owner.pid)} on ${owner.host}, \`${owner.command}\`, since ${owner.startedAt}`;
|
|
4960
5223
|
}
|
|
4961
|
-
function
|
|
4962
|
-
|
|
5224
|
+
function describeOwnerLocation(owner, context, lines, problems) {
|
|
5225
|
+
if (owner.cwd === void 0) {
|
|
5226
|
+
lines.push(" working directory: not recorded (lock written by an older automata)");
|
|
5227
|
+
return;
|
|
5228
|
+
}
|
|
5229
|
+
lines.push(` working directory: ${owner.cwd}`);
|
|
5230
|
+
if (owner.cwd === context.cwd) return;
|
|
5231
|
+
const ownerLogs = context.parentOf(owner.cwd);
|
|
5232
|
+
lines.push(` logs to ${ownerLogs} (this check reads ${context.logDirectory})`);
|
|
5233
|
+
problems.push(
|
|
5234
|
+
finding(
|
|
5235
|
+
`the tick holding the lock runs from \`${owner.cwd}\` and writes its operation logs to \`${ownerLogs}\`, while this check reads \`${context.logDirectory}\` \u2014 the two will not agree about what has run; run the check from the directory the scheduler uses`,
|
|
5236
|
+
`ls -l ${ownerLogs}/automata-execution.log`
|
|
5237
|
+
)
|
|
5238
|
+
);
|
|
5239
|
+
}
|
|
5240
|
+
function describeHeartbeat(heartbeat, now, lines) {
|
|
5241
|
+
if (heartbeat === void 0 || heartbeat === null) {
|
|
5242
|
+
lines.push(" phase: not reported (no heartbeat from this holder)");
|
|
5243
|
+
return;
|
|
5244
|
+
}
|
|
5245
|
+
const updated = Date.parse(heartbeat.updatedAt);
|
|
5246
|
+
const age = Number.isNaN(updated) ? "unknown" : `${describeDuration(now.getTime() - updated)} ago`;
|
|
5247
|
+
const item = heartbeat.item === null ? "" : ` \u2014 item ${String(heartbeat.item.index)} of ${String(heartbeat.item.total)}, ${heartbeat.item.subject}`;
|
|
5248
|
+
lines.push(` phase: ${heartbeat.phase}${item} (updated ${age})`);
|
|
5249
|
+
if (heartbeat.executor === null) return;
|
|
5250
|
+
const started = Date.parse(heartbeat.executor.startedAt);
|
|
5251
|
+
const running = Number.isNaN(started) ? `since ${heartbeat.executor.startedAt}` : `running for ${describeDuration(now.getTime() - started)}`;
|
|
5252
|
+
lines.push(` executor: ${heartbeat.executor.command}, ${running}`);
|
|
5253
|
+
}
|
|
5254
|
+
function lockSection(status, staleMinutes, context, now = /* @__PURE__ */ new Date()) {
|
|
5255
|
+
const data = {
|
|
5256
|
+
status: status.kind,
|
|
5257
|
+
staleMinutes,
|
|
5258
|
+
cwd: context.cwd,
|
|
5259
|
+
logDirectory: context.logDirectory
|
|
5260
|
+
};
|
|
4963
5261
|
switch (status.kind) {
|
|
4964
5262
|
case "free":
|
|
4965
5263
|
return build("lock", ["no tick is running in this checkout"], [], data);
|
|
4966
5264
|
case "held": {
|
|
4967
5265
|
const held = status.heldForMs === null ? "" : ` (${describeDuration(status.heldForMs)} so far)`;
|
|
4968
|
-
|
|
5266
|
+
const lines = [`a tick is running: ${describeOwner(status.owner)}${held}`];
|
|
5267
|
+
const problems = [];
|
|
5268
|
+
describeOwnerLocation(status.owner, context, lines, problems);
|
|
5269
|
+
describeHeartbeat(status.heartbeat, now, lines);
|
|
5270
|
+
return build("lock", lines, problems, {
|
|
4969
5271
|
...data,
|
|
4970
5272
|
owner: status.owner,
|
|
4971
|
-
heldForMs: status.heldForMs
|
|
5273
|
+
heldForMs: status.heldForMs,
|
|
5274
|
+
heartbeat: status.heartbeat ?? null
|
|
4972
5275
|
});
|
|
4973
5276
|
}
|
|
4974
5277
|
case "suspect": {
|
|
4975
5278
|
const held = status.heldForMs === null ? "unknown" : describeDuration(status.heldForMs);
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
5279
|
+
const lines = [`a tick has held the lock for ${held}: ${describeOwner(status.owner)}`];
|
|
5280
|
+
const problems = [];
|
|
5281
|
+
describeOwnerLocation(status.owner, context, lines, problems);
|
|
5282
|
+
describeHeartbeat(status.heartbeat, now, lines);
|
|
5283
|
+
problems.push(
|
|
5284
|
+
finding(
|
|
5285
|
+
`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\``,
|
|
5286
|
+
`ps -p ${String(status.owner.pid)} -o pid,lstart,args`
|
|
5287
|
+
)
|
|
4983
5288
|
);
|
|
5289
|
+
return build("lock", lines, problems, {
|
|
5290
|
+
...data,
|
|
5291
|
+
owner: status.owner,
|
|
5292
|
+
heldForMs: status.heldForMs,
|
|
5293
|
+
heartbeat: status.heartbeat ?? null
|
|
5294
|
+
});
|
|
4984
5295
|
}
|
|
4985
5296
|
case "stale":
|
|
4986
5297
|
return build(
|
|
@@ -4989,7 +5300,10 @@ function lockSection(status, staleMinutes) {
|
|
|
4989
5300
|
status.owner === null ? "a run lock exists but could not be parsed" : `a stale run lock is present: ${describeOwner(status.owner)}`
|
|
4990
5301
|
],
|
|
4991
5302
|
[
|
|
4992
|
-
|
|
5303
|
+
finding(
|
|
5304
|
+
"a stale run lock is present; the next tick reclaims it automatically, so no action is needed unless ticks keep being turned away",
|
|
5305
|
+
"cat .automata/automata.lock"
|
|
5306
|
+
)
|
|
4993
5307
|
],
|
|
4994
5308
|
{ ...data, owner: status.owner, heldForMs: status.heldForMs }
|
|
4995
5309
|
);
|
|
@@ -4997,7 +5311,12 @@ function lockSection(status, staleMinutes) {
|
|
|
4997
5311
|
return build(
|
|
4998
5312
|
"lock",
|
|
4999
5313
|
[`the run lock could not be read: ${status.detail}`],
|
|
5000
|
-
[
|
|
5314
|
+
[
|
|
5315
|
+
finding(
|
|
5316
|
+
`the run lock at \`.automata/automata.lock\` could not be read: ${status.detail}`,
|
|
5317
|
+
"ls -l .automata/automata.lock"
|
|
5318
|
+
)
|
|
5319
|
+
],
|
|
5001
5320
|
{ ...data, detail: status.detail }
|
|
5002
5321
|
);
|
|
5003
5322
|
}
|
|
@@ -5019,28 +5338,56 @@ function describeMissingTicks(read) {
|
|
|
5019
5338
|
if (read.error !== null) {
|
|
5020
5339
|
return {
|
|
5021
5340
|
line: `the execution log could not be read: ${read.error}`,
|
|
5022
|
-
problem:
|
|
5341
|
+
problem: finding(
|
|
5342
|
+
`the execution log \`${read.path}\` could not be read: ${read.error}`,
|
|
5343
|
+
`ls -l ${read.path}`
|
|
5344
|
+
),
|
|
5345
|
+
explainedByLiveTick: false
|
|
5023
5346
|
};
|
|
5024
5347
|
}
|
|
5025
5348
|
if (!read.present) {
|
|
5026
5349
|
return {
|
|
5027
5350
|
line: `no execution log at ${read.path}`,
|
|
5028
|
-
problem:
|
|
5351
|
+
problem: finding(
|
|
5352
|
+
`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`,
|
|
5353
|
+
`ls -la ${read.path}`
|
|
5354
|
+
),
|
|
5355
|
+
explainedByLiveTick: true
|
|
5029
5356
|
};
|
|
5030
5357
|
}
|
|
5031
5358
|
if (read.entries.length === 0) {
|
|
5032
5359
|
return {
|
|
5033
5360
|
line: "the execution log holds no tick for this repository",
|
|
5034
|
-
problem:
|
|
5361
|
+
problem: finding(
|
|
5362
|
+
"the execution log holds no tick for this repository \u2014 the scheduler has never successfully run `do-work` here",
|
|
5363
|
+
`tail -5 ${read.path}`
|
|
5364
|
+
),
|
|
5365
|
+
explainedByLiveTick: true
|
|
5035
5366
|
};
|
|
5036
5367
|
}
|
|
5037
5368
|
return null;
|
|
5038
5369
|
}
|
|
5039
|
-
function
|
|
5370
|
+
function describeLogDirectory(directory, lines, problems) {
|
|
5371
|
+
const writable = directory.writable ? "writable" : `not writable: ${directory.detail ?? "unknown"}`;
|
|
5372
|
+
lines.push(
|
|
5373
|
+
`log directory: ${directory.dir} (the parent of the working directory ${directory.cwd}), ${writable}`
|
|
5374
|
+
);
|
|
5375
|
+
if (directory.writable) return;
|
|
5376
|
+
problems.push(
|
|
5377
|
+
finding(
|
|
5378
|
+
`the operation log directory \`${directory.dir}\` is not writable (${directory.detail ?? "unknown"}), so every tick records nothing and this report can only ever be blank; make it writable by the account the scheduler runs as`,
|
|
5379
|
+
`ls -ld ${directory.dir}`
|
|
5380
|
+
)
|
|
5381
|
+
);
|
|
5382
|
+
}
|
|
5383
|
+
function describeTickHistory(ticks, cadence, now, logPath, lines, problems) {
|
|
5040
5384
|
lines.push(`last tick: ${describeTick(ticks[0], now)}`);
|
|
5041
5385
|
if (ticks[0].exitCode !== 0) {
|
|
5042
5386
|
problems.push(
|
|
5043
|
-
|
|
5387
|
+
finding(
|
|
5388
|
+
`the last tick exited ${String(ticks[0].exitCode)} \u2014 see \`Last work\` below and the work log for the item that failed`,
|
|
5389
|
+
"automata do-work --check --verbose"
|
|
5390
|
+
)
|
|
5044
5391
|
);
|
|
5045
5392
|
}
|
|
5046
5393
|
const lockHeld = ticks.filter((tick) => tick.note === "lock-held").length;
|
|
@@ -5050,7 +5397,10 @@ function describeTickHistory(ticks, cadence, now, lines, problems) {
|
|
|
5050
5397
|
);
|
|
5051
5398
|
if (lockHeld === ticks.length && ticks.length > 1) {
|
|
5052
5399
|
problems.push(
|
|
5053
|
-
|
|
5400
|
+
finding(
|
|
5401
|
+
"every recorded tick was turned away by a held run lock \u2014 a previous tick is wedged; see `Run lock` above",
|
|
5402
|
+
"cat .automata/automata.lock"
|
|
5403
|
+
)
|
|
5054
5404
|
);
|
|
5055
5405
|
}
|
|
5056
5406
|
if (cadence.medianIntervalMs === null) {
|
|
@@ -5060,24 +5410,38 @@ function describeTickHistory(ticks, cadence, now, lines, problems) {
|
|
|
5060
5410
|
lines.push(`cadence: about one tick every ${describeDuration(cadence.medianIntervalMs)}`);
|
|
5061
5411
|
if (cadence.silent) {
|
|
5062
5412
|
problems.push(
|
|
5063
|
-
|
|
5413
|
+
finding(
|
|
5414
|
+
`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)`,
|
|
5415
|
+
// The log's own tail is the only thing automata can offer: the
|
|
5416
|
+
// scheduler belongs to the host, and naming a `crontab -l` here would
|
|
5417
|
+
// be a guess about how this installation is driven.
|
|
5418
|
+
`tail -5 ${logPath}`
|
|
5419
|
+
)
|
|
5064
5420
|
);
|
|
5065
5421
|
}
|
|
5066
5422
|
}
|
|
5067
5423
|
function unfilteredLine(unit) {
|
|
5068
5424
|
return `not filtered by repository: the slug could not be resolved, so ${unit} from other checkouts may be shown`;
|
|
5069
5425
|
}
|
|
5070
|
-
function tickSection(read, now) {
|
|
5426
|
+
function tickSection(read, now, directory, lockStatus) {
|
|
5071
5427
|
const lines = [];
|
|
5072
5428
|
const problems = [];
|
|
5073
5429
|
const ticks = read.entries;
|
|
5074
5430
|
const cadence = tickCadence(ticks, now);
|
|
5431
|
+
const tickInFlight = lockStatus.kind === "held" || lockStatus.kind === "suspect";
|
|
5432
|
+
describeLogDirectory(directory, lines, problems);
|
|
5075
5433
|
const missing = describeMissingTicks(read);
|
|
5076
5434
|
if (missing !== null) {
|
|
5077
5435
|
lines.push(missing.line);
|
|
5078
|
-
|
|
5436
|
+
if (tickInFlight && missing.explainedByLiveTick) {
|
|
5437
|
+
lines.push(
|
|
5438
|
+
"a tick is running and has not recorded itself yet \u2014 the log is written when a tick ends, so this is the expected state for a first tick still in flight"
|
|
5439
|
+
);
|
|
5440
|
+
} else {
|
|
5441
|
+
problems.push(missing.problem);
|
|
5442
|
+
}
|
|
5079
5443
|
} else {
|
|
5080
|
-
describeTickHistory(ticks, cadence, now, lines, problems);
|
|
5444
|
+
describeTickHistory(ticks, cadence, now, read.path, lines, problems);
|
|
5081
5445
|
}
|
|
5082
5446
|
if (!read.filtered && read.present) lines.push(unfilteredLine("line(s)"));
|
|
5083
5447
|
if (read.skipped > 0)
|
|
@@ -5095,7 +5459,9 @@ function tickSection(read, now) {
|
|
|
5095
5459
|
otherRepos: read.otherRepos,
|
|
5096
5460
|
filtered: read.filtered,
|
|
5097
5461
|
logPath: read.path,
|
|
5098
|
-
logPresent: read.present
|
|
5462
|
+
logPresent: read.present,
|
|
5463
|
+
logDirectory: directory,
|
|
5464
|
+
tickInFlight
|
|
5099
5465
|
});
|
|
5100
5466
|
}
|
|
5101
5467
|
function describeWorkItem(item) {
|
|
@@ -5115,7 +5481,9 @@ function workSection(read, now) {
|
|
|
5115
5481
|
const records = read.entries;
|
|
5116
5482
|
if (read.error !== null) {
|
|
5117
5483
|
lines.push(`the work log could not be read: ${read.error}`);
|
|
5118
|
-
problems.push(
|
|
5484
|
+
problems.push(
|
|
5485
|
+
finding(`the work log \`${read.path}\` could not be read: ${read.error}`, `ls -l ${read.path}`)
|
|
5486
|
+
);
|
|
5119
5487
|
} else if (!read.present || records.length === 0) {
|
|
5120
5488
|
lines.push("no tick has invoked the executor in the retained window");
|
|
5121
5489
|
} else {
|
|
@@ -5138,7 +5506,12 @@ function workSection(read, now) {
|
|
|
5138
5506
|
function describeCheckout(status, lines, problems) {
|
|
5139
5507
|
if (status.branch === null) {
|
|
5140
5508
|
lines.push(`HEAD is detached at ${status.head ?? "an unknown commit"}`);
|
|
5141
|
-
problems.push(
|
|
5509
|
+
problems.push(
|
|
5510
|
+
finding(
|
|
5511
|
+
"HEAD is detached; the pre-flight expects a branch, so check one out",
|
|
5512
|
+
"git status --short --branch"
|
|
5513
|
+
)
|
|
5514
|
+
);
|
|
5142
5515
|
} else {
|
|
5143
5516
|
const at = status.head === null ? "" : ` at ${status.head}`;
|
|
5144
5517
|
lines.push(`on ${status.branch}${at}`);
|
|
@@ -5146,7 +5519,10 @@ function describeCheckout(status, lines, problems) {
|
|
|
5146
5519
|
if (status.statusError !== null) {
|
|
5147
5520
|
lines.push(`the working tree could not be inspected: ${status.statusError}`);
|
|
5148
5521
|
problems.push(
|
|
5149
|
-
|
|
5522
|
+
finding(
|
|
5523
|
+
`\`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`,
|
|
5524
|
+
"git status --porcelain"
|
|
5525
|
+
)
|
|
5150
5526
|
);
|
|
5151
5527
|
return;
|
|
5152
5528
|
}
|
|
@@ -5157,21 +5533,30 @@ function describeCheckout(status, lines, problems) {
|
|
|
5157
5533
|
lines.push(`working tree has ${String(status.dirtyPaths.length)} uncommitted change(s):`);
|
|
5158
5534
|
for (const path of status.dirtyPaths) lines.push(` ${path}`);
|
|
5159
5535
|
problems.push(
|
|
5160
|
-
|
|
5536
|
+
finding(
|
|
5537
|
+
`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`,
|
|
5538
|
+
"git status --porcelain"
|
|
5539
|
+
)
|
|
5161
5540
|
);
|
|
5162
5541
|
}
|
|
5163
5542
|
function describeBaseBranch(status, lines, problems) {
|
|
5164
5543
|
if (!status.baseLocal) {
|
|
5165
5544
|
lines.push(`base branch ${status.baseBranch} does not exist in this checkout`);
|
|
5166
5545
|
problems.push(
|
|
5167
|
-
|
|
5546
|
+
finding(
|
|
5547
|
+
`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>\``,
|
|
5548
|
+
"git branch --list --all"
|
|
5549
|
+
)
|
|
5168
5550
|
);
|
|
5169
5551
|
return;
|
|
5170
5552
|
}
|
|
5171
5553
|
if (status.upstream === null) {
|
|
5172
5554
|
lines.push(`base branch ${status.baseBranch} has no upstream`);
|
|
5173
5555
|
problems.push(
|
|
5174
|
-
|
|
5556
|
+
finding(
|
|
5557
|
+
`the base branch \`${status.baseBranch}\` has no upstream, so the pre-flight cannot fast-forward it`,
|
|
5558
|
+
`git branch -vv --list ${status.baseBranch}`
|
|
5559
|
+
)
|
|
5175
5560
|
);
|
|
5176
5561
|
return;
|
|
5177
5562
|
}
|
|
@@ -5180,7 +5565,10 @@ function describeBaseBranch(status, lines, problems) {
|
|
|
5180
5565
|
`base branch ${status.baseBranch} has no tracking configuration; counted against ${status.upstream}`
|
|
5181
5566
|
);
|
|
5182
5567
|
problems.push(
|
|
5183
|
-
|
|
5568
|
+
finding(
|
|
5569
|
+
`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}\``,
|
|
5570
|
+
`git branch -vv --list ${status.baseBranch}`
|
|
5571
|
+
)
|
|
5184
5572
|
);
|
|
5185
5573
|
}
|
|
5186
5574
|
const freshness = status.refreshed ? "" : " (not refreshed)";
|
|
@@ -5191,14 +5579,20 @@ function describeBaseBranch(status, lines, problems) {
|
|
|
5191
5579
|
);
|
|
5192
5580
|
if (status.ahead === null || status.behind === null) {
|
|
5193
5581
|
problems.push(
|
|
5194
|
-
|
|
5582
|
+
finding(
|
|
5583
|
+
`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`,
|
|
5584
|
+
`git rev-list --left-right --count ${status.upstream}...${status.baseBranch}`
|
|
5585
|
+
)
|
|
5195
5586
|
);
|
|
5196
5587
|
return;
|
|
5197
5588
|
}
|
|
5198
5589
|
if (status.ahead === 0) return;
|
|
5199
5590
|
if (status.behind > 0) {
|
|
5200
5591
|
problems.push(
|
|
5201
|
-
|
|
5592
|
+
finding(
|
|
5593
|
+
`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`,
|
|
5594
|
+
`git log --oneline --left-right ${status.upstream}...${status.baseBranch}`
|
|
5595
|
+
)
|
|
5202
5596
|
);
|
|
5203
5597
|
return;
|
|
5204
5598
|
}
|
|
@@ -5211,7 +5605,7 @@ function gitSection(status) {
|
|
|
5211
5605
|
return build(
|
|
5212
5606
|
"git",
|
|
5213
5607
|
[`not a usable git repository: ${status.error}`],
|
|
5214
|
-
[`not a usable git repository: ${status.error}
|
|
5608
|
+
[finding(`not a usable git repository: ${status.error}`, "git rev-parse --is-inside-work-tree")],
|
|
5215
5609
|
{
|
|
5216
5610
|
...status
|
|
5217
5611
|
}
|
|
@@ -5222,7 +5616,10 @@ function gitSection(status) {
|
|
|
5222
5616
|
if (status.fetchError !== null) {
|
|
5223
5617
|
lines.push(`fetch failed: ${status.fetchError}`);
|
|
5224
5618
|
problems.push(
|
|
5225
|
-
|
|
5619
|
+
finding(
|
|
5620
|
+
`\`git fetch\` failed (${status.fetchError}); the ahead/behind figures above are from the last successful fetch and may be out of date`,
|
|
5621
|
+
"git fetch origin"
|
|
5622
|
+
)
|
|
5226
5623
|
);
|
|
5227
5624
|
}
|
|
5228
5625
|
return build("git", lines, problems, { ...status });
|
|
@@ -5240,26 +5637,38 @@ function assembleReport(input) {
|
|
|
5240
5637
|
offline: input.offline,
|
|
5241
5638
|
sections: ordered,
|
|
5242
5639
|
problems,
|
|
5243
|
-
exitCode: problems.length === 0 ? 0 : 1
|
|
5640
|
+
exitCode: problems.length === 0 ? 0 : 1,
|
|
5641
|
+
trace: input.trace ?? null,
|
|
5642
|
+
blocked: input.blocked ?? null
|
|
5244
5643
|
};
|
|
5245
5644
|
}
|
|
5645
|
+
function renderBlock(title, body, empty) {
|
|
5646
|
+
return [title, ...(body.length === 0 ? [empty] : body).map((line) => ` ${line}`), ""];
|
|
5647
|
+
}
|
|
5648
|
+
function renderTrace(trace) {
|
|
5649
|
+
return renderBlock(
|
|
5650
|
+
`Commands (${String(trace.length)})`,
|
|
5651
|
+
trace.map(describeTracedCommand),
|
|
5652
|
+
"(no git or gh command was run)"
|
|
5653
|
+
);
|
|
5654
|
+
}
|
|
5655
|
+
function renderProblems(problems) {
|
|
5656
|
+
const body = problems.flatMap((problem) => [
|
|
5657
|
+
`\xB7 ${problem.section}: ${problem.summary}`,
|
|
5658
|
+
// Indented under its problem rather than beside it: the command is a
|
|
5659
|
+
// follow-up to that one finding, not another finding.
|
|
5660
|
+
...problem.command === null ? [] : [` try: ${problem.command}`]
|
|
5661
|
+
]);
|
|
5662
|
+
return [`Problems (${String(problems.length)})`, ...body.map((line) => ` ${line}`), ""];
|
|
5663
|
+
}
|
|
5246
5664
|
function renderText(report) {
|
|
5247
5665
|
const head = `automata do-work --check \u2014 ${report.repo ?? "unknown repository"} \u2014 ${report.generatedAt.toISOString()}`;
|
|
5248
|
-
const parts = [head, ""];
|
|
5666
|
+
const parts = report.blocked === null ? [head, ""] : [`blocked: ${report.blocked.trigger}`, head, ""];
|
|
5249
5667
|
for (const section of report.sections) {
|
|
5250
|
-
parts.push(section.title);
|
|
5251
|
-
if (section.lines.length === 0) {
|
|
5252
|
-
parts.push(" (nothing to report)");
|
|
5253
|
-
} else {
|
|
5254
|
-
for (const line of section.lines) parts.push(` ${line}`);
|
|
5255
|
-
}
|
|
5256
|
-
parts.push("");
|
|
5257
|
-
}
|
|
5258
|
-
if (report.problems.length > 0) {
|
|
5259
|
-
parts.push(`Problems (${String(report.problems.length)})`);
|
|
5260
|
-
for (const problem of report.problems) parts.push(` \xB7 ${problem.section}: ${problem.summary}`);
|
|
5261
|
-
parts.push("");
|
|
5668
|
+
parts.push(...renderBlock(section.title, section.lines, "(nothing to report)"));
|
|
5262
5669
|
}
|
|
5670
|
+
if (report.trace !== null) parts.push(...renderTrace(report.trace));
|
|
5671
|
+
if (report.problems.length > 0) parts.push(...renderProblems(report.problems));
|
|
5263
5672
|
parts.push(
|
|
5264
5673
|
report.problems.length === 0 ? "RESULT: healthy" : `RESULT: ${String(report.problems.length)} problem(s) found`
|
|
5265
5674
|
);
|
|
@@ -5281,13 +5690,21 @@ function toJson(report) {
|
|
|
5281
5690
|
offline: report.offline,
|
|
5282
5691
|
exitCode: report.exitCode,
|
|
5283
5692
|
problems: report.problems,
|
|
5284
|
-
sections
|
|
5693
|
+
sections,
|
|
5694
|
+
trace: report.trace,
|
|
5695
|
+
blocked: report.blocked
|
|
5285
5696
|
};
|
|
5286
5697
|
}
|
|
5287
5698
|
|
|
5288
5699
|
// src/commands/doWork.ts
|
|
5700
|
+
import { dirname as dirname3 } from "path";
|
|
5289
5701
|
var inFlightMarker = null;
|
|
5290
5702
|
var inFlightSync = null;
|
|
5703
|
+
var lockHandle = null;
|
|
5704
|
+
var heartbeatItem = null;
|
|
5705
|
+
function beat(update) {
|
|
5706
|
+
lockHandle?.heartbeat(update);
|
|
5707
|
+
}
|
|
5291
5708
|
function subjectLabel(issue, pr) {
|
|
5292
5709
|
if (issue !== null) return `#${String(issue)}`;
|
|
5293
5710
|
return pr === null ? "#?" : `PR #${String(pr)}`;
|
|
@@ -5342,11 +5759,6 @@ function resolveSettingsResult(options, verifyIdentity) {
|
|
|
5342
5759
|
throw err;
|
|
5343
5760
|
}
|
|
5344
5761
|
}
|
|
5345
|
-
function resolveSettings(options) {
|
|
5346
|
-
const result = resolveSettingsResult(options, options.dryRun !== true);
|
|
5347
|
-
if (!result.ok) fail(result.error);
|
|
5348
|
-
return result.settings;
|
|
5349
|
-
}
|
|
5350
5762
|
function requireParticipants(config) {
|
|
5351
5763
|
if (config.remoteType !== "gh") {
|
|
5352
5764
|
failSettings(
|
|
@@ -5419,6 +5831,7 @@ function buildSettings(options, verifyIdentity) {
|
|
|
5419
5831
|
configEfforts: doWork.effort,
|
|
5420
5832
|
maxRuns: options.maxRuns !== void 0 ? parsePositiveInt(options.maxRuns, "--max-runs") : doWork.maxRunsPerTick ?? DEFAULT_DO_WORK.maxRunsPerTick,
|
|
5421
5833
|
lockStaleMinutes: doWork.lockStaleMinutes ?? DEFAULT_DO_WORK.lockStaleMinutes,
|
|
5834
|
+
dumpOnBlock: doWork.dumpOnBlock ?? DEFAULT_DO_WORK.dumpOnBlock,
|
|
5422
5835
|
limit: parsePositiveInt(options.limit, "--limit"),
|
|
5423
5836
|
participants: { allowedUsers, agentUser },
|
|
5424
5837
|
prompts: {
|
|
@@ -5580,11 +5993,19 @@ function validateDoWorkConfig(section) {
|
|
|
5580
5993
|
validateOptionalString(section, "baseBranch", "doWork.baseBranch");
|
|
5581
5994
|
validateOptionalInt(section, "maxRunsPerTick", "doWork.maxRunsPerTick", 0, "a non-negative integer (0 = unlimited)");
|
|
5582
5995
|
validateOptionalInt(section, "lockStaleMinutes", "doWork.lockStaleMinutes", 1, "a positive integer");
|
|
5996
|
+
validateOptionalBoolean(section, "dumpOnBlock", "doWork.dumpOnBlock");
|
|
5583
5997
|
validateProtectedBranches(section["protectedBranches"]);
|
|
5584
5998
|
validateSettingContainer(section["models"], "models", ["claude", "codex"]);
|
|
5585
5999
|
validateSettingContainer(section["effort"], "effort", ["claude", "codex"]);
|
|
5586
6000
|
validateSettingContainer(section["prompts"], "prompts", ["issueDiscuss", "prWork", "prOrphan"]);
|
|
5587
6001
|
}
|
|
6002
|
+
function validateOptionalBoolean(container, key, path) {
|
|
6003
|
+
const value = container[key];
|
|
6004
|
+
if (value === void 0 || value === null) return;
|
|
6005
|
+
if (typeof value !== "boolean") {
|
|
6006
|
+
failSettings(`${path} must be true or false, got ${JSON.stringify(value)}.`);
|
|
6007
|
+
}
|
|
6008
|
+
}
|
|
5588
6009
|
function validateProtectedBranches(value) {
|
|
5589
6010
|
if (value === void 0 || value === null) return;
|
|
5590
6011
|
const isNonEmptyString = (b) => typeof b === "string" && b.trim().length > 0;
|
|
@@ -5894,6 +6315,11 @@ function reportNoAnswer(item, marker, runError) {
|
|
|
5894
6315
|
return runError === null ? { outcome: "answered-no-reply", detail: "run finished but posted no answer", reason: "no-answer" } : { outcome: "failed", detail: `run failed: ${runError.message}`, reason: "no-answer" };
|
|
5895
6316
|
}
|
|
5896
6317
|
async function invokeExecutor(prompt, execution, silent) {
|
|
6318
|
+
beat({
|
|
6319
|
+
phase: "item",
|
|
6320
|
+
item: heartbeatItem,
|
|
6321
|
+
executor: { command: execution.executor, startedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
6322
|
+
});
|
|
5897
6323
|
if (execution.executor === "codex") {
|
|
5898
6324
|
await runCodex(prompt, { model: execution.model, effort: execution.effort });
|
|
5899
6325
|
return;
|
|
@@ -6143,37 +6569,82 @@ var doWorkCommand = new Command6("do-work").description(
|
|
|
6143
6569
|
).option(
|
|
6144
6570
|
"--no-fetch",
|
|
6145
6571
|
"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"
|
|
6572
|
+
).option(
|
|
6573
|
+
"--verbose",
|
|
6574
|
+
"Show the work behind the report: every `git` and `gh` invocation with its duration and exit code, the discovery query as sent, and every candidate considered. Applies to --check and to a blocked-exit dump"
|
|
6146
6575
|
).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) => {
|
|
6576
|
+
const exitCode = await runDoWork(options);
|
|
6577
|
+
if (exitCode !== 0) process.exit(exitCode);
|
|
6578
|
+
});
|
|
6579
|
+
async function runDoWork(options) {
|
|
6580
|
+
try {
|
|
6581
|
+
return await runDoWorkTraced(options);
|
|
6582
|
+
} finally {
|
|
6583
|
+
stopCommandTrace();
|
|
6584
|
+
}
|
|
6585
|
+
}
|
|
6586
|
+
async function runDoWorkTraced(options) {
|
|
6147
6587
|
const startedAt = Date.now();
|
|
6148
6588
|
if (options.check === true) {
|
|
6149
6589
|
if (options.dryRun === true) {
|
|
6150
6590
|
process.stderr.write(
|
|
6151
6591
|
"Error: --check and --dry-run are two different read-only reports; run one or the other.\n"
|
|
6152
6592
|
);
|
|
6153
|
-
|
|
6593
|
+
return 1;
|
|
6154
6594
|
}
|
|
6155
|
-
|
|
6156
|
-
if (exitCode2 !== 0) process.exit(exitCode2);
|
|
6157
|
-
return;
|
|
6595
|
+
return runCheck(options);
|
|
6158
6596
|
}
|
|
6597
|
+
if (options.verbose === true) startCommandTrace();
|
|
6159
6598
|
if (options.dryRun !== true) loggableInvocation = { startedAt };
|
|
6160
|
-
const
|
|
6599
|
+
const resolved = resolveSettingsResult(options, options.dryRun !== true);
|
|
6600
|
+
if (!resolved.ok) {
|
|
6601
|
+
const payload = dumpBlocked(options, {
|
|
6602
|
+
reason: "config-invalid",
|
|
6603
|
+
trigger: `configuration is not usable \u2014 ${resolved.error}`,
|
|
6604
|
+
selection: null,
|
|
6605
|
+
resolved
|
|
6606
|
+
});
|
|
6607
|
+
if (payload !== null) out(JSON.stringify({ blocked: payload, exitCode: 1 }, null, 2) + "\n");
|
|
6608
|
+
fail(resolved.error);
|
|
6609
|
+
}
|
|
6610
|
+
const settings = resolved.settings;
|
|
6161
6611
|
if (options.dryRun === true) {
|
|
6162
6612
|
const { exitCode: exitCode2 } = await runTick(settings, options);
|
|
6163
|
-
|
|
6164
|
-
return;
|
|
6613
|
+
return exitCode2;
|
|
6165
6614
|
}
|
|
6166
6615
|
loggableInvocation = null;
|
|
6167
6616
|
const lock = acquireRunLock("do-work", settings.lockStaleMinutes);
|
|
6168
6617
|
if (!lock.ok) {
|
|
6169
|
-
const exitCode2 = reportLockHeld(lock, settings, options);
|
|
6618
|
+
const exitCode2 = reportLockHeld(lock, settings, options, resolved);
|
|
6170
6619
|
logTick([], exitCode2, startedAt, "lock-held");
|
|
6171
|
-
|
|
6172
|
-
|
|
6620
|
+
return exitCode2;
|
|
6621
|
+
}
|
|
6622
|
+
const { exitCode, reports } = await runTickUnderLock(lock.handle, settings, options);
|
|
6623
|
+
logTick(reports, exitCode, startedAt);
|
|
6624
|
+
return exitCode;
|
|
6625
|
+
}
|
|
6626
|
+
async function runTickUnderLock(handle, settings, options) {
|
|
6627
|
+
lockHandle = handle;
|
|
6628
|
+
const onSignal = interruptHandler(handle);
|
|
6629
|
+
process.once("SIGINT", onSignal);
|
|
6630
|
+
process.once("SIGTERM", onSignal);
|
|
6631
|
+
try {
|
|
6632
|
+
return await runTick(settings, options);
|
|
6633
|
+
} catch (err) {
|
|
6634
|
+
process.stderr.write(`Error: ${err.message}
|
|
6635
|
+
`);
|
|
6636
|
+
return { exitCode: 1, reports: [] };
|
|
6637
|
+
} finally {
|
|
6638
|
+
lockHandle = null;
|
|
6639
|
+
heartbeatItem = null;
|
|
6640
|
+
handle.release();
|
|
6641
|
+
process.removeListener("SIGINT", onSignal);
|
|
6642
|
+
process.removeListener("SIGTERM", onSignal);
|
|
6173
6643
|
}
|
|
6174
|
-
|
|
6644
|
+
}
|
|
6645
|
+
function interruptHandler(handle) {
|
|
6175
6646
|
let shuttingDown = false;
|
|
6176
|
-
|
|
6647
|
+
return () => {
|
|
6177
6648
|
if (shuttingDown) return;
|
|
6178
6649
|
shuttingDown = true;
|
|
6179
6650
|
progress("\nInterrupted: stopping the executor before releasing the run lock\u2026\n");
|
|
@@ -6189,26 +6660,7 @@ var doWorkCommand = new Command6("do-work").description(
|
|
|
6189
6660
|
process.exit(130);
|
|
6190
6661
|
});
|
|
6191
6662
|
};
|
|
6192
|
-
|
|
6193
|
-
process.once("SIGTERM", onSignal);
|
|
6194
|
-
let exitCode;
|
|
6195
|
-
let reports = [];
|
|
6196
|
-
try {
|
|
6197
|
-
const result = await runTick(settings, options);
|
|
6198
|
-
exitCode = result.exitCode;
|
|
6199
|
-
reports = result.reports;
|
|
6200
|
-
} catch (err) {
|
|
6201
|
-
process.stderr.write(`Error: ${err.message}
|
|
6202
|
-
`);
|
|
6203
|
-
exitCode = 1;
|
|
6204
|
-
} finally {
|
|
6205
|
-
handle.release();
|
|
6206
|
-
process.removeListener("SIGINT", onSignal);
|
|
6207
|
-
process.removeListener("SIGTERM", onSignal);
|
|
6208
|
-
}
|
|
6209
|
-
logTick(reports, exitCode, startedAt);
|
|
6210
|
-
if (exitCode !== 0) process.exit(exitCode);
|
|
6211
|
-
});
|
|
6663
|
+
}
|
|
6212
6664
|
function resolveRepoSlug() {
|
|
6213
6665
|
try {
|
|
6214
6666
|
const slug = getRepoSlug();
|
|
@@ -6217,7 +6669,24 @@ function resolveRepoSlug() {
|
|
|
6217
6669
|
return null;
|
|
6218
6670
|
}
|
|
6219
6671
|
}
|
|
6220
|
-
function
|
|
6672
|
+
function buildSelectionEvidence(settings, issues, linkMap, decisions, verbose) {
|
|
6673
|
+
const issuePass = issuePassEnabled(settings);
|
|
6674
|
+
const orphanPass = orphanPassEnabled(settings);
|
|
6675
|
+
return {
|
|
6676
|
+
decisions,
|
|
6677
|
+
issuePass,
|
|
6678
|
+
orphanPass,
|
|
6679
|
+
issues: verbose && issuePass ? issues : null,
|
|
6680
|
+
// Read off the link map rather than off `discoverOrphanPrs`, which returns
|
|
6681
|
+
// only the survivors — the whole point here is to show what was dropped.
|
|
6682
|
+
orphans: verbose && orphanPass ? linkMap.orphans.map((candidate) => ({
|
|
6683
|
+
number: candidate.pr.number,
|
|
6684
|
+
title: candidate.pr.title,
|
|
6685
|
+
matched: prMatchesFilter(candidate, settings)
|
|
6686
|
+
})) : null
|
|
6687
|
+
};
|
|
6688
|
+
}
|
|
6689
|
+
function collectSelection(settings, verbose) {
|
|
6221
6690
|
const issues = discoverIssues(settings);
|
|
6222
6691
|
const linkMap = getOpenPrLinkMap();
|
|
6223
6692
|
const policy = {
|
|
@@ -6225,52 +6694,135 @@ function collectSelection(settings) {
|
|
|
6225
6694
|
defaultBranch: linkMap.defaultBranch,
|
|
6226
6695
|
protectedBranches: settings.protectedBranches
|
|
6227
6696
|
};
|
|
6228
|
-
|
|
6697
|
+
const decisions = skipDuplicateHeadBranches([
|
|
6229
6698
|
...issues.map((issue) => decideWork(buildIssueState(issue, linkMap), settings.participants, policy)),
|
|
6230
6699
|
...discoverOrphanPrs(settings, linkMap).map(
|
|
6231
6700
|
(candidate) => decideOrphanPrWork({ prSurface: getPrSurface(candidate.pr.number) }, settings.participants, policy)
|
|
6232
6701
|
)
|
|
6233
6702
|
]);
|
|
6703
|
+
return buildSelectionEvidence(settings, issues, linkMap, decisions, verbose);
|
|
6234
6704
|
}
|
|
6235
|
-
function
|
|
6236
|
-
|
|
6705
|
+
function makeSelectionSection(lines, problems, data) {
|
|
6706
|
+
return {
|
|
6237
6707
|
id: "selection",
|
|
6238
6708
|
title: sectionTitle("selection"),
|
|
6239
|
-
lines
|
|
6240
|
-
problems: problems.map((
|
|
6709
|
+
lines,
|
|
6710
|
+
problems: problems.map((problem) => ({ section: "selection", ...problem })),
|
|
6241
6711
|
data
|
|
6242
|
-
}
|
|
6712
|
+
};
|
|
6713
|
+
}
|
|
6714
|
+
function describeSelectionEvidence(settings, evidence, lines) {
|
|
6715
|
+
lines.push(
|
|
6716
|
+
...describeIssueEvidence(settings, evidence),
|
|
6717
|
+
...describeOrphanEvidence(settings, evidence)
|
|
6718
|
+
);
|
|
6719
|
+
}
|
|
6720
|
+
function heading(text, count) {
|
|
6721
|
+
return `${text}${count === 0 ? "" : ":"}`;
|
|
6722
|
+
}
|
|
6723
|
+
function describeIssueEvidence(settings, evidence) {
|
|
6724
|
+
if (!evidence.issuePass) {
|
|
6725
|
+
return [
|
|
6726
|
+
`issue discovery: not run \u2014 --pr ${String(settings.onlyPr)} restricts this tick to that pull request`
|
|
6727
|
+
];
|
|
6728
|
+
}
|
|
6729
|
+
const restriction = settings.onlyIssue === void 0 ? "" : `, restricted to #${String(settings.onlyIssue)}`;
|
|
6730
|
+
const lines = [
|
|
6731
|
+
`discovery query: ${settings.technique} = ${settings.discoveryValue}, limit ${String(settings.limit)}${restriction}`
|
|
6732
|
+
];
|
|
6733
|
+
if (evidence.issues === null) return lines;
|
|
6734
|
+
lines.push(heading(`discovery returned ${String(evidence.issues.length)} issue(s)`, evidence.issues.length));
|
|
6735
|
+
for (const issue of evidence.issues) {
|
|
6736
|
+
lines.push(` #${String(issue.number)} ${issue.title}`);
|
|
6737
|
+
}
|
|
6738
|
+
return lines;
|
|
6739
|
+
}
|
|
6740
|
+
function describeOrphanEvidence(settings, evidence) {
|
|
6741
|
+
if (!evidence.orphanPass) {
|
|
6742
|
+
return [
|
|
6743
|
+
`orphan pull-request pass: not run \u2014 --issue ${String(settings.onlyIssue)} restricts this tick to that issue`
|
|
6744
|
+
];
|
|
6745
|
+
}
|
|
6746
|
+
if (evidence.orphans === null) return [];
|
|
6747
|
+
const restriction = settings.onlyPr === void 0 ? "" : ` (only PR #${String(settings.onlyPr)} would be taken up)`;
|
|
6748
|
+
const lines = [
|
|
6749
|
+
heading(
|
|
6750
|
+
`${String(evidence.orphans.length)} open orphan pull request(s) considered${restriction}`,
|
|
6751
|
+
evidence.orphans.length
|
|
6752
|
+
)
|
|
6753
|
+
];
|
|
6754
|
+
for (const orphan of evidence.orphans) {
|
|
6755
|
+
const verdict = orphan.matched ? "matches the filter" : "dropped by the discovery filter";
|
|
6756
|
+
lines.push(` PR #${String(orphan.number)} ${orphan.title} \u2014 ${verdict}`);
|
|
6757
|
+
}
|
|
6758
|
+
return lines;
|
|
6759
|
+
}
|
|
6760
|
+
function describeSelectionDecisions(decisions) {
|
|
6761
|
+
const work = decisions.filter((decision) => decision.kind === "work").length;
|
|
6762
|
+
return [
|
|
6763
|
+
`${String(work)} of ${String(decisions.length)} candidate(s) would be picked up`,
|
|
6764
|
+
...describePlan(decisions).trimEnd().split("\n")
|
|
6765
|
+
];
|
|
6766
|
+
}
|
|
6767
|
+
function selectionSection(settings, offline, verbose) {
|
|
6243
6768
|
if (offline) {
|
|
6244
|
-
return
|
|
6769
|
+
return makeSelectionSection(
|
|
6245
6770
|
["not run: --no-fetch makes no network call, and the selection needs live GitHub data"],
|
|
6246
6771
|
[],
|
|
6247
6772
|
{ ran: false, detail: "offline", plan: [] }
|
|
6248
6773
|
);
|
|
6249
6774
|
}
|
|
6250
6775
|
if (settings === null) {
|
|
6251
|
-
return
|
|
6776
|
+
return makeSelectionSection(
|
|
6252
6777
|
["not run: the configuration could not be resolved \u2014 see Environment below"],
|
|
6253
6778
|
[],
|
|
6254
6779
|
{ ran: false, detail: "configuration invalid", plan: [] }
|
|
6255
6780
|
);
|
|
6256
6781
|
}
|
|
6257
|
-
let
|
|
6782
|
+
let evidence;
|
|
6258
6783
|
try {
|
|
6259
|
-
|
|
6784
|
+
evidence = collectSelection(settings, verbose);
|
|
6260
6785
|
} catch (err) {
|
|
6261
6786
|
const detail = err.message;
|
|
6262
|
-
return
|
|
6787
|
+
return makeSelectionSection(
|
|
6263
6788
|
[`could not be computed: ${detail}`],
|
|
6264
|
-
[
|
|
6789
|
+
[
|
|
6790
|
+
{
|
|
6791
|
+
summary: `the GitHub selection could not be computed: ${detail}`,
|
|
6792
|
+
command: `gh issue list --limit ${String(settings.limit)}`
|
|
6793
|
+
}
|
|
6794
|
+
],
|
|
6265
6795
|
{ ran: false, detail, plan: [] }
|
|
6266
6796
|
);
|
|
6267
6797
|
}
|
|
6268
|
-
const
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
|
|
6798
|
+
const lines = [];
|
|
6799
|
+
if (verbose) describeSelectionEvidence(settings, evidence, lines);
|
|
6800
|
+
lines.push(...describeSelectionDecisions(evidence.decisions));
|
|
6801
|
+
return makeSelectionSection(lines, [], selectionData(evidence));
|
|
6802
|
+
}
|
|
6803
|
+
function selectionData(evidence) {
|
|
6804
|
+
return {
|
|
6805
|
+
ran: true,
|
|
6806
|
+
detail: null,
|
|
6807
|
+
plan: evidence.decisions.map(toPlanJson),
|
|
6808
|
+
issues: evidence.issues,
|
|
6809
|
+
orphans: evidence.orphans,
|
|
6810
|
+
issuePass: evidence.issuePass,
|
|
6811
|
+
orphanPass: evidence.orphanPass
|
|
6812
|
+
};
|
|
6813
|
+
}
|
|
6814
|
+
function selectionSectionFromEvidence(settings, evidence, verbose) {
|
|
6815
|
+
if (evidence === null) {
|
|
6816
|
+
return makeSelectionSection(
|
|
6817
|
+
["not run: the tick was blocked before discovery"],
|
|
6818
|
+
[],
|
|
6819
|
+
{ ran: false, detail: "blocked before discovery", plan: [] }
|
|
6820
|
+
);
|
|
6821
|
+
}
|
|
6822
|
+
const lines = [];
|
|
6823
|
+
if (verbose && settings !== null) describeSelectionEvidence(settings, evidence, lines);
|
|
6824
|
+
lines.push(...describeSelectionDecisions(evidence.decisions));
|
|
6825
|
+
return makeSelectionSection(lines, [], selectionData(evidence));
|
|
6274
6826
|
}
|
|
6275
6827
|
function describeExecutor(settings, lines, problems) {
|
|
6276
6828
|
const executor = settings.withOption ?? settings.configExecutor ?? "claude";
|
|
@@ -6279,15 +6831,16 @@ function describeExecutor(settings, lines, problems) {
|
|
|
6279
6831
|
const onPath = resolvedPath !== command;
|
|
6280
6832
|
lines.push(`default executor: ${executor} (${onPath ? resolvedPath : "not found on PATH"})`);
|
|
6281
6833
|
if (!onPath) {
|
|
6282
|
-
problems.push(
|
|
6283
|
-
`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
|
|
6284
|
-
|
|
6834
|
+
problems.push({
|
|
6835
|
+
summary: `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`,
|
|
6836
|
+
command: `command -v ${command}`
|
|
6837
|
+
});
|
|
6285
6838
|
}
|
|
6286
6839
|
return { executor, command, onPath };
|
|
6287
6840
|
}
|
|
6288
|
-
function describeGitHubIdentity(
|
|
6289
|
-
if (
|
|
6290
|
-
lines.push(
|
|
6841
|
+
function describeGitHubIdentity(checkIdentity, settings, lines, problems, reason) {
|
|
6842
|
+
if (!checkIdentity) {
|
|
6843
|
+
lines.push(`\`gh\` authentication not checked: ${reason}`);
|
|
6291
6844
|
return { ghAvailable: null, ghLogin: null, identityProblem: null };
|
|
6292
6845
|
}
|
|
6293
6846
|
let identity;
|
|
@@ -6296,14 +6849,18 @@ function describeGitHubIdentity(options, settings, lines, problems) {
|
|
|
6296
6849
|
} catch (err) {
|
|
6297
6850
|
const detail = err.message;
|
|
6298
6851
|
lines.push(`\`gh\` could not be queried: ${detail}`);
|
|
6299
|
-
problems.push(
|
|
6852
|
+
problems.push({
|
|
6853
|
+
summary: `\`gh\` could not be queried (${detail}); install it and run \`gh auth login\``,
|
|
6854
|
+
command: "gh auth status"
|
|
6855
|
+
});
|
|
6300
6856
|
return { ghAvailable: false, ghLogin: null, identityProblem: null };
|
|
6301
6857
|
}
|
|
6302
6858
|
if (identity.kind === "unavailable") {
|
|
6303
6859
|
lines.push(`\`gh\` could not name an account: ${identity.detail}`);
|
|
6304
|
-
problems.push(
|
|
6305
|
-
`\`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
|
|
6306
|
-
|
|
6860
|
+
problems.push({
|
|
6861
|
+
summary: `\`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`,
|
|
6862
|
+
command: "gh auth status"
|
|
6863
|
+
});
|
|
6307
6864
|
return { ghAvailable: false, ghLogin: null, identityProblem: null };
|
|
6308
6865
|
}
|
|
6309
6866
|
const ghLogin = identity.kind === "login" ? identity.login : null;
|
|
@@ -6315,7 +6872,9 @@ function describeGitHubIdentity(options, settings, lines, problems) {
|
|
|
6315
6872
|
settings.participants.agentUser,
|
|
6316
6873
|
settings.participants.allowedUsers
|
|
6317
6874
|
);
|
|
6318
|
-
if (identityProblem !== null)
|
|
6875
|
+
if (identityProblem !== null) {
|
|
6876
|
+
problems.push({ summary: identityProblem, command: "automata config get" });
|
|
6877
|
+
}
|
|
6319
6878
|
return { ghAvailable: true, ghLogin, identityProblem };
|
|
6320
6879
|
}
|
|
6321
6880
|
function readRemoteType() {
|
|
@@ -6325,7 +6884,7 @@ function readRemoteType() {
|
|
|
6325
6884
|
return null;
|
|
6326
6885
|
}
|
|
6327
6886
|
}
|
|
6328
|
-
function environmentSection(
|
|
6887
|
+
function environmentSection(checkIdentity, identitySkipReason, resolved, repo) {
|
|
6329
6888
|
const lines = [`automata ${version}`];
|
|
6330
6889
|
const problems = [];
|
|
6331
6890
|
const remoteType = readRemoteType();
|
|
@@ -6338,20 +6897,24 @@ function environmentSection(options, resolved, repo) {
|
|
|
6338
6897
|
};
|
|
6339
6898
|
if (repo === null) {
|
|
6340
6899
|
lines.push("repository slug could not be resolved from `origin`");
|
|
6341
|
-
problems.push(
|
|
6342
|
-
"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"
|
|
6343
|
-
|
|
6900
|
+
problems.push({
|
|
6901
|
+
summary: "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",
|
|
6902
|
+
command: "git remote -v"
|
|
6903
|
+
});
|
|
6344
6904
|
} else {
|
|
6345
6905
|
lines.push(`repository ${repo}`);
|
|
6346
6906
|
}
|
|
6347
6907
|
if (!resolved.ok) {
|
|
6348
6908
|
lines.push(`configuration is not usable: ${resolved.error}`);
|
|
6349
|
-
problems.push(
|
|
6909
|
+
problems.push({
|
|
6910
|
+
summary: `the configuration is not usable: ${resolved.error}`,
|
|
6911
|
+
command: "automata config get"
|
|
6912
|
+
});
|
|
6350
6913
|
return {
|
|
6351
6914
|
id: "environment",
|
|
6352
6915
|
title: sectionTitle("environment"),
|
|
6353
6916
|
lines,
|
|
6354
|
-
problems: problems.map((
|
|
6917
|
+
problems: problems.map((problem) => ({ section: "environment", ...problem })),
|
|
6355
6918
|
// `ghAvailable` is null rather than false: nothing was asked of `gh`,
|
|
6356
6919
|
// which is not the same as having asked and been refused.
|
|
6357
6920
|
data: { ...data, ghAvailable: null, ghLogin: null, identityProblem: null }
|
|
@@ -6367,16 +6930,17 @@ function environmentSection(options, resolved, repo) {
|
|
|
6367
6930
|
);
|
|
6368
6931
|
const { executor, command, onPath } = describeExecutor(settings, lines, problems);
|
|
6369
6932
|
const { ghAvailable, ghLogin, identityProblem } = describeGitHubIdentity(
|
|
6370
|
-
|
|
6933
|
+
checkIdentity,
|
|
6371
6934
|
settings,
|
|
6372
6935
|
lines,
|
|
6373
|
-
problems
|
|
6936
|
+
problems,
|
|
6937
|
+
identitySkipReason
|
|
6374
6938
|
);
|
|
6375
6939
|
return {
|
|
6376
6940
|
id: "environment",
|
|
6377
6941
|
title: sectionTitle("environment"),
|
|
6378
6942
|
lines,
|
|
6379
|
-
problems: problems.map((
|
|
6943
|
+
problems: problems.map((problem) => ({ section: "environment", ...problem })),
|
|
6380
6944
|
data: {
|
|
6381
6945
|
...data,
|
|
6382
6946
|
discovery: { technique: settings.technique, value: settings.discoveryValue },
|
|
@@ -6392,30 +6956,121 @@ function environmentSection(options, resolved, repo) {
|
|
|
6392
6956
|
}
|
|
6393
6957
|
};
|
|
6394
6958
|
}
|
|
6959
|
+
function buildCheckReport(input) {
|
|
6960
|
+
const settings = input.resolved.ok ? input.resolved.settings : null;
|
|
6961
|
+
const staleMinutes = settings?.lockStaleMinutes ?? DEFAULT_DO_WORK.lockStaleMinutes;
|
|
6962
|
+
const baseBranch = settings?.baseBranch ?? DEFAULT_DO_WORK.baseBranch;
|
|
6963
|
+
const lockStatus = inspectRunLock(staleMinutes);
|
|
6964
|
+
const logDirectory = inspectLogDirectory();
|
|
6965
|
+
const lockContext = {
|
|
6966
|
+
cwd: logDirectory.cwd,
|
|
6967
|
+
logDirectory: logDirectory.dir,
|
|
6968
|
+
parentOf: dirname3
|
|
6969
|
+
};
|
|
6970
|
+
return assembleReport({
|
|
6971
|
+
generatedAt: input.now,
|
|
6972
|
+
repo: input.repo,
|
|
6973
|
+
offline: input.offline,
|
|
6974
|
+
blocked: input.blocked,
|
|
6975
|
+
sections: [
|
|
6976
|
+
lockSection(lockStatus, staleMinutes, lockContext, input.now),
|
|
6977
|
+
tickSection(
|
|
6978
|
+
readExecutionTicks({ repo: input.repo, limit: TICK_HISTORY }),
|
|
6979
|
+
input.now,
|
|
6980
|
+
logDirectory,
|
|
6981
|
+
lockStatus
|
|
6982
|
+
),
|
|
6983
|
+
workSection(readWorkRecords({ repo: input.repo, limit: WORK_HISTORY }), input.now),
|
|
6984
|
+
gitSection(inspectRepoStatus({ baseBranch, fetch: input.fetch })),
|
|
6985
|
+
input.selection,
|
|
6986
|
+
environmentSection(input.checkIdentity, input.identitySkipReason, input.resolved, input.repo)
|
|
6987
|
+
],
|
|
6988
|
+
// Taken last, so the trace covers every command the collectors above made.
|
|
6989
|
+
trace: takeCommandTrace()
|
|
6990
|
+
});
|
|
6991
|
+
}
|
|
6395
6992
|
function runCheck(options) {
|
|
6993
|
+
const verbose = options.verbose === true;
|
|
6994
|
+
if (verbose) startCommandTrace();
|
|
6396
6995
|
const now = /* @__PURE__ */ new Date();
|
|
6397
6996
|
const offline = options.fetch === false;
|
|
6398
6997
|
const repo = resolveRepoSlug();
|
|
6399
6998
|
const resolved = resolveSettingsResult(options, false);
|
|
6400
6999
|
const settings = resolved.ok ? resolved.settings : null;
|
|
6401
|
-
const
|
|
6402
|
-
|
|
6403
|
-
const report = assembleReport({
|
|
6404
|
-
generatedAt: now,
|
|
7000
|
+
const report = buildCheckReport({
|
|
7001
|
+
now,
|
|
6405
7002
|
repo,
|
|
7003
|
+
resolved,
|
|
7004
|
+
selection: selectionSection(settings, offline, verbose),
|
|
7005
|
+
fetch: !offline,
|
|
7006
|
+
checkIdentity: !offline,
|
|
7007
|
+
identitySkipReason: "--no-fetch",
|
|
6406
7008
|
offline,
|
|
6407
|
-
|
|
6408
|
-
lockSection(inspectRunLock(staleMinutes), staleMinutes),
|
|
6409
|
-
tickSection(readExecutionTicks({ repo, limit: TICK_HISTORY }), now),
|
|
6410
|
-
workSection(readWorkRecords({ repo, limit: WORK_HISTORY }), now),
|
|
6411
|
-
gitSection(inspectRepoStatus({ baseBranch, fetch: !offline })),
|
|
6412
|
-
selectionSection(settings, offline),
|
|
6413
|
-
environmentSection(options, resolved, repo)
|
|
6414
|
-
]
|
|
7009
|
+
blocked: null
|
|
6415
7010
|
});
|
|
6416
7011
|
out(options.json === true ? JSON.stringify(toJson(report), null, 2) + "\n" : renderText(report));
|
|
6417
7012
|
return report.exitCode;
|
|
6418
7013
|
}
|
|
7014
|
+
function dumpOnBlockEnabled(resolved) {
|
|
7015
|
+
if (resolved.ok) return resolved.settings.dumpOnBlock;
|
|
7016
|
+
try {
|
|
7017
|
+
return readConfig().doWork?.dumpOnBlock ?? DEFAULT_DO_WORK.dumpOnBlock;
|
|
7018
|
+
} catch {
|
|
7019
|
+
return DEFAULT_DO_WORK.dumpOnBlock;
|
|
7020
|
+
}
|
|
7021
|
+
}
|
|
7022
|
+
function buildBlockedReport(context, verbose) {
|
|
7023
|
+
return buildCheckReport({
|
|
7024
|
+
now: /* @__PURE__ */ new Date(),
|
|
7025
|
+
repo: resolveRepoSlug(),
|
|
7026
|
+
resolved: context.resolved,
|
|
7027
|
+
selection: selectionSectionFromEvidence(
|
|
7028
|
+
context.resolved.ok ? context.resolved.settings : null,
|
|
7029
|
+
context.selection,
|
|
7030
|
+
verbose
|
|
7031
|
+
),
|
|
7032
|
+
fetch: false,
|
|
7033
|
+
checkIdentity: false,
|
|
7034
|
+
identitySkipReason: "the tick is exiting and the dump makes no network call",
|
|
7035
|
+
offline: true,
|
|
7036
|
+
blocked: { reason: context.reason, trigger: context.trigger }
|
|
7037
|
+
});
|
|
7038
|
+
}
|
|
7039
|
+
function dumpBlocked(options, context) {
|
|
7040
|
+
if (!dumpOnBlockEnabled(context.resolved)) return null;
|
|
7041
|
+
try {
|
|
7042
|
+
const report = buildBlockedReport(context, options.verbose === true);
|
|
7043
|
+
if (options.json === true) {
|
|
7044
|
+
return { reason: context.reason, trigger: context.trigger, report: toJson(report) };
|
|
7045
|
+
}
|
|
7046
|
+
progress(renderText(report));
|
|
7047
|
+
return null;
|
|
7048
|
+
} catch (err) {
|
|
7049
|
+
progress(`Warning: could not render the blocked-exit report: ${err.message}
|
|
7050
|
+
`);
|
|
7051
|
+
return null;
|
|
7052
|
+
}
|
|
7053
|
+
}
|
|
7054
|
+
function classifyBlocked(hygiene, decisions, items, reports) {
|
|
7055
|
+
const ran = reports.some((report) => report.ranExecutor === true);
|
|
7056
|
+
if (ran) return null;
|
|
7057
|
+
if (hygiene.degraded) {
|
|
7058
|
+
return {
|
|
7059
|
+
reason: "preflight-failed",
|
|
7060
|
+
trigger: `the pre-flight did not prepare the checkout (${describePreflightFailures(hygiene).join("; ")})`
|
|
7061
|
+
};
|
|
7062
|
+
}
|
|
7063
|
+
if (items.length === 0) {
|
|
7064
|
+
return {
|
|
7065
|
+
reason: "no-candidates",
|
|
7066
|
+
trigger: `no candidate was picked up (0 of ${String(decisions.length)})`
|
|
7067
|
+
};
|
|
7068
|
+
}
|
|
7069
|
+
return {
|
|
7070
|
+
reason: "all-skipped",
|
|
7071
|
+
trigger: `${String(items.length)} item(s) selected, none reached the executor`
|
|
7072
|
+
};
|
|
7073
|
+
}
|
|
6419
7074
|
function toTickLogItem(report) {
|
|
6420
7075
|
return {
|
|
6421
7076
|
subject: reportLabel(report),
|
|
@@ -6447,22 +7102,41 @@ function logTick(reports, exitCode, startedAt, note) {
|
|
|
6447
7102
|
items: reports.map(toTickLogItem)
|
|
6448
7103
|
});
|
|
6449
7104
|
}
|
|
6450
|
-
function reportLockHeld(lock, settings, options) {
|
|
7105
|
+
function reportLockHeld(lock, settings, options, resolved) {
|
|
6451
7106
|
const held = lock.heldBy;
|
|
6452
7107
|
const sentence = `Another automata instance is already running here (pid ${String(held.pid)} on ${held.host}, started ${held.startedAt}, command ${held.command}). Doing nothing.
|
|
6453
7108
|
`;
|
|
6454
7109
|
const suspectSentence = lock.suspect ? `Warning: that lock has been held longer than ${String(settings.lockStaleMinutes)} minutes. If no tick is really running, its process id was probably reused; remove ${RUN_LOCK_RELATIVE_PATH} once you have confirmed that.
|
|
6455
7110
|
` : "";
|
|
6456
7111
|
const exitCode = lock.suspect ? 2 : 0;
|
|
7112
|
+
const context = {
|
|
7113
|
+
reason: "lock-held",
|
|
7114
|
+
trigger: `run lock held by pid ${String(held.pid)} on ${held.host}`,
|
|
7115
|
+
selection: null,
|
|
7116
|
+
resolved
|
|
7117
|
+
};
|
|
6457
7118
|
if (options.json === true) {
|
|
6458
7119
|
progress(sentence + suspectSentence);
|
|
6459
7120
|
out(
|
|
6460
|
-
JSON.stringify(
|
|
7121
|
+
JSON.stringify(
|
|
7122
|
+
{
|
|
7123
|
+
lockHeld: true,
|
|
7124
|
+
suspect: lock.suspect,
|
|
7125
|
+
heldBy: held,
|
|
7126
|
+
blocked: dumpBlocked(options, context),
|
|
7127
|
+
plan: [],
|
|
7128
|
+
items: [],
|
|
7129
|
+
exitCode
|
|
7130
|
+
},
|
|
7131
|
+
null,
|
|
7132
|
+
2
|
|
7133
|
+
) + "\n"
|
|
6461
7134
|
);
|
|
6462
|
-
|
|
6463
|
-
out(sentence);
|
|
6464
|
-
if (suspectSentence) progress(suspectSentence);
|
|
7135
|
+
return exitCode;
|
|
6465
7136
|
}
|
|
7137
|
+
out(sentence);
|
|
7138
|
+
if (suspectSentence) progress(suspectSentence);
|
|
7139
|
+
dumpBlocked(options, context);
|
|
6466
7140
|
return exitCode;
|
|
6467
7141
|
}
|
|
6468
7142
|
function explainInterruptedMarker() {
|
|
@@ -6479,6 +7153,7 @@ function explainInterruptedMarker() {
|
|
|
6479
7153
|
}
|
|
6480
7154
|
}
|
|
6481
7155
|
async function runTick(settings, options) {
|
|
7156
|
+
beat({ phase: "pre-flight" });
|
|
6482
7157
|
const hygiene = runRepoHygiene({
|
|
6483
7158
|
baseBranch: settings.baseBranch,
|
|
6484
7159
|
protectedBranches: settings.protectedBranches,
|
|
@@ -6486,6 +7161,7 @@ async function runTick(settings, options) {
|
|
|
6486
7161
|
agentUser: settings.participants.agentUser,
|
|
6487
7162
|
log: progress
|
|
6488
7163
|
});
|
|
7164
|
+
beat({ phase: "discovery" });
|
|
6489
7165
|
const issues = discoverIssues(settings);
|
|
6490
7166
|
const linkMap = getOpenPrLinkMap();
|
|
6491
7167
|
const policy = {
|
|
@@ -6511,49 +7187,85 @@ ${describePlan(decisions)}`;
|
|
|
6511
7187
|
reportDryRun(items, decisions, settings, options, hygiene);
|
|
6512
7188
|
return { exitCode: 0, reports: [] };
|
|
6513
7189
|
}
|
|
7190
|
+
const { reports, deferred } = await processItems(items, settings, options, hygiene);
|
|
7191
|
+
heartbeatItem = null;
|
|
7192
|
+
beat({ phase: "summary" });
|
|
7193
|
+
reports.push(...reportDeferred(deferred, settings));
|
|
7194
|
+
const degraded = hygiene.degraded || reports.some((report) => report.outcome !== "answered");
|
|
7195
|
+
const exitCode = degraded ? 2 : 0;
|
|
7196
|
+
const blockedBy = classifyBlocked(hygiene, decisions, items, reports);
|
|
7197
|
+
const context = blockedBy === null ? null : {
|
|
7198
|
+
...blockedBy,
|
|
7199
|
+
selection: buildSelectionEvidence(
|
|
7200
|
+
settings,
|
|
7201
|
+
issues,
|
|
7202
|
+
linkMap,
|
|
7203
|
+
decisions,
|
|
7204
|
+
options.verbose === true
|
|
7205
|
+
),
|
|
7206
|
+
resolved: { ok: true, settings }
|
|
7207
|
+
};
|
|
7208
|
+
reportTickOutcome({ options, hygiene, decisions, reports, context, exitCode });
|
|
7209
|
+
return { exitCode, reports };
|
|
7210
|
+
}
|
|
7211
|
+
async function processItems(items, settings, options, hygiene) {
|
|
6514
7212
|
const reports = [];
|
|
6515
7213
|
const deferred = [];
|
|
6516
7214
|
const preflightCauses = describePreflightFailures(hygiene);
|
|
6517
7215
|
let runsUsed = 0;
|
|
6518
|
-
for (const item of items) {
|
|
7216
|
+
for (const [index, item] of items.entries()) {
|
|
6519
7217
|
if (settings.maxRuns > 0 && runsUsed >= settings.maxRuns) {
|
|
6520
7218
|
deferred.push(item);
|
|
6521
7219
|
continue;
|
|
6522
7220
|
}
|
|
6523
|
-
|
|
6524
|
-
|
|
6525
|
-
|
|
6526
|
-
} catch (err) {
|
|
6527
|
-
progress(` failed: ${err.message}
|
|
6528
|
-
`);
|
|
6529
|
-
report = {
|
|
6530
|
-
issue: item.issue?.number ?? null,
|
|
6531
|
-
pr: item.pr?.number ?? null,
|
|
6532
|
-
title: itemTitle(item),
|
|
6533
|
-
turn: item.turn,
|
|
6534
|
-
outcome: "failed",
|
|
6535
|
-
detail: err.message,
|
|
6536
|
-
sync: inFlightSync ?? void 0
|
|
6537
|
-
};
|
|
6538
|
-
}
|
|
7221
|
+
heartbeatItem = { index: index + 1, total: items.length, subject: itemLabel(item).trim() };
|
|
7222
|
+
beat({ phase: "item", item: heartbeatItem });
|
|
7223
|
+
const report = await reportForItem(item, settings, options, preflightCauses);
|
|
6539
7224
|
if (report.ranExecutor === true) runsUsed++;
|
|
6540
7225
|
reports.push(report);
|
|
6541
7226
|
}
|
|
6542
|
-
|
|
7227
|
+
return { reports, deferred };
|
|
7228
|
+
}
|
|
7229
|
+
async function reportForItem(item, settings, options, preflightCauses) {
|
|
7230
|
+
try {
|
|
7231
|
+
return await processItem(item, settings, options.silent === true, preflightCauses);
|
|
7232
|
+
} catch (err) {
|
|
7233
|
+
progress(` failed: ${err.message}
|
|
7234
|
+
`);
|
|
7235
|
+
return {
|
|
7236
|
+
issue: item.issue?.number ?? null,
|
|
7237
|
+
pr: item.pr?.number ?? null,
|
|
7238
|
+
title: itemTitle(item),
|
|
7239
|
+
turn: item.turn,
|
|
7240
|
+
outcome: "failed",
|
|
7241
|
+
detail: err.message,
|
|
7242
|
+
sync: inFlightSync ?? void 0
|
|
7243
|
+
};
|
|
7244
|
+
}
|
|
7245
|
+
}
|
|
7246
|
+
function reportDeferred(deferred, settings) {
|
|
7247
|
+
return deferred.map((item) => {
|
|
6543
7248
|
progress(`
|
|
6544
7249
|
${itemLabel(item)} deferred: --max-runs / maxRunsPerTick reached.
|
|
6545
7250
|
`);
|
|
6546
|
-
|
|
7251
|
+
return {
|
|
6547
7252
|
issue: item.issue?.number ?? null,
|
|
6548
7253
|
pr: item.pr?.number ?? null,
|
|
6549
7254
|
title: itemTitle(item),
|
|
6550
7255
|
turn: item.turn,
|
|
6551
7256
|
outcome: "deferred",
|
|
6552
7257
|
detail: `run cap of ${String(settings.maxRuns)} reached`
|
|
6553
|
-
}
|
|
6554
|
-
}
|
|
6555
|
-
|
|
6556
|
-
|
|
7258
|
+
};
|
|
7259
|
+
});
|
|
7260
|
+
}
|
|
7261
|
+
function reportTickOutcome({
|
|
7262
|
+
options,
|
|
7263
|
+
hygiene,
|
|
7264
|
+
decisions,
|
|
7265
|
+
reports,
|
|
7266
|
+
context,
|
|
7267
|
+
exitCode
|
|
7268
|
+
}) {
|
|
6557
7269
|
if (options.json) {
|
|
6558
7270
|
out(
|
|
6559
7271
|
JSON.stringify(
|
|
@@ -6562,17 +7274,18 @@ ${itemLabel(item)} deferred: --max-runs / maxRunsPerTick reached.
|
|
|
6562
7274
|
preflight: toHygieneJson(hygiene),
|
|
6563
7275
|
plan: decisions.map(toPlanJson),
|
|
6564
7276
|
items: reports.map(toItemJson),
|
|
7277
|
+
blocked: context === null ? null : dumpBlocked(options, context),
|
|
6565
7278
|
exitCode
|
|
6566
7279
|
},
|
|
6567
7280
|
null,
|
|
6568
7281
|
2
|
|
6569
7282
|
) + "\n"
|
|
6570
7283
|
);
|
|
6571
|
-
|
|
6572
|
-
summarizeHygiene(hygiene);
|
|
6573
|
-
summarize(reports);
|
|
7284
|
+
return;
|
|
6574
7285
|
}
|
|
6575
|
-
|
|
7286
|
+
summarizeHygiene(hygiene);
|
|
7287
|
+
summarize(reports);
|
|
7288
|
+
if (context !== null) dumpBlocked(options, context);
|
|
6576
7289
|
}
|
|
6577
7290
|
function toRunJson(entry) {
|
|
6578
7291
|
if (entry.kind === "refused") {
|