automata-cli 0.11.0-develop.496 → 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.
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
DEFAULT_CHECK_ISSUE_PROMPT,
|
|
4
|
+
DEFAULT_CONDUCTOR_ISSUE_PROMPT,
|
|
5
|
+
DEFAULT_CONDUCTOR_PR_PROMPT,
|
|
4
6
|
DEFAULT_DO_WORK,
|
|
5
7
|
DEFAULT_DO_WORK_ISSUE_DISCUSS_PROMPT,
|
|
6
8
|
DEFAULT_DO_WORK_PR_ORPHAN_PROMPT,
|
|
@@ -10,7 +12,7 @@ import {
|
|
|
10
12
|
readConfig,
|
|
11
13
|
readRawConfig,
|
|
12
14
|
writeConfig
|
|
13
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-TSOUCFDW.js";
|
|
14
16
|
|
|
15
17
|
// src/config/ConfigWizard.tsx
|
|
16
18
|
import { useState } from "react";
|
|
@@ -56,7 +58,9 @@ var PROMPTS_MENU_OPTIONS = [
|
|
|
56
58
|
"Check-Issue",
|
|
57
59
|
"Do Work \u2014 Discuss",
|
|
58
60
|
"Do Work \u2014 PR",
|
|
59
|
-
"Do Work \u2014 Orphan PR"
|
|
61
|
+
"Do Work \u2014 Orphan PR",
|
|
62
|
+
"Conductor \u2014 Issue",
|
|
63
|
+
"Conductor \u2014 PR"
|
|
60
64
|
];
|
|
61
65
|
function parseAllowedUsers(value) {
|
|
62
66
|
return value.split(",").map((user) => user.trim()).filter((user) => user.length > 0);
|
|
@@ -137,7 +141,9 @@ var PROMPT_SCREEN_BY_OPTION = {
|
|
|
137
141
|
"Check-Issue": "check-issue-prompt",
|
|
138
142
|
"Do Work \u2014 Discuss": "do-work-discuss-prompt",
|
|
139
143
|
"Do Work \u2014 PR": "do-work-pr-prompt",
|
|
140
|
-
"Do Work \u2014 Orphan PR": "do-work-pr-orphan-prompt"
|
|
144
|
+
"Do Work \u2014 Orphan PR": "do-work-pr-orphan-prompt",
|
|
145
|
+
"Conductor \u2014 Issue": "conductor-issue-prompt",
|
|
146
|
+
"Conductor \u2014 PR": "conductor-pr-prompt"
|
|
141
147
|
};
|
|
142
148
|
function ConfigWizard() {
|
|
143
149
|
const existing = readConfig();
|
|
@@ -202,6 +208,12 @@ function ConfigWizard() {
|
|
|
202
208
|
const [doWorkPrOrphanPrompt, setDoWorkPrOrphanPrompt] = useState(
|
|
203
209
|
existing.doWork?.prompts?.prOrphan ?? DEFAULT_DO_WORK_PR_ORPHAN_PROMPT
|
|
204
210
|
);
|
|
211
|
+
const [conductorIssuePrompt, setConductorIssuePrompt] = useState(
|
|
212
|
+
existing.conductor?.prompts?.issue ?? DEFAULT_CONDUCTOR_ISSUE_PROMPT
|
|
213
|
+
);
|
|
214
|
+
const [conductorPrPrompt, setConductorPrPrompt] = useState(
|
|
215
|
+
existing.conductor?.prompts?.pr ?? DEFAULT_CONDUCTOR_PR_PROMPT
|
|
216
|
+
);
|
|
205
217
|
const [gitTrunkBranch, setGitTrunkBranch] = useState(existing.git?.trunkBranch ?? "");
|
|
206
218
|
const initialReleaseFlowIndex = RELEASE_FLOW_OPTIONS.findIndex((o) => o.value === existing.git?.releaseFlow);
|
|
207
219
|
const [releaseFlowIndex, setReleaseFlowIndex] = useState(Math.max(initialReleaseFlowIndex, 0));
|
|
@@ -391,6 +403,28 @@ function ConfigWizard() {
|
|
|
391
403
|
},
|
|
392
404
|
onBack: () => setScreen("prompts-menu")
|
|
393
405
|
},
|
|
406
|
+
"conductor-issue-prompt": {
|
|
407
|
+
setValue: setConductorIssuePrompt,
|
|
408
|
+
onSubmit: () => {
|
|
409
|
+
savePrompt("conductor-issue.md", conductorIssuePrompt, (value, current) => ({
|
|
410
|
+
...current,
|
|
411
|
+
conductor: { ...current.conductor, prompts: { ...current.conductor?.prompts, issue: value } }
|
|
412
|
+
}));
|
|
413
|
+
setScreen("prompts-menu");
|
|
414
|
+
},
|
|
415
|
+
onBack: () => setScreen("prompts-menu")
|
|
416
|
+
},
|
|
417
|
+
"conductor-pr-prompt": {
|
|
418
|
+
setValue: setConductorPrPrompt,
|
|
419
|
+
onSubmit: () => {
|
|
420
|
+
savePrompt("conductor-pr.md", conductorPrPrompt, (value, current) => ({
|
|
421
|
+
...current,
|
|
422
|
+
conductor: { ...current.conductor, prompts: { ...current.conductor?.prompts, pr: value } }
|
|
423
|
+
}));
|
|
424
|
+
setScreen("prompts-menu");
|
|
425
|
+
},
|
|
426
|
+
onBack: () => setScreen("prompts-menu")
|
|
427
|
+
},
|
|
394
428
|
"git-trunk-branch": {
|
|
395
429
|
setValue: setGitTrunkBranch,
|
|
396
430
|
onSubmit: () => setScreen("git-release-flow"),
|
|
@@ -628,6 +662,18 @@ function ConfigWizard() {
|
|
|
628
662
|
value: doWorkPrOrphanPrompt,
|
|
629
663
|
hint: `Type prompt \xB7 Enter to save \xB7 ${BACK}`
|
|
630
664
|
},
|
|
665
|
+
"conductor-issue-prompt": {
|
|
666
|
+
title: "Prompts \u2014 Conductor \u2014 Issue",
|
|
667
|
+
label: "Instructions for the conductor on a watched issue:",
|
|
668
|
+
value: conductorIssuePrompt,
|
|
669
|
+
hint: `Type prompt \xB7 Enter to save \xB7 ${BACK}`
|
|
670
|
+
},
|
|
671
|
+
"conductor-pr-prompt": {
|
|
672
|
+
title: "Prompts \u2014 Conductor \u2014 PR",
|
|
673
|
+
label: "Instructions for the conductor on a watched pull request:",
|
|
674
|
+
value: conductorPrPrompt,
|
|
675
|
+
hint: `Type prompt \xB7 Enter to save \xB7 ${BACK}`
|
|
676
|
+
},
|
|
631
677
|
"git-trunk-branch": {
|
|
632
678
|
title: "Git \u2014 Trunk Branch",
|
|
633
679
|
label: "Branch publish-release releases to (blank = detect it from the remote):",
|
|
@@ -19,6 +19,8 @@ var DEFAULT_DO_WORK = {
|
|
|
19
19
|
var DEFAULT_DO_WORK_ISSUE_DISCUSS_PROMPT = "You are the agent named in the context below, working on a GitHub issue together with the people allowed to instruct you. Answer the messages marked NEW; the earlier messages are context only.\n\nDo not modify, create or delete any file in the repository, and do not create a branch or a pull request, UNLESS a message marked NEW explicitly asks you to implement the work. If it does: create a branch off the base branch named below, implement the change following the project's existing conventions, run the tests and the linter, and open a pull request whose body contains `Closes #<issue number>`.\n\nOtherwise do not touch the code at all: reply on the issue with the specification, the plan, or the open questions you need answered. Keep the reply short and concrete.\n\nEither way, always post a reply on the issue before you finish \u2014 including when you implemented and opened a pull request. Silence is indistinguishable from a crash, and the run will be reported as having produced no answer.\n\nPost the reply yourself with the GitHub CLI: write it to a temporary file outside the repository (the only file you may create in this case) and run `gh issue comment <issue-number> --body-file <file>`, or `gh pr comment <pr-number> --body-file <file>` when you are replying on a pull request. `do-work` does not publish your output, so printing the answer to stdout does not count as a posted reply.";
|
|
20
20
|
var DEFAULT_DO_WORK_PR_WORK_PROMPT = "You are the agent named in the context below, working on the pull request for a GitHub issue together with the people allowed to instruct you. Work on the branch named below, which is already checked out and up to date.\n\nAddress every message marked NEW and every unresolved review thread listed. Follow the project's existing conventions, run the tests and the linter, then commit and push to that branch. Do not merge the pull request and do not push to the base branch.\n\nOnce a review thread is fixed and pushed, resolve it (GraphQL `resolveReviewThread`) after replying; leave open any thread you did not fix. Reply on the pull request with a short summary of what you changed, or reply in the review thread when your answer belongs to a specific comment. Always post a reply \u2014 silence looks like a crash.";
|
|
21
21
|
var DEFAULT_DO_WORK_PR_ORPHAN_PROMPT = "You are the agent named in the context below, working on a pull request that is not linked to any issue \u2014 a dependency bump, or another change opened without one \u2014 together with the people allowed to instruct you. Work on the branch named below, which is already checked out and up to date.\n\nAddress every message marked NEW and every unresolved review thread listed. Typical work here is finding out why the checks are failing, bringing the branch up to date with the base branch, and fixing whatever the change needs in order to be mergeable. Follow the project's existing conventions, run the tests and the linter, then commit and push to that branch.\n\nDo not merge the pull request, do not close it, and do not push to the base branch. If you conclude that it should be merged or closed, say so in your reply and leave the decision to the humans.\n\nOnce a review thread is fixed and pushed, resolve it (GraphQL `resolveReviewThread`) after replying; leave open any thread you did not fix. Reply on the pull request with a short summary of what you did and what you recommend, or reply in the review thread when your answer belongs to a specific comment. Always post a reply \u2014 silence looks like a crash.";
|
|
22
|
+
var DEFAULT_CONDUCTOR_ISSUE_PROMPT = "You are the conductor: you act for the people allowed to instruct the agent named in the context below. The agent spoke last on the thread below, and no allowed user has answered yet.\n\nRead the whole thread \u2014 the issue, its pull requests, the unresolved review threads and the CI status. Decide what the agent needs next: an answer to its question, a decision, or a correction. Write the next message to the agent, short and concrete. Do not change any file, do not merge or close anything, and do not push to any branch.";
|
|
23
|
+
var DEFAULT_CONDUCTOR_PR_PROMPT = "You are the conductor: you act for the people allowed to instruct the agent named in the context below. The agent spoke last on the pull request below, and no allowed user has answered yet.\n\nRead the whole thread \u2014 the pull request, the unresolved review threads and the CI status. Decide what the agent needs next: an answer to its question, a decision on a review thread, or a correction. A failing check is the agent's work to fix; name it if the agent has not. Write the next message to the agent, short and concrete. Do not change any file, do not merge or close the pull request, and do not push to any branch.";
|
|
22
24
|
var CONFIG_DIR = ".automata";
|
|
23
25
|
var CONFIG_FILE = "config.json";
|
|
24
26
|
function configPath() {
|
|
@@ -87,6 +89,12 @@ function readConfig() {
|
|
|
87
89
|
if (config.doWork?.prompts?.prOrphan) {
|
|
88
90
|
config.doWork.prompts.prOrphan = resolvePromptRef(config.doWork.prompts.prOrphan, dir);
|
|
89
91
|
}
|
|
92
|
+
if (config.conductor?.prompts?.issue) {
|
|
93
|
+
config.conductor.prompts.issue = resolvePromptRef(config.conductor.prompts.issue, dir);
|
|
94
|
+
}
|
|
95
|
+
if (config.conductor?.prompts?.pr) {
|
|
96
|
+
config.conductor.prompts.pr = resolvePromptRef(config.conductor.prompts.pr, dir);
|
|
97
|
+
}
|
|
90
98
|
return config;
|
|
91
99
|
}
|
|
92
100
|
function writeConfig(config) {
|
|
@@ -104,6 +112,8 @@ export {
|
|
|
104
112
|
DEFAULT_DO_WORK_ISSUE_DISCUSS_PROMPT,
|
|
105
113
|
DEFAULT_DO_WORK_PR_WORK_PROMPT,
|
|
106
114
|
DEFAULT_DO_WORK_PR_ORPHAN_PROMPT,
|
|
115
|
+
DEFAULT_CONDUCTOR_ISSUE_PROMPT,
|
|
116
|
+
DEFAULT_CONDUCTOR_PR_PROMPT,
|
|
107
117
|
readRawConfig,
|
|
108
118
|
readConfig,
|
|
109
119
|
writeConfig
|
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,
|
|
@@ -11,7 +13,7 @@ import {
|
|
|
11
13
|
readConfig,
|
|
12
14
|
readRawConfig,
|
|
13
15
|
writeConfig
|
|
14
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-TSOUCFDW.js";
|
|
15
17
|
|
|
16
18
|
// src/index.ts
|
|
17
19
|
import { Command as Command8 } from "commander";
|
|
@@ -293,6 +295,65 @@ var configSetDoWorkPrompt = new Command("do-work-prompt").description("Set the t
|
|
|
293
295
|
process.stdout.write(`do-work ${turnKind} prompt set.
|
|
294
296
|
`);
|
|
295
297
|
});
|
|
298
|
+
var CONDUCTOR_PROMPT_KINDS = ["issue", "pr"];
|
|
299
|
+
var configSetConductorPrompt = new Command("conductor-prompt").description("Set the instructions for the conductor on a watched issue or pull request (prompt text or a .md filename)").argument("<kind>", `Kind of watched item: ${CONDUCTOR_PROMPT_KINDS.join(", ")}`).argument("<value>", "Prompt text, or a plain .md filename inside .automata/").action((kind, value) => {
|
|
300
|
+
if (!CONDUCTOR_PROMPT_KINDS.includes(kind)) {
|
|
301
|
+
process.stderr.write(
|
|
302
|
+
`Error: invalid kind "${kind}". Must be one of: ${CONDUCTOR_PROMPT_KINDS.join(", ")}
|
|
303
|
+
`
|
|
304
|
+
);
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
307
|
+
const prompt = value.trim();
|
|
308
|
+
if (prompt.length === 0) {
|
|
309
|
+
process.stderr.write("Error: conductor-prompt requires a non-empty value.\n");
|
|
310
|
+
process.exit(1);
|
|
311
|
+
}
|
|
312
|
+
const current = readRawConfig();
|
|
313
|
+
const prompts = { ...current.conductor?.prompts, [kind]: prompt };
|
|
314
|
+
writeConfig({ ...current, conductor: { ...current.conductor, prompts } });
|
|
315
|
+
process.stdout.write(`conductor ${kind} prompt set.
|
|
316
|
+
`);
|
|
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
|
+
});
|
|
296
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) => {
|
|
297
358
|
const branch = value.trim();
|
|
298
359
|
if (branch.length === 0) {
|
|
@@ -316,12 +377,12 @@ var configSetGitReleaseFlow = new Command("git-release-flow").description("Pin t
|
|
|
316
377
|
process.stdout.write(`git release flow set to: ${flow}
|
|
317
378
|
`);
|
|
318
379
|
});
|
|
319
|
-
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(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);
|
|
320
381
|
var configCommand = new Command("config").description("Configure automata settings").addCommand(configSet).action(async () => {
|
|
321
382
|
const [{ render }, React, { ConfigWizard }] = await Promise.all([
|
|
322
383
|
import("ink"),
|
|
323
384
|
import("react"),
|
|
324
|
-
import("./ConfigWizard-
|
|
385
|
+
import("./ConfigWizard-BYPYEVVN.js")
|
|
325
386
|
]);
|
|
326
387
|
const { waitUntilExit } = render(React.createElement(ConfigWizard));
|
|
327
388
|
await waitUntilExit();
|
|
@@ -372,8 +433,8 @@ function extractIdentity(stdout) {
|
|
|
372
433
|
if (typeof id === "string" && id) return id;
|
|
373
434
|
if (id && typeof id === "object") {
|
|
374
435
|
const o = id;
|
|
375
|
-
for (const
|
|
376
|
-
if (typeof o[
|
|
436
|
+
for (const key2 of ["displayName", "uniqueName", "providerDisplayName", "id"]) {
|
|
437
|
+
if (typeof o[key2] === "string" && o[key2]) return o[key2];
|
|
377
438
|
}
|
|
378
439
|
}
|
|
379
440
|
} catch {
|
|
@@ -2520,9 +2581,23 @@ function getPrSurface(prNumber) {
|
|
|
2520
2581
|
},
|
|
2521
2582
|
assignees: (raw.assignees ?? []).map(login).filter((name) => name.length > 0),
|
|
2522
2583
|
messages,
|
|
2523
|
-
threads
|
|
2584
|
+
threads,
|
|
2585
|
+
description: { author: login(raw.author), body: raw.body }
|
|
2524
2586
|
};
|
|
2525
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
|
+
}
|
|
2526
2601
|
function assignIssueToAgent(issueNumber, agentUser) {
|
|
2527
2602
|
const { stderr, status } = run4("gh", [
|
|
2528
2603
|
"issue",
|
|
@@ -2800,9 +2875,47 @@ async function terminateTrackedChildren(timeoutMs = 1e4, killGraceMs = 5e3) {
|
|
|
2800
2875
|
}
|
|
2801
2876
|
|
|
2802
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
|
+
}
|
|
2803
2905
|
function buildClaudeArgs(prompt, options = {}) {
|
|
2804
2906
|
const args = [];
|
|
2805
|
-
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
|
+
}
|
|
2806
2919
|
if (options.model) args.push("--model", options.model);
|
|
2807
2920
|
if (options.effort) args.push("--effort", options.effort);
|
|
2808
2921
|
if (options.verbose) args.push("--verbose", "--output-format", "stream-json");
|
|
@@ -2814,12 +2927,17 @@ function runClaude(prompt, options = {}) {
|
|
|
2814
2927
|
const claudeBin = resolveCommand("claude");
|
|
2815
2928
|
const args = buildClaudeArgs(prompt, {
|
|
2816
2929
|
yolo: true,
|
|
2930
|
+
readOnly: options.readOnly,
|
|
2931
|
+
replyTo: options.replyTo,
|
|
2817
2932
|
model: options.model,
|
|
2818
2933
|
effort: options.effort,
|
|
2819
2934
|
verbose: true
|
|
2820
2935
|
});
|
|
2821
2936
|
const sink2 = options.sink;
|
|
2822
|
-
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
|
+
});
|
|
2823
2941
|
trackChild(child);
|
|
2824
2942
|
child.stderr.on("data", (chunk) => {
|
|
2825
2943
|
process.stderr.write(chunk);
|
|
@@ -2861,21 +2979,21 @@ function runClaude(prompt, options = {}) {
|
|
|
2861
2979
|
}
|
|
2862
2980
|
function invokeClaudeCode(prompt, options = {}) {
|
|
2863
2981
|
if (options.verbose) {
|
|
2864
|
-
return invokeClaudeCodeVerbose(prompt, options
|
|
2982
|
+
return invokeClaudeCodeVerbose(prompt, options);
|
|
2865
2983
|
}
|
|
2866
|
-
invokeClaudeCodeSync(prompt, options
|
|
2984
|
+
invokeClaudeCodeSync(prompt, options);
|
|
2867
2985
|
}
|
|
2868
|
-
function invokeClaudeCodeSync(prompt,
|
|
2986
|
+
function invokeClaudeCodeSync(prompt, options) {
|
|
2869
2987
|
const claudeBin = resolveCommand("claude");
|
|
2870
|
-
const args = buildClaudeArgs(prompt, {
|
|
2988
|
+
const args = buildClaudeArgs(prompt, { ...options, verbose: false });
|
|
2871
2989
|
const result = spawnSync6(claudeBin, args, { encoding: "utf8", stdio: "inherit" });
|
|
2872
2990
|
handleSpawnError(result.error, "claude");
|
|
2873
2991
|
handleExitCode(result.status, "Claude Code");
|
|
2874
2992
|
}
|
|
2875
|
-
function invokeClaudeCodeVerbose(prompt,
|
|
2993
|
+
function invokeClaudeCodeVerbose(prompt, options) {
|
|
2876
2994
|
return new Promise((resolve2) => {
|
|
2877
2995
|
const claudeBin = resolveCommand("claude");
|
|
2878
|
-
const args = buildClaudeArgs(prompt, {
|
|
2996
|
+
const args = buildClaudeArgs(prompt, { ...options, verbose: true });
|
|
2879
2997
|
const child = spawn(claudeBin, args, { stdio: ["inherit", "pipe", "inherit"] });
|
|
2880
2998
|
const rl = createInterface({ input: child.stdout });
|
|
2881
2999
|
let turnCount = 0;
|
|
@@ -2980,7 +3098,8 @@ function toTomlBasicString(value) {
|
|
|
2980
3098
|
}
|
|
2981
3099
|
function buildCodexArgs(prompt, options = {}) {
|
|
2982
3100
|
const args = ["exec"];
|
|
2983
|
-
if (options.
|
|
3101
|
+
if (options.readOnly) args.push("--sandbox", "read-only");
|
|
3102
|
+
else if (options.yolo) args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
2984
3103
|
if (options.model) args.push("--model", options.model);
|
|
2985
3104
|
if (options.effort) args.push("-c", `model_reasoning_effort=${toTomlBasicString(options.effort)}`);
|
|
2986
3105
|
args.push(prompt);
|
|
@@ -2990,11 +3109,11 @@ function invokeCodexCode(prompt, options = {}) {
|
|
|
2990
3109
|
if (options.verbose) {
|
|
2991
3110
|
process.stderr.write("Warning: --verbose is not supported for Codex and will be ignored.\n");
|
|
2992
3111
|
}
|
|
2993
|
-
invokeCodexCodeSync(prompt, options
|
|
3112
|
+
invokeCodexCodeSync(prompt, options);
|
|
2994
3113
|
}
|
|
2995
|
-
function invokeCodexCodeSync(prompt,
|
|
3114
|
+
function invokeCodexCodeSync(prompt, options) {
|
|
2996
3115
|
const codexBin = resolveCommand("codex");
|
|
2997
|
-
const args = buildCodexArgs(prompt,
|
|
3116
|
+
const args = buildCodexArgs(prompt, options);
|
|
2998
3117
|
const result = spawnSync7(codexBin, args, { encoding: "utf8", stdio: "inherit" });
|
|
2999
3118
|
handleSpawnError(result.error, "codex");
|
|
3000
3119
|
handleExitCode(result.status, "Codex");
|
|
@@ -3002,12 +3121,20 @@ function invokeCodexCodeSync(prompt, yolo, model, effort) {
|
|
|
3002
3121
|
function runCodex(prompt, options = {}) {
|
|
3003
3122
|
return new Promise((resolve2, reject) => {
|
|
3004
3123
|
const codexBin = resolveCommand("codex");
|
|
3005
|
-
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
|
+
});
|
|
3006
3130
|
const sink2 = options.sink;
|
|
3007
|
-
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
|
+
});
|
|
3008
3135
|
trackChild(child);
|
|
3009
3136
|
child.stdout.on("data", (chunk) => {
|
|
3010
|
-
process.stdout.write(chunk);
|
|
3137
|
+
if (options.quiet !== true) process.stdout.write(chunk);
|
|
3011
3138
|
sink2?.output("stdout", chunk.toString("utf8"));
|
|
3012
3139
|
});
|
|
3013
3140
|
child.stderr.on("data", (chunk) => {
|
|
@@ -3863,6 +3990,12 @@ function selectLinkedPr(linkedPrs) {
|
|
|
3863
3990
|
const open = linkedPrs.filter((pr) => pr.state === "OPEN");
|
|
3864
3991
|
return open.length === 0 ? null : newestPr(open);
|
|
3865
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
|
+
}
|
|
3866
3999
|
function claimStates(item) {
|
|
3867
4000
|
const states = [];
|
|
3868
4001
|
if (item.issue !== null) {
|
|
@@ -5419,9 +5552,9 @@ function summarizeSync(items) {
|
|
|
5419
5552
|
const counts = /* @__PURE__ */ new Map();
|
|
5420
5553
|
for (const item of items) {
|
|
5421
5554
|
if (item.sync === void 0) continue;
|
|
5422
|
-
const
|
|
5423
|
-
if (
|
|
5424
|
-
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);
|
|
5425
5558
|
}
|
|
5426
5559
|
if (counts.size === 0) return null;
|
|
5427
5560
|
return [...counts].map(([strategy, count]) => `${strategy}:${String(count)}`).join(",");
|
|
@@ -5670,9 +5803,9 @@ function parseExecution(bracket) {
|
|
|
5670
5803
|
if (bracket === void 0) return { executor: null, model: null, effort: null };
|
|
5671
5804
|
const parts = bracket.split(" ").filter((part) => part.length > 0);
|
|
5672
5805
|
const executor = parts[0] ?? null;
|
|
5673
|
-
const find = (
|
|
5674
|
-
const hit = parts.find((part) => part.startsWith(`${
|
|
5675
|
-
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);
|
|
5676
5809
|
};
|
|
5677
5810
|
return { executor, model: find("model"), effort: find("effort") };
|
|
5678
5811
|
}
|
|
@@ -6685,15 +6818,15 @@ function describeRefusedRun(item, refusal) {
|
|
|
6685
6818
|
function isPlainObject(value) {
|
|
6686
6819
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6687
6820
|
}
|
|
6688
|
-
function validateOptionalString(container,
|
|
6689
|
-
const value = container[
|
|
6821
|
+
function validateOptionalString(container, key2, path) {
|
|
6822
|
+
const value = container[key2];
|
|
6690
6823
|
if (value === void 0 || value === null) return;
|
|
6691
6824
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
6692
6825
|
failSettings(`${path} must be a non-empty string.`);
|
|
6693
6826
|
}
|
|
6694
6827
|
}
|
|
6695
|
-
function validateOptionalInt(container,
|
|
6696
|
-
const value = container[
|
|
6828
|
+
function validateOptionalInt(container, key2, path, min, hint) {
|
|
6829
|
+
const value = container[key2];
|
|
6697
6830
|
if (value === void 0 || value === null) return;
|
|
6698
6831
|
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < min) {
|
|
6699
6832
|
failSettings(`${path} must be ${hint}, got ${JSON.stringify(value)}.`);
|
|
@@ -6718,8 +6851,8 @@ function validateDoWorkConfig(section) {
|
|
|
6718
6851
|
validateSettingContainer(section["effort"], "effort", ["claude", "codex"]);
|
|
6719
6852
|
validateSettingContainer(section["prompts"], "prompts", ["issueDiscuss", "prWork", "prOrphan"]);
|
|
6720
6853
|
}
|
|
6721
|
-
function validateOptionalBoolean(container,
|
|
6722
|
-
const value = container[
|
|
6854
|
+
function validateOptionalBoolean(container, key2, path) {
|
|
6855
|
+
const value = container[key2];
|
|
6723
6856
|
if (value === void 0 || value === null) return;
|
|
6724
6857
|
if (typeof value !== "boolean") {
|
|
6725
6858
|
failSettings(`${path} must be true or false, got ${JSON.stringify(value)}.`);
|
|
@@ -6737,12 +6870,12 @@ function validateSettingContainer(value, container, keys) {
|
|
|
6737
6870
|
if (!isPlainObject(value)) {
|
|
6738
6871
|
failSettings(`doWork.${container} must be an object, got ${JSON.stringify(value)}.`);
|
|
6739
6872
|
}
|
|
6740
|
-
for (const
|
|
6741
|
-
validateOptionalString(value,
|
|
6873
|
+
for (const key2 of keys) {
|
|
6874
|
+
validateOptionalString(value, key2, `doWork.${container}.${key2}`);
|
|
6742
6875
|
}
|
|
6743
|
-
for (const
|
|
6744
|
-
if (!keys.includes(
|
|
6745
|
-
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(", ")}.`);
|
|
6746
6879
|
}
|
|
6747
6880
|
}
|
|
6748
6881
|
}
|
|
@@ -8290,6 +8423,249 @@ import { Command as Command7 } from "commander";
|
|
|
8290
8423
|
function isWatchClosed(state) {
|
|
8291
8424
|
return state === "closed";
|
|
8292
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
|
+
}
|
|
8293
8669
|
|
|
8294
8670
|
// src/conductor/watchList.ts
|
|
8295
8671
|
function parseWatchId(raw) {
|
|
@@ -8348,6 +8724,82 @@ function pruneWatchList(config) {
|
|
|
8348
8724
|
}
|
|
8349
8725
|
}
|
|
8350
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
|
+
}
|
|
8351
8803
|
}
|
|
8352
8804
|
function loadGitHubConfig() {
|
|
8353
8805
|
const config = readRawConfig();
|
|
@@ -8419,43 +8871,59 @@ function runWatchList() {
|
|
|
8419
8871
|
function exitWith(code) {
|
|
8420
8872
|
if (code !== 0) process.exit(code);
|
|
8421
8873
|
}
|
|
8422
|
-
function
|
|
8874
|
+
function preflight() {
|
|
8423
8875
|
let config;
|
|
8424
8876
|
try {
|
|
8425
8877
|
config = readConfig();
|
|
8426
8878
|
} catch (err) {
|
|
8427
|
-
return
|
|
8879
|
+
return { problem: err.message };
|
|
8428
8880
|
}
|
|
8429
8881
|
if (!isExplicitGitHub(config)) {
|
|
8430
|
-
return
|
|
8431
|
-
azdoUnsupportedMessage(
|
|
8882
|
+
return {
|
|
8883
|
+
problem: azdoUnsupportedMessage(
|
|
8432
8884
|
"conductor",
|
|
8433
8885
|
"It needs the GitHub APIs; set the remote with `automata config set type gh`."
|
|
8434
8886
|
)
|
|
8435
|
-
|
|
8887
|
+
};
|
|
8436
8888
|
}
|
|
8437
8889
|
const allowedUsers = (config.allowedUsers ?? []).filter((user) => user.trim().length > 0);
|
|
8438
8890
|
if (allowedUsers.length === 0) {
|
|
8439
|
-
return
|
|
8891
|
+
return { problem: "No allowed users configured. Run `automata config set allowed-users <user1,user2>`." };
|
|
8440
8892
|
}
|
|
8441
8893
|
const agentUser = (config.agentUser ?? "").trim();
|
|
8442
8894
|
if (agentUser.length === 0) {
|
|
8443
|
-
return
|
|
8895
|
+
return { problem: "No agent user configured. Run `automata config set agent-user <login>`." };
|
|
8444
8896
|
}
|
|
8445
8897
|
let login2;
|
|
8446
8898
|
try {
|
|
8447
8899
|
login2 = getAuthenticatedLogin();
|
|
8448
8900
|
} catch (err) {
|
|
8449
|
-
return
|
|
8901
|
+
return { problem: `\`gh\` could not be queried: ${err.message}` };
|
|
8450
8902
|
}
|
|
8451
|
-
const
|
|
8452
|
-
if (
|
|
8903
|
+
const identityProblem = conductorIdentityProblemFor(login2, agentUser, allowedUsers);
|
|
8904
|
+
if (identityProblem !== null) return { problem: identityProblem };
|
|
8453
8905
|
const rawStale = config.doWork?.lockStaleMinutes;
|
|
8454
8906
|
if (rawStale !== void 0 && rawStale !== null) {
|
|
8455
8907
|
if (typeof rawStale !== "number" || !Number.isSafeInteger(rawStale) || rawStale < 1) {
|
|
8456
|
-
return
|
|
8908
|
+
return { problem: `doWork.lockStaleMinutes must be a positive integer, got ${JSON.stringify(rawStale)}.` };
|
|
8457
8909
|
}
|
|
8458
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;
|
|
8459
8927
|
const staleMinutes = config.doWork?.lockStaleMinutes ?? DEFAULT_DO_WORK.lockStaleMinutes;
|
|
8460
8928
|
const lock = acquireConductorLock(staleMinutes);
|
|
8461
8929
|
if (!lock.ok) {
|
|
@@ -8474,18 +8942,28 @@ function runConductor() {
|
|
|
8474
8942
|
return 0;
|
|
8475
8943
|
}
|
|
8476
8944
|
try {
|
|
8477
|
-
process.stdout.write(`Conductor: running as ${login2
|
|
8945
|
+
process.stdout.write(`Conductor: running as ${login2}.
|
|
8478
8946
|
`);
|
|
8479
|
-
pruneWatchList(config);
|
|
8480
|
-
|
|
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;
|
|
8481
8959
|
} finally {
|
|
8482
8960
|
lock.handle.release();
|
|
8483
8961
|
}
|
|
8484
8962
|
}
|
|
8485
8963
|
var conductorCommand = new Command7("conductor").description(
|
|
8486
8964
|
"Run one conductor tick as an allowed (human) account: verify the identity, take the conductor's own run lock, then act"
|
|
8487
|
-
).action(() => {
|
|
8488
|
-
exitWith(runConductor());
|
|
8965
|
+
).action(async () => {
|
|
8966
|
+
exitWith(await runConductor());
|
|
8489
8967
|
});
|
|
8490
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) => {
|
|
8491
8969
|
exitWith(runWatchAdd(id));
|