taskplane 0.30.2 → 0.30.3

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 CHANGED
@@ -61,6 +61,26 @@ IMPORTANT: If you just installed pi, make sure you've configured at least one mo
61
61
  pi install npm:taskplane
62
62
  ```
63
63
 
64
+ As of pi `0.75.0`, this installs Taskplane into pi's private extension directory (`~/.pi/agent/npm/node_modules/`) rather than the system npm-global root. To make the `taskplane` CLI available on your shell PATH, add pi's bin dir once:
65
+
66
+ ```bash
67
+ # bash / zsh
68
+ echo 'export PATH="$HOME/.pi/agent/npm/node_modules/.bin:$PATH"' >> ~/.bashrc # or ~/.zshrc
69
+ ```
70
+
71
+ ```powershell
72
+ # PowerShell
73
+ [Environment]::SetEnvironmentVariable(
74
+ "PATH",
75
+ "$HOME\.pi\agent\npm\node_modules\.bin;" + [Environment]::GetEnvironmentVariable("PATH", "User"),
76
+ "User"
77
+ )
78
+ ```
79
+
80
+ With Pi's bin dir on PATH, `pi update` keeps Taskplane current automatically — there's no second update command to remember.
81
+
82
+ > **Note for users who previously ran `npm install -g taskplane`:** That puts a second copy of Taskplane in your system npm-global, separate from pi's private copy. The two copies drift independently (`pi update` only refreshes the pi-private one), and your shell will resolve `taskplane` to whichever is earlier on PATH. Run `taskplane doctor` — it now detects this duplication and prints a remediation. The fix is `npm uninstall -g taskplane` plus the PATH change above.
83
+
64
84
  ### Option B: Single Project-Local Install
65
85
 
66
86
  ```bash
@@ -68,6 +88,8 @@ cd my-project
68
88
  pi install -l npm:taskplane
69
89
  ```
70
90
 
91
+ Project-local installs land in `.pi/npm/node_modules/`. Either invoke via `npx taskplane <cmd>`, run the binary directly at `.pi/npm/node_modules/.bin/taskplane`, or add that dir to PATH (project-scoped — e.g. via a `.envrc` if you use direnv).
92
+
71
93
  ## Quickstart
72
94
 
73
95
  ### 1. Initialize a project (to scaffold settings)
package/bin/taskplane.mjs CHANGED
@@ -2789,6 +2789,53 @@ function cmdDoctor() {
2789
2789
  ` ${OK} taskplane package installed ${c.dim}(v${pkgVersion}, ${installType})${c.reset}`,
2790
2790
  );
2791
2791
 
2792
+ // Duplication check: when both the Pi-private and the npm-global copies
2793
+ // exist with different versions, `pi update` only refreshes the Pi-private
2794
+ // one and the system-wide CLI silently drifts behind. Surface this clearly
2795
+ // so operators don't run a stale CLI thinking `pi update` covered it.
2796
+ // Background: Pi 0.75.0 (2026-05-17) moved user-scoped pi packages from
2797
+ // npm's global root to `~/.pi/agent/npm/` to avoid system-Node permission
2798
+ // errors. Users who installed via `npm install -g taskplane` before that
2799
+ // landed (or who installed via both paths) end up with two on-disk copies.
2800
+ let duplicationDetected = false;
2801
+ const duplication = detectDuplicateTaskplaneInstall();
2802
+ if (duplication) {
2803
+ duplicationDetected = true;
2804
+ console.log();
2805
+ console.log(` ${WARN} taskplane is installed in TWO locations with different versions:`);
2806
+ for (const loc of duplication.locations) {
2807
+ console.log(
2808
+ ` ${c.dim}${loc.label}:${c.reset} v${loc.version} ${c.dim}(${loc.path})${c.reset}`,
2809
+ );
2810
+ }
2811
+ console.log(
2812
+ ` ${c.dim}→ ${c.reset}\`pi update\`${c.dim} only refreshes the Pi-private copy.${c.reset}`,
2813
+ );
2814
+ console.log(
2815
+ ` ${c.dim}→ Recommended fix: drop the npm-global copy and put Pi's bin dir on PATH:${c.reset}`,
2816
+ );
2817
+ console.log(` ${c.cyan} npm uninstall -g taskplane${c.reset}`);
2818
+ // Platform-aware PATH guidance: PowerShell on Windows, bash/zsh elsewhere.
2819
+ // Linux & macOS users running PowerShell can still copy the bash form;
2820
+ // Windows users running Git Bash can still copy the PowerShell form. The
2821
+ // platform check picks the line most likely to match a fresh shell on
2822
+ // the host OS so the inline copy/paste path is one-line on the common
2823
+ // case.
2824
+ if (process.platform === "win32") {
2825
+ console.log(
2826
+ ` ${c.cyan} $env:PATH = "$HOME\\.pi\\agent\\npm\\node_modules\\.bin;" + $env:PATH${c.reset} ${c.dim}# PowerShell, add to $PROFILE${c.reset}`,
2827
+ );
2828
+ console.log(
2829
+ ` ${c.dim} or, for Git Bash:${c.reset} ${c.cyan}export PATH="$HOME/.pi/agent/npm/node_modules/.bin:$PATH"${c.reset} ${c.dim}# ~/.bashrc${c.reset}`,
2830
+ );
2831
+ } else {
2832
+ console.log(
2833
+ ` ${c.cyan} export PATH="$HOME/.pi/agent/npm/node_modules/.bin:$PATH"${c.reset} ${c.dim}# add to ~/.bashrc / ~/.zshrc${c.reset}`,
2834
+ );
2835
+ }
2836
+ issues++;
2837
+ }
2838
+
2792
2839
  if (isWorkspaceMode) {
2793
2840
  console.log();
2794
2841
  if (wsResult.error) {
@@ -3315,9 +3362,21 @@ function cmdDoctor() {
3315
3362
  if (issues === 0) {
3316
3363
  console.log(`${OK} ${c.green}All checks passed!${c.reset}\n`);
3317
3364
  } else {
3318
- console.log(
3319
- `${FAIL} ${issues} issue(s) found. Run ${c.cyan}taskplane init${c.reset} to fix config issues.\n`,
3320
- );
3365
+ // Each check above prints its own inline remediation hint, so the
3366
+ // summary points back to those rather than recommending a single
3367
+ // generic fix. The previous summary always recommended
3368
+ // `taskplane init`, which was misleading when the underlying issue
3369
+ // was a duplicate install (whose remediation is
3370
+ // `npm uninstall -g taskplane`, not init).
3371
+ if (duplicationDetected) {
3372
+ console.log(
3373
+ `${FAIL} ${issues} issue(s) found. See remediation hints above (${c.cyan}npm uninstall -g taskplane${c.reset} for the duplicate install).\n`,
3374
+ );
3375
+ } else {
3376
+ console.log(
3377
+ `${FAIL} ${issues} issue(s) found. See remediation hints above; run ${c.cyan}taskplane init${c.reset} to fix config issues.\n`,
3378
+ );
3379
+ }
3321
3380
  process.exit(1);
3322
3381
  }
3323
3382
  }
@@ -3332,17 +3391,28 @@ function cmdVersion() {
3332
3391
  console.log(`\ntaskplane ${c.bold}v${pkgVersion}${c.reset}`);
3333
3392
  console.log(` Package: ${installType}`);
3334
3393
 
3335
- // Check for project config
3394
+ // Check for project config. Only render the `version` / `installedAt`
3395
+ // fields when they're actually present and non-empty — some marker files
3396
+ // (e.g. taskplane's own source repo, where `.pi/taskplane.json` carries
3397
+ // only migration history rather than init metadata) would otherwise
3398
+ // produce the user-visible "vundefined, initialized unknown" placeholders.
3336
3399
  const projectRoot = process.cwd();
3337
3400
  const tpJson = path.join(projectRoot, ".pi", "taskplane.json");
3338
3401
  if (fs.existsSync(tpJson)) {
3339
3402
  try {
3340
3403
  const info = JSON.parse(fs.readFileSync(tpJson, "utf-8"));
3341
- console.log(
3342
- ` Config: .pi/taskplane.json (v${info.version}, initialized ${info.installedAt?.slice(0, 10) || "unknown"})`,
3343
- );
3404
+ const parts = [];
3405
+ if (typeof info.version === "string" && info.version.length > 0) {
3406
+ parts.push(`v${info.version}`);
3407
+ }
3408
+ if (typeof info.installedAt === "string" && info.installedAt.length > 0) {
3409
+ parts.push(`initialized ${info.installedAt.slice(0, 10)}`);
3410
+ }
3411
+ const suffix =
3412
+ parts.length > 0 ? ` (${parts.join(", ")})` : ` ${c.dim}(metadata only)${c.reset}`;
3413
+ console.log(` Config: .pi/taskplane.json${suffix}`);
3344
3414
  } catch {
3345
- console.log(` Config: .pi/taskplane.json (unreadable)`);
3415
+ console.log(` Config: .pi/taskplane.json ${c.dim}(unreadable)${c.reset}`);
3346
3416
  }
3347
3417
  } else {
3348
3418
  console.log(` Config: ${c.dim}not initialized (run taskplane init)${c.reset}`);
@@ -3366,6 +3436,82 @@ function getPackageVersion() {
3366
3436
  }
3367
3437
  }
3368
3438
 
3439
+ /**
3440
+ * Detect a duplicate taskplane install: one copy at Pi's private extension
3441
+ * directory (`~/.pi/agent/npm/node_modules/taskplane/`) and another at the
3442
+ * system npm-global root, with DIFFERENT versions. Same-version installs
3443
+ * are harmless and intentionally not flagged (they may drift later but
3444
+ * aren't a problem in the present tense).
3445
+ *
3446
+ * Returns null when there's no detectable duplication, or an object
3447
+ * describing the two locations + their versions when there is.
3448
+ *
3449
+ * Best-effort: silently returns null if `npm root -g` is unreachable or
3450
+ * either package.json is unreadable, since the rest of `taskplane doctor`
3451
+ * should never crash on a side check.
3452
+ *
3453
+ * Background: Pi 0.75.0 (2026-05-17) moved user-scoped pi packages from
3454
+ * npm's global root to `~/.pi/agent/npm/` to avoid permission errors with
3455
+ * system-managed Node installs (Pi changelog: "Fixed user-scoped npm pi
3456
+ * packages to install under `~/.pi/agent/npm/` instead of npm's global
3457
+ * package root"). Users who had previously run `npm install -g taskplane`
3458
+ * (or who continue to do so per older docs) now have two on-disk copies
3459
+ * that drift independently: `pi update` only refreshes the Pi-private one.
3460
+ */
3461
+ function detectDuplicateTaskplaneInstall() {
3462
+ const piPrivatePath = path.join(homedir(), ".pi", "agent", "npm", "node_modules", "taskplane");
3463
+
3464
+ let npmRootGlobal = null;
3465
+ try {
3466
+ // 5s timeout guards against the rare case where `npm` is on PATH but
3467
+ // hangs (slow registry probe, misconfigured custom prefix, etc.).
3468
+ // `taskplane doctor` is a diagnostic — it should never make the CLI
3469
+ // itself feel hung. On timeout, execSync throws and we fall back to
3470
+ // the catch branch, returning null and silently skipping the
3471
+ // duplication check (best-effort by design).
3472
+ npmRootGlobal = execSync("npm root -g", {
3473
+ encoding: "utf-8",
3474
+ stdio: ["pipe", "pipe", "pipe"],
3475
+ timeout: 5000,
3476
+ })
3477
+ .toString()
3478
+ .trim();
3479
+ } catch {
3480
+ return null;
3481
+ }
3482
+ if (!npmRootGlobal) return null;
3483
+ const npmGlobalPath = path.join(npmRootGlobal, "taskplane");
3484
+
3485
+ // Skip if Pi-private and npm-global resolve to the same path on disk.
3486
+ // This can happen on installs where Pi hasn't (yet) migrated to its
3487
+ // private directory or where the two paths point at the same install.
3488
+ if (path.resolve(piPrivatePath) === path.resolve(npmGlobalPath)) return null;
3489
+
3490
+ const candidates = [
3491
+ { path: piPrivatePath, label: "Pi-private" },
3492
+ { path: npmGlobalPath, label: "npm-global" },
3493
+ ];
3494
+
3495
+ const found = [];
3496
+ for (const candidate of candidates) {
3497
+ try {
3498
+ const pkgJsonPath = path.join(candidate.path, "package.json");
3499
+ if (!fs.existsSync(pkgJsonPath)) continue;
3500
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
3501
+ if (typeof pkg.version !== "string" || pkg.version.length === 0) continue;
3502
+ found.push({ path: candidate.path, label: candidate.label, version: pkg.version });
3503
+ } catch {
3504
+ /* unreadable — skip this candidate */
3505
+ }
3506
+ }
3507
+
3508
+ // Need both copies to be a duplication. Same-version pair is not flagged.
3509
+ if (found.length < 2) return null;
3510
+ if (found[0].version === found[1].version) return null;
3511
+
3512
+ return { locations: found };
3513
+ }
3514
+
3369
3515
  // ─── dashboard ──────────────────────────────────────────────────────────────
3370
3516
 
3371
3517
  function cmdDashboard(args) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskplane",
3
- "version": "0.30.2",
3
+ "version": "0.30.3",
4
4
  "description": "AI agent orchestration for pi — parallel task execution with checkpoint discipline",
5
5
  "keywords": [
6
6
  "pi-package",