autopilot-code 0.0.5 → 0.0.6
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/.autopilot/autopilot.json +18 -3
- package/README.md +7 -6
- package/dist/cli.js +176 -0
- package/package.json +1 -1
- package/scripts/run_autopilot.py +81 -26
- package/scripts/run_opencode_issue.sh +292 -1
- package/templates/autopilot.json +7 -1
|
@@ -2,14 +2,29 @@
|
|
|
2
2
|
"enabled": true,
|
|
3
3
|
"repo": "bakkensoftware/autopilot",
|
|
4
4
|
"agent": "opencode",
|
|
5
|
+
"allowedMergeUsers": [
|
|
6
|
+
"kellyelton"
|
|
7
|
+
],
|
|
5
8
|
"issueLabels": {
|
|
6
|
-
"queue": [
|
|
9
|
+
"queue": [
|
|
10
|
+
"autopilot:todo"
|
|
11
|
+
],
|
|
7
12
|
"blocked": "autopilot:blocked",
|
|
8
13
|
"inProgress": "autopilot:in-progress",
|
|
9
14
|
"done": "autopilot:done"
|
|
10
15
|
},
|
|
11
|
-
"priorityLabels": [
|
|
16
|
+
"priorityLabels": [
|
|
17
|
+
"p0",
|
|
18
|
+
"p1",
|
|
19
|
+
"p2"
|
|
20
|
+
],
|
|
21
|
+
"minPriority": null,
|
|
22
|
+
"ignoreIssueLabels": [
|
|
23
|
+
"autopilot:backlog"
|
|
24
|
+
],
|
|
12
25
|
"maxParallel": 1,
|
|
13
26
|
"branchPrefix": "autopilot/",
|
|
14
|
-
"allowedBaseBranch": "main"
|
|
27
|
+
"allowedBaseBranch": "main",
|
|
28
|
+
"autoResolveConflicts": true,
|
|
29
|
+
"conflictResolutionMaxAttempts": 3
|
|
15
30
|
}
|
package/README.md
CHANGED
|
@@ -22,6 +22,8 @@ Example:
|
|
|
22
22
|
"enabled": true,
|
|
23
23
|
"repo": "bakkensoftware/autopilot",
|
|
24
24
|
"agent": "opencode",
|
|
25
|
+
"autoMerge": false,
|
|
26
|
+
"allowedMergeUsers": ["github-username"],
|
|
25
27
|
"issueLabels": {
|
|
26
28
|
"queue": ["autopilot:todo"],
|
|
27
29
|
"blocked": "autopilot:blocked",
|
|
@@ -39,6 +41,8 @@ Example:
|
|
|
39
41
|
Notes:
|
|
40
42
|
- `repo` must be the GitHub `owner/name`.
|
|
41
43
|
- `agent` (optional, default `"none"`): set to `"opencode"` to enable OpenCode integration after claiming issues.
|
|
44
|
+
- `autoMerge` (optional, default `false`): if `true`, autopilot will automatically merge PRs after they are created.
|
|
45
|
+
- `allowedMergeUsers` (required when `autoMerge=true`): list of GitHub usernames allowed to auto-merge. The runner verifies the authenticated GitHub user is in this list before merging.
|
|
42
46
|
- `ignoreIssueLabels` (optional, default `["autopilot:backlog"]`): issues with any of these labels will be ignored by the runner.
|
|
43
47
|
- `heartbeatMaxAgeSecs` controls how long an in-progress issue can go without a heartbeat before it's considered stale.
|
|
44
48
|
|
|
@@ -117,13 +121,10 @@ node dist/cli.js service --foreground --interval-seconds 60 --root /mnt/f/Source
|
|
|
117
121
|
The foreground service mode runs continuously with the specified interval and logs to stdout. Press Ctrl+C to shut down cleanly.
|
|
118
122
|
|
|
119
123
|
## Roadmap
|
|
120
|
-
- Spawn a coding agent (Claude Code / OpenCode) in a worktree per issue
|
|
121
|
-
- Create PRs linked to issues; wait for checks to go green
|
|
122
|
-
- Merge PRs automatically when mergeable + checks pass
|
|
124
|
+
- ~~Spawn a coding agent (Claude Code / OpenCode) in a worktree per issue~~ (done)
|
|
125
|
+
- ~~Create PRs linked to issues; wait for checks to go green~~ (done)
|
|
126
|
+
- ~~Merge PRs automatically when mergeable + checks pass~~ (done)
|
|
123
127
|
- Close issues + apply `autopilot:done`
|
|
124
|
-
- Richer durable state:
|
|
125
|
-
- PR number / branch name / last heartbeat comment
|
|
126
|
-
- “blocked reason” codified
|
|
127
128
|
|
|
128
129
|
## Config template
|
|
129
130
|
See `templates/autopilot.json`.
|
package/dist/cli.js
CHANGED
|
@@ -14,6 +14,8 @@ const packageJson = JSON.parse((0, node_fs_1.readFileSync)(node_path_1.default.j
|
|
|
14
14
|
const version = packageJson.version;
|
|
15
15
|
const GLOBAL_CONFIG_DIR = node_path_1.default.join(process.env.HOME || "", ".config", "autopilot");
|
|
16
16
|
const GLOBAL_CONFIG_FILE = node_path_1.default.join(GLOBAL_CONFIG_DIR, "config.json");
|
|
17
|
+
const SYSTEM_DIR = "/etc/systemd/system";
|
|
18
|
+
const USER_DIR = node_path_1.default.join(process.env.HOME || "", ".config", "systemd", "user");
|
|
17
19
|
function run(cmd, args) {
|
|
18
20
|
const res = (0, node_child_process_1.spawnSync)(cmd, args, { stdio: "inherit" });
|
|
19
21
|
if (res.error)
|
|
@@ -107,6 +109,150 @@ async function collectSourceFolders() {
|
|
|
107
109
|
}
|
|
108
110
|
return folders;
|
|
109
111
|
}
|
|
112
|
+
function isRoot() {
|
|
113
|
+
return process.getuid?.() === 0;
|
|
114
|
+
}
|
|
115
|
+
function getSystemdPaths() {
|
|
116
|
+
const useSystem = isRoot();
|
|
117
|
+
const unitPath = useSystem ? node_path_1.default.join(SYSTEM_DIR, "autopilot.service") : node_path_1.default.join(USER_DIR, "autopilot.service");
|
|
118
|
+
const logPath = node_path_1.default.join(GLOBAL_CONFIG_DIR, "autopilot.log");
|
|
119
|
+
return { unitPath, logPath, useSystem };
|
|
120
|
+
}
|
|
121
|
+
function generateSystemdUnit(logPath, intervalSeconds) {
|
|
122
|
+
const execPath = process.execPath;
|
|
123
|
+
const args = [
|
|
124
|
+
"service",
|
|
125
|
+
"--foreground",
|
|
126
|
+
"--interval-seconds", intervalSeconds,
|
|
127
|
+
];
|
|
128
|
+
const user = process.env.USER || "root";
|
|
129
|
+
return `[Unit]
|
|
130
|
+
Description=Autopilot automated issue runner
|
|
131
|
+
After=network.target
|
|
132
|
+
|
|
133
|
+
[Service]
|
|
134
|
+
Type=simple
|
|
135
|
+
ExecStart=${execPath} ${args.join(" ")}
|
|
136
|
+
Restart=always
|
|
137
|
+
RestartSec=10
|
|
138
|
+
StandardOutput=journal
|
|
139
|
+
StandardError=journal
|
|
140
|
+
SyslogIdentifier=autopilot
|
|
141
|
+
${isRoot() ? `User=${user}` : ""}
|
|
142
|
+
Environment="NODE_ENV=production"
|
|
143
|
+
|
|
144
|
+
[Install]
|
|
145
|
+
WantedBy=multi-user.target
|
|
146
|
+
`;
|
|
147
|
+
}
|
|
148
|
+
function systemctl(cmd, args, check = false) {
|
|
149
|
+
const cmdArgs = [cmd, ...args];
|
|
150
|
+
const result = (0, node_child_process_1.spawnSync)("systemctl", cmdArgs, { stdio: "inherit" });
|
|
151
|
+
if (result.error) {
|
|
152
|
+
console.error(`Failed to run systemctl: ${result.error.message}`);
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
if (result.status !== 0) {
|
|
156
|
+
if (!check) {
|
|
157
|
+
console.error(`systemctl ${cmd} failed with exit code ${result.status}`);
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
function installSystemdService() {
|
|
164
|
+
const { unitPath, logPath, useSystem } = getSystemdPaths();
|
|
165
|
+
const intervalSeconds = "60";
|
|
166
|
+
if (!(0, node_fs_1.existsSync)(GLOBAL_CONFIG_FILE)) {
|
|
167
|
+
console.error("Configuration not found. Run 'autopilot init' first.");
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
if (!systemctl("--version", [], true)) {
|
|
171
|
+
console.error("systemd is not available on this system.");
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
const unitDir = node_path_1.default.dirname(unitPath);
|
|
175
|
+
if (!(0, node_fs_1.existsSync)(unitDir)) {
|
|
176
|
+
(0, node_fs_1.mkdirSync)(unitDir, { recursive: true });
|
|
177
|
+
}
|
|
178
|
+
const unitContent = generateSystemdUnit(logPath, intervalSeconds);
|
|
179
|
+
(0, node_fs_1.writeFileSync)(unitPath, unitContent, { mode: 0o644 });
|
|
180
|
+
console.log(`Systemd unit file created at: ${unitPath}`);
|
|
181
|
+
const daemonReloadArgs = useSystem ? [] : ["--user"];
|
|
182
|
+
if (!systemctl("daemon-reload", daemonReloadArgs)) {
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
if (!systemctl("enable", ["autopilot.service", ...daemonReloadArgs])) {
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
if (!systemctl("start", ["autopilot.service", ...daemonReloadArgs])) {
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
console.log("✅ Autopilot service installed, enabled and started.");
|
|
192
|
+
console.log(`\nUse: journalctl ${useSystem ? "" : "--user "} -u autopilot -f`);
|
|
193
|
+
}
|
|
194
|
+
function uninstallSystemdService() {
|
|
195
|
+
const { unitPath, useSystem } = getSystemdPaths();
|
|
196
|
+
if (!(0, node_fs_1.existsSync)(unitPath)) {
|
|
197
|
+
console.log("Service not installed.");
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const daemonReloadArgs = useSystem ? [] : ["--user"];
|
|
201
|
+
if (!systemctl("stop", ["autopilot.service", ...daemonReloadArgs], true)) {
|
|
202
|
+
console.log("Service not running, continuing...");
|
|
203
|
+
}
|
|
204
|
+
if (!systemctl("disable", ["autopilot.service", ...daemonReloadArgs], true)) {
|
|
205
|
+
console.log("Service not enabled, continuing...");
|
|
206
|
+
}
|
|
207
|
+
const { spawnSync } = require("node:child_process");
|
|
208
|
+
const { unlinkSync } = require("node:fs");
|
|
209
|
+
try {
|
|
210
|
+
unlinkSync(unitPath);
|
|
211
|
+
console.log(`Removed unit file: ${unitPath}`);
|
|
212
|
+
}
|
|
213
|
+
catch (err) {
|
|
214
|
+
console.error(`Failed to remove unit file: ${err.message}`);
|
|
215
|
+
}
|
|
216
|
+
if (!systemctl("daemon-reload", daemonReloadArgs)) {
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
console.log("✅ Autopilot service uninstalled.");
|
|
220
|
+
}
|
|
221
|
+
function startSystemdService() {
|
|
222
|
+
const { unitPath, useSystem } = getSystemdPaths();
|
|
223
|
+
if (!(0, node_fs_1.existsSync)(unitPath)) {
|
|
224
|
+
console.error("Service not installed. Run 'autopilot install' first.");
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
const daemonReloadArgs = useSystem ? [] : ["--user"];
|
|
228
|
+
if (!systemctl("start", ["autopilot.service", ...daemonReloadArgs])) {
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
console.log("✅ Autopilot service started.");
|
|
232
|
+
}
|
|
233
|
+
function stopSystemdService() {
|
|
234
|
+
const { unitPath, useSystem } = getSystemdPaths();
|
|
235
|
+
if (!(0, node_fs_1.existsSync)(unitPath)) {
|
|
236
|
+
console.error("Service not installed. Run 'autopilot install' first.");
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
const daemonReloadArgs = useSystem ? [] : ["--user"];
|
|
240
|
+
if (!systemctl("stop", ["autopilot.service", ...daemonReloadArgs], true)) {
|
|
241
|
+
console.log("Service not running or already stopped.");
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
console.log("✅ Autopilot service stopped.");
|
|
245
|
+
}
|
|
246
|
+
function statusSystemdService() {
|
|
247
|
+
const { unitPath, useSystem } = getSystemdPaths();
|
|
248
|
+
if (!(0, node_fs_1.existsSync)(unitPath)) {
|
|
249
|
+
console.log("Service not installed. Run 'autopilot install' first.");
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const daemonReloadArgs = useSystem ? [] : ["--user"];
|
|
253
|
+
const { spawnSync } = require("node:child_process");
|
|
254
|
+
spawnSync("systemctl", ["status", "autopilot.service", ...daemonReloadArgs], { stdio: "inherit" });
|
|
255
|
+
}
|
|
110
256
|
async function initCommand() {
|
|
111
257
|
console.log("🚀 Autopilot Setup\n");
|
|
112
258
|
const existingConfig = loadGlobalConfig();
|
|
@@ -232,4 +378,34 @@ program
|
|
|
232
378
|
process.exit(1);
|
|
233
379
|
}
|
|
234
380
|
});
|
|
381
|
+
program
|
|
382
|
+
.command("install")
|
|
383
|
+
.description("Install and enable autopilot as a systemd service")
|
|
384
|
+
.action(() => {
|
|
385
|
+
installSystemdService();
|
|
386
|
+
});
|
|
387
|
+
program
|
|
388
|
+
.command("uninstall")
|
|
389
|
+
.description("Uninstall and disable autopilot systemd service")
|
|
390
|
+
.action(() => {
|
|
391
|
+
uninstallSystemdService();
|
|
392
|
+
});
|
|
393
|
+
program
|
|
394
|
+
.command("start")
|
|
395
|
+
.description("Start the autopilot systemd service")
|
|
396
|
+
.action(() => {
|
|
397
|
+
startSystemdService();
|
|
398
|
+
});
|
|
399
|
+
program
|
|
400
|
+
.command("stop")
|
|
401
|
+
.description("Stop the autopilot systemd service")
|
|
402
|
+
.action(() => {
|
|
403
|
+
stopSystemdService();
|
|
404
|
+
});
|
|
405
|
+
program
|
|
406
|
+
.command("status")
|
|
407
|
+
.description("Show the status of the autopilot systemd service")
|
|
408
|
+
.action(() => {
|
|
409
|
+
statusSystemdService();
|
|
410
|
+
});
|
|
235
411
|
program.parse();
|
package/package.json
CHANGED
package/scripts/run_autopilot.py
CHANGED
|
@@ -76,6 +76,13 @@ class RepoConfig:
|
|
|
76
76
|
heartbeat_max_age_secs: int
|
|
77
77
|
agent: str
|
|
78
78
|
ignore_issue_labels: list[str]
|
|
79
|
+
min_priority: str | None
|
|
80
|
+
auto_merge: bool
|
|
81
|
+
allowed_merge_users: list[str]
|
|
82
|
+
auto_resolve_conflicts: bool
|
|
83
|
+
conflict_resolution_max_attempts: int
|
|
84
|
+
auto_fix_checks: bool
|
|
85
|
+
auto_fix_checks_max_attempts: int
|
|
79
86
|
|
|
80
87
|
|
|
81
88
|
def load_config(repo_root: Path) -> RepoConfig | None:
|
|
@@ -89,6 +96,21 @@ def load_config(repo_root: Path) -> RepoConfig | None:
|
|
|
89
96
|
labels = data.get("issueLabels", {})
|
|
90
97
|
queue = labels.get("queue", ["autopilot:todo"])
|
|
91
98
|
|
|
99
|
+
auto_merge = data.get("autoMerge", True)
|
|
100
|
+
allowed_merge_users = list(data.get("allowedMergeUsers", []))
|
|
101
|
+
|
|
102
|
+
if auto_merge and not allowed_merge_users:
|
|
103
|
+
raise RuntimeError(
|
|
104
|
+
"autoMerge is enabled but allowedMergeUsers list is empty or missing. "
|
|
105
|
+
"Please specify at least one allowed GitHub username."
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
auto_resolve_conflicts = data.get("autoResolveConflicts", True)
|
|
109
|
+
conflict_resolution_max_attempts = int(data.get("conflictResolutionMaxAttempts", 3))
|
|
110
|
+
|
|
111
|
+
auto_fix_checks = data.get("autoFixChecks", True)
|
|
112
|
+
auto_fix_checks_max_attempts = int(data.get("autoFixChecksMaxAttempts", 3))
|
|
113
|
+
|
|
92
114
|
return RepoConfig(
|
|
93
115
|
root=repo_root,
|
|
94
116
|
repo=data["repo"],
|
|
@@ -104,6 +126,13 @@ def load_config(repo_root: Path) -> RepoConfig | None:
|
|
|
104
126
|
),
|
|
105
127
|
agent=data.get("agent", "none"),
|
|
106
128
|
ignore_issue_labels=list(data.get("ignoreIssueLabels", ["autopilot:backlog"])),
|
|
129
|
+
min_priority=data.get("minPriority"),
|
|
130
|
+
auto_merge=auto_merge,
|
|
131
|
+
allowed_merge_users=allowed_merge_users,
|
|
132
|
+
auto_resolve_conflicts=auto_resolve_conflicts,
|
|
133
|
+
conflict_resolution_max_attempts=conflict_resolution_max_attempts,
|
|
134
|
+
auto_fix_checks=auto_fix_checks,
|
|
135
|
+
auto_fix_checks_max_attempts=auto_fix_checks_max_attempts,
|
|
107
136
|
)
|
|
108
137
|
|
|
109
138
|
|
|
@@ -184,6 +213,11 @@ def list_candidate_issues(cfg: RepoConfig, limit: int = 10) -> list[dict[str, An
|
|
|
184
213
|
return i
|
|
185
214
|
return 999
|
|
186
215
|
|
|
216
|
+
# Filter by minPriority if set
|
|
217
|
+
if cfg.min_priority and cfg.min_priority in cfg.priority_labels:
|
|
218
|
+
cutoff = cfg.priority_labels.index(cfg.min_priority)
|
|
219
|
+
issues = [it for it in issues if pri_rank(it) <= cutoff]
|
|
220
|
+
|
|
187
221
|
issues.sort(key=lambda x: (pri_rank(x), x.get("createdAt", "")))
|
|
188
222
|
return issues
|
|
189
223
|
|
|
@@ -216,7 +250,9 @@ def touch_heartbeat(cfg: RepoConfig, issue_number: int) -> None:
|
|
|
216
250
|
"""
|
|
217
251
|
state = load_state(cfg.root)
|
|
218
252
|
now = int(time.time())
|
|
219
|
-
|
|
253
|
+
if "activeIssues" not in state:
|
|
254
|
+
state["activeIssues"] = {}
|
|
255
|
+
state["activeIssues"][str(issue_number)] = {
|
|
220
256
|
"number": issue_number,
|
|
221
257
|
"updatedAt": now,
|
|
222
258
|
"repo": cfg.repo,
|
|
@@ -226,7 +262,13 @@ def touch_heartbeat(cfg: RepoConfig, issue_number: int) -> None:
|
|
|
226
262
|
|
|
227
263
|
def is_heartbeat_fresh(cfg: RepoConfig, issue_number: int) -> bool:
|
|
228
264
|
state = load_state(cfg.root)
|
|
229
|
-
|
|
265
|
+
active_issues = state.get("activeIssues", {})
|
|
266
|
+
if not isinstance(active_issues, dict):
|
|
267
|
+
return False
|
|
268
|
+
issue_key = str(issue_number)
|
|
269
|
+
if issue_key not in active_issues:
|
|
270
|
+
return False
|
|
271
|
+
active = active_issues[issue_key]
|
|
230
272
|
if not isinstance(active, dict):
|
|
231
273
|
return False
|
|
232
274
|
if int(active.get("number", -1)) != int(issue_number):
|
|
@@ -321,37 +363,50 @@ def run_cycle(
|
|
|
321
363
|
if not dry_run:
|
|
322
364
|
maybe_mark_blocked(cfg, it)
|
|
323
365
|
|
|
324
|
-
# 2)
|
|
325
|
-
|
|
326
|
-
|
|
366
|
+
# 2) Count active issues with fresh heartbeats from local state
|
|
367
|
+
state = load_state(cfg.root)
|
|
368
|
+
active_issues = state.get("activeIssues", {})
|
|
369
|
+
fresh_count = 0
|
|
370
|
+
if isinstance(active_issues, dict):
|
|
371
|
+
for issue_num_str, issue_data in active_issues.items():
|
|
372
|
+
if isinstance(issue_data, dict):
|
|
373
|
+
issue_num = int(issue_data.get("number", -1))
|
|
374
|
+
if is_heartbeat_fresh(cfg, issue_num):
|
|
375
|
+
fresh_count += 1
|
|
376
|
+
|
|
377
|
+
# 3) Determine how many new issues we can claim (respect maxParallel)
|
|
378
|
+
max_to_claim = cfg.max_parallel - fresh_count
|
|
379
|
+
if max_to_claim <= 0:
|
|
327
380
|
continue
|
|
328
381
|
|
|
382
|
+
# 4) Claim new issues from the queue (respecting priority labels)
|
|
329
383
|
issues = list_candidate_issues(cfg)
|
|
330
384
|
if not issues:
|
|
331
385
|
continue
|
|
332
386
|
|
|
333
|
-
issue
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
)
|
|
340
|
-
print(f"[{cfg.repo}] next issue: #{issue['number']} {issue['title']}")
|
|
341
|
-
if dry_run:
|
|
342
|
-
continue
|
|
343
|
-
claim_issue(cfg, issue, msg)
|
|
344
|
-
claimed_count += 1
|
|
345
|
-
|
|
346
|
-
# If agent==opencode, delegate to bash script
|
|
347
|
-
if cfg.agent == "opencode":
|
|
348
|
-
sh(
|
|
349
|
-
[
|
|
350
|
-
"/home/kellye/clawd/repos/autopilot/scripts/run_opencode_issue.sh",
|
|
351
|
-
str(cfg.root),
|
|
352
|
-
str(issue["number"]),
|
|
353
|
-
]
|
|
387
|
+
for issue in issues[:max_to_claim]:
|
|
388
|
+
msg = (
|
|
389
|
+
f"Autopilot claimed this issue at {time.strftime('%Y-%m-%d %H:%M:%S %Z')}.\n\n"
|
|
390
|
+
"Next: implement fix and open PR.\n\n"
|
|
391
|
+
"(Durable tracking: this repo will maintain `.autopilot/state.json` as a heartbeat; "
|
|
392
|
+
"the runner does not inspect processes.)"
|
|
354
393
|
)
|
|
394
|
+
print(f"[{cfg.repo}] next issue: #{issue['number']} {issue['title']}")
|
|
395
|
+
if dry_run:
|
|
396
|
+
claimed_count += 1
|
|
397
|
+
continue
|
|
398
|
+
claim_issue(cfg, issue, msg)
|
|
399
|
+
claimed_count += 1
|
|
400
|
+
|
|
401
|
+
# If agent==opencode, delegate to bash script
|
|
402
|
+
if cfg.agent == "opencode":
|
|
403
|
+
sh(
|
|
404
|
+
[
|
|
405
|
+
"/home/kellye/clawd/repos/autopilot/scripts/run_opencode_issue.sh",
|
|
406
|
+
str(cfg.root),
|
|
407
|
+
str(issue["number"]),
|
|
408
|
+
]
|
|
409
|
+
)
|
|
355
410
|
|
|
356
411
|
return claimed_count
|
|
357
412
|
|
|
@@ -11,11 +11,23 @@ fi
|
|
|
11
11
|
|
|
12
12
|
cd "$REPO_DIR"
|
|
13
13
|
|
|
14
|
-
# Read
|
|
14
|
+
# Read config (prefer jq, fallback to python3)
|
|
15
15
|
if command -v jq >/dev/null 2>&1; then
|
|
16
16
|
REPO=$(jq -r '.repo' < .autopilot/autopilot.json)
|
|
17
|
+
AUTO_MERGE=$(jq -r '.autoMerge // true' < .autopilot/autopilot.json)
|
|
18
|
+
AUTO_RESOLVE_CONFLICTS=$(jq -r '.autoResolveConflicts // true' < .autopilot/autopilot.json)
|
|
19
|
+
CONFLICT_RESOLUTION_MAX_ATTEMPTS=$(jq -r '.conflictResolutionMaxAttempts // 3' < .autopilot/autopilot.json)
|
|
20
|
+
AUTO_FIX_CHECKS=$(jq -r '.autoFixChecks // true' < .autopilot/autopilot.json)
|
|
21
|
+
AUTO_FIX_CHECKS_MAX_ATTEMPTS=$(jq -r '.autoFixChecksMaxAttempts // 3' < .autopilot/autopilot.json)
|
|
22
|
+
ALLOWED_USERS=$(jq -r '.allowedMergeUsers[]' < .autopilot/autopilot.json 2>/dev/null || true)
|
|
17
23
|
else
|
|
18
24
|
REPO=$(python3 -c 'import json; print(json.load(open(".autopilot/autopilot.json"))["repo"])')
|
|
25
|
+
AUTO_MERGE=$(python3 -c 'import json; print(json.load(open(".autopilot/autopilot.json")).get("autoMerge", True))')
|
|
26
|
+
AUTO_RESOLVE_CONFLICTS=$(python3 -c 'import json; print(json.load(open(".autopilot/autopilot.json")).get("autoResolveConflicts", True))')
|
|
27
|
+
CONFLICT_RESOLUTION_MAX_ATTEMPTS=$(python3 -c 'import json; print(json.load(open(".autopilot/autopilot.json")).get("conflictResolutionMaxAttempts", 3))')
|
|
28
|
+
AUTO_FIX_CHECKS=$(python3 -c 'import json; print(json.load(open(".autopilot/autopilot.json")).get("autoFixChecks", True))')
|
|
29
|
+
AUTO_FIX_CHECKS_MAX_ATTEMPTS=$(python3 -c 'import json; print(json.load(open(".autopilot/autopilot.json")).get("autoFixChecksMaxAttempts", 3))')
|
|
30
|
+
ALLOWED_USERS=$(python3 -c 'import json,sys; users=json.load(open(".autopilot/autopilot.json")).get("allowedMergeUsers", []); print("\n".join(users))' 2>/dev/null || true)
|
|
19
31
|
fi
|
|
20
32
|
|
|
21
33
|
WORKTREE="/tmp/autopilot-issue-$ISSUE_NUMBER"
|
|
@@ -83,4 +95,283 @@ if [[ -n "$PR_URL" ]]; then
|
|
|
83
95
|
gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "Autopilot opened PR: $PR_URL" || true
|
|
84
96
|
fi
|
|
85
97
|
|
|
98
|
+
# 8.5. Handle merge conflicts if auto-resolve is enabled
|
|
99
|
+
if [[ "$AUTO_RESOLVE_CONFLICTS" == "true" ]] && [[ -n "$PR_URL" ]]; then
|
|
100
|
+
echo "[run_opencode_issue.sh] Checking for merge conflicts..."
|
|
101
|
+
|
|
102
|
+
# Get PR merge status
|
|
103
|
+
MERGE_STATUS=$(gh pr view --repo "$REPO" --head "$BRANCH" --json mergeable,mergeStateStatus --jq '{mergeable, mergeStateStatus}' 2>/dev/null || echo '{"mergeable":"UNKNOWN"}')
|
|
104
|
+
|
|
105
|
+
if command -v jq >/dev/null 2>&1; then
|
|
106
|
+
IS_CONFLICTING=$(echo "$MERGE_STATUS" | jq -r 'if .mergeable == "CONFLICTING" or .mergeStateStatus == "DIRTY" then "true" else "false" end')
|
|
107
|
+
else
|
|
108
|
+
IS_CONFLICTING=$(python3 -c 'import json,sys; d=json.load(sys.stdin); print("true" if d.get("mergeable") == "CONFLICTING" or d.get("mergeStateStatus") == "DIRTY" else "false")' <<<"$MERGE_STATUS")
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
if [[ "$IS_CONFLICTING" == "true" ]]; then
|
|
112
|
+
echo "[run_opencode_issue.sh] PR has merge conflicts. Attempting auto-resolution..."
|
|
113
|
+
|
|
114
|
+
RESOLVE_ATTEMPT=0
|
|
115
|
+
MAX_ATTEMPTS=$((CONFLICT_RESOLUTION_MAX_ATTEMPTS + 0))
|
|
116
|
+
|
|
117
|
+
while [[ $RESOLVE_ATTEMPT -lt $MAX_ATTEMPTS ]]; do
|
|
118
|
+
RESOLVE_ATTEMPT=$((RESOLVE_ATTEMPT + 1))
|
|
119
|
+
echo "[run_opencode_issue.sh] Conflict resolution attempt $RESOLVE_ATTEMPT/$MAX_ATTEMPTS"
|
|
120
|
+
|
|
121
|
+
# Fetch latest main
|
|
122
|
+
echo "[run_opencode_issue.sh] Fetching latest main..."
|
|
123
|
+
git fetch origin main || true
|
|
124
|
+
|
|
125
|
+
# Checkout branch and try to rebase
|
|
126
|
+
echo "[run_opencode_issue.sh] Rebasing branch onto main..."
|
|
127
|
+
cd "$WORKTREE"
|
|
128
|
+
if git rebase origin/main; then
|
|
129
|
+
echo "[run_opencode_issue.sh] Rebase successful."
|
|
130
|
+
|
|
131
|
+
# Push rebased changes
|
|
132
|
+
git push -f origin "$BRANCH" || true
|
|
133
|
+
|
|
134
|
+
# Check if conflicts are resolved
|
|
135
|
+
MERGE_STATUS=$(gh pr view --repo "$REPO" --head "$BRANCH" --json mergeable,mergeStateStatus --jq '{mergeable, mergeStateStatus}' 2>/dev/null || echo '{"mergeable":"UNKNOWN"}')
|
|
136
|
+
|
|
137
|
+
if command -v jq >/dev/null 2>&1; then
|
|
138
|
+
IS_CONFLICTING=$(echo "$MERGE_STATUS" | jq -r 'if .mergeable == "CONFLICTING" or .mergeStateStatus == "DIRTY" then "true" else "false" end')
|
|
139
|
+
else
|
|
140
|
+
IS_CONFLICTING=$(python3 -c 'import json,sys; d=json.load(sys.stdin); print("true" if d.get("mergeable") == "CONFLICTING" or d.get("mergeStateStatus") == "DIRTY" else "false")' <<<"$MERGE_STATUS")
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
if [[ "$IS_CONFLICTING" == "false" ]]; then
|
|
144
|
+
echo "[run_opencode_issue.sh] Conflicts resolved successfully."
|
|
145
|
+
gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "✅ Autopilot successfully resolved merge conflicts." || true
|
|
146
|
+
break
|
|
147
|
+
fi
|
|
148
|
+
else
|
|
149
|
+
# Rebase failed - there are conflicts to resolve
|
|
150
|
+
echo "[run_opencode_issue.sh] Rebase encountered conflicts. Attempting resolution with agent..."
|
|
151
|
+
|
|
152
|
+
# Abort the rebase to get back to a clean state
|
|
153
|
+
git rebase --abort 2>/dev/null || true
|
|
154
|
+
|
|
155
|
+
# Try merging main instead (may be easier to resolve)
|
|
156
|
+
if git merge origin/main; then
|
|
157
|
+
echo "[run_opencode_issue.sh] Merge successful."
|
|
158
|
+
git push -f origin "$BRANCH" || true
|
|
159
|
+
|
|
160
|
+
# Check if conflicts are resolved
|
|
161
|
+
MERGE_STATUS=$(gh pr view --repo "$REPO" --head "$BRANCH" --json mergeable,mergeStateStatus --jq '{mergeable, mergeStateStatus}' 2>/dev/null || echo '{"mergeable":"UNKNOWN"}')
|
|
162
|
+
|
|
163
|
+
if command -v jq >/dev/null 2>&1; then
|
|
164
|
+
IS_CONFLICTING=$(echo "$MERGE_STATUS" | jq -r 'if .mergeable == "CONFLICTING" or .mergeStateStatus == "DIRTY" then "true" else "false" end')
|
|
165
|
+
else
|
|
166
|
+
IS_CONFLICTING=$(python3 -c 'import json,sys; d=json.load(sys.stdin); print("true" if d.get("mergeable") == "CONFLICTING" or d.get("mergeStateStatus") == "DIRTY" else "false")' <<<"$MERGE_STATUS")
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
if [[ "$IS_CONFLICTING" == "false" ]]; then
|
|
170
|
+
echo "[run_opencode_issue.sh] Conflicts resolved successfully."
|
|
171
|
+
gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "✅ Autopilot successfully resolved merge conflicts." || true
|
|
172
|
+
break
|
|
173
|
+
fi
|
|
174
|
+
else
|
|
175
|
+
# Merge also has conflicts - abort and try to resolve with agent
|
|
176
|
+
git merge --abort 2>/dev/null || true
|
|
177
|
+
|
|
178
|
+
echo "[run_opencode_issue.sh] Attempting to resolve conflicts with opencode agent..."
|
|
179
|
+
|
|
180
|
+
# Use opencode to resolve conflicts
|
|
181
|
+
CONFLICT_PROMPT="This PR has merge conflicts. Please resolve them by examining the conflicted files and making the necessary edits to resolve the conflicts. Follow these steps:
|
|
182
|
+
1. Run 'git status' to see all conflicted files
|
|
183
|
+
2. For each conflicted file, examine the conflict markers (<<<<<<<, =======, >>>>>>>)
|
|
184
|
+
3. Resolve the conflicts by choosing the appropriate code
|
|
185
|
+
4. Stage the resolved files with 'git add <file>'
|
|
186
|
+
5. Commit the resolved files if needed
|
|
187
|
+
6. The goal is to make the branch mergeable with main
|
|
188
|
+
|
|
189
|
+
After resolving all conflicts, report the files that were resolved."
|
|
190
|
+
|
|
191
|
+
if opencode run "$CONFLICT_PROMPT"; then
|
|
192
|
+
# Check if there are still conflicts
|
|
193
|
+
if [[ -z "$(git diff --name-only --diff-filter=U)" ]]; then
|
|
194
|
+
echo "[run_opencode_issue.sh] Conflicts resolved by agent."
|
|
195
|
+
|
|
196
|
+
# Complete the merge or rebase
|
|
197
|
+
git add -A
|
|
198
|
+
|
|
199
|
+
# If we were in the middle of a merge, complete it
|
|
200
|
+
if [[ -f ".git/MERGE_HEAD" ]]; then
|
|
201
|
+
git commit --no-edit || true
|
|
202
|
+
fi
|
|
203
|
+
|
|
204
|
+
# Push resolved changes
|
|
205
|
+
git push -f origin "$BRANCH" || true
|
|
206
|
+
|
|
207
|
+
# Check if conflicts are resolved
|
|
208
|
+
MERGE_STATUS=$(gh pr view --repo "$REPO" --head "$BRANCH" --json mergeable,mergeStateStatus --jq '{mergeable, mergeStateStatus}' 2>/dev/null || echo '{"mergeable":"UNKNOWN"}')
|
|
209
|
+
|
|
210
|
+
if command -v jq >/dev/null 2>&1; then
|
|
211
|
+
IS_CONFLICTING=$(echo "$MERGE_STATUS" | jq -r 'if .mergeable == "CONFLICTING" or .mergeStateStatus == "DIRTY" then "true" else "false" end')
|
|
212
|
+
else
|
|
213
|
+
IS_CONFLICTING=$(python3 -c 'import json,sys; d=json.load(sys.stdin); print("true" if d.get("mergeable") == "CONFLICTING" or d.get("mergeStateStatus") == "DIRTY" else "false")' <<<"$MERGE_STATUS")
|
|
214
|
+
fi
|
|
215
|
+
|
|
216
|
+
if [[ "$IS_CONFLICTING" == "false" ]]; then
|
|
217
|
+
echo "[run_opencode_issue.sh] Conflicts resolved successfully."
|
|
218
|
+
gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "✅ Autopilot successfully resolved merge conflicts." || true
|
|
219
|
+
break
|
|
220
|
+
fi
|
|
221
|
+
fi
|
|
222
|
+
fi
|
|
223
|
+
fi
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
# If we're here, resolution failed and we should try again
|
|
227
|
+
echo "[run_opencode_issue.sh] Conflict resolution attempt $RESOLVE_ATTEMPT failed."
|
|
228
|
+
|
|
229
|
+
if [[ $RESOLVE_ATTEMPT -ge $MAX_ATTEMPTS ]]; then
|
|
230
|
+
echo "[run_opencode_issue.sh] Failed to resolve conflicts after $MAX_ATTEMPTS attempts."
|
|
231
|
+
|
|
232
|
+
# Mark issue as blocked
|
|
233
|
+
if command -v jq >/dev/null 2>&1; then
|
|
234
|
+
LABEL_BLOCKED=$(jq -r '.issueLabels.blocked' < .autopilot/autopilot.json)
|
|
235
|
+
else
|
|
236
|
+
LABEL_BLOCKED=$(python3 -c 'import json; print(json.load(open(".autopilot/autopilot.json"))["issueLabels"]["blocked"])')
|
|
237
|
+
fi
|
|
238
|
+
|
|
239
|
+
BLOCKED_MSG="❌ Autopilot failed to resolve merge conflicts after $MAX_ATTEMPTS attempts.\n\nThe PR will remain open but autopilot has stopped attempting to resolve conflicts.\n\nTo resolve manually:\n1. Checkout the branch: \`git checkout $BRANCH\`\n2. Fetch and rebase main: \`git fetch origin main && git rebase origin/main\`\n3. Resolve conflicts as needed\n4. Push resolved changes: \`git push -f origin $BRANCH\`\n\nOnce resolved, autopilot can attempt to merge the PR."
|
|
240
|
+
|
|
241
|
+
gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "$BLOCKED_MSG" || true
|
|
242
|
+
gh issue edit "$ISSUE_NUMBER" --repo "$REPO" --add-label "$LABEL_BLOCKED" || true
|
|
243
|
+
|
|
244
|
+
exit 1
|
|
245
|
+
fi
|
|
246
|
+
|
|
247
|
+
# Wait a bit before next attempt
|
|
248
|
+
sleep 5
|
|
249
|
+
done
|
|
250
|
+
fi
|
|
251
|
+
fi
|
|
252
|
+
|
|
253
|
+
# 9. Auto-merge if enabled and user is allowed
|
|
254
|
+
if [[ "$AUTO_MERGE" == "true" ]] && [[ -n "$PR_URL" ]]; then
|
|
255
|
+
# Get authenticated GitHub user
|
|
256
|
+
AUTH_USER=$(gh api user --jq .login)
|
|
257
|
+
|
|
258
|
+
# Check if user is in allowed list
|
|
259
|
+
USER_ALLOWED=false
|
|
260
|
+
if [[ -n "$ALLOWED_USERS" ]]; then
|
|
261
|
+
while IFS= read -r user; do
|
|
262
|
+
if [[ "$AUTH_USER" == "$user" ]]; then
|
|
263
|
+
USER_ALLOWED=true
|
|
264
|
+
break
|
|
265
|
+
fi
|
|
266
|
+
done <<< "$ALLOWED_USERS"
|
|
267
|
+
fi
|
|
268
|
+
|
|
269
|
+
if [[ "$USER_ALLOWED" == "true" ]]; then
|
|
270
|
+
echo "[run_opencode_issue.sh] Auto-merge enabled. Waiting for PR checks to pass..."
|
|
271
|
+
|
|
272
|
+
# Initialize auto-fix attempt counter
|
|
273
|
+
FIX_ATTEMPT=0
|
|
274
|
+
|
|
275
|
+
# Poll PR checks until they pass or fail
|
|
276
|
+
MAX_POLL_ATTEMPTS=60
|
|
277
|
+
POLL_INTERVAL=30
|
|
278
|
+
POLL_ATTEMPT=0
|
|
279
|
+
|
|
280
|
+
while [[ $POLL_ATTEMPT -lt $MAX_POLL_ATTEMPTS ]]; do
|
|
281
|
+
POLL_ATTEMPT=$((POLL_ATTEMPT + 1))
|
|
282
|
+
|
|
283
|
+
# Get PR check status
|
|
284
|
+
CHECK_STATUS=$(gh pr view --repo "$REPO" --head "$BRANCH" --json mergeable,mergeStateStatus,statusCheckRollup --jq '.statusCheckRollup | map(.conclusion) | if any(. == "FAILURE") then "FAILED" elif any(. == "PENDING") or any(. == "QUEUED") then "PENDING" else "PASSED" end' 2>/dev/null || echo "PENDING")
|
|
285
|
+
|
|
286
|
+
echo "[run_opencode_issue.sh] Poll attempt $POLL_ATTEMPT/$MAX_POLL_ATTEMPTS: Check status = $CHECK_STATUS"
|
|
287
|
+
|
|
288
|
+
if [[ "$CHECK_STATUS" == "PASSED" ]]; then
|
|
289
|
+
echo "[run_opencode_issue.sh] All checks passed. Proceeding with merge..."
|
|
290
|
+
break
|
|
291
|
+
elif [[ "$CHECK_STATUS" == "FAILED" ]]; then
|
|
292
|
+
echo "[run_opencode_issue.sh] PR checks failed."
|
|
293
|
+
|
|
294
|
+
# Check if auto-fix is enabled
|
|
295
|
+
if [[ "$AUTO_FIX_CHECKS" == "true" ]] && [[ $FIX_ATTEMPT -lt $AUTO_FIX_CHECKS_MAX_ATTEMPTS ]]; then
|
|
296
|
+
FIX_ATTEMPT=$((FIX_ATTEMPT + 1))
|
|
297
|
+
echo "[run_opencode_issue.sh] Auto-fix enabled. Attempting to fix failing checks ($FIX_ATTEMPT/$AUTO_FIX_CHECKS_MAX_ATTEMPTS)..."
|
|
298
|
+
|
|
299
|
+
# Fetch failed check details and logs
|
|
300
|
+
CHECK_RUNS_JSON=$(gh api "repos/$REPO/commits/$(git rev-parse HEAD)/check-runs" --jq '.check_runs[] | select(.conclusion == "FAILURE") | {name: .name, conclusion: .conclusion, details_url: .details_url, output: {title: .output.title, summary: .output.summary, text: .output.text}}' 2>/dev/null || echo "[]")
|
|
301
|
+
|
|
302
|
+
# Build failure context
|
|
303
|
+
FAILURE_CONTEXT="The following CI checks failed:\n"
|
|
304
|
+
|
|
305
|
+
# Add failure details to context
|
|
306
|
+
if [[ -n "$CHECK_RUNS_JSON" ]]; then
|
|
307
|
+
FAILURE_CONTEXT+="$CHECK_RUNS_JSON\n"
|
|
308
|
+
fi
|
|
309
|
+
|
|
310
|
+
# Generate repair prompt
|
|
311
|
+
REPAIR_PROMPT="The PR checks have failed. Please analyze the CI failures and fix the issues.\n\n$FAILURE_CONTEXT\n\nWork rules:\n- Examine the failed checks and identify the root cause\n- Make the necessary code changes to fix the failures\n- Commit with message: \"autopilot: fix CI check failures (attempt $FIX_ATTEMPT/$AUTO_FIX_CHECKS_MAX_ATTEMPTS)\"\n- Push your changes to the branch $BRANCH\n- Focus only on fixing the CI failures, do not make unrelated changes"
|
|
312
|
+
|
|
313
|
+
echo "[run_opencode_issue.sh] Running opencode agent to fix CI failures..."
|
|
314
|
+
|
|
315
|
+
# Run opencode to fix the issue
|
|
316
|
+
cd "$WORKTREE"
|
|
317
|
+
if opencode run "$REPAIR_PROMPT"; then
|
|
318
|
+
# Commit any changes
|
|
319
|
+
if [[ -n "$(git status --porcelain)" ]]; then
|
|
320
|
+
git add -A
|
|
321
|
+
git commit -m "autopilot: fix CI check failures (attempt $FIX_ATTEMPT/$AUTO_FIX_CHECKS_MAX_ATTEMPTS)"
|
|
322
|
+
echo "[run_opencode_issue.sh] Committed fixes for CI failures."
|
|
323
|
+
|
|
324
|
+
# Push changes
|
|
325
|
+
git push origin "$BRANCH"
|
|
326
|
+
echo "[run_opencode_issue.sh] Pushed fixes for CI failures."
|
|
327
|
+
|
|
328
|
+
# Reset poll counter to re-check CI status
|
|
329
|
+
POLL_ATTEMPT=0
|
|
330
|
+
|
|
331
|
+
# Wait a moment for checks to start
|
|
332
|
+
sleep 10
|
|
333
|
+
|
|
334
|
+
# Continue polling
|
|
335
|
+
continue
|
|
336
|
+
else
|
|
337
|
+
echo "[run_opencode_issue.sh] No changes made by opencode agent."
|
|
338
|
+
fi
|
|
339
|
+
else
|
|
340
|
+
echo "[run_opencode_issue.sh] Opencode agent failed to fix CI issues."
|
|
341
|
+
fi
|
|
342
|
+
|
|
343
|
+
# If we're here, auto-fix failed or made no changes
|
|
344
|
+
gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "⚠️ Autopilot attempted to fix CI failures (attempt $FIX_ATTEMPT/$AUTO_FIX_CHECKS_MAX_ATTEMPTS) but was unable to resolve all issues." || true
|
|
345
|
+
fi
|
|
346
|
+
|
|
347
|
+
# If auto-fix is disabled or max attempts reached, fail
|
|
348
|
+
if [[ "$FIX_ATTEMPT" -ge "$AUTO_FIX_CHECKS_MAX_ATTEMPTS" ]]; then
|
|
349
|
+
FAILED_MSG="❌ Autopilot cannot auto-merge PR: checks failed after $AUTO_FIX_CHECKS_MAX_ATTEMPTS auto-fix attempts.\n\nPR will remain open for review."
|
|
350
|
+
else
|
|
351
|
+
FAILED_MSG="❌ Autopilot cannot auto-merge PR: checks failed (auto-fix disabled).\n\nPR will remain open for review."
|
|
352
|
+
fi
|
|
353
|
+
gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "$FAILED_MSG" || true
|
|
354
|
+
exit 1
|
|
355
|
+
fi
|
|
356
|
+
|
|
357
|
+
# Wait before next poll
|
|
358
|
+
if [[ $POLL_ATTEMPT -lt $MAX_POLL_ATTEMPTS ]]; then
|
|
359
|
+
echo "[run_opencode_issue.sh] Waiting ${POLL_INTERVAL}s before next check..."
|
|
360
|
+
sleep "$POLL_INTERVAL"
|
|
361
|
+
fi
|
|
362
|
+
done
|
|
363
|
+
else
|
|
364
|
+
BLOCKED_MSG="❌ Autopilot cannot auto-merge PR: authenticated user '$AUTH_USER' is not in the allowedMergeUsers list."
|
|
365
|
+
gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "$BLOCKED_MSG" || true
|
|
366
|
+
|
|
367
|
+
# Mark issue as blocked
|
|
368
|
+
if command -v jq >/dev/null 2>&1; then
|
|
369
|
+
LABEL_BLOCKED=$(jq -r '.issueLabels.blocked' < .autopilot/autopilot.json)
|
|
370
|
+
else
|
|
371
|
+
LABEL_BLOCKED=$(python3 -c 'import json; print(json.load(open(".autopilot/autopilot.json"))["issueLabels"]["blocked"])')
|
|
372
|
+
fi
|
|
373
|
+
gh issue edit "$ISSUE_NUMBER" --repo "$REPO" --add-label "$LABEL_BLOCKED" || true
|
|
374
|
+
fi
|
|
375
|
+
fi
|
|
376
|
+
|
|
86
377
|
echo "[run_opencode_issue.sh] Done: $PR_URL"
|
package/templates/autopilot.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"enabled": true,
|
|
3
3
|
"repo": "bakkensoftware/TicketToolbox",
|
|
4
|
+
"agent": "opencode",
|
|
5
|
+
"allowedMergeUsers": ["github-username"],
|
|
4
6
|
"issueLabels": {
|
|
5
7
|
"queue": ["autopilot:todo"],
|
|
6
8
|
"blocked": "autopilot:blocked",
|
|
@@ -11,5 +13,9 @@
|
|
|
11
13
|
"maxParallel": 2,
|
|
12
14
|
"ignoreIssueLabels": ["autopilot:backlog"],
|
|
13
15
|
"branchPrefix": "autopilot/",
|
|
14
|
-
"allowedBaseBranch": "main"
|
|
16
|
+
"allowedBaseBranch": "main",
|
|
17
|
+
"autoResolveConflicts": true,
|
|
18
|
+
"conflictResolutionMaxAttempts": 3,
|
|
19
|
+
"autoFixChecks": true,
|
|
20
|
+
"autoFixChecksMaxAttempts": 3
|
|
15
21
|
}
|