automata-cli 0.12.0-develop.554 → 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 +463 -105
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5435,6 +5435,44 @@ async function scrubExcerpt(execution, excerpt, timeoutMs = SECOND_OPINION_TIMEO
5435
5435
  return result.ok ? interpretScrubOutput(result.stdout, excerpt) : result;
5436
5436
  }
5437
5437
 
5438
+ // src/run/claudeAuth.ts
5439
+ var CLAUDE_AUTH_PATTERNS = [
5440
+ /Failed to authenticate/i,
5441
+ /OAuth (?:session|token) (?:has )?expired/i,
5442
+ /Invalid API key\s*[·\-:]\s*Please run \/login/i,
5443
+ /Please run \/login/i
5444
+ ];
5445
+ var CLAUDE_AUTH_MARKER = "<!-- automata:claude-auth -->";
5446
+ function detectClaudeAuthFailure(text) {
5447
+ return CLAUDE_AUTH_PATTERNS.some((pattern) => pattern.test(text));
5448
+ }
5449
+ var CLAUDE_AUTH_TAIL_MAX = 8192;
5450
+ var ClaudeAuthTail = class {
5451
+ text = "";
5452
+ output(_stream, chunk) {
5453
+ this.text = (this.text + chunk).slice(-CLAUDE_AUTH_TAIL_MAX);
5454
+ }
5455
+ exited() {
5456
+ }
5457
+ get tail() {
5458
+ return this.text;
5459
+ }
5460
+ };
5461
+ var CLAUDE_AUTH_REASON = "Claude Code is not signed in (its login expired and could not be refreshed)";
5462
+ var CLAUDE_AUTH_REMEDY = "Run `claude /login` again on the machine that runs automata (inside the devcontainer, if it runs there), then reply to have another attempt made.";
5463
+ function renderClaudeAuthComment(surface, excerpt) {
5464
+ const lines = [
5465
+ `automata do-work: the agent run stopped before it posted an answer on ${surface}. ${CLAUDE_AUTH_REASON}.`,
5466
+ CLAUDE_AUTH_REMEDY,
5467
+ "This comment is posted once. Automata does not post it again while the problem remains."
5468
+ ];
5469
+ if (excerpt.trim() !== "") {
5470
+ lines.push("", "Last lines of the agent's output:", "```text", excerpt.replaceAll("```", "'''"), "```");
5471
+ }
5472
+ return `${lines.join("\n")}
5473
+ ${CLAUDE_AUTH_MARKER}`;
5474
+ }
5475
+
5438
5476
  // src/run/answerRecovery.ts
5439
5477
  import { closeSync, fstatSync, openSync, readSync } from "fs";
5440
5478
  var RECOVERY_TIMEOUT_MS = 3e5;
@@ -6259,6 +6297,14 @@ function describeTickHistory(ticks, cadence, now, logPath, lines, problems) {
6259
6297
  )
6260
6298
  );
6261
6299
  }
6300
+ if (ticks[0].note === "claude-auth") {
6301
+ problems.push(
6302
+ finding(
6303
+ `${CLAUDE_AUTH_REASON}, so the last tick stopped before it answered; ${CLAUDE_AUTH_REMEDY}`,
6304
+ "claude /login"
6305
+ )
6306
+ );
6307
+ }
6262
6308
  const lockHeld = ticks.filter((tick) => tick.note === "lock-held").length;
6263
6309
  const withRuns = ticks.filter((tick) => tick.runs > 0).length;
6264
6310
  lines.push(
@@ -6638,14 +6684,20 @@ function requireParticipants(config) {
6638
6684
  );
6639
6685
  }
6640
6686
  if (!config.issueDiscoveryTechnique) {
6641
- failSettings("No issue discovery technique configured. Run `automata config set issue-discovery-technique <value>`.");
6687
+ failSettings(
6688
+ "No issue discovery technique configured. Run `automata config set issue-discovery-technique <value>`."
6689
+ );
6642
6690
  }
6643
6691
  if (!config.issueDiscoveryValue) {
6644
- failSettings("No issue discovery value configured. Run `automata config set issue-discovery-value <value>`.");
6692
+ failSettings(
6693
+ "No issue discovery value configured. Run `automata config set issue-discovery-value <value>`."
6694
+ );
6645
6695
  }
6646
6696
  const allowedUsers = (config.allowedUsers ?? []).filter((user) => user.trim().length > 0);
6647
6697
  if (allowedUsers.length === 0) {
6648
- failSettings("No allowed users configured. Run `automata config set allowed-users <user1,user2>`.");
6698
+ failSettings(
6699
+ "No allowed users configured. Run `automata config set allowed-users <user1,user2>`."
6700
+ );
6649
6701
  }
6650
6702
  const agentUser = (config.agentUser ?? "").trim();
6651
6703
  if (agentUser.length === 0) {
@@ -6864,8 +6916,20 @@ function validateDoWorkConfig(section2) {
6864
6916
  failSettings(`doWork.executor must be 'claude' or 'codex', got ${JSON.stringify(executor)}.`);
6865
6917
  }
6866
6918
  validateOptionalString(section2, "baseBranch", "doWork.baseBranch");
6867
- validateOptionalInt(section2, "maxRunsPerTick", "doWork.maxRunsPerTick", 0, "a non-negative integer (0 = unlimited)");
6868
- validateOptionalInt(section2, "lockStaleMinutes", "doWork.lockStaleMinutes", 1, "a positive integer");
6919
+ validateOptionalInt(
6920
+ section2,
6921
+ "maxRunsPerTick",
6922
+ "doWork.maxRunsPerTick",
6923
+ 0,
6924
+ "a non-negative integer (0 = unlimited)"
6925
+ );
6926
+ validateOptionalInt(
6927
+ section2,
6928
+ "lockStaleMinutes",
6929
+ "doWork.lockStaleMinutes",
6930
+ 1,
6931
+ "a positive integer"
6932
+ );
6869
6933
  validateOptionalBoolean(section2, "dumpOnBlock", "doWork.dumpOnBlock");
6870
6934
  validateOptionalBoolean(section2, "postRunLog", "doWork.postRunLog");
6871
6935
  validateProtectedBranches(section2["protectedBranches"]);
@@ -6897,7 +6961,9 @@ function validateSettingContainer(value, container, keys) {
6897
6961
  }
6898
6962
  for (const key2 of Object.keys(value)) {
6899
6963
  if (!keys.includes(key2)) {
6900
- failSettings(`doWork.${container}.${key2} is not a recognised setting; expected one of: ${keys.join(", ")}.`);
6964
+ failSettings(
6965
+ `doWork.${container}.${key2} is not a recognised setting; expected one of: ${keys.join(", ")}.`
6966
+ );
6901
6967
  }
6902
6968
  }
6903
6969
  }
@@ -6909,7 +6975,11 @@ function orphanPassEnabled(settings) {
6909
6975
  }
6910
6976
  function discoverIssues(settings) {
6911
6977
  if (!issuePassEnabled(settings)) return [];
6912
- const candidates = listCandidateIssues(settings.technique, settings.discoveryValue, settings.limit);
6978
+ const candidates = listCandidateIssues(
6979
+ settings.technique,
6980
+ settings.discoveryValue,
6981
+ settings.limit
6982
+ );
6913
6983
  if (settings.onlyIssue === void 0) {
6914
6984
  if (candidates.length === settings.limit) {
6915
6985
  progress(
@@ -7006,11 +7076,15 @@ function claimIssue(item, settings) {
7006
7076
  if (!item.needsAssignment || item.issue === null) return;
7007
7077
  try {
7008
7078
  assignIssueToAgent(item.issue.number, settings.participants.agentUser);
7009
- progress(` assigned issue #${String(item.issue.number)} to ${settings.participants.agentUser}.
7010
- `);
7079
+ progress(
7080
+ ` assigned issue #${String(item.issue.number)} to ${settings.participants.agentUser}.
7081
+ `
7082
+ );
7011
7083
  } catch (err) {
7012
- progress(` warning: could not assign issue #${String(item.issue.number)}: ${err.message}
7013
- `);
7084
+ progress(
7085
+ ` warning: could not assign issue #${String(item.issue.number)}: ${err.message}
7086
+ `
7087
+ );
7014
7088
  }
7015
7089
  }
7016
7090
  function claimPr(prNumber, agentUser) {
@@ -7019,8 +7093,10 @@ function claimPr(prNumber, agentUser) {
7019
7093
  progress(` assigned pull request #${String(prNumber)} to ${agentUser}.
7020
7094
  `);
7021
7095
  } catch (err) {
7022
- progress(` warning: could not assign pull request #${String(prNumber)}: ${err.message}
7023
- `);
7096
+ progress(
7097
+ ` warning: could not assign pull request #${String(prNumber)}: ${err.message}
7098
+ `
7099
+ );
7024
7100
  }
7025
7101
  }
7026
7102
  function notePickupOnIssue(item, marker) {
@@ -7033,13 +7109,17 @@ function notePickupOnIssue(item, marker) {
7033
7109
  `automata do-work: picked this up on pull request #${String(item.pr.number)} \u2014 ${item.pr.url}`
7034
7110
  );
7035
7111
  } catch (err) {
7036
- progress(` skipped: could not note the pickup on issue #${String(item.issue.number)} \u2014 ${err.message}
7037
- `);
7112
+ progress(
7113
+ ` skipped: could not note the pickup on issue #${String(item.issue.number)} \u2014 ${err.message}
7114
+ `
7115
+ );
7038
7116
  try {
7039
7117
  deleteMarker(marker);
7040
7118
  } catch (deleteErr) {
7041
- progress(` warning: could not withdraw the working marker: ${deleteErr.message}
7042
- `);
7119
+ progress(
7120
+ ` warning: could not withdraw the working marker: ${deleteErr.message}
7121
+ `
7122
+ );
7043
7123
  }
7044
7124
  return false;
7045
7125
  }
@@ -7052,8 +7132,10 @@ function reportIssueMessagesBuriedByNote(item, note, participants) {
7052
7132
  const messages = getIssueSurface(item.issue.number).messages;
7053
7133
  buried = messagesBetween(messages, participants, watermark, note.createdAt);
7054
7134
  } catch (err) {
7055
- progress(` warning: could not re-read issue #${String(item.issue.number)}: ${err.message}
7056
- `);
7135
+ progress(
7136
+ ` warning: could not re-read issue #${String(item.issue.number)}: ${err.message}
7137
+ `
7138
+ );
7057
7139
  return 0;
7058
7140
  }
7059
7141
  if (buried.length === 0) return 0;
@@ -7065,8 +7147,10 @@ function reportIssueMessagesBuriedByNote(item, note, participants) {
7065
7147
  `automata do-work: ${authors} posted here while this issue was being picked up, so ${buried.length === 1 ? "that message was" : "those messages were"} not included in the run. Please post again to have them acted on.`
7066
7148
  );
7067
7149
  } catch (err) {
7068
- progress(` warning: could not report the buried issue message(s): ${err.message}
7069
- `);
7150
+ progress(
7151
+ ` warning: could not report the buried issue message(s): ${err.message}
7152
+ `
7153
+ );
7070
7154
  }
7071
7155
  return buried.length;
7072
7156
  }
@@ -7084,7 +7168,9 @@ function refreshOrphanItem(item, linkMap, settings, policy) {
7084
7168
  const number = item.pr?.number ?? 0;
7085
7169
  const stillOrphan = linkMap.orphans.some((candidate) => candidate.pr.number === number);
7086
7170
  if (!stillOrphan) {
7087
- const linkedTo = issuesClosedBy(linkMap, number).map((issueNumber) => `#${String(issueNumber)}`);
7171
+ const linkedTo = issuesClosedBy(linkMap, number).map(
7172
+ (issueNumber) => `#${String(issueNumber)}`
7173
+ );
7088
7174
  return {
7089
7175
  kind: "skip",
7090
7176
  issue: null,
@@ -7126,7 +7212,8 @@ function reportOvertakenMessages(item, analysis) {
7126
7212
  `);
7127
7213
  }
7128
7214
  }
7129
- async function reconcileMarker(item, marker, participants, watermark, runError, diagnostics, recovery = null) {
7215
+ async function reconcileMarker(item, marker, participants, watermark, run5) {
7216
+ const { runError, diagnostics, recovery, claudeAuth } = run5;
7130
7217
  let analysis;
7131
7218
  try {
7132
7219
  analysis = analyseAnswer(readAnsweringSurface(item), participants, marker, watermark);
@@ -7139,7 +7226,8 @@ async function reconcileMarker(item, marker, participants, watermark, runError,
7139
7226
  }
7140
7227
  let recoveryNote = null;
7141
7228
  let recovered = false;
7142
- if (analysis.answeredAt === null && runError === null && recovery !== null) {
7229
+ const canRecover = runError === null && recovery !== null && claudeAuth === null;
7230
+ if (analysis.answeredAt === null && canRecover) {
7143
7231
  const attempt = await attemptRecovery(item, marker, participants, watermark, recovery);
7144
7232
  if ("analysis" in attempt) {
7145
7233
  analysis = attempt.analysis;
@@ -7149,31 +7237,74 @@ async function reconcileMarker(item, marker, participants, watermark, runError,
7149
7237
  }
7150
7238
  }
7151
7239
  if (analysis.answeredAt !== null) {
7152
- reportOvertakenMessages(item, analysis);
7153
- try {
7154
- deleteMarker(marker);
7155
- } catch (err) {
7156
- progress(` warning: could not delete the marker comment: ${err.message}
7157
- `);
7158
- }
7159
- if (analysis.missed.length > 0) {
7160
- return {
7161
- outcome: "answered-no-reply",
7162
- detail: `answered (${String(analysis.toReport.length)} message(s) arrived mid-run and were flagged)`,
7163
- reason: "flagged"
7164
- };
7165
- }
7166
- if (recovered) {
7167
- return { outcome: "answered", detail: "answered (recovered from the run transcript)", reason: "answered" };
7168
- }
7169
- return runError === null ? { outcome: "answered", detail: "answered", reason: "answered" } : { outcome: "answered", detail: `answered, but the run reported: ${runError.message}`, reason: "answered" };
7240
+ return reportAnswered(item, marker, analysis, runError, recovered);
7170
7241
  }
7242
+ if (claudeAuth !== null) return reportClaudeAuth(item, marker, participants, claudeAuth);
7171
7243
  return reportNoAnswer(item, marker, runError, diagnostics, recoveryNote);
7172
7244
  }
7245
+ function reportAnswered(item, marker, analysis, runError, recovered) {
7246
+ reportOvertakenMessages(item, analysis);
7247
+ try {
7248
+ deleteMarker(marker);
7249
+ } catch (err) {
7250
+ progress(` warning: could not delete the marker comment: ${err.message}
7251
+ `);
7252
+ }
7253
+ if (analysis.missed.length > 0) {
7254
+ return {
7255
+ outcome: "answered-no-reply",
7256
+ detail: `answered (${String(analysis.toReport.length)} message(s) arrived mid-run and were flagged)`,
7257
+ reason: "flagged"
7258
+ };
7259
+ }
7260
+ if (recovered) {
7261
+ return {
7262
+ outcome: "answered",
7263
+ detail: "answered (recovered from the run transcript)",
7264
+ reason: "answered"
7265
+ };
7266
+ }
7267
+ return runError === null ? { outcome: "answered", detail: "answered", reason: "answered" } : {
7268
+ outcome: "answered",
7269
+ detail: `answered, but the run reported: ${runError.message}`,
7270
+ reason: "answered"
7271
+ };
7272
+ }
7273
+ function hasClaudeAuthComment(item, marker, participants) {
7274
+ try {
7275
+ const before = new Date(marker.createdAt).getTime();
7276
+ const agent = participants.agentUser.toLowerCase();
7277
+ const last = readAnsweringSurface(item).findLast(
7278
+ (message) => message.author.toLowerCase() === agent && new Date(message.createdAt).getTime() < before
7279
+ );
7280
+ return last?.body.includes(CLAUDE_AUTH_MARKER) === true;
7281
+ } catch {
7282
+ return false;
7283
+ }
7284
+ }
7285
+ function reportClaudeAuth(item, marker, participants, excerpt) {
7286
+ const surface = markerSurfaceLabel(item);
7287
+ const result = {
7288
+ outcome: "failed",
7289
+ detail: `${CLAUDE_AUTH_REASON}. ${CLAUDE_AUTH_REMEDY}`,
7290
+ reason: "no-answer",
7291
+ claudeAuth: true
7292
+ };
7293
+ const alreadyTold = hasClaudeAuthComment(item, marker, participants);
7294
+ try {
7295
+ if (alreadyTold) deleteMarker(marker);
7296
+ else updateMarker(marker, renderClaudeAuthComment(surface, excerpt));
7297
+ } catch (err) {
7298
+ warnMarkerNotUpdated(surface, err, "that Claude Code is not signed in");
7299
+ }
7300
+ return result;
7301
+ }
7173
7302
  async function attemptRecovery(item, marker, participants, watermark, recovery) {
7174
7303
  const surface = markerSurfaceLabel(item);
7175
- progress(` recovery: the run posted no answer on ${surface}; asking the model to write it from the transcript.
7176
- `);
7304
+ progress(
7305
+ ` recovery: the run posted no answer on ${surface}; asking the model to write it from the transcript.
7306
+ `
7307
+ );
7177
7308
  const reread = () => analyseAnswer(readAnsweringSurface(item), participants, marker, watermark);
7178
7309
  const fail3 = (reason) => {
7179
7310
  progress(` recovery: failed \u2014 ${reason}
@@ -7205,12 +7336,15 @@ async function attemptRecovery(item, marker, participants, watermark, recovery)
7205
7336
  }
7206
7337
  try {
7207
7338
  const verified2 = reread();
7208
- if (verified2.answeredAt === null) return fail3(`the recovered answer is not visible on ${surface} after posting`);
7339
+ if (verified2.answeredAt === null)
7340
+ return fail3(`the recovered answer is not visible on ${surface} after posting`);
7209
7341
  progress(` recovery: posted the answer recovered from the run transcript on ${surface}.
7210
7342
  `);
7211
7343
  return { analysis: verified2, posted: true };
7212
7344
  } catch (err) {
7213
- return fail3(`could not re-read ${surface} to verify the recovered answer: ${err.message}`);
7345
+ return fail3(
7346
+ `could not re-read ${surface} to verify the recovered answer: ${err.message}`
7347
+ );
7214
7348
  }
7215
7349
  }
7216
7350
  function reportUnverified(item, marker) {
@@ -7230,6 +7364,13 @@ function reportUnverified(item, marker) {
7230
7364
  reason: "unverified"
7231
7365
  };
7232
7366
  }
7367
+ function warnMarkerNotUpdated(surface, err, what) {
7368
+ progress(
7369
+ ` warning: could not update the marker comment on ${surface}: ${err.message}
7370
+ the humans have not been told ${what}.
7371
+ `
7372
+ );
7373
+ }
7233
7374
  async function reportNoAnswer(item, marker, runError, diagnostics, recoveryNote = null) {
7234
7375
  const surface = markerSurfaceLabel(item);
7235
7376
  const sideEffects = item.turn === "issue-discuss" ? "It may still have created a branch or opened a pull request \u2014 check before assuming otherwise." : `It may still have changed the branch \`${item.branch}\` \u2014 check it before assuming otherwise.`;
@@ -7238,13 +7379,13 @@ async function reportNoAnswer(item, marker, runError, diagnostics, recoveryNote
7238
7379
  try {
7239
7380
  updateMarker(marker, explanation + recovery + (await diagnostics?.() ?? ""));
7240
7381
  } catch (err) {
7241
- progress(
7242
- ` warning: could not update the marker comment on ${surface}: ${err.message}
7243
- the humans have not been told that this run produced no answer.
7244
- `
7245
- );
7382
+ warnMarkerNotUpdated(surface, err, "that this run produced no answer");
7246
7383
  }
7247
- return runError === null ? { outcome: "answered-no-reply", detail: "run finished but posted no answer", reason: "no-answer" } : { outcome: "failed", detail: `run failed: ${runError.message}`, reason: "no-answer" };
7384
+ return runError === null ? {
7385
+ outcome: "answered-no-reply",
7386
+ detail: "run finished but posted no answer",
7387
+ reason: "no-answer"
7388
+ } : { outcome: "failed", detail: `run failed: ${runError.message}`, reason: "no-answer" };
7248
7389
  }
7249
7390
  async function invokeExecutor(prompt, execution, silent, sink2) {
7250
7391
  beat({
@@ -7256,7 +7397,12 @@ async function invokeExecutor(prompt, execution, silent, sink2) {
7256
7397
  await runCodex(prompt, { model: execution.model, effort: execution.effort, sink: sink2 });
7257
7398
  return;
7258
7399
  }
7259
- await runClaude(prompt, { model: execution.model, effort: execution.effort, printSteps: !silent, sink: sink2 });
7400
+ await runClaude(prompt, {
7401
+ model: execution.model,
7402
+ effort: execution.effort,
7403
+ printSteps: !silent,
7404
+ sink: sink2
7405
+ });
7260
7406
  }
7261
7407
  function repairIssueLink(item, baseBranch, agentUser) {
7262
7408
  const issue = item.issue;
@@ -7264,8 +7410,10 @@ function repairIssueLink(item, baseBranch, agentUser) {
7264
7410
  try {
7265
7411
  const branch = getCurrentBranch();
7266
7412
  if (branch === baseBranch) {
7267
- progress(` issue #${String(issue.number)} is still in discussion (no branch was created).
7268
- `);
7413
+ progress(
7414
+ ` issue #${String(issue.number)} is still in discussion (no branch was created).
7415
+ `
7416
+ );
7269
7417
  return false;
7270
7418
  }
7271
7419
  const pr = getCurrentBranchPr();
@@ -7277,10 +7425,15 @@ function repairIssueLink(item, baseBranch, agentUser) {
7277
7425
  if (pr.assignees.length === 0) {
7278
7426
  claimPr(pr.number, agentUser);
7279
7427
  }
7280
- const closesRef = new RegExp(String.raw`\bcloses\s+#` + String(issue.number) + String.raw`\b`, "i");
7428
+ const closesRef = new RegExp(
7429
+ String.raw`\bcloses\s+#` + String(issue.number) + String.raw`\b`,
7430
+ "i"
7431
+ );
7281
7432
  if (closesRef.test(pr.body)) {
7282
- progress(` pull request #${String(pr.number)} already closes issue #${String(issue.number)}.
7283
- `);
7433
+ progress(
7434
+ ` pull request #${String(pr.number)} already closes issue #${String(issue.number)}.
7435
+ `
7436
+ );
7284
7437
  return true;
7285
7438
  }
7286
7439
  addClosesRefToPr(pr.number, issue.number);
@@ -7288,8 +7441,10 @@ function repairIssueLink(item, baseBranch, agentUser) {
7288
7441
  `);
7289
7442
  return true;
7290
7443
  } catch (err) {
7291
- progress(` warning: could not link a pull request to issue #${String(issue.number)}: ${err.message}
7292
- `);
7444
+ progress(
7445
+ ` warning: could not link a pull request to issue #${String(issue.number)}: ${err.message}
7446
+ `
7447
+ );
7293
7448
  return false;
7294
7449
  }
7295
7450
  }
@@ -7378,8 +7533,10 @@ ${itemLabel(planned)} ${planned.turn}: ${planned.reason}
7378
7533
  }
7379
7534
  let buriedByNote = 0;
7380
7535
  if (note !== null) {
7381
- progress(` noted on issue #${String(item.issue?.number ?? 0)} that the work is on pull request #${String(item.pr?.number ?? 0)}.
7382
- `);
7536
+ progress(
7537
+ ` noted on issue #${String(item.issue?.number ?? 0)} that the work is on pull request #${String(item.pr?.number ?? 0)}.
7538
+ `
7539
+ );
7383
7540
  buriedByNote = reportIssueMessagesBuriedByNote(item, note, settings.participants);
7384
7541
  }
7385
7542
  const watermark = promptWatermark([
@@ -7414,43 +7571,64 @@ ${itemLabel(planned)} ${planned.turn}: ${planned.reason}
7414
7571
  );
7415
7572
  }
7416
7573
  const execution = toExecution(resolved);
7417
- const { runError, diagnostics, recovery } = await runRecorded(item, prompt, execution, settings, silent);
7574
+ const { runError, diagnostics, recovery, claudeAuth } = await runRecorded(
7575
+ item,
7576
+ prompt,
7577
+ execution,
7578
+ settings,
7579
+ silent
7580
+ );
7418
7581
  const ranExecutor = true;
7419
7582
  let reconciled;
7420
7583
  try {
7421
- reconciled = await reconcileMarker(
7422
- item,
7423
- marker,
7424
- settings.participants,
7425
- watermark,
7584
+ reconciled = await reconcileMarker(item, marker, settings.participants, watermark, {
7426
7585
  runError,
7427
7586
  diagnostics,
7428
- recovery
7429
- );
7587
+ recovery,
7588
+ claudeAuth
7589
+ });
7430
7590
  } finally {
7431
7591
  inFlightMarker = null;
7432
7592
  }
7433
7593
  progress(` ${reconciled.detail}
7434
7594
  `);
7435
7595
  const outcome = adjustOutcome(reconciled, item, settings, buriedByNote);
7436
- return { ...base, outcome: outcome.outcome, detail: outcome.detail, ranExecutor, execution };
7596
+ return {
7597
+ ...base,
7598
+ outcome: outcome.outcome,
7599
+ detail: outcome.detail,
7600
+ ranExecutor,
7601
+ execution,
7602
+ claudeAuth: outcome.claudeAuth
7603
+ };
7437
7604
  }
7438
7605
  async function runRecorded(item, prompt, execution, settings, silent) {
7439
7606
  const transcript = settings.postRunLog ? new RunTranscript(transcriptLabel(item)) : null;
7440
7607
  const before = transcript === null ? null : snapshotCheckout();
7608
+ const authTail = transcript === null && execution.executor === "claude" ? new ClaudeAuthTail() : null;
7441
7609
  let runError = null;
7442
7610
  try {
7443
- await invokeExecutor(prompt, execution, silent, transcript ?? void 0);
7611
+ await invokeExecutor(prompt, execution, silent, transcript ?? authTail ?? void 0);
7444
7612
  } catch (err) {
7445
7613
  runError = err;
7446
7614
  }
7447
- if (transcript === null) return { runError, diagnostics: null, recovery: null };
7615
+ if (transcript === null) {
7616
+ const claudeAuth2 = runError !== null && authTail !== null && detectClaudeAuthFailure(authTail.tail) ? "" : null;
7617
+ return { runError, diagnostics: null, recovery: null, claudeAuth: claudeAuth2 };
7618
+ }
7619
+ const tail = transcript.excerpt();
7620
+ const claudeAuth = runError !== null && execution.executor === "claude" && detectClaudeAuthFailure(tail) ? tail : null;
7448
7621
  return {
7449
7622
  runError,
7623
+ claudeAuth,
7450
7624
  recovery: async () => {
7451
7625
  const text = (transcript.saved ? readTranscriptTail(transcript.path) : null) ?? transcript.excerpt();
7452
7626
  if (text.trim() === "") return { ok: false, reason: "the run transcript is empty" };
7453
- return recoverAnswer(execution, { subject: markerSurfaceLabel(item), transcript: text, conversation: prompt });
7627
+ return recoverAnswer(execution, {
7628
+ subject: markerSurfaceLabel(item),
7629
+ transcript: text,
7630
+ conversation: prompt
7631
+ });
7454
7632
  },
7455
7633
  diagnostics: async () => {
7456
7634
  const excerpt = transcript.excerpt();
@@ -7530,8 +7708,10 @@ function summarize(reports) {
7530
7708
  }
7531
7709
  for (const report of reports) {
7532
7710
  const ran = report.execution === void 0 ? "" : ` \xB7 ${describeExecution(report.execution)}`;
7533
- out(` ${reportLabel(report)} ${report.turn ?? "-"} ${report.outcome} \u2014 ${report.detail}${ran}
7534
- `);
7711
+ out(
7712
+ ` ${reportLabel(report)} ${report.turn ?? "-"} ${report.outcome} \u2014 ${report.detail}${ran}
7713
+ `
7714
+ );
7535
7715
  }
7536
7716
  }
7537
7717
  function toItemJson(report) {
@@ -7553,7 +7733,13 @@ function toItemJson(report) {
7553
7733
  }
7554
7734
  var doWorkCommand = new Command6("do-work").description(
7555
7735
  "Run one tick of the autonomous loop: answer the issues whose newest authorized message the agent has not answered, then the open pull requests that close no issue of this repository and have one"
7556
- ).option("--with <executor>", "Executor to use: claude or codex (default: from config, else claude)").option("--model <string>", "Model identifier to pass to the executor, overriding the configured default for it").option(
7736
+ ).option(
7737
+ "--with <executor>",
7738
+ "Executor to use: claude or codex (default: from config, else claude)"
7739
+ ).option(
7740
+ "--model <string>",
7741
+ "Model identifier to pass to the executor, overriding the configured default for it"
7742
+ ).option(
7557
7743
  "--effort <level>",
7558
7744
  "Reasoning effort to pass to the executor, overriding the configured default for it"
7559
7745
  ).option("--issue <number>", "Restrict the tick to a single issue").option(
@@ -7619,7 +7805,12 @@ async function runDoWorkTraced(options) {
7619
7805
  return exitCode2;
7620
7806
  }
7621
7807
  const { exitCode, reports } = await runTickUnderLock(lock.handle, settings, options);
7622
- logTick(reports, exitCode, startedAt);
7808
+ logTick(
7809
+ reports,
7810
+ exitCode,
7811
+ startedAt,
7812
+ reports.some((report) => report.claudeAuth === true) ? "claude-auth" : void 0
7813
+ );
7623
7814
  return exitCode;
7624
7815
  }
7625
7816
  async function runTickUnderLock(handle, settings, options) {
@@ -7685,6 +7876,20 @@ function buildSelectionEvidence(settings, issues, linkMap, decisions, verbose) {
7685
7876
  })) : null
7686
7877
  };
7687
7878
  }
7879
+ function decideAll(settings, issues, linkMap, policy) {
7880
+ return skipDuplicateHeadBranches([
7881
+ ...issues.map(
7882
+ (issue) => decideWork(buildIssueState(issue, linkMap), settings.participants, policy)
7883
+ ),
7884
+ ...discoverOrphanPrs(settings, linkMap).map(
7885
+ (candidate) => decideOrphanPrWork(
7886
+ { prSurface: getPrSurface(candidate.pr.number) },
7887
+ settings.participants,
7888
+ policy
7889
+ )
7890
+ )
7891
+ ]);
7892
+ }
7688
7893
  function collectSelection(settings, verbose) {
7689
7894
  const issues = discoverIssues(settings);
7690
7895
  const linkMap = getOpenPrLinkMap();
@@ -7693,12 +7898,7 @@ function collectSelection(settings, verbose) {
7693
7898
  defaultBranch: linkMap.defaultBranch,
7694
7899
  protectedBranches: settings.protectedBranches
7695
7900
  };
7696
- const decisions = skipDuplicateHeadBranches([
7697
- ...issues.map((issue) => decideWork(buildIssueState(issue, linkMap), settings.participants, policy)),
7698
- ...discoverOrphanPrs(settings, linkMap).map(
7699
- (candidate) => decideOrphanPrWork({ prSurface: getPrSurface(candidate.pr.number) }, settings.participants, policy)
7700
- )
7701
- ]);
7901
+ const decisions = decideAll(settings, issues, linkMap, policy);
7702
7902
  return buildSelectionEvidence(settings, issues, linkMap, decisions, verbose);
7703
7903
  }
7704
7904
  function makeSelectionSection(lines, problems, data) {
@@ -7730,7 +7930,12 @@ function describeIssueEvidence(settings, evidence) {
7730
7930
  `discovery query: ${settings.technique} = ${settings.discoveryValue}, limit ${String(settings.limit)}${restriction}`
7731
7931
  ];
7732
7932
  if (evidence.issues === null) return lines;
7733
- lines.push(heading(`discovery returned ${String(evidence.issues.length)} issue(s)`, evidence.issues.length));
7933
+ lines.push(
7934
+ heading(
7935
+ `discovery returned ${String(evidence.issues.length)} issue(s)`,
7936
+ evidence.issues.length
7937
+ )
7938
+ );
7734
7939
  for (const issue of evidence.issues) {
7735
7940
  lines.push(` #${String(issue.number)} ${issue.title}`);
7736
7941
  }
@@ -7812,11 +8017,11 @@ function selectionData(evidence) {
7812
8017
  }
7813
8018
  function selectionSectionFromEvidence(settings, evidence, verbose) {
7814
8019
  if (evidence === null) {
7815
- return makeSelectionSection(
7816
- ["not run: the tick was blocked before discovery"],
7817
- [],
7818
- { ran: false, detail: "blocked before discovery", plan: [] }
7819
- );
8020
+ return makeSelectionSection(["not run: the tick was blocked before discovery"], [], {
8021
+ ran: false,
8022
+ detail: "blocked before discovery",
8023
+ plan: []
8024
+ });
7820
8025
  }
7821
8026
  const lines = [];
7822
8027
  if (verbose && settings !== null) describeSelectionEvidence(settings, evidence, lines);
@@ -8051,6 +8256,9 @@ function dumpBlocked(options, context) {
8051
8256
  }
8052
8257
  }
8053
8258
  function classifyBlocked(hygiene, decisions, items, reports) {
8259
+ if (reports.some((report) => report.claudeAuth === true)) {
8260
+ return { reason: "claude-auth", trigger: `${CLAUDE_AUTH_REASON}; run \`claude /login\` again` };
8261
+ }
8054
8262
  const ran = reports.some((report) => report.ranExecutor === true);
8055
8263
  if (ran) return null;
8056
8264
  if (hygiene.degraded) {
@@ -8168,12 +8376,7 @@ async function runTick(settings, options) {
8168
8376
  defaultBranch: linkMap.defaultBranch,
8169
8377
  protectedBranches: settings.protectedBranches
8170
8378
  };
8171
- const decisions = skipDuplicateHeadBranches([
8172
- ...issues.map((issue) => decideWork(buildIssueState(issue, linkMap), settings.participants, policy)),
8173
- ...discoverOrphanPrs(settings, linkMap).map(
8174
- (candidate) => decideOrphanPrWork({ prSurface: getPrSurface(candidate.pr.number) }, settings.participants, policy)
8175
- )
8176
- ]);
8379
+ const decisions = decideAll(settings, issues, linkMap, policy);
8177
8380
  const items = decisions.flatMap((decision) => decision.kind === "work" ? [decision.item] : []);
8178
8381
  const planText = `Work plan (${String(items.length)} of ${String(decisions.length)} candidates need an answer):
8179
8382
  ${describePlan(decisions)}`;
@@ -8222,9 +8425,23 @@ async function processItems(items, settings, options, hygiene) {
8222
8425
  const report = await reportForItem(item, settings, options, preflightCauses);
8223
8426
  if (report.ranExecutor === true) runsUsed++;
8224
8427
  reports.push(report);
8428
+ if (report.claudeAuth === true) {
8429
+ reports.push(...items.slice(index + 1).map((rest) => skipForClaudeAuth(rest)));
8430
+ break;
8431
+ }
8225
8432
  }
8226
8433
  return { reports, deferred };
8227
8434
  }
8435
+ function skipForClaudeAuth(item) {
8436
+ return {
8437
+ issue: item.issue?.number ?? null,
8438
+ pr: item.pr?.number ?? null,
8439
+ title: itemTitle(item),
8440
+ turn: item.turn,
8441
+ outcome: "skipped",
8442
+ detail: `${CLAUDE_AUTH_REASON}; run \`claude /login\` again`
8443
+ };
8444
+ }
8228
8445
  async function reportForItem(item, settings, options, preflightCauses) {
8229
8446
  try {
8230
8447
  return await processItem(item, settings, options.silent === true, preflightCauses);
@@ -8479,6 +8696,39 @@ function coveredByWatchedIssue(watch, linkedPrs) {
8479
8696
  return covered;
8480
8697
  }
8481
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
+
8482
8732
  // src/conductor/replyDecision.ts
8483
8733
  function isWatchClosed(state) {
8484
8734
  return state === "closed";
@@ -8496,10 +8746,32 @@ function prConversation(surface) {
8496
8746
  kind: "pr",
8497
8747
  number: surface.pr.number,
8498
8748
  state: surface.pr.state === "OPEN" ? "open" : "closed",
8499
- messages: [...surface.messages, ...surface.threads.flatMap((thread) => thread.comments)]
8749
+ messages: [...surface.messages, ...surface.threads.flatMap((thread) => thread.comments)],
8750
+ copilotAt: newestCopilotAt(openCopilotThreads(surface))
8500
8751
  };
8501
8752
  }
8502
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) {
8503
8775
  if (isWatchClosed(conversation.state)) {
8504
8776
  return { kind: "skip", surface: conversation, reason: "closed", detail: "the item is closed or merged" };
8505
8777
  }
@@ -8641,6 +8913,8 @@ function prSection(pr, participants) {
8641
8913
  lines.push("", `Unresolved review threads on pull request #${number}:`, "", threads.join("\n\n"));
8642
8914
  }
8643
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));
8644
8918
  return lines;
8645
8919
  }
8646
8920
  function composeConductorPrompt(input) {
@@ -8719,6 +8993,35 @@ function describeConductorExecution(execution) {
8719
8993
  return `${execution.executor}${model}${effort}`;
8720
8994
  }
8721
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
+
8722
9025
  // src/conductor/reply.ts
8723
9026
  function isComment(message) {
8724
9027
  return message.kind === "issue-comment" || message.kind === "pr-comment";
@@ -9087,6 +9390,10 @@ ${reply}
9087
9390
  `);
9088
9391
  return "replied";
9089
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
+ }
9090
9397
  async function conductItem(id, config, login2, participants, execution, dryRun, linkMap) {
9091
9398
  const label2 = `#${String(id)}`;
9092
9399
  let item;
@@ -9107,6 +9414,11 @@ async function conductItem(id, config, login2, participants, execution, dryRun,
9107
9414
  const stopped = verdict.decisions.find((decision) => decision.kind === "skip" && (decision.reason === "blocked" || decision.reason === "limit"));
9108
9415
  const why = stopped?.kind === "skip" ? `Conductor: ${label2} gets no reply: ${stopped.detail}.` : `Conductor: ${label2} needs no reply.`;
9109
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.
9110
9422
  `);
9111
9423
  return "idle";
9112
9424
  }
@@ -9383,17 +9695,63 @@ async function runConductor(options = {}) {
9383
9695
  }
9384
9696
  }
9385
9697
  async function runConductorTick(options) {
9386
- if (options.check === true && options.dryRun === true) {
9387
- return fail2("--check and --dry-run cannot be used together.");
9388
- }
9698
+ const problem = optionsProblem(options);
9699
+ if (problem !== null) return fail2(problem);
9700
+ if (options.automatic === true) return runConductorAutomatic(options.interval);
9389
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) {
9390
9748
  const checked = preflight();
9391
- if ("problem" in checked) return fail2(checked.problem);
9392
- 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 };
9393
9751
  const { config, login: login2, participants, execution } = checked;
9394
9752
  const staleMinutes = config.doWork?.lockStaleMinutes ?? DEFAULT_DO_WORK.lockStaleMinutes;
9395
9753
  const lock = acquireConductorLock(staleMinutes);
9396
- if (!lock.ok) return reportHeldLock(lock.heldBy, lock.suspect, staleMinutes);
9754
+ if (!lock.ok) return { code: reportHeldLock(lock.heldBy, lock.suspect, staleMinutes), open: null };
9397
9755
  try {
9398
9756
  process.stdout.write(`Conductor: running as ${login2}.
9399
9757
  `);
@@ -9416,7 +9774,7 @@ async function runConductorTick(options) {
9416
9774
  failed++;
9417
9775
  }
9418
9776
  }
9419
- return failed > 0 ? 1 : 0;
9777
+ return { code: failed > 0 ? 1 : 0, open: watch.length };
9420
9778
  } finally {
9421
9779
  lock.handle.release();
9422
9780
  }
@@ -9426,7 +9784,7 @@ var conductorCommand = new Command7("conductor").description(
9426
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(
9427
9785
  "--verbose",
9428
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"
9429
- ).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) => {
9430
9788
  exitWith(await runConductor(options));
9431
9789
  });
9432
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.554",
3
+ "version": "0.12.0-develop.567",
4
4
  "description": "Automata CLI tool",
5
5
  "type": "module",
6
6
  "engines": {