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.
- package/dist/index.js +463 -105
- 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(
|
|
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(
|
|
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(
|
|
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(
|
|
6868
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
7010
|
-
`)
|
|
7079
|
+
progress(
|
|
7080
|
+
` assigned issue #${String(item.issue.number)} to ${settings.participants.agentUser}.
|
|
7081
|
+
`
|
|
7082
|
+
);
|
|
7011
7083
|
} catch (err) {
|
|
7012
|
-
progress(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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)
|
|
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(
|
|
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
|
-
|
|
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 ? {
|
|
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, {
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 {
|
|
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)
|
|
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, {
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
7817
|
-
|
|
7818
|
-
|
|
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 =
|
|
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
|
-
|
|
9387
|
-
|
|
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) => {
|