automata-cli 0.6.0-develop.296 → 0.6.0
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;
|
|
@@ -3859,7 +4020,7 @@ function formatWorkRecord(tick) {
|
|
|
3859
4020
|
const header = `=== ${tick.timestamp.toISOString()} ${repoField(tick.repo)} ===
|
|
3860
4021
|
`;
|
|
3861
4022
|
const lines = ran.map(
|
|
3862
|
-
(item) =>
|
|
4023
|
+
(item) => `${oneLine(item.subject)} ${item.turn === null ? "-" : oneLine(item.turn)} ${item.outcome}${describeItemExecution(item)} \u2014 ${briefDetail(item.detail)}
|
|
3863
4024
|
`
|
|
3864
4025
|
);
|
|
3865
4026
|
return header + lines.join("") + "\n";
|
|
@@ -3943,6 +4104,19 @@ function recordTick(tick, dir = operationLogDirectory()) {
|
|
|
3943
4104
|
|
|
3944
4105
|
// src/commands/doWork.ts
|
|
3945
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
|
+
}
|
|
3946
4120
|
function out(message) {
|
|
3947
4121
|
process.stdout.write(message);
|
|
3948
4122
|
}
|
|
@@ -4029,11 +4203,13 @@ function resolveSettings(options) {
|
|
|
4029
4203
|
participants: { allowedUsers, agentUser },
|
|
4030
4204
|
prompts: {
|
|
4031
4205
|
"issue-discuss": doWork.prompts?.issueDiscuss ?? DEFAULT_DO_WORK_ISSUE_DISCUSS_PROMPT,
|
|
4032
|
-
"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
|
|
4033
4208
|
},
|
|
4034
4209
|
technique: config.issueDiscoveryTechnique,
|
|
4035
4210
|
discoveryValue: config.issueDiscoveryValue,
|
|
4036
|
-
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")
|
|
4037
4213
|
};
|
|
4038
4214
|
}
|
|
4039
4215
|
function checkAuthenticatedIdentity(agentUser, allowedUsers) {
|
|
@@ -4097,12 +4273,12 @@ function planRun(item, settings, execution) {
|
|
|
4097
4273
|
}
|
|
4098
4274
|
function describePlannedRun(item, settings, run5, execution) {
|
|
4099
4275
|
const rule = "\u2500".repeat(72);
|
|
4100
|
-
const branchAction = item.turn === "
|
|
4276
|
+
const branchAction = item.turn === "issue-discuss" ? " and pull" : " and fast-forward";
|
|
4101
4277
|
const assignment = item.needsAssignment ? `would assign to ${settings.participants.agentUser}` : "already assigned";
|
|
4102
|
-
const markerTarget =
|
|
4278
|
+
const markerTarget = markerSurfaceLabel(item);
|
|
4103
4279
|
const lines = [
|
|
4104
4280
|
rule,
|
|
4105
|
-
|
|
4281
|
+
describeSubject(item),
|
|
4106
4282
|
rule,
|
|
4107
4283
|
` Turn ${item.turn}`,
|
|
4108
4284
|
` Why ${item.reason}`,
|
|
@@ -4125,11 +4301,14 @@ function describePlannedRun(item, settings, run5, execution) {
|
|
|
4125
4301
|
];
|
|
4126
4302
|
return lines.join("\n") + "\n";
|
|
4127
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
|
+
}
|
|
4128
4307
|
function describeRefusedRun(item, refusal) {
|
|
4129
4308
|
const rule = "\u2500".repeat(72);
|
|
4130
4309
|
return [
|
|
4131
4310
|
rule,
|
|
4132
|
-
|
|
4311
|
+
describeSubject(item),
|
|
4133
4312
|
rule,
|
|
4134
4313
|
` Turn ${item.turn}`,
|
|
4135
4314
|
` Why ${item.reason}`,
|
|
@@ -4170,7 +4349,7 @@ function validateDoWorkConfig(section) {
|
|
|
4170
4349
|
validateProtectedBranches(section["protectedBranches"]);
|
|
4171
4350
|
validateSettingContainer(section["models"], "models", ["claude", "codex"]);
|
|
4172
4351
|
validateSettingContainer(section["effort"], "effort", ["claude", "codex"]);
|
|
4173
|
-
validateSettingContainer(section["prompts"], "prompts", ["issueDiscuss", "prWork"]);
|
|
4352
|
+
validateSettingContainer(section["prompts"], "prompts", ["issueDiscuss", "prWork", "prOrphan"]);
|
|
4174
4353
|
}
|
|
4175
4354
|
function validateProtectedBranches(value) {
|
|
4176
4355
|
if (value === void 0 || value === null) return;
|
|
@@ -4193,7 +4372,14 @@ function validateSettingContainer(value, container, keys) {
|
|
|
4193
4372
|
}
|
|
4194
4373
|
}
|
|
4195
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
|
+
}
|
|
4196
4381
|
function discoverIssues(settings) {
|
|
4382
|
+
if (!issuePassEnabled(settings)) return [];
|
|
4197
4383
|
const candidates = listCandidateIssues(settings.technique, settings.discoveryValue, settings.limit);
|
|
4198
4384
|
if (settings.onlyIssue === void 0) {
|
|
4199
4385
|
if (candidates.length === settings.limit) {
|
|
@@ -4215,16 +4401,57 @@ function discoverIssues(settings) {
|
|
|
4215
4401
|
}
|
|
4216
4402
|
return [surface.issue];
|
|
4217
4403
|
}
|
|
4218
|
-
function
|
|
4404
|
+
function matchesDiscoveryFilter(subject, settings) {
|
|
4219
4405
|
const value = settings.discoveryValue.toLowerCase();
|
|
4220
4406
|
switch (settings.technique) {
|
|
4221
4407
|
case "label":
|
|
4222
|
-
return
|
|
4408
|
+
return subject.labels.some((label) => label.toLowerCase() === value);
|
|
4223
4409
|
case "assignee":
|
|
4224
|
-
return
|
|
4410
|
+
return subject.assignees.some((assignee) => assignee.toLowerCase() === value);
|
|
4225
4411
|
case "title-contains":
|
|
4226
|
-
return
|
|
4412
|
+
return subject.title.toLowerCase().includes(value);
|
|
4413
|
+
}
|
|
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
|
+
);
|
|
4227
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];
|
|
4228
4455
|
}
|
|
4229
4456
|
function buildIssueState(issue, linkMap) {
|
|
4230
4457
|
const issueSurface = getIssueSurface(issue.number);
|
|
@@ -4239,16 +4466,16 @@ function buildIssueState(issue, linkMap) {
|
|
|
4239
4466
|
function describePlan(decisions) {
|
|
4240
4467
|
const lines = decisions.map((decision) => {
|
|
4241
4468
|
if (decision.kind === "skip") {
|
|
4242
|
-
return `
|
|
4469
|
+
return ` ${subjectLabel(decision.issue?.number ?? null, decision.pr?.number ?? null)} nothing to do \u2014 ${decision.detail}`;
|
|
4243
4470
|
}
|
|
4244
4471
|
const item = decision.item;
|
|
4245
4472
|
const claim = item.needsAssignment ? ", will assign to the agent" : "";
|
|
4246
|
-
return `
|
|
4473
|
+
return ` ${itemLabel(item)} ${item.turn} on ${item.branch} \u2014 ${item.reason}${claim}`;
|
|
4247
4474
|
});
|
|
4248
|
-
return lines.length === 0 ? " (
|
|
4475
|
+
return lines.length === 0 ? " (nothing matched the discovery filter)\n" : lines.join("\n") + "\n";
|
|
4249
4476
|
}
|
|
4250
4477
|
function claimIssue(item, settings) {
|
|
4251
|
-
if (!item.needsAssignment) return;
|
|
4478
|
+
if (!item.needsAssignment || item.issue === null) return;
|
|
4252
4479
|
try {
|
|
4253
4480
|
assignIssueToAgent(item.issue.number, settings.participants.agentUser);
|
|
4254
4481
|
progress(` assigned issue #${String(item.issue.number)} to ${settings.participants.agentUser}.
|
|
@@ -4259,7 +4486,8 @@ function claimIssue(item, settings) {
|
|
|
4259
4486
|
}
|
|
4260
4487
|
}
|
|
4261
4488
|
function notePickupOnIssue(item, marker) {
|
|
4262
|
-
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;
|
|
4263
4491
|
try {
|
|
4264
4492
|
return postMarker(
|
|
4265
4493
|
"issue",
|
|
@@ -4279,6 +4507,7 @@ function notePickupOnIssue(item, marker) {
|
|
|
4279
4507
|
}
|
|
4280
4508
|
}
|
|
4281
4509
|
function reportIssueMessagesBuriedByNote(item, note, participants) {
|
|
4510
|
+
if (item.issue === null) return 0;
|
|
4282
4511
|
const watermark = promptWatermark([item.issueAnalysis.messages]);
|
|
4283
4512
|
let buried;
|
|
4284
4513
|
try {
|
|
@@ -4305,24 +4534,43 @@ function reportIssueMessagesBuriedByNote(item, note, participants) {
|
|
|
4305
4534
|
}
|
|
4306
4535
|
function refreshItem(item, settings) {
|
|
4307
4536
|
const linkMap = getOpenPrLinkMap();
|
|
4308
|
-
|
|
4537
|
+
const policy = {
|
|
4309
4538
|
baseBranch: settings.baseBranch,
|
|
4310
4539
|
defaultBranch: linkMap.defaultBranch,
|
|
4311
4540
|
protectedBranches: settings.protectedBranches
|
|
4312
|
-
}
|
|
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);
|
|
4313
4559
|
}
|
|
4314
4560
|
function readAnsweringSurface(item) {
|
|
4315
|
-
|
|
4316
|
-
|
|
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)];
|
|
4317
4565
|
}
|
|
4318
|
-
|
|
4319
|
-
return [...surface.messages, ...surface.threads.flatMap((thread) => thread.comments)];
|
|
4320
|
-
}
|
|
4321
|
-
function markerSurfaceLabel(item) {
|
|
4322
|
-
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;
|
|
4323
4567
|
}
|
|
4324
4568
|
function markerSurfaceTarget(item) {
|
|
4325
|
-
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)}`;
|
|
4326
4574
|
}
|
|
4327
4575
|
function reportOvertakenMessages(item, analysis) {
|
|
4328
4576
|
if (analysis.missed.length === 0) return;
|
|
@@ -4346,7 +4594,7 @@ function reconcileMarker(item, marker, participants, watermark, runError) {
|
|
|
4346
4594
|
analysis = analyseAnswer(readAnsweringSurface(item), participants, marker, watermark);
|
|
4347
4595
|
} catch (err) {
|
|
4348
4596
|
progress(
|
|
4349
|
-
` warning: could not re-read
|
|
4597
|
+
` warning: could not re-read ${markerSurfaceLabel(item)} to check for an answer: ${err.message}
|
|
4350
4598
|
`
|
|
4351
4599
|
);
|
|
4352
4600
|
return reportUnverified(item, marker);
|
|
@@ -4395,7 +4643,7 @@ function reportNoAnswer(item, marker, runError) {
|
|
|
4395
4643
|
updateMarker(marker, explanation);
|
|
4396
4644
|
} catch (err) {
|
|
4397
4645
|
progress(
|
|
4398
|
-
` warning: could not update the marker comment on
|
|
4646
|
+
` warning: could not update the marker comment on ${surface}: ${err.message}
|
|
4399
4647
|
the humans have not been told that this run produced no answer.
|
|
4400
4648
|
`
|
|
4401
4649
|
);
|
|
@@ -4410,31 +4658,33 @@ async function invokeExecutor(prompt, execution, silent) {
|
|
|
4410
4658
|
await runClaude(prompt, { model: execution.model, effort: execution.effort, printSteps: !silent });
|
|
4411
4659
|
}
|
|
4412
4660
|
function repairIssueLink(item, baseBranch) {
|
|
4661
|
+
const issue = item.issue;
|
|
4662
|
+
if (issue === null) return false;
|
|
4413
4663
|
try {
|
|
4414
4664
|
const branch = getCurrentBranch();
|
|
4415
4665
|
if (branch === baseBranch) {
|
|
4416
|
-
progress(` issue #${String(
|
|
4666
|
+
progress(` issue #${String(issue.number)} is still in discussion (no branch was created).
|
|
4417
4667
|
`);
|
|
4418
4668
|
return false;
|
|
4419
4669
|
}
|
|
4420
4670
|
const pr = getCurrentBranchPr();
|
|
4421
4671
|
if (!pr) {
|
|
4422
|
-
progress(` issue #${String(
|
|
4672
|
+
progress(` issue #${String(issue.number)} is still in discussion (no pull request).
|
|
4423
4673
|
`);
|
|
4424
4674
|
return false;
|
|
4425
4675
|
}
|
|
4426
|
-
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");
|
|
4427
4677
|
if (closesRef.test(pr.body)) {
|
|
4428
|
-
progress(` pull request #${String(pr.number)} already closes issue #${String(
|
|
4678
|
+
progress(` pull request #${String(pr.number)} already closes issue #${String(issue.number)}.
|
|
4429
4679
|
`);
|
|
4430
4680
|
return true;
|
|
4431
4681
|
}
|
|
4432
|
-
addClosesRefToPr(pr.number,
|
|
4433
|
-
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)}.
|
|
4434
4684
|
`);
|
|
4435
4685
|
return true;
|
|
4436
4686
|
} catch (err) {
|
|
4437
|
-
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}
|
|
4438
4688
|
`);
|
|
4439
4689
|
return false;
|
|
4440
4690
|
}
|
|
@@ -4453,15 +4703,16 @@ function refuseBeforeRun(base, marker, detail, markerText) {
|
|
|
4453
4703
|
}
|
|
4454
4704
|
async function processItem(planned, settings, silent) {
|
|
4455
4705
|
progress(`
|
|
4456
|
-
|
|
4706
|
+
${itemLabel(planned)} ${planned.turn}: ${planned.reason}
|
|
4457
4707
|
`);
|
|
4458
4708
|
const refreshed = refreshItem(planned, settings);
|
|
4459
4709
|
if (refreshed.kind === "skip") {
|
|
4460
4710
|
progress(` skipped: ${refreshed.detail}
|
|
4461
4711
|
`);
|
|
4462
4712
|
return {
|
|
4463
|
-
issue: planned.issue
|
|
4464
|
-
|
|
4713
|
+
issue: planned.issue?.number ?? null,
|
|
4714
|
+
pr: planned.pr?.number ?? null,
|
|
4715
|
+
title: itemTitle(planned),
|
|
4465
4716
|
turn: planned.turn,
|
|
4466
4717
|
outcome: "skipped",
|
|
4467
4718
|
detail: `no longer actionable: ${refreshed.detail}`
|
|
@@ -4473,8 +4724,9 @@ async function processItem(planned, settings, silent) {
|
|
|
4473
4724
|
`);
|
|
4474
4725
|
}
|
|
4475
4726
|
const base = {
|
|
4476
|
-
issue: item.issue
|
|
4477
|
-
|
|
4727
|
+
issue: item.issue?.number ?? null,
|
|
4728
|
+
pr: item.pr?.number ?? null,
|
|
4729
|
+
title: itemTitle(item),
|
|
4478
4730
|
turn: item.turn
|
|
4479
4731
|
};
|
|
4480
4732
|
const prepared = item.turn === "issue-discuss" ? prepareBaseBranch(item.branch) : preparePrBranch(item.branch);
|
|
@@ -4485,9 +4737,9 @@ async function processItem(planned, settings, silent) {
|
|
|
4485
4737
|
}
|
|
4486
4738
|
claimIssue(item, settings);
|
|
4487
4739
|
let marker;
|
|
4488
|
-
const
|
|
4740
|
+
const markerTarget = markerSurfaceTarget(item);
|
|
4489
4741
|
try {
|
|
4490
|
-
marker = postMarker(
|
|
4742
|
+
marker = postMarker(markerTarget.surface, markerTarget.number, "automata do-work: working\u2026");
|
|
4491
4743
|
} catch (err) {
|
|
4492
4744
|
progress(` skipped: could not post the working marker \u2014 ${err.message}
|
|
4493
4745
|
`);
|
|
@@ -4501,7 +4753,7 @@ async function processItem(planned, settings, silent) {
|
|
|
4501
4753
|
}
|
|
4502
4754
|
let buriedByNote = 0;
|
|
4503
4755
|
if (note !== null) {
|
|
4504
|
-
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)}.
|
|
4505
4757
|
`);
|
|
4506
4758
|
buriedByNote = reportIssueMessagesBuriedByNote(item, note, settings.participants);
|
|
4507
4759
|
}
|
|
@@ -4586,13 +4838,14 @@ function summarize(reports) {
|
|
|
4586
4838
|
}
|
|
4587
4839
|
for (const report of reports) {
|
|
4588
4840
|
const ran = report.execution === void 0 ? "" : ` \xB7 ${describeExecution(report.execution)}`;
|
|
4589
|
-
out(`
|
|
4841
|
+
out(` ${reportLabel(report)} ${report.turn ?? "-"} ${report.outcome} \u2014 ${report.detail}${ran}
|
|
4590
4842
|
`);
|
|
4591
4843
|
}
|
|
4592
4844
|
}
|
|
4593
4845
|
function toItemJson(report) {
|
|
4594
4846
|
return {
|
|
4595
4847
|
issue: report.issue,
|
|
4848
|
+
pr: report.pr,
|
|
4596
4849
|
title: report.title,
|
|
4597
4850
|
turn: report.turn,
|
|
4598
4851
|
outcome: report.outcome,
|
|
@@ -4607,11 +4860,14 @@ function toItemJson(report) {
|
|
|
4607
4860
|
};
|
|
4608
4861
|
}
|
|
4609
4862
|
var doWorkCommand = new Command6("do-work").description(
|
|
4610
|
-
"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"
|
|
4611
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(
|
|
4612
4865
|
"--effort <level>",
|
|
4613
4866
|
"Reasoning effort to pass to the executor, overriding the configured default for it"
|
|
4614
|
-
).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(
|
|
4615
4871
|
"--dry-run",
|
|
4616
4872
|
"Print the work plan, plus a summary and the exact command that would be launched for each item, and exit without changing anything"
|
|
4617
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) => {
|
|
@@ -4671,7 +4927,7 @@ var doWorkCommand = new Command6("do-work").description(
|
|
|
4671
4927
|
});
|
|
4672
4928
|
function toTickLogItem(report) {
|
|
4673
4929
|
return {
|
|
4674
|
-
|
|
4930
|
+
subject: reportLabel(report),
|
|
4675
4931
|
turn: report.turn,
|
|
4676
4932
|
outcome: report.outcome,
|
|
4677
4933
|
detail: report.detail,
|
|
@@ -4735,19 +4991,24 @@ async function runTick(settings, options) {
|
|
|
4735
4991
|
baseBranch: settings.baseBranch,
|
|
4736
4992
|
protectedBranches: settings.protectedBranches,
|
|
4737
4993
|
dryRun: options.dryRun === true,
|
|
4994
|
+
agentUser: settings.participants.agentUser,
|
|
4738
4995
|
log: progress
|
|
4739
4996
|
});
|
|
4740
4997
|
const issues = discoverIssues(settings);
|
|
4741
4998
|
const linkMap = getOpenPrLinkMap();
|
|
4742
|
-
const
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
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
|
+
]);
|
|
4749
5010
|
const items = decisions.flatMap((decision) => decision.kind === "work" ? [decision.item] : []);
|
|
4750
|
-
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):
|
|
4751
5012
|
${describePlan(decisions)}`;
|
|
4752
5013
|
if (options.json) {
|
|
4753
5014
|
progress(planText);
|
|
@@ -4773,8 +5034,9 @@ ${describePlan(decisions)}`;
|
|
|
4773
5034
|
progress(` failed: ${err.message}
|
|
4774
5035
|
`);
|
|
4775
5036
|
report = {
|
|
4776
|
-
issue: item.issue
|
|
4777
|
-
|
|
5037
|
+
issue: item.issue?.number ?? null,
|
|
5038
|
+
pr: item.pr?.number ?? null,
|
|
5039
|
+
title: itemTitle(item),
|
|
4778
5040
|
turn: item.turn,
|
|
4779
5041
|
outcome: "failed",
|
|
4780
5042
|
detail: err.message
|
|
@@ -4785,11 +5047,12 @@ ${describePlan(decisions)}`;
|
|
|
4785
5047
|
}
|
|
4786
5048
|
for (const item of deferred) {
|
|
4787
5049
|
progress(`
|
|
4788
|
-
|
|
5050
|
+
${itemLabel(item)} deferred: --max-runs / maxRunsPerTick reached.
|
|
4789
5051
|
`);
|
|
4790
5052
|
reports.push({
|
|
4791
|
-
issue: item.issue
|
|
4792
|
-
|
|
5053
|
+
issue: item.issue?.number ?? null,
|
|
5054
|
+
pr: item.pr?.number ?? null,
|
|
5055
|
+
title: itemTitle(item),
|
|
4793
5056
|
turn: item.turn,
|
|
4794
5057
|
outcome: "deferred",
|
|
4795
5058
|
detail: `run cap of ${String(settings.maxRuns)} reached`
|
|
@@ -4820,7 +5083,8 @@ ${describePlan(decisions)}`;
|
|
|
4820
5083
|
function toRunJson(entry) {
|
|
4821
5084
|
if (entry.kind === "refused") {
|
|
4822
5085
|
return {
|
|
4823
|
-
issue: entry.item.issue
|
|
5086
|
+
issue: entry.item.issue?.number ?? null,
|
|
5087
|
+
pr: entry.item.pr?.number ?? null,
|
|
4824
5088
|
turn: entry.item.turn,
|
|
4825
5089
|
executor: null,
|
|
4826
5090
|
model: null,
|
|
@@ -4836,7 +5100,8 @@ function toRunJson(entry) {
|
|
|
4836
5100
|
};
|
|
4837
5101
|
}
|
|
4838
5102
|
return {
|
|
4839
|
-
issue: entry.item.issue
|
|
5103
|
+
issue: entry.item.issue?.number ?? null,
|
|
5104
|
+
pr: entry.item.pr?.number ?? null,
|
|
4840
5105
|
turn: entry.item.turn,
|
|
4841
5106
|
executor: entry.execution.executor,
|
|
4842
5107
|
model: entry.execution.model ?? null,
|
|
@@ -4946,8 +5211,9 @@ function toHygieneJson(hygiene) {
|
|
|
4946
5211
|
function toPlanJson(decision) {
|
|
4947
5212
|
if (decision.kind === "skip") {
|
|
4948
5213
|
return {
|
|
4949
|
-
issue: decision.issue
|
|
4950
|
-
|
|
5214
|
+
issue: decision.issue?.number ?? null,
|
|
5215
|
+
pr: decision.pr?.number ?? null,
|
|
5216
|
+
title: decision.issue?.title ?? decision.pr?.title ?? "",
|
|
4951
5217
|
turn: null,
|
|
4952
5218
|
skipReason: decision.reason,
|
|
4953
5219
|
reason: decision.detail
|
|
@@ -4955,11 +5221,11 @@ function toPlanJson(decision) {
|
|
|
4955
5221
|
}
|
|
4956
5222
|
const item = decision.item;
|
|
4957
5223
|
return {
|
|
4958
|
-
issue: item.issue
|
|
4959
|
-
|
|
5224
|
+
issue: item.issue?.number ?? null,
|
|
5225
|
+
pr: item.pr?.number ?? null,
|
|
5226
|
+
title: itemTitle(item),
|
|
4960
5227
|
turn: item.turn,
|
|
4961
5228
|
branch: item.branch,
|
|
4962
|
-
pr: item.pr?.number ?? null,
|
|
4963
5229
|
needsAssignment: item.needsAssignment,
|
|
4964
5230
|
reason: item.reason
|
|
4965
5231
|
};
|