taskplane 0.1.17 → 0.2.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.
@@ -8,20 +8,25 @@ import { join, basename, resolve } from "path";
8
8
 
9
9
  import { execLog } from "./execution.ts";
10
10
  import { runGit } from "./git.ts";
11
+ import { resolveOperatorId } from "./naming.ts";
11
12
  import { DEFAULT_ORCHESTRATOR_CONFIG, WorktreeError } from "./types.ts";
12
13
  import type { BulkWorktreeError, CreateLaneWorktreesResult, CreateWorktreeOptions, OrchestratorConfig, PreflightCheck, PreflightResult, RemoveAllWorktreesResult, RemoveWorktreeOutcome, RemoveWorktreeResult, WorktreeInfo } from "./types.ts";
13
14
 
14
15
  // ── Worktree Helpers ─────────────────────────────────────────────────
15
16
 
16
17
  /**
17
- * Generate branch name per §4.4 naming convention.
18
- * Format: task/lane-{N}-{batchId}
18
+ * Generate branch name per naming convention.
19
+ * Format: task/{opId}-lane-{N}-{batchId}
20
+ *
21
+ * Includes the operator identifier for collision resistance across
22
+ * concurrent operators in the same repository.
19
23
  *
20
24
  * @param laneNumber - Lane number (1-indexed)
21
25
  * @param batchId - Batch ID timestamp (e.g. "20260308T111750")
26
+ * @param opId - Operator identifier (sanitized, e.g., "henrylach")
22
27
  */
23
- export function generateBranchName(laneNumber: number, batchId: string): string {
24
- return `task/lane-${laneNumber}-${batchId}`;
28
+ export function generateBranchName(laneNumber: number, batchId: string, opId: string): string {
29
+ return `task/${opId}-lane-${laneNumber}-${batchId}`;
25
30
  }
26
31
 
27
32
  /**
@@ -52,26 +57,28 @@ export function resolveWorktreeBasePath(
52
57
  /**
53
58
  * Generate worktree path based on config's worktree_location setting.
54
59
  *
55
- * Naming rule: basename = {prefix}-{N}
56
- * Sibling mode: ../{prefix}-{N} (e.g. ../taskplane-wt-1)
57
- * Subdirectory mode: .worktrees/{prefix}-{N} (e.g. .worktrees/taskplane-wt-1)
60
+ * Naming rule: basename = {prefix}-{opId}-{N}
61
+ * Sibling mode: ../{prefix}-{opId}-{N} (e.g. ../taskplane-wt-henrylach-1)
62
+ * Subdirectory mode: .worktrees/{prefix}-{opId}-{N} (e.g. .worktrees/taskplane-wt-henrylach-1)
58
63
  *
59
64
  * Uses path.resolve() for Windows path normalization (R002 requirement).
60
65
  *
61
66
  * @param prefix - Directory prefix (e.g. "taskplane-wt")
62
67
  * @param laneNumber - Lane number (1-indexed)
63
68
  * @param repoRoot - Absolute path to the main repository root
69
+ * @param opId - Operator identifier (sanitized, e.g., "henrylach")
64
70
  * @param config - Orchestrator config (optional; defaults to subdirectory mode)
65
71
  */
66
72
  export function generateWorktreePath(
67
73
  prefix: string,
68
74
  laneNumber: number,
69
75
  repoRoot: string,
76
+ opId: string,
70
77
  config?: OrchestratorConfig,
71
78
  ): string {
72
79
  const effectiveConfig = config || DEFAULT_ORCHESTRATOR_CONFIG;
73
80
  const basePath = resolveWorktreeBasePath(repoRoot, effectiveConfig);
74
- return resolve(basePath, `${prefix}-${laneNumber}`);
81
+ return resolve(basePath, `${prefix}-${opId}-${laneNumber}`);
75
82
  }
76
83
 
77
84
  /**
@@ -195,10 +202,10 @@ export function isRegisteredWorktree(targetPath: string, cwd: string): boolean {
195
202
  * @throws - WorktreeError with stable error code on failure
196
203
  */
197
204
  export function createWorktree(opts: CreateWorktreeOptions, repoRoot: string): WorktreeInfo {
198
- const { laneNumber, batchId, baseBranch, prefix, config } = opts;
205
+ const { laneNumber, batchId, baseBranch, prefix, opId, config } = opts;
199
206
 
200
- const branch = generateBranchName(laneNumber, batchId);
201
- const worktreePath = generateWorktreePath(prefix, laneNumber, repoRoot, config);
207
+ const branch = generateBranchName(laneNumber, batchId, opId);
208
+ const worktreePath = generateWorktreePath(prefix, laneNumber, repoRoot, opId, config);
202
209
 
203
210
  // ── Pre-check 1: Validate base branch exists ─────────────────
204
211
  const baseBranchCheck = runGit(
@@ -1029,39 +1036,53 @@ export function preserveBranch(
1029
1036
  // ── Bulk Worktree Operations ─────────────────────────────────────────
1030
1037
 
1031
1038
  /**
1032
- * List all orchestrator worktrees matching a prefix pattern.
1039
+ * List all orchestrator worktrees matching a prefix and operator pattern.
1033
1040
  *
1034
1041
  * Parses `git worktree list --porcelain` via parseWorktreeList() and filters
1035
- * entries whose path basename matches `{prefix}-{N}` (where N is a number).
1042
+ * entries whose path basename matches `{prefix}-{opId}-{N}` (where N is a number).
1043
+ *
1044
+ * Operator-scoped discovery: only returns worktrees belonging to the specified
1045
+ * operator. This prevents one operator from accidentally reusing or removing
1046
+ * another operator's active worktrees in concurrent team-scale scenarios.
1036
1047
  *
1037
- * Naming invariant: basename = {prefix}-{N}. The prefix comes from config
1038
- * (e.g. "taskplane-wt"), and the lane number is appended with a single
1039
- * dash separator. No extra `-wt-` infix is added.
1048
+ * For backward compatibility, also matches the legacy pattern `{prefix}-{N}`
1049
+ * (worktrees from prior batches without operator IDs), but only when `opId`
1050
+ * is `"op"` (the default fallback), to avoid capturing other operators' resources.
1040
1051
  *
1041
1052
  * Lane number is extracted from the path basename pattern. Entries with
1042
1053
  * malformed/partial data (missing path, unparseable lane number) are
1043
1054
  * silently skipped — they are not orchestrator worktrees.
1044
1055
  *
1045
1056
  * @param prefix - Worktree directory prefix (e.g. "taskplane-wt")
1046
- * Full basename pattern: `{prefix}-{N}` (e.g. "taskplane-wt-1")
1047
1057
  * @param repoRoot - Absolute path to the main repository root
1058
+ * @param opId - Operator identifier for scoping (e.g., "henrylach")
1048
1059
  * @returns - WorktreeInfo[] sorted by laneNumber (ascending)
1049
1060
  */
1050
- export function listWorktrees(prefix: string, repoRoot: string): WorktreeInfo[] {
1061
+ export function listWorktrees(prefix: string, repoRoot: string, opId: string): WorktreeInfo[] {
1051
1062
  const entries = parseWorktreeList(repoRoot);
1052
1063
  const results: WorktreeInfo[] = [];
1053
1064
 
1054
- // Build regex pattern to match the worktree basename.
1055
- // Naming invariant: basename = {prefix}-{N} where N is one or more digits.
1056
- // Example: prefix "taskplane-wt" matches "taskplane-wt-1", "taskplane-wt-2", etc.
1057
- const pattern = new RegExp(`^${escapeRegex(prefix)}-(\\d+)$`);
1065
+ // Primary pattern: {prefix}-{opId}-{N}
1066
+ // Example: "taskplane-wt-henrylach-1"
1067
+ const primaryPattern = new RegExp(`^${escapeRegex(prefix)}-${escapeRegex(opId)}-(\\d+)$`);
1068
+
1069
+ // Legacy pattern: {prefix}-{N} (only matched when opId is the default fallback)
1070
+ // This allows cleanup of worktrees from prior batches without operator IDs.
1071
+ const legacyPattern = opId === "op"
1072
+ ? new RegExp(`^${escapeRegex(prefix)}-(\\d+)$`)
1073
+ : null;
1058
1074
 
1059
1075
  for (const entry of entries) {
1060
1076
  if (!entry.path) continue;
1061
1077
 
1062
1078
  // Extract basename from the worktree path
1063
1079
  const entryBasename = basename(resolve(entry.path));
1064
- const match = entryBasename.match(pattern);
1080
+
1081
+ // Try primary pattern first
1082
+ let match = entryBasename.match(primaryPattern);
1083
+ if (!match && legacyPattern) {
1084
+ match = entryBasename.match(legacyPattern);
1085
+ }
1065
1086
  if (!match) continue;
1066
1087
 
1067
1088
  const laneNumber = parseInt(match[1], 10);
@@ -1106,6 +1127,7 @@ export function escapeRegex(str: string): string {
1106
1127
  * @param config - Orchestrator config (prefix extracted from it)
1107
1128
  * @param repoRoot - Absolute path to the main repository root
1108
1129
  * @param baseBranch - Branch to base worktrees on (captured at batch start)
1130
+ * @param opId - Operator identifier for collision-resistant naming
1109
1131
  * @returns - CreateLaneWorktreesResult with success flag and details
1110
1132
  */
1111
1133
  export function createLaneWorktrees(
@@ -1116,13 +1138,14 @@ export function createLaneWorktrees(
1116
1138
  baseBranch: string,
1117
1139
  ): CreateLaneWorktreesResult {
1118
1140
  const prefix = config.orchestrator.worktree_prefix;
1141
+ const opId = resolveOperatorId(config);
1119
1142
  const created: WorktreeInfo[] = [];
1120
1143
  const errors: BulkWorktreeError[] = [];
1121
1144
 
1122
1145
  for (let lane = 1; lane <= count; lane++) {
1123
1146
  try {
1124
1147
  const wt = createWorktree(
1125
- { laneNumber: lane, batchId, baseBranch, prefix, config },
1148
+ { laneNumber: lane, batchId, baseBranch, prefix, opId, config },
1126
1149
  repoRoot,
1127
1150
  );
1128
1151
  created.push(wt);
@@ -1191,8 +1214,9 @@ export function ensureLaneWorktrees(
1191
1214
  baseBranch: string,
1192
1215
  ): CreateLaneWorktreesResult {
1193
1216
  const prefix = config.orchestrator.worktree_prefix;
1217
+ const opId = resolveOperatorId(config);
1194
1218
 
1195
- const existing = listWorktrees(prefix, repoRoot);
1219
+ const existing = listWorktrees(prefix, repoRoot, opId);
1196
1220
  const existingByLane = new Map<number, WorktreeInfo>();
1197
1221
  for (const wt of existing) {
1198
1222
  existingByLane.set(wt.laneNumber, wt);
@@ -1224,7 +1248,7 @@ export function ensureLaneWorktrees(
1224
1248
 
1225
1249
  try {
1226
1250
  const wt = createWorktree(
1227
- { laneNumber: lane, batchId, baseBranch, prefix, config },
1251
+ { laneNumber: lane, batchId, baseBranch, prefix, opId, config },
1228
1252
  repoRoot,
1229
1253
  );
1230
1254
  createdNow.push(wt);
@@ -1272,26 +1296,28 @@ export function ensureLaneWorktrees(
1272
1296
  }
1273
1297
 
1274
1298
  /**
1275
- * Remove all orchestrator worktrees matching a prefix.
1299
+ * Remove all orchestrator worktrees matching a prefix and operator scope.
1276
1300
  *
1277
- * Uses listWorktrees() to discover matching worktrees, then removes each
1278
- * one via removeWorktree(). Best-effort: continues on per-worktree errors
1279
- * (does not fail-fast).
1301
+ * Uses listWorktrees() to discover matching worktrees (operator-scoped),
1302
+ * then removes each one via removeWorktree(). Best-effort: continues on
1303
+ * per-worktree errors (does not fail-fast).
1280
1304
  *
1281
1305
  * When `targetBranch` is provided, branches with unmerged commits are
1282
1306
  * preserved as `saved/<branch>` refs instead of being force-deleted.
1283
1307
  *
1284
1308
  * @param prefix - Worktree directory prefix (e.g. "taskplane-wt")
1285
1309
  * @param repoRoot - Absolute path to the main repository root
1310
+ * @param opId - Operator identifier for scoping (e.g., "henrylach")
1286
1311
  * @param targetBranch - Optional target branch for unmerged commit detection (e.g. "develop")
1287
1312
  * @returns - RemoveAllWorktreesResult with per-worktree outcomes
1288
1313
  */
1289
1314
  export function removeAllWorktrees(
1290
1315
  prefix: string,
1291
1316
  repoRoot: string,
1317
+ opId: string,
1292
1318
  targetBranch?: string,
1293
1319
  ): RemoveAllWorktreesResult {
1294
- const worktrees = listWorktrees(prefix, repoRoot);
1320
+ const worktrees = listWorktrees(prefix, repoRoot, opId);
1295
1321
  const outcomes: RemoveWorktreeOutcome[] = [];
1296
1322
  const removed: WorktreeInfo[] = [];
1297
1323
  const failed: RemoveWorktreeOutcome[] = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskplane",
3
- "version": "0.1.17",
3
+ "version": "0.2.0",
4
4
  "description": "AI agent orchestration for pi — parallel task execution with checkpoint discipline",
5
5
  "keywords": [
6
6
  "pi-package",