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.
- package/LICENSE +21 -0
- package/README.md +2 -20
- package/bin/taskplane.mjs +706 -0
- package/dashboard/public/app.js +900 -0
- package/dashboard/public/index.html +92 -0
- package/dashboard/public/style.css +924 -0
- package/dashboard/server.cjs +531 -0
- package/extensions/task-orchestrator.ts +28 -0
- package/extensions/task-runner.ts +1923 -0
- package/extensions/taskplane/abort.ts +466 -0
- package/extensions/taskplane/config.ts +102 -0
- package/extensions/taskplane/discovery.ts +988 -0
- package/extensions/taskplane/engine.ts +758 -0
- package/extensions/taskplane/execution.ts +1752 -0
- package/extensions/taskplane/extension.ts +577 -0
- package/extensions/taskplane/formatting.ts +718 -0
- package/extensions/taskplane/git.ts +38 -0
- package/extensions/taskplane/index.ts +22 -0
- package/extensions/taskplane/merge.ts +795 -0
- package/extensions/taskplane/messages.ts +134 -0
- package/extensions/taskplane/persistence.ts +1121 -0
- package/extensions/taskplane/resume.ts +1092 -0
- package/extensions/taskplane/sessions.ts +92 -0
- package/extensions/taskplane/types.ts +1514 -0
- package/extensions/taskplane/waves.ts +900 -0
- package/extensions/taskplane/worktree.ts +1624 -0
- package/package.json +48 -3
- package/skills/create-taskplane-task/SKILL.md +326 -0
- package/skills/create-taskplane-task/references/context-template.md +78 -0
- package/skills/create-taskplane-task/references/prompt-template.md +246 -0
- package/templates/agents/task-merger.md +256 -0
- package/templates/agents/task-reviewer.md +81 -0
- package/templates/agents/task-worker.md +140 -0
- package/templates/config/task-orchestrator.yaml +89 -0
- package/templates/config/task-runner.yaml +99 -0
- package/templates/tasks/CONTEXT.md +31 -0
- package/templates/tasks/EXAMPLE-001-hello-world/PROMPT.md +90 -0
- 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
|
+
}
|