automata-cli 0.1.0-feature-030-do-work.212 → 0.1.0-feature-030-do-work.216
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,11 +9,12 @@ 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
|
|
15
16
|
};
|
|
16
|
-
var DEFAULT_DO_WORK_ISSUE_DISCUSS_PROMPT = "You are the agent named in the context below, working on a GitHub issue together with the people allowed to instruct you. Answer the messages marked NEW; the earlier messages are context only.\n\nDo not modify, create or delete any file, and do not create a branch or a pull request, UNLESS a message marked NEW explicitly asks you to implement the work. If it does: create a branch off the base branch named below, implement the change following the project's existing conventions, run the tests and the linter, and open a pull request whose body contains `Closes #<issue number>`.\n\nOtherwise do not touch the code at all: reply on the issue with the specification, the plan, or the open questions you need answered. Keep the reply short and concrete,
|
|
17
|
+
var DEFAULT_DO_WORK_ISSUE_DISCUSS_PROMPT = "You are the agent named in the context below, working on a GitHub issue together with the people allowed to instruct you. Answer the messages marked NEW; the earlier messages are context only.\n\nDo not modify, create or delete any file, and do not create a branch or a pull request, UNLESS a message marked NEW explicitly asks you to implement the work. If it does: create a branch off the base branch named below, implement the change following the project's existing conventions, run the tests and the linter, and open a pull request whose body contains `Closes #<issue number>`.\n\nOtherwise do not touch the code at all: reply on the issue with the specification, the plan, or the open questions you need answered. Keep the reply short and concrete.\n\nEither way, always post a reply on the issue before you finish \u2014 including when you implemented and opened a pull request. Silence is indistinguishable from a crash, and the run will be reported as having produced no answer.";
|
|
17
18
|
var DEFAULT_DO_WORK_PR_WORK_PROMPT = "You are the agent named in the context below, working on the pull request for a GitHub issue together with the people allowed to instruct you. Work on the branch named below, which is already checked out and up to date.\n\nAddress every message marked NEW and every unresolved review thread listed. Follow the project's existing conventions, run the tests and the linter, then commit and push to that branch. Do not merge the pull request and do not push to the base branch.\n\nReply on the pull request with a short summary of what you changed, or reply in the review thread when your answer belongs to a specific comment. Always post a reply \u2014 silence looks like a crash.";
|
|
18
19
|
var CONFIG_DIR = ".automata";
|
|
19
20
|
var CONFIG_FILE = "config.json";
|
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";
|
|
@@ -184,7 +184,7 @@ var configCommand = new Command("config").description("Configure automata settin
|
|
|
184
184
|
const [{ render }, React, { ConfigWizard }] = await Promise.all([
|
|
185
185
|
import("ink"),
|
|
186
186
|
import("react"),
|
|
187
|
-
import("./ConfigWizard-
|
|
187
|
+
import("./ConfigWizard-O25ZZT5R.js")
|
|
188
188
|
]);
|
|
189
189
|
const { waitUntilExit } = render(React.createElement(ConfigWizard));
|
|
190
190
|
await waitUntilExit();
|
|
@@ -2350,6 +2350,10 @@ function login(author) {
|
|
|
2350
2350
|
function byCreatedAt2(a, b) {
|
|
2351
2351
|
return a.createdAt.localeCompare(b.createdAt);
|
|
2352
2352
|
}
|
|
2353
|
+
function visibleAt(createdAt, submittedAt) {
|
|
2354
|
+
if (submittedAt === null || submittedAt === void 0) return createdAt;
|
|
2355
|
+
return submittedAt > createdAt ? submittedAt : createdAt;
|
|
2356
|
+
}
|
|
2353
2357
|
function getRepoSlug() {
|
|
2354
2358
|
const { stdout, status } = run4("git", ["remote", "get-url", "origin"]);
|
|
2355
2359
|
if (status !== 0) {
|
|
@@ -2428,6 +2432,7 @@ function getIssueSurface(issueNumber) {
|
|
|
2428
2432
|
var LINK_MAP_QUERY = `
|
|
2429
2433
|
query($owner:String!,$repo:String!,$cursor:String){
|
|
2430
2434
|
repository(owner:$owner,name:$repo){
|
|
2435
|
+
defaultBranchRef{ name }
|
|
2431
2436
|
pullRequests(states:OPEN, first:100, after:$cursor, orderBy:{field:UPDATED_AT, direction:DESC}){
|
|
2432
2437
|
pageInfo{ hasNextPage endCursor }
|
|
2433
2438
|
nodes{
|
|
@@ -2471,17 +2476,19 @@ function indexPullRequest(map, node, nameWithOwner) {
|
|
|
2471
2476
|
function getOpenPrLinkMap() {
|
|
2472
2477
|
const { owner, repo } = getRepoSlug();
|
|
2473
2478
|
const map = /* @__PURE__ */ new Map();
|
|
2479
|
+
let defaultBranch = null;
|
|
2474
2480
|
let cursor = null;
|
|
2475
2481
|
for (let page = 0; page < MAX_LINK_MAP_PAGES; page++) {
|
|
2476
2482
|
const args = ["api", "graphql", "-f", `query=${LINK_MAP_QUERY}`, "-f", `owner=${owner}`, "-f", `repo=${repo}`];
|
|
2477
2483
|
if (cursor !== null) args.push("-f", `cursor=${cursor}`);
|
|
2478
2484
|
const response = ghJson(args, "query open pull requests");
|
|
2485
|
+
defaultBranch = response.data.repository.defaultBranchRef?.name ?? defaultBranch;
|
|
2479
2486
|
const connection = response.data.repository.pullRequests;
|
|
2480
2487
|
for (const node of connection.nodes) {
|
|
2481
2488
|
indexPullRequest(map, node, `${owner}/${repo}`);
|
|
2482
2489
|
}
|
|
2483
2490
|
if (!connection.pageInfo?.hasNextPage || connection.pageInfo.endCursor === null) {
|
|
2484
|
-
return map;
|
|
2491
|
+
return { byIssue: map, defaultBranch };
|
|
2485
2492
|
}
|
|
2486
2493
|
cursor = connection.pageInfo.endCursor;
|
|
2487
2494
|
}
|
|
@@ -2499,7 +2506,7 @@ query($owner:String!,$repo:String!,$prNumber:Int!,$cursor:String){
|
|
|
2499
2506
|
isResolved isOutdated path line
|
|
2500
2507
|
comments(last:100){
|
|
2501
2508
|
pageInfo{ hasPreviousPage }
|
|
2502
|
-
nodes{ author{login} body createdAt }
|
|
2509
|
+
nodes{ author{login} body createdAt url pullRequestReview{ submittedAt } }
|
|
2503
2510
|
}
|
|
2504
2511
|
}
|
|
2505
2512
|
}
|
|
@@ -2545,11 +2552,19 @@ function getReviewThreads(prNumber) {
|
|
|
2545
2552
|
path: node.path,
|
|
2546
2553
|
line: node.line ?? null,
|
|
2547
2554
|
isResolved: node.isResolved,
|
|
2555
|
+
url: node.comments.nodes.at(-1)?.url ?? null,
|
|
2548
2556
|
comments: node.comments.nodes.map((comment) => ({
|
|
2549
2557
|
kind: "thread-comment",
|
|
2550
2558
|
author: login(comment.author),
|
|
2551
2559
|
body: comment.body,
|
|
2552
|
-
|
|
2560
|
+
// When the comment became *visible*, not when it was drafted.
|
|
2561
|
+
// GitHub stamps `createdAt` the moment a comment is added to a
|
|
2562
|
+
// pending review, and it only becomes visible when the review is
|
|
2563
|
+
// submitted — minutes later for a human working through a diff.
|
|
2564
|
+
// Using `createdAt` let an agent answer posted in between look newer
|
|
2565
|
+
// than the review, which marked every one of its threads answered
|
|
2566
|
+
// and discarded the whole review silently.
|
|
2567
|
+
createdAt: visibleAt(comment.createdAt, comment.pullRequestReview?.submittedAt)
|
|
2553
2568
|
})).sort(byCreatedAt2)
|
|
2554
2569
|
});
|
|
2555
2570
|
}
|
|
@@ -2667,12 +2682,16 @@ function isAssignedToAgent(assignees, agentUser) {
|
|
|
2667
2682
|
const agent = agentUser.toLowerCase();
|
|
2668
2683
|
return assignees.some((name) => name.toLowerCase() === agent);
|
|
2669
2684
|
}
|
|
2670
|
-
function findActionableThreads(threads, p) {
|
|
2685
|
+
function findActionableThreads(threads, p, prLastAgentAt) {
|
|
2671
2686
|
const actionable = [];
|
|
2672
2687
|
for (const thread of threads) {
|
|
2673
2688
|
if (thread.isResolved) continue;
|
|
2674
2689
|
const comments = thread.comments.filter((comment) => classifyForThread(comment.author, p) !== "other");
|
|
2675
2690
|
if (lastAuthorClass(comments, p) !== "authorized") continue;
|
|
2691
|
+
const newestAuthorized = comments.at(-1)?.createdAt;
|
|
2692
|
+
if (newestAuthorized !== void 0 && prLastAgentAt !== null && prLastAgentAt > newestAuthorized) {
|
|
2693
|
+
continue;
|
|
2694
|
+
}
|
|
2676
2695
|
actionable.push({ ...thread, comments });
|
|
2677
2696
|
}
|
|
2678
2697
|
return actionable;
|
|
@@ -2688,7 +2707,32 @@ function newestPr(prs) {
|
|
|
2688
2707
|
function plural(count, noun) {
|
|
2689
2708
|
return `${String(count)} ${noun}${count === 1 ? "" : "s"}`;
|
|
2690
2709
|
}
|
|
2691
|
-
function
|
|
2710
|
+
function protectedHeads(policy) {
|
|
2711
|
+
return [policy.baseBranch, policy.defaultBranch ?? "", ...policy.protectedBranches ?? []].filter(
|
|
2712
|
+
(branch) => branch.length > 0
|
|
2713
|
+
);
|
|
2714
|
+
}
|
|
2715
|
+
function unsafeBranchSkip(pr, issue, policy) {
|
|
2716
|
+
if (pr.isCrossRepository) {
|
|
2717
|
+
return {
|
|
2718
|
+
kind: "skip",
|
|
2719
|
+
issue,
|
|
2720
|
+
reason: "unsafe-pr-branch",
|
|
2721
|
+
detail: `pull request #${String(pr.number)} comes from a fork; its head branch is not in this repository`
|
|
2722
|
+
};
|
|
2723
|
+
}
|
|
2724
|
+
if (protectedHeads(policy).includes(pr.headRefName)) {
|
|
2725
|
+
return {
|
|
2726
|
+
kind: "skip",
|
|
2727
|
+
issue,
|
|
2728
|
+
reason: "unsafe-pr-branch",
|
|
2729
|
+
detail: `pull request #${String(pr.number)} has a protected branch (${pr.headRefName}) as its head, so a turn would have to push to it`
|
|
2730
|
+
};
|
|
2731
|
+
}
|
|
2732
|
+
return null;
|
|
2733
|
+
}
|
|
2734
|
+
function decideWork(state, p, policy) {
|
|
2735
|
+
const baseBranch = policy.baseBranch;
|
|
2692
2736
|
const { issueSurface, prSurface } = state;
|
|
2693
2737
|
const issue = issueSurface.issue;
|
|
2694
2738
|
if (issueSurface.state === "CLOSED") {
|
|
@@ -2724,25 +2768,11 @@ function decideWork(state, p, baseBranch) {
|
|
|
2724
2768
|
};
|
|
2725
2769
|
}
|
|
2726
2770
|
const surface = prSurface;
|
|
2727
|
-
|
|
2728
|
-
|
|
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
|
-
}
|
|
2771
|
+
const unsafe = unsafeBranchSkip(surface.pr, issue, policy);
|
|
2772
|
+
if (unsafe !== null) return unsafe;
|
|
2743
2773
|
const agentThreadMessages = surface.threads.flatMap((thread) => thread.comments).filter((comment) => comment.author.toLowerCase() === p.agentUser.toLowerCase());
|
|
2744
2774
|
const prAnalysis = analyzeSurface([...surface.messages, ...agentThreadMessages], p);
|
|
2745
|
-
const actionableThreads = findActionableThreads(surface.threads, p);
|
|
2775
|
+
const actionableThreads = findActionableThreads(surface.threads, p, prAnalysis.lastAgentAt);
|
|
2746
2776
|
const hasPrWork = prAnalysis.hasNewMessage || actionableThreads.length > 0;
|
|
2747
2777
|
if (!hasPrWork && !issueAnalysis.hasNewMessage) {
|
|
2748
2778
|
return {
|
|
@@ -2784,12 +2814,6 @@ function selectLinkedPr(linkedPrs) {
|
|
|
2784
2814
|
const open = linkedPrs.filter((pr) => pr.state === "OPEN");
|
|
2785
2815
|
return open.length === 0 ? null : newestPr(open);
|
|
2786
2816
|
}
|
|
2787
|
-
function agentAnsweredAfter(messages, agentUser, marker) {
|
|
2788
|
-
const agent = agentUser.toLowerCase();
|
|
2789
|
-
return messages.some(
|
|
2790
|
-
(message) => message.author.toLowerCase() === agent && message.kind !== "issue-body" && message.createdAt > marker.createdAt
|
|
2791
|
-
);
|
|
2792
|
-
}
|
|
2793
2817
|
|
|
2794
2818
|
// src/github/workPrompt.ts
|
|
2795
2819
|
function formatThreads(threads) {
|
|
@@ -2798,7 +2822,9 @@ function formatThreads(threads) {
|
|
|
2798
2822
|
const newest = thread.comments.at(-1);
|
|
2799
2823
|
const author = newest?.author ?? "unknown";
|
|
2800
2824
|
const body = newest?.body ?? "";
|
|
2801
|
-
|
|
2825
|
+
const link = thread.url === null ? "" : `
|
|
2826
|
+
${thread.url}`;
|
|
2827
|
+
return `[${author}] ${location}${link}
|
|
2802
2828
|
${body}`;
|
|
2803
2829
|
}).join("\n\n");
|
|
2804
2830
|
}
|
|
@@ -2861,8 +2887,44 @@ function composePrompt(input) {
|
|
|
2861
2887
|
return lines.join("\n");
|
|
2862
2888
|
}
|
|
2863
2889
|
|
|
2890
|
+
// src/github/markerReconciliation.ts
|
|
2891
|
+
function isAuthorized(author, p) {
|
|
2892
|
+
const login2 = author.toLowerCase();
|
|
2893
|
+
return login2 !== p.agentUser.toLowerCase() && p.allowedUsers.some((user) => user.toLowerCase() === login2);
|
|
2894
|
+
}
|
|
2895
|
+
function promptWatermark(messageSets) {
|
|
2896
|
+
let watermark = null;
|
|
2897
|
+
for (const messages of messageSets) {
|
|
2898
|
+
for (const message of messages) {
|
|
2899
|
+
if (watermark === null || message.createdAt > watermark) watermark = message.createdAt;
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
return watermark;
|
|
2903
|
+
}
|
|
2904
|
+
function messagesBetween(messages, p, watermark, until) {
|
|
2905
|
+
const since = watermark ?? "";
|
|
2906
|
+
return messages.filter(
|
|
2907
|
+
(message) => isAuthorized(message.author, p) && message.createdAt > since && message.createdAt <= until
|
|
2908
|
+
);
|
|
2909
|
+
}
|
|
2910
|
+
function analyseAnswer(messages, p, marker, watermark) {
|
|
2911
|
+
let answeredAt = null;
|
|
2912
|
+
for (const message of messages) {
|
|
2913
|
+
if (message.kind === "issue-body") continue;
|
|
2914
|
+
if (message.author.toLowerCase() !== p.agentUser.toLowerCase()) continue;
|
|
2915
|
+
if (message.createdAt <= marker.createdAt) continue;
|
|
2916
|
+
if (answeredAt === null || message.createdAt > answeredAt) answeredAt = message.createdAt;
|
|
2917
|
+
}
|
|
2918
|
+
const since = watermark ?? marker.createdAt;
|
|
2919
|
+
const toReport = messages.filter(
|
|
2920
|
+
(message) => isAuthorized(message.author, p) && message.createdAt > since
|
|
2921
|
+
);
|
|
2922
|
+
const missed = answeredAt === null ? [] : toReport.filter((message) => message.createdAt <= answeredAt);
|
|
2923
|
+
return { answeredAt, missed, toReport };
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2864
2926
|
// src/run/runLock.ts
|
|
2865
|
-
import { writeFileSync, readFileSync as readFileSync3, unlinkSync, mkdirSync, renameSync, linkSync } from "fs";
|
|
2927
|
+
import { writeFileSync, readFileSync as readFileSync3, unlinkSync, mkdirSync, renameSync, linkSync, statSync } from "fs";
|
|
2866
2928
|
import { randomUUID } from "crypto";
|
|
2867
2929
|
import { hostname } from "os";
|
|
2868
2930
|
import { join as join2 } from "path";
|
|
@@ -3022,20 +3084,81 @@ function claimStaleLock(path, token, expected) {
|
|
|
3022
3084
|
}
|
|
3023
3085
|
return true;
|
|
3024
3086
|
}
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
}
|
|
3029
|
-
const claimed = `${path}.stale.${token}`;
|
|
3087
|
+
var CLAIM_STALE_MS = 6e4;
|
|
3088
|
+
function acquireClaim(path) {
|
|
3089
|
+
const claimPath = `${path}.claim`;
|
|
3030
3090
|
try {
|
|
3031
|
-
|
|
3032
|
-
|
|
3091
|
+
writeFileSync(claimPath, JSON.stringify({ pid: process.pid, at: (/* @__PURE__ */ new Date()).toISOString() }), {
|
|
3092
|
+
encoding: "utf8",
|
|
3093
|
+
flag: "wx"
|
|
3094
|
+
});
|
|
3095
|
+
return claimPath;
|
|
3033
3096
|
} catch (err) {
|
|
3034
3097
|
if (err.code !== "EEXIST") throw err;
|
|
3035
|
-
|
|
3098
|
+
}
|
|
3099
|
+
if (!claimIsAbandoned(claimPath)) return null;
|
|
3100
|
+
try {
|
|
3101
|
+
renameSync(claimPath, `${claimPath}.abandoned.${String(process.pid)}`);
|
|
3102
|
+
} catch {
|
|
3103
|
+
return null;
|
|
3104
|
+
}
|
|
3105
|
+
try {
|
|
3106
|
+
unlinkSync(`${claimPath}.abandoned.${String(process.pid)}`);
|
|
3107
|
+
} catch {
|
|
3108
|
+
}
|
|
3109
|
+
try {
|
|
3110
|
+
writeFileSync(claimPath, JSON.stringify({ pid: process.pid, at: (/* @__PURE__ */ new Date()).toISOString() }), {
|
|
3111
|
+
encoding: "utf8",
|
|
3112
|
+
flag: "wx"
|
|
3113
|
+
});
|
|
3114
|
+
return claimPath;
|
|
3115
|
+
} catch {
|
|
3116
|
+
return null;
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
function claimIsAbandoned(claimPath) {
|
|
3120
|
+
let at = Number.NaN;
|
|
3121
|
+
try {
|
|
3122
|
+
const raw = JSON.parse(readFileSync3(claimPath, "utf8"));
|
|
3123
|
+
if (raw.at !== void 0) at = Date.parse(raw.at);
|
|
3124
|
+
} catch {
|
|
3125
|
+
}
|
|
3126
|
+
if (Number.isNaN(at)) {
|
|
3127
|
+
try {
|
|
3128
|
+
at = statSync(claimPath).mtimeMs;
|
|
3129
|
+
} catch {
|
|
3130
|
+
return true;
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
return Date.now() - at >= CLAIM_STALE_MS;
|
|
3134
|
+
}
|
|
3135
|
+
function reclaim(path, command, token, expected) {
|
|
3136
|
+
const claimPath = acquireClaim(path);
|
|
3137
|
+
if (claimPath === null) {
|
|
3138
|
+
const owner = readOwner(path);
|
|
3139
|
+
const ownerDead = owner !== null && owner.host === hostname() && !isAlive(owner.pid);
|
|
3140
|
+
return { ok: false, heldBy: owner ?? UNKNOWN_OWNER, suspect: ownerDead };
|
|
3141
|
+
}
|
|
3142
|
+
try {
|
|
3143
|
+
if (!claimStaleLock(path, token, expected)) {
|
|
3144
|
+
return { ok: false, heldBy: readOwner(path) ?? UNKNOWN_OWNER, suspect: false };
|
|
3145
|
+
}
|
|
3146
|
+
const claimed = `${path}.stale.${token}`;
|
|
3147
|
+
try {
|
|
3148
|
+
write(path, command, token, "wx");
|
|
3149
|
+
return { ok: true, handle: makeHandle(path, token) };
|
|
3150
|
+
} catch (err) {
|
|
3151
|
+
if (err.code !== "EEXIST") throw err;
|
|
3152
|
+
return { ok: false, heldBy: readOwner(path) ?? UNKNOWN_OWNER, suspect: false };
|
|
3153
|
+
} finally {
|
|
3154
|
+
try {
|
|
3155
|
+
unlinkSync(claimed);
|
|
3156
|
+
} catch {
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3036
3159
|
} finally {
|
|
3037
3160
|
try {
|
|
3038
|
-
unlinkSync(
|
|
3161
|
+
unlinkSync(claimPath);
|
|
3039
3162
|
} catch {
|
|
3040
3163
|
}
|
|
3041
3164
|
}
|
|
@@ -3147,6 +3270,7 @@ function resolveSettings(options) {
|
|
|
3147
3270
|
}
|
|
3148
3271
|
return {
|
|
3149
3272
|
baseBranch: doWork.baseBranch ?? DEFAULT_DO_WORK.baseBranch,
|
|
3273
|
+
protectedBranches: doWork.protectedBranches ?? DEFAULT_DO_WORK.protectedBranches,
|
|
3150
3274
|
executor,
|
|
3151
3275
|
// --model wins; otherwise take the default for the executor in use.
|
|
3152
3276
|
model: options.model ?? doWork.models?.[executor],
|
|
@@ -3254,6 +3378,12 @@ function validateDoWorkConfig(section) {
|
|
|
3254
3378
|
validateOptionalString(section, "baseBranch", "doWork.baseBranch");
|
|
3255
3379
|
validateOptionalInt(section, "maxRunsPerTick", "doWork.maxRunsPerTick", 0, "a non-negative integer (0 = unlimited)");
|
|
3256
3380
|
validateOptionalInt(section, "lockStaleMinutes", "doWork.lockStaleMinutes", 1, "a positive integer");
|
|
3381
|
+
const protectedBranches = section["protectedBranches"];
|
|
3382
|
+
if (protectedBranches !== void 0 && protectedBranches !== null) {
|
|
3383
|
+
if (!Array.isArray(protectedBranches) || protectedBranches.some((b) => typeof b !== "string" || b.trim().length === 0)) {
|
|
3384
|
+
fail("doWork.protectedBranches must be an array of non-empty strings.");
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3257
3387
|
for (const container of ["models", "prompts"]) {
|
|
3258
3388
|
const value = section[container];
|
|
3259
3389
|
if (value === void 0 || value === null) continue;
|
|
@@ -3306,7 +3436,7 @@ function issueMatchesFilter(surface, settings) {
|
|
|
3306
3436
|
}
|
|
3307
3437
|
function buildIssueState(issue, linkMap) {
|
|
3308
3438
|
const issueSurface = getIssueSurface(issue.number);
|
|
3309
|
-
const linkedPrs = linkMap.get(issue.number) ?? [];
|
|
3439
|
+
const linkedPrs = linkMap.byIssue.get(issue.number) ?? [];
|
|
3310
3440
|
const selected = selectLinkedPr(linkedPrs);
|
|
3311
3441
|
return {
|
|
3312
3442
|
issueSurface,
|
|
@@ -3337,16 +3467,13 @@ function claimIssue(item, settings) {
|
|
|
3337
3467
|
}
|
|
3338
3468
|
}
|
|
3339
3469
|
function notePickupOnIssue(item, marker) {
|
|
3340
|
-
if (item.turn !== "pr-work" || item.pr === null || !item.issueAnalysis.hasNewMessage) return
|
|
3470
|
+
if (item.turn !== "pr-work" || item.pr === null || !item.issueAnalysis.hasNewMessage) return null;
|
|
3341
3471
|
try {
|
|
3342
|
-
postMarker(
|
|
3472
|
+
return postMarker(
|
|
3343
3473
|
"issue",
|
|
3344
3474
|
item.issue.number,
|
|
3345
3475
|
`automata do-work: picked this up on pull request #${String(item.pr.number)} \u2014 ${item.pr.url}`
|
|
3346
3476
|
);
|
|
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
3477
|
} catch (err) {
|
|
3351
3478
|
progress(` skipped: could not note the pickup on issue #${String(item.issue.number)} \u2014 ${err.message}
|
|
3352
3479
|
`);
|
|
@@ -3359,20 +3486,37 @@ function notePickupOnIssue(item, marker) {
|
|
|
3359
3486
|
return false;
|
|
3360
3487
|
}
|
|
3361
3488
|
}
|
|
3489
|
+
function reportIssueMessagesBuriedByNote(item, note, participants) {
|
|
3490
|
+
const watermark = promptWatermark([item.issueAnalysis.messages]);
|
|
3491
|
+
let buried;
|
|
3492
|
+
try {
|
|
3493
|
+
const messages = getIssueSurface(item.issue.number).messages;
|
|
3494
|
+
buried = messagesBetween(messages, participants, watermark, note.createdAt);
|
|
3495
|
+
} catch (err) {
|
|
3496
|
+
progress(` warning: could not re-read issue #${String(item.issue.number)}: ${err.message}
|
|
3497
|
+
`);
|
|
3498
|
+
return 0;
|
|
3499
|
+
}
|
|
3500
|
+
if (buried.length === 0) return 0;
|
|
3501
|
+
const authors = [...new Set(buried.map((message) => message.author))].join(", ");
|
|
3502
|
+
try {
|
|
3503
|
+
postMarker(
|
|
3504
|
+
"issue",
|
|
3505
|
+
item.issue.number,
|
|
3506
|
+
`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.`
|
|
3507
|
+
);
|
|
3508
|
+
} catch (err) {
|
|
3509
|
+
progress(` warning: could not report the buried issue message(s): ${err.message}
|
|
3510
|
+
`);
|
|
3511
|
+
}
|
|
3512
|
+
return buried.length;
|
|
3513
|
+
}
|
|
3362
3514
|
function refreshItem(item, settings) {
|
|
3363
3515
|
const linkMap = getOpenPrLinkMap();
|
|
3364
|
-
return decideWork(
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
settings.
|
|
3368
|
-
);
|
|
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;
|
|
3516
|
+
return decideWork(buildIssueState(item.issue, linkMap), settings.participants, {
|
|
3517
|
+
baseBranch: settings.baseBranch,
|
|
3518
|
+
defaultBranch: linkMap.defaultBranch,
|
|
3519
|
+
protectedBranches: settings.protectedBranches
|
|
3376
3520
|
});
|
|
3377
3521
|
}
|
|
3378
3522
|
function readAnsweringSurface(item) {
|
|
@@ -3385,62 +3529,76 @@ function readAnsweringSurface(item) {
|
|
|
3385
3529
|
function markerSurfaceLabel(item) {
|
|
3386
3530
|
return item.turn === "pr-work" && item.pr ? `pull request #${String(item.pr.number)}` : `issue #${String(item.issue.number)}`;
|
|
3387
3531
|
}
|
|
3388
|
-
function
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3532
|
+
function markerSurfaceTarget(item) {
|
|
3533
|
+
return item.turn === "pr-work" && item.pr ? { surface: "pr", number: item.pr.number } : { surface: "issue", number: item.issue.number };
|
|
3534
|
+
}
|
|
3535
|
+
function reportOvertakenMessages(item, analysis) {
|
|
3536
|
+
if (analysis.missed.length === 0) return;
|
|
3537
|
+
const authors = [...new Set(analysis.toReport.map((message) => message.author))].join(", ");
|
|
3538
|
+
const count = analysis.toReport.length;
|
|
3539
|
+
const target = markerSurfaceTarget(item);
|
|
3392
3540
|
try {
|
|
3393
|
-
|
|
3394
|
-
|
|
3541
|
+
postMarker(
|
|
3542
|
+
target.surface,
|
|
3543
|
+
target.number,
|
|
3544
|
+
`automata do-work: ${authors} posted here while this run was already in progress, so ${count === 1 ? "that message was" : "those messages were"} not included in it. Please post again to have them acted on.`
|
|
3545
|
+
);
|
|
3546
|
+
} catch (err) {
|
|
3547
|
+
progress(` warning: could not report the overtaken message(s): ${err.message}
|
|
3548
|
+
`);
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
function reconcileMarker(item, marker, participants, watermark, runError) {
|
|
3552
|
+
let analysis;
|
|
3553
|
+
try {
|
|
3554
|
+
analysis = analyseAnswer(readAnsweringSurface(item), participants, marker, watermark);
|
|
3395
3555
|
} catch (err) {
|
|
3396
3556
|
progress(
|
|
3397
3557
|
` warning: could not re-read issue #${String(item.issue.number)} to check for an answer: ${err.message}
|
|
3398
3558
|
`
|
|
3399
3559
|
);
|
|
3400
|
-
|
|
3401
|
-
}
|
|
3402
|
-
if (answered === "unknown") {
|
|
3403
|
-
const surface2 = markerSurfaceLabel(item);
|
|
3404
|
-
try {
|
|
3405
|
-
updateMarker(
|
|
3406
|
-
marker,
|
|
3407
|
-
`automata do-work: the agent run finished, but automata could not read ${surface2} afterwards to confirm whether an answer was posted. Check this thread and the branch before assuming either. Reply here to have another attempt made.`
|
|
3408
|
-
);
|
|
3409
|
-
} catch (updateErr) {
|
|
3410
|
-
progress(` warning: could not update the marker comment: ${updateErr.message}
|
|
3411
|
-
`);
|
|
3412
|
-
}
|
|
3413
|
-
return { outcome: "answered-no-reply", detail: "could not verify whether an answer was posted" };
|
|
3560
|
+
return reportUnverified(item, marker);
|
|
3414
3561
|
}
|
|
3415
|
-
if (
|
|
3416
|
-
|
|
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
|
-
}
|
|
3562
|
+
if (analysis.answeredAt !== null) {
|
|
3563
|
+
reportOvertakenMessages(item, analysis);
|
|
3430
3564
|
try {
|
|
3431
3565
|
deleteMarker(marker);
|
|
3432
3566
|
} catch (err) {
|
|
3433
3567
|
progress(` warning: could not delete the marker comment: ${err.message}
|
|
3434
3568
|
`);
|
|
3435
3569
|
}
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3570
|
+
if (analysis.missed.length > 0) {
|
|
3571
|
+
return {
|
|
3572
|
+
outcome: "answered-no-reply",
|
|
3573
|
+
detail: `answered (${String(analysis.toReport.length)} message(s) arrived mid-run and were flagged)`,
|
|
3574
|
+
reason: "flagged"
|
|
3575
|
+
};
|
|
3439
3576
|
}
|
|
3440
|
-
return runError === null ? { outcome: "answered", detail: "answered" } : { outcome: "answered", detail: `answered, but the run reported: ${runError.message}
|
|
3577
|
+
return runError === null ? { outcome: "answered", detail: "answered", reason: "answered" } : { outcome: "answered", detail: `answered, but the run reported: ${runError.message}`, reason: "answered" };
|
|
3441
3578
|
}
|
|
3579
|
+
return reportNoAnswer(item, marker, runError);
|
|
3580
|
+
}
|
|
3581
|
+
function reportUnverified(item, marker) {
|
|
3442
3582
|
const surface = markerSurfaceLabel(item);
|
|
3443
|
-
|
|
3583
|
+
try {
|
|
3584
|
+
updateMarker(
|
|
3585
|
+
marker,
|
|
3586
|
+
`automata do-work: the agent run finished, but automata could not read ${surface} afterwards to confirm whether an answer was posted. Check this thread and the branch before assuming either. Reply here to have another attempt made.`
|
|
3587
|
+
);
|
|
3588
|
+
} catch (err) {
|
|
3589
|
+
progress(` warning: could not update the marker comment: ${err.message}
|
|
3590
|
+
`);
|
|
3591
|
+
}
|
|
3592
|
+
return {
|
|
3593
|
+
outcome: "answered-no-reply",
|
|
3594
|
+
detail: "could not verify whether an answer was posted",
|
|
3595
|
+
reason: "unverified"
|
|
3596
|
+
};
|
|
3597
|
+
}
|
|
3598
|
+
function reportNoAnswer(item, marker, runError) {
|
|
3599
|
+
const surface = markerSurfaceLabel(item);
|
|
3600
|
+
const sideEffects = item.turn === "issue-discuss" ? "It may still have created a branch or opened a pull request \u2014 check before assuming otherwise." : `It may still have changed the branch \`${item.branch}\` \u2014 check it before assuming otherwise.`;
|
|
3601
|
+
const explanation = runError === null ? `automata do-work: the agent run finished without posting an answer on ${surface}. ${sideEffects} Reply on ${surface} to have another attempt made.` : `automata do-work: the agent run failed before posting an answer (${runError.message}). ${sideEffects} Reply on ${surface} to have another attempt made.`;
|
|
3444
3602
|
try {
|
|
3445
3603
|
updateMarker(marker, explanation);
|
|
3446
3604
|
} catch (err) {
|
|
@@ -3450,7 +3608,7 @@ function reconcileMarker(item, marker, participants, runError) {
|
|
|
3450
3608
|
`
|
|
3451
3609
|
);
|
|
3452
3610
|
}
|
|
3453
|
-
return runError === null ? { outcome: "answered-no-reply", detail: "run finished but posted no answer" } : { outcome: "failed", detail: `run failed: ${runError.message}
|
|
3611
|
+
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" };
|
|
3454
3612
|
}
|
|
3455
3613
|
async function invokeExecutor(prompt, settings, silent) {
|
|
3456
3614
|
if (settings.executor === "codex") {
|
|
@@ -3465,26 +3623,28 @@ function repairIssueLink(item, baseBranch) {
|
|
|
3465
3623
|
if (branch === baseBranch) {
|
|
3466
3624
|
progress(` issue #${String(item.issue.number)} is still in discussion (no branch was created).
|
|
3467
3625
|
`);
|
|
3468
|
-
return;
|
|
3626
|
+
return false;
|
|
3469
3627
|
}
|
|
3470
3628
|
const pr = getCurrentBranchPr();
|
|
3471
3629
|
if (!pr) {
|
|
3472
3630
|
progress(` issue #${String(item.issue.number)} is still in discussion (no pull request).
|
|
3473
3631
|
`);
|
|
3474
|
-
return;
|
|
3632
|
+
return false;
|
|
3475
3633
|
}
|
|
3476
3634
|
const closesRef = new RegExp(String.raw`\bcloses\s+#` + String(item.issue.number) + String.raw`\b`, "i");
|
|
3477
3635
|
if (closesRef.test(pr.body)) {
|
|
3478
3636
|
progress(` pull request #${String(pr.number)} already closes issue #${String(item.issue.number)}.
|
|
3479
3637
|
`);
|
|
3480
|
-
return;
|
|
3638
|
+
return true;
|
|
3481
3639
|
}
|
|
3482
3640
|
addClosesRefToPr(pr.number, item.issue.number);
|
|
3483
3641
|
progress(` linked pull request #${String(pr.number)} to issue #${String(item.issue.number)}.
|
|
3484
3642
|
`);
|
|
3643
|
+
return true;
|
|
3485
3644
|
} catch (err) {
|
|
3486
3645
|
progress(` warning: could not link a pull request to issue #${String(item.issue.number)}: ${err.message}
|
|
3487
3646
|
`);
|
|
3647
|
+
return false;
|
|
3488
3648
|
}
|
|
3489
3649
|
}
|
|
3490
3650
|
async function processItem(planned, settings, silent) {
|
|
@@ -3530,10 +3690,22 @@ async function processItem(planned, settings, silent) {
|
|
|
3530
3690
|
return { ...base, outcome: "skipped", detail: `marker failed: ${err.message}` };
|
|
3531
3691
|
}
|
|
3532
3692
|
inFlightMarker = { marker, item };
|
|
3533
|
-
|
|
3693
|
+
const note = notePickupOnIssue(item, marker);
|
|
3694
|
+
if (note === false) {
|
|
3534
3695
|
inFlightMarker = null;
|
|
3535
3696
|
return { ...base, outcome: "skipped", detail: "issue pickup note failed" };
|
|
3536
3697
|
}
|
|
3698
|
+
let buriedByNote = 0;
|
|
3699
|
+
if (note !== null) {
|
|
3700
|
+
progress(` noted on issue #${String(item.issue.number)} that the work is on pull request #${String(item.pr?.number ?? 0)}.
|
|
3701
|
+
`);
|
|
3702
|
+
buriedByNote = reportIssueMessagesBuriedByNote(item, note, settings.participants);
|
|
3703
|
+
}
|
|
3704
|
+
const watermark = promptWatermark([
|
|
3705
|
+
item.issueAnalysis.messages,
|
|
3706
|
+
item.prAnalysis?.messages ?? [],
|
|
3707
|
+
item.actionableThreads.flatMap((thread) => thread.comments)
|
|
3708
|
+
]);
|
|
3537
3709
|
const prompt = composePrompt({
|
|
3538
3710
|
item,
|
|
3539
3711
|
repo: getRepoSlug(),
|
|
@@ -3567,13 +3739,29 @@ async function processItem(planned, settings, silent) {
|
|
|
3567
3739
|
}
|
|
3568
3740
|
const ranExecutor = true;
|
|
3569
3741
|
inFlightMarker = null;
|
|
3570
|
-
const reconciled = reconcileMarker(item, marker, settings.participants, runError);
|
|
3742
|
+
const reconciled = reconcileMarker(item, marker, settings.participants, watermark, runError);
|
|
3571
3743
|
progress(` ${reconciled.detail}
|
|
3572
3744
|
`);
|
|
3745
|
+
let outcome = reconciled;
|
|
3746
|
+
if (buriedByNote > 0 && outcome.outcome === "answered") {
|
|
3747
|
+
outcome = {
|
|
3748
|
+
outcome: "answered-no-reply",
|
|
3749
|
+
detail: `${outcome.detail} (${String(buriedByNote)} issue message(s) were buried by the pickup note and flagged)`,
|
|
3750
|
+
reason: "flagged"
|
|
3751
|
+
};
|
|
3752
|
+
}
|
|
3573
3753
|
if (item.turn === "issue-discuss") {
|
|
3574
|
-
repairIssueLink(item, settings.baseBranch);
|
|
3754
|
+
const linked = repairIssueLink(item, settings.baseBranch);
|
|
3755
|
+
if (linked && outcome.reason === "no-answer") {
|
|
3756
|
+
outcome = {
|
|
3757
|
+
outcome: "answered",
|
|
3758
|
+
detail: "opened a pull request (no issue comment)",
|
|
3759
|
+
reason: "answered"
|
|
3760
|
+
};
|
|
3761
|
+
progress(" a pull request was opened, so the turn is counted as answered.\n");
|
|
3762
|
+
}
|
|
3575
3763
|
}
|
|
3576
|
-
return { ...base, outcome:
|
|
3764
|
+
return { ...base, outcome: outcome.outcome, detail: outcome.detail, ranExecutor };
|
|
3577
3765
|
}
|
|
3578
3766
|
function summarize(reports) {
|
|
3579
3767
|
out("\nTick summary:\n");
|
|
@@ -3670,7 +3858,13 @@ var doWorkCommand = new Command6("do-work").description(
|
|
|
3670
3858
|
async function runTick(settings, options) {
|
|
3671
3859
|
const issues = discoverIssues(settings);
|
|
3672
3860
|
const linkMap = getOpenPrLinkMap();
|
|
3673
|
-
const decisions = issues.map(
|
|
3861
|
+
const decisions = issues.map(
|
|
3862
|
+
(issue) => decideWork(buildIssueState(issue, linkMap), settings.participants, {
|
|
3863
|
+
baseBranch: settings.baseBranch,
|
|
3864
|
+
defaultBranch: linkMap.defaultBranch,
|
|
3865
|
+
protectedBranches: settings.protectedBranches
|
|
3866
|
+
})
|
|
3867
|
+
);
|
|
3674
3868
|
const items = decisions.flatMap((decision) => decision.kind === "work" ? [decision.item] : []);
|
|
3675
3869
|
const planText = `Work plan (${String(items.length)} of ${String(issues.length)} issues need an answer):
|
|
3676
3870
|
${describePlan(decisions)}`;
|