taskplane 0.29.2 → 0.30.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.
Files changed (41) hide show
  1. package/bin/gitignore-patterns.mjs +11 -8
  2. package/bin/rpc-wrapper.mjs +410 -357
  3. package/bin/taskplane.mjs +533 -250
  4. package/extensions/reviewer-extension.ts +17 -11
  5. package/extensions/taskplane/abort.ts +50 -18
  6. package/extensions/taskplane/agent-bridge-extension.ts +232 -105
  7. package/extensions/taskplane/agent-host.ts +224 -97
  8. package/extensions/taskplane/cleanup.ts +71 -42
  9. package/extensions/taskplane/config-loader.ts +142 -58
  10. package/extensions/taskplane/config-schema.ts +6 -13
  11. package/extensions/taskplane/config.ts +10 -2
  12. package/extensions/taskplane/diagnostic-reports.ts +59 -47
  13. package/extensions/taskplane/diagnostics.ts +13 -13
  14. package/extensions/taskplane/discovery.ts +35 -61
  15. package/extensions/taskplane/engine-worker.ts +53 -46
  16. package/extensions/taskplane/engine.ts +1760 -602
  17. package/extensions/taskplane/execution.ts +426 -206
  18. package/extensions/taskplane/extension.ts +1073 -598
  19. package/extensions/taskplane/formatting.ts +136 -124
  20. package/extensions/taskplane/git.ts +0 -2
  21. package/extensions/taskplane/lane-runner.ts +542 -311
  22. package/extensions/taskplane/mailbox.ts +57 -49
  23. package/extensions/taskplane/merge.ts +662 -383
  24. package/extensions/taskplane/messages.ts +109 -51
  25. package/extensions/taskplane/migrations.ts +1 -1
  26. package/extensions/taskplane/path-resolver.ts +8 -9
  27. package/extensions/taskplane/persistence.ts +425 -262
  28. package/extensions/taskplane/process-registry.ts +36 -7
  29. package/extensions/taskplane/quality-gate.ts +107 -55
  30. package/extensions/taskplane/resume.ts +774 -267
  31. package/extensions/taskplane/sessions.ts +1 -1
  32. package/extensions/taskplane/settings-tui.ts +505 -164
  33. package/extensions/taskplane/sidecar-telemetry.ts +25 -10
  34. package/extensions/taskplane/supervisor.ts +477 -270
  35. package/extensions/taskplane/task-executor-core.ts +178 -53
  36. package/extensions/taskplane/types.ts +186 -108
  37. package/extensions/taskplane/verification.ts +27 -22
  38. package/extensions/taskplane/waves.ts +59 -43
  39. package/extensions/taskplane/workspace.ts +14 -12
  40. package/extensions/taskplane/worktree.ts +218 -196
  41. package/package.json +14 -2
@@ -112,14 +112,19 @@ export interface FingerprintDiff {
112
112
  fixed: TestFingerprint[];
113
113
  }
114
114
 
115
-
116
115
  // ── Normalization Helpers ────────────────────────────────────────────
117
116
 
118
117
  /** Max length for normalized message strings */
119
118
  const MESSAGE_NORM_MAX_LENGTH = 512;
120
119
 
121
- // eslint-disable-next-line no-control-regex
122
- const ANSI_REGEX = /[\u001b\u009b]\[[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><~]/g;
120
+ // Built via `new RegExp` so Biome's noControlCharactersInRegex (which only
121
+ // inspects regex literals) does not flag the \u001b/\u009b escapes that are
122
+ // fundamental to ANSI sequence detection. Runtime behavior is identical to
123
+ // the prior literal regex; this is a static-analysis adjustment only.
124
+ const ANSI_REGEX = new RegExp(
125
+ "[\\u001b\\u009b]\\[[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><~]",
126
+ "g",
127
+ );
123
128
 
124
129
  /** Match duration strings like (42ms), (1.2s), (3m 12s), 42 ms, 1200ms */
125
130
  const DURATION_REGEX = /\(?\d+(?:\.\d+)?\s*(?:ms|s|m)\s*(?:\d+(?:\.\d+)?\s*(?:ms|s))?\)?/g;
@@ -172,7 +177,6 @@ export function fingerprintKey(fp: TestFingerprint): string {
172
177
  return `${fp.commandId}\0${fp.file}\0${fp.case}\0${fp.kind}\0${fp.messageNorm}`;
173
178
  }
174
179
 
175
-
176
180
  // ── Command Runner ───────────────────────────────────────────────────
177
181
 
178
182
  /** Default timeout for verification commands: 5 minutes */
@@ -255,7 +259,6 @@ export function runVerificationCommands(
255
259
  return results;
256
260
  }
257
261
 
258
-
259
262
  // ── Test Output Parsers ──────────────────────────────────────────────
260
263
 
261
264
  /**
@@ -371,7 +374,7 @@ export function parseVitestOutput(commandId: string, stdout: string): TestFinger
371
374
  // This covers setup/import/runtime-at-file-load errors where Vitest marks the file as
372
375
  // failed but produces no assertionResults (or only non-failed ones).
373
376
  if (testFile.status === "failed") {
374
- const hasFailedAssertions = hasAssertions && assertions!.some(a => a.status === "failed");
377
+ const hasFailedAssertions = hasAssertions && assertions!.some((a) => a.status === "failed");
375
378
  if (!hasFailedAssertions) {
376
379
  // No assertion-level failures captured — emit suite-level runtime_error fingerprint
377
380
  const suiteMessage = testFile.message || "Suite failed with no message";
@@ -407,13 +410,15 @@ export function parseTestOutput(commandResult: CommandResult): TestFingerprint[]
407
410
 
408
411
  // If command had a spawn/timeout error, produce a command_error fingerprint
409
412
  if (error) {
410
- return [{
411
- commandId,
412
- file: "",
413
- case: "",
414
- kind: "command_error",
415
- messageNorm: normalizeMessage(error),
416
- }];
413
+ return [
414
+ {
415
+ commandId,
416
+ file: "",
417
+ case: "",
418
+ kind: "command_error",
419
+ messageNorm: normalizeMessage(error),
420
+ },
421
+ ];
417
422
  }
418
423
 
419
424
  // If exit code is 0, no failures to fingerprint
@@ -433,16 +438,17 @@ export function parseTestOutput(commandResult: CommandResult): TestFingerprint[]
433
438
 
434
439
  // Fallback: command_error fingerprint with stderr (or stdout if stderr is empty)
435
440
  const fallbackMessage = stderr.trim() || stdout.trim() || "Command failed with no output";
436
- return [{
437
- commandId,
438
- file: "",
439
- case: "",
440
- kind: "command_error",
441
- messageNorm: normalizeMessage(fallbackMessage),
442
- }];
441
+ return [
442
+ {
443
+ commandId,
444
+ file: "",
445
+ case: "",
446
+ kind: "command_error",
447
+ messageNorm: normalizeMessage(fallbackMessage),
448
+ },
449
+ ];
443
450
  }
444
451
 
445
-
446
452
  // ── Fingerprint Diffing ──────────────────────────────────────────────
447
453
 
448
454
  /**
@@ -509,7 +515,6 @@ export function diffFingerprints(
509
515
  return { newFailures, preExisting, fixed };
510
516
  }
511
517
 
512
-
513
518
  // ── Baseline Capture ─────────────────────────────────────────────────
514
519
 
515
520
  /**
@@ -7,7 +7,23 @@ import { join } from "path";
7
7
  import { parseDependencyReference } from "./discovery.ts";
8
8
  import { resolveOperatorId } from "./naming.ts";
9
9
  import { AllocationError, buildSegmentId, getTaskDurationMinutes } from "./types.ts";
10
- import type { AllocatedLane, AllocatedTask, AllocateLanesResult, AllocationErrorCode, DependencyGraph, DiscoveryError, GraphValidationResult, LaneAssignment, OrchestratorConfig, ParsedTask, TaskSegmentPlan, TaskSegmentPlanMap, WaveAssignment, WaveComputationResult, WorkspaceConfig, WorktreeInfo } from "./types.ts";
10
+ import type {
11
+ AllocatedLane,
12
+ AllocatedTask,
13
+ AllocationErrorCode,
14
+ DependencyGraph,
15
+ DiscoveryError,
16
+ GraphValidationResult,
17
+ LaneAssignment,
18
+ OrchestratorConfig,
19
+ ParsedTask,
20
+ TaskSegmentPlan,
21
+ TaskSegmentPlanMap,
22
+ WaveAssignment,
23
+ WaveComputationResult,
24
+ WorkspaceConfig,
25
+ WorktreeInfo,
26
+ } from "./types.ts";
11
27
  import { getCurrentBranch, runGit } from "./git.ts";
12
28
  import { ensureLaneWorktrees, removeAllWorktrees, removeWorktree } from "./worktree.ts";
13
29
 
@@ -56,7 +72,6 @@ export function buildDependencyGraph(
56
72
  return { dependencies, dependents, nodes };
57
73
  }
58
74
 
59
-
60
75
  // ── Graph Validation ─────────────────────────────────────────────────
61
76
 
62
77
  /**
@@ -182,7 +197,6 @@ export function validateGraph(
182
197
  };
183
198
  }
184
199
 
185
-
186
200
  // ── Wave Computation (Topological Sort) ──────────────────────────────
187
201
 
188
202
  /**
@@ -259,7 +273,6 @@ export function computeWaves(
259
273
  return { waves, errors };
260
274
  }
261
275
 
262
-
263
276
  // ── File Scope Affinity ──────────────────────────────────────────────
264
277
 
265
278
  /**
@@ -403,7 +416,6 @@ export function applyFileScopeAffinity(
403
416
  return result;
404
417
  }
405
418
 
406
-
407
419
  // ── Repo-Scoped Lane Helpers ─────────────────────────────────────────
408
420
 
409
421
  /**
@@ -505,14 +517,18 @@ export function generateLaneId(laneLocalNumber: number, repoId?: string): string
505
517
  * @param opId - Operator identifier (sanitized, e.g., "henrylach")
506
518
  * @param repoId - Repo identifier (undefined in repo mode)
507
519
  */
508
- export function generateLaneSessionId(sessionPrefix: string, laneLocalNumber: number, opId: string, repoId?: string): string {
520
+ export function generateLaneSessionId(
521
+ sessionPrefix: string,
522
+ laneLocalNumber: number,
523
+ opId: string,
524
+ repoId?: string,
525
+ ): string {
509
526
  if (repoId) {
510
527
  return `${sessionPrefix}-${opId}-${repoId}-lane-${laneLocalNumber}`;
511
528
  }
512
529
  return `${sessionPrefix}-${opId}-lane-${laneLocalNumber}`;
513
530
  }
514
531
 
515
-
516
532
  // ── Repo-Scoped Worktree Resolution ─────────────────────────────────
517
533
 
518
534
  /**
@@ -583,7 +599,7 @@ export function resolveBaseBranch(
583
599
  // instead of the orch branch, bypassing batch isolation.
584
600
  console.error(
585
601
  `[taskplane] resolveBaseBranch WARNING: orch branch "${batchBaseBranch}" not found in repo "${repoId}" at ${repoRoot} — falling back to repo HEAD. ` +
586
- `This bypasses orch branch isolation. Ensure the orch branch was created in all workspace repos.`,
602
+ `This bypasses orch branch isolation. Ensure the orch branch was created in all workspace repos.`,
587
603
  );
588
604
  } catch (err) {
589
605
  console.error(
@@ -621,16 +637,15 @@ export function resolveBaseBranch(
621
637
  if (repoId && batchBaseBranch.startsWith("orch/")) {
622
638
  throw new Error(
623
639
  `Cannot resolve base branch for repo "${repoId}" at ${repoRoot}: ` +
624
- `HEAD is detached and no defaultBranch is configured. ` +
625
- `The batch base branch "${batchBaseBranch}" is an orch branch that does not exist in this repo. ` +
626
- `Configure a defaultBranch for this repo in task-orchestrator.yaml workspace settings.`,
640
+ `HEAD is detached and no defaultBranch is configured. ` +
641
+ `The batch base branch "${batchBaseBranch}" is an orch branch that does not exist in this repo. ` +
642
+ `Configure a defaultBranch for this repo in task-orchestrator.yaml workspace settings.`,
627
643
  );
628
644
  }
629
645
 
630
646
  return batchBaseBranch;
631
647
  }
632
648
 
633
-
634
649
  // ── Segment Planning (TP-080) ───────────────────────────────────────
635
650
 
636
651
  const SEGMENT_REPO_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
@@ -778,7 +793,7 @@ function buildSegmentNodes(taskId: string, repoIds: string[]) {
778
793
  repoId,
779
794
  order,
780
795
  }));
781
- return nodes.sort((a, b) => (a.order - b.order) || a.repoId.localeCompare(b.repoId));
796
+ return nodes.sort((a, b) => a.order - b.order || a.repoId.localeCompare(b.repoId));
782
797
  }
783
798
 
784
799
  export function buildSegmentPlanForTask(
@@ -839,7 +854,6 @@ export function buildTaskSegmentPlans(
839
854
  return plans;
840
855
  }
841
856
 
842
-
843
857
  // ── Lane Assignment ──────────────────────────────────────────────────
844
858
 
845
859
  /**
@@ -877,9 +891,7 @@ export function assignTasksToLanes(
877
891
 
878
892
  // Step 3: Initialize lane weights (for load-balanced assignment)
879
893
  const laneWeights: number[] = new Array(laneCount).fill(0);
880
- const laneAssignments: LaneAssignment[][] = new Array(laneCount)
881
- .fill(null)
882
- .map(() => []);
894
+ const laneAssignments: LaneAssignment[][] = new Array(laneCount).fill(null).map(() => []);
883
895
 
884
896
  function getWeight(taskId: string): number {
885
897
  const task = pending.get(taskId);
@@ -970,7 +982,6 @@ export function assignTasksToLanes(
970
982
  return result;
971
983
  }
972
984
 
973
-
974
985
  // ── Global Lane Cap (TP-148) ─────────────────────────────────────────
975
986
 
976
987
  /**
@@ -1044,8 +1055,8 @@ export function enforceGlobalLaneCap(
1044
1055
  if (finalTotal > maxLanes) {
1045
1056
  console.error(
1046
1057
  `[taskplane] warning: global maxLanes=${maxLanes} could not be enforced — ` +
1047
- `${byRepo.size} repos each need at least 1 lane (total: ${finalTotal}). ` +
1048
- `Increase maxLanes to at least ${byRepo.size} to avoid this.`,
1058
+ `${byRepo.size} repos each need at least 1 lane (total: ${finalTotal}). ` +
1059
+ `Increase maxLanes to at least ${byRepo.size} to avoid this.`,
1049
1060
  );
1050
1061
  }
1051
1062
 
@@ -1061,7 +1072,6 @@ export function enforceGlobalLaneCap(
1061
1072
  }
1062
1073
  }
1063
1074
 
1064
-
1065
1075
  /**
1066
1076
  * Result of `allocateLanes()`.
1067
1077
  *
@@ -1145,16 +1155,13 @@ export function validateAllocationInputs(
1145
1155
  return new AllocationError(
1146
1156
  "ALLOC_INVALID_CONFIG",
1147
1157
  `Unknown assignment strategy: "${config.assignment.strategy}". ` +
1148
- `Valid strategies: ${validStrategies.join(", ")}`,
1158
+ `Valid strategies: ${validStrategies.join(", ")}`,
1149
1159
  );
1150
1160
  }
1151
1161
 
1152
1162
  // Validate worktree prefix is non-empty
1153
1163
  if (!config.orchestrator.worktree_prefix?.trim()) {
1154
- return new AllocationError(
1155
- "ALLOC_INVALID_CONFIG",
1156
- `worktree_prefix must be a non-empty string`,
1157
- );
1164
+ return new AllocationError("ALLOC_INVALID_CONFIG", `worktree_prefix must be a non-empty string`);
1158
1165
  }
1159
1166
 
1160
1167
  return null;
@@ -1336,7 +1343,12 @@ export function allocateLanes(
1336
1343
  const groupLaneNumbers = repoLaneGroups.get(groupKey)!;
1337
1344
  const groupRepoId = repoIdForGroup.get(groupKey);
1338
1345
  const groupRepoRoot = resolveRepoRoot(groupRepoId, repoRoot, workspaceConfig);
1339
- const groupBaseBranch = resolveBaseBranch(groupRepoId, groupRepoRoot, baseBranch, workspaceConfig);
1346
+ const groupBaseBranch = resolveBaseBranch(
1347
+ groupRepoId,
1348
+ groupRepoRoot,
1349
+ baseBranch,
1350
+ workspaceConfig,
1351
+ );
1340
1352
 
1341
1353
  const worktreeResult = ensureLaneWorktrees(
1342
1354
  groupLaneNumbers,
@@ -1370,16 +1382,17 @@ export function allocateLanes(
1370
1382
  const failedLanes = worktreeResult.errors
1371
1383
  .map((e) => `Lane ${e.laneNumber}: [${e.code}] ${e.message}`)
1372
1384
  .join("\n");
1373
- const withinGroupRollbackIssues = worktreeResult.rollbackErrors.length > 0
1374
- ? "\nWithin-group rollback issues:\n" +
1375
- worktreeResult.rollbackErrors
1376
- .map((e) => ` Lane ${e.laneNumber}: [${e.code}] ${e.message}`)
1377
- .join("\n")
1378
- : "";
1379
- const crossRepoRollbackIssues = rollbackErrors.length > 0
1380
- ? "\nCross-repo rollback issues:\n" +
1381
- rollbackErrors.map((e) => ` ${e}`).join("\n")
1382
- : "";
1385
+ const withinGroupRollbackIssues =
1386
+ worktreeResult.rollbackErrors.length > 0
1387
+ ? "\nWithin-group rollback issues:\n" +
1388
+ worktreeResult.rollbackErrors
1389
+ .map((e) => ` Lane ${e.laneNumber}: [${e.code}] ${e.message}`)
1390
+ .join("\n")
1391
+ : "";
1392
+ const crossRepoRollbackIssues =
1393
+ rollbackErrors.length > 0
1394
+ ? "\nCross-repo rollback issues:\n" + rollbackErrors.map((e) => ` ${e}`).join("\n")
1395
+ : "";
1383
1396
 
1384
1397
  return {
1385
1398
  success: false,
@@ -1420,7 +1433,14 @@ export function allocateLanes(
1420
1433
  for (const groupKey of createdGroupKeys) {
1421
1434
  const groupRepoId = repoIdForGroup.get(groupKey);
1422
1435
  const groupRepoRoot = resolveRepoRoot(groupRepoId, repoRoot, workspaceConfig);
1423
- removeAllWorktrees(config.orchestrator.worktree_prefix, groupRepoRoot, opId, undefined, batchId, config);
1436
+ removeAllWorktrees(
1437
+ config.orchestrator.worktree_prefix,
1438
+ groupRepoRoot,
1439
+ opId,
1440
+ undefined,
1441
+ batchId,
1442
+ config,
1443
+ );
1424
1444
  }
1425
1445
  return {
1426
1446
  success: false,
@@ -1447,10 +1467,7 @@ export function allocateLanes(
1447
1467
  (sum, t) => sum + (sizeWeights[t.task.size] || sizeWeights["M"] || 2),
1448
1468
  0,
1449
1469
  );
1450
- const estimatedMinutes = allocatedTasks.reduce(
1451
- (sum, t) => sum + t.estimatedMinutes,
1452
- 0,
1453
- );
1470
+ const estimatedMinutes = allocatedTasks.reduce((sum, t) => sum + t.estimatedMinutes, 0);
1454
1471
 
1455
1472
  const laneSessionId = generateLaneSessionId(sessionPrefix, entry.localLane, opId, entry.repoId);
1456
1473
  allocatedLanes.push({
@@ -1480,7 +1497,6 @@ export function allocateLanes(
1480
1497
  };
1481
1498
  }
1482
1499
 
1483
-
1484
1500
  // ── Full Wave Pipeline ───────────────────────────────────────────────
1485
1501
 
1486
1502
  /**
@@ -54,7 +54,6 @@ import {
54
54
  type PointerResolution,
55
55
  } from "./types.ts";
56
56
 
57
-
58
57
  // ── Path Canonicalization ────────────────────────────────────────────
59
58
 
60
59
  /**
@@ -108,7 +107,6 @@ function isPathWithinContainer(childPath: string, parentPath: string): boolean {
108
107
  return child === parent || child.startsWith(`${parent}/`);
109
108
  }
110
109
 
111
-
112
110
  // ── Pointer Resolution ───────────────────────────────────────────────
113
111
 
114
112
  /**
@@ -287,7 +285,6 @@ export function resolvePointer(
287
285
  };
288
286
  }
289
287
 
290
-
291
288
  // ── Workspace Config Loading ─────────────────────────────────────────
292
289
 
293
290
  /**
@@ -453,9 +450,10 @@ export function loadWorkspaceConfig(workspaceRoot: string): WorkspaceConfig | nu
453
450
  normalizedPaths.set(normalizedPath, repoId);
454
451
 
455
452
  // Build repo config
456
- const defaultBranch = typeof repoEntry.default_branch === "string" && repoEntry.default_branch.trim()
457
- ? repoEntry.default_branch.trim()
458
- : undefined;
453
+ const defaultBranch =
454
+ typeof repoEntry.default_branch === "string" && repoEntry.default_branch.trim()
455
+ ? repoEntry.default_branch.trim()
456
+ : undefined;
459
457
 
460
458
  repos.set(repoId, {
461
459
  id: repoId,
@@ -587,7 +585,6 @@ export function loadWorkspaceConfig(workspaceRoot: string): WorkspaceConfig | nu
587
585
  };
588
586
  }
589
587
 
590
-
591
588
  // ── Cross-Config Validation ─────────────────────────────────────────
592
589
 
593
590
  /**
@@ -603,7 +600,7 @@ export function validateTaskAreasWithinTasksRoot(
603
600
  ): void {
604
601
  const tasksRoot = workspaceConfig.routing.tasksRoot;
605
602
  const areaEntries = Object.entries(taskRunnerConfig.task_areas ?? {}).sort((a, b) =>
606
- a[0].localeCompare(b[0])
603
+ a[0].localeCompare(b[0]),
607
604
  );
608
605
 
609
606
  for (const [areaName, area] of areaEntries) {
@@ -620,7 +617,6 @@ export function validateTaskAreasWithinTasksRoot(
620
617
  }
621
618
  }
622
619
 
623
-
624
620
  // ── Execution Context Builder ────────────────────────────────────────
625
621
 
626
622
  /**
@@ -643,8 +639,14 @@ function isInsideGitRepo(cwd: string): boolean {
643
639
 
644
640
  export function buildExecutionContext(
645
641
  cwd: string,
646
- loadOrchConfig: (root: string, pointerConfigRoot?: string) => import("./types.ts").OrchestratorConfig,
647
- loadTaskConfig: (root: string, pointerConfigRoot?: string) => import("./types.ts").TaskRunnerConfig,
642
+ loadOrchConfig: (
643
+ root: string,
644
+ pointerConfigRoot?: string,
645
+ ) => import("./types.ts").OrchestratorConfig,
646
+ loadTaskConfig: (
647
+ root: string,
648
+ pointerConfigRoot?: string,
649
+ ) => import("./types.ts").TaskRunnerConfig,
648
650
  ): import("./types.ts").ExecutionContext {
649
651
  const workspaceConfig = loadWorkspaceConfig(cwd);
650
652
 
@@ -656,7 +658,7 @@ export function buildExecutionContext(
656
658
  throw new WorkspaceConfigError(
657
659
  "WORKSPACE_SETUP_REQUIRED",
658
660
  `No workspace config found at ${wsConfigFile}, and current directory is not a git repository: ${cwd}. ` +
659
- `Run Taskplane from a git repository, or create ${wsConfigFile} (taskplane init) to use workspace mode.`,
661
+ `Run Taskplane from a git repository, or create ${wsConfigFile} (taskplane init) to use workspace mode.`,
660
662
  undefined,
661
663
  cwd,
662
664
  );