diff --git a/docs/plans/2026-02-27-project-local-workspace-resolution-design.md b/docs/plans/2026-02-27-project-local-workspace-resolution-design.md deleted file mode 100644 index 39d4c4a..0000000 --- a/docs/plans/2026-02-27-project-local-workspace-resolution-design.md +++ /dev/null @@ -1,88 +0,0 @@ -# Project-Local Workspace Resolution - -**Date:** 2026-02-27 -**Status:** Approved - -## Problem - -`arc init` writes `.arc.json` to the project root containing a server-generated `workspace_id`. This ID is specific to the machine's `~/.arc/data.db`. When the same repo is used on multiple machines (e.g., macbook + linux desktop), `.arc.json` points to a workspace that doesn't exist on the other machine's database, breaking all arc commands. - -## Solution - -Move workspace binding out of the repo and into a per-machine project directory under `~/.arc/projects/`. Each machine maintains its own mapping from project path to workspace ID. No shared state between machines. - -## Design - -### Project Directory Layout - -``` -~/.arc/ -├── data.db # SQLite database (unchanged) -├── server.pid -├── server.log -├── cli-config.json # CLI config (server URL) -└── projects/ - ├── -home-bfirestone-devspace-personal-sentiolabs-arc/ - │ └── config.json - ├── -home-bfirestone-devspace-bactrack-bacstack/ - │ └── config.json - └── ... -``` - -Directory names use the full absolute path with `/` replaced by `-` (following the Claude Code `~/.claude/projects/` convention). - -**`config.json` contents:** -```json -{ - "workspace_id": "ws-05cgmu", - "workspace_name": "arc", - "project_root": "/home/bfirestone/devspace/personal/sentiolabs/arc" -} -``` - -`project_root` is stored for readability and validation. - -### Workspace Resolution - -When any `arc` command runs (e.g., `arc list` from a nested subdirectory): - -**1. Git walk (primary):** Walk up from cwd looking for `.git/`. Use that directory as the project root. Convert to project dir name and look up `~/.arc/projects//config.json`. - -**2. Prefix walk (secondary):** If no `.git/` found, convert cwd to the project dir format (`/` → `-`). Strip trailing segments longest-to-shortest, checking for a matching directory under `~/.arc/projects/` at each step. - -Example for cwd `/home/user/project/foo/bar/baz`: -1. Check `-home-user-project-foo-bar-baz` — no match -2. Check `-home-user-project-foo-bar` — no match -3. Check `-home-user-project-foo` — no match -4. Check `-home-user-project` — match, use this - -Longest match wins (most specific project). - -**3. Legacy fallback:** Check for `.arc.json` in cwd and parent dirs. If found, migrate it (see Migration section). - -### `arc init` Changes - -- **Old:** Creates `.arc.json` in the project root -- **New:** Creates `~/.arc/projects//config.json` -- No longer writes any file to the repo directory (except AGENTS.md if opted in) -- Everything else (workspace creation on server, prefix generation) stays the same - -### Migration Path - -When the legacy fallback finds a `.arc.json`: -1. Read `workspace_id` and `workspace_name` -2. Create `~/.arc/projects//config.json` with those values -3. Print: `Migrated .arc.json → ~/.arc/projects//config.json` -4. Do **not** auto-delete `.arc.json` (may be committed to git — let the user clean it up) - -## What This Does NOT Change - -- Server architecture (still centralized SQLite) -- Database location (`~/.arc/data.db` by default) -- API endpoints or data model -- Workspace creation or management on the server side - -## Future Work (Separate Features) - -- **Configurable DB path:** Add `~/.config/arc/server.json` with a `db_path` field so users can point the server at a Syncthing-shared directory. Orthogonal to this change. -- **Sync safety guardrails:** WAL checkpoint on shutdown, integrity check on startup, Syncthing conflict file detection. Relevant when DB path is configurable. diff --git a/docs/plans/2026-02-27-project-local-workspace-resolution.md b/docs/plans/2026-02-27-project-local-workspace-resolution.md deleted file mode 100644 index 3e4830b..0000000 --- a/docs/plans/2026-02-27-project-local-workspace-resolution.md +++ /dev/null @@ -1,833 +0,0 @@ -# Project-Local Workspace Resolution Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Move workspace binding from in-repo `.arc.json` to per-machine `~/.arc/projects//config.json`, enabling multi-machine workflows without conflicts. - -**Architecture:** Add a `project` package under `internal/` that handles path-to-directory-name conversion, project config read/write, and the three-tier resolution (git walk → prefix walk → legacy fallback). Update `arc init` to write to `~/.arc/projects/` instead of `.arc.json`. Update workspace resolution in `cmd/arc/main.go` to use the new project package. - -**Tech Stack:** Go stdlib (`os`, `path/filepath`, `os/exec`, `encoding/json`) - ---- - -### Task 1: Create the `internal/project` package — path conversion - -**Files:** -- Create: `internal/project/project.go` -- Test: `internal/project/project_test.go` - -This package handles converting absolute filesystem paths to project directory names (the `-home-user-repo` format) and back. - -**Step 1: Write the failing tests** - -```go -// internal/project/project_test.go -package project - -import ( - "testing" -) - -func TestPathToProjectDir(t *testing.T) { - tests := []struct { - name string - path string - expected string - }{ - {"simple path", "/home/user/project", "-home-user-project"}, - {"deep path", "/home/user/dev/org/repo", "-home-user-dev-org-repo"}, - {"root", "/", "-"}, - {"trailing slash stripped", "/home/user/project/", "-home-user-project"}, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - result := PathToProjectDir(tc.path) - if result != tc.expected { - t.Errorf("PathToProjectDir(%q) = %q, want %q", tc.path, result, tc.expected) - } - }) - } -} -``` - -**Step 2: Run test to verify it fails** - -Run: `go test ./internal/project/ -run TestPathToProjectDir -v` -Expected: FAIL — package doesn't exist yet - -**Step 3: Write minimal implementation** - -```go -// internal/project/project.go -package project - -import ( - "path/filepath" - "strings" -) - -// PathToProjectDir converts an absolute filesystem path to a project directory name. -// Replaces "/" with "-", matching the Claude Code ~/.claude/projects/ convention. -// Example: "/home/user/my-repo" → "-home-user-my-repo" -func PathToProjectDir(absPath string) string { - cleaned := filepath.Clean(absPath) - return strings.ReplaceAll(cleaned, string(filepath.Separator), "-") -} -``` - -**Step 4: Run test to verify it passes** - -Run: `go test ./internal/project/ -run TestPathToProjectDir -v` -Expected: PASS - -**Step 5: Commit** - -```bash -git add internal/project/project.go internal/project/project_test.go -git commit -m "feat(project): add path-to-project-dir conversion" -``` - ---- - -### Task 2: Add project config read/write - -**Files:** -- Modify: `internal/project/project.go` -- Test: `internal/project/project_test.go` - -Add functions to read/write the per-project `config.json` under `~/.arc/projects//`. - -**Step 1: Write the failing tests** - -Add to `internal/project/project_test.go`: - -```go -func TestWriteAndLoadConfig(t *testing.T) { - // Use a temp dir as the arc home - tmpDir := t.TempDir() - - cfg := &Config{ - WorkspaceID: "ws-abc123", - WorkspaceName: "my-project", - ProjectRoot: "/home/user/my-project", - } - - err := WriteConfig(tmpDir, "/home/user/my-project", cfg) - if err != nil { - t.Fatalf("WriteConfig failed: %v", err) - } - - loaded, err := LoadConfig(tmpDir, "/home/user/my-project") - if err != nil { - t.Fatalf("LoadConfig failed: %v", err) - } - - if loaded.WorkspaceID != cfg.WorkspaceID { - t.Errorf("WorkspaceID = %q, want %q", loaded.WorkspaceID, cfg.WorkspaceID) - } - if loaded.WorkspaceName != cfg.WorkspaceName { - t.Errorf("WorkspaceName = %q, want %q", loaded.WorkspaceName, cfg.WorkspaceName) - } - if loaded.ProjectRoot != cfg.ProjectRoot { - t.Errorf("ProjectRoot = %q, want %q", loaded.ProjectRoot, cfg.ProjectRoot) - } -} - -func TestLoadConfigNotFound(t *testing.T) { - tmpDir := t.TempDir() - - _, err := LoadConfig(tmpDir, "/nonexistent/path") - if err == nil { - t.Fatal("LoadConfig should fail for nonexistent project") - } -} -``` - -**Step 2: Run test to verify it fails** - -Run: `go test ./internal/project/ -run TestWriteAndLoadConfig -v` -Expected: FAIL — `Config`, `WriteConfig`, `LoadConfig` not defined - -**Step 3: Write minimal implementation** - -Add to `internal/project/project.go`: - -```go -import ( - "encoding/json" - "fmt" - "os" - "path/filepath" - "strings" -) - -// Config holds the per-project workspace binding. -type Config struct { - WorkspaceID string `json:"workspace_id"` - WorkspaceName string `json:"workspace_name"` - ProjectRoot string `json:"project_root"` -} - -// projectsDir returns the path to the projects directory within arcHome. -func projectsDir(arcHome string) string { - return filepath.Join(arcHome, "projects") -} - -// configPath returns the full path to a project's config.json. -func configPath(arcHome, absProjectPath string) string { - dirName := PathToProjectDir(absProjectPath) - return filepath.Join(projectsDir(arcHome), dirName, "config.json") -} - -// WriteConfig writes a project config to ~/.arc/projects//config.json. -func WriteConfig(arcHome, absProjectPath string, cfg *Config) error { - path := configPath(arcHome, absProjectPath) - - if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { - return fmt.Errorf("create project dir: %w", err) - } - - data, err := json.MarshalIndent(cfg, "", " ") - if err != nil { - return fmt.Errorf("marshal config: %w", err) - } - - return os.WriteFile(path, append(data, '\n'), 0o644) -} - -// LoadConfig reads a project config from ~/.arc/projects//config.json. -func LoadConfig(arcHome, absProjectPath string) (*Config, error) { - path := configPath(arcHome, absProjectPath) - - data, err := os.ReadFile(path) - if err != nil { - return nil, err - } - - var cfg Config - if err := json.Unmarshal(data, &cfg); err != nil { - return nil, fmt.Errorf("parse project config: %w", err) - } - - return &cfg, nil -} -``` - -**Step 4: Run tests to verify they pass** - -Run: `go test ./internal/project/ -v` -Expected: PASS - -**Step 5: Commit** - -```bash -git add internal/project/project.go internal/project/project_test.go -git commit -m "feat(project): add config read/write for ~/.arc/projects/" -``` - ---- - -### Task 3: Add project root resolution (git walk + prefix walk) - -**Files:** -- Modify: `internal/project/project.go` -- Test: `internal/project/project_test.go` - -Add the three-tier resolution: find project root via git, then prefix walk, then legacy `.arc.json` fallback. - -**Step 1: Write the failing tests** - -Add to `internal/project/project_test.go`: - -```go -import ( - "os" - "os/exec" - "path/filepath" - "testing" -) - -func TestFindProjectRootViaGit(t *testing.T) { - // Create a temp dir with a .git directory - tmpDir := t.TempDir() - gitDir := filepath.Join(tmpDir, ".git") - if err := os.Mkdir(gitDir, 0o755); err != nil { - t.Fatal(err) - } - - // Create a nested subdirectory - nested := filepath.Join(tmpDir, "a", "b", "c") - if err := os.MkdirAll(nested, 0o755); err != nil { - t.Fatal(err) - } - - root, err := FindProjectRoot(nested) - if err != nil { - t.Fatalf("FindProjectRoot failed: %v", err) - } - - if root != tmpDir { - t.Errorf("FindProjectRoot = %q, want %q", root, tmpDir) - } -} - -func TestFindProjectRootViaPrefixWalk(t *testing.T) { - // No .git dir — should fall back to prefix walk - tmpDir := t.TempDir() - arcHome := t.TempDir() - - // Register a project at tmpDir - cfg := &Config{ - WorkspaceID: "ws-test", - WorkspaceName: "test", - ProjectRoot: tmpDir, - } - if err := WriteConfig(arcHome, tmpDir, cfg); err != nil { - t.Fatal(err) - } - - // Create nested dir - nested := filepath.Join(tmpDir, "sub", "deep") - if err := os.MkdirAll(nested, 0o755); err != nil { - t.Fatal(err) - } - - root, err := FindProjectRootWithArcHome(nested, arcHome) - if err != nil { - t.Fatalf("FindProjectRootWithArcHome failed: %v", err) - } - - if root != tmpDir { - t.Errorf("FindProjectRootWithArcHome = %q, want %q", root, tmpDir) - } -} - -func TestFindProjectRootNoMatch(t *testing.T) { - tmpDir := t.TempDir() - arcHome := t.TempDir() - - _, err := FindProjectRootWithArcHome(tmpDir, arcHome) - if err == nil { - t.Fatal("FindProjectRootWithArcHome should fail when no project found") - } -} -``` - -**Step 2: Run test to verify it fails** - -Run: `go test ./internal/project/ -run TestFindProjectRoot -v` -Expected: FAIL — `FindProjectRoot`, `FindProjectRootWithArcHome` not defined - -**Step 3: Write minimal implementation** - -Add to `internal/project/project.go`: - -```go -// DefaultArcHome returns the default arc home directory (~/.arc). -func DefaultArcHome() string { - home, _ := os.UserHomeDir() - return filepath.Join(home, ".arc") -} - -// FindProjectRoot resolves the project root for the given directory -// using the default arc home (~/.arc). -// Resolution order: -// 1. Git walk — walk up looking for .git/ -// 2. Prefix walk — longest-to-shortest match in ~/.arc/projects/ -// 3. Returns error if nothing found -func FindProjectRoot(dir string) (string, error) { - return FindProjectRootWithArcHome(dir, DefaultArcHome()) -} - -// FindProjectRootWithArcHome resolves the project root using a custom arc home. -// This is the testable version of FindProjectRoot. -func FindProjectRootWithArcHome(dir string, arcHome string) (string, error) { - absDir, err := filepath.Abs(dir) - if err != nil { - return "", fmt.Errorf("resolve absolute path: %w", err) - } - - // Strategy 1: Walk up looking for .git/ - if root, err := findGitRoot(absDir); err == nil { - return root, nil - } - - // Strategy 2: Prefix walk (longest to shortest) - if root, err := findByPrefixWalk(absDir, arcHome); err == nil { - return root, nil - } - - return "", fmt.Errorf("no project found for %s\n Run 'arc init' to set up a workspace", absDir) -} - -// findGitRoot walks up from dir looking for a .git directory. -func findGitRoot(dir string) (string, error) { - current := dir - for { - gitPath := filepath.Join(current, ".git") - if info, err := os.Stat(gitPath); err == nil && info.IsDir() { - return current, nil - } - - parent := filepath.Dir(current) - if parent == current { - return "", fmt.Errorf("no .git found") - } - current = parent - } -} - -// findByPrefixWalk converts dir to the project dir format and strips trailing -// segments (longest to shortest) looking for a match in ~/.arc/projects/. -func findByPrefixWalk(dir string, arcHome string) (string, error) { - projDir := projectsDir(arcHome) - current := dir - - for { - dirName := PathToProjectDir(current) - candidate := filepath.Join(projDir, dirName, "config.json") - if _, err := os.Stat(candidate); err == nil { - return current, nil - } - - parent := filepath.Dir(current) - if parent == current { - return "", fmt.Errorf("no registered project found") - } - current = parent - } -} -``` - -**Step 4: Run tests to verify they pass** - -Run: `go test ./internal/project/ -v` -Expected: PASS - -**Step 5: Commit** - -```bash -git add internal/project/project.go internal/project/project_test.go -git commit -m "feat(project): add project root resolution (git walk + prefix walk)" -``` - ---- - -### Task 4: Add legacy `.arc.json` migration - -**Files:** -- Modify: `internal/project/project.go` -- Test: `internal/project/project_test.go` - -Add a function that finds a legacy `.arc.json`, reads it, migrates to the new format, and returns the config. - -**Step 1: Write the failing test** - -Add to `internal/project/project_test.go`: - -```go -func TestMigrateLegacyConfig(t *testing.T) { - tmpDir := t.TempDir() - arcHome := t.TempDir() - - // Create a legacy .arc.json - legacyContent := `{"workspace_id": "ws-old123", "workspace_name": "legacy-project"}` - legacyPath := filepath.Join(tmpDir, ".arc.json") - if err := os.WriteFile(legacyPath, []byte(legacyContent), 0o644); err != nil { - t.Fatal(err) - } - - cfg, err := MigrateLegacyConfig(tmpDir, arcHome) - if err != nil { - t.Fatalf("MigrateLegacyConfig failed: %v", err) - } - - if cfg.WorkspaceID != "ws-old123" { - t.Errorf("WorkspaceID = %q, want %q", cfg.WorkspaceID, "ws-old123") - } - if cfg.WorkspaceName != "legacy-project" { - t.Errorf("WorkspaceName = %q, want %q", cfg.WorkspaceName, "legacy-project") - } - - // Verify the new config was written - loaded, err := LoadConfig(arcHome, tmpDir) - if err != nil { - t.Fatalf("LoadConfig after migration failed: %v", err) - } - if loaded.WorkspaceID != "ws-old123" { - t.Errorf("Migrated config WorkspaceID = %q, want %q", loaded.WorkspaceID, "ws-old123") - } -} - -func TestFindLegacyConfigWalksUp(t *testing.T) { - tmpDir := t.TempDir() - - // Create .arc.json in the root - legacyContent := `{"workspace_id": "ws-walk", "workspace_name": "walk-test"}` - if err := os.WriteFile(filepath.Join(tmpDir, ".arc.json"), []byte(legacyContent), 0o644); err != nil { - t.Fatal(err) - } - - // Search from nested dir - nested := filepath.Join(tmpDir, "a", "b") - if err := os.MkdirAll(nested, 0o755); err != nil { - t.Fatal(err) - } - - path, err := FindLegacyConfig(nested) - if err != nil { - t.Fatalf("FindLegacyConfig failed: %v", err) - } - - expected := filepath.Join(tmpDir, ".arc.json") - if path != expected { - t.Errorf("FindLegacyConfig = %q, want %q", path, expected) - } -} -``` - -**Step 2: Run test to verify it fails** - -Run: `go test ./internal/project/ -run TestMigrate -v` -Expected: FAIL — `MigrateLegacyConfig`, `FindLegacyConfig` not defined - -**Step 3: Write minimal implementation** - -Add to `internal/project/project.go`: - -```go -// legacyConfig matches the old .arc.json structure. -type legacyConfig struct { - WorkspaceID string `json:"workspace_id"` - WorkspaceName string `json:"workspace_name"` -} - -// FindLegacyConfig searches for .arc.json starting from dir and walking up. -func FindLegacyConfig(dir string) (string, error) { - current, err := filepath.Abs(dir) - if err != nil { - return "", err - } - - for { - candidate := filepath.Join(current, ".arc.json") - if _, err := os.Stat(candidate); err == nil { - return candidate, nil - } - - parent := filepath.Dir(current) - if parent == current { - return "", os.ErrNotExist - } - current = parent - } -} - -// MigrateLegacyConfig reads a .arc.json from projectRoot, creates the new -// config under arcHome, and returns the migrated config. -// Does NOT delete the original .arc.json. -func MigrateLegacyConfig(projectRoot, arcHome string) (*Config, error) { - legacyPath := filepath.Join(projectRoot, ".arc.json") - - data, err := os.ReadFile(legacyPath) - if err != nil { - return nil, fmt.Errorf("read legacy config: %w", err) - } - - var legacy legacyConfig - if err := json.Unmarshal(data, &legacy); err != nil { - return nil, fmt.Errorf("parse legacy config: %w", err) - } - - cfg := &Config{ - WorkspaceID: legacy.WorkspaceID, - WorkspaceName: legacy.WorkspaceName, - ProjectRoot: projectRoot, - } - - if err := WriteConfig(arcHome, projectRoot, cfg); err != nil { - return nil, fmt.Errorf("write migrated config: %w", err) - } - - return cfg, nil -} -``` - -**Step 4: Run tests to verify they pass** - -Run: `go test ./internal/project/ -v` -Expected: PASS - -**Step 5: Commit** - -```bash -git add internal/project/project.go internal/project/project_test.go -git commit -m "feat(project): add legacy .arc.json migration" -``` - ---- - -### Task 5: Update `resolveWorkspace` in `cmd/arc/main.go` - -**Files:** -- Modify: `cmd/arc/main.go:114-229` (the `localConfig`, `loadLocalConfig`, `findProjectConfig`, `resolveWorkspace` functions and `WorkspaceSource` type) - -Replace the `.arc.json`-based resolution with the new project package. Keep the `-w` flag as highest priority. - -**Step 1: Update the code** - -Update `WorkspaceSource` constants and `resolveWorkspace()`: - -```go -// WorkspaceSource indicates how the workspace was resolved -type WorkspaceSource int - -const ( - WorkspaceSourceFlag WorkspaceSource = iota - WorkspaceSourceProject // ~/.arc/projects//config.json - WorkspaceSourceLegacy // .arc.json (migrated) -) - -func (s WorkspaceSource) String() string { - switch s { - case WorkspaceSourceFlag: - return "command line flag (-w)" - case WorkspaceSourceProject: - return "~/.arc/projects/ (local)" - case WorkspaceSourceLegacy: - return ".arc.json (legacy, migrated)" - default: - return "unknown" - } -} -``` - -Replace `resolveWorkspace` body to use the new project package: - -```go -func resolveWorkspace() (wsID string, source WorkspaceSource, warning string, err error) { - // Priority 1: CLI flag (explicit override) - if workspaceID != "" { - return workspaceID, WorkspaceSourceFlag, "", nil - } - - cwd, err := os.Getwd() - if err != nil { - return "", 0, "", fmt.Errorf("get current directory: %w", err) - } - - arcHome := project.DefaultArcHome() - - // Priority 2: Find project root and load config from ~/.arc/projects/ - projectRoot, rootErr := project.FindProjectRootWithArcHome(cwd, arcHome) - if rootErr == nil { - cfg, cfgErr := project.LoadConfig(arcHome, projectRoot) - if cfgErr == nil && cfg.WorkspaceID != "" { - // Validate workspace exists on server - c, clientErr := getClient() - if clientErr == nil { - _, wsErr := c.GetWorkspace(cfg.WorkspaceID) - if wsErr != nil { - return "", 0, "", fmt.Errorf("workspace '%s' (%s) not found on server\n Run 'arc init' to reconfigure this directory", - cfg.WorkspaceName, cfg.WorkspaceID) - } - } - return cfg.WorkspaceID, WorkspaceSourceProject, "", nil - } - } - - // Priority 3: Legacy .arc.json fallback with migration - legacyPath, legacyErr := project.FindLegacyConfig(cwd) - if legacyErr == nil { - legacyRoot := filepath.Dir(legacyPath) - cfg, migrateErr := project.MigrateLegacyConfig(legacyRoot, arcHome) - if migrateErr == nil && cfg.WorkspaceID != "" { - fmt.Fprintf(os.Stderr, "Migrated .arc.json → ~/.arc/projects/%s/config.json\n", - project.PathToProjectDir(legacyRoot)) - - // Validate workspace exists on server - c, clientErr := getClient() - if clientErr == nil { - _, wsErr := c.GetWorkspace(cfg.WorkspaceID) - if wsErr != nil { - return "", 0, "", fmt.Errorf("workspace '%s' (%s) not found on server\n Run 'arc init' to reconfigure this directory", - cfg.WorkspaceName, cfg.WorkspaceID) - } - } - return cfg.WorkspaceID, WorkspaceSourceLegacy, "", nil - } - } - - return "", 0, "", fmt.Errorf("no workspace configured for this directory\n Run 'arc init' to set up a workspace, or use '-w ' to specify one") -} -``` - -Remove the now-unused `loadLocalConfig` and `findProjectConfig` functions (lines 120-164 in current `main.go`). The `localConfig` struct is also no longer needed. - -Add import: `"github.com/sentiolabs/arc/internal/project"` - -**Step 2: Run tests** - -Run: `make test` -Expected: PASS — existing tests should still work (none directly test `resolveWorkspace` in isolation) - -**Step 3: Manual verification** - -The CLI should still work: -- `arc list` from a dir with `.arc.json` → migrates and works -- `arc list -w ` → flag override still works -- `arc which` → shows source correctly - -**Step 4: Commit** - -```bash -git add cmd/arc/main.go -git commit -m "feat: update workspace resolution to use ~/.arc/projects/" -``` - ---- - -### Task 6: Update `arc init` to write to `~/.arc/projects/` - -**Files:** -- Modify: `cmd/arc/init.go:144-148,176-196` (the `createProjectConfig` call and function) - -**Step 1: Update `runInit` to use project package** - -Replace the `createProjectConfig` call (line 144) with: - -```go - // Create project config in ~/.arc/projects/ - arcHome := project.DefaultArcHome() - projectCfg := &project.Config{ - WorkspaceID: ws.ID, - WorkspaceName: ws.Name, - ProjectRoot: cwd, - } - if err := project.WriteConfig(arcHome, cwd, projectCfg); err != nil { - if !quiet { - fmt.Fprintf(os.Stderr, "Warning: failed to create project config: %v\n", err) - } - } -``` - -Update the warning message at line 146 from `.arc.json` to `project config`. - -Remove the old `createProjectConfig` function (lines 176-196) — it's no longer needed. - -Add import: `"github.com/sentiolabs/arc/internal/project"` - -**Step 2: Run tests** - -Run: `make test` -Expected: PASS - -**Step 3: Build and manual test** - -Run: `make build-quick` - -Test in a temp directory: -```bash -mkdir /tmp/test-arc-init && cd /tmp/test-arc-init -git init -arc init -# Should NOT create .arc.json -# Should create ~/.arc/projects/-tmp-test-arc-init/config.json -cat ~/.arc/projects/-tmp-test-arc-init/config.json -``` - -**Step 4: Commit** - -```bash -git add cmd/arc/init.go -git commit -m "feat: arc init writes to ~/.arc/projects/ instead of .arc.json" -``` - ---- - -### Task 7: Update `arc which` to show new source info - -**Files:** -- Modify: `cmd/arc/main.go:298-356` (the `whichCmd`) - -**Step 1: Update the which command** - -The `WorkspaceSource.String()` method was already updated in Task 5. The `whichCmd` uses `source.String()` so it should already show the correct source. Verify this works correctly and add the project config path to the output: - -```go -// In the human-readable output section of whichCmd: -if source == WorkspaceSourceProject || source == WorkspaceSourceLegacy { - cwd, _ := os.Getwd() - arcHome := project.DefaultArcHome() - if root, err := project.FindProjectRootWithArcHome(cwd, arcHome); err == nil { - fmt.Printf("Config: ~/.arc/projects/%s/config.json\n", project.PathToProjectDir(root)) - } -} -``` - -**Step 2: Build and test** - -Run: `make build-quick` -Run: `./bin/arc which` from a project directory -Expected: Shows workspace, source as `~/.arc/projects/ (local)`, and config path - -**Step 3: Commit** - -```bash -git add cmd/arc/main.go -git commit -m "feat: arc which shows project config path" -``` - ---- - -### Task 8: Final integration testing and cleanup - -**Files:** -- No new files — verification only - -**Step 1: Run full test suite** - -Run: `make test` -Expected: All tests pass - -**Step 2: Build and verify end-to-end** - -```bash -make build-quick - -# Test 1: Fresh init in a new dir -mkdir /tmp/test-e2e && cd /tmp/test-e2e && git init -arc init -ls -la .arc.json # Should NOT exist -cat ~/.arc/projects/-tmp-test-e2e/config.json # Should exist -arc list # Should work -arc which # Should show source as ~/.arc/projects/ - -# Test 2: Legacy migration -cd /some/dir/with/existing/.arc.json -arc list # Should migrate and work, print migration message - -# Test 3: Nested directory resolution -cd /tmp/test-e2e/some/deep/subdir -mkdir -p /tmp/test-e2e/some/deep/subdir -arc list # Should resolve to the parent project -``` - -**Step 3: Clean up temp test directories** - -```bash -rm -rf /tmp/test-e2e /tmp/test-arc-init -``` - -**Step 4: Final commit (if any fixups needed)** - -```bash -git add -A && git commit -m "fix: address integration test findings" -``` - -**Step 5: Push** - -```bash -git push -``` diff --git a/docs/plans/2026-02-28-agentic-teams-design.md b/docs/plans/2026-02-28-agentic-teams-design.md deleted file mode 100644 index 0076e46..0000000 --- a/docs/plans/2026-02-28-agentic-teams-design.md +++ /dev/null @@ -1,364 +0,0 @@ -# Agentic Teams Integration Design - -**Date:** 2026-02-28 -**Status:** Approved - -## Problem - -Arc has labels (global, with colors), plans (inline + shared), assignees, and a dependency graph. Claude Code has experimental agent teams (TeamCreate, TaskList, SendMessage). These systems don't talk to each other. The result: agent teams reinvent work decomposition every session, arc issues go stale during team work, and there's no visibility into team activity from the web UI. - -## Design Direction - -**Arc as strategic context, Claude Code TaskList as tactical execution.** - -Arc provides the persistent layer — labeled issues, plans, dependency graphs, audit trails. Claude Code's TaskList handles real-time coordination — claim, block, complete. The team lead bridges the two: reading arc to decompose work, and syncing results back after verification. - -### Principles - -- Don't replace Anthropic's TaskList — complement it -- Team lead owns arc sync (no teammate-driven churn) -- Labels drive teammate routing via `teammate:*` convention -- Plans serve dual purpose: team brief + acceptance criteria - -## Data Model - -### `teammate:*` Label Convention - -No schema changes. Labels with the `teammate:` prefix indicate teammate routing: - -| Label | Meaning | -|---|---| -| `teammate:frontend` | Frontend — UI components, styling, client logic | -| `teammate:backend` | Backend — API, storage, business logic | -| `teammate:architect` | Architecture — design decisions, cross-cutting | -| `teammate:tests` | Tests — coverage, verification | -| `teammate:devops` | DevOps — CI/CD, infrastructure, deployment | - -Users create these via the existing `/labels` page or future `arc label` CLI commands. Colors flow through to the web Team View. - -Rules: -- An issue can have multiple `teammate:*` labels (the orchestration skill assigns to the primary or creates subtasks) -- Regular labels (`bug`, `feature`, `P1`) coexist as metadata, not routing - -### Plan Integration - -- **Shared plans** linked to an epic = team brief (read at decomposition time) -- **Inline plans** on individual issues = acceptance criteria (read at verification time) -- No changes to plan data model - -## Component 1: Enhanced `arc prime` - -### Teammate Detection - -When the orchestration skill spawns a teammate, it sets: -``` -ARC_TEAMMATE_ROLE=frontend -``` - -`arc prime` checks this env var (or a `--role` flag) to determine output mode. - -### Output Modes - -**Default mode** (no role set) — unchanged. CLI reference + session close protocol. - -**Team lead mode** (`ARC_TEAMMATE_ROLE=lead`): -- Full CLI reference -- Team orchestration guidance: task list usage, arc sync protocol -- Verification checklist: review teammate work before closing arc issues -- Session close protocol with team cleanup steps - -**Teammate mode** (`ARC_TEAMMATE_ROLE=frontend`, etc.): -- Filtered issue list: only issues with matching `teammate:*` label -- Each issue: title, status, priority, inline plan, linked shared plan summary -- Relevant dependencies -- Session close protocol (git commit/push) - -### Implementation - -- Modify `cmd/arc/prime.go`: add role detection (env var + `--role` flag) -- Add teammate-specific output template -- No new storage methods - -## Component 2: `arc team context` CLI Command - -Outputs structured data for the orchestration skill. Separates "gathering arc data" from "creating a Claude Code team." - -### Signature - -```bash -arc team context [epic-id] [flags] -``` - -**With `epic-id`:** Read epic's children, group by `teammate:*` labels, include shared plan. -**Without `epic-id`:** Read all ready issues with `teammate:*` labels in active project. - -### Flags - -| Flag | Description | -|---|---| -| `--json` | Machine-readable JSON (default for skill consumption) | -| `--format=human` | Human-readable summary (default for interactive use) | - -### JSON Output Structure - -```json -{ - "project": "my-project", - "epic": { "id": "ISSUE-5", "title": "Implement auth system", "plan": "..." }, - "roles": { - "frontend": { - "issues": [ - { "id": "ISSUE-12", "title": "Login form", "priority": 2, "plan": "...", "deps": ["ISSUE-8"] } - ] - }, - "backend": { - "issues": [ - { "id": "ISSUE-8", "title": "Auth API endpoints", "priority": 1, "plan": "..." } - ] - } - }, - "unassigned": [ - { "id": "ISSUE-20", "title": "Write tests", "priority": 3 } - ] -} -``` - -### Implementation - -- New file: `cmd/arc/team.go` -- Calls existing storage methods: `ListIssues`, `GetIssueLabels`, `GetDependencies`, `GetPlan` -- No new storage methods — read-only aggregation - -## Component 3: Team Orchestration Skill - -A Claude Code skill the team lead invokes to decompose arc issues into a team. - -### Location - -`claude-plugin/skills/arc-team-deploy/SKILL.md` - -### Workflow - -**Step 1 — Discover work:** -```bash -arc team context --json -``` - -**Step 2 — Propose team composition:** -Present grouped issues to lead/user for approval: -``` -Proposed team: - - frontend (3 issues): ISSUE-12, ISSUE-14, ISSUE-16 - - backend (2 issues): ISSUE-8, ISSUE-11 - - architect (1 issue): ISSUE-7 - -Plan brief: [summary of shared plan] -Unassigned: ISSUE-20 (no teammate:* label) -``` - -**Step 3 — Create team + tasks:** -After approval: -1. `TeamCreate` with team name -2. `TaskCreate` per issue (subject = issue title, description = issue details + plan) -3. Task dependencies mirroring arc's dependency graph -4. Spawn teammates via `Agent` tool with `ARC_TEAMMATE_ROLE=` env var -5. `TaskUpdate` to assign tasks to appropriate teammate - -**Step 4 — Team lead sync protocol:** -1. Teammate marks TaskList item complete → lead reviews (or asks QA teammate to verify) -2. If satisfied → `arc close --reason "completed by "` -3. If not → reassign or provide feedback - -### Invocation - -``` -/arc team-deploy -``` - -## Component 4: Web Team View - -### Route - -`/teams` (or `/projects/:id/teams`) - -### API Endpoint - -``` -GET /api/v1/projects/{ws}/team-context?epic_id=PROJ-5 -``` - -Returns the same grouped structure as the CLI command's JSON output. - -### Layout - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ TEAM DEPLOYMENT Epic: PROJ-5 · Implement Auth System │ -│ Plan: "JWT-based auth with refresh tokens, httpOnly cookies…" │ -├──────────────┬──────────────┬──────────────┬────────────────────┤ -│ ◈ ARCHITECT │ ◈ BACKEND │ ◈ FRONTEND │ ◈ UNASSIGNED │ -│ 1 issue │ 2 issues │ 3 issues │ 1 issue │ -├──────────────┼──────────────┼──────────────┼────────────────────┤ -│ [cards] │ [cards] │ [cards] │ [cards] │ -└──────────────┴──────────────┴──────────────┴────────────────────┘ -``` - -- **Role columns**: Each `teammate:*` label → column. Header shows role name + label color accent stripe. -- **Epic header**: Title + truncated plan summary. Expandable. -- **Issue cards**: Compact — ID, title, priority, type badge, status icon, dependency links. -- **Unassigned column**: Issues without `teammate:*` labels. -- **Read-only**: Changes happen through arc CLI. - -### Components - -| Component | Purpose | -|---|---| -| `TeamView.svelte` | Page layout — epic header + role columns | -| `RoleLane.svelte` | Single role column with header + issue cards | -| `TeamIssueCard.svelte` | Compact issue card for team context | - -### Design - -"Refined Terminal" aesthetic — dark oklch surfaces, electric indigo accents, JetBrains Mono + Instrument Sans. Mission control dashboard feel: tight, purposeful, information-dense. - -## Component 5: Brainstorming/Planning → Arc Storage - -### Current State - -``` -brainstorming skill → Write docs/plans/YYYY-MM-DD--design.md -writing-plans skill → Write docs/plans/YYYY-MM-DD-.md (checklist) -executing-plans skill → TaskCreate items, works through them -``` - -Output is ephemeral markdown — useful within a session but disconnected from arc's persistent tracking. - -### With Arc Integration - -``` -brainstorming skill → arc create "Topic" --type=epic - → arc plan set "design content" - -writing-plans skill → arc create "Step 1" --parent= --type=task - → arc create "Step 2" --parent= --type=task - → (optionally) arc label add teammate:frontend - → arc dep add (if sequential) - -executing-plans → reads arc issues, works through them - OR -team-deploy → reads arc issues, deploys a team -``` - -### Skill Modifications - -**Brainstorming skill** — change the "Write design doc" step: -- Instead of `Write` to `docs/plans/`, run: - ```bash - arc create "Design Topic" --type=epic --description="..." - arc plan set --stdin # pipe design content - ``` -- Design lives in arc as a shared plan on an epic - -**Writing-plans skill** — change the output step: -- Instead of writing a markdown checklist, run: - ```bash - arc create "Step 1: Set up auth middleware" --type=task --parent= --priority=1 - arc create "Step 2: Implement login endpoint" --type=task --parent= --priority=2 - arc dep add - ``` -- Optionally prompt: "Should I add `teammate:*` labels for team deployment?" - -**Executing-plans skill** — change the input step: -- Instead of reading a markdown file, run: - ```bash - arc list --parent= --json - ``` -- Works through arc issues instead of markdown checklist -- Calls `arc close ` as each step completes - -### CLI Convenience - -New `arc plan set` flag to accept piped content: -```bash -echo "design content" | arc plan set --stdin -``` -Avoids `--editor` (interactive) when skills write programmatically. - -### Future: Arc-Aware Skills (V2) - -Add a "read context" input phase to each skill: -- Brainstorming checks for existing related issues/plans before proposing -- Planning reads dependency graphs to understand sequencing constraints -- Purely additive — no restructuring needed since data is already in arc - -## Workflow Summary - -### Full Pipeline (brainstorm → deploy) - -``` -/brainstorm "Build auth system" - ↓ -Brainstorming skill → arc create epic + arc plan set (design) - ↓ -/writing-plans - ↓ -Planning skill → arc create child issues with deps - → optionally add teammate:* labels - ↓ -/arc team-deploy (or /executing-plans for single-agent) - ↓ -Orchestration skill → arc team context - ↓ -Proposes team composition → user approves - ↓ -Creates Claude Code team + TaskList items -Spawns teammates with ARC_TEAMMATE_ROLE env var - ↓ -Teammates get role-filtered context via arc prime -Work using Claude Code TaskList for coordination - ↓ -Teammate completes TaskList item - ↓ -Team lead verifies (optionally via QA teammate) - ↓ -Team lead runs `arc close ` → arc issue closed - ↓ -Web Team View reflects status changes in real-time -``` - -### Standalone Team Deploy (issues already exist) - -``` -User labels issues with teammate:* labels -User links shared plan to epic - ↓ -/arc team-deploy - ↓ -(same flow as above from orchestration onward) -``` - -## What Doesn't Change - -- Label schema (no new fields) -- Plan schema (no new fields) -- Issue schema (no new fields) -- Existing `arc prime` default output (non-team sessions unchanged) -- Claude Code's TaskList behavior -- The `arc-issue-tracker` agent (still useful for non-team bulk operations) - -## Verification - -```bash -# Go changes -make test - -# Web changes -cd web && bun run lint && bun run format:check - -# Manual verification -# 1. Create teammate:* labels via /labels page -# 2. Create epic with child issues, label them -# 3. Run `arc team context ` — verify grouped output -# 4. Run `arc prime` with ARC_TEAMMATE_ROLE=frontend — verify filtered output -# 5. Visit /teams — verify dashboard renders correctly -``` diff --git a/docs/plans/2026-02-28-agentic-teams.md b/docs/plans/2026-02-28-agentic-teams.md deleted file mode 100644 index 3ee09c3..0000000 --- a/docs/plans/2026-02-28-agentic-teams.md +++ /dev/null @@ -1,615 +0,0 @@ -# Agentic Teams Integration Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Integrate arc's labels, plans, and dependency graph with Claude Code agent teams so that `teammate:*` labels drive team composition, plans serve as team briefs and acceptance criteria, and a web dashboard shows team activity. - -**Architecture:** Arc serves as the strategic context layer (persistent issues, labels, plans, deps) while Claude Code's TaskList handles tactical real-time coordination. The team lead bridges the two: reading arc via `arc team context` to decompose work, spawning teammates with `ARC_TEAMMATE_ROLE` env vars so `arc prime` gives role-filtered context, and syncing results back after verification. A new web Team View renders the same grouped data. - -**Tech Stack:** Go (Cobra CLI, Echo API, sqlc storage), SvelteKit 5 (Svelte runes, TypeScript), Claude Code plugin (skills/agents markdown) - -**Skills:** Use `use-modern-go` for all Go code. Use `frontend-design` for all Svelte UI work. Use `svelte-autofixer` before finalizing any Svelte component. - ---- - -### Task 1: Enhanced `arc prime` — Role Detection - -**Files:** -- Modify: `cmd/arc/prime.go` -- Test: manual — `ARC_TEAMMATE_ROLE=frontend ./bin/arc prime` - -**Step 1: Add role flag and env var detection to prime command** - -In `cmd/arc/prime.go`, add a `--role` flag alongside the existing `--mcp` flag, and check the `ARC_TEAMMATE_ROLE` env var as fallback. - -```go -var primeRole string // add alongside primeFullMode, primeMCPMode - -// In init() or flag registration: -primeCmd.Flags().StringVar(&primeRole, "role", "", "Teammate role (lead, frontend, backend, etc.)") -``` - -In the `RunE` function, after the existing mode checks, add role detection: - -```go -// Detect role from flag or env var -role := primeRole -if role == "" { - role = os.Getenv("ARC_TEAMMATE_ROLE") -} -``` - -**Step 2: Add team lead output function** - -Create `outputTeamLeadContext(w io.Writer)` that outputs: -- Full CLI reference (reuse `outputCLIContext` content) -- Team sync protocol section: - - "After teammate completes work, verify before closing arc issues" - - `arc close --reason "completed by "` - - "Use `arc team context ` to check progress" - -**Step 3: Add teammate output function** - -Create `outputTeammateContext(w io.Writer, role string)` that outputs: -- Role identification: "You are the {role} teammate" -- Instruction to focus on issues labeled `teammate:{role}` -- Session close protocol (git commit/push — same as default) -- Concise — no full CLI reference (teammates use TaskList, not arc CLI directly) - -**Step 4: Wire role-based output into RunE** - -After the existing mode selection (`if primeMCPMode {...} else if primeFullMode {...}`), add: - -```go -if role == "lead" { - return outputTeamLeadContext(os.Stdout) -} else if role != "" { - return outputTeammateContext(os.Stdout, role) -} -``` - -This goes before the default output path so existing behavior is unchanged when no role is set. - -**Step 5: Build and test** - -Run: `make build-quick` - -Test all three modes: -```bash -./bin/arc prime # default — unchanged -ARC_TEAMMATE_ROLE=lead ./bin/arc prime # team lead -ARC_TEAMMATE_ROLE=frontend ./bin/arc prime # teammate -./bin/arc prime --role=backend # teammate via flag -``` - -Expected: Each mode outputs distinct context appropriate to the role. - -**Step 6: Commit** - -```bash -git add cmd/arc/prime.go -git commit -m "feat(cli): add role-aware output to arc prime - -Detect ARC_TEAMMATE_ROLE env var or --role flag to output -team-specific context. Lead gets sync protocol, teammates -get role-filtered guidance." -``` - ---- - -### Task 2: `arc team context` CLI Command - -**Files:** -- Create: `cmd/arc/team.go` -- Modify: `cmd/arc/main.go` (register command) -- Test: manual — `arc team context --json` - -**Step 1: Create the team command group** - -Create `cmd/arc/team.go` with a parent `teamCmd` and a `teamContextCmd` subcommand. Follow the pattern from `cmd/arc/plan.go`: - -```go -var teamCmd = &cobra.Command{ - Use: "team", - Short: "Agent team operations", -} - -func init() { - rootCmd.AddCommand(teamCmd) - teamCmd.AddCommand(teamContextCmd) -} -``` - -**Step 2: Implement `teamContextCmd`** - -The command accepts an optional epic ID argument. It: -1. Resolves the workspace (same pattern as other commands) -2. If epic ID given: fetches the epic's child issues via `ListIssues` with parent filter -3. If no epic ID: fetches all issues with `teammate:*` labels via `ListIssues` -4. Fetches labels for all issues via `GetLabelsForIssues` -5. Groups issues by their `teammate:*` label -6. Fetches plans for the epic (shared) and each issue (inline) -7. Fetches dependencies for each issue -8. Outputs the grouped structure - -```go -var teamContextCmd = &cobra.Command{ - Use: "context [epic-id]", - Short: "Output team context grouped by teammate roles", - Args: cobra.MaximumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - // Implementation - }, -} -``` - -**Step 3: Define the output struct** - -```go -type TeamContext struct { - Project string `json:"project"` - Epic *TeamContextEpic `json:"epic,omitempty"` - Roles map[string]*TeamRole `json:"roles"` - Unassigned []TeamContextIssue `json:"unassigned"` -} - -type TeamContextEpic struct { - ID string `json:"id"` - Title string `json:"title"` - Plan string `json:"plan,omitempty"` -} - -type TeamRole struct { - Issues []TeamContextIssue `json:"issues"` -} - -type TeamContextIssue struct { - ID string `json:"id"` - Title string `json:"title"` - Priority int `json:"priority"` - Status string `json:"status"` - Type string `json:"type"` - Plan string `json:"plan,omitempty"` - Deps []string `json:"deps,omitempty"` -} -``` - -**Step 4: Implement grouping logic** - -For each issue, check its labels. Labels starting with `teammate:` extract the role name (everything after the prefix). Issues with no `teammate:*` label go into `Unassigned`. - -```go -for _, issue := range issues { - labels := labelsMap[issue.ID] - role := "" - for _, l := range labels { - if strings.HasPrefix(l, "teammate:") { - role = strings.TrimPrefix(l, "teammate:") - break - } - } - // ... add to roles map or unassigned -} -``` - -**Step 5: Add human-readable output** - -When `--json` is not set, output a formatted table using `tabwriter`: -``` -ROLE ISSUES IDS -architect 1 PROJ-7 -backend 2 PROJ-8, PROJ-11 -frontend 3 PROJ-12, PROJ-14, PROJ-16 -unassigned 1 PROJ-20 -``` - -**Step 6: Register in main.go** - -The `init()` in `team.go` handles this via `rootCmd.AddCommand(teamCmd)`. Verify it compiles. - -**Step 7: Build and test** - -Run: `make build-quick` - -Test: -```bash -./bin/arc team context --json # all teammate-labeled issues -./bin/arc team context PROJ-5 --json # epic children grouped -./bin/arc team context # human-readable -``` - -**Step 8: Commit** - -```bash -git add cmd/arc/team.go -git commit -m "feat(cli): add arc team context command - -Outputs issues grouped by teammate:* labels with plans and -dependencies. Supports --json for machine consumption and -human-readable table output." -``` - ---- - -### Task 3: Team Context API Endpoint - -**Files:** -- Create: `internal/api/teams.go` -- Modify: `internal/api/server.go` (register route) -- Test: `internal/api/teams_test.go` - -**Step 1: Write the failing test** - -Create `internal/api/teams_test.go`: - -```go -func TestGetTeamContext(t *testing.T) { - server, cleanup := testServer(t) - defer cleanup() - - // Create workspace - wsID := createTestWorkspace(t, server.echo) - - // Create teammate labels - createLabel(t, server.echo, "teammate:frontend", "#3b82f6") - createLabel(t, server.echo, "teammate:backend", "#22c55e") - - // Create epic - epicID := createTestIssue(t, server.echo, wsID, "Auth System", "epic") - - // Create child issues with labels - frontendID := createTestIssue(t, server.echo, wsID, "Login form", "task") - addLabelToIssue(t, server.echo, wsID, frontendID, "teammate:frontend") - - backendID := createTestIssue(t, server.echo, wsID, "Auth API", "task") - addLabelToIssue(t, server.echo, wsID, backendID, "teammate:backend") - - // Add parent-child deps - addDependency(t, server.echo, wsID, frontendID, epicID, "parent-child") - addDependency(t, server.echo, wsID, backendID, epicID, "parent-child") - - // Request team context - req := httptest.NewRequest(http.MethodGet, - fmt.Sprintf("/api/v1/projects/%s/team-context?epic_id=%s", wsID, epicID), nil) - rec := httptest.NewRecorder() - server.echo.ServeHTTP(rec, req) - - if rec.Code != http.StatusOK { - t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String()) - } - - var ctx TeamContext - json.Unmarshal(rec.Body.Bytes(), &ctx) - - if len(ctx.Roles["frontend"].Issues) != 1 { - t.Errorf("expected 1 frontend issue, got %d", len(ctx.Roles["frontend"].Issues)) - } - if len(ctx.Roles["backend"].Issues) != 1 { - t.Errorf("expected 1 backend issue, got %d", len(ctx.Roles["backend"].Issues)) - } -} -``` - -**Step 2: Run test to verify it fails** - -Run: `make test` -Expected: FAIL — `TestGetTeamContext` fails because endpoint doesn't exist. - -**Step 3: Implement the handler** - -Create `internal/api/teams.go` with: - -```go -func (s *Server) getTeamContext(c echo.Context) error { - wsID := c.Param("ws") - epicID := c.QueryParam("epic_id") - - // Fetch issues (children of epic, or all with teammate:* labels) - // Group by teammate:* labels - // Fetch plans and dependencies - // Return TeamContext struct -} -``` - -Reuse the same `TeamContext` struct from the CLI command (move it to `internal/types/` or define locally in the API package). - -**Step 4: Register the route** - -In `internal/api/server.go`, add to the workspace-scoped routes: - -```go -ws.GET("/team-context", s.getTeamContext) -``` - -**Step 5: Run test to verify it passes** - -Run: `make test` -Expected: PASS - -**Step 6: Commit** - -```bash -git add internal/api/teams.go internal/api/teams_test.go internal/api/server.go -git commit -m "feat(api): add team-context endpoint - -GET /api/v1/projects/:ws/team-context?epic_id=... -Returns issues grouped by teammate:* labels with plans and deps." -``` - ---- - -### Task 4: Team Orchestration Skill - -**Files:** -- Create: `claude-plugin/skills/arc-team-deploy/SKILL.md` - -**Step 1: Write the skill file** - -Create `claude-plugin/skills/arc-team-deploy/SKILL.md` with: -- Frontmatter: name, description, tools needed -- When to invoke: user says "deploy team", "create agent team from arc", or `/arc team-deploy` -- Workflow steps (as documented in design): - 1. Run `arc team context --json` - 2. Parse output and present team composition proposal - 3. After approval, call TeamCreate, TaskCreate (per issue), set deps, spawn teammates - 4. Set `ARC_TEAMMATE_ROLE` env var on each teammate - 5. Assign tasks via TaskUpdate - 6. Define sync protocol for the team lead - -**Step 2: Test by reading the skill** - -Verify the skill loads correctly by checking Claude Code recognizes it: -```bash -cat claude-plugin/skills/arc-team-deploy/SKILL.md -``` - -**Step 3: Commit** - -```bash -git add claude-plugin/skills/arc-team-deploy/SKILL.md -git commit -m "feat(plugin): add arc team-deploy orchestration skill - -Skill for decomposing arc issues into Claude Code agent teams -based on teammate:* labels and shared plans." -``` - ---- - -### Task 5: Web Team View — API Client - -**Files:** -- Modify: `web/src/lib/api.ts` (add team context fetch function) -- Modify: `web/src/lib/types.ts` (add TeamContext types, or verify auto-generated) - -**Step 1: Add TypeScript types** - -If not auto-generated from OpenAPI, add types in the appropriate location: - -```typescript -interface TeamContext { - workspace: string; - epic?: { id: string; title: string; plan?: string }; - roles: Record; - unassigned: TeamContextIssue[]; -} - -interface TeamContextIssue { - id: string; - title: string; - priority: number; - status: string; - type: string; - plan?: string; - deps?: string[]; -} -``` - -**Step 2: Add API fetch function** - -In the API client file, add: - -```typescript -export async function getTeamContext(projectId: string, epicId?: string): Promise { - const params = epicId ? `?epic_id=${epicId}` : ''; - const response = await fetch(`${baseUrl}/api/v1/projects/${projectId}/team-context${params}`); - return response.json(); -} -``` - -**Step 3: Commit** - -```bash -git add web/src/lib/api.ts -git commit -m "feat(web): add team context API client function" -``` - ---- - -### Task 6: Web Team View — Page Components - -**Files:** -- Create: `web/src/routes/[workspaceId]/teams/+page.svelte` -- Create: `web/src/lib/components/RoleLane.svelte` -- Create: `web/src/lib/components/TeamIssueCard.svelte` - -> **Note:** Use the `frontend-design` skill for UI implementation. Use `svelte-autofixer` before finalizing each component. - -**Step 1: Create TeamIssueCard component** - -Compact card showing: issue ID, title, priority indicator, type badge, status icon. -If issue has deps, show them as small linked badges. -Follow existing IssueCard patterns but more compact. - -**Step 2: Create RoleLane component** - -Props: `role: string`, `color: string`, `issues: TeamContextIssue[]` - -Vertical column with: -- Header: role name (capitalized) with color accent stripe, issue count badge -- Stacked TeamIssueCards - -**Step 3: Create the Teams page** - -`web/src/routes/[workspaceId]/teams/+page.svelte` - -Layout: -- Epic selector (dropdown of epics in workspace) or URL param -- Epic header bar with plan summary (expandable) -- Horizontal flex of RoleLane columns + Unassigned column -- Fetches data via `getTeamContext(workspaceId, epicId)` - -Follow existing page patterns: -- `$state` for data -- `$effect` to reload when workspace/epic changes -- Error handling with existing patterns - -**Step 4: Add navigation link** - -Add a "Teams" link to the workspace sidebar/navigation (if one exists), following the pattern of the existing "Issues", "Ready", "Blocked", "Labels" nav items. - -**Step 5: Run lint and format** - -```bash -cd web && bun run lint && bun run format:check -``` - -Fix any issues. - -**Step 6: Run svelte-autofixer on each component** - -Use the `svelte-autofixer` MCP tool on each `.svelte` file. Fix any issues until clean. - -**Step 7: Commit** - -```bash -git add web/src/routes/[workspaceId]/teams/ web/src/lib/components/RoleLane.svelte web/src/lib/components/TeamIssueCard.svelte -git commit -m "feat(web): add Team View dashboard page - -Displays issues grouped by teammate:* role labels in a -column layout. Shows epic plan summary and dependency links." -``` - ---- - -### Task 7: Update OpenAPI Spec - -**Files:** -- Modify: `api/openapi.yaml` (add team-context endpoint) - -**Step 1: Add the endpoint definition** - -Add the `GET /projects/{ws}/team-context` endpoint with: -- Query param: `epic_id` (optional) -- Response schema: `TeamContext` object - -**Step 2: Regenerate types** - -```bash -make gen -``` - -**Step 3: Verify generated types match manually added types** - -If web types were manually added in Task 5, verify they match the generated output. Remove manual types if auto-generation covers them. - -**Step 4: Commit** - -```bash -git add api/openapi.yaml -git commit -m "docs(api): add team-context endpoint to OpenAPI spec" -``` - ---- - -### Task 8: Brainstorming/Planning Skill Modifications - -**Files:** -- This task documents what skill changes are needed. The actual skill files live in the superpowers plugin, which is external. The integration happens through arc CLI commands that the skills invoke. - -**Step 1: Verify arc CLI supports the needed operations** - -The skills will need these commands to work: -```bash -arc create "Topic" --type=epic --description="..." # ✓ exists -arc plan set "plan content" # ✓ exists (but needs --stdin flag) -arc create "Step" --type=task --parent= # ✓ exists (parent via dep add) -arc dep add # ✓ exists -arc label add teammate:frontend # needs arc label CLI (follow-up) -``` - -**Step 2: Add `--stdin` flag to `arc plan set`** - -In `cmd/arc/plan.go`, add a `--stdin` flag to `planSetCmd` that reads plan content from stdin: - -```go -planSetCmd.Flags().Bool("stdin", false, "Read plan content from stdin") -``` - -In the RunE, before the editor check: -```go -useStdin, _ := cmd.Flags().GetBool("stdin") -if useStdin { - content, err := io.ReadAll(os.Stdin) - if err != nil { - return fmt.Errorf("reading stdin: %w", err) - } - planContent = string(content) -} -``` - -**Step 3: Test stdin flag** - -```bash -echo "This is the plan content" | ./bin/arc plan set PROJ-5 --stdin -arc plan show PROJ-5 # verify content was set -``` - -**Step 4: Document skill integration points** - -Create a brief guide in the plugin for how skills should use arc: - -Add to `claude-plugin/commands/team.md`: -```markdown -# arc team - -## Subcommands - -### arc team context [epic-id] -Output team context grouped by teammate:* labels. -``` - -**Step 5: Commit** - -```bash -git add cmd/arc/plan.go claude-plugin/commands/team.md -git commit -m "feat(cli): add --stdin flag to arc plan set - -Allows programmatic plan content input from skills and scripts. -Also adds team command documentation for plugin." -``` - ---- - -## Task Dependencies - -``` -Task 1 (prime) ──────────────────────┐ - ├── Task 4 (skill) -Task 2 (team context CLI) ───────────┤ - │ │ - ├── Task 3 (API endpoint) ───┤ - │ │ │ - │ ├── Task 5 (web API client) - │ │ │ - │ │ ├── Task 6 (web components) - │ │ - │ ├── Task 7 (OpenAPI) - │ -Task 8 (stdin flag) ── independent -``` - -Tasks 1, 2, and 8 can run in parallel. -Task 3 depends on Task 2 (shared types/logic). -Tasks 5-6 depend on Task 3. -Task 4 depends on Tasks 1 and 2. -Task 7 depends on Task 3. diff --git a/docs/plans/2026-02-28-custom-prefix-override-design.md b/docs/plans/2026-02-28-custom-prefix-override-design.md deleted file mode 100644 index 17ec7b7..0000000 --- a/docs/plans/2026-02-28-custom-prefix-override-design.md +++ /dev/null @@ -1,53 +0,0 @@ -# Custom Prefix Override for `arc init` - -## Problem - -When running `arc init` in a directory like `cortex-shell`, the auto-generated prefix truncates the basename to 5 alphanumeric characters: `corte-0b7w`. This is often unreadable and doesn't convey the project name well. Users want to: - -1. Override the basename portion with a custom short name (e.g., `cxsh`) -2. Have longer default basenames (10 chars instead of 5) for better readability - -## Design - -### New `--prefix` / `-p` flag on `arc init` - -```bash -arc init --prefix cxsh # produces prefix: cxsh-0b7w -arc init -p cxsh # same thing -arc init # auto-generates with 10-char basename (existing behavior, longer) -``` - -The flag value is silently normalized (lowercased, non-alphanumeric chars stripped) and truncated to 10 characters. The 4-char base36 hash suffix (derived from the full directory path) is always appended for uniqueness. - -### Prefix format change - -``` -Before: corte-0b7w (5 basename + 1 hyphen + 4 hash = 10 max) -After: cortexshel-0b7w (10 basename + 1 hyphen + 4 hash = 15 max) -``` - -### Changes - -| File | Change | -|------|--------| -| `internal/project/naming.go` | Increase basename truncation from 5 → 10 in `GeneratePrefix()` and `GeneratePrefixFromName()`. Add `GeneratePrefixWithCustomName(dirPath, customName)` that uses the custom name as basename with the path-derived hash. | -| `internal/types/types.go` | Update `Validate()` prefix max length from 10 → 15 | -| `cmd/arc/init.go` | Add `--prefix` / `-p` string flag. Route to `GeneratePrefixWithCustomName()` when provided. | -| `internal/project/naming_test.go` | Update truncation and max-length expectations. Add tests for custom name function. | -| `internal/types/types_test.go` | Update validation tests for new 15-char limit. | - -### What doesn't change - -- 4-char base36 hash suffix (still path-derived, deterministic) -- Issue ID format (`prefix.hash`) -- Web UI (sidebar uses first char of prefix) -- API contract (prefix is still a string field) -- Existing workspaces (shorter prefixes remain valid) - -### Normalization rules - -Custom prefix values go through `normalizeForPrefix()`: -- Lowercased -- Non-alphanumeric characters stripped -- Empty result falls back to "ws" -- Truncated to 10 characters diff --git a/docs/plans/2026-02-28-custom-prefix-override.md b/docs/plans/2026-02-28-custom-prefix-override.md deleted file mode 100644 index 4fe14f5..0000000 --- a/docs/plans/2026-02-28-custom-prefix-override.md +++ /dev/null @@ -1,456 +0,0 @@ -# Custom Prefix Override Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Add `--prefix` flag to `arc init` so users can override the issue prefix basename, and increase the default basename length from 5 to 10 characters. - -**Architecture:** The change flows through three layers: CLI flag parsing (`cmd/arc/init.go`), prefix generation logic (`internal/project/naming.go`), and validation (`internal/types/types.go`). A new `GeneratePrefixWithCustomName` function handles the custom basename case while reusing the existing path-based hash suffix for uniqueness. - -**Tech Stack:** Go, Cobra CLI, SHA-256 hashing, base36 encoding - ---- - -### Task 1: Increase prefix max length validation from 10 to 15 - -**Files:** -- Modify: `internal/types/types.go:32-33` -- Modify: `internal/types/types_test.go:493-500` - -**Step 1: Update the failing test expectation** - -In `internal/types/types_test.go`, update the "prefix too long" test case: - -```go -{ - name: "prefix too long", - ws: Project{ - Name: "Test", - Prefix: "thisprefixtoolong", - }, - wantErr: true, - errMsg: "project prefix must be 15 characters or less", -}, -``` - -**Step 2: Run test to verify it fails** - -Run: `go test ./internal/types/ -run TestProjectValidate -v` -Expected: FAIL — error message still says "10 characters or less" - -**Step 3: Update validation in types.go** - -In `internal/types/types.go`, change the prefix length check: - -```go -if len(w.Prefix) > 15 { - return fmt.Errorf("project prefix must be 15 characters or less") -} -``` - -**Step 4: Run test to verify it passes** - -Run: `go test ./internal/types/ -run TestProjectValidate -v` -Expected: PASS - -**Step 5: Commit** - -```bash -git add internal/types/types.go internal/types/types_test.go -git commit -m "feat: increase workspace prefix max length from 10 to 15" -``` - ---- - -### Task 2: Increase default basename truncation from 5 to 10 - -**Files:** -- Modify: `internal/project/naming.go:126-130` (`GeneratePrefix`) -- Modify: `internal/project/naming.go:153-156` (`GeneratePrefixFromName`) -- Modify: `internal/project/naming_test.go` - -**Step 1: Update test expectations for new truncation length** - -In `internal/project/naming_test.go`, update `TestGeneratePrefixTruncation`: - -```go -func TestGeneratePrefixTruncation(t *testing.T) { - // Long basename should be truncated to 10 alphanumeric chars before hash - // "my-very-long-project-name" -> "myverylongprojectname" -> "myverylongp" -> wait, 10 chars = "myverylong" - prefix, err := GeneratePrefix("/tmp/my-very-long-project-name") - if err != nil { - t.Fatalf("GeneratePrefix failed: %v", err) - } - - // Format: xxxxxxxxxx-yyyy (10 basename + 1 dash + 4 hash = 15 chars max) - if len(prefix) > 15 { - t.Errorf("Prefix should be max 15 chars, got %q (len %d)", prefix, len(prefix)) - } - - // Should start with truncated alphanumeric basename "myverylong-" - if prefix[:11] != "myverylong-" { - t.Errorf("Expected prefix to start with 'myverylong-', got %q", prefix) - } -} -``` - -Update `TestGeneratePrefixNormalization` — the expected prefixes for truncated names change: - -```go -{"hyphens removed", "/tmp/test-id-format", "testidform"}, -{"underscores removed", "/tmp/my_cool_project", "mycoolproj"}, -{"spaces removed", "/tmp/my project", "myproject"}, -{"special chars removed", "/tmp/I was_here#yesterday!", "iwashereye"}, -{"already clean", "/tmp/myapi", "myapi"}, -{"short name", "/tmp/api", "api"}, -``` - -Update `TestGeneratePrefixFromName`: - -```go -func TestGeneratePrefixFromName(t *testing.T) { - // "my-project" normalizes to "myproject", no truncation needed (9 chars < 10) - prefix := GeneratePrefixFromName("my-project") - - // ... existing hash suffix checks ... - - // Test that basename portion is correct - basename := prefix[:lastHyphen] - if basename != "myproject" { - t.Errorf("Expected basename 'myproject', got %q", basename) - } - - // Test max length: 10 basename + 1 dash + 4 hash = 15 chars - if len(prefix) > 15 { - t.Errorf("Prefix should be max 15 chars, got %q (len %d)", prefix, len(prefix)) - } -} -``` - -**Step 2: Run tests to verify they fail** - -Run: `go test ./internal/project/ -run "TestGeneratePrefix" -v` -Expected: FAIL — truncation is still at 5 - -**Step 3: Update truncation in GeneratePrefix and GeneratePrefixFromName** - -In `internal/project/naming.go`, in `GeneratePrefix()` change: - -```go -// Max 10 chars to fit in 15-char limit: 10 basename + 1 hyphen + 4 suffix = 15 -basename := normalizeForPrefix(filepath.Base(evalPath)) -if len(basename) > 10 { - basename = basename[:10] -} -``` - -In `GeneratePrefixFromName()` change: - -```go -// Max 10 chars to fit in 15-char limit: 10 basename + 1 hyphen + 4 suffix = 15 -normalized := normalizeForPrefix(name) -if len(normalized) > 10 { - normalized = normalized[:10] -} -``` - -**Step 4: Run tests to verify they pass** - -Run: `go test ./internal/project/ -run "TestGeneratePrefix" -v` -Expected: PASS - -**Step 5: Commit** - -```bash -git add internal/project/naming.go internal/project/naming_test.go -git commit -m "feat: increase prefix basename length from 5 to 10 characters" -``` - ---- - -### Task 3: Add GeneratePrefixWithCustomName function - -**Files:** -- Modify: `internal/project/naming.go` (add new function after `GeneratePrefix`) -- Modify: `internal/project/naming_test.go` (add new test) - -**Step 1: Write the failing test** - -Add to `internal/project/naming_test.go`: - -```go -func TestGeneratePrefixWithCustomName(t *testing.T) { - // Custom name should be normalized and used as basename - prefix, err := GeneratePrefixWithCustomName("/tmp/cortex-shell", "cxsh") - if err != nil { - t.Fatalf("GeneratePrefixWithCustomName failed: %v", err) - } - - // Should start with custom basename - lastHyphen := strings.LastIndex(prefix, "-") - if lastHyphen == -1 { - t.Fatalf("Prefix should contain a hyphen, got %q", prefix) - } - basename := prefix[:lastHyphen] - if basename != "cxsh" { - t.Errorf("Expected basename 'cxsh', got %q (full prefix: %q)", basename, prefix) - } - - // Hash suffix should be 4 chars - suffix := prefix[lastHyphen+1:] - if len(suffix) != 4 { - t.Errorf("Expected 4-char hash suffix, got %q (len %d)", suffix, len(suffix)) - } - - // Should be deterministic (same path = same hash) - prefix2, err := GeneratePrefixWithCustomName("/tmp/cortex-shell", "cxsh") - if err != nil { - t.Fatalf("GeneratePrefixWithCustomName failed: %v", err) - } - if prefix != prefix2 { - t.Errorf("Should be deterministic: %q != %q", prefix, prefix2) - } - - // Different paths with same custom name should produce different prefixes - prefix3, err := GeneratePrefixWithCustomName("/tmp/other-project", "cxsh") - if err != nil { - t.Fatalf("GeneratePrefixWithCustomName failed: %v", err) - } - if prefix == prefix3 { - t.Errorf("Different paths should produce different prefixes: %q == %q", prefix, prefix3) - } - - // Custom name should be normalized (special chars stripped) - prefix4, err := GeneratePrefixWithCustomName("/tmp/test", "cx-sh!") - if err != nil { - t.Fatalf("GeneratePrefixWithCustomName failed: %v", err) - } - basename4 := prefix4[:strings.LastIndex(prefix4, "-")] - if basename4 != "cxsh" { - t.Errorf("Expected normalized basename 'cxsh', got %q", basename4) - } - - // Long custom name should be truncated to 10 - prefix5, err := GeneratePrefixWithCustomName("/tmp/test", "verylongcustomname") - if err != nil { - t.Fatalf("GeneratePrefixWithCustomName failed: %v", err) - } - if len(prefix5) > 15 { - t.Errorf("Prefix should be max 15 chars, got %q (len %d)", prefix5, len(prefix5)) - } -} -``` - -**Step 2: Run test to verify it fails** - -Run: `go test ./internal/project/ -run TestGeneratePrefixWithCustomName -v` -Expected: FAIL — function does not exist - -**Step 3: Implement GeneratePrefixWithCustomName** - -Add to `internal/project/naming.go` after `GeneratePrefix`: - -```go -// GeneratePrefixWithCustomName creates an issue prefix using a user-provided basename -// combined with a path-derived hash suffix for uniqueness. -// The custom name is normalized (lowercased, non-alphanumeric stripped) and truncated to 10 chars. -// Format: {normalized-custom-name}-{4-char-base36-hash} -func GeneratePrefixWithCustomName(dirPath, customName string) (string, error) { - absPath, err := filepath.Abs(dirPath) - if err != nil { - return "", err - } - - evalPath, err := filepath.EvalSymlinks(absPath) - if err != nil { - evalPath = absPath - } - - normalized := filepath.ToSlash(evalPath) - - // Normalize and truncate custom name - basename := normalizeForPrefix(customName) - if len(basename) > 10 { - basename = basename[:10] - } - - // Generate deterministic hash from full path using base36 - hash := sha256.Sum256([]byte(normalized)) - suffix := Base36Encode(hash[:2]) - - for len(suffix) < 4 { - suffix = "0" + suffix - } - if len(suffix) > 4 { - suffix = suffix[:4] - } - - return basename + "-" + suffix, nil -} -``` - -**Step 4: Run test to verify it passes** - -Run: `go test ./internal/project/ -run TestGeneratePrefixWithCustomName -v` -Expected: PASS - -**Step 5: Commit** - -```bash -git add internal/project/naming.go internal/project/naming_test.go -git commit -m "feat: add GeneratePrefixWithCustomName for custom prefix support" -``` - ---- - -### Task 4: Add --prefix flag to arc init command - -**Files:** -- Modify: `cmd/arc/init.go:41-43` (add flag) -- Modify: `cmd/arc/init.go:46-74` (use flag in runInit) - -**Step 1: Add the flag registration** - -In `cmd/arc/init.go`, in the `init()` function, add: - -```go -func init() { - initCmd.Flags().StringP("description", "d", "", "Workspace description") - initCmd.Flags().StringP("prefix", "p", "", "Custom issue prefix (alphanumeric, max 10 chars)") - initCmd.Flags().BoolP("quiet", "q", false, "Suppress output") - rootCmd.AddCommand(initCmd) -} -``` - -**Step 2: Update runInit to use the flag** - -In `cmd/arc/init.go`, in `runInit`, after getting `cwd`, add the custom prefix flag reading and update the prefix generation: - -```go -customPrefix, _ := cmd.Flags().GetString("prefix") - -// Generate prefix with hash for guaranteed uniqueness -var prefix string -if customPrefix != "" { - prefix, err = project.GeneratePrefixWithCustomName(cwd, customPrefix) - if err != nil { - return fmt.Errorf("generate prefix: %w", err) - } -} else { - prefix, err = project.GeneratePrefix(cwd) - if err != nil { - return fmt.Errorf("generate prefix: %w", err) - } -} -``` - -**Step 3: Update the command's Long description** - -Update the `Long` field and examples to mention `--prefix`: - -```go -Long: `Initialize arc in the current directory by creating a workspace. - -This command: -1. Creates a workspace on the server (or connects to existing) -2. Sets the workspace as default for this directory -3. Creates .arc.json with workspace configuration -4. Creates AGENTS.md with session completion instructions - -For Claude Code users: Install the arc plugin for full integration -(hooks, skills, agents). The plugin's onboard skill will handle -workspace initialization automatically. - -For Codex CLI users: Run arc setup codex to install the repo-scoped -arc skill bundle under .codex/skills. - -Examples: - arc init # Use directory name as workspace - arc init my-project # Use custom name - arc init --prefix cxsh # Custom issue prefix (e.g., cxsh-0b7w)`, -``` - -**Step 4: Build and verify** - -Run: `make build-quick && ./bin/arc init --help` -Expected: Output shows `--prefix` / `-p` flag in help text - -**Step 5: Commit** - -```bash -git add cmd/arc/init.go -git commit -m "feat: add --prefix flag to arc init for custom issue prefixes" -``` - ---- - -### Task 5: Update arc docs for init command - -**Files:** -- Modify: `claude-plugin/commands/init.md` - -**Step 1: Update the docs** - -Replace `claude-plugin/commands/init.md` with: - -```markdown ---- -description: Initialize arc in the current project -argument-hint: [workspace-name] ---- - -Initialize arc in the current directory. - -```bash -arc init # Use directory name as workspace -arc init my-project # Custom workspace name -arc init --prefix cxsh # Custom issue prefix (e.g., cxsh-0b7w) -arc init my-project -p cxsh # Both custom name and prefix -``` - -This command: -1. Creates a workspace on the arc server -2. Saves workspace config to `.arc.json` -3. Creates AGENTS.md with workflow instructions -4. Sets up Claude Code hooks (unless --skip-claude) - -**Flags:** -- `--prefix`, `-p`: Custom issue prefix basename (alphanumeric, max 10 chars). Gets normalized (lowercased, special chars stripped) and combined with a hash suffix for uniqueness. -- `--description`, `-d`: Workspace description -- `--quiet`, `-q`: Suppress output - -**Prerequisites:** -- Arc server must be running (`arc server start`) -``` - -**Step 2: Commit** - -```bash -git add claude-plugin/commands/init.md -git commit -m "docs: update arc init docs with --prefix flag" -``` - ---- - -### Task 6: Final integration test - -**Step 1: Run full test suite** - -Run: `make test` -Expected: All tests pass - -**Step 2: Manual smoke test** - -Run: -```bash -make build-quick -cd /tmp && mkdir prefix-test && cd prefix-test -arc init --prefix mytest -``` - -Expected output includes: -``` -Prefix: mytest-xxxx -Issues will be named: mytest-xxxx. -``` - -**Step 3: Commit any remaining fixes if needed** diff --git a/docs/plans/2026-02-28-label-color-picker-design.md b/docs/plans/2026-02-28-label-color-picker-design.md deleted file mode 100644 index 7e7e940..0000000 --- a/docs/plans/2026-02-28-label-color-picker-design.md +++ /dev/null @@ -1,73 +0,0 @@ -# Global Labels with Color Picker — Design - -## Problem - -Labels are currently workspace-scoped (`PRIMARY KEY (workspace_id, name)`), which is unnecessarily complex — labels like "bug", "feature", "urgent" should be shared across workspaces. Additionally, labels support a `color` field throughout the backend, but the web UI has no color picker for creating/editing labels, and issue cards display all labels in monochrome gray. - -## Design - -### Schema Migration - -Drop `workspace_id` from the `labels` table. Deduplicate on collision (keep first, drop dupes). - -```sql -CREATE TABLE labels_new ( - name TEXT PRIMARY KEY, - color TEXT, - description TEXT -); - -INSERT OR IGNORE INTO labels_new (name, color, description) - SELECT name, color, description FROM labels; - -DROP TABLE labels; -ALTER TABLE labels_new RENAME TO labels; -``` - -`issue_labels` table unchanged — already references `(issue_id, label)` with no workspace_id. - -### Backend Changes - -**Types** (`internal/types/types.go`): -- Remove `WorkspaceID` from `Label` struct - -**Storage interface** (`internal/storage/storage.go`): -- Drop `workspaceID` parameter from `GetLabel`, `ListLabels`, `DeleteLabel` - -**SQLite** (`internal/storage/sqlite/labels.go`): -- Update queries to remove workspace_id WHERE clauses - -**API** (`internal/api/labels.go`): -- Move label CRUD endpoints to top-level: - - `GET /labels`, `POST /labels`, `PUT /labels/{labelName}`, `DELETE /labels/{labelName}` -- Issue label endpoints stay workspace-scoped (they're about issues) - -**OpenAPI spec** (`api/openapi.yaml`): -- Update label schemas and paths, regenerate types - -### Web UI - -**Global labels page** — new route at `/labels` (outside workspace context): -- Grid of label cards (existing layout) -- Create/edit form with: - - Name text input - - Preset color palette (~12-16 curated dark-theme-friendly swatches) + custom hex input - - Description text input -- Delete via existing ConfirmDialog - -**Color picker component**: -- Preset palette grid (clickable swatches, selected state with ring/check) -- Optional hex text input for custom colors -- No full HSL/gradient picker — keep it simple - -**Issue cards** (`IssueCard.svelte`): -- Replace monochrome `bg-surface-600` with label's actual color -- Pattern: low-opacity background + readable text (like StatusBadge: `bg-{color}/15 text-{color}`) -- Fetch `GET /labels` once, cache label→color map -- Fallback to gray when no color set - -### What doesn't change - -- `issue_labels` table (already workspace-independent in its reference pattern) -- Issue label add/remove endpoints (stay under `/projects/{id}/issues/{id}/labels`) -- CLI `arc label` commands (will need workspace flag removed, but that's a follow-up) diff --git a/docs/plans/2026-02-28-label-color-picker.md b/docs/plans/2026-02-28-label-color-picker.md deleted file mode 100644 index 5570b4f..0000000 --- a/docs/plans/2026-02-28-label-color-picker.md +++ /dev/null @@ -1,547 +0,0 @@ -# Global Labels with Color Picker — Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Use frontend-design skill for Task 9-11 (UI components). - -**Goal:** Make labels global (drop workspace_id), add color picker UI for label create/edit, and render colored label badges on issue cards. - -**Architecture:** Migration drops workspace_id from labels table (deduplicating on collision). API endpoints move from workspace-scoped to top-level `/api/v1/labels`. Frontend gets a new `/labels` route with color picker and CRUD forms. IssueCard renders label colors. - -**Tech Stack:** Go (Echo, sqlc, goose migrations), SvelteKit 5 (runes), Tailwind CSS, openapi-fetch - ---- - -### Task 1: Schema migration — drop workspace_id from labels - -**Files:** -- Create: `internal/storage/sqlite/migrations/005_global_labels.sql` - -**Step 1: Write the migration** - -```sql --- +goose Up -CREATE TABLE labels_new ( - name TEXT PRIMARY KEY, - color TEXT, - description TEXT -); - -INSERT OR IGNORE INTO labels_new (name, color, description) - SELECT name, color, description FROM labels; - -DROP TABLE labels; - -ALTER TABLE labels_new RENAME TO labels; - --- +goose Down -CREATE TABLE labels_new ( - workspace_id TEXT NOT NULL, - name TEXT NOT NULL, - color TEXT, - description TEXT, - PRIMARY KEY (workspace_id, name), - FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE -); - -INSERT INTO labels_new (workspace_id, name, color, description) - SELECT '', name, color, description FROM labels; - -DROP TABLE labels; - -ALTER TABLE labels_new RENAME TO labels; -``` - -**Step 2: Update the schema file** - -In `internal/storage/sqlite/db/schema.sql`, update the labels table definition: - -```sql -CREATE TABLE labels ( - name TEXT PRIMARY KEY, - color TEXT, - description TEXT -); -``` - -Remove the `workspace_id` and `FOREIGN KEY` from the labels table. Keep `issue_labels` unchanged. - -**Step 3: Verify migration applies** - -Run: `go test ./internal/storage/sqlite/ -run TestMigration -v` (if exists), otherwise: `make build-quick && ./bin/arc-server` to verify it starts without errors. - -**Step 4: Commit** - -```bash -git add internal/storage/sqlite/migrations/005_global_labels.sql internal/storage/sqlite/db/schema.sql -git commit -m "feat: migration to make labels global (drop workspace_id)" -``` - ---- - -### Task 2: Update sqlc queries for global labels - -**Files:** -- Modify: `internal/storage/sqlite/db/queries/labels.sql` - -**Step 1: Update the queries to remove workspace_id** - -Replace the full file with: - -```sql --- name: CreateLabel :exec -INSERT INTO labels (name, color, description) -VALUES (?, ?, ?) -ON CONFLICT(name) DO UPDATE SET - color = excluded.color, - description = excluded.description; - --- name: GetLabel :one -SELECT * FROM labels WHERE name = ?; - --- name: ListLabels :many -SELECT * FROM labels ORDER BY name; - --- name: UpdateLabel :exec -UPDATE labels SET color = ?, description = ? -WHERE name = ?; - --- name: DeleteLabel :exec -DELETE FROM labels WHERE name = ?; - --- name: AddLabelToIssue :exec -INSERT INTO issue_labels (issue_id, label) -VALUES (?, ?) -ON CONFLICT(issue_id, label) DO NOTHING; - --- name: RemoveLabelFromIssue :exec -DELETE FROM issue_labels WHERE issue_id = ? AND label = ?; - --- name: GetIssueLabels :many -SELECT label FROM issue_labels WHERE issue_id = ? ORDER BY label; - --- name: GetIssuesByLabel :many -SELECT i.* FROM issues i -JOIN issue_labels il ON i.id = il.issue_id -WHERE il.label = ? -ORDER BY i.priority ASC, i.updated_at DESC; - --- name: GetLabelsForIssues :many -SELECT issue_id, label FROM issue_labels -WHERE issue_id IN (sqlc.slice('issue_ids')) -ORDER BY issue_id, label; - --- name: DeleteIssueLabels :exec -DELETE FROM issue_labels WHERE issue_id = ?; -``` - -**Step 2: Regenerate sqlc** - -Run: `make gen` - -This regenerates `internal/storage/sqlite/db/models.go` and `internal/storage/sqlite/db/labels.sql.go`. The `Label` model will lose `WorkspaceID`, and query params will lose workspace_id fields. - -**Step 3: Commit** - -```bash -git add internal/storage/sqlite/db/ -git commit -m "feat: update sqlc queries for global labels" -``` - ---- - -### Task 3: Update Go types and storage interface - -**Files:** -- Modify: `internal/types/types.go:233-239` -- Modify: `internal/storage/storage.go:43-52` - -**Step 1: Update Label struct** - -In `internal/types/types.go`, replace the Label struct (lines 233-239): - -```go -// Label represents a global tag that can be applied to issues. -type Label struct { - Name string `json:"name"` - Color string `json:"color,omitempty"` - Description string `json:"description,omitempty"` -} -``` - -**Step 2: Update storage interface** - -In `internal/storage/storage.go`, replace label methods (lines 43-52): - -```go - // Labels (global) - CreateLabel(ctx context.Context, label *types.Label) error - GetLabel(ctx context.Context, name string) (*types.Label, error) - ListLabels(ctx context.Context) ([]*types.Label, error) - UpdateLabel(ctx context.Context, label *types.Label) error - DeleteLabel(ctx context.Context, name string) error - AddLabelToIssue(ctx context.Context, issueID, label, actor string) error - RemoveLabelFromIssue(ctx context.Context, issueID, label, actor string) error - GetIssueLabels(ctx context.Context, issueID string) ([]string, error) - GetLabelsForIssues(ctx context.Context, issueIDs []string) (map[string][]string, error) -``` - -**Step 3: Commit** - -```bash -git add internal/types/types.go internal/storage/storage.go -git commit -m "feat: update Label type and storage interface for global labels" -``` - ---- - -### Task 4: Update SQLite label storage implementation - -**Files:** -- Modify: `internal/storage/sqlite/labels.go` - -**Step 1: Update all label methods to remove workspace_id** - -Key changes throughout the file: - -- `CreateLabel`: Remove `WorkspaceID` from params, use new `db.CreateLabelParams{Name, Color, Description}` -- `GetLabel(ctx, name)`: Drop `workspaceID` param, call `s.queries.GetLabel(ctx, name)` -- `ListLabels(ctx)`: Drop `workspaceID` param, call `s.queries.ListLabels(ctx)` -- `UpdateLabel`: Remove workspace_id from `db.UpdateLabelParams` -- `DeleteLabel(ctx, name)`: Drop `workspaceID` param -- `dbLabelToType`: Remove `WorkspaceID` field from conversion -- `GetLabelsForIssues`: Update raw SQL — no workspace_id join needed (query is already workspace-independent since it queries `issue_labels`) - -**Step 2: Run tests** - -Run: `go test ./internal/storage/sqlite/ -v` -Expected: Compilation errors first (fix any remaining workspace_id references), then PASS - -**Step 3: Commit** - -```bash -git add internal/storage/sqlite/labels.go -git commit -m "feat: update SQLite label storage for global labels" -``` - ---- - -### Task 5: Update API handlers and routes - -**Files:** -- Modify: `internal/api/labels.go` -- Modify: `internal/api/server.go:111-117` - -**Step 1: Update label handlers to remove workspace_id dependency** - -In `internal/api/labels.go`: -- `listLabels`: Call `s.store.ListLabels(ctx)` (no workspace param) -- `createLabel`: Build `types.Label` without `WorkspaceID`, call `s.store.CreateLabel` -- `updateLabel`: Call `s.store.GetLabel(ctx, name)` (no workspace), then `s.store.UpdateLabel` -- `deleteLabel`: Call `s.store.DeleteLabel(ctx, name)` (no workspace) -- `addLabelToIssue` and `removeLabelFromIssue`: Keep reading workspaceId from path (for issue validation), but label operations are global - -**Step 2: Move label CRUD routes to top-level group** - -In `internal/api/server.go`, move the 4 label CRUD routes out of the `ws` group into the `v1` group: - -```go -// Labels (global) -v1.GET("/labels", s.listLabels) -v1.POST("/labels", s.createLabel) -v1.PUT("/labels/:name", s.updateLabel) -v1.DELETE("/labels/:name", s.deleteLabel) -``` - -Keep issue-label routes under `ws`: -```go -ws.POST("/issues/:id/labels", s.addLabelToIssue) -ws.DELETE("/issues/:id/labels/:label", s.removeLabelFromIssue) -``` - -**Step 3: Run tests** - -Run: `go test ./internal/api/ -v` -Expected: PASS (update any test fixtures that reference workspace-scoped label endpoints) - -**Step 4: Commit** - -```bash -git add internal/api/labels.go internal/api/server.go -git commit -m "feat: move label CRUD endpoints to global scope" -``` - ---- - -### Task 6: Update OpenAPI spec and regenerate types - -**Files:** -- Modify: `api/openapi.yaml` - -**Step 1: Update OpenAPI spec** - -1. Remove `workspace_id` from the `Label` schema required fields and properties -2. Move label CRUD paths from `/projects/{projectId}/labels` to `/labels`: - - `GET /labels` — listLabels - - `POST /labels` — createLabel - - `PUT /labels/{labelName}` — updateLabel - - `DELETE /labels/{labelName}` — deleteLabel -3. Keep issue-label paths under projects (unchanged) - -**Step 2: Regenerate** - -Run: `make gen` - -This updates `internal/api/openapi.gen.go` and `web/src/lib/api/types.ts`. - -**Step 3: Verify build** - -Run: `make build-quick` -Expected: Builds successfully - -**Step 4: Commit** - -```bash -git add api/openapi.yaml internal/api/openapi.gen.go web/src/lib/api/types.ts -git commit -m "feat: update OpenAPI spec for global labels" -``` - ---- - -### Task 7: Update frontend API client - -**Files:** -- Modify: `web/src/lib/api/index.ts` - -**Step 1: Update listLabels and add CRUD functions** - -Replace the existing `listLabels` function and add new ones: - -```typescript -// Label APIs (global) -export async function listLabels(): Promise { - const { data, error } = await api.GET('/labels'); - if (error) handleError(error); - return data ?? []; -} - -export async function createLabel( - name: string, - color?: string, - description?: string -): Promise