From 15a619aee5c5b7692fb4acb7c35e57998a123572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigmund=20=C3=85s?= Date: Thu, 25 Jun 2026 14:44:34 +0200 Subject: [PATCH 1/2] Add current-checkout mode for autonomous runs --- README.md | 37 ++++++--- examples/invocations.md | 4 + scripts/controller.py | 95 ++++++++++++++++------ scripts/state.py | 1 + skills/autonomous-feature/SKILL.md | 16 +++- tests/test_controller.py | 123 +++++++++++++++++++++++++++++ tests/test_project_layout.py | 9 +++ 7 files changed, 247 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index e82c8dc..b799dc1 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,10 @@ 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. +## 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 | @@ -286,20 +290,33 @@ 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 + +The controller supports two explicit execution 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`. +```bash +# 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 -# 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 +# 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. ``` +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, so +normal `git diff`, local testing, and manual commit flow continue to work. 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: @@ -323,7 +340,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 diff --git a/examples/invocations.md b/examples/invocations.md index 34c72d6..1f52276 100644 --- a/examples/invocations.md +++ b/examples/invocations.md @@ -31,6 +31,10 @@ 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 ``` Drive phases and inspect token usage: diff --git a/scripts/controller.py b/scripts/controller.py index e420653..ec6d253 100755 --- a/scripts/controller.py +++ b/scripts/controller.py @@ -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. @@ -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 # --------------------------------------------------------------------------- @@ -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() + if worktree_mode == "current": + if repo.branch in {"main", "master"} and not getattr(args, "allow_main", False): + 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) @@ -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, @@ -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( @@ -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, @@ -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") diff --git a/scripts/state.py b/scripts/state.py index 8378589..193369d 100644 --- a/scripts/state.py +++ b/scripts/state.py @@ -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, } diff --git a/skills/autonomous-feature/SKILL.md b/skills/autonomous-feature/SKILL.md index 7733ca6..cc3d28f 100644 --- a/skills/autonomous-feature/SKILL.md +++ b/skills/autonomous-feature/SKILL.md @@ -53,18 +53,30 @@ lives in `references/` and is loaded only when a phase needs it. 1. Confirm this is a Git repository. Inspect `CLAUDE.md`, repository instructions, architecture, status, and tests. Use `EnterWorktree` for an isolated worktree whenever available — mandatory - when the starting worktree has uncommitted changes. + when the starting worktree has uncommitted changes. If the user explicitly asks for + current-checkout mode, stay in the current branch instead of entering a worktree, and require a + clean feature branch before proceeding. 2. Initialize: ```bash python3 "${CLAUDE_PLUGIN_ROOT}/scripts/controller.py" doctor - python3 "${CLAUDE_PLUGIN_ROOT}/scripts/controller.py" init --feature "$ARGUMENTS" --mode auto + python3 "${CLAUDE_PLUGIN_ROOT}/scripts/controller.py" init --feature "$ARGUMENTS" --mode auto --worktree-mode isolated ``` `init` prints the `run-state.json` path and run ID. With multiple concurrent runs, pass `--run-id ` to all subsequent commands. If `doctor` reports a missing prerequisite, mark the run blocked and report it rather than bypassing it. + For current-checkout mode, do not call `EnterWorktree`. Instead, keep the current branch checked + out and run: + + ```bash + python3 "${CLAUDE_PLUGIN_ROOT}/scripts/controller.py" init --feature "$ARGUMENTS" --mode auto --worktree-mode current + ``` + + The controller refuses `main`/`master` unless the user also passes `--allow-main`, and it + refuses a dirty tree in current-checkout mode. + 3. Repeatedly ask the controller for the next phase, then execute it: ```bash diff --git a/tests/test_controller.py b/tests/test_controller.py index 1e665a3..26885ac 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -81,6 +81,22 @@ def _find_state_path(self, repo: Path, state_home: Path) -> Path: ) return active[0].run_dir / "run-state.json" + def _current_branch(self, repo: Path) -> str: + result = subprocess.run( + ["git", "-C", str(repo), "branch", "--show-current"], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + return result.stdout.strip() + + def _checkout_feature_branch(self, repo: Path, name: str = "feature-branch") -> None: + subprocess.run( + ["git", "-C", str(repo), "checkout", "-q", "-b", name], + check=True, + ) + def test_init_and_status(self) -> None: repo = self.make_repo() state_home = self.make_state_home() @@ -99,6 +115,113 @@ def test_init_and_status(self) -> None: status = self.run_controller(repo, "status", state_home=state_home) self.assertEqual(status.returncode, 0, status.stderr) self.assertIn("Phase: initialized", status.stdout) + self.assertIn("Worktree mode: isolated worktree", status.stdout) + self.assertEqual(state["repository"]["worktree_mode"], "isolated") + + def test_init_current_mode_records_current_checkout_path(self) -> None: + repo = self.make_repo() + state_home = self.make_state_home() + self._checkout_feature_branch(repo) + + result = self.run_controller( + repo, + "init", + "--feature", + "Feature", + "--worktree-mode", + "current", + state_home=state_home, + ) + self.assertEqual(result.returncode, 0, result.stderr) + + state_path = self._find_state_path(repo, state_home) + state = json.loads(state_path.read_text(encoding="utf-8")) + self.assertEqual(state["repository"]["canonical_root"], str(repo.resolve())) + self.assertEqual(state["repository"]["worktree_path"], str(repo.resolve())) + self.assertEqual(state["repository"]["worktree_mode"], "current") + self.assertEqual(state["baseline"]["branch"], self._current_branch(repo)) + + status = self.run_controller(repo, "status", state_home=state_home) + self.assertEqual(status.returncode, 0, status.stderr) + self.assertIn("Worktree mode: current checkout", status.stdout) + + def test_current_mode_refuses_main_and_master(self) -> None: + for branch_name in ("main", "master"): + with self.subTest(branch=branch_name): + repo = self.make_repo() + state_home = self.make_state_home() + subprocess.run( + ["git", "-C", str(repo), "branch", "-m", branch_name], + check=True, + ) + result = self.run_controller( + repo, + "init", + "--feature", + "Feature", + "--worktree-mode", + "current", + state_home=state_home, + ) + self.assertNotEqual(result.returncode, 0) + self.assertIn(branch_name, result.stderr) + + def test_current_mode_allows_main_and_master_with_override(self) -> None: + for branch_name in ("main", "master"): + with self.subTest(branch=branch_name): + repo = self.make_repo() + state_home = self.make_state_home() + subprocess.run( + ["git", "-C", str(repo), "branch", "-m", branch_name], + check=True, + ) + result = self.run_controller( + repo, + "init", + "--feature", + "Feature", + "--worktree-mode", + "current", + "--allow-main", + state_home=state_home, + ) + self.assertEqual(result.returncode, 0, result.stderr) + + def test_current_mode_refuses_dirty_tree(self) -> None: + repo = self.make_repo() + state_home = self.make_state_home() + self._checkout_feature_branch(repo) + (repo / "README.md").write_text("# dirty\n", encoding="utf-8") + + result = self.run_controller( + repo, + "init", + "--feature", + "Feature", + "--worktree-mode", + "current", + state_home=state_home, + ) + self.assertNotEqual(result.returncode, 0) + self.assertIn("clean working tree", result.stderr) + + def test_current_mode_does_not_create_claude_worktrees(self) -> None: + repo = self.make_repo() + state_home = self.make_state_home() + self._checkout_feature_branch(repo) + worktrees_dir = repo / ".claude" / "worktrees" + + result = self.run_controller( + repo, + "init", + "--feature", + "Feature", + "--worktree-mode", + "current", + state_home=state_home, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertFalse(worktrees_dir.exists()) def test_record_passing_check(self) -> None: repo = self.make_repo() diff --git a/tests/test_project_layout.py b/tests/test_project_layout.py index 110202a..99596f1 100644 --- a/tests/test_project_layout.py +++ b/tests/test_project_layout.py @@ -85,6 +85,15 @@ def test_prompt_placeholders_are_known(self) -> None: ) self.assertTrue(placeholders <= known, f"{path}: {placeholders - known}") + def test_autonomous_feature_skill_mentions_worktree_modes(self) -> None: + text = (ROOT / "skills" / "autonomous-feature" / "SKILL.md").read_text( + encoding="utf-8" + ) + self.assertIn("--worktree-mode isolated", text) + self.assertIn("--worktree-mode current", text) + self.assertIn("EnterWorktree", text) + self.assertIn("current-checkout mode", text) + if __name__ == "__main__": unittest.main() From e9d4afcb51b82fbb3873dc11fd2a2b98d363d15e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigmund=20=C3=85s?= Date: Fri, 26 Jun 2026 12:35:29 +0200 Subject: [PATCH 2/2] Add current-checkout slash commands --- README.md | 43 +++++++++-- examples/invocations.md | 17 +++++ skills/autonomous-current/SKILL.md | 110 +++++++++++++++++++++++++++++ skills/autonomous-main/SKILL.md | 108 ++++++++++++++++++++++++++++ tests/test_controller.py | 44 ++++++++++++ tests/test_project_layout.py | 53 ++++++++++++++ 6 files changed, 368 insertions(+), 7 deletions(-) create mode 100644 skills/autonomous-current/SKILL.md create mode 100644 skills/autonomous-main/SKILL.md diff --git a/README.md b/README.md index b799dc1..81befae 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,16 @@ 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. @@ -29,7 +39,9 @@ This plugin is based on [quaat/autonomous-development](https://github.com/quaat/ | 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 | @@ -307,15 +319,32 @@ controller.py init --feature "Experiment feature" --mode auto --worktree-mode cu # 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, so -normal `git diff`, local testing, and manual commit flow continue to work. 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. +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 diff --git a/examples/invocations.md b/examples/invocations.md index 1f52276..3d0f6c4 100644 --- a/examples/invocations.md +++ b/examples/invocations.md @@ -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" @@ -35,6 +49,9 @@ 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: diff --git a/skills/autonomous-current/SKILL.md b/skills/autonomous-current/SKILL.md new file mode 100644 index 0000000..2ef6266 --- /dev/null +++ b/skills/autonomous-current/SKILL.md @@ -0,0 +1,110 @@ +--- +name: autonomous-current +description: Autonomously develop a repository feature directly in the current Git checkout. Use when the user has already created and switched to a clean non-main feature branch and wants the agent's edits to land in that branch without a disposable worktree. Codex independently enhances the idea, proposes a plan, and reviews the work while Claude reconciles, implements, verifies, and triages findings. +argument-hint: "[feature idea]" +disable-model-invocation: true +effort: high +allowed-tools: + - Read + - Grep + - Glob + - Edit + - Write + - LSP + - Agent + - Bash(git *) + - Bash(python3 *) + - Bash(codex *) +disallowed-tools: + - AskUserQuestion + - EnterWorktree + - ExitWorktree +hooks: + Stop: + - hooks: + - type: command + command: 'python3 "${CLAUDE_PLUGIN_ROOT}/scripts/stop_gate.py"' + timeout: 10 +--- + +# Autonomous feature development — current checkout + +Implement this feature idea directly in the user's current Git checkout: + +> $ARGUMENTS + +This skill is the current-checkout counterpart to `autonomous-feature`. The user has already +created a clean non-`main`/non-`master` feature branch and wants the agent's edits to land +directly in that branch — no disposable worktree, no `.claude/worktrees/*` clone, no extra +`worktree-*` branch. Use ultrathink for architecture, compatibility, and review triage. + +## Non-negotiable boundaries + +- This skill must NOT call `EnterWorktree` / `ExitWorktree`. All edits land in the current + checkout. Do not create or enter `.claude/worktrees/*`. +- Refuse to run on `main` or `master`. If the current branch is one of those, stop and ask the + user to create a feature branch first (or use `/autonomous-development:autonomous-main` when + they have explicitly authorized direct edits on main). +- Refuse to run on a dirty working tree. `git status --porcelain` must be empty before + initializing. If it is not, stop and report which entries are dirty. +- Do not create commits. The user will review and commit with their normal `git diff`/commit + flow. +- Preserve unrelated user changes. +- Never push, merge, publish, deploy, rotate credentials, or modify remote infrastructure. +- Never use `danger-full-access`, `--yolo`, `bypassPermissions`, or equivalent unrestricted modes. +- Never apply an irreversible database migration or delete user data. +- Do not weaken authorization, validation, tests, or static checks to make the workflow pass. +- Codex planning and review executions must remain read-only. +- Use no more than the configured review-round budget. +- Treat every Codex finding as a proposal requiring evidence-based triage. + +## Driver loop + +1. Confirm this is a Git repository. Inspect `CLAUDE.md`, repository instructions, architecture, + status, and tests. Verify the current branch is NOT `main`/`master` and that the working tree + is clean before initializing — the controller will fail closed on both, but checking up front + produces a clearer message for the user. + +2. Initialize in current-checkout mode (no worktree): + + ```bash + python3 "${CLAUDE_PLUGIN_ROOT}/scripts/controller.py" doctor + python3 "${CLAUDE_PLUGIN_ROOT}/scripts/controller.py" init \ + --feature "$ARGUMENTS" \ + --mode standard \ + --worktree-mode current + ``` + + `init` prints the `run-state.json` path and run ID. With multiple concurrent runs, pass + `--run-id ` to all subsequent commands. If `doctor` reports a missing prerequisite, + mark the run blocked and report it rather than bypassing it. + +3. Repeatedly ask the controller for the next phase, then execute it: + + ```bash + python3 "${CLAUDE_PLUGIN_ROOT}/scripts/controller.py" next-action --json + ``` + + The response gives `phase`, `required_action`, `completion_condition`, and `references`. + Read the referenced file under `${CLAUDE_PLUGIN_ROOT}/skills/autonomous-feature/references/` + for that phase and follow it until the completion condition holds. + +4. Do not declare success until `evaluate` succeeds: + + ```bash + python3 "${CLAUDE_PLUGIN_ROOT}/scripts/controller.py" evaluate + python3 "${CLAUDE_PLUGIN_ROOT}/scripts/controller.py" status + python3 "${CLAUDE_PLUGIN_ROOT}/scripts/controller.py" usage-report + ``` + +## Final report + +- the implemented behavior; +- principal files changed (visible to the user via plain `git diff`); +- verification commands and results; +- Codex review rounds and disposition of findings; +- adversarial review result when one was required; +- per-phase usage table from `usage-report`; +- remaining risks or explicit blocked reason; +- a suggested conventional commit message (the user commits manually — this skill must not + commit). diff --git a/skills/autonomous-main/SKILL.md b/skills/autonomous-main/SKILL.md new file mode 100644 index 0000000..fb5af8d --- /dev/null +++ b/skills/autonomous-main/SKILL.md @@ -0,0 +1,108 @@ +--- +name: autonomous-main +description: Autonomously develop a repository feature directly in the current main/master checkout. Use only when the user has explicitly authorized direct edits on main/master. Same workflow as autonomous-current but passes --allow-main to bypass the main/master refusal. Still requires a clean working tree and never commits. +argument-hint: "[feature idea]" +disable-model-invocation: true +effort: high +allowed-tools: + - Read + - Grep + - Glob + - Edit + - Write + - LSP + - Agent + - Bash(git *) + - Bash(python3 *) + - Bash(codex *) +disallowed-tools: + - AskUserQuestion + - EnterWorktree + - ExitWorktree +hooks: + Stop: + - hooks: + - type: command + command: 'python3 "${CLAUDE_PLUGIN_ROOT}/scripts/stop_gate.py"' + timeout: 10 +--- + +# Autonomous feature development — current checkout on main + +Implement this feature idea directly in the user's current checkout, including when the +current branch is `main` or `master`: + +> $ARGUMENTS + +This is the explicit-opt-in variant of `autonomous-current`. By invoking this skill the user has +acknowledged that the agent's edits will land directly in `main`/`master`. Prefer +`autonomous-feature` (isolated worktree) or `autonomous-current` (clean feature branch) when +either is appropriate; this skill exists for cases where the user really does want changes in +the current `main`/`master` checkout. + +## Non-negotiable boundaries + +- This skill must NOT call `EnterWorktree` / `ExitWorktree`. All edits land in the current + checkout. Do not create or enter `.claude/worktrees/*`. +- Require a clean working tree. `git status --porcelain` must be empty before initializing. If + it is not, stop and report which entries are dirty. +- Do not create commits. The user will review and commit with their normal `git diff`/commit + flow. +- Preserve unrelated user changes. +- Never push, merge, publish, deploy, rotate credentials, or modify remote infrastructure. +- Never use `danger-full-access`, `--yolo`, `bypassPermissions`, or equivalent unrestricted modes. +- Never apply an irreversible database migration or delete user data. +- Do not weaken authorization, validation, tests, or static checks to make the workflow pass. +- Codex planning and review executions must remain read-only. +- Use no more than the configured review-round budget. +- Treat every Codex finding as a proposal requiring evidence-based triage. + +## Driver loop + +1. Confirm this is a Git repository. Inspect `CLAUDE.md`, repository instructions, architecture, + status, and tests. Verify the working tree is clean before initializing. + +2. Initialize in current-checkout mode on main/master: + + ```bash + python3 "${CLAUDE_PLUGIN_ROOT}/scripts/controller.py" doctor + python3 "${CLAUDE_PLUGIN_ROOT}/scripts/controller.py" init \ + --feature "$ARGUMENTS" \ + --mode standard \ + --worktree-mode current \ + --allow-main + ``` + + `init` prints the `run-state.json` path and run ID. With multiple concurrent runs, pass + `--run-id ` to all subsequent commands. If `doctor` reports a missing prerequisite, + mark the run blocked and report it rather than bypassing it. + +3. Repeatedly ask the controller for the next phase, then execute it: + + ```bash + python3 "${CLAUDE_PLUGIN_ROOT}/scripts/controller.py" next-action --json + ``` + + The response gives `phase`, `required_action`, `completion_condition`, and `references`. + Read the referenced file under `${CLAUDE_PLUGIN_ROOT}/skills/autonomous-feature/references/` + for that phase and follow it until the completion condition holds. + +4. Do not declare success until `evaluate` succeeds: + + ```bash + python3 "${CLAUDE_PLUGIN_ROOT}/scripts/controller.py" evaluate + python3 "${CLAUDE_PLUGIN_ROOT}/scripts/controller.py" status + python3 "${CLAUDE_PLUGIN_ROOT}/scripts/controller.py" usage-report + ``` + +## Final report + +- the implemented behavior; +- principal files changed (visible to the user via plain `git diff`); +- verification commands and results; +- Codex review rounds and disposition of findings; +- adversarial review result when one was required; +- per-phase usage table from `usage-report`; +- remaining risks or explicit blocked reason; +- a suggested conventional commit message (the user commits manually — this skill must not + commit). diff --git a/tests/test_controller.py b/tests/test_controller.py index 26885ac..11513b0 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -223,6 +223,50 @@ def test_current_mode_does_not_create_claude_worktrees(self) -> None: self.assertEqual(result.returncode, 0, result.stderr) self.assertFalse(worktrees_dir.exists()) + def test_current_mode_with_allow_main_does_not_create_claude_worktrees(self) -> None: + """The /autonomous-development:autonomous-main wrapper passes --allow-main; it + must still skip the disposable-worktree path.""" + for branch_name in ("main", "master"): + with self.subTest(branch=branch_name): + repo = self.make_repo() + state_home = self.make_state_home() + subprocess.run( + ["git", "-C", str(repo), "branch", "-m", branch_name], + check=True, + ) + worktrees_dir = repo / ".claude" / "worktrees" + + result = self.run_controller( + repo, + "init", + "--feature", + "Feature", + "--worktree-mode", + "current", + "--allow-main", + state_home=state_home, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertFalse(worktrees_dir.exists()) + + def test_isolated_default_unchanged_for_autonomous_feature(self) -> None: + """The /autonomous-development:autonomous-feature wrapper relies on the default + worktree-mode staying `isolated` and on the run state recording it as such.""" + repo = self.make_repo() + state_home = self.make_state_home() + + # No --worktree-mode → relies on the default. + result = self.run_controller( + repo, + "init", + "--feature", + "Feature", + state_home=state_home, + ) + self.assertEqual(result.returncode, 0, result.stderr) + state = json.loads(self._find_state_path(repo, state_home).read_text(encoding="utf-8")) + self.assertEqual(state["repository"]["worktree_mode"], "isolated") + def test_record_passing_check(self) -> None: repo = self.make_repo() state_home = self.make_state_home() diff --git a/tests/test_project_layout.py b/tests/test_project_layout.py index 99596f1..098441b 100644 --- a/tests/test_project_layout.py +++ b/tests/test_project_layout.py @@ -15,6 +15,8 @@ def test_manifest_and_components(self) -> None: self.assertEqual(manifest["name"], "autonomous-development") expected = { "autonomous-feature", + "autonomous-current", + "autonomous-main", "enhance-idea", "implementation-plan", "implement-plan", @@ -94,6 +96,57 @@ def test_autonomous_feature_skill_mentions_worktree_modes(self) -> None: self.assertIn("EnterWorktree", text) self.assertIn("current-checkout mode", text) + def test_autonomous_current_skill_uses_current_mode_without_worktree(self) -> None: + text = (ROOT / "skills" / "autonomous-current" / "SKILL.md").read_text( + encoding="utf-8" + ) + self.assertIn("--mode standard", text) + self.assertIn("--worktree-mode current", text) + self.assertNotIn("--allow-main", text) + # Must NOT enter a disposable worktree. + self.assertIn("EnterWorktree", text) # mentioned only to disallow it + # The frontmatter explicitly disallows the worktree tools. + head = text.split("---", 2)[1] + self.assertIn("EnterWorktree", head) + self.assertIn("ExitWorktree", head) + # Disallows main/master. + self.assertIn("main", text) + self.assertIn("master", text) + # Never commits. + self.assertIn("Do not create commits", text) + # Does not create .claude/worktrees/*. + self.assertIn(".claude/worktrees", text) + + def test_autonomous_main_skill_passes_allow_main(self) -> None: + text = (ROOT / "skills" / "autonomous-main" / "SKILL.md").read_text( + encoding="utf-8" + ) + self.assertIn("--mode standard", text) + self.assertIn("--worktree-mode current", text) + self.assertIn("--allow-main", text) + # Must NOT enter a disposable worktree. + head = text.split("---", 2)[1] + self.assertIn("EnterWorktree", head) + self.assertIn("ExitWorktree", head) + # Never commits. + self.assertIn("Do not create commits", text) + # Still requires a clean tree. + self.assertIn("clean working tree", text) + # Does not create .claude/worktrees/*. + self.assertIn(".claude/worktrees", text) + + def test_autonomous_feature_skill_remains_isolated_default(self) -> None: + text = (ROOT / "skills" / "autonomous-feature" / "SKILL.md").read_text( + encoding="utf-8" + ) + # The default invocation must keep the isolated worktree default. + self.assertIn("--worktree-mode isolated", text) + # And the frontmatter must still allow EnterWorktree/ExitWorktree so the + # default workflow can enter a disposable worktree. + head = text.split("---", 2)[1] + self.assertIn("EnterWorktree", head) + self.assertIn("ExitWorktree", head) + if __name__ == "__main__": unittest.main()