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.
- package/dashboard/public/app.js +155 -1
- package/dashboard/public/index.html +3 -0
- package/dashboard/public/style.css +73 -6
- package/dashboard/server.cjs +5 -0
- package/extensions/taskplane/abort.ts +24 -3
- package/extensions/taskplane/discovery.ts +24 -0
- package/extensions/taskplane/engine.ts +58 -62
- package/extensions/taskplane/execution.ts +4 -2
- package/extensions/taskplane/extension.ts +12 -1
- package/extensions/taskplane/index.ts +1 -0
- package/extensions/taskplane/merge.ts +250 -6
- package/extensions/taskplane/messages.ts +207 -3
- package/extensions/taskplane/naming.ts +117 -0
- package/extensions/taskplane/persistence.ts +174 -24
- package/extensions/taskplane/resume.ts +329 -76
- package/extensions/taskplane/types.ts +153 -6
- package/extensions/taskplane/waves.ts +386 -94
- package/extensions/taskplane/workspace.ts +17 -0
- package/extensions/taskplane/worktree.ts +67 -35
- package/package.json +1 -1
- package/templates/config/task-orchestrator.yaml +7 -2
|
@@ -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
|
|
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
|
|
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
|
-
*
|
|
1038
|
-
* (
|
|
1039
|
-
*
|
|
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
|
-
//
|
|
1055
|
-
//
|
|
1056
|
-
|
|
1057
|
-
|
|
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
|
-
|
|
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,
|
|
1278
|
-
* one via removeWorktree(). Best-effort: continues on
|
|
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
|
-
|
|
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
|
|
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
|
@@ -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:
|