taskplane 0.24.21 → 0.24.22

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/README.md CHANGED
@@ -82,7 +82,7 @@ cd my-project
82
82
  taskplane init --preset full
83
83
  ```
84
84
 
85
- This creates config files in `.pi/`, agent prompts, two example tasks, and adds `.gitignore` entries for runtime artifacts. Interactive init now also prompts for worker/reviewer/merger model + thinking defaults (with `inherit` as the first option). Init auto-detects whether you're in a single repo or a multi-repo workspace. See the [install tutorial](docs/tutorials/install.md) for workspace mode and other scenarios.
85
+ This creates config files in `.pi/`, agent prompts, two example tasks, and adds `.gitignore` entries for runtime artifacts. On first install, init bootstraps global preferences at `~/.pi/agent/taskplane/preferences.json` with thinking defaults set to `high` for worker/reviewer/merger. Interactive init then prompts for worker/reviewer/merger model + thinking defaults (`inherit`, `off`, `minimal`, `low`, `medium`, `high`, `xhigh`). If 2+ providers are available from `pi --list-models`, init recommends cross-provider reviewer/merger selections. Init auto-detects whether you're in a single repo or a multi-repo workspace. See the [install tutorial](docs/tutorials/install.md) for workspace mode and other scenarios.
86
86
 
87
87
  Want to reuse model/thinking picks across projects? Run `taskplane config --save-as-defaults` in an initialized project.
88
88
 
package/bin/taskplane.mjs CHANGED
@@ -148,28 +148,56 @@ export function parsePiListModelsOutput(rawOutput) {
148
148
 
149
149
  const rows = rawOutput.split(/\r?\n/);
150
150
  const parsed = new Map();
151
+ let providerCol = 0;
152
+ let modelCol = 1;
153
+ let thinkingCol = -1;
151
154
 
152
155
  for (const row of rows) {
153
156
  const trimmed = row.trim();
154
157
  if (!trimmed) continue;
155
- if (/^provider\s+model\b/i.test(trimmed)) continue;
156
158
 
157
159
  const parts = trimmed.split(/\s+/);
158
160
  if (parts.length < 2) continue;
159
161
 
160
- const provider = parts[0].trim();
161
- const id = parts[1].trim();
162
+ if (/^provider\b/i.test(trimmed)) {
163
+ const lowerHeader = parts.map((part) => part.trim().toLowerCase());
164
+ const providerIdx = lowerHeader.indexOf("provider");
165
+ const modelIdx = lowerHeader.indexOf("model");
166
+ const thinkingIdx = lowerHeader.indexOf("thinking");
167
+ if (providerIdx >= 0) providerCol = providerIdx;
168
+ if (modelIdx >= 0) modelCol = modelIdx;
169
+ thinkingCol = thinkingIdx;
170
+ continue;
171
+ }
172
+
173
+ const provider = String(parts[providerCol] ?? parts[0] ?? "").trim();
174
+ const id = String(parts[modelCol] ?? parts[1] ?? "").trim();
162
175
  if (!provider || !id) continue;
163
176
  if (!/^[a-z0-9][a-z0-9._-]*$/i.test(provider)) continue;
164
177
  if (!/^[^\s]+$/.test(id)) continue;
165
178
 
179
+ const thinkingToken = thinkingCol >= 0 ? String(parts[thinkingCol] ?? "").trim().toLowerCase() : "";
180
+ const supportsThinking = (() => {
181
+ if (!thinkingToken) return undefined;
182
+ if (["yes", "true", "on", "supported"].includes(thinkingToken)) return true;
183
+ if (["no", "false", "off", "unsupported"].includes(thinkingToken)) return false;
184
+ return undefined;
185
+ })();
186
+
166
187
  const key = `${provider.toLowerCase()}/${id.toLowerCase()}`;
167
- if (parsed.has(key)) continue;
188
+ const existing = parsed.get(key);
189
+ if (existing) {
190
+ if (existing.supportsThinking === undefined && supportsThinking !== undefined) {
191
+ existing.supportsThinking = supportsThinking;
192
+ }
193
+ continue;
194
+ }
168
195
 
169
196
  parsed.set(key, {
170
197
  provider,
171
198
  id,
172
199
  displayName: `${provider}/${id}`,
200
+ ...(supportsThinking !== undefined ? { supportsThinking } : {}),
173
201
  });
174
202
  }
175
203
 
@@ -382,8 +410,9 @@ function buildTestingCommands(vars) {
382
410
  return commands;
383
411
  }
384
412
 
385
- const USER_PREFERENCES_SUBDIR = "taskplane";
386
- const USER_PREFERENCES_FILENAME = "preferences.json";
413
+ const GLOBAL_PREFERENCES_SUBDIR = "taskplane";
414
+ const GLOBAL_PREFERENCES_FILENAME = "preferences.json";
415
+ const PI_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"];
387
416
 
388
417
  function createInheritInitAgentConfig() {
389
418
  return {
@@ -396,10 +425,24 @@ function createInheritInitAgentConfig() {
396
425
  };
397
426
  }
398
427
 
428
+ function createBootstrapGlobalPreferencesForCli() {
429
+ return {
430
+ initAgentDefaults: {
431
+ workerModel: "",
432
+ reviewerModel: "",
433
+ mergeModel: "",
434
+ workerThinking: "high",
435
+ reviewerThinking: "high",
436
+ mergeThinking: "high",
437
+ },
438
+ };
439
+ }
440
+
399
441
  function normalizeThinkingMode(value) {
400
442
  const cleaned = String(value ?? "").trim().toLowerCase();
401
- if (cleaned === "on") return "on";
402
- if (cleaned === "off") return "off";
443
+ if (!cleaned || cleaned === "inherit") return "";
444
+ if (cleaned === "on") return "high";
445
+ if (PI_THINKING_LEVELS.includes(cleaned)) return cleaned;
403
446
  return "";
404
447
  }
405
448
 
@@ -422,52 +465,91 @@ function sanitizeInitAgentConfig(raw) {
422
465
  return defaults;
423
466
  }
424
467
 
425
- function resolveUserPreferencesPathForCli() {
468
+ function resolveGlobalPreferencesPathForCli() {
426
469
  const agentDir = process.env.PI_CODING_AGENT_DIR;
427
470
  if (agentDir) {
428
- return path.join(agentDir, USER_PREFERENCES_SUBDIR, USER_PREFERENCES_FILENAME);
471
+ return path.join(agentDir, GLOBAL_PREFERENCES_SUBDIR, GLOBAL_PREFERENCES_FILENAME);
429
472
  }
430
- return path.join(homedir(), ".pi", "agent", USER_PREFERENCES_SUBDIR, USER_PREFERENCES_FILENAME);
473
+ return path.join(homedir(), ".pi", "agent", GLOBAL_PREFERENCES_SUBDIR, GLOBAL_PREFERENCES_FILENAME);
474
+ }
475
+
476
+ function writeGlobalPreferencesForCli(rawPrefs, prefsPath = resolveGlobalPreferencesPathForCli()) {
477
+ fs.mkdirSync(path.dirname(prefsPath), { recursive: true });
478
+ const tmpPath = `${prefsPath}.tmp-${process.pid}-${Date.now()}`;
479
+ fs.writeFileSync(tmpPath, JSON.stringify(rawPrefs, null, 2) + "\n", "utf-8");
480
+ fs.renameSync(tmpPath, prefsPath);
481
+ return prefsPath;
431
482
  }
432
483
 
433
- function readUserPreferencesForCli() {
434
- const prefsPath = resolveUserPreferencesPathForCli();
484
+ function bootstrapGlobalPreferencesForCli(prefsPath) {
485
+ const raw = createBootstrapGlobalPreferencesForCli();
486
+ try {
487
+ writeGlobalPreferencesForCli(raw, prefsPath);
488
+ } catch {
489
+ // Best-effort disk write; continue with in-memory bootstrap values.
490
+ }
491
+ return raw;
492
+ }
493
+
494
+ function readGlobalPreferencesForCli() {
495
+ const prefsPath = resolveGlobalPreferencesPathForCli();
435
496
  if (!fs.existsSync(prefsPath)) {
497
+ return {
498
+ prefsPath,
499
+ raw: bootstrapGlobalPreferencesForCli(prefsPath),
500
+ wasBootstrapped: true,
501
+ };
502
+ }
503
+
504
+ let rawText = "";
505
+ try {
506
+ rawText = fs.readFileSync(prefsPath, "utf-8");
507
+ } catch {
436
508
  return {
437
509
  prefsPath,
438
510
  raw: {},
511
+ wasBootstrapped: false,
512
+ };
513
+ }
514
+
515
+ if (!rawText.trim()) {
516
+ return {
517
+ prefsPath,
518
+ raw: bootstrapGlobalPreferencesForCli(prefsPath),
519
+ wasBootstrapped: true,
439
520
  };
440
521
  }
441
522
 
442
523
  try {
443
- const raw = JSON.parse(fs.readFileSync(prefsPath, "utf-8"));
444
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
445
- return { prefsPath, raw: {} };
524
+ const raw = JSON.parse(rawText);
525
+ if (!raw || typeof raw !== "object" || Array.isArray(raw) || Object.keys(raw).length === 0) {
526
+ return {
527
+ prefsPath,
528
+ raw: bootstrapGlobalPreferencesForCli(prefsPath),
529
+ wasBootstrapped: true,
530
+ };
446
531
  }
447
- return { prefsPath, raw };
532
+ return { prefsPath, raw, wasBootstrapped: false };
448
533
  } catch {
449
- return { prefsPath, raw: {} };
534
+ return {
535
+ prefsPath,
536
+ raw: bootstrapGlobalPreferencesForCli(prefsPath),
537
+ wasBootstrapped: true,
538
+ };
450
539
  }
451
540
  }
452
541
 
453
- function writeUserPreferencesForCli(rawPrefs) {
454
- const prefsPath = resolveUserPreferencesPathForCli();
455
- fs.mkdirSync(path.dirname(prefsPath), { recursive: true });
456
- fs.writeFileSync(prefsPath, JSON.stringify(rawPrefs, null, 2) + "\n", "utf-8");
457
- return prefsPath;
458
- }
459
-
460
542
  function loadInitAgentDefaultsFromPreferences() {
461
- const { prefsPath, raw } = readUserPreferencesForCli();
543
+ const { prefsPath, raw, wasBootstrapped } = readGlobalPreferencesForCli();
462
544
  const defaults = sanitizeInitAgentConfig(raw.initAgentDefaults);
463
545
  const hasDefaults = !!(raw.initAgentDefaults && typeof raw.initAgentDefaults === "object" && !Array.isArray(raw.initAgentDefaults));
464
- return { defaults, hasDefaults, prefsPath };
546
+ return { defaults, hasDefaults, prefsPath, wasBootstrapped };
465
547
  }
466
548
 
467
549
  function saveInitAgentDefaultsToPreferences(initAgentConfig) {
468
- const { raw } = readUserPreferencesForCli();
550
+ const { raw, prefsPath } = readGlobalPreferencesForCli();
469
551
  raw.initAgentDefaults = sanitizeInitAgentConfig(initAgentConfig);
470
- const prefsPath = writeUserPreferencesForCli(raw);
552
+ writeGlobalPreferencesForCli(raw, prefsPath);
471
553
  return { prefsPath, saved: raw.initAgentDefaults };
472
554
  }
473
555
 
@@ -480,12 +562,35 @@ function splitModelReference(modelRef) {
480
562
  return { provider, id };
481
563
  }
482
564
 
565
+ function findModelInDiscovery(models, modelRef) {
566
+ if (!Array.isArray(models) || !modelRef) return null;
567
+ const ref = splitModelReference(modelRef);
568
+ if (!ref) return null;
569
+ const provider = ref.provider.toLowerCase();
570
+ const id = ref.id.toLowerCase();
571
+ return models.find((model) =>
572
+ String(model?.provider ?? "").toLowerCase() === provider
573
+ && String(model?.id ?? "").toLowerCase() === id,
574
+ ) || null;
575
+ }
576
+
483
577
  function allValuesEqual(values) {
484
578
  if (!Array.isArray(values) || values.length === 0) return true;
485
579
  const first = values[0];
486
580
  return values.every((value) => value === first);
487
581
  }
488
582
 
583
+ function countDistinctProviders(models) {
584
+ if (!Array.isArray(models)) return 0;
585
+ return new Set(models.map((model) => model?.provider).filter(Boolean)).size;
586
+ }
587
+
588
+ function needsCrossProviderInitGuidance(initAgentConfig, savedDefaults) {
589
+ const reviewerSet = !!String(initAgentConfig?.reviewerModel ?? "").trim();
590
+ const mergerSet = !!String(initAgentConfig?.mergeModel ?? "").trim();
591
+ return Boolean(savedDefaults?.wasBootstrapped) || !reviewerSet || !mergerSet;
592
+ }
593
+
489
594
  const INIT_AGENT_ROLES = [
490
595
  { key: "worker", label: "Worker", modelKey: "workerModel", thinkingKey: "workerThinking" },
491
596
  { key: "reviewer", label: "Reviewer", modelKey: "reviewerModel", thinkingKey: "reviewerThinking" },
@@ -525,6 +630,7 @@ async function promptModelForRole(roleLabel, models, {
525
630
  askImpl = ask,
526
631
  logImpl = console.log,
527
632
  currentModel = "",
633
+ preferDifferentProviderFrom = "",
528
634
  } = {}) {
529
635
  const providers = [...new Set(models.map((model) => model.provider))].sort((a, b) => a.localeCompare(b));
530
636
  const currentRef = splitModelReference(currentModel);
@@ -541,9 +647,20 @@ async function promptModelForRole(roleLabel, models, {
541
647
  };
542
648
  }),
543
649
  ];
544
- const providerDefaultIndex = currentRef
545
- ? Math.max(0, providerOptions.findIndex((option) => option.value === currentRef.provider))
546
- : 0;
650
+ const providerDefaultIndex = (() => {
651
+ if (currentRef) {
652
+ return Math.max(0, providerOptions.findIndex((option) => option.value === currentRef.provider));
653
+ }
654
+ if (preferDifferentProviderFrom) {
655
+ const preferred = providerOptions.findIndex((option) =>
656
+ typeof option.value === "string"
657
+ && option.value !== "inherit"
658
+ && option.value !== preferDifferentProviderFrom
659
+ );
660
+ if (preferred >= 0) return preferred;
661
+ }
662
+ return 0;
663
+ })();
547
664
 
548
665
  const providerChoice = await promptMenuChoice({
549
666
  title: `${roleLabel}: choose model provider`,
@@ -594,14 +711,28 @@ async function promptThinkingForRole(roleLabel, {
594
711
  askImpl = ask,
595
712
  logImpl = console.log,
596
713
  currentThinking = "",
714
+ currentModel = "",
715
+ availableModels = [],
597
716
  } = {}) {
598
717
  const thinkingOptions = [
599
718
  { value: "", label: "inherit (use current session thinking)", aliases: ["inherit"] },
600
- { value: "on", label: "on" },
601
719
  { value: "off", label: "off" },
720
+ { value: "minimal", label: "minimal" },
721
+ { value: "low", label: "low" },
722
+ { value: "medium", label: "medium" },
723
+ { value: "high", label: "high" },
724
+ { value: "xhigh", label: "xhigh" },
602
725
  ];
726
+
727
+ const selectedModel = findModelInDiscovery(availableModels, currentModel);
728
+ if (selectedModel?.supportsThinking === false) {
729
+ logImpl(` ${INFO} ${roleLabel} model does not advertise thinking support (pi says thinking=no).`);
730
+ logImpl(` ${c.dim}You can still set a thinking level; unsupported models ignore it at runtime.${c.reset}`);
731
+ }
732
+
603
733
  const normalized = normalizeThinkingMode(currentThinking);
604
- const defaultIndex = Math.max(0, thinkingOptions.findIndex((option) => option.value === normalized));
734
+ const preferredDefault = normalized || "high";
735
+ const defaultIndex = Math.max(0, thinkingOptions.findIndex((option) => option.value === preferredDefault));
605
736
 
606
737
  return promptMenuChoice({
607
738
  title: `${roleLabel}: choose thinking mode`,
@@ -613,26 +744,13 @@ async function promptThinkingForRole(roleLabel, {
613
744
  });
614
745
  }
615
746
 
616
- function applyInitAgentConfig(projectConfig, initAgentConfig) {
617
- if (!initAgentConfig) return projectConfig;
618
-
619
- projectConfig.taskRunner.worker.model = initAgentConfig.workerModel ?? "";
620
- projectConfig.taskRunner.reviewer.model = initAgentConfig.reviewerModel ?? "";
621
- projectConfig.orchestrator.merge.model = initAgentConfig.mergeModel ?? "";
622
-
623
- projectConfig.taskRunner.worker.thinking = normalizeThinkingMode(initAgentConfig.workerThinking);
624
- projectConfig.taskRunner.reviewer.thinking = normalizeThinkingMode(initAgentConfig.reviewerThinking);
625
- projectConfig.orchestrator.merge.thinking = normalizeThinkingMode(initAgentConfig.mergeThinking);
626
-
627
- return projectConfig;
628
- }
629
-
630
747
  export async function collectInitAgentConfig({
631
748
  interactive = true,
632
749
  askImpl = ask,
633
750
  confirmImpl = confirm,
634
751
  queryModelsImpl = queryAvailableModelsFromPi,
635
752
  loadInitDefaultsImpl = loadInitAgentDefaultsFromPreferences,
753
+ saveInitDefaultsImpl = saveInitAgentDefaultsToPreferences,
636
754
  logImpl = console.log,
637
755
  } = {}) {
638
756
  if (!interactive) return null;
@@ -666,17 +784,34 @@ export async function collectInitAgentConfig({
666
784
  return initAgentConfig;
667
785
  }
668
786
 
787
+ const providerCount = countDistinctProviders(discovery.models);
788
+ const shouldGuideCrossProvider = needsCrossProviderInitGuidance(initAgentConfig, savedDefaults);
789
+ const canGuideCrossProvider = shouldGuideCrossProvider && providerCount >= 2;
790
+ const shouldPersistFromInit = shouldGuideCrossProvider;
791
+
669
792
  logImpl(`\n${c.bold}Agent model setup${c.reset}`);
670
793
  logImpl(` ${c.dim}Choose models for worker/reviewer/merger (inherit is always option #1).${c.reset}`);
671
794
 
795
+ if (canGuideCrossProvider) {
796
+ logImpl(` ${INFO} ${c.bold}First-run recommendation:${c.reset} choose reviewer/merger on a different provider than worker/session.`);
797
+ logImpl(` ${c.dim}Cross-provider review catches blind spots that same-model review can miss.${c.reset}`);
798
+ } else if (shouldGuideCrossProvider) {
799
+ logImpl(` ${INFO} Cross-provider guidance skipped: only one provider is currently available.`);
800
+ logImpl(` ${c.dim}Add another provider later to enable cross-provider reviewer/merger defaults.${c.reset}`);
801
+ }
802
+
672
803
  const modelDefaults = INIT_AGENT_ROLES.map((role) => initAgentConfig[role.modelKey] || "");
673
804
  const thinkingDefaults = INIT_AGENT_ROLES.map((role) => normalizeThinkingMode(initAgentConfig[role.thinkingKey]));
674
805
  const sameModelDefaults = allValuesEqual(modelDefaults);
675
806
  const sameThinkingDefaults = allValuesEqual(thinkingDefaults);
676
- const useSameModel = await confirmImpl(
677
- "Use the same model for worker, reviewer, and merger?",
678
- sameModelDefaults && sameThinkingDefaults,
679
- );
807
+ let useSameModel = false;
808
+
809
+ if (!canGuideCrossProvider) {
810
+ useSameModel = await confirmImpl(
811
+ "Use the same model for worker, reviewer, and merger?",
812
+ sameModelDefaults && sameThinkingDefaults,
813
+ );
814
+ }
680
815
 
681
816
  if (useSameModel) {
682
817
  const selectedModel = await promptModelForRole("All agents", discovery.models, {
@@ -688,49 +823,64 @@ export async function collectInitAgentConfig({
688
823
  askImpl,
689
824
  logImpl,
690
825
  currentThinking: sameThinkingDefaults ? thinkingDefaults[0] : "",
826
+ currentModel: selectedModel,
827
+ availableModels: discovery.models,
691
828
  });
692
829
  for (const role of INIT_AGENT_ROLES) {
693
830
  initAgentConfig[role.modelKey] = selectedModel;
694
831
  initAgentConfig[role.thinkingKey] = selectedThinking;
695
832
  }
833
+ if (shouldPersistFromInit) {
834
+ try {
835
+ saveInitDefaultsImpl(initAgentConfig);
836
+ } catch (error) {
837
+ logImpl(` ${WARN} Could not save first-run defaults: ${error?.message || error}`);
838
+ }
839
+ }
696
840
  return initAgentConfig;
697
841
  }
698
842
 
843
+ let workerProviderHint = splitModelReference(initAgentConfig.workerModel)?.provider || "";
699
844
  for (const role of INIT_AGENT_ROLES) {
845
+ const preferDifferentProviderFrom = canGuideCrossProvider && role.key !== "worker"
846
+ ? workerProviderHint
847
+ : "";
700
848
  initAgentConfig[role.modelKey] = await promptModelForRole(role.label, discovery.models, {
701
849
  askImpl,
702
850
  logImpl,
703
851
  currentModel: initAgentConfig[role.modelKey],
852
+ preferDifferentProviderFrom,
704
853
  });
854
+ if (role.key === "worker") {
855
+ workerProviderHint = splitModelReference(initAgentConfig[role.modelKey])?.provider || workerProviderHint;
856
+ }
705
857
  initAgentConfig[role.thinkingKey] = await promptThinkingForRole(role.label, {
706
858
  askImpl,
707
859
  logImpl,
708
860
  currentThinking: initAgentConfig[role.thinkingKey],
861
+ currentModel: initAgentConfig[role.modelKey],
862
+ availableModels: discovery.models,
709
863
  });
710
864
  }
711
865
 
866
+ if (shouldPersistFromInit) {
867
+ try {
868
+ saveInitDefaultsImpl(initAgentConfig);
869
+ } catch (error) {
870
+ logImpl(` ${WARN} Could not save first-run defaults: ${error?.message || error}`);
871
+ }
872
+ }
873
+
712
874
  return initAgentConfig;
713
875
  }
714
876
 
715
- export function generateProjectConfig(vars, initAgentConfig = null) {
877
+ export function generateProjectConfig(vars, _initAgentConfig = null) {
716
878
  const projectConfig = {
717
879
  configVersion: 1,
718
880
  taskRunner: {
719
881
  project: { name: vars.project_name, description: "" },
720
882
  paths: { tasks: vars.tasks_root },
721
883
  testing: { commands: buildTestingCommands(vars) },
722
- standards: { docs: [], rules: [] },
723
- standardsOverrides: {},
724
- worker: { model: "", tools: "read,write,edit,bash,grep,find,ls", thinking: "" },
725
- reviewer: { model: "", tools: "read,bash,grep,find,ls", thinking: "on" },
726
- context: {
727
- workerContextWindow: 200000,
728
- warnPercent: 70,
729
- killPercent: 85,
730
- maxWorkerIterations: 20,
731
- maxReviewCycles: 2,
732
- noProgressLimit: 3,
733
- },
734
884
  taskAreas: {
735
885
  [vars.default_area]: {
736
886
  path: vars.tasks_root,
@@ -738,44 +888,22 @@ export function generateProjectConfig(vars, initAgentConfig = null) {
738
888
  context: `${vars.tasks_root}/CONTEXT.md`,
739
889
  },
740
890
  },
741
- referenceDocs: {},
742
- neverLoad: [],
743
- selfDocTargets: {},
744
- protectedDocs: [],
745
- },
746
- orchestrator: {
747
- orchestrator: {
748
- maxLanes: vars.max_lanes,
749
- worktreeLocation: "subdirectory",
750
- worktreePrefix: vars.worktree_prefix,
751
- batchIdFormat: "timestamp",
752
- spawnMode: vars.spawn_mode,
753
- sessionPrefix: vars.session_prefix,
754
- operatorId: "",
755
- },
756
- dependencies: { source: "prompt", cache: true },
757
- assignment: { strategy: "affinity-first", sizeWeights: { S: 1, M: 2, L: 4 } },
758
- preWarm: { autoDetect: false, commands: {}, always: [] },
759
- merge: {
760
- model: "",
761
- thinking: "",
762
- tools: "read,write,edit,bash,grep,find,ls",
763
- verify: [],
764
- order: "fewest-files-first",
765
- timeoutMinutes: 10,
766
- },
767
- failure: {
768
- onTaskFailure: "skip-dependents",
769
- onMergeFailure: "pause",
770
- stallTimeout: 30,
771
- maxWorkerMinutes: 30,
772
- abortGracePeriod: 60,
773
- },
774
- monitoring: { pollInterval: 5 },
775
891
  },
776
892
  };
777
893
 
778
- return applyInitAgentConfig(projectConfig, initAgentConfig);
894
+ const explicit = vars.explicit_orchestrator_overrides || {};
895
+ const orchestratorCore = {};
896
+
897
+ if (explicit.maxLanes) orchestratorCore.maxLanes = vars.max_lanes;
898
+ if (explicit.worktreePrefix) orchestratorCore.worktreePrefix = vars.worktree_prefix;
899
+ if (explicit.sessionPrefix) orchestratorCore.sessionPrefix = vars.session_prefix;
900
+ if (explicit.spawnMode) orchestratorCore.spawnMode = vars.spawn_mode;
901
+
902
+ if (Object.keys(orchestratorCore).length > 0) {
903
+ projectConfig.orchestrator = { orchestrator: orchestratorCore };
904
+ }
905
+
906
+ return projectConfig;
779
907
  }
780
908
 
781
909
  function generateWorkspaceYaml(repoNames, defaultRepo, tasksRoot) {
@@ -958,7 +1086,7 @@ function cmdConfig(args) {
958
1086
  console.log(`\n${c.bold}Taskplane Config${c.reset}\n`);
959
1087
  console.log(` ${c.cyan}taskplane config --save-as-defaults${c.reset}`);
960
1088
  console.log(` Save worker/reviewer/merger model + thinking settings from this project`);
961
- console.log(` to ${c.cyan}${resolveUserPreferencesPathForCli()}${c.reset} for future ${c.cyan}taskplane init${c.reset} runs.\n`);
1089
+ console.log(` to ${c.cyan}${resolveGlobalPreferencesPathForCli()}${c.reset} for future ${c.cyan}taskplane init${c.reset} runs.\n`);
962
1090
  return;
963
1091
  }
964
1092
 
@@ -2162,6 +2290,7 @@ function getPresetVars(preset, projectRoot, tasksRootOverride = null) {
2162
2290
  default_prefix: "TP",
2163
2291
  test_cmd,
2164
2292
  build_cmd,
2293
+ explicit_orchestrator_overrides: {},
2165
2294
  date: today(),
2166
2295
  };
2167
2296
  }
@@ -2171,7 +2300,8 @@ async function getInteractiveVars(projectRoot, tasksRootOverride = null) {
2171
2300
  const detected = detectStack(projectRoot);
2172
2301
 
2173
2302
  const project_name = await ask("Project name", dirName);
2174
- const max_lanes = parseInt(await ask("Max parallel lanes", "3")) || 3;
2303
+ const maxLanesInput = await ask("Max parallel lanes", "3");
2304
+ const max_lanes = parseInt(maxLanesInput, 10) || 3;
2175
2305
  const tasks_root = tasksRootOverride || await ask("Tasks directory", "taskplane-tasks");
2176
2306
  const default_area = await ask("Default area name", "general");
2177
2307
  const default_prefix = await ask("Task ID prefix", "TP");
@@ -2179,6 +2309,11 @@ async function getInteractiveVars(projectRoot, tasksRootOverride = null) {
2179
2309
  const build_cmd = await ask("Build command (agents run this after tests — blank to skip)", detected.build || "");
2180
2310
 
2181
2311
  const slug = slugify(project_name);
2312
+ const explicit_orchestrator_overrides = {};
2313
+ if (max_lanes !== 3) {
2314
+ explicit_orchestrator_overrides.maxLanes = true;
2315
+ }
2316
+
2182
2317
  return {
2183
2318
  project_name,
2184
2319
  max_lanes,
@@ -2189,6 +2324,7 @@ async function getInteractiveVars(projectRoot, tasksRootOverride = null) {
2189
2324
  default_prefix,
2190
2325
  test_cmd,
2191
2326
  build_cmd,
2327
+ explicit_orchestrator_overrides,
2192
2328
  date: today(),
2193
2329
  };
2194
2330
  }
@@ -3110,7 +3246,7 @@ ${c.bold}Dashboard options:${c.reset}
3110
3246
 
3111
3247
  ${c.bold}Config options:${c.reset}
3112
3248
  --save-as-defaults Save current project's worker/reviewer/merger model + thinking
3113
- settings to user preferences for future taskplane init runs
3249
+ settings to global preferences for future taskplane init runs
3114
3250
 
3115
3251
  ${c.bold}Uninstall options:${c.reset}
3116
3252
  --dry-run Show what would be removed
@@ -2713,116 +2713,8 @@ export default function (pi: ExtensionAPI) {
2713
2713
  return true;
2714
2714
  }
2715
2715
 
2716
- pi.registerCommand("task", {
2717
- description: "⚠️ [Deprecated] Start executing a task: /task <path/to/PROMPT.md>",
2718
- handler: async (args, ctx) => {
2719
- widgetCtx = ctx;
2720
- ctx.ui.notify(
2721
- "⚠️ /task is deprecated. Use /orch instead — it provides worktree isolation, " +
2722
- "dashboard, inline reviews, and supervisor monitoring. " +
2723
- "/task will be removed in a future major version.",
2724
- "warning",
2725
- );
2726
- const promptPath = args?.trim();
2727
- if (!promptPath) {
2728
- ctx.ui.notify("Usage: /task <path/to/PROMPT.md>", "error");
2729
- return;
2730
- }
2731
-
2732
- const fullPath = resolve(ctx.cwd, promptPath);
2733
- if (!existsSync(fullPath)) {
2734
- ctx.ui.notify(`File not found: ${promptPath}`, "error");
2735
- return;
2736
- }
2737
-
2738
- startTaskFromPath(ctx, fullPath);
2739
- },
2740
- });
2741
-
2742
- pi.registerCommand("task-status", {
2743
- description: "⚠️ [Deprecated] Show current task progress",
2744
- handler: async (_args, ctx) => {
2745
- widgetCtx = ctx;
2746
- ctx.ui.notify(
2747
- "⚠️ /task-status is deprecated. Use the dashboard (`taskplane dashboard`) or `/orch-status` instead.",
2748
- "warning",
2749
- );
2750
- if (!state.task) {
2751
- ctx.ui.notify("No task loaded. Use /task <path/to/PROMPT.md>", "info");
2752
- return;
2753
- }
2754
-
2755
- const statusPath = join(state.task.taskFolder, "STATUS.md");
2756
- if (!existsSync(statusPath)) {
2757
- ctx.ui.notify("STATUS.md not found", "error");
2758
- return;
2759
- }
2760
-
2761
- const parsed = parseStatusMd(readFileSync(statusPath, "utf-8"));
2762
- const lines = parsed.steps.map(s => {
2763
- const icon = s.status === "complete" ? "✅" : s.status === "in-progress" ? "🟨" : "⬜";
2764
- return `${icon} Step ${s.number}: ${s.name} (${s.totalChecked}/${s.totalItems})`;
2765
- });
2766
-
2767
- ctx.ui.notify(
2768
- `${state.task.taskId}: ${state.task.taskName}\n` +
2769
- `Phase: ${state.phase} · Iteration: ${state.totalIterations} · Reviews: ${state.reviewCounter}\n\n` +
2770
- lines.join("\n"),
2771
- "info",
2772
- );
2773
-
2774
- // Refresh widget
2775
- for (const s of parsed.steps) state.stepStatuses.set(s.number, s);
2776
- updateWidgets();
2777
- },
2778
- });
2779
-
2780
- pi.registerCommand("task-pause", {
2781
- description: "⚠️ [Deprecated] Pause task after current worker finishes",
2782
- handler: async (_args, ctx) => {
2783
- widgetCtx = ctx;
2784
- ctx.ui.notify(
2785
- "⚠️ /task-pause is deprecated. Use `/orch-pause` instead.",
2786
- "warning",
2787
- );
2788
- if (state.phase !== "running") {
2789
- ctx.ui.notify("No task is running", "warning");
2790
- return;
2791
- }
2792
- state.phase = "paused";
2793
- ctx.ui.notify("Task will pause after current worker finishes", "info");
2794
- updateWidgets();
2795
- },
2796
- });
2797
-
2798
- pi.registerCommand("task-resume", {
2799
- description: "⚠️ [Deprecated] Resume a paused task",
2800
- handler: async (_args, ctx) => {
2801
- widgetCtx = ctx;
2802
- ctx.ui.notify(
2803
- "⚠️ /task-resume is deprecated. Use `/orch-resume` instead.",
2804
- "warning",
2805
- );
2806
- if (state.phase !== "paused") {
2807
- ctx.ui.notify("Task is not paused", "warning");
2808
- return;
2809
- }
2810
- if (!state.task) {
2811
- ctx.ui.notify("No task loaded", "error");
2812
- return;
2813
- }
2814
-
2815
- state.phase = "running";
2816
- ctx.ui.notify(`Resuming ${state.task.taskId}...`, "info");
2817
- updateWidgets();
2818
-
2819
- executeTask(ctx).catch(err => {
2820
- state.phase = "error";
2821
- ctx.ui.notify(`Task error: ${err?.message || err}`, "error");
2822
- updateWidgets();
2823
- });
2824
- },
2825
- });
2716
+ // /task, /task-status, /task-pause, /task-resume removed.
2717
+ // These were deprecated in favor of /orch. Runtime V2 is the only execution path.
2826
2718
 
2827
2719
  // ── Session Lifecycle ────────────────────────────────────────────
2828
2720