automata-cli 0.11.0-develop.501 → 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.
Files changed (2) hide show
  1. package/dist/index.js +508 -51
  2. package/package.json +1 -1
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,
@@ -313,6 +315,45 @@ var configSetConductorPrompt = new Command("conductor-prompt").description("Set
313
315
  process.stdout.write(`conductor ${kind} prompt set.
314
316
  `);
315
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
+ });
316
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) => {
317
358
  const branch = value.trim();
318
359
  if (branch.length === 0) {
@@ -336,7 +377,7 @@ var configSetGitReleaseFlow = new Command("git-release-flow").description("Pin t
336
377
  process.stdout.write(`git release flow set to: ${flow}
337
378
  `);
338
379
  });
339
- 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(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);
340
381
  var configCommand = new Command("config").description("Configure automata settings").addCommand(configSet).action(async () => {
341
382
  const [{ render }, React, { ConfigWizard }] = await Promise.all([
342
383
  import("ink"),
@@ -392,8 +433,8 @@ function extractIdentity(stdout) {
392
433
  if (typeof id === "string" && id) return id;
393
434
  if (id && typeof id === "object") {
394
435
  const o = id;
395
- for (const key of ["displayName", "uniqueName", "providerDisplayName", "id"]) {
396
- 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];
397
438
  }
398
439
  }
399
440
  } catch {
@@ -2544,6 +2585,19 @@ function getPrSurface(prNumber) {
2544
2585
  description: { author: login(raw.author), body: raw.body }
2545
2586
  };
2546
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
+ }
2547
2601
  function assignIssueToAgent(issueNumber, agentUser) {
2548
2602
  const { stderr, status } = run4("gh", [
2549
2603
  "issue",
@@ -2821,9 +2875,47 @@ async function terminateTrackedChildren(timeoutMs = 1e4, killGraceMs = 5e3) {
2821
2875
  }
2822
2876
 
2823
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
+ }
2824
2905
  function buildClaudeArgs(prompt, options = {}) {
2825
2906
  const args = [];
2826
- 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
+ }
2827
2919
  if (options.model) args.push("--model", options.model);
2828
2920
  if (options.effort) args.push("--effort", options.effort);
2829
2921
  if (options.verbose) args.push("--verbose", "--output-format", "stream-json");
@@ -2835,12 +2927,17 @@ function runClaude(prompt, options = {}) {
2835
2927
  const claudeBin = resolveCommand("claude");
2836
2928
  const args = buildClaudeArgs(prompt, {
2837
2929
  yolo: true,
2930
+ readOnly: options.readOnly,
2931
+ replyTo: options.replyTo,
2838
2932
  model: options.model,
2839
2933
  effort: options.effort,
2840
2934
  verbose: true
2841
2935
  });
2842
2936
  const sink2 = options.sink;
2843
- 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
+ });
2844
2941
  trackChild(child);
2845
2942
  child.stderr.on("data", (chunk) => {
2846
2943
  process.stderr.write(chunk);
@@ -2882,21 +2979,21 @@ function runClaude(prompt, options = {}) {
2882
2979
  }
2883
2980
  function invokeClaudeCode(prompt, options = {}) {
2884
2981
  if (options.verbose) {
2885
- return invokeClaudeCodeVerbose(prompt, options.yolo ?? false, options.model, options.effort);
2982
+ return invokeClaudeCodeVerbose(prompt, options);
2886
2983
  }
2887
- invokeClaudeCodeSync(prompt, options.yolo ?? false, options.model, options.effort);
2984
+ invokeClaudeCodeSync(prompt, options);
2888
2985
  }
2889
- function invokeClaudeCodeSync(prompt, yolo, model, effort) {
2986
+ function invokeClaudeCodeSync(prompt, options) {
2890
2987
  const claudeBin = resolveCommand("claude");
2891
- const args = buildClaudeArgs(prompt, { yolo, model, effort, verbose: false });
2988
+ const args = buildClaudeArgs(prompt, { ...options, verbose: false });
2892
2989
  const result = spawnSync6(claudeBin, args, { encoding: "utf8", stdio: "inherit" });
2893
2990
  handleSpawnError(result.error, "claude");
2894
2991
  handleExitCode(result.status, "Claude Code");
2895
2992
  }
2896
- function invokeClaudeCodeVerbose(prompt, yolo, model, effort) {
2993
+ function invokeClaudeCodeVerbose(prompt, options) {
2897
2994
  return new Promise((resolve2) => {
2898
2995
  const claudeBin = resolveCommand("claude");
2899
- const args = buildClaudeArgs(prompt, { yolo, model, effort, verbose: true });
2996
+ const args = buildClaudeArgs(prompt, { ...options, verbose: true });
2900
2997
  const child = spawn(claudeBin, args, { stdio: ["inherit", "pipe", "inherit"] });
2901
2998
  const rl = createInterface({ input: child.stdout });
2902
2999
  let turnCount = 0;
@@ -3001,7 +3098,8 @@ function toTomlBasicString(value) {
3001
3098
  }
3002
3099
  function buildCodexArgs(prompt, options = {}) {
3003
3100
  const args = ["exec"];
3004
- 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");
3005
3103
  if (options.model) args.push("--model", options.model);
3006
3104
  if (options.effort) args.push("-c", `model_reasoning_effort=${toTomlBasicString(options.effort)}`);
3007
3105
  args.push(prompt);
@@ -3011,11 +3109,11 @@ function invokeCodexCode(prompt, options = {}) {
3011
3109
  if (options.verbose) {
3012
3110
  process.stderr.write("Warning: --verbose is not supported for Codex and will be ignored.\n");
3013
3111
  }
3014
- invokeCodexCodeSync(prompt, options.yolo ?? false, options.model, options.effort);
3112
+ invokeCodexCodeSync(prompt, options);
3015
3113
  }
3016
- function invokeCodexCodeSync(prompt, yolo, model, effort) {
3114
+ function invokeCodexCodeSync(prompt, options) {
3017
3115
  const codexBin = resolveCommand("codex");
3018
- const args = buildCodexArgs(prompt, { yolo, model, effort });
3116
+ const args = buildCodexArgs(prompt, options);
3019
3117
  const result = spawnSync7(codexBin, args, { encoding: "utf8", stdio: "inherit" });
3020
3118
  handleSpawnError(result.error, "codex");
3021
3119
  handleExitCode(result.status, "Codex");
@@ -3023,12 +3121,20 @@ function invokeCodexCodeSync(prompt, yolo, model, effort) {
3023
3121
  function runCodex(prompt, options = {}) {
3024
3122
  return new Promise((resolve2, reject) => {
3025
3123
  const codexBin = resolveCommand("codex");
3026
- 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
+ });
3027
3130
  const sink2 = options.sink;
3028
- 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
+ });
3029
3135
  trackChild(child);
3030
3136
  child.stdout.on("data", (chunk) => {
3031
- process.stdout.write(chunk);
3137
+ if (options.quiet !== true) process.stdout.write(chunk);
3032
3138
  sink2?.output("stdout", chunk.toString("utf8"));
3033
3139
  });
3034
3140
  child.stderr.on("data", (chunk) => {
@@ -3884,6 +3990,12 @@ function selectLinkedPr(linkedPrs) {
3884
3990
  const open = linkedPrs.filter((pr) => pr.state === "OPEN");
3885
3991
  return open.length === 0 ? null : newestPr(open);
3886
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
+ }
3887
3999
  function claimStates(item) {
3888
4000
  const states = [];
3889
4001
  if (item.issue !== null) {
@@ -5440,9 +5552,9 @@ function summarizeSync(items) {
5440
5552
  const counts = /* @__PURE__ */ new Map();
5441
5553
  for (const item of items) {
5442
5554
  if (item.sync === void 0) continue;
5443
- const key = oneLine(item.sync);
5444
- if (key.length === 0 || QUIET_SYNC.has(key)) continue;
5445
- 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);
5446
5558
  }
5447
5559
  if (counts.size === 0) return null;
5448
5560
  return [...counts].map(([strategy, count]) => `${strategy}:${String(count)}`).join(",");
@@ -5691,9 +5803,9 @@ function parseExecution(bracket) {
5691
5803
  if (bracket === void 0) return { executor: null, model: null, effort: null };
5692
5804
  const parts = bracket.split(" ").filter((part) => part.length > 0);
5693
5805
  const executor = parts[0] ?? null;
5694
- const find = (key) => {
5695
- const hit = parts.find((part) => part.startsWith(`${key}=`));
5696
- 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);
5697
5809
  };
5698
5810
  return { executor, model: find("model"), effort: find("effort") };
5699
5811
  }
@@ -6706,15 +6818,15 @@ function describeRefusedRun(item, refusal) {
6706
6818
  function isPlainObject(value) {
6707
6819
  return typeof value === "object" && value !== null && !Array.isArray(value);
6708
6820
  }
6709
- function validateOptionalString(container, key, path) {
6710
- const value = container[key];
6821
+ function validateOptionalString(container, key2, path) {
6822
+ const value = container[key2];
6711
6823
  if (value === void 0 || value === null) return;
6712
6824
  if (typeof value !== "string" || value.trim().length === 0) {
6713
6825
  failSettings(`${path} must be a non-empty string.`);
6714
6826
  }
6715
6827
  }
6716
- function validateOptionalInt(container, key, path, min, hint) {
6717
- const value = container[key];
6828
+ function validateOptionalInt(container, key2, path, min, hint) {
6829
+ const value = container[key2];
6718
6830
  if (value === void 0 || value === null) return;
6719
6831
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value < min) {
6720
6832
  failSettings(`${path} must be ${hint}, got ${JSON.stringify(value)}.`);
@@ -6739,8 +6851,8 @@ function validateDoWorkConfig(section) {
6739
6851
  validateSettingContainer(section["effort"], "effort", ["claude", "codex"]);
6740
6852
  validateSettingContainer(section["prompts"], "prompts", ["issueDiscuss", "prWork", "prOrphan"]);
6741
6853
  }
6742
- function validateOptionalBoolean(container, key, path) {
6743
- const value = container[key];
6854
+ function validateOptionalBoolean(container, key2, path) {
6855
+ const value = container[key2];
6744
6856
  if (value === void 0 || value === null) return;
6745
6857
  if (typeof value !== "boolean") {
6746
6858
  failSettings(`${path} must be true or false, got ${JSON.stringify(value)}.`);
@@ -6758,12 +6870,12 @@ function validateSettingContainer(value, container, keys) {
6758
6870
  if (!isPlainObject(value)) {
6759
6871
  failSettings(`doWork.${container} must be an object, got ${JSON.stringify(value)}.`);
6760
6872
  }
6761
- for (const key of keys) {
6762
- validateOptionalString(value, key, `doWork.${container}.${key}`);
6873
+ for (const key2 of keys) {
6874
+ validateOptionalString(value, key2, `doWork.${container}.${key2}`);
6763
6875
  }
6764
- for (const key of Object.keys(value)) {
6765
- if (!keys.includes(key)) {
6766
- 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(", ")}.`);
6767
6879
  }
6768
6880
  }
6769
6881
  }
@@ -8311,6 +8423,249 @@ import { Command as Command7 } from "commander";
8311
8423
  function isWatchClosed(state) {
8312
8424
  return state === "closed";
8313
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
+ }
8314
8669
 
8315
8670
  // src/conductor/watchList.ts
8316
8671
  function parseWatchId(raw) {
@@ -8369,6 +8724,82 @@ function pruneWatchList(config) {
8369
8724
  }
8370
8725
  }
8371
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
+ }
8372
8803
  }
8373
8804
  function loadGitHubConfig() {
8374
8805
  const config = readRawConfig();
@@ -8440,43 +8871,59 @@ function runWatchList() {
8440
8871
  function exitWith(code) {
8441
8872
  if (code !== 0) process.exit(code);
8442
8873
  }
8443
- function runConductor() {
8874
+ function preflight() {
8444
8875
  let config;
8445
8876
  try {
8446
8877
  config = readConfig();
8447
8878
  } catch (err) {
8448
- return fail2(err.message);
8879
+ return { problem: err.message };
8449
8880
  }
8450
8881
  if (!isExplicitGitHub(config)) {
8451
- return fail2(
8452
- azdoUnsupportedMessage(
8882
+ return {
8883
+ problem: azdoUnsupportedMessage(
8453
8884
  "conductor",
8454
8885
  "It needs the GitHub APIs; set the remote with `automata config set type gh`."
8455
8886
  )
8456
- );
8887
+ };
8457
8888
  }
8458
8889
  const allowedUsers = (config.allowedUsers ?? []).filter((user) => user.trim().length > 0);
8459
8890
  if (allowedUsers.length === 0) {
8460
- 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>`." };
8461
8892
  }
8462
8893
  const agentUser = (config.agentUser ?? "").trim();
8463
8894
  if (agentUser.length === 0) {
8464
- 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>`." };
8465
8896
  }
8466
8897
  let login2;
8467
8898
  try {
8468
8899
  login2 = getAuthenticatedLogin();
8469
8900
  } catch (err) {
8470
- return fail2(`\`gh\` could not be queried: ${err.message}`);
8901
+ return { problem: `\`gh\` could not be queried: ${err.message}` };
8471
8902
  }
8472
- const problem = conductorIdentityProblemFor(login2, agentUser, allowedUsers);
8473
- if (problem !== null) return fail2(problem);
8903
+ const identityProblem = conductorIdentityProblemFor(login2, agentUser, allowedUsers);
8904
+ if (identityProblem !== null) return { problem: identityProblem };
8474
8905
  const rawStale = config.doWork?.lockStaleMinutes;
8475
8906
  if (rawStale !== void 0 && rawStale !== null) {
8476
8907
  if (typeof rawStale !== "number" || !Number.isSafeInteger(rawStale) || rawStale < 1) {
8477
- 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)}.` };
8478
8909
  }
8479
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;
8480
8927
  const staleMinutes = config.doWork?.lockStaleMinutes ?? DEFAULT_DO_WORK.lockStaleMinutes;
8481
8928
  const lock = acquireConductorLock(staleMinutes);
8482
8929
  if (!lock.ok) {
@@ -8495,18 +8942,28 @@ function runConductor() {
8495
8942
  return 0;
8496
8943
  }
8497
8944
  try {
8498
- process.stdout.write(`Conductor: running as ${login2 ?? ""}.
8945
+ process.stdout.write(`Conductor: running as ${login2}.
8499
8946
  `);
8500
- pruneWatchList(config);
8501
- 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;
8502
8959
  } finally {
8503
8960
  lock.handle.release();
8504
8961
  }
8505
8962
  }
8506
8963
  var conductorCommand = new Command7("conductor").description(
8507
8964
  "Run one conductor tick as an allowed (human) account: verify the identity, take the conductor's own run lock, then act"
8508
- ).action(() => {
8509
- exitWith(runConductor());
8965
+ ).action(async () => {
8966
+ exitWith(await runConductor());
8510
8967
  });
8511
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) => {
8512
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.501",
3
+ "version": "0.11.0-develop.514",
4
4
  "description": "Automata CLI tool",
5
5
  "type": "module",
6
6
  "engines": {