From bde15d45ca41e38c878ece8a6ed8f8436b8c6b4b Mon Sep 17 00:00:00 2001 From: xarmian Date: Tue, 7 Apr 2026 14:55:23 -0400 Subject: [PATCH] Rename Phases to Plans, clean up deprecated aliases (#71) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Rename "Phases" to "Plans" and clean up deprecated phase aliases Renames the default "Phases" collection to "Plans" across the full stack: - DB migration renames existing collections in-place (name, slug, prefix PLAN, icon πŸ—ΊοΈ) - Removes all deprecated Phase* backward-compat aliases from models and store - Removes --phase CLI flag (use --parent instead) - Updates convention triggers: on-phase-start/complete β†’ on-plan-start/complete - Updates dashboard API: active_phases β†’ active_plans, /phases-progress β†’ /plans-progress - Updates all frontend components, types, and documentation Closes IDEA-124 * Fix CSRF cookie not being cleared on logout The SessionAuth middleware was re-issuing a CSRF cookie before the logout handler could clear it, resulting in two Set-Cookie headers. Skip CSRF re-issue for /api/v1/auth/ paths since auth endpoints manage their own CSRF cookies (login sets, logout clears). * Fix migration issues found in Codex review - P1: Move doc_type UPDATE from migration 024 into 025, which recreates the table with the new CHECK constraint first (SQLite enforces CHECK on UPDATE, so the old constraint would reject 'plan') - P1: Add PostgreSQL migration 005 for the collection rename (phases β†’ plans) β€” previously only existed on the SQLite path - P2: Recreate FTS triggers, indexes, and rebuild FTS after the table swap in migration 025 (DROP TABLE drops associated objects in SQLite) * Fix parent filter field name and sync .agents skill copy Codex review round 2 findings: - P1: Parent filter compared against `parent_id` (wrong) instead of `parent_link_id` β€” plan filtering in collection view was broken - P1: .agents/skills/pad/SKILL.md still had old --phase flags and "Phases" references β€” synced from the updated .claude copy - P2: Accept legacy 'phase' filter key for backward compat with existing saved views that serialized the old key name * Fix PG migration JSONB casting and add slug collision guards Codex PR review bot findings: - P1: PostgreSQL REPLACE/LIKE don't work on JSONB columns β€” cast schema::text and fields::text before string ops, then back to ::jsonb - P1: If a workspace already has a custom 'plans' collection, the rename hits UNIQUE(workspace_id, slug) β€” added NOT EXISTS guard to both SQLite and PostgreSQL migrations --- .agents/skills/pad/SKILL.md | 289 ++++++++++++++++++ CLAUDE.md | 6 +- README.md | 8 +- cmd/pad/main.go | 62 ++-- cmd/pad/query.go | 2 +- internal/cli/agents.go | 2 +- internal/cli/format.go | 6 +- internal/collections/convention_library.go | 8 +- internal/collections/defaults.go | 14 +- internal/collections/playbook_library.go | 18 +- internal/collections/templates.go | 10 +- internal/models/document.go | 2 +- internal/models/item.go | 6 - internal/models/item_links.go | 2 - internal/models/snapshot.go | 6 +- internal/models/templates.go | 10 +- internal/server/handlers_dashboard.go | 58 ++-- internal/server/handlers_dashboard_test.go | 204 ++++++------- internal/server/handlers_items.go | 22 +- internal/server/handlers_items_test.go | 46 +-- internal/server/item_lineage.go | 12 - internal/server/middleware_auth.go | 7 +- internal/server/server.go | 4 +- internal/store/export.go | 2 +- internal/store/items.go | 55 +--- internal/store/items_test.go | 2 +- .../store/migrations/024_phases_to_plans.sql | 33 ++ .../store/migrations/025_doc_type_plan.sql | 69 +++++ .../store/pgmigrations/004_doc_type_plan.sql | 11 + .../pgmigrations/005_phases_to_plans.sql | 30 ++ internal/store/snapshots.go | 6 +- internal/store/store.go | 4 + internal/store/store_test.go | 2 +- skills/pad/SKILL.md | 54 ++-- web/src/lib/api/client.ts | 4 +- .../lib/components/OnboardingChecklist.svelte | 8 +- .../components/collections/FilterBar.svelte | 20 +- .../components/collections/ItemCard.svelte | 6 +- .../lib/components/common/EmptyState.svelte | 2 +- .../components/common/QuickActionsMenu.svelte | 3 +- web/src/lib/types/index.ts | 8 +- web/src/routes/[workspace]/+page.svelte | 32 +- .../[workspace]/[collection]/+page.svelte | 25 +- .../[collection]/[slug]/+page.svelte | 6 +- .../[workspace]/conventions/+page.svelte | 6 +- 45 files changed, 768 insertions(+), 424 deletions(-) create mode 100644 .agents/skills/pad/SKILL.md create mode 100644 internal/store/migrations/024_phases_to_plans.sql create mode 100644 internal/store/migrations/025_doc_type_plan.sql create mode 100644 internal/store/pgmigrations/004_doc_type_plan.sql create mode 100644 internal/store/pgmigrations/005_phases_to_plans.sql diff --git a/.agents/skills/pad/SKILL.md b/.agents/skills/pad/SKILL.md new file mode 100644 index 00000000..6f1141dc --- /dev/null +++ b/.agents/skills/pad/SKILL.md @@ -0,0 +1,289 @@ +--- +name: pad +description: "Talk to your project. Natural-language project management β€” create items, check status, plan work, brainstorm ideas, and more." +argument-hint: +allowed-tools: + - Bash + - Read +--- + +# Pad β€” Talk to Your Project + +You are the interface between the user and their Pad workspace β€” a project management tool for developers and AI agents. Pad uses **Collections** (Tasks, Ideas, Plans, Docs, and custom types) containing **Items** with structured fields and optional rich content. + +Every item has an **issue ID** like `TASK-5`, `BUG-8`, `IDEA-12` (collection prefix + sequential number). **Always use issue IDs to reference items** β€” never use slugs. Issue IDs are short, stable, and human-readable. + +The `pad` CLI must be on PATH. It auto-starts a local server and auto-detects the workspace from `.pad.toml` in the directory tree. If `pad` is not found, tell the user: "Pad CLI not found. Install it or add it to your PATH." + +## How This Works + +There is **one command**: `/pad `. You interpret the user's intent and use the CLI to take action. You are conversational β€” discuss before acting, ask clarifying questions, and always confirm before creating or modifying items. + +## Context Loading + +On every `/pad` invocation, start by loading workspace context: + +```bash +pad project dashboard --format json # Project overview: collections, plans, attention, suggestions +pad collection list --format json # Available collections with schemas +pad item list conventions --field status=active --field trigger=always --format json # Always-on project conventions +``` + +This tells you: what collections exist, what items are in them, what's active, what needs attention, and what project conventions to always follow. + +If the conventions list includes items, treat them as project rules you must follow. They are short instructions like "run make install after code changes" or "use conventional commit format." + +## Parse $ARGUMENTS + +### No arguments +Show project status conversationally. Run `pad project dashboard --format json`, and present the dashboard in a friendly, readable way β€” highlight what's active, what needs attention, and suggest what to work on next. + +### Natural Language Routing + +Interpret the user's intent and route to the appropriate action. Here are common patterns: + +**Creating items:** +- "I have an idea for X" β†’ Create an Idea item +- "new task: fix the OAuth bug" β†’ Create a Task item +- "let's start a new plan for the API redesign" β†’ Create a Plan item +- "document the auth architecture" β†’ Create a Doc item + +**Querying:** +- "what's on my plate?" / "what should I work on?" β†’ `pad project next --format json` +- "how far along are we?" / "show me status" β†’ `pad project dashboard --format json` +- "what server am I connected to?" / "show my Pad connection info" β†’ `pad server info --format json` +- "show me all tasks" / "list bugs" β†’ `pad item list --format json` +- "find anything about OAuth" β†’ `pad item search "OAuth" --format json` + +**Updating:** +- "I finished the OAuth fix" / "mark TASK-5 as done" β†’ `pad item update TASK-5 --status done` +- "I'm starting on TASK-3" β†’ `pad item update TASK-3 --status in-progress` +- "deprioritize IDEA-7" β†’ `pad item update IDEA-7 --priority low` + +**Planning:** +- "let's create a plan" β†’ Multi-step planning workflow (see below) +- "break plan 2 into tasks" β†’ Decompose a plan into task items +- "what's blocking us?" β†’ Analyze open items and dependencies + +**Ideation:** +- "let's brainstorm about X" β†’ Multi-step ideation workflow (see below) +- "what if we added X?" β†’ Discuss, then offer to capture as an Idea + +**Dependencies:** +- "what's blocking TASK-5?" / "show deps for TASK-5" β†’ `pad item deps TASK-5 --format json` +- "TASK-5 blocks TASK-8" β†’ `pad item block TASK-5 TASK-8` +- "TASK-5 depends on TASK-3" β†’ `pad item blocked-by TASK-5 TASK-3` +- "remove the dependency" β†’ `pad item unblock TASK-5 TASK-8` + +**Reports:** +- "prep for standup" / "what did we do?" β†’ `pad project standup --format json` +- "generate changelog" / "what shipped?" β†’ `pad project changelog --format json` +- "changelog for this plan" β†’ `pad project changelog --parent PLAN-2 --format json` +- "changelog since Monday" β†’ `pad project changelog --since 2026-03-24 --format json` + +**Retrospective:** +- "plan 2 is done, let's retro" β†’ Review completed work, save retrospective + +**Onboarding:** +- "scan this codebase" / "set up my workspace" β†’ Codebase analysis + onboarding workflow (see below) +- "what conventions should this project follow?" β†’ Analyze tooling, suggest conventions from the library + +## Before Performing Work + +When you are about to take action (implement code, complete a task, create a PR, etc.), load the relevant conventions and playbooks FIRST: + +```bash +# Before implementing code: +pad item list conventions --field trigger=on-implement --field status=active --format json +pad item list playbooks --field trigger=on-implement --field status=active --format json + +# Before completing a task: +pad item list conventions --field trigger=on-task-complete --field status=active --format json + +# Before creating a PR: +pad item list conventions --field trigger=on-pr-create --field status=active --format json + +# Before committing: +pad item list conventions --field trigger=on-commit --field status=active --format json + +# Before planning: +pad item list conventions --field trigger=on-plan --field status=active --format json +``` + +Follow ALL returned conventions. If a playbook exists for the action, follow its steps in order. Conventions are project-specific rules the team has established β€” they override your defaults. + +## CLI Reference + +**IMPORTANT:** All commands that take an item reference accept issue IDs (e.g. `TASK-5`, `BUG-8`). Always prefer issue IDs over slugs. When you create an item, the CLI prints its issue ID β€” use that for subsequent commands. + +### Item CRUD +```bash +# Create items (collection accepts singular or plural: task/tasks, idea/ideas, etc.) +# The CLI prints the new item's issue ID (e.g. "Created TASK-5: ...") β€” use it for subsequent commands +pad item create "title" [--status X] [--priority X] [--assignee X] [--category X] [--content "..."] [--stdin] +pad item create task "Fix OAuth redirect" --priority high +pad item create idea "Real-time collaboration" --category infrastructure +pad item create plan "API Redesign" --status active +pad item create doc "Auth Architecture" --category architecture --stdin <<< "# Auth Architecture\n\n..." + +# Custom fields via --field flag (works for any collection's fields) +pad item create convention "Run tests" --field trigger=on-task-complete --field scope=all --field priority=must +pad item create roadmap "Feature X" --field quarter=2026-Q3 + +# List items (defaults to non-done items) +pad item list [collection] [--status X] [--priority X] [--all] [--field key=value] [--format json] +pad item list tasks # open + in_progress tasks +pad item list tasks --status done # completed tasks +pad item list conventions --field trigger=always --field status=active # filtered by custom fields +pad item list --all # everything across all collections + +# Show item detail β€” use the issue ID (e.g. TASK-5, BUG-8) +pad item show TASK-5 [--format json|markdown] + +# Update items β€” use the issue ID +pad item update TASK-5 --status done +pad item update IDEA-3 --priority high --assignee dave +pad item update DOC-1 --stdin < updated-doc.md + +# Delete (archive) β€” use the issue ID +pad item delete TASK-5 + +# Search +pad item search "query" [--format json] +``` + +### Intelligence +```bash +pad project dashboard [--format json] # Project dashboard +pad project next [--format json] # Recommended next task +pad project standup [--days N] [--format json] # Daily standup report (completed/in-progress/blockers) +pad project changelog [--days N] [--since DATE] [--parent PLAN-2] [--format json|markdown] # Release notes +``` + +### Server +```bash +pad server info [--format json] # Show local client, connection, and local server status +pad server open # Open the Pad web UI in your browser +``` + +### Dependencies +```bash +pad item block TASK-5 TASK-8 # "TASK-5 blocks TASK-8" +pad item blocked-by TASK-5 TASK-3 # "TASK-5 is blocked by TASK-3" +pad item deps TASK-5 # Show all dependencies for an item +pad item unblock TASK-5 TASK-8 # Remove a dependency +``` + +### Collections +```bash +pad collection list [--format json] # List collections with counts +pad collection create "Name" --fields "key:type[:options]; ..." [--icon "X"] +``` + +### Webhooks +```bash +# Webhooks are managed via the REST API: +# POST /api/v1/workspaces/{ws}/webhooks β€” create webhook +# GET /api/v1/workspaces/{ws}/webhooks β€” list webhooks +# DELETE /api/v1/workspaces/{ws}/webhooks/{id} β€” delete +# Events: item.created, item.updated, item.deleted, item.moved, comment.created +``` + +### Output Formats +All commands support `--format json` (for parsing) or `--format table` (default, human-readable). + +## Multi-Step Workflows + +### Ideation: "Let's brainstorm about X" + +1. **Load context:** Run `pad project dashboard --format json` and `pad item list --format json --limit 20` +2. **Search for related items:** `pad item search "X" --format json` +3. **Discuss systematically:** Ask clarifying questions, explore trade-offs, reference existing items with [[Title]] links +4. **Offer to save:** At natural checkpoints, offer to create items: + - "Want me to save this as an Idea?" β†’ `pad item create idea "X" --content "..." --stdin` + - "Should I create a Doc for this architecture decision?" β†’ `pad item create doc "X" --category decision --stdin` +5. **Never save without asking.** Always show what you'll create and get confirmation. + +### Planning: "Let's create a plan" + +1. **Load context:** `pad project dashboard --format json`, `pad item list plans --all --format json` +2. **Understand current state:** What plans exist? What's active? What's completed? +3. **Propose outline:** Present plan title + 1-line summary. Ask for feedback. +4. **Create the plan:** `pad item create plan "Plan N: Title" --status draft --stdin <<< ""` +5. **Decompose into tasks:** For each task in the plan, create a Task item: + ```bash + pad item create task "Task description" --parent PLAN-3 --priority medium + ``` +6. **Each task should be PR-sized** β€” small enough for one branch, large enough to be meaningful. +7. **Ask before creating each item.** Don't bulk-create without approval. + +### Decomposition: "Break plan X into tasks" + +1. **Load the plan:** `pad item show PLAN-2 --format markdown` +2. **Analyze the content** for actionable work items +3. **Propose task list** with titles and priorities +4. **Create approved tasks:** One `pad item create task` per approved item +5. **Link tasks to plan** using `--parent PLAN-2` flag + +### Status Check: "How are we doing?" + +1. Run `pad project dashboard --format json` +2. Present conversationally: + - Collection summaries (Tasks: 5 open, 2 in progress, 12 done) + - Active plan progress with bars + - Attention items (stalled, overdue) + - Suggested next actions +3. Offer follow-up: "Want me to dig into any of these?" + +### Daily Standup: "Prep for standup" + +1. Run `pad item list tasks --status done --format json` (recently completed) +2. Run `pad item list tasks --status in-progress --format json` (current work) +3. Run `pad project dashboard --format json` for blockers/attention items +4. Present as: Yesterday / Today / Blockers format + +### Onboarding: "Scan this codebase" / "Set up my workspace" + +1. **Check workspace state:** `pad project dashboard --format json` β€” if the workspace already has items, ask if they want to add more or start fresh sections. +2. **Analyze the codebase:** Read key project files to understand the project: + - `README.md` or `README` β€” project overview, setup instructions + - `CLAUDE.md` β€” existing AI/agent instructions + - Build config: `Makefile`, `package.json`, `go.mod`, `Cargo.toml`, `pyproject.toml`, `pom.xml` + - CI config: `.github/workflows/`, `.gitlab-ci.yml`, `.circleci/` + - Directory structure: `ls` the top-level directories to understand the layout +3. **Detect project type and tooling:** + - Language: Go, Node/TypeScript, Rust, Python, Java, etc. + - Build system: make, npm, cargo, pip, maven, etc. + - Test runner: what command runs the tests? + - Linter/formatter: what tools enforce code style? +4. **Suggest conventions:** Based on the detected tooling, suggest conventions from the library. Customize the content with the actual commands found in the project (e.g., "Run `make test`" not just "Run the test suite"). Present as a checklist and ask which to activate. +5. **Draft an architecture doc:** Summarize the project structure, tech stack, key directories, and how the pieces fit together. Offer to save as a Doc item. +6. **Propose an initial plan:** Based on recent git activity (`git log --oneline -20`) and any open TODOs, suggest a plan name and a few starter tasks. Ask before creating. +7. **Always confirm before creating each item.** Show what will be created, get approval, then create. + +### Retrospective: "Plan X is done, let's retro" + +1. Load the plan: `pad item show PLAN-2 --format markdown` +2. Load tasks: `pad item list tasks --all --format json` (filter to plan) +3. Generate retro: What shipped, what was deferred, lessons learned +4. Offer to save: `pad item create doc "Plan N Retrospective" --category retro --stdin` +5. Offer to update plan status: `pad item update PLAN-2 --status completed` + +## Key Principles + +1. **Use issue IDs, not slugs.** Every item has an ID like `TASK-5` or `BUG-8`. Use these in all commands: `pad item show TASK-5`, `pad item update BUG-8 --status done`. The CLI prints issue IDs in all output β€” look for them. +2. **Discuss before acting.** Always show what you plan to create/modify and get confirmation. +3. **Use the CLI.** Every action goes through `pad` commands β€” don't try to modify the database directly. +4. **Be conversational.** You're not a command executor. You're a project partner. +5. **Reference existing items.** Use `[[Item Title]]` links in content to connect items. +6. **Keep it practical.** Tasks should be PR-sized. Ideas should be actionable. Docs should be concise. +7. **Attribution matters.** Items you create will have `created_by: agent` and `source: cli` automatically. +8. **Follow project conventions.** Always load and follow active conventions before performing work. They are project-specific rules that override your defaults. +9. **Learn and teach.** When the user corrects your behavior or teaches you a project-specific rule, offer to save it as a convention: "Should I save this as a project convention so future agents follow it too?" Use `pad item create convention "Title" --field trigger= --field scope= --field priority=should --stdin` with an appropriate trigger inferred from the context. + +## Anything Else + +If the user's intent doesn't match any pattern above, respond helpfully. You can always: +- Run `pad item list` or `pad item search` to find relevant items +- Run `pad item show TASK-5` to load any item's detail (use the issue ID from list output) +- Suggest the appropriate workflow based on what they're trying to do diff --git a/CLAUDE.md b/CLAUDE.md index 1f19a3e6..277aedd4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -66,7 +66,7 @@ REST API at `/api/v1/`. Key endpoints: - `GET/POST /workspaces/{ws}/collections` β€” collection CRUD - `GET/POST /workspaces/{ws}/collections/{coll}/items` β€” item CRUD - `GET/PATCH/DELETE /workspaces/{ws}/items/{slug}` β€” item by slug -- `GET /workspaces/{ws}/dashboard` β€” computed project overview (active items, phases, attention, blockers) +- `GET /workspaces/{ws}/dashboard` β€” computed project overview (active items, plans, attention, blockers) - `GET /workspaces/{ws}/activity` β€” workspace activity feed (enriched with item titles + change details) - `GET/POST/DELETE /workspaces/{ws}/webhooks` β€” webhook management - `GET /workspaces/{ws}/items/{slug}/children` β€” child items linked to a parent @@ -177,9 +177,9 @@ Collection names accept singular forms: `task`β†’`tasks`, `idea`β†’`ideas`, `doc - **Collections** have JSON schemas defining typed fields (select, text, date, number, etc.) - **Items** have structured `fields` JSON + optional rich `content` (markdown) -- **Parent/child links:** Any item can be a parent of child items (`--parent REF`). Children get progress tracking, burndown charts, and nested rendering. Phases are the most common parent, but Ideas, Docs, or Tasks can also have children. +- **Parent/child links:** Any item can be a parent of child items (`--parent REF`). Children get progress tracking, burndown charts, and nested rendering. Plans are the most common parent, but Ideas, Docs, or Tasks can also have children. - **Wiki-links** `[[Title]]` resolve across all items, rendered as clickable links -- **Default collections:** Tasks, Ideas, Phases, Docs +- **Default collections:** Tasks, Ideas, Plans, Docs - **Templates:** startup (default), scrum, product β€” set via `pad workspace init --template` ## Testing diff --git a/README.md b/README.md index 2566bb79..e0222841 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ @@ -68,7 +68,7 @@ pad server info # How this client is connected to Pad - **Rich text editor** β€” Tiptap-based with markdown, formatting toolbar, and auto-save - **Wiki-links** β€” type `[[Title]]` to link between items - **Real-time updates** β€” agent creates a task in the terminal, it appears in the browser instantly (via SSE) -- **Dashboard** β€” collection overview, active work, phase tracking, activity feed +- **Dashboard** β€” collection overview, active work, plan tracking, activity feed @@ -121,7 +121,7 @@ Pad organizes work into **collections** β€” typed containers with structured fie |---|---| | **Tasks** | Work items with status, priority, assignee, effort, due date | | **Ideas** | Feature ideas with impact and category | -| **Phases** | Project milestones with progress tracking | +| **Plans** | Project milestones with progress tracking | | **Docs** | Documentation, decisions, reference material | | **Conventions** | Project rules that guide agent behavior | | **Playbooks** | Multi-step workflows for agents to follow | @@ -268,7 +268,7 @@ pad project changelog [--days N] Release notes from completed items pad project watch Real-time activity stream pad project reconcile Reconcile item and PR state -pad item create "title" Create item (task, idea, phase, doc, ...) +pad item create "title" Create item (task, idea, plan, doc, ...) pad item list [collection] List items (filters: --status, --priority, --all) pad item show Show item detail pad item update Update item fields diff --git a/cmd/pad/main.go b/cmd/pad/main.go index 16c9ac73..988ed863 100644 --- a/cmd/pad/main.go +++ b/cmd/pad/main.go @@ -1143,7 +1143,7 @@ func printOnboardingHints() { bold.Println("Get started:") fmt.Printf(" %s %s\n", cyan.Sprint("/pad"), "scan this codebase and set up my workspace") fmt.Printf(" %s %s\n", cyan.Sprint("/pad"), "what conventions should this project follow?") - fmt.Printf(" %s %s\n", cyan.Sprint("/pad"), "create a phase for what I'm working on") + fmt.Printf(" %s %s\n", cyan.Sprint("/pad"), "create a plan for what I'm working on") fmt.Println() fmt.Printf("Or open the web UI at %s\n", bold.Sprint("http://localhost:7777")) fmt.Println(dim.Sprint("Run 'pad project dashboard' to see your project dashboard")) @@ -1720,7 +1720,6 @@ func createCmd() *cobra.Command { status string assignee string roleFlag string - phase string category string parentSlug string tags string @@ -1736,7 +1735,7 @@ func createCmd() *cobra.Command { Examples: pad item create task "Fix OAuth redirect" --priority high pad item create idea "Real-time collaboration" --category infrastructure - pad item create phase "API Redesign" --status active + pad item create plan "API Redesign" --status active pad item create doc "Payment Architecture" --category architecture --stdin Run with --help-collections to see available collections and their status values.`, @@ -1757,11 +1756,7 @@ Run with --help-collections to see available collections and their status values if priority != "" { fields["priority"] = priority } - // --parent takes precedence; --phase is a backward-compat alias parentRef := parentSlug - if parentRef == "" { - parentRef = phase - } if parentRef != "" { parentItem, err := client.GetItem(ws, parentRef) if err != nil { @@ -1868,8 +1863,6 @@ Run with --help-collections to see available collections and their status values cmd.Flags().StringVar(&assignee, "assign", "", "assign to user (name or email)") cmd.Flags().StringVar(&roleFlag, "role", "", "assign agent role (slug)") cmd.Flags().StringVar(&parentSlug, "parent", "", "parent item (ref, slug, or ID)") - cmd.Flags().StringVar(&phase, "phase", "", "parent item (deprecated alias for --parent)") - cmd.Flags().Lookup("phase").Hidden = true cmd.Flags().StringVar(&category, "category", "", "category field value") cmd.Flags().StringVar(&tags, "tags", "", "JSON array of tags") cmd.Flags().StringArrayVarP(&fieldFlags, "field", "f", nil, "set arbitrary field (repeatable): --field key=value") @@ -2259,7 +2252,6 @@ func updateCmd() *cobra.Command { priority string assignee string roleFlag string - phase string parentFlag string category string tags string @@ -2277,7 +2269,7 @@ Items can be referenced by issue ID (e.g. TASK-5) or slug. Examples: pad item update TASK-5 --status done pad item update TASK-5 --status done --comment "Fixed the login bug" - pad item update PHASE-2 --status active --priority high + pad item update PLAN-2 --status active --priority high pad item update DOC-3 --stdin < updated-doc.md`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { @@ -2317,11 +2309,7 @@ Examples: } // Merge field changes with existing fields - // --parent takes precedence; --phase is a backward-compat alias parentRef := parentFlag - if parentRef == "" { - parentRef = phase - } hasFieldChanges := status != "" || priority != "" || assignee != "" || parentRef != "" || category != "" || len(fieldFlags) > 0 if hasFieldChanges { @@ -2424,8 +2412,6 @@ Examples: cmd.Flags().StringVar(&assignee, "assign", "", "assign to user (name or email)") cmd.Flags().StringVar(&roleFlag, "role", "", "assign agent role (slug)") cmd.Flags().StringVar(&parentFlag, "parent", "", "update parent item (ref, slug, or ID)") - cmd.Flags().StringVar(&phase, "phase", "", "update parent item (deprecated alias for --parent)") - cmd.Flags().Lookup("phase").Hidden = true cmd.Flags().StringVar(&category, "category", "", "update category field") cmd.Flags().StringVar(&tags, "tags", "", "update tags (JSON array)") cmd.Flags().StringArrayVarP(&fieldFlags, "field", "f", nil, "set arbitrary field (repeatable): --field key=value") @@ -2947,13 +2933,13 @@ func statusCmd() *cobra.Command { Status string `json:"status"` ItemRef string `json:"item_ref"` } `json:"active_items"` - ActivePhases []struct { + ActivePlans []struct { Slug string `json:"slug"` Title string `json:"title"` Progress int `json:"progress"` TaskCount int `json:"task_count"` DoneCount int `json:"done_count"` - } `json:"active_phases"` + } `json:"active_plans"` ByRole []struct { RoleName string `json:"role_name"` RoleSlug string `json:"role_slug"` @@ -3021,17 +3007,17 @@ func statusCmd() *cobra.Command { } } - // Active phases - if len(dash.ActivePhases) > 0 { + // Active plans + if len(dash.ActivePlans) > 0 { fmt.Println() - bold.Println("πŸ—οΈ Active Phases") - for _, p := range dash.ActivePhases { - bar := colorProgressBar(p.Progress, 20, green) + bold.Println("πŸ—οΈ Active Plans") + for _, plan := range dash.ActivePlans { + bar := colorProgressBar(plan.Progress, 20, green) fmt.Printf(" %s %s %s %s\n", - bold.Sprint(p.Title), + bold.Sprint(plan.Title), bar, - color.New(color.FgGreen).Sprintf("%d%%", p.Progress), - dim.Sprintf("(%d/%d tasks)", p.DoneCount, p.TaskCount), + color.New(color.FgGreen).Sprintf("%d%%", plan.Progress), + dim.Sprintf("(%d/%d tasks)", plan.DoneCount, plan.TaskCount), ) } } @@ -3135,7 +3121,7 @@ func nextCmd() *cobra.Command { } if len(dash.SuggestedNext) == 0 { - fmt.Println("No suggestions β€” all tasks may be complete or no active phases found.") + fmt.Println("No suggestions β€” all tasks may be complete or no active plans found.") return nil } @@ -3390,7 +3376,6 @@ func standupCmd() *cobra.Command { func changelogCmd() *cobra.Command { var days int var since string - var phase string var parentRef string cmd := &cobra.Command{ @@ -3432,20 +3417,15 @@ func changelogCmd() *cobra.Command { } } - // Filter by parent if specified (--parent or --phase) + // Filter by parent if specified filterParent := parentRef - if filterParent == "" { - filterParent = phase - } if filterParent != "" { var filtered []models.Item for _, item := range allItems { // Check parent link (populated by API enrichment) if strings.EqualFold(item.ParentLinkID, filterParent) || strings.EqualFold(item.ParentRef, filterParent) || - strings.EqualFold(item.ParentTitle, filterParent) || - strings.EqualFold(item.PhaseID, filterParent) || - strings.EqualFold(item.PhaseRef, filterParent) { + strings.EqualFold(item.ParentTitle, filterParent) { filtered = append(filtered, item) } } @@ -3609,8 +3589,6 @@ func changelogCmd() *cobra.Command { cmd.Flags().IntVar(&days, "days", 7, "show items completed in last N days") cmd.Flags().StringVar(&since, "since", "", "only show items completed after this date (YYYY-MM-DD)") cmd.Flags().StringVar(&parentRef, "parent", "", "only show items under a specific parent (ref, slug, or title)") - cmd.Flags().StringVar(&phase, "phase", "", "only show items under a specific parent (deprecated alias for --parent)") - cmd.Flags().Lookup("phase").Hidden = true return cmd } @@ -3626,8 +3604,8 @@ func collectionDefaultIcon(slug string) string { return "πŸ’‘" case "docs": return "πŸ“„" - case "phases": - return "πŸ“‹" + case "plans": + return "πŸ—ΊοΈ" default: return "β€’" } @@ -3828,7 +3806,7 @@ func completeCollectionNames(cmd *cobra.Command, args []string, toComplete strin return nil, cobra.ShellCompDirectiveNoFileComp } // Static list of common collection names (singular + plural) - names := []string{"task", "tasks", "idea", "ideas", "phase", "phases", "doc", "docs", "bug", "bugs"} + names := []string{"task", "tasks", "idea", "ideas", "plan", "plans", "doc", "docs", "bug", "bugs"} // Try to fetch dynamic collections from API cfg, err := config.Load() if err == nil && cfg.IsConfigured() { @@ -3905,7 +3883,7 @@ func normalizeCollectionSlug(input string) string { aliases := map[string]string{ "task": "tasks", "t": "tasks", "idea": "ideas", "i": "ideas", - "phase": "phases", "p": "phases", + "plan": "plans", "p": "plans", "phase": "plans", "phases": "plans", "doc": "docs", "d": "docs", "bug": "bugs", "convention": "conventions", diff --git a/cmd/pad/query.go b/cmd/pad/query.go index 5dc83d1a..7adcf1cb 100644 --- a/cmd/pad/query.go +++ b/cmd/pad/query.go @@ -36,7 +36,7 @@ func readyCmd() *cobra.Command { This is the broader query-oriented counterpart to 'pad project next'. It reuses the dashboard's suggested-next logic and returns the current actionable backlog -for active phases.`, +for active plans.`, RunE: func(cmd *cobra.Command, args []string) error { client, _ := getClient() ws := getWorkspace() diff --git a/internal/cli/agents.go b/internal/cli/agents.go index 918e76d9..eaf16742 100644 --- a/internal/cli/agents.go +++ b/internal/cli/agents.go @@ -180,7 +180,7 @@ func FormatForTool(tool AgentTool, embeddedContent []byte) []byte { body := StripFrontmatter(embeddedContent) fm := `--- name: pad -description: "Talk to your project. Natural-language project management β€” create items, check status, plan phases, brainstorm ideas, and more." +description: "Talk to your project. Natural-language project management β€” create items, check status, create plans, brainstorm ideas, and more." --- ` diff --git a/internal/cli/format.go b/internal/cli/format.go index 0c063873..ad8a9a1e 100644 --- a/internal/cli/format.go +++ b/internal/cli/format.go @@ -251,13 +251,9 @@ func PrintItemMeta(item *models.Item) { fmt.Printf("%s %s\n", label.Sprint("Collection:"), collLabel) } // Parent link - if item.ParentRef != "" || item.PhaseRef != "" { + if item.ParentRef != "" { ref := item.ParentRef title := item.ParentTitle - if ref == "" { - ref = item.PhaseRef - title = item.PhaseTitle - } parentStr := ref if title != "" { parentStr = ref + " " + title diff --git a/internal/collections/convention_library.go b/internal/collections/convention_library.go index c506d728..0192226e 100644 --- a/internal/collections/convention_library.go +++ b/internal/collections/convention_library.go @@ -6,7 +6,7 @@ type LibraryConvention struct { Title string `json:"title"` Content string `json:"content"` Category string `json:"category"` // git, quality, pm, docs, build - Trigger string `json:"trigger"` // always, on-task-start, on-task-complete, on-implement, on-commit, on-pr-create, on-phase-complete, on-plan + Trigger string `json:"trigger"` // always, on-task-start, on-task-complete, on-implement, on-commit, on-pr-create, on-plan-complete, on-plan Surfaces []string `json:"surfaces"` // all, backend, frontend, mobile, docs, devops Enforcement string `json:"enforcement"` // must, should, nice-to-have Commands []string `json:"commands,omitempty"` @@ -132,10 +132,10 @@ func ConventionLibrary() []LibraryCategory { Enforcement: "should", }, { - Title: "Retrospective on phase completion", - Content: "When all tasks in a phase are done, suggest running a retrospective before marking the phase complete. Capture: what shipped, what was deferred, and lessons learned.", + Title: "Retrospective on plan completion", + Content: "When all tasks in a plan are done, suggest running a retrospective before marking the plan complete. Capture: what shipped, what was deferred, and lessons learned.", Category: "pm", - Trigger: "on-phase-complete", + Trigger: "on-plan-complete", Surfaces: []string{"all"}, Enforcement: "nice-to-have", }, diff --git a/internal/collections/defaults.go b/internal/collections/defaults.go index 82c40e0b..f2055123 100644 --- a/internal/collections/defaults.go +++ b/internal/collections/defaults.go @@ -112,10 +112,10 @@ func Defaults() []DefaultCollection { }, }, { - Name: "Phases", - Slug: "phases", - Icon: "πŸ—οΈ", - Description: "Plan and track project phases and milestones", + Name: "Plans", + Slug: "plans", + Icon: "πŸ—ΊοΈ", + Description: "Plan and track project plans and milestones", SortOrder: 2, Schema: models.CollectionSchema{ Fields: []models.FieldDef{ @@ -152,10 +152,10 @@ func Defaults() []DefaultCollection { DefaultView: "list", ListSortBy: "sort_order", QuickActions: []models.QuickAction{ - {Label: "Plan this phase", Prompt: "/pad plan {ref} \"{title}\" β€” outline goals, deliverables, and timeline", Scope: "item", Icon: "πŸ“"}, + {Label: "Plan this", Prompt: "/pad plan {ref} \"{title}\" β€” outline goals, deliverables, and timeline", Scope: "item", Icon: "πŸ“"}, {Label: "Break into tasks", Prompt: "/pad break {ref} \"{title}\" into PR-sized tasks", Scope: "item", Icon: "πŸ“"}, {Label: "Run a retro", Prompt: "/pad run a retrospective on {ref} \"{title}\"", Scope: "item", Icon: "πŸ”„"}, - {Label: "Compare progress", Prompt: "/pad compare progress across all phases", Scope: "collection", Icon: "πŸ“Š"}, + {Label: "Compare progress", Prompt: "/pad compare progress across all plans", Scope: "collection", Icon: "πŸ“Š"}, }, }, }, @@ -217,7 +217,7 @@ func Defaults() []DefaultCollection { Key: "trigger", Label: "When", Type: "select", - Options: []string{"always", "on-task-start", "on-task-complete", "on-implement", "on-commit", "on-pr-create", "on-phase-start", "on-phase-complete", "on-plan"}, + Options: []string{"always", "on-task-start", "on-task-complete", "on-implement", "on-commit", "on-pr-create", "on-plan-start", "on-plan-complete", "on-plan"}, }, { Key: "scope", diff --git a/internal/collections/playbook_library.go b/internal/collections/playbook_library.go index 962cabb5..cdf2fbc0 100644 --- a/internal/collections/playbook_library.go +++ b/internal/collections/playbook_library.go @@ -61,18 +61,18 @@ func PlaybookLibrary() []PlaybookCategory { Description: "Planning and triage workflows", Playbooks: []LibraryPlaybook{ { - Title: "Phase Planning", + Title: "Plan Creation", Category: "planning", Trigger: "on-plan", Scope: "all", - Content: `1. Review current state β€” check the roadmap, active phases, and recent progress -2. Define the goal β€” what does success look like for this phase? + Content: `1. Review current state β€” check the roadmap, active plans, and recent progress +2. Define the goal β€” what does success look like for this plan? 3. Identify the work β€” list everything that needs to happen 4. Break into tasks β€” each task should be independently completable (one branch, one PR) 5. Estimate effort β€” flag tasks that seem too large and split them 6. Order by dependency β€” what must happen before what? -7. Set targets β€” define when the phase should start and end -8. Create the items β€” build the phase and its tasks in the project tracker +7. Set targets β€” define when the plan should start and end +8. Create the items β€” build the plan and its tasks in the project tracker ` + "\U0001F4A1 Ask your AI agent to customize this playbook for your specific project tools and workflow.", }, @@ -86,7 +86,7 @@ func PlaybookLibrary() []PlaybookCategory { 3. Determine severity β€” critical (broken for everyone), high (significant impact), medium (inconvenient), low (cosmetic) 4. Check for duplicates β€” search existing tasks for similar reports 5. Capture the details β€” create a task with: steps to reproduce, expected vs actual behavior, severity -6. Link related items β€” connect to relevant phases, architecture docs, or prior work +6. Link related items β€” connect to relevant plans, architecture docs, or prior work 7. Prioritize β€” decide if it needs immediate attention or can be scheduled ` + "\U0001F4A1 Ask your AI agent to customize this playbook for your specific project tools and workflow.", @@ -102,12 +102,12 @@ func PlaybookLibrary() []PlaybookCategory { Category: "quality", Trigger: "manual", Scope: "all", - Content: `1. Gather the data β€” load the completed phase and all its tasks + Content: `1. Gather the data β€” load the completed plan and all its tasks 2. What shipped β€” list everything that was completed 3. What was deferred β€” list anything that was planned but postponed 4. What went well β€” identify practices, tools, or decisions that helped 5. What could improve β€” identify friction, surprises, or mistakes -6. Action items β€” concrete changes for the next phase +6. Action items β€” concrete changes for the next plan 7. Save and share β€” document the retrospective for future reference ` + "\U0001F4A1 Ask your AI agent to customize this playbook for your specific project tools and workflow.", @@ -119,7 +119,7 @@ func PlaybookLibrary() []PlaybookCategory { Scope: "all", Content: `1. Read the architecture β€” understand the tech stack, structure, and key patterns 2. Read the roadmap β€” understand where the project is and where it's going -3. Review active work β€” check current phases, in-progress tasks, and recent activity +3. Review active work β€” check current plans, in-progress tasks, and recent activity 4. Set up the environment β€” get the project building and running locally 5. Read the conventions β€” understand the team's rules and expectations 6. Pick a starter task β€” choose something small to build familiarity diff --git a/internal/collections/templates.go b/internal/collections/templates.go index 68ba0c38..29284c25 100644 --- a/internal/collections/templates.go +++ b/internal/collections/templates.go @@ -77,7 +77,7 @@ func conventionsCollection(sortOrder int) DefaultCollection { Key: "trigger", Label: "When", Type: "select", - Options: []string{"always", "on-task-start", "on-task-complete", "on-implement", "on-commit", "on-pr-create", "on-phase-start", "on-phase-complete", "on-plan"}, + Options: []string{"always", "on-task-start", "on-task-complete", "on-implement", "on-commit", "on-pr-create", "on-plan-start", "on-plan-complete", "on-plan"}, }, { Key: "scope", @@ -153,7 +153,7 @@ func playbooksCollection(sortOrder int) DefaultCollection { var templates = []WorkspaceTemplate{ { Name: "startup", - Description: "Tasks, Ideas, Phases, Docs, Conventions, Playbooks", + Description: "Tasks, Ideas, Plans, Docs, Conventions, Playbooks", Collections: Defaults(), }, { @@ -423,9 +423,9 @@ var templates = []WorkspaceTemplate{ func demoSeedItems() []SeedItem { return []SeedItem{ - // Phase + // Plan { - CollectionSlug: "phases", + CollectionSlug: "plans", Title: "MVP Launch", Content: `# MVP Launch @@ -529,7 +529,7 @@ Pad is a single Go binary with an embedded SvelteKit web UI and SQLite storage. ## Related -See [[MVP Launch]] for the current phase and [[Write API documentation]] for the API docs effort. +See [[MVP Launch]] for the current plan and [[Write API documentation]] for the API docs effort. `, Fields: `{"status":"published","category":"architecture"}`, }, diff --git a/internal/models/document.go b/internal/models/document.go index 7f17f34e..6ddfdabc 100644 --- a/internal/models/document.go +++ b/internal/models/document.go @@ -4,7 +4,7 @@ import "time" // Valid document types var ValidDocTypes = []string{ - "roadmap", "phase-plan", "architecture", "ideation", + "roadmap", "plan", "architecture", "ideation", "feature-spec", "notes", "prompt-library", "reference", } diff --git a/internal/models/item.go b/internal/models/item.go index 2af141ad..0f82b370 100644 --- a/internal/models/item.go +++ b/internal/models/item.go @@ -57,11 +57,6 @@ type Item struct { ParentRef string `json:"parent_ref,omitempty"` ParentTitle string `json:"parent_title,omitempty"` - // Deprecated aliases β€” kept for API backward compatibility - PhaseID string `json:"phase_id,omitempty"` - PhaseRef string `json:"phase_ref,omitempty"` - PhaseTitle string `json:"phase_title,omitempty"` - // HasChildren is true if this item has child items linked to it. // Populated by enrichment, not stored in the DB. HasChildren bool `json:"has_children,omitempty"` @@ -520,7 +515,6 @@ type ItemListParams struct { AssignedUserID string // filter by assigned user AgentRoleID string // filter by agent role (ID or slug) ParentLinkID string // filter by parent link (item ID of the parent) - PhaseID string // deprecated alias for ParentLinkID IncludeArchived bool Limit int Offset int diff --git a/internal/models/item_links.go b/internal/models/item_links.go index b3c0c70f..f6818cc0 100644 --- a/internal/models/item_links.go +++ b/internal/models/item_links.go @@ -13,7 +13,6 @@ const ( ItemLinkTypeSupersedes = "supersedes" ItemLinkTypeImplements = "implements" ItemLinkTypeParent = "parent" - ItemLinkTypePhase = "parent" // Deprecated alias β€” use ItemLinkTypeParent ) var itemLinkTypeAliases = map[string]string{ @@ -27,7 +26,6 @@ var itemLinkTypeAliases = map[string]string{ "supersedes": ItemLinkTypeSupersedes, "implements": ItemLinkTypeImplements, "parent": ItemLinkTypeParent, - "phase": ItemLinkTypeParent, // backward compat } // NormalizeItemLinkType canonicalizes supported link types and returns an error diff --git a/internal/models/snapshot.go b/internal/models/snapshot.go index 8aa5f7fe..de21ec0a 100644 --- a/internal/models/snapshot.go +++ b/internal/models/snapshot.go @@ -12,12 +12,12 @@ type ProgressSnapshot struct { OpenTasks int `json:"open_tasks"` InProgress int `json:"in_progress"` Percentage float64 `json:"percentage"` - PhaseData string `json:"phase_data"` // JSON array of per-phase snapshots + PlanData string `json:"phase_data"` // JSON array of per-plan snapshots (legacy DB column name: phase_data) CreatedAt time.Time `json:"created_at"` } -// PhaseSnapshot is a single entry in the PhaseData JSON array. -type PhaseSnapshot struct { +// PlanSnapshot is a single entry in the PlanData JSON array. +type PlanSnapshot struct { Title string `json:"title"` Done int `json:"done"` Total int `json:"total"` diff --git a/internal/models/templates.go b/internal/models/templates.go index 17c165a7..f8e84401 100644 --- a/internal/models/templates.go +++ b/internal/models/templates.go @@ -66,19 +66,19 @@ How do we know when we're done? `, }, { - Type: "phase-plan", - Name: "Phase Plan", - Description: "Scoped implementation plan for a specific phase or milestone", + Type: "plan", + Name: "Plan", + Description: "Scoped implementation plan for a specific milestone", Icon: "\U0001F3D7\uFE0F", Content: `# {Title} ## Overview -What does this phase accomplish? What's the scope? +What does this plan accomplish? What's the scope? ## Prerequisites -What needs to be done before this phase can start? +What needs to be done before this plan can start? - [ ] Prerequisite 1 - [ ] Prerequisite 2 diff --git a/internal/server/handlers_dashboard.go b/internal/server/handlers_dashboard.go index c8e3e26a..01c18e48 100644 --- a/internal/server/handlers_dashboard.go +++ b/internal/server/handlers_dashboard.go @@ -17,7 +17,7 @@ import ( type DashboardResponse struct { Summary DashboardSummary `json:"summary"` ActiveItems []DashboardActiveItem `json:"active_items"` - ActivePhases []DashboardPhase `json:"active_phases"` + ActivePlans []DashboardPlan `json:"active_plans"` ByRole []store.RoleBreakdown `json:"by_role,omitempty"` Attention []DashboardAttention `json:"attention"` RecentActivity []DashboardActivity `json:"recent_activity"` @@ -52,7 +52,7 @@ type DashboardSummary struct { ByCollection map[string]map[string]int `json:"by_collection"` } -type DashboardPhase struct { +type DashboardPlan struct { Slug string `json:"slug"` Ref string `json:"ref,omitempty"` Title string `json:"title"` @@ -147,7 +147,7 @@ func (s *Server) handleGetDashboard(w http.ResponseWriter, r *http.Request) { ByCollection: make(map[string]map[string]int), }, ActiveItems: []DashboardActiveItem{}, - ActivePhases: []DashboardPhase{}, + ActivePlans: []DashboardPlan{}, Attention: []DashboardAttention{}, RecentActivity: []DashboardActivity{}, SuggestedNext: []DashboardSuggestion{}, @@ -180,8 +180,8 @@ func (s *Server) handleGetDashboard(w http.ResponseWriter, r *http.Request) { if !isActiveStatus(status) { continue } - // Skip phases (they have their own section) - if item.CollectionSlug == "phases" { + // Skip plans (they have their own section) + if item.CollectionSlug == "plans" { continue } ai := DashboardActiveItem{ @@ -212,28 +212,28 @@ func (s *Server) handleGetDashboard(w http.ResponseWriter, r *http.Request) { resp.ActiveItems = resp.ActiveItems[:10] } - // Active phases: items in "phases" collection where status=active - phases, err := s.store.ListItems(workspaceID, models.ItemListParams{ - CollectionSlug: "phases", + // Active plans: items in "plans" collection where status=active + plans, err := s.store.ListItems(workspaceID, models.ItemListParams{ + CollectionSlug: "plans", Fields: map[string]string{"status": "active"}, }) if err == nil { - for _, phase := range phases { - dp := DashboardPhase{ - Slug: phase.Slug, - Ref: phase.Ref, - Title: phase.Title, + for _, plan := range plans { + dp := DashboardPlan{ + Slug: plan.Slug, + Ref: plan.Ref, + Title: plan.Title, } // Compute progress from child items linked via parent link - total, done, err := s.store.GetItemProgress(phase.ID) + total, done, err := s.store.GetItemProgress(plan.ID) if err == nil && total > 0 { dp.TaskCount = total dp.DoneCount = done dp.Progress = (done * 100) / total } else { - // Fallback: use explicit progress field on the phase - progress := extractFieldValue(phase.Fields, "progress") + // Fallback: use explicit progress field on the plan + progress := extractFieldValue(plan.Fields, "progress") if progress != "" { var pval float64 if err := json.Unmarshal([]byte(progress), &pval); err == nil { @@ -242,7 +242,7 @@ func (s *Server) handleGetDashboard(w http.ResponseWriter, r *http.Request) { } } - resp.ActivePhases = append(resp.ActivePhases, dp) + resp.ActivePlans = append(resp.ActivePlans, dp) } } @@ -301,23 +301,23 @@ func (s *Server) handleGetDashboard(w http.ResponseWriter, r *http.Request) { } } - // (c) Phase completion: phases where ALL child items are done - for _, dp := range resp.ActivePhases { + // (c) Plan completion: plans where ALL child items are done + for _, dp := range resp.ActivePlans { if dp.TaskCount > 0 && dp.DoneCount == dp.TaskCount { resp.Attention = append(resp.Attention, DashboardAttention{ - Type: "phase_completion", + Type: "plan_completion", ItemSlug: dp.Slug, ItemTitle: dp.Title, - Collection: "phases", + Collection: "plans", Reason: "All " + strconv.Itoa(dp.TaskCount) + " tasks are done. Mark as completed?", }) } } // (d) Orphaned tasks: tasks with no parent link set - // Only flag these if the workspace has active phases with children linked to them. + // Only flag these if the workspace has active plans with children linked to them. hasParentWithChildren := false - for _, dp := range resp.ActivePhases { + for _, dp := range resp.ActivePlans { if dp.TaskCount > 0 { hasParentWithChildren = true break @@ -345,7 +345,7 @@ func (s *Server) handleGetDashboard(w http.ResponseWriter, r *http.Request) { ItemRef: task.Ref, ItemTitle: task.Title, Collection: "tasks", - Reason: "Task has no phase assigned", + Reason: "Task has no plan assigned", }) } } @@ -419,15 +419,15 @@ func (s *Server) handleGetDashboard(w http.ResponseWriter, r *http.Request) { } // --- Suggested Next --- - // Find open child items in active phases, sorted by priority, top 3 + // Find open child items in active plans, sorted by priority, top 3 type suggestion struct { item models.Item - phase string + plan string priority int } var candidates []suggestion - for _, dp := range resp.ActivePhases { + for _, dp := range resp.ActivePlans { parentItem, err := s.store.ResolveItem(workspaceID, dp.Slug) if err != nil || parentItem == nil { continue @@ -442,7 +442,7 @@ func (s *Server) handleGetDashboard(w http.ResponseWriter, r *http.Request) { pri := extractFieldValue(task.Fields, "priority") candidates = append(candidates, suggestion{ item: task, - phase: dp.Title, + plan: dp.Title, priority: priorityRank(pri), }) } @@ -461,7 +461,7 @@ func (s *Server) handleGetDashboard(w http.ResponseWriter, r *http.Request) { } for _, c := range candidates[:limit] { pri := extractFieldValue(c.item.Fields, "priority") - reason := "Open task in active phase \"" + c.phase + "\"" + reason := "Open task in active plan \"" + c.plan + "\"" if pri != "" { reason += " (" + pri + " priority)" } diff --git a/internal/server/handlers_dashboard_test.go b/internal/server/handlers_dashboard_test.go index fca5ab92..7afac891 100644 --- a/internal/server/handlers_dashboard_test.go +++ b/internal/server/handlers_dashboard_test.go @@ -74,8 +74,8 @@ func TestDashboardEmpty(t *testing.T) { if len(resp.ActiveItems) != 0 { t.Errorf("expected 0 active_items, got %d", len(resp.ActiveItems)) } - if len(resp.ActivePhases) != 0 { - t.Errorf("expected 0 active_phases, got %d", len(resp.ActivePhases)) + if len(resp.ActivePlans) != 0 { + t.Errorf("expected 0 active_plans, got %d", len(resp.ActivePlans)) } if len(resp.Attention) != 0 { t.Errorf("expected 0 attention items, got %d", len(resp.Attention)) @@ -93,7 +93,7 @@ func TestDashboardEmpty(t *testing.T) { if err := json.Unmarshal(raw.Body.Bytes(), &rawMap); err != nil { t.Fatalf("failed to parse raw JSON: %v", err) } - for _, key := range []string{"active_items", "active_phases", "attention", "recent_activity", "suggested_next"} { + for _, key := range []string{"active_items", "active_plans", "attention", "recent_activity", "suggested_next"} { val := string(rawMap[key]) if val == "null" { t.Errorf("expected %s to be [], got null", key) @@ -285,14 +285,14 @@ func TestDashboardActiveItemsSorting(t *testing.T) { } } -func TestDashboardActiveItemsExcludesPhases(t *testing.T) { +func TestDashboardActiveItemsExcludesPlans(t *testing.T) { srv := testServer(t) slug := createWSWithCollections(t, srv) - // Create an active phase β€” it should NOT appear in active_items - // (phases have their own section) - createItem(t, srv, slug, "phases", map[string]interface{}{ - "title": "Phase 1", + // Create an active plan β€” it should NOT appear in active_items + // (plans have their own section) + createItem(t, srv, slug, "plans", map[string]interface{}{ + "title": "Plan 1", "fields": `{"status":"active"}`, }) @@ -305,55 +305,55 @@ func TestDashboardActiveItemsExcludesPhases(t *testing.T) { resp := getDashboard(t, srv, slug) if len(resp.ActiveItems) != 1 { - t.Fatalf("expected 1 active_item (no phases), got %d", len(resp.ActiveItems)) + t.Fatalf("expected 1 active_item (no plans), got %d", len(resp.ActiveItems)) } if resp.ActiveItems[0].Title != "Active Task" { t.Errorf("expected active item to be 'Active Task', got %q", resp.ActiveItems[0].Title) } } -func TestDashboardActivePhases(t *testing.T) { +func TestDashboardActivePlans(t *testing.T) { srv := testServer(t) slug := createWSWithCollections(t, srv) - // Create a phase with status=active - phase := createItem(t, srv, slug, "phases", map[string]interface{}{ + // Create a plan with status=active + plan := createItem(t, srv, slug, "plans", map[string]interface{}{ "title": "Sprint 1", "fields": `{"status":"active"}`, }) - // Create tasks linked to the phase + // Create tasks linked to the plan createItem(t, srv, slug, "tasks", map[string]interface{}{ "title": "Task A", - "fields": `{"status":"open","priority":"high","phase":"` + phase.ID + `"}`, + "fields": `{"status":"open","priority":"high","parent":"` + plan.ID + `"}`, }) createItem(t, srv, slug, "tasks", map[string]interface{}{ "title": "Task B", - "fields": `{"status":"done","priority":"medium","phase":"` + phase.ID + `"}`, + "fields": `{"status":"done","priority":"medium","parent":"` + plan.ID + `"}`, }) createItem(t, srv, slug, "tasks", map[string]interface{}{ "title": "Task C", - "fields": `{"status":"done","priority":"low","phase":"` + phase.ID + `"}`, + "fields": `{"status":"done","priority":"low","parent":"` + plan.ID + `"}`, }) - // Also a planned phase that should NOT appear in active_phases - createItem(t, srv, slug, "phases", map[string]interface{}{ + // Also a planned plan that should NOT appear in active_plans + createItem(t, srv, slug, "plans", map[string]interface{}{ "title": "Sprint 2", "fields": `{"status":"planned"}`, }) resp := getDashboard(t, srv, slug) - if len(resp.ActivePhases) != 1 { - t.Fatalf("expected 1 active_phase, got %d", len(resp.ActivePhases)) + if len(resp.ActivePlans) != 1 { + t.Fatalf("expected 1 active_plan, got %d", len(resp.ActivePlans)) } - ap := resp.ActivePhases[0] + ap := resp.ActivePlans[0] if ap.Title != "Sprint 1" { - t.Errorf("expected phase title 'Sprint 1', got %q", ap.Title) + t.Errorf("expected plan title 'Sprint 1', got %q", ap.Title) } if ap.Slug == "" { - t.Error("expected phase slug to be set") + t.Error("expected plan slug to be set") } if ap.TaskCount != 3 { t.Errorf("expected task_count=3, got %d", ap.TaskCount) @@ -412,9 +412,9 @@ func TestDashboardAttentionOverdueEndDate(t *testing.T) { srv := testServer(t) slug := createWSWithCollections(t, srv) - // Create a phase with a past end_date (not done) - createItem(t, srv, slug, "phases", map[string]interface{}{ - "title": "Overdue Phase", + // Create a plan with a past end_date (not done) + createItem(t, srv, slug, "plans", map[string]interface{}{ + "title": "Overdue Plan", "fields": `{"status":"active","end_date":"2020-06-15"}`, }) @@ -425,8 +425,8 @@ func TestDashboardAttentionOverdueEndDate(t *testing.T) { t.Fatalf("expected 1 overdue attention item for end_date, got %d: %+v", len(overdueItems), overdueItems) } - if overdueItems[0].ItemTitle != "Overdue Phase" { - t.Errorf("expected 'Overdue Phase', got %q", overdueItems[0].ItemTitle) + if overdueItems[0].ItemTitle != "Overdue Plan" { + t.Errorf("expected 'Overdue Plan', got %q", overdueItems[0].ItemTitle) } } @@ -520,34 +520,34 @@ func TestDashboardSuggestedNext(t *testing.T) { srv := testServer(t) slug := createWSWithCollections(t, srv) - // Create an active phase - phase := createItem(t, srv, slug, "phases", map[string]interface{}{ - "title": "Active Phase", + // Create an active plan + plan := createItem(t, srv, slug, "plans", map[string]interface{}{ + "title": "Active Plan", "fields": `{"status":"active"}`, }) - // Create open tasks in the phase with different priorities + // Create open tasks in the plan with different priorities createItem(t, srv, slug, "tasks", map[string]interface{}{ "title": "Low Priority Task", - "fields": `{"status":"open","priority":"low","phase":"` + phase.ID + `"}`, + "fields": `{"status":"open","priority":"low","parent":"` + plan.ID + `"}`, }) createItem(t, srv, slug, "tasks", map[string]interface{}{ "title": "Critical Priority Task", - "fields": `{"status":"open","priority":"critical","phase":"` + phase.ID + `"}`, + "fields": `{"status":"open","priority":"critical","parent":"` + plan.ID + `"}`, }) createItem(t, srv, slug, "tasks", map[string]interface{}{ "title": "High Priority Task", - "fields": `{"status":"open","priority":"high","phase":"` + phase.ID + `"}`, + "fields": `{"status":"open","priority":"high","parent":"` + plan.ID + `"}`, }) createItem(t, srv, slug, "tasks", map[string]interface{}{ "title": "Medium Priority Task", - "fields": `{"status":"open","priority":"medium","phase":"` + phase.ID + `"}`, + "fields": `{"status":"open","priority":"medium","parent":"` + plan.ID + `"}`, }) // This one is in-progress β€” not "open", so NOT suggested createItem(t, srv, slug, "tasks", map[string]interface{}{ "title": "Already In Progress", - "fields": `{"status":"in-progress","priority":"critical","phase":"` + phase.ID + `"}`, + "fields": `{"status":"in-progress","priority":"critical","parent":"` + plan.ID + `"}`, }) resp := getDashboard(t, srv, slug) @@ -578,11 +578,11 @@ func TestDashboardSuggestedNext(t *testing.T) { } } -func TestDashboardSuggestedNextNoPhases(t *testing.T) { +func TestDashboardSuggestedNextNoPlans(t *testing.T) { srv := testServer(t) slug := createWSWithCollections(t, srv) - // Create tasks but no active phases + // Create tasks but no active plans createItem(t, srv, slug, "tasks", map[string]interface{}{ "title": "Orphan Task", "fields": `{"status":"open","priority":"high"}`, @@ -591,29 +591,29 @@ func TestDashboardSuggestedNextNoPhases(t *testing.T) { resp := getDashboard(t, srv, slug) if len(resp.SuggestedNext) != 0 { - t.Errorf("expected 0 suggested_next without active phases, got %d", len(resp.SuggestedNext)) + t.Errorf("expected 0 suggested_next without active plans, got %d", len(resp.SuggestedNext)) } } -func TestDashboardSuggestedNextFromPlannedPhase(t *testing.T) { +func TestDashboardSuggestedNextFromPlannedPlan(t *testing.T) { srv := testServer(t) slug := createWSWithCollections(t, srv) - // Planned phase β€” should NOT contribute to suggested_next - phase := createItem(t, srv, slug, "phases", map[string]interface{}{ - "title": "Planned Phase", + // Planned plan β€” should NOT contribute to suggested_next + plan := createItem(t, srv, slug, "plans", map[string]interface{}{ + "title": "Planned Plan", "fields": `{"status":"planned"}`, }) createItem(t, srv, slug, "tasks", map[string]interface{}{ - "title": "Task in Planned Phase", - "fields": `{"status":"open","priority":"high","phase":"` + phase.ID + `"}`, + "title": "Task in Planned Plan", + "fields": `{"status":"open","priority":"high","parent":"` + plan.ID + `"}`, }) resp := getDashboard(t, srv, slug) if len(resp.SuggestedNext) != 0 { - t.Errorf("expected 0 suggested_next from planned phase, got %d", len(resp.SuggestedNext)) + t.Errorf("expected 0 suggested_next from planned plan, got %d", len(resp.SuggestedNext)) } } @@ -657,12 +657,12 @@ func TestDashboardIsDoneStatus(t *testing.T) { } } -func TestDashboardPhaseCompletion(t *testing.T) { +func TestDashboardPlanCompletion(t *testing.T) { srv := testServer(t) slug := createWSWithCollections(t, srv) - // Create active phase - phase := createItem(t, srv, slug, "phases", map[string]interface{}{ + // Create active plan + plan := createItem(t, srv, slug, "plans", map[string]interface{}{ "title": "Completed Sprint", "fields": `{"status":"active"}`, }) @@ -670,52 +670,52 @@ func TestDashboardPhaseCompletion(t *testing.T) { // All tasks done createItem(t, srv, slug, "tasks", map[string]interface{}{ "title": "Done Task 1", - "fields": `{"status":"done","priority":"high","phase":"` + phase.ID + `"}`, + "fields": `{"status":"done","priority":"high","parent":"` + plan.ID + `"}`, }) createItem(t, srv, slug, "tasks", map[string]interface{}{ "title": "Done Task 2", - "fields": `{"status":"done","priority":"medium","phase":"` + phase.ID + `"}`, + "fields": `{"status":"done","priority":"medium","parent":"` + plan.ID + `"}`, }) resp := getDashboard(t, srv, slug) - phaseCompletions := filterAttention(resp.Attention, "phase_completion") - if len(phaseCompletions) != 1 { - t.Fatalf("expected 1 phase_completion attention, got %d: %+v", len(phaseCompletions), phaseCompletions) + planCompletions := filterAttention(resp.Attention, "plan_completion") + if len(planCompletions) != 1 { + t.Fatalf("expected 1 plan_completion attention, got %d: %+v", len(planCompletions), planCompletions) } - if phaseCompletions[0].ItemTitle != "Completed Sprint" { - t.Errorf("expected 'Completed Sprint', got %q", phaseCompletions[0].ItemTitle) + if planCompletions[0].ItemTitle != "Completed Sprint" { + t.Errorf("expected 'Completed Sprint', got %q", planCompletions[0].ItemTitle) } - if phaseCompletions[0].Collection != "phases" { - t.Errorf("expected collection 'phases', got %q", phaseCompletions[0].Collection) + if planCompletions[0].Collection != "plans" { + t.Errorf("expected collection 'plans', got %q", planCompletions[0].Collection) } } -func TestDashboardPhaseCompletionNotTriggeredWithOpenTasks(t *testing.T) { +func TestDashboardPlanCompletionNotTriggeredWithOpenTasks(t *testing.T) { srv := testServer(t) slug := createWSWithCollections(t, srv) - // Active phase with one done and one open task - phase := createItem(t, srv, slug, "phases", map[string]interface{}{ + // Active plan with one done and one open task + plan := createItem(t, srv, slug, "plans", map[string]interface{}{ "title": "In Progress Sprint", "fields": `{"status":"active"}`, }) createItem(t, srv, slug, "tasks", map[string]interface{}{ "title": "Done Task", - "fields": `{"status":"done","priority":"high","phase":"` + phase.ID + `"}`, + "fields": `{"status":"done","priority":"high","parent":"` + plan.ID + `"}`, }) createItem(t, srv, slug, "tasks", map[string]interface{}{ "title": "Open Task", - "fields": `{"status":"open","priority":"high","phase":"` + phase.ID + `"}`, + "fields": `{"status":"open","priority":"high","parent":"` + plan.ID + `"}`, }) resp := getDashboard(t, srv, slug) - phaseCompletions := filterAttention(resp.Attention, "phase_completion") - if len(phaseCompletions) != 0 { - t.Errorf("expected 0 phase_completion when tasks are still open, got %d", len(phaseCompletions)) + planCompletions := filterAttention(resp.Attention, "plan_completion") + if len(planCompletions) != 0 { + t.Errorf("expected 0 plan_completion when tasks are still open, got %d", len(planCompletions)) } } @@ -778,67 +778,67 @@ func TestDashboardNonexistentWorkspace(t *testing.T) { } } -func TestDashboardMultipleActivePhases(t *testing.T) { +func TestDashboardMultipleActivePlans(t *testing.T) { srv := testServer(t) slug := createWSWithCollections(t, srv) - // Create two active phases - phase1 := createItem(t, srv, slug, "phases", map[string]interface{}{ - "title": "Phase Alpha", + // Create two active plans + plan1 := createItem(t, srv, slug, "plans", map[string]interface{}{ + "title": "Plan Alpha", "fields": `{"status":"active"}`, }) - phase2 := createItem(t, srv, slug, "phases", map[string]interface{}{ - "title": "Phase Beta", + plan2 := createItem(t, srv, slug, "plans", map[string]interface{}{ + "title": "Plan Beta", "fields": `{"status":"active"}`, }) - // Tasks for phase 1: 1 done out of 2 + // Tasks for plan 1: 1 done out of 2 createItem(t, srv, slug, "tasks", map[string]interface{}{ "title": "Alpha Task 1", - "fields": `{"status":"done","priority":"high","phase":"` + phase1.ID + `"}`, + "fields": `{"status":"done","priority":"high","parent":"` + plan1.ID + `"}`, }) createItem(t, srv, slug, "tasks", map[string]interface{}{ "title": "Alpha Task 2", - "fields": `{"status":"open","priority":"medium","phase":"` + phase1.ID + `"}`, + "fields": `{"status":"open","priority":"medium","parent":"` + plan1.ID + `"}`, }) - // Tasks for phase 2: 0 done out of 1 + // Tasks for plan 2: 0 done out of 1 createItem(t, srv, slug, "tasks", map[string]interface{}{ "title": "Beta Task 1", - "fields": `{"status":"open","priority":"high","phase":"` + phase2.ID + `"}`, + "fields": `{"status":"open","priority":"high","parent":"` + plan2.ID + `"}`, }) resp := getDashboard(t, srv, slug) - if len(resp.ActivePhases) != 2 { - t.Fatalf("expected 2 active phases, got %d", len(resp.ActivePhases)) + if len(resp.ActivePlans) != 2 { + t.Fatalf("expected 2 active plans, got %d", len(resp.ActivePlans)) } - phaseMap := map[string]DashboardPhase{} - for _, p := range resp.ActivePhases { - phaseMap[p.Title] = p + planMap := map[string]DashboardPlan{} + for _, p := range resp.ActivePlans { + planMap[p.Title] = p } - alpha, ok := phaseMap["Phase Alpha"] + alpha, ok := planMap["Plan Alpha"] if !ok { - t.Fatal("expected 'Phase Alpha' in active phases") + t.Fatal("expected 'Plan Alpha' in active plans") } if alpha.TaskCount != 2 || alpha.DoneCount != 1 { - t.Errorf("Phase Alpha: expected task_count=2, done_count=1, got task_count=%d, done_count=%d", alpha.TaskCount, alpha.DoneCount) + t.Errorf("Plan Alpha: expected task_count=2, done_count=1, got task_count=%d, done_count=%d", alpha.TaskCount, alpha.DoneCount) } if alpha.Progress != 50 { - t.Errorf("Phase Alpha: expected progress=50, got %d", alpha.Progress) + t.Errorf("Plan Alpha: expected progress=50, got %d", alpha.Progress) } - beta, ok := phaseMap["Phase Beta"] + beta, ok := planMap["Plan Beta"] if !ok { - t.Fatal("expected 'Phase Beta' in active phases") + t.Fatal("expected 'Plan Beta' in active plans") } if beta.TaskCount != 1 || beta.DoneCount != 0 { - t.Errorf("Phase Beta: expected task_count=1, done_count=0, got task_count=%d, done_count=%d", beta.TaskCount, beta.DoneCount) + t.Errorf("Plan Beta: expected task_count=1, done_count=0, got task_count=%d, done_count=%d", beta.TaskCount, beta.DoneCount) } if beta.Progress != 0 { - t.Errorf("Phase Beta: expected progress=0, got %d", beta.Progress) + t.Errorf("Plan Beta: expected progress=0, got %d", beta.Progress) } } @@ -865,23 +865,23 @@ func TestDashboardOrphanedTasks(t *testing.T) { srv := testServer(t) slug := createWSWithCollections(t, srv) - // Create an active phase WITH at least one linked task (activates orphan detection) - phase := createItem(t, srv, slug, "phases", map[string]interface{}{ + // Create an active plan WITH at least one linked task (activates orphan detection) + plan := createItem(t, srv, slug, "plans", map[string]interface{}{ "title": "Sprint 1", "fields": `{"status":"active"}`, }) createItem(t, srv, slug, "tasks", map[string]interface{}{ "title": "Linked Task", - "fields": `{"status":"open","priority":"high","phase":"` + phase.ID + `"}`, + "fields": `{"status":"open","priority":"high","parent":"` + plan.ID + `"}`, }) - // Create a task WITHOUT a phase β€” should be flagged as orphaned + // Create a task WITHOUT a plan β€” should be flagged as orphaned createItem(t, srv, slug, "tasks", map[string]interface{}{ "title": "Orphan Task", "fields": `{"status":"open","priority":"medium"}`, }) - // Create a done task without a phase β€” should NOT be flagged (done tasks are excluded) + // Create a done task without a plan β€” should NOT be flagged (done tasks are excluded) createItem(t, srv, slug, "tasks", map[string]interface{}{ "title": "Done Orphan", "fields": `{"status":"done","priority":"low"}`, @@ -898,17 +898,17 @@ func TestDashboardOrphanedTasks(t *testing.T) { } } -func TestDashboardOrphanedTasksNotFlaggedWithoutPhaseLinks(t *testing.T) { +func TestDashboardOrphanedTasksNotFlaggedWithoutPlanLinks(t *testing.T) { srv := testServer(t) slug := createWSWithCollections(t, srv) - // Create an active phase but with NO linked tasks - createItem(t, srv, slug, "phases", map[string]interface{}{ - "title": "Empty Phase", + // Create an active plan but with NO linked tasks + createItem(t, srv, slug, "plans", map[string]interface{}{ + "title": "Empty Plan", "fields": `{"status":"active"}`, }) - // Task without phase β€” should NOT be flagged because no phase has tasks + // Task without plan β€” should NOT be flagged because no plan has tasks createItem(t, srv, slug, "tasks", map[string]interface{}{ "title": "Unlinked Task", "fields": `{"status":"open","priority":"medium"}`, @@ -918,7 +918,7 @@ func TestDashboardOrphanedTasksNotFlaggedWithoutPhaseLinks(t *testing.T) { orphans := filterAttention(resp.Attention, "orphaned_task") if len(orphans) != 0 { - t.Errorf("expected 0 orphaned tasks when no phase has tasks linked, got %d: %+v", len(orphans), orphans) + t.Errorf("expected 0 orphaned tasks when no plan has tasks linked, got %d: %+v", len(orphans), orphans) } } diff --git a/internal/server/handlers_items.go b/internal/server/handlers_items.go index ec293378..98498fdc 100644 --- a/internal/server/handlers_items.go +++ b/internal/server/handlers_items.go @@ -134,10 +134,10 @@ func (s *Server) handleCreateItem(w http.ResponseWriter, r *http.Request) { } // Extract parent from fields β€” it's managed via item_links, not stored in fields JSON. - // Accepts both "parent" and "phase" (backward compat) as the field key. + // Accepts both "parent" and "plan" as the field key. // Skip this if the schema actually defines a field with that key. var parentValue string - for _, key := range []string{"parent", "phase"} { + for _, key := range []string{"parent", "plan"} { if schemaHasField(schema, key) { continue } @@ -277,11 +277,11 @@ func (s *Server) handleUpdateItem(w http.ResponseWriter, r *http.Request) { } // Extract parent from fields β€” it's managed via item_links, not stored in fields JSON. - // Accepts both "parent" and "phase" (backward compat) as the field key. + // Accepts both "parent" and "plan" as the field key. // Skip this if the schema actually defines a field with that key. var parentValue string var parentProvided bool - for _, key := range []string{"parent", "phase"} { + for _, key := range []string{"parent", "plan"} { if schemaHasField(schema, key) { continue } @@ -622,15 +622,15 @@ func (s *Server) publishItemEventWithName(eventType, workspaceID, itemID, title, }) } -// handlePhasesProgress returns child item completion progress for all non-deleted phases. +// handlePlansProgress returns child item completion progress for all non-deleted plans. // This is a backward-compat endpoint; the general form is per-item via /items/{slug}/children. -func (s *Server) handlePhasesProgress(w http.ResponseWriter, r *http.Request) { +func (s *Server) handlePlansProgress(w http.ResponseWriter, r *http.Request) { workspaceID, ok := s.getWorkspaceID(w, r) if !ok { return } - progress, err := s.store.GetAllItemProgress(workspaceID, "phases") + progress, err := s.store.GetAllItemProgress(workspaceID, "plans") if err != nil { writeInternalError(w, err) return @@ -707,7 +707,7 @@ func (s *Server) handleGetItemProgress(w http.ResponseWriter, r *http.Request) { }) } -// resolveParentFilter extracts a "parent" (or legacy "phase") key from the field +// resolveParentFilter extracts a "parent" (or "plan") key from the field // filters and converts it to a ParentLinkID filter (which uses item_links instead of json_extract). // An optional schema can be passed; if the schema defines a field with the key, // that key is left as a normal field filter instead of being treated as a parent link. @@ -716,14 +716,14 @@ func (s *Server) resolveParentFilter(workspaceID string, params *models.ItemList return nil } - // Accept both "parent" and "phase" (backward compat) + // Accept both "parent" and "plan" as parent filter keys // but skip if the schema defines a real field with that key var schema *models.CollectionSchema if len(schemas) > 0 { schema = &schemas[0] } var val string - for _, key := range []string{"parent", "phase"} { + for _, key := range []string{"parent", "plan"} { if schema != nil && schemaHasField(*schema, key) { continue } @@ -752,7 +752,7 @@ func (s *Server) resolveParentFilter(workspaceID string, params *models.ItemList // resolveRelationFields resolves slugs, PREFIX-NUMBER refs, and other identifiers // in relation fields to their canonical UUIDs. This allows clients to send -// human-readable identifiers (e.g. --field phase=workspace-onboarding) and have +// human-readable identifiers (e.g. --field plan=workspace-onboarding) and have // them stored as UUIDs that the dashboard and queries expect. func (s *Server) resolveRelationFields(workspaceID string, fields map[string]any, schema models.CollectionSchema) error { for _, def := range schema.Fields { diff --git a/internal/server/handlers_items_test.go b/internal/server/handlers_items_test.go index ebc556cf..8be731ac 100644 --- a/internal/server/handlers_items_test.go +++ b/internal/server/handlers_items_test.go @@ -20,7 +20,7 @@ func TestCollectionCRUD(t *testing.T) { srv := testServer(t) slug := createWSWithCollections(t, srv) - // List collections β€” should have 4 defaults (tasks, ideas, phases, docs) + // List collections β€” should have 4 defaults (tasks, ideas, plans, docs) rr := doRequest(srv, "GET", "/api/v1/workspaces/"+slug+"/collections", nil) if rr.Code != http.StatusOK { t.Fatalf("list collections: expected 200, got %d: %s", rr.Code, rr.Body.String()) @@ -234,23 +234,23 @@ func TestListCollectionItemsResolvesRelationFieldFilterRefs(t *testing.T) { srv := testServer(t) slug := createWSWithCollections(t, srv) - phaseResp := doRequest(srv, "POST", "/api/v1/workspaces/"+slug+"/collections/phases/items", map[string]interface{}{ + planResp := doRequest(srv, "POST", "/api/v1/workspaces/"+slug+"/collections/plans/items", map[string]interface{}{ "title": "Agent Workflow Intelligence", "fields": `{"status":"active"}`, }) - if phaseResp.Code != http.StatusCreated { - t.Fatalf("create phase: expected 201, got %d: %s", phaseResp.Code, phaseResp.Body.String()) + if planResp.Code != http.StatusCreated { + t.Fatalf("create plan: expected 201, got %d: %s", planResp.Code, planResp.Body.String()) } - var phase models.Item - parseJSON(t, phaseResp, &phase) - if phase.Ref == "" { - t.Fatal("expected phase ref to be populated") + var plan models.Item + parseJSON(t, planResp, &plan) + if plan.Ref == "" { + t.Fatal("expected plan ref to be populated") } taskResp := doRequest(srv, "POST", "/api/v1/workspaces/"+slug+"/collections/tasks/items", map[string]interface{}{ "title": "Add relation filter resolution", - "fields": `{"status":"open","phase":"` + phase.Ref + `"}`, + "fields": `{"status":"open","parent":"` + plan.Ref + `"}`, }) if taskResp.Code != http.StatusCreated { t.Fatalf("create task: expected 201, got %d: %s", taskResp.Code, taskResp.Body.String()) @@ -264,15 +264,15 @@ func TestListCollectionItemsResolvesRelationFieldFilterRefs(t *testing.T) { t.Fatalf("create unrelated task: expected 201, got %d: %s", otherTaskResp.Code, otherTaskResp.Body.String()) } - rr := doRequest(srv, "GET", "/api/v1/workspaces/"+slug+"/collections/tasks/items?phase="+phase.Ref, nil) + rr := doRequest(srv, "GET", "/api/v1/workspaces/"+slug+"/collections/tasks/items?parent="+plan.Ref, nil) if rr.Code != http.StatusOK { - t.Fatalf("list tasks by phase ref: expected 200, got %d: %s", rr.Code, rr.Body.String()) + t.Fatalf("list tasks by plan ref: expected 200, got %d: %s", rr.Code, rr.Body.String()) } var items []models.Item parseJSON(t, rr, &items) if len(items) != 1 { - t.Fatalf("expected 1 task for phase ref filter, got %d", len(items)) + t.Fatalf("expected 1 task for plan ref filter, got %d", len(items)) } if items[0].Title != "Add relation filter resolution" { t.Fatalf("unexpected task returned: %q", items[0].Title) @@ -283,20 +283,20 @@ func TestListItemsResolvesRelationFieldFilterRefsAcrossCollections(t *testing.T) srv := testServer(t) slug := createWSWithCollections(t, srv) - phaseResp := doRequest(srv, "POST", "/api/v1/workspaces/"+slug+"/collections/phases/items", map[string]interface{}{ + planResp := doRequest(srv, "POST", "/api/v1/workspaces/"+slug+"/collections/plans/items", map[string]interface{}{ "title": "Open Source Launch", "fields": `{"status":"active"}`, }) - if phaseResp.Code != http.StatusCreated { - t.Fatalf("create phase: expected 201, got %d: %s", phaseResp.Code, phaseResp.Body.String()) + if planResp.Code != http.StatusCreated { + t.Fatalf("create plan: expected 201, got %d: %s", planResp.Code, planResp.Body.String()) } - var phase models.Item - parseJSON(t, phaseResp, &phase) + var plan models.Item + parseJSON(t, planResp, &plan) taskResp := doRequest(srv, "POST", "/api/v1/workspaces/"+slug+"/collections/tasks/items", map[string]interface{}{ "title": "Document release filters", - "fields": `{"status":"open","phase":"` + phase.Ref + `"}`, + "fields": `{"status":"open","parent":"` + plan.Ref + `"}`, }) if taskResp.Code != http.StatusCreated { t.Fatalf("create task: expected 201, got %d: %s", taskResp.Code, taskResp.Body.String()) @@ -310,15 +310,15 @@ func TestListItemsResolvesRelationFieldFilterRefsAcrossCollections(t *testing.T) t.Fatalf("create doc: expected 201, got %d: %s", docResp.Code, docResp.Body.String()) } - rr := doRequest(srv, "GET", "/api/v1/workspaces/"+slug+"/items?phase="+phase.Ref, nil) + rr := doRequest(srv, "GET", "/api/v1/workspaces/"+slug+"/items?parent="+plan.Ref, nil) if rr.Code != http.StatusOK { - t.Fatalf("list items by phase ref: expected 200, got %d: %s", rr.Code, rr.Body.String()) + t.Fatalf("list items by plan ref: expected 200, got %d: %s", rr.Code, rr.Body.String()) } var items []models.Item parseJSON(t, rr, &items) if len(items) != 1 { - t.Fatalf("expected 1 item for cross-collection phase ref filter, got %d", len(items)) + t.Fatalf("expected 1 item for cross-collection plan ref filter, got %d", len(items)) } if items[0].CollectionSlug != "tasks" { t.Fatalf("expected task item, got collection %q", items[0].CollectionSlug) @@ -911,8 +911,8 @@ func TestDashboard(t *testing.T) { } // Verify structure has correct field types (even if empty) - if resp.ActivePhases == nil { - t.Error("expected active_phases to be non-nil") + if resp.ActivePlans == nil { + t.Error("expected active_plans to be non-nil") } if resp.Attention == nil { t.Error("expected attention to be non-nil") diff --git a/internal/server/item_lineage.go b/internal/server/item_lineage.go index 3231a81d..455cd193 100644 --- a/internal/server/item_lineage.go +++ b/internal/server/item_lineage.go @@ -46,21 +46,13 @@ func (s *Server) enrichItemsWithParent(workspaceID string, items []models.Item) continue } items[i].ParentLinkID = pid - items[i].PhaseID = pid // backward compat if info, ok := parents[pid]; ok { items[i].ParentTitle = info.title items[i].ParentRef = info.ref - items[i].PhaseTitle = info.title // backward compat - items[i].PhaseRef = info.ref // backward compat } } } -// enrichItemsWithPhase is a deprecated alias for enrichItemsWithParent. -func (s *Server) enrichItemsWithPhase(workspaceID string, items []models.Item) { - s.enrichItemsWithParent(workspaceID, items) -} - func (s *Server) enrichItemForResponse(item *models.Item) error { if item == nil { return nil @@ -80,10 +72,6 @@ func (s *Server) enrichItemForResponse(item *models.Item) error { item.ParentLinkID = parentLink.TargetID item.ParentRef = parentLink.TargetRef item.ParentTitle = parentLink.TargetTitle - // Backward compat - item.PhaseID = parentLink.TargetID - item.PhaseRef = parentLink.TargetRef - item.PhaseTitle = parentLink.TargetTitle } return nil diff --git a/internal/server/middleware_auth.go b/internal/server/middleware_auth.go index 3802150b..8ef24159 100644 --- a/internal/server/middleware_auth.go +++ b/internal/server/middleware_auth.go @@ -119,8 +119,11 @@ func (s *Server) SessionAuth(next http.Handler) http.Handler { // Re-issue CSRF cookie if the session is valid but the cookie is missing. // This can happen when cookies expire at different times or are selectively cleared. - if _, csrfErr := r.Cookie(csrfCookie); csrfErr != nil { - setCSRFCookie(w, 7*24*60*60, s.secureCookies) + // Skip for auth endpoints β€” they manage their own CSRF cookies (login sets, logout clears). + if !strings.HasPrefix(r.URL.Path, "/api/v1/auth/") { + if _, csrfErr := r.Cookie(csrfCookie); csrfErr != nil { + setCSRFCookie(w, 7*24*60*60, s.secureCookies) + } } ctx := context.WithValue(r.Context(), ctxCurrentUser, user) diff --git a/internal/server/server.go b/internal/server/server.go index 90216010..7a569d90 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -276,8 +276,8 @@ func (s *Server) setupRouter() { }) }) - // Phases progress - r.Get("/phases-progress", s.handlePhasesProgress) + // Plans progress + r.Get("/plans-progress", s.handlePlansProgress) // Items (cross-collection, v2) r.Get("/items", s.handleListItems) diff --git a/internal/store/export.go b/internal/store/export.go index 65372ffd..2252a572 100644 --- a/internal/store/export.go +++ b/internal/store/export.go @@ -329,7 +329,7 @@ func (s *Store) rebuildFTSForWorkspace(wsID string) { } // remapFieldIDs replaces old UUIDs in a JSON fields string with their new IDs. -// This handles relation fields (e.g. phase: "uuid") without needing to parse the schema. +// This handles relation fields (e.g. parent: "uuid") without needing to parse the schema. func remapFieldIDs(fieldsJSON string, itemMap, collMap map[string]string) string { result := fieldsJSON for oldID, newID := range itemMap { diff --git a/internal/store/items.go b/internal/store/items.go index 8a5e54d7..4f1fbe3c 100644 --- a/internal/store/items.go +++ b/internal/store/items.go @@ -415,13 +415,9 @@ func (s *Store) ListItems(workspaceID string, params models.ItemListParams) ([]m } // Parent link filter via item_links - parentFilter := params.ParentLinkID - if parentFilter == "" { - parentFilter = params.PhaseID // deprecated alias - } - if parentFilter != "" { + if params.ParentLinkID != "" { query += " AND EXISTS (SELECT 1 FROM item_links il WHERE il.source_id = i.id AND il.link_type = 'parent' AND il.target_id = ?)" - args = append(args, parentFilter) + args = append(args, params.ParentLinkID) } // Field filters β€” supports comma-separated values as OR @@ -1018,11 +1014,6 @@ func (s *Store) SetParentLink(workspaceID, itemID, parentID, createdBy string) ( return nil, fmt.Errorf("parent link created but not found") } -// SetPhaseLink is a deprecated alias for SetParentLink. -func (s *Store) SetPhaseLink(workspaceID, itemID, parentID, createdBy string) (*models.ItemLink, error) { - return s.SetParentLink(workspaceID, itemID, parentID, createdBy) -} - // checkParentCycle walks the ancestor chain from parentID and returns an error // if itemID is found (which would create a cycle). func (s *Store) checkParentCycle(itemID, parentID string) error { @@ -1057,11 +1048,6 @@ func (s *Store) ClearParentLink(itemID string) error { return nil } -// ClearPhaseLink is a deprecated alias for ClearParentLink. -func (s *Store) ClearPhaseLink(itemID string) error { - return s.ClearParentLink(itemID) -} - // GetParentForItem returns the parent link for an item, or nil if it has no parent. func (s *Store) GetParentForItem(itemID string) (*models.ItemLink, error) { sStatusExpr := s.dialect.JSONExtractText("s.fields", "status") @@ -1120,11 +1106,6 @@ func (s *Store) GetParentForItem(itemID string) (*models.ItemLink, error) { return &link, nil } -// GetPhaseForItem is a deprecated alias for GetParentForItem. -func (s *Store) GetPhaseForItem(itemID string) (*models.ItemLink, error) { - return s.GetParentForItem(itemID) -} - // GetParentMap returns a map of item ID -> parent item ID for all parent links // in a workspace. Used for efficient batch lookups (e.g., dashboard, list enrichment). func (s *Store) GetParentMap(workspaceID string) (map[string]string, error) { @@ -1148,17 +1129,11 @@ func (s *Store) GetParentMap(workspaceID string) (map[string]string, error) { return m, rows.Err() } -// GetTaskPhaseMap is a deprecated alias for GetParentMap. -func (s *Store) GetTaskPhaseMap(workspaceID string) (map[string]string, error) { - return s.GetParentMap(workspaceID) -} - // --- Child Item Progress --- // GetItemProgress counts total and done child items linked to a parent via item_links. // "Done" means any terminal status as defined by the child items' collection schemas. -// Unlike the old GetPhaseProgress, this is not filtered to a specific collection β€” -// children from any collection count toward progress. +// Children from any collection count toward progress. func (s *Store) GetItemProgress(parentItemID string) (total int, done int, err error) { termPlaceholders, termArgs := s.getChildTerminalPlaceholders(parentItemID) args := append(termArgs, parentItemID) @@ -1176,11 +1151,6 @@ func (s *Store) GetItemProgress(parentItemID string) (total int, done int, err e return total, done, nil } -// GetPhaseProgress is a deprecated alias for GetItemProgress. -func (s *Store) GetPhaseProgress(parentItemID string) (total int, done int, err error) { - return s.GetItemProgress(parentItemID) -} - // getChildTerminalPlaceholders returns SQL placeholders and args for the terminal // statuses of the collections that a parent item's children belong to. // It queries the actual child items' collection schemas rather than hardcoding 'tasks'. @@ -1288,12 +1258,8 @@ type ItemProgress struct { Done int `json:"done"` } -// PhaseProgress is a deprecated alias for ItemProgress. -type PhaseProgress = ItemProgress - // GetAllItemProgress returns child item completion counts for every non-deleted -// item in the given collection within a workspace. This generalizes -// GetAllPhasesProgress β€” any collection can be queried, not just phases. +// item in the given collection within a workspace. func (s *Store) GetAllItemProgress(workspaceID, collectionSlug string) ([]ItemProgress, error) { termPlaceholders, termArgs := s.getCollectionChildTerminalPlaceholders(workspaceID, collectionSlug) args := append(termArgs, workspaceID, collectionSlug) @@ -1331,14 +1297,8 @@ func (s *Store) GetAllItemProgress(workspaceID, collectionSlug string) ([]ItemPr return result, rows.Err() } -// GetAllPhasesProgress is a deprecated alias that calls GetAllItemProgress for the "phases" collection. -func (s *Store) GetAllPhasesProgress(workspaceID string) ([]ItemProgress, error) { - return s.GetAllItemProgress(workspaceID, "phases") -} - // GetChildItems returns all non-deleted child items linked to the given parent -// via item_links. Unlike the old GetTasksForPhase, this returns children from -// any collection, not just tasks. +// via item_links. Returns children from any collection. func (s *Store) GetChildItems(parentItemID string) ([]models.Item, error) { rows, err := s.db.Query(s.q(` SELECT i.id, i.workspace_id, i.collection_id, i.title, i.slug, i.content, i.fields, i.tags, @@ -1409,11 +1369,6 @@ func (s *Store) PopulateHasChildren(items []models.Item) { } } -// GetTasksForPhase is a deprecated alias for GetChildItems. -func (s *Store) GetTasksForPhase(parentItemID string) ([]models.Item, error) { - return s.GetChildItems(parentItemID) -} - // MoveItem moves an item to a different collection within the same workspace. // It updates the collection_id, assigns a new item_number in the target collection, // and updates the fields JSON. diff --git a/internal/store/items_test.go b/internal/store/items_test.go index 5fb42c4f..642c8363 100644 --- a/internal/store/items_test.go +++ b/internal/store/items_test.go @@ -194,7 +194,7 @@ func TestSeedDefaultCollections(t *testing.T) { t.Errorf("expected collection %q to be default", c.Slug) } } - for _, expected := range []string{"tasks", "ideas", "phases", "docs", "conventions", "playbooks"} { + for _, expected := range []string{"tasks", "ideas", "plans", "docs", "conventions", "playbooks"} { if !slugs[expected] { t.Errorf("expected default collection %q", expected) } diff --git a/internal/store/migrations/024_phases_to_plans.sql b/internal/store/migrations/024_phases_to_plans.sql new file mode 100644 index 00000000..d546d0d9 --- /dev/null +++ b/internal/store/migrations/024_phases_to_plans.sql @@ -0,0 +1,33 @@ +-- Rename "Phases" collection to "Plans" +-- Guard against slug collision: only rename if 'plans' doesn't already exist in the same workspace +UPDATE collections +SET name = 'Plans', + slug = 'plans', + icon = 'πŸ—ΊοΈ', + prefix = 'PLAN', + description = 'Plan and track project plans and milestones' +WHERE slug = 'phases' +AND NOT EXISTS ( + SELECT 1 FROM collections c2 + WHERE c2.workspace_id = collections.workspace_id AND c2.slug = 'plans' +); + +-- Update convention trigger options: on-phase-start β†’ on-plan-start, on-phase-complete β†’ on-plan-complete +-- Update the conventions collection schema to use new trigger names +UPDATE collections +SET schema = REPLACE(REPLACE(schema, 'on-phase-start', 'on-plan-start'), 'on-phase-complete', 'on-plan-complete') +WHERE slug = 'conventions'; + +-- Update the playbooks collection schema similarly +UPDATE collections +SET schema = REPLACE(REPLACE(schema, 'on-phase-start', 'on-plan-start'), 'on-phase-complete', 'on-plan-complete') +WHERE slug = 'playbooks'; + +-- Update any convention items that have trigger=on-phase-start or on-phase-complete in their fields JSON +UPDATE items +SET fields = REPLACE(REPLACE(fields, '"on-phase-start"', '"on-plan-start"'), '"on-phase-complete"', '"on-plan-complete"') +WHERE collection_id IN (SELECT id FROM collections WHERE slug = 'conventions' OR slug = 'playbooks') +AND (fields LIKE '%on-phase-start%' OR fields LIKE '%on-phase-complete%'); + +-- Note: document type 'phase-plan' β†’ 'plan' is handled in 025_doc_type_plan.sql +-- which recreates the table with the new CHECK constraint first. diff --git a/internal/store/migrations/025_doc_type_plan.sql b/internal/store/migrations/025_doc_type_plan.sql new file mode 100644 index 00000000..43eabbcd --- /dev/null +++ b/internal/store/migrations/025_doc_type_plan.sql @@ -0,0 +1,69 @@ +-- Rename doc_type 'phase-plan' to 'plan' in the CHECK constraint. +-- SQLite does not support ALTER CHECK, so we recreate the table. + +CREATE TABLE documents_new ( + id TEXT PRIMARY KEY, + workspace_id TEXT NOT NULL REFERENCES workspaces(id), + title TEXT NOT NULL, + slug TEXT NOT NULL, + content TEXT NOT NULL DEFAULT '', + doc_type TEXT NOT NULL DEFAULT 'notes' + CHECK (doc_type IN ('roadmap','plan','architecture','ideation', + 'feature-spec','notes','prompt-library','reference')), + status TEXT NOT NULL DEFAULT 'draft' + CHECK (status IN ('draft','active','completed','archived')), + tags TEXT NOT NULL DEFAULT '[]', + pinned INTEGER NOT NULL DEFAULT 0, + sort_order INTEGER NOT NULL DEFAULT 0, + created_by TEXT NOT NULL DEFAULT 'user' + CHECK (created_by IN ('user','agent')), + last_modified_by TEXT NOT NULL DEFAULT 'user' + CHECK (last_modified_by IN ('user','agent')), + source TEXT NOT NULL DEFAULT 'web' + CHECK (source IN ('cli','web','skill')), + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + deleted_at TEXT, + + UNIQUE(workspace_id, slug), + UNIQUE(workspace_id, title) +); + +-- Copy data, renaming 'phase-plan' to 'plan' during the insert +INSERT INTO documents_new +SELECT id, workspace_id, title, slug, content, + CASE WHEN doc_type = 'phase-plan' THEN 'plan' ELSE doc_type END, + status, tags, pinned, sort_order, created_by, last_modified_by, + source, created_at, updated_at, deleted_at +FROM documents; + +DROP TABLE documents; + +ALTER TABLE documents_new RENAME TO documents; + +-- Recreate indexes (originally from 001_initial.sql) +CREATE INDEX IF NOT EXISTS idx_documents_workspace ON documents(workspace_id); +CREATE INDEX IF NOT EXISTS idx_documents_type ON documents(workspace_id, doc_type); +CREATE INDEX IF NOT EXISTS idx_documents_status ON documents(workspace_id, status); +CREATE INDEX IF NOT EXISTS idx_documents_updated ON documents(workspace_id, updated_at); + +-- Recreate FTS triggers (originally from 001_initial.sql) +CREATE TRIGGER IF NOT EXISTS documents_ai AFTER INSERT ON documents BEGIN + INSERT INTO documents_fts(rowid, title, content, tags) + VALUES (new.rowid, new.title, new.content, new.tags); +END; + +CREATE TRIGGER IF NOT EXISTS documents_ad AFTER DELETE ON documents BEGIN + INSERT INTO documents_fts(documents_fts, rowid, title, content, tags) + VALUES ('delete', old.rowid, old.title, old.content, old.tags); +END; + +CREATE TRIGGER IF NOT EXISTS documents_au AFTER UPDATE ON documents BEGIN + INSERT INTO documents_fts(documents_fts, rowid, title, content, tags) + VALUES ('delete', old.rowid, old.title, old.content, old.tags); + INSERT INTO documents_fts(rowid, title, content, tags) + VALUES (new.rowid, new.title, new.content, new.tags); +END; + +-- Rebuild FTS index to match potentially renamed doc_type values +INSERT INTO documents_fts(documents_fts) VALUES ('rebuild'); diff --git a/internal/store/pgmigrations/004_doc_type_plan.sql b/internal/store/pgmigrations/004_doc_type_plan.sql new file mode 100644 index 00000000..d83d7dbb --- /dev/null +++ b/internal/store/pgmigrations/004_doc_type_plan.sql @@ -0,0 +1,11 @@ +-- Rename doc_type 'phase-plan' to 'plan' in the CHECK constraint. +-- PostgreSQL supports dropping and re-adding constraints. + +-- Update existing rows first +UPDATE documents SET doc_type = 'plan' WHERE doc_type = 'phase-plan'; + +-- Drop the old constraint and add the new one +ALTER TABLE documents DROP CONSTRAINT IF EXISTS documents_doc_type_check; +ALTER TABLE documents ADD CONSTRAINT documents_doc_type_check + CHECK (doc_type IN ('roadmap','plan','architecture','ideation', + 'feature-spec','notes','prompt-library','reference')); diff --git a/internal/store/pgmigrations/005_phases_to_plans.sql b/internal/store/pgmigrations/005_phases_to_plans.sql new file mode 100644 index 00000000..82de5a65 --- /dev/null +++ b/internal/store/pgmigrations/005_phases_to_plans.sql @@ -0,0 +1,30 @@ +-- Rename "Phases" collection to "Plans" +-- Guard against slug collision: only rename if 'plans' doesn't already exist in the same workspace +UPDATE collections c +SET name = 'Plans', + slug = 'plans', + icon = 'πŸ—ΊοΈ', + prefix = 'PLAN', + description = 'Plan and track project plans and milestones' +WHERE c.slug = 'phases' +AND NOT EXISTS ( + SELECT 1 FROM collections c2 + WHERE c2.workspace_id = c.workspace_id AND c2.slug = 'plans' +); + +-- Update convention trigger options: on-phase-start β†’ on-plan-start, on-phase-complete β†’ on-plan-complete +-- Cast JSONB to text for REPLACE, then back to JSONB +UPDATE collections +SET schema = REPLACE(REPLACE(schema::text, 'on-phase-start', 'on-plan-start'), 'on-phase-complete', 'on-plan-complete')::jsonb +WHERE slug = 'conventions'; + +UPDATE collections +SET schema = REPLACE(REPLACE(schema::text, 'on-phase-start', 'on-plan-start'), 'on-phase-complete', 'on-plan-complete')::jsonb +WHERE slug = 'playbooks'; + +-- Update any convention/playbook items with old trigger names in fields JSON +-- Cast JSONB to text for REPLACE/LIKE, then back to JSONB +UPDATE items +SET fields = REPLACE(REPLACE(fields::text, '"on-phase-start"', '"on-plan-start"'), '"on-phase-complete"', '"on-plan-complete"')::jsonb +WHERE collection_id IN (SELECT id FROM collections WHERE slug = 'conventions' OR slug = 'playbooks') +AND (fields::text LIKE '%on-phase-start%' OR fields::text LIKE '%on-phase-complete%'); diff --git a/internal/store/snapshots.go b/internal/store/snapshots.go index 994a420b..bb28df40 100644 --- a/internal/store/snapshots.go +++ b/internal/store/snapshots.go @@ -12,7 +12,7 @@ func (s *Store) CreateSnapshot(snap models.ProgressSnapshot) error { _, err := s.db.Exec(s.q(` INSERT INTO progress_snapshots (id, workspace_id, total_tasks, done_tasks, open_tasks, in_progress, percentage, phase_data, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`), - newID(), snap.WorkspaceID, snap.TotalTasks, snap.DoneTasks, snap.OpenTasks, snap.InProgress, snap.Percentage, snap.PhaseData, now(), + newID(), snap.WorkspaceID, snap.TotalTasks, snap.DoneTasks, snap.OpenTasks, snap.InProgress, snap.Percentage, snap.PlanData, now(), ) if err != nil { return fmt.Errorf("insert snapshot: %w", err) @@ -54,7 +54,7 @@ func (s *Store) ListSnapshots(workspaceID string, params models.SnapshotListPara for rows.Next() { var snap models.ProgressSnapshot var createdAt string - if err := rows.Scan(&snap.ID, &snap.WorkspaceID, &snap.TotalTasks, &snap.DoneTasks, &snap.OpenTasks, &snap.InProgress, &snap.Percentage, &snap.PhaseData, &createdAt); err != nil { + if err := rows.Scan(&snap.ID, &snap.WorkspaceID, &snap.TotalTasks, &snap.DoneTasks, &snap.OpenTasks, &snap.InProgress, &snap.Percentage, &snap.PlanData, &createdAt); err != nil { return nil, fmt.Errorf("scan snapshot: %w", err) } snap.CreatedAt, _ = time.Parse(time.RFC3339, createdAt) @@ -74,7 +74,7 @@ func (s *Store) LatestSnapshot(workspaceID string) (*models.ProgressSnapshot, er ORDER BY created_at DESC LIMIT 1`), workspaceID, - ).Scan(&snap.ID, &snap.WorkspaceID, &snap.TotalTasks, &snap.DoneTasks, &snap.OpenTasks, &snap.InProgress, &snap.Percentage, &snap.PhaseData, &createdAt) + ).Scan(&snap.ID, &snap.WorkspaceID, &snap.TotalTasks, &snap.DoneTasks, &snap.OpenTasks, &snap.InProgress, &snap.Percentage, &snap.PlanData, &createdAt) if err != nil { if err.Error() == "sql: no rows in result set" { diff --git a/internal/store/store.go b/internal/store/store.go index eae00339..3bde10bf 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -140,6 +140,8 @@ func (s *Store) migrate() error { "021_phase_to_links.sql", "022_audit_trail.sql", "023_parent_link_type.sql", + "024_phases_to_plans.sql", + "025_doc_type_plan.sql", } for _, name := range migrations { @@ -187,6 +189,8 @@ func (s *Store) migratePostgres() error { "001_initial.sql", "002_audit_trail.sql", "003_parent_link_type.sql", + "004_doc_type_plan.sql", + "005_phases_to_plans.sql", } for _, name := range migrations { diff --git a/internal/store/store_test.go b/internal/store/store_test.go index e51f62b3..9b667d33 100644 --- a/internal/store/store_test.go +++ b/internal/store/store_test.go @@ -291,7 +291,7 @@ func TestDocumentListFilters(t *testing.T) { // Create docs of different types and statuses s.CreateDocument(ws.ID, models.DocumentCreate{Title: "Roadmap", DocType: "roadmap", Status: "active"}) - s.CreateDocument(ws.ID, models.DocumentCreate{Title: "Plan", DocType: "phase-plan", Status: "active"}) + s.CreateDocument(ws.ID, models.DocumentCreate{Title: "Plan", DocType: "plan", Status: "active"}) s.CreateDocument(ws.ID, models.DocumentCreate{Title: "Notes", DocType: "notes", Status: "draft"}) // Filter by type diff --git a/skills/pad/SKILL.md b/skills/pad/SKILL.md index 3bcea448..983ac5a0 100644 --- a/skills/pad/SKILL.md +++ b/skills/pad/SKILL.md @@ -1,6 +1,6 @@ --- name: pad -description: "Talk to your project. Natural-language project management β€” create items, check status, plan phases, brainstorm ideas, and more." +description: "Talk to your project. Natural-language project management β€” create items, check status, plan work, brainstorm ideas, and more." argument-hint: allowed-tools: - Bash @@ -9,7 +9,7 @@ allowed-tools: # Pad β€” Talk to Your Project -You are the interface between the user and their Pad workspace β€” a project management tool for developers and AI agents. Pad uses **Collections** (Tasks, Ideas, Phases, Docs, and custom types) containing **Items** with structured fields and optional rich content. +You are the interface between the user and their Pad workspace β€” a project management tool for developers and AI agents. Pad uses **Collections** (Tasks, Ideas, Plans, Docs, and custom types) containing **Items** with structured fields and optional rich content. Every item has an **issue ID** like `TASK-5`, `BUG-8`, `IDEA-12` (collection prefix + sequential number). **Always use issue IDs to reference items** β€” never use slugs. Issue IDs are short, stable, and human-readable. @@ -24,7 +24,7 @@ There is **one command**: `/pad `. You interpret the user's intent and On every `/pad` invocation, start by loading workspace context: ```bash -pad project dashboard --format json # Project overview: collections, phases, attention, suggestions +pad project dashboard --format json # Project overview: collections, plans, attention, suggestions pad collection list --format json # Available collections with schemas pad item list conventions --field status=active --field trigger=always --format json # Always-on project conventions pad role list --format json # Agent roles configured in workspace @@ -113,7 +113,7 @@ Interpret the user's intent and route to the appropriate action. Here are common **Creating items:** - "I have an idea for X" β†’ Create an Idea item - "new task: fix the OAuth bug" β†’ Create a Task item -- "let's start a new phase for the API redesign" β†’ Create a Phase item +- "let's start a new plan for the API redesign" β†’ Create a Plan item - "document the auth architecture" β†’ Create a Doc item **Querying:** @@ -131,8 +131,8 @@ Interpret the user's intent and route to the appropriate action. Here are common **Best practice:** Always use `--comment` when changing status to explain *why*. This creates an audit trail linking each status change to a reason. **Planning:** -- "let's plan the next phase" β†’ Multi-step planning workflow (see below) -- "break phase 2 into tasks" β†’ Decompose a phase into task items +- "let's create a plan" β†’ Multi-step planning workflow (see below) +- "break plan 2 into tasks" β†’ Decompose a plan into task items - "what's blocking us?" β†’ Analyze open items and dependencies **Ideation:** @@ -148,11 +148,11 @@ Interpret the user's intent and route to the appropriate action. Here are common **Reports:** - "prep for standup" / "what did we do?" β†’ `pad project standup --format json` - "generate changelog" / "what shipped?" β†’ `pad project changelog --format json` -- "changelog for this phase" β†’ `pad project changelog --parent PHASE-2 --format json` +- "changelog for this plan" β†’ `pad project changelog --parent PLAN-2 --format json` - "changelog since Monday" β†’ `pad project changelog --since 2026-03-24 --format json` **Retrospective:** -- "phase 2 is done, let's retro" β†’ Review completed work, save retrospective +- "plan 2 is done, let's retro" β†’ Review completed work, save retrospective **Onboarding:** - "scan this codebase" / "set up my workspace" β†’ Codebase analysis + onboarding workflow (see below) @@ -204,9 +204,9 @@ pad role delete # Delete a r # Create items (collection accepts singular or plural: task/tasks, idea/ideas, etc.) # The CLI prints the new item's issue ID (e.g. "Created TASK-5: ...") β€” use it for subsequent commands pad item create "title" [--status X] [--priority X] [--parent REF] [--role X] [--assign X] [--category X] [--content "..."] [--stdin] -pad item create task "Fix OAuth redirect" --priority high --parent PHASE-3 --role implementer --assign Dave +pad item create task "Fix OAuth redirect" --priority high --parent PLAN-3 --role implementer --assign Dave pad item create idea "Real-time collaboration" --category infrastructure -pad item create phase "API Redesign" --status active +pad item create plan "API Redesign" --status active pad item create doc "Auth Architecture" --category architecture --stdin <<< "# Auth Architecture\n\n..." # Custom fields via --field flag (works for any collection's fields) @@ -248,7 +248,7 @@ pad item search "query" [--format json] pad project dashboard [--format json] # Project dashboard pad project next [--format json] # Recommended next task pad project standup [--days N] [--format json] # Daily standup report (completed/in-progress/blockers) -pad project changelog [--days N] [--since DATE] [--parent PHASE-2] [--format json|markdown] # Release notes +pad project changelog [--days N] [--since DATE] [--parent PLAN-2] [--format json|markdown] # Release notes ``` ### Server @@ -295,27 +295,27 @@ All commands support `--format json` (for parsing) or `--format table` (default, - "Should I create a Doc for this architecture decision?" β†’ `pad item create doc "X" --category decision --stdin` 5. **Never save without asking.** Always show what you'll create and get confirmation. -### Planning: "Let's plan the next phase" +### Planning: "Let's create a plan" -1. **Load context:** `pad project dashboard --format json`, `pad item list phases --all --format json` -2. **Understand current state:** What phases exist? What's active? What's completed? -3. **Propose outline:** Present phase title + 1-line summary. Ask for feedback. -4. **Create the phase:** `pad item create phase "Phase N: Title" --status draft --stdin <<< ""` +1. **Load context:** `pad project dashboard --format json`, `pad item list plans --all --format json` +2. **Understand current state:** What plans exist? What's active? What's completed? +3. **Propose outline:** Present plan title + 1-line summary. Ask for feedback. +4. **Create the plan:** `pad item create plan "Plan N: Title" --status draft --stdin <<< ""` 5. **Decompose into tasks:** For each task in the plan, create a Task item: ```bash - pad item create task "Task description" --parent PHASE-3 --priority medium + pad item create task "Task description" --parent PLAN-3 --priority medium ``` 6. **If roles exist, suggest role assignments** for each task: "This looks like Implementer work β€” assign to Implementer?" 7. **Each task should be PR-sized** β€” small enough for one branch, large enough to be meaningful. 8. **Ask before creating each item.** Don't bulk-create without approval. -### Decomposition: "Break phase X into tasks" +### Decomposition: "Break plan X into tasks" -1. **Load the phase:** `pad item show PHASE-2 --format markdown` +1. **Load the plan:** `pad item show PLAN-2 --format markdown` 2. **Analyze the content** for actionable work items 3. **Propose task list** with titles, priorities, and suggested role assignments 4. **Create approved tasks:** One `pad item create task` per approved item -5. **Link tasks to phase** using `--parent PHASE-2` flag +5. **Link tasks to plan** using `--parent PLAN-2` flag ### Status Check: "How are we doing?" @@ -324,7 +324,7 @@ All commands support `--format json` (for parsing) or `--format table` (default, 3. Present conversationally: - If role active: role queue first ("Your Implementer queue: 3 items") - Collection summaries (Tasks: 5 open, 2 in progress, 12 done) - - Active phase progress with bars + - Active plan progress with bars - Attention items (stalled, overdue) - Suggested next actions 4. Offer follow-up: "Want me to dig into any of these?" @@ -352,17 +352,17 @@ All commands support `--format json` (for parsing) or `--format table` (default, - Linter/formatter: what tools enforce code style? 4. **Suggest conventions:** Based on the detected tooling, suggest conventions from the library. Customize the content with the actual commands found in the project (e.g., "Run `make test`" not just "Run the test suite"). Present as a checklist and ask which to activate. 5. **Draft an architecture doc:** Summarize the project structure, tech stack, key directories, and how the pieces fit together. Offer to save as a Doc item. -6. **Propose an initial phase:** Based on recent git activity (`git log --oneline -20`) and any open TODOs, suggest a phase name and a few starter tasks. Ask before creating. +6. **Propose an initial plan:** Based on recent git activity (`git log --oneline -20`) and any open TODOs, suggest a plan name and a few starter tasks. Ask before creating. 7. **Suggest agent roles:** If no roles exist yet, suggest creating roles based on the project type. For a typical dev project: Planner, Implementer, Reviewer. Don't auto-create β€” ask first. 8. **Always confirm before creating each item.** Show what will be created, get approval, then create. -### Retrospective: "Phase X is done, let's retro" +### Retrospective: "Plan X is done, let's retro" -1. Load the phase: `pad item show PHASE-2 --format markdown` -2. Load tasks: `pad item list tasks --all --format json` (filter to phase) +1. Load the plan: `pad item show PLAN-2 --format markdown` +2. Load tasks: `pad item list tasks --all --format json` (filter to plan) 3. Generate retro: What shipped, what was deferred, lessons learned -4. Offer to save: `pad item create doc "Phase N Retrospective" --category retro --stdin` -5. Offer to update phase status: `pad item update PHASE-2 --status completed` +4. Offer to save: `pad item create doc "Plan N Retrospective" --category retro --stdin` +5. Offer to update plan status: `pad item update PLAN-2 --status completed` ## Key Principles diff --git a/web/src/lib/api/client.ts b/web/src/lib/api/client.ts index fdc1c3bc..cc1b4746 100644 --- a/web/src/lib/api/client.ts +++ b/web/src/lib/api/client.ts @@ -278,8 +278,8 @@ export const api = { request(`/workspaces/${ws}/items/${slug}/children`), /** @deprecated Use progress() per-item instead */ - phasesProgress: (ws: string) => - request<{item_id: string; total: number; done: number}[]>(`/workspaces/${ws}/phases-progress`) + plansProgress: (ws: string) => + request<{item_id: string; total: number; done: number}[]>(`/workspaces/${ws}/plans-progress`) }, // ── Versions ────────────────────────────────────────────────────────────── diff --git a/web/src/lib/components/OnboardingChecklist.svelte b/web/src/lib/components/OnboardingChecklist.svelte index 75af8c01..6f0e5c6c 100644 --- a/web/src/lib/components/OnboardingChecklist.svelte +++ b/web/src/lib/components/OnboardingChecklist.svelte @@ -38,10 +38,10 @@ hint: '/pad what conventions should this project follow?' }, { - title: 'Create your first phase', - href: `/${wsSlug}/new?collection=phases`, - done: collectionHasItems('phases'), - hint: '/pad create a phase for what I\'m working on' + title: 'Create your first plan', + href: `/${wsSlug}/new?collection=plans`, + done: collectionHasItems('plans'), + hint: '/pad create a plan for what I\'m working on' }, { title: 'Add a few tasks', diff --git a/web/src/lib/components/collections/FilterBar.svelte b/web/src/lib/components/collections/FilterBar.svelte index e0b0be82..ca583c96 100644 --- a/web/src/lib/components/collections/FilterBar.svelte +++ b/web/src/lib/components/collections/FilterBar.svelte @@ -30,16 +30,16 @@ onFilterChange(next); } - let hasPhaseFilter = $derived(Object.keys(relationLabels).length > 0); - let activePhase = $derived(activeFilters.phase ?? ''); + let hasParentFilter = $derived(Object.keys(relationLabels).length > 0); + let activeParent = $derived(activeFilters.parent ?? ''); - function setPhaseFilter(e: Event) { + function setParentFilter(e: Event) { const value = (e.target as HTMLSelectElement).value; const next = { ...activeFilters }; if (value === '') { - delete next.phase; + delete next.parent; } else { - next.phase = value; + next.parent = value; } onFilterChange(next); } @@ -72,9 +72,9 @@ {/if} - {#if hasPhaseFilter} - + {#each Object.entries(relationLabels) as [id, label] (id)} {/each} @@ -132,7 +132,7 @@ background: var(--bg-hover); } - .phase-filter { + .parent-filter { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius); @@ -142,7 +142,7 @@ cursor: pointer; max-width: 180px; } - .phase-filter:focus { + .parent-filter:focus { border-color: var(--accent-blue); outline: none; } diff --git a/web/src/lib/components/collections/ItemCard.svelte b/web/src/lib/components/collections/ItemCard.svelte index 04157229..dff7bf2d 100644 --- a/web/src/lib/components/collections/ItemCard.svelte +++ b/web/src/lib/components/collections/ItemCard.svelte @@ -109,9 +109,9 @@ {formatLabel(fields.priority)} {/if} - {#if item.phase_title} + {#if item.parent_title} · - {item.phase_ref ? `${item.phase_ref}: ${item.phase_title}` : item.phase_title} + {item.parent_ref ? `${item.parent_ref}: ${item.parent_title}` : item.parent_title} {/if} {#if item.agent_role_name} · @@ -258,7 +258,7 @@ white-space: nowrap; } - .meta-phase { + .meta-parent { font-size: 0.7em; font-weight: 500; color: var(--accent-purple, var(--text-secondary)); diff --git a/web/src/lib/components/common/EmptyState.svelte b/web/src/lib/components/common/EmptyState.svelte index 5f364216..8b293eed 100644 --- a/web/src/lib/components/common/EmptyState.svelte +++ b/web/src/lib/components/common/EmptyState.svelte @@ -12,7 +12,7 @@ const messages: Record = { tasks: 'No tasks yet. Create your first task to start tracking work.', ideas: 'No ideas captured yet. Jot down your first idea.', - phases: 'No phases defined. Create a phase to plan your milestones.', + plans: 'No plans defined. Create a plan to organize your milestones.', docs: 'No documents yet. Start writing to build your knowledge base.', bugs: "No bugs reported. That's either great news or you haven't looked yet.", conventions: 'No conventions set. Define rules for how agents should work.' diff --git a/web/src/lib/components/common/QuickActionsMenu.svelte b/web/src/lib/components/common/QuickActionsMenu.svelte index 30e2630f..d3222e23 100644 --- a/web/src/lib/components/common/QuickActionsMenu.svelte +++ b/web/src/lib/components/common/QuickActionsMenu.svelte @@ -30,7 +30,8 @@ fields: Object.entries(fields) .map(([k, v]) => `${k}: ${v}`) .join(', '), - phase: item ? String(fields['phase'] ?? '') : '' + plan: item ? String(fields['plan'] ?? '') : '', + phase: item ? String(fields['phase'] ?? fields['plan'] ?? '') : '' }; for (const [key, value] of Object.entries(vars)) { diff --git a/web/src/lib/types/index.ts b/web/src/lib/types/index.ts index 1eebbb29..a6ab660d 100644 --- a/web/src/lib/types/index.ts +++ b/web/src/lib/types/index.ts @@ -321,12 +321,6 @@ export interface Item { parent_link_id?: string; parent_ref?: string; parent_title?: string; - /** @deprecated Use parent_id */ - phase_id?: string; - /** @deprecated Use parent_ref */ - phase_ref?: string; - /** @deprecated Use parent_title */ - phase_title?: string; has_children?: boolean; derived_closure?: ItemDerivedClosure; code_context?: ItemCodeContext; @@ -520,7 +514,7 @@ export interface DashboardResponse { by_collection: Record>; }; active_items: DashboardActiveItem[]; - active_phases: { + active_plans: { slug: string; title: string; progress: number; diff --git a/web/src/routes/[workspace]/+page.svelte b/web/src/routes/[workspace]/+page.svelte index 5ffd8400..7d0fbde0 100644 --- a/web/src/routes/[workspace]/+page.svelte +++ b/web/src/routes/[workspace]/+page.svelte @@ -156,7 +156,7 @@ function attentionIcon(type: string): string { if (type === 'overdue') return '\u23f0'; if (type === 'stalled') return '\u26a0'; - if (type === 'phase_complete' || type === 'phase_completion') return '\ud83c\udf89'; + if (type === 'plan_complete' || type === 'plan_completion' || type === 'phase_complete' || type === 'phase_completion') return '\ud83c\udf89'; if (type === 'orphaned_task' || type === 'orphaned') return '\ud83d\udd17'; return '?'; } @@ -253,20 +253,20 @@ {/if} - - {#if dashboard.active_phases.length > 0} + + {#if dashboard.active_plans.length > 0}
- +
-
- {#each dashboard.active_phases as phase (phase.slug)} - - {phase.title} + @@ -599,13 +599,13 @@ margin-left: auto; } - /* ── Active Phases ──────────────────────────────────────────────────── */ - .phase-list { + /* ── Active Plans ──────────────────────────────────────────────────── */ + .plan-list { display: flex; flex-direction: column; gap: var(--space-2); } - .phase-row { + .plan-row { display: flex; align-items: center; gap: var(--space-3); @@ -617,11 +617,11 @@ color: inherit; transition: border-color 0.15s, background 0.15s; } - .phase-row:hover { + .plan-row:hover { border-color: var(--accent-blue); text-decoration: none; } - .phase-title { + .plan-title { font-weight: 600; font-size: 0.9em; color: var(--text-primary); @@ -641,7 +641,7 @@ border-radius: 3px; transition: width 0.3s ease; } - .phase-meta { + .plan-meta { font-size: 0.8em; color: var(--text-secondary); white-space: nowrap; diff --git a/web/src/routes/[workspace]/[collection]/+page.svelte b/web/src/routes/[workspace]/[collection]/+page.svelte index 31a3daa6..377c7155 100644 --- a/web/src/routes/[workspace]/[collection]/+page.svelte +++ b/web/src/routes/[workspace]/[collection]/+page.svelte @@ -141,8 +141,8 @@ items = freshItems; // Update progress data without resetting view state - if (collSlug === 'phases') { - const progress = await api.items.phasesProgress(wsSlug).catch(() => []); + if (collSlug === 'plans') { + const progress = await api.items.plansProgress(wsSlug).catch(() => []); const map: Record = {}; for (const p of progress) { map[p.item_id] = { total: p.total, done: p.done }; @@ -184,10 +184,10 @@ savedViews = viewsData; activeViewId = null; - // Fetch phase progress if viewing phases collection - if (coll === 'phases') { + // Fetch plan progress if viewing plans collection + if (coll === 'plans') { try { - const progress = await api.items.phasesProgress(ws); + const progress = await api.items.plansProgress(ws); const map: Record = {}; for (const p of progress) { map[p.item_id] = { total: p.total, done: p.done }; @@ -211,12 +211,12 @@ progressLabel = 'done'; } - // Fetch phase names for relation display on task cards + // Fetch plan names for relation display on task cards if (coll === 'tasks') { try { - const phases = await api.items.listByCollection(ws, 'phases'); + const plans = await api.items.listByCollection(ws, 'plans'); const labels: Record = {}; - for (const p of phases) { + for (const p of plans) { labels[p.id] = p.title; } relationLabels = labels; @@ -260,9 +260,10 @@ // Apply field filters for (const [key, value] of Object.entries(activeFilters)) { result = result.filter((item) => { - // Phase filter uses the phase link, not fields JSON - if (key === 'phase') { - return item.phase_id === value; + // Parent filter uses the parent link, not fields JSON + // Also accept legacy 'phase' key for backward compat with saved views + if (key === 'parent' || key === 'phase') { + return item.parent_link_id === value; } const fields = parseFields(item); return fields[key] === value; @@ -305,7 +306,7 @@ const emptyHintMap: Record = { tasks: '/pad break down my current work into tasks', ideas: "/pad I have an idea for...", - phases: '/pad create a phase for what I\'m working on', + plans: '/pad create a plan for what I\'m working on', docs: '/pad document the architecture of this project', conventions: '/pad what conventions should this project follow?', playbooks: '/pad set up playbooks for our workflow', diff --git a/web/src/routes/[workspace]/[collection]/[slug]/+page.svelte b/web/src/routes/[workspace]/[collection]/[slug]/+page.svelte index 0d10fe31..22ef1b15 100644 --- a/web/src/routes/[workspace]/[collection]/[slug]/+page.svelte +++ b/web/src/routes/[workspace]/[collection]/[slug]/+page.svelte @@ -126,7 +126,7 @@ item = itemData; collection = collData; - // Fetch child item progress for any item (generalized from phases-only) + // Fetch child item progress for any item (generalized parent/child) try { const progress = await api.items.progress(wsSlug, itemData.slug); if (progress.total > 0) { @@ -464,7 +464,7 @@ try { await api.links.delete(wsSlug, linkId); itemLinks = itemLinks.filter(l => l.id !== linkId); - // Refresh item to update phase info + // Refresh item to update parent info const refreshed = await api.items.get(wsSlug, itemSlug); item = { ...refreshed, content: item.content }; toastStore.show('Relationship removed', 'success'); @@ -512,7 +512,7 @@ showAddLink = false; addLinkSearch = ''; addLinkResults = []; - // Refresh item to update phase info + // Refresh item to update parent info const refreshed = await api.items.get(wsSlug, itemSlug); item = { ...refreshed, content: item.content }; toastStore.show('Relationship added', 'success'); diff --git a/web/src/routes/[workspace]/conventions/+page.svelte b/web/src/routes/[workspace]/conventions/+page.svelte index c4a1c7fa..3da562c2 100644 --- a/web/src/routes/[workspace]/conventions/+page.svelte +++ b/web/src/routes/[workspace]/conventions/+page.svelte @@ -6,7 +6,7 @@ import { toastStore } from '$lib/stores/toast.svelte'; import { SvelteSet, SvelteMap } from 'svelte/reactivity'; - const TRIGGERS = ['always','on-task-start','on-task-complete','on-implement','on-commit','on-pr-create','on-phase-start','on-phase-complete','on-plan'] as const; + const TRIGGERS = ['always','on-task-start','on-task-complete','on-implement','on-commit','on-pr-create','on-plan-start','on-plan-complete','on-plan'] as const; type Trigger = typeof TRIGGERS[number]; const TRIGGER_META: Record = { @@ -16,8 +16,8 @@ 'on-implement': { icon: '\u{1F528}', label: 'On Implement' }, 'on-commit': { icon: '\u{1F4BE}', label: 'On Commit' }, 'on-pr-create': { icon: '\u{1F500}', label: 'On PR Create' }, - 'on-phase-start': { icon: '\u{1F3C1}', label: 'On Phase Start' }, - 'on-phase-complete': { icon: '\u{1F389}', label: 'On Phase Complete' }, + 'on-plan-start': { icon: '\u{1F3C1}', label: 'On Plan Start' }, + 'on-plan-complete': { icon: '\u{1F389}', label: 'On Plan Complete' }, 'on-plan': { icon: '\u{1F4CB}', label: 'On Plan' }, };