Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 57 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,27 @@ The main entry point is:

The plugin deliberately keeps Claude as the implementation orchestrator and uses fresh, read-only Codex executions for independent planning and review. It does not push, merge, deploy, rotate credentials, alter remote infrastructure, or apply irreversible production migrations.

### Three entry-point workflows

| Slash command | Worktree | Branch | When to use |
|---|---|---|---|
| `/autonomous-development:autonomous-feature` | Isolated `.claude/worktrees/*` (default safe) | New worktree branch | Default. Edits land in a disposable worktree; your checkout is untouched. |
| `/autonomous-development:autonomous-current` | Current checkout | Your already-created non-`main`/`master` feature branch | You created and switched to a clean feature branch yourself and want changes to land there. |
| `/autonomous-development:autonomous-main` | Current checkout | `main` / `master` (explicit opt-in via `--allow-main`) | You explicitly want direct edits on `main`/`master`. Still requires a clean tree. |

Both current-checkout workflows are opt-in. They do not create `.claude/worktrees/*`, do not enter a worktree, and never commit — the user reviews with normal `git diff` and commits manually. They require a clean working tree (`git status --porcelain` must be empty). `autonomous-current` additionally refuses `main`/`master`; `autonomous-main` passes `--allow-main` to bypass that guard.

## Upstream

This plugin is based on [quaat/autonomous-development](https://github.com/quaat/autonomous-development). This fork keeps the original workflow and adds the current-checkout mode and related documentation updates.

## Included skills

| Skill | Purpose |
|---|---|
| `/autonomous-development:autonomous-feature` | Run the complete workflow from a high-level idea |
| `/autonomous-development:autonomous-feature` | Safe default: run the complete workflow inside a disposable worktree |
| `/autonomous-development:autonomous-current` | Run the complete workflow directly on the user's already-created feature branch (current checkout, never commits) |
| `/autonomous-development:autonomous-main` | Run the complete workflow directly on `main`/`master` (explicit opt-in via `--allow-main`, never commits) |
| `/autonomous-development:enhance-idea` | Ask Codex to turn a rough feature idea into a structured proposal |
| `/autonomous-development:implementation-plan` | Ask a fresh Codex execution for a repository-grounded plan |
| `/autonomous-development:implement-plan` | Implement the accepted plan with Claude |
Expand Down Expand Up @@ -286,20 +302,50 @@ Archiving is a metadata flag; no files are deleted.
- Do not share state directories across users or store them on world-readable paths.
- Remote URLs are stored with credentials stripped (the `user:pass@` portion is removed).

## Worktree support
## Worktree modes

All commands work correctly from any linked worktree. The repository identity is derived from
the shared git object store so runs created in different worktrees belong to the same
repository and are visible to `list-runs`.
The controller supports two explicit execution modes:

```bash
# Create a linked worktree and run the workflow there
git worktree add ../experiment feature-branch
cd ../experiment
controller.py init --feature "Experiment feature"
# State stored under the same repository ID, new run ID
# Default safe workflow: isolated worktree
# Claude Code's autonomous-feature skill enters a disposable worktree before init.
controller.py init --feature "Experiment feature" --mode auto --worktree-mode isolated
```

```bash
# Current-branch workflow: use the branch you created manually
git checkout -b experiment
controller.py init --feature "Experiment feature" --mode auto --worktree-mode current
# Review the result with normal `git diff` in your checkout.
```

```bash
# Current-checkout on main/master (explicit opt-in):
controller.py init --feature "Hotfix" --mode standard --worktree-mode current --allow-main
```

Two slash commands wrap these invocations so the user does not have to call `controller.py`
directly:

```text
/autonomous-development:autonomous-current "Experiment feature" # already on a feature branch
/autonomous-development:autonomous-main "Hotfix" # explicit edits on main/master
```

Both commands run `controller.py init --mode standard --worktree-mode current`; the `main`
variant additionally passes `--allow-main`. Neither creates `.claude/worktrees/*`, neither
enters a worktree, and neither commits. The agent's changes land directly in your checkout so
normal `git diff`, local testing, and manual commit flow continue to work.

All commands still work correctly from any linked worktree. The repository identity is derived
from the shared git object store so runs created in different worktrees belong to the same
repository and are visible to `list-runs`. Current-checkout mode is opt-in for people who create
their own feature branches first and want the agent's changes to land directly in that checkout.
In current mode the controller uses the current project root for both
`repository.canonical_root` and `repository.worktree_path`, records the current branch in
baseline metadata, does not create a `.claude/worktrees/*` worktree or `worktree-*` branch,
refuses `main`/`master` unless you pass `--allow-main`, and refuses a dirty tree.

## Completion rules

A run succeeds only when:
Expand All @@ -323,7 +369,7 @@ The skill uses Codex with `--sandbox read-only`. Claude performs repository edit
- deleting unrelated user changes;
- weakening tests or security controls to obtain a passing result.

Run autonomous development in a disposable branch or worktree. The orchestrator asks Claude Code to use an isolated worktree whenever supported.
By default, run autonomous development in an isolated worktree. This fork also supports an explicit current-checkout mode when you want changes to land directly in your own feature branch.

## Customization

Expand Down
21 changes: 21 additions & 0 deletions examples/invocations.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
# Example invocations

```text
# Safe default: edits land in a disposable isolated worktree.
/autonomous-development:autonomous-feature "Add resumable multipart uploads with checksum validation and backward-compatible API behavior"
```

```text
# Direct edits on an already-created feature branch.
# Refuses main/master and a dirty tree. Never commits — review with `git diff`.
git checkout -b feature/uploads
/autonomous-development:autonomous-current "Add resumable multipart uploads"
```

```text
# Explicit opt-in: direct edits on main/master.
# Still requires a clean tree. Never commits — review with `git diff`.
/autonomous-development:autonomous-main "Patch a security hotfix in place"
```

```text
/autonomous-development:enhance-idea "Add organization-level API usage limits"
/autonomous-development:implementation-plan "Prioritize backward compatibility and migration safety"
Expand Down Expand Up @@ -31,6 +45,13 @@ controller.py init --feature "Rename a button label" --mode auto

# Force the full workflow with mandatory adversarial review.
controller.py init --feature "Add tenant-scoped billing" --mode rigorous

# Use your manually created feature branch instead of a disposable worktree.
git switch -c experiment
controller.py init --feature "Experiment feature" --mode auto --worktree-mode current

# Same mode, but explicitly authorized to land on main/master.
controller.py init --feature "Hotfix" --mode standard --worktree-mode current --allow-main
```

Drive phases and inspect token usage:
Expand Down
95 changes: 69 additions & 26 deletions scripts/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
_DEFAULT_PROFILE = {"reasoning": "high", "verbosity": "low", "reasoning_summary": "none"}

WORKFLOW_MODES = ("auto", "lean", "standard", "rigorous")
WORKTREE_MODES = ("isolated", "current")

# Conservative risk categories used by `--mode auto` escalation. Matching any
# category escalates an `auto` run to rigorous.
Expand Down Expand Up @@ -514,6 +515,32 @@ def select_mode(requested: str, feature: str) -> tuple[str, list[str]]:
return requested, [f"explicit mode requested: {requested}"]


def worktree_mode_label(worktree_mode: object) -> str:
"""Human-readable label for the init worktree mode."""
if worktree_mode == "current":
return "current checkout"
if worktree_mode == "isolated":
return "isolated worktree"
if isinstance(worktree_mode, str) and worktree_mode.strip():
return worktree_mode.strip()
return "(unknown)"


def repository_state_block(repo: RepoInfo, *, worktree_mode: str | None = None) -> dict[str, str]:
"""Return the repository block written into run-state.json."""
block = {
"id": repo.id,
"canonical_root": str(repo.canonical_root),
"git_common_dir": str(repo.git_common_dir),
"worktree_path": str(repo.worktree_path),
"display_name": repo.display_name,
"remote_display": repo.remote_display,
}
if worktree_mode:
block["worktree_mode"] = worktree_mode
return block


# ---------------------------------------------------------------------------
# Compact prompt context helpers
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -1362,17 +1389,29 @@ def cmd_init(args: argparse.Namespace) -> int:
)
run_dir.mkdir(parents=True, exist_ok=True, mode=0o700)

ctx_text = repository_context(repo)
(run_dir / "feature-request.md").write_text(
feature + "\n", encoding="utf-8"
)
(run_dir / "repository-context.txt").write_text(
ctx_text, encoding="utf-8"
)
worktree_mode = getattr(args, "worktree_mode", "isolated")
dirty = git(repo.canonical_root, "status", "--short", check=False).splitlines()

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

git status --short reports untracked files (??) too, so a tree that is clean except for benign untracked files will be refused below as 'not a clean working tree'. That matches the intended fail-closed posture, but the error message may surprise users who only have untracked files — worth a word in the message or docs. (Also note the docs/skills say git status --porcelain; equivalent for emptiness, just flagging the wording mismatch.)

if worktree_mode == "current":
if repo.branch in {"main", "master"} and not getattr(args, "allow_main", False):

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Detached HEAD bypasses this guard. repo.branch comes from git branch --show-current, which returns an empty string on a detached HEAD. So if the user is in detached-HEAD state (even sitting exactly on the tip of main), repo.branch in {"main", "master"} is False and current-checkout mode proceeds without --allow-main. Consider also refusing an empty branch name here (or explicitly documenting detached HEAD as out of scope).

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separately: --allow-main is silently ignored when --worktree-mode is isolated (no validation ties the two together). Harmless, but a no-op flag can mislead — consider warning if --allow-main is passed without --worktree-mode current.

raise WorkflowError(
"Current-checkout mode refuses to initialize on "
f"branch {repo.branch!r}. Create and check out a feature "
"branch first, or pass --allow-main to override this guard "
"on main/master."
)
if dirty:
preview = ", ".join(dirty[:5])
suffix = "" if len(dirty) <= 5 else f" (+{len(dirty) - 5} more)"
raise WorkflowError(
"Current-checkout mode requires a clean working tree. "
f"Dirty entries: {preview}{suffix}"
)

dirty = git(
repo.canonical_root, "status", "--short", check=False
).splitlines()
ctx_text = repository_context(repo) + (
f"\nWorktree mode: {worktree_mode_label(worktree_mode)}\n"
)
(run_dir / "feature-request.md").write_text(feature + "\n", encoding="utf-8")
(run_dir / "repository-context.txt").write_text(ctx_text, encoding="utf-8")

requested_mode = getattr(args, "mode", "auto")
effective_mode, mode_reasons = select_mode(requested_mode, feature)
Expand All @@ -1393,14 +1432,7 @@ def cmd_init(args: argparse.Namespace) -> int:
"phase": "initialized",
"created_at": utc_now(),
"updated_at": utc_now(),
"repository": {
"id": repo.id,
"canonical_root": str(repo.canonical_root),
"git_common_dir": str(repo.git_common_dir),
"worktree_path": str(repo.worktree_path),
"display_name": repo.display_name,
"remote_display": repo.remote_display,
},
"repository": repository_state_block(repo, worktree_mode=worktree_mode),
"baseline": {
"commit": repo.head_commit,
"branch": repo.branch,
Expand Down Expand Up @@ -2635,6 +2667,10 @@ def cmd_status(args: argparse.Namespace) -> int:
print(f"Status: {state.get('status')}")
print(f"Phase: {state.get('phase')}")
print(f"Feature: {state.get('feature')}")
repo_block = state.get("repository", {})
worktree_mode = repo_block.get("worktree_mode") if isinstance(repo_block, dict) else None
if worktree_mode:
print(f"Worktree mode: {worktree_mode_label(worktree_mode)}")
print(f"Baseline: {state.get('baseline', {}).get('commit')}")
print(f"Verification: {passed}/{len(checks)} passing")
print(
Expand Down Expand Up @@ -2959,15 +2995,11 @@ def cmd_accept_drift(args: argparse.Namespace) -> int:
require_active_run_state(state, run_ref.run_id, "accept-drift")
old_baseline = dict(state.get("baseline", {}))
old_repo_block = dict(state.get("repository", {}))
old_worktree_mode = old_repo_block.get("worktree_mode")

state["repository"] = {
"id": repo.id,
"canonical_root": str(repo.canonical_root),
"git_common_dir": str(repo.git_common_dir),
"worktree_path": str(repo.worktree_path),
"display_name": repo.display_name,
"remote_display": repo.remote_display,
}
state["repository"] = repository_state_block(
repo, worktree_mode=old_worktree_mode if isinstance(old_worktree_mode, str) else None
)
state["baseline"] = {
"commit": repo.head_commit,
"branch": repo.branch,
Expand Down Expand Up @@ -3240,6 +3272,17 @@ def build_parser() -> argparse.ArgumentParser:
default="auto",
help="Workflow rigor mode; auto escalates conservatively by risk",
)
init.add_argument(
"--worktree-mode",
choices=WORKTREE_MODES,
default="isolated",
help="Repository execution mode; isolated stays in a disposable worktree, current uses the current checkout",
)
init.add_argument(
"--allow-main",
action="store_true",
help="Allow current-checkout mode on main/master (still requires a clean tree)",
)
init.add_argument("--max-review-rounds", type=int, default=3, choices=range(1, 6))
init.add_argument("--reuse", action="store_true")
init.add_argument("--force", action="store_true")
Expand Down
1 change: 1 addition & 0 deletions scripts/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,7 @@ def migrate_v1_to_v2(
"id": repo.id,
"display_name": repo.display_name,
"canonical_root": str(repo.canonical_root),
"worktree_mode": "isolated",
"remote_display": repo.remote_display,
}

Expand Down
Loading