automata-cli 0.11.0-develop.496 → 0.11.0-develop.514

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.
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  DEFAULT_CHECK_ISSUE_PROMPT,
4
+ DEFAULT_CONDUCTOR_ISSUE_PROMPT,
5
+ DEFAULT_CONDUCTOR_PR_PROMPT,
4
6
  DEFAULT_DO_WORK,
5
7
  DEFAULT_DO_WORK_ISSUE_DISCUSS_PROMPT,
6
8
  DEFAULT_DO_WORK_PR_ORPHAN_PROMPT,
@@ -10,7 +12,7 @@ import {
10
12
  readConfig,
11
13
  readRawConfig,
12
14
  writeConfig
13
- } from "./chunk-DTZB5FOY.js";
15
+ } from "./chunk-TSOUCFDW.js";
14
16
 
15
17
  // src/config/ConfigWizard.tsx
16
18
  import { useState } from "react";
@@ -56,7 +58,9 @@ var PROMPTS_MENU_OPTIONS = [
56
58
  "Check-Issue",
57
59
  "Do Work \u2014 Discuss",
58
60
  "Do Work \u2014 PR",
59
- "Do Work \u2014 Orphan PR"
61
+ "Do Work \u2014 Orphan PR",
62
+ "Conductor \u2014 Issue",
63
+ "Conductor \u2014 PR"
60
64
  ];
61
65
  function parseAllowedUsers(value) {
62
66
  return value.split(",").map((user) => user.trim()).filter((user) => user.length > 0);
@@ -137,7 +141,9 @@ var PROMPT_SCREEN_BY_OPTION = {
137
141
  "Check-Issue": "check-issue-prompt",
138
142
  "Do Work \u2014 Discuss": "do-work-discuss-prompt",
139
143
  "Do Work \u2014 PR": "do-work-pr-prompt",
140
- "Do Work \u2014 Orphan PR": "do-work-pr-orphan-prompt"
144
+ "Do Work \u2014 Orphan PR": "do-work-pr-orphan-prompt",
145
+ "Conductor \u2014 Issue": "conductor-issue-prompt",
146
+ "Conductor \u2014 PR": "conductor-pr-prompt"
141
147
  };
142
148
  function ConfigWizard() {
143
149
  const existing = readConfig();
@@ -202,6 +208,12 @@ function ConfigWizard() {
202
208
  const [doWorkPrOrphanPrompt, setDoWorkPrOrphanPrompt] = useState(
203
209
  existing.doWork?.prompts?.prOrphan ?? DEFAULT_DO_WORK_PR_ORPHAN_PROMPT
204
210
  );
211
+ const [conductorIssuePrompt, setConductorIssuePrompt] = useState(
212
+ existing.conductor?.prompts?.issue ?? DEFAULT_CONDUCTOR_ISSUE_PROMPT
213
+ );
214
+ const [conductorPrPrompt, setConductorPrPrompt] = useState(
215
+ existing.conductor?.prompts?.pr ?? DEFAULT_CONDUCTOR_PR_PROMPT
216
+ );
205
217
  const [gitTrunkBranch, setGitTrunkBranch] = useState(existing.git?.trunkBranch ?? "");
206
218
  const initialReleaseFlowIndex = RELEASE_FLOW_OPTIONS.findIndex((o) => o.value === existing.git?.releaseFlow);
207
219
  const [releaseFlowIndex, setReleaseFlowIndex] = useState(Math.max(initialReleaseFlowIndex, 0));
@@ -391,6 +403,28 @@ function ConfigWizard() {
391
403
  },
392
404
  onBack: () => setScreen("prompts-menu")
393
405
  },
406
+ "conductor-issue-prompt": {
407
+ setValue: setConductorIssuePrompt,
408
+ onSubmit: () => {
409
+ savePrompt("conductor-issue.md", conductorIssuePrompt, (value, current) => ({
410
+ ...current,
411
+ conductor: { ...current.conductor, prompts: { ...current.conductor?.prompts, issue: value } }
412
+ }));
413
+ setScreen("prompts-menu");
414
+ },
415
+ onBack: () => setScreen("prompts-menu")
416
+ },
417
+ "conductor-pr-prompt": {
418
+ setValue: setConductorPrPrompt,
419
+ onSubmit: () => {
420
+ savePrompt("conductor-pr.md", conductorPrPrompt, (value, current) => ({
421
+ ...current,
422
+ conductor: { ...current.conductor, prompts: { ...current.conductor?.prompts, pr: value } }
423
+ }));
424
+ setScreen("prompts-menu");
425
+ },
426
+ onBack: () => setScreen("prompts-menu")
427
+ },
394
428
  "git-trunk-branch": {
395
429
  setValue: setGitTrunkBranch,
396
430
  onSubmit: () => setScreen("git-release-flow"),
@@ -628,6 +662,18 @@ function ConfigWizard() {
628
662
  value: doWorkPrOrphanPrompt,
629
663
  hint: `Type prompt \xB7 Enter to save \xB7 ${BACK}`
630
664
  },
665
+ "conductor-issue-prompt": {
666
+ title: "Prompts \u2014 Conductor \u2014 Issue",
667
+ label: "Instructions for the conductor on a watched issue:",
668
+ value: conductorIssuePrompt,
669
+ hint: `Type prompt \xB7 Enter to save \xB7 ${BACK}`
670
+ },
671
+ "conductor-pr-prompt": {
672
+ title: "Prompts \u2014 Conductor \u2014 PR",
673
+ label: "Instructions for the conductor on a watched pull request:",
674
+ value: conductorPrPrompt,
675
+ hint: `Type prompt \xB7 Enter to save \xB7 ${BACK}`
676
+ },
631
677
  "git-trunk-branch": {
632
678
  title: "Git \u2014 Trunk Branch",
633
679
  label: "Branch publish-release releases to (blank = detect it from the remote):",
@@ -19,6 +19,8 @@ var DEFAULT_DO_WORK = {
19
19
  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 in the repository, 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.\n\nPost the reply yourself with the GitHub CLI: write it to a temporary file outside the repository (the only file you may create in this case) and run `gh issue comment <issue-number> --body-file <file>`, or `gh pr comment <pr-number> --body-file <file>` when you are replying on a pull request. `do-work` does not publish your output, so printing the answer to stdout does not count as a posted reply.";
20
20
  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\nOnce a review thread is fixed and pushed, resolve it (GraphQL `resolveReviewThread`) after replying; leave open any thread you did not fix. Reply 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.";
21
21
  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\nOnce a review thread is fixed and pushed, resolve it (GraphQL `resolveReviewThread`) after replying; leave open any thread you did not fix. Reply 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.";
22
+ var DEFAULT_CONDUCTOR_ISSUE_PROMPT = "You are the conductor: you act for the people allowed to instruct the agent named in the context below. The agent spoke last on the thread below, and no allowed user has answered yet.\n\nRead the whole thread \u2014 the issue, its pull requests, the unresolved review threads and the CI status. Decide what the agent needs next: an answer to its question, a decision, or a correction. Write the next message to the agent, short and concrete. Do not change any file, do not merge or close anything, and do not push to any branch.";
23
+ var DEFAULT_CONDUCTOR_PR_PROMPT = "You are the conductor: you act for the people allowed to instruct the agent named in the context below. The agent spoke last on the pull request below, and no allowed user has answered yet.\n\nRead the whole thread \u2014 the pull request, the unresolved review threads and the CI status. Decide what the agent needs next: an answer to its question, a decision on a review thread, or a correction. A failing check is the agent's work to fix; name it if the agent has not. Write the next message to the agent, short and concrete. Do not change any file, do not merge or close the pull request, and do not push to any branch.";
22
24
  var CONFIG_DIR = ".automata";
23
25
  var CONFIG_FILE = "config.json";
24
26
  function configPath() {
@@ -87,6 +89,12 @@ function readConfig() {
87
89
  if (config.doWork?.prompts?.prOrphan) {
88
90
  config.doWork.prompts.prOrphan = resolvePromptRef(config.doWork.prompts.prOrphan, dir);
89
91
  }
92
+ if (config.conductor?.prompts?.issue) {
93
+ config.conductor.prompts.issue = resolvePromptRef(config.conductor.prompts.issue, dir);
94
+ }
95
+ if (config.conductor?.prompts?.pr) {
96
+ config.conductor.prompts.pr = resolvePromptRef(config.conductor.prompts.pr, dir);
97
+ }
90
98
  return config;
91
99
  }
92
100
  function writeConfig(config) {
@@ -104,6 +112,8 @@ export {
104
112
  DEFAULT_DO_WORK_ISSUE_DISCUSS_PROMPT,
105
113
  DEFAULT_DO_WORK_PR_WORK_PROMPT,
106
114
  DEFAULT_DO_WORK_PR_ORPHAN_PROMPT,
115
+ DEFAULT_CONDUCTOR_ISSUE_PROMPT,
116
+ DEFAULT_CONDUCTOR_PR_PROMPT,
107
117
  readRawConfig,
108
118
  readConfig,
109
119
  writeConfig
package/dist/index.js CHANGED
@@ -2,6 +2,8 @@
2
2
  import {
3
3
  DEFAULT_CHECK_ISSUE_PROMPT,
4
4
  DEFAULT_CLAUDE_SYSTEM_PROMPT,
5
+ DEFAULT_CONDUCTOR_ISSUE_PROMPT,
6
+ DEFAULT_CONDUCTOR_PR_PROMPT,
5
7
  DEFAULT_DO_WORK,
6
8
  DEFAULT_DO_WORK_ISSUE_DISCUSS_PROMPT,
7
9
  DEFAULT_DO_WORK_PR_ORPHAN_PROMPT,
@@ -11,7 +13,7 @@ import {
11
13
  readConfig,
12
14
  readRawConfig,
13
15
  writeConfig
14
- } from "./chunk-DTZB5FOY.js";
16
+ } from "./chunk-TSOUCFDW.js";
15
17
 
16
18
  // src/index.ts
17
19
  import { Command as Command8 } from "commander";
@@ -293,6 +295,65 @@ var configSetDoWorkPrompt = new Command("do-work-prompt").description("Set the t
293
295
  process.stdout.write(`do-work ${turnKind} prompt set.
294
296
  `);
295
297
  });
298
+ var CONDUCTOR_PROMPT_KINDS = ["issue", "pr"];
299
+ var configSetConductorPrompt = new Command("conductor-prompt").description("Set the instructions for the conductor on a watched issue or pull request (prompt text or a .md filename)").argument("<kind>", `Kind of watched item: ${CONDUCTOR_PROMPT_KINDS.join(", ")}`).argument("<value>", "Prompt text, or a plain .md filename inside .automata/").action((kind, value) => {
300
+ if (!CONDUCTOR_PROMPT_KINDS.includes(kind)) {
301
+ process.stderr.write(
302
+ `Error: invalid kind "${kind}". Must be one of: ${CONDUCTOR_PROMPT_KINDS.join(", ")}
303
+ `
304
+ );
305
+ process.exit(1);
306
+ }
307
+ const prompt = value.trim();
308
+ if (prompt.length === 0) {
309
+ process.stderr.write("Error: conductor-prompt requires a non-empty value.\n");
310
+ process.exit(1);
311
+ }
312
+ const current = readRawConfig();
313
+ const prompts = { ...current.conductor?.prompts, [kind]: prompt };
314
+ writeConfig({ ...current, conductor: { ...current.conductor, prompts } });
315
+ process.stdout.write(`conductor ${kind} prompt set.
316
+ `);
317
+ });
318
+ function writeConductor(patch) {
319
+ const current = readRawConfig();
320
+ writeConfig({ ...current, conductor: { ...current.conductor, ...patch } });
321
+ }
322
+ function requireExecutor(value) {
323
+ if (!VALID_EXECUTORS.includes(value)) {
324
+ process.stderr.write(`Error: invalid executor "${value}". Must be one of: ${VALID_EXECUTORS.join(", ")}
325
+ `);
326
+ process.exit(1);
327
+ }
328
+ return value;
329
+ }
330
+ var configSetConductorExecutor = new Command("conductor-executor").description("Set the executor the conductor runs").argument("<value>", `Executor: ${VALID_EXECUTORS.join(", ")}`).action((value) => {
331
+ writeConductor({ executor: requireExecutor(value) });
332
+ process.stdout.write(`conductor executor set to: ${value}
333
+ `);
334
+ });
335
+ var configSetConductorModel = new Command("conductor-model").description("Set the model the conductor passes to one executor").argument("<executor>", `Executor: ${VALID_EXECUTORS.join(", ")}`).argument("<value>", "Model identifier").action((executor, value) => {
336
+ const which = requireExecutor(executor);
337
+ const model = value.trim();
338
+ if (model.length === 0) {
339
+ process.stderr.write("Error: conductor-model requires a non-empty model identifier.\n");
340
+ process.exit(1);
341
+ }
342
+ writeConductor({ models: { ...readRawConfig().conductor?.models, [which]: model } });
343
+ process.stdout.write(`conductor ${which} model set to: ${model}
344
+ `);
345
+ });
346
+ var configSetConductorEffort = new Command("conductor-effort").description("Set the reasoning effort the conductor passes to one executor").argument("<executor>", `Executor: ${VALID_EXECUTORS.join(", ")}`).argument("<value>", "Effort level, forwarded to the executor unchanged").action((executor, value) => {
347
+ const which = requireExecutor(executor);
348
+ const effort = value.trim();
349
+ if (effort.length === 0) {
350
+ process.stderr.write("Error: conductor-effort requires a non-empty effort level.\n");
351
+ process.exit(1);
352
+ }
353
+ writeConductor({ effort: { ...readRawConfig().conductor?.effort, [which]: effort } });
354
+ process.stdout.write(`conductor ${which} effort set to: ${effort}
355
+ `);
356
+ });
296
357
  var configSetGitTrunkBranch = new Command("git-trunk-branch").description("Pin the branch `publish-release` releases to, instead of detecting it").argument("<value>", "Branch name (unset = detect it from the remote)").action((value) => {
297
358
  const branch = value.trim();
298
359
  if (branch.length === 0) {
@@ -316,12 +377,12 @@ var configSetGitReleaseFlow = new Command("git-release-flow").description("Pin t
316
377
  process.stdout.write(`git release flow set to: ${flow}
317
378
  `);
318
379
  });
319
- 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(configSetDoWorkEffort).addCommand(configSetDoWorkMaxRuns).addCommand(configSetDoWorkLockStaleMinutes).addCommand(configSetDoWorkDumpOnBlock).addCommand(configSetDoWorkPostRunLog).addCommand(configSetDoWorkPrompt).addCommand(configSetGitTrunkBranch).addCommand(configSetGitReleaseFlow);
380
+ 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(configSetDoWorkEffort).addCommand(configSetDoWorkMaxRuns).addCommand(configSetDoWorkLockStaleMinutes).addCommand(configSetDoWorkDumpOnBlock).addCommand(configSetDoWorkPostRunLog).addCommand(configSetDoWorkPrompt).addCommand(configSetConductorPrompt).addCommand(configSetConductorExecutor).addCommand(configSetConductorModel).addCommand(configSetConductorEffort).addCommand(configSetGitTrunkBranch).addCommand(configSetGitReleaseFlow);
320
381
  var configCommand = new Command("config").description("Configure automata settings").addCommand(configSet).action(async () => {
321
382
  const [{ render }, React, { ConfigWizard }] = await Promise.all([
322
383
  import("ink"),
323
384
  import("react"),
324
- import("./ConfigWizard-KI43QOGG.js")
385
+ import("./ConfigWizard-BYPYEVVN.js")
325
386
  ]);
326
387
  const { waitUntilExit } = render(React.createElement(ConfigWizard));
327
388
  await waitUntilExit();
@@ -372,8 +433,8 @@ function extractIdentity(stdout) {
372
433
  if (typeof id === "string" && id) return id;
373
434
  if (id && typeof id === "object") {
374
435
  const o = id;
375
- for (const key of ["displayName", "uniqueName", "providerDisplayName", "id"]) {
376
- if (typeof o[key] === "string" && o[key]) return o[key];
436
+ for (const key2 of ["displayName", "uniqueName", "providerDisplayName", "id"]) {
437
+ if (typeof o[key2] === "string" && o[key2]) return o[key2];
377
438
  }
378
439
  }
379
440
  } catch {
@@ -2520,9 +2581,23 @@ function getPrSurface(prNumber) {
2520
2581
  },
2521
2582
  assignees: (raw.assignees ?? []).map(login).filter((name) => name.length > 0),
2522
2583
  messages,
2523
- threads
2584
+ threads,
2585
+ description: { author: login(raw.author), body: raw.body }
2524
2586
  };
2525
2587
  }
2588
+ function getPrChecks(prNumber) {
2589
+ const raw = ghJson(
2590
+ ["pr", "view", String(prNumber), "--json", "statusCheckRollup"],
2591
+ `read the checks of pull request #${String(prNumber)}`
2592
+ );
2593
+ return (raw.statusCheckRollup ?? []).map((check) => {
2594
+ const name = check.name ?? check.context ?? "unnamed check";
2595
+ if (check.status !== void 0) return { name, status: check.status, conclusion: check.conclusion ?? null };
2596
+ const state = check.state ?? "";
2597
+ const pending = state === "PENDING" || state === "EXPECTED";
2598
+ return { name, status: pending ? "PENDING" : "COMPLETED", conclusion: pending || state === "" ? null : state };
2599
+ });
2600
+ }
2526
2601
  function assignIssueToAgent(issueNumber, agentUser) {
2527
2602
  const { stderr, status } = run4("gh", [
2528
2603
  "issue",
@@ -2800,9 +2875,47 @@ async function terminateTrackedChildren(timeoutMs = 1e4, killGraceMs = 5e3) {
2800
2875
  }
2801
2876
 
2802
2877
  // src/claude/claudeService.ts
2878
+ function claudeReadOnlyTools(replyTo) {
2879
+ const tools = [
2880
+ "Read(./**)",
2881
+ "Grep(./**)",
2882
+ "Glob(./**)",
2883
+ "Bash(gh issue view:*)",
2884
+ "Bash(gh pr view:*)",
2885
+ "Bash(gh pr diff:*)",
2886
+ "Bash(gh pr checks:*)",
2887
+ "Bash(git status:*)"
2888
+ ];
2889
+ if (replyTo) {
2890
+ const command = replyTo.kind === "pr" ? "pr" : "issue";
2891
+ tools.push(`Bash(gh ${command} comment ${String(replyTo.number)} --body-file -:*)`);
2892
+ }
2893
+ return tools;
2894
+ }
2895
+ var CLAUDE_READ_ONLY_DENIED = [
2896
+ "Edit",
2897
+ "Write",
2898
+ "NotebookEdit",
2899
+ "Bash(gh * --repo*)",
2900
+ "Bash(gh * -R*)"
2901
+ ];
2902
+ function readOnlyEnv(ghRepo) {
2903
+ return ghRepo ? { ...process.env, GH_REPO: ghRepo } : process.env;
2904
+ }
2803
2905
  function buildClaudeArgs(prompt, options = {}) {
2804
2906
  const args = [];
2805
- if (options.yolo) args.push("--dangerously-skip-permissions");
2907
+ if (options.readOnly) {
2908
+ args.push(
2909
+ "--permission-mode",
2910
+ "dontAsk",
2911
+ "--allowed-tools",
2912
+ claudeReadOnlyTools(options.replyTo).join(","),
2913
+ "--disallowed-tools",
2914
+ CLAUDE_READ_ONLY_DENIED.join(",")
2915
+ );
2916
+ } else if (options.yolo) {
2917
+ args.push("--dangerously-skip-permissions");
2918
+ }
2806
2919
  if (options.model) args.push("--model", options.model);
2807
2920
  if (options.effort) args.push("--effort", options.effort);
2808
2921
  if (options.verbose) args.push("--verbose", "--output-format", "stream-json");
@@ -2814,12 +2927,17 @@ function runClaude(prompt, options = {}) {
2814
2927
  const claudeBin = resolveCommand("claude");
2815
2928
  const args = buildClaudeArgs(prompt, {
2816
2929
  yolo: true,
2930
+ readOnly: options.readOnly,
2931
+ replyTo: options.replyTo,
2817
2932
  model: options.model,
2818
2933
  effort: options.effort,
2819
2934
  verbose: true
2820
2935
  });
2821
2936
  const sink2 = options.sink;
2822
- const child = spawn(claudeBin, args, { stdio: ["inherit", "pipe", "pipe"] });
2937
+ const child = spawn(claudeBin, args, {
2938
+ stdio: ["inherit", "pipe", "pipe"],
2939
+ env: options.readOnly ? readOnlyEnv(options.ghRepo) : process.env
2940
+ });
2823
2941
  trackChild(child);
2824
2942
  child.stderr.on("data", (chunk) => {
2825
2943
  process.stderr.write(chunk);
@@ -2861,21 +2979,21 @@ function runClaude(prompt, options = {}) {
2861
2979
  }
2862
2980
  function invokeClaudeCode(prompt, options = {}) {
2863
2981
  if (options.verbose) {
2864
- return invokeClaudeCodeVerbose(prompt, options.yolo ?? false, options.model, options.effort);
2982
+ return invokeClaudeCodeVerbose(prompt, options);
2865
2983
  }
2866
- invokeClaudeCodeSync(prompt, options.yolo ?? false, options.model, options.effort);
2984
+ invokeClaudeCodeSync(prompt, options);
2867
2985
  }
2868
- function invokeClaudeCodeSync(prompt, yolo, model, effort) {
2986
+ function invokeClaudeCodeSync(prompt, options) {
2869
2987
  const claudeBin = resolveCommand("claude");
2870
- const args = buildClaudeArgs(prompt, { yolo, model, effort, verbose: false });
2988
+ const args = buildClaudeArgs(prompt, { ...options, verbose: false });
2871
2989
  const result = spawnSync6(claudeBin, args, { encoding: "utf8", stdio: "inherit" });
2872
2990
  handleSpawnError(result.error, "claude");
2873
2991
  handleExitCode(result.status, "Claude Code");
2874
2992
  }
2875
- function invokeClaudeCodeVerbose(prompt, yolo, model, effort) {
2993
+ function invokeClaudeCodeVerbose(prompt, options) {
2876
2994
  return new Promise((resolve2) => {
2877
2995
  const claudeBin = resolveCommand("claude");
2878
- const args = buildClaudeArgs(prompt, { yolo, model, effort, verbose: true });
2996
+ const args = buildClaudeArgs(prompt, { ...options, verbose: true });
2879
2997
  const child = spawn(claudeBin, args, { stdio: ["inherit", "pipe", "inherit"] });
2880
2998
  const rl = createInterface({ input: child.stdout });
2881
2999
  let turnCount = 0;
@@ -2980,7 +3098,8 @@ function toTomlBasicString(value) {
2980
3098
  }
2981
3099
  function buildCodexArgs(prompt, options = {}) {
2982
3100
  const args = ["exec"];
2983
- if (options.yolo) args.push("--dangerously-bypass-approvals-and-sandbox");
3101
+ if (options.readOnly) args.push("--sandbox", "read-only");
3102
+ else if (options.yolo) args.push("--dangerously-bypass-approvals-and-sandbox");
2984
3103
  if (options.model) args.push("--model", options.model);
2985
3104
  if (options.effort) args.push("-c", `model_reasoning_effort=${toTomlBasicString(options.effort)}`);
2986
3105
  args.push(prompt);
@@ -2990,11 +3109,11 @@ function invokeCodexCode(prompt, options = {}) {
2990
3109
  if (options.verbose) {
2991
3110
  process.stderr.write("Warning: --verbose is not supported for Codex and will be ignored.\n");
2992
3111
  }
2993
- invokeCodexCodeSync(prompt, options.yolo ?? false, options.model, options.effort);
3112
+ invokeCodexCodeSync(prompt, options);
2994
3113
  }
2995
- function invokeCodexCodeSync(prompt, yolo, model, effort) {
3114
+ function invokeCodexCodeSync(prompt, options) {
2996
3115
  const codexBin = resolveCommand("codex");
2997
- const args = buildCodexArgs(prompt, { yolo, model, effort });
3116
+ const args = buildCodexArgs(prompt, options);
2998
3117
  const result = spawnSync7(codexBin, args, { encoding: "utf8", stdio: "inherit" });
2999
3118
  handleSpawnError(result.error, "codex");
3000
3119
  handleExitCode(result.status, "Codex");
@@ -3002,12 +3121,20 @@ function invokeCodexCodeSync(prompt, yolo, model, effort) {
3002
3121
  function runCodex(prompt, options = {}) {
3003
3122
  return new Promise((resolve2, reject) => {
3004
3123
  const codexBin = resolveCommand("codex");
3005
- const args = buildCodexArgs(prompt, { yolo: true, model: options.model, effort: options.effort });
3124
+ const args = buildCodexArgs(prompt, {
3125
+ yolo: true,
3126
+ readOnly: options.readOnly,
3127
+ model: options.model,
3128
+ effort: options.effort
3129
+ });
3006
3130
  const sink2 = options.sink;
3007
- const child = spawn2(codexBin, args, { stdio: ["inherit", "pipe", "pipe"] });
3131
+ const child = spawn2(codexBin, args, {
3132
+ stdio: ["inherit", "pipe", "pipe"],
3133
+ env: options.readOnly ? readOnlyEnv(options.ghRepo) : process.env
3134
+ });
3008
3135
  trackChild(child);
3009
3136
  child.stdout.on("data", (chunk) => {
3010
- process.stdout.write(chunk);
3137
+ if (options.quiet !== true) process.stdout.write(chunk);
3011
3138
  sink2?.output("stdout", chunk.toString("utf8"));
3012
3139
  });
3013
3140
  child.stderr.on("data", (chunk) => {
@@ -3863,6 +3990,12 @@ function selectLinkedPr(linkedPrs) {
3863
3990
  const open = linkedPrs.filter((pr) => pr.state === "OPEN");
3864
3991
  return open.length === 0 ? null : newestPr(open);
3865
3992
  }
3993
+ function agentAnsweredAfter(messages, agentUser, marker) {
3994
+ const agent = agentUser.toLowerCase();
3995
+ return messages.some(
3996
+ (message) => message.author.toLowerCase() === agent && message.kind !== "issue-body" && message.createdAt > marker.createdAt
3997
+ );
3998
+ }
3866
3999
  function claimStates(item) {
3867
4000
  const states = [];
3868
4001
  if (item.issue !== null) {
@@ -5419,9 +5552,9 @@ function summarizeSync(items) {
5419
5552
  const counts = /* @__PURE__ */ new Map();
5420
5553
  for (const item of items) {
5421
5554
  if (item.sync === void 0) continue;
5422
- const key = oneLine(item.sync);
5423
- if (key.length === 0 || QUIET_SYNC.has(key)) continue;
5424
- counts.set(key, (counts.get(key) ?? 0) + 1);
5555
+ const key2 = oneLine(item.sync);
5556
+ if (key2.length === 0 || QUIET_SYNC.has(key2)) continue;
5557
+ counts.set(key2, (counts.get(key2) ?? 0) + 1);
5425
5558
  }
5426
5559
  if (counts.size === 0) return null;
5427
5560
  return [...counts].map(([strategy, count]) => `${strategy}:${String(count)}`).join(",");
@@ -5670,9 +5803,9 @@ function parseExecution(bracket) {
5670
5803
  if (bracket === void 0) return { executor: null, model: null, effort: null };
5671
5804
  const parts = bracket.split(" ").filter((part) => part.length > 0);
5672
5805
  const executor = parts[0] ?? null;
5673
- const find = (key) => {
5674
- const hit = parts.find((part) => part.startsWith(`${key}=`));
5675
- return hit === void 0 ? null : hit.slice(key.length + 1);
5806
+ const find = (key2) => {
5807
+ const hit = parts.find((part) => part.startsWith(`${key2}=`));
5808
+ return hit === void 0 ? null : hit.slice(key2.length + 1);
5676
5809
  };
5677
5810
  return { executor, model: find("model"), effort: find("effort") };
5678
5811
  }
@@ -6685,15 +6818,15 @@ function describeRefusedRun(item, refusal) {
6685
6818
  function isPlainObject(value) {
6686
6819
  return typeof value === "object" && value !== null && !Array.isArray(value);
6687
6820
  }
6688
- function validateOptionalString(container, key, path) {
6689
- const value = container[key];
6821
+ function validateOptionalString(container, key2, path) {
6822
+ const value = container[key2];
6690
6823
  if (value === void 0 || value === null) return;
6691
6824
  if (typeof value !== "string" || value.trim().length === 0) {
6692
6825
  failSettings(`${path} must be a non-empty string.`);
6693
6826
  }
6694
6827
  }
6695
- function validateOptionalInt(container, key, path, min, hint) {
6696
- const value = container[key];
6828
+ function validateOptionalInt(container, key2, path, min, hint) {
6829
+ const value = container[key2];
6697
6830
  if (value === void 0 || value === null) return;
6698
6831
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value < min) {
6699
6832
  failSettings(`${path} must be ${hint}, got ${JSON.stringify(value)}.`);
@@ -6718,8 +6851,8 @@ function validateDoWorkConfig(section) {
6718
6851
  validateSettingContainer(section["effort"], "effort", ["claude", "codex"]);
6719
6852
  validateSettingContainer(section["prompts"], "prompts", ["issueDiscuss", "prWork", "prOrphan"]);
6720
6853
  }
6721
- function validateOptionalBoolean(container, key, path) {
6722
- const value = container[key];
6854
+ function validateOptionalBoolean(container, key2, path) {
6855
+ const value = container[key2];
6723
6856
  if (value === void 0 || value === null) return;
6724
6857
  if (typeof value !== "boolean") {
6725
6858
  failSettings(`${path} must be true or false, got ${JSON.stringify(value)}.`);
@@ -6737,12 +6870,12 @@ function validateSettingContainer(value, container, keys) {
6737
6870
  if (!isPlainObject(value)) {
6738
6871
  failSettings(`doWork.${container} must be an object, got ${JSON.stringify(value)}.`);
6739
6872
  }
6740
- for (const key of keys) {
6741
- validateOptionalString(value, key, `doWork.${container}.${key}`);
6873
+ for (const key2 of keys) {
6874
+ validateOptionalString(value, key2, `doWork.${container}.${key2}`);
6742
6875
  }
6743
- for (const key of Object.keys(value)) {
6744
- if (!keys.includes(key)) {
6745
- failSettings(`doWork.${container}.${key} is not a recognised setting; expected one of: ${keys.join(", ")}.`);
6876
+ for (const key2 of Object.keys(value)) {
6877
+ if (!keys.includes(key2)) {
6878
+ failSettings(`doWork.${container}.${key2} is not a recognised setting; expected one of: ${keys.join(", ")}.`);
6746
6879
  }
6747
6880
  }
6748
6881
  }
@@ -8290,6 +8423,249 @@ import { Command as Command7 } from "commander";
8290
8423
  function isWatchClosed(state) {
8291
8424
  return state === "closed";
8292
8425
  }
8426
+ function issueConversation(surface) {
8427
+ return {
8428
+ kind: "issue",
8429
+ number: surface.issue.number,
8430
+ state: surface.state === "CLOSED" ? "closed" : "open",
8431
+ messages: surface.messages
8432
+ };
8433
+ }
8434
+ function prConversation(surface) {
8435
+ return {
8436
+ kind: "pr",
8437
+ number: surface.pr.number,
8438
+ state: surface.pr.state === "OPEN" ? "open" : "closed",
8439
+ messages: [...surface.messages, ...surface.threads.flatMap((thread) => thread.comments)]
8440
+ };
8441
+ }
8442
+ function decideConversation(conversation, p) {
8443
+ if (isWatchClosed(conversation.state)) {
8444
+ return { kind: "skip", surface: conversation, reason: "closed", detail: "the item is closed or merged" };
8445
+ }
8446
+ const agent = p.agentUser.toLowerCase();
8447
+ const allowed = new Set(p.allowedUsers.map((user) => user.toLowerCase()));
8448
+ const relevant = conversation.messages.filter((message) => {
8449
+ const author = message.author.toLowerCase();
8450
+ return author === agent || allowed.has(author);
8451
+ });
8452
+ let newest2 = null;
8453
+ for (const message of relevant) {
8454
+ if (message.author.toLowerCase() !== agent) continue;
8455
+ if (newest2 === null || message.createdAt > newest2.createdAt) newest2 = message;
8456
+ }
8457
+ if (newest2 === null) {
8458
+ return {
8459
+ kind: "skip",
8460
+ surface: conversation,
8461
+ reason: "no-agent-message",
8462
+ detail: `${p.agentUser} has not written here`
8463
+ };
8464
+ }
8465
+ const marker = { commentId: "", createdAt: newest2.createdAt };
8466
+ const answeredBy = p.allowedUsers.find((user) => agentAnsweredAfter(relevant, user, marker));
8467
+ if (answeredBy !== void 0) {
8468
+ return {
8469
+ kind: "skip",
8470
+ surface: conversation,
8471
+ reason: "answered",
8472
+ detail: `${answeredBy} answered after ${p.agentUser}'s message at ${newest2.createdAt}`
8473
+ };
8474
+ }
8475
+ return {
8476
+ kind: "reply",
8477
+ surface: conversation,
8478
+ agentMessageAt: newest2.createdAt,
8479
+ reason: `${p.agentUser}'s message at ${newest2.createdAt} has no answer from an allowed user`
8480
+ };
8481
+ }
8482
+ function decideConductorReply(conversations, p) {
8483
+ const decisions = conversations.map((conversation) => decideConversation(conversation, p));
8484
+ return { needsReply: decisions.some((decision) => decision.kind === "reply"), decisions };
8485
+ }
8486
+
8487
+ // src/conductor/thread.ts
8488
+ function defaultReplyTarget(thread) {
8489
+ return thread.kind === "issue" ? { kind: "issue", number: thread.issue.issue.number } : { kind: "pr", number: thread.prs[0].surface.pr.number };
8490
+ }
8491
+ function postingInstruction(target) {
8492
+ const command = target.kind === "pr" ? "pr" : "issue";
8493
+ const number = String(target.number);
8494
+ return [
8495
+ `Post your reply as a comment on ${target.kind === "pr" ? "pull request" : "issue"} #${number}. What you print is discarded; only a posted comment counts.`,
8496
+ `Run \`gh ${command} comment ${number} --body-file -\` and give the body on standard input, for example with a here-document.`,
8497
+ "Post exactly one comment. Do not approve, merge or close anything."
8498
+ ].join("\n");
8499
+ }
8500
+ function visible(messages, participants) {
8501
+ return analyzeSurface(messages, participants).messages.map((message) => ({
8502
+ kind: message.kind,
8503
+ author: message.author,
8504
+ body: message.body,
8505
+ createdAt: message.createdAt
8506
+ }));
8507
+ }
8508
+ function renderMessages(messages, participants) {
8509
+ const kept = visible(messages, participants);
8510
+ return kept.length === 0 ? "(no messages)" : formatMessages(kept.map((m) => ({ ...m, isNew: false })));
8511
+ }
8512
+ function renderThread(thread, participants) {
8513
+ const comments = visible(thread.comments, participants);
8514
+ if (comments.length === 0) return null;
8515
+ const location = thread.line === null ? `${thread.path}:(file)` : `${thread.path}:${String(thread.line)}`;
8516
+ const link = thread.url === null ? "" : `
8517
+ ${thread.url}`;
8518
+ return `${location}${link}
8519
+ ${formatMessages(comments.map((m) => ({ ...m, isNew: false })))}`;
8520
+ }
8521
+ function renderChecks(checks) {
8522
+ if (checks.length === 0) return "No checks reported.";
8523
+ return checks.map((check) => `- ${check.name}: ${check.status === "COMPLETED" ? check.conclusion ?? "COMPLETED" : check.status}`).join("\n");
8524
+ }
8525
+ function descriptionLines(description, participants) {
8526
+ if (description === void 0 || description.body.trim().length === 0) return [""];
8527
+ const [kept] = visible([{ kind: "issue-body", author: description.author, body: description.body, createdAt: "" }], participants);
8528
+ if (kept === void 0) return [""];
8529
+ return ["", `Pull request description (by ${kept.author}):`, "", kept.body, ""];
8530
+ }
8531
+ function prSection(pr, participants) {
8532
+ const { surface, checks } = pr;
8533
+ const number = String(surface.pr.number);
8534
+ const lines = [
8535
+ "",
8536
+ `Pull request #${number}: ${surface.pr.title}`,
8537
+ `Pull request URL: ${surface.pr.url}`,
8538
+ `State: ${surface.pr.state}${surface.pr.isDraft ? " (draft)" : ""} \xB7 Branch: ${surface.pr.headRefName} into ${surface.pr.baseRefName}`,
8539
+ ...descriptionLines(surface.description, participants),
8540
+ `Conversation on pull request #${number} (authorized accounts and you only, oldest first):`,
8541
+ "",
8542
+ renderMessages(surface.messages, participants)
8543
+ ];
8544
+ const threads = surface.threads.filter((thread) => !thread.isResolved).map((thread) => renderThread(thread, participants)).filter((text) => text !== null);
8545
+ if (threads.length > 0) {
8546
+ lines.push("", `Unresolved review threads on pull request #${number}:`, "", threads.join("\n\n"));
8547
+ }
8548
+ lines.push("", `CI status of pull request #${number}:`, "", renderChecks(checks));
8549
+ return lines;
8550
+ }
8551
+ function composeConductorPrompt(input) {
8552
+ const { thread, repo, participants, frame } = input;
8553
+ const replyTo = input.replyTo ?? defaultReplyTarget(thread);
8554
+ const lines = [
8555
+ frame,
8556
+ "",
8557
+ "--- Thread assembled by automata ---",
8558
+ `Repository: ${repo.owner}/${repo.repo}`,
8559
+ `The agent is: ${participants.agentUser}`
8560
+ ];
8561
+ if (thread.kind === "issue") {
8562
+ lines.push(
8563
+ "",
8564
+ `Issue #${String(thread.issue.issue.number)}: ${thread.issue.issue.title}`,
8565
+ `Issue URL: ${thread.issue.issue.url}`,
8566
+ `State: ${thread.issue.state}`,
8567
+ "",
8568
+ "Conversation on the issue (authorized accounts and you only, oldest first):",
8569
+ "",
8570
+ renderMessages(thread.issue.messages, participants)
8571
+ );
8572
+ }
8573
+ for (const pr of thread.prs) lines.push(...prSection(pr, participants));
8574
+ lines.push(
8575
+ "",
8576
+ "Only the messages above exist. Anything from other accounts has been withheld",
8577
+ "deliberately \u2014 do not ask about it.",
8578
+ "",
8579
+ postingInstruction(replyTo)
8580
+ );
8581
+ return lines.join("\n");
8582
+ }
8583
+
8584
+ // src/conductor/execution.ts
8585
+ var EXECUTORS = ["claude", "codex"];
8586
+ function isRecord(value) {
8587
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8588
+ }
8589
+ function containerProblem(value, name) {
8590
+ if (value === void 0 || value === null) return null;
8591
+ if (!isRecord(value)) return `conductor.${name} must be an object, got ${JSON.stringify(value)}.`;
8592
+ for (const [key2, entry] of Object.entries(value)) {
8593
+ if (!EXECUTORS.includes(key2)) {
8594
+ return `conductor.${name}.${key2} is not a recognised setting; expected one of: ${EXECUTORS.join(", ")}.`;
8595
+ }
8596
+ if (entry !== void 0 && typeof entry !== "string") {
8597
+ return `conductor.${name}.${key2} must be a string, got ${JSON.stringify(entry)}.`;
8598
+ }
8599
+ }
8600
+ return null;
8601
+ }
8602
+ function conductorExecutionProblem(conductor) {
8603
+ const executor = conductor?.executor;
8604
+ if (executor !== void 0 && executor !== null && (typeof executor !== "string" || !EXECUTORS.includes(executor))) {
8605
+ return `conductor.executor must be one of: ${EXECUTORS.join(", ")}; got ${JSON.stringify(executor)}.`;
8606
+ }
8607
+ return containerProblem(conductor?.models, "models") ?? containerProblem(conductor?.effort, "effort");
8608
+ }
8609
+ function clean(value) {
8610
+ const trimmed2 = value?.trim();
8611
+ return trimmed2 === void 0 || trimmed2.length === 0 ? void 0 : trimmed2;
8612
+ }
8613
+ function resolveConductorExecution(conductor) {
8614
+ const executor = conductor?.executor ?? "claude";
8615
+ return {
8616
+ executor,
8617
+ model: clean(conductor?.models?.[executor]),
8618
+ effort: clean(conductor?.effort?.[executor])
8619
+ };
8620
+ }
8621
+ function describeConductorExecution(execution) {
8622
+ const model = execution.model === void 0 ? " (no model override)" : ` \xB7 model ${execution.model}`;
8623
+ const effort = execution.effort === void 0 ? "" : ` \xB7 effort ${execution.effort}`;
8624
+ return `${execution.executor}${model}${effort}`;
8625
+ }
8626
+
8627
+ // src/conductor/reply.ts
8628
+ function isComment(message) {
8629
+ return message.kind === "issue-comment" || message.kind === "pr-comment";
8630
+ }
8631
+ function key(message) {
8632
+ return `${message.createdAt}\0${message.body}`;
8633
+ }
8634
+ function newMessagesBy(before, after, login2) {
8635
+ const who = login2.toLowerCase();
8636
+ const own = (m) => isComment(m) && m.author.toLowerCase() === who;
8637
+ const known = /* @__PURE__ */ new Map();
8638
+ for (const m of before.filter(own)) known.set(key(m), (known.get(key(m)) ?? 0) + 1);
8639
+ return after.filter((m) => {
8640
+ if (!own(m)) return false;
8641
+ const left = known.get(key(m)) ?? 0;
8642
+ if (left > 0) known.set(key(m), left - 1);
8643
+ return left === 0;
8644
+ });
8645
+ }
8646
+ async function conductReply(input) {
8647
+ let before;
8648
+ try {
8649
+ before = input.read();
8650
+ } catch (err) {
8651
+ return { kind: "unverified", error: `could not read the conversation before the run: ${err.message}` };
8652
+ }
8653
+ let runError = null;
8654
+ try {
8655
+ await input.run();
8656
+ } catch (err) {
8657
+ runError = err.message;
8658
+ }
8659
+ let after;
8660
+ try {
8661
+ after = input.read();
8662
+ } catch (err) {
8663
+ return { kind: "unverified", error: `could not read the conversation after the run: ${err.message}` };
8664
+ }
8665
+ const posted = newMessagesBy(before, after, input.login);
8666
+ if (posted.length > 0) return { kind: "posted", count: posted.length, runError };
8667
+ return runError === null ? { kind: "posted-nothing" } : { kind: "run-failed", error: runError };
8668
+ }
8293
8669
 
8294
8670
  // src/conductor/watchList.ts
8295
8671
  function parseWatchId(raw) {
@@ -8348,6 +8724,82 @@ function pruneWatchList(config) {
8348
8724
  }
8349
8725
  }
8350
8726
  if (dropped.length > 0) updateWatch((current) => current.filter((id) => !dropped.includes(id)));
8727
+ return watch.filter((id) => !dropped.includes(id));
8728
+ }
8729
+ function readPr(number) {
8730
+ return { surface: getPrSurface(number), checks: getPrChecks(number) };
8731
+ }
8732
+ function readWatchedItem(id) {
8733
+ const target = getWatchTarget(id);
8734
+ if (target.kind === "pr") {
8735
+ const pr = readPr(id);
8736
+ return { thread: { kind: "pr", prs: [pr] }, conversations: [prConversation(pr.surface)] };
8737
+ }
8738
+ const issue = getIssueSurface(id);
8739
+ const prs = (getOpenPrLinkMap().byIssue.get(id) ?? []).map((ref) => readPr(ref.number));
8740
+ return {
8741
+ thread: { kind: "issue", issue, prs },
8742
+ conversations: [issueConversation(issue), ...prs.map((pr) => prConversation(pr.surface))]
8743
+ };
8744
+ }
8745
+ function readTargetMessages(target) {
8746
+ return target.kind === "issue" ? issueConversation(getIssueSurface(target.number)).messages : prConversation(getPrSurface(target.number)).messages;
8747
+ }
8748
+ function runModel(prompt, execution, replyTo) {
8749
+ const { owner, repo } = getRepoSlug();
8750
+ return runClaude(prompt, { model: execution.model, effort: execution.effort, readOnly: true, replyTo, ghRepo: `${owner}/${repo}` });
8751
+ }
8752
+ async function conductItem(id, config, login2, participants, execution) {
8753
+ const label = `#${String(id)}`;
8754
+ let item;
8755
+ try {
8756
+ item = readWatchedItem(id);
8757
+ } catch (err) {
8758
+ process.stderr.write(`Warning: could not read ${label}, skipping it this tick: ${err.message}
8759
+ `);
8760
+ return "idle";
8761
+ }
8762
+ const { thread, conversations } = item;
8763
+ const verdict = decideConductorReply(conversations, participants);
8764
+ const owed = verdict.decisions.find((decision) => decision.kind === "reply");
8765
+ if (owed === void 0) {
8766
+ process.stdout.write(`Conductor: ${label} needs no reply.
8767
+ `);
8768
+ return "idle";
8769
+ }
8770
+ const replyTo = { kind: owed.surface.kind, number: owed.surface.number };
8771
+ const frame = thread.kind === "issue" ? config.conductor?.prompts?.issue ?? DEFAULT_CONDUCTOR_ISSUE_PROMPT : config.conductor?.prompts?.pr ?? DEFAULT_CONDUCTOR_PR_PROMPT;
8772
+ const prompt = composeConductorPrompt({ thread, repo: getRepoSlug(), participants, frame, replyTo });
8773
+ const where = `${replyTo.kind === "pr" ? "pull request" : "issue"} #${String(replyTo.number)}`;
8774
+ process.stdout.write(`Conductor: ${label} needs a reply on ${where}; running ${describeConductorExecution(execution)}.
8775
+ `);
8776
+ const outcome = await conductReply({
8777
+ login: login2,
8778
+ read: () => readTargetMessages(replyTo),
8779
+ run: () => runModel(prompt, execution, replyTo)
8780
+ });
8781
+ switch (outcome.kind) {
8782
+ case "posted":
8783
+ process.stdout.write(`Conductor: posted a reply on ${where}.
8784
+ `);
8785
+ if (outcome.runError !== null) {
8786
+ process.stderr.write(`Warning: the run reported "${outcome.runError}" but the reply is posted.
8787
+ `);
8788
+ }
8789
+ return "replied";
8790
+ case "posted-nothing":
8791
+ process.stderr.write(`Error: the run for ${label} finished but posted no comment on ${where}.
8792
+ `);
8793
+ return "failed";
8794
+ case "run-failed":
8795
+ process.stderr.write(`Error: the run for ${label} failed and posted no comment on ${where}: ${outcome.error}
8796
+ `);
8797
+ return "failed";
8798
+ case "unverified":
8799
+ process.stderr.write(`Error: could not tell whether ${label} got a reply: ${outcome.error}
8800
+ `);
8801
+ return "failed";
8802
+ }
8351
8803
  }
8352
8804
  function loadGitHubConfig() {
8353
8805
  const config = readRawConfig();
@@ -8419,43 +8871,59 @@ function runWatchList() {
8419
8871
  function exitWith(code) {
8420
8872
  if (code !== 0) process.exit(code);
8421
8873
  }
8422
- function runConductor() {
8874
+ function preflight() {
8423
8875
  let config;
8424
8876
  try {
8425
8877
  config = readConfig();
8426
8878
  } catch (err) {
8427
- return fail2(err.message);
8879
+ return { problem: err.message };
8428
8880
  }
8429
8881
  if (!isExplicitGitHub(config)) {
8430
- return fail2(
8431
- azdoUnsupportedMessage(
8882
+ return {
8883
+ problem: azdoUnsupportedMessage(
8432
8884
  "conductor",
8433
8885
  "It needs the GitHub APIs; set the remote with `automata config set type gh`."
8434
8886
  )
8435
- );
8887
+ };
8436
8888
  }
8437
8889
  const allowedUsers = (config.allowedUsers ?? []).filter((user) => user.trim().length > 0);
8438
8890
  if (allowedUsers.length === 0) {
8439
- return fail2("No allowed users configured. Run `automata config set allowed-users <user1,user2>`.");
8891
+ return { problem: "No allowed users configured. Run `automata config set allowed-users <user1,user2>`." };
8440
8892
  }
8441
8893
  const agentUser = (config.agentUser ?? "").trim();
8442
8894
  if (agentUser.length === 0) {
8443
- return fail2("No agent user configured. Run `automata config set agent-user <login>`.");
8895
+ return { problem: "No agent user configured. Run `automata config set agent-user <login>`." };
8444
8896
  }
8445
8897
  let login2;
8446
8898
  try {
8447
8899
  login2 = getAuthenticatedLogin();
8448
8900
  } catch (err) {
8449
- return fail2(`\`gh\` could not be queried: ${err.message}`);
8901
+ return { problem: `\`gh\` could not be queried: ${err.message}` };
8450
8902
  }
8451
- const problem = conductorIdentityProblemFor(login2, agentUser, allowedUsers);
8452
- if (problem !== null) return fail2(problem);
8903
+ const identityProblem = conductorIdentityProblemFor(login2, agentUser, allowedUsers);
8904
+ if (identityProblem !== null) return { problem: identityProblem };
8453
8905
  const rawStale = config.doWork?.lockStaleMinutes;
8454
8906
  if (rawStale !== void 0 && rawStale !== null) {
8455
8907
  if (typeof rawStale !== "number" || !Number.isSafeInteger(rawStale) || rawStale < 1) {
8456
- return fail2(`doWork.lockStaleMinutes must be a positive integer, got ${JSON.stringify(rawStale)}.`);
8908
+ return { problem: `doWork.lockStaleMinutes must be a positive integer, got ${JSON.stringify(rawStale)}.` };
8457
8909
  }
8458
8910
  }
8911
+ const executionProblem = conductorExecutionProblem(config.conductor);
8912
+ if (executionProblem !== null) return { problem: executionProblem };
8913
+ if (config.conductor?.executor === "codex") {
8914
+ return { problem: "conductor.executor codex is not supported: Codex cannot be limited to comments on the reply target. Use claude." };
8915
+ }
8916
+ return {
8917
+ config,
8918
+ login: login2 ?? "",
8919
+ participants: { allowedUsers, agentUser },
8920
+ execution: resolveConductorExecution(config.conductor)
8921
+ };
8922
+ }
8923
+ async function runConductor() {
8924
+ const checked = preflight();
8925
+ if ("problem" in checked) return fail2(checked.problem);
8926
+ const { config, login: login2, participants, execution } = checked;
8459
8927
  const staleMinutes = config.doWork?.lockStaleMinutes ?? DEFAULT_DO_WORK.lockStaleMinutes;
8460
8928
  const lock = acquireConductorLock(staleMinutes);
8461
8929
  if (!lock.ok) {
@@ -8474,18 +8942,28 @@ function runConductor() {
8474
8942
  return 0;
8475
8943
  }
8476
8944
  try {
8477
- process.stdout.write(`Conductor: running as ${login2 ?? ""}.
8945
+ process.stdout.write(`Conductor: running as ${login2}.
8478
8946
  `);
8479
- pruneWatchList(config);
8480
- return 0;
8947
+ const watch = pruneWatchList(config);
8948
+ let failed = 0;
8949
+ for (const id of watch) {
8950
+ try {
8951
+ if (await conductItem(id, config, login2, participants, execution) === "failed") failed++;
8952
+ } catch (err) {
8953
+ process.stderr.write(`Error: #${String(id)} could not be conducted: ${err.message}
8954
+ `);
8955
+ failed++;
8956
+ }
8957
+ }
8958
+ return failed > 0 ? 1 : 0;
8481
8959
  } finally {
8482
8960
  lock.handle.release();
8483
8961
  }
8484
8962
  }
8485
8963
  var conductorCommand = new Command7("conductor").description(
8486
8964
  "Run one conductor tick as an allowed (human) account: verify the identity, take the conductor's own run lock, then act"
8487
- ).action(() => {
8488
- exitWith(runConductor());
8965
+ ).action(async () => {
8966
+ exitWith(await runConductor());
8489
8967
  });
8490
8968
  conductorCommand.command("add <id>").description("Watch an issue or pull request and make do-work pick it up (an issue also follows its linked PR)").action((id) => {
8491
8969
  exitWith(runWatchAdd(id));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "automata-cli",
3
- "version": "0.11.0-develop.496",
3
+ "version": "0.11.0-develop.514",
4
4
  "description": "Automata CLI tool",
5
5
  "type": "module",
6
6
  "engines": {