automata-cli 0.8.0-develop.368 → 0.9.0-develop.382
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";
|
|
@@ -184,6 +184,20 @@ var configSetDoWorkLockStaleMinutes = new Command("do-work-lock-stale-minutes").
|
|
|
184
184
|
process.stdout.write(`do-work lock staleness set to: ${String(minutes)} minutes
|
|
185
185
|
`);
|
|
186
186
|
});
|
|
187
|
+
var configSetDoWorkDumpOnBlock = new Command("do-work-dump-on-block").description(
|
|
188
|
+
"Set whether `do-work` prints the full health report when a tick exits having done nothing"
|
|
189
|
+
).argument("<value>", `true or false (default: ${String(DEFAULT_DO_WORK.dumpOnBlock)})`).action((value) => {
|
|
190
|
+
const normalized = value.trim().toLowerCase();
|
|
191
|
+
if (normalized !== "true" && normalized !== "false") {
|
|
192
|
+
process.stderr.write(`Error: do-work-dump-on-block must be true or false (got "${value}").
|
|
193
|
+
`);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
const dumpOnBlock = normalized === "true";
|
|
197
|
+
writeDoWork({ dumpOnBlock });
|
|
198
|
+
process.stdout.write(`do-work dump on block set to: ${String(dumpOnBlock)}
|
|
199
|
+
`);
|
|
200
|
+
});
|
|
187
201
|
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
202
|
if (!VALID_TURN_KINDS.includes(turnKind)) {
|
|
189
203
|
process.stderr.write(
|
|
@@ -225,12 +239,12 @@ var configSetGitTrunkBranch = new Command("git-trunk-branch").description("Pin t
|
|
|
225
239
|
process.stdout.write(`git trunk branch set to: ${branch}
|
|
226
240
|
`);
|
|
227
241
|
});
|
|
228
|
-
var configSet = new Command("set").description("Set a configuration value").addCommand(configSetType).addCommand(configSetIssueDiscoveryTechnique).addCommand(configSetIssueDiscoveryValue).addCommand(configSetClaudeSystemPrompt).addCommand(configSetAllowedUsers).addCommand(configSetAgentUser).addCommand(configSetDoWorkBaseBranch).addCommand(configSetDoWorkProtectedBranches).addCommand(configSetDoWorkExecutor).addCommand(configSetDoWorkModel).addCommand(configSetDoWorkEffort).addCommand(configSetDoWorkMaxRuns).addCommand(configSetDoWorkLockStaleMinutes).addCommand(configSetDoWorkPrompt).addCommand(configSetGitTrunkBranch);
|
|
242
|
+
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);
|
|
229
243
|
var configCommand = new Command("config").description("Configure automata settings").addCommand(configSet).action(async () => {
|
|
230
244
|
const [{ render }, React, { ConfigWizard }] = await Promise.all([
|
|
231
245
|
import("ink"),
|
|
232
246
|
import("react"),
|
|
233
|
-
import("./ConfigWizard-
|
|
247
|
+
import("./ConfigWizard-T7N4CZFT.js")
|
|
234
248
|
]);
|
|
235
249
|
const { waitUntilExit } = render(React.createElement(ConfigWizard));
|
|
236
250
|
await waitUntilExit();
|
|
@@ -291,6 +305,38 @@ function getPrInfo() {
|
|
|
291
305
|
};
|
|
292
306
|
}
|
|
293
307
|
|
|
308
|
+
// src/run/commandTrace.ts
|
|
309
|
+
var sink = null;
|
|
310
|
+
function startCommandTrace() {
|
|
311
|
+
sink = [];
|
|
312
|
+
}
|
|
313
|
+
function stopCommandTrace() {
|
|
314
|
+
sink = null;
|
|
315
|
+
}
|
|
316
|
+
function takeCommandTrace() {
|
|
317
|
+
const taken = sink;
|
|
318
|
+
sink = null;
|
|
319
|
+
return taken;
|
|
320
|
+
}
|
|
321
|
+
function recordCommand(command, args, startedAt, exitCode) {
|
|
322
|
+
if (sink === null) return;
|
|
323
|
+
sink.push({
|
|
324
|
+
command,
|
|
325
|
+
args: [...args],
|
|
326
|
+
// A clock that went backwards must not produce a negative duration in a
|
|
327
|
+
// report an operator is reading for anomalies.
|
|
328
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
329
|
+
exitCode
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
function describeTracedCommand(traced) {
|
|
333
|
+
const rendered = [traced.command, ...traced.args].join(" ");
|
|
334
|
+
const flat = rendered.replace(/\s+/g, " ").trim();
|
|
335
|
+
const shown = flat.length <= MAX_COMMAND_LENGTH ? flat : `${flat.slice(0, MAX_COMMAND_LENGTH)}\u2026`;
|
|
336
|
+
return `${shown} \u2014 ${String(traced.durationMs)}ms exit ${String(traced.exitCode)}`;
|
|
337
|
+
}
|
|
338
|
+
var MAX_COMMAND_LENGTH = 300;
|
|
339
|
+
|
|
294
340
|
// src/git/trunkDetection.ts
|
|
295
341
|
var TRUNK_CANDIDATES = ["main", "master"];
|
|
296
342
|
var TRUNK_BRANCH_CONFIG_KEY = "git.trunkBranch";
|
|
@@ -331,11 +377,14 @@ Set it explicitly with: automata config set git-trunk-branch <branch>`;
|
|
|
331
377
|
|
|
332
378
|
// src/git/gitService.ts
|
|
333
379
|
function run2(cmd, args) {
|
|
380
|
+
const startedAt = Date.now();
|
|
334
381
|
const result = spawnSync2(cmd, args, { encoding: "utf8" });
|
|
382
|
+
const status = result.status ?? 1;
|
|
383
|
+
recordCommand(cmd, args, startedAt, result.error ? -1 : status);
|
|
335
384
|
return {
|
|
336
385
|
stdout: result.stdout ?? "",
|
|
337
386
|
stderr: result.stderr ?? "",
|
|
338
|
-
status
|
|
387
|
+
status
|
|
339
388
|
};
|
|
340
389
|
}
|
|
341
390
|
function getCurrentBranch() {
|
|
@@ -1672,7 +1721,9 @@ import { Command as Command3 } from "commander";
|
|
|
1672
1721
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
1673
1722
|
var GITHUB_ISSUE_COMMENT_URL_RE = /github\.com\/([^/]+\/[^/]+)\/issues\/\d+#issuecomment-(\d+)/;
|
|
1674
1723
|
function run3(cmd, args) {
|
|
1724
|
+
const startedAt = Date.now();
|
|
1675
1725
|
const result = spawnSync3(cmd, args, { encoding: "utf8" });
|
|
1726
|
+
recordCommand(cmd, args, startedAt, result.error ? -1 : result.status ?? 1);
|
|
1676
1727
|
if (result.error) {
|
|
1677
1728
|
const err = result.error;
|
|
1678
1729
|
if (err.code === "ENOENT") {
|
|
@@ -1828,7 +1879,9 @@ function addCopilotReviewer(prNumber) {
|
|
|
1828
1879
|
// src/github/ghWorkService.ts
|
|
1829
1880
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
1830
1881
|
function run4(cmd, args) {
|
|
1882
|
+
const startedAt = Date.now();
|
|
1831
1883
|
const result = spawnSync4(cmd, args, { encoding: "utf8" });
|
|
1884
|
+
recordCommand(cmd, args, startedAt, result.error ? -1 : result.status ?? 1);
|
|
1832
1885
|
if (result.error) {
|
|
1833
1886
|
const err = result.error;
|
|
1834
1887
|
if (err.code === "ENOENT") {
|
|
@@ -3666,19 +3719,108 @@ function analyseAnswer(messages, p, marker, watermark) {
|
|
|
3666
3719
|
}
|
|
3667
3720
|
|
|
3668
3721
|
// src/run/runLock.ts
|
|
3669
|
-
import { writeFileSync, readFileSync as
|
|
3722
|
+
import { writeFileSync as writeFileSync2, readFileSync as readFileSync5, unlinkSync as unlinkSync2, mkdirSync, renameSync, linkSync, statSync as statSync2 } from "fs";
|
|
3670
3723
|
import { randomUUID } from "crypto";
|
|
3671
3724
|
import { hostname } from "os";
|
|
3725
|
+
import { join as join4 } from "path";
|
|
3726
|
+
|
|
3727
|
+
// src/run/heartbeat.ts
|
|
3728
|
+
import { unlinkSync, readFileSync as readFileSync4, writeFileSync } from "fs";
|
|
3672
3729
|
import { join as join3 } from "path";
|
|
3730
|
+
var HEARTBEAT_DIR = ".automata";
|
|
3731
|
+
var HEARTBEAT_FILE = "automata-heartbeat.json";
|
|
3732
|
+
var HEARTBEAT_RELATIVE_PATH = `${HEARTBEAT_DIR}/${HEARTBEAT_FILE}`;
|
|
3733
|
+
var PHASES = ["pre-flight", "discovery", "item", "summary"];
|
|
3734
|
+
function isPhase(value) {
|
|
3735
|
+
return typeof value === "string" && PHASES.includes(value);
|
|
3736
|
+
}
|
|
3737
|
+
function heartbeatPath(cwd = process.cwd()) {
|
|
3738
|
+
return join3(cwd, HEARTBEAT_DIR, HEARTBEAT_FILE);
|
|
3739
|
+
}
|
|
3740
|
+
function buildHeartbeat(token, update, now = /* @__PURE__ */ new Date()) {
|
|
3741
|
+
return {
|
|
3742
|
+
token,
|
|
3743
|
+
updatedAt: now.toISOString(),
|
|
3744
|
+
phase: update.phase,
|
|
3745
|
+
item: update.item ?? null,
|
|
3746
|
+
executor: update.executor ?? null
|
|
3747
|
+
};
|
|
3748
|
+
}
|
|
3749
|
+
function formatHeartbeat(heartbeat) {
|
|
3750
|
+
return JSON.stringify(heartbeat, null, 2) + "\n";
|
|
3751
|
+
}
|
|
3752
|
+
function parseHeartbeat(content) {
|
|
3753
|
+
let parsed;
|
|
3754
|
+
try {
|
|
3755
|
+
parsed = JSON.parse(content);
|
|
3756
|
+
} catch {
|
|
3757
|
+
return null;
|
|
3758
|
+
}
|
|
3759
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
3760
|
+
const raw = parsed;
|
|
3761
|
+
if (typeof raw.token !== "string" || raw.token.length === 0) return null;
|
|
3762
|
+
if (typeof raw.updatedAt !== "string") return null;
|
|
3763
|
+
if (!isPhase(raw.phase)) return null;
|
|
3764
|
+
return {
|
|
3765
|
+
token: raw.token,
|
|
3766
|
+
updatedAt: raw.updatedAt,
|
|
3767
|
+
phase: raw.phase,
|
|
3768
|
+
item: parseItem(raw.item),
|
|
3769
|
+
executor: parseExecutor(raw.executor)
|
|
3770
|
+
};
|
|
3771
|
+
}
|
|
3772
|
+
function parseItem(value) {
|
|
3773
|
+
if (typeof value !== "object" || value === null) return null;
|
|
3774
|
+
const raw = value;
|
|
3775
|
+
if (typeof raw.index !== "number" || typeof raw.total !== "number") return null;
|
|
3776
|
+
if (typeof raw.subject !== "string") return null;
|
|
3777
|
+
return { index: raw.index, total: raw.total, subject: raw.subject };
|
|
3778
|
+
}
|
|
3779
|
+
function parseExecutor(value) {
|
|
3780
|
+
if (typeof value !== "object" || value === null) return null;
|
|
3781
|
+
const raw = value;
|
|
3782
|
+
if (typeof raw.command !== "string" || typeof raw.startedAt !== "string") return null;
|
|
3783
|
+
return { command: raw.command, startedAt: raw.startedAt };
|
|
3784
|
+
}
|
|
3785
|
+
function writeHeartbeat(token, update, cwd = process.cwd(), now = /* @__PURE__ */ new Date()) {
|
|
3786
|
+
try {
|
|
3787
|
+
writeFileSync(heartbeatPath(cwd), formatHeartbeat(buildHeartbeat(token, update, now)), "utf8");
|
|
3788
|
+
} catch {
|
|
3789
|
+
}
|
|
3790
|
+
}
|
|
3791
|
+
function clearHeartbeat(token, cwd = process.cwd()) {
|
|
3792
|
+
if (readHeartbeat(token, cwd) === null) return;
|
|
3793
|
+
try {
|
|
3794
|
+
unlinkSync(heartbeatPath(cwd));
|
|
3795
|
+
} catch {
|
|
3796
|
+
}
|
|
3797
|
+
}
|
|
3798
|
+
function readHeartbeat(token, cwd = process.cwd()) {
|
|
3799
|
+
let content;
|
|
3800
|
+
try {
|
|
3801
|
+
content = readFileSync4(heartbeatPath(cwd), "utf8");
|
|
3802
|
+
} catch {
|
|
3803
|
+
return null;
|
|
3804
|
+
}
|
|
3805
|
+
const heartbeat = parseHeartbeat(content);
|
|
3806
|
+
if (heartbeat?.token !== token) return null;
|
|
3807
|
+
return heartbeat;
|
|
3808
|
+
}
|
|
3809
|
+
|
|
3810
|
+
// src/run/runLock.ts
|
|
3673
3811
|
var LOCK_DIR = ".automata";
|
|
3674
3812
|
var LOCK_FILE = "automata.lock";
|
|
3675
3813
|
var RUN_LOCK_RELATIVE_PATH = `${LOCK_DIR}/${LOCK_FILE}`;
|
|
3814
|
+
var AUTOMATA_OWN_PATHS = [
|
|
3815
|
+
RUN_LOCK_RELATIVE_PATH,
|
|
3816
|
+
HEARTBEAT_RELATIVE_PATH
|
|
3817
|
+
];
|
|
3676
3818
|
function lockPath() {
|
|
3677
|
-
return
|
|
3819
|
+
return join4(process.cwd(), LOCK_DIR, LOCK_FILE);
|
|
3678
3820
|
}
|
|
3679
3821
|
function processStartedAt(pid) {
|
|
3680
3822
|
try {
|
|
3681
|
-
const stat =
|
|
3823
|
+
const stat = readFileSync5(`/proc/${String(pid)}/stat`, "utf8");
|
|
3682
3824
|
const afterComm = stat.slice(stat.lastIndexOf(")") + 2);
|
|
3683
3825
|
const field = afterComm.split(" ")[19];
|
|
3684
3826
|
return field === void 0 || field.length === 0 ? null : field;
|
|
@@ -3696,7 +3838,7 @@ function isAlive(pid) {
|
|
|
3696
3838
|
}
|
|
3697
3839
|
function readOwnerError(path) {
|
|
3698
3840
|
try {
|
|
3699
|
-
|
|
3841
|
+
readFileSync5(path, "utf8");
|
|
3700
3842
|
return null;
|
|
3701
3843
|
} catch (err) {
|
|
3702
3844
|
return err.message;
|
|
@@ -3704,7 +3846,7 @@ function readOwnerError(path) {
|
|
|
3704
3846
|
}
|
|
3705
3847
|
function readOwner(path) {
|
|
3706
3848
|
try {
|
|
3707
|
-
const parsed = JSON.parse(
|
|
3849
|
+
const parsed = JSON.parse(readFileSync5(path, "utf8"));
|
|
3708
3850
|
if (typeof parsed.pid !== "number" || typeof parsed.startedAt !== "string") return null;
|
|
3709
3851
|
return {
|
|
3710
3852
|
pid: parsed.pid,
|
|
@@ -3712,7 +3854,8 @@ function readOwner(path) {
|
|
|
3712
3854
|
host: parsed.host ?? "unknown",
|
|
3713
3855
|
command: parsed.command ?? "unknown",
|
|
3714
3856
|
token: parsed.token ?? "",
|
|
3715
|
-
pidStartedAt: parsed.pidStartedAt
|
|
3857
|
+
pidStartedAt: parsed.pidStartedAt,
|
|
3858
|
+
cwd: parsed.cwd
|
|
3716
3859
|
};
|
|
3717
3860
|
} catch {
|
|
3718
3861
|
return null;
|
|
@@ -3741,10 +3884,16 @@ function heldTooLong(owner, staleMinutes) {
|
|
|
3741
3884
|
}
|
|
3742
3885
|
function makeHandle(path, token) {
|
|
3743
3886
|
let released = false;
|
|
3887
|
+
const cwd = process.cwd();
|
|
3744
3888
|
return {
|
|
3889
|
+
heartbeat(update) {
|
|
3890
|
+
if (released) return;
|
|
3891
|
+
writeHeartbeat(token, update, cwd);
|
|
3892
|
+
},
|
|
3745
3893
|
release() {
|
|
3746
3894
|
if (released) return;
|
|
3747
3895
|
released = true;
|
|
3896
|
+
clearHeartbeat(token, cwd);
|
|
3748
3897
|
const current = readOwner(path);
|
|
3749
3898
|
if (current !== null && current.token !== token) {
|
|
3750
3899
|
return;
|
|
@@ -3758,7 +3907,7 @@ function makeHandle(path, token) {
|
|
|
3758
3907
|
const owner = readOwner(takenAway);
|
|
3759
3908
|
if (owner === null || owner.token === token) {
|
|
3760
3909
|
try {
|
|
3761
|
-
|
|
3910
|
+
unlinkSync2(takenAway);
|
|
3762
3911
|
} catch {
|
|
3763
3912
|
}
|
|
3764
3913
|
return;
|
|
@@ -3768,7 +3917,7 @@ function makeHandle(path, token) {
|
|
|
3768
3917
|
} catch {
|
|
3769
3918
|
}
|
|
3770
3919
|
try {
|
|
3771
|
-
|
|
3920
|
+
unlinkSync2(takenAway);
|
|
3772
3921
|
} catch {
|
|
3773
3922
|
}
|
|
3774
3923
|
}
|
|
@@ -3781,11 +3930,12 @@ function publishLock(path, command, token) {
|
|
|
3781
3930
|
host: hostname(),
|
|
3782
3931
|
command,
|
|
3783
3932
|
token,
|
|
3784
|
-
pidStartedAt: processStartedAt(process.pid) ?? void 0
|
|
3933
|
+
pidStartedAt: processStartedAt(process.pid) ?? void 0,
|
|
3934
|
+
cwd: process.cwd()
|
|
3785
3935
|
};
|
|
3786
3936
|
const staging = `${path}.staging.${token}`;
|
|
3787
3937
|
try {
|
|
3788
|
-
|
|
3938
|
+
writeFileSync2(staging, JSON.stringify(owner, null, 2) + "\n", { encoding: "utf8", flag: "wx" });
|
|
3789
3939
|
} catch {
|
|
3790
3940
|
return false;
|
|
3791
3941
|
}
|
|
@@ -3797,7 +3947,7 @@ function publishLock(path, command, token) {
|
|
|
3797
3947
|
return false;
|
|
3798
3948
|
} finally {
|
|
3799
3949
|
try {
|
|
3800
|
-
|
|
3950
|
+
unlinkSync2(staging);
|
|
3801
3951
|
} catch {
|
|
3802
3952
|
}
|
|
3803
3953
|
}
|
|
@@ -3805,7 +3955,7 @@ function publishLock(path, command, token) {
|
|
|
3805
3955
|
function acquireRunLock(command, staleMinutes) {
|
|
3806
3956
|
const path = lockPath();
|
|
3807
3957
|
const token = randomUUID();
|
|
3808
|
-
mkdirSync(
|
|
3958
|
+
mkdirSync(join4(process.cwd(), LOCK_DIR), { recursive: true });
|
|
3809
3959
|
if (publishLock(path, command, token)) {
|
|
3810
3960
|
return { ok: true, handle: makeHandle(path, token) };
|
|
3811
3961
|
}
|
|
@@ -3840,7 +3990,7 @@ function claimStaleLock(path, token, expected) {
|
|
|
3840
3990
|
} catch {
|
|
3841
3991
|
}
|
|
3842
3992
|
try {
|
|
3843
|
-
|
|
3993
|
+
unlinkSync2(claimed);
|
|
3844
3994
|
} catch {
|
|
3845
3995
|
}
|
|
3846
3996
|
return false;
|
|
@@ -3852,7 +4002,7 @@ var CLAIM_STALE_MS = 6e4;
|
|
|
3852
4002
|
function acquireClaim(path) {
|
|
3853
4003
|
const claimPath = `${path}.claim`;
|
|
3854
4004
|
try {
|
|
3855
|
-
|
|
4005
|
+
writeFileSync2(claimPath, JSON.stringify({ pid: process.pid, at: (/* @__PURE__ */ new Date()).toISOString() }), {
|
|
3856
4006
|
encoding: "utf8",
|
|
3857
4007
|
flag: "wx"
|
|
3858
4008
|
});
|
|
@@ -3867,11 +4017,11 @@ function acquireClaim(path) {
|
|
|
3867
4017
|
return null;
|
|
3868
4018
|
}
|
|
3869
4019
|
try {
|
|
3870
|
-
|
|
4020
|
+
unlinkSync2(`${claimPath}.abandoned.${String(process.pid)}`);
|
|
3871
4021
|
} catch {
|
|
3872
4022
|
}
|
|
3873
4023
|
try {
|
|
3874
|
-
|
|
4024
|
+
writeFileSync2(claimPath, JSON.stringify({ pid: process.pid, at: (/* @__PURE__ */ new Date()).toISOString() }), {
|
|
3875
4025
|
encoding: "utf8",
|
|
3876
4026
|
flag: "wx"
|
|
3877
4027
|
});
|
|
@@ -3883,7 +4033,7 @@ function acquireClaim(path) {
|
|
|
3883
4033
|
function claimIsAbandoned(claimPath) {
|
|
3884
4034
|
let at = Number.NaN;
|
|
3885
4035
|
try {
|
|
3886
|
-
const raw = JSON.parse(
|
|
4036
|
+
const raw = JSON.parse(readFileSync5(claimPath, "utf8"));
|
|
3887
4037
|
if (raw.at !== void 0) at = Date.parse(raw.at);
|
|
3888
4038
|
} catch {
|
|
3889
4039
|
}
|
|
@@ -3915,13 +4065,13 @@ function reclaim(path, command, token, expected) {
|
|
|
3915
4065
|
return { ok: false, heldBy: readOwner(path) ?? UNKNOWN_OWNER, suspect: false };
|
|
3916
4066
|
} finally {
|
|
3917
4067
|
try {
|
|
3918
|
-
|
|
4068
|
+
unlinkSync2(claimed);
|
|
3919
4069
|
} catch {
|
|
3920
4070
|
}
|
|
3921
4071
|
}
|
|
3922
4072
|
} finally {
|
|
3923
4073
|
try {
|
|
3924
|
-
|
|
4074
|
+
unlinkSync2(claimPath);
|
|
3925
4075
|
} catch {
|
|
3926
4076
|
}
|
|
3927
4077
|
}
|
|
@@ -3946,7 +4096,12 @@ function inspectRunLock(staleMinutes, now = Date.now()) {
|
|
|
3946
4096
|
}
|
|
3947
4097
|
const held = owner;
|
|
3948
4098
|
const kind = heldTooLong(held, staleMinutes) ? "suspect" : "held";
|
|
3949
|
-
return {
|
|
4099
|
+
return {
|
|
4100
|
+
kind,
|
|
4101
|
+
owner: held,
|
|
4102
|
+
heldForMs: heldForMs(held, now),
|
|
4103
|
+
heartbeat: readHeartbeat(held.token, held.cwd ?? process.cwd())
|
|
4104
|
+
};
|
|
3950
4105
|
}
|
|
3951
4106
|
|
|
3952
4107
|
// src/git/workspaceService.ts
|
|
@@ -3958,7 +4113,7 @@ function dirtyTree() {
|
|
|
3958
4113
|
};
|
|
3959
4114
|
}
|
|
3960
4115
|
function prepareBaseBranch(baseBranch) {
|
|
3961
|
-
if (hasUncommittedChanges([
|
|
4116
|
+
if (hasUncommittedChanges([...AUTOMATA_OWN_PATHS])) return dirtyTree();
|
|
3962
4117
|
const checkout = checkoutBranch(baseBranch);
|
|
3963
4118
|
if (!checkout.ok) {
|
|
3964
4119
|
return { ok: false, reason: "checkout-failed", detail: checkout.stderr };
|
|
@@ -4054,7 +4209,7 @@ function divergenceRefusal(headRefName, pullError) {
|
|
|
4054
4209
|
};
|
|
4055
4210
|
}
|
|
4056
4211
|
function preparePrBranch(headRefName) {
|
|
4057
|
-
if (hasUncommittedChanges([
|
|
4212
|
+
if (hasUncommittedChanges([...AUTOMATA_OWN_PATHS])) return dirtyTree();
|
|
4058
4213
|
const previousRemoteSha = revParse(`refs/remotes/origin/${headRefName}`);
|
|
4059
4214
|
const fetched = fetchBranch(headRefName);
|
|
4060
4215
|
if (!fetched.ok) {
|
|
@@ -4130,7 +4285,7 @@ function findOpenPr(branch) {
|
|
|
4130
4285
|
return prs.find((pr) => pr.state === "OPEN") ?? null;
|
|
4131
4286
|
}
|
|
4132
4287
|
function rescueUncommittedChanges(options, now) {
|
|
4133
|
-
if (!hasUncommittedChanges([
|
|
4288
|
+
if (!hasUncommittedChanges([...AUTOMATA_OWN_PATHS])) {
|
|
4134
4289
|
options.log(" rescue nothing to do; the working tree is clean\n");
|
|
4135
4290
|
return { kind: "clean" };
|
|
4136
4291
|
}
|
|
@@ -4143,7 +4298,7 @@ function rescueUncommittedChanges(options, now) {
|
|
|
4143
4298
|
);
|
|
4144
4299
|
return { kind: "would-rescue", branch: target.branch, createdBranch: target.createdBranch };
|
|
4145
4300
|
}
|
|
4146
|
-
const staged = stageAllExcept([
|
|
4301
|
+
const staged = stageAllExcept([...AUTOMATA_OWN_PATHS]);
|
|
4147
4302
|
if (!staged.ok) {
|
|
4148
4303
|
options.log(` rescue FAILED to stage the changes: ${staged.stderr}
|
|
4149
4304
|
`);
|
|
@@ -4422,13 +4577,13 @@ import {
|
|
|
4422
4577
|
accessSync as accessSync2,
|
|
4423
4578
|
appendFileSync,
|
|
4424
4579
|
constants as constants2,
|
|
4425
|
-
readFileSync as
|
|
4580
|
+
readFileSync as readFileSync6,
|
|
4426
4581
|
renameSync as renameSync2,
|
|
4427
|
-
unlinkSync as
|
|
4428
|
-
writeFileSync as
|
|
4582
|
+
unlinkSync as unlinkSync3,
|
|
4583
|
+
writeFileSync as writeFileSync3
|
|
4429
4584
|
} from "fs";
|
|
4430
4585
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
4431
|
-
import { dirname as dirname2, join as
|
|
4586
|
+
import { dirname as dirname2, join as join5 } from "path";
|
|
4432
4587
|
var EXECUTION_LOG_FILE = "automata-execution.log";
|
|
4433
4588
|
var WORK_LOG_FILE = "automata-work.log";
|
|
4434
4589
|
var MAX_EXECUTION_LINES = 1e3;
|
|
@@ -4444,6 +4599,14 @@ var OUTCOMES = [
|
|
|
4444
4599
|
function operationLogDirectory() {
|
|
4445
4600
|
return dirname2(process.cwd());
|
|
4446
4601
|
}
|
|
4602
|
+
function inspectLogDirectory(dir = operationLogDirectory(), cwd = process.cwd()) {
|
|
4603
|
+
try {
|
|
4604
|
+
accessSync2(dir, constants2.W_OK);
|
|
4605
|
+
return { dir, cwd, writable: true, detail: null };
|
|
4606
|
+
} catch (err) {
|
|
4607
|
+
return { dir, cwd, writable: false, detail: err.message };
|
|
4608
|
+
}
|
|
4609
|
+
}
|
|
4447
4610
|
function repoField(repo) {
|
|
4448
4611
|
if (repo === null) return "-";
|
|
4449
4612
|
const flat = oneLine(repo);
|
|
@@ -4550,18 +4713,18 @@ function pruneOldRecords(content, now, maxAgeMs) {
|
|
|
4550
4713
|
}).map((group) => group.lines.join("\n") + "\n").join("");
|
|
4551
4714
|
}
|
|
4552
4715
|
function appendWithRetention(dir, file, content, retain) {
|
|
4553
|
-
const target =
|
|
4716
|
+
const target = join5(dir, file);
|
|
4554
4717
|
appendFileSync(target, content, "utf8");
|
|
4555
|
-
const existing =
|
|
4718
|
+
const existing = readFileSync6(target, "utf8");
|
|
4556
4719
|
const retained = retain(existing);
|
|
4557
4720
|
if (retained === existing) return;
|
|
4558
4721
|
const temp = `${target}.${randomUUID2()}.tmp`;
|
|
4559
4722
|
try {
|
|
4560
|
-
|
|
4723
|
+
writeFileSync3(temp, retained, "utf8");
|
|
4561
4724
|
renameSync2(temp, target);
|
|
4562
4725
|
} finally {
|
|
4563
4726
|
try {
|
|
4564
|
-
|
|
4727
|
+
unlinkSync3(temp);
|
|
4565
4728
|
} catch {
|
|
4566
4729
|
}
|
|
4567
4730
|
}
|
|
@@ -4599,7 +4762,7 @@ function isOutcome(value) {
|
|
|
4599
4762
|
}
|
|
4600
4763
|
function readLogFile(path) {
|
|
4601
4764
|
try {
|
|
4602
|
-
return { content:
|
|
4765
|
+
return { content: readFileSync6(path, "utf8"), error: null };
|
|
4603
4766
|
} catch (err) {
|
|
4604
4767
|
if (err.code === "ENOENT") return { content: null, error: null };
|
|
4605
4768
|
return { content: null, error: err.message };
|
|
@@ -4665,7 +4828,7 @@ function parseExecutionLine(line) {
|
|
|
4665
4828
|
};
|
|
4666
4829
|
}
|
|
4667
4830
|
function readExecutionTicks(options = {}) {
|
|
4668
|
-
const path =
|
|
4831
|
+
const path = join5(options.dir ?? operationLogDirectory(), EXECUTION_LOG_FILE);
|
|
4669
4832
|
const { content, error } = readLogFile(path);
|
|
4670
4833
|
if (content === null) return emptyResult(path, error, isFiltered(options.repo));
|
|
4671
4834
|
const entries = [];
|
|
@@ -4727,7 +4890,7 @@ function parseWorkItem(line) {
|
|
|
4727
4890
|
};
|
|
4728
4891
|
}
|
|
4729
4892
|
function readWorkRecords(options = {}) {
|
|
4730
|
-
const path =
|
|
4893
|
+
const path = join5(options.dir ?? operationLogDirectory(), WORK_LOG_FILE);
|
|
4731
4894
|
const { content, error } = readLogFile(path);
|
|
4732
4895
|
if (content === null) return emptyResult(path, error, isFiltered(options.repo));
|
|
4733
4896
|
const entries = [];
|
|
@@ -4779,11 +4942,14 @@ function readWorkRecords(options = {}) {
|
|
|
4779
4942
|
import { spawnSync as spawnSync7 } from "child_process";
|
|
4780
4943
|
var GIT_BIN = resolveCommand("git");
|
|
4781
4944
|
function git(args) {
|
|
4945
|
+
const startedAt = Date.now();
|
|
4782
4946
|
const result = spawnSync7(GIT_BIN, args, { encoding: "utf8" });
|
|
4947
|
+
const status = result.status ?? 1;
|
|
4948
|
+
recordCommand(GIT_BIN, args, startedAt, result.error ? -1 : status);
|
|
4783
4949
|
return {
|
|
4784
4950
|
stdout: result.stdout ?? "",
|
|
4785
4951
|
stderr: result.stderr ?? "",
|
|
4786
|
-
status
|
|
4952
|
+
status
|
|
4787
4953
|
};
|
|
4788
4954
|
}
|
|
4789
4955
|
function notARepo(baseBranch, detail) {
|
|
@@ -4804,7 +4970,8 @@ function notARepo(baseBranch, detail) {
|
|
|
4804
4970
|
};
|
|
4805
4971
|
}
|
|
4806
4972
|
function isOwnLockFile(porcelainEntry) {
|
|
4807
|
-
|
|
4973
|
+
const path = porcelainEntry.slice(3).trim();
|
|
4974
|
+
return AUTOMATA_OWN_PATHS.some((own) => path.endsWith(own));
|
|
4808
4975
|
}
|
|
4809
4976
|
function divergence(upstream, branch) {
|
|
4810
4977
|
const { stdout, status } = git([
|
|
@@ -4890,6 +5057,9 @@ function inspectRepoStatus(options) {
|
|
|
4890
5057
|
}
|
|
4891
5058
|
|
|
4892
5059
|
// src/run/checkReport.ts
|
|
5060
|
+
function finding(summary, command) {
|
|
5061
|
+
return { summary, command };
|
|
5062
|
+
}
|
|
4893
5063
|
var SECTION_ORDER = [
|
|
4894
5064
|
"lock",
|
|
4895
5065
|
"ticks",
|
|
@@ -4951,36 +5121,84 @@ function build(id, lines, problems, data) {
|
|
|
4951
5121
|
id,
|
|
4952
5122
|
title: SECTION_TITLES[id],
|
|
4953
5123
|
lines,
|
|
4954
|
-
problems: problems.map((
|
|
5124
|
+
problems: problems.map((item) => ({ section: id, ...item })),
|
|
4955
5125
|
data
|
|
4956
5126
|
};
|
|
4957
5127
|
}
|
|
4958
5128
|
function describeOwner(owner) {
|
|
4959
5129
|
return `pid ${String(owner.pid)} on ${owner.host}, \`${owner.command}\`, since ${owner.startedAt}`;
|
|
4960
5130
|
}
|
|
4961
|
-
function
|
|
4962
|
-
|
|
5131
|
+
function describeOwnerLocation(owner, context, lines, problems) {
|
|
5132
|
+
if (owner.cwd === void 0) {
|
|
5133
|
+
lines.push(" working directory: not recorded (lock written by an older automata)");
|
|
5134
|
+
return;
|
|
5135
|
+
}
|
|
5136
|
+
lines.push(` working directory: ${owner.cwd}`);
|
|
5137
|
+
if (owner.cwd === context.cwd) return;
|
|
5138
|
+
const ownerLogs = context.parentOf(owner.cwd);
|
|
5139
|
+
lines.push(` logs to ${ownerLogs} (this check reads ${context.logDirectory})`);
|
|
5140
|
+
problems.push(
|
|
5141
|
+
finding(
|
|
5142
|
+
`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`,
|
|
5143
|
+
`ls -l ${ownerLogs}/automata-execution.log`
|
|
5144
|
+
)
|
|
5145
|
+
);
|
|
5146
|
+
}
|
|
5147
|
+
function describeHeartbeat(heartbeat, now, lines) {
|
|
5148
|
+
if (heartbeat === void 0 || heartbeat === null) {
|
|
5149
|
+
lines.push(" phase: not reported (no heartbeat from this holder)");
|
|
5150
|
+
return;
|
|
5151
|
+
}
|
|
5152
|
+
const updated = Date.parse(heartbeat.updatedAt);
|
|
5153
|
+
const age = Number.isNaN(updated) ? "unknown" : `${describeDuration(now.getTime() - updated)} ago`;
|
|
5154
|
+
const item = heartbeat.item === null ? "" : ` \u2014 item ${String(heartbeat.item.index)} of ${String(heartbeat.item.total)}, ${heartbeat.item.subject}`;
|
|
5155
|
+
lines.push(` phase: ${heartbeat.phase}${item} (updated ${age})`);
|
|
5156
|
+
if (heartbeat.executor === null) return;
|
|
5157
|
+
const started = Date.parse(heartbeat.executor.startedAt);
|
|
5158
|
+
const running = Number.isNaN(started) ? `since ${heartbeat.executor.startedAt}` : `running for ${describeDuration(now.getTime() - started)}`;
|
|
5159
|
+
lines.push(` executor: ${heartbeat.executor.command}, ${running}`);
|
|
5160
|
+
}
|
|
5161
|
+
function lockSection(status, staleMinutes, context, now = /* @__PURE__ */ new Date()) {
|
|
5162
|
+
const data = {
|
|
5163
|
+
status: status.kind,
|
|
5164
|
+
staleMinutes,
|
|
5165
|
+
cwd: context.cwd,
|
|
5166
|
+
logDirectory: context.logDirectory
|
|
5167
|
+
};
|
|
4963
5168
|
switch (status.kind) {
|
|
4964
5169
|
case "free":
|
|
4965
5170
|
return build("lock", ["no tick is running in this checkout"], [], data);
|
|
4966
5171
|
case "held": {
|
|
4967
5172
|
const held = status.heldForMs === null ? "" : ` (${describeDuration(status.heldForMs)} so far)`;
|
|
4968
|
-
|
|
5173
|
+
const lines = [`a tick is running: ${describeOwner(status.owner)}${held}`];
|
|
5174
|
+
const problems = [];
|
|
5175
|
+
describeOwnerLocation(status.owner, context, lines, problems);
|
|
5176
|
+
describeHeartbeat(status.heartbeat, now, lines);
|
|
5177
|
+
return build("lock", lines, problems, {
|
|
4969
5178
|
...data,
|
|
4970
5179
|
owner: status.owner,
|
|
4971
|
-
heldForMs: status.heldForMs
|
|
5180
|
+
heldForMs: status.heldForMs,
|
|
5181
|
+
heartbeat: status.heartbeat ?? null
|
|
4972
5182
|
});
|
|
4973
5183
|
}
|
|
4974
5184
|
case "suspect": {
|
|
4975
5185
|
const held = status.heldForMs === null ? "unknown" : describeDuration(status.heldForMs);
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
5186
|
+
const lines = [`a tick has held the lock for ${held}: ${describeOwner(status.owner)}`];
|
|
5187
|
+
const problems = [];
|
|
5188
|
+
describeOwnerLocation(status.owner, context, lines, problems);
|
|
5189
|
+
describeHeartbeat(status.heartbeat, now, lines);
|
|
5190
|
+
problems.push(
|
|
5191
|
+
finding(
|
|
5192
|
+
`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\``,
|
|
5193
|
+
`ps -p ${String(status.owner.pid)} -o pid,lstart,args`
|
|
5194
|
+
)
|
|
4983
5195
|
);
|
|
5196
|
+
return build("lock", lines, problems, {
|
|
5197
|
+
...data,
|
|
5198
|
+
owner: status.owner,
|
|
5199
|
+
heldForMs: status.heldForMs,
|
|
5200
|
+
heartbeat: status.heartbeat ?? null
|
|
5201
|
+
});
|
|
4984
5202
|
}
|
|
4985
5203
|
case "stale":
|
|
4986
5204
|
return build(
|
|
@@ -4989,7 +5207,10 @@ function lockSection(status, staleMinutes) {
|
|
|
4989
5207
|
status.owner === null ? "a run lock exists but could not be parsed" : `a stale run lock is present: ${describeOwner(status.owner)}`
|
|
4990
5208
|
],
|
|
4991
5209
|
[
|
|
4992
|
-
|
|
5210
|
+
finding(
|
|
5211
|
+
"a stale run lock is present; the next tick reclaims it automatically, so no action is needed unless ticks keep being turned away",
|
|
5212
|
+
"cat .automata/automata.lock"
|
|
5213
|
+
)
|
|
4993
5214
|
],
|
|
4994
5215
|
{ ...data, owner: status.owner, heldForMs: status.heldForMs }
|
|
4995
5216
|
);
|
|
@@ -4997,7 +5218,12 @@ function lockSection(status, staleMinutes) {
|
|
|
4997
5218
|
return build(
|
|
4998
5219
|
"lock",
|
|
4999
5220
|
[`the run lock could not be read: ${status.detail}`],
|
|
5000
|
-
[
|
|
5221
|
+
[
|
|
5222
|
+
finding(
|
|
5223
|
+
`the run lock at \`.automata/automata.lock\` could not be read: ${status.detail}`,
|
|
5224
|
+
"ls -l .automata/automata.lock"
|
|
5225
|
+
)
|
|
5226
|
+
],
|
|
5001
5227
|
{ ...data, detail: status.detail }
|
|
5002
5228
|
);
|
|
5003
5229
|
}
|
|
@@ -5019,28 +5245,56 @@ function describeMissingTicks(read) {
|
|
|
5019
5245
|
if (read.error !== null) {
|
|
5020
5246
|
return {
|
|
5021
5247
|
line: `the execution log could not be read: ${read.error}`,
|
|
5022
|
-
problem:
|
|
5248
|
+
problem: finding(
|
|
5249
|
+
`the execution log \`${read.path}\` could not be read: ${read.error}`,
|
|
5250
|
+
`ls -l ${read.path}`
|
|
5251
|
+
),
|
|
5252
|
+
explainedByLiveTick: false
|
|
5023
5253
|
};
|
|
5024
5254
|
}
|
|
5025
5255
|
if (!read.present) {
|
|
5026
5256
|
return {
|
|
5027
5257
|
line: `no execution log at ${read.path}`,
|
|
5028
|
-
problem:
|
|
5258
|
+
problem: finding(
|
|
5259
|
+
`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`,
|
|
5260
|
+
`ls -la ${read.path}`
|
|
5261
|
+
),
|
|
5262
|
+
explainedByLiveTick: true
|
|
5029
5263
|
};
|
|
5030
5264
|
}
|
|
5031
5265
|
if (read.entries.length === 0) {
|
|
5032
5266
|
return {
|
|
5033
5267
|
line: "the execution log holds no tick for this repository",
|
|
5034
|
-
problem:
|
|
5268
|
+
problem: finding(
|
|
5269
|
+
"the execution log holds no tick for this repository \u2014 the scheduler has never successfully run `do-work` here",
|
|
5270
|
+
`tail -5 ${read.path}`
|
|
5271
|
+
),
|
|
5272
|
+
explainedByLiveTick: true
|
|
5035
5273
|
};
|
|
5036
5274
|
}
|
|
5037
5275
|
return null;
|
|
5038
5276
|
}
|
|
5039
|
-
function
|
|
5277
|
+
function describeLogDirectory(directory, lines, problems) {
|
|
5278
|
+
const writable = directory.writable ? "writable" : `not writable: ${directory.detail ?? "unknown"}`;
|
|
5279
|
+
lines.push(
|
|
5280
|
+
`log directory: ${directory.dir} (the parent of the working directory ${directory.cwd}), ${writable}`
|
|
5281
|
+
);
|
|
5282
|
+
if (directory.writable) return;
|
|
5283
|
+
problems.push(
|
|
5284
|
+
finding(
|
|
5285
|
+
`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`,
|
|
5286
|
+
`ls -ld ${directory.dir}`
|
|
5287
|
+
)
|
|
5288
|
+
);
|
|
5289
|
+
}
|
|
5290
|
+
function describeTickHistory(ticks, cadence, now, logPath, lines, problems) {
|
|
5040
5291
|
lines.push(`last tick: ${describeTick(ticks[0], now)}`);
|
|
5041
5292
|
if (ticks[0].exitCode !== 0) {
|
|
5042
5293
|
problems.push(
|
|
5043
|
-
|
|
5294
|
+
finding(
|
|
5295
|
+
`the last tick exited ${String(ticks[0].exitCode)} \u2014 see \`Last work\` below and the work log for the item that failed`,
|
|
5296
|
+
"automata do-work --check --verbose"
|
|
5297
|
+
)
|
|
5044
5298
|
);
|
|
5045
5299
|
}
|
|
5046
5300
|
const lockHeld = ticks.filter((tick) => tick.note === "lock-held").length;
|
|
@@ -5050,7 +5304,10 @@ function describeTickHistory(ticks, cadence, now, lines, problems) {
|
|
|
5050
5304
|
);
|
|
5051
5305
|
if (lockHeld === ticks.length && ticks.length > 1) {
|
|
5052
5306
|
problems.push(
|
|
5053
|
-
|
|
5307
|
+
finding(
|
|
5308
|
+
"every recorded tick was turned away by a held run lock \u2014 a previous tick is wedged; see `Run lock` above",
|
|
5309
|
+
"cat .automata/automata.lock"
|
|
5310
|
+
)
|
|
5054
5311
|
);
|
|
5055
5312
|
}
|
|
5056
5313
|
if (cadence.medianIntervalMs === null) {
|
|
@@ -5060,24 +5317,38 @@ function describeTickHistory(ticks, cadence, now, lines, problems) {
|
|
|
5060
5317
|
lines.push(`cadence: about one tick every ${describeDuration(cadence.medianIntervalMs)}`);
|
|
5061
5318
|
if (cadence.silent) {
|
|
5062
5319
|
problems.push(
|
|
5063
|
-
|
|
5320
|
+
finding(
|
|
5321
|
+
`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)`,
|
|
5322
|
+
// The log's own tail is the only thing automata can offer: the
|
|
5323
|
+
// scheduler belongs to the host, and naming a `crontab -l` here would
|
|
5324
|
+
// be a guess about how this installation is driven.
|
|
5325
|
+
`tail -5 ${logPath}`
|
|
5326
|
+
)
|
|
5064
5327
|
);
|
|
5065
5328
|
}
|
|
5066
5329
|
}
|
|
5067
5330
|
function unfilteredLine(unit) {
|
|
5068
5331
|
return `not filtered by repository: the slug could not be resolved, so ${unit} from other checkouts may be shown`;
|
|
5069
5332
|
}
|
|
5070
|
-
function tickSection(read, now) {
|
|
5333
|
+
function tickSection(read, now, directory, lockStatus) {
|
|
5071
5334
|
const lines = [];
|
|
5072
5335
|
const problems = [];
|
|
5073
5336
|
const ticks = read.entries;
|
|
5074
5337
|
const cadence = tickCadence(ticks, now);
|
|
5338
|
+
const tickInFlight = lockStatus.kind === "held" || lockStatus.kind === "suspect";
|
|
5339
|
+
describeLogDirectory(directory, lines, problems);
|
|
5075
5340
|
const missing = describeMissingTicks(read);
|
|
5076
5341
|
if (missing !== null) {
|
|
5077
5342
|
lines.push(missing.line);
|
|
5078
|
-
|
|
5343
|
+
if (tickInFlight && missing.explainedByLiveTick) {
|
|
5344
|
+
lines.push(
|
|
5345
|
+
"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"
|
|
5346
|
+
);
|
|
5347
|
+
} else {
|
|
5348
|
+
problems.push(missing.problem);
|
|
5349
|
+
}
|
|
5079
5350
|
} else {
|
|
5080
|
-
describeTickHistory(ticks, cadence, now, lines, problems);
|
|
5351
|
+
describeTickHistory(ticks, cadence, now, read.path, lines, problems);
|
|
5081
5352
|
}
|
|
5082
5353
|
if (!read.filtered && read.present) lines.push(unfilteredLine("line(s)"));
|
|
5083
5354
|
if (read.skipped > 0)
|
|
@@ -5095,7 +5366,9 @@ function tickSection(read, now) {
|
|
|
5095
5366
|
otherRepos: read.otherRepos,
|
|
5096
5367
|
filtered: read.filtered,
|
|
5097
5368
|
logPath: read.path,
|
|
5098
|
-
logPresent: read.present
|
|
5369
|
+
logPresent: read.present,
|
|
5370
|
+
logDirectory: directory,
|
|
5371
|
+
tickInFlight
|
|
5099
5372
|
});
|
|
5100
5373
|
}
|
|
5101
5374
|
function describeWorkItem(item) {
|
|
@@ -5115,7 +5388,9 @@ function workSection(read, now) {
|
|
|
5115
5388
|
const records = read.entries;
|
|
5116
5389
|
if (read.error !== null) {
|
|
5117
5390
|
lines.push(`the work log could not be read: ${read.error}`);
|
|
5118
|
-
problems.push(
|
|
5391
|
+
problems.push(
|
|
5392
|
+
finding(`the work log \`${read.path}\` could not be read: ${read.error}`, `ls -l ${read.path}`)
|
|
5393
|
+
);
|
|
5119
5394
|
} else if (!read.present || records.length === 0) {
|
|
5120
5395
|
lines.push("no tick has invoked the executor in the retained window");
|
|
5121
5396
|
} else {
|
|
@@ -5138,7 +5413,12 @@ function workSection(read, now) {
|
|
|
5138
5413
|
function describeCheckout(status, lines, problems) {
|
|
5139
5414
|
if (status.branch === null) {
|
|
5140
5415
|
lines.push(`HEAD is detached at ${status.head ?? "an unknown commit"}`);
|
|
5141
|
-
problems.push(
|
|
5416
|
+
problems.push(
|
|
5417
|
+
finding(
|
|
5418
|
+
"HEAD is detached; the pre-flight expects a branch, so check one out",
|
|
5419
|
+
"git status --short --branch"
|
|
5420
|
+
)
|
|
5421
|
+
);
|
|
5142
5422
|
} else {
|
|
5143
5423
|
const at = status.head === null ? "" : ` at ${status.head}`;
|
|
5144
5424
|
lines.push(`on ${status.branch}${at}`);
|
|
@@ -5146,7 +5426,10 @@ function describeCheckout(status, lines, problems) {
|
|
|
5146
5426
|
if (status.statusError !== null) {
|
|
5147
5427
|
lines.push(`the working tree could not be inspected: ${status.statusError}`);
|
|
5148
5428
|
problems.push(
|
|
5149
|
-
|
|
5429
|
+
finding(
|
|
5430
|
+
`\`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`,
|
|
5431
|
+
"git status --porcelain"
|
|
5432
|
+
)
|
|
5150
5433
|
);
|
|
5151
5434
|
return;
|
|
5152
5435
|
}
|
|
@@ -5157,21 +5440,30 @@ function describeCheckout(status, lines, problems) {
|
|
|
5157
5440
|
lines.push(`working tree has ${String(status.dirtyPaths.length)} uncommitted change(s):`);
|
|
5158
5441
|
for (const path of status.dirtyPaths) lines.push(` ${path}`);
|
|
5159
5442
|
problems.push(
|
|
5160
|
-
|
|
5443
|
+
finding(
|
|
5444
|
+
`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`,
|
|
5445
|
+
"git status --porcelain"
|
|
5446
|
+
)
|
|
5161
5447
|
);
|
|
5162
5448
|
}
|
|
5163
5449
|
function describeBaseBranch(status, lines, problems) {
|
|
5164
5450
|
if (!status.baseLocal) {
|
|
5165
5451
|
lines.push(`base branch ${status.baseBranch} does not exist in this checkout`);
|
|
5166
5452
|
problems.push(
|
|
5167
|
-
|
|
5453
|
+
finding(
|
|
5454
|
+
`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>\``,
|
|
5455
|
+
"git branch --list --all"
|
|
5456
|
+
)
|
|
5168
5457
|
);
|
|
5169
5458
|
return;
|
|
5170
5459
|
}
|
|
5171
5460
|
if (status.upstream === null) {
|
|
5172
5461
|
lines.push(`base branch ${status.baseBranch} has no upstream`);
|
|
5173
5462
|
problems.push(
|
|
5174
|
-
|
|
5463
|
+
finding(
|
|
5464
|
+
`the base branch \`${status.baseBranch}\` has no upstream, so the pre-flight cannot fast-forward it`,
|
|
5465
|
+
`git branch -vv --list ${status.baseBranch}`
|
|
5466
|
+
)
|
|
5175
5467
|
);
|
|
5176
5468
|
return;
|
|
5177
5469
|
}
|
|
@@ -5180,7 +5472,10 @@ function describeBaseBranch(status, lines, problems) {
|
|
|
5180
5472
|
`base branch ${status.baseBranch} has no tracking configuration; counted against ${status.upstream}`
|
|
5181
5473
|
);
|
|
5182
5474
|
problems.push(
|
|
5183
|
-
|
|
5475
|
+
finding(
|
|
5476
|
+
`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}\``,
|
|
5477
|
+
`git branch -vv --list ${status.baseBranch}`
|
|
5478
|
+
)
|
|
5184
5479
|
);
|
|
5185
5480
|
}
|
|
5186
5481
|
const freshness = status.refreshed ? "" : " (not refreshed)";
|
|
@@ -5191,14 +5486,20 @@ function describeBaseBranch(status, lines, problems) {
|
|
|
5191
5486
|
);
|
|
5192
5487
|
if (status.ahead === null || status.behind === null) {
|
|
5193
5488
|
problems.push(
|
|
5194
|
-
|
|
5489
|
+
finding(
|
|
5490
|
+
`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`,
|
|
5491
|
+
`git rev-list --left-right --count ${status.upstream}...${status.baseBranch}`
|
|
5492
|
+
)
|
|
5195
5493
|
);
|
|
5196
5494
|
return;
|
|
5197
5495
|
}
|
|
5198
5496
|
if (status.ahead === 0) return;
|
|
5199
5497
|
if (status.behind > 0) {
|
|
5200
5498
|
problems.push(
|
|
5201
|
-
|
|
5499
|
+
finding(
|
|
5500
|
+
`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`,
|
|
5501
|
+
`git log --oneline --left-right ${status.upstream}...${status.baseBranch}`
|
|
5502
|
+
)
|
|
5202
5503
|
);
|
|
5203
5504
|
return;
|
|
5204
5505
|
}
|
|
@@ -5211,7 +5512,7 @@ function gitSection(status) {
|
|
|
5211
5512
|
return build(
|
|
5212
5513
|
"git",
|
|
5213
5514
|
[`not a usable git repository: ${status.error}`],
|
|
5214
|
-
[`not a usable git repository: ${status.error}
|
|
5515
|
+
[finding(`not a usable git repository: ${status.error}`, "git rev-parse --is-inside-work-tree")],
|
|
5215
5516
|
{
|
|
5216
5517
|
...status
|
|
5217
5518
|
}
|
|
@@ -5222,7 +5523,10 @@ function gitSection(status) {
|
|
|
5222
5523
|
if (status.fetchError !== null) {
|
|
5223
5524
|
lines.push(`fetch failed: ${status.fetchError}`);
|
|
5224
5525
|
problems.push(
|
|
5225
|
-
|
|
5526
|
+
finding(
|
|
5527
|
+
`\`git fetch\` failed (${status.fetchError}); the ahead/behind figures above are from the last successful fetch and may be out of date`,
|
|
5528
|
+
"git fetch origin"
|
|
5529
|
+
)
|
|
5226
5530
|
);
|
|
5227
5531
|
}
|
|
5228
5532
|
return build("git", lines, problems, { ...status });
|
|
@@ -5240,26 +5544,38 @@ function assembleReport(input) {
|
|
|
5240
5544
|
offline: input.offline,
|
|
5241
5545
|
sections: ordered,
|
|
5242
5546
|
problems,
|
|
5243
|
-
exitCode: problems.length === 0 ? 0 : 1
|
|
5547
|
+
exitCode: problems.length === 0 ? 0 : 1,
|
|
5548
|
+
trace: input.trace ?? null,
|
|
5549
|
+
blocked: input.blocked ?? null
|
|
5244
5550
|
};
|
|
5245
5551
|
}
|
|
5552
|
+
function renderBlock(title, body, empty) {
|
|
5553
|
+
return [title, ...(body.length === 0 ? [empty] : body).map((line) => ` ${line}`), ""];
|
|
5554
|
+
}
|
|
5555
|
+
function renderTrace(trace) {
|
|
5556
|
+
return renderBlock(
|
|
5557
|
+
`Commands (${String(trace.length)})`,
|
|
5558
|
+
trace.map(describeTracedCommand),
|
|
5559
|
+
"(no git or gh command was run)"
|
|
5560
|
+
);
|
|
5561
|
+
}
|
|
5562
|
+
function renderProblems(problems) {
|
|
5563
|
+
const body = problems.flatMap((problem) => [
|
|
5564
|
+
`\xB7 ${problem.section}: ${problem.summary}`,
|
|
5565
|
+
// Indented under its problem rather than beside it: the command is a
|
|
5566
|
+
// follow-up to that one finding, not another finding.
|
|
5567
|
+
...problem.command === null ? [] : [` try: ${problem.command}`]
|
|
5568
|
+
]);
|
|
5569
|
+
return [`Problems (${String(problems.length)})`, ...body.map((line) => ` ${line}`), ""];
|
|
5570
|
+
}
|
|
5246
5571
|
function renderText(report) {
|
|
5247
5572
|
const head = `automata do-work --check \u2014 ${report.repo ?? "unknown repository"} \u2014 ${report.generatedAt.toISOString()}`;
|
|
5248
|
-
const parts = [head, ""];
|
|
5573
|
+
const parts = report.blocked === null ? [head, ""] : [`blocked: ${report.blocked.trigger}`, head, ""];
|
|
5249
5574
|
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("");
|
|
5575
|
+
parts.push(...renderBlock(section.title, section.lines, "(nothing to report)"));
|
|
5262
5576
|
}
|
|
5577
|
+
if (report.trace !== null) parts.push(...renderTrace(report.trace));
|
|
5578
|
+
if (report.problems.length > 0) parts.push(...renderProblems(report.problems));
|
|
5263
5579
|
parts.push(
|
|
5264
5580
|
report.problems.length === 0 ? "RESULT: healthy" : `RESULT: ${String(report.problems.length)} problem(s) found`
|
|
5265
5581
|
);
|
|
@@ -5281,13 +5597,21 @@ function toJson(report) {
|
|
|
5281
5597
|
offline: report.offline,
|
|
5282
5598
|
exitCode: report.exitCode,
|
|
5283
5599
|
problems: report.problems,
|
|
5284
|
-
sections
|
|
5600
|
+
sections,
|
|
5601
|
+
trace: report.trace,
|
|
5602
|
+
blocked: report.blocked
|
|
5285
5603
|
};
|
|
5286
5604
|
}
|
|
5287
5605
|
|
|
5288
5606
|
// src/commands/doWork.ts
|
|
5607
|
+
import { dirname as dirname3 } from "path";
|
|
5289
5608
|
var inFlightMarker = null;
|
|
5290
5609
|
var inFlightSync = null;
|
|
5610
|
+
var lockHandle = null;
|
|
5611
|
+
var heartbeatItem = null;
|
|
5612
|
+
function beat(update) {
|
|
5613
|
+
lockHandle?.heartbeat(update);
|
|
5614
|
+
}
|
|
5291
5615
|
function subjectLabel(issue, pr) {
|
|
5292
5616
|
if (issue !== null) return `#${String(issue)}`;
|
|
5293
5617
|
return pr === null ? "#?" : `PR #${String(pr)}`;
|
|
@@ -5342,11 +5666,6 @@ function resolveSettingsResult(options, verifyIdentity) {
|
|
|
5342
5666
|
throw err;
|
|
5343
5667
|
}
|
|
5344
5668
|
}
|
|
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
5669
|
function requireParticipants(config) {
|
|
5351
5670
|
if (config.remoteType !== "gh") {
|
|
5352
5671
|
failSettings(
|
|
@@ -5419,6 +5738,7 @@ function buildSettings(options, verifyIdentity) {
|
|
|
5419
5738
|
configEfforts: doWork.effort,
|
|
5420
5739
|
maxRuns: options.maxRuns !== void 0 ? parsePositiveInt(options.maxRuns, "--max-runs") : doWork.maxRunsPerTick ?? DEFAULT_DO_WORK.maxRunsPerTick,
|
|
5421
5740
|
lockStaleMinutes: doWork.lockStaleMinutes ?? DEFAULT_DO_WORK.lockStaleMinutes,
|
|
5741
|
+
dumpOnBlock: doWork.dumpOnBlock ?? DEFAULT_DO_WORK.dumpOnBlock,
|
|
5422
5742
|
limit: parsePositiveInt(options.limit, "--limit"),
|
|
5423
5743
|
participants: { allowedUsers, agentUser },
|
|
5424
5744
|
prompts: {
|
|
@@ -5580,11 +5900,19 @@ function validateDoWorkConfig(section) {
|
|
|
5580
5900
|
validateOptionalString(section, "baseBranch", "doWork.baseBranch");
|
|
5581
5901
|
validateOptionalInt(section, "maxRunsPerTick", "doWork.maxRunsPerTick", 0, "a non-negative integer (0 = unlimited)");
|
|
5582
5902
|
validateOptionalInt(section, "lockStaleMinutes", "doWork.lockStaleMinutes", 1, "a positive integer");
|
|
5903
|
+
validateOptionalBoolean(section, "dumpOnBlock", "doWork.dumpOnBlock");
|
|
5583
5904
|
validateProtectedBranches(section["protectedBranches"]);
|
|
5584
5905
|
validateSettingContainer(section["models"], "models", ["claude", "codex"]);
|
|
5585
5906
|
validateSettingContainer(section["effort"], "effort", ["claude", "codex"]);
|
|
5586
5907
|
validateSettingContainer(section["prompts"], "prompts", ["issueDiscuss", "prWork", "prOrphan"]);
|
|
5587
5908
|
}
|
|
5909
|
+
function validateOptionalBoolean(container, key, path) {
|
|
5910
|
+
const value = container[key];
|
|
5911
|
+
if (value === void 0 || value === null) return;
|
|
5912
|
+
if (typeof value !== "boolean") {
|
|
5913
|
+
failSettings(`${path} must be true or false, got ${JSON.stringify(value)}.`);
|
|
5914
|
+
}
|
|
5915
|
+
}
|
|
5588
5916
|
function validateProtectedBranches(value) {
|
|
5589
5917
|
if (value === void 0 || value === null) return;
|
|
5590
5918
|
const isNonEmptyString = (b) => typeof b === "string" && b.trim().length > 0;
|
|
@@ -5894,6 +6222,11 @@ function reportNoAnswer(item, marker, runError) {
|
|
|
5894
6222
|
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
6223
|
}
|
|
5896
6224
|
async function invokeExecutor(prompt, execution, silent) {
|
|
6225
|
+
beat({
|
|
6226
|
+
phase: "item",
|
|
6227
|
+
item: heartbeatItem,
|
|
6228
|
+
executor: { command: execution.executor, startedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
6229
|
+
});
|
|
5897
6230
|
if (execution.executor === "codex") {
|
|
5898
6231
|
await runCodex(prompt, { model: execution.model, effort: execution.effort });
|
|
5899
6232
|
return;
|
|
@@ -6143,37 +6476,82 @@ var doWorkCommand = new Command6("do-work").description(
|
|
|
6143
6476
|
).option(
|
|
6144
6477
|
"--no-fetch",
|
|
6145
6478
|
"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"
|
|
6479
|
+
).option(
|
|
6480
|
+
"--verbose",
|
|
6481
|
+
"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
6482
|
).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) => {
|
|
6483
|
+
const exitCode = await runDoWork(options);
|
|
6484
|
+
if (exitCode !== 0) process.exit(exitCode);
|
|
6485
|
+
});
|
|
6486
|
+
async function runDoWork(options) {
|
|
6487
|
+
try {
|
|
6488
|
+
return await runDoWorkTraced(options);
|
|
6489
|
+
} finally {
|
|
6490
|
+
stopCommandTrace();
|
|
6491
|
+
}
|
|
6492
|
+
}
|
|
6493
|
+
async function runDoWorkTraced(options) {
|
|
6147
6494
|
const startedAt = Date.now();
|
|
6148
6495
|
if (options.check === true) {
|
|
6149
6496
|
if (options.dryRun === true) {
|
|
6150
6497
|
process.stderr.write(
|
|
6151
6498
|
"Error: --check and --dry-run are two different read-only reports; run one or the other.\n"
|
|
6152
6499
|
);
|
|
6153
|
-
|
|
6500
|
+
return 1;
|
|
6154
6501
|
}
|
|
6155
|
-
|
|
6156
|
-
if (exitCode2 !== 0) process.exit(exitCode2);
|
|
6157
|
-
return;
|
|
6502
|
+
return runCheck(options);
|
|
6158
6503
|
}
|
|
6504
|
+
if (options.verbose === true) startCommandTrace();
|
|
6159
6505
|
if (options.dryRun !== true) loggableInvocation = { startedAt };
|
|
6160
|
-
const
|
|
6506
|
+
const resolved = resolveSettingsResult(options, options.dryRun !== true);
|
|
6507
|
+
if (!resolved.ok) {
|
|
6508
|
+
const payload = dumpBlocked(options, {
|
|
6509
|
+
reason: "config-invalid",
|
|
6510
|
+
trigger: `configuration is not usable \u2014 ${resolved.error}`,
|
|
6511
|
+
selection: null,
|
|
6512
|
+
resolved
|
|
6513
|
+
});
|
|
6514
|
+
if (payload !== null) out(JSON.stringify({ blocked: payload, exitCode: 1 }, null, 2) + "\n");
|
|
6515
|
+
fail(resolved.error);
|
|
6516
|
+
}
|
|
6517
|
+
const settings = resolved.settings;
|
|
6161
6518
|
if (options.dryRun === true) {
|
|
6162
6519
|
const { exitCode: exitCode2 } = await runTick(settings, options);
|
|
6163
|
-
|
|
6164
|
-
return;
|
|
6520
|
+
return exitCode2;
|
|
6165
6521
|
}
|
|
6166
6522
|
loggableInvocation = null;
|
|
6167
6523
|
const lock = acquireRunLock("do-work", settings.lockStaleMinutes);
|
|
6168
6524
|
if (!lock.ok) {
|
|
6169
|
-
const exitCode2 = reportLockHeld(lock, settings, options);
|
|
6525
|
+
const exitCode2 = reportLockHeld(lock, settings, options, resolved);
|
|
6170
6526
|
logTick([], exitCode2, startedAt, "lock-held");
|
|
6171
|
-
|
|
6172
|
-
return;
|
|
6527
|
+
return exitCode2;
|
|
6173
6528
|
}
|
|
6174
|
-
const
|
|
6529
|
+
const { exitCode, reports } = await runTickUnderLock(lock.handle, settings, options);
|
|
6530
|
+
logTick(reports, exitCode, startedAt);
|
|
6531
|
+
return exitCode;
|
|
6532
|
+
}
|
|
6533
|
+
async function runTickUnderLock(handle, settings, options) {
|
|
6534
|
+
lockHandle = handle;
|
|
6535
|
+
const onSignal = interruptHandler(handle);
|
|
6536
|
+
process.once("SIGINT", onSignal);
|
|
6537
|
+
process.once("SIGTERM", onSignal);
|
|
6538
|
+
try {
|
|
6539
|
+
return await runTick(settings, options);
|
|
6540
|
+
} catch (err) {
|
|
6541
|
+
process.stderr.write(`Error: ${err.message}
|
|
6542
|
+
`);
|
|
6543
|
+
return { exitCode: 1, reports: [] };
|
|
6544
|
+
} finally {
|
|
6545
|
+
lockHandle = null;
|
|
6546
|
+
heartbeatItem = null;
|
|
6547
|
+
handle.release();
|
|
6548
|
+
process.removeListener("SIGINT", onSignal);
|
|
6549
|
+
process.removeListener("SIGTERM", onSignal);
|
|
6550
|
+
}
|
|
6551
|
+
}
|
|
6552
|
+
function interruptHandler(handle) {
|
|
6175
6553
|
let shuttingDown = false;
|
|
6176
|
-
|
|
6554
|
+
return () => {
|
|
6177
6555
|
if (shuttingDown) return;
|
|
6178
6556
|
shuttingDown = true;
|
|
6179
6557
|
progress("\nInterrupted: stopping the executor before releasing the run lock\u2026\n");
|
|
@@ -6189,26 +6567,7 @@ var doWorkCommand = new Command6("do-work").description(
|
|
|
6189
6567
|
process.exit(130);
|
|
6190
6568
|
});
|
|
6191
6569
|
};
|
|
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
|
-
});
|
|
6570
|
+
}
|
|
6212
6571
|
function resolveRepoSlug() {
|
|
6213
6572
|
try {
|
|
6214
6573
|
const slug = getRepoSlug();
|
|
@@ -6217,7 +6576,24 @@ function resolveRepoSlug() {
|
|
|
6217
6576
|
return null;
|
|
6218
6577
|
}
|
|
6219
6578
|
}
|
|
6220
|
-
function
|
|
6579
|
+
function buildSelectionEvidence(settings, issues, linkMap, decisions, verbose) {
|
|
6580
|
+
const issuePass = issuePassEnabled(settings);
|
|
6581
|
+
const orphanPass = orphanPassEnabled(settings);
|
|
6582
|
+
return {
|
|
6583
|
+
decisions,
|
|
6584
|
+
issuePass,
|
|
6585
|
+
orphanPass,
|
|
6586
|
+
issues: verbose && issuePass ? issues : null,
|
|
6587
|
+
// Read off the link map rather than off `discoverOrphanPrs`, which returns
|
|
6588
|
+
// only the survivors — the whole point here is to show what was dropped.
|
|
6589
|
+
orphans: verbose && orphanPass ? linkMap.orphans.map((candidate) => ({
|
|
6590
|
+
number: candidate.pr.number,
|
|
6591
|
+
title: candidate.pr.title,
|
|
6592
|
+
matched: prMatchesFilter(candidate, settings)
|
|
6593
|
+
})) : null
|
|
6594
|
+
};
|
|
6595
|
+
}
|
|
6596
|
+
function collectSelection(settings, verbose) {
|
|
6221
6597
|
const issues = discoverIssues(settings);
|
|
6222
6598
|
const linkMap = getOpenPrLinkMap();
|
|
6223
6599
|
const policy = {
|
|
@@ -6225,52 +6601,135 @@ function collectSelection(settings) {
|
|
|
6225
6601
|
defaultBranch: linkMap.defaultBranch,
|
|
6226
6602
|
protectedBranches: settings.protectedBranches
|
|
6227
6603
|
};
|
|
6228
|
-
|
|
6604
|
+
const decisions = skipDuplicateHeadBranches([
|
|
6229
6605
|
...issues.map((issue) => decideWork(buildIssueState(issue, linkMap), settings.participants, policy)),
|
|
6230
6606
|
...discoverOrphanPrs(settings, linkMap).map(
|
|
6231
6607
|
(candidate) => decideOrphanPrWork({ prSurface: getPrSurface(candidate.pr.number) }, settings.participants, policy)
|
|
6232
6608
|
)
|
|
6233
6609
|
]);
|
|
6610
|
+
return buildSelectionEvidence(settings, issues, linkMap, decisions, verbose);
|
|
6234
6611
|
}
|
|
6235
|
-
function
|
|
6236
|
-
|
|
6612
|
+
function makeSelectionSection(lines, problems, data) {
|
|
6613
|
+
return {
|
|
6237
6614
|
id: "selection",
|
|
6238
6615
|
title: sectionTitle("selection"),
|
|
6239
|
-
lines
|
|
6240
|
-
problems: problems.map((
|
|
6616
|
+
lines,
|
|
6617
|
+
problems: problems.map((problem) => ({ section: "selection", ...problem })),
|
|
6241
6618
|
data
|
|
6242
|
-
}
|
|
6619
|
+
};
|
|
6620
|
+
}
|
|
6621
|
+
function describeSelectionEvidence(settings, evidence, lines) {
|
|
6622
|
+
lines.push(
|
|
6623
|
+
...describeIssueEvidence(settings, evidence),
|
|
6624
|
+
...describeOrphanEvidence(settings, evidence)
|
|
6625
|
+
);
|
|
6626
|
+
}
|
|
6627
|
+
function heading(text, count) {
|
|
6628
|
+
return `${text}${count === 0 ? "" : ":"}`;
|
|
6629
|
+
}
|
|
6630
|
+
function describeIssueEvidence(settings, evidence) {
|
|
6631
|
+
if (!evidence.issuePass) {
|
|
6632
|
+
return [
|
|
6633
|
+
`issue discovery: not run \u2014 --pr ${String(settings.onlyPr)} restricts this tick to that pull request`
|
|
6634
|
+
];
|
|
6635
|
+
}
|
|
6636
|
+
const restriction = settings.onlyIssue === void 0 ? "" : `, restricted to #${String(settings.onlyIssue)}`;
|
|
6637
|
+
const lines = [
|
|
6638
|
+
`discovery query: ${settings.technique} = ${settings.discoveryValue}, limit ${String(settings.limit)}${restriction}`
|
|
6639
|
+
];
|
|
6640
|
+
if (evidence.issues === null) return lines;
|
|
6641
|
+
lines.push(heading(`discovery returned ${String(evidence.issues.length)} issue(s)`, evidence.issues.length));
|
|
6642
|
+
for (const issue of evidence.issues) {
|
|
6643
|
+
lines.push(` #${String(issue.number)} ${issue.title}`);
|
|
6644
|
+
}
|
|
6645
|
+
return lines;
|
|
6646
|
+
}
|
|
6647
|
+
function describeOrphanEvidence(settings, evidence) {
|
|
6648
|
+
if (!evidence.orphanPass) {
|
|
6649
|
+
return [
|
|
6650
|
+
`orphan pull-request pass: not run \u2014 --issue ${String(settings.onlyIssue)} restricts this tick to that issue`
|
|
6651
|
+
];
|
|
6652
|
+
}
|
|
6653
|
+
if (evidence.orphans === null) return [];
|
|
6654
|
+
const restriction = settings.onlyPr === void 0 ? "" : ` (only PR #${String(settings.onlyPr)} would be taken up)`;
|
|
6655
|
+
const lines = [
|
|
6656
|
+
heading(
|
|
6657
|
+
`${String(evidence.orphans.length)} open orphan pull request(s) considered${restriction}`,
|
|
6658
|
+
evidence.orphans.length
|
|
6659
|
+
)
|
|
6660
|
+
];
|
|
6661
|
+
for (const orphan of evidence.orphans) {
|
|
6662
|
+
const verdict = orphan.matched ? "matches the filter" : "dropped by the discovery filter";
|
|
6663
|
+
lines.push(` PR #${String(orphan.number)} ${orphan.title} \u2014 ${verdict}`);
|
|
6664
|
+
}
|
|
6665
|
+
return lines;
|
|
6666
|
+
}
|
|
6667
|
+
function describeSelectionDecisions(decisions) {
|
|
6668
|
+
const work = decisions.filter((decision) => decision.kind === "work").length;
|
|
6669
|
+
return [
|
|
6670
|
+
`${String(work)} of ${String(decisions.length)} candidate(s) would be picked up`,
|
|
6671
|
+
...describePlan(decisions).trimEnd().split("\n")
|
|
6672
|
+
];
|
|
6673
|
+
}
|
|
6674
|
+
function selectionSection(settings, offline, verbose) {
|
|
6243
6675
|
if (offline) {
|
|
6244
|
-
return
|
|
6676
|
+
return makeSelectionSection(
|
|
6245
6677
|
["not run: --no-fetch makes no network call, and the selection needs live GitHub data"],
|
|
6246
6678
|
[],
|
|
6247
6679
|
{ ran: false, detail: "offline", plan: [] }
|
|
6248
6680
|
);
|
|
6249
6681
|
}
|
|
6250
6682
|
if (settings === null) {
|
|
6251
|
-
return
|
|
6683
|
+
return makeSelectionSection(
|
|
6252
6684
|
["not run: the configuration could not be resolved \u2014 see Environment below"],
|
|
6253
6685
|
[],
|
|
6254
6686
|
{ ran: false, detail: "configuration invalid", plan: [] }
|
|
6255
6687
|
);
|
|
6256
6688
|
}
|
|
6257
|
-
let
|
|
6689
|
+
let evidence;
|
|
6258
6690
|
try {
|
|
6259
|
-
|
|
6691
|
+
evidence = collectSelection(settings, verbose);
|
|
6260
6692
|
} catch (err) {
|
|
6261
6693
|
const detail = err.message;
|
|
6262
|
-
return
|
|
6694
|
+
return makeSelectionSection(
|
|
6263
6695
|
[`could not be computed: ${detail}`],
|
|
6264
|
-
[
|
|
6696
|
+
[
|
|
6697
|
+
{
|
|
6698
|
+
summary: `the GitHub selection could not be computed: ${detail}`,
|
|
6699
|
+
command: `gh issue list --limit ${String(settings.limit)}`
|
|
6700
|
+
}
|
|
6701
|
+
],
|
|
6265
6702
|
{ ran: false, detail, plan: [] }
|
|
6266
6703
|
);
|
|
6267
6704
|
}
|
|
6268
|
-
const
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
|
|
6705
|
+
const lines = [];
|
|
6706
|
+
if (verbose) describeSelectionEvidence(settings, evidence, lines);
|
|
6707
|
+
lines.push(...describeSelectionDecisions(evidence.decisions));
|
|
6708
|
+
return makeSelectionSection(lines, [], selectionData(evidence));
|
|
6709
|
+
}
|
|
6710
|
+
function selectionData(evidence) {
|
|
6711
|
+
return {
|
|
6712
|
+
ran: true,
|
|
6713
|
+
detail: null,
|
|
6714
|
+
plan: evidence.decisions.map(toPlanJson),
|
|
6715
|
+
issues: evidence.issues,
|
|
6716
|
+
orphans: evidence.orphans,
|
|
6717
|
+
issuePass: evidence.issuePass,
|
|
6718
|
+
orphanPass: evidence.orphanPass
|
|
6719
|
+
};
|
|
6720
|
+
}
|
|
6721
|
+
function selectionSectionFromEvidence(settings, evidence, verbose) {
|
|
6722
|
+
if (evidence === null) {
|
|
6723
|
+
return makeSelectionSection(
|
|
6724
|
+
["not run: the tick was blocked before discovery"],
|
|
6725
|
+
[],
|
|
6726
|
+
{ ran: false, detail: "blocked before discovery", plan: [] }
|
|
6727
|
+
);
|
|
6728
|
+
}
|
|
6729
|
+
const lines = [];
|
|
6730
|
+
if (verbose && settings !== null) describeSelectionEvidence(settings, evidence, lines);
|
|
6731
|
+
lines.push(...describeSelectionDecisions(evidence.decisions));
|
|
6732
|
+
return makeSelectionSection(lines, [], selectionData(evidence));
|
|
6274
6733
|
}
|
|
6275
6734
|
function describeExecutor(settings, lines, problems) {
|
|
6276
6735
|
const executor = settings.withOption ?? settings.configExecutor ?? "claude";
|
|
@@ -6279,15 +6738,16 @@ function describeExecutor(settings, lines, problems) {
|
|
|
6279
6738
|
const onPath = resolvedPath !== command;
|
|
6280
6739
|
lines.push(`default executor: ${executor} (${onPath ? resolvedPath : "not found on PATH"})`);
|
|
6281
6740
|
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
|
-
|
|
6741
|
+
problems.push({
|
|
6742
|
+
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`,
|
|
6743
|
+
command: `command -v ${command}`
|
|
6744
|
+
});
|
|
6285
6745
|
}
|
|
6286
6746
|
return { executor, command, onPath };
|
|
6287
6747
|
}
|
|
6288
|
-
function describeGitHubIdentity(
|
|
6289
|
-
if (
|
|
6290
|
-
lines.push(
|
|
6748
|
+
function describeGitHubIdentity(checkIdentity, settings, lines, problems, reason) {
|
|
6749
|
+
if (!checkIdentity) {
|
|
6750
|
+
lines.push(`\`gh\` authentication not checked: ${reason}`);
|
|
6291
6751
|
return { ghAvailable: null, ghLogin: null, identityProblem: null };
|
|
6292
6752
|
}
|
|
6293
6753
|
let identity;
|
|
@@ -6296,14 +6756,18 @@ function describeGitHubIdentity(options, settings, lines, problems) {
|
|
|
6296
6756
|
} catch (err) {
|
|
6297
6757
|
const detail = err.message;
|
|
6298
6758
|
lines.push(`\`gh\` could not be queried: ${detail}`);
|
|
6299
|
-
problems.push(
|
|
6759
|
+
problems.push({
|
|
6760
|
+
summary: `\`gh\` could not be queried (${detail}); install it and run \`gh auth login\``,
|
|
6761
|
+
command: "gh auth status"
|
|
6762
|
+
});
|
|
6300
6763
|
return { ghAvailable: false, ghLogin: null, identityProblem: null };
|
|
6301
6764
|
}
|
|
6302
6765
|
if (identity.kind === "unavailable") {
|
|
6303
6766
|
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
|
-
|
|
6767
|
+
problems.push({
|
|
6768
|
+
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`,
|
|
6769
|
+
command: "gh auth status"
|
|
6770
|
+
});
|
|
6307
6771
|
return { ghAvailable: false, ghLogin: null, identityProblem: null };
|
|
6308
6772
|
}
|
|
6309
6773
|
const ghLogin = identity.kind === "login" ? identity.login : null;
|
|
@@ -6315,7 +6779,9 @@ function describeGitHubIdentity(options, settings, lines, problems) {
|
|
|
6315
6779
|
settings.participants.agentUser,
|
|
6316
6780
|
settings.participants.allowedUsers
|
|
6317
6781
|
);
|
|
6318
|
-
if (identityProblem !== null)
|
|
6782
|
+
if (identityProblem !== null) {
|
|
6783
|
+
problems.push({ summary: identityProblem, command: "automata config get" });
|
|
6784
|
+
}
|
|
6319
6785
|
return { ghAvailable: true, ghLogin, identityProblem };
|
|
6320
6786
|
}
|
|
6321
6787
|
function readRemoteType() {
|
|
@@ -6325,7 +6791,7 @@ function readRemoteType() {
|
|
|
6325
6791
|
return null;
|
|
6326
6792
|
}
|
|
6327
6793
|
}
|
|
6328
|
-
function environmentSection(
|
|
6794
|
+
function environmentSection(checkIdentity, identitySkipReason, resolved, repo) {
|
|
6329
6795
|
const lines = [`automata ${version}`];
|
|
6330
6796
|
const problems = [];
|
|
6331
6797
|
const remoteType = readRemoteType();
|
|
@@ -6338,20 +6804,24 @@ function environmentSection(options, resolved, repo) {
|
|
|
6338
6804
|
};
|
|
6339
6805
|
if (repo === null) {
|
|
6340
6806
|
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
|
-
|
|
6807
|
+
problems.push({
|
|
6808
|
+
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",
|
|
6809
|
+
command: "git remote -v"
|
|
6810
|
+
});
|
|
6344
6811
|
} else {
|
|
6345
6812
|
lines.push(`repository ${repo}`);
|
|
6346
6813
|
}
|
|
6347
6814
|
if (!resolved.ok) {
|
|
6348
6815
|
lines.push(`configuration is not usable: ${resolved.error}`);
|
|
6349
|
-
problems.push(
|
|
6816
|
+
problems.push({
|
|
6817
|
+
summary: `the configuration is not usable: ${resolved.error}`,
|
|
6818
|
+
command: "automata config get"
|
|
6819
|
+
});
|
|
6350
6820
|
return {
|
|
6351
6821
|
id: "environment",
|
|
6352
6822
|
title: sectionTitle("environment"),
|
|
6353
6823
|
lines,
|
|
6354
|
-
problems: problems.map((
|
|
6824
|
+
problems: problems.map((problem) => ({ section: "environment", ...problem })),
|
|
6355
6825
|
// `ghAvailable` is null rather than false: nothing was asked of `gh`,
|
|
6356
6826
|
// which is not the same as having asked and been refused.
|
|
6357
6827
|
data: { ...data, ghAvailable: null, ghLogin: null, identityProblem: null }
|
|
@@ -6367,16 +6837,17 @@ function environmentSection(options, resolved, repo) {
|
|
|
6367
6837
|
);
|
|
6368
6838
|
const { executor, command, onPath } = describeExecutor(settings, lines, problems);
|
|
6369
6839
|
const { ghAvailable, ghLogin, identityProblem } = describeGitHubIdentity(
|
|
6370
|
-
|
|
6840
|
+
checkIdentity,
|
|
6371
6841
|
settings,
|
|
6372
6842
|
lines,
|
|
6373
|
-
problems
|
|
6843
|
+
problems,
|
|
6844
|
+
identitySkipReason
|
|
6374
6845
|
);
|
|
6375
6846
|
return {
|
|
6376
6847
|
id: "environment",
|
|
6377
6848
|
title: sectionTitle("environment"),
|
|
6378
6849
|
lines,
|
|
6379
|
-
problems: problems.map((
|
|
6850
|
+
problems: problems.map((problem) => ({ section: "environment", ...problem })),
|
|
6380
6851
|
data: {
|
|
6381
6852
|
...data,
|
|
6382
6853
|
discovery: { technique: settings.technique, value: settings.discoveryValue },
|
|
@@ -6392,30 +6863,121 @@ function environmentSection(options, resolved, repo) {
|
|
|
6392
6863
|
}
|
|
6393
6864
|
};
|
|
6394
6865
|
}
|
|
6866
|
+
function buildCheckReport(input) {
|
|
6867
|
+
const settings = input.resolved.ok ? input.resolved.settings : null;
|
|
6868
|
+
const staleMinutes = settings?.lockStaleMinutes ?? DEFAULT_DO_WORK.lockStaleMinutes;
|
|
6869
|
+
const baseBranch = settings?.baseBranch ?? DEFAULT_DO_WORK.baseBranch;
|
|
6870
|
+
const lockStatus = inspectRunLock(staleMinutes);
|
|
6871
|
+
const logDirectory = inspectLogDirectory();
|
|
6872
|
+
const lockContext = {
|
|
6873
|
+
cwd: logDirectory.cwd,
|
|
6874
|
+
logDirectory: logDirectory.dir,
|
|
6875
|
+
parentOf: dirname3
|
|
6876
|
+
};
|
|
6877
|
+
return assembleReport({
|
|
6878
|
+
generatedAt: input.now,
|
|
6879
|
+
repo: input.repo,
|
|
6880
|
+
offline: input.offline,
|
|
6881
|
+
blocked: input.blocked,
|
|
6882
|
+
sections: [
|
|
6883
|
+
lockSection(lockStatus, staleMinutes, lockContext, input.now),
|
|
6884
|
+
tickSection(
|
|
6885
|
+
readExecutionTicks({ repo: input.repo, limit: TICK_HISTORY }),
|
|
6886
|
+
input.now,
|
|
6887
|
+
logDirectory,
|
|
6888
|
+
lockStatus
|
|
6889
|
+
),
|
|
6890
|
+
workSection(readWorkRecords({ repo: input.repo, limit: WORK_HISTORY }), input.now),
|
|
6891
|
+
gitSection(inspectRepoStatus({ baseBranch, fetch: input.fetch })),
|
|
6892
|
+
input.selection,
|
|
6893
|
+
environmentSection(input.checkIdentity, input.identitySkipReason, input.resolved, input.repo)
|
|
6894
|
+
],
|
|
6895
|
+
// Taken last, so the trace covers every command the collectors above made.
|
|
6896
|
+
trace: takeCommandTrace()
|
|
6897
|
+
});
|
|
6898
|
+
}
|
|
6395
6899
|
function runCheck(options) {
|
|
6900
|
+
const verbose = options.verbose === true;
|
|
6901
|
+
if (verbose) startCommandTrace();
|
|
6396
6902
|
const now = /* @__PURE__ */ new Date();
|
|
6397
6903
|
const offline = options.fetch === false;
|
|
6398
6904
|
const repo = resolveRepoSlug();
|
|
6399
6905
|
const resolved = resolveSettingsResult(options, false);
|
|
6400
6906
|
const settings = resolved.ok ? resolved.settings : null;
|
|
6401
|
-
const
|
|
6402
|
-
|
|
6403
|
-
const report = assembleReport({
|
|
6404
|
-
generatedAt: now,
|
|
6907
|
+
const report = buildCheckReport({
|
|
6908
|
+
now,
|
|
6405
6909
|
repo,
|
|
6910
|
+
resolved,
|
|
6911
|
+
selection: selectionSection(settings, offline, verbose),
|
|
6912
|
+
fetch: !offline,
|
|
6913
|
+
checkIdentity: !offline,
|
|
6914
|
+
identitySkipReason: "--no-fetch",
|
|
6406
6915
|
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
|
-
]
|
|
6916
|
+
blocked: null
|
|
6415
6917
|
});
|
|
6416
6918
|
out(options.json === true ? JSON.stringify(toJson(report), null, 2) + "\n" : renderText(report));
|
|
6417
6919
|
return report.exitCode;
|
|
6418
6920
|
}
|
|
6921
|
+
function dumpOnBlockEnabled(resolved) {
|
|
6922
|
+
if (resolved.ok) return resolved.settings.dumpOnBlock;
|
|
6923
|
+
try {
|
|
6924
|
+
return readConfig().doWork?.dumpOnBlock ?? DEFAULT_DO_WORK.dumpOnBlock;
|
|
6925
|
+
} catch {
|
|
6926
|
+
return DEFAULT_DO_WORK.dumpOnBlock;
|
|
6927
|
+
}
|
|
6928
|
+
}
|
|
6929
|
+
function buildBlockedReport(context, verbose) {
|
|
6930
|
+
return buildCheckReport({
|
|
6931
|
+
now: /* @__PURE__ */ new Date(),
|
|
6932
|
+
repo: resolveRepoSlug(),
|
|
6933
|
+
resolved: context.resolved,
|
|
6934
|
+
selection: selectionSectionFromEvidence(
|
|
6935
|
+
context.resolved.ok ? context.resolved.settings : null,
|
|
6936
|
+
context.selection,
|
|
6937
|
+
verbose
|
|
6938
|
+
),
|
|
6939
|
+
fetch: false,
|
|
6940
|
+
checkIdentity: false,
|
|
6941
|
+
identitySkipReason: "the tick is exiting and the dump makes no network call",
|
|
6942
|
+
offline: true,
|
|
6943
|
+
blocked: { reason: context.reason, trigger: context.trigger }
|
|
6944
|
+
});
|
|
6945
|
+
}
|
|
6946
|
+
function dumpBlocked(options, context) {
|
|
6947
|
+
if (!dumpOnBlockEnabled(context.resolved)) return null;
|
|
6948
|
+
try {
|
|
6949
|
+
const report = buildBlockedReport(context, options.verbose === true);
|
|
6950
|
+
if (options.json === true) {
|
|
6951
|
+
return { reason: context.reason, trigger: context.trigger, report: toJson(report) };
|
|
6952
|
+
}
|
|
6953
|
+
progress(renderText(report));
|
|
6954
|
+
return null;
|
|
6955
|
+
} catch (err) {
|
|
6956
|
+
progress(`Warning: could not render the blocked-exit report: ${err.message}
|
|
6957
|
+
`);
|
|
6958
|
+
return null;
|
|
6959
|
+
}
|
|
6960
|
+
}
|
|
6961
|
+
function classifyBlocked(hygiene, decisions, items, reports) {
|
|
6962
|
+
const ran = reports.some((report) => report.ranExecutor === true);
|
|
6963
|
+
if (ran) return null;
|
|
6964
|
+
if (hygiene.degraded) {
|
|
6965
|
+
return {
|
|
6966
|
+
reason: "preflight-failed",
|
|
6967
|
+
trigger: `the pre-flight did not prepare the checkout (${describePreflightFailures(hygiene).join("; ")})`
|
|
6968
|
+
};
|
|
6969
|
+
}
|
|
6970
|
+
if (items.length === 0) {
|
|
6971
|
+
return {
|
|
6972
|
+
reason: "no-candidates",
|
|
6973
|
+
trigger: `no candidate was picked up (0 of ${String(decisions.length)})`
|
|
6974
|
+
};
|
|
6975
|
+
}
|
|
6976
|
+
return {
|
|
6977
|
+
reason: "all-skipped",
|
|
6978
|
+
trigger: `${String(items.length)} item(s) selected, none reached the executor`
|
|
6979
|
+
};
|
|
6980
|
+
}
|
|
6419
6981
|
function toTickLogItem(report) {
|
|
6420
6982
|
return {
|
|
6421
6983
|
subject: reportLabel(report),
|
|
@@ -6447,22 +7009,41 @@ function logTick(reports, exitCode, startedAt, note) {
|
|
|
6447
7009
|
items: reports.map(toTickLogItem)
|
|
6448
7010
|
});
|
|
6449
7011
|
}
|
|
6450
|
-
function reportLockHeld(lock, settings, options) {
|
|
7012
|
+
function reportLockHeld(lock, settings, options, resolved) {
|
|
6451
7013
|
const held = lock.heldBy;
|
|
6452
7014
|
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
7015
|
`;
|
|
6454
7016
|
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
7017
|
` : "";
|
|
6456
7018
|
const exitCode = lock.suspect ? 2 : 0;
|
|
7019
|
+
const context = {
|
|
7020
|
+
reason: "lock-held",
|
|
7021
|
+
trigger: `run lock held by pid ${String(held.pid)} on ${held.host}`,
|
|
7022
|
+
selection: null,
|
|
7023
|
+
resolved
|
|
7024
|
+
};
|
|
6457
7025
|
if (options.json === true) {
|
|
6458
7026
|
progress(sentence + suspectSentence);
|
|
6459
7027
|
out(
|
|
6460
|
-
JSON.stringify(
|
|
7028
|
+
JSON.stringify(
|
|
7029
|
+
{
|
|
7030
|
+
lockHeld: true,
|
|
7031
|
+
suspect: lock.suspect,
|
|
7032
|
+
heldBy: held,
|
|
7033
|
+
blocked: dumpBlocked(options, context),
|
|
7034
|
+
plan: [],
|
|
7035
|
+
items: [],
|
|
7036
|
+
exitCode
|
|
7037
|
+
},
|
|
7038
|
+
null,
|
|
7039
|
+
2
|
|
7040
|
+
) + "\n"
|
|
6461
7041
|
);
|
|
6462
|
-
|
|
6463
|
-
out(sentence);
|
|
6464
|
-
if (suspectSentence) progress(suspectSentence);
|
|
7042
|
+
return exitCode;
|
|
6465
7043
|
}
|
|
7044
|
+
out(sentence);
|
|
7045
|
+
if (suspectSentence) progress(suspectSentence);
|
|
7046
|
+
dumpBlocked(options, context);
|
|
6466
7047
|
return exitCode;
|
|
6467
7048
|
}
|
|
6468
7049
|
function explainInterruptedMarker() {
|
|
@@ -6479,6 +7060,7 @@ function explainInterruptedMarker() {
|
|
|
6479
7060
|
}
|
|
6480
7061
|
}
|
|
6481
7062
|
async function runTick(settings, options) {
|
|
7063
|
+
beat({ phase: "pre-flight" });
|
|
6482
7064
|
const hygiene = runRepoHygiene({
|
|
6483
7065
|
baseBranch: settings.baseBranch,
|
|
6484
7066
|
protectedBranches: settings.protectedBranches,
|
|
@@ -6486,6 +7068,7 @@ async function runTick(settings, options) {
|
|
|
6486
7068
|
agentUser: settings.participants.agentUser,
|
|
6487
7069
|
log: progress
|
|
6488
7070
|
});
|
|
7071
|
+
beat({ phase: "discovery" });
|
|
6489
7072
|
const issues = discoverIssues(settings);
|
|
6490
7073
|
const linkMap = getOpenPrLinkMap();
|
|
6491
7074
|
const policy = {
|
|
@@ -6511,49 +7094,85 @@ ${describePlan(decisions)}`;
|
|
|
6511
7094
|
reportDryRun(items, decisions, settings, options, hygiene);
|
|
6512
7095
|
return { exitCode: 0, reports: [] };
|
|
6513
7096
|
}
|
|
7097
|
+
const { reports, deferred } = await processItems(items, settings, options, hygiene);
|
|
7098
|
+
heartbeatItem = null;
|
|
7099
|
+
beat({ phase: "summary" });
|
|
7100
|
+
reports.push(...reportDeferred(deferred, settings));
|
|
7101
|
+
const degraded = hygiene.degraded || reports.some((report) => report.outcome !== "answered");
|
|
7102
|
+
const exitCode = degraded ? 2 : 0;
|
|
7103
|
+
const blockedBy = classifyBlocked(hygiene, decisions, items, reports);
|
|
7104
|
+
const context = blockedBy === null ? null : {
|
|
7105
|
+
...blockedBy,
|
|
7106
|
+
selection: buildSelectionEvidence(
|
|
7107
|
+
settings,
|
|
7108
|
+
issues,
|
|
7109
|
+
linkMap,
|
|
7110
|
+
decisions,
|
|
7111
|
+
options.verbose === true
|
|
7112
|
+
),
|
|
7113
|
+
resolved: { ok: true, settings }
|
|
7114
|
+
};
|
|
7115
|
+
reportTickOutcome({ options, hygiene, decisions, reports, context, exitCode });
|
|
7116
|
+
return { exitCode, reports };
|
|
7117
|
+
}
|
|
7118
|
+
async function processItems(items, settings, options, hygiene) {
|
|
6514
7119
|
const reports = [];
|
|
6515
7120
|
const deferred = [];
|
|
6516
7121
|
const preflightCauses = describePreflightFailures(hygiene);
|
|
6517
7122
|
let runsUsed = 0;
|
|
6518
|
-
for (const item of items) {
|
|
7123
|
+
for (const [index, item] of items.entries()) {
|
|
6519
7124
|
if (settings.maxRuns > 0 && runsUsed >= settings.maxRuns) {
|
|
6520
7125
|
deferred.push(item);
|
|
6521
7126
|
continue;
|
|
6522
7127
|
}
|
|
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
|
-
}
|
|
7128
|
+
heartbeatItem = { index: index + 1, total: items.length, subject: itemLabel(item).trim() };
|
|
7129
|
+
beat({ phase: "item", item: heartbeatItem });
|
|
7130
|
+
const report = await reportForItem(item, settings, options, preflightCauses);
|
|
6539
7131
|
if (report.ranExecutor === true) runsUsed++;
|
|
6540
7132
|
reports.push(report);
|
|
6541
7133
|
}
|
|
6542
|
-
|
|
7134
|
+
return { reports, deferred };
|
|
7135
|
+
}
|
|
7136
|
+
async function reportForItem(item, settings, options, preflightCauses) {
|
|
7137
|
+
try {
|
|
7138
|
+
return await processItem(item, settings, options.silent === true, preflightCauses);
|
|
7139
|
+
} catch (err) {
|
|
7140
|
+
progress(` failed: ${err.message}
|
|
7141
|
+
`);
|
|
7142
|
+
return {
|
|
7143
|
+
issue: item.issue?.number ?? null,
|
|
7144
|
+
pr: item.pr?.number ?? null,
|
|
7145
|
+
title: itemTitle(item),
|
|
7146
|
+
turn: item.turn,
|
|
7147
|
+
outcome: "failed",
|
|
7148
|
+
detail: err.message,
|
|
7149
|
+
sync: inFlightSync ?? void 0
|
|
7150
|
+
};
|
|
7151
|
+
}
|
|
7152
|
+
}
|
|
7153
|
+
function reportDeferred(deferred, settings) {
|
|
7154
|
+
return deferred.map((item) => {
|
|
6543
7155
|
progress(`
|
|
6544
7156
|
${itemLabel(item)} deferred: --max-runs / maxRunsPerTick reached.
|
|
6545
7157
|
`);
|
|
6546
|
-
|
|
7158
|
+
return {
|
|
6547
7159
|
issue: item.issue?.number ?? null,
|
|
6548
7160
|
pr: item.pr?.number ?? null,
|
|
6549
7161
|
title: itemTitle(item),
|
|
6550
7162
|
turn: item.turn,
|
|
6551
7163
|
outcome: "deferred",
|
|
6552
7164
|
detail: `run cap of ${String(settings.maxRuns)} reached`
|
|
6553
|
-
}
|
|
6554
|
-
}
|
|
6555
|
-
|
|
6556
|
-
|
|
7165
|
+
};
|
|
7166
|
+
});
|
|
7167
|
+
}
|
|
7168
|
+
function reportTickOutcome({
|
|
7169
|
+
options,
|
|
7170
|
+
hygiene,
|
|
7171
|
+
decisions,
|
|
7172
|
+
reports,
|
|
7173
|
+
context,
|
|
7174
|
+
exitCode
|
|
7175
|
+
}) {
|
|
6557
7176
|
if (options.json) {
|
|
6558
7177
|
out(
|
|
6559
7178
|
JSON.stringify(
|
|
@@ -6562,17 +7181,18 @@ ${itemLabel(item)} deferred: --max-runs / maxRunsPerTick reached.
|
|
|
6562
7181
|
preflight: toHygieneJson(hygiene),
|
|
6563
7182
|
plan: decisions.map(toPlanJson),
|
|
6564
7183
|
items: reports.map(toItemJson),
|
|
7184
|
+
blocked: context === null ? null : dumpBlocked(options, context),
|
|
6565
7185
|
exitCode
|
|
6566
7186
|
},
|
|
6567
7187
|
null,
|
|
6568
7188
|
2
|
|
6569
7189
|
) + "\n"
|
|
6570
7190
|
);
|
|
6571
|
-
|
|
6572
|
-
summarizeHygiene(hygiene);
|
|
6573
|
-
summarize(reports);
|
|
7191
|
+
return;
|
|
6574
7192
|
}
|
|
6575
|
-
|
|
7193
|
+
summarizeHygiene(hygiene);
|
|
7194
|
+
summarize(reports);
|
|
7195
|
+
if (context !== null) dumpBlocked(options, context);
|
|
6576
7196
|
}
|
|
6577
7197
|
function toRunJson(entry) {
|
|
6578
7198
|
if (entry.kind === "refused") {
|