automata-cli 0.11.0-develop.501 → 0.11.0-develop.519

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 +787 -98
  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,
@@ -88,11 +90,11 @@ function writeDoWork(patch) {
88
90
  const current = readRawConfig();
89
91
  writeConfig({ ...current, doWork: { ...current.doWork, ...patch } });
90
92
  }
91
- function parseNonNegativeInt(value, label) {
93
+ function parseNonNegativeInt(value, label2) {
92
94
  const trimmed2 = value.trim();
93
95
  const parsed = Number.parseInt(trimmed2, 10);
94
96
  if (Number.isNaN(parsed) || parsed < 0 || String(parsed) !== trimmed2 || !Number.isSafeInteger(parsed)) {
95
- process.stderr.write(`Error: ${label} must be a non-negative integer (got "${value}").
97
+ process.stderr.write(`Error: ${label2} must be a non-negative integer (got "${value}").
96
98
  `);
97
99
  process.exit(1);
98
100
  }
@@ -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 {
@@ -1664,7 +1705,7 @@ function formatSonarIssue(issue) {
1664
1705
  const location = formatLocation(issue.path, issue.line);
1665
1706
  if (location) lines.push(` Location: ${location}`);
1666
1707
  if (issue.severity || issue.type) {
1667
- const labels = [issue.severity, issue.type].filter((label) => Boolean(label)).map(sanitizeText).join(" / ");
1708
+ const labels = [issue.severity, issue.type].filter((label2) => Boolean(label2)).map(sanitizeText).join(" / ");
1668
1709
  lines.push(` Classification: ${labels}`);
1669
1710
  }
1670
1711
  if (issue.rule) lines.push(` Rule: ${sanitizeText(issue.rule)}`);
@@ -1677,7 +1718,7 @@ function formatSonarHotspot(hotspot) {
1677
1718
  if (location) lines.push(` Location: ${location}`);
1678
1719
  if (hotspot.status) lines.push(` Status: ${sanitizeText(hotspot.status)}`);
1679
1720
  if (hotspot.vulnerabilityProbability || hotspot.securityCategory) {
1680
- const labels = [hotspot.vulnerabilityProbability, hotspot.securityCategory].filter((label) => Boolean(label)).map(sanitizeText).join(" / ");
1721
+ const labels = [hotspot.vulnerabilityProbability, hotspot.securityCategory].filter((label2) => Boolean(label2)).map(sanitizeText).join(" / ");
1681
1722
  lines.push(` Classification: ${labels}`);
1682
1723
  }
1683
1724
  if (hotspot.rule) {
@@ -2327,7 +2368,7 @@ function getIssueSurface(issueNumber) {
2327
2368
  issue: { number: raw.number, title: raw.title, body: raw.body, url: raw.url },
2328
2369
  state: raw.state === "CLOSED" ? "CLOSED" : "OPEN",
2329
2370
  assignees: (raw.assignees ?? []).map(login).filter((name) => name.length > 0),
2330
- labels: (raw.labels ?? []).map((label) => label.name ?? "").filter((name) => name.length > 0),
2371
+ labels: (raw.labels ?? []).map((label2) => label2.name ?? "").filter((name) => name.length > 0),
2331
2372
  messages
2332
2373
  };
2333
2374
  }
@@ -2386,7 +2427,7 @@ function toPullRequestRef(node) {
2386
2427
  function toOrphanPr(node) {
2387
2428
  return {
2388
2429
  pr: toPullRequestRef(node),
2389
- labels: (node.labels?.nodes ?? []).map((label) => label.name ?? "").filter((name) => name.length > 0),
2430
+ labels: (node.labels?.nodes ?? []).map((label2) => label2.name ?? "").filter((name) => name.length > 0),
2390
2431
  assignees: (node.assignees?.nodes ?? []).map((assignee) => assignee.login ?? "").filter((login2) => login2.length > 0)
2391
2432
  };
2392
2433
  }
@@ -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) {
@@ -4377,6 +4489,9 @@ var UNKNOWN_OWNER = {
4377
4489
  command: "unknown",
4378
4490
  token: ""
4379
4491
  };
4492
+ function inspectConductorLock(staleMinutes, now = Date.now()) {
4493
+ return inspectRunLock(staleMinutes, now, CONDUCTOR_LOCK_FILE);
4494
+ }
4380
4495
  function acquireConductorLock(staleMinutes) {
4381
4496
  return acquireRunLock("conductor", staleMinutes, CONDUCTOR_LOCK_FILE);
4382
4497
  }
@@ -4487,8 +4602,8 @@ function heldForMs(owner, now) {
4487
4602
  const startedAt = Date.parse(owner.startedAt);
4488
4603
  return Number.isNaN(startedAt) ? null : now - startedAt;
4489
4604
  }
4490
- function inspectRunLock(staleMinutes, now = Date.now()) {
4491
- const path = lockPath();
4605
+ function inspectRunLock(staleMinutes, now = Date.now(), lockFile = LOCK_FILE) {
4606
+ const path = lockPath(lockFile);
4492
4607
  try {
4493
4608
  statSync2(path);
4494
4609
  } catch (err) {
@@ -4673,9 +4788,9 @@ function detectRescueTarget(options, now) {
4673
4788
  return { branch: current, createdBranch: false, source: current };
4674
4789
  }
4675
4790
  const source = detached ? "detached HEAD" : current === options.baseBranch ? options.baseBranch : current;
4676
- const label = detached ? "detached" : flattenBranchName(source);
4791
+ const label2 = detached ? "detached" : flattenBranchName(source);
4677
4792
  return {
4678
- branch: `${RESCUE_BRANCH_PREFIX}${label}-${utcStamp(now)}`,
4793
+ branch: `${RESCUE_BRANCH_PREFIX}${label2}-${utcStamp(now)}`,
4679
4794
  createdBranch: true,
4680
4795
  source
4681
4796
  };
@@ -5091,9 +5206,9 @@ var RunTranscript = class {
5091
5206
  stderrAtLineStart = true;
5092
5207
  /** A private key began in an earlier line and has not ended yet. */
5093
5208
  inPrivateKey = false;
5094
- constructor(label, root = process.cwd(), now = /* @__PURE__ */ new Date()) {
5209
+ constructor(label2, root = process.cwd(), now = /* @__PURE__ */ new Date()) {
5095
5210
  const stamp = now.toISOString().replace(/[:.]/g, "-");
5096
- this.fileName = `${stamp}-${label}.log`;
5211
+ this.fileName = `${stamp}-${label2}.log`;
5097
5212
  const dir = join5(root, RUN_TRANSCRIPT_DIR);
5098
5213
  this.path = join5(dir, this.fileName);
5099
5214
  try {
@@ -5440,9 +5555,9 @@ function summarizeSync(items) {
5440
5555
  const counts = /* @__PURE__ */ new Map();
5441
5556
  for (const item of items) {
5442
5557
  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);
5558
+ const key2 = oneLine(item.sync);
5559
+ if (key2.length === 0 || QUIET_SYNC.has(key2)) continue;
5560
+ counts.set(key2, (counts.get(key2) ?? 0) + 1);
5446
5561
  }
5447
5562
  if (counts.size === 0) return null;
5448
5563
  return [...counts].map(([strategy, count]) => `${strategy}:${String(count)}`).join(",");
@@ -5691,9 +5806,9 @@ function parseExecution(bracket) {
5691
5806
  if (bracket === void 0) return { executor: null, model: null, effort: null };
5692
5807
  const parts = bracket.split(" ").filter((part) => part.length > 0);
5693
5808
  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);
5809
+ const find = (key2) => {
5810
+ const hit = parts.find((part) => part.startsWith(`${key2}=`));
5811
+ return hit === void 0 ? null : hit.slice(key2.length + 1);
5697
5812
  };
5698
5813
  return { executor, model: find("model"), effort: find("effort") };
5699
5814
  }
@@ -6360,12 +6475,12 @@ function gitSection(status) {
6360
6475
  return build("git", lines, problems, { ...status });
6361
6476
  }
6362
6477
  function assembleReport(input) {
6363
- const byId = new Map(input.sections.map((section) => [section.id, section]));
6478
+ const byId = new Map(input.sections.map((section2) => [section2.id, section2]));
6364
6479
  const ordered = SECTION_ORDER.flatMap((id) => {
6365
- const section = byId.get(id);
6366
- return section === void 0 ? [] : [section];
6480
+ const section2 = byId.get(id);
6481
+ return section2 === void 0 ? [] : [section2];
6367
6482
  });
6368
- const problems = ordered.flatMap((section) => section.problems);
6483
+ const problems = ordered.flatMap((section2) => section2.problems);
6369
6484
  return {
6370
6485
  generatedAt: input.generatedAt,
6371
6486
  repo: input.repo,
@@ -6399,8 +6514,8 @@ function renderProblems(problems) {
6399
6514
  function renderText(report) {
6400
6515
  const head = `automata do-work --check \u2014 ${report.repo ?? "unknown repository"} \u2014 ${report.generatedAt.toISOString()}`;
6401
6516
  const parts = report.blocked === null ? [head, ""] : [`blocked: ${report.blocked.trigger}`, head, ""];
6402
- for (const section of report.sections) {
6403
- parts.push(...renderBlock(section.title, section.lines, "(nothing to report)"));
6517
+ for (const section2 of report.sections) {
6518
+ parts.push(...renderBlock(section2.title, section2.lines, "(nothing to report)"));
6404
6519
  }
6405
6520
  if (report.trace !== null) parts.push(...renderTrace(report.trace));
6406
6521
  if (report.problems.length > 0) parts.push(...renderProblems(report.problems));
@@ -6411,12 +6526,12 @@ function renderText(report) {
6411
6526
  }
6412
6527
  function toJson(report) {
6413
6528
  const sections = {};
6414
- for (const section of report.sections) {
6415
- sections[section.id] = {
6416
- title: section.title,
6417
- lines: section.lines,
6418
- problems: section.problems,
6419
- data: section.data
6529
+ for (const section2 of report.sections) {
6530
+ sections[section2.id] = {
6531
+ title: section2.title,
6532
+ lines: section2.lines,
6533
+ problems: section2.problems,
6534
+ data: section2.data
6420
6535
  };
6421
6536
  }
6422
6537
  return {
@@ -6475,14 +6590,14 @@ var SettingsError = class extends Error {
6475
6590
  function failSettings(message) {
6476
6591
  throw new SettingsError(message);
6477
6592
  }
6478
- function parsePositiveInt(value, label) {
6593
+ function parsePositiveInt(value, label2) {
6479
6594
  const trimmed2 = value.trim();
6480
6595
  if (!/^\d+$/.test(trimmed2)) {
6481
- failSettings(`${label} must be a positive integer (got "${value}").`);
6596
+ failSettings(`${label2} must be a positive integer (got "${value}").`);
6482
6597
  }
6483
6598
  const parsed = Number(trimmed2);
6484
6599
  if (!Number.isSafeInteger(parsed) || parsed <= 0) {
6485
- failSettings(`${label} must be a positive integer within the safe range (got "${value}").`);
6600
+ failSettings(`${label2} must be a positive integer within the safe range (got "${value}").`);
6486
6601
  }
6487
6602
  return parsed;
6488
6603
  }
@@ -6637,14 +6752,14 @@ function planRun(item, settings, execution) {
6637
6752
  }
6638
6753
  function describeAssignment(item, agentUser) {
6639
6754
  return claimStates(item).map((claim) => {
6640
- const label = claim.surface === "issue" ? "issue" : `pull request #${String(claim.number)}`;
6755
+ const label2 = claim.surface === "issue" ? "issue" : `pull request #${String(claim.number)}`;
6641
6756
  switch (claim.state) {
6642
6757
  case "would-claim":
6643
- return `would assign ${label} to ${agentUser}`;
6758
+ return `would assign ${label2} to ${agentUser}`;
6644
6759
  case "already-assigned":
6645
- return `${label} already assigned`;
6760
+ return `${label2} already assigned`;
6646
6761
  case "rule-exempt":
6647
- return `${label} not claimed (orphan pass)`;
6762
+ return `${label2} not claimed (orphan pass)`;
6648
6763
  }
6649
6764
  }).join(" \xB7 ");
6650
6765
  }
@@ -6706,41 +6821,41 @@ function describeRefusedRun(item, refusal) {
6706
6821
  function isPlainObject(value) {
6707
6822
  return typeof value === "object" && value !== null && !Array.isArray(value);
6708
6823
  }
6709
- function validateOptionalString(container, key, path) {
6710
- const value = container[key];
6824
+ function validateOptionalString(container, key2, path) {
6825
+ const value = container[key2];
6711
6826
  if (value === void 0 || value === null) return;
6712
6827
  if (typeof value !== "string" || value.trim().length === 0) {
6713
6828
  failSettings(`${path} must be a non-empty string.`);
6714
6829
  }
6715
6830
  }
6716
- function validateOptionalInt(container, key, path, min, hint) {
6717
- const value = container[key];
6831
+ function validateOptionalInt(container, key2, path, min, hint) {
6832
+ const value = container[key2];
6718
6833
  if (value === void 0 || value === null) return;
6719
6834
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value < min) {
6720
6835
  failSettings(`${path} must be ${hint}, got ${JSON.stringify(value)}.`);
6721
6836
  }
6722
6837
  }
6723
- function validateDoWorkConfig(section) {
6724
- if (section === void 0 || section === null) return;
6725
- if (!isPlainObject(section)) {
6726
- failSettings(`doWork must be an object, got ${JSON.stringify(section)}.`);
6838
+ function validateDoWorkConfig(section2) {
6839
+ if (section2 === void 0 || section2 === null) return;
6840
+ if (!isPlainObject(section2)) {
6841
+ failSettings(`doWork must be an object, got ${JSON.stringify(section2)}.`);
6727
6842
  }
6728
- const executor = section["executor"];
6843
+ const executor = section2["executor"];
6729
6844
  if (executor !== void 0 && executor !== "claude" && executor !== "codex") {
6730
6845
  failSettings(`doWork.executor must be 'claude' or 'codex', got ${JSON.stringify(executor)}.`);
6731
6846
  }
6732
- validateOptionalString(section, "baseBranch", "doWork.baseBranch");
6733
- validateOptionalInt(section, "maxRunsPerTick", "doWork.maxRunsPerTick", 0, "a non-negative integer (0 = unlimited)");
6734
- validateOptionalInt(section, "lockStaleMinutes", "doWork.lockStaleMinutes", 1, "a positive integer");
6735
- validateOptionalBoolean(section, "dumpOnBlock", "doWork.dumpOnBlock");
6736
- validateOptionalBoolean(section, "postRunLog", "doWork.postRunLog");
6737
- validateProtectedBranches(section["protectedBranches"]);
6738
- validateSettingContainer(section["models"], "models", ["claude", "codex"]);
6739
- validateSettingContainer(section["effort"], "effort", ["claude", "codex"]);
6740
- validateSettingContainer(section["prompts"], "prompts", ["issueDiscuss", "prWork", "prOrphan"]);
6741
- }
6742
- function validateOptionalBoolean(container, key, path) {
6743
- const value = container[key];
6847
+ validateOptionalString(section2, "baseBranch", "doWork.baseBranch");
6848
+ validateOptionalInt(section2, "maxRunsPerTick", "doWork.maxRunsPerTick", 0, "a non-negative integer (0 = unlimited)");
6849
+ validateOptionalInt(section2, "lockStaleMinutes", "doWork.lockStaleMinutes", 1, "a positive integer");
6850
+ validateOptionalBoolean(section2, "dumpOnBlock", "doWork.dumpOnBlock");
6851
+ validateOptionalBoolean(section2, "postRunLog", "doWork.postRunLog");
6852
+ validateProtectedBranches(section2["protectedBranches"]);
6853
+ validateSettingContainer(section2["models"], "models", ["claude", "codex"]);
6854
+ validateSettingContainer(section2["effort"], "effort", ["claude", "codex"]);
6855
+ validateSettingContainer(section2["prompts"], "prompts", ["issueDiscuss", "prWork", "prOrphan"]);
6856
+ }
6857
+ function validateOptionalBoolean(container, key2, path) {
6858
+ const value = container[key2];
6744
6859
  if (value === void 0 || value === null) return;
6745
6860
  if (typeof value !== "boolean") {
6746
6861
  failSettings(`${path} must be true or false, got ${JSON.stringify(value)}.`);
@@ -6758,12 +6873,12 @@ function validateSettingContainer(value, container, keys) {
6758
6873
  if (!isPlainObject(value)) {
6759
6874
  failSettings(`doWork.${container} must be an object, got ${JSON.stringify(value)}.`);
6760
6875
  }
6761
- for (const key of keys) {
6762
- validateOptionalString(value, key, `doWork.${container}.${key}`);
6876
+ for (const key2 of keys) {
6877
+ validateOptionalString(value, key2, `doWork.${container}.${key2}`);
6763
6878
  }
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(", ")}.`);
6879
+ for (const key2 of Object.keys(value)) {
6880
+ if (!keys.includes(key2)) {
6881
+ failSettings(`doWork.${container}.${key2} is not a recognised setting; expected one of: ${keys.join(", ")}.`);
6767
6882
  }
6768
6883
  }
6769
6884
  }
@@ -6800,7 +6915,7 @@ function matchesDiscoveryFilter(subject, settings) {
6800
6915
  const value = settings.discoveryValue.toLowerCase();
6801
6916
  switch (settings.technique) {
6802
6917
  case "label":
6803
- return subject.labels.some((label) => label.toLowerCase() === value);
6918
+ return subject.labels.some((label2) => label2.toLowerCase() === value);
6804
6919
  case "assignee":
6805
6920
  return subject.assignees.some((assignee) => assignee.toLowerCase() === value);
6806
6921
  case "title-contains":
@@ -8311,6 +8426,362 @@ import { Command as Command7 } from "commander";
8311
8426
  function isWatchClosed(state) {
8312
8427
  return state === "closed";
8313
8428
  }
8429
+ function issueConversation(surface) {
8430
+ return {
8431
+ kind: "issue",
8432
+ number: surface.issue.number,
8433
+ state: surface.state === "CLOSED" ? "closed" : "open",
8434
+ messages: surface.messages
8435
+ };
8436
+ }
8437
+ function prConversation(surface) {
8438
+ return {
8439
+ kind: "pr",
8440
+ number: surface.pr.number,
8441
+ state: surface.pr.state === "OPEN" ? "open" : "closed",
8442
+ messages: [...surface.messages, ...surface.threads.flatMap((thread) => thread.comments)]
8443
+ };
8444
+ }
8445
+ function decideConversation(conversation, p) {
8446
+ if (isWatchClosed(conversation.state)) {
8447
+ return { kind: "skip", surface: conversation, reason: "closed", detail: "the item is closed or merged" };
8448
+ }
8449
+ const agent = p.agentUser.toLowerCase();
8450
+ const allowed = new Set(p.allowedUsers.map((user) => user.toLowerCase()));
8451
+ const relevant = conversation.messages.filter((message) => {
8452
+ const author = message.author.toLowerCase();
8453
+ return author === agent || allowed.has(author);
8454
+ });
8455
+ let newest2 = null;
8456
+ for (const message of relevant) {
8457
+ if (message.author.toLowerCase() !== agent) continue;
8458
+ if (newest2 === null || message.createdAt > newest2.createdAt) newest2 = message;
8459
+ }
8460
+ if (newest2 === null) {
8461
+ return {
8462
+ kind: "skip",
8463
+ surface: conversation,
8464
+ reason: "no-agent-message",
8465
+ detail: `${p.agentUser} has not written here`
8466
+ };
8467
+ }
8468
+ const marker = { commentId: "", createdAt: newest2.createdAt };
8469
+ const answeredBy = p.allowedUsers.find((user) => agentAnsweredAfter(relevant, user, marker));
8470
+ if (answeredBy !== void 0) {
8471
+ return {
8472
+ kind: "skip",
8473
+ surface: conversation,
8474
+ reason: "answered",
8475
+ detail: `${answeredBy} answered after ${p.agentUser}'s message at ${newest2.createdAt}`
8476
+ };
8477
+ }
8478
+ return {
8479
+ kind: "reply",
8480
+ surface: conversation,
8481
+ agentMessageAt: newest2.createdAt,
8482
+ reason: `${p.agentUser}'s message at ${newest2.createdAt} has no answer from an allowed user`
8483
+ };
8484
+ }
8485
+ function decideConductorReply(conversations, p) {
8486
+ const decisions = conversations.map((conversation) => decideConversation(conversation, p));
8487
+ return { needsReply: decisions.some((decision) => decision.kind === "reply"), decisions };
8488
+ }
8489
+
8490
+ // src/conductor/thread.ts
8491
+ function defaultReplyTarget(thread) {
8492
+ return thread.kind === "issue" ? { kind: "issue", number: thread.issue.issue.number } : { kind: "pr", number: thread.prs[0].surface.pr.number };
8493
+ }
8494
+ function postingInstruction(target) {
8495
+ const command = target.kind === "pr" ? "pr" : "issue";
8496
+ const number = String(target.number);
8497
+ return [
8498
+ `Post your reply as a comment on ${target.kind === "pr" ? "pull request" : "issue"} #${number}. What you print is discarded; only a posted comment counts.`,
8499
+ `Run \`gh ${command} comment ${number} --body-file -\` and give the body on standard input, for example with a here-document.`,
8500
+ "Post exactly one comment. Do not approve, merge or close anything."
8501
+ ].join("\n");
8502
+ }
8503
+ function dryRunInstruction(target) {
8504
+ return [
8505
+ `Write your reply to ${target.kind === "pr" ? "pull request" : "issue"} #${String(target.number)}. This is a dry run: you cannot post a comment, and you must not try.`,
8506
+ "Make the reply, and nothing else, your final message. Do not approve, merge or close anything."
8507
+ ].join("\n");
8508
+ }
8509
+ function visible(messages, participants) {
8510
+ return analyzeSurface(messages, participants).messages.map((message) => ({
8511
+ kind: message.kind,
8512
+ author: message.author,
8513
+ body: message.body,
8514
+ createdAt: message.createdAt
8515
+ }));
8516
+ }
8517
+ function renderMessages(messages, participants) {
8518
+ const kept = visible(messages, participants);
8519
+ return kept.length === 0 ? "(no messages)" : formatMessages(kept.map((m) => ({ ...m, isNew: false })));
8520
+ }
8521
+ function renderThread(thread, participants) {
8522
+ const comments = visible(thread.comments, participants);
8523
+ if (comments.length === 0) return null;
8524
+ const location = thread.line === null ? `${thread.path}:(file)` : `${thread.path}:${String(thread.line)}`;
8525
+ const link = thread.url === null ? "" : `
8526
+ ${thread.url}`;
8527
+ return `${location}${link}
8528
+ ${formatMessages(comments.map((m) => ({ ...m, isNew: false })))}`;
8529
+ }
8530
+ function renderChecks(checks) {
8531
+ if (checks.length === 0) return "No checks reported.";
8532
+ return checks.map((check) => `- ${check.name}: ${check.status === "COMPLETED" ? check.conclusion ?? "COMPLETED" : check.status}`).join("\n");
8533
+ }
8534
+ function descriptionLines(description, participants) {
8535
+ if (description === void 0 || description.body.trim().length === 0) return [""];
8536
+ const [kept] = visible([{ kind: "issue-body", author: description.author, body: description.body, createdAt: "" }], participants);
8537
+ if (kept === void 0) return [""];
8538
+ return ["", `Pull request description (by ${kept.author}):`, "", kept.body, ""];
8539
+ }
8540
+ function prSection(pr, participants) {
8541
+ const { surface, checks } = pr;
8542
+ const number = String(surface.pr.number);
8543
+ const lines = [
8544
+ "",
8545
+ `Pull request #${number}: ${surface.pr.title}`,
8546
+ `Pull request URL: ${surface.pr.url}`,
8547
+ `State: ${surface.pr.state}${surface.pr.isDraft ? " (draft)" : ""} \xB7 Branch: ${surface.pr.headRefName} into ${surface.pr.baseRefName}`,
8548
+ ...descriptionLines(surface.description, participants),
8549
+ `Conversation on pull request #${number} (authorized accounts and you only, oldest first):`,
8550
+ "",
8551
+ renderMessages(surface.messages, participants)
8552
+ ];
8553
+ const threads = surface.threads.filter((thread) => !thread.isResolved).map((thread) => renderThread(thread, participants)).filter((text) => text !== null);
8554
+ if (threads.length > 0) {
8555
+ lines.push("", `Unresolved review threads on pull request #${number}:`, "", threads.join("\n\n"));
8556
+ }
8557
+ lines.push("", `CI status of pull request #${number}:`, "", renderChecks(checks));
8558
+ return lines;
8559
+ }
8560
+ function composeConductorPrompt(input) {
8561
+ const { thread, repo, participants, frame } = input;
8562
+ const replyTo = input.replyTo ?? defaultReplyTarget(thread);
8563
+ const lines = [
8564
+ frame,
8565
+ "",
8566
+ "--- Thread assembled by automata ---",
8567
+ `Repository: ${repo.owner}/${repo.repo}`,
8568
+ `The agent is: ${participants.agentUser}`
8569
+ ];
8570
+ if (thread.kind === "issue") {
8571
+ lines.push(
8572
+ "",
8573
+ `Issue #${String(thread.issue.issue.number)}: ${thread.issue.issue.title}`,
8574
+ `Issue URL: ${thread.issue.issue.url}`,
8575
+ `State: ${thread.issue.state}`,
8576
+ "",
8577
+ "Conversation on the issue (authorized accounts and you only, oldest first):",
8578
+ "",
8579
+ renderMessages(thread.issue.messages, participants)
8580
+ );
8581
+ }
8582
+ for (const pr of thread.prs) lines.push(...prSection(pr, participants));
8583
+ lines.push(
8584
+ "",
8585
+ "Only the messages above exist. Anything from other accounts has been withheld",
8586
+ "deliberately \u2014 do not ask about it.",
8587
+ "",
8588
+ input.dryRun === true ? dryRunInstruction(replyTo) : postingInstruction(replyTo)
8589
+ );
8590
+ return lines.join("\n");
8591
+ }
8592
+
8593
+ // src/conductor/execution.ts
8594
+ var EXECUTORS = ["claude", "codex"];
8595
+ function isRecord(value) {
8596
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8597
+ }
8598
+ function containerProblem(value, name) {
8599
+ if (value === void 0 || value === null) return null;
8600
+ if (!isRecord(value)) return `conductor.${name} must be an object, got ${JSON.stringify(value)}.`;
8601
+ for (const [key2, entry] of Object.entries(value)) {
8602
+ if (!EXECUTORS.includes(key2)) {
8603
+ return `conductor.${name}.${key2} is not a recognised setting; expected one of: ${EXECUTORS.join(", ")}.`;
8604
+ }
8605
+ if (entry !== void 0 && typeof entry !== "string") {
8606
+ return `conductor.${name}.${key2} must be a string, got ${JSON.stringify(entry)}.`;
8607
+ }
8608
+ }
8609
+ return null;
8610
+ }
8611
+ function conductorExecutionProblem(conductor) {
8612
+ const executor = conductor?.executor;
8613
+ if (executor !== void 0 && executor !== null && (typeof executor !== "string" || !EXECUTORS.includes(executor))) {
8614
+ return `conductor.executor must be one of: ${EXECUTORS.join(", ")}; got ${JSON.stringify(executor)}.`;
8615
+ }
8616
+ return containerProblem(conductor?.models, "models") ?? containerProblem(conductor?.effort, "effort");
8617
+ }
8618
+ function clean(value) {
8619
+ const trimmed2 = value?.trim();
8620
+ return trimmed2 === void 0 || trimmed2.length === 0 ? void 0 : trimmed2;
8621
+ }
8622
+ function resolveConductorExecution(conductor) {
8623
+ const executor = conductor?.executor ?? "claude";
8624
+ return {
8625
+ executor,
8626
+ model: clean(conductor?.models?.[executor]),
8627
+ effort: clean(conductor?.effort?.[executor])
8628
+ };
8629
+ }
8630
+ function describeConductorExecution(execution) {
8631
+ const model = execution.model === void 0 ? " (no model override)" : ` \xB7 model ${execution.model}`;
8632
+ const effort = execution.effort === void 0 ? "" : ` \xB7 effort ${execution.effort}`;
8633
+ return `${execution.executor}${model}${effort}`;
8634
+ }
8635
+
8636
+ // src/conductor/reply.ts
8637
+ function isComment(message) {
8638
+ return message.kind === "issue-comment" || message.kind === "pr-comment";
8639
+ }
8640
+ function key(message) {
8641
+ return `${message.createdAt}\0${message.body}`;
8642
+ }
8643
+ function newMessagesBy(before, after, login2) {
8644
+ const who = login2.toLowerCase();
8645
+ const own = (m) => isComment(m) && m.author.toLowerCase() === who;
8646
+ const known = /* @__PURE__ */ new Map();
8647
+ for (const m of before.filter(own)) known.set(key(m), (known.get(key(m)) ?? 0) + 1);
8648
+ return after.filter((m) => {
8649
+ if (!own(m)) return false;
8650
+ const left = known.get(key(m)) ?? 0;
8651
+ if (left > 0) known.set(key(m), left - 1);
8652
+ return left === 0;
8653
+ });
8654
+ }
8655
+ async function conductReply(input) {
8656
+ let before;
8657
+ try {
8658
+ before = input.read();
8659
+ } catch (err) {
8660
+ return { kind: "unverified", error: `could not read the conversation before the run: ${err.message}` };
8661
+ }
8662
+ let runError = null;
8663
+ try {
8664
+ await input.run();
8665
+ } catch (err) {
8666
+ runError = err.message;
8667
+ }
8668
+ let after;
8669
+ try {
8670
+ after = input.read();
8671
+ } catch (err) {
8672
+ return { kind: "unverified", error: `could not read the conversation after the run: ${err.message}` };
8673
+ }
8674
+ const posted = newMessagesBy(before, after, input.login);
8675
+ if (posted.length > 0) return { kind: "posted", count: posted.length, runError };
8676
+ return runError === null ? { kind: "posted-nothing" } : { kind: "run-failed", error: runError };
8677
+ }
8678
+
8679
+ // src/conductor/checkReport.ts
8680
+ function section(title, lines, problems = []) {
8681
+ return { title, lines, problems };
8682
+ }
8683
+ function label(id, kind) {
8684
+ return `${kind === "pr" ? "PR" : "issue"} #${String(id)}`;
8685
+ }
8686
+ function lockSection2(status, staleMinutes) {
8687
+ const fix = "remove .automata/conductor.lock once you have confirmed that no conductor runs";
8688
+ switch (status.kind) {
8689
+ case "free":
8690
+ return section("Run lock", ["no conductor is running in this checkout"]);
8691
+ case "held": {
8692
+ const held = status.heldForMs === null ? "" : ` (${describeDuration(status.heldForMs)} so far)`;
8693
+ return section("Run lock", [
8694
+ `a conductor is running: pid ${String(status.owner.pid)} on ${status.owner.host}, started ${status.owner.startedAt}${held}`,
8695
+ "a tick started now would do nothing"
8696
+ ]);
8697
+ }
8698
+ case "suspect":
8699
+ return section(
8700
+ "Run lock",
8701
+ [`held longer than ${String(staleMinutes)} minutes by pid ${String(status.owner.pid)} on ${status.owner.host}`],
8702
+ [{ section: "lock", summary: `the lock looks alive but outlived the stale window, probably a reused pid: ${fix}` }]
8703
+ );
8704
+ case "stale":
8705
+ return section("Run lock", ["a stale lock is present; the next tick takes it over"]);
8706
+ case "unreadable":
8707
+ return section(
8708
+ "Run lock",
8709
+ [`the lock cannot be read: ${status.detail}`],
8710
+ [{ section: "lock", summary: `the lock file cannot be read (${status.detail}); fix its permissions` }]
8711
+ );
8712
+ }
8713
+ }
8714
+ function decisionLine(decision) {
8715
+ const where = label(decision.surface.number, decision.surface.kind);
8716
+ return decision.kind === "reply" ? `reply on ${where}: ${decision.reason}` : `skip ${where} (${decision.reason}): ${decision.detail}`;
8717
+ }
8718
+ function watchSections(watched) {
8719
+ const listLines = [];
8720
+ const replyLines = [];
8721
+ const problems = [];
8722
+ for (const entry of watched) {
8723
+ if ("unavailable" in entry) {
8724
+ listLines.push(`#${String(entry.id)}: unavailable (${entry.unavailable}); a tick keeps it and skips it`);
8725
+ problems.push({ section: "watch", summary: `#${String(entry.id)} could not be read: ${entry.unavailable}` });
8726
+ continue;
8727
+ }
8728
+ const name = label(entry.id, entry.kind);
8729
+ if (entry.state === "closed") {
8730
+ listLines.push(`${name}: closed, a tick would drop it from the watch list`);
8731
+ continue;
8732
+ }
8733
+ listLines.push(`${name}: open`);
8734
+ for (const decision of entry.decisions) replyLines.push(decisionLine(decision));
8735
+ }
8736
+ return [
8737
+ section(`Watch list (${String(watched.length)})`, listLines, problems),
8738
+ section("Replies a tick would write", replyLines)
8739
+ ];
8740
+ }
8741
+ function buildConductorCheck(input) {
8742
+ const sections = input.configProblem === null ? [
8743
+ section("Configuration", [`running as ${input.identity}`, `executor: ${input.execution}`]),
8744
+ lockSection2(input.lock, input.staleMinutes),
8745
+ ...watchSections(input.watched)
8746
+ ] : [
8747
+ section(
8748
+ "Configuration",
8749
+ [input.configProblem],
8750
+ [{ section: "configuration", summary: input.configProblem }]
8751
+ )
8752
+ ];
8753
+ const problems = sections.flatMap((s) => s.problems);
8754
+ const parts = [
8755
+ `automata conductor --check \u2014 ${input.repo ?? "unknown repository"} \u2014 ${input.generatedAt.toISOString()}`,
8756
+ ""
8757
+ ];
8758
+ for (const s of sections) {
8759
+ parts.push(s.title, ...(s.lines.length === 0 ? ["(nothing to report)"] : s.lines).map((l) => ` ${l}`), "");
8760
+ }
8761
+ if (problems.length > 0) {
8762
+ parts.push(`Problems (${String(problems.length)})`, ...problems.map((p) => ` \xB7 ${p.section}: ${p.summary}`), "");
8763
+ }
8764
+ parts.push(problems.length === 0 ? "RESULT: healthy" : `RESULT: ${String(problems.length)} problem(s) found`);
8765
+ return { text: parts.join("\n") + "\n", exitCode: problems.length === 0 ? 0 : 1, problems };
8766
+ }
8767
+
8768
+ // src/conductor/dryRunReply.ts
8769
+ function claudeResult(raw) {
8770
+ let result = null;
8771
+ for (const line of raw.split("\n")) {
8772
+ if (!line.trim().startsWith("{")) continue;
8773
+ try {
8774
+ const event = JSON.parse(line);
8775
+ if (event["type"] === "result" && typeof event["result"] === "string") result = event["result"];
8776
+ } catch {
8777
+ }
8778
+ }
8779
+ return result;
8780
+ }
8781
+ function extractDryRunReply(executor, stdout) {
8782
+ const text = (executor === "claude" ? claudeResult(stdout) : stdout)?.trim();
8783
+ return text === void 0 || text.length === 0 ? null : text;
8784
+ }
8314
8785
 
8315
8786
  // src/conductor/watchList.ts
8316
8787
  function parseWatchId(raw) {
@@ -8369,6 +8840,118 @@ function pruneWatchList(config) {
8369
8840
  }
8370
8841
  }
8371
8842
  if (dropped.length > 0) updateWatch((current) => current.filter((id) => !dropped.includes(id)));
8843
+ return watch.filter((id) => !dropped.includes(id));
8844
+ }
8845
+ function onceLinkMap() {
8846
+ let map = null;
8847
+ return () => map ??= getOpenPrLinkMap();
8848
+ }
8849
+ function readPr(number) {
8850
+ return { surface: getPrSurface(number), checks: getPrChecks(number) };
8851
+ }
8852
+ function readWatchedItem(id, linkMap = getOpenPrLinkMap) {
8853
+ const target = getWatchTarget(id);
8854
+ if (target.kind === "pr") {
8855
+ const pr = readPr(id);
8856
+ return { thread: { kind: "pr", prs: [pr] }, conversations: [prConversation(pr.surface)] };
8857
+ }
8858
+ const issue = getIssueSurface(id);
8859
+ const prs = (linkMap().byIssue.get(id) ?? []).map((ref) => readPr(ref.number));
8860
+ return {
8861
+ thread: { kind: "issue", issue, prs },
8862
+ conversations: [issueConversation(issue), ...prs.map((pr) => prConversation(pr.surface))]
8863
+ };
8864
+ }
8865
+ function readTargetMessages(target) {
8866
+ return target.kind === "issue" ? issueConversation(getIssueSurface(target.number)).messages : prConversation(getPrSurface(target.number)).messages;
8867
+ }
8868
+ function runModel(prompt, execution, replyTo) {
8869
+ const { owner, repo } = getRepoSlug();
8870
+ return runClaude(prompt, { model: execution.model, effort: execution.effort, readOnly: true, replyTo, ghRepo: `${owner}/${repo}` });
8871
+ }
8872
+ async function runModelForText(prompt, execution) {
8873
+ const { owner, repo } = getRepoSlug();
8874
+ let stdout = "";
8875
+ const sink2 = {
8876
+ output: (stream, text) => {
8877
+ if (stream === "stdout") stdout += text;
8878
+ },
8879
+ exited: () => void 0
8880
+ };
8881
+ const options = { model: execution.model, effort: execution.effort, readOnly: true, ghRepo: `${owner}/${repo}`, sink: sink2 };
8882
+ await runClaude(prompt, options);
8883
+ return extractDryRunReply(execution.executor, stdout);
8884
+ }
8885
+ function targetKey(target) {
8886
+ return `${target.kind}#${String(target.number)}`;
8887
+ }
8888
+ async function conductItem(id, config, login2, participants, execution, dryRun) {
8889
+ const label2 = `#${String(id)}`;
8890
+ let item;
8891
+ try {
8892
+ item = readWatchedItem(id, dryRun?.linkMap);
8893
+ } catch (err) {
8894
+ process.stderr.write(`Warning: could not read ${label2}, skipping it this tick: ${err.message}
8895
+ `);
8896
+ return "idle";
8897
+ }
8898
+ const { thread, conversations } = item;
8899
+ const verdict = decideConductorReply(conversations, participants);
8900
+ const owed = verdict.decisions.find(
8901
+ (decision) => decision.kind === "reply" && dryRun?.answered.has(targetKey(decision.surface)) !== true
8902
+ );
8903
+ if (owed === void 0) {
8904
+ process.stdout.write(`Conductor: ${label2} needs no reply.
8905
+ `);
8906
+ return "idle";
8907
+ }
8908
+ const replyTo = { kind: owed.surface.kind, number: owed.surface.number };
8909
+ const frame = thread.kind === "issue" ? config.conductor?.prompts?.issue ?? DEFAULT_CONDUCTOR_ISSUE_PROMPT : config.conductor?.prompts?.pr ?? DEFAULT_CONDUCTOR_PR_PROMPT;
8910
+ const prompt = composeConductorPrompt({ thread, repo: getRepoSlug(), participants, frame, replyTo, dryRun: dryRun !== void 0 });
8911
+ const where = `${replyTo.kind === "pr" ? "pull request" : "issue"} #${String(replyTo.number)}`;
8912
+ process.stdout.write(`Conductor: ${label2} needs a reply on ${where}; running ${describeConductorExecution(execution)}.
8913
+ `);
8914
+ if (dryRun) {
8915
+ const reply = await runModelForText(prompt, execution);
8916
+ if (reply === null) {
8917
+ process.stderr.write(`Error: the dry run for ${label2} produced no reply text.
8918
+ `);
8919
+ return "failed";
8920
+ }
8921
+ dryRun.answered.add(targetKey(replyTo));
8922
+ process.stdout.write(`--- Dry run: reply for ${where} (not posted) ---
8923
+ ${reply}
8924
+ ---
8925
+ `);
8926
+ return "replied";
8927
+ }
8928
+ const outcome = await conductReply({
8929
+ login: login2,
8930
+ read: () => readTargetMessages(replyTo),
8931
+ run: () => runModel(prompt, execution, replyTo)
8932
+ });
8933
+ switch (outcome.kind) {
8934
+ case "posted":
8935
+ process.stdout.write(`Conductor: posted a reply on ${where}.
8936
+ `);
8937
+ if (outcome.runError !== null) {
8938
+ process.stderr.write(`Warning: the run reported "${outcome.runError}" but the reply is posted.
8939
+ `);
8940
+ }
8941
+ return "replied";
8942
+ case "posted-nothing":
8943
+ process.stderr.write(`Error: the run for ${label2} finished but posted no comment on ${where}.
8944
+ `);
8945
+ return "failed";
8946
+ case "run-failed":
8947
+ process.stderr.write(`Error: the run for ${label2} failed and posted no comment on ${where}: ${outcome.error}
8948
+ `);
8949
+ return "failed";
8950
+ case "unverified":
8951
+ process.stderr.write(`Error: could not tell whether ${label2} got a reply: ${outcome.error}
8952
+ `);
8953
+ return "failed";
8954
+ }
8372
8955
  }
8373
8956
  function loadGitHubConfig() {
8374
8957
  const config = readRawConfig();
@@ -8440,43 +9023,139 @@ function runWatchList() {
8440
9023
  function exitWith(code) {
8441
9024
  if (code !== 0) process.exit(code);
8442
9025
  }
8443
- function runConductor() {
9026
+ function preflight() {
8444
9027
  let config;
8445
9028
  try {
8446
9029
  config = readConfig();
8447
9030
  } catch (err) {
8448
- return fail2(err.message);
9031
+ return { problem: err.message };
8449
9032
  }
8450
9033
  if (!isExplicitGitHub(config)) {
8451
- return fail2(
8452
- azdoUnsupportedMessage(
9034
+ return {
9035
+ problem: azdoUnsupportedMessage(
8453
9036
  "conductor",
8454
9037
  "It needs the GitHub APIs; set the remote with `automata config set type gh`."
8455
9038
  )
8456
- );
9039
+ };
8457
9040
  }
8458
9041
  const allowedUsers = (config.allowedUsers ?? []).filter((user) => user.trim().length > 0);
8459
9042
  if (allowedUsers.length === 0) {
8460
- return fail2("No allowed users configured. Run `automata config set allowed-users <user1,user2>`.");
9043
+ return { problem: "No allowed users configured. Run `automata config set allowed-users <user1,user2>`." };
8461
9044
  }
8462
9045
  const agentUser = (config.agentUser ?? "").trim();
8463
9046
  if (agentUser.length === 0) {
8464
- return fail2("No agent user configured. Run `automata config set agent-user <login>`.");
9047
+ return { problem: "No agent user configured. Run `automata config set agent-user <login>`." };
8465
9048
  }
8466
9049
  let login2;
8467
9050
  try {
8468
9051
  login2 = getAuthenticatedLogin();
8469
9052
  } catch (err) {
8470
- return fail2(`\`gh\` could not be queried: ${err.message}`);
9053
+ return { problem: `\`gh\` could not be queried: ${err.message}` };
8471
9054
  }
8472
- const problem = conductorIdentityProblemFor(login2, agentUser, allowedUsers);
8473
- if (problem !== null) return fail2(problem);
9055
+ const identityProblem = conductorIdentityProblemFor(login2, agentUser, allowedUsers);
9056
+ if (identityProblem !== null) return { problem: identityProblem };
8474
9057
  const rawStale = config.doWork?.lockStaleMinutes;
8475
9058
  if (rawStale !== void 0 && rawStale !== null) {
8476
9059
  if (typeof rawStale !== "number" || !Number.isSafeInteger(rawStale) || rawStale < 1) {
8477
- return fail2(`doWork.lockStaleMinutes must be a positive integer, got ${JSON.stringify(rawStale)}.`);
9060
+ return { problem: `doWork.lockStaleMinutes must be a positive integer, got ${JSON.stringify(rawStale)}.` };
9061
+ }
9062
+ }
9063
+ const executionProblem = conductorExecutionProblem(config.conductor);
9064
+ if (executionProblem !== null) return { problem: executionProblem };
9065
+ if (config.conductor?.executor === "codex") {
9066
+ return { problem: "conductor.executor codex is not supported: Codex cannot be limited to comments on the reply target. Use claude." };
9067
+ }
9068
+ return {
9069
+ config,
9070
+ login: login2 ?? "",
9071
+ participants: { allowedUsers, agentUser },
9072
+ execution: resolveConductorExecution(config.conductor)
9073
+ };
9074
+ }
9075
+ function staleMinutesOf(config) {
9076
+ return config.doWork?.lockStaleMinutes ?? DEFAULT_DO_WORK.lockStaleMinutes;
9077
+ }
9078
+ function checkWatched(id, participants, linkMap) {
9079
+ try {
9080
+ const target = getWatchTarget(id);
9081
+ if (isWatchClosed(target.state)) return { id, kind: target.kind, state: "closed", decisions: [] };
9082
+ const { conversations } = readWatchedItem(id, linkMap);
9083
+ return { id, kind: target.kind, state: "open", decisions: decideConductorReply(conversations, participants).decisions };
9084
+ } catch (err) {
9085
+ return { id, unavailable: err.message };
9086
+ }
9087
+ }
9088
+ function runConductorCheck() {
9089
+ const checked = preflight();
9090
+ const generatedAt = /* @__PURE__ */ new Date();
9091
+ let repo = null;
9092
+ try {
9093
+ const slug = getRepoSlug();
9094
+ repo = `${slug.owner}/${slug.repo}`;
9095
+ } catch {
9096
+ }
9097
+ const linkMap = onceLinkMap();
9098
+ const report = "problem" in checked ? buildConductorCheck({
9099
+ generatedAt,
9100
+ repo,
9101
+ configProblem: checked.problem,
9102
+ identity: "",
9103
+ execution: "",
9104
+ lock: { kind: "free" },
9105
+ staleMinutes: DEFAULT_DO_WORK.lockStaleMinutes,
9106
+ watched: []
9107
+ }) : buildConductorCheck({
9108
+ generatedAt,
9109
+ repo,
9110
+ configProblem: null,
9111
+ identity: checked.login,
9112
+ execution: describeConductorExecution(checked.execution),
9113
+ lock: inspectConductorLock(staleMinutesOf(checked.config)),
9114
+ staleMinutes: staleMinutesOf(checked.config),
9115
+ watched: normalizeWatch(checked.config.conductor?.watch).map((id) => checkWatched(id, checked.participants, linkMap))
9116
+ });
9117
+ process.stdout.write(report.text);
9118
+ return report.exitCode;
9119
+ }
9120
+ async function runConductorDryRun(checked) {
9121
+ const { config, login: login2, participants, execution } = checked;
9122
+ const lock = inspectConductorLock(staleMinutesOf(config));
9123
+ if (lock.kind === "held" || lock.kind === "suspect") {
9124
+ process.stderr.write(`Warning: a conductor is running here (pid ${String(lock.owner.pid)}); the dry run does not take the lock.
9125
+ `);
9126
+ }
9127
+ process.stdout.write(`Conductor: dry run as ${login2}; nothing is posted and the watch list is not changed.
9128
+ `);
9129
+ let failed = 0;
9130
+ const context = { linkMap: onceLinkMap(), answered: /* @__PURE__ */ new Set() };
9131
+ for (const id of normalizeWatch(config.conductor?.watch)) {
9132
+ try {
9133
+ if (isWatchClosed(getWatchTarget(id).state)) {
9134
+ process.stdout.write(`Conductor: #${String(id)} is closed; a tick would drop it from the watch list.
9135
+ `);
9136
+ continue;
9137
+ }
9138
+ } catch {
9139
+ }
9140
+ try {
9141
+ if (await conductItem(id, config, login2, participants, execution, context) === "failed") failed++;
9142
+ } catch (err) {
9143
+ process.stderr.write(`Error: #${String(id)} could not be conducted: ${err.message}
9144
+ `);
9145
+ failed++;
8478
9146
  }
8479
9147
  }
9148
+ return failed > 0 ? 1 : 0;
9149
+ }
9150
+ async function runConductor(options = {}) {
9151
+ if (options.check === true && options.dryRun === true) {
9152
+ return fail2("--check and --dry-run cannot be used together.");
9153
+ }
9154
+ if (options.check === true) return runConductorCheck();
9155
+ const checked = preflight();
9156
+ if ("problem" in checked) return fail2(checked.problem);
9157
+ if (options.dryRun === true) return runConductorDryRun(checked);
9158
+ const { config, login: login2, participants, execution } = checked;
8480
9159
  const staleMinutes = config.doWork?.lockStaleMinutes ?? DEFAULT_DO_WORK.lockStaleMinutes;
8481
9160
  const lock = acquireConductorLock(staleMinutes);
8482
9161
  if (!lock.ok) {
@@ -8495,18 +9174,28 @@ function runConductor() {
8495
9174
  return 0;
8496
9175
  }
8497
9176
  try {
8498
- process.stdout.write(`Conductor: running as ${login2 ?? ""}.
9177
+ process.stdout.write(`Conductor: running as ${login2}.
8499
9178
  `);
8500
- pruneWatchList(config);
8501
- return 0;
9179
+ const watch = pruneWatchList(config);
9180
+ let failed = 0;
9181
+ for (const id of watch) {
9182
+ try {
9183
+ if (await conductItem(id, config, login2, participants, execution) === "failed") failed++;
9184
+ } catch (err) {
9185
+ process.stderr.write(`Error: #${String(id)} could not be conducted: ${err.message}
9186
+ `);
9187
+ failed++;
9188
+ }
9189
+ }
9190
+ return failed > 0 ? 1 : 0;
8502
9191
  } finally {
8503
9192
  lock.handle.release();
8504
9193
  }
8505
9194
  }
8506
9195
  var conductorCommand = new Command7("conductor").description(
8507
9196
  "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());
9197
+ ).option("--check", "Report what a tick would do, including pending prunes, and change nothing; exit 1 on a problem").option("--dry-run", "Run the model for each owed reply and print it instead of posting; no lock, no prune").action(async (options) => {
9198
+ exitWith(await runConductor(options));
8510
9199
  });
8511
9200
  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
9201
  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.519",
4
4
  "description": "Automata CLI tool",
5
5
  "type": "module",
6
6
  "engines": {