taskplane 0.29.2 → 0.30.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.
Files changed (41) hide show
  1. package/bin/gitignore-patterns.mjs +11 -8
  2. package/bin/rpc-wrapper.mjs +410 -357
  3. package/bin/taskplane.mjs +533 -250
  4. package/extensions/reviewer-extension.ts +17 -11
  5. package/extensions/taskplane/abort.ts +50 -18
  6. package/extensions/taskplane/agent-bridge-extension.ts +232 -105
  7. package/extensions/taskplane/agent-host.ts +224 -97
  8. package/extensions/taskplane/cleanup.ts +71 -42
  9. package/extensions/taskplane/config-loader.ts +142 -58
  10. package/extensions/taskplane/config-schema.ts +6 -13
  11. package/extensions/taskplane/config.ts +10 -2
  12. package/extensions/taskplane/diagnostic-reports.ts +59 -47
  13. package/extensions/taskplane/diagnostics.ts +13 -13
  14. package/extensions/taskplane/discovery.ts +35 -61
  15. package/extensions/taskplane/engine-worker.ts +53 -46
  16. package/extensions/taskplane/engine.ts +1760 -602
  17. package/extensions/taskplane/execution.ts +426 -206
  18. package/extensions/taskplane/extension.ts +1073 -598
  19. package/extensions/taskplane/formatting.ts +136 -124
  20. package/extensions/taskplane/git.ts +0 -2
  21. package/extensions/taskplane/lane-runner.ts +542 -311
  22. package/extensions/taskplane/mailbox.ts +57 -49
  23. package/extensions/taskplane/merge.ts +662 -383
  24. package/extensions/taskplane/messages.ts +109 -51
  25. package/extensions/taskplane/migrations.ts +1 -1
  26. package/extensions/taskplane/path-resolver.ts +8 -9
  27. package/extensions/taskplane/persistence.ts +425 -262
  28. package/extensions/taskplane/process-registry.ts +36 -7
  29. package/extensions/taskplane/quality-gate.ts +107 -55
  30. package/extensions/taskplane/resume.ts +774 -267
  31. package/extensions/taskplane/sessions.ts +1 -1
  32. package/extensions/taskplane/settings-tui.ts +505 -164
  33. package/extensions/taskplane/sidecar-telemetry.ts +25 -10
  34. package/extensions/taskplane/supervisor.ts +477 -270
  35. package/extensions/taskplane/task-executor-core.ts +178 -53
  36. package/extensions/taskplane/types.ts +186 -108
  37. package/extensions/taskplane/verification.ts +27 -22
  38. package/extensions/taskplane/waves.ts +59 -43
  39. package/extensions/taskplane/workspace.ts +14 -12
  40. package/extensions/taskplane/worktree.ts +218 -196
  41. package/package.json +14 -2
package/bin/taskplane.mjs CHANGED
@@ -17,7 +17,7 @@ const nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
17
17
  if (nodeMajor < MIN_NODE_MAJOR) {
18
18
  console.error(
19
19
  `\x1b[31m❌ Taskplane requires Node.js >= ${MIN_NODE_MAJOR}.0.0 (found ${process.versions.node}).\x1b[0m\n` +
20
- ` Upgrade: https://nodejs.org/\n`
20
+ ` Upgrade: https://nodejs.org/\n`,
21
21
  );
22
22
  process.exit(1);
23
23
  }
@@ -173,7 +173,12 @@ export function parsePiListModelsOutput(rawOutput) {
173
173
  if (!/^[a-z0-9][a-z0-9._-]*$/i.test(provider)) continue;
174
174
  if (!/^[^\s]+$/.test(id)) continue;
175
175
 
176
- const thinkingToken = thinkingCol >= 0 ? String(parts[thinkingCol] ?? "").trim().toLowerCase() : "";
176
+ const thinkingToken =
177
+ thinkingCol >= 0
178
+ ? String(parts[thinkingCol] ?? "")
179
+ .trim()
180
+ .toLowerCase()
181
+ : "";
177
182
  const supportsThinking = (() => {
178
183
  if (!thinkingToken) return undefined;
179
184
  if (["yes", "true", "on", "supported"].includes(thinkingToken)) return true;
@@ -198,8 +203,8 @@ export function parsePiListModelsOutput(rawOutput) {
198
203
  });
199
204
  }
200
205
 
201
- return [...parsed.values()].sort((a, b) =>
202
- a.provider.localeCompare(b.provider) || a.id.localeCompare(b.id)
206
+ return [...parsed.values()].sort(
207
+ (a, b) => a.provider.localeCompare(b.provider) || a.id.localeCompare(b.id),
203
208
  );
204
209
  }
205
210
 
@@ -349,7 +354,9 @@ function createBootstrapGlobalPreferencesForCli() {
349
354
  }
350
355
 
351
356
  function normalizeThinkingMode(value) {
352
- const cleaned = String(value ?? "").trim().toLowerCase();
357
+ const cleaned = String(value ?? "")
358
+ .trim()
359
+ .toLowerCase();
353
360
  if (!cleaned || cleaned === "inherit") return "";
354
361
  if (cleaned === "on") return "high";
355
362
  if (PI_THINKING_LEVELS.includes(cleaned)) return cleaned;
@@ -365,12 +372,17 @@ function sanitizeInitAgentConfig(raw) {
365
372
  const defaults = createInheritInitAgentConfig();
366
373
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return defaults;
367
374
 
368
- if (typeof raw.workerModel === "string") defaults.workerModel = normalizeModelValue(raw.workerModel);
369
- if (typeof raw.reviewerModel === "string") defaults.reviewerModel = normalizeModelValue(raw.reviewerModel);
375
+ if (typeof raw.workerModel === "string")
376
+ defaults.workerModel = normalizeModelValue(raw.workerModel);
377
+ if (typeof raw.reviewerModel === "string")
378
+ defaults.reviewerModel = normalizeModelValue(raw.reviewerModel);
370
379
  if (typeof raw.mergeModel === "string") defaults.mergeModel = normalizeModelValue(raw.mergeModel);
371
- if (raw.workerThinking !== undefined) defaults.workerThinking = normalizeThinkingMode(raw.workerThinking);
372
- if (raw.reviewerThinking !== undefined) defaults.reviewerThinking = normalizeThinkingMode(raw.reviewerThinking);
373
- if (raw.mergeThinking !== undefined) defaults.mergeThinking = normalizeThinkingMode(raw.mergeThinking);
380
+ if (raw.workerThinking !== undefined)
381
+ defaults.workerThinking = normalizeThinkingMode(raw.workerThinking);
382
+ if (raw.reviewerThinking !== undefined)
383
+ defaults.reviewerThinking = normalizeThinkingMode(raw.reviewerThinking);
384
+ if (raw.mergeThinking !== undefined)
385
+ defaults.mergeThinking = normalizeThinkingMode(raw.mergeThinking);
374
386
 
375
387
  return defaults;
376
388
  }
@@ -380,7 +392,13 @@ function resolveGlobalPreferencesPathForCli() {
380
392
  if (agentDir) {
381
393
  return path.join(agentDir, GLOBAL_PREFERENCES_SUBDIR, GLOBAL_PREFERENCES_FILENAME);
382
394
  }
383
- return path.join(homedir(), ".pi", "agent", GLOBAL_PREFERENCES_SUBDIR, GLOBAL_PREFERENCES_FILENAME);
395
+ return path.join(
396
+ homedir(),
397
+ ".pi",
398
+ "agent",
399
+ GLOBAL_PREFERENCES_SUBDIR,
400
+ GLOBAL_PREFERENCES_FILENAME,
401
+ );
384
402
  }
385
403
 
386
404
  function writeGlobalPreferencesForCli(rawPrefs, prefsPath = resolveGlobalPreferencesPathForCli()) {
@@ -452,7 +470,11 @@ function readGlobalPreferencesForCli() {
452
470
  function loadInitAgentDefaultsFromPreferences() {
453
471
  const { prefsPath, raw, wasBootstrapped } = readGlobalPreferencesForCli();
454
472
  const defaults = sanitizeInitAgentConfig(raw.initAgentDefaults);
455
- const hasDefaults = !!(raw.initAgentDefaults && typeof raw.initAgentDefaults === "object" && !Array.isArray(raw.initAgentDefaults));
473
+ const hasDefaults = !!(
474
+ raw.initAgentDefaults &&
475
+ typeof raw.initAgentDefaults === "object" &&
476
+ !Array.isArray(raw.initAgentDefaults)
477
+ );
456
478
  return { defaults, hasDefaults, prefsPath, wasBootstrapped };
457
479
  }
458
480
 
@@ -478,10 +500,13 @@ function findModelInDiscovery(models, modelRef) {
478
500
  if (!ref) return null;
479
501
  const provider = ref.provider.toLowerCase();
480
502
  const id = ref.id.toLowerCase();
481
- return models.find((model) =>
482
- String(model?.provider ?? "").toLowerCase() === provider
483
- && String(model?.id ?? "").toLowerCase() === id,
484
- ) || null;
503
+ return (
504
+ models.find(
505
+ (model) =>
506
+ String(model?.provider ?? "").toLowerCase() === provider &&
507
+ String(model?.id ?? "").toLowerCase() === id,
508
+ ) || null
509
+ );
485
510
  }
486
511
 
487
512
  function allValuesEqual(values) {
@@ -507,16 +532,24 @@ const INIT_AGENT_ROLES = [
507
532
  { key: "merge", label: "Merger", modelKey: "mergeModel", thinkingKey: "mergeThinking" },
508
533
  ];
509
534
 
510
- async function promptMenuChoice({ title, question, options, defaultIndex = 0, askImpl = ask, logImpl = console.log }) {
535
+ async function promptMenuChoice({
536
+ title,
537
+ question,
538
+ options,
539
+ defaultIndex = 0,
540
+ askImpl = ask,
541
+ logImpl = console.log,
542
+ }) {
511
543
  while (true) {
512
544
  if (title) logImpl(`\n ${title}`);
513
545
  for (let i = 0; i < options.length; i++) {
514
546
  logImpl(` ${i + 1}. ${options[i].label}`);
515
547
  }
516
548
 
517
- const resolvedDefault = Number.isInteger(defaultIndex) && defaultIndex >= 0 && defaultIndex < options.length
518
- ? defaultIndex
519
- : 0;
549
+ const resolvedDefault =
550
+ Number.isInteger(defaultIndex) && defaultIndex >= 0 && defaultIndex < options.length
551
+ ? defaultIndex
552
+ : 0;
520
553
  const answer = String(await askImpl(question, String(resolvedDefault + 1))).trim();
521
554
  const asNum = Number.parseInt(answer, 10);
522
555
  if (!Number.isNaN(asNum) && asNum >= 1 && asNum <= options.length) {
@@ -536,13 +569,14 @@ async function promptMenuChoice({ title, question, options, defaultIndex = 0, as
536
569
  }
537
570
  }
538
571
 
539
- async function promptModelForRole(roleLabel, models, {
540
- askImpl = ask,
541
- logImpl = console.log,
542
- currentModel = "",
543
- preferDifferentProviderFrom = "",
544
- } = {}) {
545
- const providers = [...new Set(models.map((model) => model.provider))].sort((a, b) => a.localeCompare(b));
572
+ async function promptModelForRole(
573
+ roleLabel,
574
+ models,
575
+ { askImpl = ask, logImpl = console.log, currentModel = "", preferDifferentProviderFrom = "" } = {},
576
+ ) {
577
+ const providers = [...new Set(models.map((model) => model.provider))].sort((a, b) =>
578
+ a.localeCompare(b),
579
+ );
546
580
  const currentRef = splitModelReference(currentModel);
547
581
 
548
582
  while (true) {
@@ -559,13 +593,17 @@ async function promptModelForRole(roleLabel, models, {
559
593
  ];
560
594
  const providerDefaultIndex = (() => {
561
595
  if (currentRef) {
562
- return Math.max(0, providerOptions.findIndex((option) => option.value === currentRef.provider));
596
+ return Math.max(
597
+ 0,
598
+ providerOptions.findIndex((option) => option.value === currentRef.provider),
599
+ );
563
600
  }
564
601
  if (preferDifferentProviderFrom) {
565
- const preferred = providerOptions.findIndex((option) =>
566
- typeof option.value === "string"
567
- && option.value !== "inherit"
568
- && option.value !== preferDifferentProviderFrom
602
+ const preferred = providerOptions.findIndex(
603
+ (option) =>
604
+ typeof option.value === "string" &&
605
+ option.value !== "inherit" &&
606
+ option.value !== preferDifferentProviderFrom,
569
607
  );
570
608
  if (preferred >= 0) return preferred;
571
609
  }
@@ -617,13 +655,16 @@ async function promptModelForRole(roleLabel, models, {
617
655
  }
618
656
  }
619
657
 
620
- async function promptThinkingForRole(roleLabel, {
621
- askImpl = ask,
622
- logImpl = console.log,
623
- currentThinking = "",
624
- currentModel = "",
625
- availableModels = [],
626
- } = {}) {
658
+ async function promptThinkingForRole(
659
+ roleLabel,
660
+ {
661
+ askImpl = ask,
662
+ logImpl = console.log,
663
+ currentThinking = "",
664
+ currentModel = "",
665
+ availableModels = [],
666
+ } = {},
667
+ ) {
627
668
  const thinkingOptions = [
628
669
  { value: "", label: "inherit (use current session thinking)", aliases: ["inherit"] },
629
670
  { value: "off", label: "off" },
@@ -636,13 +677,20 @@ async function promptThinkingForRole(roleLabel, {
636
677
 
637
678
  const selectedModel = findModelInDiscovery(availableModels, currentModel);
638
679
  if (selectedModel?.supportsThinking === false) {
639
- logImpl(` ${INFO} ${roleLabel} model does not advertise thinking support (pi says thinking=no).`);
640
- logImpl(` ${c.dim}You can still set a thinking level; unsupported models ignore it at runtime.${c.reset}`);
680
+ logImpl(
681
+ ` ${INFO} ${roleLabel} model does not advertise thinking support (pi says thinking=no).`,
682
+ );
683
+ logImpl(
684
+ ` ${c.dim}You can still set a thinking level; unsupported models ignore it at runtime.${c.reset}`,
685
+ );
641
686
  }
642
687
 
643
688
  const normalized = normalizeThinkingMode(currentThinking);
644
689
  const preferredDefault = normalized || "high";
645
- const defaultIndex = Math.max(0, thinkingOptions.findIndex((option) => option.value === preferredDefault));
690
+ const defaultIndex = Math.max(
691
+ 0,
692
+ thinkingOptions.findIndex((option) => option.value === preferredDefault),
693
+ );
646
694
 
647
695
  return promptMenuChoice({
648
696
  title: `${roleLabel}: choose thinking mode`,
@@ -700,18 +748,28 @@ export async function collectInitAgentConfig({
700
748
  const shouldPersistFromInit = shouldGuideCrossProvider;
701
749
 
702
750
  logImpl(`\n${c.bold}Agent model setup${c.reset}`);
703
- logImpl(` ${c.dim}Choose models for worker/reviewer/merger (inherit is always option #1).${c.reset}`);
751
+ logImpl(
752
+ ` ${c.dim}Choose models for worker/reviewer/merger (inherit is always option #1).${c.reset}`,
753
+ );
704
754
 
705
755
  if (canGuideCrossProvider) {
706
- logImpl(` ${INFO} ${c.bold}First-run recommendation:${c.reset} choose reviewer/merger on a different provider than worker/session.`);
707
- logImpl(` ${c.dim}Cross-provider review catches blind spots that same-model review can miss.${c.reset}`);
756
+ logImpl(
757
+ ` ${INFO} ${c.bold}First-run recommendation:${c.reset} choose reviewer/merger on a different provider than worker/session.`,
758
+ );
759
+ logImpl(
760
+ ` ${c.dim}Cross-provider review catches blind spots that same-model review can miss.${c.reset}`,
761
+ );
708
762
  } else if (shouldGuideCrossProvider) {
709
763
  logImpl(` ${INFO} Cross-provider guidance skipped: only one provider is currently available.`);
710
- logImpl(` ${c.dim}Add another provider later to enable cross-provider reviewer/merger defaults.${c.reset}`);
764
+ logImpl(
765
+ ` ${c.dim}Add another provider later to enable cross-provider reviewer/merger defaults.${c.reset}`,
766
+ );
711
767
  }
712
768
 
713
769
  const modelDefaults = INIT_AGENT_ROLES.map((role) => initAgentConfig[role.modelKey] || "");
714
- const thinkingDefaults = INIT_AGENT_ROLES.map((role) => normalizeThinkingMode(initAgentConfig[role.thinkingKey]));
770
+ const thinkingDefaults = INIT_AGENT_ROLES.map((role) =>
771
+ normalizeThinkingMode(initAgentConfig[role.thinkingKey]),
772
+ );
715
773
  const sameModelDefaults = allValuesEqual(modelDefaults);
716
774
  const sameThinkingDefaults = allValuesEqual(thinkingDefaults);
717
775
  let useSameModel = false;
@@ -752,9 +810,8 @@ export async function collectInitAgentConfig({
752
810
 
753
811
  let workerProviderHint = splitModelReference(initAgentConfig.workerModel)?.provider || "";
754
812
  for (const role of INIT_AGENT_ROLES) {
755
- const preferDifferentProviderFrom = canGuideCrossProvider && role.key !== "worker"
756
- ? workerProviderHint
757
- : "";
813
+ const preferDifferentProviderFrom =
814
+ canGuideCrossProvider && role.key !== "worker" ? workerProviderHint : "";
758
815
  initAgentConfig[role.modelKey] = await promptModelForRole(role.label, discovery.models, {
759
816
  askImpl,
760
817
  logImpl,
@@ -762,7 +819,8 @@ export async function collectInitAgentConfig({
762
819
  preferDifferentProviderFrom,
763
820
  });
764
821
  if (role.key === "worker") {
765
- workerProviderHint = splitModelReference(initAgentConfig[role.modelKey])?.provider || workerProviderHint;
822
+ workerProviderHint =
823
+ splitModelReference(initAgentConfig[role.modelKey])?.provider || workerProviderHint;
766
824
  }
767
825
  initAgentConfig[role.thinkingKey] = await promptThinkingForRole(role.label, {
768
826
  askImpl,
@@ -824,9 +882,7 @@ export function generateProjectConfig(vars, _initAgentConfig = null) {
824
882
 
825
883
  function generateWorkspaceYaml(repoNames, defaultRepo, tasksRoot) {
826
884
  const normalizedTasksRoot = fwdSlash(tasksRoot);
827
- const reposBlock = repoNames
828
- .map((name) => ` ${name}:\n path: "${name}"`)
829
- .join("\n");
885
+ const reposBlock = repoNames.map((name) => ` ${name}:\n path: "${name}"`).join("\n");
830
886
  return `repos:\n${reposBlock}\nrouting:\n tasks_root: "${normalizedTasksRoot}"\n default_repo: "${defaultRepo}"\n task_packet_repo: "${defaultRepo}"\n`;
831
887
  }
832
888
 
@@ -876,7 +932,9 @@ async function autoCommitTaskFiles(projectRoot, tasksRoot) {
876
932
  } catch (err) {
877
933
  // Git commit failed — warn but don't block init
878
934
  console.log(`\n ${WARN} Could not auto-commit task files to git.`);
879
- console.log(` ${c.dim}Run manually before using /orch: git add ${tasksRoot} && git commit -m "add taskplane tasks"${c.reset}`);
935
+ console.log(
936
+ ` ${c.dim}Run manually before using /orch: git add ${tasksRoot} && git commit -m "add taskplane tasks"${c.reset}`,
937
+ );
880
938
  }
881
939
  }
882
940
 
@@ -899,7 +957,9 @@ function discoverTaskAreaMetadata(projectRoot, configRoot = projectRoot, configP
899
957
  }
900
958
  return { paths: [...paths], contexts: [...contexts], areaRepoIds };
901
959
  }
902
- } catch { /* fall through to YAML */ }
960
+ } catch {
961
+ /* fall through to YAML */
962
+ }
903
963
  }
904
964
 
905
965
  const runnerPath = path.join(configRoot, configPrefix, "task-runner.yaml");
@@ -978,7 +1038,8 @@ function pruneEmptyDir(dirPath) {
978
1038
  function listExampleTaskTemplates() {
979
1039
  const tasksTemplatesDir = path.join(TEMPLATES_DIR, "tasks");
980
1040
  try {
981
- return fs.readdirSync(tasksTemplatesDir, { withFileTypes: true })
1041
+ return fs
1042
+ .readdirSync(tasksTemplatesDir, { withFileTypes: true })
982
1043
  .filter((entry) => entry.isDirectory() && /^EXAMPLE-\d+/i.test(entry.name))
983
1044
  .map((entry) => entry.name)
984
1045
  .sort();
@@ -997,7 +1058,12 @@ function resolveProjectConfigJsonPath(projectRoot) {
997
1058
  try {
998
1059
  const pointer = JSON.parse(fs.readFileSync(pointerPath, "utf-8"));
999
1060
  if (pointer?.config_repo && pointer?.config_path) {
1000
- const pointedPath = path.resolve(projectRoot, pointer.config_repo, pointer.config_path, "taskplane-config.json");
1061
+ const pointedPath = path.resolve(
1062
+ projectRoot,
1063
+ pointer.config_repo,
1064
+ pointer.config_path,
1065
+ "taskplane-config.json",
1066
+ );
1001
1067
  if (fs.existsSync(pointedPath)) return pointedPath;
1002
1068
  }
1003
1069
  } catch {
@@ -1024,7 +1090,9 @@ function cmdConfig(args) {
1024
1090
  console.log(`\n${c.bold}Taskplane Config${c.reset}\n`);
1025
1091
  console.log(` ${c.cyan}taskplane config --save-as-defaults${c.reset}`);
1026
1092
  console.log(` Save worker/reviewer/merger model + thinking settings from this project`);
1027
- console.log(` to ${c.cyan}${resolveGlobalPreferencesPathForCli()}${c.reset} for future ${c.cyan}taskplane init${c.reset} runs.\n`);
1093
+ console.log(
1094
+ ` to ${c.cyan}${resolveGlobalPreferencesPathForCli()}${c.reset} for future ${c.cyan}taskplane init${c.reset} runs.\n`,
1095
+ );
1028
1096
  return;
1029
1097
  }
1030
1098
 
@@ -1047,16 +1115,23 @@ function cmdConfig(args) {
1047
1115
  console.log(`\n${OK} ${c.bold}Saved init defaults.${c.reset}`);
1048
1116
  console.log(` Source: ${c.cyan}${configPath}${c.reset}`);
1049
1117
  console.log(` Target: ${c.cyan}${prefsPath}${c.reset}`);
1050
- console.log(` worker: ${saved.workerModel || "inherit"} (${saved.workerThinking || "inherit"})`);
1051
- console.log(` reviewer: ${saved.reviewerModel || "inherit"} (${saved.reviewerThinking || "inherit"})`);
1052
- console.log(` merger: ${saved.mergeModel || "inherit"} (${saved.mergeThinking || "inherit"})\n`);
1118
+ console.log(
1119
+ ` worker: ${saved.workerModel || "inherit"} (${saved.workerThinking || "inherit"})`,
1120
+ );
1121
+ console.log(
1122
+ ` reviewer: ${saved.reviewerModel || "inherit"} (${saved.reviewerThinking || "inherit"})`,
1123
+ );
1124
+ console.log(
1125
+ ` merger: ${saved.mergeModel || "inherit"} (${saved.mergeThinking || "inherit"})\n`,
1126
+ );
1053
1127
  }
1054
1128
 
1055
1129
  async function cmdUninstall(args) {
1056
1130
  const projectRoot = process.cwd();
1057
1131
  const dryRun = args.includes("--dry-run");
1058
1132
  const yes = args.includes("--yes") || args.includes("-y");
1059
- const removePackage = args.includes("--package") || args.includes("--all") || args.includes("--package-only");
1133
+ const removePackage =
1134
+ args.includes("--package") || args.includes("--all") || args.includes("--package-only");
1060
1135
  const packageOnly = args.includes("--package-only");
1061
1136
  const removeProject = !packageOnly;
1062
1137
  const removeTasks = removeProject && (args.includes("--remove-tasks") || args.includes("--all"));
@@ -1083,22 +1158,18 @@ async function cmdUninstall(args) {
1083
1158
  ".pi/orch-abort-signal",
1084
1159
  ];
1085
1160
 
1086
- const sidecarPrefixes = [
1087
- "lane-state-",
1088
- "worker-conversation-",
1089
- "merge-result-",
1090
- "merge-request-",
1091
- ];
1161
+ const sidecarPrefixes = ["lane-state-", "worker-conversation-", "merge-result-", "merge-request-"];
1092
1162
 
1093
1163
  const filesToDelete = managedFiles
1094
- .map(rel => ({ rel, abs: path.join(projectRoot, rel) }))
1164
+ .map((rel) => ({ rel, abs: path.join(projectRoot, rel) }))
1095
1165
  .filter(({ abs }) => fs.existsSync(abs));
1096
1166
 
1097
1167
  const piDir = path.join(projectRoot, ".pi");
1098
1168
  const sidecarsToDelete = fs.existsSync(piDir)
1099
- ? fs.readdirSync(piDir)
1100
- .filter(name => sidecarPrefixes.some(prefix => name.startsWith(prefix)))
1101
- .map(name => ({ rel: path.join(".pi", name), abs: path.join(piDir, name) }))
1169
+ ? fs
1170
+ .readdirSync(piDir)
1171
+ .filter((name) => sidecarPrefixes.some((prefix) => name.startsWith(prefix)))
1172
+ .map((name) => ({ rel: path.join(".pi", name), abs: path.join(piDir, name) }))
1102
1173
  : [];
1103
1174
 
1104
1175
  let taskDirsToDelete = [];
@@ -1106,27 +1177,34 @@ async function cmdUninstall(args) {
1106
1177
  const areaPaths = discoverTaskAreaPaths(projectRoot);
1107
1178
  const rootPrefix = path.resolve(projectRoot) + path.sep;
1108
1179
  taskDirsToDelete = areaPaths
1109
- .map(rel => ({ rel, abs: path.resolve(projectRoot, rel) }))
1180
+ .map((rel) => ({ rel, abs: path.resolve(projectRoot, rel) }))
1110
1181
  .filter(({ abs }) => abs.startsWith(rootPrefix) && fs.existsSync(abs));
1111
1182
  }
1112
1183
 
1113
1184
  const inferredInstallType = inferTaskplaneInstallScope();
1114
1185
  const packageScope = local ? "local" : global ? "global" : inferredInstallType;
1115
- const piRemoveCmd = packageScope === "local"
1116
- ? "pi remove -l npm:taskplane"
1117
- : "pi remove npm:taskplane";
1186
+ const piRemoveCmd =
1187
+ packageScope === "local" ? "pi remove -l npm:taskplane" : "pi remove npm:taskplane";
1118
1188
 
1119
1189
  if (!removeProject && !removePackage) {
1120
1190
  console.log(` ${WARN} Nothing to do. Use one of:`);
1121
- console.log(` ${c.cyan}taskplane uninstall${c.reset} # remove project-scaffolded files`);
1122
- console.log(` ${c.cyan}taskplane uninstall --package${c.reset} # remove installed package via pi`);
1191
+ console.log(
1192
+ ` ${c.cyan}taskplane uninstall${c.reset} # remove project-scaffolded files`,
1193
+ );
1194
+ console.log(
1195
+ ` ${c.cyan}taskplane uninstall --package${c.reset} # remove installed package via pi`,
1196
+ );
1123
1197
  console.log();
1124
1198
  return;
1125
1199
  }
1126
1200
 
1127
1201
  if (removeProject) {
1128
1202
  console.log(`${c.bold}Project cleanup:${c.reset}`);
1129
- if (filesToDelete.length === 0 && sidecarsToDelete.length === 0 && taskDirsToDelete.length === 0) {
1203
+ if (
1204
+ filesToDelete.length === 0 &&
1205
+ sidecarsToDelete.length === 0 &&
1206
+ taskDirsToDelete.length === 0
1207
+ ) {
1130
1208
  console.log(` ${c.dim}No Taskplane-managed project files found.${c.reset}`);
1131
1209
  }
1132
1210
  for (const f of filesToDelete) console.log(` - remove ${f.rel}`);
@@ -1136,7 +1214,9 @@ async function cmdUninstall(args) {
1136
1214
  console.log(` ${c.dim}No task area directories found in config.${c.reset}`);
1137
1215
  }
1138
1216
  if (!removeTasks) {
1139
- console.log(` ${c.dim}Task directories are preserved by default (use --remove-tasks to delete them).${c.reset}`);
1217
+ console.log(
1218
+ ` ${c.dim}Task directories are preserved by default (use --remove-tasks to delete them).${c.reset}`,
1219
+ );
1140
1220
  }
1141
1221
  console.log();
1142
1222
  }
@@ -1144,7 +1224,9 @@ async function cmdUninstall(args) {
1144
1224
  if (removePackage) {
1145
1225
  console.log(`${c.bold}Package cleanup:${c.reset}`);
1146
1226
  console.log(` - run ${piRemoveCmd}`);
1147
- console.log(` ${c.dim}(removes extensions, skills, and dashboard files from this install scope)${c.reset}`);
1227
+ console.log(
1228
+ ` ${c.dim}(removes extensions, skills, and dashboard files from this install scope)${c.reset}`,
1229
+ );
1148
1230
  console.log();
1149
1231
  }
1150
1232
 
@@ -1160,7 +1242,10 @@ async function cmdUninstall(args) {
1160
1242
  return;
1161
1243
  }
1162
1244
  if (removeTasks) {
1163
- const taskConfirm = await confirm("This will delete task area directories recursively. Continue?", false);
1245
+ const taskConfirm = await confirm(
1246
+ "This will delete task area directories recursively. Continue?",
1247
+ false,
1248
+ );
1164
1249
  if (!taskConfirm) {
1165
1250
  console.log(" Aborted.");
1166
1251
  return;
@@ -1246,7 +1331,7 @@ function ensureGitignoreEntries(projectRoot, { dryRun = false, prefix = "" } = {
1246
1331
  const gitignorePath = path.join(projectRoot, ".gitignore");
1247
1332
  const fileExists = fs.existsSync(gitignorePath);
1248
1333
  const existingContent = fileExists ? fs.readFileSync(gitignorePath, "utf-8") : "";
1249
- const existingLines = new Set(existingContent.split(/\r?\n/).map(l => l.trim()));
1334
+ const existingLines = new Set(existingContent.split(/\r?\n/).map((l) => l.trim()));
1250
1335
 
1251
1336
  const allEntries = [...TASKPLANE_GITIGNORE_ENTRIES, ...TASKPLANE_GITIGNORE_NPM_ENTRIES];
1252
1337
  const added = [];
@@ -1267,15 +1352,13 @@ function ensureGitignoreEntries(projectRoot, { dryRun = false, prefix = "" } = {
1267
1352
 
1268
1353
  if (!dryRun) {
1269
1354
  // Build the block of new entries with headers
1270
- const runtimeAdded = added.filter(e => !e.endsWith("npm/"));
1271
- const npmAdded = added.filter(e => e.endsWith("npm/"));
1355
+ const runtimeAdded = added.filter((e) => !e.endsWith("npm/"));
1356
+ const npmAdded = added.filter((e) => e.endsWith("npm/"));
1272
1357
  const newLines = [];
1273
1358
 
1274
1359
  if (runtimeAdded.length > 0) {
1275
1360
  // Only add header if it's not already present
1276
- const headerToCheck = prefix
1277
- ? TASKPLANE_GITIGNORE_HEADER
1278
- : TASKPLANE_GITIGNORE_HEADER;
1361
+ const headerToCheck = prefix ? TASKPLANE_GITIGNORE_HEADER : TASKPLANE_GITIGNORE_HEADER;
1279
1362
  if (!existingLines.has(headerToCheck)) {
1280
1363
  newLines.push(TASKPLANE_GITIGNORE_HEADER);
1281
1364
  }
@@ -1321,16 +1404,17 @@ function ensureGitignoreEntries(projectRoot, { dryRun = false, prefix = "" } = {
1321
1404
  * @param {boolean} options.interactive - If false, skip prompt and don't untrack
1322
1405
  * @param {string} options.prefix - Path prefix for workspace-scoped scanning (e.g., ".taskplane/")
1323
1406
  */
1324
- async function detectAndOfferUntrackArtifacts(projectRoot, { dryRun = false, interactive = true, prefix = "" } = {}) {
1407
+ async function detectAndOfferUntrackArtifacts(
1408
+ projectRoot,
1409
+ { dryRun = false, interactive = true, prefix = "" } = {},
1410
+ ) {
1325
1411
  // Only run in a git repo
1326
1412
  if (!isInsideGitRepo(projectRoot)) return { found: [], untracked: false };
1327
1413
 
1328
1414
  // Get list of tracked files under the relevant directories
1329
1415
  // For workspace mode (prefix=".taskplane/"), scan .taskplane/.pi/ and .taskplane/.worktrees/
1330
1416
  // For repo mode (no prefix), scan .pi/ and .worktrees/
1331
- const scanDirs = prefix
1332
- ? [`${prefix}.pi/`, `${prefix}.worktrees/`]
1333
- : [".pi/", ".worktrees/"];
1417
+ const scanDirs = prefix ? [`${prefix}.pi/`, `${prefix}.worktrees/`] : [".pi/", ".worktrees/"];
1334
1418
 
1335
1419
  let trackedFiles;
1336
1420
  try {
@@ -1338,7 +1422,9 @@ async function detectAndOfferUntrackArtifacts(projectRoot, { dryRun = false, int
1338
1422
  cwd: projectRoot,
1339
1423
  stdio: ["pipe", "pipe", "pipe"],
1340
1424
  timeout: 10000,
1341
- }).toString().trim();
1425
+ })
1426
+ .toString()
1427
+ .trim();
1342
1428
  trackedFiles = raw ? raw.split(/\r?\n/) : [];
1343
1429
  } catch {
1344
1430
  return { found: [], untracked: false };
@@ -1348,13 +1434,13 @@ async function detectAndOfferUntrackArtifacts(projectRoot, { dryRun = false, int
1348
1434
 
1349
1435
  // Build regex patterns for matching (with prefix if workspace-scoped)
1350
1436
  const prefixedPatterns = prefix
1351
- ? ALL_GITIGNORE_PATTERNS.map(p => `${prefix}${p}`)
1437
+ ? ALL_GITIGNORE_PATTERNS.map((p) => `${prefix}${p}`)
1352
1438
  : ALL_GITIGNORE_PATTERNS;
1353
- const patterns = prefixedPatterns.map(p => patternToRegex(p));
1439
+ const patterns = prefixedPatterns.map((p) => patternToRegex(p));
1354
1440
 
1355
1441
  // Find tracked files that match runtime artifact patterns
1356
- const matchedFiles = trackedFiles.filter(file => {
1357
- return patterns.some(regex => regex.test(file));
1442
+ const matchedFiles = trackedFiles.filter((file) => {
1443
+ return patterns.some((regex) => regex.test(file));
1358
1444
  });
1359
1445
 
1360
1446
  if (matchedFiles.length === 0) return { found: [], untracked: false };
@@ -1434,7 +1520,9 @@ function isGitRepoRoot(dir) {
1434
1520
  cwd: dir,
1435
1521
  stdio: ["pipe", "pipe", "pipe"],
1436
1522
  timeout: 5000,
1437
- }).toString().trim();
1523
+ })
1524
+ .toString()
1525
+ .trim();
1438
1526
  // Normalize paths for comparison (handles Windows path separators
1439
1527
  // and 8.3 short name mismatches on Windows)
1440
1528
  const normalizedToplevel = path.resolve(toplevel);
@@ -1442,7 +1530,9 @@ function isGitRepoRoot(dir) {
1442
1530
  // On Windows, fs.realpathSync.native resolves 8.3 short names to
1443
1531
  // long names, matching what git returns. Without this, paths like
1444
1532
  // C:\Users\HENRYL~1\... won't match C:\Users\HenryLach\...
1445
- try { normalizedDir = fs.realpathSync.native(normalizedDir); } catch {}
1533
+ try {
1534
+ normalizedDir = fs.realpathSync.native(normalizedDir);
1535
+ } catch {}
1446
1536
  return normalizedToplevel === normalizedDir;
1447
1537
  } catch {
1448
1538
  return false;
@@ -1548,9 +1638,7 @@ function detectInitMode(dir) {
1548
1638
  mode: "workspace",
1549
1639
  subRepos,
1550
1640
  alreadyInitialized: existingConfigRepo !== null,
1551
- existingConfigPath: existingConfigRepo
1552
- ? path.join(dir, existingConfigRepo, ".taskplane")
1553
- : null,
1641
+ existingConfigPath: existingConfigRepo ? path.join(dir, existingConfigRepo, ".taskplane") : null,
1554
1642
  };
1555
1643
  }
1556
1644
 
@@ -1593,7 +1681,11 @@ async function cmdInit(args) {
1593
1681
  if (path.isAbsolute(tasksRootRaw)) {
1594
1682
  die("--tasks-root must be relative to the project root (absolute paths are not allowed).");
1595
1683
  }
1596
- tasksRootOverride = tasksRootRaw.trim().replace(/\\/g, "/").replace(/^\.\/+/, "").replace(/\/+$/, "");
1684
+ tasksRootOverride = tasksRootRaw
1685
+ .trim()
1686
+ .replace(/\\/g, "/")
1687
+ .replace(/^\.\/+/, "")
1688
+ .replace(/\/+$/, "");
1597
1689
  if (!tasksRootOverride || tasksRootOverride === ".") {
1598
1690
  die("--tasks-root must not be empty.");
1599
1691
  }
@@ -1607,7 +1699,9 @@ async function cmdInit(args) {
1607
1699
  console.log(`\n${c.bold}Taskplane Init${c.reset}\n`);
1608
1700
 
1609
1701
  if (tasksRootOverride && !noExamplesFlag && !includeExamples) {
1610
- console.log(` ${INFO} Using custom --tasks-root (${tasksRootOverride}); skipping example tasks by default.`);
1702
+ console.log(
1703
+ ` ${INFO} Using custom --tasks-root (${tasksRootOverride}); skipping example tasks by default.`,
1704
+ );
1611
1705
  console.log(` Use --include-examples to scaffold examples into that directory.\n`);
1612
1706
  }
1613
1707
 
@@ -1619,8 +1713,8 @@ async function cmdInit(args) {
1619
1713
  if (detection.mode === "error") {
1620
1714
  die(
1621
1715
  "Not a git repo and no git repos found in subdirectories.\n" +
1622
- " Run from inside a git repository, or from a workspace root\n" +
1623
- " that contains git repositories as subdirectories."
1716
+ " Run from inside a git repository, or from a workspace root\n" +
1717
+ " that contains git repositories as subdirectories.",
1624
1718
  );
1625
1719
  }
1626
1720
 
@@ -1631,14 +1725,16 @@ async function cmdInit(args) {
1631
1725
  // Non-interactive: default to repo mode (safe default, no prompt)
1632
1726
  resolvedMode = "repo";
1633
1727
  console.log(` ${INFO} Ambiguous layout detected (git repo with git repo subdirectories).`);
1634
- console.log(` Defaulting to ${c.cyan}repo mode${c.reset} (use interactive mode for workspace).\n`);
1728
+ console.log(
1729
+ ` Defaulting to ${c.cyan}repo mode${c.reset} (use interactive mode for workspace).\n`,
1730
+ );
1635
1731
  } else {
1636
1732
  // Interactive: prompt the user
1637
1733
  console.log(` ${WARN} This directory is a git repo AND contains git repos as subdirectories.`);
1638
1734
  console.log(` Subdirectory repos found: ${detection.subRepos.join(", ")}\n`);
1639
1735
  const modeChoice = await ask(
1640
1736
  "Mode: (r)epo — treat as single monorepo, or (w)orkspace — treat subdirs as independent repos",
1641
- "r"
1737
+ "r",
1642
1738
  );
1643
1739
  resolvedMode = modeChoice.toLowerCase().startsWith("w") ? "workspace" : "repo";
1644
1740
  console.log();
@@ -1659,7 +1755,9 @@ async function cmdInit(args) {
1659
1755
  // Scenario B: existing monorepo config — block reinit unless --force
1660
1756
  if (effectiveAlreadyInitialized && !force && resolvedMode === "repo") {
1661
1757
  console.log(` ${INFO} Project already initialized (config exists in .pi/).`);
1662
- console.log(` Run ${c.cyan}taskplane doctor${c.reset} to verify, or use ${c.cyan}--force${c.reset} to reinitialize.\n`);
1758
+ console.log(
1759
+ ` Run ${c.cyan}taskplane doctor${c.reset} to verify, or use ${c.cyan}--force${c.reset} to reinitialize.\n`,
1760
+ );
1663
1761
  return;
1664
1762
  }
1665
1763
 
@@ -1690,23 +1788,30 @@ async function cmdInit(args) {
1690
1788
  } catch {}
1691
1789
  return null;
1692
1790
  })();
1693
- const workspaceTasksRoot = (existingWorkspaceJson?.routing?.tasks_root
1694
- || existingRootYaml?.routing?.tasks_root
1695
- || "taskplane-tasks").replace(/\\/g, "/");
1696
- const workspaceDefaultRepo = existingWorkspaceJson?.routing?.default_repo
1697
- || existingRootYaml?.routing?.default_repo
1698
- || configRepo;
1791
+ const workspaceTasksRoot = (
1792
+ existingWorkspaceJson?.routing?.tasks_root ||
1793
+ existingRootYaml?.routing?.tasks_root ||
1794
+ "taskplane-tasks"
1795
+ ).replace(/\\/g, "/");
1796
+ const workspaceDefaultRepo =
1797
+ existingWorkspaceJson?.routing?.default_repo ||
1798
+ existingRootYaml?.routing?.default_repo ||
1799
+ configRepo;
1699
1800
  const workspaceRepoNames = Array.from(
1700
1801
  new Set([
1701
1802
  ...detection.subRepos,
1702
- ...((Array.isArray(existingWorkspaceJson?.repos) ? existingWorkspaceJson.repos : [])
1803
+ ...(Array.isArray(existingWorkspaceJson?.repos) ? existingWorkspaceJson.repos : [])
1703
1804
  .map((repo) => repo?.name)
1704
- .filter(Boolean)),
1805
+ .filter(Boolean),
1705
1806
  ]),
1706
1807
  ).sort();
1707
1808
 
1708
- console.log(` ${c.dim}Mode: workspace (${detection.subRepos.length} git repositories found)${c.reset}`);
1709
- console.log(` ${INFO} Found existing Taskplane config in ${c.cyan}${configRepo}/.taskplane/${c.reset}`);
1809
+ console.log(
1810
+ ` ${c.dim}Mode: workspace (${detection.subRepos.length} git repositories found)${c.reset}`,
1811
+ );
1812
+ console.log(
1813
+ ` ${INFO} Found existing Taskplane config in ${c.cyan}${configRepo}/.taskplane/${c.reset}`,
1814
+ );
1710
1815
  console.log(` Using existing configuration.\n`);
1711
1816
 
1712
1817
  // ── Pointer idempotency ─────────────────────────────────
@@ -1739,16 +1844,27 @@ async function cmdInit(args) {
1739
1844
  // Malformed pointer file — treat as invalid, will be overwritten
1740
1845
  console.log(` ${WARN} .pi/taskplane-pointer.json exists but is malformed — will overwrite.`);
1741
1846
  }
1742
- if (existingPointer && existingPointer.config_repo === configRepo && existingPointer.config_path === ".taskplane") {
1743
- console.log(` ${c.dim}skip${c.reset} .pi/taskplane-pointer.json (already points to ${configRepo}/.taskplane/)`);
1847
+ if (
1848
+ existingPointer &&
1849
+ existingPointer.config_repo === configRepo &&
1850
+ existingPointer.config_path === ".taskplane"
1851
+ ) {
1852
+ console.log(
1853
+ ` ${c.dim}skip${c.reset} .pi/taskplane-pointer.json (already points to ${configRepo}/.taskplane/)`,
1854
+ );
1744
1855
  console.log(`\n${OK} ${c.bold}Workspace already configured.${c.reset}`);
1745
1856
  console.log(` Run ${c.cyan}taskplane doctor${c.reset} to verify.\n`);
1746
1857
  return;
1747
1858
  }
1748
1859
  // Pointer exists but points elsewhere (or was malformed) — prompt to overwrite
1749
1860
  if (existingPointer && !isPreset) {
1750
- console.log(` ${WARN} .pi/taskplane-pointer.json already exists (points to ${existingPointer.config_repo}/.taskplane/).`);
1751
- const proceed = await confirm(" Update pointer to point to " + configRepo + "/.taskplane/?", true);
1861
+ console.log(
1862
+ ` ${WARN} .pi/taskplane-pointer.json already exists (points to ${existingPointer.config_repo}/.taskplane/).`,
1863
+ );
1864
+ const proceed = await confirm(
1865
+ " Update pointer to point to " + configRepo + "/.taskplane/?",
1866
+ true,
1867
+ );
1752
1868
  if (!proceed) {
1753
1869
  console.log(" Aborted.");
1754
1870
  return;
@@ -1762,11 +1878,9 @@ async function cmdInit(args) {
1762
1878
  config_repo: configRepo,
1763
1879
  config_path: ".taskplane",
1764
1880
  };
1765
- writeFile(
1766
- pointerPath,
1767
- JSON.stringify(pointer, null, 2) + "\n",
1768
- { label: ".pi/taskplane-pointer.json" }
1769
- );
1881
+ writeFile(pointerPath, JSON.stringify(pointer, null, 2) + "\n", {
1882
+ label: ".pi/taskplane-pointer.json",
1883
+ });
1770
1884
 
1771
1885
  writeFile(
1772
1886
  workspaceYamlPath,
@@ -1776,11 +1890,16 @@ async function cmdInit(args) {
1776
1890
 
1777
1891
  // ── Gitignore enforcement in config repo (Scenario D) ───
1778
1892
  // Ensure .gitignore exists even when reusing existing config
1779
- const gitignoreResult = ensureGitignoreEntries(configRepoRoot, { dryRun: false, prefix: ".taskplane/" });
1893
+ const gitignoreResult = ensureGitignoreEntries(configRepoRoot, {
1894
+ dryRun: false,
1895
+ prefix: ".taskplane/",
1896
+ });
1780
1897
  if (gitignoreResult.created) {
1781
1898
  console.log(` ${c.green}create${c.reset} ${configRepo}/.gitignore`);
1782
1899
  } else if (gitignoreResult.added.length > 0) {
1783
- console.log(` ${c.green}update${c.reset} ${configRepo}/.gitignore (${gitignoreResult.added.length} entries added)`);
1900
+ console.log(
1901
+ ` ${c.green}update${c.reset} ${configRepo}/.gitignore (${gitignoreResult.added.length} entries added)`,
1902
+ );
1784
1903
  }
1785
1904
 
1786
1905
  console.log(`\n${OK} ${c.bold}Workspace pointer created.${c.reset}\n`);
@@ -1788,8 +1907,12 @@ async function cmdInit(args) {
1788
1907
  console.log(` Pointer: ${c.cyan}.pi/taskplane-pointer.json${c.reset}`);
1789
1908
  console.log(` Workspace config: ${c.cyan}.pi/taskplane-workspace.yaml${c.reset}\n`);
1790
1909
  console.log(`${c.bold}Quick start:${c.reset}`);
1791
- console.log(` ${c.cyan}pi${c.reset} # start pi (taskplane auto-loads)`);
1792
- console.log(` ${c.cyan}taskplane doctor${c.reset} # verify setup`);
1910
+ console.log(
1911
+ ` ${c.cyan}pi${c.reset} # start pi (taskplane auto-loads)`,
1912
+ );
1913
+ console.log(
1914
+ ` ${c.cyan}taskplane doctor${c.reset} # verify setup`,
1915
+ );
1793
1916
  console.log();
1794
1917
  return;
1795
1918
  }
@@ -1798,7 +1921,9 @@ async function cmdInit(args) {
1798
1921
  if (resolvedMode === "repo") {
1799
1922
  console.log(` ${c.dim}Mode: repo (standard monorepo)${c.reset}`);
1800
1923
  } else if (resolvedMode === "workspace") {
1801
- console.log(` ${c.dim}Mode: workspace (${detection.subRepos.length} git repositories found)${c.reset}`);
1924
+ console.log(
1925
+ ` ${c.dim}Mode: workspace (${detection.subRepos.length} git repositories found)${c.reset}`,
1926
+ );
1802
1927
  }
1803
1928
  console.log();
1804
1929
 
@@ -1813,7 +1938,9 @@ async function cmdInit(args) {
1813
1938
  if (isPreset || dryRun) {
1814
1939
  // Non-interactive: pick first repo alphabetically as default
1815
1940
  configRepoName = detection.subRepos[0];
1816
- console.log(` ${INFO} Using ${c.cyan}${configRepoName}${c.reset} as config repo (first alphabetically).\n`);
1941
+ console.log(
1942
+ ` ${INFO} Using ${c.cyan}${configRepoName}${c.reset} as config repo (first alphabetically).\n`,
1943
+ );
1817
1944
  } else {
1818
1945
  // Interactive: prompt user to choose config repo
1819
1946
  console.log(` Which repo should hold Taskplane config?`);
@@ -1821,10 +1948,7 @@ async function cmdInit(args) {
1821
1948
  console.log(` ${c.dim}${i + 1}.${c.reset} ${detection.subRepos[i]}`);
1822
1949
  }
1823
1950
  console.log();
1824
- const configRepoAnswer = await ask(
1825
- "Config repo (name or number)",
1826
- detection.subRepos[0]
1827
- );
1951
+ const configRepoAnswer = await ask("Config repo (name or number)", detection.subRepos[0]);
1828
1952
  // Accept numeric index or repo name
1829
1953
  const asNum = parseInt(configRepoAnswer, 10);
1830
1954
  if (asNum >= 1 && asNum <= detection.subRepos.length) {
@@ -1873,7 +1997,14 @@ async function cmdInit(args) {
1873
1997
  // ── Dry-run: show what would be created ─────────────────────
1874
1998
  if (dryRun) {
1875
1999
  console.log(`\n${c.bold}Dry run — files that would be created:${c.reset}\n`);
1876
- printWorkspaceFileList(vars, noExamples, preset, exampleTemplateDirs, configRepoName, configRepoRoot);
2000
+ printWorkspaceFileList(
2001
+ vars,
2002
+ noExamples,
2003
+ preset,
2004
+ exampleTemplateDirs,
2005
+ configRepoName,
2006
+ configRepoRoot,
2007
+ );
1877
2008
  console.log(` ${c.green}create${c.reset} .pi/taskplane-pointer.json`);
1878
2009
  console.log(` ${c.green}create${c.reset} .pi/taskplane-workspace.yaml`);
1879
2010
  console.log();
@@ -1894,7 +2025,7 @@ async function cmdInit(args) {
1894
2025
  copyTemplate(
1895
2026
  path.join(TEMPLATES_DIR, "agents", "local", agent),
1896
2027
  path.join(taskplaneDir, "agents", agent),
1897
- { skipIfExists, label: `${configRepoName}/.taskplane/agents/${agent}` }
2028
+ { skipIfExists, label: `${configRepoName}/.taskplane/agents/${agent}` },
1898
2029
  );
1899
2030
  }
1900
2031
 
@@ -1903,7 +2034,7 @@ async function cmdInit(args) {
1903
2034
  writeFile(
1904
2035
  path.join(taskplaneDir, "taskplane-config.json"),
1905
2036
  JSON.stringify(projectConfig, null, 2) + "\n",
1906
- { skipIfExists, label: `${configRepoName}/.taskplane/taskplane-config.json` }
2037
+ { skipIfExists, label: `${configRepoName}/.taskplane/taskplane-config.json` },
1907
2038
  );
1908
2039
 
1909
2040
  // Version tracker (always overwrite)
@@ -1916,12 +2047,12 @@ async function cmdInit(args) {
1916
2047
  writeFile(
1917
2048
  path.join(taskplaneDir, "taskplane.json"),
1918
2049
  JSON.stringify(versionInfo, null, 2) + "\n",
1919
- { label: `${configRepoName}/.taskplane/taskplane.json` }
2050
+ { label: `${configRepoName}/.taskplane/taskplane.json` },
1920
2051
  );
1921
2052
 
1922
2053
  // Workspace definition (workspace.json)
1923
2054
  const workspaceConfig = {
1924
- repos: detection.subRepos.map(name => ({
2055
+ repos: detection.subRepos.map((name) => ({
1925
2056
  name,
1926
2057
  path: `../${name}`,
1927
2058
  default_branch: "main",
@@ -1935,7 +2066,7 @@ async function cmdInit(args) {
1935
2066
  writeFile(
1936
2067
  path.join(taskplaneDir, "workspace.json"),
1937
2068
  JSON.stringify(workspaceConfig, null, 2) + "\n",
1938
- { skipIfExists, label: `${configRepoName}/.taskplane/workspace.json` }
2069
+ { skipIfExists, label: `${configRepoName}/.taskplane/workspace.json` },
1939
2070
  );
1940
2071
 
1941
2072
  // CONTEXT.md — tasks area context
@@ -1946,11 +2077,10 @@ async function cmdInit(args) {
1946
2077
  : vars.tasks_root;
1947
2078
  const tasksDir = path.join(configRepoRoot, tasksRootInRepo);
1948
2079
  const contextSrc = fs.readFileSync(path.join(TEMPLATES_DIR, "tasks", "CONTEXT.md"), "utf-8");
1949
- writeFile(
1950
- path.join(tasksDir, "CONTEXT.md"),
1951
- interpolate(contextSrc, vars),
1952
- { skipIfExists, label: `${configRepoName}/${vars.tasks_root}/CONTEXT.md` }
1953
- );
2080
+ writeFile(path.join(tasksDir, "CONTEXT.md"), interpolate(contextSrc, vars), {
2081
+ skipIfExists,
2082
+ label: `${configRepoName}/${vars.tasks_root}/CONTEXT.md`,
2083
+ });
1954
2084
 
1955
2085
  // Example tasks
1956
2086
  if (!noExamples) {
@@ -1976,19 +2106,30 @@ async function cmdInit(args) {
1976
2106
  // Use .taskplane/ prefix so patterns apply within the config repo's
1977
2107
  // .taskplane/ directory (e.g., ".taskplane/.pi/batch-state.json")
1978
2108
  // Per spec: standard .pi/ patterns + .worktrees/ in config repo root
1979
- const gitignoreResult = ensureGitignoreEntries(configRepoRoot, { dryRun: false, prefix: ".taskplane/" });
2109
+ const gitignoreResult = ensureGitignoreEntries(configRepoRoot, {
2110
+ dryRun: false,
2111
+ prefix: ".taskplane/",
2112
+ });
1980
2113
 
1981
2114
  if (gitignoreResult.created) {
1982
2115
  console.log(` ${c.green}create${c.reset} ${configRepoName}/.gitignore`);
1983
2116
  } else if (gitignoreResult.added.length > 0) {
1984
- console.log(` ${c.green}update${c.reset} ${configRepoName}/.gitignore (${gitignoreResult.added.length} entries added)`);
2117
+ console.log(
2118
+ ` ${c.green}update${c.reset} ${configRepoName}/.gitignore (${gitignoreResult.added.length} entries added)`,
2119
+ );
1985
2120
  } else {
1986
- console.log(` ${c.dim}skip${c.reset} ${configRepoName}/.gitignore (all entries already present)`);
2121
+ console.log(
2122
+ ` ${c.dim}skip${c.reset} ${configRepoName}/.gitignore (all entries already present)`,
2123
+ );
1987
2124
  }
1988
2125
 
1989
2126
  // Check for tracked runtime artifacts in config repo (workspace-scoped)
1990
2127
  const wsIsInteractive = !isPreset && !dryRun;
1991
- await detectAndOfferUntrackArtifacts(configRepoRoot, { dryRun: false, interactive: wsIsInteractive, prefix: ".taskplane/" });
2128
+ await detectAndOfferUntrackArtifacts(configRepoRoot, {
2129
+ dryRun: false,
2130
+ interactive: wsIsInteractive,
2131
+ prefix: ".taskplane/",
2132
+ });
1992
2133
 
1993
2134
  // ── Pointer file in workspace root .pi/ ─────────────────────
1994
2135
  const pointer = {
@@ -1998,7 +2139,7 @@ async function cmdInit(args) {
1998
2139
  writeFile(
1999
2140
  path.join(projectRoot, ".pi", "taskplane-pointer.json"),
2000
2141
  JSON.stringify(pointer, null, 2) + "\n",
2001
- { label: ".pi/taskplane-pointer.json" }
2142
+ { label: ".pi/taskplane-pointer.json" },
2002
2143
  );
2003
2144
  writeFile(
2004
2145
  path.join(projectRoot, ".pi", "taskplane-workspace.yaml"),
@@ -2010,19 +2151,24 @@ async function cmdInit(args) {
2010
2151
  await autoCommitTaskFiles(configRepoRoot, vars.tasks_root);
2011
2152
  // Also stage and commit .taskplane/ directory and .gitignore
2012
2153
  try {
2013
- execSync('git add .taskplane/ .gitignore', { cwd: configRepoRoot, stdio: "pipe" });
2154
+ execSync("git add .taskplane/ .gitignore", { cwd: configRepoRoot, stdio: "pipe" });
2014
2155
  const status = execSync("git diff --cached --name-only", { cwd: configRepoRoot, stdio: "pipe" })
2015
- .toString().trim();
2156
+ .toString()
2157
+ .trim();
2016
2158
  if (status) {
2017
2159
  execSync('git commit -m "chore: initialize taskplane workspace config"', {
2018
2160
  cwd: configRepoRoot,
2019
2161
  stdio: "pipe",
2020
2162
  });
2021
- console.log(`\n ${c.green}git${c.reset} committed .taskplane/ and .gitignore to ${configRepoName}`);
2163
+ console.log(
2164
+ `\n ${c.green}git${c.reset} committed .taskplane/ and .gitignore to ${configRepoName}`,
2165
+ );
2022
2166
  }
2023
2167
  } catch (err) {
2024
2168
  console.log(`\n ${WARN} Could not auto-commit .taskplane/ to ${configRepoName}.`);
2025
- console.log(` ${c.dim}Run manually: cd ${configRepoName} && git add .taskplane/ .gitignore && git commit -m "add taskplane config"${c.reset}`);
2169
+ console.log(
2170
+ ` ${c.dim}Run manually: cd ${configRepoName} && git add .taskplane/ .gitignore && git commit -m "add taskplane config"${c.reset}`,
2171
+ );
2026
2172
  }
2027
2173
 
2028
2174
  // ── Post-init guidance ──────────────────────────────────────
@@ -2030,16 +2176,26 @@ async function cmdInit(args) {
2030
2176
  console.log(` Config repo: ${c.cyan}${configRepoName}/.taskplane/${c.reset}`);
2031
2177
  console.log(` Pointer: ${c.cyan}.pi/taskplane-pointer.json${c.reset}`);
2032
2178
  console.log(` Workspace: ${c.cyan}.pi/taskplane-workspace.yaml${c.reset}\n`);
2033
- console.log(` ${WARN} ${c.bold}Important:${c.reset} merge these changes to your default branch (e.g., ${c.cyan}develop${c.reset})`);
2179
+ console.log(
2180
+ ` ${WARN} ${c.bold}Important:${c.reset} merge these changes to your default branch (e.g., ${c.cyan}develop${c.reset})`,
2181
+ );
2034
2182
  console.log(` before other team members run ${c.cyan}taskplane init${c.reset}.\n`);
2035
2183
  console.log(` cd ${configRepoName}`);
2036
2184
  console.log(` git push && ${c.dim}[create PR / merge to default branch]${c.reset}\n`);
2037
2185
  console.log(`${c.bold}Quick start:${c.reset}`);
2038
- console.log(` ${c.cyan}pi${c.reset} # start pi (taskplane auto-loads)`);
2039
- console.log(` ${c.cyan}/orch${c.reset} # start the taskplane supervisor`);
2040
- console.log(` ${c.cyan}/orch all${c.reset} # run all open tasks`);
2186
+ console.log(
2187
+ ` ${c.cyan}pi${c.reset} # start pi (taskplane auto-loads)`,
2188
+ );
2189
+ console.log(
2190
+ ` ${c.cyan}/orch${c.reset} # start the taskplane supervisor`,
2191
+ );
2192
+ console.log(
2193
+ ` ${c.cyan}/orch all${c.reset} # run all open tasks`,
2194
+ );
2041
2195
  if (inferTaskplaneInstallScope() === "global") {
2042
- console.log(` ${c.cyan}taskplane config --save-as-defaults${c.reset} # save these agent defaults for future inits`);
2196
+ console.log(
2197
+ ` ${c.cyan}taskplane config --save-as-defaults${c.reset} # save these agent defaults for future inits`,
2198
+ );
2043
2199
  }
2044
2200
  console.log();
2045
2201
  return;
@@ -2101,7 +2257,7 @@ async function cmdInit(args) {
2101
2257
  copyTemplate(
2102
2258
  path.join(TEMPLATES_DIR, "agents", "local", agent),
2103
2259
  path.join(projectRoot, ".pi", "agents", agent),
2104
- { skipIfExists, label: `.pi/agents/${agent}` }
2260
+ { skipIfExists, label: `.pi/agents/${agent}` },
2105
2261
  );
2106
2262
  }
2107
2263
 
@@ -2122,16 +2278,15 @@ async function cmdInit(args) {
2122
2278
  writeFile(
2123
2279
  path.join(projectRoot, ".pi", "taskplane.json"),
2124
2280
  JSON.stringify(versionInfo, null, 2) + "\n",
2125
- { label: ".pi/taskplane.json" }
2281
+ { label: ".pi/taskplane.json" },
2126
2282
  );
2127
2283
 
2128
2284
  // CONTEXT.md
2129
2285
  const contextSrc = fs.readFileSync(path.join(TEMPLATES_DIR, "tasks", "CONTEXT.md"), "utf-8");
2130
- writeFile(
2131
- path.join(projectRoot, vars.tasks_root, "CONTEXT.md"),
2132
- interpolate(contextSrc, vars),
2133
- { skipIfExists, label: `${vars.tasks_root}/CONTEXT.md` }
2134
- );
2286
+ writeFile(path.join(projectRoot, vars.tasks_root, "CONTEXT.md"), interpolate(contextSrc, vars), {
2287
+ skipIfExists,
2288
+ label: `${vars.tasks_root}/CONTEXT.md`,
2289
+ });
2135
2290
 
2136
2291
  // Example tasks
2137
2292
  if (!noExamples) {
@@ -2164,7 +2319,9 @@ async function cmdInit(args) {
2164
2319
  if (gitignoreResult.created) {
2165
2320
  console.log(` ${c.green}create${c.reset} .gitignore`);
2166
2321
  } else if (gitignoreResult.added.length > 0) {
2167
- console.log(` ${c.green}update${c.reset} .gitignore (${gitignoreResult.added.length} entries added)`);
2322
+ console.log(
2323
+ ` ${c.green}update${c.reset} .gitignore (${gitignoreResult.added.length} entries added)`,
2324
+ );
2168
2325
  } else {
2169
2326
  console.log(` ${c.dim}skip${c.reset} .gitignore (all entries already present)`);
2170
2327
  }
@@ -2179,11 +2336,19 @@ async function cmdInit(args) {
2179
2336
  // Report
2180
2337
  console.log(`\n${OK} ${c.bold}Taskplane initialized!${c.reset}\n`);
2181
2338
  console.log(`${c.bold}Quick start:${c.reset}`);
2182
- console.log(` ${c.cyan}pi${c.reset} # start pi (taskplane auto-loads)`);
2183
- console.log(` ${c.cyan}/orch${c.reset} # start the taskplane supervisor`);
2184
- console.log(` ${c.cyan}/orch all${c.reset} # run all open tasks`);
2339
+ console.log(
2340
+ ` ${c.cyan}pi${c.reset} # start pi (taskplane auto-loads)`,
2341
+ );
2342
+ console.log(
2343
+ ` ${c.cyan}/orch${c.reset} # start the taskplane supervisor`,
2344
+ );
2345
+ console.log(
2346
+ ` ${c.cyan}/orch all${c.reset} # run all open tasks`,
2347
+ );
2185
2348
  if (inferTaskplaneInstallScope() === "global") {
2186
- console.log(` ${c.cyan}taskplane config --save-as-defaults${c.reset} # save these agent defaults for future inits`);
2349
+ console.log(
2350
+ ` ${c.cyan}taskplane config --save-as-defaults${c.reset} # save these agent defaults for future inits`,
2351
+ );
2187
2352
  }
2188
2353
  console.log();
2189
2354
  }
@@ -2214,12 +2379,22 @@ async function getInteractiveVars(projectRoot, tasksRootOverride = null) {
2214
2379
  const project_name = await ask("Project name", dirName);
2215
2380
  const maxLanesInput = await ask("Max parallel lanes", "3");
2216
2381
  const max_lanes = parseInt(maxLanesInput, 10) || 3;
2217
- const tasks_root_raw = tasksRootOverride || await ask("Tasks directory", "taskplane-tasks");
2218
- const tasks_root = tasks_root_raw.trim().replace(/\\/g, "/").replace(/^\.\//g, "").replace(/\/+$/g, "");
2382
+ const tasks_root_raw = tasksRootOverride || (await ask("Tasks directory", "taskplane-tasks"));
2383
+ const tasks_root = tasks_root_raw
2384
+ .trim()
2385
+ .replace(/\\/g, "/")
2386
+ .replace(/^\.\//g, "")
2387
+ .replace(/\/+$/g, "");
2219
2388
  const default_area = await ask("Default area name", "general");
2220
2389
  const default_prefix = await ask("Task ID prefix", "TP");
2221
- const test_cmd = await ask("Test command (agents run this to verify work — blank to skip)", detected.test || "");
2222
- const build_cmd = await ask("Build command (agents run this after tests — blank to skip)", detected.build || "");
2390
+ const test_cmd = await ask(
2391
+ "Test command (agents run this to verify work — blank to skip)",
2392
+ detected.test || "",
2393
+ );
2394
+ const build_cmd = await ask(
2395
+ "Build command (agents run this after tests — blank to skip)",
2396
+ detected.build || "",
2397
+ );
2223
2398
 
2224
2399
  const slug = slugify(project_name);
2225
2400
  const explicit_orchestrator_overrides = {};
@@ -2265,7 +2440,9 @@ function printFileList(vars, noExamples, preset, exampleTemplateDirs = [], proje
2265
2440
  const gitignoreResult = ensureGitignoreEntries(projectRoot, { dryRun: true });
2266
2441
  if (gitignoreResult.added.length > 0) {
2267
2442
  const action = fs.existsSync(path.join(projectRoot, ".gitignore")) ? "update" : "create";
2268
- console.log(` ${c.green}${action}${c.reset} .gitignore (${gitignoreResult.added.length} entries)`);
2443
+ console.log(
2444
+ ` ${c.green}${action}${c.reset} .gitignore (${gitignoreResult.added.length} entries)`,
2445
+ );
2269
2446
  } else {
2270
2447
  console.log(` ${c.dim}skip${c.reset} .gitignore (all entries already present)`);
2271
2448
  }
@@ -2278,7 +2455,14 @@ function printFileList(vars, noExamples, preset, exampleTemplateDirs = [], proje
2278
2455
  * Print the list of files that would be created for workspace mode (dry-run).
2279
2456
  * Similar to printFileList but paths are scoped to <configRepo>/.taskplane/.
2280
2457
  */
2281
- function printWorkspaceFileList(vars, noExamples, preset, exampleTemplateDirs, configRepoName, configRepoRoot) {
2458
+ function printWorkspaceFileList(
2459
+ vars,
2460
+ noExamples,
2461
+ preset,
2462
+ exampleTemplateDirs,
2463
+ configRepoName,
2464
+ configRepoRoot,
2465
+ ) {
2282
2466
  const prefix = `${configRepoName}/.taskplane`;
2283
2467
  const files = [
2284
2468
  `${prefix}/agents/task-worker.md`,
@@ -2299,12 +2483,19 @@ function printWorkspaceFileList(vars, noExamples, preset, exampleTemplateDirs, c
2299
2483
  for (const f of files) console.log(` ${c.green}create${c.reset} ${f}`);
2300
2484
 
2301
2485
  // Show gitignore entries that would be added to config repo (workspace-scoped)
2302
- const gitignoreResult = ensureGitignoreEntries(configRepoRoot, { dryRun: true, prefix: ".taskplane/" });
2486
+ const gitignoreResult = ensureGitignoreEntries(configRepoRoot, {
2487
+ dryRun: true,
2488
+ prefix: ".taskplane/",
2489
+ });
2303
2490
  if (gitignoreResult.added.length > 0) {
2304
2491
  const action = fs.existsSync(path.join(configRepoRoot, ".gitignore")) ? "update" : "create";
2305
- console.log(` ${c.green}${action}${c.reset} ${configRepoName}/.gitignore (${gitignoreResult.added.length} entries)`);
2492
+ console.log(
2493
+ ` ${c.green}${action}${c.reset} ${configRepoName}/.gitignore (${gitignoreResult.added.length} entries)`,
2494
+ );
2306
2495
  } else {
2307
- console.log(` ${c.dim}skip${c.reset} ${configRepoName}/.gitignore (all entries already present)`);
2496
+ console.log(
2497
+ ` ${c.dim}skip${c.reset} ${configRepoName}/.gitignore (all entries already present)`,
2498
+ );
2308
2499
  }
2309
2500
  }
2310
2501
 
@@ -2463,7 +2654,7 @@ function loadWorkspaceConfigForDoctor(projectRoot) {
2463
2654
  function parseWorkspaceYaml(raw) {
2464
2655
  const lines = raw.split(/\r?\n/);
2465
2656
  const result = { repos: {}, routing: {} };
2466
- let section = null; // "repos" | "routing" | null
2657
+ let section = null; // "repos" | "routing" | null
2467
2658
  let currentRepoId = null; // current repo being parsed
2468
2659
 
2469
2660
  for (const line of lines) {
@@ -2594,7 +2785,9 @@ function cmdDoctor() {
2594
2785
  const pkgVersion = getPackageVersion();
2595
2786
  const isProjectLocal = PACKAGE_ROOT.includes(".pi");
2596
2787
  const installType = isProjectLocal ? "project-local" : "global";
2597
- console.log(` ${OK} taskplane package installed ${c.dim}(v${pkgVersion}, ${installType})${c.reset}`);
2788
+ console.log(
2789
+ ` ${OK} taskplane package installed ${c.dim}(v${pkgVersion}, ${installType})${c.reset}`,
2790
+ );
2598
2791
 
2599
2792
  if (isWorkspaceMode) {
2600
2793
  console.log();
@@ -2603,7 +2796,9 @@ function cmdDoctor() {
2603
2796
  const codeHint = wsResult.error.code ? ` [${wsResult.error.code}]` : "";
2604
2797
  console.log(` ${FAIL} workspace mode detected but config is invalid${codeHint}`);
2605
2798
  console.log(` ${c.dim}${wsResult.error.message}${c.reset}`);
2606
- console.log(` ${c.dim}→ Fix .pi/taskplane-workspace.yaml or remove it to use repo mode${c.reset}`);
2799
+ console.log(
2800
+ ` ${c.dim}→ Fix .pi/taskplane-workspace.yaml or remove it to use repo mode${c.reset}`,
2801
+ );
2607
2802
  issues++;
2608
2803
  } else {
2609
2804
  // Valid workspace config — show summary banner
@@ -2612,7 +2807,9 @@ function cmdDoctor() {
2612
2807
  const repoCount = repoIds.length;
2613
2808
  const defaultRepo = cfg.routing.defaultRepo;
2614
2809
  const tasksRoot = cfg.routing.tasksRoot;
2615
- console.log(` ${OK} workspace mode ${c.dim}(${repoCount} repo${repoCount !== 1 ? "s" : ""}, default: ${defaultRepo})${c.reset}`);
2810
+ console.log(
2811
+ ` ${OK} workspace mode ${c.dim}(${repoCount} repo${repoCount !== 1 ? "s" : ""}, default: ${defaultRepo})${c.reset}`,
2812
+ );
2616
2813
  console.log(` ${c.dim}repos: ${repoIds.join(", ")}${c.reset}`);
2617
2814
  console.log(` ${c.dim}tasks_root: ${tasksRoot}${c.reset}`);
2618
2815
  }
@@ -2628,22 +2825,32 @@ function cmdDoctor() {
2628
2825
  let pointer = null;
2629
2826
  if (!fs.existsSync(pointerPath)) {
2630
2827
  console.log(` ${FAIL} .pi/taskplane-pointer.json missing [POINTER_MISSING]`);
2631
- console.log(` ${c.dim}→ Run ${c.cyan}taskplane init${c.dim} to create the workspace pointer${c.reset}`);
2828
+ console.log(
2829
+ ` ${c.dim}→ Run ${c.cyan}taskplane init${c.dim} to create the workspace pointer${c.reset}`,
2830
+ );
2632
2831
  issues++;
2633
2832
  } else {
2634
2833
  try {
2635
2834
  pointer = JSON.parse(fs.readFileSync(pointerPath, "utf-8"));
2636
2835
  if (!pointer.config_repo || !pointer.config_path) {
2637
- console.log(` ${FAIL} .pi/taskplane-pointer.json missing required fields (config_repo, config_path) [POINTER_SCHEMA_INVALID]`);
2638
- console.log(` ${c.dim} Run ${c.cyan}taskplane init${c.dim} to recreate the pointer${c.reset}`);
2836
+ console.log(
2837
+ ` ${FAIL} .pi/taskplane-pointer.json missing required fields (config_repo, config_path) [POINTER_SCHEMA_INVALID]`,
2838
+ );
2839
+ console.log(
2840
+ ` ${c.dim}→ Run ${c.cyan}taskplane init${c.dim} to recreate the pointer${c.reset}`,
2841
+ );
2639
2842
  pointer = null;
2640
2843
  issues++;
2641
2844
  } else {
2642
- console.log(` ${OK} .pi/taskplane-pointer.json ${c.dim}(→ ${pointer.config_repo}/${pointer.config_path})${c.reset}`);
2845
+ console.log(
2846
+ ` ${OK} .pi/taskplane-pointer.json ${c.dim}(→ ${pointer.config_repo}/${pointer.config_path})${c.reset}`,
2847
+ );
2643
2848
  }
2644
2849
  } catch {
2645
2850
  console.log(` ${FAIL} .pi/taskplane-pointer.json is not valid JSON [POINTER_PARSE_ERROR]`);
2646
- console.log(` ${c.dim}→ Run ${c.cyan}taskplane init${c.dim} to recreate the pointer${c.reset}`);
2851
+ console.log(
2852
+ ` ${c.dim}→ Run ${c.cyan}taskplane init${c.dim} to recreate the pointer${c.reset}`,
2853
+ );
2647
2854
  issues++;
2648
2855
  }
2649
2856
  }
@@ -2658,12 +2865,16 @@ function cmdDoctor() {
2658
2865
  configRepoRoot = null;
2659
2866
  issues++;
2660
2867
  } else if (!isInsideGitRepo(configRepoRoot)) {
2661
- console.log(` ${FAIL} config repo is not a git repository: ${pointer.config_repo} [CONFIG_REPO_NOT_GIT]`);
2868
+ console.log(
2869
+ ` ${FAIL} config repo is not a git repository: ${pointer.config_repo} [CONFIG_REPO_NOT_GIT]`,
2870
+ );
2662
2871
  console.log(` ${c.dim}→ Run: git init ${configRepoRoot}${c.reset}`);
2663
2872
  configRepoRoot = null;
2664
2873
  issues++;
2665
2874
  } else {
2666
- console.log(` ${OK} config repo: ${pointer.config_repo} ${c.dim}(${configRepoRoot})${c.reset}`);
2875
+ console.log(
2876
+ ` ${OK} config repo: ${pointer.config_repo} ${c.dim}(${configRepoRoot})${c.reset}`,
2877
+ );
2667
2878
  }
2668
2879
  }
2669
2880
 
@@ -2672,8 +2883,12 @@ function cmdDoctor() {
2672
2883
  if (configRepoRoot) {
2673
2884
  const taskplaneDir = path.join(configRepoRoot, pointer.config_path);
2674
2885
  if (!fs.existsSync(taskplaneDir)) {
2675
- console.log(` ${FAIL} ${pointer.config_repo}/${pointer.config_path}/ not found [CONFIG_DIR_NOT_FOUND]`);
2676
- console.log(` ${c.dim} Run ${c.cyan}taskplane init${c.dim} to create the config directory${c.reset}`);
2886
+ console.log(
2887
+ ` ${FAIL} ${pointer.config_repo}/${pointer.config_path}/ not found [CONFIG_DIR_NOT_FOUND]`,
2888
+ );
2889
+ console.log(
2890
+ ` ${c.dim}→ Run ${c.cyan}taskplane init${c.dim} to create the config directory${c.reset}`,
2891
+ );
2677
2892
  issues++;
2678
2893
  } else {
2679
2894
  console.log(` ${OK} ${pointer.config_repo}/${pointer.config_path}/ exists`);
@@ -2689,7 +2904,9 @@ function cmdDoctor() {
2689
2904
  cwd: configRepoRoot,
2690
2905
  stdio: ["pipe", "pipe", "pipe"],
2691
2906
  timeout: 5000,
2692
- }).toString().trim();
2907
+ })
2908
+ .toString()
2909
+ .trim();
2693
2910
 
2694
2911
  // Detect default branch (try origin/HEAD, fall back to main/master heuristic)
2695
2912
  let defaultBranch = null;
@@ -2698,7 +2915,9 @@ function cmdDoctor() {
2698
2915
  cwd: configRepoRoot,
2699
2916
  stdio: ["pipe", "pipe", "pipe"],
2700
2917
  timeout: 5000,
2701
- }).toString().trim();
2918
+ })
2919
+ .toString()
2920
+ .trim();
2702
2921
  // refs/remotes/origin/main → main
2703
2922
  defaultBranch = originHead.replace(/^refs\/remotes\/origin\//, "");
2704
2923
  } catch {
@@ -2721,28 +2940,40 @@ function cmdDoctor() {
2721
2940
  if (defaultBranch && currentBranch !== defaultBranch) {
2722
2941
  // Check if .taskplane/ exists on the default branch via git ls-tree
2723
2942
  try {
2724
- const lsOutput = execFileSync("git", ["ls-tree", "--name-only", defaultBranch, pointer.config_path + "/"], {
2725
- cwd: configRepoRoot,
2726
- stdio: ["pipe", "pipe", "pipe"],
2727
- timeout: 5000,
2728
- }).toString().trim();
2943
+ const lsOutput = execFileSync(
2944
+ "git",
2945
+ ["ls-tree", "--name-only", defaultBranch, pointer.config_path + "/"],
2946
+ {
2947
+ cwd: configRepoRoot,
2948
+ stdio: ["pipe", "pipe", "pipe"],
2949
+ timeout: 5000,
2950
+ },
2951
+ )
2952
+ .toString()
2953
+ .trim();
2729
2954
 
2730
2955
  if (lsOutput) {
2731
2956
  console.log(` ${OK} ${pointer.config_path}/ exists on default branch (${defaultBranch})`);
2732
2957
  } else {
2733
- console.log(` ${WARN} ${pointer.config_path}/ exists on current branch (${currentBranch}) but not on default branch (${defaultBranch})`);
2958
+ console.log(
2959
+ ` ${WARN} ${pointer.config_path}/ exists on current branch (${currentBranch}) but not on default branch (${defaultBranch})`,
2960
+ );
2734
2961
  console.log(` ${c.dim}→ Merge to ${defaultBranch} so teammates can onboard${c.reset}`);
2735
2962
  }
2736
2963
  } catch {
2737
2964
  // ls-tree failed — directory doesn't exist on that branch
2738
- console.log(` ${WARN} ${pointer.config_path}/ exists on current branch (${currentBranch}) but not on default branch (${defaultBranch})`);
2965
+ console.log(
2966
+ ` ${WARN} ${pointer.config_path}/ exists on current branch (${currentBranch}) but not on default branch (${defaultBranch})`,
2967
+ );
2739
2968
  console.log(` ${c.dim}→ Merge to ${defaultBranch} so teammates can onboard${c.reset}`);
2740
2969
  }
2741
2970
  } else if (defaultBranch && currentBranch === defaultBranch) {
2742
2971
  console.log(` ${OK} ${pointer.config_path}/ on default branch (${defaultBranch})`);
2743
2972
  } else {
2744
2973
  // Could not determine default branch — skip this check silently
2745
- console.log(` ${INFO} could not determine default branch for ${pointer.config_repo} — skipping branch check`);
2974
+ console.log(
2975
+ ` ${INFO} could not determine default branch for ${pointer.config_repo} — skipping branch check`,
2976
+ );
2746
2977
  }
2747
2978
  } catch {
2748
2979
  // git commands failed — skip branch check
@@ -2760,8 +2991,12 @@ function cmdDoctor() {
2760
2991
 
2761
2992
  // Check path exists on disk
2762
2993
  if (!fs.existsSync(resolvedPath)) {
2763
- console.log(` ${FAIL} repo: ${repoId} — path not found: ${resolvedPath} [WORKSPACE_REPO_PATH_NOT_FOUND]`);
2764
- console.log(` ${c.dim} Check repos.${repoId}.path in .pi/taskplane-workspace.yaml${c.reset}`);
2994
+ console.log(
2995
+ ` ${FAIL} repo: ${repoId}path not found: ${resolvedPath} [WORKSPACE_REPO_PATH_NOT_FOUND]`,
2996
+ );
2997
+ console.log(
2998
+ ` ${c.dim}→ Check repos.${repoId}.path in .pi/taskplane-workspace.yaml${c.reset}`,
2999
+ );
2765
3000
  issues++;
2766
3001
  continue;
2767
3002
  }
@@ -2775,9 +3010,13 @@ function cmdDoctor() {
2775
3010
  });
2776
3011
  console.log(` ${OK} repo: ${repoId} ${c.dim}(${resolvedPath})${c.reset}`);
2777
3012
  } catch {
2778
- console.log(` ${FAIL} repo: ${repoId} — not a git repository: ${resolvedPath} [WORKSPACE_REPO_NOT_GIT]`);
3013
+ console.log(
3014
+ ` ${FAIL} repo: ${repoId} — not a git repository: ${resolvedPath} [WORKSPACE_REPO_NOT_GIT]`,
3015
+ );
2779
3016
  console.log(` ${c.dim}→ Run: git init ${resolvedPath}${c.reset}`);
2780
- console.log(` ${c.dim} or fix repos.${repoId}.path in .pi/taskplane-workspace.yaml${c.reset}`);
3017
+ console.log(
3018
+ ` ${c.dim} or fix repos.${repoId}.path in .pi/taskplane-workspace.yaml${c.reset}`,
3019
+ );
2781
3020
  issues++;
2782
3021
  }
2783
3022
  }
@@ -2785,11 +3024,13 @@ function cmdDoctor() {
2785
3024
 
2786
3025
  // Check project config (common — both modes)
2787
3026
  console.log();
2788
- const hasUnifiedJson = fs.existsSync(path.join(configLocation.root, configLocation.prefix, "taskplane-config.json"));
2789
- const hasYamlFallback = !hasUnifiedJson && (
2790
- fs.existsSync(path.join(configLocation.root, configLocation.prefix, "task-runner.yaml")) ||
2791
- fs.existsSync(path.join(configLocation.root, configLocation.prefix, "task-orchestrator.yaml"))
3027
+ const hasUnifiedJson = fs.existsSync(
3028
+ path.join(configLocation.root, configLocation.prefix, "taskplane-config.json"),
2792
3029
  );
3030
+ const hasYamlFallback =
3031
+ !hasUnifiedJson &&
3032
+ (fs.existsSync(path.join(configLocation.root, configLocation.prefix, "task-runner.yaml")) ||
3033
+ fs.existsSync(path.join(configLocation.root, configLocation.prefix, "task-orchestrator.yaml")));
2793
3034
  const configFiles = [
2794
3035
  // JSON is required unless legacy YAML exists as fallback
2795
3036
  { path: "taskplane-config.json", required: !hasYamlFallback, hide: false },
@@ -2840,8 +3081,16 @@ function cmdDoctor() {
2840
3081
  // Detect YAML config files without a JSON equivalent (taskplane-config.json).
2841
3082
  {
2842
3083
  const yamlRunnerPath = path.join(configLocation.root, configLocation.prefix, "task-runner.yaml");
2843
- const yamlOrchestratorPath = path.join(configLocation.root, configLocation.prefix, "task-orchestrator.yaml");
2844
- const jsonConfigPath = path.join(configLocation.root, configLocation.prefix, "taskplane-config.json");
3084
+ const yamlOrchestratorPath = path.join(
3085
+ configLocation.root,
3086
+ configLocation.prefix,
3087
+ "task-orchestrator.yaml",
3088
+ );
3089
+ const jsonConfigPath = path.join(
3090
+ configLocation.root,
3091
+ configLocation.prefix,
3092
+ "taskplane-config.json",
3093
+ );
2845
3094
 
2846
3095
  const hasYamlRunner = fs.existsSync(yamlRunnerPath);
2847
3096
  const hasYamlOrchestrator = fs.existsSync(yamlOrchestratorPath);
@@ -2849,12 +3098,18 @@ function cmdDoctor() {
2849
3098
 
2850
3099
  if ((hasYamlRunner || hasYamlOrchestrator) && !hasJsonConfig) {
2851
3100
  console.log(` ${WARN} legacy YAML config detected in ${configLocation.label}`);
2852
- console.log(` ${c.dim}→ Run /taskplane-settings to migrate to taskplane-config.json${c.reset}`);
3101
+ console.log(
3102
+ ` ${c.dim}→ Run /taskplane-settings to migrate to taskplane-config.json${c.reset}`,
3103
+ );
2853
3104
  }
2854
3105
  }
2855
3106
 
2856
3107
  // Check task areas from config
2857
- const { paths: taskAreaPaths, contexts: taskAreaContexts, areaRepoIds } = discoverTaskAreaMetadata(projectRoot, configLocation.root, configLocation.prefix);
3108
+ const {
3109
+ paths: taskAreaPaths,
3110
+ contexts: taskAreaContexts,
3111
+ areaRepoIds,
3112
+ } = discoverTaskAreaMetadata(projectRoot, configLocation.root, configLocation.prefix);
2858
3113
  if (taskAreaPaths.length > 0) {
2859
3114
  console.log();
2860
3115
  for (const areaPath of taskAreaPaths) {
@@ -2886,8 +3141,12 @@ function cmdDoctor() {
2886
3141
  if (knownRepoIds.includes(repoId)) {
2887
3142
  console.log(` ${OK} area '${areaName}' repo_id: ${repoId}`);
2888
3143
  } else {
2889
- console.log(` ${FAIL} area '${areaName}' repo_id '${repoId}' does not match any workspace repo [AREA_REPO_ID_UNKNOWN]`);
2890
- console.log(` ${c.dim} Available repos: ${knownRepoIds.join(", ")}. Fix repoId in ${configLocation.label}/taskplane-config.json${c.reset}`);
3144
+ console.log(
3145
+ ` ${FAIL} area '${areaName}' repo_id '${repoId}' does not match any workspace repo [AREA_REPO_ID_UNKNOWN]`,
3146
+ );
3147
+ console.log(
3148
+ ` ${c.dim}→ Available repos: ${knownRepoIds.join(", ")}. Fix repoId in ${configLocation.label}/taskplane-config.json${c.reset}`,
3149
+ );
2891
3150
  issues++;
2892
3151
  }
2893
3152
  }
@@ -2921,22 +3180,30 @@ function cmdDoctor() {
2921
3180
  const gitignorePath = path.join(configRepoRoot, ".gitignore");
2922
3181
  const gitignoreExists = fs.existsSync(gitignorePath);
2923
3182
  if (!gitignoreExists) {
2924
- console.log(` ${WARN} ${configRepoName}/.gitignore missing — Taskplane runtime entries not protected`);
2925
- console.log(` ${c.dim} Run ${c.cyan}taskplane init${c.dim} to add them, or add manually${c.reset}`);
3183
+ console.log(
3184
+ ` ${WARN} ${configRepoName}/.gitignore missing Taskplane runtime entries not protected`,
3185
+ );
3186
+ console.log(
3187
+ ` ${c.dim}→ Run ${c.cyan}taskplane init${c.dim} to add them, or add manually${c.reset}`,
3188
+ );
2926
3189
  // WARN doesn't increment issues (it's advisory, not a failure)
2927
3190
  } else {
2928
3191
  const content = fs.readFileSync(gitignorePath, "utf-8");
2929
- const existingLines = new Set(content.split(/\r?\n/).map(l => l.trim()));
3192
+ const existingLines = new Set(content.split(/\r?\n/).map((l) => l.trim()));
2930
3193
  const allEntries = [...TASKPLANE_GITIGNORE_ENTRIES, ...TASKPLANE_GITIGNORE_NPM_ENTRIES];
2931
3194
  const missing = allEntries
2932
- .map(entry => `${prefix}${entry}`)
2933
- .filter(prefixed => !existingLines.has(prefixed));
3195
+ .map((entry) => `${prefix}${entry}`)
3196
+ .filter((prefixed) => !existingLines.has(prefixed));
2934
3197
 
2935
3198
  if (missing.length === 0) {
2936
3199
  console.log(` ${OK} ${configRepoName}/.gitignore has all Taskplane runtime entries`);
2937
3200
  } else {
2938
- console.log(` ${WARN} ${configRepoName}/.gitignore missing ${missing.length} Taskplane runtime entr${missing.length === 1 ? "y" : "ies"}`);
2939
- console.log(` ${c.dim} Run ${c.cyan}taskplane init${c.dim} to add them, or add manually${c.reset}`);
3201
+ console.log(
3202
+ ` ${WARN} ${configRepoName}/.gitignore missing ${missing.length} Taskplane runtime entr${missing.length === 1 ? "y" : "ies"}`,
3203
+ );
3204
+ console.log(
3205
+ ` ${c.dim}→ Run ${c.cyan}taskplane init${c.dim} to add them, or add manually${c.reset}`,
3206
+ );
2940
3207
  }
2941
3208
  }
2942
3209
 
@@ -2947,22 +3214,26 @@ function cmdDoctor() {
2947
3214
  cwd: configRepoRoot,
2948
3215
  stdio: ["pipe", "pipe", "pipe"],
2949
3216
  timeout: 10000,
2950
- }).toString().trim();
3217
+ })
3218
+ .toString()
3219
+ .trim();
2951
3220
  const trackedFiles = raw ? raw.split(/\r?\n/) : [];
2952
3221
 
2953
3222
  if (trackedFiles.length > 0) {
2954
- const prefixedPatterns = ALL_GITIGNORE_PATTERNS.map(p => `${prefix}${p}`);
2955
- const patterns = prefixedPatterns.map(p => patternToRegex(p));
2956
- const matchedFiles = trackedFiles.filter(file =>
2957
- patterns.some(regex => regex.test(file))
2958
- );
3223
+ const prefixedPatterns = ALL_GITIGNORE_PATTERNS.map((p) => `${prefix}${p}`);
3224
+ const patterns = prefixedPatterns.map((p) => patternToRegex(p));
3225
+ const matchedFiles = trackedFiles.filter((file) => patterns.some((regex) => regex.test(file)));
2959
3226
 
2960
3227
  if (matchedFiles.length > 0) {
2961
- console.log(` ${FAIL} ${matchedFiles.length} runtime artifact${matchedFiles.length === 1 ? "" : "s"} tracked by git in ${configRepoName}`);
3228
+ console.log(
3229
+ ` ${FAIL} ${matchedFiles.length} runtime artifact${matchedFiles.length === 1 ? "" : "s"} tracked by git in ${configRepoName}`,
3230
+ );
2962
3231
  for (const file of matchedFiles) {
2963
3232
  console.log(` ${c.dim}${file}${c.reset}`);
2964
3233
  }
2965
- console.log(` ${c.dim}→ Run: cd ${configRepoName} && git rm --cached ${matchedFiles.join(" ")}${c.reset}`);
3234
+ console.log(
3235
+ ` ${c.dim}→ Run: cd ${configRepoName} && git rm --cached ${matchedFiles.join(" ")}${c.reset}`,
3236
+ );
2966
3237
  issues++;
2967
3238
  } else {
2968
3239
  console.log(` ${OK} no runtime artifacts tracked by git in ${configRepoName}`);
@@ -2983,18 +3254,24 @@ function cmdDoctor() {
2983
3254
  const gitignoreExists = fs.existsSync(gitignorePath);
2984
3255
  if (!gitignoreExists) {
2985
3256
  console.log(` ${WARN} .gitignore missing — Taskplane runtime entries not protected`);
2986
- console.log(` ${c.dim}→ Run ${c.cyan}taskplane init${c.dim} to add them, or add manually${c.reset}`);
3257
+ console.log(
3258
+ ` ${c.dim}→ Run ${c.cyan}taskplane init${c.dim} to add them, or add manually${c.reset}`,
3259
+ );
2987
3260
  } else {
2988
3261
  const content = fs.readFileSync(gitignorePath, "utf-8");
2989
- const existingLines = new Set(content.split(/\r?\n/).map(l => l.trim()));
3262
+ const existingLines = new Set(content.split(/\r?\n/).map((l) => l.trim()));
2990
3263
  const allEntries = [...TASKPLANE_GITIGNORE_ENTRIES, ...TASKPLANE_GITIGNORE_NPM_ENTRIES];
2991
- const missing = allEntries.filter(entry => !existingLines.has(entry));
3264
+ const missing = allEntries.filter((entry) => !existingLines.has(entry));
2992
3265
 
2993
3266
  if (missing.length === 0) {
2994
3267
  console.log(` ${OK} .gitignore has all Taskplane runtime entries`);
2995
3268
  } else {
2996
- console.log(` ${WARN} .gitignore missing ${missing.length} Taskplane runtime entr${missing.length === 1 ? "y" : "ies"}`);
2997
- console.log(` ${c.dim} Run ${c.cyan}taskplane init${c.dim} to add them, or add manually${c.reset}`);
3269
+ console.log(
3270
+ ` ${WARN} .gitignore missing ${missing.length} Taskplane runtime entr${missing.length === 1 ? "y" : "ies"}`,
3271
+ );
3272
+ console.log(
3273
+ ` ${c.dim}→ Run ${c.cyan}taskplane init${c.dim} to add them, or add manually${c.reset}`,
3274
+ );
2998
3275
  }
2999
3276
  }
3000
3277
 
@@ -3005,17 +3282,19 @@ function cmdDoctor() {
3005
3282
  cwd: projectRoot,
3006
3283
  stdio: ["pipe", "pipe", "pipe"],
3007
3284
  timeout: 10000,
3008
- }).toString().trim();
3285
+ })
3286
+ .toString()
3287
+ .trim();
3009
3288
  const trackedFiles = raw ? raw.split(/\r?\n/) : [];
3010
3289
 
3011
3290
  if (trackedFiles.length > 0) {
3012
- const patterns = ALL_GITIGNORE_PATTERNS.map(p => patternToRegex(p));
3013
- const matchedFiles = trackedFiles.filter(file =>
3014
- patterns.some(regex => regex.test(file))
3015
- );
3291
+ const patterns = ALL_GITIGNORE_PATTERNS.map((p) => patternToRegex(p));
3292
+ const matchedFiles = trackedFiles.filter((file) => patterns.some((regex) => regex.test(file)));
3016
3293
 
3017
3294
  if (matchedFiles.length > 0) {
3018
- console.log(` ${FAIL} ${matchedFiles.length} runtime artifact${matchedFiles.length === 1 ? "" : "s"} tracked by git`);
3295
+ console.log(
3296
+ ` ${FAIL} ${matchedFiles.length} runtime artifact${matchedFiles.length === 1 ? "" : "s"} tracked by git`,
3297
+ );
3019
3298
  for (const file of matchedFiles) {
3020
3299
  console.log(` ${c.dim}${file}${c.reset}`);
3021
3300
  }
@@ -3036,7 +3315,9 @@ function cmdDoctor() {
3036
3315
  if (issues === 0) {
3037
3316
  console.log(`${OK} ${c.green}All checks passed!${c.reset}\n`);
3038
3317
  } else {
3039
- console.log(`${FAIL} ${issues} issue(s) found. Run ${c.cyan}taskplane init${c.reset} to fix config issues.\n`);
3318
+ console.log(
3319
+ `${FAIL} ${issues} issue(s) found. Run ${c.cyan}taskplane init${c.reset} to fix config issues.\n`,
3320
+ );
3040
3321
  process.exit(1);
3041
3322
  }
3042
3323
  }
@@ -3057,7 +3338,9 @@ function cmdVersion() {
3057
3338
  if (fs.existsSync(tpJson)) {
3058
3339
  try {
3059
3340
  const info = JSON.parse(fs.readFileSync(tpJson, "utf-8"));
3060
- console.log(` Config: .pi/taskplane.json (v${info.version}, initialized ${info.installedAt?.slice(0, 10) || "unknown"})`);
3341
+ console.log(
3342
+ ` Config: .pi/taskplane.json (v${info.version}, initialized ${info.installedAt?.slice(0, 10) || "unknown"})`,
3343
+ );
3061
3344
  } catch {
3062
3345
  console.log(` Config: .pi/taskplane.json (unreadable)`);
3063
3346
  }