taskplane 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2 -20
  3. package/bin/taskplane.mjs +706 -0
  4. package/dashboard/public/app.js +900 -0
  5. package/dashboard/public/index.html +92 -0
  6. package/dashboard/public/style.css +924 -0
  7. package/dashboard/server.cjs +531 -0
  8. package/extensions/task-orchestrator.ts +28 -0
  9. package/extensions/task-runner.ts +1923 -0
  10. package/extensions/taskplane/abort.ts +466 -0
  11. package/extensions/taskplane/config.ts +102 -0
  12. package/extensions/taskplane/discovery.ts +988 -0
  13. package/extensions/taskplane/engine.ts +758 -0
  14. package/extensions/taskplane/execution.ts +1752 -0
  15. package/extensions/taskplane/extension.ts +577 -0
  16. package/extensions/taskplane/formatting.ts +718 -0
  17. package/extensions/taskplane/git.ts +38 -0
  18. package/extensions/taskplane/index.ts +22 -0
  19. package/extensions/taskplane/merge.ts +795 -0
  20. package/extensions/taskplane/messages.ts +134 -0
  21. package/extensions/taskplane/persistence.ts +1121 -0
  22. package/extensions/taskplane/resume.ts +1092 -0
  23. package/extensions/taskplane/sessions.ts +92 -0
  24. package/extensions/taskplane/types.ts +1514 -0
  25. package/extensions/taskplane/waves.ts +900 -0
  26. package/extensions/taskplane/worktree.ts +1624 -0
  27. package/package.json +48 -3
  28. package/skills/create-taskplane-task/SKILL.md +326 -0
  29. package/skills/create-taskplane-task/references/context-template.md +78 -0
  30. package/skills/create-taskplane-task/references/prompt-template.md +246 -0
  31. package/templates/agents/task-merger.md +256 -0
  32. package/templates/agents/task-reviewer.md +81 -0
  33. package/templates/agents/task-worker.md +140 -0
  34. package/templates/config/task-orchestrator.yaml +89 -0
  35. package/templates/config/task-runner.yaml +99 -0
  36. package/templates/tasks/CONTEXT.md +31 -0
  37. package/templates/tasks/EXAMPLE-001-hello-world/PROMPT.md +90 -0
  38. package/templates/tasks/EXAMPLE-001-hello-world/STATUS.md +73 -0
@@ -0,0 +1,706 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Taskplane CLI — Project scaffolding, diagnostics, and dashboard launcher.
5
+ *
6
+ * This CLI handles what the pi package system cannot: project-local config
7
+ * scaffolding, installation health checks, and dashboard management.
8
+ *
9
+ * Extensions, skills, and themes are delivered via `pi install npm:taskplane`
10
+ * and auto-discovered by pi. This CLI is for everything else.
11
+ */
12
+
13
+ import fs from "node:fs";
14
+ import path from "node:path";
15
+ import readline from "node:readline";
16
+ import { fileURLToPath } from "node:url";
17
+ import { execSync, spawn } from "node:child_process";
18
+
19
+ // ─── Paths ──────────────────────────────────────────────────────────────────
20
+
21
+ const __filename = fileURLToPath(import.meta.url);
22
+ const __dirname = path.dirname(__filename);
23
+ const PACKAGE_ROOT = path.resolve(__dirname, "..");
24
+ const TEMPLATES_DIR = path.join(PACKAGE_ROOT, "templates");
25
+ const DASHBOARD_SERVER = path.join(PACKAGE_ROOT, "dashboard", "server.cjs");
26
+
27
+ // ─── ANSI Colors ────────────────────────────────────────────────────────────
28
+
29
+ const c = {
30
+ reset: "\x1b[0m",
31
+ bold: "\x1b[1m",
32
+ dim: "\x1b[2m",
33
+ red: "\x1b[31m",
34
+ green: "\x1b[32m",
35
+ yellow: "\x1b[33m",
36
+ blue: "\x1b[34m",
37
+ cyan: "\x1b[36m",
38
+ };
39
+
40
+ const OK = `${c.green}✅${c.reset}`;
41
+ const WARN = `${c.yellow}⚠️${c.reset}`;
42
+ const FAIL = `${c.red}❌${c.reset}`;
43
+ const INFO = `${c.cyan}ℹ${c.reset}`;
44
+
45
+ // ─── Utilities ──────────────────────────────────────────────────────────────
46
+
47
+ function die(msg) {
48
+ console.error(`${FAIL} ${msg}`);
49
+ process.exit(1);
50
+ }
51
+
52
+ function today() {
53
+ return new Date().toISOString().slice(0, 10);
54
+ }
55
+
56
+ function slugify(str) {
57
+ return str
58
+ .toLowerCase()
59
+ .replace(/[^a-z0-9]+/g, "-")
60
+ .replace(/^-|-$/g, "");
61
+ }
62
+
63
+ /** Prompt the user for input. Returns the answer or defaultValue. */
64
+ function ask(question, defaultValue) {
65
+ const rl = readline.createInterface({
66
+ input: process.stdin,
67
+ output: process.stdout,
68
+ });
69
+ const suffix = defaultValue != null ? ` ${c.dim}(${defaultValue})${c.reset}` : "";
70
+ return new Promise((resolve) => {
71
+ rl.question(` ${question}${suffix}: `, (answer) => {
72
+ rl.close();
73
+ resolve(answer.trim() || defaultValue || "");
74
+ });
75
+ });
76
+ }
77
+
78
+ /** Prompt yes/no. Returns boolean. */
79
+ async function confirm(question, defaultYes = true) {
80
+ const hint = defaultYes ? "Y/n" : "y/N";
81
+ const answer = await ask(`${question} [${hint}]`);
82
+ if (!answer) return defaultYes;
83
+ return answer.toLowerCase().startsWith("y");
84
+ }
85
+
86
+ /** Read a YAML file if it exists, return null otherwise. */
87
+ function readYaml(filePath) {
88
+ try {
89
+ // Dynamic import of yaml — it's a dependency of the package
90
+ const raw = fs.readFileSync(filePath, "utf-8");
91
+ // Simple YAML value extraction (avoids requiring yaml at top level for fast startup)
92
+ return raw;
93
+ } catch {
94
+ return null;
95
+ }
96
+ }
97
+
98
+ /** Check if a command exists on PATH. */
99
+ function commandExists(cmd) {
100
+ try {
101
+ const which = process.platform === "win32" ? "where" : "which";
102
+ execSync(`${which} ${cmd}`, { stdio: "pipe" });
103
+ return true;
104
+ } catch {
105
+ return false;
106
+ }
107
+ }
108
+
109
+ /** Get command version string. */
110
+ function getVersion(cmd, flag = "--version") {
111
+ try {
112
+ return execSync(`${cmd} ${flag}`, { stdio: "pipe" }).toString().trim();
113
+ } catch {
114
+ return null;
115
+ }
116
+ }
117
+
118
+ /** Write a file, creating parent directories as needed. Optionally skip if exists. */
119
+ function writeFile(dest, content, { skipIfExists = false, label = "" } = {}) {
120
+ if (skipIfExists && fs.existsSync(dest)) {
121
+ if (label) console.log(` ${c.dim}skip${c.reset} ${label} (already exists)`);
122
+ return false;
123
+ }
124
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
125
+ fs.writeFileSync(dest, content, "utf-8");
126
+ if (label) console.log(` ${c.green}create${c.reset} ${label}`);
127
+ return true;
128
+ }
129
+
130
+ /** Copy a file from templates, creating parent dirs. */
131
+ function copyTemplate(src, dest, { skipIfExists = false, label = "" } = {}) {
132
+ if (skipIfExists && fs.existsSync(dest)) {
133
+ if (label) console.log(` ${c.dim}skip${c.reset} ${label} (already exists)`);
134
+ return false;
135
+ }
136
+ const content = fs.readFileSync(src, "utf-8");
137
+ return writeFile(dest, content, { label });
138
+ }
139
+
140
+ /** Replace {{variables}} in template content. */
141
+ function interpolate(content, vars) {
142
+ return content.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? `{{${key}}}`);
143
+ }
144
+
145
+ // ─── Stack Detection ────────────────────────────────────────────────────────
146
+
147
+ function detectStack(projectRoot) {
148
+ const checks = [
149
+ { file: "package.json", stack: "node", test: "npm test", build: "npm run build" },
150
+ { file: "go.mod", stack: "go", test: "go test ./...", build: "go build ./..." },
151
+ { file: "Cargo.toml", stack: "rust", test: "cargo test", build: "cargo build" },
152
+ { file: "pyproject.toml", stack: "python", test: "pytest", build: "" },
153
+ { file: "pom.xml", stack: "java-maven", test: "mvn test", build: "mvn package" },
154
+ { file: "build.gradle", stack: "java-gradle", test: "gradle test", build: "gradle build" },
155
+ ];
156
+ for (const { file, stack, test, build } of checks) {
157
+ if (fs.existsSync(path.join(projectRoot, file))) {
158
+ return { stack, test, build };
159
+ }
160
+ }
161
+ return { stack: "unknown", test: "", build: "" };
162
+ }
163
+
164
+ // ─── YAML Generation ────────────────────────────────────────────────────────
165
+
166
+ function generateTaskRunnerYaml(vars) {
167
+ return `# ═══════════════════════════════════════════════════════════════════════
168
+ # Task Runner Configuration — ${vars.project_name}
169
+ # ═══════════════════════════════════════════════════════════════════════
170
+ #
171
+ # This file configures the /task command (task-runner extension).
172
+ # Edit freely — this file is owned by you, not the package.
173
+
174
+ # ── Task Areas ────────────────────────────────────────────────────────
175
+ # Define where tasks live. Each area has a folder path, ID prefix, and
176
+ # a CONTEXT.md file that provides domain context to agents.
177
+
178
+ task_areas:
179
+ ${vars.default_area}:
180
+ path: "${vars.tasks_root}"
181
+ prefix: "${vars.default_prefix}"
182
+ context: "${vars.tasks_root}/CONTEXT.md"
183
+
184
+ # ── Reference Docs ────────────────────────────────────────────────────
185
+ # Docs that tasks can reference in their "Context to Read First" section.
186
+ # Add your project's architecture docs, API specs, etc.
187
+
188
+ reference_docs: {}
189
+
190
+ # ── Standards ─────────────────────────────────────────────────────────
191
+ # Coding standards and rules. Agents follow these during implementation.
192
+
193
+ standards: {}
194
+
195
+ # ── Testing ───────────────────────────────────────────────────────────
196
+ # Commands that agents run to verify their work.
197
+
198
+ testing:
199
+ commands:${vars.test_cmd ? `\n unit: "${vars.test_cmd}"` : ""}${vars.build_cmd ? `\n build: "${vars.build_cmd}"` : ""}
200
+ `;
201
+ }
202
+
203
+ function generateOrchestratorYaml(vars) {
204
+ return `# ═══════════════════════════════════════════════════════════════════════
205
+ # Parallel Task Orchestrator Configuration — ${vars.project_name}
206
+ # ═══════════════════════════════════════════════════════════════════════
207
+ #
208
+ # This file configures the /orch commands (task-orchestrator extension).
209
+ # Edit freely — this file is owned by you, not the package.
210
+
211
+ orchestrator:
212
+ max_lanes: ${vars.max_lanes}
213
+ worktree_location: "subdirectory"
214
+ worktree_prefix: "${vars.worktree_prefix}"
215
+ integration_branch: "${vars.integration_branch}"
216
+ batch_id_format: "timestamp"
217
+ spawn_mode: "subprocess"
218
+ tmux_prefix: "orch"
219
+
220
+ dependencies:
221
+ source: "prompt"
222
+ cache: true
223
+
224
+ assignment:
225
+ strategy: "affinity-first"
226
+ size_weights:
227
+ S: 1
228
+ M: 2
229
+ L: 4
230
+
231
+ pre_warm:
232
+ auto_detect: false
233
+ commands: {}
234
+ always: []
235
+
236
+ merge:
237
+ model: ""
238
+ tools: "read,write,edit,bash,grep,find,ls"
239
+ verify: []
240
+ order: "fewest-files-first"
241
+
242
+ failure:
243
+ on_task_failure: "skip-dependents"
244
+ on_merge_failure: "pause"
245
+ stall_timeout: 30
246
+ max_worker_minutes: 30
247
+ abort_grace_period: 60
248
+
249
+ monitoring:
250
+ poll_interval: 5
251
+ `;
252
+ }
253
+
254
+ // ═════════════════════════════════════════════════════════════════════════════
255
+ // COMMANDS
256
+ // ═════════════════════════════════════════════════════════════════════════════
257
+
258
+ // ─── init ───────────────────────────────────────────────────────────────────
259
+
260
+ async function cmdInit(args) {
261
+ const projectRoot = process.cwd();
262
+ const force = args.includes("--force");
263
+ const dryRun = args.includes("--dry-run");
264
+ const noExamples = args.includes("--no-examples");
265
+ const presetIdx = args.indexOf("--preset");
266
+ const preset = presetIdx !== -1 ? args[presetIdx + 1] : null;
267
+
268
+ console.log(`\n${c.bold}Taskplane Init${c.reset}\n`);
269
+
270
+ // Check for existing config
271
+ const hasConfig =
272
+ fs.existsSync(path.join(projectRoot, ".pi", "task-runner.yaml")) ||
273
+ fs.existsSync(path.join(projectRoot, ".pi", "task-orchestrator.yaml"));
274
+
275
+ if (hasConfig && !force) {
276
+ console.log(`${WARN} Taskplane config already exists in this project.`);
277
+ const proceed = await confirm(" Overwrite existing files?", false);
278
+ if (!proceed) {
279
+ console.log(" Aborted.");
280
+ return;
281
+ }
282
+ }
283
+
284
+ // Gather config values
285
+ let vars;
286
+ if (preset === "minimal" || preset === "full" || preset === "runner-only") {
287
+ vars = getPresetVars(preset, projectRoot);
288
+ console.log(` Using preset: ${c.cyan}${preset}${c.reset}\n`);
289
+ } else {
290
+ vars = await getInteractiveVars(projectRoot);
291
+ }
292
+
293
+ if (dryRun) {
294
+ console.log(`\n${c.bold}Dry run — files that would be created:${c.reset}\n`);
295
+ printFileList(vars, noExamples, preset);
296
+ return;
297
+ }
298
+
299
+ // Scaffold files
300
+ console.log(`\n${c.bold}Creating files...${c.reset}\n`);
301
+ const skipIfExists = !force;
302
+
303
+ // Agent prompts
304
+ for (const agent of ["task-worker.md", "task-reviewer.md", "task-merger.md"]) {
305
+ copyTemplate(
306
+ path.join(TEMPLATES_DIR, "agents", agent),
307
+ path.join(projectRoot, ".pi", "agents", agent),
308
+ { skipIfExists, label: `.pi/agents/${agent}` }
309
+ );
310
+ }
311
+
312
+ // Task runner config
313
+ writeFile(
314
+ path.join(projectRoot, ".pi", "task-runner.yaml"),
315
+ generateTaskRunnerYaml(vars),
316
+ { skipIfExists, label: ".pi/task-runner.yaml" }
317
+ );
318
+
319
+ // Orchestrator config (skip for runner-only preset)
320
+ if (preset !== "runner-only") {
321
+ writeFile(
322
+ path.join(projectRoot, ".pi", "task-orchestrator.yaml"),
323
+ generateOrchestratorYaml(vars),
324
+ { skipIfExists, label: ".pi/task-orchestrator.yaml" }
325
+ );
326
+ }
327
+
328
+ // Version tracker (always overwrite)
329
+ const versionInfo = {
330
+ version: getPackageVersion(),
331
+ installedAt: new Date().toISOString(),
332
+ lastUpgraded: new Date().toISOString(),
333
+ components: { agents: getPackageVersion(), config: getPackageVersion() },
334
+ };
335
+ writeFile(
336
+ path.join(projectRoot, ".pi", "taskplane.json"),
337
+ JSON.stringify(versionInfo, null, 2) + "\n",
338
+ { label: ".pi/taskplane.json" }
339
+ );
340
+
341
+ // CONTEXT.md
342
+ const contextSrc = fs.readFileSync(path.join(TEMPLATES_DIR, "tasks", "CONTEXT.md"), "utf-8");
343
+ writeFile(
344
+ path.join(projectRoot, vars.tasks_root, "CONTEXT.md"),
345
+ interpolate(contextSrc, vars),
346
+ { skipIfExists, label: `${vars.tasks_root}/CONTEXT.md` }
347
+ );
348
+
349
+ // Example task
350
+ if (!noExamples) {
351
+ const exampleDir = path.join(TEMPLATES_DIR, "tasks", "EXAMPLE-001-hello-world");
352
+ const destDir = path.join(projectRoot, vars.tasks_root, "EXAMPLE-001-hello-world");
353
+ for (const file of ["PROMPT.md", "STATUS.md"]) {
354
+ const src = fs.readFileSync(path.join(exampleDir, file), "utf-8");
355
+ writeFile(path.join(destDir, file), interpolate(src, vars), {
356
+ skipIfExists,
357
+ label: `${vars.tasks_root}/EXAMPLE-001-hello-world/${file}`,
358
+ });
359
+ }
360
+ }
361
+
362
+ // Report
363
+ console.log(`\n${OK} ${c.bold}Taskplane initialized!${c.reset}\n`);
364
+ console.log(`${c.bold}Quick start:${c.reset}`);
365
+ console.log(` ${c.cyan}pi${c.reset} # start pi (taskplane auto-loads)`);
366
+ if (!noExamples) {
367
+ console.log(
368
+ ` ${c.cyan}/task ${vars.tasks_root}/EXAMPLE-001-hello-world/PROMPT.md${c.reset} # run the example task`
369
+ );
370
+ }
371
+ if (preset !== "runner-only") {
372
+ console.log(
373
+ ` ${c.cyan}/orch all${c.reset} # orchestrate all pending tasks`
374
+ );
375
+ }
376
+ console.log();
377
+ }
378
+
379
+ function getPresetVars(preset, projectRoot) {
380
+ const dirName = path.basename(projectRoot);
381
+ const { test: test_cmd, build: build_cmd } = detectStack(projectRoot);
382
+ return {
383
+ project_name: dirName,
384
+ integration_branch: "main",
385
+ max_lanes: 3,
386
+ worktree_prefix: `${slugify(dirName)}-wt`,
387
+ tasks_root: "taskplane-tasks",
388
+ default_area: "general",
389
+ default_prefix: "TP",
390
+ test_cmd,
391
+ build_cmd,
392
+ date: today(),
393
+ };
394
+ }
395
+
396
+ async function getInteractiveVars(projectRoot) {
397
+ const dirName = path.basename(projectRoot);
398
+ const detected = detectStack(projectRoot);
399
+
400
+ const project_name = await ask("Project name", dirName);
401
+ const integration_branch = await ask("Integration branch", "main");
402
+ const max_lanes = parseInt(await ask("Max parallel lanes", "3")) || 3;
403
+ const tasks_root = await ask("Tasks directory", "taskplane-tasks");
404
+ const default_area = await ask("Default area name", "general");
405
+ const default_prefix = await ask("Task ID prefix", "TP");
406
+ const test_cmd = await ask("Test command", detected.test || "");
407
+ const build_cmd = await ask("Build command", detected.build || "");
408
+
409
+ return {
410
+ project_name,
411
+ integration_branch,
412
+ max_lanes,
413
+ worktree_prefix: `${slugify(project_name)}-wt`,
414
+ tasks_root,
415
+ default_area,
416
+ default_prefix,
417
+ test_cmd,
418
+ build_cmd,
419
+ date: today(),
420
+ };
421
+ }
422
+
423
+ function printFileList(vars, noExamples, preset) {
424
+ const files = [
425
+ ".pi/agents/task-worker.md",
426
+ ".pi/agents/task-reviewer.md",
427
+ ".pi/agents/task-merger.md",
428
+ ".pi/task-runner.yaml",
429
+ ];
430
+ if (preset !== "runner-only") files.push(".pi/task-orchestrator.yaml");
431
+ files.push(".pi/taskplane.json");
432
+ files.push(`${vars.tasks_root}/CONTEXT.md`);
433
+ if (!noExamples) {
434
+ files.push(`${vars.tasks_root}/EXAMPLE-001-hello-world/PROMPT.md`);
435
+ files.push(`${vars.tasks_root}/EXAMPLE-001-hello-world/STATUS.md`);
436
+ }
437
+ for (const f of files) console.log(` ${c.green}create${c.reset} ${f}`);
438
+ console.log();
439
+ }
440
+
441
+ // ─── doctor ─────────────────────────────────────────────────────────────────
442
+
443
+ function cmdDoctor() {
444
+ const projectRoot = process.cwd();
445
+ let issues = 0;
446
+
447
+ console.log(`\n${c.bold}Taskplane Doctor${c.reset}\n`);
448
+
449
+ // Check prerequisites
450
+ const checks = [
451
+ { label: "pi installed", check: () => commandExists("pi"), detail: () => getVersion("pi") },
452
+ {
453
+ label: "Node.js >= 20.0.0",
454
+ check: () => {
455
+ const v = process.versions.node;
456
+ return parseInt(v.split(".")[0]) >= 20;
457
+ },
458
+ detail: () => `v${process.versions.node}`,
459
+ },
460
+ { label: "git installed", check: () => commandExists("git"), detail: () => getVersion("git") },
461
+ ];
462
+
463
+ for (const { label, check, detail } of checks) {
464
+ const ok = check();
465
+ const info = ok && detail ? ` ${c.dim}(${detail()})${c.reset}` : "";
466
+ console.log(` ${ok ? OK : FAIL} ${label}${info}`);
467
+ if (!ok) issues++;
468
+ }
469
+
470
+ // Check tmux (optional)
471
+ const hasTmux = commandExists("tmux");
472
+ console.log(
473
+ ` ${hasTmux ? OK : `${WARN}`} tmux installed${hasTmux ? ` ${c.dim}(${getVersion("tmux", "-V")})${c.reset}` : ` ${c.dim}(optional — needed for spawn_mode: tmux)${c.reset}`}`
474
+ );
475
+
476
+ // Check package installation
477
+ const pkgJson = path.join(PACKAGE_ROOT, "package.json");
478
+ const pkgVersion = getPackageVersion();
479
+ const isProjectLocal = PACKAGE_ROOT.includes(".pi");
480
+ const installType = isProjectLocal ? "project-local" : "global";
481
+ console.log(` ${OK} taskplane package installed ${c.dim}(v${pkgVersion}, ${installType})${c.reset}`);
482
+
483
+ // Check project config
484
+ console.log();
485
+ const configFiles = [
486
+ { path: ".pi/task-runner.yaml", required: true },
487
+ { path: ".pi/task-orchestrator.yaml", required: true },
488
+ { path: ".pi/agents/task-worker.md", required: true },
489
+ { path: ".pi/agents/task-reviewer.md", required: true },
490
+ { path: ".pi/agents/task-merger.md", required: true },
491
+ { path: ".pi/taskplane.json", required: false },
492
+ ];
493
+
494
+ for (const { path: relPath, required } of configFiles) {
495
+ const exists = fs.existsSync(path.join(projectRoot, relPath));
496
+ if (exists) {
497
+ console.log(` ${OK} ${relPath} exists`);
498
+ } else if (required) {
499
+ console.log(` ${FAIL} ${relPath} missing`);
500
+ issues++;
501
+ } else {
502
+ console.log(` ${WARN} ${relPath} missing ${c.dim}(optional)${c.reset}`);
503
+ }
504
+ }
505
+
506
+ // Check task areas from config
507
+ const runnerYaml = readYaml(path.join(projectRoot, ".pi", "task-runner.yaml"));
508
+ if (runnerYaml) {
509
+ // Simple regex extraction of task area paths
510
+ const pathMatches = [...runnerYaml.matchAll(/^\s+path:\s*"?([^"\n]+)"?/gm)];
511
+ const contextMatches = [...runnerYaml.matchAll(/^\s+context:\s*"?([^"\n]+)"?/gm)];
512
+
513
+ if (pathMatches.length > 0) {
514
+ console.log();
515
+ for (const match of pathMatches) {
516
+ const areaPath = match[1].trim();
517
+ const exists = fs.existsSync(path.join(projectRoot, areaPath));
518
+ if (exists) {
519
+ console.log(` ${OK} task area path: ${areaPath}`);
520
+ } else {
521
+ console.log(` ${FAIL} task area path: ${areaPath} ${c.dim}(directory not found)${c.reset}`);
522
+ console.log(` ${c.dim}→ Run: mkdir -p ${areaPath}${c.reset}`);
523
+ issues++;
524
+ }
525
+ }
526
+ for (const match of contextMatches) {
527
+ const ctxPath = match[1].trim();
528
+ const exists = fs.existsSync(path.join(projectRoot, ctxPath));
529
+ if (exists) {
530
+ console.log(` ${OK} CONTEXT.md: ${ctxPath}`);
531
+ } else {
532
+ console.log(` ${WARN} CONTEXT.md: ${ctxPath} ${c.dim}(not found)${c.reset}`);
533
+ }
534
+ }
535
+ }
536
+ }
537
+
538
+ console.log();
539
+ if (issues === 0) {
540
+ console.log(`${OK} ${c.green}All checks passed!${c.reset}\n`);
541
+ } else {
542
+ console.log(`${FAIL} ${issues} issue(s) found. Run ${c.cyan}taskplane init${c.reset} to fix config issues.\n`);
543
+ process.exit(1);
544
+ }
545
+ }
546
+
547
+ // ─── version ────────────────────────────────────────────────────────────────
548
+
549
+ function cmdVersion() {
550
+ const pkgVersion = getPackageVersion();
551
+ const isProjectLocal = PACKAGE_ROOT.includes(".pi");
552
+ const installType = isProjectLocal ? `project-local: ${PACKAGE_ROOT}` : `global: ${PACKAGE_ROOT}`;
553
+
554
+ console.log(`\ntaskplane ${c.bold}v${pkgVersion}${c.reset}`);
555
+ console.log(` Package: ${installType}`);
556
+
557
+ // Check for project config
558
+ const projectRoot = process.cwd();
559
+ const tpJson = path.join(projectRoot, ".pi", "taskplane.json");
560
+ if (fs.existsSync(tpJson)) {
561
+ try {
562
+ const info = JSON.parse(fs.readFileSync(tpJson, "utf-8"));
563
+ console.log(` Config: .pi/taskplane.json (v${info.version}, initialized ${info.installedAt?.slice(0, 10) || "unknown"})`);
564
+ } catch {
565
+ console.log(` Config: .pi/taskplane.json (unreadable)`);
566
+ }
567
+ } else {
568
+ console.log(` Config: ${c.dim}not initialized (run taskplane init)${c.reset}`);
569
+ }
570
+
571
+ // Pi version
572
+ const piVersion = getVersion("pi");
573
+ if (piVersion) console.log(` Pi: ${piVersion}`);
574
+
575
+ // Node version
576
+ console.log(` Node: v${process.versions.node}`);
577
+ console.log();
578
+ }
579
+
580
+ function getPackageVersion() {
581
+ try {
582
+ const pkg = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, "package.json"), "utf-8"));
583
+ return pkg.version || "unknown";
584
+ } catch {
585
+ return "unknown";
586
+ }
587
+ }
588
+
589
+ // ─── dashboard ──────────────────────────────────────────────────────────────
590
+
591
+ function cmdDashboard(args) {
592
+ const projectRoot = process.cwd();
593
+
594
+ if (!fs.existsSync(DASHBOARD_SERVER)) {
595
+ die(`Dashboard server not found at ${DASHBOARD_SERVER}`);
596
+ }
597
+
598
+ // Pass through args to server.cjs, adding --root
599
+ const serverArgs = ["--root", projectRoot];
600
+
601
+ // Forward --port and --no-open if provided
602
+ for (let i = 0; i < args.length; i++) {
603
+ if (args[i] === "--port" && args[i + 1]) {
604
+ serverArgs.push("--port", args[i + 1]);
605
+ i++;
606
+ } else if (args[i] === "--no-open") {
607
+ serverArgs.push("--no-open");
608
+ }
609
+ }
610
+
611
+ console.log(`\n${c.bold}Taskplane Dashboard${c.reset}`);
612
+ console.log(` Project: ${projectRoot}`);
613
+ console.log(` Server: ${DASHBOARD_SERVER}\n`);
614
+
615
+ const child = spawn("node", [DASHBOARD_SERVER, ...serverArgs], {
616
+ stdio: "inherit",
617
+ cwd: projectRoot,
618
+ });
619
+
620
+ child.on("error", (err) => {
621
+ die(`Failed to start dashboard: ${err.message}`);
622
+ });
623
+
624
+ // Forward signals
625
+ process.on("SIGINT", () => child.kill("SIGINT"));
626
+ process.on("SIGTERM", () => child.kill("SIGTERM"));
627
+
628
+ child.on("exit", (code) => {
629
+ process.exit(code ?? 0);
630
+ });
631
+ }
632
+
633
+ // ─── help ───────────────────────────────────────────────────────────────────
634
+
635
+ function showHelp() {
636
+ const version = getPackageVersion();
637
+ console.log(`
638
+ ${c.bold}taskplane${c.reset} v${version} — AI agent orchestration for pi
639
+
640
+ ${c.bold}Usage:${c.reset}
641
+ taskplane <command> [options]
642
+
643
+ ${c.bold}Commands:${c.reset}
644
+ ${c.cyan}init${c.reset} Scaffold Taskplane config in the current project
645
+ ${c.cyan}doctor${c.reset} Validate installation and project configuration
646
+ ${c.cyan}version${c.reset} Show version information
647
+ ${c.cyan}dashboard${c.reset} Launch the web-based orchestrator dashboard
648
+ ${c.cyan}help${c.reset} Show this help message
649
+
650
+ ${c.bold}Init options:${c.reset}
651
+ --preset <name> Use a preset: minimal, full, runner-only
652
+ --no-examples Skip example task scaffolding
653
+ --force Overwrite existing files without prompting
654
+ --dry-run Show what would be created without writing
655
+
656
+ ${c.bold}Dashboard options:${c.reset}
657
+ --port <number> Port to listen on (default: 8099)
658
+ --no-open Don't auto-open browser
659
+
660
+ ${c.bold}Examples:${c.reset}
661
+ taskplane init # Interactive project setup
662
+ taskplane init --preset full # Quick setup with defaults
663
+ taskplane init --dry-run # Preview what would be created
664
+ taskplane doctor # Check installation health
665
+ taskplane dashboard # Launch web dashboard
666
+ taskplane dashboard --port 3000 # Dashboard on custom port
667
+
668
+ ${c.bold}Getting started:${c.reset}
669
+ 1. pi install npm:taskplane # Install the pi package
670
+ 2. cd my-project && taskplane init # Scaffold project config
671
+ 3. pi # Start pi — /task and /orch are ready
672
+ `);
673
+ }
674
+
675
+ // ═════════════════════════════════════════════════════════════════════════════
676
+ // MAIN
677
+ // ═════════════════════════════════════════════════════════════════════════════
678
+
679
+ const [command, ...args] = process.argv.slice(2);
680
+
681
+ switch (command) {
682
+ case "init":
683
+ await cmdInit(args);
684
+ break;
685
+ case "doctor":
686
+ cmdDoctor();
687
+ break;
688
+ case "version":
689
+ case "--version":
690
+ case "-v":
691
+ cmdVersion();
692
+ break;
693
+ case "dashboard":
694
+ cmdDashboard(args);
695
+ break;
696
+ case "help":
697
+ case "--help":
698
+ case "-h":
699
+ case undefined:
700
+ showHelp();
701
+ break;
702
+ default:
703
+ console.error(`${FAIL} Unknown command: ${command}`);
704
+ showHelp();
705
+ process.exit(1);
706
+ }