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-LTCVLM4I.js";
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-LTCVLM4I.js";
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
- if (turnKind === "issue-discuss") {
202
- prompts.issueDiscuss = prompt;
203
- } else {
204
- prompts.prWork = prompt;
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-HDHKPY6J.js")
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 agentThreadMessages = surface.threads.flatMap((thread) => thread.comments).filter((comment) => comment.author.toLowerCase() === p.agentUser.toLowerCase());
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
- lines.push(
3051
- "",
3052
- "Full conversation on the issue (authorized accounts and you only, oldest first):",
3053
- "",
3054
- formatMessages(item.issueAnalysis.messages)
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
- return { ok: false, reason: "pull-failed", detail: pull.stderr };
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 detectRescueTarget(baseBranch, now) {
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(baseBranch);
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.baseBranch, now);
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(` rescue FAILED to push ${target.branch}: ${pushed.stderr}
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) => `#${String(item.issue)} ${item.turn === null ? "-" : oneLine(item.turn)} ${item.outcome}${describeItemExecution(item)} \u2014 ${briefDetail(item.detail)}
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 === "pr-work" ? " and fast-forward" : " and pull";
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 = item.turn === "pr-work" && item.pr ? `pull request #${String(item.pr.number)}` : `issue #${String(item.issue.number)}`;
4278
+ const markerTarget = markerSurfaceLabel(item);
4103
4279
  const lines = [
4104
4280
  rule,
4105
- `Issue #${String(item.issue.number)} \u2014 ${item.issue.title}`,
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
- `Issue #${String(item.issue.number)} \u2014 ${item.issue.title}`,
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 issueMatchesFilter(surface, settings) {
4404
+ function matchesDiscoveryFilter(subject, settings) {
4219
4405
  const value = settings.discoveryValue.toLowerCase();
4220
4406
  switch (settings.technique) {
4221
4407
  case "label":
4222
- return surface.labels.some((label) => label.toLowerCase() === value);
4408
+ return subject.labels.some((label) => label.toLowerCase() === value);
4223
4409
  case "assignee":
4224
- return surface.assignees.some((assignee) => assignee.toLowerCase() === value);
4410
+ return subject.assignees.some((assignee) => assignee.toLowerCase() === value);
4225
4411
  case "title-contains":
4226
- return surface.issue.title.toLowerCase().includes(value);
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 ` #${String(decision.issue.number)} nothing to do \u2014 ${decision.detail}`;
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 ` #${String(item.issue.number)} ${item.turn} on ${item.branch} \u2014 ${item.reason}${claim}`;
4473
+ return ` ${itemLabel(item)} ${item.turn} on ${item.branch} \u2014 ${item.reason}${claim}`;
4247
4474
  });
4248
- return lines.length === 0 ? " (no issues matched the discovery filter)\n" : lines.join("\n") + "\n";
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 || !item.issueAnalysis.hasNewMessage) return 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
- return decideWork(buildIssueState(item.issue, linkMap), settings.participants, {
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
- if (item.turn === "issue-discuss" || item.pr === null) {
4316
- return getIssueSurface(item.issue.number).messages;
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
- const surface = getPrSurface(item.pr.number);
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.turn === "pr-work" && item.pr ? { surface: "pr", number: item.pr.number } : { surface: "issue", number: item.issue.number };
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 issue #${String(item.issue.number)} to check for an answer: ${err.message}
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 issue #${String(item.issue.number)}: ${err.message}
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(item.issue.number)} is still in discussion (no branch was created).
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(item.issue.number)} is still in discussion (no pull request).
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(item.issue.number) + String.raw`\b`, "i");
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(item.issue.number)}.
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, item.issue.number);
4433
- progress(` linked pull request #${String(pr.number)} to issue #${String(item.issue.number)}.
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(item.issue.number)}: ${err.message}
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
- #${String(planned.issue.number)} ${planned.turn}: ${planned.reason}
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.number,
4464
- title: planned.issue.title,
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.number,
4477
- title: item.issue.title,
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 markerSurface = item.turn === "pr-work" && item.pr ? item.pr.number : item.issue.number;
4740
+ const markerTarget = markerSurfaceTarget(item);
4489
4741
  try {
4490
- marker = postMarker(item.turn === "pr-work" ? "pr" : "issue", markerSurface, "automata do-work: working\u2026");
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.number)} that the work is on pull request #${String(item.pr?.number ?? 0)}.
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(` #${String(report.issue)} ${report.turn ?? "-"} ${report.outcome} \u2014 ${report.detail}${ran}
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: find the issues whose newest authorized message the agent has not answered, and answer them"
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("--limit <n>", "Maximum number of issues to fetch", "10").option("--max-runs <n>", "Maximum number of model runs this tick").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
- issue: report.issue,
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 decisions = issues.map(
4743
- (issue) => decideWork(buildIssueState(issue, linkMap), settings.participants, {
4744
- baseBranch: settings.baseBranch,
4745
- defaultBranch: linkMap.defaultBranch,
4746
- protectedBranches: settings.protectedBranches
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(issues.length)} issues need an answer):
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.number,
4777
- title: item.issue.title,
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
- #${String(item.issue.number)} deferred: --max-runs / maxRunsPerTick reached.
5050
+ ${itemLabel(item)} deferred: --max-runs / maxRunsPerTick reached.
4789
5051
  `);
4790
5052
  reports.push({
4791
- issue: item.issue.number,
4792
- title: item.issue.title,
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.number,
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.number,
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.number,
4950
- title: decision.issue.title,
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.number,
4959
- title: item.issue.title,
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "automata-cli",
3
- "version": "0.6.0-develop.296",
3
+ "version": "0.6.0",
4
4
  "description": "Automata CLI tool",
5
5
  "type": "module",
6
6
  "engines": {