automata-cli 0.6.0-develop.293 → 0.6.0-develop.300
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/README.md
CHANGED
|
@@ -129,7 +129,7 @@ See [docs/execute-prompt.md](docs/execute-prompt.md) for full details.
|
|
|
129
129
|
|
|
130
130
|
## `automata do-work`
|
|
131
131
|
|
|
132
|
-
One tick of the autonomous loop: answer the open issues whose newest message from an authorized account the agent has not answered. Reference: [docs/do-work.md](docs/do-work.md). Process, trust model and setup: [the wiki](docs/wiki/Home.md).
|
|
132
|
+
One tick of the autonomous loop: answer the open issues whose newest message from an authorized account the agent has not answered, then do the same for the open pull requests that close no issue of this repository. Reference: [docs/do-work.md](docs/do-work.md). Process, trust model and setup: [the wiki](docs/wiki/Home.md).
|
|
133
133
|
|
|
134
134
|
---
|
|
135
135
|
|
|
@@ -3,13 +3,14 @@ import {
|
|
|
3
3
|
DEFAULT_CHECK_ISSUE_PROMPT,
|
|
4
4
|
DEFAULT_DO_WORK,
|
|
5
5
|
DEFAULT_DO_WORK_ISSUE_DISCUSS_PROMPT,
|
|
6
|
+
DEFAULT_DO_WORK_PR_ORPHAN_PROMPT,
|
|
6
7
|
DEFAULT_DO_WORK_PR_WORK_PROMPT,
|
|
7
8
|
DEFAULT_FIX_COMMENTS_PROMPT,
|
|
8
9
|
DEFAULT_SONAR_PROMPT,
|
|
9
10
|
readConfig,
|
|
10
11
|
readRawConfig,
|
|
11
12
|
writeConfig
|
|
12
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-MIPBYVK5.js";
|
|
13
14
|
|
|
14
15
|
// src/config/ConfigWizard.tsx
|
|
15
16
|
import { useState } from "react";
|
|
@@ -41,7 +42,8 @@ var PROMPTS_MENU_OPTIONS = [
|
|
|
41
42
|
"Fix-Comments",
|
|
42
43
|
"Check-Issue",
|
|
43
44
|
"Do Work \u2014 Discuss",
|
|
44
|
-
"Do Work \u2014 PR"
|
|
45
|
+
"Do Work \u2014 PR",
|
|
46
|
+
"Do Work \u2014 Orphan PR"
|
|
45
47
|
];
|
|
46
48
|
function parseAllowedUsers(value) {
|
|
47
49
|
return value.split(",").map((user) => user.trim()).filter((user) => user.length > 0);
|
|
@@ -121,7 +123,8 @@ var PROMPT_SCREEN_BY_OPTION = {
|
|
|
121
123
|
"Fix-Comments": "fix-comments-prompt",
|
|
122
124
|
"Check-Issue": "check-issue-prompt",
|
|
123
125
|
"Do Work \u2014 Discuss": "do-work-discuss-prompt",
|
|
124
|
-
"Do Work \u2014 PR": "do-work-pr-prompt"
|
|
126
|
+
"Do Work \u2014 PR": "do-work-pr-prompt",
|
|
127
|
+
"Do Work \u2014 Orphan PR": "do-work-pr-orphan-prompt"
|
|
125
128
|
};
|
|
126
129
|
function ConfigWizard() {
|
|
127
130
|
const existing = readConfig();
|
|
@@ -167,6 +170,9 @@ function ConfigWizard() {
|
|
|
167
170
|
const [doWorkPrPrompt, setDoWorkPrPrompt] = useState(
|
|
168
171
|
existing.doWork?.prompts?.prWork ?? DEFAULT_DO_WORK_PR_WORK_PROMPT
|
|
169
172
|
);
|
|
173
|
+
const [doWorkPrOrphanPrompt, setDoWorkPrOrphanPrompt] = useState(
|
|
174
|
+
existing.doWork?.prompts?.prOrphan ?? DEFAULT_DO_WORK_PR_ORPHAN_PROMPT
|
|
175
|
+
);
|
|
170
176
|
const [pendingRemote, setPendingRemote] = useState(existing.remoteType ?? "gh");
|
|
171
177
|
const [pendingTechnique, setPendingTechnique] = useState(
|
|
172
178
|
existing.issueDiscoveryTechnique ?? "label"
|
|
@@ -362,6 +368,17 @@ function ConfigWizard() {
|
|
|
362
368
|
setScreen("prompts-menu");
|
|
363
369
|
},
|
|
364
370
|
onBack: () => setScreen("prompts-menu")
|
|
371
|
+
},
|
|
372
|
+
"do-work-pr-orphan-prompt": {
|
|
373
|
+
setValue: setDoWorkPrOrphanPrompt,
|
|
374
|
+
onSubmit: () => {
|
|
375
|
+
savePrompt("do-work-pr-orphan.md", doWorkPrOrphanPrompt, (value, current) => ({
|
|
376
|
+
...current,
|
|
377
|
+
doWork: { ...current.doWork, prompts: { ...current.doWork?.prompts, prOrphan: value } }
|
|
378
|
+
}));
|
|
379
|
+
setScreen("prompts-menu");
|
|
380
|
+
},
|
|
381
|
+
onBack: () => setScreen("prompts-menu")
|
|
365
382
|
}
|
|
366
383
|
};
|
|
367
384
|
const menus = {
|
|
@@ -527,6 +544,12 @@ function ConfigWizard() {
|
|
|
527
544
|
label: "Pull request turn instructions:",
|
|
528
545
|
value: doWorkPrPrompt,
|
|
529
546
|
hint: `Type prompt \xB7 Enter to save \xB7 ${BACK}`
|
|
547
|
+
},
|
|
548
|
+
"do-work-pr-orphan-prompt": {
|
|
549
|
+
title: "Prompts \u2014 Do Work \u2014 Orphan PR",
|
|
550
|
+
label: "Instructions for a pull request with no linked issue:",
|
|
551
|
+
value: doWorkPrOrphanPrompt,
|
|
552
|
+
hint: `Type prompt \xB7 Enter to save \xB7 ${BACK}`
|
|
530
553
|
}
|
|
531
554
|
};
|
|
532
555
|
const menuViews = {
|
|
@@ -16,6 +16,7 @@ var DEFAULT_DO_WORK = {
|
|
|
16
16
|
};
|
|
17
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.";
|
|
18
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.";
|
|
19
|
+
var DEFAULT_DO_WORK_PR_ORPHAN_PROMPT = "You are the agent named in the context below, working on a pull request that is not linked to any issue \u2014 a dependency bump, or another change opened without one \u2014 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. Typical work here is finding out why the checks are failing, bringing the branch up to date with the base branch, and fixing whatever the change needs in order to be mergeable. Follow the project's existing conventions, run the tests and the linter, then commit and push to that branch.\n\nDo not merge the pull request, do not close it, and do not push to the base branch. If you conclude that it should be merged or closed, say so in your reply and leave the decision to the humans.\n\nReply on the pull request with a short summary of what you did and what you recommend, or reply in the review thread when your answer belongs to a specific comment. Always post a reply \u2014 silence looks like a crash.";
|
|
19
20
|
var CONFIG_DIR = ".automata";
|
|
20
21
|
var CONFIG_FILE = "config.json";
|
|
21
22
|
function configPath() {
|
|
@@ -81,6 +82,9 @@ function readConfig() {
|
|
|
81
82
|
if (config.doWork?.prompts?.prWork) {
|
|
82
83
|
config.doWork.prompts.prWork = resolvePromptRef(config.doWork.prompts.prWork, dir);
|
|
83
84
|
}
|
|
85
|
+
if (config.doWork?.prompts?.prOrphan) {
|
|
86
|
+
config.doWork.prompts.prOrphan = resolvePromptRef(config.doWork.prompts.prOrphan, dir);
|
|
87
|
+
}
|
|
84
88
|
return config;
|
|
85
89
|
}
|
|
86
90
|
function writeConfig(config) {
|
|
@@ -97,6 +101,7 @@ export {
|
|
|
97
101
|
DEFAULT_DO_WORK,
|
|
98
102
|
DEFAULT_DO_WORK_ISSUE_DISCUSS_PROMPT,
|
|
99
103
|
DEFAULT_DO_WORK_PR_WORK_PROMPT,
|
|
104
|
+
DEFAULT_DO_WORK_PR_ORPHAN_PROMPT,
|
|
100
105
|
readRawConfig,
|
|
101
106
|
readConfig,
|
|
102
107
|
writeConfig
|
package/dist/index.js
CHANGED
|
@@ -4,13 +4,14 @@ import {
|
|
|
4
4
|
DEFAULT_CLAUDE_SYSTEM_PROMPT,
|
|
5
5
|
DEFAULT_DO_WORK,
|
|
6
6
|
DEFAULT_DO_WORK_ISSUE_DISCUSS_PROMPT,
|
|
7
|
+
DEFAULT_DO_WORK_PR_ORPHAN_PROMPT,
|
|
7
8
|
DEFAULT_DO_WORK_PR_WORK_PROMPT,
|
|
8
9
|
DEFAULT_FIX_COMMENTS_PROMPT,
|
|
9
10
|
DEFAULT_SONAR_PROMPT,
|
|
10
11
|
readConfig,
|
|
11
12
|
readRawConfig,
|
|
12
13
|
writeConfig
|
|
13
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-MIPBYVK5.js";
|
|
14
15
|
|
|
15
16
|
// src/index.ts
|
|
16
17
|
import { Command as Command7 } from "commander";
|
|
@@ -28,7 +29,7 @@ import { Command } from "commander";
|
|
|
28
29
|
var VALID_TYPES = ["gh", "azdo"];
|
|
29
30
|
var VALID_TECHNIQUES = ["label", "assignee", "title-contains"];
|
|
30
31
|
var VALID_EXECUTORS = ["claude", "codex"];
|
|
31
|
-
var VALID_TURN_KINDS = ["issue-discuss", "pr-work"];
|
|
32
|
+
var VALID_TURN_KINDS = ["issue-discuss", "pr-work", "pr-orphan"];
|
|
32
33
|
function writeDoWork(patch) {
|
|
33
34
|
const current = readRawConfig();
|
|
34
35
|
writeConfig({ ...current, doWork: { ...current.doWork, ...patch } });
|
|
@@ -198,10 +199,16 @@ var configSetDoWorkPrompt = new Command("do-work-prompt").description("Set the t
|
|
|
198
199
|
}
|
|
199
200
|
const current = readRawConfig();
|
|
200
201
|
const prompts = { ...current.doWork?.prompts };
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
202
|
+
switch (turnKind) {
|
|
203
|
+
case "issue-discuss":
|
|
204
|
+
prompts.issueDiscuss = prompt;
|
|
205
|
+
break;
|
|
206
|
+
case "pr-work":
|
|
207
|
+
prompts.prWork = prompt;
|
|
208
|
+
break;
|
|
209
|
+
case "pr-orphan":
|
|
210
|
+
prompts.prOrphan = prompt;
|
|
211
|
+
break;
|
|
205
212
|
}
|
|
206
213
|
writeDoWork({ prompts });
|
|
207
214
|
process.stdout.write(`do-work ${turnKind} prompt set.
|
|
@@ -212,7 +219,7 @@ var configCommand = new Command("config").description("Configure automata settin
|
|
|
212
219
|
const [{ render }, React, { ConfigWizard }] = await Promise.all([
|
|
213
220
|
import("ink"),
|
|
214
221
|
import("react"),
|
|
215
|
-
import("./ConfigWizard-
|
|
222
|
+
import("./ConfigWizard-D6NYL2E6.js")
|
|
216
223
|
]);
|
|
217
224
|
const { waitUntilExit } = render(React.createElement(ConfigWizard));
|
|
218
225
|
await waitUntilExit();
|
|
@@ -766,6 +773,18 @@ function fetchBranch(branch) {
|
|
|
766
773
|
function pullFastForwardOnly(branch) {
|
|
767
774
|
return gitCommand(branch === void 0 ? ["pull", "--ff-only"] : ["pull", "--ff-only", "origin", branch]);
|
|
768
775
|
}
|
|
776
|
+
function revParse(ref) {
|
|
777
|
+
const { stdout, status } = run2("git", ["rev-parse", "--verify", "--quiet", ref]);
|
|
778
|
+
if (status !== 0) return null;
|
|
779
|
+
const sha = stdout.trim();
|
|
780
|
+
return sha.length === 0 ? null : sha;
|
|
781
|
+
}
|
|
782
|
+
function isAncestorCommit(maybeAncestor, descendant) {
|
|
783
|
+
return run2("git", ["merge-base", "--is-ancestor", maybeAncestor, descendant]).status === 0;
|
|
784
|
+
}
|
|
785
|
+
function resetHardTo(ref) {
|
|
786
|
+
return gitCommand(["reset", "--hard", ref]);
|
|
787
|
+
}
|
|
769
788
|
function fetchPrune() {
|
|
770
789
|
const result = run2("git", ["fetch", "--prune"]);
|
|
771
790
|
if (result.status !== 0) {
|
|
@@ -2567,6 +2586,8 @@ query($owner:String!,$repo:String!,$cursor:String){
|
|
|
2567
2586
|
pageInfo{ hasNextPage endCursor }
|
|
2568
2587
|
nodes{
|
|
2569
2588
|
number url title headRefName baseRefName isCrossRepository isDraft updatedAt
|
|
2589
|
+
labels(first:50){ nodes{ name } }
|
|
2590
|
+
assignees(first:50){ nodes{ login } }
|
|
2570
2591
|
closingIssuesReferences(first:50){
|
|
2571
2592
|
pageInfo{ hasNextPage }
|
|
2572
2593
|
nodes{ number repository{ nameWithOwner } }
|
|
@@ -2577,24 +2598,16 @@ query($owner:String!,$repo:String!,$cursor:String){
|
|
|
2577
2598
|
}`.trim();
|
|
2578
2599
|
var MAX_LINK_MAP_PAGES = 50;
|
|
2579
2600
|
function indexPullRequest(map, node, nameWithOwner) {
|
|
2580
|
-
const ref =
|
|
2581
|
-
number: node.number,
|
|
2582
|
-
url: node.url,
|
|
2583
|
-
title: node.title,
|
|
2584
|
-
headRefName: node.headRefName,
|
|
2585
|
-
baseRefName: node.baseRefName,
|
|
2586
|
-
isCrossRepository: node.isCrossRepository,
|
|
2587
|
-
state: "OPEN",
|
|
2588
|
-
isDraft: node.isDraft,
|
|
2589
|
-
updatedAt: node.updatedAt
|
|
2590
|
-
};
|
|
2601
|
+
const ref = toPullRequestRef(node);
|
|
2591
2602
|
if (node.closingIssuesReferences.pageInfo?.hasNextPage) {
|
|
2592
2603
|
throw new Error(
|
|
2593
2604
|
`Pull request #${String(node.number)} closes more than 50 issues, so the issue-to-pull-request map cannot be read completely. Refusing the tick rather than risk starting work on an issue that already has a pull request.`
|
|
2594
2605
|
);
|
|
2595
2606
|
}
|
|
2607
|
+
let closedAnyHere = false;
|
|
2596
2608
|
for (const issue of node.closingIssuesReferences.nodes) {
|
|
2597
|
-
if (issue.repository.nameWithOwner !== nameWithOwner) continue;
|
|
2609
|
+
if (issue.repository.nameWithOwner.toLowerCase() !== nameWithOwner.toLowerCase()) continue;
|
|
2610
|
+
closedAnyHere = true;
|
|
2598
2611
|
const existing = map.get(issue.number);
|
|
2599
2612
|
if (existing) {
|
|
2600
2613
|
existing.push(ref);
|
|
@@ -2602,10 +2615,32 @@ function indexPullRequest(map, node, nameWithOwner) {
|
|
|
2602
2615
|
map.set(issue.number, [ref]);
|
|
2603
2616
|
}
|
|
2604
2617
|
}
|
|
2618
|
+
return closedAnyHere;
|
|
2619
|
+
}
|
|
2620
|
+
function toPullRequestRef(node) {
|
|
2621
|
+
return {
|
|
2622
|
+
number: node.number,
|
|
2623
|
+
url: node.url,
|
|
2624
|
+
title: node.title,
|
|
2625
|
+
headRefName: node.headRefName,
|
|
2626
|
+
baseRefName: node.baseRefName,
|
|
2627
|
+
isCrossRepository: node.isCrossRepository,
|
|
2628
|
+
state: "OPEN",
|
|
2629
|
+
isDraft: node.isDraft,
|
|
2630
|
+
updatedAt: node.updatedAt
|
|
2631
|
+
};
|
|
2632
|
+
}
|
|
2633
|
+
function toOrphanPr(node) {
|
|
2634
|
+
return {
|
|
2635
|
+
pr: toPullRequestRef(node),
|
|
2636
|
+
labels: (node.labels?.nodes ?? []).map((label) => label.name ?? "").filter((name) => name.length > 0),
|
|
2637
|
+
assignees: (node.assignees?.nodes ?? []).map((assignee) => assignee.login ?? "").filter((login2) => login2.length > 0)
|
|
2638
|
+
};
|
|
2605
2639
|
}
|
|
2606
2640
|
function getOpenPrLinkMap() {
|
|
2607
2641
|
const { owner, repo } = getRepoSlug();
|
|
2608
2642
|
const map = /* @__PURE__ */ new Map();
|
|
2643
|
+
const orphans = [];
|
|
2609
2644
|
let defaultBranch = null;
|
|
2610
2645
|
let cursor = null;
|
|
2611
2646
|
for (let page = 0; page < MAX_LINK_MAP_PAGES; page++) {
|
|
@@ -2615,10 +2650,12 @@ function getOpenPrLinkMap() {
|
|
|
2615
2650
|
defaultBranch = response.data.repository.defaultBranchRef?.name ?? defaultBranch;
|
|
2616
2651
|
const connection = response.data.repository.pullRequests;
|
|
2617
2652
|
for (const node of connection.nodes) {
|
|
2618
|
-
indexPullRequest(map, node, `${owner}/${repo}`)
|
|
2653
|
+
if (!indexPullRequest(map, node, `${owner}/${repo}`)) {
|
|
2654
|
+
orphans.push(toOrphanPr(node));
|
|
2655
|
+
}
|
|
2619
2656
|
}
|
|
2620
2657
|
if (!connection.pageInfo?.hasNextPage || connection.pageInfo.endCursor === null) {
|
|
2621
|
-
return { byIssue: map, defaultBranch };
|
|
2658
|
+
return { byIssue: map, defaultBranch, orphans };
|
|
2622
2659
|
}
|
|
2623
2660
|
cursor = connection.pageInfo.endCursor;
|
|
2624
2661
|
}
|
|
@@ -2811,14 +2848,15 @@ function toHeadState(state) {
|
|
|
2811
2848
|
}
|
|
2812
2849
|
function listPullRequestsForHead(branch) {
|
|
2813
2850
|
const raw = ghJson(
|
|
2814
|
-
["pr", "list", "--head", branch, "--state", "all", "--json", "number,state,url,updatedAt"],
|
|
2851
|
+
["pr", "list", "--head", branch, "--state", "all", "--json", "number,state,url,updatedAt,author"],
|
|
2815
2852
|
`list pull requests for branch ${branch}`
|
|
2816
2853
|
);
|
|
2817
2854
|
return raw.map((pr) => ({
|
|
2818
2855
|
number: pr.number,
|
|
2819
2856
|
url: pr.url,
|
|
2820
2857
|
state: toHeadState(pr.state),
|
|
2821
|
-
updatedAt: pr.updatedAt
|
|
2858
|
+
updatedAt: pr.updatedAt,
|
|
2859
|
+
author: pr.author?.login ?? ""
|
|
2822
2860
|
})).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
2823
2861
|
}
|
|
2824
2862
|
var MISSING_LABEL_PATTERNS = [
|
|
@@ -2870,6 +2908,13 @@ function parseCreatedPrUrl(stdout, head) {
|
|
|
2870
2908
|
}
|
|
2871
2909
|
|
|
2872
2910
|
// src/github/workDetection.ts
|
|
2911
|
+
var NO_ISSUE_MESSAGES = {
|
|
2912
|
+
messages: [],
|
|
2913
|
+
newMessages: [],
|
|
2914
|
+
newMessageCount: 0,
|
|
2915
|
+
hasNewMessage: false,
|
|
2916
|
+
lastAgentAt: null
|
|
2917
|
+
};
|
|
2873
2918
|
function isAssignedToAgent(assignees, agentUser) {
|
|
2874
2919
|
const agent = agentUser.toLowerCase();
|
|
2875
2920
|
return assignees.some((name) => name.toLowerCase() === agent);
|
|
@@ -2909,6 +2954,7 @@ function unsafeBranchSkip(pr, issue, policy) {
|
|
|
2909
2954
|
return {
|
|
2910
2955
|
kind: "skip",
|
|
2911
2956
|
issue,
|
|
2957
|
+
pr,
|
|
2912
2958
|
reason: "unsafe-pr-branch",
|
|
2913
2959
|
detail: `pull request #${String(pr.number)} comes from a fork; its head branch is not in this repository`
|
|
2914
2960
|
};
|
|
@@ -2917,18 +2963,24 @@ function unsafeBranchSkip(pr, issue, policy) {
|
|
|
2917
2963
|
return {
|
|
2918
2964
|
kind: "skip",
|
|
2919
2965
|
issue,
|
|
2966
|
+
pr,
|
|
2920
2967
|
reason: "unsafe-pr-branch",
|
|
2921
2968
|
detail: `pull request #${String(pr.number)} has a protected branch (${pr.headRefName}) as its head, so a turn would have to push to it`
|
|
2922
2969
|
};
|
|
2923
2970
|
}
|
|
2924
2971
|
return null;
|
|
2925
2972
|
}
|
|
2973
|
+
function analysePrSurface(surface, p) {
|
|
2974
|
+
const agentThreadMessages = surface.threads.flatMap((thread) => thread.comments).filter((comment) => comment.author.toLowerCase() === p.agentUser.toLowerCase());
|
|
2975
|
+
const prAnalysis = analyzeSurface([...surface.messages, ...agentThreadMessages], p);
|
|
2976
|
+
return { prAnalysis, actionableThreads: findActionableThreads(surface.threads, p, prAnalysis.lastAgentAt) };
|
|
2977
|
+
}
|
|
2926
2978
|
function decideWork(state, p, policy) {
|
|
2927
2979
|
const baseBranch = policy.baseBranch;
|
|
2928
2980
|
const { issueSurface, prSurface } = state;
|
|
2929
2981
|
const issue = issueSurface.issue;
|
|
2930
2982
|
if (issueSurface.state === "CLOSED") {
|
|
2931
|
-
return { kind: "skip", issue, reason: "issue-closed", detail: "the issue is closed" };
|
|
2983
|
+
return { kind: "skip", issue, pr: null, reason: "issue-closed", detail: "the issue is closed" };
|
|
2932
2984
|
}
|
|
2933
2985
|
const issueAnalysis = analyzeSurface(issueSurface.messages, p);
|
|
2934
2986
|
const needsAssignment = !isAssignedToAgent(issueSurface.assignees, p.agentUser);
|
|
@@ -2939,6 +2991,7 @@ function decideWork(state, p, policy) {
|
|
|
2939
2991
|
return {
|
|
2940
2992
|
kind: "skip",
|
|
2941
2993
|
issue,
|
|
2994
|
+
pr: null,
|
|
2942
2995
|
reason: "no-new-messages",
|
|
2943
2996
|
detail: issueAnalysis.lastAgentAt === null ? "no messages from authorized accounts" : `nothing new since the agent's message at ${issueAnalysis.lastAgentAt}`
|
|
2944
2997
|
};
|
|
@@ -2962,14 +3015,13 @@ function decideWork(state, p, policy) {
|
|
|
2962
3015
|
const surface = prSurface;
|
|
2963
3016
|
const unsafe = unsafeBranchSkip(surface.pr, issue, policy);
|
|
2964
3017
|
if (unsafe !== null) return unsafe;
|
|
2965
|
-
const
|
|
2966
|
-
const prAnalysis = analyzeSurface([...surface.messages, ...agentThreadMessages], p);
|
|
2967
|
-
const actionableThreads = findActionableThreads(surface.threads, p, prAnalysis.lastAgentAt);
|
|
3018
|
+
const { prAnalysis, actionableThreads } = analysePrSurface(surface, p);
|
|
2968
3019
|
const hasPrWork = prAnalysis.hasNewMessage || actionableThreads.length > 0;
|
|
2969
3020
|
if (!hasPrWork && !issueAnalysis.hasNewMessage) {
|
|
2970
3021
|
return {
|
|
2971
3022
|
kind: "skip",
|
|
2972
3023
|
issue,
|
|
3024
|
+
pr: surface.pr,
|
|
2973
3025
|
reason: "no-new-messages",
|
|
2974
3026
|
detail: `nothing new on issue or pull request #${String(surface.pr.number)}`
|
|
2975
3027
|
};
|
|
@@ -3002,12 +3054,87 @@ function decideWork(state, p, policy) {
|
|
|
3002
3054
|
}
|
|
3003
3055
|
};
|
|
3004
3056
|
}
|
|
3057
|
+
function decideOrphanPrWork(state, p, policy) {
|
|
3058
|
+
const surface = state.prSurface;
|
|
3059
|
+
const pr = surface.pr;
|
|
3060
|
+
if (pr.state !== "OPEN") {
|
|
3061
|
+
return {
|
|
3062
|
+
kind: "skip",
|
|
3063
|
+
issue: null,
|
|
3064
|
+
pr,
|
|
3065
|
+
reason: "pr-closed",
|
|
3066
|
+
detail: `pull request #${String(pr.number)} is ${pr.state.toLowerCase()}`
|
|
3067
|
+
};
|
|
3068
|
+
}
|
|
3069
|
+
const unsafe = unsafeBranchSkip(pr, null, policy);
|
|
3070
|
+
if (unsafe !== null) return unsafe;
|
|
3071
|
+
const { prAnalysis, actionableThreads } = analysePrSurface(surface, p);
|
|
3072
|
+
if (!prAnalysis.hasNewMessage && actionableThreads.length === 0) {
|
|
3073
|
+
return {
|
|
3074
|
+
kind: "skip",
|
|
3075
|
+
issue: null,
|
|
3076
|
+
pr,
|
|
3077
|
+
reason: "no-new-messages",
|
|
3078
|
+
detail: prAnalysis.lastAgentAt === null ? `no messages from authorized accounts on pull request #${String(pr.number)}` : `nothing new on pull request #${String(pr.number)} since the agent's message at ${prAnalysis.lastAgentAt}`
|
|
3079
|
+
};
|
|
3080
|
+
}
|
|
3081
|
+
const reasons = [];
|
|
3082
|
+
if (prAnalysis.hasNewMessage) {
|
|
3083
|
+
reasons.push(plural(prAnalysis.newMessageCount, "new pull request message"));
|
|
3084
|
+
}
|
|
3085
|
+
if (actionableThreads.length > 0) {
|
|
3086
|
+
reasons.push(plural(actionableThreads.length, "unresolved review thread"));
|
|
3087
|
+
}
|
|
3088
|
+
return {
|
|
3089
|
+
kind: "work",
|
|
3090
|
+
item: {
|
|
3091
|
+
issue: null,
|
|
3092
|
+
turn: "pr-orphan",
|
|
3093
|
+
pr,
|
|
3094
|
+
branch: pr.headRefName,
|
|
3095
|
+
// Assignment is an issue mechanism: it makes the claim visible in the
|
|
3096
|
+
// issue list, and with `issueDiscoveryTechnique: assignee` assigning the
|
|
3097
|
+
// agent here would change what the discovery filter matches next tick.
|
|
3098
|
+
// The `working…` marker on the pull request is the claim.
|
|
3099
|
+
needsAssignment: false,
|
|
3100
|
+
issueAnalysis: NO_ISSUE_MESSAGES,
|
|
3101
|
+
prAnalysis,
|
|
3102
|
+
actionableThreads,
|
|
3103
|
+
reason: `${reasons.join(", ")} on pull request #${String(pr.number)} (no linked issue)`,
|
|
3104
|
+
ambiguousPrs: []
|
|
3105
|
+
}
|
|
3106
|
+
};
|
|
3107
|
+
}
|
|
3108
|
+
function skipDuplicateHeadBranches(decisions) {
|
|
3109
|
+
const owners = /* @__PURE__ */ new Map();
|
|
3110
|
+
return decisions.map((decision) => {
|
|
3111
|
+
if (decision.kind !== "work") return decision;
|
|
3112
|
+
const { item } = decision;
|
|
3113
|
+
if (item.turn === "issue-discuss" || item.pr === null) return decision;
|
|
3114
|
+
const owner = owners.get(item.branch);
|
|
3115
|
+
if (owner === void 0) {
|
|
3116
|
+
owners.set(item.branch, item.pr);
|
|
3117
|
+
return decision;
|
|
3118
|
+
}
|
|
3119
|
+
return {
|
|
3120
|
+
kind: "skip",
|
|
3121
|
+
issue: item.issue,
|
|
3122
|
+
pr: item.pr,
|
|
3123
|
+
reason: "branch-busy",
|
|
3124
|
+
detail: `pull request #${String(item.pr.number)} shares its head branch (${item.branch}) with pull request #${String(owner.number)}, which this tick works on first`
|
|
3125
|
+
};
|
|
3126
|
+
});
|
|
3127
|
+
}
|
|
3005
3128
|
function selectLinkedPr(linkedPrs) {
|
|
3006
3129
|
const open = linkedPrs.filter((pr) => pr.state === "OPEN");
|
|
3007
3130
|
return open.length === 0 ? null : newestPr(open);
|
|
3008
3131
|
}
|
|
3009
3132
|
|
|
3010
3133
|
// src/github/workPrompt.ts
|
|
3134
|
+
function promptBaseBranch(item, configuredBaseBranch) {
|
|
3135
|
+
const prBase = item.pr?.baseRefName ?? "";
|
|
3136
|
+
return prBase.length > 0 ? prBase : configuredBaseBranch;
|
|
3137
|
+
}
|
|
3011
3138
|
function formatThreads(threads) {
|
|
3012
3139
|
return threads.map((thread) => {
|
|
3013
3140
|
const location = thread.line === null ? `${thread.path}:(file)` : `${thread.path}:${String(thread.line)}`;
|
|
@@ -3029,10 +3156,11 @@ function composePrompt(input) {
|
|
|
3029
3156
|
`Repository: ${repo.owner}/${repo.repo}`,
|
|
3030
3157
|
`You are: ${agentUser}`,
|
|
3031
3158
|
`Turn: ${item.turn}`,
|
|
3032
|
-
`Base branch: ${baseBranch}
|
|
3033
|
-
`Issue #${String(item.issue.number)}: ${item.issue.title}`,
|
|
3034
|
-
`Issue URL: ${item.issue.url}`
|
|
3159
|
+
`Base branch: ${promptBaseBranch(item, baseBranch)}`
|
|
3035
3160
|
];
|
|
3161
|
+
if (item.issue) {
|
|
3162
|
+
lines.push(`Issue #${String(item.issue.number)}: ${item.issue.title}`, `Issue URL: ${item.issue.url}`);
|
|
3163
|
+
}
|
|
3036
3164
|
if (item.pr) {
|
|
3037
3165
|
lines.push(
|
|
3038
3166
|
`Pull request #${String(item.pr.number)}: ${item.pr.title}`,
|
|
@@ -3047,12 +3175,14 @@ function composePrompt(input) {
|
|
|
3047
3175
|
if (newMessages.length > 0) {
|
|
3048
3176
|
lines.push("", "New since your last message \u2014 this is what you must answer:", "", formatMessages(newMessages));
|
|
3049
3177
|
}
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3178
|
+
if (item.issue) {
|
|
3179
|
+
lines.push(
|
|
3180
|
+
"",
|
|
3181
|
+
"Full conversation on the issue (authorized accounts and you only, oldest first):",
|
|
3182
|
+
"",
|
|
3183
|
+
formatMessages(item.issueAnalysis.messages)
|
|
3184
|
+
);
|
|
3185
|
+
}
|
|
3056
3186
|
if (item.prAnalysis && item.prAnalysis.messages.length > 0) {
|
|
3057
3187
|
lines.push(
|
|
3058
3188
|
"",
|
|
@@ -3484,8 +3614,19 @@ function prepareBaseBranch(baseBranch) {
|
|
|
3484
3614
|
}
|
|
3485
3615
|
return { ok: true, branch: baseBranch };
|
|
3486
3616
|
}
|
|
3617
|
+
function resetToForcePushedRemote(headRefName, previousRemoteSha) {
|
|
3618
|
+
const localSha = revParse(`refs/heads/${headRefName}`);
|
|
3619
|
+
if (previousRemoteSha === null || localSha === null) return null;
|
|
3620
|
+
if (!isAncestorCommit(localSha, previousRemoteSha)) return null;
|
|
3621
|
+
const reset = resetHardTo(`refs/remotes/origin/${headRefName}`);
|
|
3622
|
+
if (!reset.ok) {
|
|
3623
|
+
return { ok: false, reason: "pull-failed", detail: reset.stderr };
|
|
3624
|
+
}
|
|
3625
|
+
return { ok: true, branch: headRefName };
|
|
3626
|
+
}
|
|
3487
3627
|
function preparePrBranch(headRefName) {
|
|
3488
3628
|
if (hasUncommittedChanges([RUN_LOCK_RELATIVE_PATH])) return dirtyTree();
|
|
3629
|
+
const previousRemoteSha = revParse(`refs/remotes/origin/${headRefName}`);
|
|
3489
3630
|
const fetched = fetchBranch(headRefName);
|
|
3490
3631
|
if (!fetched.ok) {
|
|
3491
3632
|
return { ok: false, reason: "checkout-failed", detail: fetched.stderr };
|
|
@@ -3500,7 +3641,13 @@ function preparePrBranch(headRefName) {
|
|
|
3500
3641
|
}
|
|
3501
3642
|
const pull = pullFastForwardOnly(headRefName);
|
|
3502
3643
|
if (!pull.ok) {
|
|
3503
|
-
|
|
3644
|
+
const reset = resetToForcePushedRemote(headRefName, previousRemoteSha);
|
|
3645
|
+
if (reset !== null) return reset;
|
|
3646
|
+
return {
|
|
3647
|
+
ok: false,
|
|
3648
|
+
reason: "pull-failed",
|
|
3649
|
+
detail: `${pull.stderr} \u2014 the local ${headRefName} has commits origin/${headRefName} does not, so it was not reset automatically; inspect them and \`git reset --hard origin/${headRefName}\` yourself`
|
|
3650
|
+
};
|
|
3504
3651
|
}
|
|
3505
3652
|
return { ok: true, branch: headRefName };
|
|
3506
3653
|
}
|
|
@@ -3514,14 +3661,25 @@ function utcStamp(now) {
|
|
|
3514
3661
|
function flattenBranchName(branch) {
|
|
3515
3662
|
return branch.replaceAll("/", "-");
|
|
3516
3663
|
}
|
|
3517
|
-
function
|
|
3664
|
+
function agentOwnsBranch(branch, agentUser) {
|
|
3665
|
+
if (agentUser.length === 0) return false;
|
|
3666
|
+
let open;
|
|
3667
|
+
try {
|
|
3668
|
+
open = findOpenPr(branch);
|
|
3669
|
+
} catch {
|
|
3670
|
+
return false;
|
|
3671
|
+
}
|
|
3672
|
+
if (open === null) return true;
|
|
3673
|
+
return open.author.toLowerCase() === agentUser.toLowerCase();
|
|
3674
|
+
}
|
|
3675
|
+
function detectRescueTarget(options, now) {
|
|
3518
3676
|
const current = getCurrentBranch();
|
|
3519
3677
|
const detached = current === "HEAD" || current.length === 0;
|
|
3520
|
-
if (!detached && current !== baseBranch) {
|
|
3678
|
+
if (!detached && current !== options.baseBranch && agentOwnsBranch(current, options.agentUser)) {
|
|
3521
3679
|
return { branch: current, createdBranch: false, source: current };
|
|
3522
3680
|
}
|
|
3523
|
-
const source = detached ? "detached HEAD" : baseBranch;
|
|
3524
|
-
const label = detached ? "detached" : flattenBranchName(
|
|
3681
|
+
const source = detached ? "detached HEAD" : current === options.baseBranch ? options.baseBranch : current;
|
|
3682
|
+
const label = detached ? "detached" : flattenBranchName(source);
|
|
3525
3683
|
return {
|
|
3526
3684
|
branch: `${RESCUE_BRANCH_PREFIX}${label}-${utcStamp(now)}`,
|
|
3527
3685
|
createdBranch: true,
|
|
@@ -3544,7 +3702,7 @@ function rescueUncommittedChanges(options, now) {
|
|
|
3544
3702
|
options.log(" rescue nothing to do; the working tree is clean\n");
|
|
3545
3703
|
return { kind: "clean" };
|
|
3546
3704
|
}
|
|
3547
|
-
const target = detectRescueTarget(options
|
|
3705
|
+
const target = detectRescueTarget(options, now);
|
|
3548
3706
|
if (options.dryRun) {
|
|
3549
3707
|
options.log(
|
|
3550
3708
|
target.createdBranch ? ` rescue would create ${target.branch} off ${target.source}, commit the uncommitted changes, push it and open a draft PR
|
|
@@ -3575,8 +3733,11 @@ function rescueUncommittedChanges(options, now) {
|
|
|
3575
3733
|
}
|
|
3576
3734
|
const pushed = pushSetUpstream(target.branch);
|
|
3577
3735
|
if (!pushed.ok) {
|
|
3578
|
-
options.log(
|
|
3579
|
-
`
|
|
3736
|
+
options.log(
|
|
3737
|
+
` rescue FAILED to push ${target.branch}: ${pushed.stderr}
|
|
3738
|
+
rescue the work is committed locally on ${target.branch}; push it by hand before it is lost
|
|
3739
|
+
`
|
|
3740
|
+
);
|
|
3580
3741
|
return { kind: "failed", step: "push", detail: pushed.stderr };
|
|
3581
3742
|
}
|
|
3582
3743
|
let existing;
|
|
@@ -3782,17 +3943,195 @@ function runRepoHygiene(options, now = /* @__PURE__ */ new Date()) {
|
|
|
3782
3943
|
return { rescue, base, prunes: outcomes, degraded };
|
|
3783
3944
|
}
|
|
3784
3945
|
|
|
3946
|
+
// src/run/operationLog.ts
|
|
3947
|
+
import {
|
|
3948
|
+
accessSync,
|
|
3949
|
+
appendFileSync,
|
|
3950
|
+
constants,
|
|
3951
|
+
readFileSync as readFileSync4,
|
|
3952
|
+
renameSync as renameSync2,
|
|
3953
|
+
unlinkSync as unlinkSync2,
|
|
3954
|
+
writeFileSync as writeFileSync2
|
|
3955
|
+
} from "fs";
|
|
3956
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
3957
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
3958
|
+
var EXECUTION_LOG_FILE = "automata-execution.log";
|
|
3959
|
+
var WORK_LOG_FILE = "automata-work.log";
|
|
3960
|
+
var MAX_EXECUTION_LINES = 1e3;
|
|
3961
|
+
var WORK_RECORD_MAX_AGE_DAYS = 30;
|
|
3962
|
+
var MAX_DETAIL_LENGTH = 200;
|
|
3963
|
+
var OUTCOMES = [
|
|
3964
|
+
"answered",
|
|
3965
|
+
"answered-no-reply",
|
|
3966
|
+
"skipped",
|
|
3967
|
+
"failed",
|
|
3968
|
+
"deferred"
|
|
3969
|
+
];
|
|
3970
|
+
function operationLogDirectory() {
|
|
3971
|
+
return dirname2(process.cwd());
|
|
3972
|
+
}
|
|
3973
|
+
function repoField(repo) {
|
|
3974
|
+
if (repo === null) return "-";
|
|
3975
|
+
const flat = oneLine(repo);
|
|
3976
|
+
return flat.length === 0 ? "-" : flat;
|
|
3977
|
+
}
|
|
3978
|
+
function formatExecutionLine(tick) {
|
|
3979
|
+
const counts = {
|
|
3980
|
+
answered: 0,
|
|
3981
|
+
"answered-no-reply": 0,
|
|
3982
|
+
skipped: 0,
|
|
3983
|
+
failed: 0,
|
|
3984
|
+
deferred: 0
|
|
3985
|
+
};
|
|
3986
|
+
let runs = 0;
|
|
3987
|
+
for (const item of tick.items) {
|
|
3988
|
+
counts[item.outcome]++;
|
|
3989
|
+
if (item.ranExecutor) runs++;
|
|
3990
|
+
}
|
|
3991
|
+
const fields = [
|
|
3992
|
+
tick.timestamp.toISOString(),
|
|
3993
|
+
tick.command,
|
|
3994
|
+
`repo=${repoField(tick.repo)}`,
|
|
3995
|
+
`items=${String(tick.items.length)}`,
|
|
3996
|
+
...OUTCOMES.map((outcome) => `${outcome}=${String(counts[outcome])}`),
|
|
3997
|
+
`runs=${String(runs)}`,
|
|
3998
|
+
`exit=${String(tick.exitCode)}`,
|
|
3999
|
+
`dur=${(tick.durationMs / 1e3).toFixed(1)}s`
|
|
4000
|
+
];
|
|
4001
|
+
if (tick.note !== void 0 && tick.note.length > 0) fields.push(`note=${tick.note}`);
|
|
4002
|
+
return fields.join(" ") + "\n";
|
|
4003
|
+
}
|
|
4004
|
+
function oneLine(value) {
|
|
4005
|
+
return value.replace(/\s+/g, " ").trim();
|
|
4006
|
+
}
|
|
4007
|
+
function briefDetail(detail) {
|
|
4008
|
+
const flat = oneLine(detail);
|
|
4009
|
+
return flat.length <= MAX_DETAIL_LENGTH ? flat : `${flat.slice(0, MAX_DETAIL_LENGTH)}\u2026`;
|
|
4010
|
+
}
|
|
4011
|
+
function describeItemExecution(item) {
|
|
4012
|
+
if (item.executor === void 0) return "";
|
|
4013
|
+
const model = item.model === void 0 ? "" : ` model=${oneLine(item.model)}`;
|
|
4014
|
+
const effort = item.effort === void 0 ? "" : ` effort=${oneLine(item.effort)}`;
|
|
4015
|
+
return ` [${oneLine(item.executor)}${model}${effort}]`;
|
|
4016
|
+
}
|
|
4017
|
+
function formatWorkRecord(tick) {
|
|
4018
|
+
const ran = tick.items.filter((item) => item.ranExecutor);
|
|
4019
|
+
if (ran.length === 0) return null;
|
|
4020
|
+
const header = `=== ${tick.timestamp.toISOString()} ${repoField(tick.repo)} ===
|
|
4021
|
+
`;
|
|
4022
|
+
const lines = ran.map(
|
|
4023
|
+
(item) => `${oneLine(item.subject)} ${item.turn === null ? "-" : oneLine(item.turn)} ${item.outcome}${describeItemExecution(item)} \u2014 ${briefDetail(item.detail)}
|
|
4024
|
+
`
|
|
4025
|
+
);
|
|
4026
|
+
return header + lines.join("") + "\n";
|
|
4027
|
+
}
|
|
4028
|
+
function trimToLastLines(content, max) {
|
|
4029
|
+
const lines = content.split("\n");
|
|
4030
|
+
if (lines.at(-1) === "") lines.pop();
|
|
4031
|
+
if (lines.length <= max) return content;
|
|
4032
|
+
return lines.slice(lines.length - max).join("\n") + "\n";
|
|
4033
|
+
}
|
|
4034
|
+
var RECORD_HEADER = /^=== (\S+) /;
|
|
4035
|
+
function pruneOldRecords(content, now, maxAgeMs) {
|
|
4036
|
+
if (content.length === 0) return content;
|
|
4037
|
+
const cutoff = now.getTime() - maxAgeMs;
|
|
4038
|
+
const lines = content.split("\n");
|
|
4039
|
+
if (lines.at(-1) === "") lines.pop();
|
|
4040
|
+
const groups = [];
|
|
4041
|
+
let current = { header: null, lines: [] };
|
|
4042
|
+
for (const line of lines) {
|
|
4043
|
+
const header = RECORD_HEADER.exec(line);
|
|
4044
|
+
if (header === null) {
|
|
4045
|
+
current.lines.push(line);
|
|
4046
|
+
continue;
|
|
4047
|
+
}
|
|
4048
|
+
groups.push(current);
|
|
4049
|
+
current = { header: header[1], lines: [line] };
|
|
4050
|
+
}
|
|
4051
|
+
groups.push(current);
|
|
4052
|
+
return groups.filter((group) => {
|
|
4053
|
+
if (group.lines.length === 0) return false;
|
|
4054
|
+
if (group.header === null) return true;
|
|
4055
|
+
const parsed = Date.parse(group.header);
|
|
4056
|
+
return Number.isNaN(parsed) || parsed >= cutoff;
|
|
4057
|
+
}).map((group) => group.lines.join("\n") + "\n").join("");
|
|
4058
|
+
}
|
|
4059
|
+
function appendWithRetention(dir, file, content, retain) {
|
|
4060
|
+
const target = join3(dir, file);
|
|
4061
|
+
appendFileSync(target, content, "utf8");
|
|
4062
|
+
const existing = readFileSync4(target, "utf8");
|
|
4063
|
+
const retained = retain(existing);
|
|
4064
|
+
if (retained === existing) return;
|
|
4065
|
+
const temp = `${target}.${randomUUID2()}.tmp`;
|
|
4066
|
+
try {
|
|
4067
|
+
writeFileSync2(temp, retained, "utf8");
|
|
4068
|
+
renameSync2(temp, target);
|
|
4069
|
+
} finally {
|
|
4070
|
+
try {
|
|
4071
|
+
unlinkSync2(temp);
|
|
4072
|
+
} catch {
|
|
4073
|
+
}
|
|
4074
|
+
}
|
|
4075
|
+
}
|
|
4076
|
+
function recordTick(tick, dir = operationLogDirectory()) {
|
|
4077
|
+
try {
|
|
4078
|
+
accessSync(dir, constants.W_OK);
|
|
4079
|
+
} catch {
|
|
4080
|
+
return;
|
|
4081
|
+
}
|
|
4082
|
+
try {
|
|
4083
|
+
appendWithRetention(
|
|
4084
|
+
dir,
|
|
4085
|
+
EXECUTION_LOG_FILE,
|
|
4086
|
+
formatExecutionLine(tick),
|
|
4087
|
+
(existing) => trimToLastLines(existing, MAX_EXECUTION_LINES)
|
|
4088
|
+
);
|
|
4089
|
+
} catch {
|
|
4090
|
+
}
|
|
4091
|
+
try {
|
|
4092
|
+
const record = formatWorkRecord(tick);
|
|
4093
|
+
if (record === null) return;
|
|
4094
|
+
const maxAgeMs = WORK_RECORD_MAX_AGE_DAYS * 24 * 60 * 60 * 1e3;
|
|
4095
|
+
appendWithRetention(
|
|
4096
|
+
dir,
|
|
4097
|
+
WORK_LOG_FILE,
|
|
4098
|
+
record,
|
|
4099
|
+
(existing) => pruneOldRecords(existing, tick.timestamp, maxAgeMs)
|
|
4100
|
+
);
|
|
4101
|
+
} catch {
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
|
|
3785
4105
|
// src/commands/doWork.ts
|
|
3786
4106
|
var inFlightMarker = null;
|
|
4107
|
+
function subjectLabel(issue, pr) {
|
|
4108
|
+
if (issue !== null) return `#${String(issue)}`;
|
|
4109
|
+
return pr === null ? "#?" : `PR #${String(pr)}`;
|
|
4110
|
+
}
|
|
4111
|
+
function itemLabel(item) {
|
|
4112
|
+
return subjectLabel(item.issue?.number ?? null, item.pr?.number ?? null);
|
|
4113
|
+
}
|
|
4114
|
+
function reportLabel(report) {
|
|
4115
|
+
return subjectLabel(report.issue, report.pr);
|
|
4116
|
+
}
|
|
4117
|
+
function itemTitle(item) {
|
|
4118
|
+
return item.issue?.title ?? item.pr?.title ?? "";
|
|
4119
|
+
}
|
|
3787
4120
|
function out(message) {
|
|
3788
4121
|
process.stdout.write(message);
|
|
3789
4122
|
}
|
|
3790
4123
|
function progress(message) {
|
|
3791
4124
|
process.stderr.write(message);
|
|
3792
4125
|
}
|
|
4126
|
+
var loggableInvocation = null;
|
|
3793
4127
|
function fail(message) {
|
|
3794
4128
|
process.stderr.write(`Error: ${message}
|
|
3795
4129
|
`);
|
|
4130
|
+
if (loggableInvocation !== null) {
|
|
4131
|
+
const { startedAt } = loggableInvocation;
|
|
4132
|
+
loggableInvocation = null;
|
|
4133
|
+
logTick([], 1, startedAt, "config-error");
|
|
4134
|
+
}
|
|
3796
4135
|
process.exit(1);
|
|
3797
4136
|
}
|
|
3798
4137
|
function parsePositiveInt(value, label) {
|
|
@@ -3864,11 +4203,13 @@ function resolveSettings(options) {
|
|
|
3864
4203
|
participants: { allowedUsers, agentUser },
|
|
3865
4204
|
prompts: {
|
|
3866
4205
|
"issue-discuss": doWork.prompts?.issueDiscuss ?? DEFAULT_DO_WORK_ISSUE_DISCUSS_PROMPT,
|
|
3867
|
-
"pr-work": doWork.prompts?.prWork ?? DEFAULT_DO_WORK_PR_WORK_PROMPT
|
|
4206
|
+
"pr-work": doWork.prompts?.prWork ?? DEFAULT_DO_WORK_PR_WORK_PROMPT,
|
|
4207
|
+
"pr-orphan": doWork.prompts?.prOrphan ?? DEFAULT_DO_WORK_PR_ORPHAN_PROMPT
|
|
3868
4208
|
},
|
|
3869
4209
|
technique: config.issueDiscoveryTechnique,
|
|
3870
4210
|
discoveryValue: config.issueDiscoveryValue,
|
|
3871
|
-
onlyIssue: options.issue === void 0 ? void 0 : parsePositiveInt(options.issue, "--issue")
|
|
4211
|
+
onlyIssue: options.issue === void 0 ? void 0 : parsePositiveInt(options.issue, "--issue"),
|
|
4212
|
+
onlyPr: options.pr === void 0 ? void 0 : parsePositiveInt(options.pr, "--pr")
|
|
3872
4213
|
};
|
|
3873
4214
|
}
|
|
3874
4215
|
function checkAuthenticatedIdentity(agentUser, allowedUsers) {
|
|
@@ -3932,12 +4273,12 @@ function planRun(item, settings, execution) {
|
|
|
3932
4273
|
}
|
|
3933
4274
|
function describePlannedRun(item, settings, run5, execution) {
|
|
3934
4275
|
const rule = "\u2500".repeat(72);
|
|
3935
|
-
const branchAction = item.turn === "
|
|
4276
|
+
const branchAction = item.turn === "issue-discuss" ? " and pull" : " and fast-forward";
|
|
3936
4277
|
const assignment = item.needsAssignment ? `would assign to ${settings.participants.agentUser}` : "already assigned";
|
|
3937
|
-
const markerTarget =
|
|
4278
|
+
const markerTarget = markerSurfaceLabel(item);
|
|
3938
4279
|
const lines = [
|
|
3939
4280
|
rule,
|
|
3940
|
-
|
|
4281
|
+
describeSubject(item),
|
|
3941
4282
|
rule,
|
|
3942
4283
|
` Turn ${item.turn}`,
|
|
3943
4284
|
` Why ${item.reason}`,
|
|
@@ -3960,11 +4301,14 @@ function describePlannedRun(item, settings, run5, execution) {
|
|
|
3960
4301
|
];
|
|
3961
4302
|
return lines.join("\n") + "\n";
|
|
3962
4303
|
}
|
|
4304
|
+
function describeSubject(item) {
|
|
4305
|
+
return item.issue !== null ? `Issue #${String(item.issue.number)} \u2014 ${item.issue.title}` : `Pull request #${String(item.pr?.number ?? 0)} \u2014 ${itemTitle(item)}`;
|
|
4306
|
+
}
|
|
3963
4307
|
function describeRefusedRun(item, refusal) {
|
|
3964
4308
|
const rule = "\u2500".repeat(72);
|
|
3965
4309
|
return [
|
|
3966
4310
|
rule,
|
|
3967
|
-
|
|
4311
|
+
describeSubject(item),
|
|
3968
4312
|
rule,
|
|
3969
4313
|
` Turn ${item.turn}`,
|
|
3970
4314
|
` Why ${item.reason}`,
|
|
@@ -4005,7 +4349,7 @@ function validateDoWorkConfig(section) {
|
|
|
4005
4349
|
validateProtectedBranches(section["protectedBranches"]);
|
|
4006
4350
|
validateSettingContainer(section["models"], "models", ["claude", "codex"]);
|
|
4007
4351
|
validateSettingContainer(section["effort"], "effort", ["claude", "codex"]);
|
|
4008
|
-
validateSettingContainer(section["prompts"], "prompts", ["issueDiscuss", "prWork"]);
|
|
4352
|
+
validateSettingContainer(section["prompts"], "prompts", ["issueDiscuss", "prWork", "prOrphan"]);
|
|
4009
4353
|
}
|
|
4010
4354
|
function validateProtectedBranches(value) {
|
|
4011
4355
|
if (value === void 0 || value === null) return;
|
|
@@ -4028,7 +4372,14 @@ function validateSettingContainer(value, container, keys) {
|
|
|
4028
4372
|
}
|
|
4029
4373
|
}
|
|
4030
4374
|
}
|
|
4375
|
+
function issuePassEnabled(settings) {
|
|
4376
|
+
return settings.onlyPr === void 0 || settings.onlyIssue !== void 0;
|
|
4377
|
+
}
|
|
4378
|
+
function orphanPassEnabled(settings) {
|
|
4379
|
+
return settings.onlyIssue === void 0 || settings.onlyPr !== void 0;
|
|
4380
|
+
}
|
|
4031
4381
|
function discoverIssues(settings) {
|
|
4382
|
+
if (!issuePassEnabled(settings)) return [];
|
|
4032
4383
|
const candidates = listCandidateIssues(settings.technique, settings.discoveryValue, settings.limit);
|
|
4033
4384
|
if (settings.onlyIssue === void 0) {
|
|
4034
4385
|
if (candidates.length === settings.limit) {
|
|
@@ -4050,17 +4401,58 @@ function discoverIssues(settings) {
|
|
|
4050
4401
|
}
|
|
4051
4402
|
return [surface.issue];
|
|
4052
4403
|
}
|
|
4053
|
-
function
|
|
4404
|
+
function matchesDiscoveryFilter(subject, settings) {
|
|
4054
4405
|
const value = settings.discoveryValue.toLowerCase();
|
|
4055
4406
|
switch (settings.technique) {
|
|
4056
4407
|
case "label":
|
|
4057
|
-
return
|
|
4408
|
+
return subject.labels.some((label) => label.toLowerCase() === value);
|
|
4058
4409
|
case "assignee":
|
|
4059
|
-
return
|
|
4410
|
+
return subject.assignees.some((assignee) => assignee.toLowerCase() === value);
|
|
4060
4411
|
case "title-contains":
|
|
4061
|
-
return
|
|
4412
|
+
return subject.title.toLowerCase().includes(value);
|
|
4062
4413
|
}
|
|
4063
4414
|
}
|
|
4415
|
+
function issueMatchesFilter(surface, settings) {
|
|
4416
|
+
return matchesDiscoveryFilter(
|
|
4417
|
+
{ labels: surface.labels, assignees: surface.assignees, title: surface.issue.title },
|
|
4418
|
+
settings
|
|
4419
|
+
);
|
|
4420
|
+
}
|
|
4421
|
+
function prMatchesFilter(candidate, settings) {
|
|
4422
|
+
return matchesDiscoveryFilter(
|
|
4423
|
+
{ labels: candidate.labels, assignees: candidate.assignees, title: candidate.pr.title },
|
|
4424
|
+
settings
|
|
4425
|
+
);
|
|
4426
|
+
}
|
|
4427
|
+
function issuesClosedBy(linkMap, prNumber) {
|
|
4428
|
+
return [...linkMap.byIssue.entries()].filter(([, prs]) => prs.some((pr) => pr.number === prNumber)).map(([issueNumber]) => issueNumber);
|
|
4429
|
+
}
|
|
4430
|
+
function discoverOrphanPrs(settings, linkMap) {
|
|
4431
|
+
if (!orphanPassEnabled(settings)) return [];
|
|
4432
|
+
if (settings.onlyPr === void 0) {
|
|
4433
|
+
return linkMap.orphans.filter((candidate) => prMatchesFilter(candidate, settings));
|
|
4434
|
+
}
|
|
4435
|
+
const target = settings.onlyPr;
|
|
4436
|
+
const match = linkMap.orphans.find((candidate) => candidate.pr.number === target);
|
|
4437
|
+
if (match === void 0) {
|
|
4438
|
+
const closes = issuesClosedBy(linkMap, target);
|
|
4439
|
+
if (closes.length > 0) {
|
|
4440
|
+
throw new Error(
|
|
4441
|
+
`Pull request #${String(target)} closes issue #${String(closes[0])} of this repository, so it is not orphaned and the issue pass owns it. Use \`--issue ${String(closes[0])}\` instead.`
|
|
4442
|
+
);
|
|
4443
|
+
}
|
|
4444
|
+
throw new Error(
|
|
4445
|
+
`#${String(target)} is not an open pull request of this repository. The orphan pass only ever works on open pull requests; use \`--issue\` for an issue.`
|
|
4446
|
+
);
|
|
4447
|
+
}
|
|
4448
|
+
if (!prMatchesFilter(match, settings)) {
|
|
4449
|
+
progress(
|
|
4450
|
+
`Note: pull request #${String(target)} does not match the configured discovery filter (${settings.technique} = ${settings.discoveryValue}); processing it anyway because --pr was given.
|
|
4451
|
+
`
|
|
4452
|
+
);
|
|
4453
|
+
}
|
|
4454
|
+
return [match];
|
|
4455
|
+
}
|
|
4064
4456
|
function buildIssueState(issue, linkMap) {
|
|
4065
4457
|
const issueSurface = getIssueSurface(issue.number);
|
|
4066
4458
|
const linkedPrs = linkMap.byIssue.get(issue.number) ?? [];
|
|
@@ -4074,16 +4466,16 @@ function buildIssueState(issue, linkMap) {
|
|
|
4074
4466
|
function describePlan(decisions) {
|
|
4075
4467
|
const lines = decisions.map((decision) => {
|
|
4076
4468
|
if (decision.kind === "skip") {
|
|
4077
|
-
return `
|
|
4469
|
+
return ` ${subjectLabel(decision.issue?.number ?? null, decision.pr?.number ?? null)} nothing to do \u2014 ${decision.detail}`;
|
|
4078
4470
|
}
|
|
4079
4471
|
const item = decision.item;
|
|
4080
4472
|
const claim = item.needsAssignment ? ", will assign to the agent" : "";
|
|
4081
|
-
return `
|
|
4473
|
+
return ` ${itemLabel(item)} ${item.turn} on ${item.branch} \u2014 ${item.reason}${claim}`;
|
|
4082
4474
|
});
|
|
4083
|
-
return lines.length === 0 ? " (
|
|
4475
|
+
return lines.length === 0 ? " (nothing matched the discovery filter)\n" : lines.join("\n") + "\n";
|
|
4084
4476
|
}
|
|
4085
4477
|
function claimIssue(item, settings) {
|
|
4086
|
-
if (!item.needsAssignment) return;
|
|
4478
|
+
if (!item.needsAssignment || item.issue === null) return;
|
|
4087
4479
|
try {
|
|
4088
4480
|
assignIssueToAgent(item.issue.number, settings.participants.agentUser);
|
|
4089
4481
|
progress(` assigned issue #${String(item.issue.number)} to ${settings.participants.agentUser}.
|
|
@@ -4094,7 +4486,8 @@ function claimIssue(item, settings) {
|
|
|
4094
4486
|
}
|
|
4095
4487
|
}
|
|
4096
4488
|
function notePickupOnIssue(item, marker) {
|
|
4097
|
-
if (item.turn !== "pr-work" || item.pr === null ||
|
|
4489
|
+
if (item.turn !== "pr-work" || item.pr === null || item.issue === null) return null;
|
|
4490
|
+
if (!item.issueAnalysis.hasNewMessage) return null;
|
|
4098
4491
|
try {
|
|
4099
4492
|
return postMarker(
|
|
4100
4493
|
"issue",
|
|
@@ -4114,6 +4507,7 @@ function notePickupOnIssue(item, marker) {
|
|
|
4114
4507
|
}
|
|
4115
4508
|
}
|
|
4116
4509
|
function reportIssueMessagesBuriedByNote(item, note, participants) {
|
|
4510
|
+
if (item.issue === null) return 0;
|
|
4117
4511
|
const watermark = promptWatermark([item.issueAnalysis.messages]);
|
|
4118
4512
|
let buried;
|
|
4119
4513
|
try {
|
|
@@ -4140,24 +4534,43 @@ function reportIssueMessagesBuriedByNote(item, note, participants) {
|
|
|
4140
4534
|
}
|
|
4141
4535
|
function refreshItem(item, settings) {
|
|
4142
4536
|
const linkMap = getOpenPrLinkMap();
|
|
4143
|
-
|
|
4537
|
+
const policy = {
|
|
4144
4538
|
baseBranch: settings.baseBranch,
|
|
4145
4539
|
defaultBranch: linkMap.defaultBranch,
|
|
4146
4540
|
protectedBranches: settings.protectedBranches
|
|
4147
|
-
}
|
|
4541
|
+
};
|
|
4542
|
+
if (item.issue === null) return refreshOrphanItem(item, linkMap, settings, policy);
|
|
4543
|
+
return decideWork(buildIssueState(item.issue, linkMap), settings.participants, policy);
|
|
4544
|
+
}
|
|
4545
|
+
function refreshOrphanItem(item, linkMap, settings, policy) {
|
|
4546
|
+
const number = item.pr?.number ?? 0;
|
|
4547
|
+
const stillOrphan = linkMap.orphans.some((candidate) => candidate.pr.number === number);
|
|
4548
|
+
if (!stillOrphan) {
|
|
4549
|
+
const linkedTo = issuesClosedBy(linkMap, number).map((issueNumber) => `#${String(issueNumber)}`);
|
|
4550
|
+
return {
|
|
4551
|
+
kind: "skip",
|
|
4552
|
+
issue: null,
|
|
4553
|
+
pr: item.pr,
|
|
4554
|
+
reason: linkedTo.length > 0 ? "pr-linked" : "pr-closed",
|
|
4555
|
+
detail: linkedTo.length > 0 ? `pull request #${String(number)} now closes ${linkedTo.join(", ")}, so it is no longer orphaned` : `pull request #${String(number)} is no longer an open pull request of this repository`
|
|
4556
|
+
};
|
|
4557
|
+
}
|
|
4558
|
+
return decideOrphanPrWork({ prSurface: getPrSurface(number) }, settings.participants, policy);
|
|
4148
4559
|
}
|
|
4149
4560
|
function readAnsweringSurface(item) {
|
|
4150
|
-
|
|
4151
|
-
|
|
4561
|
+
const pr = item.pr;
|
|
4562
|
+
if (pr !== null && item.turn !== "issue-discuss") {
|
|
4563
|
+
const surface = getPrSurface(pr.number);
|
|
4564
|
+
return [...surface.messages, ...surface.threads.flatMap((thread) => thread.comments)];
|
|
4152
4565
|
}
|
|
4153
|
-
|
|
4154
|
-
return [...surface.messages, ...surface.threads.flatMap((thread) => thread.comments)];
|
|
4155
|
-
}
|
|
4156
|
-
function markerSurfaceLabel(item) {
|
|
4157
|
-
return item.turn === "pr-work" && item.pr ? `pull request #${String(item.pr.number)}` : `issue #${String(item.issue.number)}`;
|
|
4566
|
+
return item.issue === null ? [] : getIssueSurface(item.issue.number).messages;
|
|
4158
4567
|
}
|
|
4159
4568
|
function markerSurfaceTarget(item) {
|
|
4160
|
-
return item.
|
|
4569
|
+
return item.pr !== null && item.turn !== "issue-discuss" ? { surface: "pr", number: item.pr.number } : { surface: "issue", number: item.issue?.number ?? 0 };
|
|
4570
|
+
}
|
|
4571
|
+
function markerSurfaceLabel(item) {
|
|
4572
|
+
const target = markerSurfaceTarget(item);
|
|
4573
|
+
return target.surface === "pr" ? `pull request #${String(target.number)}` : `issue #${String(target.number)}`;
|
|
4161
4574
|
}
|
|
4162
4575
|
function reportOvertakenMessages(item, analysis) {
|
|
4163
4576
|
if (analysis.missed.length === 0) return;
|
|
@@ -4181,7 +4594,7 @@ function reconcileMarker(item, marker, participants, watermark, runError) {
|
|
|
4181
4594
|
analysis = analyseAnswer(readAnsweringSurface(item), participants, marker, watermark);
|
|
4182
4595
|
} catch (err) {
|
|
4183
4596
|
progress(
|
|
4184
|
-
` warning: could not re-read
|
|
4597
|
+
` warning: could not re-read ${markerSurfaceLabel(item)} to check for an answer: ${err.message}
|
|
4185
4598
|
`
|
|
4186
4599
|
);
|
|
4187
4600
|
return reportUnverified(item, marker);
|
|
@@ -4230,7 +4643,7 @@ function reportNoAnswer(item, marker, runError) {
|
|
|
4230
4643
|
updateMarker(marker, explanation);
|
|
4231
4644
|
} catch (err) {
|
|
4232
4645
|
progress(
|
|
4233
|
-
` warning: could not update the marker comment on
|
|
4646
|
+
` warning: could not update the marker comment on ${surface}: ${err.message}
|
|
4234
4647
|
the humans have not been told that this run produced no answer.
|
|
4235
4648
|
`
|
|
4236
4649
|
);
|
|
@@ -4245,31 +4658,33 @@ async function invokeExecutor(prompt, execution, silent) {
|
|
|
4245
4658
|
await runClaude(prompt, { model: execution.model, effort: execution.effort, printSteps: !silent });
|
|
4246
4659
|
}
|
|
4247
4660
|
function repairIssueLink(item, baseBranch) {
|
|
4661
|
+
const issue = item.issue;
|
|
4662
|
+
if (issue === null) return false;
|
|
4248
4663
|
try {
|
|
4249
4664
|
const branch = getCurrentBranch();
|
|
4250
4665
|
if (branch === baseBranch) {
|
|
4251
|
-
progress(` issue #${String(
|
|
4666
|
+
progress(` issue #${String(issue.number)} is still in discussion (no branch was created).
|
|
4252
4667
|
`);
|
|
4253
4668
|
return false;
|
|
4254
4669
|
}
|
|
4255
4670
|
const pr = getCurrentBranchPr();
|
|
4256
4671
|
if (!pr) {
|
|
4257
|
-
progress(` issue #${String(
|
|
4672
|
+
progress(` issue #${String(issue.number)} is still in discussion (no pull request).
|
|
4258
4673
|
`);
|
|
4259
4674
|
return false;
|
|
4260
4675
|
}
|
|
4261
|
-
const closesRef = new RegExp(String.raw`\bcloses\s+#` + String(
|
|
4676
|
+
const closesRef = new RegExp(String.raw`\bcloses\s+#` + String(issue.number) + String.raw`\b`, "i");
|
|
4262
4677
|
if (closesRef.test(pr.body)) {
|
|
4263
|
-
progress(` pull request #${String(pr.number)} already closes issue #${String(
|
|
4678
|
+
progress(` pull request #${String(pr.number)} already closes issue #${String(issue.number)}.
|
|
4264
4679
|
`);
|
|
4265
4680
|
return true;
|
|
4266
4681
|
}
|
|
4267
|
-
addClosesRefToPr(pr.number,
|
|
4268
|
-
progress(` linked pull request #${String(pr.number)} to issue #${String(
|
|
4682
|
+
addClosesRefToPr(pr.number, issue.number);
|
|
4683
|
+
progress(` linked pull request #${String(pr.number)} to issue #${String(issue.number)}.
|
|
4269
4684
|
`);
|
|
4270
4685
|
return true;
|
|
4271
4686
|
} catch (err) {
|
|
4272
|
-
progress(` warning: could not link a pull request to issue #${String(
|
|
4687
|
+
progress(` warning: could not link a pull request to issue #${String(issue.number)}: ${err.message}
|
|
4273
4688
|
`);
|
|
4274
4689
|
return false;
|
|
4275
4690
|
}
|
|
@@ -4288,15 +4703,16 @@ function refuseBeforeRun(base, marker, detail, markerText) {
|
|
|
4288
4703
|
}
|
|
4289
4704
|
async function processItem(planned, settings, silent) {
|
|
4290
4705
|
progress(`
|
|
4291
|
-
|
|
4706
|
+
${itemLabel(planned)} ${planned.turn}: ${planned.reason}
|
|
4292
4707
|
`);
|
|
4293
4708
|
const refreshed = refreshItem(planned, settings);
|
|
4294
4709
|
if (refreshed.kind === "skip") {
|
|
4295
4710
|
progress(` skipped: ${refreshed.detail}
|
|
4296
4711
|
`);
|
|
4297
4712
|
return {
|
|
4298
|
-
issue: planned.issue
|
|
4299
|
-
|
|
4713
|
+
issue: planned.issue?.number ?? null,
|
|
4714
|
+
pr: planned.pr?.number ?? null,
|
|
4715
|
+
title: itemTitle(planned),
|
|
4300
4716
|
turn: planned.turn,
|
|
4301
4717
|
outcome: "skipped",
|
|
4302
4718
|
detail: `no longer actionable: ${refreshed.detail}`
|
|
@@ -4308,8 +4724,9 @@ async function processItem(planned, settings, silent) {
|
|
|
4308
4724
|
`);
|
|
4309
4725
|
}
|
|
4310
4726
|
const base = {
|
|
4311
|
-
issue: item.issue
|
|
4312
|
-
|
|
4727
|
+
issue: item.issue?.number ?? null,
|
|
4728
|
+
pr: item.pr?.number ?? null,
|
|
4729
|
+
title: itemTitle(item),
|
|
4313
4730
|
turn: item.turn
|
|
4314
4731
|
};
|
|
4315
4732
|
const prepared = item.turn === "issue-discuss" ? prepareBaseBranch(item.branch) : preparePrBranch(item.branch);
|
|
@@ -4320,9 +4737,9 @@ async function processItem(planned, settings, silent) {
|
|
|
4320
4737
|
}
|
|
4321
4738
|
claimIssue(item, settings);
|
|
4322
4739
|
let marker;
|
|
4323
|
-
const
|
|
4740
|
+
const markerTarget = markerSurfaceTarget(item);
|
|
4324
4741
|
try {
|
|
4325
|
-
marker = postMarker(
|
|
4742
|
+
marker = postMarker(markerTarget.surface, markerTarget.number, "automata do-work: working\u2026");
|
|
4326
4743
|
} catch (err) {
|
|
4327
4744
|
progress(` skipped: could not post the working marker \u2014 ${err.message}
|
|
4328
4745
|
`);
|
|
@@ -4336,7 +4753,7 @@ async function processItem(planned, settings, silent) {
|
|
|
4336
4753
|
}
|
|
4337
4754
|
let buriedByNote = 0;
|
|
4338
4755
|
if (note !== null) {
|
|
4339
|
-
progress(` noted on issue #${String(item.issue
|
|
4756
|
+
progress(` noted on issue #${String(item.issue?.number ?? 0)} that the work is on pull request #${String(item.pr?.number ?? 0)}.
|
|
4340
4757
|
`);
|
|
4341
4758
|
buriedByNote = reportIssueMessagesBuriedByNote(item, note, settings.participants);
|
|
4342
4759
|
}
|
|
@@ -4421,13 +4838,14 @@ function summarize(reports) {
|
|
|
4421
4838
|
}
|
|
4422
4839
|
for (const report of reports) {
|
|
4423
4840
|
const ran = report.execution === void 0 ? "" : ` \xB7 ${describeExecution(report.execution)}`;
|
|
4424
|
-
out(`
|
|
4841
|
+
out(` ${reportLabel(report)} ${report.turn ?? "-"} ${report.outcome} \u2014 ${report.detail}${ran}
|
|
4425
4842
|
`);
|
|
4426
4843
|
}
|
|
4427
4844
|
}
|
|
4428
4845
|
function toItemJson(report) {
|
|
4429
4846
|
return {
|
|
4430
4847
|
issue: report.issue,
|
|
4848
|
+
pr: report.pr,
|
|
4431
4849
|
title: report.title,
|
|
4432
4850
|
turn: report.turn,
|
|
4433
4851
|
outcome: report.outcome,
|
|
@@ -4442,23 +4860,30 @@ function toItemJson(report) {
|
|
|
4442
4860
|
};
|
|
4443
4861
|
}
|
|
4444
4862
|
var doWorkCommand = new Command6("do-work").description(
|
|
4445
|
-
"Run one tick of the autonomous loop:
|
|
4863
|
+
"Run one tick of the autonomous loop: answer the issues whose newest authorized message the agent has not answered, then the open pull requests that close no issue of this repository and have one"
|
|
4446
4864
|
).option("--with <executor>", "Executor to use: claude or codex (default: from config, else claude)").option("--model <string>", "Model identifier to pass to the executor, overriding the configured default for it").option(
|
|
4447
4865
|
"--effort <level>",
|
|
4448
4866
|
"Reasoning effort to pass to the executor, overriding the configured default for it"
|
|
4449
|
-
).option("--issue <number>", "Restrict the tick to a single issue").option(
|
|
4867
|
+
).option("--issue <number>", "Restrict the tick to a single issue").option(
|
|
4868
|
+
"--pr <number>",
|
|
4869
|
+
"Restrict the orphan-pull-request pass to a single pull request (one that closes no issue of this repository)"
|
|
4870
|
+
).option("--limit <n>", "Maximum number of issues to fetch", "10").option("--max-runs <n>", "Maximum number of model runs this tick").option(
|
|
4450
4871
|
"--dry-run",
|
|
4451
4872
|
"Print the work plan, plus a summary and the exact command that would be launched for each item, and exit without changing anything"
|
|
4452
4873
|
).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) => {
|
|
4874
|
+
const startedAt = Date.now();
|
|
4875
|
+
if (options.dryRun !== true) loggableInvocation = { startedAt };
|
|
4453
4876
|
const settings = resolveSettings(options);
|
|
4454
4877
|
if (options.dryRun === true) {
|
|
4455
|
-
const exitCode2 = await runTick(settings, options);
|
|
4878
|
+
const { exitCode: exitCode2 } = await runTick(settings, options);
|
|
4456
4879
|
if (exitCode2 !== 0) process.exit(exitCode2);
|
|
4457
4880
|
return;
|
|
4458
4881
|
}
|
|
4882
|
+
loggableInvocation = null;
|
|
4459
4883
|
const lock = acquireRunLock("do-work", settings.lockStaleMinutes);
|
|
4460
4884
|
if (!lock.ok) {
|
|
4461
4885
|
const exitCode2 = reportLockHeld(lock, settings, options);
|
|
4886
|
+
logTick([], exitCode2, startedAt, "lock-held");
|
|
4462
4887
|
if (exitCode2 !== 0) process.exit(exitCode2);
|
|
4463
4888
|
return;
|
|
4464
4889
|
}
|
|
@@ -4483,8 +4908,11 @@ var doWorkCommand = new Command6("do-work").description(
|
|
|
4483
4908
|
process.once("SIGINT", onSignal);
|
|
4484
4909
|
process.once("SIGTERM", onSignal);
|
|
4485
4910
|
let exitCode;
|
|
4911
|
+
let reports = [];
|
|
4486
4912
|
try {
|
|
4487
|
-
|
|
4913
|
+
const result = await runTick(settings, options);
|
|
4914
|
+
exitCode = result.exitCode;
|
|
4915
|
+
reports = result.reports;
|
|
4488
4916
|
} catch (err) {
|
|
4489
4917
|
process.stderr.write(`Error: ${err.message}
|
|
4490
4918
|
`);
|
|
@@ -4494,8 +4922,39 @@ var doWorkCommand = new Command6("do-work").description(
|
|
|
4494
4922
|
process.removeListener("SIGINT", onSignal);
|
|
4495
4923
|
process.removeListener("SIGTERM", onSignal);
|
|
4496
4924
|
}
|
|
4925
|
+
logTick(reports, exitCode, startedAt);
|
|
4497
4926
|
if (exitCode !== 0) process.exit(exitCode);
|
|
4498
4927
|
});
|
|
4928
|
+
function toTickLogItem(report) {
|
|
4929
|
+
return {
|
|
4930
|
+
subject: reportLabel(report),
|
|
4931
|
+
turn: report.turn,
|
|
4932
|
+
outcome: report.outcome,
|
|
4933
|
+
detail: report.detail,
|
|
4934
|
+
ranExecutor: report.ranExecutor ?? false,
|
|
4935
|
+
executor: report.execution?.executor,
|
|
4936
|
+
model: report.execution?.model,
|
|
4937
|
+
effort: report.execution?.effort
|
|
4938
|
+
};
|
|
4939
|
+
}
|
|
4940
|
+
function logTick(reports, exitCode, startedAt, note) {
|
|
4941
|
+
let repo;
|
|
4942
|
+
try {
|
|
4943
|
+
const slug = getRepoSlug();
|
|
4944
|
+
repo = `${slug.owner}/${slug.repo}`;
|
|
4945
|
+
} catch {
|
|
4946
|
+
repo = null;
|
|
4947
|
+
}
|
|
4948
|
+
recordTick({
|
|
4949
|
+
command: "do-work",
|
|
4950
|
+
repo,
|
|
4951
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
4952
|
+
durationMs: Date.now() - startedAt,
|
|
4953
|
+
exitCode,
|
|
4954
|
+
note,
|
|
4955
|
+
items: reports.map(toTickLogItem)
|
|
4956
|
+
});
|
|
4957
|
+
}
|
|
4499
4958
|
function reportLockHeld(lock, settings, options) {
|
|
4500
4959
|
const held = lock.heldBy;
|
|
4501
4960
|
const sentence = `Another automata instance is already running here (pid ${String(held.pid)} on ${held.host}, started ${held.startedAt}, command ${held.command}). Doing nothing.
|
|
@@ -4532,19 +4991,24 @@ async function runTick(settings, options) {
|
|
|
4532
4991
|
baseBranch: settings.baseBranch,
|
|
4533
4992
|
protectedBranches: settings.protectedBranches,
|
|
4534
4993
|
dryRun: options.dryRun === true,
|
|
4994
|
+
agentUser: settings.participants.agentUser,
|
|
4535
4995
|
log: progress
|
|
4536
4996
|
});
|
|
4537
4997
|
const issues = discoverIssues(settings);
|
|
4538
4998
|
const linkMap = getOpenPrLinkMap();
|
|
4539
|
-
const
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4999
|
+
const policy = {
|
|
5000
|
+
baseBranch: settings.baseBranch,
|
|
5001
|
+
defaultBranch: linkMap.defaultBranch,
|
|
5002
|
+
protectedBranches: settings.protectedBranches
|
|
5003
|
+
};
|
|
5004
|
+
const decisions = skipDuplicateHeadBranches([
|
|
5005
|
+
...issues.map((issue) => decideWork(buildIssueState(issue, linkMap), settings.participants, policy)),
|
|
5006
|
+
...discoverOrphanPrs(settings, linkMap).map(
|
|
5007
|
+
(candidate) => decideOrphanPrWork({ prSurface: getPrSurface(candidate.pr.number) }, settings.participants, policy)
|
|
5008
|
+
)
|
|
5009
|
+
]);
|
|
4546
5010
|
const items = decisions.flatMap((decision) => decision.kind === "work" ? [decision.item] : []);
|
|
4547
|
-
const planText = `Work plan (${String(items.length)} of ${String(
|
|
5011
|
+
const planText = `Work plan (${String(items.length)} of ${String(decisions.length)} candidates need an answer):
|
|
4548
5012
|
${describePlan(decisions)}`;
|
|
4549
5013
|
if (options.json) {
|
|
4550
5014
|
progress(planText);
|
|
@@ -4553,7 +5017,7 @@ ${describePlan(decisions)}`;
|
|
|
4553
5017
|
}
|
|
4554
5018
|
if (options.dryRun) {
|
|
4555
5019
|
reportDryRun(items, decisions, settings, options, hygiene);
|
|
4556
|
-
return 0;
|
|
5020
|
+
return { exitCode: 0, reports: [] };
|
|
4557
5021
|
}
|
|
4558
5022
|
const reports = [];
|
|
4559
5023
|
const deferred = [];
|
|
@@ -4570,8 +5034,9 @@ ${describePlan(decisions)}`;
|
|
|
4570
5034
|
progress(` failed: ${err.message}
|
|
4571
5035
|
`);
|
|
4572
5036
|
report = {
|
|
4573
|
-
issue: item.issue
|
|
4574
|
-
|
|
5037
|
+
issue: item.issue?.number ?? null,
|
|
5038
|
+
pr: item.pr?.number ?? null,
|
|
5039
|
+
title: itemTitle(item),
|
|
4575
5040
|
turn: item.turn,
|
|
4576
5041
|
outcome: "failed",
|
|
4577
5042
|
detail: err.message
|
|
@@ -4582,11 +5047,12 @@ ${describePlan(decisions)}`;
|
|
|
4582
5047
|
}
|
|
4583
5048
|
for (const item of deferred) {
|
|
4584
5049
|
progress(`
|
|
4585
|
-
|
|
5050
|
+
${itemLabel(item)} deferred: --max-runs / maxRunsPerTick reached.
|
|
4586
5051
|
`);
|
|
4587
5052
|
reports.push({
|
|
4588
|
-
issue: item.issue
|
|
4589
|
-
|
|
5053
|
+
issue: item.issue?.number ?? null,
|
|
5054
|
+
pr: item.pr?.number ?? null,
|
|
5055
|
+
title: itemTitle(item),
|
|
4590
5056
|
turn: item.turn,
|
|
4591
5057
|
outcome: "deferred",
|
|
4592
5058
|
detail: `run cap of ${String(settings.maxRuns)} reached`
|
|
@@ -4612,12 +5078,13 @@ ${describePlan(decisions)}`;
|
|
|
4612
5078
|
summarizeHygiene(hygiene);
|
|
4613
5079
|
summarize(reports);
|
|
4614
5080
|
}
|
|
4615
|
-
return exitCode;
|
|
5081
|
+
return { exitCode, reports };
|
|
4616
5082
|
}
|
|
4617
5083
|
function toRunJson(entry) {
|
|
4618
5084
|
if (entry.kind === "refused") {
|
|
4619
5085
|
return {
|
|
4620
|
-
issue: entry.item.issue
|
|
5086
|
+
issue: entry.item.issue?.number ?? null,
|
|
5087
|
+
pr: entry.item.pr?.number ?? null,
|
|
4621
5088
|
turn: entry.item.turn,
|
|
4622
5089
|
executor: null,
|
|
4623
5090
|
model: null,
|
|
@@ -4633,7 +5100,8 @@ function toRunJson(entry) {
|
|
|
4633
5100
|
};
|
|
4634
5101
|
}
|
|
4635
5102
|
return {
|
|
4636
|
-
issue: entry.item.issue
|
|
5103
|
+
issue: entry.item.issue?.number ?? null,
|
|
5104
|
+
pr: entry.item.pr?.number ?? null,
|
|
4637
5105
|
turn: entry.item.turn,
|
|
4638
5106
|
executor: entry.execution.executor,
|
|
4639
5107
|
model: entry.execution.model ?? null,
|
|
@@ -4743,8 +5211,9 @@ function toHygieneJson(hygiene) {
|
|
|
4743
5211
|
function toPlanJson(decision) {
|
|
4744
5212
|
if (decision.kind === "skip") {
|
|
4745
5213
|
return {
|
|
4746
|
-
issue: decision.issue
|
|
4747
|
-
|
|
5214
|
+
issue: decision.issue?.number ?? null,
|
|
5215
|
+
pr: decision.pr?.number ?? null,
|
|
5216
|
+
title: decision.issue?.title ?? decision.pr?.title ?? "",
|
|
4748
5217
|
turn: null,
|
|
4749
5218
|
skipReason: decision.reason,
|
|
4750
5219
|
reason: decision.detail
|
|
@@ -4752,11 +5221,11 @@ function toPlanJson(decision) {
|
|
|
4752
5221
|
}
|
|
4753
5222
|
const item = decision.item;
|
|
4754
5223
|
return {
|
|
4755
|
-
issue: item.issue
|
|
4756
|
-
|
|
5224
|
+
issue: item.issue?.number ?? null,
|
|
5225
|
+
pr: item.pr?.number ?? null,
|
|
5226
|
+
title: itemTitle(item),
|
|
4757
5227
|
turn: item.turn,
|
|
4758
5228
|
branch: item.branch,
|
|
4759
|
-
pr: item.pr?.number ?? null,
|
|
4760
5229
|
needsAssignment: item.needsAssignment,
|
|
4761
5230
|
reason: item.reason
|
|
4762
5231
|
};
|