automata-cli 0.1.0-feature-030-do-work.208 → 0.1.0-feature-030-do-work.212
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 +299 -68
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -34,8 +34,9 @@ function writeDoWork(patch) {
|
|
|
34
34
|
writeConfig({ ...current, doWork: { ...current.doWork, ...patch } });
|
|
35
35
|
}
|
|
36
36
|
function parseNonNegativeInt(value, label) {
|
|
37
|
-
const
|
|
38
|
-
|
|
37
|
+
const trimmed = value.trim();
|
|
38
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
39
|
+
if (Number.isNaN(parsed) || parsed < 0 || String(parsed) !== trimmed || !Number.isSafeInteger(parsed)) {
|
|
39
40
|
process.stderr.write(`Error: ${label} must be a non-negative integer (got "${value}").
|
|
40
41
|
`);
|
|
41
42
|
process.exit(1);
|
|
@@ -722,7 +723,7 @@ function createTrackingBranch(branch) {
|
|
|
722
723
|
return gitCommand(["checkout", "-b", branch, `origin/${branch}`]);
|
|
723
724
|
}
|
|
724
725
|
function fetchBranch(branch) {
|
|
725
|
-
return gitCommand(["fetch", "origin", branch]);
|
|
726
|
+
return gitCommand(["fetch", "origin", `+refs/heads/${branch}:refs/remotes/origin/${branch}`]);
|
|
726
727
|
}
|
|
727
728
|
function pullFastForwardOnly(branch) {
|
|
728
729
|
return gitCommand(branch === void 0 ? ["pull", "--ff-only"] : ["pull", "--ff-only", "origin", branch]);
|
|
@@ -2430,22 +2431,24 @@ query($owner:String!,$repo:String!,$cursor:String){
|
|
|
2430
2431
|
pullRequests(states:OPEN, first:100, after:$cursor, orderBy:{field:UPDATED_AT, direction:DESC}){
|
|
2431
2432
|
pageInfo{ hasNextPage endCursor }
|
|
2432
2433
|
nodes{
|
|
2433
|
-
number url title headRefName isDraft updatedAt
|
|
2434
|
+
number url title headRefName baseRefName isCrossRepository isDraft updatedAt
|
|
2434
2435
|
closingIssuesReferences(first:50){
|
|
2435
2436
|
pageInfo{ hasNextPage }
|
|
2436
|
-
nodes{ number }
|
|
2437
|
+
nodes{ number repository{ nameWithOwner } }
|
|
2437
2438
|
}
|
|
2438
2439
|
}
|
|
2439
2440
|
}
|
|
2440
2441
|
}
|
|
2441
2442
|
}`.trim();
|
|
2442
2443
|
var MAX_LINK_MAP_PAGES = 50;
|
|
2443
|
-
function indexPullRequest(map, node) {
|
|
2444
|
+
function indexPullRequest(map, node, nameWithOwner) {
|
|
2444
2445
|
const ref = {
|
|
2445
2446
|
number: node.number,
|
|
2446
2447
|
url: node.url,
|
|
2447
2448
|
title: node.title,
|
|
2448
2449
|
headRefName: node.headRefName,
|
|
2450
|
+
baseRefName: node.baseRefName,
|
|
2451
|
+
isCrossRepository: node.isCrossRepository,
|
|
2449
2452
|
state: "OPEN",
|
|
2450
2453
|
isDraft: node.isDraft,
|
|
2451
2454
|
updatedAt: node.updatedAt
|
|
@@ -2456,6 +2459,7 @@ function indexPullRequest(map, node) {
|
|
|
2456
2459
|
);
|
|
2457
2460
|
}
|
|
2458
2461
|
for (const issue of node.closingIssuesReferences.nodes) {
|
|
2462
|
+
if (issue.repository.nameWithOwner !== nameWithOwner) continue;
|
|
2459
2463
|
const existing = map.get(issue.number);
|
|
2460
2464
|
if (existing) {
|
|
2461
2465
|
existing.push(ref);
|
|
@@ -2474,7 +2478,7 @@ function getOpenPrLinkMap() {
|
|
|
2474
2478
|
const response = ghJson(args, "query open pull requests");
|
|
2475
2479
|
const connection = response.data.repository.pullRequests;
|
|
2476
2480
|
for (const node of connection.nodes) {
|
|
2477
|
-
indexPullRequest(map, node);
|
|
2481
|
+
indexPullRequest(map, node, `${owner}/${repo}`);
|
|
2478
2482
|
}
|
|
2479
2483
|
if (!connection.pageInfo?.hasNextPage || connection.pageInfo.endCursor === null) {
|
|
2480
2484
|
return map;
|
|
@@ -2494,6 +2498,7 @@ query($owner:String!,$repo:String!,$prNumber:Int!,$cursor:String){
|
|
|
2494
2498
|
nodes{
|
|
2495
2499
|
isResolved isOutdated path line
|
|
2496
2500
|
comments(last:100){
|
|
2501
|
+
pageInfo{ hasPreviousPage }
|
|
2497
2502
|
nodes{ author{login} body createdAt }
|
|
2498
2503
|
}
|
|
2499
2504
|
}
|
|
@@ -2531,6 +2536,11 @@ function getReviewThreads(prNumber) {
|
|
|
2531
2536
|
);
|
|
2532
2537
|
const connection = response.data.repository.pullRequest.reviewThreads;
|
|
2533
2538
|
for (const node of connection.nodes) {
|
|
2539
|
+
if (node.comments.pageInfo?.hasPreviousPage) {
|
|
2540
|
+
throw new Error(
|
|
2541
|
+
`Review thread on ${node.path} in pull request #${String(prNumber)} has more than 100 comments, so the earliest ones were not read. Refusing rather than risk suppressing a maintainer's request.`
|
|
2542
|
+
);
|
|
2543
|
+
}
|
|
2534
2544
|
threads.push({
|
|
2535
2545
|
path: node.path,
|
|
2536
2546
|
line: node.line ?? null,
|
|
@@ -2559,7 +2569,7 @@ function getPrSurface(prNumber) {
|
|
|
2559
2569
|
"view",
|
|
2560
2570
|
String(prNumber),
|
|
2561
2571
|
"--json",
|
|
2562
|
-
"number,title,url,headRefName,state,isDraft,body,author,createdAt,updatedAt,comments,reviews"
|
|
2572
|
+
"number,title,url,headRefName,baseRefName,isCrossRepository,state,isDraft,body,author,createdAt,updatedAt,comments,reviews"
|
|
2563
2573
|
],
|
|
2564
2574
|
`read pull request #${String(prNumber)}`
|
|
2565
2575
|
);
|
|
@@ -2587,6 +2597,8 @@ function getPrSurface(prNumber) {
|
|
|
2587
2597
|
url: raw.url,
|
|
2588
2598
|
title: raw.title,
|
|
2589
2599
|
headRefName: raw.headRefName,
|
|
2600
|
+
baseRefName: raw.baseRefName ?? "",
|
|
2601
|
+
isCrossRepository: raw.isCrossRepository ?? false,
|
|
2590
2602
|
state,
|
|
2591
2603
|
isDraft: raw.isDraft ?? false,
|
|
2592
2604
|
updatedAt: raw.updatedAt ?? raw.createdAt
|
|
@@ -2712,7 +2724,24 @@ function decideWork(state, p, baseBranch) {
|
|
|
2712
2724
|
};
|
|
2713
2725
|
}
|
|
2714
2726
|
const surface = prSurface;
|
|
2715
|
-
|
|
2727
|
+
if (surface.pr.isCrossRepository) {
|
|
2728
|
+
return {
|
|
2729
|
+
kind: "skip",
|
|
2730
|
+
issue,
|
|
2731
|
+
reason: "unsafe-pr-branch",
|
|
2732
|
+
detail: `pull request #${String(surface.pr.number)} comes from a fork; its head branch is not in this repository`
|
|
2733
|
+
};
|
|
2734
|
+
}
|
|
2735
|
+
if (surface.pr.headRefName === baseBranch) {
|
|
2736
|
+
return {
|
|
2737
|
+
kind: "skip",
|
|
2738
|
+
issue,
|
|
2739
|
+
reason: "unsafe-pr-branch",
|
|
2740
|
+
detail: `pull request #${String(surface.pr.number)} has the base branch (${baseBranch}) as its head`
|
|
2741
|
+
};
|
|
2742
|
+
}
|
|
2743
|
+
const agentThreadMessages = surface.threads.flatMap((thread) => thread.comments).filter((comment) => comment.author.toLowerCase() === p.agentUser.toLowerCase());
|
|
2744
|
+
const prAnalysis = analyzeSurface([...surface.messages, ...agentThreadMessages], p);
|
|
2716
2745
|
const actionableThreads = findActionableThreads(surface.threads, p);
|
|
2717
2746
|
const hasPrWork = prAnalysis.hasNewMessage || actionableThreads.length > 0;
|
|
2718
2747
|
if (!hasPrWork && !issueAnalysis.hasNewMessage) {
|
|
@@ -2833,7 +2862,7 @@ function composePrompt(input) {
|
|
|
2833
2862
|
}
|
|
2834
2863
|
|
|
2835
2864
|
// src/run/runLock.ts
|
|
2836
|
-
import { writeFileSync, readFileSync as readFileSync3, unlinkSync, mkdirSync, renameSync } from "fs";
|
|
2865
|
+
import { writeFileSync, readFileSync as readFileSync3, unlinkSync, mkdirSync, renameSync, linkSync } from "fs";
|
|
2837
2866
|
import { randomUUID } from "crypto";
|
|
2838
2867
|
import { hostname } from "os";
|
|
2839
2868
|
import { join as join2 } from "path";
|
|
@@ -2843,6 +2872,16 @@ var RUN_LOCK_RELATIVE_PATH = `${LOCK_DIR}/${LOCK_FILE}`;
|
|
|
2843
2872
|
function lockPath() {
|
|
2844
2873
|
return join2(process.cwd(), LOCK_DIR, LOCK_FILE);
|
|
2845
2874
|
}
|
|
2875
|
+
function processStartedAt(pid) {
|
|
2876
|
+
try {
|
|
2877
|
+
const stat = readFileSync3(`/proc/${String(pid)}/stat`, "utf8");
|
|
2878
|
+
const afterComm = stat.slice(stat.lastIndexOf(")") + 2);
|
|
2879
|
+
const field = afterComm.split(" ")[19];
|
|
2880
|
+
return field === void 0 || field.length === 0 ? null : field;
|
|
2881
|
+
} catch {
|
|
2882
|
+
return null;
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2846
2885
|
function isAlive(pid) {
|
|
2847
2886
|
try {
|
|
2848
2887
|
process.kill(pid, 0);
|
|
@@ -2860,7 +2899,8 @@ function readOwner(path) {
|
|
|
2860
2899
|
startedAt: parsed.startedAt,
|
|
2861
2900
|
host: parsed.host ?? "unknown",
|
|
2862
2901
|
command: parsed.command ?? "unknown",
|
|
2863
|
-
token: parsed.token ?? ""
|
|
2902
|
+
token: parsed.token ?? "",
|
|
2903
|
+
pidStartedAt: parsed.pidStartedAt
|
|
2864
2904
|
};
|
|
2865
2905
|
} catch {
|
|
2866
2906
|
return null;
|
|
@@ -2869,12 +2909,24 @@ function readOwner(path) {
|
|
|
2869
2909
|
function isStale(owner, staleMinutes) {
|
|
2870
2910
|
if (owner === null) return true;
|
|
2871
2911
|
if (owner.host === hostname()) {
|
|
2872
|
-
|
|
2912
|
+
if (!isAlive(owner.pid)) return true;
|
|
2913
|
+
const startedAt2 = processStartedAt(owner.pid);
|
|
2914
|
+
if (owner.pidStartedAt !== void 0 && startedAt2 !== null && startedAt2 !== owner.pidStartedAt) {
|
|
2915
|
+
return true;
|
|
2916
|
+
}
|
|
2917
|
+
return false;
|
|
2873
2918
|
}
|
|
2874
2919
|
const startedAt = Date.parse(owner.startedAt);
|
|
2875
2920
|
if (Number.isNaN(startedAt)) return true;
|
|
2876
2921
|
return Date.now() - startedAt > staleMinutes * 60 * 1e3;
|
|
2877
2922
|
}
|
|
2923
|
+
function heldTooLong(owner, staleMinutes) {
|
|
2924
|
+
if (owner.host !== hostname()) return false;
|
|
2925
|
+
if (owner.pidStartedAt !== void 0 && processStartedAt(owner.pid) !== null) return false;
|
|
2926
|
+
const startedAt = Date.parse(owner.startedAt);
|
|
2927
|
+
if (Number.isNaN(startedAt)) return true;
|
|
2928
|
+
return Date.now() - startedAt > staleMinutes * 60 * 1e3;
|
|
2929
|
+
}
|
|
2878
2930
|
function makeHandle(path, token) {
|
|
2879
2931
|
let released = false;
|
|
2880
2932
|
return {
|
|
@@ -2885,8 +2937,26 @@ function makeHandle(path, token) {
|
|
|
2885
2937
|
if (current !== null && current.token !== token) {
|
|
2886
2938
|
return;
|
|
2887
2939
|
}
|
|
2940
|
+
const takenAway = `${path}.releasing.${token}`;
|
|
2888
2941
|
try {
|
|
2889
|
-
|
|
2942
|
+
renameSync(path, takenAway);
|
|
2943
|
+
} catch {
|
|
2944
|
+
return;
|
|
2945
|
+
}
|
|
2946
|
+
const owner = readOwner(takenAway);
|
|
2947
|
+
if (owner === null || owner.token === token) {
|
|
2948
|
+
try {
|
|
2949
|
+
unlinkSync(takenAway);
|
|
2950
|
+
} catch {
|
|
2951
|
+
}
|
|
2952
|
+
return;
|
|
2953
|
+
}
|
|
2954
|
+
try {
|
|
2955
|
+
linkSync(takenAway, path);
|
|
2956
|
+
} catch {
|
|
2957
|
+
}
|
|
2958
|
+
try {
|
|
2959
|
+
unlinkSync(takenAway);
|
|
2890
2960
|
} catch {
|
|
2891
2961
|
}
|
|
2892
2962
|
}
|
|
@@ -2898,7 +2968,8 @@ function write(path, command, token, flag = "wx") {
|
|
|
2898
2968
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2899
2969
|
host: hostname(),
|
|
2900
2970
|
command,
|
|
2901
|
-
token
|
|
2971
|
+
token,
|
|
2972
|
+
pidStartedAt: processStartedAt(process.pid) ?? void 0
|
|
2902
2973
|
};
|
|
2903
2974
|
writeFileSync(path, JSON.stringify(owner, null, 2) + "\n", { encoding: "utf8", flag });
|
|
2904
2975
|
}
|
|
@@ -2914,9 +2985,10 @@ function acquireRunLock(command, staleMinutes) {
|
|
|
2914
2985
|
}
|
|
2915
2986
|
const owner = readOwner(path);
|
|
2916
2987
|
if (!isStale(owner, staleMinutes)) {
|
|
2917
|
-
|
|
2988
|
+
const held = owner;
|
|
2989
|
+
return { ok: false, heldBy: held, suspect: heldTooLong(held, staleMinutes) };
|
|
2918
2990
|
}
|
|
2919
|
-
return reclaim(path, command, token);
|
|
2991
|
+
return reclaim(path, command, token, owner);
|
|
2920
2992
|
}
|
|
2921
2993
|
var UNKNOWN_OWNER = {
|
|
2922
2994
|
pid: 0,
|
|
@@ -2925,18 +2997,34 @@ var UNKNOWN_OWNER = {
|
|
|
2925
2997
|
command: "unknown",
|
|
2926
2998
|
token: ""
|
|
2927
2999
|
};
|
|
2928
|
-
function claimStaleLock(path, token) {
|
|
3000
|
+
function claimStaleLock(path, token, expected) {
|
|
3001
|
+
const claimed = `${path}.stale.${token}`;
|
|
2929
3002
|
try {
|
|
2930
|
-
renameSync(path,
|
|
2931
|
-
return true;
|
|
3003
|
+
renameSync(path, claimed);
|
|
2932
3004
|
} catch (err) {
|
|
2933
3005
|
if (err.code === "ENOENT") return false;
|
|
2934
3006
|
throw err;
|
|
2935
3007
|
}
|
|
3008
|
+
if (expected !== void 0) {
|
|
3009
|
+
const taken = readOwner(claimed);
|
|
3010
|
+
const sameFile = expected === null && taken === null || expected !== null && taken !== null && taken.token === expected.token;
|
|
3011
|
+
if (!sameFile) {
|
|
3012
|
+
try {
|
|
3013
|
+
linkSync(claimed, path);
|
|
3014
|
+
} catch {
|
|
3015
|
+
}
|
|
3016
|
+
try {
|
|
3017
|
+
unlinkSync(claimed);
|
|
3018
|
+
} catch {
|
|
3019
|
+
}
|
|
3020
|
+
return false;
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
return true;
|
|
2936
3024
|
}
|
|
2937
|
-
function reclaim(path, command, token) {
|
|
2938
|
-
if (!claimStaleLock(path, token)) {
|
|
2939
|
-
return { ok: false, heldBy: readOwner(path) ?? UNKNOWN_OWNER };
|
|
3025
|
+
function reclaim(path, command, token, expected) {
|
|
3026
|
+
if (!claimStaleLock(path, token, expected)) {
|
|
3027
|
+
return { ok: false, heldBy: readOwner(path) ?? UNKNOWN_OWNER, suspect: false };
|
|
2940
3028
|
}
|
|
2941
3029
|
const claimed = `${path}.stale.${token}`;
|
|
2942
3030
|
try {
|
|
@@ -2944,7 +3032,7 @@ function reclaim(path, command, token) {
|
|
|
2944
3032
|
return { ok: true, handle: makeHandle(path, token) };
|
|
2945
3033
|
} catch (err) {
|
|
2946
3034
|
if (err.code !== "EEXIST") throw err;
|
|
2947
|
-
return { ok: false, heldBy: readOwner(path) ?? UNKNOWN_OWNER };
|
|
3035
|
+
return { ok: false, heldBy: readOwner(path) ?? UNKNOWN_OWNER, suspect: false };
|
|
2948
3036
|
} finally {
|
|
2949
3037
|
try {
|
|
2950
3038
|
unlinkSync(claimed);
|
|
@@ -2995,6 +3083,7 @@ function preparePrBranch(headRefName) {
|
|
|
2995
3083
|
}
|
|
2996
3084
|
|
|
2997
3085
|
// src/commands/doWork.ts
|
|
3086
|
+
var inFlightMarker = null;
|
|
2998
3087
|
function out(message) {
|
|
2999
3088
|
process.stdout.write(message);
|
|
3000
3089
|
}
|
|
@@ -3043,8 +3132,8 @@ function resolveSettings(options) {
|
|
|
3043
3132
|
if (agentUser.length === 0) {
|
|
3044
3133
|
fail("No agent user configured. Run `automata config set agent-user <login>`.");
|
|
3045
3134
|
}
|
|
3135
|
+
validateDoWorkConfig(config.doWork);
|
|
3046
3136
|
const doWork = config.doWork ?? {};
|
|
3047
|
-
validateDoWorkConfig(doWork);
|
|
3048
3137
|
let executor = doWork.executor ?? DEFAULT_DO_WORK.executor;
|
|
3049
3138
|
if (options.with !== void 0) {
|
|
3050
3139
|
const requested = options.with.toLowerCase();
|
|
@@ -3136,33 +3225,49 @@ function describePlannedRun(item, settings, run5) {
|
|
|
3136
3225
|
];
|
|
3137
3226
|
return lines.join("\n") + "\n";
|
|
3138
3227
|
}
|
|
3139
|
-
function
|
|
3140
|
-
|
|
3141
|
-
|
|
3228
|
+
function isPlainObject(value) {
|
|
3229
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3230
|
+
}
|
|
3231
|
+
function validateOptionalString(container, key, path) {
|
|
3232
|
+
const value = container[key];
|
|
3233
|
+
if (value === void 0 || value === null) return;
|
|
3234
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
3235
|
+
fail(`${path} must be a non-empty string.`);
|
|
3142
3236
|
}
|
|
3143
|
-
|
|
3144
|
-
|
|
3237
|
+
}
|
|
3238
|
+
function validateOptionalInt(container, key, path, min, hint) {
|
|
3239
|
+
const value = container[key];
|
|
3240
|
+
if (value === void 0 || value === null) return;
|
|
3241
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < min) {
|
|
3242
|
+
fail(`${path} must be ${hint}, got ${JSON.stringify(value)}.`);
|
|
3145
3243
|
}
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
}
|
|
3244
|
+
}
|
|
3245
|
+
function validateDoWorkConfig(section) {
|
|
3246
|
+
if (section === void 0 || section === null) return;
|
|
3247
|
+
if (!isPlainObject(section)) {
|
|
3248
|
+
fail(`doWork must be an object, got ${JSON.stringify(section)}.`);
|
|
3152
3249
|
}
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
}
|
|
3250
|
+
const executor = section["executor"];
|
|
3251
|
+
if (executor !== void 0 && executor !== "claude" && executor !== "codex") {
|
|
3252
|
+
fail(`doWork.executor must be 'claude' or 'codex', got ${JSON.stringify(executor)}.`);
|
|
3157
3253
|
}
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
[
|
|
3163
|
-
|
|
3164
|
-
if (
|
|
3165
|
-
fail(
|
|
3254
|
+
validateOptionalString(section, "baseBranch", "doWork.baseBranch");
|
|
3255
|
+
validateOptionalInt(section, "maxRunsPerTick", "doWork.maxRunsPerTick", 0, "a non-negative integer (0 = unlimited)");
|
|
3256
|
+
validateOptionalInt(section, "lockStaleMinutes", "doWork.lockStaleMinutes", 1, "a positive integer");
|
|
3257
|
+
for (const container of ["models", "prompts"]) {
|
|
3258
|
+
const value = section[container];
|
|
3259
|
+
if (value === void 0 || value === null) continue;
|
|
3260
|
+
if (!isPlainObject(value)) {
|
|
3261
|
+
fail(`doWork.${container} must be an object, got ${JSON.stringify(value)}.`);
|
|
3262
|
+
}
|
|
3263
|
+
const keys = container === "models" ? ["claude", "codex"] : ["issueDiscuss", "prWork"];
|
|
3264
|
+
for (const key of keys) {
|
|
3265
|
+
validateOptionalString(value, key, `doWork.${container}.${key}`);
|
|
3266
|
+
}
|
|
3267
|
+
for (const key of Object.keys(value)) {
|
|
3268
|
+
if (!keys.includes(key)) {
|
|
3269
|
+
fail(`doWork.${container}.${key} is not a recognised setting; expected one of: ${keys.join(", ")}.`);
|
|
3270
|
+
}
|
|
3166
3271
|
}
|
|
3167
3272
|
}
|
|
3168
3273
|
}
|
|
@@ -3220,6 +3325,40 @@ function describePlan(decisions) {
|
|
|
3220
3325
|
});
|
|
3221
3326
|
return lines.length === 0 ? " (no issues matched the discovery filter)\n" : lines.join("\n") + "\n";
|
|
3222
3327
|
}
|
|
3328
|
+
function claimIssue(item, settings) {
|
|
3329
|
+
if (!item.needsAssignment) return;
|
|
3330
|
+
try {
|
|
3331
|
+
assignIssueToAgent(item.issue.number, settings.participants.agentUser);
|
|
3332
|
+
progress(` assigned issue #${String(item.issue.number)} to ${settings.participants.agentUser}.
|
|
3333
|
+
`);
|
|
3334
|
+
} catch (err) {
|
|
3335
|
+
progress(` warning: could not assign issue #${String(item.issue.number)}: ${err.message}
|
|
3336
|
+
`);
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
function notePickupOnIssue(item, marker) {
|
|
3340
|
+
if (item.turn !== "pr-work" || item.pr === null || !item.issueAnalysis.hasNewMessage) return true;
|
|
3341
|
+
try {
|
|
3342
|
+
postMarker(
|
|
3343
|
+
"issue",
|
|
3344
|
+
item.issue.number,
|
|
3345
|
+
`automata do-work: picked this up on pull request #${String(item.pr.number)} \u2014 ${item.pr.url}`
|
|
3346
|
+
);
|
|
3347
|
+
progress(` noted on issue #${String(item.issue.number)} that the work is on pull request #${String(item.pr.number)}.
|
|
3348
|
+
`);
|
|
3349
|
+
return true;
|
|
3350
|
+
} catch (err) {
|
|
3351
|
+
progress(` skipped: could not note the pickup on issue #${String(item.issue.number)} \u2014 ${err.message}
|
|
3352
|
+
`);
|
|
3353
|
+
try {
|
|
3354
|
+
deleteMarker(marker);
|
|
3355
|
+
} catch (deleteErr) {
|
|
3356
|
+
progress(` warning: could not withdraw the working marker: ${deleteErr.message}
|
|
3357
|
+
`);
|
|
3358
|
+
}
|
|
3359
|
+
return false;
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3223
3362
|
function refreshItem(item, settings) {
|
|
3224
3363
|
const linkMap = getOpenPrLinkMap();
|
|
3225
3364
|
return decideWork(
|
|
@@ -3228,6 +3367,14 @@ function refreshItem(item, settings) {
|
|
|
3228
3367
|
settings.baseBranch
|
|
3229
3368
|
);
|
|
3230
3369
|
}
|
|
3370
|
+
function authorizedMessagesSince(messages, participants, marker) {
|
|
3371
|
+
const allowed = participants.allowedUsers.map((user) => user.toLowerCase());
|
|
3372
|
+
const agent = participants.agentUser.toLowerCase();
|
|
3373
|
+
return messages.filter((message) => {
|
|
3374
|
+
const author = message.author.toLowerCase();
|
|
3375
|
+
return author !== agent && allowed.includes(author) && message.createdAt > marker.createdAt;
|
|
3376
|
+
});
|
|
3377
|
+
}
|
|
3231
3378
|
function readAnsweringSurface(item) {
|
|
3232
3379
|
if (item.turn === "issue-discuss" || item.pr === null) {
|
|
3233
3380
|
return getIssueSurface(item.issue.number).messages;
|
|
@@ -3238,10 +3385,13 @@ function readAnsweringSurface(item) {
|
|
|
3238
3385
|
function markerSurfaceLabel(item) {
|
|
3239
3386
|
return item.turn === "pr-work" && item.pr ? `pull request #${String(item.pr.number)}` : `issue #${String(item.issue.number)}`;
|
|
3240
3387
|
}
|
|
3241
|
-
function reconcileMarker(item, marker,
|
|
3388
|
+
function reconcileMarker(item, marker, participants, runError) {
|
|
3389
|
+
const agentUser = participants.agentUser;
|
|
3390
|
+
let surfaceMessages = [];
|
|
3242
3391
|
let answered;
|
|
3243
3392
|
try {
|
|
3244
|
-
|
|
3393
|
+
surfaceMessages = readAnsweringSurface(item);
|
|
3394
|
+
answered = agentAnsweredAfter(surfaceMessages, agentUser, marker);
|
|
3245
3395
|
} catch (err) {
|
|
3246
3396
|
progress(
|
|
3247
3397
|
` warning: could not re-read issue #${String(item.issue.number)} to check for an answer: ${err.message}
|
|
@@ -3263,12 +3413,30 @@ function reconcileMarker(item, marker, agentUser, runError) {
|
|
|
3263
3413
|
return { outcome: "answered-no-reply", detail: "could not verify whether an answer was posted" };
|
|
3264
3414
|
}
|
|
3265
3415
|
if (answered) {
|
|
3416
|
+
const interleaved = authorizedMessagesSince(surfaceMessages, participants, marker);
|
|
3417
|
+
if (interleaved.length > 0) {
|
|
3418
|
+
const authors = [...new Set(interleaved.map((message) => message.author))].join(", ");
|
|
3419
|
+
try {
|
|
3420
|
+
postMarker(
|
|
3421
|
+
item.turn === "pr-work" ? "pr" : "issue",
|
|
3422
|
+
item.turn === "pr-work" && item.pr ? item.pr.number : item.issue.number,
|
|
3423
|
+
`automata do-work: ${authors} posted here while this run was already in progress, so ${interleaved.length === 1 ? "that message was" : "those messages were"} not included in it. Please post again to have them acted on.`
|
|
3424
|
+
);
|
|
3425
|
+
} catch (err) {
|
|
3426
|
+
progress(` warning: could not report the interleaved message(s): ${err.message}
|
|
3427
|
+
`);
|
|
3428
|
+
}
|
|
3429
|
+
}
|
|
3266
3430
|
try {
|
|
3267
3431
|
deleteMarker(marker);
|
|
3268
3432
|
} catch (err) {
|
|
3269
3433
|
progress(` warning: could not delete the marker comment: ${err.message}
|
|
3270
3434
|
`);
|
|
3271
3435
|
}
|
|
3436
|
+
const noted = interleaved.length > 0 ? ` (${String(interleaved.length)} message(s) arrived mid-run and were flagged)` : "";
|
|
3437
|
+
if (interleaved.length > 0) {
|
|
3438
|
+
return { outcome: "answered-no-reply", detail: `answered${noted}` };
|
|
3439
|
+
}
|
|
3272
3440
|
return runError === null ? { outcome: "answered", detail: "answered" } : { outcome: "answered", detail: `answered, but the run reported: ${runError.message}` };
|
|
3273
3441
|
}
|
|
3274
3442
|
const surface = markerSurfaceLabel(item);
|
|
@@ -3305,7 +3473,8 @@ function repairIssueLink(item, baseBranch) {
|
|
|
3305
3473
|
`);
|
|
3306
3474
|
return;
|
|
3307
3475
|
}
|
|
3308
|
-
|
|
3476
|
+
const closesRef = new RegExp(String.raw`\bcloses\s+#` + String(item.issue.number) + String.raw`\b`, "i");
|
|
3477
|
+
if (closesRef.test(pr.body)) {
|
|
3309
3478
|
progress(` pull request #${String(pr.number)} already closes issue #${String(item.issue.number)}.
|
|
3310
3479
|
`);
|
|
3311
3480
|
return;
|
|
@@ -3350,16 +3519,7 @@ async function processItem(planned, settings, silent) {
|
|
|
3350
3519
|
`);
|
|
3351
3520
|
return { ...base, outcome: "skipped", detail: `${prepared.reason}: ${prepared.detail}` };
|
|
3352
3521
|
}
|
|
3353
|
-
|
|
3354
|
-
try {
|
|
3355
|
-
assignIssueToAgent(item.issue.number, settings.participants.agentUser);
|
|
3356
|
-
progress(` assigned issue #${String(item.issue.number)} to ${settings.participants.agentUser}.
|
|
3357
|
-
`);
|
|
3358
|
-
} catch (err) {
|
|
3359
|
-
progress(` warning: could not assign issue #${String(item.issue.number)}: ${err.message}
|
|
3360
|
-
`);
|
|
3361
|
-
}
|
|
3362
|
-
}
|
|
3522
|
+
claimIssue(item, settings);
|
|
3363
3523
|
let marker;
|
|
3364
3524
|
const markerSurface = item.turn === "pr-work" && item.pr ? item.pr.number : item.issue.number;
|
|
3365
3525
|
try {
|
|
@@ -3369,6 +3529,11 @@ async function processItem(planned, settings, silent) {
|
|
|
3369
3529
|
`);
|
|
3370
3530
|
return { ...base, outcome: "skipped", detail: `marker failed: ${err.message}` };
|
|
3371
3531
|
}
|
|
3532
|
+
inFlightMarker = { marker, item };
|
|
3533
|
+
if (!notePickupOnIssue(item, marker)) {
|
|
3534
|
+
inFlightMarker = null;
|
|
3535
|
+
return { ...base, outcome: "skipped", detail: "issue pickup note failed" };
|
|
3536
|
+
}
|
|
3372
3537
|
const prompt = composePrompt({
|
|
3373
3538
|
item,
|
|
3374
3539
|
repo: getRepoSlug(),
|
|
@@ -3376,19 +3541,39 @@ async function processItem(planned, settings, silent) {
|
|
|
3376
3541
|
baseBranch: settings.baseBranch,
|
|
3377
3542
|
frame: settings.prompts[item.turn]
|
|
3378
3543
|
});
|
|
3544
|
+
const MAX_PROMPT_BYTES = 96 * 1024;
|
|
3545
|
+
const promptBytes = Buffer.byteLength(prompt, "utf8");
|
|
3546
|
+
if (promptBytes > MAX_PROMPT_BYTES) {
|
|
3547
|
+
const detail = `the composed prompt is ${String(Math.round(promptBytes / 1024))} KiB, over the ${String(MAX_PROMPT_BYTES / 1024)} KiB limit for a single command-line argument. The conversation is too long to hand to the executor this way.`;
|
|
3548
|
+
progress(` failed: ${detail}
|
|
3549
|
+
`);
|
|
3550
|
+
inFlightMarker = null;
|
|
3551
|
+
try {
|
|
3552
|
+
updateMarker(
|
|
3553
|
+
marker,
|
|
3554
|
+
`automata do-work: could not start a run because ${detail} Summarise the discussion in a new issue, or shorten the thread, and try again.`
|
|
3555
|
+
);
|
|
3556
|
+
} catch (err) {
|
|
3557
|
+
progress(` warning: could not update the marker comment: ${err.message}
|
|
3558
|
+
`);
|
|
3559
|
+
}
|
|
3560
|
+
return { ...base, outcome: "failed", detail };
|
|
3561
|
+
}
|
|
3379
3562
|
let runError = null;
|
|
3380
3563
|
try {
|
|
3381
3564
|
await invokeExecutor(prompt, settings, silent);
|
|
3382
3565
|
} catch (err) {
|
|
3383
3566
|
runError = err;
|
|
3384
3567
|
}
|
|
3385
|
-
const
|
|
3568
|
+
const ranExecutor = true;
|
|
3569
|
+
inFlightMarker = null;
|
|
3570
|
+
const reconciled = reconcileMarker(item, marker, settings.participants, runError);
|
|
3386
3571
|
progress(` ${reconciled.detail}
|
|
3387
3572
|
`);
|
|
3388
3573
|
if (item.turn === "issue-discuss") {
|
|
3389
3574
|
repairIssueLink(item, settings.baseBranch);
|
|
3390
3575
|
}
|
|
3391
|
-
return { ...base, outcome: reconciled.outcome, detail: reconciled.detail };
|
|
3576
|
+
return { ...base, outcome: reconciled.outcome, detail: reconciled.detail, ranExecutor };
|
|
3392
3577
|
}
|
|
3393
3578
|
function summarize(reports) {
|
|
3394
3579
|
out("\nTick summary:\n");
|
|
@@ -3408,12 +3593,33 @@ var doWorkCommand = new Command6("do-work").description(
|
|
|
3408
3593
|
"Print the work plan, plus a summary and the exact command that would be launched for each item, and exit without changing anything"
|
|
3409
3594
|
).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) => {
|
|
3410
3595
|
const settings = resolveSettings(options);
|
|
3596
|
+
if (options.dryRun === true) {
|
|
3597
|
+
const exitCode2 = await runTick(settings, options);
|
|
3598
|
+
if (exitCode2 !== 0) process.exit(exitCode2);
|
|
3599
|
+
return;
|
|
3600
|
+
}
|
|
3411
3601
|
const lock = acquireRunLock("do-work", settings.lockStaleMinutes);
|
|
3412
3602
|
if (!lock.ok) {
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
);
|
|
3603
|
+
const held = lock.heldBy;
|
|
3604
|
+
const sentence = `Another automata instance is already running here (pid ${String(held.pid)} on ${held.host}, started ${held.startedAt}, command ${held.command}). Doing nothing.
|
|
3605
|
+
`;
|
|
3606
|
+
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.
|
|
3607
|
+
` : "";
|
|
3608
|
+
const exitCode2 = lock.suspect ? 2 : 0;
|
|
3609
|
+
if (options.json === true) {
|
|
3610
|
+
progress(sentence + suspectSentence);
|
|
3611
|
+
out(
|
|
3612
|
+
JSON.stringify(
|
|
3613
|
+
{ lockHeld: true, suspect: lock.suspect, heldBy: held, plan: [], items: [], exitCode: exitCode2 },
|
|
3614
|
+
null,
|
|
3615
|
+
2
|
|
3616
|
+
) + "\n"
|
|
3617
|
+
);
|
|
3618
|
+
} else {
|
|
3619
|
+
out(sentence);
|
|
3620
|
+
if (suspectSentence) progress(suspectSentence);
|
|
3621
|
+
}
|
|
3622
|
+
if (exitCode2 !== 0) process.exit(exitCode2);
|
|
3417
3623
|
return;
|
|
3418
3624
|
}
|
|
3419
3625
|
const handle = lock.handle;
|
|
@@ -3422,6 +3628,18 @@ var doWorkCommand = new Command6("do-work").description(
|
|
|
3422
3628
|
if (shuttingDown) return;
|
|
3423
3629
|
shuttingDown = true;
|
|
3424
3630
|
progress("\nInterrupted: stopping the executor before releasing the run lock\u2026\n");
|
|
3631
|
+
const pending = inFlightMarker;
|
|
3632
|
+
if (pending !== null) {
|
|
3633
|
+
try {
|
|
3634
|
+
updateMarker(
|
|
3635
|
+
pending.marker,
|
|
3636
|
+
`automata do-work: this run was interrupted before it finished, so no answer was produced. The branch \`${pending.item.branch}\` may have been changed. Reply here to have another attempt made.`
|
|
3637
|
+
);
|
|
3638
|
+
} catch (err) {
|
|
3639
|
+
progress(`Warning: could not update the in-flight marker: ${err.message}
|
|
3640
|
+
`);
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
3425
3643
|
void terminateTrackedChildren().then((allExited) => {
|
|
3426
3644
|
if (allExited) {
|
|
3427
3645
|
handle.release();
|
|
@@ -3473,8 +3691,21 @@ ${describePlan(decisions)}`;
|
|
|
3473
3691
|
deferred.push(item);
|
|
3474
3692
|
continue;
|
|
3475
3693
|
}
|
|
3476
|
-
|
|
3477
|
-
|
|
3694
|
+
let report;
|
|
3695
|
+
try {
|
|
3696
|
+
report = await processItem(item, settings, options.silent === true);
|
|
3697
|
+
} catch (err) {
|
|
3698
|
+
progress(` failed: ${err.message}
|
|
3699
|
+
`);
|
|
3700
|
+
report = {
|
|
3701
|
+
issue: item.issue.number,
|
|
3702
|
+
title: item.issue.title,
|
|
3703
|
+
turn: item.turn,
|
|
3704
|
+
outcome: "failed",
|
|
3705
|
+
detail: err.message
|
|
3706
|
+
};
|
|
3707
|
+
}
|
|
3708
|
+
if (report.ranExecutor === true) runsUsed++;
|
|
3478
3709
|
reports.push(report);
|
|
3479
3710
|
}
|
|
3480
3711
|
for (const item of deferred) {
|