-
Notifications
You must be signed in to change notification settings - Fork 2
Add opt-in current-checkout mode for autonomous runs #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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): | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Detached HEAD bypasses this guard.
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Separately: |
||
| 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") | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
git status --shortreports 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 saygit status --porcelain; equivalent for emptiness, just flagging the wording mismatch.)