autopilot-code 0.8.0 → 0.10.0

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/README.md CHANGED
@@ -15,6 +15,53 @@ A repo is considered autopilot-enabled when it contains:
15
15
 
16
16
  - `.autopilot/autopilot.json` with `enabled: true`
17
17
 
18
+ ## Quick Start
19
+
20
+ ### Choosing an Agent
21
+
22
+ Autopilot supports multiple agent types:
23
+
24
+ **opencode** (recommended): Full-featured agent with extensive code understanding
25
+ ```json
26
+ {
27
+ "agent": "opencode"
28
+ }
29
+ ```
30
+
31
+ **claude**: Fast, targeted code changes
32
+ ```json
33
+ {
34
+ "agent": "claude"
35
+ }
36
+ ```
37
+
38
+ ### Enabling the New Runner
39
+
40
+ The new runner provides enhanced progress tracking and session continuity:
41
+
42
+ ```json
43
+ {
44
+ "useNewRunner": true,
45
+ "enablePlanningStep": true
46
+ }
47
+ ```
48
+
49
+ ### Understanding Step Labels
50
+
51
+ When using the new runner, issues progress through these labels:
52
+
53
+ - `autopilot:planning` - Creating implementation plan
54
+ - `autopilot:implementing` - Writing code
55
+ - `autopilot:pr-created` - Pull request created
56
+ - `autopilot:waiting-checks` - Waiting for CI
57
+ - `autopilot:fixing-checks` - Fixing failing CI
58
+ - `autopilot:merging` - Merging PR
59
+
60
+ To create these labels in your repository:
61
+ ```bash
62
+ autopilot setup-labels --repo owner/repo
63
+ ```
64
+
18
65
  Example:
19
66
 
20
67
  ```json
@@ -47,7 +94,8 @@ Example:
47
94
 
48
95
  Notes:
49
96
  - `repo` must be the GitHub `owner/name`.
50
- - `agent` (optional, default `"none"`): set to `"opencode"` to enable OpenCode integration after claiming issues.
97
+ - `agent` (optional, default `"opencode"`): set to `"opencode"` or `"claude"` to choose which coding agent to use.
98
+ - `useNewRunner` (optional, default `false`): enable the new runner with step labels and session continuity.
51
99
  - `autoMerge` (optional, default `true`): if `true`, autopilot will automatically merge PRs after checks pass.
52
100
  - `mergeMethod` (optional, default `"squash"`): merge strategy to use. Options: `"squash"`, `"merge"`, or `"rebase"`.
53
101
  - `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.
@@ -57,7 +105,8 @@ Notes:
57
105
  - `conflictResolutionMaxAttempts` (optional, default `3`): maximum number of attempts to resolve merge conflicts.
58
106
  - `autoFixChecks` (optional, default `true`): if `true`, autopilot will attempt to automatically fix failing CI checks.
59
107
  - `autoFixChecksMaxAttempts` (optional, default `3`): maximum number of attempts to fix failing checks.
60
- - `heartbeatMaxAgeSecs` controls how long an in-progress issue can go without a heartbeat before it's considered stale.
108
+ - `enablePlanningStep` (optional, default `true`): if `true`, add an explicit planning phase before implementation (requires `useNewRunner: true`).
109
+ - `agentPath` (optional): custom path to agent executable (defaults to searching PATH).
61
110
 
62
111
  ## Workflow (labels)
63
112
  Autopilot uses labels as a kanban state machine:
package/dist/cli.js CHANGED
@@ -548,4 +548,63 @@ program
548
548
  .action(() => {
549
549
  logsSystemdService();
550
550
  });
551
+ program
552
+ .command("setup-labels")
553
+ .description("Create required GitHub labels for autopilot workflow")
554
+ .option("--repo <owner/repo>", "Repository in owner/repo format")
555
+ .action((opts) => {
556
+ if (!opts.repo) {
557
+ const cwd = process.cwd();
558
+ const repoName = getRepoName(cwd);
559
+ if (!repoName) {
560
+ console.error("No --repo option provided and could not detect repo from current directory.");
561
+ console.error("Usage: autopilot setup-labels --repo owner/repo");
562
+ process.exit(1);
563
+ }
564
+ opts.repo = repoName;
565
+ }
566
+ const labels = [
567
+ { name: "autopilot:todo", color: "7057ff", description: "Ready to be picked up by autopilot" },
568
+ { name: "autopilot:in-progress", color: "0075ca", description: "Claimed by autopilot" },
569
+ { name: "autopilot:blocked", color: "d93f0b", description: "Needs human input or missing heartbeat" },
570
+ { name: "autopilot:done", color: "3fb950", description: "Completed" },
571
+ { name: "autopilot:backlog", color: "d4c5f9", description: "Captured, not ready" },
572
+ { name: "autopilot:planning", color: "5319e7", description: "Creating implementation plan" },
573
+ { name: "autopilot:implementing", color: "1f883d", description: "Writing code" },
574
+ { name: "autopilot:pr-created", color: "fbca04", description: "Pull request created" },
575
+ { name: "autopilot:waiting-checks", color: "0e8a16", description: "Waiting for CI checks" },
576
+ { name: "autopilot:fixing-checks", color: "cfd3d7", description: "Fixing failing CI checks" },
577
+ { name: "autopilot:merging", color: "bfd4f2", description: "Merging pull request" }
578
+ ];
579
+ console.log(`Creating labels for ${opts.repo}...`);
580
+ labels.forEach(label => {
581
+ const args = [
582
+ "api",
583
+ "--method",
584
+ "POST",
585
+ "-H",
586
+ "Accept: application/vnd.github+json",
587
+ "repos/${opts.repo}/labels",
588
+ "-f",
589
+ `name=${label.name}`,
590
+ "-f",
591
+ `color=${label.color}`,
592
+ "-f",
593
+ `description=${label.description}`
594
+ ];
595
+ const res = (0, node_child_process_1.spawnSync)("gh", args, { stdio: "pipe" });
596
+ if (res.status === 0) {
597
+ console.log(`✅ Created: ${label.name}`);
598
+ }
599
+ else if (res.stderr && res.stderr.toString().includes("Label already exists")) {
600
+ console.log(`ℹ️ Exists: ${label.name}`);
601
+ }
602
+ else {
603
+ console.error(`❌ Failed to create ${label.name}`);
604
+ if (res.stderr)
605
+ console.error(res.stderr.toString());
606
+ }
607
+ });
608
+ console.log("\n✅ Label setup complete!");
609
+ });
551
610
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autopilot-code",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "private": false,
5
5
  "description": "Repo-issue–driven autopilot runner",
6
6
  "license": "MIT",
@@ -25,6 +25,12 @@ from dataclasses import dataclass
25
25
  from pathlib import Path
26
26
  from typing import Any
27
27
  import shutil
28
+ from sys import path as sys_path
29
+
30
+ # Add scripts directory to path for imports
31
+ script_dir = Path(__file__).parent
32
+ sys_path.insert(0, str(script_dir))
33
+ from issue_runner import IssueRunner
28
34
 
29
35
  STATE_DIR = ".autopilot"
30
36
  STATE_FILE = "state.json"
@@ -87,6 +93,8 @@ class RepoConfig:
87
93
  auto_fix_checks: bool
88
94
  auto_fix_checks_max_attempts: int
89
95
  auto_update: bool
96
+ use_new_runner: bool = False
97
+ config: dict[str, Any] = None
90
98
 
91
99
 
92
100
  def load_config(repo_root: Path) -> RepoConfig | None:
@@ -136,6 +144,8 @@ def load_config(repo_root: Path) -> RepoConfig | None:
136
144
  auto_fix_checks=auto_fix_checks,
137
145
  auto_fix_checks_max_attempts=auto_fix_checks_max_attempts,
138
146
  auto_update=data.get("autoUpdate", False),
147
+ use_new_runner=data.get("useNewRunner", False),
148
+ config=data,
139
149
  )
140
150
 
141
151
 
@@ -366,6 +376,18 @@ def claim_issue(cfg: RepoConfig, issue: dict[str, Any], note: str) -> None:
366
376
  touch_heartbeat(cfg, num)
367
377
 
368
378
 
379
+ def clear_active_issue(cfg: RepoConfig, issue_number: int) -> None:
380
+ """Clear an issue from active issues state."""
381
+ state = load_state(cfg.root)
382
+ active_issues = state.get("activeIssues", {})
383
+ if isinstance(active_issues, dict):
384
+ issue_key = str(issue_number)
385
+ if issue_key in active_issues:
386
+ del active_issues[issue_key]
387
+ state["activeIssues"] = active_issues
388
+ write_state(cfg.root, state)
389
+
390
+
369
391
  def list_in_progress_issues(cfg: RepoConfig, limit: int = 20) -> list[dict[str, Any]]:
370
392
  cmd = [
371
393
  "gh",
@@ -736,6 +758,49 @@ def maybe_mark_blocked(cfg: RepoConfig, issue: dict[str, Any]) -> None:
736
758
  )
737
759
 
738
760
 
761
+ def run_issue(cfg: RepoConfig, issue_number: int) -> bool:
762
+ """
763
+ Run the agent on an issue.
764
+
765
+ Uses either the new Python runner or legacy bash script
766
+ based on configuration.
767
+ """
768
+ if cfg.use_new_runner:
769
+ return run_issue_new(cfg, issue_number)
770
+ else:
771
+ return run_issue_legacy(cfg, issue_number)
772
+
773
+
774
+ def run_issue_new(cfg: RepoConfig, issue_number: int) -> bool:
775
+ """Run issue using new Python state machine runner."""
776
+ # Touch heartbeat before starting
777
+ touch_heartbeat(cfg, issue_number)
778
+
779
+ runner = IssueRunner(
780
+ repo=cfg.repo,
781
+ repo_root=cfg.root,
782
+ config=cfg.config
783
+ )
784
+
785
+ try:
786
+ success = runner.run(issue_number)
787
+ finally:
788
+ # Clear from active issues when done
789
+ clear_active_issue(cfg, issue_number)
790
+
791
+ return success
792
+
793
+
794
+ def run_issue_legacy(cfg: RepoConfig, issue_number: int) -> bool:
795
+ """Run issue using legacy bash script (existing behavior)."""
796
+ script_path = Path(__file__).parent / "run_opencode_issue.sh"
797
+ result = subprocess.run(
798
+ [str(script_path), str(cfg.root), str(issue_number)],
799
+ cwd=cfg.root
800
+ )
801
+ return result.returncode == 0
802
+
803
+
739
804
  def run_cycle(
740
805
  all_configs: list[RepoConfig],
741
806
  dry_run: bool = False,
@@ -816,18 +881,9 @@ def run_cycle(
816
881
  start_msg = f"🚀 Autopilot is now starting work on issue #{issue['number']}.\n\nI'll post regular progress updates as I work through the implementation."
817
882
  sh(["gh", "issue", "comment", str(issue["number"]), "--repo", cfg.repo, "--body", start_msg])
818
883
 
819
- # If agent==opencode, delegate to bash script
884
+ # Run the issue using the appropriate runner
820
885
  if cfg.agent == "opencode":
821
- # Script is in the same directory as this Python file
822
- script_dir = Path(__file__).parent
823
- script_path = script_dir / "run_opencode_issue.sh"
824
- sh(
825
- [
826
- str(script_path),
827
- str(cfg.root),
828
- str(issue["number"]),
829
- ]
830
- )
886
+ run_issue(cfg, issue["number"])
831
887
 
832
888
  # Check if autopilot needs to update
833
889
  if not dry_run and check_autopilot_needs_update():
@@ -1,18 +1,29 @@
1
1
  {
2
2
  "enabled": true,
3
- "repo": "bakkensoftware/TicketToolbox",
3
+ "repo": "owner/repo",
4
4
  "agent": "opencode",
5
- "allowedMergeUsers": ["github-username"],
5
+ "useNewRunner": false,
6
+ "autoMerge": true,
7
+ "mergeMethod": "squash",
8
+ "allowedMergeUsers": [],
6
9
  "issueLabels": {
7
10
  "queue": ["autopilot:todo"],
8
- "blocked": "autopilot:blocked",
9
11
  "inProgress": "autopilot:in-progress",
12
+ "blocked": "autopilot:blocked",
10
13
  "done": "autopilot:done"
11
14
  },
15
+ "stepLabels": {
16
+ "planning": "autopilot:planning",
17
+ "implementing": "autopilot:implementing",
18
+ "prCreated": "autopilot:pr-created",
19
+ "waitingChecks": "autopilot:waiting-checks",
20
+ "fixingChecks": "autopilot:fixing-checks",
21
+ "merging": "autopilot:merging"
22
+ },
12
23
  "priorityLabels": ["p0", "p1", "p2"],
13
24
  "minPriority": null,
14
- "maxParallel": 2,
15
25
  "ignoreIssueLabels": ["autopilot:backlog"],
26
+ "maxParallel": 2,
16
27
  "branchPrefix": "autopilot/",
17
28
  "allowedBaseBranch": "main",
18
29
  "autoResolveConflicts": true,
@@ -20,5 +31,6 @@
20
31
  "autoFixChecks": true,
21
32
  "autoFixChecksMaxAttempts": 3,
22
33
  "autoUpdate": true,
23
- "enablePlanningStep": true
34
+ "enablePlanningStep": true,
35
+ "agentPath": ""
24
36
  }