From df2ac184dd3b3f0e718484bf011a41bfabf313f3 Mon Sep 17 00:00:00 2001 From: mkorwel Date: Thu, 19 Mar 2026 09:53:51 -0700 Subject: [PATCH] feat(workspaces): transform workspaces feature into a distributable extension --- .../skills/workspaces/tests/matrix.test.ts | 80 ------------- .../workspaces/tests/orchestration.test.ts | 89 --------------- .../workspaces/tests/playbooks/fix.test.ts | 23 ---- .../workspaces/tests/playbooks/ready.test.ts | 33 ------ .../workspaces/tests/playbooks/review.test.ts | 36 ------ .../skills/workspaces/tests/provider.test.ts | 64 ----------- .../workspaces/docs}/FUTURE_STATE.md | 0 .../workspaces/docs}/GEMINI.md | 0 .../workspaces/docs}/NETWORK_RESEARCH.md | 0 .../workspaces/docs}/NEXT_MISSION.md | 0 .../workspaces/docs}/README.md | 0 .../workspaces/docs}/SKILL.md | 0 .../docs}/plan.workerabstraction.md | 0 .../workspaces/policies/workspace-policy.toml | 0 .../workspaces/scripts/TaskRunner.ts | 0 .../workspaces/scripts/attach.ts | 0 .../workspaces/scripts/check.ts | 0 .../workspaces/scripts/clean.ts | 0 .../workspaces/scripts/entrypoint.ts | 0 .../workspaces/scripts/fleet.ts | 0 .../workspaces/scripts/logs.ts | 0 .../workspaces/scripts/orchestrator.ts | 0 .../workspaces/scripts/playbooks/fix.ts | 0 .../workspaces/scripts/playbooks/implement.ts | 0 .../workspaces/scripts/playbooks/ready.ts | 0 .../workspaces/scripts/playbooks/review.ts | 0 .../scripts/providers/BaseProvider.ts | 0 .../scripts/providers/GceConnectionManager.ts | 0 .../scripts/providers/GceCosProvider.ts | 0 .../scripts/providers/ProviderFactory.ts | 0 .../workspaces/scripts/provision-worker.sh | 0 .../workspaces/scripts/setup.ts | 4 +- .../workspaces/scripts/status.ts | 0 .../workspaces/scripts/worker.ts | 0 extensions/workspaces/skills/SKILL.md | 39 +++++++ gemini-extension.json | 11 ++ scripts/workspaces.ts | 108 +++++++++--------- 37 files changed, 105 insertions(+), 382 deletions(-) delete mode 100644 .gemini/skills/workspaces/tests/matrix.test.ts delete mode 100644 .gemini/skills/workspaces/tests/orchestration.test.ts delete mode 100644 .gemini/skills/workspaces/tests/playbooks/fix.test.ts delete mode 100644 .gemini/skills/workspaces/tests/playbooks/ready.test.ts delete mode 100644 .gemini/skills/workspaces/tests/playbooks/review.test.ts delete mode 100644 .gemini/skills/workspaces/tests/provider.test.ts rename {.gemini/skills/workspaces => extensions/workspaces/docs}/FUTURE_STATE.md (100%) rename {.gemini/skills/workspaces => extensions/workspaces/docs}/GEMINI.md (100%) rename {.gemini/skills/workspaces => extensions/workspaces/docs}/NETWORK_RESEARCH.md (100%) rename {.gemini/skills/workspaces => extensions/workspaces/docs}/NEXT_MISSION.md (100%) rename {.gemini/skills/workspaces => extensions/workspaces/docs}/README.md (100%) rename {.gemini/skills/workspaces => extensions/workspaces/docs}/SKILL.md (100%) rename {.gemini/skills/workspaces => extensions/workspaces/docs}/plan.workerabstraction.md (100%) rename .gemini/skills/workspaces/policy.toml => extensions/workspaces/policies/workspace-policy.toml (100%) rename {.gemini/skills => extensions}/workspaces/scripts/TaskRunner.ts (100%) rename {.gemini/skills => extensions}/workspaces/scripts/attach.ts (100%) rename {.gemini/skills => extensions}/workspaces/scripts/check.ts (100%) rename {.gemini/skills => extensions}/workspaces/scripts/clean.ts (100%) rename {.gemini/skills => extensions}/workspaces/scripts/entrypoint.ts (100%) rename {.gemini/skills => extensions}/workspaces/scripts/fleet.ts (100%) rename {.gemini/skills => extensions}/workspaces/scripts/logs.ts (100%) rename {.gemini/skills => extensions}/workspaces/scripts/orchestrator.ts (100%) rename {.gemini/skills => extensions}/workspaces/scripts/playbooks/fix.ts (100%) rename {.gemini/skills => extensions}/workspaces/scripts/playbooks/implement.ts (100%) rename {.gemini/skills => extensions}/workspaces/scripts/playbooks/ready.ts (100%) rename {.gemini/skills => extensions}/workspaces/scripts/playbooks/review.ts (100%) rename {.gemini/skills => extensions}/workspaces/scripts/providers/BaseProvider.ts (100%) rename {.gemini/skills => extensions}/workspaces/scripts/providers/GceConnectionManager.ts (100%) rename {.gemini/skills => extensions}/workspaces/scripts/providers/GceCosProvider.ts (100%) rename {.gemini/skills => extensions}/workspaces/scripts/providers/ProviderFactory.ts (100%) rename {.gemini/skills => extensions}/workspaces/scripts/provision-worker.sh (100%) rename {.gemini/skills => extensions}/workspaces/scripts/setup.ts (98%) rename {.gemini/skills => extensions}/workspaces/scripts/status.ts (100%) rename {.gemini/skills => extensions}/workspaces/scripts/worker.ts (100%) create mode 100644 extensions/workspaces/skills/SKILL.md create mode 100644 gemini-extension.json diff --git a/.gemini/skills/workspaces/tests/matrix.test.ts b/.gemini/skills/workspaces/tests/matrix.test.ts deleted file mode 100644 index 76f3035f51..0000000000 --- a/.gemini/skills/workspaces/tests/matrix.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { spawnSync, spawn } from 'child_process'; -import fs from 'fs'; -import readline from 'readline'; -import { runOrchestrator } from '../scripts/orchestrator.ts'; -import { runWorker } from '../scripts/worker.ts'; -import { ProviderFactory } from '../scripts/providers/ProviderFactory.ts'; - -vi.mock('child_process'); -vi.mock('fs'); -vi.mock('readline'); -vi.mock('../scripts/providers/ProviderFactory.ts'); - -describe('Workspace Tooling Matrix', () => { - const mockSettings = { - maintainer: { - workspace: { - projectId: 'test-project', - zone: 'us-west1-a', - remoteWorkDir: '/home/node/dev/main' - } - } - }; - - const mockProvider = { - provision: vi.fn().mockResolvedValue(0), - ensureReady: vi.fn().mockResolvedValue(0), - setup: vi.fn().mockResolvedValue(0), - exec: vi.fn().mockResolvedValue(0), - getExecOutput: vi.fn().mockResolvedValue({ status: 0, stdout: '', stderr: '' }), - sync: vi.fn().mockResolvedValue(0), - getStatus: vi.fn().mockResolvedValue({ name: 'test-instance', status: 'RUNNING' }), - stop: vi.fn().mockResolvedValue(0) - }; - - beforeEach(() => { - vi.clearAllMocks(); - vi.mocked(fs.existsSync).mockReturnValue(true); - vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockSettings)); - vi.mocked(ProviderFactory.getProvider).mockReturnValue(mockProvider as any); - - vi.mocked(spawnSync).mockImplementation((cmd: any) => { - if (cmd === 'gh') return { status: 0, stdout: Buffer.from('test-branch\n') } as any; - return { status: 0, stdout: Buffer.from('') } as any; - }); - - vi.mocked(spawn).mockImplementation(() => { - return { - stdout: { pipe: vi.fn(), on: vi.fn() }, - stderr: { pipe: vi.fn(), on: vi.fn() }, - on: vi.fn((event, cb) => { if (event === 'close') cb(0); }), - pid: 1234 - } as any; - }); - - vi.spyOn(process, 'chdir').mockImplementation(() => {}); - }); - - describe('Implement Playbook', () => { - it('should create a branch and run research/implementation', async () => { - await runOrchestrator(['456', 'implement'], {}); - - expect(mockProvider.exec).toHaveBeenCalledWith(expect.stringContaining('git worktree add'), expect.any(Object)); - expect(mockProvider.exec).toHaveBeenCalledWith(expect.stringContaining('tmux new-session'), expect.any(Object)); - }); - }); - - describe('Fix Playbook', () => { - it('should launch the agentic fix-pr skill', async () => { - vi.mocked(fs.existsSync).mockReturnValue(true); - await runWorker(['123', 'test-branch', '/path/policy', 'fix']); - - const spawnSyncCalls = vi.mocked(spawnSync).mock.calls; - const fixCall = spawnSyncCalls.find(call => - JSON.stringify(call).includes("activate the 'fix-pr' skill") - ); - expect(fixCall).toBeDefined(); - }); - }); -}); diff --git a/.gemini/skills/workspaces/tests/orchestration.test.ts b/.gemini/skills/workspaces/tests/orchestration.test.ts deleted file mode 100644 index c62339ddc7..0000000000 --- a/.gemini/skills/workspaces/tests/orchestration.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { spawnSync } from 'child_process'; -import fs from 'fs'; -import readline from 'readline'; -import { runOrchestrator } from '../scripts/orchestrator.ts'; -import { runSetup } from '../scripts/setup.ts'; -import { ProviderFactory } from '../scripts/providers/ProviderFactory.ts'; - -vi.mock('child_process'); -vi.mock('fs'); -vi.mock('readline'); -vi.mock('../scripts/providers/ProviderFactory.ts'); - -describe('Workspace Orchestration (Refactored)', () => { - const mockSettings = { - maintainer: { - workspace: { - projectId: 'test-project', - zone: 'us-west1-a', - remoteWorkDir: '/home/node/dev/main' - } - } - }; - - const mockProvider = { - provision: vi.fn().mockResolvedValue(0), - ensureReady: vi.fn().mockResolvedValue(0), - setup: vi.fn().mockResolvedValue(0), - exec: vi.fn().mockResolvedValue(0), - getExecOutput: vi.fn().mockResolvedValue({ status: 0, stdout: '', stderr: '' }), - sync: vi.fn().mockResolvedValue(0), - getStatus: vi.fn().mockResolvedValue({ name: 'test-instance', status: 'RUNNING' }), - stop: vi.fn().mockResolvedValue(0) - }; - - beforeEach(() => { - vi.clearAllMocks(); - vi.mocked(fs.existsSync).mockReturnValue(true); - vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockSettings)); - vi.mocked(fs.writeFileSync).mockReturnValue(undefined as any); - - // Explicitly set the mock return value for each test - vi.mocked(ProviderFactory.getProvider).mockReturnValue(mockProvider as any); - - vi.mocked(spawnSync).mockImplementation((cmd: any) => { - if (cmd === 'gh') return { status: 0, stdout: Buffer.from('test-branch\n') } as any; - return { status: 0, stdout: Buffer.from('') } as any; - }); - - vi.spyOn(process, 'chdir').mockImplementation(() => {}); - }); - - describe('orchestrator.ts', () => { - it('should wake the worker and execute remote commands', async () => { - await runOrchestrator(['123'], { USER: 'testuser' }); - - expect(mockProvider.ensureReady).toHaveBeenCalled(); - expect(mockProvider.exec).toHaveBeenCalledWith(expect.stringContaining('git worktree add'), expect.any(Object)); - }); - }); - - describe('setup.ts', () => { - const mockInterface = { - question: vi.fn(), - close: vi.fn() - }; - - beforeEach(() => { - vi.mocked(readline.createInterface).mockReturnValue(mockInterface as any); - }); - - it('should use the provider to configure SSH and sync scripts', async () => { - mockInterface.question - .mockImplementationOnce((q, cb) => cb('test-project')) - .mockImplementationOnce((q, cb) => cb('us-west1-a')) - .mockImplementationOnce((q, cb) => cb('.internal')) // dnsSuffix - .mockImplementationOnce((q, cb) => cb('n')) // sync auth - .mockImplementationOnce((q, cb) => cb('n')) // scoped token - .mockImplementationOnce((q, cb) => cb('n')); // clone - - // Ensure mockProvider is returned - vi.mocked(ProviderFactory.getProvider).mockReturnValue(mockProvider as any); - - await runSetup({ USER: 'testuser' }); - - expect(mockProvider.setup).toHaveBeenCalled(); - }); - }); -}); diff --git a/.gemini/skills/workspaces/tests/playbooks/fix.test.ts b/.gemini/skills/workspaces/tests/playbooks/fix.test.ts deleted file mode 100644 index 8e5e3f683d..0000000000 --- a/.gemini/skills/workspaces/tests/playbooks/fix.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { spawnSync } from 'child_process'; -import fs from 'fs'; -import { runFixPlaybook } from '../../scripts/playbooks/fix.ts'; - -vi.mock('child_process'); -vi.mock('fs'); - -describe('Fix Playbook', () => { - beforeEach(() => { - vi.resetAllMocks(); - vi.mocked(spawnSync).mockReturnValue({ status: 0 } as any); - }); - - it('should launch the agentic fix-pr skill via spawnSync', async () => { - const status = await runFixPlaybook('123', '/tmp/target', '/path/policy', '/path/gemini'); - - expect(status).toBe(0); - const spawnCalls = vi.mocked(spawnSync).mock.calls; - - expect(spawnCalls.some(c => JSON.stringify(c).includes("activate the 'fix-pr' skill"))).toBe(true); - }); -}); diff --git a/.gemini/skills/workspaces/tests/playbooks/ready.test.ts b/.gemini/skills/workspaces/tests/playbooks/ready.test.ts deleted file mode 100644 index c75a9540a6..0000000000 --- a/.gemini/skills/workspaces/tests/playbooks/ready.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { spawnSync, spawn } from 'child_process'; -import fs from 'fs'; -import { runReadyPlaybook } from '../../scripts/playbooks/ready.ts'; - -vi.mock('child_process'); -vi.mock('fs'); - -describe('Ready Playbook', () => { - beforeEach(() => { - vi.resetAllMocks(); - vi.mocked(fs.mkdirSync).mockReturnValue(undefined as any); - vi.mocked(fs.writeFileSync).mockReturnValue(undefined as any); - vi.mocked(fs.createWriteStream).mockReturnValue({ pipe: vi.fn() } as any); - - vi.mocked(spawn).mockImplementation(() => { - return { - stdout: { pipe: vi.fn(), on: vi.fn() }, - stderr: { pipe: vi.fn(), on: vi.fn() }, - on: vi.fn((event, cb) => { if (event === 'close') cb(0); }) - } as any; - }); - }); - - it('should register and run clean, preflight, and conflict checks', async () => { - runReadyPlaybook('123', '/tmp/target', '/path/policy', '/path/gemini'); - - const spawnCalls = vi.mocked(spawn).mock.calls; - - expect(spawnCalls.some(c => c[0].includes('npm run clean'))).toBe(true); - expect(spawnCalls.some(c => c[0].includes('git fetch origin main'))).toBe(true); - }); -}); diff --git a/.gemini/skills/workspaces/tests/playbooks/review.test.ts b/.gemini/skills/workspaces/tests/playbooks/review.test.ts deleted file mode 100644 index 23140c071c..0000000000 --- a/.gemini/skills/workspaces/tests/playbooks/review.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { spawn } from 'child_process'; -import fs from 'fs'; -import { runReviewPlaybook } from '../../scripts/playbooks/review.ts'; - -vi.mock('child_process'); -vi.mock('fs'); - -describe('Review Playbook', () => { - beforeEach(() => { - vi.resetAllMocks(); - vi.mocked(fs.mkdirSync).mockReturnValue(undefined as any); - vi.mocked(fs.writeFileSync).mockReturnValue(undefined as any); - vi.mocked(fs.createWriteStream).mockReturnValue({ pipe: vi.fn() } as any); - - vi.mocked(spawn).mockImplementation(() => { - return { - stdout: { pipe: vi.fn(), on: vi.fn() }, - stderr: { pipe: vi.fn(), on: vi.fn() }, - on: vi.fn((event, cb) => { if (event === 'close') cb(0); }) - } as any; - }); - }); - - it('should register and run build, ci, and review tasks', async () => { - // We don't await because TaskRunner uses setInterval and we'd need to mock timers - // but we can check if spawn was called with the right commands. - runReviewPlaybook('123', '/tmp/target', '/path/policy', '/path/gemini'); - - const spawnCalls = vi.mocked(spawn).mock.calls; - - expect(spawnCalls.some(c => c[0].includes('npm ci'))).toBe(true); - expect(spawnCalls.some(c => c[0].includes('gh pr checks'))).toBe(true); - expect(spawnCalls.some(c => c[0].includes("activate the 'review-pr' skill"))).toBe(true); - }); -}); diff --git a/.gemini/skills/workspaces/tests/provider.test.ts b/.gemini/skills/workspaces/tests/provider.test.ts deleted file mode 100644 index 786e762fa8..0000000000 --- a/.gemini/skills/workspaces/tests/provider.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { spawnSync } from 'child_process'; -import fs from 'fs'; -import path from 'path'; -import { GceCosProvider } from '../scripts/providers/GceCosProvider.ts'; - -vi.mock('child_process'); -vi.mock('fs'); - -describe('GceCosProvider', () => { - const mockConfig = { - projectId: 'test-project', - zone: 'us-west1-a', - instanceName: 'test-instance', - repoRoot: '/test-root' - }; - - let provider: GceCosProvider; - - beforeEach(() => { - vi.resetAllMocks(); - provider = new GceCosProvider(mockConfig.projectId, mockConfig.zone, mockConfig.instanceName, mockConfig.repoRoot); - - vi.mocked(spawnSync).mockReturnValue({ status: 0, stdout: Buffer.from(''), stderr: Buffer.from('') } as any); - }); - - it('should provision an instance with COS image and startup script', async () => { - await provider.provision(); - - const calls = vi.mocked(spawnSync).mock.calls; - const createCall = calls.find(c => c[1].includes('create')); - - expect(createCall).toBeDefined(); - expect(createCall![1]).toContain('cos-stable'); - expect(createCall![1]).toContain('test-instance'); - }); - - it('should attempt direct SSH and fallback to IAP on failure', async () => { - // Fail direct SSH - vi.mocked(spawnSync) - .mockReturnValueOnce({ status: 1, stdout: Buffer.from(''), stderr: Buffer.from('fail') } as any) // direct - .mockReturnValueOnce({ status: 0, stdout: Buffer.from('ok'), stderr: Buffer.from('') } as any); // IAP - - const result = await provider.exec('echo 1'); - - expect(result).toBe(0); - const calls = vi.mocked(spawnSync).mock.calls; - expect(calls[0][0]).toBe('ssh'); - expect(calls[1][1]).toContain('--tunnel-through-iap'); - }); - - it('should sync files with IAP fallback', async () => { - // Fail direct rsync - vi.mocked(spawnSync) - .mockReturnValueOnce({ status: 1 } as any) // direct - .mockReturnValueOnce({ status: 0 } as any); // IAP - - await provider.sync('./local', '/remote'); - - const calls = vi.mocked(spawnSync).mock.calls; - expect(calls[0][0]).toBe('rsync'); - expect(calls[1][1]).toContain('gcloud compute ssh --project test-project --zone us-west1-a --tunnel-through-iap --quiet'); - }); -}); diff --git a/.gemini/skills/workspaces/FUTURE_STATE.md b/extensions/workspaces/docs/FUTURE_STATE.md similarity index 100% rename from .gemini/skills/workspaces/FUTURE_STATE.md rename to extensions/workspaces/docs/FUTURE_STATE.md diff --git a/.gemini/skills/workspaces/GEMINI.md b/extensions/workspaces/docs/GEMINI.md similarity index 100% rename from .gemini/skills/workspaces/GEMINI.md rename to extensions/workspaces/docs/GEMINI.md diff --git a/.gemini/skills/workspaces/NETWORK_RESEARCH.md b/extensions/workspaces/docs/NETWORK_RESEARCH.md similarity index 100% rename from .gemini/skills/workspaces/NETWORK_RESEARCH.md rename to extensions/workspaces/docs/NETWORK_RESEARCH.md diff --git a/.gemini/skills/workspaces/NEXT_MISSION.md b/extensions/workspaces/docs/NEXT_MISSION.md similarity index 100% rename from .gemini/skills/workspaces/NEXT_MISSION.md rename to extensions/workspaces/docs/NEXT_MISSION.md diff --git a/.gemini/skills/workspaces/README.md b/extensions/workspaces/docs/README.md similarity index 100% rename from .gemini/skills/workspaces/README.md rename to extensions/workspaces/docs/README.md diff --git a/.gemini/skills/workspaces/SKILL.md b/extensions/workspaces/docs/SKILL.md similarity index 100% rename from .gemini/skills/workspaces/SKILL.md rename to extensions/workspaces/docs/SKILL.md diff --git a/.gemini/skills/workspaces/plan.workerabstraction.md b/extensions/workspaces/docs/plan.workerabstraction.md similarity index 100% rename from .gemini/skills/workspaces/plan.workerabstraction.md rename to extensions/workspaces/docs/plan.workerabstraction.md diff --git a/.gemini/skills/workspaces/policy.toml b/extensions/workspaces/policies/workspace-policy.toml similarity index 100% rename from .gemini/skills/workspaces/policy.toml rename to extensions/workspaces/policies/workspace-policy.toml diff --git a/.gemini/skills/workspaces/scripts/TaskRunner.ts b/extensions/workspaces/scripts/TaskRunner.ts similarity index 100% rename from .gemini/skills/workspaces/scripts/TaskRunner.ts rename to extensions/workspaces/scripts/TaskRunner.ts diff --git a/.gemini/skills/workspaces/scripts/attach.ts b/extensions/workspaces/scripts/attach.ts similarity index 100% rename from .gemini/skills/workspaces/scripts/attach.ts rename to extensions/workspaces/scripts/attach.ts diff --git a/.gemini/skills/workspaces/scripts/check.ts b/extensions/workspaces/scripts/check.ts similarity index 100% rename from .gemini/skills/workspaces/scripts/check.ts rename to extensions/workspaces/scripts/check.ts diff --git a/.gemini/skills/workspaces/scripts/clean.ts b/extensions/workspaces/scripts/clean.ts similarity index 100% rename from .gemini/skills/workspaces/scripts/clean.ts rename to extensions/workspaces/scripts/clean.ts diff --git a/.gemini/skills/workspaces/scripts/entrypoint.ts b/extensions/workspaces/scripts/entrypoint.ts similarity index 100% rename from .gemini/skills/workspaces/scripts/entrypoint.ts rename to extensions/workspaces/scripts/entrypoint.ts diff --git a/.gemini/skills/workspaces/scripts/fleet.ts b/extensions/workspaces/scripts/fleet.ts similarity index 100% rename from .gemini/skills/workspaces/scripts/fleet.ts rename to extensions/workspaces/scripts/fleet.ts diff --git a/.gemini/skills/workspaces/scripts/logs.ts b/extensions/workspaces/scripts/logs.ts similarity index 100% rename from .gemini/skills/workspaces/scripts/logs.ts rename to extensions/workspaces/scripts/logs.ts diff --git a/.gemini/skills/workspaces/scripts/orchestrator.ts b/extensions/workspaces/scripts/orchestrator.ts similarity index 100% rename from .gemini/skills/workspaces/scripts/orchestrator.ts rename to extensions/workspaces/scripts/orchestrator.ts diff --git a/.gemini/skills/workspaces/scripts/playbooks/fix.ts b/extensions/workspaces/scripts/playbooks/fix.ts similarity index 100% rename from .gemini/skills/workspaces/scripts/playbooks/fix.ts rename to extensions/workspaces/scripts/playbooks/fix.ts diff --git a/.gemini/skills/workspaces/scripts/playbooks/implement.ts b/extensions/workspaces/scripts/playbooks/implement.ts similarity index 100% rename from .gemini/skills/workspaces/scripts/playbooks/implement.ts rename to extensions/workspaces/scripts/playbooks/implement.ts diff --git a/.gemini/skills/workspaces/scripts/playbooks/ready.ts b/extensions/workspaces/scripts/playbooks/ready.ts similarity index 100% rename from .gemini/skills/workspaces/scripts/playbooks/ready.ts rename to extensions/workspaces/scripts/playbooks/ready.ts diff --git a/.gemini/skills/workspaces/scripts/playbooks/review.ts b/extensions/workspaces/scripts/playbooks/review.ts similarity index 100% rename from .gemini/skills/workspaces/scripts/playbooks/review.ts rename to extensions/workspaces/scripts/playbooks/review.ts diff --git a/.gemini/skills/workspaces/scripts/providers/BaseProvider.ts b/extensions/workspaces/scripts/providers/BaseProvider.ts similarity index 100% rename from .gemini/skills/workspaces/scripts/providers/BaseProvider.ts rename to extensions/workspaces/scripts/providers/BaseProvider.ts diff --git a/.gemini/skills/workspaces/scripts/providers/GceConnectionManager.ts b/extensions/workspaces/scripts/providers/GceConnectionManager.ts similarity index 100% rename from .gemini/skills/workspaces/scripts/providers/GceConnectionManager.ts rename to extensions/workspaces/scripts/providers/GceConnectionManager.ts diff --git a/.gemini/skills/workspaces/scripts/providers/GceCosProvider.ts b/extensions/workspaces/scripts/providers/GceCosProvider.ts similarity index 100% rename from .gemini/skills/workspaces/scripts/providers/GceCosProvider.ts rename to extensions/workspaces/scripts/providers/GceCosProvider.ts diff --git a/.gemini/skills/workspaces/scripts/providers/ProviderFactory.ts b/extensions/workspaces/scripts/providers/ProviderFactory.ts similarity index 100% rename from .gemini/skills/workspaces/scripts/providers/ProviderFactory.ts rename to extensions/workspaces/scripts/providers/ProviderFactory.ts diff --git a/.gemini/skills/workspaces/scripts/provision-worker.sh b/extensions/workspaces/scripts/provision-worker.sh similarity index 100% rename from .gemini/skills/workspaces/scripts/provision-worker.sh rename to extensions/workspaces/scripts/provision-worker.sh diff --git a/.gemini/skills/workspaces/scripts/setup.ts b/extensions/workspaces/scripts/setup.ts similarity index 98% rename from .gemini/skills/workspaces/scripts/setup.ts rename to extensions/workspaces/scripts/setup.ts index bd850e0d6a..8f9fa53406 100644 --- a/.gemini/skills/workspaces/scripts/setup.ts +++ b/extensions/workspaces/scripts/setup.ts @@ -301,8 +301,8 @@ and full builds) to a dedicated, high-performance GCP worker. await provider.exec(`sudo chmod -R 777 ${workspaceRoot}`); // 1. Sync Scripts & Policies - await provider.sync('.gemini/skills/workspaces/scripts/', `${persistentScripts}/`, { delete: true, sudo: true }); - await provider.sync('.gemini/skills/workspaces/policy.toml', `${workspaceRoot}/policies/workspace-policy.toml`, { sudo: true }); + await provider.sync('extensions/workspaces/scripts/', `${persistentScripts}/`, { delete: true, sudo: true }); + await provider.sync('extensions/workspaces/policies/workspace-policy.toml', `${workspaceRoot}/policies/workspace-policy.toml`, { sudo: true }); // 2. Initialize Remote Gemini Config with Auth console.log('⚙️ Initializing remote Gemini configuration...'); diff --git a/.gemini/skills/workspaces/scripts/status.ts b/extensions/workspaces/scripts/status.ts similarity index 100% rename from .gemini/skills/workspaces/scripts/status.ts rename to extensions/workspaces/scripts/status.ts diff --git a/.gemini/skills/workspaces/scripts/worker.ts b/extensions/workspaces/scripts/worker.ts similarity index 100% rename from .gemini/skills/workspaces/scripts/worker.ts rename to extensions/workspaces/scripts/worker.ts diff --git a/extensions/workspaces/skills/SKILL.md b/extensions/workspaces/skills/SKILL.md new file mode 100644 index 0000000000..6f60f551f1 --- /dev/null +++ b/extensions/workspaces/skills/SKILL.md @@ -0,0 +1,39 @@ +--- +name: workspaces +description: Expertise in managing and utilizing Gemini Workspaces for high-performance remote development tasks. +--- + +# Gemini Workspaces Skill + +This skill enables the agent to utilize **Gemini Workspaces**—a high-performance, persistent remote development platform. It allows the agent to move intensive tasks (PR reviews, complex repairs, full builds) from the local environment to a dedicated cloud worker. + +## 🛠️ Key Capabilities +1. **Persistent Execution**: Jobs run in remote `tmux` sessions. Disconnecting or crashing the local terminal does not stop the remote work. +2. **Parallel Infrastructure**: The agent can launch a heavy task (like a full build or CI run) in a workspace while continuing to assist the user locally. +3. **Behavioral Fidelity**: Remote workers have full tool access (Git, Node, Docker, etc.) and high-performance compute, allowing the agent to provide behavioral proofs of its work. + +## 📋 Instructions for the Agent + +### When to use Workspaces +- **Intensive Tasks**: Full preflight runs, large-scale refactors, or deep PR reviews. +- **Persistent Logic**: When a task is expected to take longer than a few minutes and needs to survive local connection drops. +- **Environment Isolation**: When you need a clean, high-performance environment to verify a fix without polluting the user's local machine. + +### How to use Workspaces +1. **Setup**: If the user hasn't initialized their environment, instruct them to run `npm run workspace:setup`. +2. **Launch**: Use the `workspace` command to start a playbook: + ```bash + npm run workspace [action] + ``` + - Actions: `review` (default), `fix`, `ready`. +3. **Check Status**: See global state and active sessions with `npm run workspace:status`, or deep-dive into specific PR logs with `npm run workspace:check `. +4. **Cleanup**: + - **Bulk**: Clear all sessions/worktrees with `npm run workspace:clean-all`. + - **Surgical**: Kill a specific PR task with `npm run workspace:kill `. +5. **Fleet**: Manage VM lifecycle with `npm run workspace:fleet [stop|provision|list]`. + +## ⚠️ Important Constraints +- **Absolute Paths**: Always use absolute paths (e.g., `/mnt/disks/data/...`) when orchestrating remote commands. +- **npx tsx**: When running scripts manually from the skill directory, always prefix with `npx tsx` to ensure dependencies are available. +- **Be Behavioral**: Prioritize results from live execution (behavioral proofs) over static reading. +- **Multi-tasking**: Remind the user they can continue chatting in the main window while the heavy workspace task runs in the separate terminal window. diff --git a/gemini-extension.json b/gemini-extension.json new file mode 100644 index 0000000000..3e0d5073a0 --- /dev/null +++ b/gemini-extension.json @@ -0,0 +1,11 @@ +{ + "name": "workspaces", + "version": "0.1.0", + "description": "High-performance remote development workspaces for Gemini CLI.", + "author": "Google Gemini Team", + "license": "Apache-2.0", + "skills": [ + "extensions/workspaces/skills/SKILL.md" + ], + "contextFileName": "extensions/workspaces/docs/GEMINI.md" +} diff --git a/scripts/workspaces.ts b/scripts/workspaces.ts index d10326ed1a..38454984ba 100755 --- a/scripts/workspaces.ts +++ b/scripts/workspaces.ts @@ -1,79 +1,77 @@ #!/usr/bin/env npx tsx /** - * @license - * Copyright 2026 Google LLC - * SPDX-License-Identifier: Apache-2.0 + * Unified Workspaces Entry Point (Local) + * + * Central CLI for managing Gemini Workspaces. + * Usage: scripts/workspaces.ts [args] */ -import { spawnSync } from 'node:child_process'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { spawnSync } from 'child_process'; +import path from 'path'; +import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = path.resolve(__dirname, '..'); const commands: Record = { - setup: '.gemini/skills/workspaces/scripts/setup.ts', - shell: '.gemini/skills/workspaces/scripts/orchestrator.ts shell', - check: '.gemini/skills/workspaces/scripts/check.ts', - 'clean-all': '.gemini/skills/workspaces/scripts/clean.ts', - kill: '.gemini/skills/workspaces/scripts/clean.ts', - fleet: '.gemini/skills/workspaces/scripts/fleet.ts', - status: '.gemini/skills/workspaces/scripts/status.ts', - attach: '.gemini/skills/workspaces/scripts/attach.ts', - logs: '.gemini/skills/workspaces/scripts/logs.ts', + 'setup': 'extensions/workspaces/scripts/setup.ts', + 'shell': 'extensions/workspaces/scripts/orchestrator.ts shell', + 'check': 'extensions/workspaces/scripts/check.ts', + 'clean-all': 'extensions/workspaces/scripts/clean.ts', + 'kill': 'extensions/workspaces/scripts/clean.ts', + 'fleet': 'extensions/workspaces/scripts/fleet.ts', + 'status': 'extensions/workspaces/scripts/status.ts', + 'attach': 'extensions/workspaces/scripts/attach.ts', + 'logs': 'extensions/workspaces/scripts/logs.ts', }; function printUsage() { - console.log('Gemini Workspaces Management CLI'); - console.log( - '\nUsage: scripts/workspaces.ts [args] [--open foreground|tab|window]', - ); - console.log('\nCommands:'); - console.log( - ' setup Initialize or reconfigure your remote worker', - ); - console.log(' [action] Launch a PR task (review, fix, ready)'); - console.log(' shell [id] Open an ad-hoc interactive session'); - console.log(' status See worker and session overview'); - console.log(' check Deep-dive into PR logs'); - console.log(' kill Surgical removal of a task'); - console.log(' clean-all Full remote cleanup'); - console.log(' fleet Manage VM life cycle (stop, provision)'); - process.exit(1); + console.log('Gemini Workspaces Management CLI'); + console.log('\nUsage: scripts/workspaces.ts [args] [--open foreground|tab|window]'); + console.log('\nCommands:'); + console.log(' setup Initialize or reconfigure your remote worker'); + console.log(' [action] Launch a PR task (review, fix, ready)'); + console.log(' shell [id] Open an ad-hoc interactive session'); + console.log(' status See worker and session overview'); + console.log(' check Deep-dive into PR logs'); + console.log(' kill Surgical removal of a task'); + console.log(' clean-all Full remote cleanup'); + console.log(' fleet Manage VM life cycle (stop, provision)'); + process.exit(1); } async function main() { - const args = process.argv.slice(2); - const cmd = args[0]; + const args = process.argv.slice(2); + const cmd = args[0]; - if (!cmd || cmd === '--help' || cmd === '-h') { - printUsage(); - } + if (!cmd || cmd === '--help' || cmd === '-h') { + printUsage(); + } - let scriptPath = commands[cmd]; - let finalArgs = args.slice(1); + let scriptPath = commands[cmd]; + let finalArgs = args.slice(1); - // Default: If it's a number, it's a PR orchestrator task - if (!scriptPath && /^\d+$/.test(cmd)) { - scriptPath = '.gemini/skills/workspaces/scripts/orchestrator.ts'; - finalArgs = args; // Pass the PR number as the first arg - } + // Default: If it's a number, it's a PR orchestrator task + if (!scriptPath && /^\d+$/.test(cmd)) { + scriptPath = 'extensions/workspaces/scripts/orchestrator.ts'; + finalArgs = args; // Pass the PR number as the first arg + } - if (!scriptPath) { - console.error(`❌ Unknown command: ${cmd}`); - printUsage(); - } + if (!scriptPath) { + console.error(`❌ Unknown command: ${cmd}`); + printUsage(); + } - const [realScript, ...internalArgs] = scriptPath.split(' '); - const fullScriptPath = path.join(REPO_ROOT, realScript); + const [realScript, ...internalArgs] = scriptPath.split(' '); + const fullScriptPath = path.join(REPO_ROOT, realScript); - const result = spawnSync( - 'npx', - ['tsx', fullScriptPath, ...internalArgs, ...finalArgs], - { stdio: 'inherit' }, - ); + const result = spawnSync('npx', [ + 'tsx', + fullScriptPath, + ...internalArgs, + ...finalArgs + ], { stdio: 'inherit' }); - process.exit(result.status ?? 0); + process.exit(result.status ?? 0); } main().catch(console.error);