taskplane 0.28.1 → 0.28.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.
@@ -517,9 +517,11 @@ function renderSummary(batch) {
517
517
  const checkboxDone = ws.checked === ws.total && ws.total > 0;
518
518
  const pastWave = ws.waveIdx < currentWaveIdx;
519
519
  const batchDone = batch.phase === "completed";
520
- // TP-178: During merging, only past waves are done; current wave is merging, future waves are pending (#493)
520
+ // TP-178: During merging, only past waves are truly done. The current wave's
521
+ // checkboxDone/allSucceeded can be true (tasks finished) but the wave itself
522
+ // isn't done until the merge completes. (#493)
521
523
  const isMerging = batch.phase === "merging";
522
- const isDone = checkboxDone || pastWave || batchDone || ws.allSucceeded;
524
+ const isDone = batchDone || pastWave || (!isMerging && (checkboxDone || ws.allSucceeded));
523
525
  const isMergingWave = isMerging && ws.waveIdx === currentWaveIdx;
524
526
  const isCurrent = ws.waveIdx === currentWaveIdx && (batch.phase === "executing" || isMerging);
525
527
  const isFuture = ws.waveIdx > currentWaveIdx && (batch.phase === "executing" || isMerging);
@@ -711,7 +713,10 @@ function renderLanesTasks(batch, sessions) {
711
713
  // TP-176: Succeeded tasks always show 100% regardless of sidecar/statusData (#491).
712
714
  let progressHtml = "";
713
715
  const v2p = ls && ls._v2Progress;
714
- const useV2 = v2p && v2p.total > 0 && ls.taskId === task.taskId;
716
+ const taskMatch = v2p && ls.taskId === task.taskId;
717
+ // Split V2 usage: progress needs totals > 0, but step/iter can be used whenever present
718
+ const useV2Progress = taskMatch && v2p.total > 0;
719
+ const useV2Step = taskMatch && !!v2p.currentStep;
715
720
  if (task.status === "succeeded") {
716
721
  // #491 fix: succeeded tasks always show 100%
717
722
  progressHtml = `
@@ -719,9 +724,9 @@ function renderLanesTasks(batch, sessions) {
719
724
  <div class="task-progress-bar"><div class="task-progress-fill pct-hi" style="width:100%"></div></div>
720
725
  <span class="task-progress-text">100%</span>
721
726
  </div>`;
722
- } else if (sd || useV2) {
723
- const displayChecked = useV2 ? v2p.checked : (sd ? sd.checked : 0);
724
- const displayTotal = useV2 ? v2p.total : (sd ? sd.total : 0);
727
+ } else if (useV2Progress || (sd && sd.total > 0)) {
728
+ const displayChecked = useV2Progress ? v2p.checked : sd.checked;
729
+ const displayTotal = useV2Progress ? v2p.total : sd.total;
725
730
  const displayProgress = displayTotal > 0 ? Math.round((displayChecked / displayTotal) * 100) : 0;
726
731
  const fillClass = pctClass(displayProgress);
727
732
  progressHtml = `
@@ -731,20 +736,20 @@ function renderLanesTasks(batch, sessions) {
731
736
  </div>
732
737
  <span class="task-progress-text">${displayProgress}% ${displayChecked}/${displayTotal}</span>
733
738
  </div>`;
734
- } else if (task.status === "pending") {
735
- progressHtml = `
736
- <div class="task-progress">
737
- <div class="task-progress-bar"><div class="task-progress-fill pct-0" style="width:0%"></div></div>
738
- <span class="task-progress-text">0%</span>
739
- </div>`;
740
739
  } else if (task.status === "running") {
741
- // TP-178: Show executing indicator for running tasks without sidecar data (#494)
742
- // This covers non-final segment execution where the sidecar hasn't started yet.
740
+ // #494 fix: running tasks without meaningful totals show executing indicator
741
+ // This covers non-final segments, early execution before sidecar captures, and stale 0/0 data
743
742
  progressHtml = `
744
743
  <div class="task-progress">
745
744
  <div class="task-progress-bar"><div class="task-progress-fill pct-low task-progress-executing" style="width:100%"></div></div>
746
745
  <span class="task-progress-text">executing…</span>
747
746
  </div>`;
747
+ } else if (task.status === "pending") {
748
+ progressHtml = `
749
+ <div class="task-progress">
750
+ <div class="task-progress-bar"><div class="task-progress-fill pct-0" style="width:0%"></div></div>
751
+ <span class="task-progress-text">0%</span>
752
+ </div>`;
748
753
  } else {
749
754
  progressHtml = '<span style="color:var(--text-faint)">—</span>';
750
755
  }
@@ -756,10 +761,11 @@ function renderLanesTasks(batch, sessions) {
756
761
  if (task.status === "succeeded") {
757
762
  // TP-178: Succeeded tasks always show "Complete" regardless of sidecar data (#491)
758
763
  stepHtml = '<span style="color:var(--green)">Complete</span>';
759
- } else if (sd || (useV2 && v2p)) {
760
- const stepName = (useV2 && v2p && v2p.currentStep) ? v2p.currentStep : (sd ? sd.currentStep : "Unknown");
761
- const iter = (useV2 && v2p && v2p.iteration != null) ? v2p.iteration : (sd ? sd.iteration : 0);
762
- const revs = (useV2 && v2p && v2p.reviews != null) ? v2p.reviews : (sd ? sd.reviews : 0);
764
+ } else if (sd || useV2Step) {
765
+ // #488 fix: prefer V2 step name whenever present (even if totals are 0)
766
+ const stepName = useV2Step ? v2p.currentStep : (sd ? sd.currentStep : "Unknown");
767
+ const iter = (useV2Step && v2p.iteration != null) ? v2p.iteration : (sd ? sd.iteration : 0);
768
+ const revs = (useV2Step && v2p.reviews != null) ? v2p.reviews : (sd ? sd.reviews : 0);
763
769
  stepHtml = escapeHtml(stepName);
764
770
  if (iter > 0) stepHtml += `<span class="task-iter">i${iter}</span>`;
765
771
  if (revs > 0) stepHtml += `<span class="task-iter">r${revs}</span>`;
@@ -27,6 +27,7 @@ import { writeFileSync, readFileSync, existsSync, mkdirSync, renameSync, unlinkS
27
27
  import { join, dirname } from "path";
28
28
  import { spawn as nodeSpawn } from "child_process";
29
29
  import { resolvePiCliPath, resolveTaskplaneAgentTemplate } from "./path-resolver.ts";
30
+ import { loadPiSettingsPackages, filterExcludedExtensions } from "./settings-loader.ts";
30
31
  import { randomBytes } from "crypto";
31
32
  import { buildExpansionRequestId, type SegmentExpansionRequest } from "./types.ts";
32
33
 
@@ -445,6 +446,27 @@ export default function (pi: ExtensionAPI) {
445
446
  ];
446
447
  if (reviewerModel) args.push("--model", reviewerModel);
447
448
  if (reviewerThinking) args.push("--thinking", reviewerThinking);
449
+
450
+ // TP-180: Forward user-installed extensions to reviewer agent
451
+ // Use TASKPLANE_STATE_ROOT (canonical project root) for settings resolution,
452
+ // falling back to cwd (which may be a worktree without .pi/settings.json).
453
+ const settingsRoot = process.env.TASKPLANE_STATE_ROOT || cwd;
454
+ const reviewerPackages = loadPiSettingsPackages(settingsRoot);
455
+ // Apply reviewer-specific exclusions from config (JSON array via env)
456
+ let reviewerExclusions: string[] = [];
457
+ try {
458
+ const rawExclude = process.env.TASKPLANE_REVIEWER_EXCLUDE_EXTENSIONS;
459
+ if (rawExclude) {
460
+ const parsed = JSON.parse(rawExclude);
461
+ if (Array.isArray(parsed)) {
462
+ reviewerExclusions = parsed.filter((v: unknown): v is string => typeof v === "string");
463
+ }
464
+ }
465
+ } catch { /* ignore malformed */ }
466
+ const filteredReviewerPackages = filterExcludedExtensions(reviewerPackages, reviewerExclusions);
467
+ for (const pkg of filteredReviewerPackages) {
468
+ args.push("-e", pkg);
469
+ }
448
470
  const proc = nodeSpawn(process.execPath, args, {
449
471
  shell: false,
450
472
  cwd,
@@ -1125,6 +1125,7 @@ export function toOrchestratorConfig(config: TaskplaneConfig): import("./types.t
1125
1125
  verify: [...o.merge.verify],
1126
1126
  order: o.merge.order,
1127
1127
  timeout_minutes: o.merge.timeoutMinutes ?? 90,
1128
+ exclude_extensions: [...(o.merge.excludeExtensions ?? [])],
1128
1129
  },
1129
1130
  failure: {
1130
1131
  on_task_failure: o.failure.onTaskFailure,
@@ -1183,7 +1184,9 @@ export function toTaskRunnerConfig(config: TaskplaneConfig): import("./types.ts"
1183
1184
  model: config.taskRunner.reviewer.model,
1184
1185
  thinking: config.taskRunner.reviewer.thinking,
1185
1186
  tools: config.taskRunner.reviewer.tools,
1187
+ excludeExtensions: [...(config.taskRunner.reviewer.excludeExtensions ?? [])],
1186
1188
  },
1189
+ workerExcludeExtensions: [...(config.taskRunner.worker.excludeExtensions ?? [])],
1187
1190
  };
1188
1191
  }
1189
1192
 
@@ -111,6 +111,8 @@ export interface WorkerConfig {
111
111
  thinking: string;
112
112
  /** Optional spawn mode override for task-runner (Runtime V2 subprocess-only). */
113
113
  spawnMode?: "subprocess";
114
+ /** Package specifiers to exclude from extension forwarding for worker agents (exact match). @since TP-180 */
115
+ excludeExtensions?: string[];
114
116
  }
115
117
 
116
118
  /** Reviewer agent configuration */
@@ -121,6 +123,8 @@ export interface ReviewerConfig {
121
123
  tools: string;
122
124
  /** Thinking mode for reviewer */
123
125
  thinking: string;
126
+ /** Package specifiers to exclude from extension forwarding for reviewer agents (exact match). @since TP-180 */
127
+ excludeExtensions?: string[];
124
128
  }
125
129
 
126
130
  /** Context/resource limits for task execution */
@@ -310,6 +314,8 @@ export interface MergeConfig {
310
314
  order: "fewest-files-first" | "sequential";
311
315
  /** Merge-agent timeout in minutes */
312
316
  timeoutMinutes?: number;
317
+ /** Package specifiers to exclude from extension forwarding for merge agents (exact match). @since TP-180 */
318
+ excludeExtensions?: string[];
313
319
  }
314
320
 
315
321
  /** Failure policy settings */
@@ -588,8 +594,8 @@ export const DEFAULT_TASK_RUNNER_SECTION: TaskRunnerSection = {
588
594
  testing: { commands: {} },
589
595
  standards: { docs: [], rules: [] },
590
596
  standardsOverrides: {},
591
- worker: { model: "", tools: "read,write,edit,bash,grep,find,ls", thinking: "" },
592
- reviewer: { model: "", tools: "read,bash,grep,find,ls", thinking: "on" },
597
+ worker: { model: "", tools: "read,write,edit,bash,grep,find,ls", thinking: "", excludeExtensions: [] },
598
+ reviewer: { model: "", tools: "read,bash,grep,find,ls", thinking: "on", excludeExtensions: [] },
593
599
  context: {
594
600
  workerContextWindow: 0,
595
601
  warnPercent: 85,
@@ -645,6 +651,7 @@ export const DEFAULT_ORCHESTRATOR_SECTION: OrchestratorSection = {
645
651
  verify: [],
646
652
  order: "fewest-files-first",
647
653
  timeoutMinutes: 90,
654
+ excludeExtensions: [],
648
655
  },
649
656
  failure: {
650
657
  onTaskFailure: "skip-dependents",
@@ -6,7 +6,7 @@ import { existsSync, readdirSync, readFileSync, renameSync, unlinkSync } from "f
6
6
  import { join, resolve } from "path";
7
7
 
8
8
  import { formatDiscoveryResults, runDiscovery } from "./discovery.ts";
9
- import { buildReviewerEnv, computeTransitiveDependents, execLog, executeLaneV2, executeWave, killV2LaneAgents, resolveCanonicalTaskPaths } from "./execution.ts";
9
+ import { buildReviewerEnv, buildWorkerExcludeEnv, computeTransitiveDependents, execLog, executeLaneV2, executeWave, killV2LaneAgents, resolveCanonicalTaskPaths } from "./execution.ts";
10
10
  import type { RuntimeBackend } from "./execution.ts";
11
11
  import type { MonitorUpdateCallback } from "./execution.ts";
12
12
  // classifyExit no longer called directly — Tier 0 uses exitDiagnostic.classification
@@ -1381,7 +1381,7 @@ async function attemptWorkerCrashRetry(
1381
1381
  retryPauseSignal,
1382
1382
  wsRoot,
1383
1383
  isWsMode,
1384
- { ORCH_BATCH_ID: batchState.batchId, ...buildReviewerEnv(runnerConfig?.reviewer) }, // TP-089: ensure mailbox works for retries
1384
+ { ORCH_BATCH_ID: batchState.batchId, ...buildReviewerEnv(runnerConfig?.reviewer), ...buildWorkerExcludeEnv(runnerConfig?.workerExcludeExtensions) }, // TP-089: ensure mailbox works for retries
1385
1385
  );
1386
1386
 
1387
1387
  const retryOutcome = retryResult.tasks[0];
@@ -1640,7 +1640,7 @@ async function attemptModelFallbackRetry(
1640
1640
  // Pass TASKPLANE_MODEL_FALLBACK=1 as extra env var to signal
1641
1641
  // the task-runner to use the session model instead of configured model.
1642
1642
  // TP-089: Also include ORCH_BATCH_ID so mailbox steering works for retries.
1643
- const modelFallbackEnv = { TASKPLANE_MODEL_FALLBACK: "1", ORCH_BATCH_ID: batchState.batchId, ...buildReviewerEnv(runnerConfig?.reviewer) };
1643
+ const modelFallbackEnv = { TASKPLANE_MODEL_FALLBACK: "1", ORCH_BATCH_ID: batchState.batchId, ...buildReviewerEnv(runnerConfig?.reviewer), ...buildWorkerExcludeEnv(runnerConfig?.workerExcludeExtensions) };
1644
1644
  const retryResult = await executeLaneV2(
1645
1645
  retryLane,
1646
1646
  orchConfig,
@@ -1887,7 +1887,9 @@ async function attemptStaleWorktreeRecovery(
1887
1887
  model: runnerConfig?.reviewer?.model || "",
1888
1888
  thinking: runnerConfig?.reviewer?.thinking || "",
1889
1889
  tools: runnerConfig?.reviewer?.tools || "",
1890
+ excludeExtensions: runnerConfig?.reviewer?.excludeExtensions ?? [],
1890
1891
  },
1892
+ runnerConfig?.workerExcludeExtensions ?? [],
1891
1893
  );
1892
1894
 
1893
1895
  return retryResult;
@@ -2490,7 +2492,9 @@ export async function executeOrchBatch(
2490
2492
  model: runnerConfig?.reviewer?.model || "",
2491
2493
  thinking: runnerConfig?.reviewer?.thinking || "",
2492
2494
  tools: runnerConfig?.reviewer?.tools || "",
2495
+ excludeExtensions: runnerConfig?.reviewer?.excludeExtensions ?? [],
2493
2496
  },
2497
+ runnerConfig?.workerExcludeExtensions ?? [],
2494
2498
  );
2495
2499
 
2496
2500
  // ── TP-039: Tier 0 — Stale worktree recovery ────────────
@@ -1754,7 +1754,8 @@ export async function executeWave(
1754
1754
  runtimeBackend?: RuntimeBackend,
1755
1755
  onSupervisorAlert?: SupervisorAlertCallback,
1756
1756
  supervisorAutonomy: "interactive" | "supervised" | "autonomous" = "autonomous",
1757
- reviewerConfig?: { model?: string; thinking?: string; tools?: string },
1757
+ reviewerConfig?: { model?: string; thinking?: string; tools?: string; excludeExtensions?: string[] },
1758
+ workerExcludeExtensions?: string[],
1758
1759
  ): Promise<WaveExecutionResult> {
1759
1760
  const startedAt = Date.now();
1760
1761
  const policy = config.failure.on_task_failure;
@@ -1862,6 +1863,7 @@ export async function executeWave(
1862
1863
  ORCH_BATCH_ID: batchId,
1863
1864
  TASKPLANE_SUPERVISOR_AUTONOMY: supervisorAutonomy,
1864
1865
  ...buildReviewerEnv(reviewerConfig),
1866
+ ...buildWorkerExcludeEnv(workerExcludeExtensions),
1865
1867
  }, onSupervisorAlert),
1866
1868
  );
1867
1869
 
@@ -2503,13 +2505,44 @@ import { executeTaskV2, type LaneRunnerConfig, type LaneRunnerTaskResult } from
2503
2505
  *
2504
2506
  * @since TP-160
2505
2507
  */
2508
+ /**
2509
+ * Parse a JSON string array from an env var value, returning empty array on failure.
2510
+ * @since TP-180
2511
+ */
2512
+ function parseJsonArrayEnv(value?: string): string[] {
2513
+ if (!value) return [];
2514
+ try {
2515
+ const parsed = JSON.parse(value);
2516
+ if (Array.isArray(parsed)) return parsed.filter((v: unknown): v is string => typeof v === "string");
2517
+ } catch { /* ignore malformed */ }
2518
+ return [];
2519
+ }
2520
+
2506
2521
  export function buildReviewerEnv(
2507
- reviewerConfig?: { model?: string; thinking?: string; tools?: string } | null,
2522
+ reviewerConfig?: { model?: string; thinking?: string; tools?: string; excludeExtensions?: string[] } | null,
2508
2523
  ): Record<string, string> {
2509
2524
  const env: Record<string, string> = {};
2510
2525
  if (reviewerConfig?.model) env.TASKPLANE_REVIEWER_MODEL = reviewerConfig.model;
2511
2526
  if (reviewerConfig?.thinking) env.TASKPLANE_REVIEWER_THINKING = reviewerConfig.thinking;
2512
2527
  if (reviewerConfig?.tools) env.TASKPLANE_REVIEWER_TOOLS = reviewerConfig.tools;
2528
+ // TP-180: Forward reviewer extension exclusions as JSON array
2529
+ if (reviewerConfig?.excludeExtensions && reviewerConfig.excludeExtensions.length > 0) {
2530
+ env.TASKPLANE_REVIEWER_EXCLUDE_EXTENSIONS = JSON.stringify(reviewerConfig.excludeExtensions);
2531
+ }
2532
+ return env;
2533
+ }
2534
+
2535
+ /**
2536
+ * Build worker extension exclusion env vars from config.
2537
+ * @since TP-180
2538
+ */
2539
+ export function buildWorkerExcludeEnv(
2540
+ workerExcludeExtensions?: string[] | null,
2541
+ ): Record<string, string> {
2542
+ const env: Record<string, string> = {};
2543
+ if (workerExcludeExtensions && workerExcludeExtensions.length > 0) {
2544
+ env.TASKPLANE_WORKER_EXCLUDE_EXTENSIONS = JSON.stringify(workerExcludeExtensions);
2545
+ }
2513
2546
  return env;
2514
2547
  }
2515
2548
 
@@ -2606,6 +2639,9 @@ export async function executeLaneV2(
2606
2639
  reviewerModel: extraEnvVars?.TASKPLANE_REVIEWER_MODEL || "",
2607
2640
  reviewerThinking: extraEnvVars?.TASKPLANE_REVIEWER_THINKING || "",
2608
2641
  reviewerTools: extraEnvVars?.TASKPLANE_REVIEWER_TOOLS || "",
2642
+ // TP-180: Extension exclusion lists from config
2643
+ workerExcludeExtensions: parseJsonArrayEnv(extraEnvVars?.TASKPLANE_WORKER_EXCLUDE_EXTENSIONS),
2644
+ reviewerExcludeExtensions: parseJsonArrayEnv(extraEnvVars?.TASKPLANE_REVIEWER_EXCLUDE_EXTENSIONS),
2609
2645
  supervisorAutonomy,
2610
2646
  projectName: config.project?.name || "project",
2611
2647
  maxIterations: 20,