automata-cli 0.12.0-develop.562 → 0.12.0-develop.567

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 +150 -9
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -8696,6 +8696,39 @@ function coveredByWatchedIssue(watch, linkedPrs) {
8696
8696
  return covered;
8697
8697
  }
8698
8698
 
8699
+ // src/conductor/copilot.ts
8700
+ function isCopilotAuthor(author) {
8701
+ return author.toLowerCase().includes("copilot");
8702
+ }
8703
+ function openCopilotThreads(surface) {
8704
+ return surface.threads.filter(
8705
+ (thread) => !thread.isResolved && thread.comments.length > 0 && isCopilotAuthor(thread.comments[0].author)
8706
+ );
8707
+ }
8708
+ function newestCopilotAt(threads) {
8709
+ let newest2 = null;
8710
+ for (const comment of threads.flatMap((thread) => thread.comments)) {
8711
+ if (isCopilotAuthor(comment.author) && (newest2 === null || comment.createdAt > newest2)) newest2 = comment.createdAt;
8712
+ }
8713
+ return newest2;
8714
+ }
8715
+ function renderCopilotBlock(number, threads, agentUser) {
8716
+ const items = threads.map((thread) => {
8717
+ const location = thread.line === null ? `${thread.path}:(file)` : `${thread.path}:${String(thread.line)}`;
8718
+ const first = thread.comments[0];
8719
+ const link = thread.url === null ? "" : ` (${thread.url})`;
8720
+ return `- ${location}${link}
8721
+ ${first.author}: ${first.body.trim().replaceAll("\n", "\n ")}`;
8722
+ });
8723
+ return [
8724
+ `Open Copilot review comments on pull request #${String(number)}:`,
8725
+ "",
8726
+ items.join("\n"),
8727
+ "",
8728
+ `Add to your reply an instruction to ${agentUser}: fix each comment above, and then resolve its review thread. Do not resolve a thread yourself.`
8729
+ ].join("\n");
8730
+ }
8731
+
8699
8732
  // src/conductor/replyDecision.ts
8700
8733
  function isWatchClosed(state) {
8701
8734
  return state === "closed";
@@ -8713,10 +8746,32 @@ function prConversation(surface) {
8713
8746
  kind: "pr",
8714
8747
  number: surface.pr.number,
8715
8748
  state: surface.pr.state === "OPEN" ? "open" : "closed",
8716
- messages: [...surface.messages, ...surface.threads.flatMap((thread) => thread.comments)]
8749
+ messages: [...surface.messages, ...surface.threads.flatMap((thread) => thread.comments)],
8750
+ copilotAt: newestCopilotAt(openCopilotThreads(surface))
8717
8751
  };
8718
8752
  }
8719
8753
  function decideConversation(conversation, p) {
8754
+ const decision = decideByAgentMessage(conversation, p);
8755
+ if (decision.kind === "reply") return decision;
8756
+ return copilotTurn(conversation) ?? decision;
8757
+ }
8758
+ function copilotTurn(conversation) {
8759
+ const copilotAt = conversation.copilotAt ?? null;
8760
+ if (copilotAt === null || isWatchClosed(conversation.state)) return null;
8761
+ let lastReply = "";
8762
+ for (const message of conversation.messages) {
8763
+ if (message.body.includes(REPLY_MARKER) && message.createdAt > lastReply) lastReply = message.createdAt;
8764
+ }
8765
+ if (copilotAt <= lastReply) return null;
8766
+ return {
8767
+ kind: "reply",
8768
+ surface: conversation,
8769
+ agentMessageAt: copilotAt,
8770
+ copilot: true,
8771
+ reason: `an unresolved Copilot review thread has a comment at ${copilotAt} that no conductor reply follows`
8772
+ };
8773
+ }
8774
+ function decideByAgentMessage(conversation, p) {
8720
8775
  if (isWatchClosed(conversation.state)) {
8721
8776
  return { kind: "skip", surface: conversation, reason: "closed", detail: "the item is closed or merged" };
8722
8777
  }
@@ -8858,6 +8913,8 @@ function prSection(pr, participants) {
8858
8913
  lines.push("", `Unresolved review threads on pull request #${number}:`, "", threads.join("\n\n"));
8859
8914
  }
8860
8915
  lines.push("", `CI status of pull request #${number}:`, "", renderChecks(checks));
8916
+ const copilot = openCopilotThreads(surface);
8917
+ if (copilot.length > 0) lines.push("", renderCopilotBlock(surface.pr.number, copilot, participants.agentUser));
8861
8918
  return lines;
8862
8919
  }
8863
8920
  function composeConductorPrompt(input) {
@@ -8936,6 +8993,35 @@ function describeConductorExecution(execution) {
8936
8993
  return `${execution.executor}${model}${effort}`;
8937
8994
  }
8938
8995
 
8996
+ // src/conductor/automatic.ts
8997
+ var DEFAULT_INTERVAL_SECONDS = 60;
8998
+ function parseInterval(raw) {
8999
+ if (raw === void 0) return DEFAULT_INTERVAL_SECONDS;
9000
+ if (!/^\d+$/.test(raw.trim()) || !Number.isSafeInteger(Number(raw)) || Number(raw) < 1) {
9001
+ return `--interval must be a positive whole number of seconds, got "${raw}".`;
9002
+ }
9003
+ return Number(raw);
9004
+ }
9005
+ async function runAutomatic(loop) {
9006
+ if (loop.stopped()) {
9007
+ loop.log("Conductor: stopped.");
9008
+ return 0;
9009
+ }
9010
+ const result = await loop.tick();
9011
+ if (result.open === 0) {
9012
+ loop.log("Conductor: no open item is watched any more; leaving the loop.");
9013
+ return 0;
9014
+ }
9015
+ if (loop.stopped()) {
9016
+ loop.log("Conductor: stopped.");
9017
+ return 0;
9018
+ }
9019
+ if (result.code !== 0) loop.log(`Conductor: the tick ended with exit code ${String(result.code)}; the next tick runs anyway.`);
9020
+ loop.log(`Conductor: next tick in ${String(loop.intervalSeconds)} s.`);
9021
+ await loop.sleep(loop.intervalSeconds);
9022
+ return runAutomatic(loop);
9023
+ }
9024
+
8939
9025
  // src/conductor/reply.ts
8940
9026
  function isComment(message) {
8941
9027
  return message.kind === "issue-comment" || message.kind === "pr-comment";
@@ -9304,6 +9390,10 @@ ${reply}
9304
9390
  `);
9305
9391
  return "replied";
9306
9392
  }
9393
+ function agentIsWorking(config) {
9394
+ const status = inspectRunLock(config.doWork?.lockStaleMinutes ?? DEFAULT_DO_WORK.lockStaleMinutes);
9395
+ return status.kind === "held" || status.kind === "suspect";
9396
+ }
9307
9397
  async function conductItem(id, config, login2, participants, execution, dryRun, linkMap) {
9308
9398
  const label2 = `#${String(id)}`;
9309
9399
  let item;
@@ -9324,6 +9414,11 @@ async function conductItem(id, config, login2, participants, execution, dryRun,
9324
9414
  const stopped = verdict.decisions.find((decision) => decision.kind === "skip" && (decision.reason === "blocked" || decision.reason === "limit"));
9325
9415
  const why = stopped?.kind === "skip" ? `Conductor: ${label2} gets no reply: ${stopped.detail}.` : `Conductor: ${label2} needs no reply.`;
9326
9416
  process.stdout.write(`${why}
9417
+ `);
9418
+ return "idle";
9419
+ }
9420
+ if (owed.kind === "reply" && owed.copilot === true && dryRun === void 0 && agentIsWorking(config)) {
9421
+ process.stdout.write(`Conductor: ${label2} has open Copilot review threads, but the agent is working (the do-work run lock is held); waiting.
9327
9422
  `);
9328
9423
  return "idle";
9329
9424
  }
@@ -9600,17 +9695,63 @@ async function runConductor(options = {}) {
9600
9695
  }
9601
9696
  }
9602
9697
  async function runConductorTick(options) {
9603
- if (options.check === true && options.dryRun === true) {
9604
- return fail2("--check and --dry-run cannot be used together.");
9605
- }
9698
+ const problem = optionsProblem(options);
9699
+ if (problem !== null) return fail2(problem);
9700
+ if (options.automatic === true) return runConductorAutomatic(options.interval);
9606
9701
  if (options.check === true) return runConductorCheck();
9702
+ return (await runTick2(options)).code;
9703
+ }
9704
+ function optionsProblem(options) {
9705
+ if (options.check === true && options.dryRun === true) return "--check and --dry-run cannot be used together.";
9706
+ if (options.automatic !== true) {
9707
+ return options.interval === void 0 ? null : "--interval needs --automatic.";
9708
+ }
9709
+ if (options.check === true || options.dryRun === true) return "--automatic cannot be used with --check or --dry-run.";
9710
+ const interval = parseInterval(options.interval);
9711
+ return typeof interval === "string" ? interval : null;
9712
+ }
9713
+ async function runConductorAutomatic(rawInterval) {
9714
+ const intervalSeconds = parseInterval(rawInterval);
9715
+ let stop = false;
9716
+ let wake = null;
9717
+ const onSignal = () => {
9718
+ if (stop) process.exit(130);
9719
+ stop = true;
9720
+ process.stdout.write("Conductor: stopping after the current tick.\n");
9721
+ wake?.();
9722
+ };
9723
+ process.on("SIGINT", onSignal);
9724
+ process.on("SIGTERM", onSignal);
9725
+ try {
9726
+ process.stdout.write(`Conductor: automatic mode, one tick every ${String(intervalSeconds)} s. Press Ctrl+C to stop.
9727
+ `);
9728
+ return await runAutomatic({
9729
+ intervalSeconds,
9730
+ tick: () => runTick2({}),
9731
+ stopped: () => stop,
9732
+ log: (line) => process.stdout.write(`${line}
9733
+ `),
9734
+ sleep: (seconds) => new Promise((resolve2) => {
9735
+ const timer = setTimeout(resolve2, seconds * 1e3);
9736
+ wake = () => {
9737
+ clearTimeout(timer);
9738
+ resolve2();
9739
+ };
9740
+ })
9741
+ });
9742
+ } finally {
9743
+ process.removeListener("SIGINT", onSignal);
9744
+ process.removeListener("SIGTERM", onSignal);
9745
+ }
9746
+ }
9747
+ async function runTick2(options) {
9607
9748
  const checked = preflight();
9608
- if ("problem" in checked) return fail2(checked.problem);
9609
- if (options.dryRun === true) return runConductorDryRun(checked);
9749
+ if ("problem" in checked) return { code: fail2(checked.problem), open: null };
9750
+ if (options.dryRun === true) return { code: await runConductorDryRun(checked), open: null };
9610
9751
  const { config, login: login2, participants, execution } = checked;
9611
9752
  const staleMinutes = config.doWork?.lockStaleMinutes ?? DEFAULT_DO_WORK.lockStaleMinutes;
9612
9753
  const lock = acquireConductorLock(staleMinutes);
9613
- if (!lock.ok) return reportHeldLock(lock.heldBy, lock.suspect, staleMinutes);
9754
+ if (!lock.ok) return { code: reportHeldLock(lock.heldBy, lock.suspect, staleMinutes), open: null };
9614
9755
  try {
9615
9756
  process.stdout.write(`Conductor: running as ${login2}.
9616
9757
  `);
@@ -9633,7 +9774,7 @@ async function runConductorTick(options) {
9633
9774
  failed++;
9634
9775
  }
9635
9776
  }
9636
- return failed > 0 ? 1 : 0;
9777
+ return { code: failed > 0 ? 1 : 0, open: watch.length };
9637
9778
  } finally {
9638
9779
  lock.handle.release();
9639
9780
  }
@@ -9643,7 +9784,7 @@ var conductorCommand = new Command7("conductor").description(
9643
9784
  ).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").option(
9644
9785
  "--verbose",
9645
9786
  "Trace every `git` and `gh` call with its duration and exit code, show the model's steps, and list the tool calls the run was denied"
9646
- ).action(async (options) => {
9787
+ ).option("--automatic", "Run a tick every --interval seconds until no open item is watched (Ctrl+C stops it)").option("--interval <seconds>", "Seconds between two ticks with --automatic (default 60)").action(async (options) => {
9647
9788
  exitWith(await runConductor(options));
9648
9789
  });
9649
9790
  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) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "automata-cli",
3
- "version": "0.12.0-develop.562",
3
+ "version": "0.12.0-develop.567",
4
4
  "description": "Automata CLI tool",
5
5
  "type": "module",
6
6
  "engines": {