From ac32b3ef14c0959355004d44bd2612d84a5dbb1b Mon Sep 17 00:00:00 2001 From: Noisemaker111 <139656120+Noisemaker111@users.noreply.github.com> Date: Thu, 10 Sep 2026 12:28:23 -0400 Subject: [PATCH] Use shared agents-and-main branch switching instead of project launcher --- .github/workflows/agent-checks.yml | 2 - DEVELOPMENT.md | 44 +++------------- scripts/install_workspace.ps1 | 29 ----------- scripts/test_workspace.py | 38 -------------- scripts/workspace.py | 83 ------------------------------ 5 files changed, 8 insertions(+), 188 deletions(-) delete mode 100644 scripts/install_workspace.ps1 delete mode 100644 scripts/test_workspace.py delete mode 100644 scripts/workspace.py diff --git a/.github/workflows/agent-checks.yml b/.github/workflows/agent-checks.yml index d4a7eed..58fdce6 100644 --- a/.github/workflows/agent-checks.yml +++ b/.github/workflows/agent-checks.yml @@ -30,5 +30,3 @@ jobs: run: python -m unittest discover -s experiments/command_specialist -p test_contract.py -v - name: Frozen main candidate and patch notes run: python scripts/check_release.py - - name: Session workspace isolation - run: python -m unittest discover -s scripts -p test_workspace.py -v diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index d2ea3e6..46c2b34 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -18,7 +18,6 @@ existing PRs for the same work. Run commands from the repository root with Pytho python -m compileall -q scripts experiments/command_specialist python -m unittest discover -s experiments/command_specialist -p test_bindings.py -v python -m unittest discover -s experiments/command_specialist -p test_contract.py -v -python -m unittest discover -s scripts -p test_workspace.py -v ``` Also verify the affected user operation through the public CLI and reopen its saved @@ -77,40 +76,13 @@ by Jon and agents: human authorization is a workflow rule, not an independently verified GitHub reviewer identity. The coordinator cannot merge stable. No package or runtime deployment is implied by either merge. -## Fresh session workspaces +## Switching branches -Install the PowerShell shortcut once from a reviewed checkout: +Use the shared `agents-and-main` skill command: `sb agents` or `sb main` inside +the intended project checkout. It switches the branch and fast-forwards from +origin; Git retains the selection. Install the command once from the skill's +`scripts/install.ps1`. No project-specific launcher or selection state is needed. -```powershell -./scripts/install_workspace.ps1 -Repository C:/Users/Jk101/Projects/shell-forensics -``` - -The installer copies the helper into `~/.local/share/shell-forensics` and puts -`sf.ps1` in `~/.local/bin` (which must be on PATH). It requires Git and Python; -`sf codex` also requires the Codex CLI. Re-run the installer to update the helper. - -| Command | Effect | -| --- | --- | -| `sf agents` | Select development for future workspaces (the initial default). | -| `sf main` | Select stable for future workspaces. | -| `sf status` | Show the saved selection. | -| `sf new --name my-task` | Fetch the selected branch and print a fresh owned workspace path. | -| `sf codex` | Create a fresh workspace and launch Codex CLI with `-C` pointing there. | - -Run these from any directory. For the Codex desktop app, open the path printed by -`sf new`. Existing sessions opened at home, Projects, or the old repository folder -are not automatically moved. The root checkout is an anchor, not a rolling live -installation. Never flip its branch underneath another session. - -Each session gets its own writable branch under `.worktrees/sessions`, starting -at an exact fetched commit. Integration goes through the normal agents PR/CI path; -new sessions receive later integrated changes, while active sessions keep their -files. The selector neither merges nor verifies CI itself. Selection and creation -receipts persist under the common Git directory's `workspace-selector`; `sf new ---json` returns the receipt. A fetch failure stops creation rather than using stale -source. Dependencies and runtime state still need the normal per-session setup; -this does not isolate the shared model service or implement a host adapter. - -No workspaces are automatically deleted or refreshed. Once a session is finished, -inspect its changes before removing its worktree with Git. Workspace creation can -consume disk space; keep unfinished work and evidence when cleaning up. +Switch only a checkout you own. Agents keep active work in isolated worktrees; +the command refuses dirty checkouts and branches owned by another worktree. +It does not move existing sessions or reload running processes. diff --git a/scripts/install_workspace.ps1 b/scripts/install_workspace.ps1 deleted file mode 100644 index d5af057..0000000 --- a/scripts/install_workspace.ps1 +++ /dev/null @@ -1,29 +0,0 @@ -param( - [Parameter(Mandatory=$true)][string]$Repository, - [string]$Destination = "$env:USERPROFILE/.local/bin" -) -$ErrorActionPreference = 'Stop' -$repositoryPath = (Resolve-Path -LiteralPath $Repository).Path -$helperDirectory = "$env:USERPROFILE/.local/share/shell-forensics" -New-Item -ItemType Directory -Force -Path $helperDirectory, $Destination | Out-Null -$helper = Join-Path $helperDirectory 'workspace.py' -Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'workspace.py') -Destination $helper -$wrapper = @' -param([Parameter(ValueFromRemainingArguments=$true)][string[]]$CommandArgs) -$ErrorActionPreference = 'Stop' -$pythonScript = '__HELPER__' -$repository = '__REPOSITORY__' -if ($CommandArgs.Count -gt 0 -and $CommandArgs[0] -eq 'codex') { - $result = & python $pythonScript --repo $repository new --json - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - $workspace = $result | ConvertFrom-Json - $launchArgs = @('-C', $workspace.path) + @($CommandArgs | Select-Object -Skip 1) - & codex @launchArgs - exit $LASTEXITCODE -} -& python $pythonScript --repo $repository @CommandArgs -exit $LASTEXITCODE -'@ -$wrapper = $wrapper.Replace('__HELPER__', $helper.Replace("'", "''")).Replace('__REPOSITORY__', $repositoryPath.Replace("'", "''")) -Set-Content -LiteralPath (Join-Path $Destination 'sf.ps1') -Value $wrapper -Encoding utf8 -Write-Output "Installed sf in $Destination. This directory must be on PATH." diff --git a/scripts/test_workspace.py b/scripts/test_workspace.py deleted file mode 100644 index 361189e..0000000 --- a/scripts/test_workspace.py +++ /dev/null @@ -1,38 +0,0 @@ -"""Consequential invariant: channel switching preserves active workspaces.""" -from pathlib import Path -import tempfile -import unittest -from workspace import create, git, selected, state_directory, write_json - -class WorkspaceIsolation(unittest.TestCase): - def test_switch_preserves_dirty_workspace_and_fetches_new_tip(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - origin, repo = root / 'origin', root / 'checkout' - origin.mkdir() - git(origin, 'init', '-b', 'main') - git(origin, 'config', 'user.name', 'Fixture') - git(origin, 'config', 'user.email', 'fixture@example.invalid') - (origin / 'source').write_text('stable') - git(origin, 'add', 'source') - git(origin, 'commit', '-m', 'stable') - stable = git(origin, 'rev-parse', 'HEAD') - git(origin, 'checkout', '-b', 'agents') - git(root, 'clone', str(origin), str(repo)) - first = create(repo, 'first') - dirty = Path(first['path']) / 'source' - dirty.write_text('active edits') - write_json(state_directory(repo) / 'selection.json', {'channel': 'main'}) - self.assertEqual(create(repo, 'stable')['revision'], stable) - (origin / 'source').write_text('new development') - git(origin, 'commit', '-am', 'new development') - write_json(state_directory(repo) / 'selection.json', {'channel': 'agents'}) - fresh = create(repo, 'fresh') - self.assertEqual(fresh['revision'], git(origin, 'rev-parse', 'HEAD')) - self.assertEqual(git(first['path'], 'rev-parse', 'HEAD'), first['revision']) - self.assertEqual(dirty.read_text(), 'active edits') - self.assertEqual(selected(repo), 'agents') - self.assertEqual(git(repo, 'rev-parse', 'HEAD'), stable) - -if __name__ == '__main__': - unittest.main() diff --git a/scripts/workspace.py b/scripts/workspace.py deleted file mode 100644 index d35c21b..0000000 --- a/scripts/workspace.py +++ /dev/null @@ -1,83 +0,0 @@ -"""Select a source channel and create independent, fresh session worktrees.""" -import argparse -from datetime import datetime, timezone -import json -from pathlib import Path -import re -import subprocess -import uuid - - -def git(repo, *args): - return subprocess.run( - ["git", "-C", str(repo), *args], check=True, capture_output=True, - text=True, encoding="utf-8", timeout=90, - ).stdout.strip() - - -def state_directory(repo): - return Path(git(repo, "rev-parse", "--path-format=absolute", "--git-common-dir")) / "workspace-selector" - - -def write_json(path, value): - path.parent.mkdir(parents=True, exist_ok=True) - temporary = path.with_suffix("." + uuid.uuid4().hex + ".tmp") - temporary.write_text(json.dumps(value, indent=2) + "\n", encoding="utf-8") - temporary.replace(path) - - -def selected(repo): - path = state_directory(repo) / "selection.json" - channel = json.loads(path.read_text(encoding="utf-8"))["channel"] if path.exists() else "agents" - if channel not in ("agents", "main"): - raise ValueError("Invalid saved channel") - return channel - - -def create(repo, name="session"): - if not re.fullmatch(r"[a-zA-Z0-9][a-zA-Z0-9_-]{0,47}", name): - raise ValueError("Name must be 1-48 letters, digits, underscores or hyphens") - channel = selected(repo) - # Private fetch refs prevent races with other sessions fetching origin. - fetch_ref = "refs/workspace-selector/" + uuid.uuid4().hex - try: - git(repo, "fetch", "--no-write-fetch-head", "origin", f"refs/heads/{channel}:{fetch_ref}") - revision = git(repo, "rev-parse", fetch_ref) - finally: - git(repo, "update-ref", "-d", fetch_ref) - state = state_directory(repo) - root = Path(git(repo, "worktree", "list", "--porcelain").splitlines()[0].removeprefix("worktree ")) - identifier = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S") + "-" + uuid.uuid4().hex[:8] - path = root / ".worktrees" / "sessions" / (channel + "-" + name + "-" + identifier) - branch = "session/" + name + "-" + identifier - git(repo, "worktree", "add", "-b", branch, str(path), revision) - receipt = dict(channel=channel, revision=revision, branch=branch, path=str(path), created_at=identifier) - write_json(state / "sessions" / (identifier + ".json"), receipt) - return receipt - - -def main(): - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--repo", type=Path, default=Path(__file__).resolve().parent.parent) - commands = parser.add_subparsers(dest="command", required=True) - for command in ("agents", "main", "status"): - commands.add_parser(command) - new = commands.add_parser("new") - new.add_argument("--name", default="session") - new.add_argument("--json", action="store_true") - args = parser.parse_args() - try: - if args.command in ("agents", "main"): - write_json(state_directory(args.repo) / "selection.json", {"channel": args.command}) - print(f"Selected {args.command} for new workspaces only.") - elif args.command == "status": - print(selected(args.repo)) - else: - receipt = create(args.repo, args.name) - print(json.dumps(receipt) if args.json else receipt["path"]) - except (OSError, ValueError, KeyError, subprocess.SubprocessError) as error: - parser.exit(1, f"Workspace creation/selection failed: {error}\n") - - -if __name__ == "__main__": - main()