automata-cli 0.1.0-feature-030-do-work.214 → 0.1.0-feature-030-do-work.218

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