automata-cli 0.11.0-develop.514 → 0.11.0-develop.526
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 +467 -89
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -90,11 +90,11 @@ function writeDoWork(patch) {
|
|
|
90
90
|
const current = readRawConfig();
|
|
91
91
|
writeConfig({ ...current, doWork: { ...current.doWork, ...patch } });
|
|
92
92
|
}
|
|
93
|
-
function parseNonNegativeInt(value,
|
|
93
|
+
function parseNonNegativeInt(value, label2) {
|
|
94
94
|
const trimmed2 = value.trim();
|
|
95
95
|
const parsed = Number.parseInt(trimmed2, 10);
|
|
96
96
|
if (Number.isNaN(parsed) || parsed < 0 || String(parsed) !== trimmed2 || !Number.isSafeInteger(parsed)) {
|
|
97
|
-
process.stderr.write(`Error: ${
|
|
97
|
+
process.stderr.write(`Error: ${label2} must be a non-negative integer (got "${value}").
|
|
98
98
|
`);
|
|
99
99
|
process.exit(1);
|
|
100
100
|
}
|
|
@@ -1705,7 +1705,7 @@ function formatSonarIssue(issue) {
|
|
|
1705
1705
|
const location = formatLocation(issue.path, issue.line);
|
|
1706
1706
|
if (location) lines.push(` Location: ${location}`);
|
|
1707
1707
|
if (issue.severity || issue.type) {
|
|
1708
|
-
const labels = [issue.severity, issue.type].filter((
|
|
1708
|
+
const labels = [issue.severity, issue.type].filter((label2) => Boolean(label2)).map(sanitizeText).join(" / ");
|
|
1709
1709
|
lines.push(` Classification: ${labels}`);
|
|
1710
1710
|
}
|
|
1711
1711
|
if (issue.rule) lines.push(` Rule: ${sanitizeText(issue.rule)}`);
|
|
@@ -1718,7 +1718,7 @@ function formatSonarHotspot(hotspot) {
|
|
|
1718
1718
|
if (location) lines.push(` Location: ${location}`);
|
|
1719
1719
|
if (hotspot.status) lines.push(` Status: ${sanitizeText(hotspot.status)}`);
|
|
1720
1720
|
if (hotspot.vulnerabilityProbability || hotspot.securityCategory) {
|
|
1721
|
-
const labels = [hotspot.vulnerabilityProbability, hotspot.securityCategory].filter((
|
|
1721
|
+
const labels = [hotspot.vulnerabilityProbability, hotspot.securityCategory].filter((label2) => Boolean(label2)).map(sanitizeText).join(" / ");
|
|
1722
1722
|
lines.push(` Classification: ${labels}`);
|
|
1723
1723
|
}
|
|
1724
1724
|
if (hotspot.rule) {
|
|
@@ -2368,7 +2368,7 @@ function getIssueSurface(issueNumber) {
|
|
|
2368
2368
|
issue: { number: raw.number, title: raw.title, body: raw.body, url: raw.url },
|
|
2369
2369
|
state: raw.state === "CLOSED" ? "CLOSED" : "OPEN",
|
|
2370
2370
|
assignees: (raw.assignees ?? []).map(login).filter((name) => name.length > 0),
|
|
2371
|
-
labels: (raw.labels ?? []).map((
|
|
2371
|
+
labels: (raw.labels ?? []).map((label2) => label2.name ?? "").filter((name) => name.length > 0),
|
|
2372
2372
|
messages
|
|
2373
2373
|
};
|
|
2374
2374
|
}
|
|
@@ -2427,7 +2427,7 @@ function toPullRequestRef(node) {
|
|
|
2427
2427
|
function toOrphanPr(node) {
|
|
2428
2428
|
return {
|
|
2429
2429
|
pr: toPullRequestRef(node),
|
|
2430
|
-
labels: (node.labels?.nodes ?? []).map((
|
|
2430
|
+
labels: (node.labels?.nodes ?? []).map((label2) => label2.name ?? "").filter((name) => name.length > 0),
|
|
2431
2431
|
assignees: (node.assignees?.nodes ?? []).map((assignee) => assignee.login ?? "").filter((login2) => login2.length > 0)
|
|
2432
2432
|
};
|
|
2433
2433
|
}
|
|
@@ -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;
|
|
@@ -4489,6 +4505,9 @@ var UNKNOWN_OWNER = {
|
|
|
4489
4505
|
command: "unknown",
|
|
4490
4506
|
token: ""
|
|
4491
4507
|
};
|
|
4508
|
+
function inspectConductorLock(staleMinutes, now = Date.now()) {
|
|
4509
|
+
return inspectRunLock(staleMinutes, now, CONDUCTOR_LOCK_FILE);
|
|
4510
|
+
}
|
|
4492
4511
|
function acquireConductorLock(staleMinutes) {
|
|
4493
4512
|
return acquireRunLock("conductor", staleMinutes, CONDUCTOR_LOCK_FILE);
|
|
4494
4513
|
}
|
|
@@ -4599,8 +4618,8 @@ function heldForMs(owner, now) {
|
|
|
4599
4618
|
const startedAt = Date.parse(owner.startedAt);
|
|
4600
4619
|
return Number.isNaN(startedAt) ? null : now - startedAt;
|
|
4601
4620
|
}
|
|
4602
|
-
function inspectRunLock(staleMinutes, now = Date.now()) {
|
|
4603
|
-
const path = lockPath();
|
|
4621
|
+
function inspectRunLock(staleMinutes, now = Date.now(), lockFile = LOCK_FILE) {
|
|
4622
|
+
const path = lockPath(lockFile);
|
|
4604
4623
|
try {
|
|
4605
4624
|
statSync2(path);
|
|
4606
4625
|
} catch (err) {
|
|
@@ -4785,9 +4804,9 @@ function detectRescueTarget(options, now) {
|
|
|
4785
4804
|
return { branch: current, createdBranch: false, source: current };
|
|
4786
4805
|
}
|
|
4787
4806
|
const source = detached ? "detached HEAD" : current === options.baseBranch ? options.baseBranch : current;
|
|
4788
|
-
const
|
|
4807
|
+
const label2 = detached ? "detached" : flattenBranchName(source);
|
|
4789
4808
|
return {
|
|
4790
|
-
branch: `${RESCUE_BRANCH_PREFIX}${
|
|
4809
|
+
branch: `${RESCUE_BRANCH_PREFIX}${label2}-${utcStamp(now)}`,
|
|
4791
4810
|
createdBranch: true,
|
|
4792
4811
|
source
|
|
4793
4812
|
};
|
|
@@ -5203,9 +5222,9 @@ var RunTranscript = class {
|
|
|
5203
5222
|
stderrAtLineStart = true;
|
|
5204
5223
|
/** A private key began in an earlier line and has not ended yet. */
|
|
5205
5224
|
inPrivateKey = false;
|
|
5206
|
-
constructor(
|
|
5225
|
+
constructor(label2, root = process.cwd(), now = /* @__PURE__ */ new Date()) {
|
|
5207
5226
|
const stamp = now.toISOString().replace(/[:.]/g, "-");
|
|
5208
|
-
this.fileName = `${stamp}-${
|
|
5227
|
+
this.fileName = `${stamp}-${label2}.log`;
|
|
5209
5228
|
const dir = join5(root, RUN_TRANSCRIPT_DIR);
|
|
5210
5229
|
this.path = join5(dir, this.fileName);
|
|
5211
5230
|
try {
|
|
@@ -6472,12 +6491,12 @@ function gitSection(status) {
|
|
|
6472
6491
|
return build("git", lines, problems, { ...status });
|
|
6473
6492
|
}
|
|
6474
6493
|
function assembleReport(input) {
|
|
6475
|
-
const byId = new Map(input.sections.map((
|
|
6494
|
+
const byId = new Map(input.sections.map((section2) => [section2.id, section2]));
|
|
6476
6495
|
const ordered = SECTION_ORDER.flatMap((id) => {
|
|
6477
|
-
const
|
|
6478
|
-
return
|
|
6496
|
+
const section2 = byId.get(id);
|
|
6497
|
+
return section2 === void 0 ? [] : [section2];
|
|
6479
6498
|
});
|
|
6480
|
-
const problems = ordered.flatMap((
|
|
6499
|
+
const problems = ordered.flatMap((section2) => section2.problems);
|
|
6481
6500
|
return {
|
|
6482
6501
|
generatedAt: input.generatedAt,
|
|
6483
6502
|
repo: input.repo,
|
|
@@ -6511,8 +6530,8 @@ function renderProblems(problems) {
|
|
|
6511
6530
|
function renderText(report) {
|
|
6512
6531
|
const head = `automata do-work --check \u2014 ${report.repo ?? "unknown repository"} \u2014 ${report.generatedAt.toISOString()}`;
|
|
6513
6532
|
const parts = report.blocked === null ? [head, ""] : [`blocked: ${report.blocked.trigger}`, head, ""];
|
|
6514
|
-
for (const
|
|
6515
|
-
parts.push(...renderBlock(
|
|
6533
|
+
for (const section2 of report.sections) {
|
|
6534
|
+
parts.push(...renderBlock(section2.title, section2.lines, "(nothing to report)"));
|
|
6516
6535
|
}
|
|
6517
6536
|
if (report.trace !== null) parts.push(...renderTrace(report.trace));
|
|
6518
6537
|
if (report.problems.length > 0) parts.push(...renderProblems(report.problems));
|
|
@@ -6523,12 +6542,12 @@ function renderText(report) {
|
|
|
6523
6542
|
}
|
|
6524
6543
|
function toJson(report) {
|
|
6525
6544
|
const sections = {};
|
|
6526
|
-
for (const
|
|
6527
|
-
sections[
|
|
6528
|
-
title:
|
|
6529
|
-
lines:
|
|
6530
|
-
problems:
|
|
6531
|
-
data:
|
|
6545
|
+
for (const section2 of report.sections) {
|
|
6546
|
+
sections[section2.id] = {
|
|
6547
|
+
title: section2.title,
|
|
6548
|
+
lines: section2.lines,
|
|
6549
|
+
problems: section2.problems,
|
|
6550
|
+
data: section2.data
|
|
6532
6551
|
};
|
|
6533
6552
|
}
|
|
6534
6553
|
return {
|
|
@@ -6587,14 +6606,14 @@ var SettingsError = class extends Error {
|
|
|
6587
6606
|
function failSettings(message) {
|
|
6588
6607
|
throw new SettingsError(message);
|
|
6589
6608
|
}
|
|
6590
|
-
function parsePositiveInt(value,
|
|
6609
|
+
function parsePositiveInt(value, label2) {
|
|
6591
6610
|
const trimmed2 = value.trim();
|
|
6592
6611
|
if (!/^\d+$/.test(trimmed2)) {
|
|
6593
|
-
failSettings(`${
|
|
6612
|
+
failSettings(`${label2} must be a positive integer (got "${value}").`);
|
|
6594
6613
|
}
|
|
6595
6614
|
const parsed = Number(trimmed2);
|
|
6596
6615
|
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
6597
|
-
failSettings(`${
|
|
6616
|
+
failSettings(`${label2} must be a positive integer within the safe range (got "${value}").`);
|
|
6598
6617
|
}
|
|
6599
6618
|
return parsed;
|
|
6600
6619
|
}
|
|
@@ -6749,14 +6768,14 @@ function planRun(item, settings, execution) {
|
|
|
6749
6768
|
}
|
|
6750
6769
|
function describeAssignment(item, agentUser) {
|
|
6751
6770
|
return claimStates(item).map((claim) => {
|
|
6752
|
-
const
|
|
6771
|
+
const label2 = claim.surface === "issue" ? "issue" : `pull request #${String(claim.number)}`;
|
|
6753
6772
|
switch (claim.state) {
|
|
6754
6773
|
case "would-claim":
|
|
6755
|
-
return `would assign ${
|
|
6774
|
+
return `would assign ${label2} to ${agentUser}`;
|
|
6756
6775
|
case "already-assigned":
|
|
6757
|
-
return `${
|
|
6776
|
+
return `${label2} already assigned`;
|
|
6758
6777
|
case "rule-exempt":
|
|
6759
|
-
return `${
|
|
6778
|
+
return `${label2} not claimed (orphan pass)`;
|
|
6760
6779
|
}
|
|
6761
6780
|
}).join(" \xB7 ");
|
|
6762
6781
|
}
|
|
@@ -6832,24 +6851,24 @@ function validateOptionalInt(container, key2, path, min, hint) {
|
|
|
6832
6851
|
failSettings(`${path} must be ${hint}, got ${JSON.stringify(value)}.`);
|
|
6833
6852
|
}
|
|
6834
6853
|
}
|
|
6835
|
-
function validateDoWorkConfig(
|
|
6836
|
-
if (
|
|
6837
|
-
if (!isPlainObject(
|
|
6838
|
-
failSettings(`doWork must be an object, got ${JSON.stringify(
|
|
6854
|
+
function validateDoWorkConfig(section2) {
|
|
6855
|
+
if (section2 === void 0 || section2 === null) return;
|
|
6856
|
+
if (!isPlainObject(section2)) {
|
|
6857
|
+
failSettings(`doWork must be an object, got ${JSON.stringify(section2)}.`);
|
|
6839
6858
|
}
|
|
6840
|
-
const executor =
|
|
6859
|
+
const executor = section2["executor"];
|
|
6841
6860
|
if (executor !== void 0 && executor !== "claude" && executor !== "codex") {
|
|
6842
6861
|
failSettings(`doWork.executor must be 'claude' or 'codex', got ${JSON.stringify(executor)}.`);
|
|
6843
6862
|
}
|
|
6844
|
-
validateOptionalString(
|
|
6845
|
-
validateOptionalInt(
|
|
6846
|
-
validateOptionalInt(
|
|
6847
|
-
validateOptionalBoolean(
|
|
6848
|
-
validateOptionalBoolean(
|
|
6849
|
-
validateProtectedBranches(
|
|
6850
|
-
validateSettingContainer(
|
|
6851
|
-
validateSettingContainer(
|
|
6852
|
-
validateSettingContainer(
|
|
6863
|
+
validateOptionalString(section2, "baseBranch", "doWork.baseBranch");
|
|
6864
|
+
validateOptionalInt(section2, "maxRunsPerTick", "doWork.maxRunsPerTick", 0, "a non-negative integer (0 = unlimited)");
|
|
6865
|
+
validateOptionalInt(section2, "lockStaleMinutes", "doWork.lockStaleMinutes", 1, "a positive integer");
|
|
6866
|
+
validateOptionalBoolean(section2, "dumpOnBlock", "doWork.dumpOnBlock");
|
|
6867
|
+
validateOptionalBoolean(section2, "postRunLog", "doWork.postRunLog");
|
|
6868
|
+
validateProtectedBranches(section2["protectedBranches"]);
|
|
6869
|
+
validateSettingContainer(section2["models"], "models", ["claude", "codex"]);
|
|
6870
|
+
validateSettingContainer(section2["effort"], "effort", ["claude", "codex"]);
|
|
6871
|
+
validateSettingContainer(section2["prompts"], "prompts", ["issueDiscuss", "prWork", "prOrphan"]);
|
|
6853
6872
|
}
|
|
6854
6873
|
function validateOptionalBoolean(container, key2, path) {
|
|
6855
6874
|
const value = container[key2];
|
|
@@ -6912,7 +6931,7 @@ function matchesDiscoveryFilter(subject, settings) {
|
|
|
6912
6931
|
const value = settings.discoveryValue.toLowerCase();
|
|
6913
6932
|
switch (settings.technique) {
|
|
6914
6933
|
case "label":
|
|
6915
|
-
return subject.labels.some((
|
|
6934
|
+
return subject.labels.some((label2) => label2.toLowerCase() === value);
|
|
6916
6935
|
case "assignee":
|
|
6917
6936
|
return subject.assignees.some((assignee) => assignee.toLowerCase() === value);
|
|
6918
6937
|
case "title-contains":
|
|
@@ -8419,6 +8438,44 @@ function toPlanJson(decision) {
|
|
|
8419
8438
|
// src/commands/conductor.ts
|
|
8420
8439
|
import { Command as Command7 } from "commander";
|
|
8421
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
|
+
|
|
8422
8479
|
// src/conductor/replyDecision.ts
|
|
8423
8480
|
function isWatchClosed(state) {
|
|
8424
8481
|
return state === "closed";
|
|
@@ -8479,10 +8536,33 @@ function decideConversation(conversation, p) {
|
|
|
8479
8536
|
reason: `${p.agentUser}'s message at ${newest2.createdAt} has no answer from an allowed user`
|
|
8480
8537
|
};
|
|
8481
8538
|
}
|
|
8482
|
-
function decideConductorReply(conversations, p) {
|
|
8483
|
-
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
|
+
);
|
|
8484
8545
|
return { needsReply: decisions.some((decision) => decision.kind === "reply"), decisions };
|
|
8485
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
|
+
}
|
|
8486
8566
|
|
|
8487
8567
|
// src/conductor/thread.ts
|
|
8488
8568
|
function defaultReplyTarget(thread) {
|
|
@@ -8494,7 +8574,19 @@ function postingInstruction(target) {
|
|
|
8494
8574
|
return [
|
|
8495
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.`,
|
|
8496
8576
|
`Run \`gh ${command} comment ${number} --body-file -\` and give the body on standard input, for example with a here-document.`,
|
|
8497
|
-
|
|
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()
|
|
8580
|
+
].join("\n");
|
|
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
|
+
}
|
|
8585
|
+
function dryRunInstruction(target) {
|
|
8586
|
+
return [
|
|
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.`,
|
|
8588
|
+
"Make the reply, and nothing else, your final message. Do not approve, merge or close anything.",
|
|
8589
|
+
needsHumanInstruction()
|
|
8498
8590
|
].join("\n");
|
|
8499
8591
|
}
|
|
8500
8592
|
function visible(messages, participants) {
|
|
@@ -8576,7 +8668,7 @@ function composeConductorPrompt(input) {
|
|
|
8576
8668
|
"Only the messages above exist. Anything from other accounts has been withheld",
|
|
8577
8669
|
"deliberately \u2014 do not ask about it.",
|
|
8578
8670
|
"",
|
|
8579
|
-
postingInstruction(replyTo)
|
|
8671
|
+
input.dryRun === true ? dryRunInstruction(replyTo) : postingInstruction(replyTo)
|
|
8580
8672
|
);
|
|
8581
8673
|
return lines.join("\n");
|
|
8582
8674
|
}
|
|
@@ -8667,6 +8759,117 @@ async function conductReply(input) {
|
|
|
8667
8759
|
return runError === null ? { kind: "posted-nothing" } : { kind: "run-failed", error: runError };
|
|
8668
8760
|
}
|
|
8669
8761
|
|
|
8762
|
+
// src/conductor/checkReport.ts
|
|
8763
|
+
function section(title, lines, problems = []) {
|
|
8764
|
+
return { title, lines, problems };
|
|
8765
|
+
}
|
|
8766
|
+
function label(id, kind) {
|
|
8767
|
+
return `${kind === "pr" ? "PR" : "issue"} #${String(id)}`;
|
|
8768
|
+
}
|
|
8769
|
+
function lockSection2(status, staleMinutes) {
|
|
8770
|
+
const fix = "remove .automata/conductor.lock once you have confirmed that no conductor runs";
|
|
8771
|
+
switch (status.kind) {
|
|
8772
|
+
case "free":
|
|
8773
|
+
return section("Run lock", ["no conductor is running in this checkout"]);
|
|
8774
|
+
case "held": {
|
|
8775
|
+
const held = status.heldForMs === null ? "" : ` (${describeDuration(status.heldForMs)} so far)`;
|
|
8776
|
+
return section("Run lock", [
|
|
8777
|
+
`a conductor is running: pid ${String(status.owner.pid)} on ${status.owner.host}, started ${status.owner.startedAt}${held}`,
|
|
8778
|
+
"a tick started now would do nothing"
|
|
8779
|
+
]);
|
|
8780
|
+
}
|
|
8781
|
+
case "suspect":
|
|
8782
|
+
return section(
|
|
8783
|
+
"Run lock",
|
|
8784
|
+
[`held longer than ${String(staleMinutes)} minutes by pid ${String(status.owner.pid)} on ${status.owner.host}`],
|
|
8785
|
+
[{ section: "lock", summary: `the lock looks alive but outlived the stale window, probably a reused pid: ${fix}` }]
|
|
8786
|
+
);
|
|
8787
|
+
case "stale":
|
|
8788
|
+
return section("Run lock", ["a stale lock is present; the next tick takes it over"]);
|
|
8789
|
+
case "unreadable":
|
|
8790
|
+
return section(
|
|
8791
|
+
"Run lock",
|
|
8792
|
+
[`the lock cannot be read: ${status.detail}`],
|
|
8793
|
+
[{ section: "lock", summary: `the lock file cannot be read (${status.detail}); fix its permissions` }]
|
|
8794
|
+
);
|
|
8795
|
+
}
|
|
8796
|
+
}
|
|
8797
|
+
function decisionLine(decision) {
|
|
8798
|
+
const where = label(decision.surface.number, decision.surface.kind);
|
|
8799
|
+
return decision.kind === "reply" ? `reply on ${where}: ${decision.reason}` : `skip ${where} (${decision.reason}): ${decision.detail}`;
|
|
8800
|
+
}
|
|
8801
|
+
function watchSections(watched) {
|
|
8802
|
+
const listLines = [];
|
|
8803
|
+
const replyLines = [];
|
|
8804
|
+
const problems = [];
|
|
8805
|
+
for (const entry of watched) {
|
|
8806
|
+
if ("unavailable" in entry) {
|
|
8807
|
+
listLines.push(`#${String(entry.id)}: unavailable (${entry.unavailable}); a tick keeps it and skips it`);
|
|
8808
|
+
problems.push({ section: "watch", summary: `#${String(entry.id)} could not be read: ${entry.unavailable}` });
|
|
8809
|
+
continue;
|
|
8810
|
+
}
|
|
8811
|
+
const name = label(entry.id, entry.kind);
|
|
8812
|
+
if (entry.state === "closed") {
|
|
8813
|
+
listLines.push(`${name}: closed, a tick would drop it from the watch list`);
|
|
8814
|
+
continue;
|
|
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
|
+
}
|
|
8820
|
+
listLines.push(`${name}: open`);
|
|
8821
|
+
for (const decision of entry.decisions) replyLines.push(decisionLine(decision));
|
|
8822
|
+
}
|
|
8823
|
+
return [
|
|
8824
|
+
section(`Watch list (${String(watched.length)})`, listLines, problems),
|
|
8825
|
+
section("Replies a tick would write", replyLines)
|
|
8826
|
+
];
|
|
8827
|
+
}
|
|
8828
|
+
function buildConductorCheck(input) {
|
|
8829
|
+
const sections = input.configProblem === null ? [
|
|
8830
|
+
section("Configuration", [`running as ${input.identity}`, `executor: ${input.execution}`]),
|
|
8831
|
+
lockSection2(input.lock, input.staleMinutes),
|
|
8832
|
+
...watchSections(input.watched)
|
|
8833
|
+
] : [
|
|
8834
|
+
section(
|
|
8835
|
+
"Configuration",
|
|
8836
|
+
[input.configProblem],
|
|
8837
|
+
[{ section: "configuration", summary: input.configProblem }]
|
|
8838
|
+
)
|
|
8839
|
+
];
|
|
8840
|
+
const problems = sections.flatMap((s) => s.problems);
|
|
8841
|
+
const parts = [
|
|
8842
|
+
`automata conductor --check \u2014 ${input.repo ?? "unknown repository"} \u2014 ${input.generatedAt.toISOString()}`,
|
|
8843
|
+
""
|
|
8844
|
+
];
|
|
8845
|
+
for (const s of sections) {
|
|
8846
|
+
parts.push(s.title, ...(s.lines.length === 0 ? ["(nothing to report)"] : s.lines).map((l) => ` ${l}`), "");
|
|
8847
|
+
}
|
|
8848
|
+
if (problems.length > 0) {
|
|
8849
|
+
parts.push(`Problems (${String(problems.length)})`, ...problems.map((p) => ` \xB7 ${p.section}: ${p.summary}`), "");
|
|
8850
|
+
}
|
|
8851
|
+
parts.push(problems.length === 0 ? "RESULT: healthy" : `RESULT: ${String(problems.length)} problem(s) found`);
|
|
8852
|
+
return { text: parts.join("\n") + "\n", exitCode: problems.length === 0 ? 0 : 1, problems };
|
|
8853
|
+
}
|
|
8854
|
+
|
|
8855
|
+
// src/conductor/dryRunReply.ts
|
|
8856
|
+
function claudeResult(raw) {
|
|
8857
|
+
let result = null;
|
|
8858
|
+
for (const line of raw.split("\n")) {
|
|
8859
|
+
if (!line.trim().startsWith("{")) continue;
|
|
8860
|
+
try {
|
|
8861
|
+
const event = JSON.parse(line);
|
|
8862
|
+
if (event["type"] === "result" && typeof event["result"] === "string") result = event["result"];
|
|
8863
|
+
} catch {
|
|
8864
|
+
}
|
|
8865
|
+
}
|
|
8866
|
+
return result;
|
|
8867
|
+
}
|
|
8868
|
+
function extractDryRunReply(executor, stdout) {
|
|
8869
|
+
const text = (executor === "claude" ? claudeResult(stdout) : stdout)?.trim();
|
|
8870
|
+
return text === void 0 || text.length === 0 ? null : text;
|
|
8871
|
+
}
|
|
8872
|
+
|
|
8670
8873
|
// src/conductor/watchList.ts
|
|
8671
8874
|
function parseWatchId(raw) {
|
|
8672
8875
|
const text = raw.trim().replace(/^#/, "");
|
|
@@ -8726,58 +8929,133 @@ function pruneWatchList(config) {
|
|
|
8726
8929
|
if (dropped.length > 0) updateWatch((current) => current.filter((id) => !dropped.includes(id)));
|
|
8727
8930
|
return watch.filter((id) => !dropped.includes(id));
|
|
8728
8931
|
}
|
|
8932
|
+
function onceLinkMap() {
|
|
8933
|
+
let map = null;
|
|
8934
|
+
return () => map ??= getOpenPrLinkMap();
|
|
8935
|
+
}
|
|
8729
8936
|
function readPr(number) {
|
|
8730
8937
|
return { surface: getPrSurface(number), checks: getPrChecks(number) };
|
|
8731
8938
|
}
|
|
8732
|
-
function readWatchedItem(id) {
|
|
8939
|
+
function readWatchedItem(id, linkMap = getOpenPrLinkMap) {
|
|
8733
8940
|
const target = getWatchTarget(id);
|
|
8734
8941
|
if (target.kind === "pr") {
|
|
8735
8942
|
const pr = readPr(id);
|
|
8736
|
-
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 };
|
|
8737
8944
|
}
|
|
8738
8945
|
const issue = getIssueSurface(id);
|
|
8739
|
-
const prs = (
|
|
8946
|
+
const prs = (linkMap().byIssue.get(id) ?? []).map((ref) => readPr(ref.number));
|
|
8740
8947
|
return {
|
|
8948
|
+
target,
|
|
8741
8949
|
thread: { kind: "issue", issue, prs },
|
|
8742
|
-
conversations: [issueConversation(issue), ...prs.map((pr) => prConversation(pr.surface))]
|
|
8950
|
+
conversations: [issueConversation(issue), ...prs.map((pr) => prConversation(pr.surface))],
|
|
8951
|
+
labels: target.labels
|
|
8743
8952
|
};
|
|
8744
8953
|
}
|
|
8745
8954
|
function readTargetMessages(target) {
|
|
8746
8955
|
return target.kind === "issue" ? issueConversation(getIssueSurface(target.number)).messages : prConversation(getPrSurface(target.number)).messages;
|
|
8747
8956
|
}
|
|
8748
|
-
function runModel(prompt, execution, replyTo) {
|
|
8957
|
+
function runModel(prompt, execution, replyTo, sink2) {
|
|
8958
|
+
const { owner, repo } = getRepoSlug();
|
|
8959
|
+
return runClaude(prompt, { model: execution.model, effort: execution.effort, readOnly: true, replyTo, ghRepo: `${owner}/${repo}`, sink: sink2 });
|
|
8960
|
+
}
|
|
8961
|
+
function captureStdout() {
|
|
8962
|
+
let stdout = "";
|
|
8963
|
+
const sink2 = {
|
|
8964
|
+
output: (stream, text) => {
|
|
8965
|
+
if (stream === "stdout") stdout += text;
|
|
8966
|
+
},
|
|
8967
|
+
exited: () => void 0
|
|
8968
|
+
};
|
|
8969
|
+
return { sink: sink2, text: () => stdout };
|
|
8970
|
+
}
|
|
8971
|
+
async function runModelForText(prompt, execution) {
|
|
8749
8972
|
const { owner, repo } = getRepoSlug();
|
|
8750
|
-
|
|
8973
|
+
const captured = captureStdout();
|
|
8974
|
+
const options = { model: execution.model, effort: execution.effort, readOnly: true, ghRepo: `${owner}/${repo}`, sink: captured.sink };
|
|
8975
|
+
await runClaude(prompt, options);
|
|
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.`;
|
|
8983
|
+
}
|
|
8984
|
+
function targetKey(target) {
|
|
8985
|
+
return `${target.kind}#${String(target.number)}`;
|
|
8986
|
+
}
|
|
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";
|
|
8751
9018
|
}
|
|
8752
|
-
async function conductItem(id, config, login2, participants, execution) {
|
|
8753
|
-
const
|
|
9019
|
+
async function conductItem(id, config, login2, participants, execution, dryRun, linkMap) {
|
|
9020
|
+
const label2 = `#${String(id)}`;
|
|
8754
9021
|
let item;
|
|
8755
9022
|
try {
|
|
8756
|
-
item = readWatchedItem(id);
|
|
9023
|
+
item = readWatchedItem(id, dryRun?.linkMap ?? linkMap);
|
|
8757
9024
|
} catch (err) {
|
|
8758
|
-
process.stderr.write(`Warning: could not read ${
|
|
9025
|
+
process.stderr.write(`Warning: could not read ${label2}, skipping it this tick: ${err.message}
|
|
8759
9026
|
`);
|
|
8760
9027
|
return "idle";
|
|
8761
9028
|
}
|
|
8762
9029
|
const { thread, conversations } = item;
|
|
8763
|
-
const
|
|
8764
|
-
const
|
|
9030
|
+
const loop = loopStateOf(conversations, item.labels, resolveMaxReplies(config.conductor));
|
|
9031
|
+
const verdict = decideConductorReply(conversations, participants, loop);
|
|
9032
|
+
const owed = verdict.decisions.find(
|
|
9033
|
+
(decision) => decision.kind === "reply" && dryRun?.answered.has(targetKey(decision.surface)) !== true
|
|
9034
|
+
);
|
|
8765
9035
|
if (owed === void 0) {
|
|
8766
|
-
|
|
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}
|
|
8767
9039
|
`);
|
|
8768
9040
|
return "idle";
|
|
8769
9041
|
}
|
|
8770
9042
|
const replyTo = { kind: owed.surface.kind, number: owed.surface.number };
|
|
8771
9043
|
const frame = thread.kind === "issue" ? config.conductor?.prompts?.issue ?? DEFAULT_CONDUCTOR_ISSUE_PROMPT : config.conductor?.prompts?.pr ?? DEFAULT_CONDUCTOR_PR_PROMPT;
|
|
8772
|
-
const prompt = composeConductorPrompt({ thread, repo: getRepoSlug(), participants, frame, replyTo });
|
|
9044
|
+
const prompt = composeConductorPrompt({ thread, repo: getRepoSlug(), participants, frame, replyTo, dryRun: dryRun !== void 0 });
|
|
8773
9045
|
const where = `${replyTo.kind === "pr" ? "pull request" : "issue"} #${String(replyTo.number)}`;
|
|
8774
|
-
process.stdout.write(`Conductor: ${
|
|
9046
|
+
process.stdout.write(`Conductor: ${label2} needs a reply on ${where}; running ${describeConductorExecution(execution)}.
|
|
8775
9047
|
`);
|
|
9048
|
+
if (dryRun) return conductDryRunItem(prompt, execution, label2, where, replyTo, dryRun);
|
|
9049
|
+
const captured = captureStdout();
|
|
8776
9050
|
const outcome = await conductReply({
|
|
8777
9051
|
login: login2,
|
|
8778
9052
|
read: () => readTargetMessages(replyTo),
|
|
8779
|
-
run: () => runModel(prompt, execution, replyTo)
|
|
9053
|
+
run: () => runModel(prompt, execution, replyTo, captured.sink)
|
|
8780
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
|
+
}
|
|
8781
9059
|
switch (outcome.kind) {
|
|
8782
9060
|
case "posted":
|
|
8783
9061
|
process.stdout.write(`Conductor: posted a reply on ${where}.
|
|
@@ -8788,15 +9066,15 @@ async function conductItem(id, config, login2, participants, execution) {
|
|
|
8788
9066
|
}
|
|
8789
9067
|
return "replied";
|
|
8790
9068
|
case "posted-nothing":
|
|
8791
|
-
process.stderr.write(`Error: the run for ${
|
|
9069
|
+
process.stderr.write(`Error: the run for ${label2} finished but posted no comment on ${where}.
|
|
8792
9070
|
`);
|
|
8793
9071
|
return "failed";
|
|
8794
9072
|
case "run-failed":
|
|
8795
|
-
process.stderr.write(`Error: the run for ${
|
|
9073
|
+
process.stderr.write(`Error: the run for ${label2} failed and posted no comment on ${where}: ${outcome.error}
|
|
8796
9074
|
`);
|
|
8797
9075
|
return "failed";
|
|
8798
9076
|
case "unverified":
|
|
8799
|
-
process.stderr.write(`Error: could not tell whether ${
|
|
9077
|
+
process.stderr.write(`Error: could not tell whether ${label2} got a reply: ${outcome.error}
|
|
8800
9078
|
`);
|
|
8801
9079
|
return "failed";
|
|
8802
9080
|
}
|
|
@@ -8910,6 +9188,8 @@ function preflight() {
|
|
|
8910
9188
|
}
|
|
8911
9189
|
const executionProblem = conductorExecutionProblem(config.conductor);
|
|
8912
9190
|
if (executionProblem !== null) return { problem: executionProblem };
|
|
9191
|
+
const maxReplies = maxRepliesProblem(config.conductor);
|
|
9192
|
+
if (maxReplies !== null) return { problem: maxReplies };
|
|
8913
9193
|
if (config.conductor?.executor === "codex") {
|
|
8914
9194
|
return { problem: "conductor.executor codex is not supported: Codex cannot be limited to comments on the reply target. Use claude." };
|
|
8915
9195
|
}
|
|
@@ -8920,35 +9200,133 @@ function preflight() {
|
|
|
8920
9200
|
execution: resolveConductorExecution(config.conductor)
|
|
8921
9201
|
};
|
|
8922
9202
|
}
|
|
8923
|
-
|
|
9203
|
+
function staleMinutesOf(config) {
|
|
9204
|
+
return config.doWork?.lockStaleMinutes ?? DEFAULT_DO_WORK.lockStaleMinutes;
|
|
9205
|
+
}
|
|
9206
|
+
function checkWatched(id, participants, maxReplies, linkMap, coveredBy) {
|
|
9207
|
+
try {
|
|
9208
|
+
const target = getWatchTarget(id);
|
|
9209
|
+
if (isWatchClosed(target.state)) return { id, kind: target.kind, state: "closed", decisions: [] };
|
|
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 };
|
|
9214
|
+
} catch (err) {
|
|
9215
|
+
return { id, unavailable: err.message };
|
|
9216
|
+
}
|
|
9217
|
+
}
|
|
9218
|
+
function runConductorCheck() {
|
|
8924
9219
|
const checked = preflight();
|
|
8925
|
-
|
|
9220
|
+
const generatedAt = /* @__PURE__ */ new Date();
|
|
9221
|
+
let repo = null;
|
|
9222
|
+
try {
|
|
9223
|
+
const slug = getRepoSlug();
|
|
9224
|
+
repo = `${slug.owner}/${slug.repo}`;
|
|
9225
|
+
} catch {
|
|
9226
|
+
}
|
|
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);
|
|
9230
|
+
const report = "problem" in checked ? buildConductorCheck({
|
|
9231
|
+
generatedAt,
|
|
9232
|
+
repo,
|
|
9233
|
+
configProblem: checked.problem,
|
|
9234
|
+
identity: "",
|
|
9235
|
+
execution: "",
|
|
9236
|
+
lock: { kind: "free" },
|
|
9237
|
+
staleMinutes: DEFAULT_DO_WORK.lockStaleMinutes,
|
|
9238
|
+
watched: []
|
|
9239
|
+
}) : buildConductorCheck({
|
|
9240
|
+
generatedAt,
|
|
9241
|
+
repo,
|
|
9242
|
+
configProblem: null,
|
|
9243
|
+
identity: checked.login,
|
|
9244
|
+
execution: describeConductorExecution(checked.execution),
|
|
9245
|
+
lock: inspectConductorLock(staleMinutesOf(checked.config)),
|
|
9246
|
+
staleMinutes: staleMinutesOf(checked.config),
|
|
9247
|
+
watched: watchIds.map((id) => checkWatched(id, checked.participants, resolveMaxReplies(checked.config.conductor), linkMap, covered.get(id)))
|
|
9248
|
+
});
|
|
9249
|
+
process.stdout.write(report.text);
|
|
9250
|
+
return report.exitCode;
|
|
9251
|
+
}
|
|
9252
|
+
async function runConductorDryRun(checked) {
|
|
8926
9253
|
const { config, login: login2, participants, execution } = checked;
|
|
8927
|
-
const
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
9254
|
+
const lock = inspectConductorLock(staleMinutesOf(config));
|
|
9255
|
+
if (lock.kind === "held" || lock.kind === "suspect") {
|
|
9256
|
+
process.stderr.write(`Warning: a conductor is running here (pid ${String(lock.owner.pid)}); the dry run does not take the lock.
|
|
9257
|
+
`);
|
|
9258
|
+
}
|
|
9259
|
+
process.stdout.write(`Conductor: dry run as ${login2}; nothing is posted and the watch list is not changed.
|
|
9260
|
+
`);
|
|
9261
|
+
let failed = 0;
|
|
9262
|
+
const context = { linkMap: onceLinkMap(), answered: /* @__PURE__ */ new Set() };
|
|
9263
|
+
const watchIds = normalizeWatch(config.conductor?.watch);
|
|
9264
|
+
const covered = coveredOf(watchIds, context.linkMap);
|
|
9265
|
+
for (const id of watchIds) {
|
|
9266
|
+
try {
|
|
9267
|
+
if (isWatchClosed(getWatchTarget(id).state)) {
|
|
9268
|
+
process.stdout.write(`Conductor: #${String(id)} is closed; a tick would drop it from the watch list.
|
|
9269
|
+
`);
|
|
9270
|
+
continue;
|
|
9271
|
+
}
|
|
9272
|
+
} catch {
|
|
9273
|
+
}
|
|
9274
|
+
const coveredBy = covered.get(id);
|
|
9275
|
+
if (coveredBy !== void 0) {
|
|
9276
|
+
process.stdout.write(`${coveredNote(id, coveredBy)}
|
|
9277
|
+
`);
|
|
9278
|
+
continue;
|
|
9279
|
+
}
|
|
9280
|
+
try {
|
|
9281
|
+
if (await conductItem(id, config, login2, participants, execution, context) === "failed") failed++;
|
|
9282
|
+
} catch (err) {
|
|
9283
|
+
process.stderr.write(`Error: #${String(id)} could not be conducted: ${err.message}
|
|
9284
|
+
`);
|
|
9285
|
+
failed++;
|
|
9286
|
+
}
|
|
9287
|
+
}
|
|
9288
|
+
return failed > 0 ? 1 : 0;
|
|
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.
|
|
8933
9293
|
`
|
|
8934
|
-
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
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.
|
|
8938
9298
|
`
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
|
|
8942
|
-
|
|
9299
|
+
);
|
|
9300
|
+
return 2;
|
|
9301
|
+
}
|
|
9302
|
+
async function runConductor(options = {}) {
|
|
9303
|
+
if (options.check === true && options.dryRun === true) {
|
|
9304
|
+
return fail2("--check and --dry-run cannot be used together.");
|
|
8943
9305
|
}
|
|
9306
|
+
if (options.check === true) return runConductorCheck();
|
|
9307
|
+
const checked = preflight();
|
|
9308
|
+
if ("problem" in checked) return fail2(checked.problem);
|
|
9309
|
+
if (options.dryRun === true) return runConductorDryRun(checked);
|
|
9310
|
+
const { config, login: login2, participants, execution } = checked;
|
|
9311
|
+
const staleMinutes = config.doWork?.lockStaleMinutes ?? DEFAULT_DO_WORK.lockStaleMinutes;
|
|
9312
|
+
const lock = acquireConductorLock(staleMinutes);
|
|
9313
|
+
if (!lock.ok) return reportHeldLock(lock.heldBy, lock.suspect, staleMinutes);
|
|
8944
9314
|
try {
|
|
8945
9315
|
process.stdout.write(`Conductor: running as ${login2}.
|
|
8946
9316
|
`);
|
|
8947
9317
|
const watch = pruneWatchList(config);
|
|
8948
9318
|
let failed = 0;
|
|
9319
|
+
const linkMap = onceLinkMap();
|
|
9320
|
+
const covered = coveredOf(watch, linkMap);
|
|
8949
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
|
+
}
|
|
8950
9328
|
try {
|
|
8951
|
-
if (await conductItem(id, config, login2, participants, execution) === "failed") failed++;
|
|
9329
|
+
if (await conductItem(id, config, login2, participants, execution, void 0, linkMap) === "failed") failed++;
|
|
8952
9330
|
} catch (err) {
|
|
8953
9331
|
process.stderr.write(`Error: #${String(id)} could not be conducted: ${err.message}
|
|
8954
9332
|
`);
|
|
@@ -8962,8 +9340,8 @@ async function runConductor() {
|
|
|
8962
9340
|
}
|
|
8963
9341
|
var conductorCommand = new Command7("conductor").description(
|
|
8964
9342
|
"Run one conductor tick as an allowed (human) account: verify the identity, take the conductor's own run lock, then act"
|
|
8965
|
-
).action(async () => {
|
|
8966
|
-
exitWith(await runConductor());
|
|
9343
|
+
).option("--check", "Report what a tick would do, including pending prunes, and change nothing; exit 1 on a problem").option("--dry-run", "Run the model for each owed reply and print it instead of posting; no lock, no prune").action(async (options) => {
|
|
9344
|
+
exitWith(await runConductor(options));
|
|
8967
9345
|
});
|
|
8968
9346
|
conductorCommand.command("add <id>").description("Watch an issue or pull request and make do-work pick it up (an issue also follows its linked PR)").action((id) => {
|
|
8969
9347
|
exitWith(runWatchAdd(id));
|