taskplane 0.1.18 → 0.2.1

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[] = [];
@@ -1347,12 +1373,13 @@ export function removeAllWorktrees(
1347
1373
  * Execute a command synchronously and return { ok, stdout }.
1348
1374
  * Returns ok=false on any error (non-zero exit, command not found, etc.).
1349
1375
  */
1350
- export function execCheck(command: string): { ok: boolean; stdout: string } {
1376
+ export function execCheck(command: string, cwd?: string): { ok: boolean; stdout: string } {
1351
1377
  try {
1352
1378
  const stdout = execSync(command, {
1353
1379
  encoding: "utf-8",
1354
1380
  timeout: 10_000,
1355
1381
  stdio: ["pipe", "pipe", "pipe"],
1382
+ ...(cwd ? { cwd } : {}),
1356
1383
  }).trim();
1357
1384
  return { ok: true, stdout };
1358
1385
  } catch {
@@ -1391,7 +1418,7 @@ export function meetsMinVersion(actual: [number, number], minimum: [number, numb
1391
1418
  * - tmux version >= 2.6
1392
1419
  * - tmux functional (can create/destroy sessions)
1393
1420
  */
1394
- export function runPreflight(config: OrchestratorConfig): PreflightResult {
1421
+ export function runPreflight(config: OrchestratorConfig, repoRoot?: string): PreflightResult {
1395
1422
  const checks: PreflightCheck[] = [];
1396
1423
  const tmuxRequired = config.orchestrator.spawn_mode === "tmux";
1397
1424
 
@@ -1424,14 +1451,19 @@ export function runPreflight(config: OrchestratorConfig): PreflightResult {
1424
1451
  }
1425
1452
 
1426
1453
  // ── Git worktree support ─────────────────────────────────────
1427
- const worktreeResult = execCheck("git worktree list");
1454
+ // In workspace mode, cwd may not be a git repo — run from a repo root
1455
+ const worktreeResult = execCheck("git worktree list", repoRoot);
1428
1456
  checks.push({
1429
1457
  name: "git-worktree",
1430
1458
  status: worktreeResult.ok ? "pass" : "fail",
1431
1459
  message: worktreeResult.ok
1432
1460
  ? "Worktree support available"
1433
1461
  : "Git worktree not available",
1434
- hint: worktreeResult.ok ? undefined : "Upgrade Git to 2.15+",
1462
+ hint: worktreeResult.ok
1463
+ ? undefined
1464
+ : repoRoot
1465
+ ? "Upgrade Git to 2.15+"
1466
+ : "Workspace root is not a git repo. Check workspace config repo paths.",
1435
1467
  });
1436
1468
 
1437
1469
  // ── TMUX availability and version ────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskplane",
3
- "version": "0.1.18",
3
+ "version": "0.2.1",
4
4
  "description": "AI agent orchestration for pi — parallel task execution with checkpoint discipline",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -19,8 +19,8 @@ orchestrator:
19
19
  max_lanes: 3
20
20
 
21
21
  # Where to create worktree directories.
22
- # "sibling" = ../{prefix}-{N} (e.g. ../project-wt-1)
23
- # "subdirectory" = .worktrees/{prefix}-{N} (e.g. .worktrees/project-wt-1)
22
+ # "sibling" = ../{prefix}-{opId}-{N} (e.g. ../project-wt-alice-1)
23
+ # "subdirectory" = .worktrees/{prefix}-{opId}-{N} (e.g. .worktrees/project-wt-alice-1)
24
24
  worktree_location: "subdirectory"
25
25
  worktree_prefix: "project-wt"
26
26
 
@@ -34,6 +34,11 @@ orchestrator:
34
34
  # Prefix for TMUX session names when tmux mode is enabled.
35
35
  tmux_prefix: "orch"
36
36
 
37
+ # Optional operator identifier for team-scale collision resistance.
38
+ # Auto-detected from OS username if empty. Set explicitly in CI or
39
+ # when multiple operators share the same machine.
40
+ # operator_id: ""
41
+
37
42
  # ── Dependency Analysis ───────────────────────────────────────────────
38
43
 
39
44
  dependencies: