automata-cli 0.11.0-develop.514 → 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 +295 -63
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -90,11 +90,11 @@ function writeDoWork(patch) {
90
90
  const current = readRawConfig();
91
91
  writeConfig({ ...current, doWork: { ...current.doWork, ...patch } });
92
92
  }
93
- function parseNonNegativeInt(value, label) {
93
+ function parseNonNegativeInt(value, label2) {
94
94
  const trimmed2 = value.trim();
95
95
  const parsed = Number.parseInt(trimmed2, 10);
96
96
  if (Number.isNaN(parsed) || parsed < 0 || String(parsed) !== trimmed2 || !Number.isSafeInteger(parsed)) {
97
- 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}").
98
98
  `);
99
99
  process.exit(1);
100
100
  }
@@ -1705,7 +1705,7 @@ function formatSonarIssue(issue) {
1705
1705
  const location = formatLocation(issue.path, issue.line);
1706
1706
  if (location) lines.push(` Location: ${location}`);
1707
1707
  if (issue.severity || issue.type) {
1708
- 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(" / ");
1709
1709
  lines.push(` Classification: ${labels}`);
1710
1710
  }
1711
1711
  if (issue.rule) lines.push(` Rule: ${sanitizeText(issue.rule)}`);
@@ -1718,7 +1718,7 @@ function formatSonarHotspot(hotspot) {
1718
1718
  if (location) lines.push(` Location: ${location}`);
1719
1719
  if (hotspot.status) lines.push(` Status: ${sanitizeText(hotspot.status)}`);
1720
1720
  if (hotspot.vulnerabilityProbability || hotspot.securityCategory) {
1721
- 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(" / ");
1722
1722
  lines.push(` Classification: ${labels}`);
1723
1723
  }
1724
1724
  if (hotspot.rule) {
@@ -2368,7 +2368,7 @@ function getIssueSurface(issueNumber) {
2368
2368
  issue: { number: raw.number, title: raw.title, body: raw.body, url: raw.url },
2369
2369
  state: raw.state === "CLOSED" ? "CLOSED" : "OPEN",
2370
2370
  assignees: (raw.assignees ?? []).map(login).filter((name) => name.length > 0),
2371
- 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),
2372
2372
  messages
2373
2373
  };
2374
2374
  }
@@ -2427,7 +2427,7 @@ function toPullRequestRef(node) {
2427
2427
  function toOrphanPr(node) {
2428
2428
  return {
2429
2429
  pr: toPullRequestRef(node),
2430
- 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),
2431
2431
  assignees: (node.assignees?.nodes ?? []).map((assignee) => assignee.login ?? "").filter((login2) => login2.length > 0)
2432
2432
  };
2433
2433
  }
@@ -4489,6 +4489,9 @@ var UNKNOWN_OWNER = {
4489
4489
  command: "unknown",
4490
4490
  token: ""
4491
4491
  };
4492
+ function inspectConductorLock(staleMinutes, now = Date.now()) {
4493
+ return inspectRunLock(staleMinutes, now, CONDUCTOR_LOCK_FILE);
4494
+ }
4492
4495
  function acquireConductorLock(staleMinutes) {
4493
4496
  return acquireRunLock("conductor", staleMinutes, CONDUCTOR_LOCK_FILE);
4494
4497
  }
@@ -4599,8 +4602,8 @@ function heldForMs(owner, now) {
4599
4602
  const startedAt = Date.parse(owner.startedAt);
4600
4603
  return Number.isNaN(startedAt) ? null : now - startedAt;
4601
4604
  }
4602
- function inspectRunLock(staleMinutes, now = Date.now()) {
4603
- const path = lockPath();
4605
+ function inspectRunLock(staleMinutes, now = Date.now(), lockFile = LOCK_FILE) {
4606
+ const path = lockPath(lockFile);
4604
4607
  try {
4605
4608
  statSync2(path);
4606
4609
  } catch (err) {
@@ -4785,9 +4788,9 @@ function detectRescueTarget(options, now) {
4785
4788
  return { branch: current, createdBranch: false, source: current };
4786
4789
  }
4787
4790
  const source = detached ? "detached HEAD" : current === options.baseBranch ? options.baseBranch : current;
4788
- const label = detached ? "detached" : flattenBranchName(source);
4791
+ const label2 = detached ? "detached" : flattenBranchName(source);
4789
4792
  return {
4790
- branch: `${RESCUE_BRANCH_PREFIX}${label}-${utcStamp(now)}`,
4793
+ branch: `${RESCUE_BRANCH_PREFIX}${label2}-${utcStamp(now)}`,
4791
4794
  createdBranch: true,
4792
4795
  source
4793
4796
  };
@@ -5203,9 +5206,9 @@ var RunTranscript = class {
5203
5206
  stderrAtLineStart = true;
5204
5207
  /** A private key began in an earlier line and has not ended yet. */
5205
5208
  inPrivateKey = false;
5206
- constructor(label, root = process.cwd(), now = /* @__PURE__ */ new Date()) {
5209
+ constructor(label2, root = process.cwd(), now = /* @__PURE__ */ new Date()) {
5207
5210
  const stamp = now.toISOString().replace(/[:.]/g, "-");
5208
- this.fileName = `${stamp}-${label}.log`;
5211
+ this.fileName = `${stamp}-${label2}.log`;
5209
5212
  const dir = join5(root, RUN_TRANSCRIPT_DIR);
5210
5213
  this.path = join5(dir, this.fileName);
5211
5214
  try {
@@ -6472,12 +6475,12 @@ function gitSection(status) {
6472
6475
  return build("git", lines, problems, { ...status });
6473
6476
  }
6474
6477
  function assembleReport(input) {
6475
- const byId = new Map(input.sections.map((section) => [section.id, section]));
6478
+ const byId = new Map(input.sections.map((section2) => [section2.id, section2]));
6476
6479
  const ordered = SECTION_ORDER.flatMap((id) => {
6477
- const section = byId.get(id);
6478
- return section === void 0 ? [] : [section];
6480
+ const section2 = byId.get(id);
6481
+ return section2 === void 0 ? [] : [section2];
6479
6482
  });
6480
- const problems = ordered.flatMap((section) => section.problems);
6483
+ const problems = ordered.flatMap((section2) => section2.problems);
6481
6484
  return {
6482
6485
  generatedAt: input.generatedAt,
6483
6486
  repo: input.repo,
@@ -6511,8 +6514,8 @@ function renderProblems(problems) {
6511
6514
  function renderText(report) {
6512
6515
  const head = `automata do-work --check \u2014 ${report.repo ?? "unknown repository"} \u2014 ${report.generatedAt.toISOString()}`;
6513
6516
  const parts = report.blocked === null ? [head, ""] : [`blocked: ${report.blocked.trigger}`, head, ""];
6514
- for (const section of report.sections) {
6515
- 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)"));
6516
6519
  }
6517
6520
  if (report.trace !== null) parts.push(...renderTrace(report.trace));
6518
6521
  if (report.problems.length > 0) parts.push(...renderProblems(report.problems));
@@ -6523,12 +6526,12 @@ function renderText(report) {
6523
6526
  }
6524
6527
  function toJson(report) {
6525
6528
  const sections = {};
6526
- for (const section of report.sections) {
6527
- sections[section.id] = {
6528
- title: section.title,
6529
- lines: section.lines,
6530
- problems: section.problems,
6531
- 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
6532
6535
  };
6533
6536
  }
6534
6537
  return {
@@ -6587,14 +6590,14 @@ var SettingsError = class extends Error {
6587
6590
  function failSettings(message) {
6588
6591
  throw new SettingsError(message);
6589
6592
  }
6590
- function parsePositiveInt(value, label) {
6593
+ function parsePositiveInt(value, label2) {
6591
6594
  const trimmed2 = value.trim();
6592
6595
  if (!/^\d+$/.test(trimmed2)) {
6593
- failSettings(`${label} must be a positive integer (got "${value}").`);
6596
+ failSettings(`${label2} must be a positive integer (got "${value}").`);
6594
6597
  }
6595
6598
  const parsed = Number(trimmed2);
6596
6599
  if (!Number.isSafeInteger(parsed) || parsed <= 0) {
6597
- 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}").`);
6598
6601
  }
6599
6602
  return parsed;
6600
6603
  }
@@ -6749,14 +6752,14 @@ function planRun(item, settings, execution) {
6749
6752
  }
6750
6753
  function describeAssignment(item, agentUser) {
6751
6754
  return claimStates(item).map((claim) => {
6752
- const label = claim.surface === "issue" ? "issue" : `pull request #${String(claim.number)}`;
6755
+ const label2 = claim.surface === "issue" ? "issue" : `pull request #${String(claim.number)}`;
6753
6756
  switch (claim.state) {
6754
6757
  case "would-claim":
6755
- return `would assign ${label} to ${agentUser}`;
6758
+ return `would assign ${label2} to ${agentUser}`;
6756
6759
  case "already-assigned":
6757
- return `${label} already assigned`;
6760
+ return `${label2} already assigned`;
6758
6761
  case "rule-exempt":
6759
- return `${label} not claimed (orphan pass)`;
6762
+ return `${label2} not claimed (orphan pass)`;
6760
6763
  }
6761
6764
  }).join(" \xB7 ");
6762
6765
  }
@@ -6832,24 +6835,24 @@ function validateOptionalInt(container, key2, path, min, hint) {
6832
6835
  failSettings(`${path} must be ${hint}, got ${JSON.stringify(value)}.`);
6833
6836
  }
6834
6837
  }
6835
- function validateDoWorkConfig(section) {
6836
- if (section === void 0 || section === null) return;
6837
- if (!isPlainObject(section)) {
6838
- 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)}.`);
6839
6842
  }
6840
- const executor = section["executor"];
6843
+ const executor = section2["executor"];
6841
6844
  if (executor !== void 0 && executor !== "claude" && executor !== "codex") {
6842
6845
  failSettings(`doWork.executor must be 'claude' or 'codex', got ${JSON.stringify(executor)}.`);
6843
6846
  }
6844
- validateOptionalString(section, "baseBranch", "doWork.baseBranch");
6845
- validateOptionalInt(section, "maxRunsPerTick", "doWork.maxRunsPerTick", 0, "a non-negative integer (0 = unlimited)");
6846
- validateOptionalInt(section, "lockStaleMinutes", "doWork.lockStaleMinutes", 1, "a positive integer");
6847
- validateOptionalBoolean(section, "dumpOnBlock", "doWork.dumpOnBlock");
6848
- validateOptionalBoolean(section, "postRunLog", "doWork.postRunLog");
6849
- validateProtectedBranches(section["protectedBranches"]);
6850
- validateSettingContainer(section["models"], "models", ["claude", "codex"]);
6851
- validateSettingContainer(section["effort"], "effort", ["claude", "codex"]);
6852
- validateSettingContainer(section["prompts"], "prompts", ["issueDiscuss", "prWork", "prOrphan"]);
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"]);
6853
6856
  }
6854
6857
  function validateOptionalBoolean(container, key2, path) {
6855
6858
  const value = container[key2];
@@ -6912,7 +6915,7 @@ function matchesDiscoveryFilter(subject, settings) {
6912
6915
  const value = settings.discoveryValue.toLowerCase();
6913
6916
  switch (settings.technique) {
6914
6917
  case "label":
6915
- return subject.labels.some((label) => label.toLowerCase() === value);
6918
+ return subject.labels.some((label2) => label2.toLowerCase() === value);
6916
6919
  case "assignee":
6917
6920
  return subject.assignees.some((assignee) => assignee.toLowerCase() === value);
6918
6921
  case "title-contains":
@@ -8497,6 +8500,12 @@ function postingInstruction(target) {
8497
8500
  "Post exactly one comment. Do not approve, merge or close anything."
8498
8501
  ].join("\n");
8499
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
+ }
8500
8509
  function visible(messages, participants) {
8501
8510
  return analyzeSurface(messages, participants).messages.map((message) => ({
8502
8511
  kind: message.kind,
@@ -8576,7 +8585,7 @@ function composeConductorPrompt(input) {
8576
8585
  "Only the messages above exist. Anything from other accounts has been withheld",
8577
8586
  "deliberately \u2014 do not ask about it.",
8578
8587
  "",
8579
- postingInstruction(replyTo)
8588
+ input.dryRun === true ? dryRunInstruction(replyTo) : postingInstruction(replyTo)
8580
8589
  );
8581
8590
  return lines.join("\n");
8582
8591
  }
@@ -8667,6 +8676,113 @@ async function conductReply(input) {
8667
8676
  return runError === null ? { kind: "posted-nothing" } : { kind: "run-failed", error: runError };
8668
8677
  }
8669
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
+ }
8785
+
8670
8786
  // src/conductor/watchList.ts
8671
8787
  function parseWatchId(raw) {
8672
8788
  const text = raw.trim().replace(/^#/, "");
@@ -8726,17 +8842,21 @@ function pruneWatchList(config) {
8726
8842
  if (dropped.length > 0) updateWatch((current) => current.filter((id) => !dropped.includes(id)));
8727
8843
  return watch.filter((id) => !dropped.includes(id));
8728
8844
  }
8845
+ function onceLinkMap() {
8846
+ let map = null;
8847
+ return () => map ??= getOpenPrLinkMap();
8848
+ }
8729
8849
  function readPr(number) {
8730
8850
  return { surface: getPrSurface(number), checks: getPrChecks(number) };
8731
8851
  }
8732
- function readWatchedItem(id) {
8852
+ function readWatchedItem(id, linkMap = getOpenPrLinkMap) {
8733
8853
  const target = getWatchTarget(id);
8734
8854
  if (target.kind === "pr") {
8735
8855
  const pr = readPr(id);
8736
8856
  return { thread: { kind: "pr", prs: [pr] }, conversations: [prConversation(pr.surface)] };
8737
8857
  }
8738
8858
  const issue = getIssueSurface(id);
8739
- const prs = (getOpenPrLinkMap().byIssue.get(id) ?? []).map((ref) => readPr(ref.number));
8859
+ const prs = (linkMap().byIssue.get(id) ?? []).map((ref) => readPr(ref.number));
8740
8860
  return {
8741
8861
  thread: { kind: "issue", issue, prs },
8742
8862
  conversations: [issueConversation(issue), ...prs.map((pr) => prConversation(pr.surface))]
@@ -8749,30 +8869,62 @@ function runModel(prompt, execution, replyTo) {
8749
8869
  const { owner, repo } = getRepoSlug();
8750
8870
  return runClaude(prompt, { model: execution.model, effort: execution.effort, readOnly: true, replyTo, ghRepo: `${owner}/${repo}` });
8751
8871
  }
8752
- async function conductItem(id, config, login2, participants, execution) {
8753
- const label = `#${String(id)}`;
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)}`;
8754
8890
  let item;
8755
8891
  try {
8756
- item = readWatchedItem(id);
8892
+ item = readWatchedItem(id, dryRun?.linkMap);
8757
8893
  } catch (err) {
8758
- process.stderr.write(`Warning: could not read ${label}, skipping it this tick: ${err.message}
8894
+ process.stderr.write(`Warning: could not read ${label2}, skipping it this tick: ${err.message}
8759
8895
  `);
8760
8896
  return "idle";
8761
8897
  }
8762
8898
  const { thread, conversations } = item;
8763
8899
  const verdict = decideConductorReply(conversations, participants);
8764
- const owed = verdict.decisions.find((decision) => decision.kind === "reply");
8900
+ const owed = verdict.decisions.find(
8901
+ (decision) => decision.kind === "reply" && dryRun?.answered.has(targetKey(decision.surface)) !== true
8902
+ );
8765
8903
  if (owed === void 0) {
8766
- process.stdout.write(`Conductor: ${label} needs no reply.
8904
+ process.stdout.write(`Conductor: ${label2} needs no reply.
8767
8905
  `);
8768
8906
  return "idle";
8769
8907
  }
8770
8908
  const replyTo = { kind: owed.surface.kind, number: owed.surface.number };
8771
8909
  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 });
8910
+ const prompt = composeConductorPrompt({ thread, repo: getRepoSlug(), participants, frame, replyTo, dryRun: dryRun !== void 0 });
8773
8911
  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)}.
8912
+ process.stdout.write(`Conductor: ${label2} needs a reply on ${where}; running ${describeConductorExecution(execution)}.
8775
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
+ }
8776
8928
  const outcome = await conductReply({
8777
8929
  login: login2,
8778
8930
  read: () => readTargetMessages(replyTo),
@@ -8788,15 +8940,15 @@ async function conductItem(id, config, login2, participants, execution) {
8788
8940
  }
8789
8941
  return "replied";
8790
8942
  case "posted-nothing":
8791
- process.stderr.write(`Error: the run for ${label} finished but posted no comment on ${where}.
8943
+ process.stderr.write(`Error: the run for ${label2} finished but posted no comment on ${where}.
8792
8944
  `);
8793
8945
  return "failed";
8794
8946
  case "run-failed":
8795
- process.stderr.write(`Error: the run for ${label} failed and posted no comment on ${where}: ${outcome.error}
8947
+ process.stderr.write(`Error: the run for ${label2} failed and posted no comment on ${where}: ${outcome.error}
8796
8948
  `);
8797
8949
  return "failed";
8798
8950
  case "unverified":
8799
- process.stderr.write(`Error: could not tell whether ${label} got a reply: ${outcome.error}
8951
+ process.stderr.write(`Error: could not tell whether ${label2} got a reply: ${outcome.error}
8800
8952
  `);
8801
8953
  return "failed";
8802
8954
  }
@@ -8920,9 +9072,89 @@ function preflight() {
8920
9072
  execution: resolveConductorExecution(config.conductor)
8921
9073
  };
8922
9074
  }
8923
- async function runConductor() {
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++;
9146
+ }
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();
8924
9155
  const checked = preflight();
8925
9156
  if ("problem" in checked) return fail2(checked.problem);
9157
+ if (options.dryRun === true) return runConductorDryRun(checked);
8926
9158
  const { config, login: login2, participants, execution } = checked;
8927
9159
  const staleMinutes = config.doWork?.lockStaleMinutes ?? DEFAULT_DO_WORK.lockStaleMinutes;
8928
9160
  const lock = acquireConductorLock(staleMinutes);
@@ -8962,8 +9194,8 @@ async function runConductor() {
8962
9194
  }
8963
9195
  var conductorCommand = new Command7("conductor").description(
8964
9196
  "Run one conductor tick as an allowed (human) account: verify the identity, take the conductor's own run lock, then act"
8965
- ).action(async () => {
8966
- exitWith(await 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));
8967
9199
  });
8968
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) => {
8969
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.514",
3
+ "version": "0.11.0-develop.519",
4
4
  "description": "Automata CLI tool",
5
5
  "type": "module",
6
6
  "engines": {