taskplane 0.3.0 → 0.4.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/README.md +14 -5
- package/bin/gitignore-patterns.mjs +78 -0
- package/bin/taskplane.mjs +1356 -34
- package/dashboard/server.cjs +13 -1
- package/extensions/task-runner.ts +212 -57
- package/extensions/taskplane/config-loader.ts +860 -0
- package/extensions/taskplane/config-schema.ts +468 -0
- package/extensions/taskplane/config.ts +37 -93
- package/extensions/taskplane/engine.ts +2 -0
- package/extensions/taskplane/extension.ts +19 -0
- package/extensions/taskplane/merge.ts +12 -4
- package/extensions/taskplane/resume.ts +23 -14
- package/extensions/taskplane/settings-tui.ts +1387 -0
- package/extensions/taskplane/types.ts +81 -1
- package/extensions/taskplane/workspace.ts +204 -10
- package/package.json +1 -1
- package/skills/create-taskplane-task/SKILL.md +7 -5
- package/skills/create-taskplane-task/references/prompt-template.md +4 -3
- package/templates/agents/local/task-merger.md +27 -0
- package/templates/agents/local/task-reviewer.md +29 -0
- package/templates/agents/local/task-worker.md +30 -0
- package/templates/agents/task-worker.md +45 -31
- package/templates/config/task-orchestrator.yaml +3 -0
package/dashboard/server.cjs
CHANGED
|
@@ -23,6 +23,12 @@ const MAX_PORT_ATTEMPTS = 20;
|
|
|
23
23
|
const POLL_INTERVAL = 2000; // ms between state checks
|
|
24
24
|
|
|
25
25
|
// REPO_ROOT is resolved after parseArgs() — see initialization below.
|
|
26
|
+
// In workspace mode, REPO_ROOT is the workspace root (passed via --root).
|
|
27
|
+
// All dashboard state paths (batch-state, lane-state, conversation logs,
|
|
28
|
+
// batch-history) live at <REPO_ROOT>/.pi/ — this is runtime/sidecar state
|
|
29
|
+
// which does NOT follow the taskplane-pointer.json resolution chain.
|
|
30
|
+
// The pointer directs config/agent lookups to a config repo, but the
|
|
31
|
+
// dashboard only reads state files, so no pointer resolution is needed here.
|
|
26
32
|
let REPO_ROOT;
|
|
27
33
|
let BATCH_STATE_PATH;
|
|
28
34
|
let BATCH_HISTORY_PATH;
|
|
@@ -630,7 +636,13 @@ async function findPort(server, start, explicit) {
|
|
|
630
636
|
async function main() {
|
|
631
637
|
const opts = parseArgs();
|
|
632
638
|
|
|
633
|
-
// Resolve project root: --root flag > cwd
|
|
639
|
+
// Resolve project root: --root flag > cwd.
|
|
640
|
+
// In workspace mode this is the workspace root. All state/sidecar files
|
|
641
|
+
// (batch-state, lane-state, conversation logs, batch-history) live at
|
|
642
|
+
// <REPO_ROOT>/.pi/ and are NOT affected by taskplane-pointer.json.
|
|
643
|
+
// The pointer only redirects config/agent resolution in task-runner and
|
|
644
|
+
// orchestrator — the dashboard reads only runtime state, so no pointer
|
|
645
|
+
// resolution is performed here.
|
|
634
646
|
REPO_ROOT = path.resolve(opts.root || process.cwd());
|
|
635
647
|
BATCH_STATE_PATH = path.join(REPO_ROOT, ".pi", "batch-state.json");
|
|
636
648
|
BATCH_HISTORY_PATH = path.join(REPO_ROOT, ".pi", "batch-history.json");
|
|
@@ -26,7 +26,9 @@ import {
|
|
|
26
26
|
} from "fs";
|
|
27
27
|
import { tmpdir } from "os";
|
|
28
28
|
import { join, dirname, basename, resolve } from "path";
|
|
29
|
-
import {
|
|
29
|
+
import { loadProjectConfig, toTaskConfig } from "./taskplane/config-loader.ts";
|
|
30
|
+
import { loadWorkspaceConfig, resolvePointer } from "./taskplane/workspace.ts";
|
|
31
|
+
import type { PointerResolution } from "./taskplane/types.ts";
|
|
30
32
|
|
|
31
33
|
|
|
32
34
|
// ── Types ────────────────────────────────────────────────────────────
|
|
@@ -137,54 +139,75 @@ const DEFAULT_CONFIG: TaskConfig = {
|
|
|
137
139
|
},
|
|
138
140
|
};
|
|
139
141
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
142
|
+
// ── Pointer Resolution (Workspace Mode) ──────────────────────────────
|
|
143
|
+
|
|
144
|
+
/** Track whether a pointer warning has been logged this session (log once). */
|
|
145
|
+
let _pointerWarningLogged = false;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Resolve the workspace pointer for config and agent path redirection.
|
|
149
|
+
*
|
|
150
|
+
* In workspace mode (TASKPLANE_WORKSPACE_ROOT set), reads the pointer
|
|
151
|
+
* file and resolves config/agent roots to the config repo. In repo mode,
|
|
152
|
+
* returns null (no pointer resolution needed).
|
|
153
|
+
*
|
|
154
|
+
* All pointer failures are non-fatal: missing, malformed, or invalid
|
|
155
|
+
* pointer files produce a warning and fall back to existing paths.
|
|
156
|
+
* Warning is logged to stderr once per session for operator visibility.
|
|
157
|
+
*
|
|
158
|
+
* @returns PointerResolution with resolved paths, or null in repo mode
|
|
159
|
+
*/
|
|
160
|
+
function resolveTaskRunnerPointer(): PointerResolution | null {
|
|
161
|
+
const wsRoot = process.env.TASKPLANE_WORKSPACE_ROOT;
|
|
162
|
+
if (!wsRoot) return null; // repo mode — no pointer needed
|
|
163
|
+
|
|
148
164
|
try {
|
|
149
|
-
const
|
|
150
|
-
const
|
|
151
|
-
// Parse standards_overrides: Record<areaName, { docs?, rules? }>
|
|
152
|
-
const rawOverrides = loaded?.standards_overrides || {};
|
|
153
|
-
const parsedOverrides: Record<string, { docs?: string[]; rules?: string[] }> = {};
|
|
154
|
-
for (const [key, val] of Object.entries(rawOverrides)) {
|
|
155
|
-
if (val && typeof val === "object") {
|
|
156
|
-
const v = val as any;
|
|
157
|
-
parsedOverrides[key] = {
|
|
158
|
-
docs: Array.isArray(v.docs) ? v.docs : undefined,
|
|
159
|
-
rules: Array.isArray(v.rules) ? v.rules : undefined,
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
}
|
|
165
|
+
const wsConfig = loadWorkspaceConfig(wsRoot);
|
|
166
|
+
const result = resolvePointer(wsRoot, wsConfig);
|
|
163
167
|
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (val && typeof val === "object" && (val as any).path) {
|
|
169
|
-
parsedAreas[key] = { path: (val as any).path };
|
|
170
|
-
}
|
|
168
|
+
// Surface pointer warnings once per session for operator visibility
|
|
169
|
+
if (result?.warning && !_pointerWarningLogged) {
|
|
170
|
+
_pointerWarningLogged = true;
|
|
171
|
+
console.error(`[task-runner] pointer: ${result.warning}`);
|
|
171
172
|
}
|
|
172
173
|
|
|
173
|
-
return
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
174
|
+
return result;
|
|
175
|
+
} catch {
|
|
176
|
+
// Workspace config load failure — fall back gracefully
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Reset pointer warning state (for testing only). */
|
|
182
|
+
export function _resetPointerWarning(): void {
|
|
183
|
+
_pointerWarningLogged = false;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Expose loadAgentDef for testing (not part of public API). */
|
|
187
|
+
export const _loadAgentDef = (cwd: string, name: string) => loadAgentDef(cwd, name);
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Load task-runner config via the unified config loader.
|
|
191
|
+
*
|
|
192
|
+
* Reads `.pi/taskplane-config.json` first; falls back to YAML files;
|
|
193
|
+
* then defaults. Returns the legacy snake_case TaskConfig shape so all
|
|
194
|
+
* downstream consumers remain unchanged.
|
|
195
|
+
*
|
|
196
|
+
* Config root resolution order (workspace mode with pointer):
|
|
197
|
+
* 1. cwd has config files → use cwd (local override)
|
|
198
|
+
* 2. Pointer-resolved config root has config files → use it
|
|
199
|
+
* 3. TASKPLANE_WORKSPACE_ROOT has config files → use it (legacy fallback)
|
|
200
|
+
* 4. Fall back to cwd (loaders will return defaults)
|
|
201
|
+
*
|
|
202
|
+
* Repo mode: pointer is ignored, existing behavior unchanged.
|
|
203
|
+
*/
|
|
204
|
+
export function loadConfig(cwd: string): TaskConfig {
|
|
205
|
+
try {
|
|
206
|
+
const pointer = resolveTaskRunnerPointer();
|
|
207
|
+
const unified = loadProjectConfig(cwd, pointer?.configRoot);
|
|
208
|
+
return toTaskConfig(unified);
|
|
187
209
|
} catch {
|
|
210
|
+
// If config loading fails (e.g., malformed JSON), fall back to defaults
|
|
188
211
|
return { ...DEFAULT_CONFIG };
|
|
189
212
|
}
|
|
190
213
|
}
|
|
@@ -339,21 +362,153 @@ function clearConversationLog(prefix: string): void {
|
|
|
339
362
|
|
|
340
363
|
// ── Agent Loader ─────────────────────────────────────────────────────
|
|
341
364
|
|
|
365
|
+
/**
|
|
366
|
+
* Parse a markdown agent file into frontmatter key-value pairs and body content.
|
|
367
|
+
* Returns null if the file doesn't exist or has no frontmatter block.
|
|
368
|
+
*/
|
|
369
|
+
function parseAgentFile(filePath: string): { fm: Record<string, string>; body: string } | null {
|
|
370
|
+
if (!existsSync(filePath)) return null;
|
|
371
|
+
const raw = readFileSync(filePath, "utf-8").replace(/\r\n/g, "\n");
|
|
372
|
+
const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
373
|
+
if (!match) return null;
|
|
374
|
+
const fm: Record<string, string> = {};
|
|
375
|
+
for (const line of match[1].split("\n")) {
|
|
376
|
+
const idx = line.indexOf(":");
|
|
377
|
+
if (idx > 0) fm[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
|
|
378
|
+
}
|
|
379
|
+
return { fm, body: match[2].trim() };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** Cached package root — resolved once, reused for all agent file lookups. */
|
|
383
|
+
let _packageRoot: string | null = null;
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Find the taskplane package root directory.
|
|
387
|
+
*
|
|
388
|
+
* Strategy: this file lives at <package-root>/extensions/task-runner.ts.
|
|
389
|
+
* When pi loads it via `-e`, it resolves the full path. We can find the
|
|
390
|
+
* package root by searching for package.json with name "taskplane"
|
|
391
|
+
* starting from known candidate locations.
|
|
392
|
+
*/
|
|
393
|
+
function findPackageRoot(): string {
|
|
394
|
+
if (_packageRoot !== null) return _packageRoot;
|
|
395
|
+
|
|
396
|
+
// Strategy 1: Walk up from this file's location via require.resolve or npm paths
|
|
397
|
+
const candidates: string[] = [];
|
|
398
|
+
|
|
399
|
+
// The extension is loaded by pi from the installed package location.
|
|
400
|
+
// Check well-known npm global paths.
|
|
401
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
402
|
+
if (home) {
|
|
403
|
+
candidates.push(join(home, "AppData", "Roaming", "npm", "node_modules", "taskplane"));
|
|
404
|
+
candidates.push(join(home, ".npm-global", "lib", "node_modules", "taskplane"));
|
|
405
|
+
}
|
|
406
|
+
candidates.push(join("/usr", "local", "lib", "node_modules", "taskplane"));
|
|
407
|
+
|
|
408
|
+
// Strategy 2: resolve from pi's node_modules peer
|
|
409
|
+
try {
|
|
410
|
+
const piPath = process.argv[1] || "";
|
|
411
|
+
const piPkgDir = resolve(piPath, "..", "..");
|
|
412
|
+
candidates.push(join(piPkgDir, "..", "taskplane"));
|
|
413
|
+
} catch { /* ignore */ }
|
|
414
|
+
|
|
415
|
+
// Strategy 3: Check TASKPLANE_WORKSPACE_ROOT project-local install
|
|
416
|
+
const wsRoot = process.env.TASKPLANE_WORKSPACE_ROOT;
|
|
417
|
+
if (wsRoot) {
|
|
418
|
+
candidates.push(join(wsRoot, ".pi", "npm", "node_modules", "taskplane"));
|
|
419
|
+
candidates.push(join(wsRoot, "node_modules", "taskplane"));
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
for (const dir of candidates) {
|
|
423
|
+
try {
|
|
424
|
+
const pkgPath = join(dir, "package.json");
|
|
425
|
+
if (existsSync(pkgPath)) {
|
|
426
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
427
|
+
if (pkg.name === "taskplane") {
|
|
428
|
+
_packageRoot = dir;
|
|
429
|
+
return dir;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
} catch { /* ignore */ }
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
_packageRoot = "";
|
|
436
|
+
return "";
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Resolve the package-shipped base agent file path.
|
|
441
|
+
* Base files live in the package's templates/agents/ directory.
|
|
442
|
+
*/
|
|
443
|
+
function resolveBaseAgentPath(name: string): string {
|
|
444
|
+
const root = findPackageRoot();
|
|
445
|
+
if (!root) return "";
|
|
446
|
+
return join(root, "templates", "agents", `${name}.md`);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Load an agent definition with prompt inheritance.
|
|
451
|
+
*
|
|
452
|
+
* Inheritance model (default: compose base + local):
|
|
453
|
+
* 1. Load base agent from the shipped package (templates/agents/{name}.md)
|
|
454
|
+
* 2. Load local agent from .pi/agents/{name}.md (if it exists) — or from
|
|
455
|
+
* the pointer-resolved agent root in workspace mode
|
|
456
|
+
* 3. If local file has `standalone: true` in frontmatter, use it as-is (no base)
|
|
457
|
+
* 4. Otherwise, compose: base prompt + separator + local content
|
|
458
|
+
* 5. Local frontmatter values (tools, model) override base values
|
|
459
|
+
*
|
|
460
|
+
* Local override resolution order:
|
|
461
|
+
* 1. `<cwd>/.pi/agents/{name}.md` — worktree/repo local override (always first)
|
|
462
|
+
* 2. `<cwd>/agents/{name}.md` — worktree/repo local override (legacy location)
|
|
463
|
+
* 3. `<pointerAgentRoot>/{name}.md` — pointer-resolved config repo agents (workspace mode)
|
|
464
|
+
* First found wins. If none found, base file is used directly.
|
|
465
|
+
*
|
|
466
|
+
* If no base file exists (e.g., custom agent), local file is used as-is.
|
|
467
|
+
*/
|
|
342
468
|
function loadAgentDef(cwd: string, name: string): { systemPrompt: string; tools: string; model: string } | null {
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
469
|
+
const basePath = resolveBaseAgentPath(name);
|
|
470
|
+
const localPaths = [join(cwd, ".pi", "agents", `${name}.md`), join(cwd, "agents", `${name}.md`)];
|
|
471
|
+
|
|
472
|
+
// In workspace mode, add pointer-resolved agent root as fallback
|
|
473
|
+
const pointer = resolveTaskRunnerPointer();
|
|
474
|
+
if (pointer?.agentRoot) {
|
|
475
|
+
localPaths.push(join(pointer.agentRoot, `${name}.md`));
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Load base from package
|
|
479
|
+
const baseDef = parseAgentFile(basePath);
|
|
480
|
+
|
|
481
|
+
// Load local override (first found wins)
|
|
482
|
+
let localDef: { fm: Record<string, string>; body: string } | null = null;
|
|
483
|
+
for (const p of localPaths) {
|
|
484
|
+
localDef = parseAgentFile(p);
|
|
485
|
+
if (localDef) break;
|
|
355
486
|
}
|
|
356
|
-
|
|
487
|
+
|
|
488
|
+
// No base and no local → null
|
|
489
|
+
if (!baseDef && !localDef) return null;
|
|
490
|
+
|
|
491
|
+
// Local with standalone: true → use local as-is, ignore base
|
|
492
|
+
if (localDef?.fm.standalone === "true") {
|
|
493
|
+
return {
|
|
494
|
+
systemPrompt: localDef.body,
|
|
495
|
+
tools: localDef.fm.tools || "read,grep,find,ls",
|
|
496
|
+
model: localDef.fm.model || "",
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Compose base + local
|
|
501
|
+
const basePrompt = baseDef?.body || "";
|
|
502
|
+
const localPrompt = localDef?.body || "";
|
|
503
|
+
const composedPrompt = localPrompt
|
|
504
|
+
? basePrompt + "\n\n---\n\n## Project-Specific Guidance\n\n" + localPrompt
|
|
505
|
+
: basePrompt;
|
|
506
|
+
|
|
507
|
+
// Local frontmatter overrides base (tools, model)
|
|
508
|
+
const tools = localDef?.fm.tools || baseDef?.fm.tools || "read,grep,find,ls";
|
|
509
|
+
const model = localDef?.fm.model || baseDef?.fm.model || "";
|
|
510
|
+
|
|
511
|
+
return { systemPrompt: composedPrompt.trim(), tools, model };
|
|
357
512
|
}
|
|
358
513
|
|
|
359
514
|
// ── PROMPT.md Parser ─────────────────────────────────────────────────
|