diff --git a/.windsurf/workflows/kanban-sync.md b/.claude/commands/kanban-sync.md similarity index 76% rename from .windsurf/workflows/kanban-sync.md rename to .claude/commands/kanban-sync.md index b123de1..470de65 100644 --- a/.windsurf/workflows/kanban-sync.md +++ b/.claude/commands/kanban-sync.md @@ -1,11 +1,9 @@ --- -name: "Kanban Sync" -description: Reconcile Planka board state with OpenSpec changes. Planka is a read-only dashboard - OpenSpec is the source of truth. -category: Workflow -tags: [workflow, kanban, planka, tasks, project-management, sync] +description: Reconcile Planka board state with OpenSpec changes +allowed-tools: Bash, Read, Grep, Glob, AskUserQuestion --- -# Planka ↔ OpenSpec Reconciliation Sync +# Planka <-> OpenSpec Reconciliation Sync **OpenSpec is the source of truth for all agentic work.** Planka provides a read-only view so humans and other agents can see what's happening. This workflow reads OpenSpec state and reconciles Planka to match. @@ -25,7 +23,7 @@ If this fails, skip the Planka sync silently. Planka sync is best-effort - never 1. **OpenSpec owns the state** - `tasks.md`, artifacts, and change status live in OpenSpec 2. **Planka is a projection** - it reflects OpenSpec state, never the other way around -3. **Sync is one-directional** - OpenSpec → Planka, never Planka → OpenSpec +3. **Sync is one-directional** - OpenSpec -> Planka, never Planka -> OpenSpec 4. **Sync is idempotent** - running it twice produces the same result 5. **Sync is best-effort** - if Planka is down, work continues uninterrupted 6. **Non-agentic work** is managed directly in Planka (no OpenSpec involvement) @@ -34,7 +32,7 @@ If this fails, skip the Planka sync silently. Planka sync is best-effort - never ## When This Runs -This workflow is triggered automatically after any opsx workflow completes (via project-level instruction in `.windsurf/rules/kanban-update.md`). It can also be invoked manually via `/kanban-tasks`. +This workflow is triggered automatically after any opsx workflow completes (via project-level instruction in `CLAUDE.md`). It can also be invoked manually via `/kanban-sync`. --- @@ -81,7 +79,7 @@ PROJECT_ID=$(pcli project create --name "$PROJECT_NAME" --type "public" | jq -r ```bash # Get project details to find boards -BOARD_ID=$(pcli project get $PROJECT_ID | jq -r --arg name "$BOARD_NAME" '.data.included.boards[] | select(.name == $name) | .id') +BOARD_ID=$(pcli board list --project "$PROJECT_NAME" | jq -r --arg name "$BOARD_NAME" '.data[] | select(.name == $name) | .id') ``` If no board found: @@ -93,7 +91,7 @@ BOARD_ID=$(pcli board create --project $PROJECT_ID --name "$BOARD_NAME" | jq -r After obtaining the board, get its current lists: ```bash -EXISTING_LISTS=$(pcli board get $BOARD_ID | jq -r '.data.included.lists[]? | .name') +EXISTING_LISTS=$(pcli board get $BOARD_ID | jq -r '.data.lists[]? | .name') ``` Create any missing lists with explicit positions to maintain correct ordering: @@ -112,7 +110,7 @@ Skip any list that already exists (match by name). ### 5. Find or create the `agent` label ```bash -LABEL_ID=$(pcli board get $BOARD_ID | jq -r '.data.included.labels[]? | select(.name == "agent") | .id') +LABEL_ID=$(pcli board get $BOARD_ID | jq -r '.data.labels[]? | select(.name == "agent") | .id') ``` If no `agent` label found: @@ -171,7 +169,7 @@ For each active OpenSpec change that has no matching Planka card: ```bash # Determine the correct list based on change state (see step 2) -LIST_ID=$(pcli board get $BOARD_ID | jq -r --arg list "" '.data.included.lists[] | select(.name == $list) | .id') +LIST_ID=$(pcli board get $BOARD_ID | jq -r --arg list "" '.data.lists[] | select(.name == $list) | .id') CARD_ID=$(pcli card create --list $LIST_ID --name "" --description "" | jq -r '.data.id') # Add agent label @@ -183,27 +181,44 @@ pcli card add-label $CARD_ID --label $LABEL_ID For each agent-labelled card that exists but is in the wrong list (based on current OpenSpec state): ```bash -TARGET_LIST_ID=$(pcli board get $BOARD_ID | jq -r --arg list "" '.data.included.lists[] | select(.name == $list) | .id') +TARGET_LIST_ID=$(pcli board get $BOARD_ID | jq -r --arg list "" '.data.lists[] | select(.name == $list) | .id') pcli card move $CARD_ID --list $TARGET_LIST_ID ``` This ensures cards move through the board as work progresses: -- `Planning` → `In Progress` when artifacts are complete and apply begins -- `In Progress` → `Review` when all tasks are marked done -- `Review` → `Done` when the change is archived +- `Planning` -> `In Progress` when artifacts are complete and apply begins +- `In Progress` -> `Review` when all tasks are marked done +- `Review` -> `Done` when the change is archived ### 6. Reconcile: sync task lists For each OpenSpec change that has a `tasks.md`: -- If the Planka card has no task list → create one and add all tasks -- If the Planka card has a task list → compare task names and completion state, update as needed +First, check if the card already has task lists by using `pcli card get`: ```bash -# Create task list if missing -TL_ID=$(pcli task-list create --card $CARD_ID --name "Implementation" --show-on-front | jq -r '.data.id') +# Get card details including existing task lists and tasks +CARD_DATA=$(pcli card get $CARD_ID) +EXISTING_TL=$(echo "$CARD_DATA" | jq -r '.data.taskLists[0].id // empty') +``` + +- If `EXISTING_TL` is empty (no task list exists) -> create one and add all tasks +- If `EXISTING_TL` is set (task list already exists) -> compare existing tasks by name and update completion state as needed; only create tasks that don't already exist + +```bash +# Create task list ONLY if none exists +if [ -z "$EXISTING_TL" ]; then + TL_ID=$(pcli task-list create --card $CARD_ID --name "Implementation" --show-on-front | jq -r '.data.id') +else + TL_ID="$EXISTING_TL" +fi + +# Get existing task names to avoid duplicates +EXISTING_TASKS=$(echo "$CARD_DATA" | jq -r '.data.tasks[] | select(.taskListId == "'$TL_ID'") | .name') # For each task in tasks.md: +# - If a task with the same name already exists, update its completion state if needed +# - If no matching task exists, create it pcli task create --task-list $TL_ID --name "" # For tasks already in Planka, update completion state to match tasks.md: @@ -213,10 +228,10 @@ pcli task update --completed # if tasks.md shows [x] ### 7. Reconcile: move completed/archived changes For each agent-labelled Planka card that has no matching active OpenSpec change: -- The change was likely archived → move the card to "Done" +- The change was likely archived -> move the card to "Done" ```bash -DONE_LIST_ID=$(pcli board get $BOARD_ID | jq -r '.data.included.lists[] | select(.name == "Done") | .id') +DONE_LIST_ID=$(pcli board get $BOARD_ID | jq -r '.data.lists[] | select(.name == "Done") | .id') pcli card move $CARD_ID --list $DONE_LIST_ID ``` @@ -237,8 +252,8 @@ After reconciliation, briefly summarise what changed: Cards **without** the `agent` label are human-managed and fully read-write. The kanban skill (`/kanban`) handles these directly - creating cards, moving them, adding checklists, etc. The distinction: -- **Has `agent` label** → read-only projection, managed by this sync workflow -- **No `agent` label** → regular Planka card, managed directly by humans +- **Has `agent` label** -> read-only projection, managed by this sync workflow +- **No `agent` label** -> regular Planka card, managed directly by humans --- diff --git a/.windsurf/workflows/opsx-apply.md b/.claude/commands/opsx/apply.md similarity index 100% rename from .windsurf/workflows/opsx-apply.md rename to .claude/commands/opsx/apply.md diff --git a/.windsurf/workflows/opsx-archive.md b/.claude/commands/opsx/archive.md similarity index 100% rename from .windsurf/workflows/opsx-archive.md rename to .claude/commands/opsx/archive.md diff --git a/.windsurf/workflows/opsx-bulk-archive.md b/.claude/commands/opsx/bulk-archive.md similarity index 100% rename from .windsurf/workflows/opsx-bulk-archive.md rename to .claude/commands/opsx/bulk-archive.md diff --git a/.windsurf/workflows/opsx-continue.md b/.claude/commands/opsx/continue.md similarity index 100% rename from .windsurf/workflows/opsx-continue.md rename to .claude/commands/opsx/continue.md diff --git a/.windsurf/workflows/opsx-explore.md b/.claude/commands/opsx/explore.md similarity index 100% rename from .windsurf/workflows/opsx-explore.md rename to .claude/commands/opsx/explore.md diff --git a/.windsurf/workflows/opsx-ff.md b/.claude/commands/opsx/ff.md similarity index 100% rename from .windsurf/workflows/opsx-ff.md rename to .claude/commands/opsx/ff.md diff --git a/.windsurf/workflows/opsx-new.md b/.claude/commands/opsx/new.md similarity index 100% rename from .windsurf/workflows/opsx-new.md rename to .claude/commands/opsx/new.md diff --git a/.windsurf/workflows/opsx-onboard.md b/.claude/commands/opsx/onboard.md similarity index 100% rename from .windsurf/workflows/opsx-onboard.md rename to .claude/commands/opsx/onboard.md diff --git a/.windsurf/workflows/opsx-sync.md b/.claude/commands/opsx/sync.md similarity index 100% rename from .windsurf/workflows/opsx-sync.md rename to .claude/commands/opsx/sync.md diff --git a/.windsurf/workflows/opsx-verify.md b/.claude/commands/opsx/verify.md similarity index 100% rename from .windsurf/workflows/opsx-verify.md rename to .claude/commands/opsx/verify.md diff --git a/.windsurf/skills/kanban/SKILL.md b/.claude/skills/kanban/SKILL.md similarity index 93% rename from .windsurf/skills/kanban/SKILL.md rename to .claude/skills/kanban/SKILL.md index 91046d3..3d88b37 100644 --- a/.windsurf/skills/kanban/SKILL.md +++ b/.claude/skills/kanban/SKILL.md @@ -1,10 +1,6 @@ --- name: kanban description: Manage Planka project boards using the pcli CLI. Use when the user wants to interact with Planka boards, cards, lists, tasks, labels, or comments. -compatibility: Requires pcli binary in PATH and PLANKA_URL + PLANKA_API_KEY environment variables set -metadata: - author: steve-cliff - version: "1.0" --- # pcli - Planka CLI @@ -128,10 +124,10 @@ Board details include lists directly in `.data.lists[]`, not in an `included` se pcli board get | jq '.data.lists[] | {id, name, position}' ``` -### Project Get Response -Project details include boards in `.data.included.boards[]`: +### Finding Boards in a Project +Use `board list --project` to find boards by project name: ```bash -pcli project get | jq '.data.included.boards[] | {id, name}' +pcli board list --project "" | jq '.data[] | {id, name}' ``` ## Error Handling @@ -182,7 +178,7 @@ if [ -z "$PROJECT_ID" ]; then fi # Find or create board -BOARD_ID=$(pcli project get $PROJECT_ID | jq -r --arg name "$BOARD_NAME" '.data.included.boards[] | select(.name == $name) | .id') +BOARD_ID=$(pcli board list --project "$PROJECT_NAME" | jq -r --arg name "$BOARD_NAME" '.data[] | select(.name == $name) | .id') if [ -z "$BOARD_ID" ]; then BOARD_ID=$(pcli board create --project $PROJECT_ID --name "$BOARD_NAME" | jq -r '.data.id') fi diff --git a/.windsurf/skills/openspec-apply-change/SKILL.md b/.claude/skills/openspec-apply-change/SKILL.md similarity index 100% rename from .windsurf/skills/openspec-apply-change/SKILL.md rename to .claude/skills/openspec-apply-change/SKILL.md diff --git a/.windsurf/skills/openspec-archive-change/SKILL.md b/.claude/skills/openspec-archive-change/SKILL.md similarity index 100% rename from .windsurf/skills/openspec-archive-change/SKILL.md rename to .claude/skills/openspec-archive-change/SKILL.md diff --git a/.windsurf/skills/openspec-bulk-archive-change/SKILL.md b/.claude/skills/openspec-bulk-archive-change/SKILL.md similarity index 100% rename from .windsurf/skills/openspec-bulk-archive-change/SKILL.md rename to .claude/skills/openspec-bulk-archive-change/SKILL.md diff --git a/.windsurf/skills/openspec-continue-change/SKILL.md b/.claude/skills/openspec-continue-change/SKILL.md similarity index 100% rename from .windsurf/skills/openspec-continue-change/SKILL.md rename to .claude/skills/openspec-continue-change/SKILL.md diff --git a/.windsurf/skills/openspec-explore/SKILL.md b/.claude/skills/openspec-explore/SKILL.md similarity index 100% rename from .windsurf/skills/openspec-explore/SKILL.md rename to .claude/skills/openspec-explore/SKILL.md diff --git a/.windsurf/skills/openspec-ff-change/SKILL.md b/.claude/skills/openspec-ff-change/SKILL.md similarity index 100% rename from .windsurf/skills/openspec-ff-change/SKILL.md rename to .claude/skills/openspec-ff-change/SKILL.md diff --git a/.windsurf/skills/openspec-new-change/SKILL.md b/.claude/skills/openspec-new-change/SKILL.md similarity index 100% rename from .windsurf/skills/openspec-new-change/SKILL.md rename to .claude/skills/openspec-new-change/SKILL.md diff --git a/.windsurf/skills/openspec-onboard/SKILL.md b/.claude/skills/openspec-onboard/SKILL.md similarity index 100% rename from .windsurf/skills/openspec-onboard/SKILL.md rename to .claude/skills/openspec-onboard/SKILL.md diff --git a/.windsurf/skills/openspec-sync-specs/SKILL.md b/.claude/skills/openspec-sync-specs/SKILL.md similarity index 100% rename from .windsurf/skills/openspec-sync-specs/SKILL.md rename to .claude/skills/openspec-sync-specs/SKILL.md diff --git a/.windsurf/skills/openspec-verify-change/SKILL.md b/.claude/skills/openspec-verify-change/SKILL.md similarity index 100% rename from .windsurf/skills/openspec-verify-change/SKILL.md rename to .claude/skills/openspec-verify-change/SKILL.md diff --git a/.windsurf/rules/kanban-update.md b/.windsurf/rules/kanban-update.md deleted file mode 100644 index 3cb31ca..0000000 --- a/.windsurf/rules/kanban-update.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -trigger: always_on ---- - -## Planka Sync - -After completing any opsx workflow (/opsx:new, /opsx:ff, /opsx:continue, /opsx:apply, /opsx:verify, /opsx:archive, /opsx:bulk-archive), automatically reconcile Planka board state with OpenSpec by: - -1. Reading the kanban-sync workflow documentation at `.windsurf/workflows/kanban-sync.md` -2. Implementing the reconciliation steps programmatically using `pcli` commands -3. Following the workflow logic to sync OpenSpec changes to Planka cards - -This ensures the Planka dashboard stays current without modifying the opsx workflows themselves. - -If the sync fails (pcli unavailable, network issues), log a brief warning and continue. Never block or retry - Planka sync is best-effort. Do NOT attempt to execute the markdown file as a bash script - it is documentation that must be implemented programmatically. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..5c7fb7b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,58 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +PCLI is a Go CLI tool for interacting with the Planka (v2.0) project management API. It uses the Cobra framework with minimal dependencies (only Cobra + standard library). + +## Build & Test Commands + +```bash +go build -o pcli . # Build for development +./build.sh # Multi-platform release build (linux/darwin/windows) +./test.sh # Integration tests (requires live Planka instance) +``` + +Integration tests require `PLANKA_URL` and `PLANKA_API_KEY` environment variables pointing to a running Planka instance. + +There is no unit test suite or linter configured — tests are shell-based integration tests in `test.sh`. + +## Architecture + +**Entry point**: `main.go` → `cmd.Execute()` + +**Package layout**: +- `cmd/` — Cobra command definitions, one file per resource (project.go, board.go, card.go, etc.) +- `client/` — HTTP API client, one file per resource matching cmd/ structure +- `model/types.go` — All domain types (Project, Board, Card, List, Label, Task, Comment, etc.) +- `output/output.go` — Dual-format output: JSON envelope (`{data, error}`) or human-readable table via tabwriter +- `logging/logging.go` — slog-based structured JSON logging to stderr + +**Command hierarchy**: `pcli ` (e.g., `pcli card create --list --name "task"`) + +**Key patterns**: +- Global flags (`--format`, `--url`, `--api-key`, `--log-level`) set in `cmd/root.go` PersistentPreRunE, which validates config and initializes the API client +- Client methods follow `List*/Get*/Create*/Update*/Delete*` naming +- API responses use typed structs with included relationships (e.g., board responses include lists and cards) +- `friendlyAPIError()` in `cmd/errors.go` translates HTTP status codes to contextual messages +- `DoWithFallback` in client supports trying alternate API endpoint patterns +- Output uses reflection-based table rendering per type + +**Configuration**: Environment variables `PLANKA_URL` and `PLANKA_API_KEY`, overridable via CLI flags. + +## Adding a New Resource + +1. Define model structs in `model/types.go` +2. Add client methods in `client/.go` +3. Add command definitions in `cmd/.go` with subcommands (list, get, create, update, delete) +4. Wire the parent command into `root.go` init() + +## Planka Sync + +**IMPORTANT** +After completing any opsx workflow (/opsx:new, /opsx:ff, /opsx:continue, +/opsx:apply, /opsx:verify, /opsx:archive, /opsx:bulk-archive), automatically +reconcile Planka board state by running `/kanban-sync`. +If the sync fails, log a brief warning and continue. Never block or retry — +Planka sync is best-effort. diff --git a/README.md b/README.md index 03c4524..06bc0cf 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,9 @@ pcli label delete ```bash # Show status summary of all boards and their lists pcli status + +# Show status filtered by project name +pcli status --project "MyProject" ``` ## Output Formats diff --git a/client/boards.go b/client/boards.go index f220e86..7b5821e 100644 --- a/client/boards.go +++ b/client/boards.go @@ -43,8 +43,11 @@ func (c *Client) GetBoard(ctx context.Context, id string) (*model.Board, error) var response struct { Item model.Board `json:"item"` Included struct { - Lists []model.List `json:"lists"` - Cards []model.Card `json:"cards"` + Lists []model.List `json:"lists"` + Cards []model.Card `json:"cards"` + Labels []model.Label `json:"labels"` + CardLabels []model.CardLabel `json:"cardLabels"` + CardMemberships []model.CardMembership `json:"cardMemberships"` } `json:"included"` } @@ -54,6 +57,9 @@ func (c *Client) GetBoard(ctx context.Context, id string) (*model.Board, error) response.Item.Lists = response.Included.Lists response.Item.Cards = response.Included.Cards + response.Item.Labels = response.Included.Labels + response.Item.CardLabels = response.Included.CardLabels + response.Item.CardMemberships = response.Included.CardMemberships return &response.Item, nil } diff --git a/client/cards.go b/client/cards.go index 99ab311..53422fd 100644 --- a/client/cards.go +++ b/client/cards.go @@ -9,21 +9,31 @@ import ( "git.franklin.lab/steve.cliff/pcli/model" ) -func (c *Client) GetCard(ctx context.Context, id string) (*model.Card, error) { +func (c *Client) GetCard(ctx context.Context, id string) (*model.CardDetail, error) { data, err := c.DoNoBody(ctx, "GET", fmt.Sprintf("/api/cards/%s", id)) if err != nil { return nil, err } var response struct { - Item model.Card `json:"item"` + Item model.Card `json:"item"` + Included struct { + TaskLists []model.TaskList `json:"taskLists"` + Tasks []model.Task `json:"tasks"` + } `json:"included"` } if err := json.Unmarshal(data, &response); err != nil { return nil, fmt.Errorf("failed to unmarshal card response: %w", err) } - return &response.Item, nil + result := &model.CardDetail{ + Card: response.Item, + TaskLists: response.Included.TaskLists, + Tasks: response.Included.Tasks, + } + + return result, nil } func (c *Client) CreateCard(ctx context.Context, listId string, fields map[string]any) (*model.Card, error) { @@ -195,11 +205,34 @@ func (c *Client) ListCardsByBoard(ctx context.Context, boardId string, limit int listNames[list.ID] = name } + // Build label ID -> name map + labelNames := make(map[string]string) + for _, label := range board.Labels { + name := "" + if label.Name != nil { + name = *label.Name + } + labelNames[label.ID] = name + } + + // Build card ID -> label names map + cardLabelNames := make(map[string][]string) + for _, cl := range board.CardLabels { + if name, ok := labelNames[cl.LabelID]; ok { + cardLabelNames[cl.CardID] = append(cardLabelNames[cl.CardID], name) + } + } + var allCards []model.CardWithList for _, card := range board.Cards { + labels := cardLabelNames[card.ID] + if labels == nil { + labels = []string{} + } cardWithList := model.CardWithList{ Card: card, ListName: listNames[card.ListID], + Labels: labels, } allCards = append(allCards, cardWithList) diff --git a/cmd/status.go b/cmd/status.go index d8184e5..5c3c55c 100644 --- a/cmd/status.go +++ b/cmd/status.go @@ -11,15 +11,33 @@ import ( var statusCmd = &cobra.Command{ Use: "status", - Short: "Show status summary of all boards and their lists", - Long: "Displays a summary of all boards, their lists, and the number of cards in each list", + Short: "Show status summary of boards and their lists", + Long: "Displays a summary of boards, their lists, and the number of cards in each list.\nUse --project to filter by project name.", RunE: func(cmd *cobra.Command, args []string) error { + projectName, _ := cmd.Flags().GetString("project") + // Get all boards boards, err := getClient().ListBoards(getContext()) if err != nil { return fmt.Errorf("failed to list boards: %w", err) } + // Filter boards by project if --project flag is provided + if projectName != "" { + projectID, err := resolveProjectNameToID(projectName) + if err != nil { + return err + } + + filtered := boards[:0] + for _, board := range boards { + if board.ProjectID == projectID { + filtered = append(filtered, board) + } + } + boards = filtered + } + // Build status summary with error collection summary := model.StatusSummary{ TotalBoards: len(boards), @@ -102,4 +120,6 @@ var statusCmd = &cobra.Command{ func init() { rootCmd.AddCommand(statusCmd) + + statusCmd.Flags().String("project", "", "Filter status by project name") } diff --git a/examples/workflows/kanban/kanban.md b/examples/workflows/kanban/kanban.md index 7a3e480..feb68ad 100644 --- a/examples/workflows/kanban/kanban.md +++ b/examples/workflows/kanban/kanban.md @@ -49,7 +49,7 @@ Most commands require IDs. Discover them by querying first: pcli board list | jq '.data[] | {id, title}' # Find lists on a board -pcli board get | jq '.data.included.lists[] | {id, title}' +pcli board get | jq '.data.lists[] | {id, name}' # Find cards on a list or board pcli card list --board | jq '.data[] | {id, name}' diff --git a/model/types.go b/model/types.go index 1af1f04..5b5c067 100644 --- a/model/types.go +++ b/model/types.go @@ -30,8 +30,11 @@ type Board struct { ExpandTaskListsByDefault bool `json:"expandTaskListsByDefault"` CreatedAt *string `json:"createdAt"` UpdatedAt *string `json:"updatedAt"` - Lists []List `json:"lists,omitempty"` - Cards []Card `json:"cards,omitempty"` + Lists []List `json:"lists,omitempty"` + Cards []Card `json:"cards,omitempty"` + Labels []Label `json:"labels,omitempty"` + CardLabels []CardLabel `json:"cardLabels,omitempty"` + CardMemberships []CardMembership `json:"cardMemberships,omitempty"` } type List struct { @@ -71,9 +74,16 @@ type Card struct { UpdatedAt *string `json:"updatedAt"` } +type CardDetail struct { + Card + TaskLists []TaskList `json:"taskLists,omitempty"` + Tasks []Task `json:"tasks,omitempty"` +} + type CardWithList struct { Card - ListName string `json:"listName"` + ListName string `json:"listName"` + Labels []string `json:"labels"` } type Comment struct { diff --git a/openspec/changes/add-project-filter-to-status/.openspec.yaml b/openspec/changes/add-project-filter-to-status/.openspec.yaml new file mode 100644 index 0000000..e3dce8f --- /dev/null +++ b/openspec/changes/add-project-filter-to-status/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-02-18 diff --git a/openspec/changes/add-project-filter-to-status/design.md b/openspec/changes/add-project-filter-to-status/design.md new file mode 100644 index 0000000..c596c4c --- /dev/null +++ b/openspec/changes/add-project-filter-to-status/design.md @@ -0,0 +1,38 @@ +## Context + +The `pcli status` command fetches all boards and aggregates card counts per list. It has no filtering capability. The `board list` command already supports `--project ` filtering via a `resolveProjectNameToID()` helper in `cmd/board.go` that does case-insensitive name matching against all projects. The Board model already contains a `ProjectID` field. + +## Goals / Non-Goals + +**Goals:** +- Add `--project` flag to `pcli status` that filters boards by project name +- Reuse the existing `resolveProjectNameToID()` pattern from `cmd/board.go` +- Update help text and README to document the new flag + +**Non-Goals:** +- Filtering by project ID (only name-based, consistent with `board list`) +- Multiple project filtering (single project only) +- Caching project lookups across commands +- Changing the status output structure or adding new fields + +## Decisions + +### Filter boards before fetching details +**Decision**: Apply the project filter on the initial `ListBoards()` result, before calling `GetBoard()` for each board. This avoids unnecessary API calls for boards that will be filtered out. + +**Alternative**: Filter after fetching all board details. Rejected because it wastes API calls and time for boards not in the target project. + +### Reuse resolveProjectNameToID from board.go +**Decision**: Call the existing `resolveProjectNameToID()` function directly — it is already exported within the `cmd` package. + +**Alternative**: Extract to a shared utility. Rejected as premature — two call sites within the same package doesn't warrant a new abstraction. + +### totalBoards reflects filtered count +**Decision**: When `--project` is used, `totalBoards` in the output reflects only the matching boards, not all boards. This matches the semantics of "how many boards am I looking at." + +**Alternative**: Show total and filtered counts separately. Rejected — adds complexity for minimal value. + +## Risks / Trade-offs + +- **[Extra API call]** → When `--project` is used, an additional `ListProjects()` call is made to resolve the name. This is lightweight and consistent with `board list --project`. +- **[No partial match]** → Project name must match exactly (case-insensitive). Consistent with existing `board list` behavior. Users who misspell get a clear "project not found" error. diff --git a/openspec/changes/add-project-filter-to-status/proposal.md b/openspec/changes/add-project-filter-to-status/proposal.md new file mode 100644 index 0000000..c04d0ed --- /dev/null +++ b/openspec/changes/add-project-filter-to-status/proposal.md @@ -0,0 +1,30 @@ +## Why + +The `pcli status` command currently shows a summary of **all** boards across all projects. In instances with many projects, this produces noisy output and makes it difficult to focus on a specific project's boards. The `board list` command already supports `--project` filtering — the status command should offer the same capability for consistency and usability. + +## What Changes + +- Add an optional `--project` flag to `pcli status` that filters the status summary to boards belonging to the named project +- When `--project` is provided, only boards matching that project are included; `totalBoards` reflects the filtered count +- When `--project` is omitted, behavior is unchanged (all boards shown) +- Update command help text (`Short`, `Long`) to mention the filtering capability +- Update README status section with `--project` usage example +- Reuse the existing `resolveProjectNameToID()` helper from `cmd/board.go` + +## Capabilities + +### New Capabilities + +_None — this enhances existing capabilities._ + +### Modified Capabilities + +- `status-command`: Add optional `--project` flag for project-scoped filtering +- `cli-commands`: Update status command documentation to include `--project` flag + +## Impact + +- **Code**: `cmd/status.go` (add flag, filter logic), `cmd/board.go` (no change — reuses existing helper) +- **Documentation**: `README.md` status section, command help strings +- **API**: No new API calls — uses existing `ListProjects()` for name resolution, same `ListBoards()` + `GetBoard()` flow +- **Breaking changes**: None — flag is optional, default behavior preserved diff --git a/openspec/changes/add-project-filter-to-status/specs/cli-commands/spec.md b/openspec/changes/add-project-filter-to-status/specs/cli-commands/spec.md new file mode 100644 index 0000000..c3e0a77 --- /dev/null +++ b/openspec/changes/add-project-filter-to-status/specs/cli-commands/spec.md @@ -0,0 +1,20 @@ +## MODIFIED Requirements + +### Requirement: Status command +The system SHALL provide a top-level `status` command registered directly on the root command (not as a subcommand of any resource group). `pcli status` SHALL take no positional arguments. The command SHALL accept an optional `--project ` flag (string) to filter boards by project name. The command SHALL fetch all boards via `ListBoards`, optionally filter by project, then fetch each board's details via `GetBoard` sequentially, aggregate card counts per list, and output the result via the standard `output.Print` mechanism respecting the global `--format` flag. + +#### Scenario: Run status command +- **WHEN** `pcli status` is executed +- **THEN** the system SHALL output a summary of all boards with their lists and card counts + +#### Scenario: Run status command with project filter +- **WHEN** `pcli status --project "MyProject"` is executed +- **THEN** the system SHALL output a summary of only boards belonging to "MyProject" + +#### Scenario: Status command respects format flag +- **WHEN** `pcli status --format table` is executed +- **THEN** the output SHALL be in table format + +#### Scenario: Status command default format +- **WHEN** `pcli status` is executed without a `--format` flag +- **THEN** the output SHALL be in JSON envelope format diff --git a/openspec/changes/add-project-filter-to-status/specs/status-command/spec.md b/openspec/changes/add-project-filter-to-status/specs/status-command/spec.md new file mode 100644 index 0000000..1735ef3 --- /dev/null +++ b/openspec/changes/add-project-filter-to-status/specs/status-command/spec.md @@ -0,0 +1,49 @@ +## ADDED Requirements + +### Requirement: Status command project filtering +The system SHALL accept an optional `--project ` flag on the `pcli status` command. When `--project` is provided, the system SHALL resolve the project name to a project ID using case-insensitive exact matching against all accessible projects. The system SHALL then filter the board list to include only boards whose `projectId` matches the resolved project ID. The `totalBoards` count in the output SHALL reflect the filtered board count. When `--project` is omitted, behavior SHALL be unchanged (all boards shown). + +#### Scenario: Status filtered by project name +- **WHEN** `pcli status --project "MyProject"` is executed and the project exists with 2 boards +- **THEN** the output SHALL include only the 2 boards belonging to "MyProject" +- **AND** `totalBoards` SHALL be 2 + +#### Scenario: Status filtered by project name case-insensitive +- **WHEN** `pcli status --project "myproject"` is executed and a project named "MyProject" exists +- **THEN** the output SHALL include boards belonging to "MyProject" + +#### Scenario: Status with project filter and no matching boards +- **WHEN** `pcli status --project "EmptyProject"` is executed and the project exists but has no boards +- **THEN** the output SHALL show `totalBoards` as 0 and an empty boards array + +#### Scenario: Status with project not found +- **WHEN** `pcli status --project "NonExistent"` is executed and no project with that name exists +- **THEN** the system SHALL output an error "project not found: NonExistent" +- **AND** the system SHALL exit with code 1 + +#### Scenario: Status without project flag +- **WHEN** `pcli status` is executed without `--project` +- **THEN** the output SHALL include all boards across all projects (unchanged behavior) + +## MODIFIED Requirements + +### Requirement: Status command summary output +The system SHALL provide a top-level `pcli status` command that outputs a summary of all boards (or boards filtered by `--project`), their lists, and card counts. The summary SHALL include the total number of boards. For each board, the summary SHALL include the board name and a breakdown of each list within that board showing the list name, the number of open cards (where `isClosed` is false), and the number of closed cards (where `isClosed` is true). Empty lists SHALL be included in the output with 0 open and 0 closed cards. + +#### Scenario: Status with multiple boards and lists +- **WHEN** `pcli status` is executed and there are boards with lists containing cards +- **THEN** the output SHALL include the total board count +- **AND** each board SHALL list all its lists with open and closed card counts + +#### Scenario: Status with empty lists +- **WHEN** a board contains a list with no cards +- **THEN** that list SHALL appear in the output with 0 open cards and 0 closed cards + +#### Scenario: Status with no boards +- **WHEN** `pcli status` is executed and there are no boards +- **THEN** the output SHALL indicate 0 boards + +#### Scenario: Status with closed cards +- **WHEN** a list contains both open and closed cards +- **THEN** the open card count SHALL exclude closed cards +- **AND** the closed card count SHALL be shown separately diff --git a/openspec/changes/add-project-filter-to-status/tasks.md b/openspec/changes/add-project-filter-to-status/tasks.md new file mode 100644 index 0000000..954df52 --- /dev/null +++ b/openspec/changes/add-project-filter-to-status/tasks.md @@ -0,0 +1,19 @@ +## 1. Core Implementation + +- [x] 1.1 Add `--project` flag to `statusCmd` in `cmd/status.go` +- [x] 1.2 Add project filtering logic: resolve name via `resolveProjectNameToID()`, filter boards by `ProjectID` before fetching details +- [x] 1.3 Update `totalBoards` count to reflect filtered results + +## 2. Command Help Text + +- [x] 2.1 Update `Short` and `Long` descriptions on `statusCmd` to mention `--project` filtering + +## 3. Documentation + +- [x] 3.1 Update README.md status section with `--project` usage example + +## 4. Verification + +- [x] 4.1 Build and manually test `pcli status --project ` with an existing project +- [x] 4.2 Test error case: `pcli status --project "nonexistent"` returns "project not found" error +- [x] 4.3 Test default behavior: `pcli status` without `--project` still shows all boards diff --git a/openspec/changes/expand-board-included-parsing/.openspec.yaml b/openspec/changes/expand-board-included-parsing/.openspec.yaml new file mode 100644 index 0000000..e3dce8f --- /dev/null +++ b/openspec/changes/expand-board-included-parsing/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-02-18 diff --git a/openspec/changes/expand-board-included-parsing/design.md b/openspec/changes/expand-board-included-parsing/design.md new file mode 100644 index 0000000..c9e2568 --- /dev/null +++ b/openspec/changes/expand-board-included-parsing/design.md @@ -0,0 +1,37 @@ +## Context + +The Planka v2 API `GET /api/boards/:id` response includes an `included` object with: `users`, `boardMemberships`, `labels`, `lists`, `cards`, `cardMemberships`, `cardLabels`, `taskLists`, `tasks`, `attachments`, `customFieldGroups`, `customFields`, `customFieldValues`. Currently `GetBoard` only parses `lists` and `cards`, discarding everything else. + +The `CardLabel` and `Label` types already exist in `model/types.go`. The `Board` struct has `Lists` and `Cards` fields but not `Labels`, `CardLabels`, or `CardMemberships`. + +## Goals / Non-Goals + +**Goals:** +- Parse `labels`, `cardLabels`, and `cardMemberships` from the `GetBoard` response +- Make label data available on the `Board` struct for downstream consumers +- Enrich `ListCardsByBoard` output so `card list --board` includes label names per card + +**Non-Goals:** +- Parsing all included fields (users, taskLists, attachments, customFields, etc.) — only what's needed now +- Adding label data to `card list --list` (uses a different API endpoint that doesn't include labels) +- Changing the `card get` response (already returns card-level data via a different endpoint) + +## Decisions + +### Add fields to Board struct rather than creating a separate BoardDetail type +**Decision**: Add `Labels`, `CardLabels`, and `CardMemberships` as optional fields on the existing `Board` struct with `omitempty`. + +**Alternative**: Create a `BoardDetail` struct for the enriched response. Rejected — the fields are simply absent when not fetched (e.g., `ListBoards`), and `omitempty` handles this cleanly without a type split. + +### Add Labels field to CardWithList for card list --board output +**Decision**: Add a `Labels` field (`[]string` of label names) to the `CardWithList` struct. `ListCardsByBoard` will resolve card→label associations via the `cardLabels` join table and `labels` list from the board response. + +**Alternative**: Return full `Label` objects per card. Rejected — label names are sufficient for display and filtering; full objects add noise to output. + +### Use join-table approach matching the API structure +**Decision**: Keep `CardLabel` as the join entity (cardId + labelId), and resolve to label names at the point of use (in `ListCardsByBoard`). This mirrors the Planka API's relational structure. + +## Risks / Trade-offs + +- **[Additive JSON fields]** → `card list --board` output will include a new `labels` array per card. Existing consumers that don't use this field are unaffected. Scripts using strict JSON parsing may need updating. +- **[Partial included parsing]** → We still skip users, taskLists, attachments, etc. This is intentional — parse only what's needed. Future changes can add more fields incrementally. diff --git a/openspec/changes/expand-board-included-parsing/proposal.md b/openspec/changes/expand-board-included-parsing/proposal.md new file mode 100644 index 0000000..d8643df --- /dev/null +++ b/openspec/changes/expand-board-included-parsing/proposal.md @@ -0,0 +1,26 @@ +## Why + +The `GetBoard` API response includes `cardLabels`, `labels`, and `cardMemberships` in its `included` data, but `pcli` only parses `lists` and `cards` — silently discarding the rest. This means there is no way to determine which labels are attached to which cards without making per-card API calls. The kanban sync workflow needs to identify agent-labelled cards from a board listing, and currently must fall back to name-matching because label data is unavailable. + +## What Changes + +- Expand the `Board` struct in `model/types.go` to include `Labels`, `CardLabels`, and `CardMemberships` fields +- Update `GetBoard` in `client/boards.go` to parse these additional `included` fields from the API response +- Update `ListCardsByBoard` to enrich card output with label names (via `cardLabels` join table + `labels`) + +## Capabilities + +### New Capabilities + +_None — this enhances existing capabilities._ + +### Modified Capabilities + +- `api-client`: `GetBoard` SHALL parse `labels`, `cardLabels`, and `cardMemberships` from the board response `included` data +- `card-operations`: `ListCardsByBoard` (card list --board) SHALL include label names on each card + +## Impact + +- **Code**: `model/types.go` (Board struct), `client/boards.go` (GetBoard parsing), `client/cards.go` (ListCardsByBoard enrichment) +- **API**: No new API calls — parsing data already returned by `GET /api/boards/:id` +- **Breaking changes**: None — new fields are additive; JSON output gains new fields but existing fields unchanged diff --git a/openspec/changes/expand-board-included-parsing/specs/api-client/spec.md b/openspec/changes/expand-board-included-parsing/specs/api-client/spec.md new file mode 100644 index 0000000..46a7f79 --- /dev/null +++ b/openspec/changes/expand-board-included-parsing/specs/api-client/spec.md @@ -0,0 +1,25 @@ +## MODIFIED Requirements + +### Requirement: Board operations +The client SHALL provide a method to get a single board by ID (`GET /boards/{id}`), list board actions (`GET /boards/{boardId}/actions`) with pagination support, create a board (`POST /projects/{projectId}/boards`), and delete a board (`DELETE /boards/{id}`). `GetBoard` SHALL parse the `included` object from the response and populate the Board model with `lists`, `cards`, `labels`, `cardLabels`, and `cardMemberships`. + +#### Scenario: Get board +- **WHEN** `GetBoard` is called with a board ID +- **THEN** the client SHALL send `GET /boards/{id}` and return a Board model including its included lists, cards, labels, cardLabels, and cardMemberships + +#### Scenario: Get board includes labels +- **WHEN** `GetBoard` is called and the board has labels defined +- **THEN** the returned Board SHALL contain a `Labels` slice with all board labels + +#### Scenario: Get board includes card-label associations +- **WHEN** `GetBoard` is called and cards on the board have labels attached +- **THEN** the returned Board SHALL contain a `CardLabels` slice with all card-label associations +- **AND** each `CardLabel` entry SHALL contain `cardId` and `labelId` fields + +#### Scenario: Get board includes card memberships +- **WHEN** `GetBoard` is called and cards on the board have members assigned +- **THEN** the returned Board SHALL contain a `CardMemberships` slice with all card-membership associations + +#### Scenario: List board actions +- **WHEN** `ListBoardActions` is called with a board ID and limit +- **THEN** the client SHALL paginate through `GET /boards/{boardId}/actions` and return action items diff --git a/openspec/changes/expand-board-included-parsing/specs/card-operations/spec.md b/openspec/changes/expand-board-included-parsing/specs/card-operations/spec.md new file mode 100644 index 0000000..2a9594c --- /dev/null +++ b/openspec/changes/expand-board-included-parsing/specs/card-operations/spec.md @@ -0,0 +1,23 @@ +## MODIFIED Requirements + +### Requirement: Enriched board-level card listing +The system SHALL provide a `card list --board ` operation that returns all cards across all lists in a board, with each card enriched with the `listName` field and a `labels` field. The operation SHALL: (1) call `GET /boards/{id}` to retrieve the board and its included lists, cards, labels, and cardLabels, (2) build a card-to-label-names map by joining `cardLabels` with `labels`, (3) inject `listName` and `labels` into each card. The `labels` field SHALL be an array of label name strings. The `--limit` flag SHALL apply to the total number of cards returned across all lists. + +#### Scenario: List all cards on a board +- **WHEN** `pcli card list --board ` is executed +- **THEN** the system SHALL return all cards from all lists in the board +- **AND** each card SHALL include a `listName` field with the name of its containing list +- **AND** each card SHALL include a `labels` field with an array of label names attached to that card + +#### Scenario: Card with multiple labels +- **WHEN** a card has two labels ("bug" and "urgent") attached +- **THEN** the `labels` array for that card SHALL contain both "bug" and "urgent" + +#### Scenario: Card with no labels +- **WHEN** a card has no labels attached +- **THEN** the `labels` array for that card SHALL be an empty array (not null) + +#### Scenario: Board card listing with limit +- **WHEN** `pcli card list --board --limit 10` is executed +- **THEN** the system SHALL return at most 10 cards total across all lists +- **AND** each card SHALL include the `listName` and `labels` fields diff --git a/openspec/changes/expand-board-included-parsing/tasks.md b/openspec/changes/expand-board-included-parsing/tasks.md new file mode 100644 index 0000000..1ff3fb8 --- /dev/null +++ b/openspec/changes/expand-board-included-parsing/tasks.md @@ -0,0 +1,15 @@ +## 1. Model Changes + +- [x] 1.1 Add `Labels []Label`, `CardLabels []CardLabel`, and `CardMemberships []CardMembership` fields to `Board` struct in `model/types.go` (with `json:",omitempty"`) +- [x] 1.2 Add `Labels []string` field to `CardWithList` struct in `model/types.go` + +## 2. Client Changes + +- [x] 2.1 Update `GetBoard` in `client/boards.go` to parse `labels`, `cardLabels`, and `cardMemberships` from `included` response +- [x] 2.2 Update `ListCardsByBoard` in `client/cards.go` to build label-name map from board's `CardLabels` and `Labels`, and populate `Labels` on each `CardWithList` + +## 3. Verification + +- [x] 3.1 Build and test `pcli board get ` — verify JSON output includes labels and cardLabels when present +- [x] 3.2 Test `pcli card list --board ` — verify each card includes a `labels` array +- [x] 3.3 Test card with no labels returns empty array (not null) diff --git a/openspec/config.yaml b/openspec/config.yaml deleted file mode 100644 index 392946c..0000000 --- a/openspec/config.yaml +++ /dev/null @@ -1,20 +0,0 @@ -schema: spec-driven - -# Project context (optional) -# This is shown to AI when creating artifacts. -# Add your tech stack, conventions, style guides, domain knowledge, etc. -# Example: -# context: | -# Tech stack: TypeScript, React, Node.js -# We use conventional commits -# Domain: e-commerce platform - -# Per-artifact rules (optional) -# Add custom rules for specific artifacts. -# Example: -# rules: -# proposal: -# - Keep proposals under 500 words -# - Always include a "Non-goals" section -# tasks: -# - Break tasks into chunks of max 2 hours diff --git a/output/output.go b/output/output.go index 7046c63..69567c2 100644 --- a/output/output.go +++ b/output/output.go @@ -109,6 +109,8 @@ func printTable(data any, w io.Writer) error { return printBoardTable([]model.Board{*data}, tw) case *model.Card: return printCardTable([]model.Card{*data}, tw) + case *model.CardDetail: + return printCardDetailTable(data, tw) case *model.Comment: return printCommentTable([]model.Comment{*data}, tw) case *model.TaskList: @@ -164,6 +166,29 @@ func printCardWithListTable(cards []model.CardWithList, tw *tabwriter.Writer) er return nil } +func printCardDetailTable(card *model.CardDetail, tw *tabwriter.Writer) error { + fmt.Fprintln(tw, "ID\tNAME\tLIST_ID\tTYPE\tCLOSED") + fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%v\n", card.ID, card.Name, card.ListID, card.Type, card.IsClosed) + + if len(card.TaskLists) > 0 { + fmt.Fprintln(tw) + fmt.Fprintln(tw, "TASK_LIST_ID\tTASK_LIST_NAME\tPOSITION") + for _, tl := range card.TaskLists { + fmt.Fprintf(tw, "%s\t%s\t%.0f\n", tl.ID, tl.Name, tl.Position) + } + } + + if len(card.Tasks) > 0 { + fmt.Fprintln(tw) + fmt.Fprintln(tw, "TASK_ID\tTASK_NAME\tTASK_LIST_ID\tCOMPLETED") + for _, t := range card.Tasks { + fmt.Fprintf(tw, "%s\t%s\t%s\t%v\n", t.ID, t.Name, t.TaskListID, t.IsCompleted) + } + } + + return nil +} + func printCommentTable(comments []model.Comment, tw *tabwriter.Writer) error { fmt.Fprintln(tw, "ID\tCARD_ID\tTEXT\tCREATED_AT") for _, c := range comments {