From baee84dfa5a56fdba9dd910beffeeb2b3398d1dc Mon Sep 17 00:00:00 2001 From: Dave Date: Thu, 19 Feb 2026 16:38:49 +0000 Subject: [PATCH 1/2] Move story 32 to current and rename to multi-instance worktree support Co-Authored-By: Claude Opus 4.6 --- .../32_multi_instance_worktree_support.md} | 16 +++++++---- ...9_directory_based_workflow_coordination.md | 6 +++- .../30_worktree_agent_orchestration.md | 4 +++ ...33_worktree_diff_and_editor_integration.md | 4 +++ .../34_agent_configuration_and_roles.md | 4 +++ .../34_agent_security_and_sandboxing.md | 28 ------------------- .../35_agent_security_and_sandboxing.md | 4 +++ .../upcoming/36_enforce_story_front_matter.md | 5 ++++ 8 files changed, 36 insertions(+), 35 deletions(-) rename .story_kit/stories/{upcoming/32_worktree_agent_orchestration.md => current/32_multi_instance_worktree_support.md} (57%) delete mode 100644 .story_kit/stories/upcoming/34_agent_security_and_sandboxing.md diff --git a/.story_kit/stories/upcoming/32_worktree_agent_orchestration.md b/.story_kit/stories/current/32_multi_instance_worktree_support.md similarity index 57% rename from .story_kit/stories/upcoming/32_worktree_agent_orchestration.md rename to .story_kit/stories/current/32_multi_instance_worktree_support.md index a229859..1a92410 100644 --- a/.story_kit/stories/upcoming/32_worktree_agent_orchestration.md +++ b/.story_kit/stories/current/32_multi_instance_worktree_support.md @@ -1,18 +1,22 @@ -# Story 32: Worktree Agent Orchestration — Dynamic Port Management +--- +name: "Multi-Instance Worktree Support" +test_plan: pending +--- +# Story 32: Multi-Instance Worktree Support ## User Story -**As a** developer running multiple agents in parallel worktrees, -**I want** each server instance to bind to a unique port automatically, -**So that** I can run multiple worktree-based agents concurrently without port conflicts. +**As a** developer working across multiple git worktrees, +**I want** to run separate app instances (server + frontend) per worktree on different ports, +**So that** I can QA each worktree independently without port conflicts. ## Acceptance Criteria - [ ] Server discovers an available port instead of hardcoding 3001 (e.g., try 3001, then 3002, etc., or use port 0 and report back). - [ ] Server prints the actual bound port on startup so callers can discover it. - [ ] Frontend dev server proxy target is configurable (env var or auto-detected from server). - [ ] WebSocket client in the frontend reads the port dynamically rather than hardcoding it. -- [ ] Agent pool can target agents at different worktree server instances by URL. - [ ] A simple registry or file-based mechanism lets a supervisor discover which ports map to which worktrees. ## Out of Scope +- Agent orchestration across worktrees (separate story). - Service mesh or container orchestration. -- Multi-machine distributed agents (local only for now). +- Multi-machine distributed instances (local only for now). diff --git a/.story_kit/stories/upcoming/29_directory_based_workflow_coordination.md b/.story_kit/stories/upcoming/29_directory_based_workflow_coordination.md index 6addb04..4d2de87 100644 --- a/.story_kit/stories/upcoming/29_directory_based_workflow_coordination.md +++ b/.story_kit/stories/upcoming/29_directory_based_workflow_coordination.md @@ -1,3 +1,7 @@ +--- +name: Directory-Based Workflow Coordination and Locks +test_plan: pending +--- # Story 29: Directory-Based Workflow Coordination and Locks ## User Story @@ -12,4 +16,4 @@ As a user, I want directory-based story workflow coordination with lock tracking ## Out of Scope - Implementing the lock mechanism or agents in code. - Enforcing locks at runtime. -- Multi-agent orchestration beyond documenting the workflow. \ No newline at end of file +- Multi-agent orchestration beyond documenting the workflow. diff --git a/.story_kit/stories/upcoming/30_worktree_agent_orchestration.md b/.story_kit/stories/upcoming/30_worktree_agent_orchestration.md index 1c2e1bd..8213daa 100644 --- a/.story_kit/stories/upcoming/30_worktree_agent_orchestration.md +++ b/.story_kit/stories/upcoming/30_worktree_agent_orchestration.md @@ -1,3 +1,7 @@ +--- +name: Worktree-Based Agent Orchestration +test_plan: pending +--- # Story 30: Worktree-Based Agent Orchestration ## User Story diff --git a/.story_kit/stories/upcoming/33_worktree_diff_and_editor_integration.md b/.story_kit/stories/upcoming/33_worktree_diff_and_editor_integration.md index ffe3029..897f1dd 100644 --- a/.story_kit/stories/upcoming/33_worktree_diff_and_editor_integration.md +++ b/.story_kit/stories/upcoming/33_worktree_diff_and_editor_integration.md @@ -1,3 +1,7 @@ +--- +name: Worktree Diff Inspection and Editor Integration +test_plan: pending +--- # Story 33: Worktree Diff Inspection and Editor Integration ## User Story diff --git a/.story_kit/stories/upcoming/34_agent_configuration_and_roles.md b/.story_kit/stories/upcoming/34_agent_configuration_and_roles.md index 983cb3a..b827181 100644 --- a/.story_kit/stories/upcoming/34_agent_configuration_and_roles.md +++ b/.story_kit/stories/upcoming/34_agent_configuration_and_roles.md @@ -1,3 +1,7 @@ +--- +name: Per-Project Agent Configuration and Role Definitions +test_plan: pending +--- # Story 34: Per-Project Agent Configuration and Role Definitions ## User Story diff --git a/.story_kit/stories/upcoming/34_agent_security_and_sandboxing.md b/.story_kit/stories/upcoming/34_agent_security_and_sandboxing.md deleted file mode 100644 index bf9b51f..0000000 --- a/.story_kit/stories/upcoming/34_agent_security_and_sandboxing.md +++ /dev/null @@ -1,28 +0,0 @@ -# Story 34: Agent Security and Sandboxing - -## User Story -**As a** supervisor orchestrating multiple autonomous agents, -**I want to** constrain what each agent can access and do, -**So that** agents can't escape their worktree, damage shared state, or perform unintended actions. - -## Acceptance Criteria -- [ ] Agent creation accepts an `allowed_tools` list to restrict Claude Code tool access per agent. -- [ ] Agent creation accepts a `disallowed_tools` list as an alternative to allowlisting. -- [ ] Agents without Bash access can still perform useful coding work (Read, Edit, Write, Glob, Grep). -- [ ] Investigate replacing direct Bash/shell access with Rust-implemented tool proxies that enforce boundaries: - - Scoped `exec_shell` that only runs allowlisted commands (e.g., `cargo test`, `npm test`) within the agent's worktree. - - Scoped `read_file` / `write_file` that reject paths outside the agent's worktree root. - - Scoped `git` operations that only work within the agent's worktree. -- [ ] Evaluate `--max-turns` and `--max-budget-usd` as safety limits for runaway agents. -- [ ] Document the trust model: what the supervisor controls vs what agents can do autonomously. - -## Questions to Explore -- Can we use MCP (Model Context Protocol) to expose our Rust-implemented tools to Claude Code, replacing its built-in Bash/filesystem tools with scoped versions? -- What's the right granularity for shell allowlists — command-level (`cargo test`) or pattern-level (`cargo *`)? -- Should agents have read access outside their worktree (e.g., to reference shared specs) but write access only within it? -- Is OS-level sandboxing (Docker, macOS sandbox profiles) worth the complexity for a personal tool? - -## Out of Scope -- Multi-user authentication or authorization (single-user personal tool). -- Network-level isolation between agents. -- Encrypting agent communication channels (all local). diff --git a/.story_kit/stories/upcoming/35_agent_security_and_sandboxing.md b/.story_kit/stories/upcoming/35_agent_security_and_sandboxing.md index bf9b51f..8f46549 100644 --- a/.story_kit/stories/upcoming/35_agent_security_and_sandboxing.md +++ b/.story_kit/stories/upcoming/35_agent_security_and_sandboxing.md @@ -1,3 +1,7 @@ +--- +name: Agent Security and Sandboxing +test_plan: pending +--- # Story 34: Agent Security and Sandboxing ## User Story diff --git a/.story_kit/stories/upcoming/36_enforce_story_front_matter.md b/.story_kit/stories/upcoming/36_enforce_story_front_matter.md index 7f1ede9..080e4a8 100644 --- a/.story_kit/stories/upcoming/36_enforce_story_front_matter.md +++ b/.story_kit/stories/upcoming/36_enforce_story_front_matter.md @@ -1,3 +1,7 @@ +--- +name: Enforce Front Matter on All Story Files +test_plan: pending +--- # Story 36: Enforce Front Matter on All Story Files ## User Story @@ -8,3 +12,4 @@ As a user, I want the system to validate that every story file has valid front m - [ ] Existing story files are updated to include valid front matter. - [ ] The TODO panel displays the story name from front matter instead of falling back to the file stem. - [ ] A CLI or API command can check all stories for front matter compliance. +- [ ] A `POST /workflow/stories/create` endpoint creates a new story file with valid front matter (name, test_plan) and story scaffold, so agents never need to manually construct the file format. From e54209eb5ad005482baadbf26d43d73791eafcd0 Mon Sep 17 00:00:00 2001 From: Dave Date: Thu, 19 Feb 2026 17:14:33 +0000 Subject: [PATCH 2/2] Story 32: Multi-Instance Worktree Support Add configurable port via STORYKIT_PORT env var (default 3001). Server prints machine-readable STORYKIT_PORT= on startup and writes .story_kit_port file for discovery. Frontend proxy and WebSocket read VITE_STORYKIT_PORT env var instead of hardcoding port 3001. 7 new tests (4 backend, 3 frontend) all passing. Co-Authored-By: Claude Opus 4.6 --- .../32_multi_instance_worktree_support.md | 2 +- frontend/src/api/client.test.ts | 20 ++++- frontend/src/api/client.ts | 16 +++- frontend/vite.config.ts | 31 ++++---- server/src/main.rs | 76 +++++++++++++++++-- 5 files changed, 120 insertions(+), 25 deletions(-) rename .story_kit/stories/{current => archived}/32_multi_instance_worktree_support.md (98%) diff --git a/.story_kit/stories/current/32_multi_instance_worktree_support.md b/.story_kit/stories/archived/32_multi_instance_worktree_support.md similarity index 98% rename from .story_kit/stories/current/32_multi_instance_worktree_support.md rename to .story_kit/stories/archived/32_multi_instance_worktree_support.md index 1a92410..a758711 100644 --- a/.story_kit/stories/current/32_multi_instance_worktree_support.md +++ b/.story_kit/stories/archived/32_multi_instance_worktree_support.md @@ -1,6 +1,6 @@ --- name: "Multi-Instance Worktree Support" -test_plan: pending +test_plan: approved --- # Story 32: Multi-Instance Worktree Support diff --git a/frontend/src/api/client.test.ts b/frontend/src/api/client.test.ts index 81f933c..07caf6c 100644 --- a/frontend/src/api/client.test.ts +++ b/frontend/src/api/client.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { api } from "./client"; +import { api, resolveWsHost } from "./client"; const mockFetch = vi.fn(); @@ -135,4 +135,22 @@ describe("api client", () => { expect(result.exit_code).toBe(0); }); }); + + describe("resolveWsHost", () => { + it("uses env port in dev mode", () => { + expect(resolveWsHost(true, "4200", "example.com")).toBe("127.0.0.1:4200"); + }); + + it("defaults to 3001 in dev mode when no env port", () => { + expect(resolveWsHost(true, undefined, "example.com")).toBe( + "127.0.0.1:3001", + ); + }); + + it("uses location host in production", () => { + expect(resolveWsHost(false, "4200", "myapp.com:8080")).toBe( + "myapp.com:8080", + ); + }); + }); }); diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 2fa846c..14c6d45 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -57,6 +57,14 @@ export interface CommandOutput { const DEFAULT_API_BASE = "/api"; const DEFAULT_WS_PATH = "/ws"; +export function resolveWsHost( + isDev: boolean, + envPort: string | undefined, + locationHost: string, +): string { + return isDev ? `127.0.0.1:${envPort || "3001"}` : locationHost; +} + function buildApiUrl(path: string, baseUrl = DEFAULT_API_BASE): string { return `${baseUrl}${path}`; } @@ -225,9 +233,11 @@ export class ChatWebSocket { ChatWebSocket.refCount += 1; const protocol = window.location.protocol === "https:" ? "wss" : "ws"; - const wsHost = import.meta.env.DEV - ? "127.0.0.1:3001" - : window.location.host; + const wsHost = resolveWsHost( + import.meta.env.DEV, + import.meta.env.VITE_STORYKIT_PORT, + window.location.host, + ); const wsUrl = `${protocol}://${wsHost}${wsPath}`; if ( diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index c203979..106910b 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -2,19 +2,22 @@ import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; // https://vite.dev/config/ -export default defineConfig(() => ({ - plugins: [react()], - server: { - port: 5173, - proxy: { - "/api": { - target: "http://127.0.0.1:3001", - timeout: 120000, +export default defineConfig(() => { + const backendPort = process.env.VITE_STORYKIT_PORT || "3001"; + return { + plugins: [react()], + server: { + port: 5173, + proxy: { + "/api": { + target: `http://127.0.0.1:${backendPort}`, + timeout: 120000, + }, }, }, - }, - build: { - outDir: "dist", - emptyOutDir: true, - }, -})); + build: { + outDir: "dist", + emptyOutDir: true, + }, + }; +}); diff --git a/server/src/main.rs b/server/src/main.rs index 5dd32ce..5f0e182 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -14,9 +14,31 @@ use crate::store::JsonFileStore; use crate::workflow::WorkflowState; use poem::Server; use poem::listener::TcpListener; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::Arc; +const DEFAULT_PORT: u16 = 3001; + +fn parse_port(value: Option) -> u16 { + value + .and_then(|v| v.parse::().ok()) + .unwrap_or(DEFAULT_PORT) +} + +fn resolve_port() -> u16 { + parse_port(std::env::var("STORYKIT_PORT").ok()) +} + +fn write_port_file(dir: &Path, port: u16) -> Option { + let path = dir.join(".story_kit_port"); + std::fs::write(&path, port.to_string()).ok()?; + Some(path) +} + +fn remove_port_file(path: &Path) { + let _ = std::fs::remove_file(path); +} + #[tokio::main] async fn main() -> Result<(), std::io::Error> { let app_state = Arc::new(SessionState::default()); @@ -35,13 +57,55 @@ async fn main() -> Result<(), std::io::Error> { let app = build_routes(ctx); + let port = resolve_port(); + let addr = format!("127.0.0.1:{port}"); + println!( "\x1b[95;1m ____ _ _ ___ _ \n / ___|| |_ ___ _ __| | _|_ _| |_ \n \\___ \\| __/ _ \\| '__| |/ /| || __|\n ___) | || (_) | | | < | || |_ \n |____/ \\__\\___/|_| |_|\\_\\___|\\__|\n\x1b[0m" ); - println!("\x1b[96;1mFrontend:\x1b[0m \x1b[94mhttp://127.0.0.1:3001\x1b[0m"); - println!("\x1b[92;1mOpenAPI Docs:\x1b[0m \x1b[94mhttp://127.0.0.1:3001/docs\x1b[0m"); + println!("STORYKIT_PORT={port}"); + println!("\x1b[96;1mFrontend:\x1b[0m \x1b[94mhttp://{addr}\x1b[0m"); + println!("\x1b[92;1mOpenAPI Docs:\x1b[0m \x1b[94mhttp://{addr}/docs\x1b[0m"); - Server::new(TcpListener::bind("127.0.0.1:3001")) - .run(app) - .await + let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); + let port_file = write_port_file(&cwd, port); + + let result = Server::new(TcpListener::bind(&addr)).run(app).await; + + if let Some(ref path) = port_file { + remove_port_file(path); + } + + result +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_port_defaults_to_3001() { + assert_eq!(parse_port(None), 3001); + } + + #[test] + fn parse_port_reads_valid_value() { + assert_eq!(parse_port(Some("4200".to_string())), 4200); + } + + #[test] + fn parse_port_ignores_invalid_value() { + assert_eq!(parse_port(Some("not_a_number".to_string())), 3001); + } + + #[test] + fn write_and_remove_port_file() { + let tmp = tempfile::tempdir().unwrap(); + + let path = write_port_file(tmp.path(), 4567).expect("should write port file"); + assert_eq!(std::fs::read_to_string(&path).unwrap(), "4567"); + + remove_port_file(&path); + assert!(!path.exists()); + } }