automata-cli 0.11.0-develop.501 → 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 +787 -98
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import {
|
|
3
3
|
DEFAULT_CHECK_ISSUE_PROMPT,
|
|
4
4
|
DEFAULT_CLAUDE_SYSTEM_PROMPT,
|
|
5
|
+
DEFAULT_CONDUCTOR_ISSUE_PROMPT,
|
|
6
|
+
DEFAULT_CONDUCTOR_PR_PROMPT,
|
|
5
7
|
DEFAULT_DO_WORK,
|
|
6
8
|
DEFAULT_DO_WORK_ISSUE_DISCUSS_PROMPT,
|
|
7
9
|
DEFAULT_DO_WORK_PR_ORPHAN_PROMPT,
|
|
@@ -88,11 +90,11 @@ function writeDoWork(patch) {
|
|
|
88
90
|
const current = readRawConfig();
|
|
89
91
|
writeConfig({ ...current, doWork: { ...current.doWork, ...patch } });
|
|
90
92
|
}
|
|
91
|
-
function parseNonNegativeInt(value,
|
|
93
|
+
function parseNonNegativeInt(value, label2) {
|
|
92
94
|
const trimmed2 = value.trim();
|
|
93
95
|
const parsed = Number.parseInt(trimmed2, 10);
|
|
94
96
|
if (Number.isNaN(parsed) || parsed < 0 || String(parsed) !== trimmed2 || !Number.isSafeInteger(parsed)) {
|
|
95
|
-
process.stderr.write(`Error: ${
|
|
97
|
+
process.stderr.write(`Error: ${label2} must be a non-negative integer (got "${value}").
|
|
96
98
|
`);
|
|
97
99
|
process.exit(1);
|
|
98
100
|
}
|
|
@@ -313,6 +315,45 @@ var configSetConductorPrompt = new Command("conductor-prompt").description("Set
|
|
|
313
315
|
process.stdout.write(`conductor ${kind} prompt set.
|
|
314
316
|
`);
|
|
315
317
|
});
|
|
318
|
+
function writeConductor(patch) {
|
|
319
|
+
const current = readRawConfig();
|
|
320
|
+
writeConfig({ ...current, conductor: { ...current.conductor, ...patch } });
|
|
321
|
+
}
|
|
322
|
+
function requireExecutor(value) {
|
|
323
|
+
if (!VALID_EXECUTORS.includes(value)) {
|
|
324
|
+
process.stderr.write(`Error: invalid executor "${value}". Must be one of: ${VALID_EXECUTORS.join(", ")}
|
|
325
|
+
`);
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
return value;
|
|
329
|
+
}
|
|
330
|
+
var configSetConductorExecutor = new Command("conductor-executor").description("Set the executor the conductor runs").argument("<value>", `Executor: ${VALID_EXECUTORS.join(", ")}`).action((value) => {
|
|
331
|
+
writeConductor({ executor: requireExecutor(value) });
|
|
332
|
+
process.stdout.write(`conductor executor set to: ${value}
|
|
333
|
+
`);
|
|
334
|
+
});
|
|
335
|
+
var configSetConductorModel = new Command("conductor-model").description("Set the model the conductor passes to one executor").argument("<executor>", `Executor: ${VALID_EXECUTORS.join(", ")}`).argument("<value>", "Model identifier").action((executor, value) => {
|
|
336
|
+
const which = requireExecutor(executor);
|
|
337
|
+
const model = value.trim();
|
|
338
|
+
if (model.length === 0) {
|
|
339
|
+
process.stderr.write("Error: conductor-model requires a non-empty model identifier.\n");
|
|
340
|
+
process.exit(1);
|
|
341
|
+
}
|
|
342
|
+
writeConductor({ models: { ...readRawConfig().conductor?.models, [which]: model } });
|
|
343
|
+
process.stdout.write(`conductor ${which} model set to: ${model}
|
|
344
|
+
`);
|
|
345
|
+
});
|
|
346
|
+
var configSetConductorEffort = new Command("conductor-effort").description("Set the reasoning effort the conductor passes to one executor").argument("<executor>", `Executor: ${VALID_EXECUTORS.join(", ")}`).argument("<value>", "Effort level, forwarded to the executor unchanged").action((executor, value) => {
|
|
347
|
+
const which = requireExecutor(executor);
|
|
348
|
+
const effort = value.trim();
|
|
349
|
+
if (effort.length === 0) {
|
|
350
|
+
process.stderr.write("Error: conductor-effort requires a non-empty effort level.\n");
|
|
351
|
+
process.exit(1);
|
|
352
|
+
}
|
|
353
|
+
writeConductor({ effort: { ...readRawConfig().conductor?.effort, [which]: effort } });
|
|
354
|
+
process.stdout.write(`conductor ${which} effort set to: ${effort}
|
|
355
|
+
`);
|
|
356
|
+
});
|
|
316
357
|
var configSetGitTrunkBranch = new Command("git-trunk-branch").description("Pin the branch `publish-release` releases to, instead of detecting it").argument("<value>", "Branch name (unset = detect it from the remote)").action((value) => {
|
|
317
358
|
const branch = value.trim();
|
|
318
359
|
if (branch.length === 0) {
|
|
@@ -336,7 +377,7 @@ var configSetGitReleaseFlow = new Command("git-release-flow").description("Pin t
|
|
|
336
377
|
process.stdout.write(`git release flow set to: ${flow}
|
|
337
378
|
`);
|
|
338
379
|
});
|
|
339
|
-
var configSet = new Command("set").description("Set a configuration value").addCommand(configSetType).addCommand(configSetIssueDiscoveryTechnique).addCommand(configSetIssueDiscoveryValue).addCommand(configSetClaudeSystemPrompt).addCommand(configSetAllowedUsers).addCommand(configSetAgentUser).addCommand(configSetDoWorkBaseBranch).addCommand(configSetDoWorkProtectedBranches).addCommand(configSetDoWorkExecutor).addCommand(configSetDoWorkModel).addCommand(configSetDoWorkEffort).addCommand(configSetDoWorkMaxRuns).addCommand(configSetDoWorkLockStaleMinutes).addCommand(configSetDoWorkDumpOnBlock).addCommand(configSetDoWorkPostRunLog).addCommand(configSetDoWorkPrompt).addCommand(configSetConductorPrompt).addCommand(configSetGitTrunkBranch).addCommand(configSetGitReleaseFlow);
|
|
380
|
+
var configSet = new Command("set").description("Set a configuration value").addCommand(configSetType).addCommand(configSetIssueDiscoveryTechnique).addCommand(configSetIssueDiscoveryValue).addCommand(configSetClaudeSystemPrompt).addCommand(configSetAllowedUsers).addCommand(configSetAgentUser).addCommand(configSetDoWorkBaseBranch).addCommand(configSetDoWorkProtectedBranches).addCommand(configSetDoWorkExecutor).addCommand(configSetDoWorkModel).addCommand(configSetDoWorkEffort).addCommand(configSetDoWorkMaxRuns).addCommand(configSetDoWorkLockStaleMinutes).addCommand(configSetDoWorkDumpOnBlock).addCommand(configSetDoWorkPostRunLog).addCommand(configSetDoWorkPrompt).addCommand(configSetConductorPrompt).addCommand(configSetConductorExecutor).addCommand(configSetConductorModel).addCommand(configSetConductorEffort).addCommand(configSetGitTrunkBranch).addCommand(configSetGitReleaseFlow);
|
|
340
381
|
var configCommand = new Command("config").description("Configure automata settings").addCommand(configSet).action(async () => {
|
|
341
382
|
const [{ render }, React, { ConfigWizard }] = await Promise.all([
|
|
342
383
|
import("ink"),
|
|
@@ -392,8 +433,8 @@ function extractIdentity(stdout) {
|
|
|
392
433
|
if (typeof id === "string" && id) return id;
|
|
393
434
|
if (id && typeof id === "object") {
|
|
394
435
|
const o = id;
|
|
395
|
-
for (const
|
|
396
|
-
if (typeof o[
|
|
436
|
+
for (const key2 of ["displayName", "uniqueName", "providerDisplayName", "id"]) {
|
|
437
|
+
if (typeof o[key2] === "string" && o[key2]) return o[key2];
|
|
397
438
|
}
|
|
398
439
|
}
|
|
399
440
|
} catch {
|
|
@@ -1664,7 +1705,7 @@ function formatSonarIssue(issue) {
|
|
|
1664
1705
|
const location = formatLocation(issue.path, issue.line);
|
|
1665
1706
|
if (location) lines.push(` Location: ${location}`);
|
|
1666
1707
|
if (issue.severity || issue.type) {
|
|
1667
|
-
const labels = [issue.severity, issue.type].filter((
|
|
1708
|
+
const labels = [issue.severity, issue.type].filter((label2) => Boolean(label2)).map(sanitizeText).join(" / ");
|
|
1668
1709
|
lines.push(` Classification: ${labels}`);
|
|
1669
1710
|
}
|
|
1670
1711
|
if (issue.rule) lines.push(` Rule: ${sanitizeText(issue.rule)}`);
|
|
@@ -1677,7 +1718,7 @@ function formatSonarHotspot(hotspot) {
|
|
|
1677
1718
|
if (location) lines.push(` Location: ${location}`);
|
|
1678
1719
|
if (hotspot.status) lines.push(` Status: ${sanitizeText(hotspot.status)}`);
|
|
1679
1720
|
if (hotspot.vulnerabilityProbability || hotspot.securityCategory) {
|
|
1680
|
-
const labels = [hotspot.vulnerabilityProbability, hotspot.securityCategory].filter((
|
|
1721
|
+
const labels = [hotspot.vulnerabilityProbability, hotspot.securityCategory].filter((label2) => Boolean(label2)).map(sanitizeText).join(" / ");
|
|
1681
1722
|
lines.push(` Classification: ${labels}`);
|
|
1682
1723
|
}
|
|
1683
1724
|
if (hotspot.rule) {
|
|
@@ -2327,7 +2368,7 @@ function getIssueSurface(issueNumber) {
|
|
|
2327
2368
|
issue: { number: raw.number, title: raw.title, body: raw.body, url: raw.url },
|
|
2328
2369
|
state: raw.state === "CLOSED" ? "CLOSED" : "OPEN",
|
|
2329
2370
|
assignees: (raw.assignees ?? []).map(login).filter((name) => name.length > 0),
|
|
2330
|
-
labels: (raw.labels ?? []).map((
|
|
2371
|
+
labels: (raw.labels ?? []).map((label2) => label2.name ?? "").filter((name) => name.length > 0),
|
|
2331
2372
|
messages
|
|
2332
2373
|
};
|
|
2333
2374
|
}
|
|
@@ -2386,7 +2427,7 @@ function toPullRequestRef(node) {
|
|
|
2386
2427
|
function toOrphanPr(node) {
|
|
2387
2428
|
return {
|
|
2388
2429
|
pr: toPullRequestRef(node),
|
|
2389
|
-
labels: (node.labels?.nodes ?? []).map((
|
|
2430
|
+
labels: (node.labels?.nodes ?? []).map((label2) => label2.name ?? "").filter((name) => name.length > 0),
|
|
2390
2431
|
assignees: (node.assignees?.nodes ?? []).map((assignee) => assignee.login ?? "").filter((login2) => login2.length > 0)
|
|
2391
2432
|
};
|
|
2392
2433
|
}
|
|
@@ -2544,6 +2585,19 @@ function getPrSurface(prNumber) {
|
|
|
2544
2585
|
description: { author: login(raw.author), body: raw.body }
|
|
2545
2586
|
};
|
|
2546
2587
|
}
|
|
2588
|
+
function getPrChecks(prNumber) {
|
|
2589
|
+
const raw = ghJson(
|
|
2590
|
+
["pr", "view", String(prNumber), "--json", "statusCheckRollup"],
|
|
2591
|
+
`read the checks of pull request #${String(prNumber)}`
|
|
2592
|
+
);
|
|
2593
|
+
return (raw.statusCheckRollup ?? []).map((check) => {
|
|
2594
|
+
const name = check.name ?? check.context ?? "unnamed check";
|
|
2595
|
+
if (check.status !== void 0) return { name, status: check.status, conclusion: check.conclusion ?? null };
|
|
2596
|
+
const state = check.state ?? "";
|
|
2597
|
+
const pending = state === "PENDING" || state === "EXPECTED";
|
|
2598
|
+
return { name, status: pending ? "PENDING" : "COMPLETED", conclusion: pending || state === "" ? null : state };
|
|
2599
|
+
});
|
|
2600
|
+
}
|
|
2547
2601
|
function assignIssueToAgent(issueNumber, agentUser) {
|
|
2548
2602
|
const { stderr, status } = run4("gh", [
|
|
2549
2603
|
"issue",
|
|
@@ -2821,9 +2875,47 @@ async function terminateTrackedChildren(timeoutMs = 1e4, killGraceMs = 5e3) {
|
|
|
2821
2875
|
}
|
|
2822
2876
|
|
|
2823
2877
|
// src/claude/claudeService.ts
|
|
2878
|
+
function claudeReadOnlyTools(replyTo) {
|
|
2879
|
+
const tools = [
|
|
2880
|
+
"Read(./**)",
|
|
2881
|
+
"Grep(./**)",
|
|
2882
|
+
"Glob(./**)",
|
|
2883
|
+
"Bash(gh issue view:*)",
|
|
2884
|
+
"Bash(gh pr view:*)",
|
|
2885
|
+
"Bash(gh pr diff:*)",
|
|
2886
|
+
"Bash(gh pr checks:*)",
|
|
2887
|
+
"Bash(git status:*)"
|
|
2888
|
+
];
|
|
2889
|
+
if (replyTo) {
|
|
2890
|
+
const command = replyTo.kind === "pr" ? "pr" : "issue";
|
|
2891
|
+
tools.push(`Bash(gh ${command} comment ${String(replyTo.number)} --body-file -:*)`);
|
|
2892
|
+
}
|
|
2893
|
+
return tools;
|
|
2894
|
+
}
|
|
2895
|
+
var CLAUDE_READ_ONLY_DENIED = [
|
|
2896
|
+
"Edit",
|
|
2897
|
+
"Write",
|
|
2898
|
+
"NotebookEdit",
|
|
2899
|
+
"Bash(gh * --repo*)",
|
|
2900
|
+
"Bash(gh * -R*)"
|
|
2901
|
+
];
|
|
2902
|
+
function readOnlyEnv(ghRepo) {
|
|
2903
|
+
return ghRepo ? { ...process.env, GH_REPO: ghRepo } : process.env;
|
|
2904
|
+
}
|
|
2824
2905
|
function buildClaudeArgs(prompt, options = {}) {
|
|
2825
2906
|
const args = [];
|
|
2826
|
-
if (options.
|
|
2907
|
+
if (options.readOnly) {
|
|
2908
|
+
args.push(
|
|
2909
|
+
"--permission-mode",
|
|
2910
|
+
"dontAsk",
|
|
2911
|
+
"--allowed-tools",
|
|
2912
|
+
claudeReadOnlyTools(options.replyTo).join(","),
|
|
2913
|
+
"--disallowed-tools",
|
|
2914
|
+
CLAUDE_READ_ONLY_DENIED.join(",")
|
|
2915
|
+
);
|
|
2916
|
+
} else if (options.yolo) {
|
|
2917
|
+
args.push("--dangerously-skip-permissions");
|
|
2918
|
+
}
|
|
2827
2919
|
if (options.model) args.push("--model", options.model);
|
|
2828
2920
|
if (options.effort) args.push("--effort", options.effort);
|
|
2829
2921
|
if (options.verbose) args.push("--verbose", "--output-format", "stream-json");
|
|
@@ -2835,12 +2927,17 @@ function runClaude(prompt, options = {}) {
|
|
|
2835
2927
|
const claudeBin = resolveCommand("claude");
|
|
2836
2928
|
const args = buildClaudeArgs(prompt, {
|
|
2837
2929
|
yolo: true,
|
|
2930
|
+
readOnly: options.readOnly,
|
|
2931
|
+
replyTo: options.replyTo,
|
|
2838
2932
|
model: options.model,
|
|
2839
2933
|
effort: options.effort,
|
|
2840
2934
|
verbose: true
|
|
2841
2935
|
});
|
|
2842
2936
|
const sink2 = options.sink;
|
|
2843
|
-
const child = spawn(claudeBin, args, {
|
|
2937
|
+
const child = spawn(claudeBin, args, {
|
|
2938
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
2939
|
+
env: options.readOnly ? readOnlyEnv(options.ghRepo) : process.env
|
|
2940
|
+
});
|
|
2844
2941
|
trackChild(child);
|
|
2845
2942
|
child.stderr.on("data", (chunk) => {
|
|
2846
2943
|
process.stderr.write(chunk);
|
|
@@ -2882,21 +2979,21 @@ function runClaude(prompt, options = {}) {
|
|
|
2882
2979
|
}
|
|
2883
2980
|
function invokeClaudeCode(prompt, options = {}) {
|
|
2884
2981
|
if (options.verbose) {
|
|
2885
|
-
return invokeClaudeCodeVerbose(prompt, options
|
|
2982
|
+
return invokeClaudeCodeVerbose(prompt, options);
|
|
2886
2983
|
}
|
|
2887
|
-
invokeClaudeCodeSync(prompt, options
|
|
2984
|
+
invokeClaudeCodeSync(prompt, options);
|
|
2888
2985
|
}
|
|
2889
|
-
function invokeClaudeCodeSync(prompt,
|
|
2986
|
+
function invokeClaudeCodeSync(prompt, options) {
|
|
2890
2987
|
const claudeBin = resolveCommand("claude");
|
|
2891
|
-
const args = buildClaudeArgs(prompt, {
|
|
2988
|
+
const args = buildClaudeArgs(prompt, { ...options, verbose: false });
|
|
2892
2989
|
const result = spawnSync6(claudeBin, args, { encoding: "utf8", stdio: "inherit" });
|
|
2893
2990
|
handleSpawnError(result.error, "claude");
|
|
2894
2991
|
handleExitCode(result.status, "Claude Code");
|
|
2895
2992
|
}
|
|
2896
|
-
function invokeClaudeCodeVerbose(prompt,
|
|
2993
|
+
function invokeClaudeCodeVerbose(prompt, options) {
|
|
2897
2994
|
return new Promise((resolve2) => {
|
|
2898
2995
|
const claudeBin = resolveCommand("claude");
|
|
2899
|
-
const args = buildClaudeArgs(prompt, {
|
|
2996
|
+
const args = buildClaudeArgs(prompt, { ...options, verbose: true });
|
|
2900
2997
|
const child = spawn(claudeBin, args, { stdio: ["inherit", "pipe", "inherit"] });
|
|
2901
2998
|
const rl = createInterface({ input: child.stdout });
|
|
2902
2999
|
let turnCount = 0;
|
|
@@ -3001,7 +3098,8 @@ function toTomlBasicString(value) {
|
|
|
3001
3098
|
}
|
|
3002
3099
|
function buildCodexArgs(prompt, options = {}) {
|
|
3003
3100
|
const args = ["exec"];
|
|
3004
|
-
if (options.
|
|
3101
|
+
if (options.readOnly) args.push("--sandbox", "read-only");
|
|
3102
|
+
else if (options.yolo) args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
3005
3103
|
if (options.model) args.push("--model", options.model);
|
|
3006
3104
|
if (options.effort) args.push("-c", `model_reasoning_effort=${toTomlBasicString(options.effort)}`);
|
|
3007
3105
|
args.push(prompt);
|
|
@@ -3011,11 +3109,11 @@ function invokeCodexCode(prompt, options = {}) {
|
|
|
3011
3109
|
if (options.verbose) {
|
|
3012
3110
|
process.stderr.write("Warning: --verbose is not supported for Codex and will be ignored.\n");
|
|
3013
3111
|
}
|
|
3014
|
-
invokeCodexCodeSync(prompt, options
|
|
3112
|
+
invokeCodexCodeSync(prompt, options);
|
|
3015
3113
|
}
|
|
3016
|
-
function invokeCodexCodeSync(prompt,
|
|
3114
|
+
function invokeCodexCodeSync(prompt, options) {
|
|
3017
3115
|
const codexBin = resolveCommand("codex");
|
|
3018
|
-
const args = buildCodexArgs(prompt,
|
|
3116
|
+
const args = buildCodexArgs(prompt, options);
|
|
3019
3117
|
const result = spawnSync7(codexBin, args, { encoding: "utf8", stdio: "inherit" });
|
|
3020
3118
|
handleSpawnError(result.error, "codex");
|
|
3021
3119
|
handleExitCode(result.status, "Codex");
|
|
@@ -3023,12 +3121,20 @@ function invokeCodexCodeSync(prompt, yolo, model, effort) {
|
|
|
3023
3121
|
function runCodex(prompt, options = {}) {
|
|
3024
3122
|
return new Promise((resolve2, reject) => {
|
|
3025
3123
|
const codexBin = resolveCommand("codex");
|
|
3026
|
-
const args = buildCodexArgs(prompt, {
|
|
3124
|
+
const args = buildCodexArgs(prompt, {
|
|
3125
|
+
yolo: true,
|
|
3126
|
+
readOnly: options.readOnly,
|
|
3127
|
+
model: options.model,
|
|
3128
|
+
effort: options.effort
|
|
3129
|
+
});
|
|
3027
3130
|
const sink2 = options.sink;
|
|
3028
|
-
const child = spawn2(codexBin, args, {
|
|
3131
|
+
const child = spawn2(codexBin, args, {
|
|
3132
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
3133
|
+
env: options.readOnly ? readOnlyEnv(options.ghRepo) : process.env
|
|
3134
|
+
});
|
|
3029
3135
|
trackChild(child);
|
|
3030
3136
|
child.stdout.on("data", (chunk) => {
|
|
3031
|
-
process.stdout.write(chunk);
|
|
3137
|
+
if (options.quiet !== true) process.stdout.write(chunk);
|
|
3032
3138
|
sink2?.output("stdout", chunk.toString("utf8"));
|
|
3033
3139
|
});
|
|
3034
3140
|
child.stderr.on("data", (chunk) => {
|
|
@@ -3884,6 +3990,12 @@ function selectLinkedPr(linkedPrs) {
|
|
|
3884
3990
|
const open = linkedPrs.filter((pr) => pr.state === "OPEN");
|
|
3885
3991
|
return open.length === 0 ? null : newestPr(open);
|
|
3886
3992
|
}
|
|
3993
|
+
function agentAnsweredAfter(messages, agentUser, marker) {
|
|
3994
|
+
const agent = agentUser.toLowerCase();
|
|
3995
|
+
return messages.some(
|
|
3996
|
+
(message) => message.author.toLowerCase() === agent && message.kind !== "issue-body" && message.createdAt > marker.createdAt
|
|
3997
|
+
);
|
|
3998
|
+
}
|
|
3887
3999
|
function claimStates(item) {
|
|
3888
4000
|
const states = [];
|
|
3889
4001
|
if (item.issue !== null) {
|
|
@@ -4377,6 +4489,9 @@ var UNKNOWN_OWNER = {
|
|
|
4377
4489
|
command: "unknown",
|
|
4378
4490
|
token: ""
|
|
4379
4491
|
};
|
|
4492
|
+
function inspectConductorLock(staleMinutes, now = Date.now()) {
|
|
4493
|
+
return inspectRunLock(staleMinutes, now, CONDUCTOR_LOCK_FILE);
|
|
4494
|
+
}
|
|
4380
4495
|
function acquireConductorLock(staleMinutes) {
|
|
4381
4496
|
return acquireRunLock("conductor", staleMinutes, CONDUCTOR_LOCK_FILE);
|
|
4382
4497
|
}
|
|
@@ -4487,8 +4602,8 @@ function heldForMs(owner, now) {
|
|
|
4487
4602
|
const startedAt = Date.parse(owner.startedAt);
|
|
4488
4603
|
return Number.isNaN(startedAt) ? null : now - startedAt;
|
|
4489
4604
|
}
|
|
4490
|
-
function inspectRunLock(staleMinutes, now = Date.now()) {
|
|
4491
|
-
const path = lockPath();
|
|
4605
|
+
function inspectRunLock(staleMinutes, now = Date.now(), lockFile = LOCK_FILE) {
|
|
4606
|
+
const path = lockPath(lockFile);
|
|
4492
4607
|
try {
|
|
4493
4608
|
statSync2(path);
|
|
4494
4609
|
} catch (err) {
|
|
@@ -4673,9 +4788,9 @@ function detectRescueTarget(options, now) {
|
|
|
4673
4788
|
return { branch: current, createdBranch: false, source: current };
|
|
4674
4789
|
}
|
|
4675
4790
|
const source = detached ? "detached HEAD" : current === options.baseBranch ? options.baseBranch : current;
|
|
4676
|
-
const
|
|
4791
|
+
const label2 = detached ? "detached" : flattenBranchName(source);
|
|
4677
4792
|
return {
|
|
4678
|
-
branch: `${RESCUE_BRANCH_PREFIX}${
|
|
4793
|
+
branch: `${RESCUE_BRANCH_PREFIX}${label2}-${utcStamp(now)}`,
|
|
4679
4794
|
createdBranch: true,
|
|
4680
4795
|
source
|
|
4681
4796
|
};
|
|
@@ -5091,9 +5206,9 @@ var RunTranscript = class {
|
|
|
5091
5206
|
stderrAtLineStart = true;
|
|
5092
5207
|
/** A private key began in an earlier line and has not ended yet. */
|
|
5093
5208
|
inPrivateKey = false;
|
|
5094
|
-
constructor(
|
|
5209
|
+
constructor(label2, root = process.cwd(), now = /* @__PURE__ */ new Date()) {
|
|
5095
5210
|
const stamp = now.toISOString().replace(/[:.]/g, "-");
|
|
5096
|
-
this.fileName = `${stamp}-${
|
|
5211
|
+
this.fileName = `${stamp}-${label2}.log`;
|
|
5097
5212
|
const dir = join5(root, RUN_TRANSCRIPT_DIR);
|
|
5098
5213
|
this.path = join5(dir, this.fileName);
|
|
5099
5214
|
try {
|
|
@@ -5440,9 +5555,9 @@ function summarizeSync(items) {
|
|
|
5440
5555
|
const counts = /* @__PURE__ */ new Map();
|
|
5441
5556
|
for (const item of items) {
|
|
5442
5557
|
if (item.sync === void 0) continue;
|
|
5443
|
-
const
|
|
5444
|
-
if (
|
|
5445
|
-
counts.set(
|
|
5558
|
+
const key2 = oneLine(item.sync);
|
|
5559
|
+
if (key2.length === 0 || QUIET_SYNC.has(key2)) continue;
|
|
5560
|
+
counts.set(key2, (counts.get(key2) ?? 0) + 1);
|
|
5446
5561
|
}
|
|
5447
5562
|
if (counts.size === 0) return null;
|
|
5448
5563
|
return [...counts].map(([strategy, count]) => `${strategy}:${String(count)}`).join(",");
|
|
@@ -5691,9 +5806,9 @@ function parseExecution(bracket) {
|
|
|
5691
5806
|
if (bracket === void 0) return { executor: null, model: null, effort: null };
|
|
5692
5807
|
const parts = bracket.split(" ").filter((part) => part.length > 0);
|
|
5693
5808
|
const executor = parts[0] ?? null;
|
|
5694
|
-
const find = (
|
|
5695
|
-
const hit = parts.find((part) => part.startsWith(`${
|
|
5696
|
-
return hit === void 0 ? null : hit.slice(
|
|
5809
|
+
const find = (key2) => {
|
|
5810
|
+
const hit = parts.find((part) => part.startsWith(`${key2}=`));
|
|
5811
|
+
return hit === void 0 ? null : hit.slice(key2.length + 1);
|
|
5697
5812
|
};
|
|
5698
5813
|
return { executor, model: find("model"), effort: find("effort") };
|
|
5699
5814
|
}
|
|
@@ -6360,12 +6475,12 @@ function gitSection(status) {
|
|
|
6360
6475
|
return build("git", lines, problems, { ...status });
|
|
6361
6476
|
}
|
|
6362
6477
|
function assembleReport(input) {
|
|
6363
|
-
const byId = new Map(input.sections.map((
|
|
6478
|
+
const byId = new Map(input.sections.map((section2) => [section2.id, section2]));
|
|
6364
6479
|
const ordered = SECTION_ORDER.flatMap((id) => {
|
|
6365
|
-
const
|
|
6366
|
-
return
|
|
6480
|
+
const section2 = byId.get(id);
|
|
6481
|
+
return section2 === void 0 ? [] : [section2];
|
|
6367
6482
|
});
|
|
6368
|
-
const problems = ordered.flatMap((
|
|
6483
|
+
const problems = ordered.flatMap((section2) => section2.problems);
|
|
6369
6484
|
return {
|
|
6370
6485
|
generatedAt: input.generatedAt,
|
|
6371
6486
|
repo: input.repo,
|
|
@@ -6399,8 +6514,8 @@ function renderProblems(problems) {
|
|
|
6399
6514
|
function renderText(report) {
|
|
6400
6515
|
const head = `automata do-work --check \u2014 ${report.repo ?? "unknown repository"} \u2014 ${report.generatedAt.toISOString()}`;
|
|
6401
6516
|
const parts = report.blocked === null ? [head, ""] : [`blocked: ${report.blocked.trigger}`, head, ""];
|
|
6402
|
-
for (const
|
|
6403
|
-
parts.push(...renderBlock(
|
|
6517
|
+
for (const section2 of report.sections) {
|
|
6518
|
+
parts.push(...renderBlock(section2.title, section2.lines, "(nothing to report)"));
|
|
6404
6519
|
}
|
|
6405
6520
|
if (report.trace !== null) parts.push(...renderTrace(report.trace));
|
|
6406
6521
|
if (report.problems.length > 0) parts.push(...renderProblems(report.problems));
|
|
@@ -6411,12 +6526,12 @@ function renderText(report) {
|
|
|
6411
6526
|
}
|
|
6412
6527
|
function toJson(report) {
|
|
6413
6528
|
const sections = {};
|
|
6414
|
-
for (const
|
|
6415
|
-
sections[
|
|
6416
|
-
title:
|
|
6417
|
-
lines:
|
|
6418
|
-
problems:
|
|
6419
|
-
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
|
|
6420
6535
|
};
|
|
6421
6536
|
}
|
|
6422
6537
|
return {
|
|
@@ -6475,14 +6590,14 @@ var SettingsError = class extends Error {
|
|
|
6475
6590
|
function failSettings(message) {
|
|
6476
6591
|
throw new SettingsError(message);
|
|
6477
6592
|
}
|
|
6478
|
-
function parsePositiveInt(value,
|
|
6593
|
+
function parsePositiveInt(value, label2) {
|
|
6479
6594
|
const trimmed2 = value.trim();
|
|
6480
6595
|
if (!/^\d+$/.test(trimmed2)) {
|
|
6481
|
-
failSettings(`${
|
|
6596
|
+
failSettings(`${label2} must be a positive integer (got "${value}").`);
|
|
6482
6597
|
}
|
|
6483
6598
|
const parsed = Number(trimmed2);
|
|
6484
6599
|
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
6485
|
-
failSettings(`${
|
|
6600
|
+
failSettings(`${label2} must be a positive integer within the safe range (got "${value}").`);
|
|
6486
6601
|
}
|
|
6487
6602
|
return parsed;
|
|
6488
6603
|
}
|
|
@@ -6637,14 +6752,14 @@ function planRun(item, settings, execution) {
|
|
|
6637
6752
|
}
|
|
6638
6753
|
function describeAssignment(item, agentUser) {
|
|
6639
6754
|
return claimStates(item).map((claim) => {
|
|
6640
|
-
const
|
|
6755
|
+
const label2 = claim.surface === "issue" ? "issue" : `pull request #${String(claim.number)}`;
|
|
6641
6756
|
switch (claim.state) {
|
|
6642
6757
|
case "would-claim":
|
|
6643
|
-
return `would assign ${
|
|
6758
|
+
return `would assign ${label2} to ${agentUser}`;
|
|
6644
6759
|
case "already-assigned":
|
|
6645
|
-
return `${
|
|
6760
|
+
return `${label2} already assigned`;
|
|
6646
6761
|
case "rule-exempt":
|
|
6647
|
-
return `${
|
|
6762
|
+
return `${label2} not claimed (orphan pass)`;
|
|
6648
6763
|
}
|
|
6649
6764
|
}).join(" \xB7 ");
|
|
6650
6765
|
}
|
|
@@ -6706,41 +6821,41 @@ function describeRefusedRun(item, refusal) {
|
|
|
6706
6821
|
function isPlainObject(value) {
|
|
6707
6822
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6708
6823
|
}
|
|
6709
|
-
function validateOptionalString(container,
|
|
6710
|
-
const value = container[
|
|
6824
|
+
function validateOptionalString(container, key2, path) {
|
|
6825
|
+
const value = container[key2];
|
|
6711
6826
|
if (value === void 0 || value === null) return;
|
|
6712
6827
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
6713
6828
|
failSettings(`${path} must be a non-empty string.`);
|
|
6714
6829
|
}
|
|
6715
6830
|
}
|
|
6716
|
-
function validateOptionalInt(container,
|
|
6717
|
-
const value = container[
|
|
6831
|
+
function validateOptionalInt(container, key2, path, min, hint) {
|
|
6832
|
+
const value = container[key2];
|
|
6718
6833
|
if (value === void 0 || value === null) return;
|
|
6719
6834
|
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < min) {
|
|
6720
6835
|
failSettings(`${path} must be ${hint}, got ${JSON.stringify(value)}.`);
|
|
6721
6836
|
}
|
|
6722
6837
|
}
|
|
6723
|
-
function validateDoWorkConfig(
|
|
6724
|
-
if (
|
|
6725
|
-
if (!isPlainObject(
|
|
6726
|
-
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)}.`);
|
|
6727
6842
|
}
|
|
6728
|
-
const executor =
|
|
6843
|
+
const executor = section2["executor"];
|
|
6729
6844
|
if (executor !== void 0 && executor !== "claude" && executor !== "codex") {
|
|
6730
6845
|
failSettings(`doWork.executor must be 'claude' or 'codex', got ${JSON.stringify(executor)}.`);
|
|
6731
6846
|
}
|
|
6732
|
-
validateOptionalString(
|
|
6733
|
-
validateOptionalInt(
|
|
6734
|
-
validateOptionalInt(
|
|
6735
|
-
validateOptionalBoolean(
|
|
6736
|
-
validateOptionalBoolean(
|
|
6737
|
-
validateProtectedBranches(
|
|
6738
|
-
validateSettingContainer(
|
|
6739
|
-
validateSettingContainer(
|
|
6740
|
-
validateSettingContainer(
|
|
6741
|
-
}
|
|
6742
|
-
function validateOptionalBoolean(container,
|
|
6743
|
-
const value = container[
|
|
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"]);
|
|
6856
|
+
}
|
|
6857
|
+
function validateOptionalBoolean(container, key2, path) {
|
|
6858
|
+
const value = container[key2];
|
|
6744
6859
|
if (value === void 0 || value === null) return;
|
|
6745
6860
|
if (typeof value !== "boolean") {
|
|
6746
6861
|
failSettings(`${path} must be true or false, got ${JSON.stringify(value)}.`);
|
|
@@ -6758,12 +6873,12 @@ function validateSettingContainer(value, container, keys) {
|
|
|
6758
6873
|
if (!isPlainObject(value)) {
|
|
6759
6874
|
failSettings(`doWork.${container} must be an object, got ${JSON.stringify(value)}.`);
|
|
6760
6875
|
}
|
|
6761
|
-
for (const
|
|
6762
|
-
validateOptionalString(value,
|
|
6876
|
+
for (const key2 of keys) {
|
|
6877
|
+
validateOptionalString(value, key2, `doWork.${container}.${key2}`);
|
|
6763
6878
|
}
|
|
6764
|
-
for (const
|
|
6765
|
-
if (!keys.includes(
|
|
6766
|
-
failSettings(`doWork.${container}.${
|
|
6879
|
+
for (const key2 of Object.keys(value)) {
|
|
6880
|
+
if (!keys.includes(key2)) {
|
|
6881
|
+
failSettings(`doWork.${container}.${key2} is not a recognised setting; expected one of: ${keys.join(", ")}.`);
|
|
6767
6882
|
}
|
|
6768
6883
|
}
|
|
6769
6884
|
}
|
|
@@ -6800,7 +6915,7 @@ function matchesDiscoveryFilter(subject, settings) {
|
|
|
6800
6915
|
const value = settings.discoveryValue.toLowerCase();
|
|
6801
6916
|
switch (settings.technique) {
|
|
6802
6917
|
case "label":
|
|
6803
|
-
return subject.labels.some((
|
|
6918
|
+
return subject.labels.some((label2) => label2.toLowerCase() === value);
|
|
6804
6919
|
case "assignee":
|
|
6805
6920
|
return subject.assignees.some((assignee) => assignee.toLowerCase() === value);
|
|
6806
6921
|
case "title-contains":
|
|
@@ -8311,6 +8426,362 @@ import { Command as Command7 } from "commander";
|
|
|
8311
8426
|
function isWatchClosed(state) {
|
|
8312
8427
|
return state === "closed";
|
|
8313
8428
|
}
|
|
8429
|
+
function issueConversation(surface) {
|
|
8430
|
+
return {
|
|
8431
|
+
kind: "issue",
|
|
8432
|
+
number: surface.issue.number,
|
|
8433
|
+
state: surface.state === "CLOSED" ? "closed" : "open",
|
|
8434
|
+
messages: surface.messages
|
|
8435
|
+
};
|
|
8436
|
+
}
|
|
8437
|
+
function prConversation(surface) {
|
|
8438
|
+
return {
|
|
8439
|
+
kind: "pr",
|
|
8440
|
+
number: surface.pr.number,
|
|
8441
|
+
state: surface.pr.state === "OPEN" ? "open" : "closed",
|
|
8442
|
+
messages: [...surface.messages, ...surface.threads.flatMap((thread) => thread.comments)]
|
|
8443
|
+
};
|
|
8444
|
+
}
|
|
8445
|
+
function decideConversation(conversation, p) {
|
|
8446
|
+
if (isWatchClosed(conversation.state)) {
|
|
8447
|
+
return { kind: "skip", surface: conversation, reason: "closed", detail: "the item is closed or merged" };
|
|
8448
|
+
}
|
|
8449
|
+
const agent = p.agentUser.toLowerCase();
|
|
8450
|
+
const allowed = new Set(p.allowedUsers.map((user) => user.toLowerCase()));
|
|
8451
|
+
const relevant = conversation.messages.filter((message) => {
|
|
8452
|
+
const author = message.author.toLowerCase();
|
|
8453
|
+
return author === agent || allowed.has(author);
|
|
8454
|
+
});
|
|
8455
|
+
let newest2 = null;
|
|
8456
|
+
for (const message of relevant) {
|
|
8457
|
+
if (message.author.toLowerCase() !== agent) continue;
|
|
8458
|
+
if (newest2 === null || message.createdAt > newest2.createdAt) newest2 = message;
|
|
8459
|
+
}
|
|
8460
|
+
if (newest2 === null) {
|
|
8461
|
+
return {
|
|
8462
|
+
kind: "skip",
|
|
8463
|
+
surface: conversation,
|
|
8464
|
+
reason: "no-agent-message",
|
|
8465
|
+
detail: `${p.agentUser} has not written here`
|
|
8466
|
+
};
|
|
8467
|
+
}
|
|
8468
|
+
const marker = { commentId: "", createdAt: newest2.createdAt };
|
|
8469
|
+
const answeredBy = p.allowedUsers.find((user) => agentAnsweredAfter(relevant, user, marker));
|
|
8470
|
+
if (answeredBy !== void 0) {
|
|
8471
|
+
return {
|
|
8472
|
+
kind: "skip",
|
|
8473
|
+
surface: conversation,
|
|
8474
|
+
reason: "answered",
|
|
8475
|
+
detail: `${answeredBy} answered after ${p.agentUser}'s message at ${newest2.createdAt}`
|
|
8476
|
+
};
|
|
8477
|
+
}
|
|
8478
|
+
return {
|
|
8479
|
+
kind: "reply",
|
|
8480
|
+
surface: conversation,
|
|
8481
|
+
agentMessageAt: newest2.createdAt,
|
|
8482
|
+
reason: `${p.agentUser}'s message at ${newest2.createdAt} has no answer from an allowed user`
|
|
8483
|
+
};
|
|
8484
|
+
}
|
|
8485
|
+
function decideConductorReply(conversations, p) {
|
|
8486
|
+
const decisions = conversations.map((conversation) => decideConversation(conversation, p));
|
|
8487
|
+
return { needsReply: decisions.some((decision) => decision.kind === "reply"), decisions };
|
|
8488
|
+
}
|
|
8489
|
+
|
|
8490
|
+
// src/conductor/thread.ts
|
|
8491
|
+
function defaultReplyTarget(thread) {
|
|
8492
|
+
return thread.kind === "issue" ? { kind: "issue", number: thread.issue.issue.number } : { kind: "pr", number: thread.prs[0].surface.pr.number };
|
|
8493
|
+
}
|
|
8494
|
+
function postingInstruction(target) {
|
|
8495
|
+
const command = target.kind === "pr" ? "pr" : "issue";
|
|
8496
|
+
const number = String(target.number);
|
|
8497
|
+
return [
|
|
8498
|
+
`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
|
+
`Run \`gh ${command} comment ${number} --body-file -\` and give the body on standard input, for example with a here-document.`,
|
|
8500
|
+
"Post exactly one comment. Do not approve, merge or close anything."
|
|
8501
|
+
].join("\n");
|
|
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
|
+
}
|
|
8509
|
+
function visible(messages, participants) {
|
|
8510
|
+
return analyzeSurface(messages, participants).messages.map((message) => ({
|
|
8511
|
+
kind: message.kind,
|
|
8512
|
+
author: message.author,
|
|
8513
|
+
body: message.body,
|
|
8514
|
+
createdAt: message.createdAt
|
|
8515
|
+
}));
|
|
8516
|
+
}
|
|
8517
|
+
function renderMessages(messages, participants) {
|
|
8518
|
+
const kept = visible(messages, participants);
|
|
8519
|
+
return kept.length === 0 ? "(no messages)" : formatMessages(kept.map((m) => ({ ...m, isNew: false })));
|
|
8520
|
+
}
|
|
8521
|
+
function renderThread(thread, participants) {
|
|
8522
|
+
const comments = visible(thread.comments, participants);
|
|
8523
|
+
if (comments.length === 0) return null;
|
|
8524
|
+
const location = thread.line === null ? `${thread.path}:(file)` : `${thread.path}:${String(thread.line)}`;
|
|
8525
|
+
const link = thread.url === null ? "" : `
|
|
8526
|
+
${thread.url}`;
|
|
8527
|
+
return `${location}${link}
|
|
8528
|
+
${formatMessages(comments.map((m) => ({ ...m, isNew: false })))}`;
|
|
8529
|
+
}
|
|
8530
|
+
function renderChecks(checks) {
|
|
8531
|
+
if (checks.length === 0) return "No checks reported.";
|
|
8532
|
+
return checks.map((check) => `- ${check.name}: ${check.status === "COMPLETED" ? check.conclusion ?? "COMPLETED" : check.status}`).join("\n");
|
|
8533
|
+
}
|
|
8534
|
+
function descriptionLines(description, participants) {
|
|
8535
|
+
if (description === void 0 || description.body.trim().length === 0) return [""];
|
|
8536
|
+
const [kept] = visible([{ kind: "issue-body", author: description.author, body: description.body, createdAt: "" }], participants);
|
|
8537
|
+
if (kept === void 0) return [""];
|
|
8538
|
+
return ["", `Pull request description (by ${kept.author}):`, "", kept.body, ""];
|
|
8539
|
+
}
|
|
8540
|
+
function prSection(pr, participants) {
|
|
8541
|
+
const { surface, checks } = pr;
|
|
8542
|
+
const number = String(surface.pr.number);
|
|
8543
|
+
const lines = [
|
|
8544
|
+
"",
|
|
8545
|
+
`Pull request #${number}: ${surface.pr.title}`,
|
|
8546
|
+
`Pull request URL: ${surface.pr.url}`,
|
|
8547
|
+
`State: ${surface.pr.state}${surface.pr.isDraft ? " (draft)" : ""} \xB7 Branch: ${surface.pr.headRefName} into ${surface.pr.baseRefName}`,
|
|
8548
|
+
...descriptionLines(surface.description, participants),
|
|
8549
|
+
`Conversation on pull request #${number} (authorized accounts and you only, oldest first):`,
|
|
8550
|
+
"",
|
|
8551
|
+
renderMessages(surface.messages, participants)
|
|
8552
|
+
];
|
|
8553
|
+
const threads = surface.threads.filter((thread) => !thread.isResolved).map((thread) => renderThread(thread, participants)).filter((text) => text !== null);
|
|
8554
|
+
if (threads.length > 0) {
|
|
8555
|
+
lines.push("", `Unresolved review threads on pull request #${number}:`, "", threads.join("\n\n"));
|
|
8556
|
+
}
|
|
8557
|
+
lines.push("", `CI status of pull request #${number}:`, "", renderChecks(checks));
|
|
8558
|
+
return lines;
|
|
8559
|
+
}
|
|
8560
|
+
function composeConductorPrompt(input) {
|
|
8561
|
+
const { thread, repo, participants, frame } = input;
|
|
8562
|
+
const replyTo = input.replyTo ?? defaultReplyTarget(thread);
|
|
8563
|
+
const lines = [
|
|
8564
|
+
frame,
|
|
8565
|
+
"",
|
|
8566
|
+
"--- Thread assembled by automata ---",
|
|
8567
|
+
`Repository: ${repo.owner}/${repo.repo}`,
|
|
8568
|
+
`The agent is: ${participants.agentUser}`
|
|
8569
|
+
];
|
|
8570
|
+
if (thread.kind === "issue") {
|
|
8571
|
+
lines.push(
|
|
8572
|
+
"",
|
|
8573
|
+
`Issue #${String(thread.issue.issue.number)}: ${thread.issue.issue.title}`,
|
|
8574
|
+
`Issue URL: ${thread.issue.issue.url}`,
|
|
8575
|
+
`State: ${thread.issue.state}`,
|
|
8576
|
+
"",
|
|
8577
|
+
"Conversation on the issue (authorized accounts and you only, oldest first):",
|
|
8578
|
+
"",
|
|
8579
|
+
renderMessages(thread.issue.messages, participants)
|
|
8580
|
+
);
|
|
8581
|
+
}
|
|
8582
|
+
for (const pr of thread.prs) lines.push(...prSection(pr, participants));
|
|
8583
|
+
lines.push(
|
|
8584
|
+
"",
|
|
8585
|
+
"Only the messages above exist. Anything from other accounts has been withheld",
|
|
8586
|
+
"deliberately \u2014 do not ask about it.",
|
|
8587
|
+
"",
|
|
8588
|
+
input.dryRun === true ? dryRunInstruction(replyTo) : postingInstruction(replyTo)
|
|
8589
|
+
);
|
|
8590
|
+
return lines.join("\n");
|
|
8591
|
+
}
|
|
8592
|
+
|
|
8593
|
+
// src/conductor/execution.ts
|
|
8594
|
+
var EXECUTORS = ["claude", "codex"];
|
|
8595
|
+
function isRecord(value) {
|
|
8596
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8597
|
+
}
|
|
8598
|
+
function containerProblem(value, name) {
|
|
8599
|
+
if (value === void 0 || value === null) return null;
|
|
8600
|
+
if (!isRecord(value)) return `conductor.${name} must be an object, got ${JSON.stringify(value)}.`;
|
|
8601
|
+
for (const [key2, entry] of Object.entries(value)) {
|
|
8602
|
+
if (!EXECUTORS.includes(key2)) {
|
|
8603
|
+
return `conductor.${name}.${key2} is not a recognised setting; expected one of: ${EXECUTORS.join(", ")}.`;
|
|
8604
|
+
}
|
|
8605
|
+
if (entry !== void 0 && typeof entry !== "string") {
|
|
8606
|
+
return `conductor.${name}.${key2} must be a string, got ${JSON.stringify(entry)}.`;
|
|
8607
|
+
}
|
|
8608
|
+
}
|
|
8609
|
+
return null;
|
|
8610
|
+
}
|
|
8611
|
+
function conductorExecutionProblem(conductor) {
|
|
8612
|
+
const executor = conductor?.executor;
|
|
8613
|
+
if (executor !== void 0 && executor !== null && (typeof executor !== "string" || !EXECUTORS.includes(executor))) {
|
|
8614
|
+
return `conductor.executor must be one of: ${EXECUTORS.join(", ")}; got ${JSON.stringify(executor)}.`;
|
|
8615
|
+
}
|
|
8616
|
+
return containerProblem(conductor?.models, "models") ?? containerProblem(conductor?.effort, "effort");
|
|
8617
|
+
}
|
|
8618
|
+
function clean(value) {
|
|
8619
|
+
const trimmed2 = value?.trim();
|
|
8620
|
+
return trimmed2 === void 0 || trimmed2.length === 0 ? void 0 : trimmed2;
|
|
8621
|
+
}
|
|
8622
|
+
function resolveConductorExecution(conductor) {
|
|
8623
|
+
const executor = conductor?.executor ?? "claude";
|
|
8624
|
+
return {
|
|
8625
|
+
executor,
|
|
8626
|
+
model: clean(conductor?.models?.[executor]),
|
|
8627
|
+
effort: clean(conductor?.effort?.[executor])
|
|
8628
|
+
};
|
|
8629
|
+
}
|
|
8630
|
+
function describeConductorExecution(execution) {
|
|
8631
|
+
const model = execution.model === void 0 ? " (no model override)" : ` \xB7 model ${execution.model}`;
|
|
8632
|
+
const effort = execution.effort === void 0 ? "" : ` \xB7 effort ${execution.effort}`;
|
|
8633
|
+
return `${execution.executor}${model}${effort}`;
|
|
8634
|
+
}
|
|
8635
|
+
|
|
8636
|
+
// src/conductor/reply.ts
|
|
8637
|
+
function isComment(message) {
|
|
8638
|
+
return message.kind === "issue-comment" || message.kind === "pr-comment";
|
|
8639
|
+
}
|
|
8640
|
+
function key(message) {
|
|
8641
|
+
return `${message.createdAt}\0${message.body}`;
|
|
8642
|
+
}
|
|
8643
|
+
function newMessagesBy(before, after, login2) {
|
|
8644
|
+
const who = login2.toLowerCase();
|
|
8645
|
+
const own = (m) => isComment(m) && m.author.toLowerCase() === who;
|
|
8646
|
+
const known = /* @__PURE__ */ new Map();
|
|
8647
|
+
for (const m of before.filter(own)) known.set(key(m), (known.get(key(m)) ?? 0) + 1);
|
|
8648
|
+
return after.filter((m) => {
|
|
8649
|
+
if (!own(m)) return false;
|
|
8650
|
+
const left = known.get(key(m)) ?? 0;
|
|
8651
|
+
if (left > 0) known.set(key(m), left - 1);
|
|
8652
|
+
return left === 0;
|
|
8653
|
+
});
|
|
8654
|
+
}
|
|
8655
|
+
async function conductReply(input) {
|
|
8656
|
+
let before;
|
|
8657
|
+
try {
|
|
8658
|
+
before = input.read();
|
|
8659
|
+
} catch (err) {
|
|
8660
|
+
return { kind: "unverified", error: `could not read the conversation before the run: ${err.message}` };
|
|
8661
|
+
}
|
|
8662
|
+
let runError = null;
|
|
8663
|
+
try {
|
|
8664
|
+
await input.run();
|
|
8665
|
+
} catch (err) {
|
|
8666
|
+
runError = err.message;
|
|
8667
|
+
}
|
|
8668
|
+
let after;
|
|
8669
|
+
try {
|
|
8670
|
+
after = input.read();
|
|
8671
|
+
} catch (err) {
|
|
8672
|
+
return { kind: "unverified", error: `could not read the conversation after the run: ${err.message}` };
|
|
8673
|
+
}
|
|
8674
|
+
const posted = newMessagesBy(before, after, input.login);
|
|
8675
|
+
if (posted.length > 0) return { kind: "posted", count: posted.length, runError };
|
|
8676
|
+
return runError === null ? { kind: "posted-nothing" } : { kind: "run-failed", error: runError };
|
|
8677
|
+
}
|
|
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
|
+
}
|
|
8314
8785
|
|
|
8315
8786
|
// src/conductor/watchList.ts
|
|
8316
8787
|
function parseWatchId(raw) {
|
|
@@ -8369,6 +8840,118 @@ function pruneWatchList(config) {
|
|
|
8369
8840
|
}
|
|
8370
8841
|
}
|
|
8371
8842
|
if (dropped.length > 0) updateWatch((current) => current.filter((id) => !dropped.includes(id)));
|
|
8843
|
+
return watch.filter((id) => !dropped.includes(id));
|
|
8844
|
+
}
|
|
8845
|
+
function onceLinkMap() {
|
|
8846
|
+
let map = null;
|
|
8847
|
+
return () => map ??= getOpenPrLinkMap();
|
|
8848
|
+
}
|
|
8849
|
+
function readPr(number) {
|
|
8850
|
+
return { surface: getPrSurface(number), checks: getPrChecks(number) };
|
|
8851
|
+
}
|
|
8852
|
+
function readWatchedItem(id, linkMap = getOpenPrLinkMap) {
|
|
8853
|
+
const target = getWatchTarget(id);
|
|
8854
|
+
if (target.kind === "pr") {
|
|
8855
|
+
const pr = readPr(id);
|
|
8856
|
+
return { thread: { kind: "pr", prs: [pr] }, conversations: [prConversation(pr.surface)] };
|
|
8857
|
+
}
|
|
8858
|
+
const issue = getIssueSurface(id);
|
|
8859
|
+
const prs = (linkMap().byIssue.get(id) ?? []).map((ref) => readPr(ref.number));
|
|
8860
|
+
return {
|
|
8861
|
+
thread: { kind: "issue", issue, prs },
|
|
8862
|
+
conversations: [issueConversation(issue), ...prs.map((pr) => prConversation(pr.surface))]
|
|
8863
|
+
};
|
|
8864
|
+
}
|
|
8865
|
+
function readTargetMessages(target) {
|
|
8866
|
+
return target.kind === "issue" ? issueConversation(getIssueSurface(target.number)).messages : prConversation(getPrSurface(target.number)).messages;
|
|
8867
|
+
}
|
|
8868
|
+
function runModel(prompt, execution, replyTo) {
|
|
8869
|
+
const { owner, repo } = getRepoSlug();
|
|
8870
|
+
return runClaude(prompt, { model: execution.model, effort: execution.effort, readOnly: true, replyTo, ghRepo: `${owner}/${repo}` });
|
|
8871
|
+
}
|
|
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)}`;
|
|
8890
|
+
let item;
|
|
8891
|
+
try {
|
|
8892
|
+
item = readWatchedItem(id, dryRun?.linkMap);
|
|
8893
|
+
} catch (err) {
|
|
8894
|
+
process.stderr.write(`Warning: could not read ${label2}, skipping it this tick: ${err.message}
|
|
8895
|
+
`);
|
|
8896
|
+
return "idle";
|
|
8897
|
+
}
|
|
8898
|
+
const { thread, conversations } = item;
|
|
8899
|
+
const verdict = decideConductorReply(conversations, participants);
|
|
8900
|
+
const owed = verdict.decisions.find(
|
|
8901
|
+
(decision) => decision.kind === "reply" && dryRun?.answered.has(targetKey(decision.surface)) !== true
|
|
8902
|
+
);
|
|
8903
|
+
if (owed === void 0) {
|
|
8904
|
+
process.stdout.write(`Conductor: ${label2} needs no reply.
|
|
8905
|
+
`);
|
|
8906
|
+
return "idle";
|
|
8907
|
+
}
|
|
8908
|
+
const replyTo = { kind: owed.surface.kind, number: owed.surface.number };
|
|
8909
|
+
const frame = thread.kind === "issue" ? config.conductor?.prompts?.issue ?? DEFAULT_CONDUCTOR_ISSUE_PROMPT : config.conductor?.prompts?.pr ?? DEFAULT_CONDUCTOR_PR_PROMPT;
|
|
8910
|
+
const prompt = composeConductorPrompt({ thread, repo: getRepoSlug(), participants, frame, replyTo, dryRun: dryRun !== void 0 });
|
|
8911
|
+
const where = `${replyTo.kind === "pr" ? "pull request" : "issue"} #${String(replyTo.number)}`;
|
|
8912
|
+
process.stdout.write(`Conductor: ${label2} needs a reply on ${where}; running ${describeConductorExecution(execution)}.
|
|
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
|
+
}
|
|
8928
|
+
const outcome = await conductReply({
|
|
8929
|
+
login: login2,
|
|
8930
|
+
read: () => readTargetMessages(replyTo),
|
|
8931
|
+
run: () => runModel(prompt, execution, replyTo)
|
|
8932
|
+
});
|
|
8933
|
+
switch (outcome.kind) {
|
|
8934
|
+
case "posted":
|
|
8935
|
+
process.stdout.write(`Conductor: posted a reply on ${where}.
|
|
8936
|
+
`);
|
|
8937
|
+
if (outcome.runError !== null) {
|
|
8938
|
+
process.stderr.write(`Warning: the run reported "${outcome.runError}" but the reply is posted.
|
|
8939
|
+
`);
|
|
8940
|
+
}
|
|
8941
|
+
return "replied";
|
|
8942
|
+
case "posted-nothing":
|
|
8943
|
+
process.stderr.write(`Error: the run for ${label2} finished but posted no comment on ${where}.
|
|
8944
|
+
`);
|
|
8945
|
+
return "failed";
|
|
8946
|
+
case "run-failed":
|
|
8947
|
+
process.stderr.write(`Error: the run for ${label2} failed and posted no comment on ${where}: ${outcome.error}
|
|
8948
|
+
`);
|
|
8949
|
+
return "failed";
|
|
8950
|
+
case "unverified":
|
|
8951
|
+
process.stderr.write(`Error: could not tell whether ${label2} got a reply: ${outcome.error}
|
|
8952
|
+
`);
|
|
8953
|
+
return "failed";
|
|
8954
|
+
}
|
|
8372
8955
|
}
|
|
8373
8956
|
function loadGitHubConfig() {
|
|
8374
8957
|
const config = readRawConfig();
|
|
@@ -8440,43 +9023,139 @@ function runWatchList() {
|
|
|
8440
9023
|
function exitWith(code) {
|
|
8441
9024
|
if (code !== 0) process.exit(code);
|
|
8442
9025
|
}
|
|
8443
|
-
function
|
|
9026
|
+
function preflight() {
|
|
8444
9027
|
let config;
|
|
8445
9028
|
try {
|
|
8446
9029
|
config = readConfig();
|
|
8447
9030
|
} catch (err) {
|
|
8448
|
-
return
|
|
9031
|
+
return { problem: err.message };
|
|
8449
9032
|
}
|
|
8450
9033
|
if (!isExplicitGitHub(config)) {
|
|
8451
|
-
return
|
|
8452
|
-
azdoUnsupportedMessage(
|
|
9034
|
+
return {
|
|
9035
|
+
problem: azdoUnsupportedMessage(
|
|
8453
9036
|
"conductor",
|
|
8454
9037
|
"It needs the GitHub APIs; set the remote with `automata config set type gh`."
|
|
8455
9038
|
)
|
|
8456
|
-
|
|
9039
|
+
};
|
|
8457
9040
|
}
|
|
8458
9041
|
const allowedUsers = (config.allowedUsers ?? []).filter((user) => user.trim().length > 0);
|
|
8459
9042
|
if (allowedUsers.length === 0) {
|
|
8460
|
-
return
|
|
9043
|
+
return { problem: "No allowed users configured. Run `automata config set allowed-users <user1,user2>`." };
|
|
8461
9044
|
}
|
|
8462
9045
|
const agentUser = (config.agentUser ?? "").trim();
|
|
8463
9046
|
if (agentUser.length === 0) {
|
|
8464
|
-
return
|
|
9047
|
+
return { problem: "No agent user configured. Run `automata config set agent-user <login>`." };
|
|
8465
9048
|
}
|
|
8466
9049
|
let login2;
|
|
8467
9050
|
try {
|
|
8468
9051
|
login2 = getAuthenticatedLogin();
|
|
8469
9052
|
} catch (err) {
|
|
8470
|
-
return
|
|
9053
|
+
return { problem: `\`gh\` could not be queried: ${err.message}` };
|
|
8471
9054
|
}
|
|
8472
|
-
const
|
|
8473
|
-
if (
|
|
9055
|
+
const identityProblem = conductorIdentityProblemFor(login2, agentUser, allowedUsers);
|
|
9056
|
+
if (identityProblem !== null) return { problem: identityProblem };
|
|
8474
9057
|
const rawStale = config.doWork?.lockStaleMinutes;
|
|
8475
9058
|
if (rawStale !== void 0 && rawStale !== null) {
|
|
8476
9059
|
if (typeof rawStale !== "number" || !Number.isSafeInteger(rawStale) || rawStale < 1) {
|
|
8477
|
-
return
|
|
9060
|
+
return { problem: `doWork.lockStaleMinutes must be a positive integer, got ${JSON.stringify(rawStale)}.` };
|
|
9061
|
+
}
|
|
9062
|
+
}
|
|
9063
|
+
const executionProblem = conductorExecutionProblem(config.conductor);
|
|
9064
|
+
if (executionProblem !== null) return { problem: executionProblem };
|
|
9065
|
+
if (config.conductor?.executor === "codex") {
|
|
9066
|
+
return { problem: "conductor.executor codex is not supported: Codex cannot be limited to comments on the reply target. Use claude." };
|
|
9067
|
+
}
|
|
9068
|
+
return {
|
|
9069
|
+
config,
|
|
9070
|
+
login: login2 ?? "",
|
|
9071
|
+
participants: { allowedUsers, agentUser },
|
|
9072
|
+
execution: resolveConductorExecution(config.conductor)
|
|
9073
|
+
};
|
|
9074
|
+
}
|
|
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++;
|
|
8478
9146
|
}
|
|
8479
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();
|
|
9155
|
+
const checked = preflight();
|
|
9156
|
+
if ("problem" in checked) return fail2(checked.problem);
|
|
9157
|
+
if (options.dryRun === true) return runConductorDryRun(checked);
|
|
9158
|
+
const { config, login: login2, participants, execution } = checked;
|
|
8480
9159
|
const staleMinutes = config.doWork?.lockStaleMinutes ?? DEFAULT_DO_WORK.lockStaleMinutes;
|
|
8481
9160
|
const lock = acquireConductorLock(staleMinutes);
|
|
8482
9161
|
if (!lock.ok) {
|
|
@@ -8495,18 +9174,28 @@ function runConductor() {
|
|
|
8495
9174
|
return 0;
|
|
8496
9175
|
}
|
|
8497
9176
|
try {
|
|
8498
|
-
process.stdout.write(`Conductor: running as ${login2
|
|
9177
|
+
process.stdout.write(`Conductor: running as ${login2}.
|
|
8499
9178
|
`);
|
|
8500
|
-
pruneWatchList(config);
|
|
8501
|
-
|
|
9179
|
+
const watch = pruneWatchList(config);
|
|
9180
|
+
let failed = 0;
|
|
9181
|
+
for (const id of watch) {
|
|
9182
|
+
try {
|
|
9183
|
+
if (await conductItem(id, config, login2, participants, execution) === "failed") failed++;
|
|
9184
|
+
} catch (err) {
|
|
9185
|
+
process.stderr.write(`Error: #${String(id)} could not be conducted: ${err.message}
|
|
9186
|
+
`);
|
|
9187
|
+
failed++;
|
|
9188
|
+
}
|
|
9189
|
+
}
|
|
9190
|
+
return failed > 0 ? 1 : 0;
|
|
8502
9191
|
} finally {
|
|
8503
9192
|
lock.handle.release();
|
|
8504
9193
|
}
|
|
8505
9194
|
}
|
|
8506
9195
|
var conductorCommand = new Command7("conductor").description(
|
|
8507
9196
|
"Run one conductor tick as an allowed (human) account: verify the identity, take the conductor's own run lock, then act"
|
|
8508
|
-
).action(() => {
|
|
8509
|
-
exitWith(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));
|
|
8510
9199
|
});
|
|
8511
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) => {
|
|
8512
9201
|
exitWith(runWatchAdd(id));
|