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