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:
Dustin Byrne
2026-03-25 20:14:04 -04:00
parent e142e049d3
commit 8a2a5ac14b
7 changed files with 514 additions and 151 deletions

View File

@@ -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;
}

View File

@@ -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();