feat(sync): implement kanban-project-sync script with concurrency control and background execution
This commit is contained in:
+28
-265
@@ -1,282 +1,45 @@
|
||||
---
|
||||
description: Reconcile Planka board state with OpenSpec changes
|
||||
allowed-tools: Bash, Read, Grep, Glob, AskUserQuestion
|
||||
allowed-tools: Bash, Read
|
||||
---
|
||||
|
||||
# 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.
|
||||
Runs the `kanban-project-sync` script to reconcile Planka board state with OpenSpec changes.
|
||||
|
||||
## Prerequisites
|
||||
## How It Works
|
||||
|
||||
Before syncing, verify connectivity:
|
||||
The sync is handled by the `kanban-project-sync` bash script (on PATH). It:
|
||||
1. Checks Planka connectivity
|
||||
2. Bootstraps project/board/lists/label infrastructure (idempotent)
|
||||
3. Reads OpenSpec state and maps changes to board lists
|
||||
4. Creates/moves/updates Planka cards and task checklists
|
||||
5. Moves orphaned cards to Done
|
||||
|
||||
**OpenSpec is the source of truth.** Planka is a read-only projection. Sync is one-directional (OpenSpec -> Planka) and idempotent.
|
||||
|
||||
## Running the Sync
|
||||
|
||||
Read project config and invoke the script in background mode:
|
||||
|
||||
```bash
|
||||
pcli status
|
||||
PROJECT_NAME=$(yq -r '.planka.project' project.yaml)
|
||||
BOARD_NAME=$(yq -r '.planka.board' project.yaml)
|
||||
kanban-project-sync --project "$PROJECT_NAME" --board "$BOARD_NAME" --background
|
||||
```
|
||||
|
||||
If this fails, skip the Planka sync silently. Planka sync is best-effort - never block agentic work because Planka is unavailable.
|
||||
The `--background` flag makes the script fire-and-forget — it detaches and logs to `/tmp/kanban-project-sync-<project>-<board>.log`.
|
||||
|
||||
---
|
||||
## Concurrency
|
||||
|
||||
## Core Principles
|
||||
|
||||
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
|
||||
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)
|
||||
|
||||
---
|
||||
|
||||
## When This Runs
|
||||
|
||||
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`.
|
||||
|
||||
---
|
||||
|
||||
## Board Structure
|
||||
|
||||
The board lists map to OpenSpec lifecycle stages:
|
||||
|
||||
| List | Position | Purpose |
|
||||
|------|----------|---------|
|
||||
| **Backlog** | 1 | Non-agentic work items (human-managed, read-write) |
|
||||
| **To Do** | 2 | Non-agentic work items ready to start (human-managed, read-write) |
|
||||
| **Planning** | 3 | OpenSpec changes with artifacts still being created (`opsx:new`, `opsx:continue`, `opsx:ff`) |
|
||||
| **In Progress** | 4 | Active implementation — tasks being worked (`opsx:apply`) |
|
||||
| **Review** | 5 | All tasks complete, awaiting verification (`opsx:verify`) |
|
||||
| **Done** | 6 | Completed and archived (`opsx:archive`) |
|
||||
|
||||
---
|
||||
|
||||
## Bootstrap: Ensure Project, Board, Lists, and Label Exist
|
||||
|
||||
Before reconciling, ensure all required Planka infrastructure exists. This makes the sync self-bootstrapping — running it on a fresh Planka instance will create everything needed.
|
||||
|
||||
### 1. Read project config
|
||||
|
||||
```bash
|
||||
PROJECT_NAME=$(yq '.planka.project' project.yaml)
|
||||
BOARD_NAME=$(yq '.planka.board' project.yaml)
|
||||
```
|
||||
|
||||
If `project.yaml` doesn't exist or has no `planka` section, ask the user for the project and board name, then offer to create the file.
|
||||
|
||||
### 2. Find or create the project
|
||||
|
||||
```bash
|
||||
PROJECT_ID=$(pcli project list | jq -r --arg name "$PROJECT_NAME" '.data[] | select(.name == $name) | .id')
|
||||
```
|
||||
|
||||
If no project found:
|
||||
```bash
|
||||
PROJECT_ID=$(pcli project create --name "$PROJECT_NAME" --type "public" | jq -r '.data.id')
|
||||
```
|
||||
|
||||
### 3. Find or create the board
|
||||
|
||||
```bash
|
||||
# Get project details to find boards
|
||||
BOARD_ID=$(pcli board list --project "$PROJECT_NAME" | jq -r --arg name "$BOARD_NAME" '.data[] | select(.name == $name) | .id')
|
||||
```
|
||||
|
||||
If no board found:
|
||||
```bash
|
||||
BOARD_ID=$(pcli board create --project $PROJECT_ID --name "$BOARD_NAME" | jq -r '.data.id')
|
||||
```
|
||||
|
||||
### 4. Find or create the lists
|
||||
|
||||
After obtaining the board, get its current lists:
|
||||
```bash
|
||||
EXISTING_LISTS=$(pcli board get $BOARD_ID | jq -r '.data.lists[]? | .name')
|
||||
```
|
||||
|
||||
Create any missing lists with explicit positions to maintain correct ordering:
|
||||
```bash
|
||||
# Only create lists that don't already exist
|
||||
pcli list create --board $BOARD_ID --name "Backlog" --position 65536
|
||||
pcli list create --board $BOARD_ID --name "To Do" --position 131072
|
||||
pcli list create --board $BOARD_ID --name "Planning" --position 196608
|
||||
pcli list create --board $BOARD_ID --name "In Progress" --position 262144
|
||||
pcli list create --board $BOARD_ID --name "Review" --position 327680
|
||||
pcli list create --board $BOARD_ID --name "Done" --position 393216
|
||||
```
|
||||
|
||||
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.labels[]? | select(.name == "agent") | .id')
|
||||
```
|
||||
|
||||
If no `agent` label found:
|
||||
```bash
|
||||
LABEL_ID=$(pcli label create --board $BOARD_ID --name "agent" --color "berry-red" | jq -r '.data.id')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reconciliation Steps
|
||||
|
||||
### 1. Gather OpenSpec state
|
||||
|
||||
```bash
|
||||
openspec list --json
|
||||
```
|
||||
|
||||
This returns all active changes with their names, schemas, and status.
|
||||
|
||||
For each active change:
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
|
||||
Parse to get:
|
||||
- Change name
|
||||
- Schema name
|
||||
- Artifact completion status (how many artifacts complete vs total)
|
||||
- Whether all `applyRequires` artifacts are done
|
||||
- Whether tasks exist and their completion state
|
||||
|
||||
If a `tasks.md` exists, read it and parse checkbox state (`- [ ]` vs `- [x]`).
|
||||
|
||||
### 2. Determine target list for each change
|
||||
|
||||
Map each change to the correct board list based on its OpenSpec state:
|
||||
|
||||
| Condition | Target List |
|
||||
|-----------|-------------|
|
||||
| Artifacts incomplete (not all `applyRequires` done) | **Planning** |
|
||||
| Artifacts complete, tasks exist with incomplete items | **In Progress** |
|
||||
| All tasks complete (all `[x]`) | **Review** |
|
||||
| Change archived (not in active list) | **Done** |
|
||||
|
||||
### 3. Gather Planka state
|
||||
|
||||
```bash
|
||||
pcli card list --board $BOARD_ID | jq '.data[] | select(.labels[]?.name == "agent")'
|
||||
```
|
||||
|
||||
Build a map of existing agent-labelled cards by name, including which list they're currently in.
|
||||
|
||||
### 4. Reconcile: create missing cards
|
||||
|
||||
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 "<target-list>" '.data.lists[] | select(.name == $list) | .id')
|
||||
CARD_ID=$(pcli card create --list $LIST_ID --name "<change-name>" --description "<schema: schema-name>" | jq -r '.data.id')
|
||||
|
||||
# Add agent label
|
||||
pcli card add-label $CARD_ID --label $LABEL_ID
|
||||
```
|
||||
|
||||
### 5. Reconcile: move cards to correct list
|
||||
|
||||
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 "<target-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
|
||||
|
||||
### 6. Reconcile: sync task lists
|
||||
|
||||
For each OpenSpec change that has a `tasks.md`:
|
||||
|
||||
First, check if the card already has task lists by using `pcli card get`:
|
||||
|
||||
```bash
|
||||
# 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 (in order), assign incrementing positions:
|
||||
# position = (index + 1) * 65536 (i.e. 65536, 131072, 196608, ...)
|
||||
# - If a task with the same name already exists, update its completion state if needed
|
||||
# - If no matching task exists, create it with explicit position
|
||||
pcli task create --task-list $TL_ID --name "<task description>" --position <pos>
|
||||
|
||||
# For tasks already in Planka, update completion state to match tasks.md:
|
||||
pcli task update <task-id> --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"
|
||||
|
||||
```bash
|
||||
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
|
||||
```
|
||||
|
||||
### 8. Report
|
||||
|
||||
After reconciliation, briefly summarise what changed:
|
||||
- Infrastructure created (project/board/lists/label): yes/no
|
||||
- Cards created: N
|
||||
- Cards moved: N (list details)
|
||||
- Tasks synced: N
|
||||
- Cards moved to Done: N
|
||||
- Errors (if any): list them
|
||||
|
||||
---
|
||||
|
||||
## Non-Agentic Work
|
||||
|
||||
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
|
||||
|
||||
---
|
||||
|
||||
## ID Discovery
|
||||
|
||||
Planka IDs cannot be cached across sessions. Each sync run must discover IDs dynamically:
|
||||
|
||||
1. Read project and board name from `project.yaml`
|
||||
2. Find the project by name: `pcli project list | jq ...`
|
||||
3. Find the board by name within the project
|
||||
4. Find lists on the board: `pcli board get <board-id> | jq ...`
|
||||
5. Find agent cards: `pcli card list --board <board-id> | jq '.data[] | select(.labels[]?.name == "agent")'`
|
||||
6. Match cards to changes by name
|
||||
|
||||
---
|
||||
The script handles its own concurrency:
|
||||
- Uses `flock` to ensure only one sync runs per project-board pair
|
||||
- If a sync is already running, sets a pending flag and exits immediately
|
||||
- The running sync re-runs after completion if the pending flag is set
|
||||
- Multiple pending requests coalesce into a single re-run
|
||||
|
||||
## Guardrails
|
||||
|
||||
- **Never modify an `agent`-labelled card outside of this sync workflow**
|
||||
- **Never read Planka to determine what work to do** - query OpenSpec instead
|
||||
- **Always discover IDs dynamically** - never hardcode or cache across sessions
|
||||
- **Sync failures are silent** - log a warning but never block opsx workflows
|
||||
- **One board per project** - if multiple boards exist, ask the user which to sync to
|
||||
- **Idempotent** - safe to run multiple times, will not create duplicates
|
||||
- **Bootstrap is safe** - creating project/board/lists is idempotent; existing resources are reused
|
||||
- Sync is **best-effort** — if Planka is unreachable or the script fails, log a warning and continue
|
||||
- Never block agentic work because of sync
|
||||
- If `kanban-project-sync` is not on PATH, log a warning and skip
|
||||
|
||||
@@ -124,6 +124,20 @@ Board details include lists directly in `.data.lists[]`, not in an `included` se
|
||||
pcli board get <board-id> | jq '.data.lists[] | {id, name, position}'
|
||||
```
|
||||
|
||||
### Card List Labels
|
||||
Card list returns labels as **plain strings**, not objects:
|
||||
```bash
|
||||
# Labels are strings like "agent", NOT objects like {name: "agent"}
|
||||
pcli card list --board <board-id> | jq '.data[] | select(.labels[]? == "agent")'
|
||||
```
|
||||
|
||||
### Card Get Response
|
||||
Card get includes `taskLists` and `tasks` arrays (when they exist):
|
||||
```bash
|
||||
pcli card get <card-id> | jq '.data.taskLists[0].id'
|
||||
pcli card get <card-id> | jq '.data.tasks[] | {name, isCompleted}'
|
||||
```
|
||||
|
||||
### Finding Boards in a Project
|
||||
Use `board list --project` to find boards by project name:
|
||||
```bash
|
||||
|
||||
@@ -53,6 +53,14 @@ There is no unit test suite or linter configured — tests are shell-based integ
|
||||
**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.
|
||||
reconcile Planka board state by running the sync script:
|
||||
|
||||
```bash
|
||||
PROJECT_NAME=$(yq -r '.planka.project' project.yaml)
|
||||
BOARD_NAME=$(yq -r '.planka.board' project.yaml)
|
||||
kanban-project-sync --project "$PROJECT_NAME" --board "$BOARD_NAME" --background
|
||||
```
|
||||
|
||||
The script handles concurrency (flock-based locking with coalescing pending queue).
|
||||
If the script is not on PATH or fails, log a brief warning and continue.
|
||||
Never block or retry — Planka sync is best-effort.
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-02-19
|
||||
@@ -0,0 +1,86 @@
|
||||
## Context
|
||||
|
||||
The kanban sync workflow is currently a 280-line markdown document (`.claude/commands/kanban-sync.md`) that the LLM reads and interprets each time sync is triggered. The sync logic is deterministic — it reads OpenSpec state, compares it with Planka board state, and reconciles. No LLM judgment is needed. The overhead is significant: each sync consumes tokens for reasoning through ~10 phases and executing ~15-30 shell commands.
|
||||
|
||||
There is also no concurrency protection. If two opsx workflows complete in quick succession, two syncs could run simultaneously and produce conflicting Planka API calls.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Replace LLM-interpreted sync with a standalone bash script
|
||||
- Implement concurrency control so only one sync runs at a time
|
||||
- Support coalescing: if sync is requested while one runs, queue exactly one follow-up
|
||||
- Support foreground (default, for humans) and background (for LLM/skill invocation) modes
|
||||
- Keep the existing sync logic and board structure unchanged
|
||||
|
||||
**Non-Goals:**
|
||||
- Changing the sync algorithm or board layout
|
||||
- Adding bidirectional sync (Planka → OpenSpec)
|
||||
- Supporting multiple simultaneous boards
|
||||
- Adding a daemon/service mode — this remains an on-demand script
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Externally deployed bash script (`kanban-project-sync`)
|
||||
|
||||
**Rationale**: The sync logic uses `pcli`, `jq`, `yq`, and `openspec` CLI tools — all shell-native. A bash script is the natural fit. The script is deployed to developer instances via existing tooling and placed on PATH, like other shared scripts. It does not live in the project repo.
|
||||
|
||||
**Convention**: The script assumes it is run from the project root directory (uses `pwd` to locate `openspec/` directory). This is consistent with how `openspec` and `pcli` already work.
|
||||
|
||||
**Alternative considered**: A Go subcommand in pcli (`pcli sync`). Rejected because the sync orchestrates `openspec` (a separate tool) and would require shelling out anyway. The script approach keeps concerns separated.
|
||||
|
||||
**Alternative considered**: Placing the script in the project root alongside `build.sh`. Rejected — the script is a shared developer tool, not project-specific. External deployment keeps it consistent with other tooling.
|
||||
|
||||
### 2. `flock` for locking with pending file for coalescing
|
||||
|
||||
**Rationale**: `flock` is the standard POSIX advisory locking mechanism. It's atomic, handles process crashes (lock auto-releases), and avoids manual PID-file management. The pending flag file is a simple touch/rm mechanism that coalesces multiple queued requests into one re-run.
|
||||
|
||||
**Lock file**: `/tmp/kanban-project-sync-<project>-<board>.lock` — scoped per project-board to allow independent syncs for different boards.
|
||||
|
||||
**Pending file**: `/tmp/kanban-project-sync-<project>-<board>.pending` — touched by callers that can't acquire the lock.
|
||||
|
||||
**Flow**:
|
||||
```
|
||||
Caller:
|
||||
Try flock (non-blocking)
|
||||
├── Got lock:
|
||||
│ loop:
|
||||
│ run_sync()
|
||||
│ if pending file exists: rm pending, continue loop
|
||||
│ else: break
|
||||
│ release lock (fd close)
|
||||
│
|
||||
└── Lock held:
|
||||
touch pending file
|
||||
exit 0
|
||||
```
|
||||
|
||||
**Alternative considered**: PID file with `kill -0` checks. Rejected — racy, doesn't handle crashes cleanly, more complex than `flock`.
|
||||
|
||||
### 3. Foreground default, `--background` flag for detached mode
|
||||
|
||||
**Rationale**: Humans running the script manually want to see output. The LLM/skill always passes `--background` to avoid blocking. Background mode uses `nohup` + `&` with output redirected to a log file at `/tmp/kanban-project-sync-<project>-<board>.log`.
|
||||
|
||||
### 4. Script reads `project.yaml` for defaults but CLI args override
|
||||
|
||||
**Rationale**: `--project` and `--board` are required arguments. The script does NOT read `project.yaml` itself — that's the caller's concern. This keeps the script generic. The kanban-sync command/skill reads `project.yaml` and passes the values. A human can pass whatever project/board they want.
|
||||
|
||||
### 5. Exit codes
|
||||
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| 0 | Success (sync completed) or queued (pending flag set) |
|
||||
| 1 | Error (sync failed) |
|
||||
| 2 | Skipped (Planka offline / connectivity check failed) |
|
||||
|
||||
When a caller can't acquire the lock and sets the pending flag, it exits 0 — from the caller's perspective, the sync will happen.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
**[`flock` availability on macOS]** → macOS doesn't ship `flock` by default. Mitigation: document that `brew install util-linux` is needed. This is a dev tooling script, not a production binary — acceptable friction.
|
||||
|
||||
**[Background mode log management]** → Log files in `/tmp` accumulate. Mitigation: Each run overwrites the log file (not appends), so only the last run's output is kept per project-board pair.
|
||||
|
||||
**[Pending flag race window]** → Tiny window between "check pending" and "release lock" where a new pending could be missed. Mitigation: The check-and-release happens while still holding the flock, so no other process can set pending between check and release. The lock holder is the only one that reads/clears the pending file.
|
||||
|
||||
**[Script drift from sync logic]** → If the sync algorithm changes, the script must be updated manually (no longer auto-follows the markdown doc). Mitigation: The sync logic has been stable. The kanban-sync.md doc will reference the script, making it clear where changes go.
|
||||
@@ -0,0 +1,30 @@
|
||||
## Why
|
||||
|
||||
The kanban sync is currently implemented as a detailed workflow document that the LLM interprets and executes command-by-command. Each sync burns significant tokens as the LLM reasons through ~10 phases and executes ~15-30 `pcli` commands. The sync logic is entirely deterministic — there are no decisions that require LLM judgment. Moving this to a standalone bash script eliminates the overhead and makes sync near-instant. Additionally, there is no concurrency protection — two syncs could run simultaneously and clash on Planka API state.
|
||||
|
||||
## What Changes
|
||||
|
||||
- New `kanban-project-sync` bash script (deployed externally, on PATH) that performs the full 10-phase Planka board reconciliation
|
||||
- Script accepts `--project` and `--board` as required inputs, with optional `--background` flag
|
||||
- Script assumes it is run from the project root (uses `pwd` to locate `openspec/` directory)
|
||||
- Implements `flock`-based concurrency control with a coalescing pending flag (depth-1 queue)
|
||||
- Default mode is foreground (human-friendly); `--background` detaches for fire-and-forget use
|
||||
- `.claude/commands/kanban-sync.md` simplified to just invoke the script with `--background`
|
||||
- `CLAUDE.md` sync instructions updated to reference the script
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `kanban-sync-concurrency`: Concurrency control for sync execution — flock-based locking with a pending flag that coalesces multiple requests, and automatic re-run after completion if a sync was requested during execution
|
||||
|
||||
### Modified Capabilities
|
||||
_(none — no existing spec-level requirements change)_
|
||||
|
||||
## Impact
|
||||
|
||||
- **New external script**: `kanban-project-sync` — deployed to developer instances via existing tooling, placed on PATH
|
||||
- **Modified**: `.claude/commands/kanban-sync.md` — reduced from full workflow to script invocation
|
||||
- **Modified**: `CLAUDE.md` — updated Planka Sync section
|
||||
- **Dependencies**: Requires `flock` (standard on Linux, available on macOS via `brew install util-linux`), `jq`, `yq`, `pcli`, `openspec` on PATH
|
||||
- **No Go code changes** — this is entirely in the tooling/workflow layer
|
||||
- **Convention**: Script must be run from the project root directory (same as `openspec` and `pcli`)
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Exclusive sync execution
|
||||
The sync script SHALL use `flock` advisory locking to ensure only one sync instance runs at a time per project-board pair. The lock file SHALL be located at `/tmp/kanban-project-sync-<project>-<board>.lock`.
|
||||
|
||||
#### Scenario: First sync acquires lock
|
||||
- **WHEN** kanban-project-sync is invoked and no other sync is running for the same project-board
|
||||
- **THEN** the script acquires the flock and executes the full sync
|
||||
|
||||
#### Scenario: Second sync finds lock held
|
||||
- **WHEN** kanban-project-sync is invoked while another sync is running for the same project-board
|
||||
- **THEN** the script SHALL NOT execute the sync, SHALL touch the pending flag file, and SHALL exit with code 0
|
||||
|
||||
### Requirement: Coalescing pending queue
|
||||
The sync script SHALL implement a depth-1 coalescing queue using a pending flag file at `/tmp/kanban-project-sync-<project>-<board>.pending`. Multiple pending requests SHALL coalesce into a single re-run.
|
||||
|
||||
#### Scenario: Pending flag set during sync
|
||||
- **WHEN** a sync completes and the pending flag file exists
|
||||
- **THEN** the script SHALL clear the pending flag and re-run the sync before releasing the lock
|
||||
|
||||
#### Scenario: No pending flag after sync
|
||||
- **WHEN** a sync completes and no pending flag file exists
|
||||
- **THEN** the script SHALL release the lock and exit
|
||||
|
||||
#### Scenario: Multiple pending requests coalesce
|
||||
- **WHEN** three sync requests arrive while a sync is running
|
||||
- **THEN** only one additional sync SHALL run after the current one completes (not three)
|
||||
|
||||
### Requirement: Foreground and background modes
|
||||
The sync script SHALL default to foreground execution (output to stdout/stderr). When `--background` is passed, the script SHALL detach and redirect output to a log file at `/tmp/kanban-project-sync-<project>-<board>.log`.
|
||||
|
||||
#### Scenario: Foreground execution
|
||||
- **WHEN** kanban-project-sync is invoked without `--background`
|
||||
- **THEN** sync output SHALL be written to stdout/stderr and the script SHALL block until complete
|
||||
|
||||
#### Scenario: Background execution
|
||||
- **WHEN** kanban-project-sync is invoked with `--background`
|
||||
- **THEN** the script SHALL detach from the terminal, redirect output to the log file, and return immediately
|
||||
|
||||
### Requirement: Required arguments
|
||||
The sync script SHALL require `--project <name>` and `--board <name>` arguments. The script SHALL NOT read `project.yaml` directly.
|
||||
|
||||
#### Scenario: Missing arguments
|
||||
- **WHEN** kanban-project-sync is invoked without `--project` or `--board`
|
||||
- **THEN** the script SHALL print usage information and exit with code 1
|
||||
|
||||
### Requirement: Connectivity check
|
||||
The sync script SHALL run `pcli status` before syncing. If the check fails, the script SHALL exit with code 2.
|
||||
|
||||
#### Scenario: Planka offline
|
||||
- **WHEN** `pcli status` returns a non-zero exit code
|
||||
- **THEN** the script SHALL print a message indicating Planka is unreachable and exit with code 2
|
||||
|
||||
### Requirement: Idempotent sync phases
|
||||
The sync script SHALL implement all 10 phases of the existing kanban-sync workflow: connectivity check, bootstrap infrastructure (find-or-create project, board, lists, label), gather OpenSpec state, determine target lists, gather Planka agent cards, create missing cards, move cards to correct lists, sync task checklists, handle archived changes, and report. Each phase SHALL be idempotent — running the script twice with no state changes SHALL produce no Planka API mutations on the second run.
|
||||
|
||||
#### Scenario: Clean re-run with no changes
|
||||
- **WHEN** the sync script runs twice in succession with no OpenSpec or Planka state changes between runs
|
||||
- **THEN** the second run SHALL make no create/update/move API calls and SHALL report zero changes
|
||||
@@ -0,0 +1,32 @@
|
||||
## 1. Script Skeleton and Concurrency
|
||||
|
||||
- [x] 1.1 Create `kanban-project-sync` script with argument parsing (`--project`, `--board`, `--background`), usage help, and exit code constants
|
||||
- [x] 1.2 Implement `flock`-based locking with pending flag file and re-run loop
|
||||
- [x] 1.3 Implement `--background` mode (re-exec self detached with output to log file)
|
||||
- [x] 1.4 Add connectivity check (`pcli status`) with exit code 2 on failure
|
||||
|
||||
## 2. Bootstrap Phase
|
||||
|
||||
- [x] 2.1 Implement find-or-create project by name
|
||||
- [x] 2.2 Implement find-or-create board by name within project
|
||||
- [x] 2.3 Implement find-or-create lists (Backlog, To Do, Planning, In Progress, Review, Done) with correct positions
|
||||
- [x] 2.4 Implement find-or-create `agent` label on the board
|
||||
|
||||
## 3. Reconciliation Phases
|
||||
|
||||
- [x] 3.1 Gather OpenSpec state — list active changes, parse artifact completion and task checkbox state
|
||||
- [x] 3.2 Determine target list for each change based on OpenSpec state (Planning / In Progress / Review)
|
||||
- [x] 3.3 Gather existing Planka agent-labelled cards and build name→card map
|
||||
- [x] 3.4 Create missing cards (new OpenSpec changes without a Planka card) with agent label
|
||||
- [x] 3.5 Move cards to correct list when current list doesn't match target
|
||||
- [x] 3.6 Sync task checklists — create/update task lists and tasks on cards from `tasks.md`
|
||||
- [x] 3.7 Move orphaned agent cards (no matching active change) to Done
|
||||
|
||||
## 4. Reporting and Summary
|
||||
|
||||
- [x] 4.1 Add summary output — infrastructure created, cards created/moved, tasks synced, errors
|
||||
|
||||
## 5. Skill and Documentation Updates
|
||||
|
||||
- [x] 5.1 Simplify `.claude/commands/kanban-sync.md` to invoke `kanban-project-sync --background`
|
||||
- [x] 5.2 Update `CLAUDE.md` Planka Sync section to reference the script
|
||||
@@ -0,0 +1,57 @@
|
||||
### Requirement: Exclusive sync execution
|
||||
The sync script SHALL use `flock` advisory locking to ensure only one sync instance runs at a time per project-board pair. The lock file SHALL be located at `/tmp/kanban-project-sync-<project>-<board>.lock`.
|
||||
|
||||
#### Scenario: First sync acquires lock
|
||||
- **WHEN** kanban-project-sync is invoked and no other sync is running for the same project-board
|
||||
- **THEN** the script acquires the flock and executes the full sync
|
||||
|
||||
#### Scenario: Second sync finds lock held
|
||||
- **WHEN** kanban-project-sync is invoked while another sync is running for the same project-board
|
||||
- **THEN** the script SHALL NOT execute the sync, SHALL touch the pending flag file, and SHALL exit with code 0
|
||||
|
||||
### Requirement: Coalescing pending queue
|
||||
The sync script SHALL implement a depth-1 coalescing queue using a pending flag file at `/tmp/kanban-project-sync-<project>-<board>.pending`. Multiple pending requests SHALL coalesce into a single re-run.
|
||||
|
||||
#### Scenario: Pending flag set during sync
|
||||
- **WHEN** a sync completes and the pending flag file exists
|
||||
- **THEN** the script SHALL clear the pending flag and re-run the sync before releasing the lock
|
||||
|
||||
#### Scenario: No pending flag after sync
|
||||
- **WHEN** a sync completes and no pending flag file exists
|
||||
- **THEN** the script SHALL release the lock and exit
|
||||
|
||||
#### Scenario: Multiple pending requests coalesce
|
||||
- **WHEN** three sync requests arrive while a sync is running
|
||||
- **THEN** only one additional sync SHALL run after the current one completes (not three)
|
||||
|
||||
### Requirement: Foreground and background modes
|
||||
The sync script SHALL default to foreground execution (output to stdout/stderr). When `--background` is passed, the script SHALL detach and redirect output to a log file at `/tmp/kanban-project-sync-<project>-<board>.log`.
|
||||
|
||||
#### Scenario: Foreground execution
|
||||
- **WHEN** kanban-project-sync is invoked without `--background`
|
||||
- **THEN** sync output SHALL be written to stdout/stderr and the script SHALL block until complete
|
||||
|
||||
#### Scenario: Background execution
|
||||
- **WHEN** kanban-project-sync is invoked with `--background`
|
||||
- **THEN** the script SHALL detach from the terminal, redirect output to the log file, and return immediately
|
||||
|
||||
### Requirement: Required arguments
|
||||
The sync script SHALL require `--project <name>` and `--board <name>` arguments. The script SHALL NOT read `project.yaml` directly.
|
||||
|
||||
#### Scenario: Missing arguments
|
||||
- **WHEN** kanban-project-sync is invoked without `--project` or `--board`
|
||||
- **THEN** the script SHALL print usage information and exit with code 1
|
||||
|
||||
### Requirement: Connectivity check
|
||||
The sync script SHALL run `pcli status` before syncing. If the check fails, the script SHALL exit with code 2.
|
||||
|
||||
#### Scenario: Planka offline
|
||||
- **WHEN** `pcli status` returns a non-zero exit code
|
||||
- **THEN** the script SHALL print a message indicating Planka is unreachable and exit with code 2
|
||||
|
||||
### Requirement: Idempotent sync phases
|
||||
The sync script SHALL implement all 10 phases of the existing kanban-sync workflow: connectivity check, bootstrap infrastructure (find-or-create project, board, lists, label), gather OpenSpec state, determine target lists, gather Planka agent cards, create missing cards, move cards to correct lists, sync task checklists, handle archived changes, and report. Each phase SHALL be idempotent — running the script twice with no state changes SHALL produce no Planka API mutations on the second run.
|
||||
|
||||
#### Scenario: Clean re-run with no changes
|
||||
- **WHEN** the sync script runs twice in succession with no OpenSpec or Planka state changes between runs
|
||||
- **THEN** the second run SHALL make no create/update/move API calls and SHALL report zero changes
|
||||
Reference in New Issue
Block a user