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 100% rename from .story_kit/stories/current/32_multi_instance_worktree_support.md rename to .story_kit/stories/archived/32_multi_instance_worktree_support.md diff --git a/.story_kit/stories/current/30_worktree_agent_orchestration.md b/.story_kit/stories/current/30_worktree_agent_orchestration.md index 942839a..8213daa 100644 --- a/.story_kit/stories/current/30_worktree_agent_orchestration.md +++ b/.story_kit/stories/current/30_worktree_agent_orchestration.md @@ -1,8 +1,7 @@ --- name: Worktree-Based Agent Orchestration -test_plan: approved +test_plan: pending --- - # Story 30: Worktree-Based Agent Orchestration ## User Story diff --git a/.story_kit/stories/current/36_enforce_story_front_matter.md b/.story_kit/stories/current/36_enforce_story_front_matter.md index 0ad9f51..080e4a8 100644 --- a/.story_kit/stories/current/36_enforce_story_front_matter.md +++ b/.story_kit/stories/current/36_enforce_story_front_matter.md @@ -2,7 +2,6 @@ name: Enforce Front Matter on All Story Files test_plan: pending --- - # Story 36: Enforce Front Matter on All Story Files ## User Story @@ -13,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. 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 7180b15..4d2de87 100644 --- a/.story_kit/stories/upcoming/29_directory_based_workflow_coordination.md +++ b/.story_kit/stories/upcoming/29_directory_based_workflow_coordination.md @@ -2,7 +2,6 @@ name: Directory-Based Workflow Coordination and Locks test_plan: pending --- - # Story 29: Directory-Based Workflow Coordination and Locks ## User Story @@ -17,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/33_worktree_diff_and_editor_integration.md b/.story_kit/stories/upcoming/33_worktree_diff_and_editor_integration.md index 1347f42..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 @@ -2,7 +2,6 @@ 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 5ca9e35..b827181 100644 --- a/.story_kit/stories/upcoming/34_agent_configuration_and_roles.md +++ b/.story_kit/stories/upcoming/34_agent_configuration_and_roles.md @@ -2,7 +2,6 @@ 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/35_agent_security_and_sandboxing.md b/.story_kit/stories/upcoming/35_agent_security_and_sandboxing.md index 990a5e3..8f46549 100644 --- a/.story_kit/stories/upcoming/35_agent_security_and_sandboxing.md +++ b/.story_kit/stories/upcoming/35_agent_security_and_sandboxing.md @@ -2,8 +2,7 @@ name: Agent Security and Sandboxing test_plan: pending --- - -# Story 35: Agent Security and Sandboxing +# Story 34: Agent Security and Sandboxing ## User Story **As a** supervisor orchestrating multiple autonomous agents, 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()); + } }