mirror of
https://github.com/daggerhashimoto/openclaw-nerve
synced 2026-04-21 10:37:17 +00:00
feat: bundle nerve-kanban skill and auto-install during setup (#83)
* feat: bundle nerve-kanban skill and install during setup
This commit is contained in:
parent
0ff9725f83
commit
08933d79e2
3 changed files with 794 additions and 2 deletions
|
|
@ -11,8 +11,9 @@
|
|||
/** Mask a token for display, with a guard for short tokens. */
|
||||
// Show token in prompts so users can verify what they entered
|
||||
|
||||
import { existsSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { existsSync, readdirSync, mkdirSync, copyFileSync, lstatSync } from 'node:fs';
|
||||
import { resolve, join } from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { networkInterfaces } from 'node:os';
|
||||
import { input, password, confirm, select } from '@inquirer/prompts';
|
||||
|
|
@ -38,6 +39,8 @@ import { detectGatewayConfig, getEnvGatewayToken, restartGateway, approveAllPend
|
|||
|
||||
const PROJECT_ROOT = resolve(process.cwd());
|
||||
const ENV_PATH = resolve(PROJECT_ROOT, '.env');
|
||||
const SKILLS_SRC = resolve(PROJECT_ROOT, 'skills');
|
||||
const SKILLS_DEST = resolve(homedir(), '.openclaw', 'workspace', 'skills');
|
||||
const TOTAL_SECTIONS = 6;
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
|
@ -145,6 +148,53 @@ process.on('SIGINT', () => {
|
|||
process.exit(130);
|
||||
});
|
||||
|
||||
// ── Skill installation ───────────────────────────────────────────────
|
||||
|
||||
function copyDirSync(src: string, dest: string): void {
|
||||
mkdirSync(dest, { recursive: true });
|
||||
for (const entry of readdirSync(src)) {
|
||||
const srcPath = join(src, entry);
|
||||
const destPath = join(dest, entry);
|
||||
const stat = lstatSync(srcPath);
|
||||
if (stat.isSymbolicLink()) continue;
|
||||
if (stat.isDirectory()) {
|
||||
copyDirSync(srcPath, destPath);
|
||||
} else {
|
||||
copyFileSync(srcPath, destPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function installBundledSkills(): void {
|
||||
if (!existsSync(SKILLS_SRC)) return;
|
||||
|
||||
let installed = 0;
|
||||
let entries: string[];
|
||||
try {
|
||||
entries = readdirSync(SKILLS_SRC);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const skillName of entries) {
|
||||
try {
|
||||
const skillSrc = join(SKILLS_SRC, skillName);
|
||||
if (!lstatSync(skillSrc).isDirectory()) continue;
|
||||
if (!existsSync(join(skillSrc, 'SKILL.md'))) continue;
|
||||
|
||||
const skillDest = join(SKILLS_DEST, skillName);
|
||||
copyDirSync(skillSrc, skillDest);
|
||||
installed++;
|
||||
} catch (err) {
|
||||
warn(`Failed to install skill "${skillName}": ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (installed > 0) {
|
||||
success(`Installed ${installed} bundled skill${installed > 1 ? 's' : ''} to ${SKILLS_DEST}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Main ─────────────────────────────────────────────────────────────
|
||||
|
||||
async function main(): Promise<void> {
|
||||
|
|
@ -242,6 +292,9 @@ async function main(): Promise<void> {
|
|||
console.log('');
|
||||
success('Configuration written to .env');
|
||||
|
||||
// Install bundled agent skills
|
||||
installBundledSkills();
|
||||
|
||||
printSummary(config);
|
||||
|
||||
// When invoked from install.sh, build is already done — skip misleading "next steps"
|
||||
|
|
@ -1002,6 +1055,10 @@ async function runDefaults(existing: EnvConfig): Promise<void> {
|
|||
writeEnvFile(ENV_PATH, config);
|
||||
|
||||
success('Configuration written to .env');
|
||||
|
||||
// Install bundled agent skills
|
||||
installBundledSkills();
|
||||
|
||||
printSummary(config);
|
||||
|
||||
// Apply all gateway config patches silently (non-interactive = implicit consent)
|
||||
|
|
|
|||
61
skills/nerve-kanban/SKILL.md
Normal file
61
skills/nerve-kanban/SKILL.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
---
|
||||
name: nerve-kanban
|
||||
description: Interact with the Nerve Kanban board API. CRUD tasks, manage workflow (execute, approve, reject, abort), handle proposals, configure the board. All endpoints are under /api/kanban on the Nerve server.
|
||||
---
|
||||
|
||||
# Nerve Kanban API Skill
|
||||
|
||||
Use this skill to manage tasks on the Nerve Kanban board via its REST API.
|
||||
|
||||
## Base URL
|
||||
|
||||
All endpoints are relative to the Nerve server origin (e.g. `http://localhost:3000`). Prefix every path with `/api/kanban`.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
- **Tasks** flow through columns: `backlog` → `todo` → `in-progress` → `review` → `done` (or `cancelled`).
|
||||
- **CAS versioning**: Updates and reorders require the current `version` number. If it mismatches, you get a `409 version_conflict` with the server's latest task. Re-read and retry.
|
||||
- **Workflow actions** enforce valid transitions. You can't execute a task that's already in review.
|
||||
- **Proposals** let agents suggest task creation or updates. The operator (or auto-policy) approves/rejects them.
|
||||
- **Actors** are either `"operator"` or `"agent:<name>"`.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Action | Method | Path |
|
||||
|---|---|---|
|
||||
| List tasks | GET | `/api/kanban/tasks` |
|
||||
| Create task | POST | `/api/kanban/tasks` |
|
||||
| Update task | PATCH | `/api/kanban/tasks/:id` |
|
||||
| Delete task | DELETE | `/api/kanban/tasks/:id` |
|
||||
| Reorder/move | POST | `/api/kanban/tasks/:id/reorder` |
|
||||
| Execute (spawn agent) | POST | `/api/kanban/tasks/:id/execute` |
|
||||
| Approve (review→done) | POST | `/api/kanban/tasks/:id/approve` |
|
||||
| Reject (review→todo) | POST | `/api/kanban/tasks/:id/reject` |
|
||||
| Abort (in-progress→todo) | POST | `/api/kanban/tasks/:id/abort` |
|
||||
| Complete run (webhook) | POST | `/api/kanban/tasks/:id/complete` |
|
||||
| List proposals | GET | `/api/kanban/proposals` |
|
||||
| Create proposal | POST | `/api/kanban/proposals` |
|
||||
| Approve proposal | POST | `/api/kanban/proposals/:id/approve` |
|
||||
| Reject proposal | POST | `/api/kanban/proposals/:id/reject` |
|
||||
| Get config | GET | `/api/kanban/config` |
|
||||
| Update config | PUT | `/api/kanban/config` |
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Creating and executing a task
|
||||
1. `POST /api/kanban/tasks` with `{ "title": "...", "description": "..." }` → returns task with `id` and `version`.
|
||||
2. `POST /api/kanban/tasks/:id/execute` → moves to `in-progress`, spawns an agent session.
|
||||
3. The agent session runs, and on completion the task moves to `review` automatically.
|
||||
4. `POST /api/kanban/tasks/:id/approve` → moves to `done`.
|
||||
|
||||
### Handling version conflicts
|
||||
Always send `version` in PATCH and reorder requests. On `409`, read `latest` from the response and retry with the updated version.
|
||||
|
||||
### Proposing changes (as an agent)
|
||||
Agents that can't directly modify the board should use proposals:
|
||||
1. `POST /api/kanban/proposals` with `{ "type": "create", "payload": { "title": "..." }, "proposedBy": "agent:myname" }`.
|
||||
2. The operator approves or rejects via `/api/kanban/proposals/:id/approve` or `/api/kanban/proposals/:id/reject`.
|
||||
|
||||
## Full API Reference
|
||||
|
||||
See [references/api.md](references/api.md) for complete endpoint documentation, type definitions, error codes, and example requests.
|
||||
674
skills/nerve-kanban/references/api.md
Normal file
674
skills/nerve-kanban/references/api.md
Normal file
|
|
@ -0,0 +1,674 @@
|
|||
# Nerve Kanban API Reference
|
||||
|
||||
All paths relative to server origin. Content-Type: `application/json` for all request bodies.
|
||||
|
||||
---
|
||||
|
||||
## Type Definitions
|
||||
|
||||
### TaskStatus
|
||||
```typescript
|
||||
type TaskStatus = 'backlog' | 'todo' | 'in-progress' | 'review' | 'done' | 'cancelled';
|
||||
```
|
||||
|
||||
### TaskPriority
|
||||
```typescript
|
||||
type TaskPriority = 'critical' | 'high' | 'normal' | 'low';
|
||||
```
|
||||
|
||||
### TaskActor
|
||||
```typescript
|
||||
type TaskActor = 'operator' | `agent:${string}`;
|
||||
```
|
||||
|
||||
### ThinkingLevel
|
||||
```typescript
|
||||
type ThinkingLevel = 'off' | 'low' | 'medium' | 'high';
|
||||
```
|
||||
|
||||
### ProposalStatus
|
||||
```typescript
|
||||
type ProposalStatus = 'pending' | 'approved' | 'rejected';
|
||||
```
|
||||
|
||||
### KanbanTask
|
||||
```typescript
|
||||
interface KanbanTask {
|
||||
id: string; // URL-safe slug derived from title
|
||||
title: string;
|
||||
description?: string;
|
||||
status: TaskStatus;
|
||||
priority: TaskPriority;
|
||||
createdBy: TaskActor;
|
||||
createdAt: number; // epoch ms
|
||||
updatedAt: number; // epoch ms
|
||||
version: number; // CAS version, starts at 1
|
||||
sourceSessionKey?: string;
|
||||
assignee?: TaskActor;
|
||||
labels: string[];
|
||||
columnOrder: number; // position within column
|
||||
run?: TaskRunLink;
|
||||
result?: string; // agent output after execution
|
||||
resultAt?: number; // epoch ms
|
||||
model?: string; // LLM model override
|
||||
thinking?: ThinkingLevel;
|
||||
dueAt?: number; // epoch ms
|
||||
estimateMin?: number;
|
||||
actualMin?: number;
|
||||
feedback: TaskFeedback[];
|
||||
}
|
||||
```
|
||||
|
||||
### TaskRunLink
|
||||
```typescript
|
||||
interface TaskRunLink {
|
||||
sessionKey: string;
|
||||
sessionId?: string;
|
||||
runId?: string;
|
||||
startedAt: number;
|
||||
endedAt?: number;
|
||||
status: 'running' | 'done' | 'error' | 'aborted';
|
||||
error?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### TaskFeedback
|
||||
```typescript
|
||||
interface TaskFeedback {
|
||||
at: number;
|
||||
by: TaskActor;
|
||||
note: string;
|
||||
}
|
||||
```
|
||||
|
||||
### KanbanBoardConfig
|
||||
```typescript
|
||||
interface KanbanBoardConfig {
|
||||
columns: Array<{
|
||||
key: TaskStatus;
|
||||
title: string;
|
||||
wipLimit?: number;
|
||||
visible: boolean;
|
||||
}>;
|
||||
defaults: {
|
||||
status: TaskStatus; // default for new tasks
|
||||
priority: TaskPriority; // default for new tasks
|
||||
};
|
||||
reviewRequired: boolean;
|
||||
allowDoneDragBypass: boolean;
|
||||
quickViewLimit: number;
|
||||
proposalPolicy: 'confirm' | 'auto';
|
||||
defaultModel?: string;
|
||||
defaultThinking?: ThinkingLevel;
|
||||
}
|
||||
```
|
||||
|
||||
### KanbanProposal
|
||||
```typescript
|
||||
interface KanbanProposal {
|
||||
id: string; // UUID
|
||||
type: 'create' | 'update';
|
||||
payload: Record<string, unknown>;
|
||||
sourceSessionKey?: string;
|
||||
proposedBy: TaskActor;
|
||||
proposedAt: number; // epoch ms
|
||||
status: ProposalStatus;
|
||||
version: number;
|
||||
resolvedAt?: number;
|
||||
resolvedBy?: TaskActor;
|
||||
reason?: string; // rejection reason
|
||||
resultTaskId?: string; // created/updated task ID on approval
|
||||
}
|
||||
```
|
||||
|
||||
### TaskListResult (pagination envelope)
|
||||
```typescript
|
||||
interface TaskListResult {
|
||||
items: KanbanTask[];
|
||||
total: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
hasMore: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Responses
|
||||
|
||||
All errors return JSON:
|
||||
|
||||
| Error Code | HTTP Status | Shape |
|
||||
|---|---|---|
|
||||
| `validation_error` | 400 | `{ error, details }` |
|
||||
| `not_found` | 404 | `{ error, details }` |
|
||||
| `version_conflict` | 409 | `{ error, serverVersion, latest }` |
|
||||
| `invalid_transition` | 409 | `{ error, from, to, message }` |
|
||||
| `already_resolved` | 409 | `{ error, proposal }` |
|
||||
|
||||
---
|
||||
|
||||
## Endpoints
|
||||
|
||||
### GET /api/kanban/tasks
|
||||
|
||||
List tasks with optional filters and pagination.
|
||||
|
||||
**Query Parameters:**
|
||||
|
||||
| Param | Type | Description |
|
||||
|---|---|---|
|
||||
| `status` | string (repeatable, comma-separated) | Filter by status(es) |
|
||||
| `priority` | string (repeatable, comma-separated) | Filter by priority(ies) |
|
||||
| `assignee` | string | Filter by assignee (e.g. `operator`, `agent:kim`) |
|
||||
| `label` | string | Filter by label |
|
||||
| `q` | string | Full-text search (title, description, labels) |
|
||||
| `limit` | number | Page size (default 50, max 200) |
|
||||
| `offset` | number | Skip N tasks (default 0) |
|
||||
|
||||
**Response:** `TaskListResult`
|
||||
|
||||
**Sort order:** status column order → columnOrder → updatedAt desc.
|
||||
|
||||
```bash
|
||||
# List all todo tasks
|
||||
curl 'http://localhost:3000/api/kanban/tasks?status=todo'
|
||||
|
||||
# Search with pagination
|
||||
curl 'http://localhost:3000/api/kanban/tasks?q=deploy&limit=10&offset=0'
|
||||
|
||||
# Multiple status filters
|
||||
curl 'http://localhost:3000/api/kanban/tasks?status=todo,in-progress'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### POST /api/kanban/tasks
|
||||
|
||||
Create a new task.
|
||||
|
||||
**Body:**
|
||||
|
||||
| Field | Type | Required | Default | Notes |
|
||||
|---|---|---|---|---|
|
||||
| `title` | string (1-500) | **yes** | | |
|
||||
| `description` | string (max 10000) | no | | |
|
||||
| `status` | TaskStatus | no | config default | |
|
||||
| `priority` | TaskPriority | no | config default | |
|
||||
| `createdBy` | TaskActor | no | `"operator"` | |
|
||||
| `sourceSessionKey` | string (max 500) | no | | |
|
||||
| `assignee` | TaskActor | no | | |
|
||||
| `labels` | string[] (max 50 items, 100 chars each) | no | `[]` | |
|
||||
| `model` | string (max 200) | no | | LLM model for execution |
|
||||
| `thinking` | ThinkingLevel | no | | |
|
||||
| `dueAt` | number | no | | epoch ms |
|
||||
| `estimateMin` | number (≥0) | no | | |
|
||||
|
||||
**Response:** `201` with `KanbanTask`. ID is a URL-safe slug derived from title.
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/kanban/tasks \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"title": "Fix login bug",
|
||||
"description": "Users report 500 on /auth/login with special chars",
|
||||
"priority": "high",
|
||||
"labels": ["bug", "auth"]
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### PATCH /api/kanban/tasks/:id
|
||||
|
||||
Update a task. Requires CAS version.
|
||||
|
||||
**Path params:** `id` - task ID (slug)
|
||||
|
||||
**Body:**
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|---|---|---|---|
|
||||
| `version` | number | **yes** | Must match current version |
|
||||
| `title` | string (1-500) | no | |
|
||||
| `description` | string \| null | no | null clears |
|
||||
| `status` | TaskStatus | no | |
|
||||
| `priority` | TaskPriority | no | |
|
||||
| `assignee` | TaskActor \| null | no | null clears |
|
||||
| `labels` | string[] | no | |
|
||||
| `model` | string \| null | no | null clears |
|
||||
| `thinking` | ThinkingLevel \| null | no | null clears |
|
||||
| `dueAt` | number \| null | no | null clears |
|
||||
| `estimateMin` | number \| null | no | null clears |
|
||||
| `actualMin` | number \| null | no | null clears |
|
||||
| `result` | string \| null | no | null clears |
|
||||
| `resultAt` | number \| null | no | null clears |
|
||||
| `run` | TaskRunLink \| null | no | null clears |
|
||||
| `feedback` | TaskFeedback[] | no | |
|
||||
|
||||
**Response:** `200` with updated `KanbanTask` (version incremented).
|
||||
|
||||
**Errors:** `409 version_conflict` if version mismatches (response includes `serverVersion` and `latest` task).
|
||||
|
||||
```bash
|
||||
curl -X PATCH http://localhost:3000/api/kanban/tasks/fix-login-bug \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"version": 1,
|
||||
"priority": "critical",
|
||||
"labels": ["bug", "auth", "urgent"]
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### DELETE /api/kanban/tasks/:id
|
||||
|
||||
Permanently delete a task.
|
||||
|
||||
**Path params:** `id` - task ID
|
||||
|
||||
**Response:** `200` `{ "ok": true }`
|
||||
|
||||
**Errors:** `404` if not found.
|
||||
|
||||
```bash
|
||||
curl -X DELETE http://localhost:3000/api/kanban/tasks/fix-login-bug
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### POST /api/kanban/tasks/:id/reorder
|
||||
|
||||
Move a task to a different column and/or position.
|
||||
|
||||
**Path params:** `id` - task ID
|
||||
|
||||
**Body:**
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|---|---|---|---|
|
||||
| `version` | number | **yes** | CAS version |
|
||||
| `targetStatus` | TaskStatus | **yes** | Destination column |
|
||||
| `targetIndex` | number (≥0) | **yes** | Position in column (0-based) |
|
||||
|
||||
**Response:** `200` with updated `KanbanTask`.
|
||||
|
||||
**Errors:** `409 version_conflict` on stale version.
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/kanban/tasks/fix-login-bug/reorder \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{ "version": 2, "targetStatus": "in-progress", "targetIndex": 0 }'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### POST /api/kanban/tasks/:id/execute
|
||||
|
||||
Start execution. Moves task from `todo`/`backlog` → `in-progress` and spawns an agent session.
|
||||
|
||||
**Path params:** `id` - task ID
|
||||
|
||||
**Body (optional):**
|
||||
|
||||
| Field | Type | Notes |
|
||||
|---|---|---|
|
||||
| `model` | string (max 200) | Override model for this run |
|
||||
| `thinking` | ThinkingLevel | Override thinking level |
|
||||
|
||||
**Response:** `200` with updated `KanbanTask` (status=`in-progress`, `run.status`=`running`).
|
||||
|
||||
**Errors:** `409 invalid_transition` if task is not in `todo` or `backlog`.
|
||||
|
||||
**Side effects:** Spawns a gateway subagent session with label `kb-<id>`. Background poller watches for completion and auto-transitions to `review`.
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/kanban/tasks/fix-login-bug/execute \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{ "model": "claude-sonnet-4-20250514" }'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### POST /api/kanban/tasks/:id/approve
|
||||
|
||||
Approve a task in review. Moves `review` → `done`.
|
||||
|
||||
**Path params:** `id` - task ID
|
||||
|
||||
**Body (optional):**
|
||||
|
||||
| Field | Type | Notes |
|
||||
|---|---|---|
|
||||
| `note` | string (max 5000) | Approval note (added to feedback) |
|
||||
|
||||
**Response:** `200` with updated `KanbanTask`.
|
||||
|
||||
**Errors:** `409 invalid_transition` if not in `review`.
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/kanban/tasks/fix-login-bug/approve \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{ "note": "Looks good, merging." }'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### POST /api/kanban/tasks/:id/reject
|
||||
|
||||
Reject a task in review. Moves `review` → `todo`. Clears the run and result so it can be re-executed.
|
||||
|
||||
**Path params:** `id` - task ID
|
||||
|
||||
**Body:**
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|---|---|---|---|
|
||||
| `note` | string (1-5000) | **yes** | Rejection reason (added to feedback) |
|
||||
|
||||
**Response:** `200` with updated `KanbanTask`.
|
||||
|
||||
**Errors:** `409 invalid_transition` if not in `review`.
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/kanban/tasks/fix-login-bug/reject \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{ "note": "Missed edge case with unicode chars. Retry." }'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### POST /api/kanban/tasks/:id/abort
|
||||
|
||||
Abort a running task. Moves `in-progress` → `todo`, marks run as `aborted`.
|
||||
|
||||
**Path params:** `id` - task ID
|
||||
|
||||
**Body (optional):**
|
||||
|
||||
| Field | Type | Notes |
|
||||
|---|---|---|
|
||||
| `note` | string (max 5000) | Abort reason (added to feedback) |
|
||||
|
||||
**Response:** `200` with updated `KanbanTask`.
|
||||
|
||||
**Errors:** `409 invalid_transition` if not in `in-progress` with an active run.
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/kanban/tasks/fix-login-bug/abort \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{ "note": "Taking too long, will rethink approach." }'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### POST /api/kanban/tasks/:id/complete
|
||||
|
||||
Webhook to mark a run as complete. Called automatically by the background poller, but can also be called externally.
|
||||
|
||||
**Path params:** `id` - task ID
|
||||
|
||||
**Body (optional):**
|
||||
|
||||
| Field | Type | Notes |
|
||||
|---|---|---|
|
||||
| `result` | string (max 50000) | Agent output. Kanban markers are auto-extracted as proposals. |
|
||||
| `error` | string (max 5000) | If set, marks run as error and moves task to `todo`. |
|
||||
|
||||
**Behavior:**
|
||||
- With `error`: run status → `error`, task → `todo`.
|
||||
- Without `error`: run status → `done`, task → `review`, `result` stored on task.
|
||||
|
||||
**Response:** `200` with updated `KanbanTask`.
|
||||
|
||||
```bash
|
||||
# Success
|
||||
curl -X POST http://localhost:3000/api/kanban/tasks/fix-login-bug/complete \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{ "result": "Fixed the bug. Escaped special chars in auth handler." }'
|
||||
|
||||
# Error
|
||||
curl -X POST http://localhost:3000/api/kanban/tasks/fix-login-bug/complete \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{ "error": "Agent timed out" }'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### GET /api/kanban/config
|
||||
|
||||
Get board configuration.
|
||||
|
||||
**Response:** `KanbanBoardConfig`
|
||||
|
||||
```bash
|
||||
curl http://localhost:3000/api/kanban/config
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### PUT /api/kanban/config
|
||||
|
||||
Update board configuration. Partial updates supported (unset fields keep current values).
|
||||
|
||||
**Body:** Any subset of `KanbanBoardConfig` fields:
|
||||
|
||||
| Field | Type | Notes |
|
||||
|---|---|---|
|
||||
| `columns` | Column[] (1-10) | Full replacement if provided |
|
||||
| `defaults` | `{ status, priority }` | Merged with existing |
|
||||
| `reviewRequired` | boolean | |
|
||||
| `allowDoneDragBypass` | boolean | |
|
||||
| `quickViewLimit` | number (1-50) | |
|
||||
| `proposalPolicy` | `'confirm'` \| `'auto'` | `auto` = proposals auto-approve |
|
||||
| `defaultModel` | string (max 100) | Default model for task execution |
|
||||
| `defaultThinking` | ThinkingLevel | Default thinking level |
|
||||
|
||||
**Column shape:**
|
||||
```typescript
|
||||
{ key: TaskStatus, title: string, wipLimit?: number, visible: boolean }
|
||||
```
|
||||
|
||||
**Response:** `200` with full updated `KanbanBoardConfig`.
|
||||
|
||||
```bash
|
||||
curl -X PUT http://localhost:3000/api/kanban/config \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"proposalPolicy": "auto",
|
||||
"defaultModel": "claude-sonnet-4-20250514",
|
||||
"defaults": { "priority": "high" }
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### GET /api/kanban/proposals
|
||||
|
||||
List proposals, optionally filtered by status.
|
||||
|
||||
**Query Parameters:**
|
||||
|
||||
| Param | Type | Notes |
|
||||
|---|---|---|
|
||||
| `status` | ProposalStatus | `pending`, `approved`, or `rejected` |
|
||||
|
||||
**Response:** `{ proposals: KanbanProposal[] }` (sorted most recent first).
|
||||
|
||||
```bash
|
||||
# All pending proposals
|
||||
curl 'http://localhost:3000/api/kanban/proposals?status=pending'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### POST /api/kanban/proposals
|
||||
|
||||
Create a proposal for task creation or update.
|
||||
|
||||
**Body:**
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|---|---|---|---|
|
||||
| `type` | `'create'` \| `'update'` | **yes** | |
|
||||
| `payload` | object | **yes** | See below |
|
||||
| `sourceSessionKey` | string (max 500) | no | |
|
||||
| `proposedBy` | TaskActor | no | default `"operator"` |
|
||||
|
||||
**Payload for `type: 'create'`:**
|
||||
|
||||
| Field | Type | Required |
|
||||
|---|---|---|
|
||||
| `title` | string (1-500) | **yes** |
|
||||
| `description` | string (max 10000) | no |
|
||||
| `status` | TaskStatus | no |
|
||||
| `priority` | TaskPriority | no |
|
||||
| `assignee` | TaskActor | no |
|
||||
| `labels` | string[] | no |
|
||||
| `model` | string | no |
|
||||
| `thinking` | ThinkingLevel | no |
|
||||
| `dueAt` | number | no |
|
||||
| `estimateMin` | number | no |
|
||||
|
||||
**Payload for `type: 'update'`:**
|
||||
|
||||
| Field | Type | Required |
|
||||
|---|---|---|
|
||||
| `id` | string | **yes** |
|
||||
| `title` | string (1-500) | no |
|
||||
| `description` | string (max 10000) | no |
|
||||
| `status` | TaskStatus | no |
|
||||
| `priority` | TaskPriority | no |
|
||||
| `assignee` | TaskActor | no |
|
||||
| `labels` | string[] | no |
|
||||
| `result` | string (max 50000) | no |
|
||||
|
||||
**Behavior:** If board `proposalPolicy` is `'auto'`, the proposal is immediately applied and returned as `approved`. Otherwise it stays `pending`.
|
||||
|
||||
**Response:** `201` with `KanbanProposal`.
|
||||
|
||||
```bash
|
||||
# Agent proposes a new task
|
||||
curl -X POST http://localhost:3000/api/kanban/proposals \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"type": "create",
|
||||
"payload": { "title": "Add rate limiting to uploads", "priority": "high" },
|
||||
"proposedBy": "agent:kim"
|
||||
}'
|
||||
|
||||
# Agent proposes updating an existing task
|
||||
curl -X POST http://localhost:3000/api/kanban/proposals \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"type": "update",
|
||||
"payload": { "id": "fix-login-bug", "status": "done", "result": "Fixed and deployed." },
|
||||
"proposedBy": "agent:kim"
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### POST /api/kanban/proposals/:id/approve
|
||||
|
||||
Approve a pending proposal. Executes the proposed action (creates/updates the task).
|
||||
|
||||
**Path params:** `id` - proposal UUID
|
||||
|
||||
**Response:** `200` with `{ proposal: KanbanProposal, task: KanbanTask }`.
|
||||
|
||||
**Errors:**
|
||||
- `404` if proposal not found.
|
||||
- `409 already_resolved` if proposal was already approved/rejected.
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/kanban/proposals/abc-uuid/approve
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### POST /api/kanban/proposals/:id/reject
|
||||
|
||||
Reject a pending proposal.
|
||||
|
||||
**Path params:** `id` - proposal UUID
|
||||
|
||||
**Body (optional):**
|
||||
|
||||
| Field | Type | Notes |
|
||||
|---|---|---|
|
||||
| `reason` | string (max 5000) | Rejection reason |
|
||||
|
||||
**Response:** `200` with `{ proposal: KanbanProposal }`.
|
||||
|
||||
**Errors:**
|
||||
- `404` if proposal not found.
|
||||
- `409 already_resolved` if already resolved.
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/kanban/proposals/abc-uuid/reject \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{ "reason": "Not needed right now." }'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task Lifecycle / State Machine
|
||||
|
||||
```text
|
||||
backlog ──┐
|
||||
├──→ in-progress ──→ review ──→ done
|
||||
todo ─────┘ │ │
|
||||
│ (abort) │ (reject)
|
||||
└──→ todo ←────┘
|
||||
```
|
||||
|
||||
**Valid transitions via workflow endpoints:**
|
||||
|
||||
| From | To | Endpoint | Notes |
|
||||
|---|---|---|---|
|
||||
| `todo`, `backlog` | `in-progress` | `/execute` | Spawns agent |
|
||||
| `in-progress` | `review` | (automatic) | On run completion |
|
||||
| `in-progress` | `todo` | `/abort` | Cancels run |
|
||||
| `review` | `done` | `/approve` | |
|
||||
| `review` | `todo` | `/reject` | Clears run + result |
|
||||
|
||||
Direct status changes via PATCH are also possible but bypass workflow guardrails.
|
||||
|
||||
## Fetch Examples (JavaScript)
|
||||
|
||||
```javascript
|
||||
// List todo tasks
|
||||
const res = await fetch('http://localhost:3000/api/kanban/tasks?status=todo');
|
||||
const { items, total, hasMore } = await res.json();
|
||||
|
||||
// Create a task
|
||||
const task = await fetch('http://localhost:3000/api/kanban/tasks', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
title: 'Implement caching layer',
|
||||
description: 'Add Redis caching for API responses',
|
||||
priority: 'high',
|
||||
labels: ['backend', 'performance'],
|
||||
}),
|
||||
}).then(r => r.json());
|
||||
|
||||
// Update with CAS
|
||||
const updated = await fetch(`http://localhost:3000/api/kanban/tasks/${task.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
version: task.version,
|
||||
assignee: 'agent:kim',
|
||||
}),
|
||||
}).then(r => r.json());
|
||||
|
||||
// Execute
|
||||
await fetch(`http://localhost:3000/api/kanban/tasks/${task.id}/execute`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ model: 'claude-sonnet-4-20250514' }),
|
||||
});
|
||||
```
|
||||
Loading…
Reference in a new issue