automata-cli 0.11.0-develop.514 → 0.11.0-develop.519
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 +295 -63
- 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
|
}
|
|
@@ -4489,6 +4489,9 @@ var UNKNOWN_OWNER = {
|
|
|
4489
4489
|
command: "unknown",
|
|
4490
4490
|
token: ""
|
|
4491
4491
|
};
|
|
4492
|
+
function inspectConductorLock(staleMinutes, now = Date.now()) {
|
|
4493
|
+
return inspectRunLock(staleMinutes, now, CONDUCTOR_LOCK_FILE);
|
|
4494
|
+
}
|
|
4492
4495
|
function acquireConductorLock(staleMinutes) {
|
|
4493
4496
|
return acquireRunLock("conductor", staleMinutes, CONDUCTOR_LOCK_FILE);
|
|
4494
4497
|
}
|
|
@@ -4599,8 +4602,8 @@ function heldForMs(owner, now) {
|
|
|
4599
4602
|
const startedAt = Date.parse(owner.startedAt);
|
|
4600
4603
|
return Number.isNaN(startedAt) ? null : now - startedAt;
|
|
4601
4604
|
}
|
|
4602
|
-
function inspectRunLock(staleMinutes, now = Date.now()) {
|
|
4603
|
-
const path = lockPath();
|
|
4605
|
+
function inspectRunLock(staleMinutes, now = Date.now(), lockFile = LOCK_FILE) {
|
|
4606
|
+
const path = lockPath(lockFile);
|
|
4604
4607
|
try {
|
|
4605
4608
|
statSync2(path);
|
|
4606
4609
|
} catch (err) {
|
|
@@ -4785,9 +4788,9 @@ function detectRescueTarget(options, now) {
|
|
|
4785
4788
|
return { branch: current, createdBranch: false, source: current };
|
|
4786
4789
|
}
|
|
4787
4790
|
const source = detached ? "detached HEAD" : current === options.baseBranch ? options.baseBranch : current;
|
|
4788
|
-
const
|
|
4791
|
+
const label2 = detached ? "detached" : flattenBranchName(source);
|
|
4789
4792
|
return {
|
|
4790
|
-
branch: `${RESCUE_BRANCH_PREFIX}${
|
|
4793
|
+
branch: `${RESCUE_BRANCH_PREFIX}${label2}-${utcStamp(now)}`,
|
|
4791
4794
|
createdBranch: true,
|
|
4792
4795
|
source
|
|
4793
4796
|
};
|
|
@@ -5203,9 +5206,9 @@ var RunTranscript = class {
|
|
|
5203
5206
|
stderrAtLineStart = true;
|
|
5204
5207
|
/** A private key began in an earlier line and has not ended yet. */
|
|
5205
5208
|
inPrivateKey = false;
|
|
5206
|
-
constructor(
|
|
5209
|
+
constructor(label2, root = process.cwd(), now = /* @__PURE__ */ new Date()) {
|
|
5207
5210
|
const stamp = now.toISOString().replace(/[:.]/g, "-");
|
|
5208
|
-
this.fileName = `${stamp}-${
|
|
5211
|
+
this.fileName = `${stamp}-${label2}.log`;
|
|
5209
5212
|
const dir = join5(root, RUN_TRANSCRIPT_DIR);
|
|
5210
5213
|
this.path = join5(dir, this.fileName);
|
|
5211
5214
|
try {
|
|
@@ -6472,12 +6475,12 @@ function gitSection(status) {
|
|
|
6472
6475
|
return build("git", lines, problems, { ...status });
|
|
6473
6476
|
}
|
|
6474
6477
|
function assembleReport(input) {
|
|
6475
|
-
const byId = new Map(input.sections.map((
|
|
6478
|
+
const byId = new Map(input.sections.map((section2) => [section2.id, section2]));
|
|
6476
6479
|
const ordered = SECTION_ORDER.flatMap((id) => {
|
|
6477
|
-
const
|
|
6478
|
-
return
|
|
6480
|
+
const section2 = byId.get(id);
|
|
6481
|
+
return section2 === void 0 ? [] : [section2];
|
|
6479
6482
|
});
|
|
6480
|
-
const problems = ordered.flatMap((
|
|
6483
|
+
const problems = ordered.flatMap((section2) => section2.problems);
|
|
6481
6484
|
return {
|
|
6482
6485
|
generatedAt: input.generatedAt,
|
|
6483
6486
|
repo: input.repo,
|
|
@@ -6511,8 +6514,8 @@ function renderProblems(problems) {
|
|
|
6511
6514
|
function renderText(report) {
|
|
6512
6515
|
const head = `automata do-work --check \u2014 ${report.repo ?? "unknown repository"} \u2014 ${report.generatedAt.toISOString()}`;
|
|
6513
6516
|
const parts = report.blocked === null ? [head, ""] : [`blocked: ${report.blocked.trigger}`, head, ""];
|
|
6514
|
-
for (const
|
|
6515
|
-
parts.push(...renderBlock(
|
|
6517
|
+
for (const section2 of report.sections) {
|
|
6518
|
+
parts.push(...renderBlock(section2.title, section2.lines, "(nothing to report)"));
|
|
6516
6519
|
}
|
|
6517
6520
|
if (report.trace !== null) parts.push(...renderTrace(report.trace));
|
|
6518
6521
|
if (report.problems.length > 0) parts.push(...renderProblems(report.problems));
|
|
@@ -6523,12 +6526,12 @@ function renderText(report) {
|
|
|
6523
6526
|
}
|
|
6524
6527
|
function toJson(report) {
|
|
6525
6528
|
const sections = {};
|
|
6526
|
-
for (const
|
|
6527
|
-
sections[
|
|
6528
|
-
title:
|
|
6529
|
-
lines:
|
|
6530
|
-
problems:
|
|
6531
|
-
data:
|
|
6529
|
+
for (const section2 of report.sections) {
|
|
6530
|
+
sections[section2.id] = {
|
|
6531
|
+
title: section2.title,
|
|
6532
|
+
lines: section2.lines,
|
|
6533
|
+
problems: section2.problems,
|
|
6534
|
+
data: section2.data
|
|
6532
6535
|
};
|
|
6533
6536
|
}
|
|
6534
6537
|
return {
|
|
@@ -6587,14 +6590,14 @@ var SettingsError = class extends Error {
|
|
|
6587
6590
|
function failSettings(message) {
|
|
6588
6591
|
throw new SettingsError(message);
|
|
6589
6592
|
}
|
|
6590
|
-
function parsePositiveInt(value,
|
|
6593
|
+
function parsePositiveInt(value, label2) {
|
|
6591
6594
|
const trimmed2 = value.trim();
|
|
6592
6595
|
if (!/^\d+$/.test(trimmed2)) {
|
|
6593
|
-
failSettings(`${
|
|
6596
|
+
failSettings(`${label2} must be a positive integer (got "${value}").`);
|
|
6594
6597
|
}
|
|
6595
6598
|
const parsed = Number(trimmed2);
|
|
6596
6599
|
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
6597
|
-
failSettings(`${
|
|
6600
|
+
failSettings(`${label2} must be a positive integer within the safe range (got "${value}").`);
|
|
6598
6601
|
}
|
|
6599
6602
|
return parsed;
|
|
6600
6603
|
}
|
|
@@ -6749,14 +6752,14 @@ function planRun(item, settings, execution) {
|
|
|
6749
6752
|
}
|
|
6750
6753
|
function describeAssignment(item, agentUser) {
|
|
6751
6754
|
return claimStates(item).map((claim) => {
|
|
6752
|
-
const
|
|
6755
|
+
const label2 = claim.surface === "issue" ? "issue" : `pull request #${String(claim.number)}`;
|
|
6753
6756
|
switch (claim.state) {
|
|
6754
6757
|
case "would-claim":
|
|
6755
|
-
return `would assign ${
|
|
6758
|
+
return `would assign ${label2} to ${agentUser}`;
|
|
6756
6759
|
case "already-assigned":
|
|
6757
|
-
return `${
|
|
6760
|
+
return `${label2} already assigned`;
|
|
6758
6761
|
case "rule-exempt":
|
|
6759
|
-
return `${
|
|
6762
|
+
return `${label2} not claimed (orphan pass)`;
|
|
6760
6763
|
}
|
|
6761
6764
|
}).join(" \xB7 ");
|
|
6762
6765
|
}
|
|
@@ -6832,24 +6835,24 @@ function validateOptionalInt(container, key2, path, min, hint) {
|
|
|
6832
6835
|
failSettings(`${path} must be ${hint}, got ${JSON.stringify(value)}.`);
|
|
6833
6836
|
}
|
|
6834
6837
|
}
|
|
6835
|
-
function validateDoWorkConfig(
|
|
6836
|
-
if (
|
|
6837
|
-
if (!isPlainObject(
|
|
6838
|
-
failSettings(`doWork must be an object, got ${JSON.stringify(
|
|
6838
|
+
function validateDoWorkConfig(section2) {
|
|
6839
|
+
if (section2 === void 0 || section2 === null) return;
|
|
6840
|
+
if (!isPlainObject(section2)) {
|
|
6841
|
+
failSettings(`doWork must be an object, got ${JSON.stringify(section2)}.`);
|
|
6839
6842
|
}
|
|
6840
|
-
const executor =
|
|
6843
|
+
const executor = section2["executor"];
|
|
6841
6844
|
if (executor !== void 0 && executor !== "claude" && executor !== "codex") {
|
|
6842
6845
|
failSettings(`doWork.executor must be 'claude' or 'codex', got ${JSON.stringify(executor)}.`);
|
|
6843
6846
|
}
|
|
6844
|
-
validateOptionalString(
|
|
6845
|
-
validateOptionalInt(
|
|
6846
|
-
validateOptionalInt(
|
|
6847
|
-
validateOptionalBoolean(
|
|
6848
|
-
validateOptionalBoolean(
|
|
6849
|
-
validateProtectedBranches(
|
|
6850
|
-
validateSettingContainer(
|
|
6851
|
-
validateSettingContainer(
|
|
6852
|
-
validateSettingContainer(
|
|
6847
|
+
validateOptionalString(section2, "baseBranch", "doWork.baseBranch");
|
|
6848
|
+
validateOptionalInt(section2, "maxRunsPerTick", "doWork.maxRunsPerTick", 0, "a non-negative integer (0 = unlimited)");
|
|
6849
|
+
validateOptionalInt(section2, "lockStaleMinutes", "doWork.lockStaleMinutes", 1, "a positive integer");
|
|
6850
|
+
validateOptionalBoolean(section2, "dumpOnBlock", "doWork.dumpOnBlock");
|
|
6851
|
+
validateOptionalBoolean(section2, "postRunLog", "doWork.postRunLog");
|
|
6852
|
+
validateProtectedBranches(section2["protectedBranches"]);
|
|
6853
|
+
validateSettingContainer(section2["models"], "models", ["claude", "codex"]);
|
|
6854
|
+
validateSettingContainer(section2["effort"], "effort", ["claude", "codex"]);
|
|
6855
|
+
validateSettingContainer(section2["prompts"], "prompts", ["issueDiscuss", "prWork", "prOrphan"]);
|
|
6853
6856
|
}
|
|
6854
6857
|
function validateOptionalBoolean(container, key2, path) {
|
|
6855
6858
|
const value = container[key2];
|
|
@@ -6912,7 +6915,7 @@ function matchesDiscoveryFilter(subject, settings) {
|
|
|
6912
6915
|
const value = settings.discoveryValue.toLowerCase();
|
|
6913
6916
|
switch (settings.technique) {
|
|
6914
6917
|
case "label":
|
|
6915
|
-
return subject.labels.some((
|
|
6918
|
+
return subject.labels.some((label2) => label2.toLowerCase() === value);
|
|
6916
6919
|
case "assignee":
|
|
6917
6920
|
return subject.assignees.some((assignee) => assignee.toLowerCase() === value);
|
|
6918
6921
|
case "title-contains":
|
|
@@ -8497,6 +8500,12 @@ function postingInstruction(target) {
|
|
|
8497
8500
|
"Post exactly one comment. Do not approve, merge or close anything."
|
|
8498
8501
|
].join("\n");
|
|
8499
8502
|
}
|
|
8503
|
+
function dryRunInstruction(target) {
|
|
8504
|
+
return [
|
|
8505
|
+
`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."
|
|
8507
|
+
].join("\n");
|
|
8508
|
+
}
|
|
8500
8509
|
function visible(messages, participants) {
|
|
8501
8510
|
return analyzeSurface(messages, participants).messages.map((message) => ({
|
|
8502
8511
|
kind: message.kind,
|
|
@@ -8576,7 +8585,7 @@ function composeConductorPrompt(input) {
|
|
|
8576
8585
|
"Only the messages above exist. Anything from other accounts has been withheld",
|
|
8577
8586
|
"deliberately \u2014 do not ask about it.",
|
|
8578
8587
|
"",
|
|
8579
|
-
postingInstruction(replyTo)
|
|
8588
|
+
input.dryRun === true ? dryRunInstruction(replyTo) : postingInstruction(replyTo)
|
|
8580
8589
|
);
|
|
8581
8590
|
return lines.join("\n");
|
|
8582
8591
|
}
|
|
@@ -8667,6 +8676,113 @@ async function conductReply(input) {
|
|
|
8667
8676
|
return runError === null ? { kind: "posted-nothing" } : { kind: "run-failed", error: runError };
|
|
8668
8677
|
}
|
|
8669
8678
|
|
|
8679
|
+
// src/conductor/checkReport.ts
|
|
8680
|
+
function section(title, lines, problems = []) {
|
|
8681
|
+
return { title, lines, problems };
|
|
8682
|
+
}
|
|
8683
|
+
function label(id, kind) {
|
|
8684
|
+
return `${kind === "pr" ? "PR" : "issue"} #${String(id)}`;
|
|
8685
|
+
}
|
|
8686
|
+
function lockSection2(status, staleMinutes) {
|
|
8687
|
+
const fix = "remove .automata/conductor.lock once you have confirmed that no conductor runs";
|
|
8688
|
+
switch (status.kind) {
|
|
8689
|
+
case "free":
|
|
8690
|
+
return section("Run lock", ["no conductor is running in this checkout"]);
|
|
8691
|
+
case "held": {
|
|
8692
|
+
const held = status.heldForMs === null ? "" : ` (${describeDuration(status.heldForMs)} so far)`;
|
|
8693
|
+
return section("Run lock", [
|
|
8694
|
+
`a conductor is running: pid ${String(status.owner.pid)} on ${status.owner.host}, started ${status.owner.startedAt}${held}`,
|
|
8695
|
+
"a tick started now would do nothing"
|
|
8696
|
+
]);
|
|
8697
|
+
}
|
|
8698
|
+
case "suspect":
|
|
8699
|
+
return section(
|
|
8700
|
+
"Run lock",
|
|
8701
|
+
[`held longer than ${String(staleMinutes)} minutes by pid ${String(status.owner.pid)} on ${status.owner.host}`],
|
|
8702
|
+
[{ section: "lock", summary: `the lock looks alive but outlived the stale window, probably a reused pid: ${fix}` }]
|
|
8703
|
+
);
|
|
8704
|
+
case "stale":
|
|
8705
|
+
return section("Run lock", ["a stale lock is present; the next tick takes it over"]);
|
|
8706
|
+
case "unreadable":
|
|
8707
|
+
return section(
|
|
8708
|
+
"Run lock",
|
|
8709
|
+
[`the lock cannot be read: ${status.detail}`],
|
|
8710
|
+
[{ section: "lock", summary: `the lock file cannot be read (${status.detail}); fix its permissions` }]
|
|
8711
|
+
);
|
|
8712
|
+
}
|
|
8713
|
+
}
|
|
8714
|
+
function decisionLine(decision) {
|
|
8715
|
+
const where = label(decision.surface.number, decision.surface.kind);
|
|
8716
|
+
return decision.kind === "reply" ? `reply on ${where}: ${decision.reason}` : `skip ${where} (${decision.reason}): ${decision.detail}`;
|
|
8717
|
+
}
|
|
8718
|
+
function watchSections(watched) {
|
|
8719
|
+
const listLines = [];
|
|
8720
|
+
const replyLines = [];
|
|
8721
|
+
const problems = [];
|
|
8722
|
+
for (const entry of watched) {
|
|
8723
|
+
if ("unavailable" in entry) {
|
|
8724
|
+
listLines.push(`#${String(entry.id)}: unavailable (${entry.unavailable}); a tick keeps it and skips it`);
|
|
8725
|
+
problems.push({ section: "watch", summary: `#${String(entry.id)} could not be read: ${entry.unavailable}` });
|
|
8726
|
+
continue;
|
|
8727
|
+
}
|
|
8728
|
+
const name = label(entry.id, entry.kind);
|
|
8729
|
+
if (entry.state === "closed") {
|
|
8730
|
+
listLines.push(`${name}: closed, a tick would drop it from the watch list`);
|
|
8731
|
+
continue;
|
|
8732
|
+
}
|
|
8733
|
+
listLines.push(`${name}: open`);
|
|
8734
|
+
for (const decision of entry.decisions) replyLines.push(decisionLine(decision));
|
|
8735
|
+
}
|
|
8736
|
+
return [
|
|
8737
|
+
section(`Watch list (${String(watched.length)})`, listLines, problems),
|
|
8738
|
+
section("Replies a tick would write", replyLines)
|
|
8739
|
+
];
|
|
8740
|
+
}
|
|
8741
|
+
function buildConductorCheck(input) {
|
|
8742
|
+
const sections = input.configProblem === null ? [
|
|
8743
|
+
section("Configuration", [`running as ${input.identity}`, `executor: ${input.execution}`]),
|
|
8744
|
+
lockSection2(input.lock, input.staleMinutes),
|
|
8745
|
+
...watchSections(input.watched)
|
|
8746
|
+
] : [
|
|
8747
|
+
section(
|
|
8748
|
+
"Configuration",
|
|
8749
|
+
[input.configProblem],
|
|
8750
|
+
[{ section: "configuration", summary: input.configProblem }]
|
|
8751
|
+
)
|
|
8752
|
+
];
|
|
8753
|
+
const problems = sections.flatMap((s) => s.problems);
|
|
8754
|
+
const parts = [
|
|
8755
|
+
`automata conductor --check \u2014 ${input.repo ?? "unknown repository"} \u2014 ${input.generatedAt.toISOString()}`,
|
|
8756
|
+
""
|
|
8757
|
+
];
|
|
8758
|
+
for (const s of sections) {
|
|
8759
|
+
parts.push(s.title, ...(s.lines.length === 0 ? ["(nothing to report)"] : s.lines).map((l) => ` ${l}`), "");
|
|
8760
|
+
}
|
|
8761
|
+
if (problems.length > 0) {
|
|
8762
|
+
parts.push(`Problems (${String(problems.length)})`, ...problems.map((p) => ` \xB7 ${p.section}: ${p.summary}`), "");
|
|
8763
|
+
}
|
|
8764
|
+
parts.push(problems.length === 0 ? "RESULT: healthy" : `RESULT: ${String(problems.length)} problem(s) found`);
|
|
8765
|
+
return { text: parts.join("\n") + "\n", exitCode: problems.length === 0 ? 0 : 1, problems };
|
|
8766
|
+
}
|
|
8767
|
+
|
|
8768
|
+
// src/conductor/dryRunReply.ts
|
|
8769
|
+
function claudeResult(raw) {
|
|
8770
|
+
let result = null;
|
|
8771
|
+
for (const line of raw.split("\n")) {
|
|
8772
|
+
if (!line.trim().startsWith("{")) continue;
|
|
8773
|
+
try {
|
|
8774
|
+
const event = JSON.parse(line);
|
|
8775
|
+
if (event["type"] === "result" && typeof event["result"] === "string") result = event["result"];
|
|
8776
|
+
} catch {
|
|
8777
|
+
}
|
|
8778
|
+
}
|
|
8779
|
+
return result;
|
|
8780
|
+
}
|
|
8781
|
+
function extractDryRunReply(executor, stdout) {
|
|
8782
|
+
const text = (executor === "claude" ? claudeResult(stdout) : stdout)?.trim();
|
|
8783
|
+
return text === void 0 || text.length === 0 ? null : text;
|
|
8784
|
+
}
|
|
8785
|
+
|
|
8670
8786
|
// src/conductor/watchList.ts
|
|
8671
8787
|
function parseWatchId(raw) {
|
|
8672
8788
|
const text = raw.trim().replace(/^#/, "");
|
|
@@ -8726,17 +8842,21 @@ function pruneWatchList(config) {
|
|
|
8726
8842
|
if (dropped.length > 0) updateWatch((current) => current.filter((id) => !dropped.includes(id)));
|
|
8727
8843
|
return watch.filter((id) => !dropped.includes(id));
|
|
8728
8844
|
}
|
|
8845
|
+
function onceLinkMap() {
|
|
8846
|
+
let map = null;
|
|
8847
|
+
return () => map ??= getOpenPrLinkMap();
|
|
8848
|
+
}
|
|
8729
8849
|
function readPr(number) {
|
|
8730
8850
|
return { surface: getPrSurface(number), checks: getPrChecks(number) };
|
|
8731
8851
|
}
|
|
8732
|
-
function readWatchedItem(id) {
|
|
8852
|
+
function readWatchedItem(id, linkMap = getOpenPrLinkMap) {
|
|
8733
8853
|
const target = getWatchTarget(id);
|
|
8734
8854
|
if (target.kind === "pr") {
|
|
8735
8855
|
const pr = readPr(id);
|
|
8736
8856
|
return { thread: { kind: "pr", prs: [pr] }, conversations: [prConversation(pr.surface)] };
|
|
8737
8857
|
}
|
|
8738
8858
|
const issue = getIssueSurface(id);
|
|
8739
|
-
const prs = (
|
|
8859
|
+
const prs = (linkMap().byIssue.get(id) ?? []).map((ref) => readPr(ref.number));
|
|
8740
8860
|
return {
|
|
8741
8861
|
thread: { kind: "issue", issue, prs },
|
|
8742
8862
|
conversations: [issueConversation(issue), ...prs.map((pr) => prConversation(pr.surface))]
|
|
@@ -8749,30 +8869,62 @@ function runModel(prompt, execution, replyTo) {
|
|
|
8749
8869
|
const { owner, repo } = getRepoSlug();
|
|
8750
8870
|
return runClaude(prompt, { model: execution.model, effort: execution.effort, readOnly: true, replyTo, ghRepo: `${owner}/${repo}` });
|
|
8751
8871
|
}
|
|
8752
|
-
async function
|
|
8753
|
-
const
|
|
8872
|
+
async function runModelForText(prompt, execution) {
|
|
8873
|
+
const { owner, repo } = getRepoSlug();
|
|
8874
|
+
let stdout = "";
|
|
8875
|
+
const sink2 = {
|
|
8876
|
+
output: (stream, text) => {
|
|
8877
|
+
if (stream === "stdout") stdout += text;
|
|
8878
|
+
},
|
|
8879
|
+
exited: () => void 0
|
|
8880
|
+
};
|
|
8881
|
+
const options = { model: execution.model, effort: execution.effort, readOnly: true, ghRepo: `${owner}/${repo}`, sink: sink2 };
|
|
8882
|
+
await runClaude(prompt, options);
|
|
8883
|
+
return extractDryRunReply(execution.executor, stdout);
|
|
8884
|
+
}
|
|
8885
|
+
function targetKey(target) {
|
|
8886
|
+
return `${target.kind}#${String(target.number)}`;
|
|
8887
|
+
}
|
|
8888
|
+
async function conductItem(id, config, login2, participants, execution, dryRun) {
|
|
8889
|
+
const label2 = `#${String(id)}`;
|
|
8754
8890
|
let item;
|
|
8755
8891
|
try {
|
|
8756
|
-
item = readWatchedItem(id);
|
|
8892
|
+
item = readWatchedItem(id, dryRun?.linkMap);
|
|
8757
8893
|
} catch (err) {
|
|
8758
|
-
process.stderr.write(`Warning: could not read ${
|
|
8894
|
+
process.stderr.write(`Warning: could not read ${label2}, skipping it this tick: ${err.message}
|
|
8759
8895
|
`);
|
|
8760
8896
|
return "idle";
|
|
8761
8897
|
}
|
|
8762
8898
|
const { thread, conversations } = item;
|
|
8763
8899
|
const verdict = decideConductorReply(conversations, participants);
|
|
8764
|
-
const owed = verdict.decisions.find(
|
|
8900
|
+
const owed = verdict.decisions.find(
|
|
8901
|
+
(decision) => decision.kind === "reply" && dryRun?.answered.has(targetKey(decision.surface)) !== true
|
|
8902
|
+
);
|
|
8765
8903
|
if (owed === void 0) {
|
|
8766
|
-
process.stdout.write(`Conductor: ${
|
|
8904
|
+
process.stdout.write(`Conductor: ${label2} needs no reply.
|
|
8767
8905
|
`);
|
|
8768
8906
|
return "idle";
|
|
8769
8907
|
}
|
|
8770
8908
|
const replyTo = { kind: owed.surface.kind, number: owed.surface.number };
|
|
8771
8909
|
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 });
|
|
8910
|
+
const prompt = composeConductorPrompt({ thread, repo: getRepoSlug(), participants, frame, replyTo, dryRun: dryRun !== void 0 });
|
|
8773
8911
|
const where = `${replyTo.kind === "pr" ? "pull request" : "issue"} #${String(replyTo.number)}`;
|
|
8774
|
-
process.stdout.write(`Conductor: ${
|
|
8912
|
+
process.stdout.write(`Conductor: ${label2} needs a reply on ${where}; running ${describeConductorExecution(execution)}.
|
|
8775
8913
|
`);
|
|
8914
|
+
if (dryRun) {
|
|
8915
|
+
const reply = await runModelForText(prompt, execution);
|
|
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
|
+
}
|
|
8776
8928
|
const outcome = await conductReply({
|
|
8777
8929
|
login: login2,
|
|
8778
8930
|
read: () => readTargetMessages(replyTo),
|
|
@@ -8788,15 +8940,15 @@ async function conductItem(id, config, login2, participants, execution) {
|
|
|
8788
8940
|
}
|
|
8789
8941
|
return "replied";
|
|
8790
8942
|
case "posted-nothing":
|
|
8791
|
-
process.stderr.write(`Error: the run for ${
|
|
8943
|
+
process.stderr.write(`Error: the run for ${label2} finished but posted no comment on ${where}.
|
|
8792
8944
|
`);
|
|
8793
8945
|
return "failed";
|
|
8794
8946
|
case "run-failed":
|
|
8795
|
-
process.stderr.write(`Error: the run for ${
|
|
8947
|
+
process.stderr.write(`Error: the run for ${label2} failed and posted no comment on ${where}: ${outcome.error}
|
|
8796
8948
|
`);
|
|
8797
8949
|
return "failed";
|
|
8798
8950
|
case "unverified":
|
|
8799
|
-
process.stderr.write(`Error: could not tell whether ${
|
|
8951
|
+
process.stderr.write(`Error: could not tell whether ${label2} got a reply: ${outcome.error}
|
|
8800
8952
|
`);
|
|
8801
8953
|
return "failed";
|
|
8802
8954
|
}
|
|
@@ -8920,9 +9072,89 @@ function preflight() {
|
|
|
8920
9072
|
execution: resolveConductorExecution(config.conductor)
|
|
8921
9073
|
};
|
|
8922
9074
|
}
|
|
8923
|
-
|
|
9075
|
+
function staleMinutesOf(config) {
|
|
9076
|
+
return config.doWork?.lockStaleMinutes ?? DEFAULT_DO_WORK.lockStaleMinutes;
|
|
9077
|
+
}
|
|
9078
|
+
function checkWatched(id, participants, linkMap) {
|
|
9079
|
+
try {
|
|
9080
|
+
const target = getWatchTarget(id);
|
|
9081
|
+
if (isWatchClosed(target.state)) return { id, kind: target.kind, state: "closed", decisions: [] };
|
|
9082
|
+
const { conversations } = readWatchedItem(id, linkMap);
|
|
9083
|
+
return { id, kind: target.kind, state: "open", decisions: decideConductorReply(conversations, participants).decisions };
|
|
9084
|
+
} catch (err) {
|
|
9085
|
+
return { id, unavailable: err.message };
|
|
9086
|
+
}
|
|
9087
|
+
}
|
|
9088
|
+
function runConductorCheck() {
|
|
9089
|
+
const checked = preflight();
|
|
9090
|
+
const generatedAt = /* @__PURE__ */ new Date();
|
|
9091
|
+
let repo = null;
|
|
9092
|
+
try {
|
|
9093
|
+
const slug = getRepoSlug();
|
|
9094
|
+
repo = `${slug.owner}/${slug.repo}`;
|
|
9095
|
+
} catch {
|
|
9096
|
+
}
|
|
9097
|
+
const linkMap = onceLinkMap();
|
|
9098
|
+
const report = "problem" in checked ? buildConductorCheck({
|
|
9099
|
+
generatedAt,
|
|
9100
|
+
repo,
|
|
9101
|
+
configProblem: checked.problem,
|
|
9102
|
+
identity: "",
|
|
9103
|
+
execution: "",
|
|
9104
|
+
lock: { kind: "free" },
|
|
9105
|
+
staleMinutes: DEFAULT_DO_WORK.lockStaleMinutes,
|
|
9106
|
+
watched: []
|
|
9107
|
+
}) : buildConductorCheck({
|
|
9108
|
+
generatedAt,
|
|
9109
|
+
repo,
|
|
9110
|
+
configProblem: null,
|
|
9111
|
+
identity: checked.login,
|
|
9112
|
+
execution: describeConductorExecution(checked.execution),
|
|
9113
|
+
lock: inspectConductorLock(staleMinutesOf(checked.config)),
|
|
9114
|
+
staleMinutes: staleMinutesOf(checked.config),
|
|
9115
|
+
watched: normalizeWatch(checked.config.conductor?.watch).map((id) => checkWatched(id, checked.participants, linkMap))
|
|
9116
|
+
});
|
|
9117
|
+
process.stdout.write(report.text);
|
|
9118
|
+
return report.exitCode;
|
|
9119
|
+
}
|
|
9120
|
+
async function runConductorDryRun(checked) {
|
|
9121
|
+
const { config, login: login2, participants, execution } = checked;
|
|
9122
|
+
const lock = inspectConductorLock(staleMinutesOf(config));
|
|
9123
|
+
if (lock.kind === "held" || lock.kind === "suspect") {
|
|
9124
|
+
process.stderr.write(`Warning: a conductor is running here (pid ${String(lock.owner.pid)}); the dry run does not take the lock.
|
|
9125
|
+
`);
|
|
9126
|
+
}
|
|
9127
|
+
process.stdout.write(`Conductor: dry run as ${login2}; nothing is posted and the watch list is not changed.
|
|
9128
|
+
`);
|
|
9129
|
+
let failed = 0;
|
|
9130
|
+
const context = { linkMap: onceLinkMap(), answered: /* @__PURE__ */ new Set() };
|
|
9131
|
+
for (const id of normalizeWatch(config.conductor?.watch)) {
|
|
9132
|
+
try {
|
|
9133
|
+
if (isWatchClosed(getWatchTarget(id).state)) {
|
|
9134
|
+
process.stdout.write(`Conductor: #${String(id)} is closed; a tick would drop it from the watch list.
|
|
9135
|
+
`);
|
|
9136
|
+
continue;
|
|
9137
|
+
}
|
|
9138
|
+
} catch {
|
|
9139
|
+
}
|
|
9140
|
+
try {
|
|
9141
|
+
if (await conductItem(id, config, login2, participants, execution, context) === "failed") failed++;
|
|
9142
|
+
} catch (err) {
|
|
9143
|
+
process.stderr.write(`Error: #${String(id)} could not be conducted: ${err.message}
|
|
9144
|
+
`);
|
|
9145
|
+
failed++;
|
|
9146
|
+
}
|
|
9147
|
+
}
|
|
9148
|
+
return failed > 0 ? 1 : 0;
|
|
9149
|
+
}
|
|
9150
|
+
async function runConductor(options = {}) {
|
|
9151
|
+
if (options.check === true && options.dryRun === true) {
|
|
9152
|
+
return fail2("--check and --dry-run cannot be used together.");
|
|
9153
|
+
}
|
|
9154
|
+
if (options.check === true) return runConductorCheck();
|
|
8924
9155
|
const checked = preflight();
|
|
8925
9156
|
if ("problem" in checked) return fail2(checked.problem);
|
|
9157
|
+
if (options.dryRun === true) return runConductorDryRun(checked);
|
|
8926
9158
|
const { config, login: login2, participants, execution } = checked;
|
|
8927
9159
|
const staleMinutes = config.doWork?.lockStaleMinutes ?? DEFAULT_DO_WORK.lockStaleMinutes;
|
|
8928
9160
|
const lock = acquireConductorLock(staleMinutes);
|
|
@@ -8962,8 +9194,8 @@ async function runConductor() {
|
|
|
8962
9194
|
}
|
|
8963
9195
|
var conductorCommand = new Command7("conductor").description(
|
|
8964
9196
|
"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());
|
|
9197
|
+
).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) => {
|
|
9198
|
+
exitWith(await runConductor(options));
|
|
8967
9199
|
});
|
|
8968
9200
|
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
9201
|
exitWith(runWatchAdd(id));
|