taskplane 0.3.0 → 0.3.1

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/bin/taskplane.mjs CHANGED
@@ -645,10 +645,11 @@ async function cmdInit(args) {
645
645
  console.log(`\n${c.bold}Creating files...${c.reset}\n`);
646
646
  const skipIfExists = !force;
647
647
 
648
- // Agent prompts
648
+ // Agent prompts — copy thin local files (base prompts ship in the package
649
+ // and are composed automatically by the task-runner at runtime)
649
650
  for (const agent of ["task-worker.md", "task-reviewer.md", "task-merger.md"]) {
650
651
  copyTemplate(
651
- path.join(TEMPLATES_DIR, "agents", agent),
652
+ path.join(TEMPLATES_DIR, "agents", "local", agent),
652
653
  path.join(projectRoot, ".pi", "agents", agent),
653
654
  { skipIfExists, label: `.pi/agents/${agent}` }
654
655
  );
@@ -339,21 +339,141 @@ function clearConversationLog(prefix: string): void {
339
339
 
340
340
  // ── Agent Loader ─────────────────────────────────────────────────────
341
341
 
342
+ /**
343
+ * Parse a markdown agent file into frontmatter key-value pairs and body content.
344
+ * Returns null if the file doesn't exist or has no frontmatter block.
345
+ */
346
+ function parseAgentFile(filePath: string): { fm: Record<string, string>; body: string } | null {
347
+ if (!existsSync(filePath)) return null;
348
+ const raw = readFileSync(filePath, "utf-8").replace(/\r\n/g, "\n");
349
+ const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
350
+ if (!match) return null;
351
+ const fm: Record<string, string> = {};
352
+ for (const line of match[1].split("\n")) {
353
+ const idx = line.indexOf(":");
354
+ if (idx > 0) fm[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
355
+ }
356
+ return { fm, body: match[2].trim() };
357
+ }
358
+
359
+ /** Cached package root — resolved once, reused for all agent file lookups. */
360
+ let _packageRoot: string | null = null;
361
+
362
+ /**
363
+ * Find the taskplane package root directory.
364
+ *
365
+ * Strategy: this file lives at <package-root>/extensions/task-runner.ts.
366
+ * When pi loads it via `-e`, it resolves the full path. We can find the
367
+ * package root by searching for package.json with name "taskplane"
368
+ * starting from known candidate locations.
369
+ */
370
+ function findPackageRoot(): string {
371
+ if (_packageRoot !== null) return _packageRoot;
372
+
373
+ // Strategy 1: Walk up from this file's location via require.resolve or npm paths
374
+ const candidates: string[] = [];
375
+
376
+ // The extension is loaded by pi from the installed package location.
377
+ // Check well-known npm global paths.
378
+ const home = process.env.HOME || process.env.USERPROFILE || "";
379
+ if (home) {
380
+ candidates.push(join(home, "AppData", "Roaming", "npm", "node_modules", "taskplane"));
381
+ candidates.push(join(home, ".npm-global", "lib", "node_modules", "taskplane"));
382
+ }
383
+ candidates.push(join("/usr", "local", "lib", "node_modules", "taskplane"));
384
+
385
+ // Strategy 2: resolve from pi's node_modules peer
386
+ try {
387
+ const piPath = process.argv[1] || "";
388
+ const piPkgDir = resolve(piPath, "..", "..");
389
+ candidates.push(join(piPkgDir, "..", "taskplane"));
390
+ } catch { /* ignore */ }
391
+
392
+ // Strategy 3: Check TASKPLANE_WORKSPACE_ROOT project-local install
393
+ const wsRoot = process.env.TASKPLANE_WORKSPACE_ROOT;
394
+ if (wsRoot) {
395
+ candidates.push(join(wsRoot, ".pi", "npm", "node_modules", "taskplane"));
396
+ candidates.push(join(wsRoot, "node_modules", "taskplane"));
397
+ }
398
+
399
+ for (const dir of candidates) {
400
+ try {
401
+ const pkgPath = join(dir, "package.json");
402
+ if (existsSync(pkgPath)) {
403
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
404
+ if (pkg.name === "taskplane") {
405
+ _packageRoot = dir;
406
+ return dir;
407
+ }
408
+ }
409
+ } catch { /* ignore */ }
410
+ }
411
+
412
+ _packageRoot = "";
413
+ return "";
414
+ }
415
+
416
+ /**
417
+ * Resolve the package-shipped base agent file path.
418
+ * Base files live in the package's templates/agents/ directory.
419
+ */
420
+ function resolveBaseAgentPath(name: string): string {
421
+ const root = findPackageRoot();
422
+ if (!root) return "";
423
+ return join(root, "templates", "agents", `${name}.md`);
424
+ }
425
+
426
+ /**
427
+ * Load an agent definition with prompt inheritance.
428
+ *
429
+ * Inheritance model (default: compose base + local):
430
+ * 1. Load base agent from the shipped package (templates/agents/{name}.md)
431
+ * 2. Load local agent from .pi/agents/{name}.md (if it exists)
432
+ * 3. If local file has `standalone: true` in frontmatter, use it as-is (no base)
433
+ * 4. Otherwise, compose: base prompt + separator + local content
434
+ * 5. Local frontmatter values (tools, model) override base values
435
+ *
436
+ * If no local file exists, the base file is used directly.
437
+ * If no base file exists (e.g., custom agent), local file is used as-is.
438
+ */
342
439
  function loadAgentDef(cwd: string, name: string): { systemPrompt: string; tools: string; model: string } | null {
343
- const paths = [join(cwd, ".pi", "agents", `${name}.md`), join(cwd, "agents", `${name}.md`)];
344
- for (const p of paths) {
345
- if (!existsSync(p)) continue;
346
- const raw = readFileSync(p, "utf-8").replace(/\r\n/g, "\n");
347
- const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
348
- if (!match) continue;
349
- const fm: Record<string, string> = {};
350
- for (const line of match[1].split("\n")) {
351
- const idx = line.indexOf(":");
352
- if (idx > 0) fm[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
353
- }
354
- return { systemPrompt: match[2].trim(), tools: fm.tools || "read,grep,find,ls", model: fm.model || "" };
440
+ const basePath = resolveBaseAgentPath(name);
441
+ const localPaths = [join(cwd, ".pi", "agents", `${name}.md`), join(cwd, "agents", `${name}.md`)];
442
+
443
+ // Load base from package
444
+ const baseDef = parseAgentFile(basePath);
445
+
446
+ // Load local override (first found wins)
447
+ let localDef: { fm: Record<string, string>; body: string } | null = null;
448
+ for (const p of localPaths) {
449
+ localDef = parseAgentFile(p);
450
+ if (localDef) break;
451
+ }
452
+
453
+ // No base and no local → null
454
+ if (!baseDef && !localDef) return null;
455
+
456
+ // Local with standalone: true → use local as-is, ignore base
457
+ if (localDef?.fm.standalone === "true") {
458
+ return {
459
+ systemPrompt: localDef.body,
460
+ tools: localDef.fm.tools || "read,grep,find,ls",
461
+ model: localDef.fm.model || "",
462
+ };
355
463
  }
356
- return null;
464
+
465
+ // Compose base + local
466
+ const basePrompt = baseDef?.body || "";
467
+ const localPrompt = localDef?.body || "";
468
+ const composedPrompt = localPrompt
469
+ ? basePrompt + "\n\n---\n\n## Project-Specific Guidance\n\n" + localPrompt
470
+ : basePrompt;
471
+
472
+ // Local frontmatter overrides base (tools, model)
473
+ const tools = localDef?.fm.tools || baseDef?.fm.tools || "read,grep,find,ls";
474
+ const model = localDef?.fm.model || baseDef?.fm.model || "";
475
+
476
+ return { systemPrompt: composedPrompt.trim(), tools, model };
357
477
  }
358
478
 
359
479
  // ── PROMPT.md Parser ─────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskplane",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "AI agent orchestration for pi — parallel task execution with checkpoint discipline",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: task-merger
3
+ # tools: read,write,edit,bash,grep,find,ls
4
+ # model:
5
+ # standalone: true
6
+ ---
7
+
8
+ <!-- ═══════════════════════════════════════════════════════════════════
9
+ Project-Specific Merger Guidance
10
+
11
+ This file is COMPOSED with the base task-merger prompt shipped in the
12
+ taskplane package. Your content here is appended after the base prompt.
13
+
14
+ The base prompt (maintained by taskplane) handles:
15
+ - Branch merge workflow (fast-forward, 3-way, conflict resolution)
16
+ - Post-merge verification command execution
17
+ - Result file JSON format and writing conventions
18
+
19
+ Add project-specific merge rules below. Common examples:
20
+ - Post-merge verification commands (build, lint, test)
21
+ - Conflict resolution preferences
22
+ - Protected files that should never be auto-merged
23
+
24
+ To override frontmatter values (tools, model), uncomment and edit above.
25
+ To use this file as a FULLY STANDALONE prompt (ignoring the base),
26
+ uncomment `standalone: true` above and write the complete prompt below.
27
+ ═══════════════════════════════════════════════════════════════════ -->
@@ -0,0 +1,29 @@
1
+ ---
2
+ name: task-reviewer
3
+ # tools: read,write,bash,grep,find,ls
4
+ # model: openai/gpt-5.3-codex
5
+ # standalone: true
6
+ ---
7
+
8
+ <!-- ═══════════════════════════════════════════════════════════════════
9
+ Project-Specific Reviewer Guidance
10
+
11
+ This file is COMPOSED with the base task-reviewer prompt shipped in the
12
+ taskplane package. Your content here is appended after the base prompt.
13
+
14
+ The base prompt (maintained by taskplane) handles:
15
+ - Plan review and code review workflows
16
+ - Verdict format (APPROVE / REVISE)
17
+ - Review file output conventions
18
+ - Plan granularity guidance
19
+
20
+ Add project-specific review criteria below. Common examples:
21
+ - Required test coverage thresholds
22
+ - Security review checklist items
23
+ - Architecture constraints to enforce
24
+ - Performance requirements
25
+
26
+ To override frontmatter values (tools, model), uncomment and edit above.
27
+ To use this file as a FULLY STANDALONE prompt (ignoring the base),
28
+ uncomment `standalone: true` above and write the complete prompt below.
29
+ ═══════════════════════════════════════════════════════════════════ -->
@@ -0,0 +1,30 @@
1
+ ---
2
+ name: task-worker
3
+ # tools: read,write,edit,bash,grep,find,ls
4
+ # model: anthropic/claude-sonnet-4-20250514
5
+ # standalone: true
6
+ ---
7
+
8
+ <!-- ═══════════════════════════════════════════════════════════════════
9
+ Project-Specific Worker Guidance
10
+
11
+ This file is COMPOSED with the base task-worker prompt shipped in the
12
+ taskplane package. Your content here is appended after the base prompt.
13
+
14
+ The base prompt (maintained by taskplane) handles:
15
+ - STATUS.md-first workflow and checkpoint discipline
16
+ - Fresh-context loop behavior and iteration rules
17
+ - Git commit conventions and .DONE file creation
18
+ - Review response handling
19
+
20
+ Add project-specific rules below. Common examples:
21
+ - Preferred package manager (pnpm, yarn, bun)
22
+ - Test commands (make test, npm run test:unit)
23
+ - Coding standards (linting, formatting)
24
+ - Framework-specific patterns
25
+ - Environment or deployment constraints
26
+
27
+ To override frontmatter values (tools, model), uncomment and edit above.
28
+ To use this file as a FULLY STANDALONE prompt (ignoring the base),
29
+ uncomment `standalone: true` above and write the complete prompt below.
30
+ ═══════════════════════════════════════════════════════════════════ -->