diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..0524676 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,41 @@ +{ + "permissions": { + "allow": [ + "Bash(pnpm --version:*)", + "Bash(docker:*)", + "Bash(node:*)", + "Bash(echo:*)", + "Bash(pnpm install:*)", + "Bash(curl:*)", + "Bash(sudo sh:*)", + "Bash(sudo usermod:*)", + "Bash(npx convex deploy)", + "Bash(pnpm dev)", + "Bash(sudo docker compose ps:*)", + "Bash(python3:*)", + "Bash(git commit:*)", + "Bash(openclaw:*)", + "Bash(dpkg:*)", + "Bash(snap list:*)", + "Bash(apt list:*)", + "Bash(pnpm --filter @clawe/shared test:*)", + "Bash(pnpm check:*)", + "Bash(pnpm prettier:*)", + "Bash(pnpm build:*)", + "Bash(pnpm test:*)", + "Bash(npx convex run:*)", + "Bash(npx convex dev:*)", + "Bash(pnpm add:*)", + "Bash(pnpm run format:check:*)", + "Bash(git -C /home/ubuntu/clawe config user.name)", + "Bash(git -C /home/ubuntu/clawe config user.email)", + "Bash(git -C /home/ubuntu/clawe config:*)", + "Bash(git -C /home/ubuntu/clawe log:*)", + "Bash(pnpm --filter web test:*)", + "Bash(npx prettier:*)", + "Bash(pnpm --filter @clawe/cli test:*)", + "Bash(pnpm ls:*)", + "Bash(pnpm fix:*)" + ] + } +} diff --git a/.gitignore b/.gitignore index 5f91ca7..d7f1edd 100644 --- a/.gitignore +++ b/.gitignore @@ -50,4 +50,7 @@ docker/openclaw/data/ .data/ # OpenClaw state directory (shared with Docker in dev) -.openclaw/ \ No newline at end of file +.openclaw/* +!.openclaw/.gitkeep +.openclaw/logs/* +!.openclaw/logs/.gitkeep \ No newline at end of file diff --git a/.openclaw/.gitkeep b/.openclaw/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/web/src/app/(dashboard)/board/page.tsx b/apps/web/src/app/(dashboard)/board/page.tsx index 6a027dd..5b7ad1d 100644 --- a/apps/web/src/app/(dashboard)/board/page.tsx +++ b/apps/web/src/app/(dashboard)/board/page.tsx @@ -20,6 +20,7 @@ import { import { KanbanBoard, type KanbanTask, + type KanbanSubtask, type KanbanColumnDef, } from "@/components/kanban"; import { LiveFeed, LiveFeedTitle } from "@/components/live-feed"; @@ -42,16 +43,21 @@ function mapPriority(priority?: string): "low" | "medium" | "high" { // Map Convex task to Kanban task format function mapTask(task: TaskWithAssignees): KanbanTask { - const subtasks: KanbanTask[] = - task.subtasks - ?.filter((st) => !st.done) - .map((st, i) => ({ - id: `${task._id}-${i}`, - title: st.title, - description: st.description, - priority: "medium", - subtasks: [], - })) || []; + const subtasks: KanbanSubtask[] = + task.subtasks?.map((st, i) => ({ + id: `${task._id}-${i}`, + title: st.title, + description: st.description, + done: st.done || false, + status: st.status ?? (st.done ? "done" : "pending"), + blockedReason: st.blockedReason, + assignee: (() => { + if (!st.assigneeId) return undefined; + const agent = task.assignees?.find((a) => a._id === st.assigneeId); + return agent ? `${agent.emoji || ""} ${agent.name}`.trim() : undefined; + })(), + doneAt: st.doneAt, + })) || []; return { id: task._id, diff --git a/apps/web/src/components/kanban/_components/document-viewer-modal.tsx b/apps/web/src/components/kanban/_components/document-viewer-modal.tsx index 1f83c94..c601243 100644 --- a/apps/web/src/components/kanban/_components/document-viewer-modal.tsx +++ b/apps/web/src/components/kanban/_components/document-viewer-modal.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useMemo } from "react"; import axios from "axios"; import { Dialog, @@ -16,6 +16,51 @@ import type { DocumentWithCreator } from "@clawe/backend/types"; const VIEWER_HEIGHT = "h-[500px]"; +/** Very simple markdown → HTML renderer for preview mode */ +function renderMarkdown(md: string): string { + const html = md + // Escape HTML + .replace(/&/g, "&") + .replace(//g, ">") + // Headers + .replace( + /^#### (.+)$/gm, + '

$1

', + ) + .replace( + /^### (.+)$/gm, + '

$1

', + ) + .replace(/^## (.+)$/gm, '

$1

') + .replace(/^# (.+)$/gm, '

$1

') + // Bold & italic + .replace(/\*\*(.+?)\*\*/g, "$1") + .replace(/\*(.+?)\*/g, "$1") + // Inline code + .replace( + /`([^`]+)`/g, + '$1', + ) + // Unordered lists + .replace(/^- (.+)$/gm, '
  • $1
  • ') + // Ordered lists + .replace(/^\d+\. (.+)$/gm, '
  • $1
  • ') + // Horizontal rules + .replace( + /^---$/gm, + '
    ', + ) + // Paragraphs (double newlines) + .replace(/\n\n/g, '

    ') + // Single newlines within paragraphs + .replace(/\n/g, "
    "); + + return `

    ${html}

    `; +} + +type ViewMode = "preview" | "raw"; + export type DocumentViewerModalProps = { document: DocumentWithCreator | null; open: boolean; @@ -29,11 +74,13 @@ export const DocumentViewerModal = ({ }: DocumentViewerModalProps) => { const [fileContent, setFileContent] = useState(null); const [isLoading, setIsLoading] = useState(false); + const [viewMode, setViewMode] = useState("preview"); useEffect(() => { const fileUrl = document?.fileUrl; if (!fileUrl || !open) { setFileContent(null); + setViewMode("preview"); return; } @@ -63,9 +110,14 @@ export const DocumentViewerModal = ({ }; }, [document?.fileUrl, open]); - if (!document) return null; + const content = fileContent ?? document?.content; - const content = fileContent ?? document.content; + const previewHtml = useMemo(() => { + if (!content) return ""; + return renderMarkdown(content); + }, [content]); + + if (!document) return null; return ( @@ -73,14 +125,40 @@ export const DocumentViewerModal = ({ {document.title} - {document.fileUrl && ( - - )} +
    +
    + + +
    + {document.fileUrl && ( + + )} +
    @@ -95,7 +173,14 @@ export const DocumentViewerModal = ({ ) : content ? (
    -
    {content}
    + {viewMode === "raw" ? ( +
    {content}
    + ) : ( +
    + )}
    ) : (
    void; + open?: boolean; + onToggle?: () => void; + maxVisible?: number; + onShowAll?: () => void; }; export const DocumentsSection = ({ taskId, onViewDocument, + open = true, + onToggle, + maxVisible, + onShowAll, }: DocumentsSectionProps) => { const documents = useQuery(api.documents.getForTask, { taskId: taskId as Id<"tasks">, }); - // Filter to only show deliverables - const deliverables = documents?.filter((d) => d.type === "deliverable") ?? []; + // Filter to only show deliverables, sorted newest first + const deliverables = ( + documents?.filter((d) => d.type === "deliverable") ?? [] + ).sort((a, b) => b.createdAt - a.createdAt); if (deliverables.length === 0) { return null; @@ -29,46 +52,76 @@ export const DocumentsSection = ({ return (
    -

    - Documents ({deliverables.length}) -

    -
      - {deliverables.map((doc) => ( -
    • -
      - - {doc.title} -
      -
      - {doc.fileUrl && ( - <> - - - - )} -
      -
    • - ))} -
    + + {open && ( + <> +
    + {(maxVisible + ? deliverables.slice(0, maxVisible) + : deliverables + ).map((doc) => { + const creatorLabel = doc.creator + ? `${doc.creator.emoji || ""} ${doc.creator.name}`.trim() + : null; + + return ( +
    onViewDocument(doc)} + > + {/* Icon + Title + Meta */} +
    +
    + +
    +
    +

    + {doc.title} +

    +

    + {creatorLabel ? `by ${creatorLabel}` : ""} + {creatorLabel ? " · " : ""} + {timeAgo(doc.createdAt)} +

    +
    +
    + + {/* View overlay on hover */} +
    + + + View + +
    +
    + ); + })} +
    + {maxVisible && deliverables.length > maxVisible && ( + + )} + + )}
    ); }; diff --git a/apps/web/src/components/kanban/kanban-card.tsx b/apps/web/src/components/kanban/kanban-card.tsx index a9bc63b..7016a4e 100644 --- a/apps/web/src/components/kanban/kanban-card.tsx +++ b/apps/web/src/components/kanban/kanban-card.tsx @@ -7,6 +7,10 @@ import { AlignLeft, User, FileText, + Circle, + CheckCircle2, + Loader2, + AlertTriangle, } from "lucide-react"; import { cn } from "@clawe/ui/lib/utils"; import { @@ -141,6 +145,7 @@ export const KanbanCard = ({ ) : ( )} + {task.subtasks.filter((st) => st.done).length}/ {task.subtasks.length} subtask {task.subtasks.length !== 1 && "s"} @@ -149,7 +154,7 @@ export const KanbanCard = ({ {task.documentCount && task.documentCount > 0 && ( - {task.documentCount} document + {task.documentCount} doc {task.documentCount !== 1 && "s"} )} @@ -159,17 +164,38 @@ export const KanbanCard = ({ {/* Expanded subtasks */} {expanded && hasSubtasks && ( -
    - {task.subtasks.map((subtask) => ( - - ))} -
    +
      + {task.subtasks.map((subtask) => { + const status = + subtask.status ?? (subtask.done ? "done" : "pending"); + return ( +
    • + {status === "done" && ( + + )} + {status === "in_progress" && ( + + )} + {status === "blocked" && ( + + )} + {status === "pending" && ( + + )} + + {subtask.title} + +
    • + ); + })} +
    )}
    ); diff --git a/apps/web/src/components/kanban/task-detail-modal.tsx b/apps/web/src/components/kanban/task-detail-modal.tsx index ab54af8..cc2d1c0 100644 --- a/apps/web/src/components/kanban/task-detail-modal.tsx +++ b/apps/web/src/components/kanban/task-detail-modal.tsx @@ -12,13 +12,33 @@ import { import { Button } from "@clawe/ui/components/button"; import { Textarea } from "@clawe/ui/components/textarea"; import { cn } from "@clawe/ui/lib/utils"; -import { Circle, ThumbsUp, Pencil } from "lucide-react"; +import { + Circle, + CheckCircle2, + Loader2, + AlertTriangle, + ThumbsUp, + Pencil, + ChevronDown, + ChevronRight, +} from "lucide-react"; import type { Id } from "@clawe/backend/dataModel"; import type { KanbanTask } from "./types"; import type { DocumentWithCreator } from "@clawe/backend/types"; import { DocumentsSection } from "./_components/documents-section"; import { DocumentViewerModal } from "./_components/document-viewer-modal"; +function timeAgo(timestamp: number): string { + const diff = Date.now() - timestamp; + const minutes = Math.floor(diff / 60000); + const hours = Math.floor(diff / 3600000); + const days = Math.floor(diff / 86400000); + if (minutes < 1) return "now"; + if (minutes < 60) return `${minutes}m`; + if (hours < 24) return `${hours}h`; + return `${days}d`; +} + const priorityConfig: Record< KanbanTask["priority"], { label: string; className: string } @@ -26,7 +46,7 @@ const priorityConfig: Record< high: { label: "High", className: - "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400", + "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400", }, medium: { label: "Medium", @@ -55,6 +75,10 @@ export const TaskDetailModal = ({ const [showFeedback, setShowFeedback] = useState(false); const [feedback, setFeedback] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); + const [subtasksOpen, setSubtasksOpen] = useState(false); + const [docsOpen, setDocsOpen] = useState(true); + const [docsShowAll, setDocsShowAll] = useState(false); + const [descExpanded, setDescExpanded] = useState(false); const approve = useMutation(api.tasks.approve); const requestChanges = useMutation(api.tasks.requestChanges); @@ -64,6 +88,9 @@ export const TaskDetailModal = ({ const priority = priorityConfig[task.priority]; const hasSubtasks = task.subtasks.length > 0; const isReview = task.status === "review"; + const doneCount = task.subtasks.filter((st) => st.done).length; + const totalCount = task.subtasks.length; + const progressPercent = totalCount > 0 ? (doneCount / totalCount) * 100 : 0; const handleApprove = async () => { setIsSubmitting(true); @@ -104,51 +131,154 @@ export const TaskDetailModal = ({ return ( <> - + - {task.title} + + {task.title} + -
    - {/* Priority badge */} +
    + {/* Priority & Assignee row */}
    - {priority.label} Priority + {priority.label} + {task.assignee && ( + + Assigned to {task.assignee} + + )}
    {/* Description */} {task.description && (
    -

    +

    Description

    -

    {task.description}

    +

    + {task.description} +

    + {task.description.length > 120 && ( + + )}
    )} - {/* Subtasks list */} + {/* Subtasks */} {hasSubtasks && (
    -

    - Subtasks ({task.subtasks.length}) -

    -
      - {task.subtasks.map((subtask) => ( -
    • - - {subtask.title} -
    • - ))} -
    + + + {/* Progress bar (always visible) */} +
    +
    +
    + + {subtasksOpen && ( +
      + {task.subtasks.map((subtask) => { + const status = + subtask.status ?? (subtask.done ? "done" : "pending"); + return ( +
    • +
      + {status === "done" && ( + + )} + {status === "in_progress" && ( + + )} + {status === "blocked" && ( + + )} + {status === "pending" && ( + + )} +
      +
      + + {subtask.title} + + {status === "blocked" && subtask.blockedReason && ( +

      + {subtask.blockedReason} +

      + )} +
      +
      + {subtask.assignee && ( + {subtask.assignee} + )} + {subtask.assignee && subtask.doneAt && ( + · + )} + {subtask.doneAt && ( + {timeAgo(subtask.doneAt)} + )} +
      +
    • + ); + })} +
    + )}
    )} @@ -156,6 +286,10 @@ export const TaskDetailModal = ({ setDocsOpen(!docsOpen)} + maxVisible={docsShowAll ? undefined : 2} + onShowAll={() => setDocsShowAll(true)} /> {/* Review actions */} diff --git a/apps/web/src/components/kanban/types.ts b/apps/web/src/components/kanban/types.ts index 21b50f9..1e5304f 100644 --- a/apps/web/src/components/kanban/types.ts +++ b/apps/web/src/components/kanban/types.ts @@ -1,3 +1,16 @@ +export type SubtaskStatus = "pending" | "in_progress" | "done" | "blocked"; + +export type KanbanSubtask = { + id: string; + title: string; + description?: string; + done?: boolean; + status?: SubtaskStatus; + blockedReason?: string; + assignee?: string; + doneAt?: number; +}; + // Kanban's own task type (isolated from Convex) export type KanbanTask = { id: string; @@ -6,7 +19,7 @@ export type KanbanTask = { status?: "inbox" | "assigned" | "in_progress" | "review" | "done"; priority: "low" | "medium" | "high"; assignee?: string; - subtasks: KanbanTask[]; + subtasks: KanbanSubtask[]; documentCount?: number; }; diff --git a/docker/openclaw/templates/shared/CLAWE-CLI.md b/docker/openclaw/templates/shared/CLAWE-CLI.md index d8909bc..0ce3df0 100644 --- a/docker/openclaw/templates/shared/CLAWE-CLI.md +++ b/docker/openclaw/templates/shared/CLAWE-CLI.md @@ -66,13 +66,42 @@ Options: - `--assign` — Who should do it - `--by` — Who created it (you) - `--priority` — `low`, `normal`, `high`, or `urgent` +- `--description` — Task description with context Example: ```bash -clawe task:create "Write blog post about AI teams" --assign agent:inky:main --by agent:main:main --priority high +clawe task:create "Write blog post about AI teams" --assign agent:inky:main --by agent:main:main --priority high --description "2000 words, practical focus" ``` +### Plan a Full Task (Recommended) + +Create a task with description + subtasks + assignments in one shot: + +```bash +clawe task:plan '' +``` + +Example: + +```bash +clawe task:plan '{ + "title": "Blog Post: AI Agent Teams", + "description": "Write a 2000-word post about multi-agent coordination. Target: developers. Tone: practical.", + "priority": "high", + "assignee": "agent:inky:main", + "by": "agent:main:main", + "subtasks": [ + { "title": "Research topic", "assign": "agent:scout:main" }, + { "title": "Write first draft (2000 words)" }, + { "title": "SEO optimization", "assign": "agent:scout:main" }, + { "title": "Create hero image", "assign": "agent:pixel:main" } + ] +}' +``` + +This creates the task, all subtasks, assigns agents, and sends notifications — all atomically. + ### View Task Details ```bash @@ -204,6 +233,7 @@ Options: | Check messages | `clawe check ` | | My tasks | `clawe tasks ` | | Create task | `clawe task:create "" --assign <who> --by <me>` | +| Plan full task | `clawe task:plan '<json>'` | | Update status | `clawe task:status <id> <status> --by <me>` | | Comment | `clawe task:comment <id> "<msg>" --by <me>` | | Squad status | `clawe squad` | diff --git a/docker/openclaw/templates/workspaces/clawe/SOUL.md b/docker/openclaw/templates/workspaces/clawe/SOUL.md index 49f26ae..c4df86d 100644 --- a/docker/openclaw/templates/workspaces/clawe/SOUL.md +++ b/docker/openclaw/templates/workspaces/clawe/SOUL.md @@ -45,6 +45,45 @@ You are the **squad lead**, not a worker. Your job is to: If a specialist "isn't available" or "not set up" — tell the human. Do NOT fall back to doing it yourself. You are a manager, not a backup worker. +## Task Planning + +When your human asks for something, use `clawe task:plan` to create a complete task in one shot: + +```bash +clawe task:plan '{ + "title": "Blog Post: Database Migration Best Practices", + "description": "Write a 2000-word blog post covering zero-downtime migrations, rollback strategies, and schema versioning. Target audience: mid-level developers. Tone: practical and direct.", + "priority": "high", + "assignee": "agent:inky:main", + "by": "agent:main:main", + "subtasks": [ + { "title": "Research topic and find 3 competitor articles", "assign": "agent:scout:main" }, + { "title": "Write first draft (2000 words)", "description": "Include code examples for PostgreSQL and MySQL" }, + { "title": "SEO optimization — titles, meta, keywords", "assign": "agent:scout:main" }, + { "title": "Create hero image and 2 diagrams", "assign": "agent:pixel:main" }, + { "title": "Final review and polish" } + ] +}' +``` + +### Planning Rules + +1. **Always include a description** — Give context, goals, constraints, and target audience +2. **Break into clear subtasks** — Each subtask should be a concrete deliverable +3. **Assign subtasks to the right specialist** — Don't leave assignments vague +4. **Set priority** — urgent/high/normal/low +5. **Think about dependencies** — Order subtasks logically (research before writing) + +### Agent Routing + +| Need | Assign to | +| ---------------------------------- | ----------------------------- | +| Writing, blog posts, copy, docs | `agent:inky:main` (Inky ✍️) | +| Images, diagrams, visual assets | `agent:pixel:main` (Pixel 🎨) | +| SEO, keywords, competitor research | `agent:scout:main` (Scout 🔍) | + +For multi-agent tasks, set the primary assignee to the main contributor and assign individual subtasks to others. + ## Shared Team Resources Coordinate via shared files: diff --git a/docker/openclaw/templates/workspaces/inky/HEARTBEAT.md b/docker/openclaw/templates/workspaces/inky/HEARTBEAT.md index 61ba4c2..87fb678 100644 --- a/docker/openclaw/templates/workspaces/inky/HEARTBEAT.md +++ b/docker/openclaw/templates/workspaces/inky/HEARTBEAT.md @@ -6,29 +6,37 @@ When you wake up, do the following: 1. Run: `clawe check agent:inky:main` 2. Read `shared/WORKING.md` for team state +3. **Always** run: `clawe tasks agent:inky:main` to check for active tasks ## If Notifications Found Process each notification — usually task assignments from Clawe. -## If Tasks Assigned +## If You Have Active Tasks + +Even without new notifications, check your active tasks for pending subtasks assigned to you. For each task in "in_progress" status: + +1. Run `clawe task:view <taskId>` to see subtask details +2. Find the **next incomplete subtask** assigned to you (✍️ Inky) +3. Check if its dependencies are met (previous subtasks should be done) +4. If ready — **do the work** ```bash -# View your tasks -clawe tasks agent:inky:main +# View task details +clawe task:view <taskId> -# For each task: -clawe task:status <taskId> in_progress --by agent:inky:main +# Mark subtask in progress +clawe subtask:progress <taskId> <index> --by agent:inky:main # Do the work... -# Mark subtasks done -clawe subtask:check <taskId> 0 --by agent:inky:main +# Mark subtask done +clawe subtask:check <taskId> <index> --by agent:inky:main # Register deliverables clawe deliver <taskId> /path/to/file.md "Article Draft" --by agent:inky:main -# Submit for review +# When ALL subtasks are done, submit for review clawe task:status <taskId> review --by agent:inky:main ``` diff --git a/docker/openclaw/templates/workspaces/pixel/HEARTBEAT.md b/docker/openclaw/templates/workspaces/pixel/HEARTBEAT.md index e775d8e..f970f1f 100644 --- a/docker/openclaw/templates/workspaces/pixel/HEARTBEAT.md +++ b/docker/openclaw/templates/workspaces/pixel/HEARTBEAT.md @@ -6,30 +6,38 @@ When you wake up, do the following: 1. Run: `clawe check agent:pixel:main` 2. Read `shared/WORKING.md` for team state +3. **Always** run: `clawe tasks agent:pixel:main` to check for active tasks ## If Notifications Found Process each notification — usually task assignments from Clawe. -## If Tasks Assigned +## If You Have Active Tasks + +Even without new notifications, check your active tasks for pending subtasks assigned to you. For each task in "in_progress" status: + +1. Run `clawe task:view <taskId>` to see subtask details +2. Find the **next incomplete subtask** assigned to you (🎨 Pixel) +3. Check if its dependencies are met (previous subtasks should be done) +4. If ready — **do the work** ```bash -# View your tasks -clawe tasks agent:pixel:main +# View task details +clawe task:view <taskId> -# For each task: -clawe task:status <taskId> in_progress --by agent:pixel:main +# Mark subtask in progress +clawe subtask:progress <taskId> <index> --by agent:pixel:main # Do the work... -# Mark subtasks done -clawe subtask:check <taskId> 0 --by agent:pixel:main +# Mark subtask done +clawe subtask:check <taskId> <index> --by agent:pixel:main # Register deliverables clawe deliver <taskId> /path/to/image.png "Hero Image" --by agent:pixel:main -# Submit for review -clawe task:status <taskId> review --by agent:pixel:main +# If a subtask is blocked, mark it and explain why +clawe subtask:block <taskId> <index> --reason "explanation" --by agent:pixel:main ``` ## If Nothing to Do diff --git a/docker/openclaw/templates/workspaces/scout/HEARTBEAT.md b/docker/openclaw/templates/workspaces/scout/HEARTBEAT.md index 63b465a..eade2b6 100644 --- a/docker/openclaw/templates/workspaces/scout/HEARTBEAT.md +++ b/docker/openclaw/templates/workspaces/scout/HEARTBEAT.md @@ -6,30 +6,38 @@ When you wake up, do the following: 1. Run: `clawe check agent:scout:main` 2. Read `shared/WORKING.md` for team state +3. **Always** run: `clawe tasks agent:scout:main` to check for active tasks ## If Notifications Found Process each notification — usually task assignments from Clawe. -## If Tasks Assigned +## If You Have Active Tasks + +Even without new notifications, check your active tasks for pending subtasks assigned to you. For each task in "in_progress" status: + +1. Run `clawe task:view <taskId>` to see subtask details +2. Find the **next incomplete subtask** assigned to you (🔍 Scout) +3. Check if its dependencies are met (previous subtasks should be done) +4. If ready — **do the work** ```bash -# View your tasks -clawe tasks agent:scout:main +# View task details +clawe task:view <taskId> -# For each task: -clawe task:status <taskId> in_progress --by agent:scout:main +# Mark subtask in progress +clawe subtask:progress <taskId> <index> --by agent:scout:main # Do the work... -# Mark subtasks done -clawe subtask:check <taskId> 0 --by agent:scout:main +# Mark subtask done +clawe subtask:check <taskId> <index> --by agent:scout:main # Register deliverables clawe deliver <taskId> /path/to/report.md "SEO Analysis" --by agent:scout:main -# Submit for review -clawe task:status <taskId> review --by agent:scout:main +# If a subtask is blocked, mark it and explain why +clawe subtask:block <taskId> <index> --reason "explanation" --by agent:scout:main ``` ## If Nothing to Do diff --git a/packages/backend/convex/schema.ts b/packages/backend/convex/schema.ts index f273beb..2127e36 100644 --- a/packages/backend/convex/schema.ts +++ b/packages/backend/convex/schema.ts @@ -49,6 +49,15 @@ export default defineSchema({ done: v.boolean(), doneAt: v.optional(v.number()), assigneeId: v.optional(v.id("agents")), + status: v.optional( + v.union( + v.literal("pending"), + v.literal("in_progress"), + v.literal("done"), + v.literal("blocked"), + ), + ), + blockedReason: v.optional(v.string()), }), ), ), diff --git a/packages/backend/convex/tasks.ts b/packages/backend/convex/tasks.ts index 245394a..8abdffe 100644 --- a/packages/backend/convex/tasks.ts +++ b/packages/backend/convex/tasks.ts @@ -307,6 +307,23 @@ export const updateStatus = mutation({ } } + // Gate: can't move to "review" or "done" if subtasks are still pending/in_progress + if ( + (args.status === "review" || args.status === "done") && + task.subtasks && + task.subtasks.length > 0 + ) { + const unfinished = task.subtasks.filter((st) => { + const status = st.status ?? (st.done ? "done" : "pending"); + return status !== "done" && status !== "blocked"; + }); + if (unfinished.length > 0) { + throw new Error( + `Cannot move to ${args.status}: ${unfinished.length} subtask(s) still pending or in progress`, + ); + } + } + // Update task const updates: Record<string, unknown> = { status: args.status, @@ -616,12 +633,21 @@ export const addSubtask = mutation({ }, }); -// Mark subtask as done/undone +// Update subtask status export const updateSubtask = mutation({ args: { taskId: v.id("tasks"), subtaskIndex: v.number(), - done: v.boolean(), + done: v.optional(v.boolean()), + status: v.optional( + v.union( + v.literal("pending"), + v.literal("in_progress"), + v.literal("done"), + v.literal("blocked"), + ), + ), + blockedReason: v.optional(v.string()), bySessionKey: v.optional(v.string()), }, handler: async (ctx, args) => { @@ -634,17 +660,25 @@ export const updateSubtask = mutation({ const subtasks = [...task.subtasks]; const currentSubtask = subtasks[args.subtaskIndex]; - // We already checked this exists above, but TypeScript needs reassurance if (!currentSubtask) { throw new Error("Subtask not found"); } + // Determine new status — support both legacy `done` flag and new `status` field + let newStatus = args.status; + if (!newStatus && args.done !== undefined) { + newStatus = args.done ? "done" : "pending"; + } + const isDone = newStatus === "done"; + const updatedSubtask = { title: currentSubtask.title, description: currentSubtask.description, assigneeId: currentSubtask.assigneeId, - done: args.done, - doneAt: args.done ? now : undefined, + done: isDone, + doneAt: isDone ? now : undefined, + status: newStatus ?? currentSubtask.status ?? "pending", + blockedReason: newStatus === "blocked" ? args.blockedReason : undefined, }; subtasks[args.subtaskIndex] = updatedSubtask; @@ -653,22 +687,23 @@ export const updateSubtask = mutation({ updatedAt: now, }); - // Log activity if completing - if (args.done) { - let agentId = undefined; - let agentName = "System"; - if (args.bySessionKey) { - const sessionKey = args.bySessionKey; - const agent = await ctx.db - .query("agents") - .withIndex("by_sessionKey", (q) => q.eq("sessionKey", sessionKey)) - .first(); - if (agent) { - agentId = agent._id; - agentName = agent.name; - } + // Find agent + let agentId = undefined; + let agentName = "System"; + if (args.bySessionKey) { + const sessionKey = args.bySessionKey; + const agent = await ctx.db + .query("agents") + .withIndex("by_sessionKey", (q) => q.eq("sessionKey", sessionKey)) + .first(); + if (agent) { + agentId = agent._id; + agentName = agent.name; } + } + // Log activity + if (isDone) { await ctx.db.insert("activities", { type: "subtask_completed", agentId, @@ -676,6 +711,27 @@ export const updateSubtask = mutation({ message: `${agentName} completed "${updatedSubtask.title}" on "${task.title}"`, createdAt: now, }); + } else if (newStatus === "blocked") { + await ctx.db.insert("activities", { + type: "subtask_blocked" as any, + agentId, + taskId: args.taskId, + message: `${agentName} blocked "${updatedSubtask.title}" on "${task.title}"${args.blockedReason ? `: ${args.blockedReason}` : ""}`, + createdAt: now, + }); + + // Notify task creator about the blocked subtask + if (task.createdBy) { + await ctx.db.insert("notifications", { + targetAgentId: task.createdBy, + sourceAgentId: agentId, + type: "review_requested", + taskId: args.taskId, + content: `⚠️ Subtask blocked: "${updatedSubtask.title}" on "${task.title}"${args.blockedReason ? ` — ${args.blockedReason}` : ""}`, + delivered: false, + createdAt: now, + }); + } } }, }); @@ -754,6 +810,139 @@ export const createFromDashboard = mutation({ }, }); +// Create a full task with description, subtasks, and assignments in one atomic operation +export const createWithPlan = mutation({ + args: { + title: v.string(), + description: v.string(), + priority: v.optional( + v.union( + v.literal("low"), + v.literal("normal"), + v.literal("high"), + v.literal("urgent"), + ), + ), + assigneeSessionKey: v.optional(v.string()), + createdBySessionKey: v.optional(v.string()), + subtasks: v.array( + v.object({ + title: v.string(), + description: v.optional(v.string()), + assigneeSessionKey: v.optional(v.string()), + }), + ), + }, + handler: async (ctx, args) => { + const now = Date.now(); + + // Resolve creator + let createdBy = undefined; + let creatorName = "System"; + if (args.createdBySessionKey) { + const sessionKey = args.createdBySessionKey; + const creator = await ctx.db + .query("agents") + .withIndex("by_sessionKey", (q) => q.eq("sessionKey", sessionKey)) + .first(); + if (creator) { + createdBy = creator._id; + creatorName = creator.name; + } + } + + // Resolve primary assignee + const assigneeIds: Id<"agents">[] = []; + if (args.assigneeSessionKey) { + const sessionKey = args.assigneeSessionKey; + const assignee = await ctx.db + .query("agents") + .withIndex("by_sessionKey", (q) => q.eq("sessionKey", sessionKey)) + .first(); + if (assignee) { + assigneeIds.push(assignee._id); + } + } + + // Resolve subtask assignees and build subtasks array + const subtasks = []; + for (const st of args.subtasks) { + let assigneeId = undefined; + if (st.assigneeSessionKey) { + const sessionKey = st.assigneeSessionKey; + const assignee = await ctx.db + .query("agents") + .withIndex("by_sessionKey", (q) => q.eq("sessionKey", sessionKey)) + .first(); + if (assignee) { + assigneeId = assignee._id; + // Also add subtask assignees to task-level assignees if not already there + if (!assigneeIds.includes(assignee._id)) { + assigneeIds.push(assignee._id); + } + } + } + + subtasks.push({ + title: st.title, + description: st.description, + done: false, + assigneeId, + }); + } + + // Create the task + const taskId = await ctx.db.insert("tasks", { + title: args.title, + description: args.description, + status: assigneeIds.length > 0 ? "assigned" : "inbox", + priority: args.priority ?? "normal", + assigneeIds: assigneeIds.length > 0 ? assigneeIds : undefined, + subtasks: subtasks.length > 0 ? subtasks : undefined, + createdBy, + createdAt: now, + updatedAt: now, + }); + + // Log activity + await ctx.db.insert("activities", { + type: "task_created", + agentId: createdBy, + taskId, + message: `${creatorName} planned task: ${args.title} (${subtasks.length} subtasks)`, + createdAt: now, + }); + + // Send notifications to all assigned agents + for (const assigneeId of assigneeIds) { + const assignee = await ctx.db.get(assigneeId); + if (assignee) { + // Build notification content with subtask details + const agentSubtasks = subtasks + .filter((st) => st.assigneeId === assigneeId) + .map((st) => ` • ${st.title}`) + .join("\n"); + + const content = agentSubtasks + ? `📋 New task: "${args.title}"\n\nYour subtasks:\n${agentSubtasks}` + : `📋 New task assigned: "${args.title}"`; + + await ctx.db.insert("notifications", { + targetAgentId: assigneeId, + sourceAgentId: createdBy, + type: "task_assigned", + taskId, + content, + delivered: false, + createdAt: now, + }); + } + } + + return taskId; + }, +}); + // Delete a task export const remove = mutation({ args: { taskId: v.id("tasks") }, diff --git a/packages/cli/src/commands/subtask-check.spec.ts b/packages/cli/src/commands/subtask-check.spec.ts index 787a6b4..e1bd0a3 100644 --- a/packages/cli/src/commands/subtask-check.spec.ts +++ b/packages/cli/src/commands/subtask-check.spec.ts @@ -24,6 +24,7 @@ describe("subtaskCheck", () => { taskId: "task-123", subtaskIndex: 0, done: true, + status: "done", bySessionKey: undefined, }); expect(console.log).toHaveBeenCalledWith("✅ Subtask 0 marked as done"); @@ -38,6 +39,7 @@ describe("subtaskCheck", () => { taskId: "task-456", subtaskIndex: 2, done: true, + status: "done", bySessionKey: "agent:inky:main", }); }); @@ -51,6 +53,7 @@ describe("subtaskCheck", () => { taskId: "task-789", subtaskIndex: 5, done: true, + status: "done", bySessionKey: undefined, }); }); @@ -71,9 +74,10 @@ describe("subtaskUncheck", () => { taskId: "task-123", subtaskIndex: 1, done: false, + status: "pending", bySessionKey: undefined, }); - expect(console.log).toHaveBeenCalledWith("✅ Subtask 1 marked as not done"); + expect(console.log).toHaveBeenCalledWith("✅ Subtask 1 marked as pending"); }); it("marks subtask as not done with agent attribution", async () => { @@ -85,6 +89,7 @@ describe("subtaskUncheck", () => { taskId: "task-456", subtaskIndex: 0, done: false, + status: "pending", bySessionKey: "agent:main:main", }); }); diff --git a/packages/cli/src/commands/subtask-check.ts b/packages/cli/src/commands/subtask-check.ts index 2043b3f..ac6869e 100644 --- a/packages/cli/src/commands/subtask-check.ts +++ b/packages/cli/src/commands/subtask-check.ts @@ -15,6 +15,7 @@ export async function subtaskCheck( taskId: taskId as Id<"tasks">, subtaskIndex: parseInt(index, 10), done: true, + status: "done", bySessionKey: options.by, }); @@ -30,8 +31,51 @@ export async function subtaskUncheck( taskId: taskId as Id<"tasks">, subtaskIndex: parseInt(index, 10), done: false, + status: "pending", bySessionKey: options.by, }); - console.log(`✅ Subtask ${index} marked as not done`); + console.log(`✅ Subtask ${index} marked as pending`); +} + +interface SubtaskBlockOptions { + by?: string; + reason?: string; +} + +export async function subtaskBlock( + taskId: string, + index: string, + options: SubtaskBlockOptions, +): Promise<void> { + await client.mutation(api.tasks.updateSubtask, { + taskId: taskId as Id<"tasks">, + subtaskIndex: parseInt(index, 10), + status: "blocked", + blockedReason: options.reason, + bySessionKey: options.by, + }); + + console.log( + `⚠️ Subtask ${index} marked as blocked${options.reason ? `: ${options.reason}` : ""}`, + ); +} + +interface SubtaskProgressOptions { + by?: string; +} + +export async function subtaskProgress( + taskId: string, + index: string, + options: SubtaskProgressOptions, +): Promise<void> { + await client.mutation(api.tasks.updateSubtask, { + taskId: taskId as Id<"tasks">, + subtaskIndex: parseInt(index, 10), + status: "in_progress", + bySessionKey: options.by, + }); + + console.log(`🔄 Subtask ${index} marked as in progress`); } diff --git a/packages/cli/src/commands/task-create.spec.ts b/packages/cli/src/commands/task-create.spec.ts index e13d972..6b3d1c7 100644 --- a/packages/cli/src/commands/task-create.spec.ts +++ b/packages/cli/src/commands/task-create.spec.ts @@ -58,6 +58,22 @@ describe("taskCreate", () => { }); }); + it("creates a task with description", async () => { + vi.mocked(client.mutation).mockResolvedValue("task-desc"); + + await taskCreate("Write blog post", { + description: "2000 words, practical focus", + }); + + expect(client.mutation).toHaveBeenCalledWith(expect.anything(), { + title: "Write blog post", + description: "2000 words, practical focus", + assigneeSessionKey: undefined, + createdBySessionKey: undefined, + priority: undefined, + }); + }); + it("creates a task with all options", async () => { vi.mocked(client.mutation).mockResolvedValue("task-full"); @@ -65,10 +81,12 @@ describe("taskCreate", () => { assign: "agent:inky:main", by: "agent:main:main", priority: "high", + description: "Detailed task description", }); expect(client.mutation).toHaveBeenCalledWith(expect.anything(), { title: "Full featured task", + description: "Detailed task description", assigneeSessionKey: "agent:inky:main", createdBySessionKey: "agent:main:main", priority: "high", diff --git a/packages/cli/src/commands/task-create.ts b/packages/cli/src/commands/task-create.ts index bb17b52..730f32a 100644 --- a/packages/cli/src/commands/task-create.ts +++ b/packages/cli/src/commands/task-create.ts @@ -5,6 +5,7 @@ interface TaskCreateOptions { assign?: string; by?: string; priority?: "low" | "normal" | "high" | "urgent"; + description?: string; } export async function taskCreate( @@ -13,6 +14,7 @@ export async function taskCreate( ): Promise<void> { const taskId = await client.mutation(api.tasks.create, { title, + description: options.description, assigneeSessionKey: options.assign, createdBySessionKey: options.by, priority: options.priority, diff --git a/packages/cli/src/commands/task-plan.spec.ts b/packages/cli/src/commands/task-plan.spec.ts new file mode 100644 index 0000000..4080c16 --- /dev/null +++ b/packages/cli/src/commands/task-plan.spec.ts @@ -0,0 +1,231 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { taskPlan } from "./task-plan.js"; + +vi.mock("../client.js", () => ({ + client: { + mutation: vi.fn(), + }, +})); + +import { client } from "../client.js"; + +describe("taskPlan", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.spyOn(console, "log").mockImplementation(() => {}); + vi.spyOn(console, "error").mockImplementation(() => {}); + vi.spyOn(process, "exit").mockImplementation(() => { + throw new Error("process.exit"); + }); + }); + + it("creates a task plan with all fields", async () => { + vi.mocked(client.mutation).mockResolvedValue("task-plan-1"); + + const plan = JSON.stringify({ + title: "Blog Post: AI Teams", + description: "Write a 2000-word post", + priority: "high", + assignee: "agent:inky:main", + by: "agent:main:main", + subtasks: [ + { title: "Research topic", assign: "agent:scout:main" }, + { title: "Write first draft" }, + { title: "Create hero image", assign: "agent:pixel:main" }, + ], + }); + + await taskPlan(plan); + + expect(client.mutation).toHaveBeenCalledWith(expect.anything(), { + title: "Blog Post: AI Teams", + description: "Write a 2000-word post", + priority: "high", + assigneeSessionKey: "agent:inky:main", + createdBySessionKey: "agent:main:main", + subtasks: [ + { + title: "Research topic", + description: undefined, + assigneeSessionKey: "agent:scout:main", + }, + { + title: "Write first draft", + description: undefined, + assigneeSessionKey: undefined, + }, + { + title: "Create hero image", + description: undefined, + assigneeSessionKey: "agent:pixel:main", + }, + ], + }); + expect(console.log).toHaveBeenCalledWith("✅ Task planned: task-plan-1"); + }); + + it("creates a minimal task plan", async () => { + vi.mocked(client.mutation).mockResolvedValue("task-plan-2"); + + const plan = JSON.stringify({ + title: "Quick task", + description: "Do something simple", + subtasks: [{ title: "Step one" }], + }); + + await taskPlan(plan); + + expect(client.mutation).toHaveBeenCalledWith(expect.anything(), { + title: "Quick task", + description: "Do something simple", + priority: undefined, + assigneeSessionKey: undefined, + createdBySessionKey: undefined, + subtasks: [ + { + title: "Step one", + description: undefined, + assigneeSessionKey: undefined, + }, + ], + }); + }); + + it("maps subtask descriptions correctly", async () => { + vi.mocked(client.mutation).mockResolvedValue("task-plan-3"); + + const plan = JSON.stringify({ + title: "Detailed task", + description: "Task with subtask descriptions", + subtasks: [ + { title: "Research", description: "Find 3 competitor articles" }, + { title: "Write draft" }, + ], + }); + + await taskPlan(plan); + + expect(client.mutation).toHaveBeenCalledWith(expect.anything(), { + title: "Detailed task", + description: "Task with subtask descriptions", + priority: undefined, + assigneeSessionKey: undefined, + createdBySessionKey: undefined, + subtasks: [ + { + title: "Research", + description: "Find 3 competitor articles", + assigneeSessionKey: undefined, + }, + { + title: "Write draft", + description: undefined, + assigneeSessionKey: undefined, + }, + ], + }); + }); + + it("exits on invalid JSON", async () => { + await expect(taskPlan("not-json")).rejects.toThrow("process.exit"); + + expect(console.error).toHaveBeenCalledWith( + "Error: Invalid JSON. Expected a task plan object.", + ); + expect(client.mutation).not.toHaveBeenCalled(); + }); + + it("exits when title is missing", async () => { + const plan = JSON.stringify({ + description: "No title here", + subtasks: [{ title: "Step" }], + }); + + await expect(taskPlan(plan)).rejects.toThrow("process.exit"); + + expect(console.error).toHaveBeenCalledWith( + "Error: Plan must include 'title'", + ); + expect(client.mutation).not.toHaveBeenCalled(); + }); + + it("exits when description is missing", async () => { + const plan = JSON.stringify({ + title: "No description", + subtasks: [{ title: "Step" }], + }); + + await expect(taskPlan(plan)).rejects.toThrow("process.exit"); + + expect(console.error).toHaveBeenCalledWith( + "Error: Plan must include 'description'", + ); + expect(client.mutation).not.toHaveBeenCalled(); + }); + + it("exits when subtasks are missing", async () => { + const plan = JSON.stringify({ + title: "No subtasks", + description: "Missing subtasks array", + }); + + await expect(taskPlan(plan)).rejects.toThrow("process.exit"); + + expect(console.error).toHaveBeenCalledWith( + "Error: Plan must include at least one subtask", + ); + expect(client.mutation).not.toHaveBeenCalled(); + }); + + it("exits when subtasks array is empty", async () => { + const plan = JSON.stringify({ + title: "Empty subtasks", + description: "Has empty subtasks array", + subtasks: [], + }); + + await expect(taskPlan(plan)).rejects.toThrow("process.exit"); + + expect(console.error).toHaveBeenCalledWith( + "Error: Plan must include at least one subtask", + ); + expect(client.mutation).not.toHaveBeenCalled(); + }); + + it("handles mutation error", async () => { + vi.mocked(client.mutation).mockRejectedValue(new Error("Convex error")); + + const plan = JSON.stringify({ + title: "Failing task", + description: "This will fail", + subtasks: [{ title: "Step" }], + }); + + await expect(taskPlan(plan)).rejects.toThrow("process.exit"); + + expect(console.error).toHaveBeenCalledWith("Error:", "Convex error"); + }); + + it("prints subtask listing on success", async () => { + vi.mocked(client.mutation).mockResolvedValue("task-plan-list"); + + const plan = JSON.stringify({ + title: "Task with listing", + description: "Check output", + subtasks: [ + { title: "Research", assign: "agent:scout:main" }, + { title: "Write draft" }, + ], + }); + + await taskPlan(plan); + + expect(console.log).toHaveBeenCalledWith( + " 0. Research → agent:scout:main", + ); + expect(console.log).toHaveBeenCalledWith(" 1. Write draft"); + expect(console.log).toHaveBeenCalledWith( + "Notifications sent to all assigned agents.", + ); + }); +}); diff --git a/packages/cli/src/commands/task-plan.ts b/packages/cli/src/commands/task-plan.ts new file mode 100644 index 0000000..8db0a91 --- /dev/null +++ b/packages/cli/src/commands/task-plan.ts @@ -0,0 +1,93 @@ +import { client } from "../client.js"; +import { api } from "@clawe/backend"; + +interface TaskPlan { + title: string; + description: string; + priority?: "low" | "normal" | "high" | "urgent"; + assignee?: string; // primary assignee session key + by?: string; // creator session key + subtasks: Array<{ + title: string; + description?: string; + assign?: string; // subtask assignee session key + }>; +} + +export async function taskPlan(planJson: string): Promise<void> { + let plan: TaskPlan; + + try { + plan = JSON.parse(planJson); + } catch { + console.error("Error: Invalid JSON. Expected a task plan object."); + console.error(""); + console.error("Example:"); + console.error( + JSON.stringify( + { + title: "Blog Post: Topic", + description: "Write a 2000-word post about...", + priority: "high", + assignee: "agent:inky:main", + by: "agent:main:main", + subtasks: [ + { title: "Research topic", assign: "agent:scout:main" }, + { title: "Write first draft" }, + { title: "Create hero image", assign: "agent:pixel:main" }, + ], + }, + null, + 2, + ), + ); + process.exit(1); + } + + // Validate required fields + if (!plan.title) { + console.error("Error: Plan must include 'title'"); + process.exit(1); + } + if (!plan.description) { + console.error("Error: Plan must include 'description'"); + process.exit(1); + } + if (!plan.subtasks || plan.subtasks.length === 0) { + console.error("Error: Plan must include at least one subtask"); + process.exit(1); + } + + console.log(`📋 Creating task plan: ${plan.title}`); + console.log(` ${plan.subtasks.length} subtask(s)`); + if (plan.assignee) console.log(` Assigned to: ${plan.assignee}`); + console.log(""); + + try { + const taskId = await client.mutation(api.tasks.createWithPlan, { + title: plan.title, + description: plan.description, + priority: plan.priority, + assigneeSessionKey: plan.assignee, + createdBySessionKey: plan.by, + subtasks: plan.subtasks.map((st) => ({ + title: st.title, + description: st.description, + assigneeSessionKey: st.assign, + })), + }); + + console.log(`✅ Task planned: ${taskId}`); + console.log(""); + console.log("Subtasks:"); + plan.subtasks.forEach((st, i) => { + const assignLabel = st.assign ? ` → ${st.assign}` : ""; + console.log(` ${i}. ${st.title}${assignLabel}`); + }); + console.log(""); + console.log("Notifications sent to all assigned agents."); + } catch (err) { + console.error("Error:", err instanceof Error ? err.message : err); + process.exit(1); + } +} diff --git a/packages/cli/src/commands/task-view.ts b/packages/cli/src/commands/task-view.ts index a82a989..2e18074 100644 --- a/packages/cli/src/commands/task-view.ts +++ b/packages/cli/src/commands/task-view.ts @@ -32,15 +32,28 @@ export async function taskView(taskId: string): Promise<void> { st: { done: boolean; title: string; + status?: string; + blockedReason?: string; assignee?: { emoji?: string; name: string } | null; }, i: number, ) => { - const check = st.done ? "✅" : "⬜"; + const status = st.status ?? (st.done ? "done" : "pending"); + const icons: Record<string, string> = { + done: "✅", + in_progress: "🔄", + blocked: "🚫", + pending: "⬜", + }; + const icon = icons[status] || "⬜"; const assignee = st.assignee ? ` → ${st.assignee.emoji || ""} ${st.assignee.name}` : ""; - console.log(` ${i}. ${check} ${st.title}${assignee}`); + const blocked = + status === "blocked" && st.blockedReason + ? ` (${st.blockedReason})` + : ""; + console.log(` ${i}. ${icon} ${st.title}${assignee}${blocked}`); }, ); console.log(); diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 5fb24a2..81f23dd 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,12 +1,18 @@ import { check } from "./commands/check.js"; import { tasks } from "./commands/tasks.js"; import { taskCreate } from "./commands/task-create.js"; +import { taskPlan } from "./commands/task-plan.js"; import { taskView } from "./commands/task-view.js"; import { taskStatus } from "./commands/task-status.js"; import { taskComment } from "./commands/task-comment.js"; import { taskAssign } from "./commands/task-assign.js"; import { subtaskAdd } from "./commands/subtask-add.js"; -import { subtaskCheck, subtaskUncheck } from "./commands/subtask-check.js"; +import { + subtaskCheck, + subtaskUncheck, + subtaskBlock, + subtaskProgress, +} from "./commands/subtask-check.js"; import { deliver, deliverables } from "./commands/deliver.js"; import { notify } from "./commands/notify.js"; import { squad } from "./commands/squad.js"; @@ -46,6 +52,9 @@ Commands: --assign <sessionKey> Assign to agent --by <sessionKey> Created by agent --priority <low|normal|high|urgent> + --description <text> Task description + clawe task:plan <json> Create task with full plan (JSON) + Includes description + subtasks + assignments clawe task:view <taskId> View full task details clawe task:status <taskId> <status> Update task status --by <sessionKey> Updated by agent @@ -57,6 +66,9 @@ Commands: --assign <sessionKey> Assign to agent --description <text> Subtask description clawe subtask:check <taskId> <index> Mark subtask done + clawe subtask:block <taskId> <index> Mark subtask blocked + --reason <text> Reason for blocking + clawe subtask:progress <taskId> <idx> Mark subtask in progress --by <sessionKey> Completed by clawe subtask:uncheck <taskId> <idx> Mark subtask not done clawe deliver <taskId> <path> <title> Register a deliverable @@ -122,13 +134,14 @@ async function main(): Promise<void> { const title = positionalArgs[0]; if (!title) { console.error( - "Usage: clawe task:create <title> [--assign <key>] [--by <key>]", + "Usage: clawe task:create <title> [--assign <key>] [--by <key>] [--description <text>]", ); process.exit(1); } await taskCreate(title, { assign: options.assign, by: options.by, + description: options.description, priority: options.priority as | "low" | "normal" @@ -139,6 +152,21 @@ async function main(): Promise<void> { break; } + case "task:plan": { + const planJson = positionalArgs[0]; + if (!planJson) { + console.error("Usage: clawe task:plan '<json>'"); + console.error(""); + console.error("Example:"); + console.error( + ` clawe task:plan '{"title":"Blog Post","description":"Write about...","subtasks":[{"title":"Research"}]}'`, + ); + process.exit(1); + } + await taskPlan(planJson); + break; + } + case "task:view": case "task:show": { const taskId = positionalArgs[0]; @@ -231,6 +259,35 @@ async function main(): Promise<void> { break; } + case "subtask:block": { + const taskId = positionalArgs[0]; + const index = positionalArgs[1]; + if (!taskId || !index) { + console.error( + "Usage: clawe subtask:block <taskId> <index> [--by <key>] [--reason <text>]", + ); + process.exit(1); + } + await subtaskBlock(taskId, index, { + by: options.by, + reason: options.reason, + }); + break; + } + + case "subtask:progress": { + const taskId = positionalArgs[0]; + const index = positionalArgs[1]; + if (!taskId || !index) { + console.error( + "Usage: clawe subtask:progress <taskId> <index> [--by <key>]", + ); + process.exit(1); + } + await subtaskProgress(taskId, index, { by: options.by }); + break; + } + case "deliver": { const taskId = positionalArgs[0]; const path = positionalArgs[1];