automata-cli 0.8.0-develop.353 → 0.8.0-develop.368
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 +1131 -89
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1175,6 +1175,32 @@ function resolveReleaseVersion(requested, trunkRef, latestTag) {
|
|
|
1175
1175
|
return { ok: true, version: version2, notice: `Auto-detected version: ${latest} \u2192 ${version2}` };
|
|
1176
1176
|
}
|
|
1177
1177
|
|
|
1178
|
+
// src/git/changelogGate.ts
|
|
1179
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1180
|
+
import { join } from "path";
|
|
1181
|
+
var CHANGELOG_FILE = "CHANGELOG.md";
|
|
1182
|
+
var VERSION_HEADING = /^## \[(\d+\.\d+\.\d+)\] - \d{4}-\d{2}-\d{2}$/;
|
|
1183
|
+
function checkChangelogSection(version2, changelog) {
|
|
1184
|
+
if (changelog === null) {
|
|
1185
|
+
return { ok: true };
|
|
1186
|
+
}
|
|
1187
|
+
const documented = changelog.split("\n").map((line) => VERSION_HEADING.exec(line.trimEnd())).some((match) => match !== null && match[1] === version2);
|
|
1188
|
+
if (documented) {
|
|
1189
|
+
return { ok: true };
|
|
1190
|
+
}
|
|
1191
|
+
return {
|
|
1192
|
+
ok: false,
|
|
1193
|
+
message: `${CHANGELOG_FILE} has no section for ${version2}. Add a '## [${version2}] - YYYY-MM-DD' heading \u2014 rename the current '## [Unreleased]' heading to it and open a fresh, empty '## [Unreleased]' above \u2014 then commit and re-run. See docs/maintenance.md#what-a-release-does-to-it.`
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
function readChangelog(dir = process.cwd()) {
|
|
1197
|
+
try {
|
|
1198
|
+
return readFileSync2(join(dir, CHANGELOG_FILE), "utf8");
|
|
1199
|
+
} catch {
|
|
1200
|
+
return null;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1178
1204
|
// src/commands/git.ts
|
|
1179
1205
|
var FAIL_CONCLUSIONS = /* @__PURE__ */ new Set(["FAILURE", "TIMED_OUT", "ACTION_REQUIRED", "CANCELLED"]);
|
|
1180
1206
|
var SKIP_CONCLUSIONS = /* @__PURE__ */ new Set(["SKIPPED", "NEUTRAL"]);
|
|
@@ -1556,7 +1582,12 @@ Tags are fetched from origin first, in --dry-run too, so the version a dry run
|
|
|
1556
1582
|
prints is the one a real run would use.
|
|
1557
1583
|
|
|
1558
1584
|
When [version] is omitted the latest semver tag on origin/<trunk> is detected
|
|
1559
|
-
and the minor segment is incremented (e.g. 1.2.0 \u2192 1.3.0)
|
|
1585
|
+
and the minor segment is incremented (e.g. 1.2.0 \u2192 1.3.0).
|
|
1586
|
+
|
|
1587
|
+
CHANGELOG.md must already carry a '## [<version>] - YYYY-MM-DD' section for the
|
|
1588
|
+
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 CHANGELOG.md is
|
|
1590
|
+
not subject to the check.`
|
|
1560
1591
|
).action((version2, options) => {
|
|
1561
1592
|
const dryRun = options.dryRun ?? false;
|
|
1562
1593
|
const preconditions = checkReleasePreconditions();
|
|
@@ -1595,6 +1626,12 @@ ${fetched.message}
|
|
|
1595
1626
|
}
|
|
1596
1627
|
if (tagExists(resolvedVersion)) {
|
|
1597
1628
|
process.stderr.write(`Error: Tag '${resolvedVersion}' already exists.
|
|
1629
|
+
`);
|
|
1630
|
+
process.exit(1);
|
|
1631
|
+
}
|
|
1632
|
+
const changelog = checkChangelogSection(resolvedVersion, readChangelog());
|
|
1633
|
+
if (!changelog.ok) {
|
|
1634
|
+
process.stderr.write(`Error: ${changelog.message}
|
|
1598
1635
|
`);
|
|
1599
1636
|
process.exit(1);
|
|
1600
1637
|
}
|
|
@@ -1831,10 +1868,16 @@ function getRepoSlug() {
|
|
|
1831
1868
|
return { owner: match[1], repo: match[2] };
|
|
1832
1869
|
}
|
|
1833
1870
|
function getAuthenticatedLogin() {
|
|
1834
|
-
const
|
|
1835
|
-
|
|
1871
|
+
const identity = getAuthenticatedIdentity();
|
|
1872
|
+
return identity.kind === "login" ? identity.login : null;
|
|
1873
|
+
}
|
|
1874
|
+
function getAuthenticatedIdentity() {
|
|
1875
|
+
const { stdout, stderr, status } = run4("gh", ["api", "user", "--jq", ".login"]);
|
|
1876
|
+
if (status !== 0) {
|
|
1877
|
+
return { kind: "unavailable", detail: stderr.trim() || `gh api user exited ${String(status)}` };
|
|
1878
|
+
}
|
|
1836
1879
|
const login2 = stdout.trim();
|
|
1837
|
-
return login2.length > 0 ? login2 :
|
|
1880
|
+
return login2.length > 0 ? { kind: "login", login: login2 } : { kind: "no-user" };
|
|
1838
1881
|
}
|
|
1839
1882
|
function listCandidateIssues(technique, value, limit) {
|
|
1840
1883
|
const args = [
|
|
@@ -2234,10 +2277,27 @@ function parseCreatedPrUrl(stdout, head) {
|
|
|
2234
2277
|
// src/claude/claudeService.ts
|
|
2235
2278
|
import { spawn, spawnSync as spawnSync5 } from "child_process";
|
|
2236
2279
|
import { createInterface } from "readline";
|
|
2237
|
-
import { existsSync as existsSync2 } from "fs";
|
|
2238
|
-
import { delimiter, join } from "path";
|
|
2239
2280
|
|
|
2240
2281
|
// src/cli/spawnUtils.ts
|
|
2282
|
+
import { accessSync, constants, statSync } from "fs";
|
|
2283
|
+
import { delimiter, join as join2 } from "path";
|
|
2284
|
+
function resolveCommand(name) {
|
|
2285
|
+
const pathDirs = (process.env["PATH"] ?? "").split(delimiter);
|
|
2286
|
+
for (const dir of pathDirs) {
|
|
2287
|
+
const candidate = join2(dir, name);
|
|
2288
|
+
if (isLaunchable(candidate)) return candidate;
|
|
2289
|
+
}
|
|
2290
|
+
return name;
|
|
2291
|
+
}
|
|
2292
|
+
function isLaunchable(candidate) {
|
|
2293
|
+
try {
|
|
2294
|
+
if (!statSync(candidate).isFile()) return false;
|
|
2295
|
+
accessSync(candidate, constants.X_OK);
|
|
2296
|
+
return true;
|
|
2297
|
+
} catch {
|
|
2298
|
+
return false;
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2241
2301
|
var SHELL_SAFE = /^[A-Za-z0-9_@%+=:,./-]+$/;
|
|
2242
2302
|
var ESCAPED_QUOTE = String.raw`'\''`;
|
|
2243
2303
|
function shellQuote(arg) {
|
|
@@ -2261,8 +2321,10 @@ function handleSpawnError(error, toolName) {
|
|
|
2261
2321
|
}
|
|
2262
2322
|
function handleExitCode(status, toolName) {
|
|
2263
2323
|
if (status === null) {
|
|
2264
|
-
process.stderr.write(
|
|
2265
|
-
`)
|
|
2324
|
+
process.stderr.write(
|
|
2325
|
+
`Error: ${toolName} terminated abnormally (exit code is null, likely due to a signal).
|
|
2326
|
+
`
|
|
2327
|
+
);
|
|
2266
2328
|
process.exit(1);
|
|
2267
2329
|
}
|
|
2268
2330
|
if (status !== 0) {
|
|
@@ -2271,14 +2333,20 @@ function handleExitCode(status, toolName) {
|
|
|
2271
2333
|
process.exit(status);
|
|
2272
2334
|
}
|
|
2273
2335
|
}
|
|
2274
|
-
function
|
|
2275
|
-
if (value === void 0) return void 0;
|
|
2336
|
+
function normalizeEffortOption(value) {
|
|
2337
|
+
if (value === void 0) return { ok: true, value: void 0 };
|
|
2276
2338
|
const trimmed2 = value.trim();
|
|
2277
|
-
if (trimmed2.length === 0) {
|
|
2278
|
-
|
|
2339
|
+
if (trimmed2.length === 0) return { ok: false, error: "--effort must be a non-empty level." };
|
|
2340
|
+
return { ok: true, value: trimmed2 };
|
|
2341
|
+
}
|
|
2342
|
+
function resolveEffortOption(value) {
|
|
2343
|
+
const result = normalizeEffortOption(value);
|
|
2344
|
+
if (!result.ok) {
|
|
2345
|
+
process.stderr.write(`Error: ${result.error}
|
|
2346
|
+
`);
|
|
2279
2347
|
process.exit(1);
|
|
2280
2348
|
}
|
|
2281
|
-
return
|
|
2349
|
+
return result.value;
|
|
2282
2350
|
}
|
|
2283
2351
|
|
|
2284
2352
|
// src/cli/childRegistry.ts
|
|
@@ -2324,14 +2392,6 @@ async function terminateTrackedChildren(timeoutMs = 1e4, killGraceMs = 5e3) {
|
|
|
2324
2392
|
}
|
|
2325
2393
|
|
|
2326
2394
|
// src/claude/claudeService.ts
|
|
2327
|
-
function resolveCommand(name) {
|
|
2328
|
-
const pathDirs = (process.env["PATH"] ?? "").split(delimiter);
|
|
2329
|
-
for (const dir of pathDirs) {
|
|
2330
|
-
const candidate = join(dir, name);
|
|
2331
|
-
if (existsSync2(candidate)) return candidate;
|
|
2332
|
-
}
|
|
2333
|
-
return name;
|
|
2334
|
-
}
|
|
2335
2395
|
function buildClaudeArgs(prompt, options = {}) {
|
|
2336
2396
|
const args = [];
|
|
2337
2397
|
if (options.yolo) args.push("--dangerously-skip-permissions");
|
|
@@ -2743,7 +2803,7 @@ function linkPrToIssue(issueNumber, commentUrl, askCopilotReview, agentUser) {
|
|
|
2743
2803
|
}
|
|
2744
2804
|
|
|
2745
2805
|
// src/commands/execute.ts
|
|
2746
|
-
import { readFileSync as
|
|
2806
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
2747
2807
|
import { Command as Command4 } from "commander";
|
|
2748
2808
|
function readStdin() {
|
|
2749
2809
|
return new Promise((resolve2, reject) => {
|
|
@@ -2766,7 +2826,7 @@ async function resolvePrompt(options) {
|
|
|
2766
2826
|
if (hasFile) {
|
|
2767
2827
|
const path = options.filePrompt;
|
|
2768
2828
|
try {
|
|
2769
|
-
return
|
|
2829
|
+
return readFileSync3(path, "utf8");
|
|
2770
2830
|
} catch {
|
|
2771
2831
|
process.stderr.write(`Error: Cannot read file: ${path}
|
|
2772
2832
|
`);
|
|
@@ -3133,6 +3193,16 @@ var executePromptCommand = new Command5("execute-prompt").description("Execute a
|
|
|
3133
3193
|
// src/commands/doWork.ts
|
|
3134
3194
|
import { Command as Command6 } from "commander";
|
|
3135
3195
|
|
|
3196
|
+
// src/github/identity.ts
|
|
3197
|
+
function identityProblemFor(login2, agentUser, allowedUsers) {
|
|
3198
|
+
if (login2 === null) return null;
|
|
3199
|
+
if (login2.toLowerCase() === agentUser.toLowerCase()) return null;
|
|
3200
|
+
if (allowedUsers.some((user) => user.toLowerCase() === login2.toLowerCase())) {
|
|
3201
|
+
return `\`gh\` is authenticated as "${login2}", which is listed in allowedUsers. Everything do-work posts would be attributed to an account that is allowed to instruct the agent, so its own marker comment would look like a new instruction and each tick would answer the previous tick forever. Authenticate \`gh\` as the agent account (${agentUser}) in this environment, or correct \`agentUser\`.`;
|
|
3202
|
+
}
|
|
3203
|
+
return `\`gh\` is authenticated as "${login2}" but agentUser is "${agentUser}". Comments posted under that identity are neither the agent's nor an authorized user's, so they are filtered out of the conversation: the answer boundary would never advance and the same message would start a run on every tick. Authenticate \`gh\` as the agent account (${agentUser}) in this environment, or correct \`agentUser\`.`;
|
|
3204
|
+
}
|
|
3205
|
+
|
|
3136
3206
|
// src/github/workDetection.ts
|
|
3137
3207
|
var NO_ISSUE_MESSAGES = {
|
|
3138
3208
|
messages: [],
|
|
@@ -3596,19 +3666,19 @@ function analyseAnswer(messages, p, marker, watermark) {
|
|
|
3596
3666
|
}
|
|
3597
3667
|
|
|
3598
3668
|
// src/run/runLock.ts
|
|
3599
|
-
import { writeFileSync, readFileSync as
|
|
3669
|
+
import { writeFileSync, readFileSync as readFileSync4, unlinkSync, mkdirSync, renameSync, linkSync, statSync as statSync2 } from "fs";
|
|
3600
3670
|
import { randomUUID } from "crypto";
|
|
3601
3671
|
import { hostname } from "os";
|
|
3602
|
-
import { join as
|
|
3672
|
+
import { join as join3 } from "path";
|
|
3603
3673
|
var LOCK_DIR = ".automata";
|
|
3604
3674
|
var LOCK_FILE = "automata.lock";
|
|
3605
3675
|
var RUN_LOCK_RELATIVE_PATH = `${LOCK_DIR}/${LOCK_FILE}`;
|
|
3606
3676
|
function lockPath() {
|
|
3607
|
-
return
|
|
3677
|
+
return join3(process.cwd(), LOCK_DIR, LOCK_FILE);
|
|
3608
3678
|
}
|
|
3609
3679
|
function processStartedAt(pid) {
|
|
3610
3680
|
try {
|
|
3611
|
-
const stat =
|
|
3681
|
+
const stat = readFileSync4(`/proc/${String(pid)}/stat`, "utf8");
|
|
3612
3682
|
const afterComm = stat.slice(stat.lastIndexOf(")") + 2);
|
|
3613
3683
|
const field = afterComm.split(" ")[19];
|
|
3614
3684
|
return field === void 0 || field.length === 0 ? null : field;
|
|
@@ -3624,9 +3694,17 @@ function isAlive(pid) {
|
|
|
3624
3694
|
return err.code === "EPERM";
|
|
3625
3695
|
}
|
|
3626
3696
|
}
|
|
3697
|
+
function readOwnerError(path) {
|
|
3698
|
+
try {
|
|
3699
|
+
readFileSync4(path, "utf8");
|
|
3700
|
+
return null;
|
|
3701
|
+
} catch (err) {
|
|
3702
|
+
return err.message;
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3627
3705
|
function readOwner(path) {
|
|
3628
3706
|
try {
|
|
3629
|
-
const parsed = JSON.parse(
|
|
3707
|
+
const parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
3630
3708
|
if (typeof parsed.pid !== "number" || typeof parsed.startedAt !== "string") return null;
|
|
3631
3709
|
return {
|
|
3632
3710
|
pid: parsed.pid,
|
|
@@ -3727,7 +3805,7 @@ function publishLock(path, command, token) {
|
|
|
3727
3805
|
function acquireRunLock(command, staleMinutes) {
|
|
3728
3806
|
const path = lockPath();
|
|
3729
3807
|
const token = randomUUID();
|
|
3730
|
-
mkdirSync(
|
|
3808
|
+
mkdirSync(join3(process.cwd(), LOCK_DIR), { recursive: true });
|
|
3731
3809
|
if (publishLock(path, command, token)) {
|
|
3732
3810
|
return { ok: true, handle: makeHandle(path, token) };
|
|
3733
3811
|
}
|
|
@@ -3805,13 +3883,13 @@ function acquireClaim(path) {
|
|
|
3805
3883
|
function claimIsAbandoned(claimPath) {
|
|
3806
3884
|
let at = Number.NaN;
|
|
3807
3885
|
try {
|
|
3808
|
-
const raw = JSON.parse(
|
|
3886
|
+
const raw = JSON.parse(readFileSync4(claimPath, "utf8"));
|
|
3809
3887
|
if (raw.at !== void 0) at = Date.parse(raw.at);
|
|
3810
3888
|
} catch {
|
|
3811
3889
|
}
|
|
3812
3890
|
if (Number.isNaN(at)) {
|
|
3813
3891
|
try {
|
|
3814
|
-
at =
|
|
3892
|
+
at = statSync2(claimPath).mtimeMs;
|
|
3815
3893
|
} catch {
|
|
3816
3894
|
return true;
|
|
3817
3895
|
}
|
|
@@ -3848,6 +3926,28 @@ function reclaim(path, command, token, expected) {
|
|
|
3848
3926
|
}
|
|
3849
3927
|
}
|
|
3850
3928
|
}
|
|
3929
|
+
function heldForMs(owner, now) {
|
|
3930
|
+
const startedAt = Date.parse(owner.startedAt);
|
|
3931
|
+
return Number.isNaN(startedAt) ? null : now - startedAt;
|
|
3932
|
+
}
|
|
3933
|
+
function inspectRunLock(staleMinutes, now = Date.now()) {
|
|
3934
|
+
const path = lockPath();
|
|
3935
|
+
try {
|
|
3936
|
+
statSync2(path);
|
|
3937
|
+
} catch (err) {
|
|
3938
|
+
if (err.code === "ENOENT") return { kind: "free" };
|
|
3939
|
+
return { kind: "unreadable", detail: err.message };
|
|
3940
|
+
}
|
|
3941
|
+
const readError = readOwnerError(path);
|
|
3942
|
+
if (readError !== null) return { kind: "unreadable", detail: readError };
|
|
3943
|
+
const owner = readOwner(path);
|
|
3944
|
+
if (isStale(owner, staleMinutes)) {
|
|
3945
|
+
return { kind: "stale", owner, heldForMs: owner === null ? null : heldForMs(owner, now) };
|
|
3946
|
+
}
|
|
3947
|
+
const held = owner;
|
|
3948
|
+
const kind = heldTooLong(held, staleMinutes) ? "suspect" : "held";
|
|
3949
|
+
return { kind, owner: held, heldForMs: heldForMs(held, now) };
|
|
3950
|
+
}
|
|
3851
3951
|
|
|
3852
3952
|
// src/git/workspaceService.ts
|
|
3853
3953
|
function dirtyTree() {
|
|
@@ -3881,11 +3981,11 @@ function resetToForcePushedRemote(headRefName, previousRemoteSha) {
|
|
|
3881
3981
|
}
|
|
3882
3982
|
function rebaseOntoAlreadyAppliedRemote(headRefName) {
|
|
3883
3983
|
const upstream = `refs/remotes/origin/${headRefName}`;
|
|
3884
|
-
const
|
|
3885
|
-
if (
|
|
3886
|
-
if (
|
|
3887
|
-
if (
|
|
3888
|
-
if (
|
|
3984
|
+
const divergence2 = describeDivergence(upstream, `refs/heads/${headRefName}`);
|
|
3985
|
+
if (divergence2 === null) return null;
|
|
3986
|
+
if (divergence2.merges > 0) return null;
|
|
3987
|
+
if (divergence2.commits.length === 0) return null;
|
|
3988
|
+
if (divergence2.commits.some((commit) => !commit.alreadyUpstream)) return null;
|
|
3889
3989
|
if (isRebaseInProgress()) {
|
|
3890
3990
|
return {
|
|
3891
3991
|
ok: false,
|
|
@@ -3922,18 +4022,18 @@ function rebaseOntoAlreadyAppliedRemote(headRefName) {
|
|
|
3922
4022
|
return { ok: true, branch: headRefName, strategy: "rebase" };
|
|
3923
4023
|
}
|
|
3924
4024
|
function divergenceRefusal(headRefName, pullError) {
|
|
3925
|
-
const
|
|
4025
|
+
const divergence2 = describeDivergence(
|
|
3926
4026
|
`refs/remotes/origin/${headRefName}`,
|
|
3927
4027
|
`refs/heads/${headRefName}`
|
|
3928
4028
|
);
|
|
3929
|
-
if (
|
|
4029
|
+
if (divergence2 === null) {
|
|
3930
4030
|
return {
|
|
3931
4031
|
ok: false,
|
|
3932
4032
|
reason: "pull-failed",
|
|
3933
4033
|
detail: `${pullError} \u2014 how the local ${headRefName} relates to origin/${headRefName} could not be established, so nothing was changed and ${headRefName} is untouched; inspect it with \`git log --oneline origin/${headRefName}...${headRefName}\` before deciding what to do`
|
|
3934
4034
|
};
|
|
3935
4035
|
}
|
|
3936
|
-
const unpushedCount =
|
|
4036
|
+
const unpushedCount = divergence2.commits.filter((c) => !c.alreadyUpstream).length + divergence2.merges;
|
|
3937
4037
|
if (unpushedCount === 0) {
|
|
3938
4038
|
return {
|
|
3939
4039
|
ok: false,
|
|
@@ -4319,16 +4419,16 @@ function describeRescueRemains(rescue) {
|
|
|
4319
4419
|
|
|
4320
4420
|
// src/run/operationLog.ts
|
|
4321
4421
|
import {
|
|
4322
|
-
accessSync,
|
|
4422
|
+
accessSync as accessSync2,
|
|
4323
4423
|
appendFileSync,
|
|
4324
|
-
constants,
|
|
4325
|
-
readFileSync as
|
|
4424
|
+
constants as constants2,
|
|
4425
|
+
readFileSync as readFileSync5,
|
|
4326
4426
|
renameSync as renameSync2,
|
|
4327
4427
|
unlinkSync as unlinkSync2,
|
|
4328
4428
|
writeFileSync as writeFileSync2
|
|
4329
4429
|
} from "fs";
|
|
4330
4430
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
4331
|
-
import { dirname as dirname2, join as
|
|
4431
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
4332
4432
|
var EXECUTION_LOG_FILE = "automata-execution.log";
|
|
4333
4433
|
var WORK_LOG_FILE = "automata-work.log";
|
|
4334
4434
|
var MAX_EXECUTION_LINES = 1e3;
|
|
@@ -4450,9 +4550,9 @@ function pruneOldRecords(content, now, maxAgeMs) {
|
|
|
4450
4550
|
}).map((group) => group.lines.join("\n") + "\n").join("");
|
|
4451
4551
|
}
|
|
4452
4552
|
function appendWithRetention(dir, file, content, retain) {
|
|
4453
|
-
const target =
|
|
4553
|
+
const target = join4(dir, file);
|
|
4454
4554
|
appendFileSync(target, content, "utf8");
|
|
4455
|
-
const existing =
|
|
4555
|
+
const existing = readFileSync5(target, "utf8");
|
|
4456
4556
|
const retained = retain(existing);
|
|
4457
4557
|
if (retained === existing) return;
|
|
4458
4558
|
const temp = `${target}.${randomUUID2()}.tmp`;
|
|
@@ -4468,7 +4568,7 @@ function appendWithRetention(dir, file, content, retain) {
|
|
|
4468
4568
|
}
|
|
4469
4569
|
function recordTick(tick, dir = operationLogDirectory()) {
|
|
4470
4570
|
try {
|
|
4471
|
-
|
|
4571
|
+
accessSync2(dir, constants2.W_OK);
|
|
4472
4572
|
} catch {
|
|
4473
4573
|
return;
|
|
4474
4574
|
}
|
|
@@ -4494,6 +4594,696 @@ function recordTick(tick, dir = operationLogDirectory()) {
|
|
|
4494
4594
|
} catch {
|
|
4495
4595
|
}
|
|
4496
4596
|
}
|
|
4597
|
+
function isOutcome(value) {
|
|
4598
|
+
return OUTCOMES.includes(value);
|
|
4599
|
+
}
|
|
4600
|
+
function readLogFile(path) {
|
|
4601
|
+
try {
|
|
4602
|
+
return { content: readFileSync5(path, "utf8"), error: null };
|
|
4603
|
+
} catch (err) {
|
|
4604
|
+
if (err.code === "ENOENT") return { content: null, error: null };
|
|
4605
|
+
return { content: null, error: err.message };
|
|
4606
|
+
}
|
|
4607
|
+
}
|
|
4608
|
+
function emptyResult(path, error, filtered) {
|
|
4609
|
+
return { entries: [], present: false, error, skipped: 0, otherRepos: 0, filtered, path };
|
|
4610
|
+
}
|
|
4611
|
+
function isFiltered(want) {
|
|
4612
|
+
return want !== void 0 && want !== null;
|
|
4613
|
+
}
|
|
4614
|
+
function matchesRepo(entryRepo, want) {
|
|
4615
|
+
if (want === void 0 || want === null) return true;
|
|
4616
|
+
return entryRepo === null || entryRepo === want;
|
|
4617
|
+
}
|
|
4618
|
+
function readInt(value) {
|
|
4619
|
+
if (value === void 0) return 0;
|
|
4620
|
+
if (!/^-?\d+$/.test(value)) return null;
|
|
4621
|
+
const parsed = Number(value);
|
|
4622
|
+
return Number.isSafeInteger(parsed) ? parsed : null;
|
|
4623
|
+
}
|
|
4624
|
+
function readDurationSeconds(value) {
|
|
4625
|
+
if (value === void 0) return 0;
|
|
4626
|
+
if (!/^\d+(?:\.\d+)?s?$/.test(value)) return null;
|
|
4627
|
+
const parsed = Number.parseFloat(value);
|
|
4628
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
4629
|
+
}
|
|
4630
|
+
function parseExecutionLine(line) {
|
|
4631
|
+
const fields = line.trim().split(" ").filter((field) => field.length > 0);
|
|
4632
|
+
if (fields.length < 2) return null;
|
|
4633
|
+
const timestamp = new Date(fields[0]);
|
|
4634
|
+
if (Number.isNaN(timestamp.getTime())) return null;
|
|
4635
|
+
const pairs = /* @__PURE__ */ new Map();
|
|
4636
|
+
for (const field of fields.slice(2)) {
|
|
4637
|
+
const eq = field.indexOf("=");
|
|
4638
|
+
if (eq <= 0) continue;
|
|
4639
|
+
pairs.set(field.slice(0, eq), field.slice(eq + 1));
|
|
4640
|
+
}
|
|
4641
|
+
const exit = readInt(pairs.get("exit"));
|
|
4642
|
+
if (exit === null) return null;
|
|
4643
|
+
const counts = {};
|
|
4644
|
+
for (const outcome of OUTCOMES) {
|
|
4645
|
+
const count = readInt(pairs.get(outcome));
|
|
4646
|
+
if (count === null) return null;
|
|
4647
|
+
counts[outcome] = count;
|
|
4648
|
+
}
|
|
4649
|
+
const items = readInt(pairs.get("items"));
|
|
4650
|
+
const runs = readInt(pairs.get("runs"));
|
|
4651
|
+
const durationSeconds = readDurationSeconds(pairs.get("dur"));
|
|
4652
|
+
if (items === null || runs === null || durationSeconds === null) return null;
|
|
4653
|
+
const repo = pairs.get("repo");
|
|
4654
|
+
const note = pairs.get("note");
|
|
4655
|
+
return {
|
|
4656
|
+
timestamp,
|
|
4657
|
+
command: fields[1],
|
|
4658
|
+
repo: repo === void 0 || repo === "-" ? null : repo,
|
|
4659
|
+
items,
|
|
4660
|
+
counts,
|
|
4661
|
+
runs,
|
|
4662
|
+
exitCode: exit,
|
|
4663
|
+
durationSeconds,
|
|
4664
|
+
note: note === void 0 || note.length === 0 ? null : note
|
|
4665
|
+
};
|
|
4666
|
+
}
|
|
4667
|
+
function readExecutionTicks(options = {}) {
|
|
4668
|
+
const path = join4(options.dir ?? operationLogDirectory(), EXECUTION_LOG_FILE);
|
|
4669
|
+
const { content, error } = readLogFile(path);
|
|
4670
|
+
if (content === null) return emptyResult(path, error, isFiltered(options.repo));
|
|
4671
|
+
const entries = [];
|
|
4672
|
+
let skipped = 0;
|
|
4673
|
+
let otherRepos = 0;
|
|
4674
|
+
for (const line of content.split("\n")) {
|
|
4675
|
+
if (line.trim().length === 0) continue;
|
|
4676
|
+
const tick = parseExecutionLine(line);
|
|
4677
|
+
if (tick === null) {
|
|
4678
|
+
skipped++;
|
|
4679
|
+
continue;
|
|
4680
|
+
}
|
|
4681
|
+
if (!matchesRepo(tick.repo, options.repo)) {
|
|
4682
|
+
otherRepos++;
|
|
4683
|
+
continue;
|
|
4684
|
+
}
|
|
4685
|
+
entries.push(tick);
|
|
4686
|
+
}
|
|
4687
|
+
entries.reverse();
|
|
4688
|
+
return {
|
|
4689
|
+
entries: options.limit === void 0 ? entries : entries.slice(0, options.limit),
|
|
4690
|
+
present: true,
|
|
4691
|
+
error: null,
|
|
4692
|
+
skipped,
|
|
4693
|
+
otherRepos,
|
|
4694
|
+
filtered: isFiltered(options.repo),
|
|
4695
|
+
path
|
|
4696
|
+
};
|
|
4697
|
+
}
|
|
4698
|
+
var WORK_ITEM_LINE = /^(#\S+|PR #\S+) (\S+) (answered-no-reply|answered|skipped|failed|deferred)(?: \[([^\]]*)\])?(?: sync=(.*?))? — (.*)$/;
|
|
4699
|
+
function parseExecution(bracket) {
|
|
4700
|
+
if (bracket === void 0) return { executor: null, model: null, effort: null };
|
|
4701
|
+
const parts = bracket.split(" ").filter((part) => part.length > 0);
|
|
4702
|
+
const executor = parts[0] ?? null;
|
|
4703
|
+
const find = (key) => {
|
|
4704
|
+
const hit = parts.find((part) => part.startsWith(`${key}=`));
|
|
4705
|
+
return hit === void 0 ? null : hit.slice(key.length + 1);
|
|
4706
|
+
};
|
|
4707
|
+
return { executor, model: find("model"), effort: find("effort") };
|
|
4708
|
+
}
|
|
4709
|
+
function parseWorkHeader(line) {
|
|
4710
|
+
const header = /^=== (\S+) (\S+) ===$/.exec(line);
|
|
4711
|
+
if (header === null) return "not-a-header";
|
|
4712
|
+
const timestamp = new Date(header[1]);
|
|
4713
|
+
if (Number.isNaN(timestamp.getTime())) return null;
|
|
4714
|
+
return { timestamp, repo: header[2] === "-" ? null : header[2], items: [] };
|
|
4715
|
+
}
|
|
4716
|
+
function parseWorkItem(line) {
|
|
4717
|
+
const item = WORK_ITEM_LINE.exec(line);
|
|
4718
|
+
if (item === null) return null;
|
|
4719
|
+
return {
|
|
4720
|
+
subject: item[1],
|
|
4721
|
+
turn: item[2] === "-" ? null : item[2],
|
|
4722
|
+
// The alternation in the pattern admits nothing else.
|
|
4723
|
+
outcome: isOutcome(item[3]) ? item[3] : "skipped",
|
|
4724
|
+
...parseExecution(item[4]),
|
|
4725
|
+
sync: item[5] === void 0 || item[5].length === 0 ? null : item[5],
|
|
4726
|
+
detail: item[6]
|
|
4727
|
+
};
|
|
4728
|
+
}
|
|
4729
|
+
function readWorkRecords(options = {}) {
|
|
4730
|
+
const path = join4(options.dir ?? operationLogDirectory(), WORK_LOG_FILE);
|
|
4731
|
+
const { content, error } = readLogFile(path);
|
|
4732
|
+
if (content === null) return emptyResult(path, error, isFiltered(options.repo));
|
|
4733
|
+
const entries = [];
|
|
4734
|
+
let skipped = 0;
|
|
4735
|
+
let otherRepos = 0;
|
|
4736
|
+
let current = null;
|
|
4737
|
+
const close = () => {
|
|
4738
|
+
if (current === null) return;
|
|
4739
|
+
if (matchesRepo(current.repo, options.repo)) {
|
|
4740
|
+
entries.push(current);
|
|
4741
|
+
} else {
|
|
4742
|
+
otherRepos++;
|
|
4743
|
+
}
|
|
4744
|
+
current = null;
|
|
4745
|
+
};
|
|
4746
|
+
for (const line of content.split("\n")) {
|
|
4747
|
+
if (line.trim().length === 0) continue;
|
|
4748
|
+
const header = parseWorkHeader(line);
|
|
4749
|
+
if (header === null) {
|
|
4750
|
+
skipped++;
|
|
4751
|
+
continue;
|
|
4752
|
+
}
|
|
4753
|
+
if (header !== "not-a-header") {
|
|
4754
|
+
close();
|
|
4755
|
+
current = header;
|
|
4756
|
+
continue;
|
|
4757
|
+
}
|
|
4758
|
+
const item = parseWorkItem(line);
|
|
4759
|
+
if (item === null || current === null) {
|
|
4760
|
+
skipped++;
|
|
4761
|
+
continue;
|
|
4762
|
+
}
|
|
4763
|
+
current.items.push(item);
|
|
4764
|
+
}
|
|
4765
|
+
close();
|
|
4766
|
+
entries.reverse();
|
|
4767
|
+
return {
|
|
4768
|
+
entries: options.limit === void 0 ? entries : entries.slice(0, options.limit),
|
|
4769
|
+
present: true,
|
|
4770
|
+
error: null,
|
|
4771
|
+
skipped,
|
|
4772
|
+
otherRepos,
|
|
4773
|
+
filtered: isFiltered(options.repo),
|
|
4774
|
+
path
|
|
4775
|
+
};
|
|
4776
|
+
}
|
|
4777
|
+
|
|
4778
|
+
// src/git/repoStatus.ts
|
|
4779
|
+
import { spawnSync as spawnSync7 } from "child_process";
|
|
4780
|
+
var GIT_BIN = resolveCommand("git");
|
|
4781
|
+
function git(args) {
|
|
4782
|
+
const result = spawnSync7(GIT_BIN, args, { encoding: "utf8" });
|
|
4783
|
+
return {
|
|
4784
|
+
stdout: result.stdout ?? "",
|
|
4785
|
+
stderr: result.stderr ?? "",
|
|
4786
|
+
status: result.status ?? 1
|
|
4787
|
+
};
|
|
4788
|
+
}
|
|
4789
|
+
function notARepo(baseBranch, detail) {
|
|
4790
|
+
return {
|
|
4791
|
+
branch: null,
|
|
4792
|
+
head: null,
|
|
4793
|
+
dirtyPaths: [],
|
|
4794
|
+
statusError: null,
|
|
4795
|
+
baseBranch,
|
|
4796
|
+
baseLocal: false,
|
|
4797
|
+
upstream: null,
|
|
4798
|
+
upstreamTracked: false,
|
|
4799
|
+
ahead: null,
|
|
4800
|
+
behind: null,
|
|
4801
|
+
refreshed: false,
|
|
4802
|
+
fetchError: null,
|
|
4803
|
+
error: detail
|
|
4804
|
+
};
|
|
4805
|
+
}
|
|
4806
|
+
function isOwnLockFile(porcelainEntry) {
|
|
4807
|
+
return porcelainEntry.slice(3).trim().endsWith(RUN_LOCK_RELATIVE_PATH);
|
|
4808
|
+
}
|
|
4809
|
+
function divergence(upstream, branch) {
|
|
4810
|
+
const { stdout, status } = git([
|
|
4811
|
+
"rev-list",
|
|
4812
|
+
"--left-right",
|
|
4813
|
+
"--count",
|
|
4814
|
+
`${upstream}...${branch}`
|
|
4815
|
+
]);
|
|
4816
|
+
if (status !== 0) return null;
|
|
4817
|
+
const parts = stdout.trim().split(/\s+/);
|
|
4818
|
+
if (parts.length < 2) return null;
|
|
4819
|
+
const behind = Number.parseInt(parts[0], 10);
|
|
4820
|
+
const ahead = Number.parseInt(parts[1], 10);
|
|
4821
|
+
if (Number.isNaN(behind) || Number.isNaN(ahead)) return null;
|
|
4822
|
+
return { ahead, behind };
|
|
4823
|
+
}
|
|
4824
|
+
function readHead() {
|
|
4825
|
+
const head = git(["rev-parse", "--short", "HEAD"]);
|
|
4826
|
+
if (head.status === 0) return { head: head.stdout.trim(), fatal: null };
|
|
4827
|
+
if (git(["rev-parse", "--is-inside-work-tree"]).status === 0) return { head: null, fatal: null };
|
|
4828
|
+
return { head: null, fatal: head.stderr.trim() || "not a git repository" };
|
|
4829
|
+
}
|
|
4830
|
+
function readDirtyPaths() {
|
|
4831
|
+
const porcelain = git(["status", "--porcelain"]);
|
|
4832
|
+
if (porcelain.status !== 0) {
|
|
4833
|
+
return { paths: [], error: porcelain.stderr.trim() || "git status --porcelain failed" };
|
|
4834
|
+
}
|
|
4835
|
+
return {
|
|
4836
|
+
paths: porcelain.stdout.split("\n").filter((line) => line.trim().length > 0).filter((line) => !isOwnLockFile(line)),
|
|
4837
|
+
error: null
|
|
4838
|
+
};
|
|
4839
|
+
}
|
|
4840
|
+
function refreshBase(baseBranch) {
|
|
4841
|
+
const fetched = git([
|
|
4842
|
+
"fetch",
|
|
4843
|
+
"origin",
|
|
4844
|
+
`+refs/heads/${baseBranch}:refs/remotes/origin/${baseBranch}`
|
|
4845
|
+
]);
|
|
4846
|
+
if (fetched.status === 0) return { refreshed: true, fetchError: null };
|
|
4847
|
+
return { refreshed: false, fetchError: fetched.stderr.trim() || "git fetch failed" };
|
|
4848
|
+
}
|
|
4849
|
+
function resolveUpstream(baseBranch, baseLocal) {
|
|
4850
|
+
if (baseLocal) {
|
|
4851
|
+
const configured = git([
|
|
4852
|
+
"rev-parse",
|
|
4853
|
+
"--abbrev-ref",
|
|
4854
|
+
"--symbolic-full-name",
|
|
4855
|
+
`${baseBranch}@{u}`
|
|
4856
|
+
]);
|
|
4857
|
+
if (configured.status === 0) return { upstream: configured.stdout.trim(), tracked: true };
|
|
4858
|
+
}
|
|
4859
|
+
if (git(["rev-parse", "--verify", "--quiet", `refs/remotes/origin/${baseBranch}`]).status === 0) {
|
|
4860
|
+
return { upstream: `origin/${baseBranch}`, tracked: false };
|
|
4861
|
+
}
|
|
4862
|
+
return { upstream: null, tracked: false };
|
|
4863
|
+
}
|
|
4864
|
+
function inspectRepoStatus(options) {
|
|
4865
|
+
const { baseBranch } = options;
|
|
4866
|
+
const { head, fatal } = readHead();
|
|
4867
|
+
if (fatal !== null) return notARepo(baseBranch, fatal);
|
|
4868
|
+
const symbolic = git(["symbolic-ref", "--quiet", "--short", "HEAD"]);
|
|
4869
|
+
const branch = symbolic.status === 0 ? symbolic.stdout.trim() : null;
|
|
4870
|
+
const { paths: dirtyPaths, error: statusError } = readDirtyPaths();
|
|
4871
|
+
const baseLocal = git(["rev-parse", "--verify", "--quiet", `refs/heads/${baseBranch}`]).status === 0;
|
|
4872
|
+
const { refreshed, fetchError } = options.fetch ? refreshBase(baseBranch) : { refreshed: false, fetchError: null };
|
|
4873
|
+
const { upstream, tracked } = resolveUpstream(baseBranch, baseLocal);
|
|
4874
|
+
const counts = baseLocal && upstream !== null ? divergence(upstream, baseBranch) : null;
|
|
4875
|
+
return {
|
|
4876
|
+
branch,
|
|
4877
|
+
head,
|
|
4878
|
+
dirtyPaths,
|
|
4879
|
+
statusError,
|
|
4880
|
+
baseBranch,
|
|
4881
|
+
baseLocal,
|
|
4882
|
+
upstream,
|
|
4883
|
+
upstreamTracked: tracked,
|
|
4884
|
+
ahead: counts?.ahead ?? null,
|
|
4885
|
+
behind: counts?.behind ?? null,
|
|
4886
|
+
refreshed,
|
|
4887
|
+
fetchError,
|
|
4888
|
+
error: null
|
|
4889
|
+
};
|
|
4890
|
+
}
|
|
4891
|
+
|
|
4892
|
+
// src/run/checkReport.ts
|
|
4893
|
+
var SECTION_ORDER = [
|
|
4894
|
+
"lock",
|
|
4895
|
+
"ticks",
|
|
4896
|
+
"work",
|
|
4897
|
+
"git",
|
|
4898
|
+
"selection",
|
|
4899
|
+
"environment"
|
|
4900
|
+
];
|
|
4901
|
+
var SECTION_TITLES = {
|
|
4902
|
+
lock: "Run lock",
|
|
4903
|
+
ticks: "Recent ticks",
|
|
4904
|
+
work: "Last work",
|
|
4905
|
+
git: "Repository",
|
|
4906
|
+
selection: "Selection",
|
|
4907
|
+
environment: "Environment"
|
|
4908
|
+
};
|
|
4909
|
+
function sectionTitle(id) {
|
|
4910
|
+
return SECTION_TITLES[id];
|
|
4911
|
+
}
|
|
4912
|
+
var TICK_HISTORY = 20;
|
|
4913
|
+
var WORK_HISTORY = 3;
|
|
4914
|
+
var SILENCE_MULTIPLIER = 3;
|
|
4915
|
+
var MIN_INTERVALS = 3;
|
|
4916
|
+
function median(values) {
|
|
4917
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
4918
|
+
const middle = Math.floor(sorted.length / 2);
|
|
4919
|
+
return sorted.length % 2 === 1 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
|
|
4920
|
+
}
|
|
4921
|
+
function tickCadence(ticks, now) {
|
|
4922
|
+
if (ticks.length === 0) return { medianIntervalMs: null, sinceNewestMs: null, silent: false };
|
|
4923
|
+
const sinceNewestMs = now.getTime() - ticks[0].timestamp.getTime();
|
|
4924
|
+
const intervals = [];
|
|
4925
|
+
for (let i = 0; i + 1 < ticks.length; i++) {
|
|
4926
|
+
const gap = ticks[i].timestamp.getTime() - ticks[i + 1].timestamp.getTime();
|
|
4927
|
+
if (gap > 0) intervals.push(gap);
|
|
4928
|
+
}
|
|
4929
|
+
if (intervals.length < MIN_INTERVALS)
|
|
4930
|
+
return { medianIntervalMs: null, sinceNewestMs, silent: false };
|
|
4931
|
+
const medianIntervalMs = median(intervals);
|
|
4932
|
+
return {
|
|
4933
|
+
medianIntervalMs,
|
|
4934
|
+
sinceNewestMs,
|
|
4935
|
+
silent: sinceNewestMs > SILENCE_MULTIPLIER * medianIntervalMs
|
|
4936
|
+
};
|
|
4937
|
+
}
|
|
4938
|
+
function describeDuration(ms) {
|
|
4939
|
+
if (ms < 0) return "in the future";
|
|
4940
|
+
const seconds = Math.floor(ms / 1e3);
|
|
4941
|
+
if (seconds < 60) return `${String(seconds)}s`;
|
|
4942
|
+
const minutes = Math.floor(seconds / 60);
|
|
4943
|
+
if (minutes < 60) return `${String(minutes)}m`;
|
|
4944
|
+
const hours = Math.floor(minutes / 60);
|
|
4945
|
+
if (hours < 24) return `${String(hours)}h ${String(minutes % 60)}m`;
|
|
4946
|
+
const days = Math.floor(hours / 24);
|
|
4947
|
+
return `${String(days)}d ${String(hours % 24)}h`;
|
|
4948
|
+
}
|
|
4949
|
+
function build(id, lines, problems, data) {
|
|
4950
|
+
return {
|
|
4951
|
+
id,
|
|
4952
|
+
title: SECTION_TITLES[id],
|
|
4953
|
+
lines,
|
|
4954
|
+
problems: problems.map((summary) => ({ section: id, summary })),
|
|
4955
|
+
data
|
|
4956
|
+
};
|
|
4957
|
+
}
|
|
4958
|
+
function describeOwner(owner) {
|
|
4959
|
+
return `pid ${String(owner.pid)} on ${owner.host}, \`${owner.command}\`, since ${owner.startedAt}`;
|
|
4960
|
+
}
|
|
4961
|
+
function lockSection(status, staleMinutes) {
|
|
4962
|
+
const data = { status: status.kind, staleMinutes };
|
|
4963
|
+
switch (status.kind) {
|
|
4964
|
+
case "free":
|
|
4965
|
+
return build("lock", ["no tick is running in this checkout"], [], data);
|
|
4966
|
+
case "held": {
|
|
4967
|
+
const held = status.heldForMs === null ? "" : ` (${describeDuration(status.heldForMs)} so far)`;
|
|
4968
|
+
return build("lock", [`a tick is running: ${describeOwner(status.owner)}${held}`], [], {
|
|
4969
|
+
...data,
|
|
4970
|
+
owner: status.owner,
|
|
4971
|
+
heldForMs: status.heldForMs
|
|
4972
|
+
});
|
|
4973
|
+
}
|
|
4974
|
+
case "suspect": {
|
|
4975
|
+
const held = status.heldForMs === null ? "unknown" : describeDuration(status.heldForMs);
|
|
4976
|
+
return build(
|
|
4977
|
+
"lock",
|
|
4978
|
+
[`a tick has held the lock for ${held}: ${describeOwner(status.owner)}`],
|
|
4979
|
+
[
|
|
4980
|
+
`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\``
|
|
4981
|
+
],
|
|
4982
|
+
{ ...data, owner: status.owner, heldForMs: status.heldForMs }
|
|
4983
|
+
);
|
|
4984
|
+
}
|
|
4985
|
+
case "stale":
|
|
4986
|
+
return build(
|
|
4987
|
+
"lock",
|
|
4988
|
+
[
|
|
4989
|
+
status.owner === null ? "a run lock exists but could not be parsed" : `a stale run lock is present: ${describeOwner(status.owner)}`
|
|
4990
|
+
],
|
|
4991
|
+
[
|
|
4992
|
+
"a stale run lock is present; the next tick reclaims it automatically, so no action is needed unless ticks keep being turned away"
|
|
4993
|
+
],
|
|
4994
|
+
{ ...data, owner: status.owner, heldForMs: status.heldForMs }
|
|
4995
|
+
);
|
|
4996
|
+
case "unreadable":
|
|
4997
|
+
return build(
|
|
4998
|
+
"lock",
|
|
4999
|
+
[`the run lock could not be read: ${status.detail}`],
|
|
5000
|
+
[`the run lock at \`.automata/automata.lock\` could not be read: ${status.detail}`],
|
|
5001
|
+
{ ...data, detail: status.detail }
|
|
5002
|
+
);
|
|
5003
|
+
}
|
|
5004
|
+
}
|
|
5005
|
+
function describeTick(tick, now) {
|
|
5006
|
+
const counts = [
|
|
5007
|
+
`answered=${String(tick.counts.answered)}`,
|
|
5008
|
+
`no-reply=${String(tick.counts["answered-no-reply"])}`,
|
|
5009
|
+
`skipped=${String(tick.counts.skipped)}`,
|
|
5010
|
+
`failed=${String(tick.counts.failed)}`,
|
|
5011
|
+
`deferred=${String(tick.counts.deferred)}`,
|
|
5012
|
+
`runs=${String(tick.runs)}`
|
|
5013
|
+
].join(" ");
|
|
5014
|
+
const note = tick.note === null ? "" : ` note=${tick.note}`;
|
|
5015
|
+
const age = describeDuration(now.getTime() - tick.timestamp.getTime());
|
|
5016
|
+
return `${tick.timestamp.toISOString()} (${age} ago) exit=${String(tick.exitCode)} ${counts}${note}`;
|
|
5017
|
+
}
|
|
5018
|
+
function describeMissingTicks(read) {
|
|
5019
|
+
if (read.error !== null) {
|
|
5020
|
+
return {
|
|
5021
|
+
line: `the execution log could not be read: ${read.error}`,
|
|
5022
|
+
problem: `the execution log \`${read.path}\` could not be read: ${read.error}`
|
|
5023
|
+
};
|
|
5024
|
+
}
|
|
5025
|
+
if (!read.present) {
|
|
5026
|
+
return {
|
|
5027
|
+
line: `no execution log at ${read.path}`,
|
|
5028
|
+
problem: `no execution log at \`${read.path}\` \u2014 no tick has ever run here, or automata cannot write to the workspace root; check that the scheduler runs \`do-work\` from inside the checkout`
|
|
5029
|
+
};
|
|
5030
|
+
}
|
|
5031
|
+
if (read.entries.length === 0) {
|
|
5032
|
+
return {
|
|
5033
|
+
line: "the execution log holds no tick for this repository",
|
|
5034
|
+
problem: "the execution log holds no tick for this repository \u2014 the scheduler has never successfully run `do-work` here"
|
|
5035
|
+
};
|
|
5036
|
+
}
|
|
5037
|
+
return null;
|
|
5038
|
+
}
|
|
5039
|
+
function describeTickHistory(ticks, cadence, now, lines, problems) {
|
|
5040
|
+
lines.push(`last tick: ${describeTick(ticks[0], now)}`);
|
|
5041
|
+
if (ticks[0].exitCode !== 0) {
|
|
5042
|
+
problems.push(
|
|
5043
|
+
`the last tick exited ${String(ticks[0].exitCode)} \u2014 see \`Last work\` below and the work log for the item that failed`
|
|
5044
|
+
);
|
|
5045
|
+
}
|
|
5046
|
+
const lockHeld = ticks.filter((tick) => tick.note === "lock-held").length;
|
|
5047
|
+
const withRuns = ticks.filter((tick) => tick.runs > 0).length;
|
|
5048
|
+
lines.push(
|
|
5049
|
+
`history: ${String(ticks.length)} tick(s), ${String(withRuns)} invoked the executor, ${String(lockHeld)} were turned away by a held lock`
|
|
5050
|
+
);
|
|
5051
|
+
if (lockHeld === ticks.length && ticks.length > 1) {
|
|
5052
|
+
problems.push(
|
|
5053
|
+
"every recorded tick was turned away by a held run lock \u2014 a previous tick is wedged; see `Run lock` above"
|
|
5054
|
+
);
|
|
5055
|
+
}
|
|
5056
|
+
if (cadence.medianIntervalMs === null) {
|
|
5057
|
+
lines.push("cadence: not enough history to judge whether the scheduler is still firing");
|
|
5058
|
+
return;
|
|
5059
|
+
}
|
|
5060
|
+
lines.push(`cadence: about one tick every ${describeDuration(cadence.medianIntervalMs)}`);
|
|
5061
|
+
if (cadence.silent) {
|
|
5062
|
+
problems.push(
|
|
5063
|
+
`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)`
|
|
5064
|
+
);
|
|
5065
|
+
}
|
|
5066
|
+
}
|
|
5067
|
+
function unfilteredLine(unit) {
|
|
5068
|
+
return `not filtered by repository: the slug could not be resolved, so ${unit} from other checkouts may be shown`;
|
|
5069
|
+
}
|
|
5070
|
+
function tickSection(read, now) {
|
|
5071
|
+
const lines = [];
|
|
5072
|
+
const problems = [];
|
|
5073
|
+
const ticks = read.entries;
|
|
5074
|
+
const cadence = tickCadence(ticks, now);
|
|
5075
|
+
const missing = describeMissingTicks(read);
|
|
5076
|
+
if (missing !== null) {
|
|
5077
|
+
lines.push(missing.line);
|
|
5078
|
+
problems.push(missing.problem);
|
|
5079
|
+
} else {
|
|
5080
|
+
describeTickHistory(ticks, cadence, now, lines, problems);
|
|
5081
|
+
}
|
|
5082
|
+
if (!read.filtered && read.present) lines.push(unfilteredLine("line(s)"));
|
|
5083
|
+
if (read.skipped > 0)
|
|
5084
|
+
lines.push(`${String(read.skipped)} log line(s) could not be parsed and were ignored`);
|
|
5085
|
+
if (read.otherRepos > 0)
|
|
5086
|
+
lines.push(`${String(read.otherRepos)} line(s) belonged to another repository`);
|
|
5087
|
+
return build("ticks", lines, problems, {
|
|
5088
|
+
newest: ticks[0] ?? null,
|
|
5089
|
+
history: ticks,
|
|
5090
|
+
lockHeldCount: ticks.filter((tick) => tick.note === "lock-held").length,
|
|
5091
|
+
medianIntervalMs: cadence.medianIntervalMs,
|
|
5092
|
+
sinceNewestMs: cadence.sinceNewestMs,
|
|
5093
|
+
silent: cadence.silent,
|
|
5094
|
+
skipped: read.skipped,
|
|
5095
|
+
otherRepos: read.otherRepos,
|
|
5096
|
+
filtered: read.filtered,
|
|
5097
|
+
logPath: read.path,
|
|
5098
|
+
logPresent: read.present
|
|
5099
|
+
});
|
|
5100
|
+
}
|
|
5101
|
+
function describeWorkItem(item) {
|
|
5102
|
+
const how = item.executor === null ? "" : ` [${[item.executor, item.model, item.effort].filter((part) => part !== null).join(" ")}]`;
|
|
5103
|
+
const sync = item.sync === null ? "" : ` sync=${item.sync}`;
|
|
5104
|
+
return ` ${item.subject} ${item.turn ?? "-"} ${item.outcome}${how}${sync} \u2014 ${item.detail}`;
|
|
5105
|
+
}
|
|
5106
|
+
function describeWorkRecords(records, now) {
|
|
5107
|
+
return records.flatMap((record) => [
|
|
5108
|
+
`${record.timestamp.toISOString()} (${describeDuration(now.getTime() - record.timestamp.getTime())} ago)`,
|
|
5109
|
+
...record.items.map(describeWorkItem)
|
|
5110
|
+
]);
|
|
5111
|
+
}
|
|
5112
|
+
function workSection(read, now) {
|
|
5113
|
+
const lines = [];
|
|
5114
|
+
const problems = [];
|
|
5115
|
+
const records = read.entries;
|
|
5116
|
+
if (read.error !== null) {
|
|
5117
|
+
lines.push(`the work log could not be read: ${read.error}`);
|
|
5118
|
+
problems.push(`the work log \`${read.path}\` could not be read: ${read.error}`);
|
|
5119
|
+
} else if (!read.present || records.length === 0) {
|
|
5120
|
+
lines.push("no tick has invoked the executor in the retained window");
|
|
5121
|
+
} else {
|
|
5122
|
+
lines.push(...describeWorkRecords(records, now));
|
|
5123
|
+
}
|
|
5124
|
+
if (!read.filtered && read.present) lines.push(unfilteredLine("record(s)"));
|
|
5125
|
+
if (read.skipped > 0)
|
|
5126
|
+
lines.push(`${String(read.skipped)} log line(s) could not be parsed and were ignored`);
|
|
5127
|
+
if (read.otherRepos > 0)
|
|
5128
|
+
lines.push(`${String(read.otherRepos)} record(s) belonged to another repository`);
|
|
5129
|
+
return build("work", lines, problems, {
|
|
5130
|
+
records,
|
|
5131
|
+
skipped: read.skipped,
|
|
5132
|
+
otherRepos: read.otherRepos,
|
|
5133
|
+
filtered: read.filtered,
|
|
5134
|
+
logPath: read.path,
|
|
5135
|
+
logPresent: read.present
|
|
5136
|
+
});
|
|
5137
|
+
}
|
|
5138
|
+
function describeCheckout(status, lines, problems) {
|
|
5139
|
+
if (status.branch === null) {
|
|
5140
|
+
lines.push(`HEAD is detached at ${status.head ?? "an unknown commit"}`);
|
|
5141
|
+
problems.push("HEAD is detached; the pre-flight expects a branch, so check one out");
|
|
5142
|
+
} else {
|
|
5143
|
+
const at = status.head === null ? "" : ` at ${status.head}`;
|
|
5144
|
+
lines.push(`on ${status.branch}${at}`);
|
|
5145
|
+
}
|
|
5146
|
+
if (status.statusError !== null) {
|
|
5147
|
+
lines.push(`the working tree could not be inspected: ${status.statusError}`);
|
|
5148
|
+
problems.push(
|
|
5149
|
+
`\`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`
|
|
5150
|
+
);
|
|
5151
|
+
return;
|
|
5152
|
+
}
|
|
5153
|
+
if (status.dirtyPaths.length === 0) {
|
|
5154
|
+
lines.push("working tree is clean");
|
|
5155
|
+
return;
|
|
5156
|
+
}
|
|
5157
|
+
lines.push(`working tree has ${String(status.dirtyPaths.length)} uncommitted change(s):`);
|
|
5158
|
+
for (const path of status.dirtyPaths) lines.push(` ${path}`);
|
|
5159
|
+
problems.push(
|
|
5160
|
+
`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`
|
|
5161
|
+
);
|
|
5162
|
+
}
|
|
5163
|
+
function describeBaseBranch(status, lines, problems) {
|
|
5164
|
+
if (!status.baseLocal) {
|
|
5165
|
+
lines.push(`base branch ${status.baseBranch} does not exist in this checkout`);
|
|
5166
|
+
problems.push(
|
|
5167
|
+
`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>\``
|
|
5168
|
+
);
|
|
5169
|
+
return;
|
|
5170
|
+
}
|
|
5171
|
+
if (status.upstream === null) {
|
|
5172
|
+
lines.push(`base branch ${status.baseBranch} has no upstream`);
|
|
5173
|
+
problems.push(
|
|
5174
|
+
`the base branch \`${status.baseBranch}\` has no upstream, so the pre-flight cannot fast-forward it`
|
|
5175
|
+
);
|
|
5176
|
+
return;
|
|
5177
|
+
}
|
|
5178
|
+
if (!status.upstreamTracked) {
|
|
5179
|
+
lines.push(
|
|
5180
|
+
`base branch ${status.baseBranch} has no tracking configuration; counted against ${status.upstream}`
|
|
5181
|
+
);
|
|
5182
|
+
problems.push(
|
|
5183
|
+
`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}\``
|
|
5184
|
+
);
|
|
5185
|
+
}
|
|
5186
|
+
const freshness = status.refreshed ? "" : " (not refreshed)";
|
|
5187
|
+
const ahead = status.ahead === null ? "?" : String(status.ahead);
|
|
5188
|
+
const behind = status.behind === null ? "?" : String(status.behind);
|
|
5189
|
+
lines.push(
|
|
5190
|
+
`base branch ${status.baseBranch} vs ${status.upstream}: ahead ${ahead}, behind ${behind}${freshness}`
|
|
5191
|
+
);
|
|
5192
|
+
if (status.ahead === null || status.behind === null) {
|
|
5193
|
+
problems.push(
|
|
5194
|
+
`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`
|
|
5195
|
+
);
|
|
5196
|
+
return;
|
|
5197
|
+
}
|
|
5198
|
+
if (status.ahead === 0) return;
|
|
5199
|
+
if (status.behind > 0) {
|
|
5200
|
+
problems.push(
|
|
5201
|
+
`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`
|
|
5202
|
+
);
|
|
5203
|
+
return;
|
|
5204
|
+
}
|
|
5205
|
+
lines.push(` ${String(status.ahead)} local commit(s) not on ${status.upstream}`);
|
|
5206
|
+
}
|
|
5207
|
+
function gitSection(status) {
|
|
5208
|
+
const lines = [];
|
|
5209
|
+
const problems = [];
|
|
5210
|
+
if (status.error !== null) {
|
|
5211
|
+
return build(
|
|
5212
|
+
"git",
|
|
5213
|
+
[`not a usable git repository: ${status.error}`],
|
|
5214
|
+
[`not a usable git repository: ${status.error}`],
|
|
5215
|
+
{
|
|
5216
|
+
...status
|
|
5217
|
+
}
|
|
5218
|
+
);
|
|
5219
|
+
}
|
|
5220
|
+
describeCheckout(status, lines, problems);
|
|
5221
|
+
describeBaseBranch(status, lines, problems);
|
|
5222
|
+
if (status.fetchError !== null) {
|
|
5223
|
+
lines.push(`fetch failed: ${status.fetchError}`);
|
|
5224
|
+
problems.push(
|
|
5225
|
+
`\`git fetch\` failed (${status.fetchError}); the ahead/behind figures above are from the last successful fetch and may be out of date`
|
|
5226
|
+
);
|
|
5227
|
+
}
|
|
5228
|
+
return build("git", lines, problems, { ...status });
|
|
5229
|
+
}
|
|
5230
|
+
function assembleReport(input) {
|
|
5231
|
+
const byId = new Map(input.sections.map((section) => [section.id, section]));
|
|
5232
|
+
const ordered = SECTION_ORDER.flatMap((id) => {
|
|
5233
|
+
const section = byId.get(id);
|
|
5234
|
+
return section === void 0 ? [] : [section];
|
|
5235
|
+
});
|
|
5236
|
+
const problems = ordered.flatMap((section) => section.problems);
|
|
5237
|
+
return {
|
|
5238
|
+
generatedAt: input.generatedAt,
|
|
5239
|
+
repo: input.repo,
|
|
5240
|
+
offline: input.offline,
|
|
5241
|
+
sections: ordered,
|
|
5242
|
+
problems,
|
|
5243
|
+
exitCode: problems.length === 0 ? 0 : 1
|
|
5244
|
+
};
|
|
5245
|
+
}
|
|
5246
|
+
function renderText(report) {
|
|
5247
|
+
const head = `automata do-work --check \u2014 ${report.repo ?? "unknown repository"} \u2014 ${report.generatedAt.toISOString()}`;
|
|
5248
|
+
const parts = [head, ""];
|
|
5249
|
+
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("");
|
|
5262
|
+
}
|
|
5263
|
+
parts.push(
|
|
5264
|
+
report.problems.length === 0 ? "RESULT: healthy" : `RESULT: ${String(report.problems.length)} problem(s) found`
|
|
5265
|
+
);
|
|
5266
|
+
return parts.join("\n") + "\n";
|
|
5267
|
+
}
|
|
5268
|
+
function toJson(report) {
|
|
5269
|
+
const sections = {};
|
|
5270
|
+
for (const section of report.sections) {
|
|
5271
|
+
sections[section.id] = {
|
|
5272
|
+
title: section.title,
|
|
5273
|
+
lines: section.lines,
|
|
5274
|
+
problems: section.problems,
|
|
5275
|
+
data: section.data
|
|
5276
|
+
};
|
|
5277
|
+
}
|
|
5278
|
+
return {
|
|
5279
|
+
generatedAt: report.generatedAt.toISOString(),
|
|
5280
|
+
repo: report.repo,
|
|
5281
|
+
offline: report.offline,
|
|
5282
|
+
exitCode: report.exitCode,
|
|
5283
|
+
problems: report.problems,
|
|
5284
|
+
sections
|
|
5285
|
+
};
|
|
5286
|
+
}
|
|
4497
5287
|
|
|
4498
5288
|
// src/commands/doWork.ts
|
|
4499
5289
|
var inFlightMarker = null;
|
|
@@ -4528,55 +5318,89 @@ function fail(message) {
|
|
|
4528
5318
|
}
|
|
4529
5319
|
process.exit(1);
|
|
4530
5320
|
}
|
|
5321
|
+
var SettingsError = class extends Error {
|
|
5322
|
+
};
|
|
5323
|
+
function failSettings(message) {
|
|
5324
|
+
throw new SettingsError(message);
|
|
5325
|
+
}
|
|
4531
5326
|
function parsePositiveInt(value, label) {
|
|
4532
5327
|
const trimmed2 = value.trim();
|
|
4533
5328
|
if (!/^\d+$/.test(trimmed2)) {
|
|
4534
|
-
|
|
5329
|
+
failSettings(`${label} must be a positive integer (got "${value}").`);
|
|
4535
5330
|
}
|
|
4536
5331
|
const parsed = Number(trimmed2);
|
|
4537
5332
|
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
4538
|
-
|
|
5333
|
+
failSettings(`${label} must be a positive integer within the safe range (got "${value}").`);
|
|
4539
5334
|
}
|
|
4540
5335
|
return parsed;
|
|
4541
5336
|
}
|
|
4542
|
-
function
|
|
4543
|
-
let config;
|
|
5337
|
+
function resolveSettingsResult(options, verifyIdentity) {
|
|
4544
5338
|
try {
|
|
4545
|
-
|
|
5339
|
+
return { ok: true, settings: buildSettings(options, verifyIdentity) };
|
|
4546
5340
|
} catch (err) {
|
|
4547
|
-
|
|
5341
|
+
if (err instanceof SettingsError) return { ok: false, error: err.message };
|
|
5342
|
+
throw err;
|
|
4548
5343
|
}
|
|
5344
|
+
}
|
|
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
|
+
function requireParticipants(config) {
|
|
4549
5351
|
if (config.remoteType !== "gh") {
|
|
4550
|
-
|
|
5352
|
+
failSettings(
|
|
4551
5353
|
"do-work is only supported for GitHub remotes. Set it with `automata config set type gh`. Azure DevOps lacks the issue conversation APIs this needs \u2014 see docs/azdo-gap.md."
|
|
4552
5354
|
);
|
|
4553
5355
|
}
|
|
4554
5356
|
if (!config.issueDiscoveryTechnique) {
|
|
4555
|
-
|
|
5357
|
+
failSettings("No issue discovery technique configured. Run `automata config set issue-discovery-technique <value>`.");
|
|
4556
5358
|
}
|
|
4557
5359
|
if (!config.issueDiscoveryValue) {
|
|
4558
|
-
|
|
5360
|
+
failSettings("No issue discovery value configured. Run `automata config set issue-discovery-value <value>`.");
|
|
4559
5361
|
}
|
|
4560
5362
|
const allowedUsers = (config.allowedUsers ?? []).filter((user) => user.trim().length > 0);
|
|
4561
5363
|
if (allowedUsers.length === 0) {
|
|
4562
|
-
|
|
5364
|
+
failSettings("No allowed users configured. Run `automata config set allowed-users <user1,user2>`.");
|
|
4563
5365
|
}
|
|
4564
5366
|
const agentUser = (config.agentUser ?? "").trim();
|
|
4565
5367
|
if (agentUser.length === 0) {
|
|
4566
|
-
|
|
5368
|
+
failSettings("No agent user configured. Run `automata config set agent-user <login>`.");
|
|
4567
5369
|
}
|
|
5370
|
+
return {
|
|
5371
|
+
allowedUsers,
|
|
5372
|
+
agentUser,
|
|
5373
|
+
technique: config.issueDiscoveryTechnique,
|
|
5374
|
+
discoveryValue: config.issueDiscoveryValue
|
|
5375
|
+
};
|
|
5376
|
+
}
|
|
5377
|
+
function resolveWithOption(value) {
|
|
5378
|
+
if (value === void 0) return void 0;
|
|
5379
|
+
const requested = value.toLowerCase();
|
|
5380
|
+
if (requested !== "claude" && requested !== "codex") {
|
|
5381
|
+
failSettings(`--with must be 'claude' or 'codex', got '${value}'.`);
|
|
5382
|
+
}
|
|
5383
|
+
return requested;
|
|
5384
|
+
}
|
|
5385
|
+
function resolveEffort(value) {
|
|
5386
|
+
const result = normalizeEffortOption(value);
|
|
5387
|
+
if (!result.ok) failSettings(result.error);
|
|
5388
|
+
return result.value;
|
|
5389
|
+
}
|
|
5390
|
+
function buildSettings(options, verifyIdentity) {
|
|
5391
|
+
let config;
|
|
5392
|
+
try {
|
|
5393
|
+
config = readConfig();
|
|
5394
|
+
} catch (err) {
|
|
5395
|
+
failSettings(err.message);
|
|
5396
|
+
}
|
|
5397
|
+
const { allowedUsers, agentUser, technique, discoveryValue } = requireParticipants(config);
|
|
4568
5398
|
validateDoWorkConfig(config.doWork);
|
|
4569
5399
|
const doWork = config.doWork ?? {};
|
|
4570
|
-
|
|
4571
|
-
if (
|
|
4572
|
-
const
|
|
4573
|
-
if (
|
|
4574
|
-
fail(`--with must be 'claude' or 'codex', got '${options.with}'.`);
|
|
4575
|
-
}
|
|
4576
|
-
withOption = requested;
|
|
4577
|
-
}
|
|
4578
|
-
if (options.dryRun !== true) {
|
|
4579
|
-
checkAuthenticatedIdentity(agentUser, allowedUsers);
|
|
5400
|
+
const withOption = resolveWithOption(options.with);
|
|
5401
|
+
if (verifyIdentity) {
|
|
5402
|
+
const problem = describeIdentityProblem(agentUser, allowedUsers);
|
|
5403
|
+
if (problem !== null) failSettings(problem);
|
|
4580
5404
|
}
|
|
4581
5405
|
return {
|
|
4582
5406
|
baseBranch: doWork.baseBranch ?? DEFAULT_DO_WORK.baseBranch,
|
|
@@ -4588,8 +5412,10 @@ function resolveSettings(options) {
|
|
|
4588
5412
|
// Rejected here rather than per item: an empty `--effort` is an operator
|
|
4589
5413
|
// mistake on this invocation, not a property of any one work item. The
|
|
4590
5414
|
// configured per-executor defaults are trimmed inside `resolveExecution`,
|
|
4591
|
-
// which is where the executor in use is finally known.
|
|
4592
|
-
|
|
5415
|
+
// which is where the executor in use is finally known. Routed through
|
|
5416
|
+
// `failSettings` rather than `resolveEffortOption`, whose rejection exits the
|
|
5417
|
+
// process — which would take `--check` down before it printed a section.
|
|
5418
|
+
effortOption: resolveEffort(options.effort),
|
|
4593
5419
|
configEfforts: doWork.effort,
|
|
4594
5420
|
maxRuns: options.maxRuns !== void 0 ? parsePositiveInt(options.maxRuns, "--max-runs") : doWork.maxRunsPerTick ?? DEFAULT_DO_WORK.maxRunsPerTick,
|
|
4595
5421
|
lockStaleMinutes: doWork.lockStaleMinutes ?? DEFAULT_DO_WORK.lockStaleMinutes,
|
|
@@ -4600,30 +5426,22 @@ function resolveSettings(options) {
|
|
|
4600
5426
|
"pr-work": doWork.prompts?.prWork ?? DEFAULT_DO_WORK_PR_WORK_PROMPT,
|
|
4601
5427
|
"pr-orphan": doWork.prompts?.prOrphan ?? DEFAULT_DO_WORK_PR_ORPHAN_PROMPT
|
|
4602
5428
|
},
|
|
4603
|
-
technique
|
|
4604
|
-
discoveryValue
|
|
5429
|
+
technique,
|
|
5430
|
+
discoveryValue,
|
|
4605
5431
|
onlyIssue: options.issue === void 0 ? void 0 : parsePositiveInt(options.issue, "--issue"),
|
|
4606
5432
|
onlyPr: options.pr === void 0 ? void 0 : parsePositiveInt(options.pr, "--pr")
|
|
4607
5433
|
};
|
|
4608
5434
|
}
|
|
4609
|
-
function
|
|
5435
|
+
function describeIdentityProblem(agentUser, allowedUsers) {
|
|
4610
5436
|
const login2 = getAuthenticatedLogin();
|
|
4611
5437
|
if (login2 === null) {
|
|
4612
5438
|
progress(
|
|
4613
5439
|
`Warning: could not determine which account \`gh\` is authenticated as; assuming it is the agent (${agentUser}).
|
|
4614
5440
|
`
|
|
4615
5441
|
);
|
|
4616
|
-
return;
|
|
4617
|
-
}
|
|
4618
|
-
if (login2.toLowerCase() === agentUser.toLowerCase()) return;
|
|
4619
|
-
if (allowedUsers.some((user) => user.toLowerCase() === login2.toLowerCase())) {
|
|
4620
|
-
fail(
|
|
4621
|
-
`\`gh\` is authenticated as "${login2}", which is listed in allowedUsers. Everything do-work posts would be attributed to an account that is allowed to instruct the agent, so its own marker comment would look like a new instruction and each tick would answer the previous tick forever. Authenticate \`gh\` as the agent account (${agentUser}) in this environment, or correct \`agentUser\`.`
|
|
4622
|
-
);
|
|
5442
|
+
return null;
|
|
4623
5443
|
}
|
|
4624
|
-
|
|
4625
|
-
`\`gh\` is authenticated as "${login2}" but agentUser is "${agentUser}". Comments posted under that identity are neither the agent's nor an authorized user's, so they are filtered out of the conversation: the answer boundary would never advance and the same message would start a run on every tick. Authenticate \`gh\` as the agent account (${agentUser}) in this environment, or correct \`agentUser\`.`
|
|
4626
|
-
);
|
|
5444
|
+
return identityProblemFor(login2, agentUser, allowedUsers);
|
|
4627
5445
|
}
|
|
4628
5446
|
function resolveItemExecution(item, settings) {
|
|
4629
5447
|
const trigger = triggeringMessage(item);
|
|
@@ -4740,24 +5558,24 @@ function validateOptionalString(container, key, path) {
|
|
|
4740
5558
|
const value = container[key];
|
|
4741
5559
|
if (value === void 0 || value === null) return;
|
|
4742
5560
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
4743
|
-
|
|
5561
|
+
failSettings(`${path} must be a non-empty string.`);
|
|
4744
5562
|
}
|
|
4745
5563
|
}
|
|
4746
5564
|
function validateOptionalInt(container, key, path, min, hint) {
|
|
4747
5565
|
const value = container[key];
|
|
4748
5566
|
if (value === void 0 || value === null) return;
|
|
4749
5567
|
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < min) {
|
|
4750
|
-
|
|
5568
|
+
failSettings(`${path} must be ${hint}, got ${JSON.stringify(value)}.`);
|
|
4751
5569
|
}
|
|
4752
5570
|
}
|
|
4753
5571
|
function validateDoWorkConfig(section) {
|
|
4754
5572
|
if (section === void 0 || section === null) return;
|
|
4755
5573
|
if (!isPlainObject(section)) {
|
|
4756
|
-
|
|
5574
|
+
failSettings(`doWork must be an object, got ${JSON.stringify(section)}.`);
|
|
4757
5575
|
}
|
|
4758
5576
|
const executor = section["executor"];
|
|
4759
5577
|
if (executor !== void 0 && executor !== "claude" && executor !== "codex") {
|
|
4760
|
-
|
|
5578
|
+
failSettings(`doWork.executor must be 'claude' or 'codex', got ${JSON.stringify(executor)}.`);
|
|
4761
5579
|
}
|
|
4762
5580
|
validateOptionalString(section, "baseBranch", "doWork.baseBranch");
|
|
4763
5581
|
validateOptionalInt(section, "maxRunsPerTick", "doWork.maxRunsPerTick", 0, "a non-negative integer (0 = unlimited)");
|
|
@@ -4771,20 +5589,20 @@ function validateProtectedBranches(value) {
|
|
|
4771
5589
|
if (value === void 0 || value === null) return;
|
|
4772
5590
|
const isNonEmptyString = (b) => typeof b === "string" && b.trim().length > 0;
|
|
4773
5591
|
if (!Array.isArray(value) || !value.every(isNonEmptyString)) {
|
|
4774
|
-
|
|
5592
|
+
failSettings("doWork.protectedBranches must be an array of non-empty strings.");
|
|
4775
5593
|
}
|
|
4776
5594
|
}
|
|
4777
5595
|
function validateSettingContainer(value, container, keys) {
|
|
4778
5596
|
if (value === void 0 || value === null) return;
|
|
4779
5597
|
if (!isPlainObject(value)) {
|
|
4780
|
-
|
|
5598
|
+
failSettings(`doWork.${container} must be an object, got ${JSON.stringify(value)}.`);
|
|
4781
5599
|
}
|
|
4782
5600
|
for (const key of keys) {
|
|
4783
5601
|
validateOptionalString(value, key, `doWork.${container}.${key}`);
|
|
4784
5602
|
}
|
|
4785
5603
|
for (const key of Object.keys(value)) {
|
|
4786
5604
|
if (!keys.includes(key)) {
|
|
4787
|
-
|
|
5605
|
+
failSettings(`doWork.${container}.${key} is not a recognised setting; expected one of: ${keys.join(", ")}.`);
|
|
4788
5606
|
}
|
|
4789
5607
|
}
|
|
4790
5608
|
}
|
|
@@ -5319,8 +6137,25 @@ var doWorkCommand = new Command6("do-work").description(
|
|
|
5319
6137
|
).option("--limit <n>", "Maximum number of issues to fetch", "10").option("--max-runs <n>", "Maximum number of model runs this tick").option(
|
|
5320
6138
|
"--dry-run",
|
|
5321
6139
|
"Print the work plan, plus a summary and the exact command that would be launched for each item, and exit without changing anything"
|
|
6140
|
+
).option(
|
|
6141
|
+
"--check",
|
|
6142
|
+
"Print a read-only health report for the loop \u2014 run lock, tick history, last work, repository state, per-candidate selection and environment \u2014 and exit 0 when it found no problem, 1 when it did"
|
|
6143
|
+
).option(
|
|
6144
|
+
"--no-fetch",
|
|
6145
|
+
"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"
|
|
5322
6146
|
).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) => {
|
|
5323
6147
|
const startedAt = Date.now();
|
|
6148
|
+
if (options.check === true) {
|
|
6149
|
+
if (options.dryRun === true) {
|
|
6150
|
+
process.stderr.write(
|
|
6151
|
+
"Error: --check and --dry-run are two different read-only reports; run one or the other.\n"
|
|
6152
|
+
);
|
|
6153
|
+
process.exit(1);
|
|
6154
|
+
}
|
|
6155
|
+
const exitCode2 = runCheck(options);
|
|
6156
|
+
if (exitCode2 !== 0) process.exit(exitCode2);
|
|
6157
|
+
return;
|
|
6158
|
+
}
|
|
5324
6159
|
if (options.dryRun !== true) loggableInvocation = { startedAt };
|
|
5325
6160
|
const settings = resolveSettings(options);
|
|
5326
6161
|
if (options.dryRun === true) {
|
|
@@ -5374,6 +6209,213 @@ var doWorkCommand = new Command6("do-work").description(
|
|
|
5374
6209
|
logTick(reports, exitCode, startedAt);
|
|
5375
6210
|
if (exitCode !== 0) process.exit(exitCode);
|
|
5376
6211
|
});
|
|
6212
|
+
function resolveRepoSlug() {
|
|
6213
|
+
try {
|
|
6214
|
+
const slug = getRepoSlug();
|
|
6215
|
+
return `${slug.owner}/${slug.repo}`;
|
|
6216
|
+
} catch {
|
|
6217
|
+
return null;
|
|
6218
|
+
}
|
|
6219
|
+
}
|
|
6220
|
+
function collectSelection(settings) {
|
|
6221
|
+
const issues = discoverIssues(settings);
|
|
6222
|
+
const linkMap = getOpenPrLinkMap();
|
|
6223
|
+
const policy = {
|
|
6224
|
+
baseBranch: settings.baseBranch,
|
|
6225
|
+
defaultBranch: linkMap.defaultBranch,
|
|
6226
|
+
protectedBranches: settings.protectedBranches
|
|
6227
|
+
};
|
|
6228
|
+
return skipDuplicateHeadBranches([
|
|
6229
|
+
...issues.map((issue) => decideWork(buildIssueState(issue, linkMap), settings.participants, policy)),
|
|
6230
|
+
...discoverOrphanPrs(settings, linkMap).map(
|
|
6231
|
+
(candidate) => decideOrphanPrWork({ prSurface: getPrSurface(candidate.pr.number) }, settings.participants, policy)
|
|
6232
|
+
)
|
|
6233
|
+
]);
|
|
6234
|
+
}
|
|
6235
|
+
function selectionSection(settings, offline) {
|
|
6236
|
+
const section = (lines2, problems, data) => ({
|
|
6237
|
+
id: "selection",
|
|
6238
|
+
title: sectionTitle("selection"),
|
|
6239
|
+
lines: lines2,
|
|
6240
|
+
problems: problems.map((summary) => ({ section: "selection", summary })),
|
|
6241
|
+
data
|
|
6242
|
+
});
|
|
6243
|
+
if (offline) {
|
|
6244
|
+
return section(
|
|
6245
|
+
["not run: --no-fetch makes no network call, and the selection needs live GitHub data"],
|
|
6246
|
+
[],
|
|
6247
|
+
{ ran: false, detail: "offline", plan: [] }
|
|
6248
|
+
);
|
|
6249
|
+
}
|
|
6250
|
+
if (settings === null) {
|
|
6251
|
+
return section(
|
|
6252
|
+
["not run: the configuration could not be resolved \u2014 see Environment below"],
|
|
6253
|
+
[],
|
|
6254
|
+
{ ran: false, detail: "configuration invalid", plan: [] }
|
|
6255
|
+
);
|
|
6256
|
+
}
|
|
6257
|
+
let decisions;
|
|
6258
|
+
try {
|
|
6259
|
+
decisions = collectSelection(settings);
|
|
6260
|
+
} catch (err) {
|
|
6261
|
+
const detail = err.message;
|
|
6262
|
+
return section(
|
|
6263
|
+
[`could not be computed: ${detail}`],
|
|
6264
|
+
[`the GitHub selection could not be computed: ${detail}`],
|
|
6265
|
+
{ ran: false, detail, plan: [] }
|
|
6266
|
+
);
|
|
6267
|
+
}
|
|
6268
|
+
const work = decisions.filter((decision) => decision.kind === "work").length;
|
|
6269
|
+
const lines = [
|
|
6270
|
+
`${String(work)} of ${String(decisions.length)} candidate(s) would be picked up`,
|
|
6271
|
+
...describePlan(decisions).trimEnd().split("\n")
|
|
6272
|
+
];
|
|
6273
|
+
return section(lines, [], { ran: true, detail: null, plan: decisions.map(toPlanJson) });
|
|
6274
|
+
}
|
|
6275
|
+
function describeExecutor(settings, lines, problems) {
|
|
6276
|
+
const executor = settings.withOption ?? settings.configExecutor ?? "claude";
|
|
6277
|
+
const command = executor === "codex" ? "codex" : "claude";
|
|
6278
|
+
const resolvedPath = resolveCommand(command);
|
|
6279
|
+
const onPath = resolvedPath !== command;
|
|
6280
|
+
lines.push(`default executor: ${executor} (${onPath ? resolvedPath : "not found on PATH"})`);
|
|
6281
|
+
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
|
+
);
|
|
6285
|
+
}
|
|
6286
|
+
return { executor, command, onPath };
|
|
6287
|
+
}
|
|
6288
|
+
function describeGitHubIdentity(options, settings, lines, problems) {
|
|
6289
|
+
if (options.fetch === false) {
|
|
6290
|
+
lines.push("`gh` authentication not checked: --no-fetch");
|
|
6291
|
+
return { ghAvailable: null, ghLogin: null, identityProblem: null };
|
|
6292
|
+
}
|
|
6293
|
+
let identity;
|
|
6294
|
+
try {
|
|
6295
|
+
identity = getAuthenticatedIdentity();
|
|
6296
|
+
} catch (err) {
|
|
6297
|
+
const detail = err.message;
|
|
6298
|
+
lines.push(`\`gh\` could not be queried: ${detail}`);
|
|
6299
|
+
problems.push(`\`gh\` could not be queried (${detail}); install it and run \`gh auth login\``);
|
|
6300
|
+
return { ghAvailable: false, ghLogin: null, identityProblem: null };
|
|
6301
|
+
}
|
|
6302
|
+
if (identity.kind === "unavailable") {
|
|
6303
|
+
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
|
+
);
|
|
6307
|
+
return { ghAvailable: false, ghLogin: null, identityProblem: null };
|
|
6308
|
+
}
|
|
6309
|
+
const ghLogin = identity.kind === "login" ? identity.login : null;
|
|
6310
|
+
lines.push(
|
|
6311
|
+
ghLogin === null ? "`gh` is available but its account could not be determined (an app installation token has none)" : `\`gh\` is authenticated as ${ghLogin}`
|
|
6312
|
+
);
|
|
6313
|
+
const identityProblem = identityProblemFor(
|
|
6314
|
+
ghLogin,
|
|
6315
|
+
settings.participants.agentUser,
|
|
6316
|
+
settings.participants.allowedUsers
|
|
6317
|
+
);
|
|
6318
|
+
if (identityProblem !== null) problems.push(identityProblem);
|
|
6319
|
+
return { ghAvailable: true, ghLogin, identityProblem };
|
|
6320
|
+
}
|
|
6321
|
+
function readRemoteType() {
|
|
6322
|
+
try {
|
|
6323
|
+
return readConfig().remoteType ?? null;
|
|
6324
|
+
} catch {
|
|
6325
|
+
return null;
|
|
6326
|
+
}
|
|
6327
|
+
}
|
|
6328
|
+
function environmentSection(options, resolved, repo) {
|
|
6329
|
+
const lines = [`automata ${version}`];
|
|
6330
|
+
const problems = [];
|
|
6331
|
+
const remoteType = readRemoteType();
|
|
6332
|
+
const data = {
|
|
6333
|
+
version,
|
|
6334
|
+
repo,
|
|
6335
|
+
remoteType,
|
|
6336
|
+
configValid: resolved.ok,
|
|
6337
|
+
configError: resolved.ok ? null : resolved.error
|
|
6338
|
+
};
|
|
6339
|
+
if (repo === null) {
|
|
6340
|
+
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
|
+
);
|
|
6344
|
+
} else {
|
|
6345
|
+
lines.push(`repository ${repo}`);
|
|
6346
|
+
}
|
|
6347
|
+
if (!resolved.ok) {
|
|
6348
|
+
lines.push(`configuration is not usable: ${resolved.error}`);
|
|
6349
|
+
problems.push(`the configuration is not usable: ${resolved.error}`);
|
|
6350
|
+
return {
|
|
6351
|
+
id: "environment",
|
|
6352
|
+
title: sectionTitle("environment"),
|
|
6353
|
+
lines,
|
|
6354
|
+
problems: problems.map((summary) => ({ section: "environment", summary })),
|
|
6355
|
+
// `ghAvailable` is null rather than false: nothing was asked of `gh`,
|
|
6356
|
+
// which is not the same as having asked and been refused.
|
|
6357
|
+
data: { ...data, ghAvailable: null, ghLogin: null, identityProblem: null }
|
|
6358
|
+
};
|
|
6359
|
+
}
|
|
6360
|
+
const settings = resolved.settings;
|
|
6361
|
+
const runCap = settings.maxRuns === 0 ? "unlimited" : String(settings.maxRuns);
|
|
6362
|
+
lines.push(
|
|
6363
|
+
"configuration parses and validates",
|
|
6364
|
+
`discovery: ${settings.technique} = ${settings.discoveryValue}`,
|
|
6365
|
+
`base branch: ${settings.baseBranch}`,
|
|
6366
|
+
`run cap: ${runCap}; lock stale after ${String(settings.lockStaleMinutes)} minutes`
|
|
6367
|
+
);
|
|
6368
|
+
const { executor, command, onPath } = describeExecutor(settings, lines, problems);
|
|
6369
|
+
const { ghAvailable, ghLogin, identityProblem } = describeGitHubIdentity(
|
|
6370
|
+
options,
|
|
6371
|
+
settings,
|
|
6372
|
+
lines,
|
|
6373
|
+
problems
|
|
6374
|
+
);
|
|
6375
|
+
return {
|
|
6376
|
+
id: "environment",
|
|
6377
|
+
title: sectionTitle("environment"),
|
|
6378
|
+
lines,
|
|
6379
|
+
problems: problems.map((summary) => ({ section: "environment", summary })),
|
|
6380
|
+
data: {
|
|
6381
|
+
...data,
|
|
6382
|
+
discovery: { technique: settings.technique, value: settings.discoveryValue },
|
|
6383
|
+
baseBranch: settings.baseBranch,
|
|
6384
|
+
maxRuns: settings.maxRuns,
|
|
6385
|
+
lockStaleMinutes: settings.lockStaleMinutes,
|
|
6386
|
+
executor,
|
|
6387
|
+
executorCommand: command,
|
|
6388
|
+
executorOnPath: onPath,
|
|
6389
|
+
ghAvailable,
|
|
6390
|
+
ghLogin,
|
|
6391
|
+
identityProblem
|
|
6392
|
+
}
|
|
6393
|
+
};
|
|
6394
|
+
}
|
|
6395
|
+
function runCheck(options) {
|
|
6396
|
+
const now = /* @__PURE__ */ new Date();
|
|
6397
|
+
const offline = options.fetch === false;
|
|
6398
|
+
const repo = resolveRepoSlug();
|
|
6399
|
+
const resolved = resolveSettingsResult(options, false);
|
|
6400
|
+
const settings = resolved.ok ? resolved.settings : null;
|
|
6401
|
+
const staleMinutes = settings?.lockStaleMinutes ?? DEFAULT_DO_WORK.lockStaleMinutes;
|
|
6402
|
+
const baseBranch = settings?.baseBranch ?? DEFAULT_DO_WORK.baseBranch;
|
|
6403
|
+
const report = assembleReport({
|
|
6404
|
+
generatedAt: now,
|
|
6405
|
+
repo,
|
|
6406
|
+
offline,
|
|
6407
|
+
sections: [
|
|
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
|
+
]
|
|
6415
|
+
});
|
|
6416
|
+
out(options.json === true ? JSON.stringify(toJson(report), null, 2) + "\n" : renderText(report));
|
|
6417
|
+
return report.exitCode;
|
|
6418
|
+
}
|
|
5377
6419
|
function toTickLogItem(report) {
|
|
5378
6420
|
return {
|
|
5379
6421
|
subject: reportLabel(report),
|