taskplane 0.24.21 → 0.24.23
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 +1 -1
- package/bin/taskplane.mjs +239 -103
- package/extensions/task-runner.ts +2 -110
- package/extensions/taskplane/agent-bridge-extension.ts +196 -3
- package/extensions/taskplane/config-loader.ts +306 -269
- package/extensions/taskplane/config-schema.ts +49 -32
- package/extensions/taskplane/engine-worker.ts +4 -0
- package/extensions/taskplane/engine.ts +884 -12
- package/extensions/taskplane/execution.ts +12 -1
- package/extensions/taskplane/extension.ts +4 -0
- package/extensions/taskplane/lane-runner.ts +5 -0
- package/extensions/taskplane/merge.ts +7 -1
- package/extensions/taskplane/persistence.ts +12 -0
- package/extensions/taskplane/resume.ts +132 -22
- package/extensions/taskplane/settings-tui.ts +149 -172
- package/extensions/taskplane/supervisor.ts +2 -2
- package/extensions/taskplane/types.ts +82 -17
- package/package.json +1 -1
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
|
|
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
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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
|
|
386
|
-
const
|
|
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 === "
|
|
402
|
-
if (cleaned === "
|
|
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
|
|
468
|
+
function resolveGlobalPreferencesPathForCli() {
|
|
426
469
|
const agentDir = process.env.PI_CODING_AGENT_DIR;
|
|
427
470
|
if (agentDir) {
|
|
428
|
-
return path.join(agentDir,
|
|
471
|
+
return path.join(agentDir, GLOBAL_PREFERENCES_SUBDIR, GLOBAL_PREFERENCES_FILENAME);
|
|
429
472
|
}
|
|
430
|
-
return path.join(homedir(), ".pi", "agent",
|
|
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
|
|
434
|
-
const
|
|
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(
|
|
444
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
445
|
-
return {
|
|
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 {
|
|
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 } =
|
|
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 } =
|
|
550
|
+
const { raw, prefsPath } = readGlobalPreferencesForCli();
|
|
469
551
|
raw.initAgentDefaults = sanitizeInitAgentConfig(initAgentConfig);
|
|
470
|
-
|
|
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 =
|
|
545
|
-
|
|
546
|
-
|
|
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
|
|
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
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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,
|
|
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
|
-
|
|
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}${
|
|
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
|
|
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
|
|
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
|
-
|
|
2717
|
-
|
|
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
|
|