taskplane 0.1.14 → 0.1.16
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/bin/taskplane.mjs +317 -9
- package/extensions/taskplane/abort.ts +461 -466
- package/extensions/taskplane/config.ts +17 -12
- package/extensions/taskplane/discovery.ts +168 -32
- package/extensions/taskplane/engine.ts +781 -758
- package/extensions/taskplane/execution.ts +112 -48
- package/extensions/taskplane/extension.ts +780 -693
- package/extensions/taskplane/git.ts +25 -7
- package/extensions/taskplane/index.ts +23 -22
- package/extensions/taskplane/merge.ts +18 -16
- package/extensions/taskplane/messages.ts +146 -134
- package/extensions/taskplane/persistence.ts +1136 -1121
- package/extensions/taskplane/resume.ts +1102 -1092
- package/extensions/taskplane/types.ts +243 -3
- package/extensions/taskplane/waves.ts +894 -900
- package/extensions/taskplane/workspace.ts +382 -0
- package/extensions/taskplane/worktree.ts +113 -11
- package/package.json +1 -1
- package/templates/config/task-orchestrator.yaml +86 -89
- package/templates/config/task-runner.yaml +95 -99
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Config loading from YAML
|
|
3
|
-
* @module orch/config
|
|
4
|
-
*/
|
|
5
|
-
import { readFileSync, existsSync } from "fs";
|
|
6
|
-
import { join } from "path";
|
|
7
|
-
import { parse as yamlParse } from "yaml";
|
|
8
|
-
|
|
9
|
-
import { DEFAULT_ORCHESTRATOR_CONFIG, DEFAULT_TASK_RUNNER_CONFIG } from "./types.ts";
|
|
10
|
-
import type { OrchestratorConfig, TaskArea, TaskRunnerConfig } from "./types.ts";
|
|
11
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Config loading from YAML
|
|
3
|
+
* @module orch/config
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync, existsSync } from "fs";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
import { parse as yamlParse } from "yaml";
|
|
8
|
+
|
|
9
|
+
import { DEFAULT_ORCHESTRATOR_CONFIG, DEFAULT_TASK_RUNNER_CONFIG } from "./types.ts";
|
|
10
|
+
import type { OrchestratorConfig, TaskArea, TaskRunnerConfig } from "./types.ts";
|
|
11
|
+
|
|
12
12
|
// ── Config Loading ───────────────────────────────────────────────────
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -84,11 +84,16 @@ export function loadTaskRunnerConfig(cwd: string): TaskRunnerConfig {
|
|
|
84
84
|
if (loaded?.task_areas) {
|
|
85
85
|
for (const [name, area] of Object.entries(loaded.task_areas)) {
|
|
86
86
|
const a = area as any;
|
|
87
|
-
|
|
87
|
+
const ta: TaskArea = {
|
|
88
88
|
path: a?.path || "",
|
|
89
89
|
prefix: a?.prefix || "",
|
|
90
90
|
context: a?.context || "",
|
|
91
91
|
};
|
|
92
|
+
// Parse repo_id (snake_case YAML key) into repoId for routing
|
|
93
|
+
if (a?.repo_id && typeof a.repo_id === "string" && a.repo_id.trim()) {
|
|
94
|
+
ta.repoId = a.repo_id.trim();
|
|
95
|
+
}
|
|
96
|
+
taskAreas[name] = ta;
|
|
92
97
|
}
|
|
93
98
|
}
|
|
94
99
|
return {
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Task discovery, PROMPT.md parsing, dependency resolution
|
|
3
|
-
* @module orch/discovery
|
|
4
|
-
*/
|
|
5
|
-
import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from "fs";
|
|
6
|
-
import { join, dirname, basename, resolve } from "path";
|
|
7
|
-
|
|
8
|
-
import
|
|
9
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Task discovery, PROMPT.md parsing, dependency resolution
|
|
3
|
+
* @module orch/discovery
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from "fs";
|
|
6
|
+
import { join, dirname, basename, resolve } from "path";
|
|
7
|
+
|
|
8
|
+
import { FATAL_DISCOVERY_CODES } from "./types.ts";
|
|
9
|
+
import type { DiscoveryError, DiscoveryResult, ParsedTask, TaskArea, WorkspaceConfig } from "./types.ts";
|
|
10
|
+
|
|
10
11
|
// ── PROMPT.md Parsing ────────────────────────────────────────────────
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -184,6 +185,54 @@ export function parsePromptForOrchestrator(
|
|
|
184
185
|
}
|
|
185
186
|
}
|
|
186
187
|
|
|
188
|
+
// ── Extract execution target (repo ID) ──────────────────────
|
|
189
|
+
// Repo ID validation: lowercase alphanumeric + hyphens, starting with alnum
|
|
190
|
+
const REPO_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
191
|
+
|
|
192
|
+
let promptRepoId: string | undefined;
|
|
193
|
+
|
|
194
|
+
// Priority 1: Section-based "## Execution Target" with "Repo: <id>" line
|
|
195
|
+
// Capture everything from section header to the next heading or --- divider.
|
|
196
|
+
// We avoid \n$ (which in multiline mode matches blank lines) by using a two-pass
|
|
197
|
+
// approach: find the section start, then slice to the next section boundary.
|
|
198
|
+
const execTargetHeaderIdx = content.search(/^##\s+Execution Target\s*$/m);
|
|
199
|
+
let execTargetSectionBody: string | null = null;
|
|
200
|
+
if (execTargetHeaderIdx !== -1) {
|
|
201
|
+
const afterHeader = content.indexOf("\n", execTargetHeaderIdx);
|
|
202
|
+
if (afterHeader !== -1) {
|
|
203
|
+
const rest = content.slice(afterHeader + 1);
|
|
204
|
+
const nextSectionMatch = rest.search(/^##\s|^---/m);
|
|
205
|
+
execTargetSectionBody = nextSectionMatch !== -1
|
|
206
|
+
? rest.slice(0, nextSectionMatch)
|
|
207
|
+
: rest;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (execTargetSectionBody !== null) {
|
|
211
|
+
// Match "Repo: api" or "**Repo:** api" or "Repo: api" with whitespace
|
|
212
|
+
const repoLineMatch = execTargetSectionBody.match(
|
|
213
|
+
/^\s*\*?\*?Repo:?\*?\*?\s+(\S+)/mi,
|
|
214
|
+
);
|
|
215
|
+
if (repoLineMatch) {
|
|
216
|
+
const candidate = repoLineMatch[1].trim().toLowerCase();
|
|
217
|
+
if (REPO_ID_PATTERN.test(candidate)) {
|
|
218
|
+
promptRepoId = candidate;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Priority 2 (fallback): Inline "**Repo:** <id>" anywhere in content
|
|
224
|
+
if (!promptRepoId) {
|
|
225
|
+
const inlineRepoMatch = content.match(
|
|
226
|
+
/^\*\*Repo:\*\*\s+(\S+)/m,
|
|
227
|
+
);
|
|
228
|
+
if (inlineRepoMatch) {
|
|
229
|
+
const candidate = inlineRepoMatch[1].trim().toLowerCase();
|
|
230
|
+
if (REPO_ID_PATTERN.test(candidate)) {
|
|
231
|
+
promptRepoId = candidate;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
187
236
|
// ── Extract file scope ───────────────────────────────────────
|
|
188
237
|
const fileScope: string[] = [];
|
|
189
238
|
const fileScopeMatch = content.match(
|
|
@@ -214,12 +263,13 @@ export function parsePromptForOrchestrator(
|
|
|
214
263
|
promptPath: resolve(promptPath),
|
|
215
264
|
areaName,
|
|
216
265
|
status: "pending",
|
|
266
|
+
...(promptRepoId ? { promptRepoId } : {}),
|
|
217
267
|
},
|
|
218
268
|
error: null,
|
|
219
269
|
};
|
|
220
270
|
}
|
|
221
271
|
|
|
222
|
-
|
|
272
|
+
|
|
223
273
|
// ── Area Scanning ────────────────────────────────────────────────────
|
|
224
274
|
|
|
225
275
|
/**
|
|
@@ -291,7 +341,7 @@ export function scanAreaForTasks(
|
|
|
291
341
|
return { tasks, errors };
|
|
292
342
|
}
|
|
293
343
|
|
|
294
|
-
|
|
344
|
+
|
|
295
345
|
// ── Completed Task Set ───────────────────────────────────────────────
|
|
296
346
|
|
|
297
347
|
/**
|
|
@@ -361,7 +411,7 @@ export function buildCompletedTaskSet(areaPaths: string[]): Set<string> {
|
|
|
361
411
|
return completed;
|
|
362
412
|
}
|
|
363
413
|
|
|
364
|
-
|
|
414
|
+
|
|
365
415
|
// ── Argument Resolution ──────────────────────────────────────────────
|
|
366
416
|
|
|
367
417
|
/**
|
|
@@ -440,6 +490,8 @@ export interface DiscoveryOptions {
|
|
|
440
490
|
refreshDependencies?: boolean;
|
|
441
491
|
dependencySource?: "prompt" | "agent";
|
|
442
492
|
useDependencyCache?: boolean;
|
|
493
|
+
/** Workspace config for repo routing (null/undefined = repo mode, no routing). */
|
|
494
|
+
workspaceConfig?: WorkspaceConfig | null;
|
|
443
495
|
}
|
|
444
496
|
|
|
445
497
|
export interface DependencyCacheFile {
|
|
@@ -531,7 +583,7 @@ export function applyDependenciesFromCache(
|
|
|
531
583
|
return { applied };
|
|
532
584
|
}
|
|
533
585
|
|
|
534
|
-
|
|
586
|
+
|
|
535
587
|
// ── Task Registry ────────────────────────────────────────────────────
|
|
536
588
|
|
|
537
589
|
/**
|
|
@@ -637,7 +689,7 @@ export function buildTaskRegistry(
|
|
|
637
689
|
return { pending, completed, errors };
|
|
638
690
|
}
|
|
639
691
|
|
|
640
|
-
|
|
692
|
+
|
|
641
693
|
// ── Cross-Area Dependency Resolution ─────────────────────────────────
|
|
642
694
|
|
|
643
695
|
/** Candidate match for a dependency reference found in task areas. */
|
|
@@ -806,7 +858,93 @@ export function resolveDependencies(
|
|
|
806
858
|
return errors;
|
|
807
859
|
}
|
|
808
860
|
|
|
809
|
-
|
|
861
|
+
|
|
862
|
+
// ── Task-to-Repo Routing ─────────────────────────────────────────────
|
|
863
|
+
|
|
864
|
+
/** Repo ID validation: lowercase alphanumeric + hyphens, starting with alnum */
|
|
865
|
+
const ROUTING_REPO_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Resolve the target repo for each discovered task using the routing
|
|
869
|
+
* precedence chain:
|
|
870
|
+
*
|
|
871
|
+
* 1. `task.promptRepoId` — declared in PROMPT.md metadata
|
|
872
|
+
* 2. `taskArea.repoId` — area-level config from task-runner.yaml
|
|
873
|
+
* 3. `workspaceConfig.routing.defaultRepo` — workspace-level default
|
|
874
|
+
*
|
|
875
|
+
* Only applied in workspace mode (when `workspaceConfig` is provided).
|
|
876
|
+
* In repo mode this function is never called.
|
|
877
|
+
*
|
|
878
|
+
* Returns an array of DiscoveryError for routing failures:
|
|
879
|
+
* - TASK_REPO_UNRESOLVED: no source provided a repo ID
|
|
880
|
+
* - TASK_REPO_UNKNOWN: resolved repo ID is not in workspace repos map
|
|
881
|
+
*/
|
|
882
|
+
export function resolveTaskRouting(
|
|
883
|
+
discovery: DiscoveryResult,
|
|
884
|
+
taskAreas: Record<string, TaskArea>,
|
|
885
|
+
workspaceConfig: WorkspaceConfig,
|
|
886
|
+
): DiscoveryError[] {
|
|
887
|
+
const errors: DiscoveryError[] = [];
|
|
888
|
+
const validRepoIds = workspaceConfig.repos;
|
|
889
|
+
|
|
890
|
+
for (const task of discovery.pending.values()) {
|
|
891
|
+
// Precedence 1: prompt-declared repo
|
|
892
|
+
let resolvedId = task.promptRepoId;
|
|
893
|
+
let source = "prompt";
|
|
894
|
+
|
|
895
|
+
// Precedence 2: area-level repo
|
|
896
|
+
if (!resolvedId) {
|
|
897
|
+
const area = taskAreas[task.areaName];
|
|
898
|
+
if (area?.repoId) {
|
|
899
|
+
const candidate = area.repoId.trim().toLowerCase();
|
|
900
|
+
if (ROUTING_REPO_ID_PATTERN.test(candidate)) {
|
|
901
|
+
resolvedId = candidate;
|
|
902
|
+
source = "area";
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Precedence 3: workspace default repo
|
|
908
|
+
if (!resolvedId) {
|
|
909
|
+
resolvedId = workspaceConfig.routing.defaultRepo;
|
|
910
|
+
source = "default";
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// Validate resolution
|
|
914
|
+
if (!resolvedId) {
|
|
915
|
+
errors.push({
|
|
916
|
+
code: "TASK_REPO_UNRESOLVED",
|
|
917
|
+
message:
|
|
918
|
+
`Task ${task.taskId} has no resolved repo. ` +
|
|
919
|
+
`Add a Repo: field to the PROMPT, set repo_id on area "${task.areaName}", ` +
|
|
920
|
+
`or set routing.default_repo in the workspace config.`,
|
|
921
|
+
taskId: task.taskId,
|
|
922
|
+
taskPath: task.promptPath,
|
|
923
|
+
});
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
if (!validRepoIds.has(resolvedId)) {
|
|
928
|
+
errors.push({
|
|
929
|
+
code: "TASK_REPO_UNKNOWN",
|
|
930
|
+
message:
|
|
931
|
+
`Task ${task.taskId} resolved to repo "${resolvedId}" (via ${source}), ` +
|
|
932
|
+
`but no repo with that ID exists in the workspace config. ` +
|
|
933
|
+
`Known repos: ${[...validRepoIds.keys()].join(", ")}`,
|
|
934
|
+
taskId: task.taskId,
|
|
935
|
+
taskPath: task.promptPath,
|
|
936
|
+
});
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// Attach resolved repo to the task
|
|
941
|
+
task.resolvedRepoId = resolvedId;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
return errors;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
|
|
810
948
|
// ── Discovery Pipeline (Public) ──────────────────────────────────────
|
|
811
949
|
|
|
812
950
|
/**
|
|
@@ -901,6 +1039,13 @@ export function runDiscovery(
|
|
|
901
1039
|
}
|
|
902
1040
|
}
|
|
903
1041
|
|
|
1042
|
+
// Step 6: Task-to-repo routing (workspace mode only)
|
|
1043
|
+
const workspaceConfig = options.workspaceConfig;
|
|
1044
|
+
if (workspaceConfig && workspaceConfig.mode === "workspace") {
|
|
1045
|
+
const routingErrors = resolveTaskRouting(discovery, taskAreas, workspaceConfig);
|
|
1046
|
+
discovery.errors.push(...routingErrors);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
904
1049
|
return discovery;
|
|
905
1050
|
}
|
|
906
1051
|
|
|
@@ -939,8 +1084,12 @@ export function formatDiscoveryResults(result: DiscoveryResult): string {
|
|
|
939
1084
|
task.dependencies.length > 0
|
|
940
1085
|
? ` → depends on: ${task.dependencies.join(", ")}`
|
|
941
1086
|
: "";
|
|
1087
|
+
const repo =
|
|
1088
|
+
task.resolvedRepoId
|
|
1089
|
+
? ` → repo: ${task.resolvedRepoId}`
|
|
1090
|
+
: "";
|
|
942
1091
|
lines.push(
|
|
943
|
-
` ${task.taskId} [${task.size}] ${task.taskName}${deps}`,
|
|
1092
|
+
` ${task.taskId} [${task.size}] ${task.taskName}${deps}${repo}`,
|
|
944
1093
|
);
|
|
945
1094
|
}
|
|
946
1095
|
}
|
|
@@ -949,22 +1098,9 @@ export function formatDiscoveryResults(result: DiscoveryResult): string {
|
|
|
949
1098
|
|
|
950
1099
|
// Show errors
|
|
951
1100
|
if (result.errors.length > 0) {
|
|
952
|
-
const
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
e.code === "DEP_UNRESOLVED" ||
|
|
956
|
-
e.code === "DEP_PENDING" ||
|
|
957
|
-
e.code === "DEP_AMBIGUOUS" ||
|
|
958
|
-
e.code === "PARSE_MISSING_ID",
|
|
959
|
-
);
|
|
960
|
-
const warnings = result.errors.filter(
|
|
961
|
-
(e) =>
|
|
962
|
-
e.code !== "DUPLICATE_ID" &&
|
|
963
|
-
e.code !== "DEP_UNRESOLVED" &&
|
|
964
|
-
e.code !== "DEP_PENDING" &&
|
|
965
|
-
e.code !== "DEP_AMBIGUOUS" &&
|
|
966
|
-
e.code !== "PARSE_MISSING_ID",
|
|
967
|
-
);
|
|
1101
|
+
const fatalCodes = new Set<string>(FATAL_DISCOVERY_CODES);
|
|
1102
|
+
const fatalErrors = result.errors.filter((e) => fatalCodes.has(e.code));
|
|
1103
|
+
const warnings = result.errors.filter((e) => !fatalCodes.has(e.code));
|
|
968
1104
|
|
|
969
1105
|
if (fatalErrors.length > 0) {
|
|
970
1106
|
lines.push("❌ Errors:");
|