automata-cli 0.11.0-develop.519 → 0.11.0-develop.529
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 +199 -53
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2625,9 +2625,25 @@ function getWatchTarget(id) {
|
|
|
2625
2625
|
number: raw.number,
|
|
2626
2626
|
kind: raw.pull_request === void 0 ? "issue" : "pr",
|
|
2627
2627
|
state: raw.state === "closed" ? "closed" : "open",
|
|
2628
|
-
title: raw.title
|
|
2628
|
+
title: raw.title,
|
|
2629
|
+
labels: (raw.labels ?? []).map((label2) => typeof label2 === "string" ? label2 : label2.name ?? "").filter((name) => name.length > 0)
|
|
2629
2630
|
};
|
|
2630
2631
|
}
|
|
2632
|
+
function addLabel(target, name, description) {
|
|
2633
|
+
const edit = target.kind === "pr" ? "pr" : "issue";
|
|
2634
|
+
const args = [edit, "edit", String(target.number), "--add-label", name];
|
|
2635
|
+
let result = run4("gh", args);
|
|
2636
|
+
if (result.status !== 0 && isMissingLabelError(result.stderr)) {
|
|
2637
|
+
const created = run4("gh", ["label", "create", name, "--description", description]);
|
|
2638
|
+
if (created.status !== 0 && !/already exists/i.test(created.stderr)) {
|
|
2639
|
+
throw new Error(created.stderr.trim() || `Failed to create the label ${name}.`);
|
|
2640
|
+
}
|
|
2641
|
+
result = run4("gh", args);
|
|
2642
|
+
}
|
|
2643
|
+
if (result.status !== 0) {
|
|
2644
|
+
throw new Error(result.stderr.trim() || `Failed to add the label ${name} to #${String(target.number)}.`);
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2631
2647
|
function applyDiscovery(target, technique, value) {
|
|
2632
2648
|
const edit = target.kind === "pr" ? "pr" : "issue";
|
|
2633
2649
|
let flag;
|
|
@@ -8422,6 +8438,44 @@ function toPlanJson(decision) {
|
|
|
8422
8438
|
// src/commands/conductor.ts
|
|
8423
8439
|
import { Command as Command7 } from "commander";
|
|
8424
8440
|
|
|
8441
|
+
// src/conductor/loopSafety.ts
|
|
8442
|
+
var BLOCKED_LABEL = "conductor-blocked";
|
|
8443
|
+
var REPLY_MARKER = "<!-- automata:conductor -->";
|
|
8444
|
+
var NEEDS_HUMAN_PREFIX = "NEEDS-HUMAN:";
|
|
8445
|
+
var DEFAULT_MAX_REPLIES_PER_ITEM = 5;
|
|
8446
|
+
function maxRepliesProblem(conductor) {
|
|
8447
|
+
const value = conductor?.maxRepliesPerItem;
|
|
8448
|
+
if (value === void 0 || value === null) return null;
|
|
8449
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1) {
|
|
8450
|
+
return `conductor.maxRepliesPerItem must be a positive integer, got ${JSON.stringify(value)}.`;
|
|
8451
|
+
}
|
|
8452
|
+
return null;
|
|
8453
|
+
}
|
|
8454
|
+
function resolveMaxReplies(conductor) {
|
|
8455
|
+
return conductor?.maxRepliesPerItem ?? DEFAULT_MAX_REPLIES_PER_ITEM;
|
|
8456
|
+
}
|
|
8457
|
+
function countConductorReplies(messages) {
|
|
8458
|
+
return messages.filter(
|
|
8459
|
+
(message) => (message.kind === "issue-comment" || message.kind === "pr-comment") && message.body.includes(REPLY_MARKER)
|
|
8460
|
+
).length;
|
|
8461
|
+
}
|
|
8462
|
+
function parseNeedsHuman(output) {
|
|
8463
|
+
const lines = output.trim().split("\n");
|
|
8464
|
+
const last = lines.at(-1)?.trim() ?? "";
|
|
8465
|
+
if (!last.startsWith(NEEDS_HUMAN_PREFIX)) return null;
|
|
8466
|
+
const reason = last.slice(NEEDS_HUMAN_PREFIX.length).trim();
|
|
8467
|
+
return reason.length > 0 ? reason : "no reason given";
|
|
8468
|
+
}
|
|
8469
|
+
function coveredByWatchedIssue(watch, linkedPrs) {
|
|
8470
|
+
const covered = /* @__PURE__ */ new Map();
|
|
8471
|
+
for (const id of watch) {
|
|
8472
|
+
for (const pr of linkedPrs(id)) {
|
|
8473
|
+
if (pr !== id && watch.includes(pr) && !covered.has(pr)) covered.set(pr, id);
|
|
8474
|
+
}
|
|
8475
|
+
}
|
|
8476
|
+
return covered;
|
|
8477
|
+
}
|
|
8478
|
+
|
|
8425
8479
|
// src/conductor/replyDecision.ts
|
|
8426
8480
|
function isWatchClosed(state) {
|
|
8427
8481
|
return state === "closed";
|
|
@@ -8482,10 +8536,33 @@ function decideConversation(conversation, p) {
|
|
|
8482
8536
|
reason: `${p.agentUser}'s message at ${newest2.createdAt} has no answer from an allowed user`
|
|
8483
8537
|
};
|
|
8484
8538
|
}
|
|
8485
|
-
function decideConductorReply(conversations, p) {
|
|
8486
|
-
const
|
|
8539
|
+
function decideConductorReply(conversations, p, loop) {
|
|
8540
|
+
const decided = conversations.map((conversation) => decideConversation(conversation, p));
|
|
8541
|
+
const stop = loop === void 0 ? null : loopStop(loop);
|
|
8542
|
+
const decisions = decided.map(
|
|
8543
|
+
(decision) => decision.kind === "reply" && stop !== null ? { kind: "skip", surface: decision.surface, reason: stop.reason, detail: stop.detail } : decision
|
|
8544
|
+
);
|
|
8487
8545
|
return { needsReply: decisions.some((decision) => decision.kind === "reply"), decisions };
|
|
8488
8546
|
}
|
|
8547
|
+
function loopStop(loop) {
|
|
8548
|
+
if (loop.blocked) {
|
|
8549
|
+
return { reason: "blocked", detail: `the item has the ${BLOCKED_LABEL} label; remove it to resume` };
|
|
8550
|
+
}
|
|
8551
|
+
if (loop.replies >= loop.maxReplies) {
|
|
8552
|
+
return {
|
|
8553
|
+
reason: "limit",
|
|
8554
|
+
detail: `the conductor already replied ${String(loop.replies)} times; the limit is conductor.maxRepliesPerItem (${String(loop.maxReplies)})`
|
|
8555
|
+
};
|
|
8556
|
+
}
|
|
8557
|
+
return null;
|
|
8558
|
+
}
|
|
8559
|
+
function loopStateOf(conversations, labels, maxReplies) {
|
|
8560
|
+
return {
|
|
8561
|
+
blocked: labels.some((label2) => label2.toLowerCase() === BLOCKED_LABEL),
|
|
8562
|
+
replies: countConductorReplies(conversations.flatMap((conversation) => conversation.messages)),
|
|
8563
|
+
maxReplies
|
|
8564
|
+
};
|
|
8565
|
+
}
|
|
8489
8566
|
|
|
8490
8567
|
// src/conductor/thread.ts
|
|
8491
8568
|
function defaultReplyTarget(thread) {
|
|
@@ -8497,13 +8574,19 @@ function postingInstruction(target) {
|
|
|
8497
8574
|
return [
|
|
8498
8575
|
`Post your reply as a comment on ${target.kind === "pr" ? "pull request" : "issue"} #${number}. What you print is discarded; only a posted comment counts.`,
|
|
8499
8576
|
`Run \`gh ${command} comment ${number} --body-file -\` and give the body on standard input, for example with a here-document.`,
|
|
8500
|
-
|
|
8577
|
+
`End the comment body with the line \`${REPLY_MARKER}\` on its own; automata counts the replies with it.`,
|
|
8578
|
+
"Post exactly one comment. Do not approve, merge or close anything.",
|
|
8579
|
+
needsHumanInstruction()
|
|
8501
8580
|
].join("\n");
|
|
8502
8581
|
}
|
|
8582
|
+
function needsHumanInstruction() {
|
|
8583
|
+
return `If you cannot go on without a person, post no comment. Make the last line of your final message \`${NEEDS_HUMAN_PREFIX} <one-line reason>\`; automata then labels the item and stays silent.`;
|
|
8584
|
+
}
|
|
8503
8585
|
function dryRunInstruction(target) {
|
|
8504
8586
|
return [
|
|
8505
8587
|
`Write your reply to ${target.kind === "pr" ? "pull request" : "issue"} #${String(target.number)}. This is a dry run: you cannot post a comment, and you must not try.`,
|
|
8506
|
-
"Make the reply, and nothing else, your final message. Do not approve, merge or close anything."
|
|
8588
|
+
"Make the reply, and nothing else, your final message. Do not approve, merge or close anything.",
|
|
8589
|
+
needsHumanInstruction()
|
|
8507
8590
|
].join("\n");
|
|
8508
8591
|
}
|
|
8509
8592
|
function visible(messages, participants) {
|
|
@@ -8730,6 +8813,10 @@ function watchSections(watched) {
|
|
|
8730
8813
|
listLines.push(`${name}: closed, a tick would drop it from the watch list`);
|
|
8731
8814
|
continue;
|
|
8732
8815
|
}
|
|
8816
|
+
if (entry.coveredBy !== void 0) {
|
|
8817
|
+
listLines.push(`${name}: open, covered by watched issue #${String(entry.coveredBy)}; a tick gives it no separate reply`);
|
|
8818
|
+
continue;
|
|
8819
|
+
}
|
|
8733
8820
|
listLines.push(`${name}: open`);
|
|
8734
8821
|
for (const decision of entry.decisions) replyLines.push(decisionLine(decision));
|
|
8735
8822
|
}
|
|
@@ -8853,24 +8940,25 @@ function readWatchedItem(id, linkMap = getOpenPrLinkMap) {
|
|
|
8853
8940
|
const target = getWatchTarget(id);
|
|
8854
8941
|
if (target.kind === "pr") {
|
|
8855
8942
|
const pr = readPr(id);
|
|
8856
|
-
return { thread: { kind: "pr", prs: [pr] }, conversations: [prConversation(pr.surface)] };
|
|
8943
|
+
return { target, thread: { kind: "pr", prs: [pr] }, conversations: [prConversation(pr.surface)], labels: target.labels };
|
|
8857
8944
|
}
|
|
8858
8945
|
const issue = getIssueSurface(id);
|
|
8859
8946
|
const prs = (linkMap().byIssue.get(id) ?? []).map((ref) => readPr(ref.number));
|
|
8860
8947
|
return {
|
|
8948
|
+
target,
|
|
8861
8949
|
thread: { kind: "issue", issue, prs },
|
|
8862
|
-
conversations: [issueConversation(issue), ...prs.map((pr) => prConversation(pr.surface))]
|
|
8950
|
+
conversations: [issueConversation(issue), ...prs.map((pr) => prConversation(pr.surface))],
|
|
8951
|
+
labels: target.labels
|
|
8863
8952
|
};
|
|
8864
8953
|
}
|
|
8865
8954
|
function readTargetMessages(target) {
|
|
8866
8955
|
return target.kind === "issue" ? issueConversation(getIssueSurface(target.number)).messages : prConversation(getPrSurface(target.number)).messages;
|
|
8867
8956
|
}
|
|
8868
|
-
function runModel(prompt, execution, replyTo) {
|
|
8957
|
+
function runModel(prompt, execution, replyTo, sink2) {
|
|
8869
8958
|
const { owner, repo } = getRepoSlug();
|
|
8870
|
-
return runClaude(prompt, { model: execution.model, effort: execution.effort, readOnly: true, replyTo, ghRepo: `${owner}/${repo}
|
|
8959
|
+
return runClaude(prompt, { model: execution.model, effort: execution.effort, readOnly: true, replyTo, ghRepo: `${owner}/${repo}`, sink: sink2 });
|
|
8871
8960
|
}
|
|
8872
|
-
|
|
8873
|
-
const { owner, repo } = getRepoSlug();
|
|
8961
|
+
function captureStdout() {
|
|
8874
8962
|
let stdout = "";
|
|
8875
8963
|
const sink2 = {
|
|
8876
8964
|
output: (stream, text) => {
|
|
@@ -8878,30 +8966,76 @@ async function runModelForText(prompt, execution) {
|
|
|
8878
8966
|
},
|
|
8879
8967
|
exited: () => void 0
|
|
8880
8968
|
};
|
|
8881
|
-
|
|
8969
|
+
return { sink: sink2, text: () => stdout };
|
|
8970
|
+
}
|
|
8971
|
+
async function runModelForText(prompt, execution) {
|
|
8972
|
+
const { owner, repo } = getRepoSlug();
|
|
8973
|
+
const captured = captureStdout();
|
|
8974
|
+
const options = { model: execution.model, effort: execution.effort, readOnly: true, ghRepo: `${owner}/${repo}`, sink: captured.sink };
|
|
8882
8975
|
await runClaude(prompt, options);
|
|
8883
|
-
return extractDryRunReply(execution.executor,
|
|
8976
|
+
return extractDryRunReply(execution.executor, captured.text());
|
|
8977
|
+
}
|
|
8978
|
+
function coveredOf(watch, linkMap) {
|
|
8979
|
+
return coveredByWatchedIssue(watch, (id) => (linkMap().byIssue.get(id) ?? []).map((ref) => ref.number));
|
|
8980
|
+
}
|
|
8981
|
+
function coveredNote(id, issue) {
|
|
8982
|
+
return `Conductor: #${String(id)} is covered by watched issue #${String(issue)} and gets no separate reply.`;
|
|
8884
8983
|
}
|
|
8885
8984
|
function targetKey(target) {
|
|
8886
8985
|
return `${target.kind}#${String(target.number)}`;
|
|
8887
8986
|
}
|
|
8888
|
-
|
|
8987
|
+
function blockItem(item, label2, reason) {
|
|
8988
|
+
try {
|
|
8989
|
+
addLabel(item.target, BLOCKED_LABEL, "The conductor stopped and needs a person; remove this label to resume");
|
|
8990
|
+
} catch (err) {
|
|
8991
|
+
process.stderr.write(`Error: ${label2} needs a human (${reason}) but the ${BLOCKED_LABEL} label was not applied: ${err.message}
|
|
8992
|
+
`);
|
|
8993
|
+
return "failed";
|
|
8994
|
+
}
|
|
8995
|
+
process.stdout.write(`Conductor: ${label2} needs a human (${reason}); applied the ${BLOCKED_LABEL} label.
|
|
8996
|
+
`);
|
|
8997
|
+
return "idle";
|
|
8998
|
+
}
|
|
8999
|
+
async function conductDryRunItem(prompt, execution, label2, where, replyTo, dryRun) {
|
|
9000
|
+
const reply = await runModelForText(prompt, execution);
|
|
9001
|
+
if (reply === null) {
|
|
9002
|
+
process.stderr.write(`Error: the dry run for ${label2} produced no reply text.
|
|
9003
|
+
`);
|
|
9004
|
+
return "failed";
|
|
9005
|
+
}
|
|
9006
|
+
const needsHuman = parseNeedsHuman(reply);
|
|
9007
|
+
if (needsHuman !== null) {
|
|
9008
|
+
process.stdout.write(`--- Dry run: ${label2} needs a human (not labelled): ${needsHuman} ---
|
|
9009
|
+
`);
|
|
9010
|
+
return "idle";
|
|
9011
|
+
}
|
|
9012
|
+
dryRun.answered.add(targetKey(replyTo));
|
|
9013
|
+
process.stdout.write(`--- Dry run: reply for ${where} (not posted) ---
|
|
9014
|
+
${reply}
|
|
9015
|
+
---
|
|
9016
|
+
`);
|
|
9017
|
+
return "replied";
|
|
9018
|
+
}
|
|
9019
|
+
async function conductItem(id, config, login2, participants, execution, dryRun, linkMap) {
|
|
8889
9020
|
const label2 = `#${String(id)}`;
|
|
8890
9021
|
let item;
|
|
8891
9022
|
try {
|
|
8892
|
-
item = readWatchedItem(id, dryRun?.linkMap);
|
|
9023
|
+
item = readWatchedItem(id, dryRun?.linkMap ?? linkMap);
|
|
8893
9024
|
} catch (err) {
|
|
8894
9025
|
process.stderr.write(`Warning: could not read ${label2}, skipping it this tick: ${err.message}
|
|
8895
9026
|
`);
|
|
8896
9027
|
return "idle";
|
|
8897
9028
|
}
|
|
8898
9029
|
const { thread, conversations } = item;
|
|
8899
|
-
const
|
|
9030
|
+
const loop = loopStateOf(conversations, item.labels, resolveMaxReplies(config.conductor));
|
|
9031
|
+
const verdict = decideConductorReply(conversations, participants, loop);
|
|
8900
9032
|
const owed = verdict.decisions.find(
|
|
8901
9033
|
(decision) => decision.kind === "reply" && dryRun?.answered.has(targetKey(decision.surface)) !== true
|
|
8902
9034
|
);
|
|
8903
9035
|
if (owed === void 0) {
|
|
8904
|
-
|
|
9036
|
+
const stopped = verdict.decisions.find((decision) => decision.kind === "skip" && (decision.reason === "blocked" || decision.reason === "limit"));
|
|
9037
|
+
const why = stopped?.kind === "skip" ? `Conductor: ${label2} gets no reply: ${stopped.detail}.` : `Conductor: ${label2} needs no reply.`;
|
|
9038
|
+
process.stdout.write(`${why}
|
|
8905
9039
|
`);
|
|
8906
9040
|
return "idle";
|
|
8907
9041
|
}
|
|
@@ -8911,25 +9045,17 @@ async function conductItem(id, config, login2, participants, execution, dryRun)
|
|
|
8911
9045
|
const where = `${replyTo.kind === "pr" ? "pull request" : "issue"} #${String(replyTo.number)}`;
|
|
8912
9046
|
process.stdout.write(`Conductor: ${label2} needs a reply on ${where}; running ${describeConductorExecution(execution)}.
|
|
8913
9047
|
`);
|
|
8914
|
-
if (dryRun)
|
|
8915
|
-
|
|
8916
|
-
if (reply === null) {
|
|
8917
|
-
process.stderr.write(`Error: the dry run for ${label2} produced no reply text.
|
|
8918
|
-
`);
|
|
8919
|
-
return "failed";
|
|
8920
|
-
}
|
|
8921
|
-
dryRun.answered.add(targetKey(replyTo));
|
|
8922
|
-
process.stdout.write(`--- Dry run: reply for ${where} (not posted) ---
|
|
8923
|
-
${reply}
|
|
8924
|
-
---
|
|
8925
|
-
`);
|
|
8926
|
-
return "replied";
|
|
8927
|
-
}
|
|
9048
|
+
if (dryRun) return conductDryRunItem(prompt, execution, label2, where, replyTo, dryRun);
|
|
9049
|
+
const captured = captureStdout();
|
|
8928
9050
|
const outcome = await conductReply({
|
|
8929
9051
|
login: login2,
|
|
8930
9052
|
read: () => readTargetMessages(replyTo),
|
|
8931
|
-
run: () => runModel(prompt, execution, replyTo)
|
|
9053
|
+
run: () => runModel(prompt, execution, replyTo, captured.sink)
|
|
8932
9054
|
});
|
|
9055
|
+
const needsHuman = parseNeedsHuman(extractDryRunReply(execution.executor, captured.text()) ?? "");
|
|
9056
|
+
if (needsHuman !== null && outcome.kind !== "run-failed" && outcome.kind !== "unverified") {
|
|
9057
|
+
return blockItem(item, label2, needsHuman);
|
|
9058
|
+
}
|
|
8933
9059
|
switch (outcome.kind) {
|
|
8934
9060
|
case "posted":
|
|
8935
9061
|
process.stdout.write(`Conductor: posted a reply on ${where}.
|
|
@@ -9062,6 +9188,8 @@ function preflight() {
|
|
|
9062
9188
|
}
|
|
9063
9189
|
const executionProblem = conductorExecutionProblem(config.conductor);
|
|
9064
9190
|
if (executionProblem !== null) return { problem: executionProblem };
|
|
9191
|
+
const maxReplies = maxRepliesProblem(config.conductor);
|
|
9192
|
+
if (maxReplies !== null) return { problem: maxReplies };
|
|
9065
9193
|
if (config.conductor?.executor === "codex") {
|
|
9066
9194
|
return { problem: "conductor.executor codex is not supported: Codex cannot be limited to comments on the reply target. Use claude." };
|
|
9067
9195
|
}
|
|
@@ -9075,12 +9203,14 @@ function preflight() {
|
|
|
9075
9203
|
function staleMinutesOf(config) {
|
|
9076
9204
|
return config.doWork?.lockStaleMinutes ?? DEFAULT_DO_WORK.lockStaleMinutes;
|
|
9077
9205
|
}
|
|
9078
|
-
function checkWatched(id, participants, linkMap) {
|
|
9206
|
+
function checkWatched(id, participants, maxReplies, linkMap, coveredBy) {
|
|
9079
9207
|
try {
|
|
9080
9208
|
const target = getWatchTarget(id);
|
|
9081
9209
|
if (isWatchClosed(target.state)) return { id, kind: target.kind, state: "closed", decisions: [] };
|
|
9082
|
-
|
|
9083
|
-
|
|
9210
|
+
if (coveredBy !== void 0) return { id, kind: target.kind, state: "open", decisions: [], coveredBy };
|
|
9211
|
+
const { conversations, labels } = readWatchedItem(id, linkMap);
|
|
9212
|
+
const loop = loopStateOf(conversations, labels, maxReplies);
|
|
9213
|
+
return { id, kind: target.kind, state: "open", decisions: decideConductorReply(conversations, participants, loop).decisions };
|
|
9084
9214
|
} catch (err) {
|
|
9085
9215
|
return { id, unavailable: err.message };
|
|
9086
9216
|
}
|
|
@@ -9095,6 +9225,8 @@ function runConductorCheck() {
|
|
|
9095
9225
|
} catch {
|
|
9096
9226
|
}
|
|
9097
9227
|
const linkMap = onceLinkMap();
|
|
9228
|
+
const watchIds = "problem" in checked ? [] : normalizeWatch(checked.config.conductor?.watch);
|
|
9229
|
+
const covered = "problem" in checked ? /* @__PURE__ */ new Map() : coveredOf(watchIds, linkMap);
|
|
9098
9230
|
const report = "problem" in checked ? buildConductorCheck({
|
|
9099
9231
|
generatedAt,
|
|
9100
9232
|
repo,
|
|
@@ -9112,7 +9244,7 @@ function runConductorCheck() {
|
|
|
9112
9244
|
execution: describeConductorExecution(checked.execution),
|
|
9113
9245
|
lock: inspectConductorLock(staleMinutesOf(checked.config)),
|
|
9114
9246
|
staleMinutes: staleMinutesOf(checked.config),
|
|
9115
|
-
watched:
|
|
9247
|
+
watched: watchIds.map((id) => checkWatched(id, checked.participants, resolveMaxReplies(checked.config.conductor), linkMap, covered.get(id)))
|
|
9116
9248
|
});
|
|
9117
9249
|
process.stdout.write(report.text);
|
|
9118
9250
|
return report.exitCode;
|
|
@@ -9128,7 +9260,9 @@ async function runConductorDryRun(checked) {
|
|
|
9128
9260
|
`);
|
|
9129
9261
|
let failed = 0;
|
|
9130
9262
|
const context = { linkMap: onceLinkMap(), answered: /* @__PURE__ */ new Set() };
|
|
9131
|
-
|
|
9263
|
+
const watchIds = normalizeWatch(config.conductor?.watch);
|
|
9264
|
+
const covered = coveredOf(watchIds, context.linkMap);
|
|
9265
|
+
for (const id of watchIds) {
|
|
9132
9266
|
try {
|
|
9133
9267
|
if (isWatchClosed(getWatchTarget(id).state)) {
|
|
9134
9268
|
process.stdout.write(`Conductor: #${String(id)} is closed; a tick would drop it from the watch list.
|
|
@@ -9137,6 +9271,12 @@ async function runConductorDryRun(checked) {
|
|
|
9137
9271
|
}
|
|
9138
9272
|
} catch {
|
|
9139
9273
|
}
|
|
9274
|
+
const coveredBy = covered.get(id);
|
|
9275
|
+
if (coveredBy !== void 0) {
|
|
9276
|
+
process.stdout.write(`${coveredNote(id, coveredBy)}
|
|
9277
|
+
`);
|
|
9278
|
+
continue;
|
|
9279
|
+
}
|
|
9140
9280
|
try {
|
|
9141
9281
|
if (await conductItem(id, config, login2, participants, execution, context) === "failed") failed++;
|
|
9142
9282
|
} catch (err) {
|
|
@@ -9147,6 +9287,18 @@ async function runConductorDryRun(checked) {
|
|
|
9147
9287
|
}
|
|
9148
9288
|
return failed > 0 ? 1 : 0;
|
|
9149
9289
|
}
|
|
9290
|
+
function reportHeldLock(held, suspect, staleMinutes) {
|
|
9291
|
+
process.stdout.write(
|
|
9292
|
+
`Another conductor is already running here (pid ${String(held.pid)} on ${held.host}, started ${held.startedAt}). Doing nothing.
|
|
9293
|
+
`
|
|
9294
|
+
);
|
|
9295
|
+
if (!suspect) return 0;
|
|
9296
|
+
process.stderr.write(
|
|
9297
|
+
`Warning: that lock has been held longer than ${String(staleMinutes)} minutes. If no tick is really running, its process id was probably reused; remove ${CONDUCTOR_LOCK_RELATIVE_PATH} once you have confirmed that.
|
|
9298
|
+
`
|
|
9299
|
+
);
|
|
9300
|
+
return 2;
|
|
9301
|
+
}
|
|
9150
9302
|
async function runConductor(options = {}) {
|
|
9151
9303
|
if (options.check === true && options.dryRun === true) {
|
|
9152
9304
|
return fail2("--check and --dry-run cannot be used together.");
|
|
@@ -9158,29 +9310,23 @@ async function runConductor(options = {}) {
|
|
|
9158
9310
|
const { config, login: login2, participants, execution } = checked;
|
|
9159
9311
|
const staleMinutes = config.doWork?.lockStaleMinutes ?? DEFAULT_DO_WORK.lockStaleMinutes;
|
|
9160
9312
|
const lock = acquireConductorLock(staleMinutes);
|
|
9161
|
-
if (!lock.ok)
|
|
9162
|
-
const held = lock.heldBy;
|
|
9163
|
-
process.stdout.write(
|
|
9164
|
-
`Another conductor is already running here (pid ${String(held.pid)} on ${held.host}, started ${held.startedAt}). Doing nothing.
|
|
9165
|
-
`
|
|
9166
|
-
);
|
|
9167
|
-
if (lock.suspect) {
|
|
9168
|
-
process.stderr.write(
|
|
9169
|
-
`Warning: that lock has been held longer than ${String(staleMinutes)} minutes. If no tick is really running, its process id was probably reused; remove ${CONDUCTOR_LOCK_RELATIVE_PATH} once you have confirmed that.
|
|
9170
|
-
`
|
|
9171
|
-
);
|
|
9172
|
-
return 2;
|
|
9173
|
-
}
|
|
9174
|
-
return 0;
|
|
9175
|
-
}
|
|
9313
|
+
if (!lock.ok) return reportHeldLock(lock.heldBy, lock.suspect, staleMinutes);
|
|
9176
9314
|
try {
|
|
9177
9315
|
process.stdout.write(`Conductor: running as ${login2}.
|
|
9178
9316
|
`);
|
|
9179
9317
|
const watch = pruneWatchList(config);
|
|
9180
9318
|
let failed = 0;
|
|
9319
|
+
const linkMap = onceLinkMap();
|
|
9320
|
+
const covered = coveredOf(watch, linkMap);
|
|
9181
9321
|
for (const id of watch) {
|
|
9322
|
+
const coveredBy = covered.get(id);
|
|
9323
|
+
if (coveredBy !== void 0) {
|
|
9324
|
+
process.stdout.write(`${coveredNote(id, coveredBy)}
|
|
9325
|
+
`);
|
|
9326
|
+
continue;
|
|
9327
|
+
}
|
|
9182
9328
|
try {
|
|
9183
|
-
if (await conductItem(id, config, login2, participants, execution) === "failed") failed++;
|
|
9329
|
+
if (await conductItem(id, config, login2, participants, execution, void 0, linkMap) === "failed") failed++;
|
|
9184
9330
|
} catch (err) {
|
|
9185
9331
|
process.stderr.write(`Error: #${String(id)} could not be conducted: ${err.message}
|
|
9186
9332
|
`);
|