feat: taskplane-style specs + CLI step tracking
- Triage agent generates full PROMPT.md (mission, steps, file scope, review level, docs requirements, commit conventions, guardrails) - Executor agent reports progress via hai task CLI - hai task update <id> <step> <status> — step lifecycle - hai task log <id> <message> — execution log - hai task discover <id> <what> <disp> — record discoveries - hai task show <id> — display steps, progress, discoveries, log - Steps auto-parsed from PROMPT.md headings into task.json - Task types extended with steps, reviews, discoveries, log
This commit is contained in:
@@ -1,17 +1,21 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { runDashboard } from "./commands/dashboard.js";
|
||||
import { runTaskCreate, runTaskList, runTaskMove, runTaskMerge } from "./commands/task.js";
|
||||
import { runTaskCreate, runTaskList, runTaskMove, runTaskMerge, runTaskUpdate, runTaskLog, runTaskDiscover, runTaskShow } from "./commands/task.js";
|
||||
|
||||
const HELP = `
|
||||
hai — AI-orchestrated task board
|
||||
|
||||
Usage:
|
||||
hai dashboard Start the board web UI
|
||||
hai task create [desc] Create a new task (goes to triage)
|
||||
hai task list List all tasks
|
||||
hai task move <id> <col> Move a task to a column
|
||||
hai task merge <id> Merge an in-review task and close it
|
||||
hai dashboard Start the board web UI
|
||||
hai task create [desc] Create a new task (goes to triage)
|
||||
hai task list List all tasks
|
||||
hai task show <id> Show task details, steps, log
|
||||
hai task move <id> <col> Move a task to a column
|
||||
hai task update <id> <step> <status> Update step status (pending|in-progress|done|skipped)
|
||||
hai task log <id> <message> Add a log entry
|
||||
hai task discover <id> <what> <disp> Record a discovery
|
||||
hai task merge <id> Merge an in-review task and close it
|
||||
|
||||
Options:
|
||||
--port, -p <port> Dashboard port (default: 4040)
|
||||
@@ -69,12 +73,40 @@ async function main() {
|
||||
await runTaskMove(id, column);
|
||||
break;
|
||||
}
|
||||
case "merge": {
|
||||
case "show": {
|
||||
const id = args[2];
|
||||
if (!id) {
|
||||
console.error("Usage: hai task merge <id>");
|
||||
if (!id) { console.error("Usage: hai task show <id>"); process.exit(1); }
|
||||
await runTaskShow(id);
|
||||
break;
|
||||
}
|
||||
case "update": {
|
||||
const id = args[2], step = args[3], status = args[4];
|
||||
if (!id || !step || !status) {
|
||||
console.error("Usage: hai task update <id> <step> <status>");
|
||||
console.error("Status: pending | in-progress | done | skipped");
|
||||
process.exit(1);
|
||||
}
|
||||
await runTaskUpdate(id, step, status);
|
||||
break;
|
||||
}
|
||||
case "log": {
|
||||
const id = args[2], message = args.slice(3).join(" ");
|
||||
if (!id || !message) { console.error("Usage: hai task log <id> <message>"); process.exit(1); }
|
||||
await runTaskLog(id, message);
|
||||
break;
|
||||
}
|
||||
case "discover": {
|
||||
const id = args[2], what = args[3], disp = args[4], loc = args[5];
|
||||
if (!id || !what || !disp) {
|
||||
console.error("Usage: hai task discover <id> <discovery> <disposition> [location]");
|
||||
process.exit(1);
|
||||
}
|
||||
await runTaskDiscover(id, what, disp, loc);
|
||||
break;
|
||||
}
|
||||
case "merge": {
|
||||
const id = args[2];
|
||||
if (!id) { console.error("Usage: hai task merge <id>"); process.exit(1); }
|
||||
await runTaskMerge(id);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { TaskStore, COLUMNS, COLUMN_LABELS, type Column, type MergeResult } from "@hai/core";
|
||||
import { TaskStore, COLUMNS, COLUMN_LABELS, type Column, type MergeResult, type StepStatus } from "@hai/core";
|
||||
import { aiMergeTask } from "@hai/engine";
|
||||
import { createInterface } from "node:readline/promises";
|
||||
|
||||
const STEP_STATUSES: StepStatus[] = ["pending", "in-progress", "done", "skipped"];
|
||||
|
||||
async function getStore(): Promise<TaskStore> {
|
||||
const store = new TaskStore(process.cwd());
|
||||
await store.init();
|
||||
@@ -68,6 +70,96 @@ export async function runTaskList() {
|
||||
}
|
||||
}
|
||||
|
||||
export async function runTaskUpdate(id: string, stepStr: string, status: string) {
|
||||
const stepIndex = parseInt(stepStr, 10);
|
||||
if (isNaN(stepIndex)) {
|
||||
console.error(`Invalid step number: ${stepStr}`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (!STEP_STATUSES.includes(status as StepStatus)) {
|
||||
console.error(`Invalid status: ${status}`);
|
||||
console.error(`Valid statuses: ${STEP_STATUSES.join(", ")}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const store = await getStore();
|
||||
const task = await store.updateStep(id, stepIndex, status as StepStatus);
|
||||
|
||||
const step = task.steps[stepIndex];
|
||||
console.log();
|
||||
console.log(` ✓ ${task.id} Step ${stepIndex} (${step.name}) → ${status}`);
|
||||
console.log(` Progress: ${task.steps.filter((s) => s.status === "done").length}/${task.steps.length} steps done`);
|
||||
console.log();
|
||||
}
|
||||
|
||||
export async function runTaskLog(id: string, message: string, outcome?: string) {
|
||||
const store = await getStore();
|
||||
await store.logEntry(id, message, outcome);
|
||||
|
||||
console.log();
|
||||
console.log(` ✓ ${id}: logged "${message}"`);
|
||||
console.log();
|
||||
}
|
||||
|
||||
export async function runTaskDiscover(id: string, discovery: string, disposition: string, location?: string) {
|
||||
const store = await getStore();
|
||||
await store.addDiscovery(id, discovery, disposition, location);
|
||||
|
||||
console.log();
|
||||
console.log(` ✓ ${id}: discovery recorded`);
|
||||
console.log(` ${discovery}`);
|
||||
console.log(` → ${disposition}`);
|
||||
console.log();
|
||||
}
|
||||
|
||||
export async function runTaskShow(id: string) {
|
||||
const store = await getStore();
|
||||
const task = await store.getTask(id);
|
||||
|
||||
console.log();
|
||||
console.log(` ${task.id}: ${task.title || task.description.slice(0, 60)}`);
|
||||
console.log(` Column: ${COLUMN_LABELS[task.column]}${task.size ? ` · Size: ${task.size}` : ""}${task.reviewLevel !== undefined ? ` · Review: ${task.reviewLevel}` : ""}`);
|
||||
if (task.dependencies.length) {
|
||||
console.log(` Dependencies: ${task.dependencies.join(", ")}`);
|
||||
}
|
||||
console.log();
|
||||
|
||||
// Steps
|
||||
if (task.steps.length > 0) {
|
||||
console.log(` Steps (${task.steps.filter((s) => s.status === "done").length}/${task.steps.length}):`);
|
||||
for (let i = 0; i < task.steps.length; i++) {
|
||||
const s = task.steps[i];
|
||||
const icon = s.status === "done" ? "✓"
|
||||
: s.status === "in-progress" ? "▸"
|
||||
: s.status === "skipped" ? "–"
|
||||
: " ";
|
||||
const marker = i === task.currentStep && s.status !== "done" ? " ◀" : "";
|
||||
console.log(` [${icon}] ${i}: ${s.name}${marker}`);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Discoveries
|
||||
if (task.discoveries.length > 0) {
|
||||
console.log(` Discoveries:`);
|
||||
for (const d of task.discoveries) {
|
||||
console.log(` • ${d.discovery} → ${d.disposition}${d.location ? ` (${d.location})` : ""}`);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Recent log
|
||||
if (task.log.length > 0) {
|
||||
const recent = task.log.slice(-5);
|
||||
console.log(` Log (last ${recent.length}):`);
|
||||
for (const l of recent) {
|
||||
const ts = new Date(l.timestamp).toLocaleTimeString();
|
||||
console.log(` ${ts} ${l.action}${l.outcome ? ` → ${l.outcome}` : ""}`);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
|
||||
export async function runTaskMerge(id: string) {
|
||||
const cwd = process.cwd();
|
||||
const store = await getStore();
|
||||
|
||||
Reference in New Issue
Block a user