automata-cli 0.11.0-develop.501 → 0.11.0-develop.514
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 +508 -51
- 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,
|
|
@@ -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 {
|
|
@@ -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) {
|
|
@@ -5440,9 +5552,9 @@ function summarizeSync(items) {
|
|
|
5440
5552
|
const counts = /* @__PURE__ */ new Map();
|
|
5441
5553
|
for (const item of items) {
|
|
5442
5554
|
if (item.sync === void 0) continue;
|
|
5443
|
-
const
|
|
5444
|
-
if (
|
|
5445
|
-
counts.set(
|
|
5555
|
+
const key2 = oneLine(item.sync);
|
|
5556
|
+
if (key2.length === 0 || QUIET_SYNC.has(key2)) continue;
|
|
5557
|
+
counts.set(key2, (counts.get(key2) ?? 0) + 1);
|
|
5446
5558
|
}
|
|
5447
5559
|
if (counts.size === 0) return null;
|
|
5448
5560
|
return [...counts].map(([strategy, count]) => `${strategy}:${String(count)}`).join(",");
|
|
@@ -5691,9 +5803,9 @@ function parseExecution(bracket) {
|
|
|
5691
5803
|
if (bracket === void 0) return { executor: null, model: null, effort: null };
|
|
5692
5804
|
const parts = bracket.split(" ").filter((part) => part.length > 0);
|
|
5693
5805
|
const executor = parts[0] ?? null;
|
|
5694
|
-
const find = (
|
|
5695
|
-
const hit = parts.find((part) => part.startsWith(`${
|
|
5696
|
-
return hit === void 0 ? null : hit.slice(
|
|
5806
|
+
const find = (key2) => {
|
|
5807
|
+
const hit = parts.find((part) => part.startsWith(`${key2}=`));
|
|
5808
|
+
return hit === void 0 ? null : hit.slice(key2.length + 1);
|
|
5697
5809
|
};
|
|
5698
5810
|
return { executor, model: find("model"), effort: find("effort") };
|
|
5699
5811
|
}
|
|
@@ -6706,15 +6818,15 @@ function describeRefusedRun(item, refusal) {
|
|
|
6706
6818
|
function isPlainObject(value) {
|
|
6707
6819
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6708
6820
|
}
|
|
6709
|
-
function validateOptionalString(container,
|
|
6710
|
-
const value = container[
|
|
6821
|
+
function validateOptionalString(container, key2, path) {
|
|
6822
|
+
const value = container[key2];
|
|
6711
6823
|
if (value === void 0 || value === null) return;
|
|
6712
6824
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
6713
6825
|
failSettings(`${path} must be a non-empty string.`);
|
|
6714
6826
|
}
|
|
6715
6827
|
}
|
|
6716
|
-
function validateOptionalInt(container,
|
|
6717
|
-
const value = container[
|
|
6828
|
+
function validateOptionalInt(container, key2, path, min, hint) {
|
|
6829
|
+
const value = container[key2];
|
|
6718
6830
|
if (value === void 0 || value === null) return;
|
|
6719
6831
|
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < min) {
|
|
6720
6832
|
failSettings(`${path} must be ${hint}, got ${JSON.stringify(value)}.`);
|
|
@@ -6739,8 +6851,8 @@ function validateDoWorkConfig(section) {
|
|
|
6739
6851
|
validateSettingContainer(section["effort"], "effort", ["claude", "codex"]);
|
|
6740
6852
|
validateSettingContainer(section["prompts"], "prompts", ["issueDiscuss", "prWork", "prOrphan"]);
|
|
6741
6853
|
}
|
|
6742
|
-
function validateOptionalBoolean(container,
|
|
6743
|
-
const value = container[
|
|
6854
|
+
function validateOptionalBoolean(container, key2, path) {
|
|
6855
|
+
const value = container[key2];
|
|
6744
6856
|
if (value === void 0 || value === null) return;
|
|
6745
6857
|
if (typeof value !== "boolean") {
|
|
6746
6858
|
failSettings(`${path} must be true or false, got ${JSON.stringify(value)}.`);
|
|
@@ -6758,12 +6870,12 @@ function validateSettingContainer(value, container, keys) {
|
|
|
6758
6870
|
if (!isPlainObject(value)) {
|
|
6759
6871
|
failSettings(`doWork.${container} must be an object, got ${JSON.stringify(value)}.`);
|
|
6760
6872
|
}
|
|
6761
|
-
for (const
|
|
6762
|
-
validateOptionalString(value,
|
|
6873
|
+
for (const key2 of keys) {
|
|
6874
|
+
validateOptionalString(value, key2, `doWork.${container}.${key2}`);
|
|
6763
6875
|
}
|
|
6764
|
-
for (const
|
|
6765
|
-
if (!keys.includes(
|
|
6766
|
-
failSettings(`doWork.${container}.${
|
|
6876
|
+
for (const key2 of Object.keys(value)) {
|
|
6877
|
+
if (!keys.includes(key2)) {
|
|
6878
|
+
failSettings(`doWork.${container}.${key2} is not a recognised setting; expected one of: ${keys.join(", ")}.`);
|
|
6767
6879
|
}
|
|
6768
6880
|
}
|
|
6769
6881
|
}
|
|
@@ -8311,6 +8423,249 @@ import { Command as Command7 } from "commander";
|
|
|
8311
8423
|
function isWatchClosed(state) {
|
|
8312
8424
|
return state === "closed";
|
|
8313
8425
|
}
|
|
8426
|
+
function issueConversation(surface) {
|
|
8427
|
+
return {
|
|
8428
|
+
kind: "issue",
|
|
8429
|
+
number: surface.issue.number,
|
|
8430
|
+
state: surface.state === "CLOSED" ? "closed" : "open",
|
|
8431
|
+
messages: surface.messages
|
|
8432
|
+
};
|
|
8433
|
+
}
|
|
8434
|
+
function prConversation(surface) {
|
|
8435
|
+
return {
|
|
8436
|
+
kind: "pr",
|
|
8437
|
+
number: surface.pr.number,
|
|
8438
|
+
state: surface.pr.state === "OPEN" ? "open" : "closed",
|
|
8439
|
+
messages: [...surface.messages, ...surface.threads.flatMap((thread) => thread.comments)]
|
|
8440
|
+
};
|
|
8441
|
+
}
|
|
8442
|
+
function decideConversation(conversation, p) {
|
|
8443
|
+
if (isWatchClosed(conversation.state)) {
|
|
8444
|
+
return { kind: "skip", surface: conversation, reason: "closed", detail: "the item is closed or merged" };
|
|
8445
|
+
}
|
|
8446
|
+
const agent = p.agentUser.toLowerCase();
|
|
8447
|
+
const allowed = new Set(p.allowedUsers.map((user) => user.toLowerCase()));
|
|
8448
|
+
const relevant = conversation.messages.filter((message) => {
|
|
8449
|
+
const author = message.author.toLowerCase();
|
|
8450
|
+
return author === agent || allowed.has(author);
|
|
8451
|
+
});
|
|
8452
|
+
let newest2 = null;
|
|
8453
|
+
for (const message of relevant) {
|
|
8454
|
+
if (message.author.toLowerCase() !== agent) continue;
|
|
8455
|
+
if (newest2 === null || message.createdAt > newest2.createdAt) newest2 = message;
|
|
8456
|
+
}
|
|
8457
|
+
if (newest2 === null) {
|
|
8458
|
+
return {
|
|
8459
|
+
kind: "skip",
|
|
8460
|
+
surface: conversation,
|
|
8461
|
+
reason: "no-agent-message",
|
|
8462
|
+
detail: `${p.agentUser} has not written here`
|
|
8463
|
+
};
|
|
8464
|
+
}
|
|
8465
|
+
const marker = { commentId: "", createdAt: newest2.createdAt };
|
|
8466
|
+
const answeredBy = p.allowedUsers.find((user) => agentAnsweredAfter(relevant, user, marker));
|
|
8467
|
+
if (answeredBy !== void 0) {
|
|
8468
|
+
return {
|
|
8469
|
+
kind: "skip",
|
|
8470
|
+
surface: conversation,
|
|
8471
|
+
reason: "answered",
|
|
8472
|
+
detail: `${answeredBy} answered after ${p.agentUser}'s message at ${newest2.createdAt}`
|
|
8473
|
+
};
|
|
8474
|
+
}
|
|
8475
|
+
return {
|
|
8476
|
+
kind: "reply",
|
|
8477
|
+
surface: conversation,
|
|
8478
|
+
agentMessageAt: newest2.createdAt,
|
|
8479
|
+
reason: `${p.agentUser}'s message at ${newest2.createdAt} has no answer from an allowed user`
|
|
8480
|
+
};
|
|
8481
|
+
}
|
|
8482
|
+
function decideConductorReply(conversations, p) {
|
|
8483
|
+
const decisions = conversations.map((conversation) => decideConversation(conversation, p));
|
|
8484
|
+
return { needsReply: decisions.some((decision) => decision.kind === "reply"), decisions };
|
|
8485
|
+
}
|
|
8486
|
+
|
|
8487
|
+
// src/conductor/thread.ts
|
|
8488
|
+
function defaultReplyTarget(thread) {
|
|
8489
|
+
return thread.kind === "issue" ? { kind: "issue", number: thread.issue.issue.number } : { kind: "pr", number: thread.prs[0].surface.pr.number };
|
|
8490
|
+
}
|
|
8491
|
+
function postingInstruction(target) {
|
|
8492
|
+
const command = target.kind === "pr" ? "pr" : "issue";
|
|
8493
|
+
const number = String(target.number);
|
|
8494
|
+
return [
|
|
8495
|
+
`Post your reply as a comment on ${target.kind === "pr" ? "pull request" : "issue"} #${number}. What you print is discarded; only a posted comment counts.`,
|
|
8496
|
+
`Run \`gh ${command} comment ${number} --body-file -\` and give the body on standard input, for example with a here-document.`,
|
|
8497
|
+
"Post exactly one comment. Do not approve, merge or close anything."
|
|
8498
|
+
].join("\n");
|
|
8499
|
+
}
|
|
8500
|
+
function visible(messages, participants) {
|
|
8501
|
+
return analyzeSurface(messages, participants).messages.map((message) => ({
|
|
8502
|
+
kind: message.kind,
|
|
8503
|
+
author: message.author,
|
|
8504
|
+
body: message.body,
|
|
8505
|
+
createdAt: message.createdAt
|
|
8506
|
+
}));
|
|
8507
|
+
}
|
|
8508
|
+
function renderMessages(messages, participants) {
|
|
8509
|
+
const kept = visible(messages, participants);
|
|
8510
|
+
return kept.length === 0 ? "(no messages)" : formatMessages(kept.map((m) => ({ ...m, isNew: false })));
|
|
8511
|
+
}
|
|
8512
|
+
function renderThread(thread, participants) {
|
|
8513
|
+
const comments = visible(thread.comments, participants);
|
|
8514
|
+
if (comments.length === 0) return null;
|
|
8515
|
+
const location = thread.line === null ? `${thread.path}:(file)` : `${thread.path}:${String(thread.line)}`;
|
|
8516
|
+
const link = thread.url === null ? "" : `
|
|
8517
|
+
${thread.url}`;
|
|
8518
|
+
return `${location}${link}
|
|
8519
|
+
${formatMessages(comments.map((m) => ({ ...m, isNew: false })))}`;
|
|
8520
|
+
}
|
|
8521
|
+
function renderChecks(checks) {
|
|
8522
|
+
if (checks.length === 0) return "No checks reported.";
|
|
8523
|
+
return checks.map((check) => `- ${check.name}: ${check.status === "COMPLETED" ? check.conclusion ?? "COMPLETED" : check.status}`).join("\n");
|
|
8524
|
+
}
|
|
8525
|
+
function descriptionLines(description, participants) {
|
|
8526
|
+
if (description === void 0 || description.body.trim().length === 0) return [""];
|
|
8527
|
+
const [kept] = visible([{ kind: "issue-body", author: description.author, body: description.body, createdAt: "" }], participants);
|
|
8528
|
+
if (kept === void 0) return [""];
|
|
8529
|
+
return ["", `Pull request description (by ${kept.author}):`, "", kept.body, ""];
|
|
8530
|
+
}
|
|
8531
|
+
function prSection(pr, participants) {
|
|
8532
|
+
const { surface, checks } = pr;
|
|
8533
|
+
const number = String(surface.pr.number);
|
|
8534
|
+
const lines = [
|
|
8535
|
+
"",
|
|
8536
|
+
`Pull request #${number}: ${surface.pr.title}`,
|
|
8537
|
+
`Pull request URL: ${surface.pr.url}`,
|
|
8538
|
+
`State: ${surface.pr.state}${surface.pr.isDraft ? " (draft)" : ""} \xB7 Branch: ${surface.pr.headRefName} into ${surface.pr.baseRefName}`,
|
|
8539
|
+
...descriptionLines(surface.description, participants),
|
|
8540
|
+
`Conversation on pull request #${number} (authorized accounts and you only, oldest first):`,
|
|
8541
|
+
"",
|
|
8542
|
+
renderMessages(surface.messages, participants)
|
|
8543
|
+
];
|
|
8544
|
+
const threads = surface.threads.filter((thread) => !thread.isResolved).map((thread) => renderThread(thread, participants)).filter((text) => text !== null);
|
|
8545
|
+
if (threads.length > 0) {
|
|
8546
|
+
lines.push("", `Unresolved review threads on pull request #${number}:`, "", threads.join("\n\n"));
|
|
8547
|
+
}
|
|
8548
|
+
lines.push("", `CI status of pull request #${number}:`, "", renderChecks(checks));
|
|
8549
|
+
return lines;
|
|
8550
|
+
}
|
|
8551
|
+
function composeConductorPrompt(input) {
|
|
8552
|
+
const { thread, repo, participants, frame } = input;
|
|
8553
|
+
const replyTo = input.replyTo ?? defaultReplyTarget(thread);
|
|
8554
|
+
const lines = [
|
|
8555
|
+
frame,
|
|
8556
|
+
"",
|
|
8557
|
+
"--- Thread assembled by automata ---",
|
|
8558
|
+
`Repository: ${repo.owner}/${repo.repo}`,
|
|
8559
|
+
`The agent is: ${participants.agentUser}`
|
|
8560
|
+
];
|
|
8561
|
+
if (thread.kind === "issue") {
|
|
8562
|
+
lines.push(
|
|
8563
|
+
"",
|
|
8564
|
+
`Issue #${String(thread.issue.issue.number)}: ${thread.issue.issue.title}`,
|
|
8565
|
+
`Issue URL: ${thread.issue.issue.url}`,
|
|
8566
|
+
`State: ${thread.issue.state}`,
|
|
8567
|
+
"",
|
|
8568
|
+
"Conversation on the issue (authorized accounts and you only, oldest first):",
|
|
8569
|
+
"",
|
|
8570
|
+
renderMessages(thread.issue.messages, participants)
|
|
8571
|
+
);
|
|
8572
|
+
}
|
|
8573
|
+
for (const pr of thread.prs) lines.push(...prSection(pr, participants));
|
|
8574
|
+
lines.push(
|
|
8575
|
+
"",
|
|
8576
|
+
"Only the messages above exist. Anything from other accounts has been withheld",
|
|
8577
|
+
"deliberately \u2014 do not ask about it.",
|
|
8578
|
+
"",
|
|
8579
|
+
postingInstruction(replyTo)
|
|
8580
|
+
);
|
|
8581
|
+
return lines.join("\n");
|
|
8582
|
+
}
|
|
8583
|
+
|
|
8584
|
+
// src/conductor/execution.ts
|
|
8585
|
+
var EXECUTORS = ["claude", "codex"];
|
|
8586
|
+
function isRecord(value) {
|
|
8587
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8588
|
+
}
|
|
8589
|
+
function containerProblem(value, name) {
|
|
8590
|
+
if (value === void 0 || value === null) return null;
|
|
8591
|
+
if (!isRecord(value)) return `conductor.${name} must be an object, got ${JSON.stringify(value)}.`;
|
|
8592
|
+
for (const [key2, entry] of Object.entries(value)) {
|
|
8593
|
+
if (!EXECUTORS.includes(key2)) {
|
|
8594
|
+
return `conductor.${name}.${key2} is not a recognised setting; expected one of: ${EXECUTORS.join(", ")}.`;
|
|
8595
|
+
}
|
|
8596
|
+
if (entry !== void 0 && typeof entry !== "string") {
|
|
8597
|
+
return `conductor.${name}.${key2} must be a string, got ${JSON.stringify(entry)}.`;
|
|
8598
|
+
}
|
|
8599
|
+
}
|
|
8600
|
+
return null;
|
|
8601
|
+
}
|
|
8602
|
+
function conductorExecutionProblem(conductor) {
|
|
8603
|
+
const executor = conductor?.executor;
|
|
8604
|
+
if (executor !== void 0 && executor !== null && (typeof executor !== "string" || !EXECUTORS.includes(executor))) {
|
|
8605
|
+
return `conductor.executor must be one of: ${EXECUTORS.join(", ")}; got ${JSON.stringify(executor)}.`;
|
|
8606
|
+
}
|
|
8607
|
+
return containerProblem(conductor?.models, "models") ?? containerProblem(conductor?.effort, "effort");
|
|
8608
|
+
}
|
|
8609
|
+
function clean(value) {
|
|
8610
|
+
const trimmed2 = value?.trim();
|
|
8611
|
+
return trimmed2 === void 0 || trimmed2.length === 0 ? void 0 : trimmed2;
|
|
8612
|
+
}
|
|
8613
|
+
function resolveConductorExecution(conductor) {
|
|
8614
|
+
const executor = conductor?.executor ?? "claude";
|
|
8615
|
+
return {
|
|
8616
|
+
executor,
|
|
8617
|
+
model: clean(conductor?.models?.[executor]),
|
|
8618
|
+
effort: clean(conductor?.effort?.[executor])
|
|
8619
|
+
};
|
|
8620
|
+
}
|
|
8621
|
+
function describeConductorExecution(execution) {
|
|
8622
|
+
const model = execution.model === void 0 ? " (no model override)" : ` \xB7 model ${execution.model}`;
|
|
8623
|
+
const effort = execution.effort === void 0 ? "" : ` \xB7 effort ${execution.effort}`;
|
|
8624
|
+
return `${execution.executor}${model}${effort}`;
|
|
8625
|
+
}
|
|
8626
|
+
|
|
8627
|
+
// src/conductor/reply.ts
|
|
8628
|
+
function isComment(message) {
|
|
8629
|
+
return message.kind === "issue-comment" || message.kind === "pr-comment";
|
|
8630
|
+
}
|
|
8631
|
+
function key(message) {
|
|
8632
|
+
return `${message.createdAt}\0${message.body}`;
|
|
8633
|
+
}
|
|
8634
|
+
function newMessagesBy(before, after, login2) {
|
|
8635
|
+
const who = login2.toLowerCase();
|
|
8636
|
+
const own = (m) => isComment(m) && m.author.toLowerCase() === who;
|
|
8637
|
+
const known = /* @__PURE__ */ new Map();
|
|
8638
|
+
for (const m of before.filter(own)) known.set(key(m), (known.get(key(m)) ?? 0) + 1);
|
|
8639
|
+
return after.filter((m) => {
|
|
8640
|
+
if (!own(m)) return false;
|
|
8641
|
+
const left = known.get(key(m)) ?? 0;
|
|
8642
|
+
if (left > 0) known.set(key(m), left - 1);
|
|
8643
|
+
return left === 0;
|
|
8644
|
+
});
|
|
8645
|
+
}
|
|
8646
|
+
async function conductReply(input) {
|
|
8647
|
+
let before;
|
|
8648
|
+
try {
|
|
8649
|
+
before = input.read();
|
|
8650
|
+
} catch (err) {
|
|
8651
|
+
return { kind: "unverified", error: `could not read the conversation before the run: ${err.message}` };
|
|
8652
|
+
}
|
|
8653
|
+
let runError = null;
|
|
8654
|
+
try {
|
|
8655
|
+
await input.run();
|
|
8656
|
+
} catch (err) {
|
|
8657
|
+
runError = err.message;
|
|
8658
|
+
}
|
|
8659
|
+
let after;
|
|
8660
|
+
try {
|
|
8661
|
+
after = input.read();
|
|
8662
|
+
} catch (err) {
|
|
8663
|
+
return { kind: "unverified", error: `could not read the conversation after the run: ${err.message}` };
|
|
8664
|
+
}
|
|
8665
|
+
const posted = newMessagesBy(before, after, input.login);
|
|
8666
|
+
if (posted.length > 0) return { kind: "posted", count: posted.length, runError };
|
|
8667
|
+
return runError === null ? { kind: "posted-nothing" } : { kind: "run-failed", error: runError };
|
|
8668
|
+
}
|
|
8314
8669
|
|
|
8315
8670
|
// src/conductor/watchList.ts
|
|
8316
8671
|
function parseWatchId(raw) {
|
|
@@ -8369,6 +8724,82 @@ function pruneWatchList(config) {
|
|
|
8369
8724
|
}
|
|
8370
8725
|
}
|
|
8371
8726
|
if (dropped.length > 0) updateWatch((current) => current.filter((id) => !dropped.includes(id)));
|
|
8727
|
+
return watch.filter((id) => !dropped.includes(id));
|
|
8728
|
+
}
|
|
8729
|
+
function readPr(number) {
|
|
8730
|
+
return { surface: getPrSurface(number), checks: getPrChecks(number) };
|
|
8731
|
+
}
|
|
8732
|
+
function readWatchedItem(id) {
|
|
8733
|
+
const target = getWatchTarget(id);
|
|
8734
|
+
if (target.kind === "pr") {
|
|
8735
|
+
const pr = readPr(id);
|
|
8736
|
+
return { thread: { kind: "pr", prs: [pr] }, conversations: [prConversation(pr.surface)] };
|
|
8737
|
+
}
|
|
8738
|
+
const issue = getIssueSurface(id);
|
|
8739
|
+
const prs = (getOpenPrLinkMap().byIssue.get(id) ?? []).map((ref) => readPr(ref.number));
|
|
8740
|
+
return {
|
|
8741
|
+
thread: { kind: "issue", issue, prs },
|
|
8742
|
+
conversations: [issueConversation(issue), ...prs.map((pr) => prConversation(pr.surface))]
|
|
8743
|
+
};
|
|
8744
|
+
}
|
|
8745
|
+
function readTargetMessages(target) {
|
|
8746
|
+
return target.kind === "issue" ? issueConversation(getIssueSurface(target.number)).messages : prConversation(getPrSurface(target.number)).messages;
|
|
8747
|
+
}
|
|
8748
|
+
function runModel(prompt, execution, replyTo) {
|
|
8749
|
+
const { owner, repo } = getRepoSlug();
|
|
8750
|
+
return runClaude(prompt, { model: execution.model, effort: execution.effort, readOnly: true, replyTo, ghRepo: `${owner}/${repo}` });
|
|
8751
|
+
}
|
|
8752
|
+
async function conductItem(id, config, login2, participants, execution) {
|
|
8753
|
+
const label = `#${String(id)}`;
|
|
8754
|
+
let item;
|
|
8755
|
+
try {
|
|
8756
|
+
item = readWatchedItem(id);
|
|
8757
|
+
} catch (err) {
|
|
8758
|
+
process.stderr.write(`Warning: could not read ${label}, skipping it this tick: ${err.message}
|
|
8759
|
+
`);
|
|
8760
|
+
return "idle";
|
|
8761
|
+
}
|
|
8762
|
+
const { thread, conversations } = item;
|
|
8763
|
+
const verdict = decideConductorReply(conversations, participants);
|
|
8764
|
+
const owed = verdict.decisions.find((decision) => decision.kind === "reply");
|
|
8765
|
+
if (owed === void 0) {
|
|
8766
|
+
process.stdout.write(`Conductor: ${label} needs no reply.
|
|
8767
|
+
`);
|
|
8768
|
+
return "idle";
|
|
8769
|
+
}
|
|
8770
|
+
const replyTo = { kind: owed.surface.kind, number: owed.surface.number };
|
|
8771
|
+
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 });
|
|
8773
|
+
const where = `${replyTo.kind === "pr" ? "pull request" : "issue"} #${String(replyTo.number)}`;
|
|
8774
|
+
process.stdout.write(`Conductor: ${label} needs a reply on ${where}; running ${describeConductorExecution(execution)}.
|
|
8775
|
+
`);
|
|
8776
|
+
const outcome = await conductReply({
|
|
8777
|
+
login: login2,
|
|
8778
|
+
read: () => readTargetMessages(replyTo),
|
|
8779
|
+
run: () => runModel(prompt, execution, replyTo)
|
|
8780
|
+
});
|
|
8781
|
+
switch (outcome.kind) {
|
|
8782
|
+
case "posted":
|
|
8783
|
+
process.stdout.write(`Conductor: posted a reply on ${where}.
|
|
8784
|
+
`);
|
|
8785
|
+
if (outcome.runError !== null) {
|
|
8786
|
+
process.stderr.write(`Warning: the run reported "${outcome.runError}" but the reply is posted.
|
|
8787
|
+
`);
|
|
8788
|
+
}
|
|
8789
|
+
return "replied";
|
|
8790
|
+
case "posted-nothing":
|
|
8791
|
+
process.stderr.write(`Error: the run for ${label} finished but posted no comment on ${where}.
|
|
8792
|
+
`);
|
|
8793
|
+
return "failed";
|
|
8794
|
+
case "run-failed":
|
|
8795
|
+
process.stderr.write(`Error: the run for ${label} failed and posted no comment on ${where}: ${outcome.error}
|
|
8796
|
+
`);
|
|
8797
|
+
return "failed";
|
|
8798
|
+
case "unverified":
|
|
8799
|
+
process.stderr.write(`Error: could not tell whether ${label} got a reply: ${outcome.error}
|
|
8800
|
+
`);
|
|
8801
|
+
return "failed";
|
|
8802
|
+
}
|
|
8372
8803
|
}
|
|
8373
8804
|
function loadGitHubConfig() {
|
|
8374
8805
|
const config = readRawConfig();
|
|
@@ -8440,43 +8871,59 @@ function runWatchList() {
|
|
|
8440
8871
|
function exitWith(code) {
|
|
8441
8872
|
if (code !== 0) process.exit(code);
|
|
8442
8873
|
}
|
|
8443
|
-
function
|
|
8874
|
+
function preflight() {
|
|
8444
8875
|
let config;
|
|
8445
8876
|
try {
|
|
8446
8877
|
config = readConfig();
|
|
8447
8878
|
} catch (err) {
|
|
8448
|
-
return
|
|
8879
|
+
return { problem: err.message };
|
|
8449
8880
|
}
|
|
8450
8881
|
if (!isExplicitGitHub(config)) {
|
|
8451
|
-
return
|
|
8452
|
-
azdoUnsupportedMessage(
|
|
8882
|
+
return {
|
|
8883
|
+
problem: azdoUnsupportedMessage(
|
|
8453
8884
|
"conductor",
|
|
8454
8885
|
"It needs the GitHub APIs; set the remote with `automata config set type gh`."
|
|
8455
8886
|
)
|
|
8456
|
-
|
|
8887
|
+
};
|
|
8457
8888
|
}
|
|
8458
8889
|
const allowedUsers = (config.allowedUsers ?? []).filter((user) => user.trim().length > 0);
|
|
8459
8890
|
if (allowedUsers.length === 0) {
|
|
8460
|
-
return
|
|
8891
|
+
return { problem: "No allowed users configured. Run `automata config set allowed-users <user1,user2>`." };
|
|
8461
8892
|
}
|
|
8462
8893
|
const agentUser = (config.agentUser ?? "").trim();
|
|
8463
8894
|
if (agentUser.length === 0) {
|
|
8464
|
-
return
|
|
8895
|
+
return { problem: "No agent user configured. Run `automata config set agent-user <login>`." };
|
|
8465
8896
|
}
|
|
8466
8897
|
let login2;
|
|
8467
8898
|
try {
|
|
8468
8899
|
login2 = getAuthenticatedLogin();
|
|
8469
8900
|
} catch (err) {
|
|
8470
|
-
return
|
|
8901
|
+
return { problem: `\`gh\` could not be queried: ${err.message}` };
|
|
8471
8902
|
}
|
|
8472
|
-
const
|
|
8473
|
-
if (
|
|
8903
|
+
const identityProblem = conductorIdentityProblemFor(login2, agentUser, allowedUsers);
|
|
8904
|
+
if (identityProblem !== null) return { problem: identityProblem };
|
|
8474
8905
|
const rawStale = config.doWork?.lockStaleMinutes;
|
|
8475
8906
|
if (rawStale !== void 0 && rawStale !== null) {
|
|
8476
8907
|
if (typeof rawStale !== "number" || !Number.isSafeInteger(rawStale) || rawStale < 1) {
|
|
8477
|
-
return
|
|
8908
|
+
return { problem: `doWork.lockStaleMinutes must be a positive integer, got ${JSON.stringify(rawStale)}.` };
|
|
8478
8909
|
}
|
|
8479
8910
|
}
|
|
8911
|
+
const executionProblem = conductorExecutionProblem(config.conductor);
|
|
8912
|
+
if (executionProblem !== null) return { problem: executionProblem };
|
|
8913
|
+
if (config.conductor?.executor === "codex") {
|
|
8914
|
+
return { problem: "conductor.executor codex is not supported: Codex cannot be limited to comments on the reply target. Use claude." };
|
|
8915
|
+
}
|
|
8916
|
+
return {
|
|
8917
|
+
config,
|
|
8918
|
+
login: login2 ?? "",
|
|
8919
|
+
participants: { allowedUsers, agentUser },
|
|
8920
|
+
execution: resolveConductorExecution(config.conductor)
|
|
8921
|
+
};
|
|
8922
|
+
}
|
|
8923
|
+
async function runConductor() {
|
|
8924
|
+
const checked = preflight();
|
|
8925
|
+
if ("problem" in checked) return fail2(checked.problem);
|
|
8926
|
+
const { config, login: login2, participants, execution } = checked;
|
|
8480
8927
|
const staleMinutes = config.doWork?.lockStaleMinutes ?? DEFAULT_DO_WORK.lockStaleMinutes;
|
|
8481
8928
|
const lock = acquireConductorLock(staleMinutes);
|
|
8482
8929
|
if (!lock.ok) {
|
|
@@ -8495,18 +8942,28 @@ function runConductor() {
|
|
|
8495
8942
|
return 0;
|
|
8496
8943
|
}
|
|
8497
8944
|
try {
|
|
8498
|
-
process.stdout.write(`Conductor: running as ${login2
|
|
8945
|
+
process.stdout.write(`Conductor: running as ${login2}.
|
|
8499
8946
|
`);
|
|
8500
|
-
pruneWatchList(config);
|
|
8501
|
-
|
|
8947
|
+
const watch = pruneWatchList(config);
|
|
8948
|
+
let failed = 0;
|
|
8949
|
+
for (const id of watch) {
|
|
8950
|
+
try {
|
|
8951
|
+
if (await conductItem(id, config, login2, participants, execution) === "failed") failed++;
|
|
8952
|
+
} catch (err) {
|
|
8953
|
+
process.stderr.write(`Error: #${String(id)} could not be conducted: ${err.message}
|
|
8954
|
+
`);
|
|
8955
|
+
failed++;
|
|
8956
|
+
}
|
|
8957
|
+
}
|
|
8958
|
+
return failed > 0 ? 1 : 0;
|
|
8502
8959
|
} finally {
|
|
8503
8960
|
lock.handle.release();
|
|
8504
8961
|
}
|
|
8505
8962
|
}
|
|
8506
8963
|
var conductorCommand = new Command7("conductor").description(
|
|
8507
8964
|
"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());
|
|
8965
|
+
).action(async () => {
|
|
8966
|
+
exitWith(await runConductor());
|
|
8510
8967
|
});
|
|
8511
8968
|
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
8969
|
exitWith(runWatchAdd(id));
|