automata-cli 0.1.0-feature-030-do-work.214 → 0.1.0-feature-030-do-work.218
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.
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
readConfig,
|
|
10
10
|
readRawConfig,
|
|
11
11
|
writeConfig
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-LTCVLM4I.js";
|
|
13
13
|
|
|
14
14
|
// src/config/ConfigWizard.tsx
|
|
15
15
|
import { useState } from "react";
|
|
@@ -147,6 +147,9 @@ function ConfigWizard() {
|
|
|
147
147
|
);
|
|
148
148
|
const initialExecutorIndex = EXECUTOR_OPTIONS.findIndex((o) => o.value === existing.doWork?.executor);
|
|
149
149
|
const [doWorkExecutorIndex, setDoWorkExecutorIndex] = useState(Math.max(initialExecutorIndex, 0));
|
|
150
|
+
const [doWorkProtectedBranches, setDoWorkProtectedBranches] = useState(
|
|
151
|
+
(existing.doWork?.protectedBranches ?? DEFAULT_DO_WORK.protectedBranches).join(", ")
|
|
152
|
+
);
|
|
150
153
|
const [doWorkClaudeModel, setDoWorkClaudeModel] = useState(existing.doWork?.models?.claude ?? "");
|
|
151
154
|
const [doWorkCodexModel, setDoWorkCodexModel] = useState(existing.doWork?.models?.codex ?? "");
|
|
152
155
|
const [doWorkLockStale, setDoWorkLockStale] = useState(
|
|
@@ -246,9 +249,24 @@ function ConfigWizard() {
|
|
|
246
249
|
},
|
|
247
250
|
"do-work-base-branch": {
|
|
248
251
|
setValue: setDoWorkBaseBranch,
|
|
249
|
-
onSubmit: () => setScreen("do-work-
|
|
252
|
+
onSubmit: () => setScreen("do-work-protected-branches"),
|
|
250
253
|
onBack: () => setScreen("main")
|
|
251
254
|
},
|
|
255
|
+
"do-work-protected-branches": {
|
|
256
|
+
setValue: (update) => {
|
|
257
|
+
setValidationError("");
|
|
258
|
+
setDoWorkProtectedBranches(update);
|
|
259
|
+
},
|
|
260
|
+
onSubmit: () => {
|
|
261
|
+
if (parseAllowedUsers(doWorkProtectedBranches).length === 0) {
|
|
262
|
+
setValidationError("Enter at least one branch name.");
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
setValidationError("");
|
|
266
|
+
setScreen("do-work-executor");
|
|
267
|
+
},
|
|
268
|
+
onBack: () => setScreen("do-work-base-branch")
|
|
269
|
+
},
|
|
252
270
|
"do-work-claude-model": {
|
|
253
271
|
setValue: setDoWorkClaudeModel,
|
|
254
272
|
onSubmit: () => setScreen("do-work-codex-model"),
|
|
@@ -293,6 +311,7 @@ function ConfigWizard() {
|
|
|
293
311
|
doWork: {
|
|
294
312
|
...current.doWork,
|
|
295
313
|
baseBranch: doWorkBaseBranch.trim() || void 0,
|
|
314
|
+
protectedBranches: parseAllowedUsers(doWorkProtectedBranches),
|
|
296
315
|
executor: EXECUTOR_OPTIONS[doWorkExecutorIndex].value,
|
|
297
316
|
models: {
|
|
298
317
|
claude: doWorkClaudeModel.trim() || void 0,
|
|
@@ -375,7 +394,7 @@ function ConfigWizard() {
|
|
|
375
394
|
length: EXECUTOR_OPTIONS.length,
|
|
376
395
|
setIndex: setDoWorkExecutorIndex,
|
|
377
396
|
onSelect: () => setScreen("do-work-claude-model"),
|
|
378
|
-
onBack: () => setScreen("do-work-
|
|
397
|
+
onBack: () => setScreen("do-work-protected-branches")
|
|
379
398
|
}
|
|
380
399
|
};
|
|
381
400
|
useInput((input, key) => {
|
|
@@ -439,6 +458,12 @@ function ConfigWizard() {
|
|
|
439
458
|
value: doWorkBaseBranch,
|
|
440
459
|
hint: `Type branch \xB7 Enter to continue \xB7 ${BACK}`
|
|
441
460
|
},
|
|
461
|
+
"do-work-protected-branches": {
|
|
462
|
+
title: "Do Work \u2014 Protected Branches",
|
|
463
|
+
label: "Branches a build turn must never push to (comma separated):",
|
|
464
|
+
value: doWorkProtectedBranches,
|
|
465
|
+
hint: `Type branches \xB7 Enter to continue \xB7 ${BACK}`
|
|
466
|
+
},
|
|
442
467
|
"do-work-claude-model": {
|
|
443
468
|
title: "Do Work \u2014 Claude Model",
|
|
444
469
|
label: "Default model when the executor is Claude (blank = the executor's own default):",
|
|
@@ -9,6 +9,7 @@ var DEFAULT_SONAR_PROMPT = "You are an expert software engineer. You have been g
|
|
|
9
9
|
var DEFAULT_CHECK_ISSUE_PROMPT = "You are an expert software engineer working on a GitHub issue. Below is the conversation on that issue, restricted to the people allowed to instruct you and your own previous replies. Messages marked as new arrived after your last run: treat them as the current instruction and read the earlier messages only as context. Do what the new messages ask, following the project's existing conventions and style, and make minimal, targeted changes. Run tests and linting before finishing, then reply on the issue with a short summary of what you did.";
|
|
10
10
|
var DEFAULT_DO_WORK = {
|
|
11
11
|
baseBranch: "develop",
|
|
12
|
+
protectedBranches: ["main", "master"],
|
|
12
13
|
executor: "claude",
|
|
13
14
|
maxRunsPerTick: 0,
|
|
14
15
|
lockStaleMinutes: 120
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
readConfig,
|
|
11
11
|
readRawConfig,
|
|
12
12
|
writeConfig
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-LTCVLM4I.js";
|
|
14
14
|
|
|
15
15
|
// src/index.ts
|
|
16
16
|
import { Command as Command7 } from "commander";
|
|
@@ -109,6 +109,16 @@ var configSetDoWorkBaseBranch = new Command("do-work-base-branch").description("
|
|
|
109
109
|
process.stdout.write(`do-work base branch set to: ${branch}
|
|
110
110
|
`);
|
|
111
111
|
});
|
|
112
|
+
var configSetDoWorkProtectedBranches = new Command("do-work-protected-branches").description("Set the branches a `do-work` build turn must never check out and push to").argument("<value>", `Comma-separated branch names (default: ${DEFAULT_DO_WORK.protectedBranches.join(",")})`).action((value) => {
|
|
113
|
+
const branches = value.split(",").map((branch) => branch.trim()).filter((branch) => branch.length > 0);
|
|
114
|
+
if (branches.length === 0) {
|
|
115
|
+
process.stderr.write("Error: do-work-protected-branches requires at least one branch name.\n");
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
writeDoWork({ protectedBranches: branches });
|
|
119
|
+
process.stdout.write(`do-work protected branches set to: ${branches.join(", ")}
|
|
120
|
+
`);
|
|
121
|
+
});
|
|
112
122
|
var configSetDoWorkExecutor = new Command("do-work-executor").description("Set the default executor `do-work` invokes").argument("<value>", `Executor: ${VALID_EXECUTORS.join(", ")}`).action((value) => {
|
|
113
123
|
if (!VALID_EXECUTORS.includes(value)) {
|
|
114
124
|
process.stderr.write(`Error: invalid executor "${value}". Must be one of: ${VALID_EXECUTORS.join(", ")}
|
|
@@ -179,12 +189,12 @@ var configSetDoWorkPrompt = new Command("do-work-prompt").description("Set the t
|
|
|
179
189
|
process.stdout.write(`do-work ${turnKind} prompt set.
|
|
180
190
|
`);
|
|
181
191
|
});
|
|
182
|
-
var configSet = new Command("set").description("Set a configuration value").addCommand(configSetType).addCommand(configSetIssueDiscoveryTechnique).addCommand(configSetIssueDiscoveryValue).addCommand(configSetClaudeSystemPrompt).addCommand(configSetAllowedUsers).addCommand(configSetAgentUser).addCommand(configSetDoWorkBaseBranch).addCommand(configSetDoWorkExecutor).addCommand(configSetDoWorkModel).addCommand(configSetDoWorkMaxRuns).addCommand(configSetDoWorkLockStaleMinutes).addCommand(configSetDoWorkPrompt);
|
|
192
|
+
var configSet = new Command("set").description("Set a configuration value").addCommand(configSetType).addCommand(configSetIssueDiscoveryTechnique).addCommand(configSetIssueDiscoveryValue).addCommand(configSetClaudeSystemPrompt).addCommand(configSetAllowedUsers).addCommand(configSetAgentUser).addCommand(configSetDoWorkBaseBranch).addCommand(configSetDoWorkProtectedBranches).addCommand(configSetDoWorkExecutor).addCommand(configSetDoWorkModel).addCommand(configSetDoWorkMaxRuns).addCommand(configSetDoWorkLockStaleMinutes).addCommand(configSetDoWorkPrompt);
|
|
183
193
|
var configCommand = new Command("config").description("Configure automata settings").addCommand(configSet).action(async () => {
|
|
184
194
|
const [{ render }, React, { ConfigWizard }] = await Promise.all([
|
|
185
195
|
import("ink"),
|
|
186
196
|
import("react"),
|
|
187
|
-
import("./ConfigWizard-
|
|
197
|
+
import("./ConfigWizard-5B5AOGUU.js")
|
|
188
198
|
]);
|
|
189
199
|
const { waitUntilExit } = render(React.createElement(ConfigWizard));
|
|
190
200
|
await waitUntilExit();
|
|
@@ -2350,6 +2360,10 @@ function login(author) {
|
|
|
2350
2360
|
function byCreatedAt2(a, b) {
|
|
2351
2361
|
return a.createdAt.localeCompare(b.createdAt);
|
|
2352
2362
|
}
|
|
2363
|
+
function visibleAt(createdAt, submittedAt) {
|
|
2364
|
+
if (submittedAt === null || submittedAt === void 0) return createdAt;
|
|
2365
|
+
return submittedAt > createdAt ? submittedAt : createdAt;
|
|
2366
|
+
}
|
|
2353
2367
|
function getRepoSlug() {
|
|
2354
2368
|
const { stdout, status } = run4("git", ["remote", "get-url", "origin"]);
|
|
2355
2369
|
if (status !== 0) {
|
|
@@ -2502,7 +2516,7 @@ query($owner:String!,$repo:String!,$prNumber:Int!,$cursor:String){
|
|
|
2502
2516
|
isResolved isOutdated path line
|
|
2503
2517
|
comments(last:100){
|
|
2504
2518
|
pageInfo{ hasPreviousPage }
|
|
2505
|
-
nodes{ author{login} body createdAt url }
|
|
2519
|
+
nodes{ author{login} body createdAt url pullRequestReview{ submittedAt } }
|
|
2506
2520
|
}
|
|
2507
2521
|
}
|
|
2508
2522
|
}
|
|
@@ -2553,7 +2567,14 @@ function getReviewThreads(prNumber) {
|
|
|
2553
2567
|
kind: "thread-comment",
|
|
2554
2568
|
author: login(comment.author),
|
|
2555
2569
|
body: comment.body,
|
|
2556
|
-
|
|
2570
|
+
// When the comment became *visible*, not when it was drafted.
|
|
2571
|
+
// GitHub stamps `createdAt` the moment a comment is added to a
|
|
2572
|
+
// pending review, and it only becomes visible when the review is
|
|
2573
|
+
// submitted — minutes later for a human working through a diff.
|
|
2574
|
+
// Using `createdAt` let an agent answer posted in between look newer
|
|
2575
|
+
// than the review, which marked every one of its threads answered
|
|
2576
|
+
// and discarded the whole review silently.
|
|
2577
|
+
createdAt: visibleAt(comment.createdAt, comment.pullRequestReview?.submittedAt)
|
|
2557
2578
|
})).sort(byCreatedAt2)
|
|
2558
2579
|
});
|
|
2559
2580
|
}
|
|
@@ -2696,7 +2717,32 @@ function newestPr(prs) {
|
|
|
2696
2717
|
function plural(count, noun) {
|
|
2697
2718
|
return `${String(count)} ${noun}${count === 1 ? "" : "s"}`;
|
|
2698
2719
|
}
|
|
2699
|
-
function
|
|
2720
|
+
function protectedHeads(policy) {
|
|
2721
|
+
return [policy.baseBranch, policy.defaultBranch ?? "", ...policy.protectedBranches ?? []].filter(
|
|
2722
|
+
(branch) => branch.length > 0
|
|
2723
|
+
);
|
|
2724
|
+
}
|
|
2725
|
+
function unsafeBranchSkip(pr, issue, policy) {
|
|
2726
|
+
if (pr.isCrossRepository) {
|
|
2727
|
+
return {
|
|
2728
|
+
kind: "skip",
|
|
2729
|
+
issue,
|
|
2730
|
+
reason: "unsafe-pr-branch",
|
|
2731
|
+
detail: `pull request #${String(pr.number)} comes from a fork; its head branch is not in this repository`
|
|
2732
|
+
};
|
|
2733
|
+
}
|
|
2734
|
+
if (protectedHeads(policy).includes(pr.headRefName)) {
|
|
2735
|
+
return {
|
|
2736
|
+
kind: "skip",
|
|
2737
|
+
issue,
|
|
2738
|
+
reason: "unsafe-pr-branch",
|
|
2739
|
+
detail: `pull request #${String(pr.number)} has a protected branch (${pr.headRefName}) as its head, so a turn would have to push to it`
|
|
2740
|
+
};
|
|
2741
|
+
}
|
|
2742
|
+
return null;
|
|
2743
|
+
}
|
|
2744
|
+
function decideWork(state, p, policy) {
|
|
2745
|
+
const baseBranch = policy.baseBranch;
|
|
2700
2746
|
const { issueSurface, prSurface } = state;
|
|
2701
2747
|
const issue = issueSurface.issue;
|
|
2702
2748
|
if (issueSurface.state === "CLOSED") {
|
|
@@ -2732,25 +2778,8 @@ function decideWork(state, p, baseBranch, defaultBranch = null) {
|
|
|
2732
2778
|
};
|
|
2733
2779
|
}
|
|
2734
2780
|
const surface = prSurface;
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
kind: "skip",
|
|
2738
|
-
issue,
|
|
2739
|
-
reason: "unsafe-pr-branch",
|
|
2740
|
-
detail: `pull request #${String(surface.pr.number)} comes from a fork; its head branch is not in this repository`
|
|
2741
|
-
};
|
|
2742
|
-
}
|
|
2743
|
-
const protectedHead = [baseBranch, defaultBranch].filter(
|
|
2744
|
-
(branch) => branch !== null && branch.length > 0
|
|
2745
|
-
);
|
|
2746
|
-
if (protectedHead.includes(surface.pr.headRefName)) {
|
|
2747
|
-
return {
|
|
2748
|
-
kind: "skip",
|
|
2749
|
-
issue,
|
|
2750
|
-
reason: "unsafe-pr-branch",
|
|
2751
|
-
detail: `pull request #${String(surface.pr.number)} has a protected branch (${surface.pr.headRefName}) as its head, so a turn would have to push to it`
|
|
2752
|
-
};
|
|
2753
|
-
}
|
|
2781
|
+
const unsafe = unsafeBranchSkip(surface.pr, issue, policy);
|
|
2782
|
+
if (unsafe !== null) return unsafe;
|
|
2754
2783
|
const agentThreadMessages = surface.threads.flatMap((thread) => thread.comments).filter((comment) => comment.author.toLowerCase() === p.agentUser.toLowerCase());
|
|
2755
2784
|
const prAnalysis = analyzeSurface([...surface.messages, ...agentThreadMessages], p);
|
|
2756
2785
|
const actionableThreads = findActionableThreads(surface.threads, p, prAnalysis.lastAgentAt);
|
|
@@ -2873,15 +2902,21 @@ function isAuthorized(author, p) {
|
|
|
2873
2902
|
const login2 = author.toLowerCase();
|
|
2874
2903
|
return login2 !== p.agentUser.toLowerCase() && p.allowedUsers.some((user) => user.toLowerCase() === login2);
|
|
2875
2904
|
}
|
|
2876
|
-
function promptWatermark(
|
|
2905
|
+
function promptWatermark(messageSets) {
|
|
2877
2906
|
let watermark = null;
|
|
2878
|
-
for (const
|
|
2879
|
-
for (const message of
|
|
2907
|
+
for (const messages of messageSets) {
|
|
2908
|
+
for (const message of messages) {
|
|
2880
2909
|
if (watermark === null || message.createdAt > watermark) watermark = message.createdAt;
|
|
2881
2910
|
}
|
|
2882
2911
|
}
|
|
2883
2912
|
return watermark;
|
|
2884
2913
|
}
|
|
2914
|
+
function messagesBetween(messages, p, watermark, until) {
|
|
2915
|
+
const since = watermark ?? "";
|
|
2916
|
+
return messages.filter(
|
|
2917
|
+
(message) => isAuthorized(message.author, p) && message.createdAt > since && message.createdAt <= until
|
|
2918
|
+
);
|
|
2919
|
+
}
|
|
2885
2920
|
function analyseAnswer(messages, p, marker, watermark) {
|
|
2886
2921
|
let answeredAt = null;
|
|
2887
2922
|
for (const message of messages) {
|
|
@@ -2894,12 +2929,12 @@ function analyseAnswer(messages, p, marker, watermark) {
|
|
|
2894
2929
|
const toReport = messages.filter(
|
|
2895
2930
|
(message) => isAuthorized(message.author, p) && message.createdAt > since
|
|
2896
2931
|
);
|
|
2897
|
-
const missed = answeredAt === null ? [] : toReport.filter((message) => message.createdAt
|
|
2932
|
+
const missed = answeredAt === null ? [] : toReport.filter((message) => message.createdAt <= answeredAt);
|
|
2898
2933
|
return { answeredAt, missed, toReport };
|
|
2899
2934
|
}
|
|
2900
2935
|
|
|
2901
2936
|
// src/run/runLock.ts
|
|
2902
|
-
import { writeFileSync, readFileSync as readFileSync3, unlinkSync, mkdirSync, renameSync, linkSync } from "fs";
|
|
2937
|
+
import { writeFileSync, readFileSync as readFileSync3, unlinkSync, mkdirSync, renameSync, linkSync, statSync } from "fs";
|
|
2903
2938
|
import { randomUUID } from "crypto";
|
|
2904
2939
|
import { hostname } from "os";
|
|
2905
2940
|
import { join as join2 } from "path";
|
|
@@ -2999,7 +3034,7 @@ function makeHandle(path, token) {
|
|
|
2999
3034
|
}
|
|
3000
3035
|
};
|
|
3001
3036
|
}
|
|
3002
|
-
function
|
|
3037
|
+
function publishLock(path, command, token) {
|
|
3003
3038
|
const owner = {
|
|
3004
3039
|
pid: process.pid,
|
|
3005
3040
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -3008,17 +3043,31 @@ function write(path, command, token, flag = "wx") {
|
|
|
3008
3043
|
token,
|
|
3009
3044
|
pidStartedAt: processStartedAt(process.pid) ?? void 0
|
|
3010
3045
|
};
|
|
3011
|
-
|
|
3046
|
+
const staging = `${path}.staging.${token}`;
|
|
3047
|
+
try {
|
|
3048
|
+
writeFileSync(staging, JSON.stringify(owner, null, 2) + "\n", { encoding: "utf8", flag: "wx" });
|
|
3049
|
+
} catch {
|
|
3050
|
+
return false;
|
|
3051
|
+
}
|
|
3052
|
+
try {
|
|
3053
|
+
linkSync(staging, path);
|
|
3054
|
+
return true;
|
|
3055
|
+
} catch (err) {
|
|
3056
|
+
if (err.code !== "EEXIST") throw err;
|
|
3057
|
+
return false;
|
|
3058
|
+
} finally {
|
|
3059
|
+
try {
|
|
3060
|
+
unlinkSync(staging);
|
|
3061
|
+
} catch {
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3012
3064
|
}
|
|
3013
3065
|
function acquireRunLock(command, staleMinutes) {
|
|
3014
3066
|
const path = lockPath();
|
|
3015
3067
|
const token = randomUUID();
|
|
3016
3068
|
mkdirSync(join2(process.cwd(), LOCK_DIR), { recursive: true });
|
|
3017
|
-
|
|
3018
|
-
write(path, command, token);
|
|
3069
|
+
if (publishLock(path, command, token)) {
|
|
3019
3070
|
return { ok: true, handle: makeHandle(path, token) };
|
|
3020
|
-
} catch (err) {
|
|
3021
|
-
if (err.code !== "EEXIST") throw err;
|
|
3022
3071
|
}
|
|
3023
3072
|
const owner = readOwner(path);
|
|
3024
3073
|
if (!isStale(owner, staleMinutes)) {
|
|
@@ -3071,11 +3120,14 @@ function acquireClaim(path) {
|
|
|
3071
3120
|
} catch (err) {
|
|
3072
3121
|
if (err.code !== "EEXIST") throw err;
|
|
3073
3122
|
}
|
|
3123
|
+
if (!claimIsAbandoned(claimPath)) return null;
|
|
3074
3124
|
try {
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3125
|
+
renameSync(claimPath, `${claimPath}.abandoned.${String(process.pid)}`);
|
|
3126
|
+
} catch {
|
|
3127
|
+
return null;
|
|
3128
|
+
}
|
|
3129
|
+
try {
|
|
3130
|
+
unlinkSync(`${claimPath}.abandoned.${String(process.pid)}`);
|
|
3079
3131
|
} catch {
|
|
3080
3132
|
}
|
|
3081
3133
|
try {
|
|
@@ -3088,10 +3140,28 @@ function acquireClaim(path) {
|
|
|
3088
3140
|
return null;
|
|
3089
3141
|
}
|
|
3090
3142
|
}
|
|
3143
|
+
function claimIsAbandoned(claimPath) {
|
|
3144
|
+
let at = Number.NaN;
|
|
3145
|
+
try {
|
|
3146
|
+
const raw = JSON.parse(readFileSync3(claimPath, "utf8"));
|
|
3147
|
+
if (raw.at !== void 0) at = Date.parse(raw.at);
|
|
3148
|
+
} catch {
|
|
3149
|
+
}
|
|
3150
|
+
if (Number.isNaN(at)) {
|
|
3151
|
+
try {
|
|
3152
|
+
at = statSync(claimPath).mtimeMs;
|
|
3153
|
+
} catch {
|
|
3154
|
+
return true;
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
return Date.now() - at >= CLAIM_STALE_MS;
|
|
3158
|
+
}
|
|
3091
3159
|
function reclaim(path, command, token, expected) {
|
|
3092
3160
|
const claimPath = acquireClaim(path);
|
|
3093
3161
|
if (claimPath === null) {
|
|
3094
|
-
|
|
3162
|
+
const owner = readOwner(path);
|
|
3163
|
+
const ownerDead = owner !== null && owner.host === hostname() && !isAlive(owner.pid);
|
|
3164
|
+
return { ok: false, heldBy: owner ?? UNKNOWN_OWNER, suspect: ownerDead };
|
|
3095
3165
|
}
|
|
3096
3166
|
try {
|
|
3097
3167
|
if (!claimStaleLock(path, token, expected)) {
|
|
@@ -3099,10 +3169,9 @@ function reclaim(path, command, token, expected) {
|
|
|
3099
3169
|
}
|
|
3100
3170
|
const claimed = `${path}.stale.${token}`;
|
|
3101
3171
|
try {
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
if (err.code !== "EEXIST") throw err;
|
|
3172
|
+
if (publishLock(path, command, token)) {
|
|
3173
|
+
return { ok: true, handle: makeHandle(path, token) };
|
|
3174
|
+
}
|
|
3106
3175
|
return { ok: false, heldBy: readOwner(path) ?? UNKNOWN_OWNER, suspect: false };
|
|
3107
3176
|
} finally {
|
|
3108
3177
|
try {
|
|
@@ -3224,6 +3293,7 @@ function resolveSettings(options) {
|
|
|
3224
3293
|
}
|
|
3225
3294
|
return {
|
|
3226
3295
|
baseBranch: doWork.baseBranch ?? DEFAULT_DO_WORK.baseBranch,
|
|
3296
|
+
protectedBranches: doWork.protectedBranches ?? DEFAULT_DO_WORK.protectedBranches,
|
|
3227
3297
|
executor,
|
|
3228
3298
|
// --model wins; otherwise take the default for the executor in use.
|
|
3229
3299
|
model: options.model ?? doWork.models?.[executor],
|
|
@@ -3331,6 +3401,12 @@ function validateDoWorkConfig(section) {
|
|
|
3331
3401
|
validateOptionalString(section, "baseBranch", "doWork.baseBranch");
|
|
3332
3402
|
validateOptionalInt(section, "maxRunsPerTick", "doWork.maxRunsPerTick", 0, "a non-negative integer (0 = unlimited)");
|
|
3333
3403
|
validateOptionalInt(section, "lockStaleMinutes", "doWork.lockStaleMinutes", 1, "a positive integer");
|
|
3404
|
+
const protectedBranches = section["protectedBranches"];
|
|
3405
|
+
if (protectedBranches !== void 0 && protectedBranches !== null) {
|
|
3406
|
+
if (!Array.isArray(protectedBranches) || protectedBranches.some((b) => typeof b !== "string" || b.trim().length === 0)) {
|
|
3407
|
+
fail("doWork.protectedBranches must be an array of non-empty strings.");
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3334
3410
|
for (const container of ["models", "prompts"]) {
|
|
3335
3411
|
const value = section[container];
|
|
3336
3412
|
if (value === void 0 || value === null) continue;
|
|
@@ -3414,16 +3490,13 @@ function claimIssue(item, settings) {
|
|
|
3414
3490
|
}
|
|
3415
3491
|
}
|
|
3416
3492
|
function notePickupOnIssue(item, marker) {
|
|
3417
|
-
if (item.turn !== "pr-work" || item.pr === null || !item.issueAnalysis.hasNewMessage) return
|
|
3493
|
+
if (item.turn !== "pr-work" || item.pr === null || !item.issueAnalysis.hasNewMessage) return null;
|
|
3418
3494
|
try {
|
|
3419
|
-
postMarker(
|
|
3495
|
+
return postMarker(
|
|
3420
3496
|
"issue",
|
|
3421
3497
|
item.issue.number,
|
|
3422
3498
|
`automata do-work: picked this up on pull request #${String(item.pr.number)} \u2014 ${item.pr.url}`
|
|
3423
3499
|
);
|
|
3424
|
-
progress(` noted on issue #${String(item.issue.number)} that the work is on pull request #${String(item.pr.number)}.
|
|
3425
|
-
`);
|
|
3426
|
-
return true;
|
|
3427
3500
|
} catch (err) {
|
|
3428
3501
|
progress(` skipped: could not note the pickup on issue #${String(item.issue.number)} \u2014 ${err.message}
|
|
3429
3502
|
`);
|
|
@@ -3436,14 +3509,38 @@ function notePickupOnIssue(item, marker) {
|
|
|
3436
3509
|
return false;
|
|
3437
3510
|
}
|
|
3438
3511
|
}
|
|
3512
|
+
function reportIssueMessagesBuriedByNote(item, note, participants) {
|
|
3513
|
+
const watermark = promptWatermark([item.issueAnalysis.messages]);
|
|
3514
|
+
let buried;
|
|
3515
|
+
try {
|
|
3516
|
+
const messages = getIssueSurface(item.issue.number).messages;
|
|
3517
|
+
buried = messagesBetween(messages, participants, watermark, note.createdAt);
|
|
3518
|
+
} catch (err) {
|
|
3519
|
+
progress(` warning: could not re-read issue #${String(item.issue.number)}: ${err.message}
|
|
3520
|
+
`);
|
|
3521
|
+
return 0;
|
|
3522
|
+
}
|
|
3523
|
+
if (buried.length === 0) return 0;
|
|
3524
|
+
const authors = [...new Set(buried.map((message) => message.author))].join(", ");
|
|
3525
|
+
try {
|
|
3526
|
+
postMarker(
|
|
3527
|
+
"issue",
|
|
3528
|
+
item.issue.number,
|
|
3529
|
+
`automata do-work: ${authors} posted here while this issue was being picked up, so ${buried.length === 1 ? "that message was" : "those messages were"} not included in the run. Please post again to have them acted on.`
|
|
3530
|
+
);
|
|
3531
|
+
} catch (err) {
|
|
3532
|
+
progress(` warning: could not report the buried issue message(s): ${err.message}
|
|
3533
|
+
`);
|
|
3534
|
+
}
|
|
3535
|
+
return buried.length;
|
|
3536
|
+
}
|
|
3439
3537
|
function refreshItem(item, settings) {
|
|
3440
3538
|
const linkMap = getOpenPrLinkMap();
|
|
3441
|
-
return decideWork(
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
settings.
|
|
3445
|
-
|
|
3446
|
-
);
|
|
3539
|
+
return decideWork(buildIssueState(item.issue, linkMap), settings.participants, {
|
|
3540
|
+
baseBranch: settings.baseBranch,
|
|
3541
|
+
defaultBranch: linkMap.defaultBranch,
|
|
3542
|
+
protectedBranches: settings.protectedBranches
|
|
3543
|
+
});
|
|
3447
3544
|
}
|
|
3448
3545
|
function readAnsweringSurface(item) {
|
|
3449
3546
|
if (item.turn === "issue-discuss" || item.pr === null) {
|
|
@@ -3496,10 +3593,11 @@ function reconcileMarker(item, marker, participants, watermark, runError) {
|
|
|
3496
3593
|
if (analysis.missed.length > 0) {
|
|
3497
3594
|
return {
|
|
3498
3595
|
outcome: "answered-no-reply",
|
|
3499
|
-
detail: `answered (${String(analysis.toReport.length)} message(s) arrived mid-run and were flagged)
|
|
3596
|
+
detail: `answered (${String(analysis.toReport.length)} message(s) arrived mid-run and were flagged)`,
|
|
3597
|
+
reason: "flagged"
|
|
3500
3598
|
};
|
|
3501
3599
|
}
|
|
3502
|
-
return runError === null ? { outcome: "answered", detail: "answered" } : { outcome: "answered", detail: `answered, but the run reported: ${runError.message}
|
|
3600
|
+
return runError === null ? { outcome: "answered", detail: "answered", reason: "answered" } : { outcome: "answered", detail: `answered, but the run reported: ${runError.message}`, reason: "answered" };
|
|
3503
3601
|
}
|
|
3504
3602
|
return reportNoAnswer(item, marker, runError);
|
|
3505
3603
|
}
|
|
@@ -3514,7 +3612,11 @@ function reportUnverified(item, marker) {
|
|
|
3514
3612
|
progress(` warning: could not update the marker comment: ${err.message}
|
|
3515
3613
|
`);
|
|
3516
3614
|
}
|
|
3517
|
-
return {
|
|
3615
|
+
return {
|
|
3616
|
+
outcome: "answered-no-reply",
|
|
3617
|
+
detail: "could not verify whether an answer was posted",
|
|
3618
|
+
reason: "unverified"
|
|
3619
|
+
};
|
|
3518
3620
|
}
|
|
3519
3621
|
function reportNoAnswer(item, marker, runError) {
|
|
3520
3622
|
const surface = markerSurfaceLabel(item);
|
|
@@ -3529,7 +3631,7 @@ function reportNoAnswer(item, marker, runError) {
|
|
|
3529
3631
|
`
|
|
3530
3632
|
);
|
|
3531
3633
|
}
|
|
3532
|
-
return runError === null ? { outcome: "answered-no-reply", detail: "run finished but posted no answer" } : { outcome: "failed", detail: `run failed: ${runError.message}
|
|
3634
|
+
return runError === null ? { outcome: "answered-no-reply", detail: "run finished but posted no answer", reason: "no-answer" } : { outcome: "failed", detail: `run failed: ${runError.message}`, reason: "no-answer" };
|
|
3533
3635
|
}
|
|
3534
3636
|
async function invokeExecutor(prompt, settings, silent) {
|
|
3535
3637
|
if (settings.executor === "codex") {
|
|
@@ -3611,13 +3713,21 @@ async function processItem(planned, settings, silent) {
|
|
|
3611
3713
|
return { ...base, outcome: "skipped", detail: `marker failed: ${err.message}` };
|
|
3612
3714
|
}
|
|
3613
3715
|
inFlightMarker = { marker, item };
|
|
3614
|
-
|
|
3716
|
+
const note = notePickupOnIssue(item, marker);
|
|
3717
|
+
if (note === false) {
|
|
3615
3718
|
inFlightMarker = null;
|
|
3616
3719
|
return { ...base, outcome: "skipped", detail: "issue pickup note failed" };
|
|
3617
3720
|
}
|
|
3721
|
+
let buriedByNote = 0;
|
|
3722
|
+
if (note !== null) {
|
|
3723
|
+
progress(` noted on issue #${String(item.issue.number)} that the work is on pull request #${String(item.pr?.number ?? 0)}.
|
|
3724
|
+
`);
|
|
3725
|
+
buriedByNote = reportIssueMessagesBuriedByNote(item, note, settings.participants);
|
|
3726
|
+
}
|
|
3618
3727
|
const watermark = promptWatermark([
|
|
3619
|
-
item.issueAnalysis,
|
|
3620
|
-
|
|
3728
|
+
item.issueAnalysis.messages,
|
|
3729
|
+
item.prAnalysis?.messages ?? [],
|
|
3730
|
+
item.actionableThreads.flatMap((thread) => thread.comments)
|
|
3621
3731
|
]);
|
|
3622
3732
|
const prompt = composePrompt({
|
|
3623
3733
|
item,
|
|
@@ -3656,10 +3766,21 @@ async function processItem(planned, settings, silent) {
|
|
|
3656
3766
|
progress(` ${reconciled.detail}
|
|
3657
3767
|
`);
|
|
3658
3768
|
let outcome = reconciled;
|
|
3769
|
+
if (buriedByNote > 0 && outcome.outcome === "answered") {
|
|
3770
|
+
outcome = {
|
|
3771
|
+
outcome: "answered-no-reply",
|
|
3772
|
+
detail: `${outcome.detail} (${String(buriedByNote)} issue message(s) were buried by the pickup note and flagged)`,
|
|
3773
|
+
reason: "flagged"
|
|
3774
|
+
};
|
|
3775
|
+
}
|
|
3659
3776
|
if (item.turn === "issue-discuss") {
|
|
3660
3777
|
const linked = repairIssueLink(item, settings.baseBranch);
|
|
3661
|
-
if (linked && outcome.
|
|
3662
|
-
outcome = {
|
|
3778
|
+
if (linked && outcome.reason === "no-answer") {
|
|
3779
|
+
outcome = {
|
|
3780
|
+
outcome: "answered",
|
|
3781
|
+
detail: "opened a pull request (no issue comment)",
|
|
3782
|
+
reason: "answered"
|
|
3783
|
+
};
|
|
3663
3784
|
progress(" a pull request was opened, so the turn is counted as answered.\n");
|
|
3664
3785
|
}
|
|
3665
3786
|
}
|
|
@@ -3761,12 +3882,11 @@ async function runTick(settings, options) {
|
|
|
3761
3882
|
const issues = discoverIssues(settings);
|
|
3762
3883
|
const linkMap = getOpenPrLinkMap();
|
|
3763
3884
|
const decisions = issues.map(
|
|
3764
|
-
(issue) => decideWork(
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
settings.
|
|
3768
|
-
|
|
3769
|
-
)
|
|
3885
|
+
(issue) => decideWork(buildIssueState(issue, linkMap), settings.participants, {
|
|
3886
|
+
baseBranch: settings.baseBranch,
|
|
3887
|
+
defaultBranch: linkMap.defaultBranch,
|
|
3888
|
+
protectedBranches: settings.protectedBranches
|
|
3889
|
+
})
|
|
3770
3890
|
);
|
|
3771
3891
|
const items = decisions.flatMap((decision) => decision.kind === "work" ? [decision.item] : []);
|
|
3772
3892
|
const planText = `Work plan (${String(items.length)} of ${String(issues.length)} issues need an answer):
|