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
package/bin/taskplane.mjs
CHANGED
|
@@ -212,7 +212,6 @@ orchestrator:
|
|
|
212
212
|
max_lanes: ${vars.max_lanes}
|
|
213
213
|
worktree_location: "subdirectory"
|
|
214
214
|
worktree_prefix: "${vars.worktree_prefix}"
|
|
215
|
-
integration_branch: "${vars.integration_branch}"
|
|
216
215
|
batch_id_format: "timestamp"
|
|
217
216
|
spawn_mode: "subprocess"
|
|
218
217
|
tmux_prefix: "${vars.tmux_prefix}"
|
|
@@ -293,15 +292,17 @@ async function autoCommitTaskFiles(projectRoot, tasksRoot) {
|
|
|
293
292
|
|
|
294
293
|
function discoverTaskAreaMetadata(projectRoot) {
|
|
295
294
|
const runnerPath = path.join(projectRoot, ".pi", "task-runner.yaml");
|
|
296
|
-
if (!fs.existsSync(runnerPath)) return { paths: [], contexts: [] };
|
|
295
|
+
if (!fs.existsSync(runnerPath)) return { paths: [], contexts: [], areaRepoIds: {} };
|
|
297
296
|
|
|
298
297
|
const raw = readYaml(runnerPath);
|
|
299
|
-
if (!raw) return { paths: [], contexts: [] };
|
|
298
|
+
if (!raw) return { paths: [], contexts: [], areaRepoIds: {} };
|
|
300
299
|
|
|
301
300
|
const lines = raw.split(/\r?\n/);
|
|
302
301
|
let inTaskAreas = false;
|
|
302
|
+
let currentAreaName = null;
|
|
303
303
|
const paths = new Set();
|
|
304
304
|
const contexts = new Set();
|
|
305
|
+
const areaRepoIds = {}; // area name → repo_id (only areas that declare one)
|
|
305
306
|
|
|
306
307
|
for (const line of lines) {
|
|
307
308
|
const trimmed = line.trim();
|
|
@@ -318,6 +319,13 @@ function discoverTaskAreaMetadata(projectRoot) {
|
|
|
318
319
|
break;
|
|
319
320
|
}
|
|
320
321
|
|
|
322
|
+
// Area name line (2-space indent): " taskplane-tasks:"
|
|
323
|
+
const areaNameMatch = line.match(/^ ([A-Za-z0-9][A-Za-z0-9_-]*)\s*:\s*$/);
|
|
324
|
+
if (areaNameMatch) {
|
|
325
|
+
currentAreaName = areaNameMatch[1];
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
|
|
321
329
|
const pathMatch = line.match(/^\s{4}path:\s*["']?([^"'\n#]+)["']?\s*(?:#.*)?$/);
|
|
322
330
|
if (pathMatch?.[1]) {
|
|
323
331
|
paths.add(pathMatch[1].trim());
|
|
@@ -327,9 +335,18 @@ function discoverTaskAreaMetadata(projectRoot) {
|
|
|
327
335
|
if (contextMatch?.[1]) {
|
|
328
336
|
contexts.add(contextMatch[1].trim());
|
|
329
337
|
}
|
|
338
|
+
|
|
339
|
+
// Extract repo_id per area (workspace mode routing validation)
|
|
340
|
+
// Only store when trimmed value is non-empty — aligns with orchestrator
|
|
341
|
+
// config.ts behavior which ignores empty/whitespace repo_id values.
|
|
342
|
+
const repoIdMatch = line.match(/^\s{4}repo_id:\s*["']?([^"'\n#]+)["']?\s*(?:#.*)?$/);
|
|
343
|
+
const repoIdValue = repoIdMatch?.[1]?.trim();
|
|
344
|
+
if (repoIdValue && currentAreaName) {
|
|
345
|
+
areaRepoIds[currentAreaName] = repoIdValue;
|
|
346
|
+
}
|
|
330
347
|
}
|
|
331
348
|
|
|
332
|
-
return { paths: [...paths], contexts: [...contexts] };
|
|
349
|
+
return { paths: [...paths], contexts: [...contexts], areaRepoIds };
|
|
333
350
|
}
|
|
334
351
|
|
|
335
352
|
function discoverTaskAreaPaths(projectRoot) {
|
|
@@ -707,7 +724,6 @@ function getPresetVars(preset, projectRoot, tasksRootOverride = null) {
|
|
|
707
724
|
const { test: test_cmd, build: build_cmd } = detectStack(projectRoot);
|
|
708
725
|
return {
|
|
709
726
|
project_name: dirName,
|
|
710
|
-
integration_branch: "main",
|
|
711
727
|
max_lanes: 3,
|
|
712
728
|
worktree_prefix: `${slug}-wt`,
|
|
713
729
|
tmux_prefix: `${slug}-orch`,
|
|
@@ -725,7 +741,6 @@ async function getInteractiveVars(projectRoot, tasksRootOverride = null) {
|
|
|
725
741
|
const detected = detectStack(projectRoot);
|
|
726
742
|
|
|
727
743
|
const project_name = await ask("Project name", dirName);
|
|
728
|
-
const integration_branch = await ask("Default branch (fallback — orchestrator uses your current branch at runtime)", "main");
|
|
729
744
|
const max_lanes = parseInt(await ask("Max parallel lanes", "3")) || 3;
|
|
730
745
|
const tasks_root = tasksRootOverride || await ask("Tasks directory", "taskplane-tasks");
|
|
731
746
|
const default_area = await ask("Default area name", "general");
|
|
@@ -736,7 +751,6 @@ async function getInteractiveVars(projectRoot, tasksRootOverride = null) {
|
|
|
736
751
|
const slug = slugify(project_name);
|
|
737
752
|
return {
|
|
738
753
|
project_name,
|
|
739
|
-
integration_branch,
|
|
740
754
|
max_lanes,
|
|
741
755
|
worktree_prefix: `${slug}-wt`,
|
|
742
756
|
tmux_prefix: `${slug}-orch`,
|
|
@@ -769,6 +783,215 @@ function printFileList(vars, noExamples, preset, exampleTemplateDirs = []) {
|
|
|
769
783
|
console.log();
|
|
770
784
|
}
|
|
771
785
|
|
|
786
|
+
// ─── Workspace Mode Detection (for doctor) ─────────────────────────────────
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Lightweight workspace config loader for doctor diagnostics.
|
|
790
|
+
*
|
|
791
|
+
* Unlike the orchestrator's `loadWorkspaceConfig()` in workspace.ts (which
|
|
792
|
+
* throws on invalid config), this returns a result object so doctor can
|
|
793
|
+
* report errors as diagnostics and continue checking remaining items.
|
|
794
|
+
*
|
|
795
|
+
* Mode determination rules (mirrors workspace.ts):
|
|
796
|
+
* 1. No config file → { mode: "repo", config: null, error: null }
|
|
797
|
+
* 2. Config file present + valid → { mode: "workspace", config: {...}, error: null }
|
|
798
|
+
* 3. Config file present + invalid → { mode: "workspace", config: null, error: { code, message } }
|
|
799
|
+
*
|
|
800
|
+
* @param {string} projectRoot - Absolute path to the project root
|
|
801
|
+
* @returns {Promise<{ mode: string, config: object|null, error: object|null }>}
|
|
802
|
+
*/
|
|
803
|
+
function loadWorkspaceConfigForDoctor(projectRoot) {
|
|
804
|
+
const configFile = path.join(projectRoot, ".pi", "taskplane-workspace.yaml");
|
|
805
|
+
|
|
806
|
+
// 1. File existence — absent = repo mode
|
|
807
|
+
if (!fs.existsSync(configFile)) {
|
|
808
|
+
return { mode: "repo", config: null, error: null };
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// 2. File read
|
|
812
|
+
let rawContent;
|
|
813
|
+
try {
|
|
814
|
+
rawContent = fs.readFileSync(configFile, "utf-8");
|
|
815
|
+
} catch (err) {
|
|
816
|
+
return {
|
|
817
|
+
mode: "workspace",
|
|
818
|
+
config: null,
|
|
819
|
+
error: {
|
|
820
|
+
code: "WORKSPACE_FILE_READ_ERROR",
|
|
821
|
+
message: `Cannot read workspace config file: ${err.message}`,
|
|
822
|
+
},
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// 3. YAML parse using lightweight line-based extraction
|
|
827
|
+
// (avoids importing the yaml module for CLI startup speed)
|
|
828
|
+
let parsed;
|
|
829
|
+
try {
|
|
830
|
+
parsed = parseWorkspaceYaml(rawContent);
|
|
831
|
+
} catch (err) {
|
|
832
|
+
return {
|
|
833
|
+
mode: "workspace",
|
|
834
|
+
config: null,
|
|
835
|
+
error: {
|
|
836
|
+
code: "WORKSPACE_FILE_PARSE_ERROR",
|
|
837
|
+
message: `Cannot parse workspace config: ${err.message}`,
|
|
838
|
+
},
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// 4. Schema validation: repos map present and non-empty
|
|
843
|
+
if (!parsed.repos || Object.keys(parsed.repos).length === 0) {
|
|
844
|
+
return {
|
|
845
|
+
mode: "workspace",
|
|
846
|
+
config: null,
|
|
847
|
+
error: {
|
|
848
|
+
code: "WORKSPACE_SCHEMA_INVALID",
|
|
849
|
+
message: "Workspace config must define at least one repo under 'repos'.",
|
|
850
|
+
},
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// 5. Schema validation: routing present
|
|
855
|
+
if (!parsed.routing || (!parsed.routing.default_repo && !parsed.routing.tasks_root)) {
|
|
856
|
+
return {
|
|
857
|
+
mode: "workspace",
|
|
858
|
+
config: null,
|
|
859
|
+
error: {
|
|
860
|
+
code: "WORKSPACE_SCHEMA_INVALID",
|
|
861
|
+
message: "Workspace config must contain a 'routing' mapping with default_repo and tasks_root.",
|
|
862
|
+
},
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// 6. Per-repo validation: path field present
|
|
867
|
+
const repoKeys = Object.keys(parsed.repos).sort();
|
|
868
|
+
for (const repoId of repoKeys) {
|
|
869
|
+
const repo = parsed.repos[repoId];
|
|
870
|
+
if (!repo.path) {
|
|
871
|
+
return {
|
|
872
|
+
mode: "workspace",
|
|
873
|
+
config: null,
|
|
874
|
+
error: {
|
|
875
|
+
code: "WORKSPACE_REPO_PATH_MISSING",
|
|
876
|
+
message: `Repo '${repoId}' is missing a 'path' field.`,
|
|
877
|
+
},
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// 7. Routing validation
|
|
883
|
+
if (!parsed.routing.tasks_root) {
|
|
884
|
+
return {
|
|
885
|
+
mode: "workspace",
|
|
886
|
+
config: null,
|
|
887
|
+
error: {
|
|
888
|
+
code: "WORKSPACE_MISSING_TASKS_ROOT",
|
|
889
|
+
message: "Workspace config 'routing.tasks_root' is missing or empty.",
|
|
890
|
+
},
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
if (!parsed.routing.default_repo) {
|
|
895
|
+
return {
|
|
896
|
+
mode: "workspace",
|
|
897
|
+
config: null,
|
|
898
|
+
error: {
|
|
899
|
+
code: "WORKSPACE_MISSING_DEFAULT_REPO",
|
|
900
|
+
message: "Workspace config 'routing.default_repo' is missing or empty.",
|
|
901
|
+
},
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const defaultRepoId = parsed.routing.default_repo;
|
|
906
|
+
if (!parsed.repos[defaultRepoId]) {
|
|
907
|
+
const available = Object.keys(parsed.repos).join(", ");
|
|
908
|
+
return {
|
|
909
|
+
mode: "workspace",
|
|
910
|
+
config: null,
|
|
911
|
+
error: {
|
|
912
|
+
code: "WORKSPACE_DEFAULT_REPO_NOT_FOUND",
|
|
913
|
+
message: `routing.default_repo '${defaultRepoId}' does not match any repo ID. Available: ${available}`,
|
|
914
|
+
},
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// Valid workspace config — build summary for doctor display
|
|
919
|
+
return {
|
|
920
|
+
mode: "workspace",
|
|
921
|
+
config: {
|
|
922
|
+
repos: parsed.repos,
|
|
923
|
+
routing: {
|
|
924
|
+
tasksRoot: parsed.routing.tasks_root,
|
|
925
|
+
defaultRepo: defaultRepoId,
|
|
926
|
+
},
|
|
927
|
+
configPath: configFile,
|
|
928
|
+
},
|
|
929
|
+
error: null,
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* Lightweight YAML parser for workspace config.
|
|
935
|
+
* Extracts repos (id → { path, default_branch }) and routing fields.
|
|
936
|
+
* Does NOT handle all YAML — only the workspace config subset.
|
|
937
|
+
*/
|
|
938
|
+
function parseWorkspaceYaml(raw) {
|
|
939
|
+
const lines = raw.split(/\r?\n/);
|
|
940
|
+
const result = { repos: {}, routing: {} };
|
|
941
|
+
let section = null; // "repos" | "routing" | null
|
|
942
|
+
let currentRepoId = null; // current repo being parsed
|
|
943
|
+
|
|
944
|
+
for (const line of lines) {
|
|
945
|
+
const trimmed = line.trim();
|
|
946
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
947
|
+
|
|
948
|
+
// Top-level keys
|
|
949
|
+
if (/^repos\s*:\s*$/.test(trimmed)) {
|
|
950
|
+
section = "repos";
|
|
951
|
+
currentRepoId = null;
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
if (/^routing\s*:\s*$/.test(trimmed)) {
|
|
955
|
+
section = "routing";
|
|
956
|
+
currentRepoId = null;
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
// Any other top-level key ends current section
|
|
960
|
+
if (/^[a-z_]+\s*:/.test(line) && !line.startsWith(" ") && !line.startsWith("\t")) {
|
|
961
|
+
section = null;
|
|
962
|
+
currentRepoId = null;
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (section === "repos") {
|
|
967
|
+
// Repo ID line (2-space indent): " api:"
|
|
968
|
+
const repoIdMatch = line.match(/^ ([a-z0-9][a-z0-9_-]*)\s*:\s*$/);
|
|
969
|
+
if (repoIdMatch) {
|
|
970
|
+
currentRepoId = repoIdMatch[1];
|
|
971
|
+
result.repos[currentRepoId] = {};
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
974
|
+
// Repo property lines (4-space indent): " path: ../api-repo"
|
|
975
|
+
if (currentRepoId) {
|
|
976
|
+
const propMatch = line.match(/^\s{4}(\w+)\s*:\s*["']?([^"'\n#]+?)["']?\s*(?:#.*)?$/);
|
|
977
|
+
if (propMatch) {
|
|
978
|
+
result.repos[currentRepoId][propMatch[1]] = propMatch[2].trim();
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
if (section === "routing") {
|
|
984
|
+
// Routing property lines (2-space indent): " default_repo: api"
|
|
985
|
+
const propMatch = line.match(/^\s{2}(\w+)\s*:\s*["']?([^"'\n#]+?)["']?\s*(?:#.*)?$/);
|
|
986
|
+
if (propMatch) {
|
|
987
|
+
result.routing[propMatch[1]] = propMatch[2].trim();
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
return result;
|
|
993
|
+
}
|
|
994
|
+
|
|
772
995
|
// ─── doctor ─────────────────────────────────────────────────────────────────
|
|
773
996
|
|
|
774
997
|
function cmdDoctor() {
|
|
@@ -811,7 +1034,66 @@ function cmdDoctor() {
|
|
|
811
1034
|
const installType = isProjectLocal ? "project-local" : "global";
|
|
812
1035
|
console.log(` ${OK} taskplane package installed ${c.dim}(v${pkgVersion}, ${installType})${c.reset}`);
|
|
813
1036
|
|
|
814
|
-
//
|
|
1037
|
+
// Detect workspace mode
|
|
1038
|
+
const wsResult = loadWorkspaceConfigForDoctor(projectRoot);
|
|
1039
|
+
const isWorkspaceMode = wsResult.mode === "workspace";
|
|
1040
|
+
|
|
1041
|
+
if (isWorkspaceMode) {
|
|
1042
|
+
console.log();
|
|
1043
|
+
if (wsResult.error) {
|
|
1044
|
+
// Config present but invalid — report as failure
|
|
1045
|
+
const codeHint = wsResult.error.code ? ` [${wsResult.error.code}]` : "";
|
|
1046
|
+
console.log(` ${FAIL} workspace mode detected but config is invalid${codeHint}`);
|
|
1047
|
+
console.log(` ${c.dim}${wsResult.error.message}${c.reset}`);
|
|
1048
|
+
console.log(` ${c.dim}→ Fix .pi/taskplane-workspace.yaml or remove it to use repo mode${c.reset}`);
|
|
1049
|
+
issues++;
|
|
1050
|
+
} else {
|
|
1051
|
+
// Valid workspace config — show summary banner
|
|
1052
|
+
const cfg = wsResult.config;
|
|
1053
|
+
const repoIds = Object.keys(cfg.repos);
|
|
1054
|
+
const repoCount = repoIds.length;
|
|
1055
|
+
const defaultRepo = cfg.routing.defaultRepo;
|
|
1056
|
+
const tasksRoot = cfg.routing.tasksRoot;
|
|
1057
|
+
console.log(` ${OK} workspace mode ${c.dim}(${repoCount} repo${repoCount !== 1 ? "s" : ""}, default: ${defaultRepo})${c.reset}`);
|
|
1058
|
+
console.log(` ${c.dim}repos: ${repoIds.join(", ")}${c.reset}`);
|
|
1059
|
+
console.log(` ${c.dim}tasks_root: ${tasksRoot}${c.reset}`);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// Step 1: Validate repo topology (workspace mode + valid config only)
|
|
1064
|
+
if (isWorkspaceMode && wsResult.config) {
|
|
1065
|
+
console.log();
|
|
1066
|
+
const repoIds = Object.keys(wsResult.config.repos).sort();
|
|
1067
|
+
for (const repoId of repoIds) {
|
|
1068
|
+
const repo = wsResult.config.repos[repoId];
|
|
1069
|
+
const resolvedPath = path.resolve(projectRoot, repo.path);
|
|
1070
|
+
|
|
1071
|
+
// Check path exists on disk
|
|
1072
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
1073
|
+
console.log(` ${FAIL} repo: ${repoId} — path not found: ${resolvedPath} [WORKSPACE_REPO_PATH_NOT_FOUND]`);
|
|
1074
|
+
console.log(` ${c.dim}→ Check repos.${repoId}.path in .pi/taskplane-workspace.yaml${c.reset}`);
|
|
1075
|
+
issues++;
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// Check path is a git repository
|
|
1080
|
+
try {
|
|
1081
|
+
execSync("git rev-parse --git-dir", {
|
|
1082
|
+
cwd: resolvedPath,
|
|
1083
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1084
|
+
timeout: 5000,
|
|
1085
|
+
});
|
|
1086
|
+
console.log(` ${OK} repo: ${repoId} ${c.dim}(${resolvedPath})${c.reset}`);
|
|
1087
|
+
} catch {
|
|
1088
|
+
console.log(` ${FAIL} repo: ${repoId} — not a git repository: ${resolvedPath} [WORKSPACE_REPO_NOT_GIT]`);
|
|
1089
|
+
console.log(` ${c.dim}→ Run: git init ${resolvedPath}${c.reset}`);
|
|
1090
|
+
console.log(` ${c.dim} or fix repos.${repoId}.path in .pi/taskplane-workspace.yaml${c.reset}`);
|
|
1091
|
+
issues++;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// Check project config (common — both modes)
|
|
815
1097
|
console.log();
|
|
816
1098
|
const configFiles = [
|
|
817
1099
|
{ path: ".pi/task-runner.yaml", required: true },
|
|
@@ -822,20 +1104,30 @@ function cmdDoctor() {
|
|
|
822
1104
|
{ path: ".pi/taskplane.json", required: false },
|
|
823
1105
|
];
|
|
824
1106
|
|
|
1107
|
+
// In workspace mode, include workspace config in the config files check
|
|
1108
|
+
if (isWorkspaceMode && !wsResult.error) {
|
|
1109
|
+
configFiles.push({ path: ".pi/taskplane-workspace.yaml", required: true });
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
let missingRequiredConfigs = 0;
|
|
825
1113
|
for (const { path: relPath, required } of configFiles) {
|
|
826
1114
|
const exists = fs.existsSync(path.join(projectRoot, relPath));
|
|
827
1115
|
if (exists) {
|
|
828
1116
|
console.log(` ${OK} ${relPath} exists`);
|
|
829
1117
|
} else if (required) {
|
|
830
1118
|
console.log(` ${FAIL} ${relPath} missing`);
|
|
1119
|
+
missingRequiredConfigs++;
|
|
831
1120
|
issues++;
|
|
832
1121
|
} else {
|
|
833
1122
|
console.log(` ${WARN} ${relPath} missing ${c.dim}(optional)${c.reset}`);
|
|
834
1123
|
}
|
|
835
1124
|
}
|
|
1125
|
+
if (missingRequiredConfigs > 0) {
|
|
1126
|
+
console.log(` ${c.dim}→ Run: taskplane init${c.reset}`);
|
|
1127
|
+
}
|
|
836
1128
|
|
|
837
1129
|
// Check task areas from config
|
|
838
|
-
const { paths: taskAreaPaths, contexts: taskAreaContexts } = discoverTaskAreaMetadata(projectRoot);
|
|
1130
|
+
const { paths: taskAreaPaths, contexts: taskAreaContexts, areaRepoIds } = discoverTaskAreaMetadata(projectRoot);
|
|
839
1131
|
if (taskAreaPaths.length > 0) {
|
|
840
1132
|
console.log();
|
|
841
1133
|
for (const areaPath of taskAreaPaths) {
|
|
@@ -858,6 +1150,22 @@ function cmdDoctor() {
|
|
|
858
1150
|
}
|
|
859
1151
|
}
|
|
860
1152
|
|
|
1153
|
+
// Validate area repo_id routing targets (workspace mode + valid config only)
|
|
1154
|
+
if (isWorkspaceMode && wsResult.config && Object.keys(areaRepoIds).length > 0) {
|
|
1155
|
+
const knownRepoIds = Object.keys(wsResult.config.repos).sort();
|
|
1156
|
+
const areaNames = Object.keys(areaRepoIds).sort();
|
|
1157
|
+
for (const areaName of areaNames) {
|
|
1158
|
+
const repoId = areaRepoIds[areaName];
|
|
1159
|
+
if (knownRepoIds.includes(repoId)) {
|
|
1160
|
+
console.log(` ${OK} area '${areaName}' repo_id: ${repoId}`);
|
|
1161
|
+
} else {
|
|
1162
|
+
console.log(` ${FAIL} area '${areaName}' repo_id '${repoId}' does not match any workspace repo [AREA_REPO_ID_UNKNOWN]`);
|
|
1163
|
+
console.log(` ${c.dim}→ Available repos: ${knownRepoIds.join(", ")}. Fix repo_id in .pi/task-runner.yaml${c.reset}`);
|
|
1164
|
+
issues++;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
861
1169
|
console.log();
|
|
862
1170
|
if (issues === 0) {
|
|
863
1171
|
console.log(`${OK} ${c.green}All checks passed!${c.reset}\n`);
|