taskplane 0.24.19 → 0.24.21
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 +4 -2
- package/bin/taskplane.mjs +581 -36
- package/extensions/task-runner.ts +18 -18
- package/extensions/taskplane/config-loader.ts +1221 -1161
- package/extensions/taskplane/config-schema.ts +20 -2
- package/extensions/taskplane/merge.ts +12 -1
- package/extensions/taskplane/settings-tui.ts +167 -16
- package/package.json +1 -1
- package/templates/config/task-runner.yaml +1 -1
package/bin/taskplane.mjs
CHANGED
|
@@ -25,6 +25,7 @@ if (nodeMajor < MIN_NODE_MAJOR) {
|
|
|
25
25
|
import fs from "node:fs";
|
|
26
26
|
import path from "node:path";
|
|
27
27
|
import readline from "node:readline";
|
|
28
|
+
import { homedir } from "node:os";
|
|
28
29
|
import { fileURLToPath } from "node:url";
|
|
29
30
|
import { execSync, execFileSync, spawn } from "node:child_process";
|
|
30
31
|
import {
|
|
@@ -135,6 +136,110 @@ function getVersion(cmd, flag = "--version") {
|
|
|
135
136
|
}
|
|
136
137
|
}
|
|
137
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Parse the tabular output from `pi --list-models` into structured model rows.
|
|
141
|
+
*
|
|
142
|
+
* Expected format:
|
|
143
|
+
* provider model context ...
|
|
144
|
+
* anthropic claude-sonnet-4-6 ...
|
|
145
|
+
*/
|
|
146
|
+
export function parsePiListModelsOutput(rawOutput) {
|
|
147
|
+
if (typeof rawOutput !== "string" || rawOutput.trim() === "") return [];
|
|
148
|
+
|
|
149
|
+
const rows = rawOutput.split(/\r?\n/);
|
|
150
|
+
const parsed = new Map();
|
|
151
|
+
|
|
152
|
+
for (const row of rows) {
|
|
153
|
+
const trimmed = row.trim();
|
|
154
|
+
if (!trimmed) continue;
|
|
155
|
+
if (/^provider\s+model\b/i.test(trimmed)) continue;
|
|
156
|
+
|
|
157
|
+
const parts = trimmed.split(/\s+/);
|
|
158
|
+
if (parts.length < 2) continue;
|
|
159
|
+
|
|
160
|
+
const provider = parts[0].trim();
|
|
161
|
+
const id = parts[1].trim();
|
|
162
|
+
if (!provider || !id) continue;
|
|
163
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/i.test(provider)) continue;
|
|
164
|
+
if (!/^[^\s]+$/.test(id)) continue;
|
|
165
|
+
|
|
166
|
+
const key = `${provider.toLowerCase()}/${id.toLowerCase()}`;
|
|
167
|
+
if (parsed.has(key)) continue;
|
|
168
|
+
|
|
169
|
+
parsed.set(key, {
|
|
170
|
+
provider,
|
|
171
|
+
id,
|
|
172
|
+
displayName: `${provider}/${id}`,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return [...parsed.values()].sort((a, b) =>
|
|
177
|
+
a.provider.localeCompare(b.provider) || a.id.localeCompare(b.id)
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function extractExecFailure(error) {
|
|
182
|
+
if (!error || typeof error !== "object") return "Unknown error";
|
|
183
|
+
const err = error;
|
|
184
|
+
if (typeof err.stderr === "string" && err.stderr.trim()) return err.stderr.trim();
|
|
185
|
+
if (Buffer.isBuffer(err.stderr)) {
|
|
186
|
+
const msg = err.stderr.toString("utf-8").trim();
|
|
187
|
+
if (msg) return msg;
|
|
188
|
+
}
|
|
189
|
+
if (typeof err.message === "string" && err.message.trim()) return err.message.trim();
|
|
190
|
+
return "Unknown error";
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Query available models from pi in standalone CLI context.
|
|
195
|
+
*
|
|
196
|
+
* Returns a structured model list and diagnostics; never throws.
|
|
197
|
+
*/
|
|
198
|
+
export function queryAvailableModelsFromPi({
|
|
199
|
+
execFileSyncImpl = execFileSync,
|
|
200
|
+
commandExistsImpl = commandExists,
|
|
201
|
+
timeoutMs = 10000,
|
|
202
|
+
} = {}) {
|
|
203
|
+
if (!commandExistsImpl("pi")) {
|
|
204
|
+
return {
|
|
205
|
+
models: [],
|
|
206
|
+
source: "pi --list-models",
|
|
207
|
+
available: false,
|
|
208
|
+
error: "pi is not available on PATH",
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const output = execFileSyncImpl("pi", ["--list-models"], {
|
|
214
|
+
encoding: "utf-8",
|
|
215
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
216
|
+
timeout: timeoutMs,
|
|
217
|
+
});
|
|
218
|
+
const models = parsePiListModelsOutput(output);
|
|
219
|
+
if (models.length === 0) {
|
|
220
|
+
return {
|
|
221
|
+
models: [],
|
|
222
|
+
source: "pi --list-models",
|
|
223
|
+
available: false,
|
|
224
|
+
error: "pi returned no parseable model rows",
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
models,
|
|
229
|
+
source: "pi --list-models",
|
|
230
|
+
available: true,
|
|
231
|
+
error: null,
|
|
232
|
+
};
|
|
233
|
+
} catch (error) {
|
|
234
|
+
return {
|
|
235
|
+
models: [],
|
|
236
|
+
source: "pi --list-models",
|
|
237
|
+
available: false,
|
|
238
|
+
error: extractExecFailure(error),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
138
243
|
/** Write a file, creating parent directories as needed. Optionally skip if exists. */
|
|
139
244
|
function writeFile(dest, content, { skipIfExists = false, label = "" } = {}) {
|
|
140
245
|
if (skipIfExists && fs.existsSync(dest)) {
|
|
@@ -277,8 +382,338 @@ function buildTestingCommands(vars) {
|
|
|
277
382
|
return commands;
|
|
278
383
|
}
|
|
279
384
|
|
|
280
|
-
|
|
385
|
+
const USER_PREFERENCES_SUBDIR = "taskplane";
|
|
386
|
+
const USER_PREFERENCES_FILENAME = "preferences.json";
|
|
387
|
+
|
|
388
|
+
function createInheritInitAgentConfig() {
|
|
281
389
|
return {
|
|
390
|
+
workerModel: "",
|
|
391
|
+
reviewerModel: "",
|
|
392
|
+
mergeModel: "",
|
|
393
|
+
workerThinking: "",
|
|
394
|
+
reviewerThinking: "",
|
|
395
|
+
mergeThinking: "",
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function normalizeThinkingMode(value) {
|
|
400
|
+
const cleaned = String(value ?? "").trim().toLowerCase();
|
|
401
|
+
if (cleaned === "on") return "on";
|
|
402
|
+
if (cleaned === "off") return "off";
|
|
403
|
+
return "";
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function normalizeModelValue(value) {
|
|
407
|
+
const trimmed = String(value ?? "").trim();
|
|
408
|
+
return trimmed.toLowerCase() === "inherit" ? "" : trimmed;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function sanitizeInitAgentConfig(raw) {
|
|
412
|
+
const defaults = createInheritInitAgentConfig();
|
|
413
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return defaults;
|
|
414
|
+
|
|
415
|
+
if (typeof raw.workerModel === "string") defaults.workerModel = normalizeModelValue(raw.workerModel);
|
|
416
|
+
if (typeof raw.reviewerModel === "string") defaults.reviewerModel = normalizeModelValue(raw.reviewerModel);
|
|
417
|
+
if (typeof raw.mergeModel === "string") defaults.mergeModel = normalizeModelValue(raw.mergeModel);
|
|
418
|
+
if (raw.workerThinking !== undefined) defaults.workerThinking = normalizeThinkingMode(raw.workerThinking);
|
|
419
|
+
if (raw.reviewerThinking !== undefined) defaults.reviewerThinking = normalizeThinkingMode(raw.reviewerThinking);
|
|
420
|
+
if (raw.mergeThinking !== undefined) defaults.mergeThinking = normalizeThinkingMode(raw.mergeThinking);
|
|
421
|
+
|
|
422
|
+
return defaults;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function resolveUserPreferencesPathForCli() {
|
|
426
|
+
const agentDir = process.env.PI_CODING_AGENT_DIR;
|
|
427
|
+
if (agentDir) {
|
|
428
|
+
return path.join(agentDir, USER_PREFERENCES_SUBDIR, USER_PREFERENCES_FILENAME);
|
|
429
|
+
}
|
|
430
|
+
return path.join(homedir(), ".pi", "agent", USER_PREFERENCES_SUBDIR, USER_PREFERENCES_FILENAME);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function readUserPreferencesForCli() {
|
|
434
|
+
const prefsPath = resolveUserPreferencesPathForCli();
|
|
435
|
+
if (!fs.existsSync(prefsPath)) {
|
|
436
|
+
return {
|
|
437
|
+
prefsPath,
|
|
438
|
+
raw: {},
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
try {
|
|
443
|
+
const raw = JSON.parse(fs.readFileSync(prefsPath, "utf-8"));
|
|
444
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
445
|
+
return { prefsPath, raw: {} };
|
|
446
|
+
}
|
|
447
|
+
return { prefsPath, raw };
|
|
448
|
+
} catch {
|
|
449
|
+
return { prefsPath, raw: {} };
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
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
|
+
function loadInitAgentDefaultsFromPreferences() {
|
|
461
|
+
const { prefsPath, raw } = readUserPreferencesForCli();
|
|
462
|
+
const defaults = sanitizeInitAgentConfig(raw.initAgentDefaults);
|
|
463
|
+
const hasDefaults = !!(raw.initAgentDefaults && typeof raw.initAgentDefaults === "object" && !Array.isArray(raw.initAgentDefaults));
|
|
464
|
+
return { defaults, hasDefaults, prefsPath };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function saveInitAgentDefaultsToPreferences(initAgentConfig) {
|
|
468
|
+
const { raw } = readUserPreferencesForCli();
|
|
469
|
+
raw.initAgentDefaults = sanitizeInitAgentConfig(initAgentConfig);
|
|
470
|
+
const prefsPath = writeUserPreferencesForCli(raw);
|
|
471
|
+
return { prefsPath, saved: raw.initAgentDefaults };
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function splitModelReference(modelRef) {
|
|
475
|
+
const trimmed = String(modelRef ?? "").trim();
|
|
476
|
+
if (!trimmed.includes("/")) return null;
|
|
477
|
+
const [provider, ...idParts] = trimmed.split("/");
|
|
478
|
+
const id = idParts.join("/").trim();
|
|
479
|
+
if (!provider || !id) return null;
|
|
480
|
+
return { provider, id };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function allValuesEqual(values) {
|
|
484
|
+
if (!Array.isArray(values) || values.length === 0) return true;
|
|
485
|
+
const first = values[0];
|
|
486
|
+
return values.every((value) => value === first);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const INIT_AGENT_ROLES = [
|
|
490
|
+
{ key: "worker", label: "Worker", modelKey: "workerModel", thinkingKey: "workerThinking" },
|
|
491
|
+
{ key: "reviewer", label: "Reviewer", modelKey: "reviewerModel", thinkingKey: "reviewerThinking" },
|
|
492
|
+
{ key: "merge", label: "Merger", modelKey: "mergeModel", thinkingKey: "mergeThinking" },
|
|
493
|
+
];
|
|
494
|
+
|
|
495
|
+
async function promptMenuChoice({ title, question, options, defaultIndex = 0, askImpl = ask, logImpl = console.log }) {
|
|
496
|
+
while (true) {
|
|
497
|
+
if (title) logImpl(`\n ${title}`);
|
|
498
|
+
for (let i = 0; i < options.length; i++) {
|
|
499
|
+
logImpl(` ${i + 1}. ${options[i].label}`);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const resolvedDefault = Number.isInteger(defaultIndex) && defaultIndex >= 0 && defaultIndex < options.length
|
|
503
|
+
? defaultIndex
|
|
504
|
+
: 0;
|
|
505
|
+
const answer = String(await askImpl(question, String(resolvedDefault + 1))).trim();
|
|
506
|
+
const asNum = Number.parseInt(answer, 10);
|
|
507
|
+
if (!Number.isNaN(asNum) && asNum >= 1 && asNum <= options.length) {
|
|
508
|
+
return options[asNum - 1].value;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const lower = answer.toLowerCase();
|
|
512
|
+
const byAlias = options.find((option) => {
|
|
513
|
+
const aliases = [option.value, ...(option.aliases || [])]
|
|
514
|
+
.filter(Boolean)
|
|
515
|
+
.map((entry) => String(entry).toLowerCase());
|
|
516
|
+
return aliases.includes(lower);
|
|
517
|
+
});
|
|
518
|
+
if (byAlias) return byAlias.value;
|
|
519
|
+
|
|
520
|
+
logImpl(` ${WARN} Invalid selection. Enter a menu number.`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
async function promptModelForRole(roleLabel, models, {
|
|
525
|
+
askImpl = ask,
|
|
526
|
+
logImpl = console.log,
|
|
527
|
+
currentModel = "",
|
|
528
|
+
} = {}) {
|
|
529
|
+
const providers = [...new Set(models.map((model) => model.provider))].sort((a, b) => a.localeCompare(b));
|
|
530
|
+
const currentRef = splitModelReference(currentModel);
|
|
531
|
+
|
|
532
|
+
while (true) {
|
|
533
|
+
const providerOptions = [
|
|
534
|
+
{ value: "inherit", label: "inherit (use current session model)", aliases: ["inherit"] },
|
|
535
|
+
...providers.map((provider) => {
|
|
536
|
+
const count = models.filter((model) => model.provider === provider).length;
|
|
537
|
+
return {
|
|
538
|
+
value: provider,
|
|
539
|
+
label: `${provider} (${count} models)`,
|
|
540
|
+
aliases: [provider],
|
|
541
|
+
};
|
|
542
|
+
}),
|
|
543
|
+
];
|
|
544
|
+
const providerDefaultIndex = currentRef
|
|
545
|
+
? Math.max(0, providerOptions.findIndex((option) => option.value === currentRef.provider))
|
|
546
|
+
: 0;
|
|
547
|
+
|
|
548
|
+
const providerChoice = await promptMenuChoice({
|
|
549
|
+
title: `${roleLabel}: choose model provider`,
|
|
550
|
+
question: `${roleLabel} provider (number or provider name)`,
|
|
551
|
+
options: providerOptions,
|
|
552
|
+
defaultIndex: providerDefaultIndex,
|
|
553
|
+
askImpl,
|
|
554
|
+
logImpl,
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
if (providerChoice === "inherit") return "";
|
|
558
|
+
|
|
559
|
+
const providerModels = models
|
|
560
|
+
.filter((model) => model.provider === providerChoice)
|
|
561
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
562
|
+
|
|
563
|
+
const modelOptions = [
|
|
564
|
+
{ value: "back", label: "← back to providers", aliases: ["back"] },
|
|
565
|
+
...providerModels.map((model) => ({
|
|
566
|
+
value: model.id,
|
|
567
|
+
label: model.id,
|
|
568
|
+
aliases: [model.id, `${model.provider}/${model.id}`],
|
|
569
|
+
})),
|
|
570
|
+
];
|
|
571
|
+
const modelDefaultIndex = (() => {
|
|
572
|
+
if (currentRef?.provider === providerChoice) {
|
|
573
|
+
const idx = providerModels.findIndex((model) => model.id === currentRef.id);
|
|
574
|
+
if (idx >= 0) return idx + 1;
|
|
575
|
+
}
|
|
576
|
+
return providerModels.length > 0 ? 1 : 0;
|
|
577
|
+
})();
|
|
578
|
+
|
|
579
|
+
const modelChoice = await promptMenuChoice({
|
|
580
|
+
title: `${roleLabel}: choose model (${providerChoice})`,
|
|
581
|
+
question: `${roleLabel} model (number or model id)`,
|
|
582
|
+
options: modelOptions,
|
|
583
|
+
defaultIndex: modelDefaultIndex,
|
|
584
|
+
askImpl,
|
|
585
|
+
logImpl,
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
if (modelChoice === "back") continue;
|
|
589
|
+
return `${providerChoice}/${modelChoice}`;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
async function promptThinkingForRole(roleLabel, {
|
|
594
|
+
askImpl = ask,
|
|
595
|
+
logImpl = console.log,
|
|
596
|
+
currentThinking = "",
|
|
597
|
+
} = {}) {
|
|
598
|
+
const thinkingOptions = [
|
|
599
|
+
{ value: "", label: "inherit (use current session thinking)", aliases: ["inherit"] },
|
|
600
|
+
{ value: "on", label: "on" },
|
|
601
|
+
{ value: "off", label: "off" },
|
|
602
|
+
];
|
|
603
|
+
const normalized = normalizeThinkingMode(currentThinking);
|
|
604
|
+
const defaultIndex = Math.max(0, thinkingOptions.findIndex((option) => option.value === normalized));
|
|
605
|
+
|
|
606
|
+
return promptMenuChoice({
|
|
607
|
+
title: `${roleLabel}: choose thinking mode`,
|
|
608
|
+
question: `${roleLabel} thinking (number or value)`,
|
|
609
|
+
options: thinkingOptions,
|
|
610
|
+
defaultIndex,
|
|
611
|
+
askImpl,
|
|
612
|
+
logImpl,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
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
|
+
export async function collectInitAgentConfig({
|
|
631
|
+
interactive = true,
|
|
632
|
+
askImpl = ask,
|
|
633
|
+
confirmImpl = confirm,
|
|
634
|
+
queryModelsImpl = queryAvailableModelsFromPi,
|
|
635
|
+
loadInitDefaultsImpl = loadInitAgentDefaultsFromPreferences,
|
|
636
|
+
logImpl = console.log,
|
|
637
|
+
} = {}) {
|
|
638
|
+
if (!interactive) return null;
|
|
639
|
+
|
|
640
|
+
const savedDefaults = await loadInitDefaultsImpl();
|
|
641
|
+
const initAgentConfig = sanitizeInitAgentConfig(savedDefaults?.defaults);
|
|
642
|
+
const hasSavedDefaults = !!savedDefaults?.hasDefaults;
|
|
643
|
+
|
|
644
|
+
if (hasSavedDefaults) {
|
|
645
|
+
logImpl(`\n ${INFO} Loaded saved init defaults from ${savedDefaults.prefsPath}`);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
let discovery;
|
|
649
|
+
try {
|
|
650
|
+
discovery = await queryModelsImpl();
|
|
651
|
+
} catch (error) {
|
|
652
|
+
discovery = {
|
|
653
|
+
models: [],
|
|
654
|
+
available: false,
|
|
655
|
+
error: error?.message || "unknown error",
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (!discovery?.available || !Array.isArray(discovery.models) || discovery.models.length === 0) {
|
|
660
|
+
logImpl(`\n ${WARN} Model list unavailable (${discovery?.error || "unknown"}).`);
|
|
661
|
+
if (hasSavedDefaults) {
|
|
662
|
+
logImpl(" Using saved defaults from preferences for worker/reviewer/merger.\n");
|
|
663
|
+
} else {
|
|
664
|
+
logImpl(" Skipping model picker and using inherit defaults for worker/reviewer/merger.\n");
|
|
665
|
+
}
|
|
666
|
+
return initAgentConfig;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
logImpl(`\n${c.bold}Agent model setup${c.reset}`);
|
|
670
|
+
logImpl(` ${c.dim}Choose models for worker/reviewer/merger (inherit is always option #1).${c.reset}`);
|
|
671
|
+
|
|
672
|
+
const modelDefaults = INIT_AGENT_ROLES.map((role) => initAgentConfig[role.modelKey] || "");
|
|
673
|
+
const thinkingDefaults = INIT_AGENT_ROLES.map((role) => normalizeThinkingMode(initAgentConfig[role.thinkingKey]));
|
|
674
|
+
const sameModelDefaults = allValuesEqual(modelDefaults);
|
|
675
|
+
const sameThinkingDefaults = allValuesEqual(thinkingDefaults);
|
|
676
|
+
const useSameModel = await confirmImpl(
|
|
677
|
+
"Use the same model for worker, reviewer, and merger?",
|
|
678
|
+
sameModelDefaults && sameThinkingDefaults,
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
if (useSameModel) {
|
|
682
|
+
const selectedModel = await promptModelForRole("All agents", discovery.models, {
|
|
683
|
+
askImpl,
|
|
684
|
+
logImpl,
|
|
685
|
+
currentModel: sameModelDefaults ? modelDefaults[0] : "",
|
|
686
|
+
});
|
|
687
|
+
const selectedThinking = await promptThinkingForRole("All agents", {
|
|
688
|
+
askImpl,
|
|
689
|
+
logImpl,
|
|
690
|
+
currentThinking: sameThinkingDefaults ? thinkingDefaults[0] : "",
|
|
691
|
+
});
|
|
692
|
+
for (const role of INIT_AGENT_ROLES) {
|
|
693
|
+
initAgentConfig[role.modelKey] = selectedModel;
|
|
694
|
+
initAgentConfig[role.thinkingKey] = selectedThinking;
|
|
695
|
+
}
|
|
696
|
+
return initAgentConfig;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
for (const role of INIT_AGENT_ROLES) {
|
|
700
|
+
initAgentConfig[role.modelKey] = await promptModelForRole(role.label, discovery.models, {
|
|
701
|
+
askImpl,
|
|
702
|
+
logImpl,
|
|
703
|
+
currentModel: initAgentConfig[role.modelKey],
|
|
704
|
+
});
|
|
705
|
+
initAgentConfig[role.thinkingKey] = await promptThinkingForRole(role.label, {
|
|
706
|
+
askImpl,
|
|
707
|
+
logImpl,
|
|
708
|
+
currentThinking: initAgentConfig[role.thinkingKey],
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
return initAgentConfig;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
export function generateProjectConfig(vars, initAgentConfig = null) {
|
|
716
|
+
const projectConfig = {
|
|
282
717
|
configVersion: 1,
|
|
283
718
|
taskRunner: {
|
|
284
719
|
project: { name: vars.project_name, description: "" },
|
|
@@ -286,8 +721,8 @@ function generateProjectConfig(vars) {
|
|
|
286
721
|
testing: { commands: buildTestingCommands(vars) },
|
|
287
722
|
standards: { docs: [], rules: [] },
|
|
288
723
|
standardsOverrides: {},
|
|
289
|
-
worker: { model: "", tools: "read,write,edit,bash,grep,find,ls", thinking: "
|
|
290
|
-
reviewer: { model: "
|
|
724
|
+
worker: { model: "", tools: "read,write,edit,bash,grep,find,ls", thinking: "" },
|
|
725
|
+
reviewer: { model: "", tools: "read,bash,grep,find,ls", thinking: "on" },
|
|
291
726
|
context: {
|
|
292
727
|
workerContextWindow: 200000,
|
|
293
728
|
warnPercent: 70,
|
|
@@ -323,6 +758,7 @@ function generateProjectConfig(vars) {
|
|
|
323
758
|
preWarm: { autoDetect: false, commands: {}, always: [] },
|
|
324
759
|
merge: {
|
|
325
760
|
model: "",
|
|
761
|
+
thinking: "",
|
|
326
762
|
tools: "read,write,edit,bash,grep,find,ls",
|
|
327
763
|
verify: [],
|
|
328
764
|
order: "fewest-files-first",
|
|
@@ -338,6 +774,8 @@ function generateProjectConfig(vars) {
|
|
|
338
774
|
monitoring: { pollInterval: 5 },
|
|
339
775
|
},
|
|
340
776
|
};
|
|
777
|
+
|
|
778
|
+
return applyInitAgentConfig(projectConfig, initAgentConfig);
|
|
341
779
|
}
|
|
342
780
|
|
|
343
781
|
function generateWorkspaceYaml(repoNames, defaultRepo, tasksRoot) {
|
|
@@ -483,6 +921,71 @@ function listExampleTaskTemplates() {
|
|
|
483
921
|
}
|
|
484
922
|
}
|
|
485
923
|
|
|
924
|
+
function inferTaskplaneInstallScope() {
|
|
925
|
+
return /[\\/]\.pi[\\/]/.test(PACKAGE_ROOT) ? "local" : "global";
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function resolveProjectConfigJsonPath(projectRoot) {
|
|
929
|
+
const pointerPath = path.join(projectRoot, ".pi", "taskplane-pointer.json");
|
|
930
|
+
if (fs.existsSync(pointerPath)) {
|
|
931
|
+
try {
|
|
932
|
+
const pointer = JSON.parse(fs.readFileSync(pointerPath, "utf-8"));
|
|
933
|
+
if (pointer?.config_repo && pointer?.config_path) {
|
|
934
|
+
const pointedPath = path.resolve(projectRoot, pointer.config_repo, pointer.config_path, "taskplane-config.json");
|
|
935
|
+
if (fs.existsSync(pointedPath)) return pointedPath;
|
|
936
|
+
}
|
|
937
|
+
} catch {
|
|
938
|
+
// fall through to local .pi path
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
return path.join(projectRoot, ".pi", "taskplane-config.json");
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function extractInitAgentConfigFromProjectConfig(projectConfig) {
|
|
946
|
+
return sanitizeInitAgentConfig({
|
|
947
|
+
workerModel: projectConfig?.taskRunner?.worker?.model,
|
|
948
|
+
reviewerModel: projectConfig?.taskRunner?.reviewer?.model,
|
|
949
|
+
mergeModel: projectConfig?.orchestrator?.merge?.model,
|
|
950
|
+
workerThinking: projectConfig?.taskRunner?.worker?.thinking,
|
|
951
|
+
reviewerThinking: projectConfig?.taskRunner?.reviewer?.thinking,
|
|
952
|
+
mergeThinking: projectConfig?.orchestrator?.merge?.thinking,
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function cmdConfig(args) {
|
|
957
|
+
if (!args.includes("--save-as-defaults")) {
|
|
958
|
+
console.log(`\n${c.bold}Taskplane Config${c.reset}\n`);
|
|
959
|
+
console.log(` ${c.cyan}taskplane config --save-as-defaults${c.reset}`);
|
|
960
|
+
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`);
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const projectRoot = process.cwd();
|
|
966
|
+
const configPath = resolveProjectConfigJsonPath(projectRoot);
|
|
967
|
+
if (!fs.existsSync(configPath)) {
|
|
968
|
+
die(`Project config not found at ${configPath}. Run ${c.cyan}taskplane init${c.reset} first.`);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
let projectConfig;
|
|
972
|
+
try {
|
|
973
|
+
projectConfig = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
974
|
+
} catch (error) {
|
|
975
|
+
die(`Could not read ${configPath}: ${error?.message || error}`);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const defaults = extractInitAgentConfigFromProjectConfig(projectConfig);
|
|
979
|
+
const { prefsPath, saved } = saveInitAgentDefaultsToPreferences(defaults);
|
|
980
|
+
|
|
981
|
+
console.log(`\n${OK} ${c.bold}Saved init defaults.${c.reset}`);
|
|
982
|
+
console.log(` Source: ${c.cyan}${configPath}${c.reset}`);
|
|
983
|
+
console.log(` Target: ${c.cyan}${prefsPath}${c.reset}`);
|
|
984
|
+
console.log(` worker: ${saved.workerModel || "inherit"} (${saved.workerThinking || "inherit"})`);
|
|
985
|
+
console.log(` reviewer: ${saved.reviewerModel || "inherit"} (${saved.reviewerThinking || "inherit"})`);
|
|
986
|
+
console.log(` merger: ${saved.mergeModel || "inherit"} (${saved.mergeThinking || "inherit"})\n`);
|
|
987
|
+
}
|
|
988
|
+
|
|
486
989
|
async function cmdUninstall(args) {
|
|
487
990
|
const projectRoot = process.cwd();
|
|
488
991
|
const dryRun = args.includes("--dry-run");
|
|
@@ -540,7 +1043,7 @@ async function cmdUninstall(args) {
|
|
|
540
1043
|
.filter(({ abs }) => abs.startsWith(rootPrefix) && fs.existsSync(abs));
|
|
541
1044
|
}
|
|
542
1045
|
|
|
543
|
-
const inferredInstallType =
|
|
1046
|
+
const inferredInstallType = inferTaskplaneInstallScope();
|
|
544
1047
|
const packageScope = local ? "local" : global ? "global" : inferredInstallType;
|
|
545
1048
|
const piRemoveCmd = packageScope === "local"
|
|
546
1049
|
? "pi remove -l npm:taskplane"
|
|
@@ -1306,6 +1809,10 @@ async function cmdInit(args) {
|
|
|
1306
1809
|
return;
|
|
1307
1810
|
}
|
|
1308
1811
|
|
|
1812
|
+
const initAgentConfig = await collectInitAgentConfig({
|
|
1813
|
+
interactive: !isPreset,
|
|
1814
|
+
});
|
|
1815
|
+
|
|
1309
1816
|
// ── Scaffold .taskplane/ in config repo ─────────────────────
|
|
1310
1817
|
console.log(`\n${c.bold}Creating files in ${configRepoName}/.taskplane/...${c.reset}\n`);
|
|
1311
1818
|
// Skip existing files only when --force was NOT used AND the user did NOT confirm overwrite
|
|
@@ -1337,7 +1844,7 @@ async function cmdInit(args) {
|
|
|
1337
1844
|
}
|
|
1338
1845
|
|
|
1339
1846
|
// Project config JSON (taskplane-config.json)
|
|
1340
|
-
const projectConfig = generateProjectConfig(vars);
|
|
1847
|
+
const projectConfig = generateProjectConfig(vars, initAgentConfig);
|
|
1341
1848
|
writeFile(
|
|
1342
1849
|
path.join(taskplaneDir, "taskplane-config.json"),
|
|
1343
1850
|
JSON.stringify(projectConfig, null, 2) + "\n",
|
|
@@ -1473,6 +1980,9 @@ async function cmdInit(args) {
|
|
|
1473
1980
|
console.log(` ${c.cyan}/orch${c.reset} # start the taskplane supervisor`);
|
|
1474
1981
|
console.log(` ${c.cyan}/orch all${c.reset} # run all open tasks`);
|
|
1475
1982
|
}
|
|
1983
|
+
if (inferTaskplaneInstallScope() === "global") {
|
|
1984
|
+
console.log(` ${c.cyan}taskplane config --save-as-defaults${c.reset} # save these agent defaults for future inits`);
|
|
1985
|
+
}
|
|
1476
1986
|
console.log();
|
|
1477
1987
|
return;
|
|
1478
1988
|
}
|
|
@@ -1518,6 +2028,10 @@ async function cmdInit(args) {
|
|
|
1518
2028
|
return;
|
|
1519
2029
|
}
|
|
1520
2030
|
|
|
2031
|
+
const initAgentConfig = await collectInitAgentConfig({
|
|
2032
|
+
interactive: !isPreset,
|
|
2033
|
+
});
|
|
2034
|
+
|
|
1521
2035
|
// Scaffold files
|
|
1522
2036
|
console.log(`\n${c.bold}Creating files...${c.reset}\n`);
|
|
1523
2037
|
// Skip existing files only when --force was NOT used AND the user did NOT confirm overwrite
|
|
@@ -1552,7 +2066,7 @@ async function cmdInit(args) {
|
|
|
1552
2066
|
// Unified project config JSON
|
|
1553
2067
|
writeFile(
|
|
1554
2068
|
path.join(projectRoot, ".pi", "taskplane-config.json"),
|
|
1555
|
-
JSON.stringify(generateProjectConfig(vars), null, 2) + "\n",
|
|
2069
|
+
JSON.stringify(generateProjectConfig(vars, initAgentConfig), null, 2) + "\n",
|
|
1556
2070
|
{ skipIfExists, label: ".pi/taskplane-config.json" },
|
|
1557
2071
|
);
|
|
1558
2072
|
|
|
@@ -1628,6 +2142,9 @@ async function cmdInit(args) {
|
|
|
1628
2142
|
console.log(` ${c.cyan}/orch${c.reset} # start the taskplane supervisor`);
|
|
1629
2143
|
console.log(` ${c.cyan}/orch all${c.reset} # run all open tasks`);
|
|
1630
2144
|
}
|
|
2145
|
+
if (inferTaskplaneInstallScope() === "global") {
|
|
2146
|
+
console.log(` ${c.cyan}taskplane config --save-as-defaults${c.reset} # save these agent defaults for future inits`);
|
|
2147
|
+
}
|
|
1631
2148
|
console.log();
|
|
1632
2149
|
}
|
|
1633
2150
|
|
|
@@ -2573,6 +3090,7 @@ ${c.bold}Usage:${c.reset}
|
|
|
2573
3090
|
${c.bold}Commands:${c.reset}
|
|
2574
3091
|
${c.cyan}init${c.reset} Scaffold Taskplane config in the current project
|
|
2575
3092
|
${c.cyan}doctor${c.reset} Validate installation and project configuration
|
|
3093
|
+
${c.cyan}config${c.reset} Manage CLI config utilities (e.g., save init defaults)
|
|
2576
3094
|
${c.cyan}version${c.reset} Show version information
|
|
2577
3095
|
${c.cyan}dashboard${c.reset} Launch the web-based orchestrator dashboard
|
|
2578
3096
|
${c.cyan}uninstall${c.reset} Remove Taskplane project files and/or package install
|
|
@@ -2590,6 +3108,10 @@ ${c.bold}Dashboard options:${c.reset}
|
|
|
2590
3108
|
--port <number> Port to listen on (default: 8099)
|
|
2591
3109
|
--no-open Don't auto-open browser
|
|
2592
3110
|
|
|
3111
|
+
${c.bold}Config options:${c.reset}
|
|
3112
|
+
--save-as-defaults Save current project's worker/reviewer/merger model + thinking
|
|
3113
|
+
settings to user preferences for future taskplane init runs
|
|
3114
|
+
|
|
2593
3115
|
${c.bold}Uninstall options:${c.reset}
|
|
2594
3116
|
--dry-run Show what would be removed
|
|
2595
3117
|
--yes, -y Skip confirmation prompts
|
|
@@ -2607,6 +3129,7 @@ ${c.bold}Examples:${c.reset}
|
|
|
2607
3129
|
# Use existing task area path
|
|
2608
3130
|
taskplane init --dry-run # Preview what would be created
|
|
2609
3131
|
taskplane doctor # Check installation health
|
|
3132
|
+
taskplane config --save-as-defaults # Save current agent settings as init defaults
|
|
2610
3133
|
taskplane dashboard # Launch web dashboard
|
|
2611
3134
|
taskplane dashboard --port 3000 # Dashboard on custom port
|
|
2612
3135
|
taskplane uninstall --dry-run # Preview uninstall actions
|
|
@@ -2623,34 +3146,56 @@ ${c.bold}Getting started:${c.reset}
|
|
|
2623
3146
|
// MAIN
|
|
2624
3147
|
// ═════════════════════════════════════════════════════════════════════════════
|
|
2625
3148
|
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
3149
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
3150
|
+
const [command, ...args] = argv;
|
|
3151
|
+
|
|
3152
|
+
switch (command) {
|
|
3153
|
+
case "init":
|
|
3154
|
+
await cmdInit(args);
|
|
3155
|
+
break;
|
|
3156
|
+
case "doctor":
|
|
3157
|
+
cmdDoctor();
|
|
3158
|
+
break;
|
|
3159
|
+
case "config":
|
|
3160
|
+
cmdConfig(args);
|
|
3161
|
+
break;
|
|
3162
|
+
case "version":
|
|
3163
|
+
case "--version":
|
|
3164
|
+
case "-v":
|
|
3165
|
+
cmdVersion();
|
|
3166
|
+
break;
|
|
3167
|
+
case "dashboard":
|
|
3168
|
+
cmdDashboard(args);
|
|
3169
|
+
break;
|
|
3170
|
+
case "uninstall":
|
|
3171
|
+
await cmdUninstall(args);
|
|
3172
|
+
break;
|
|
3173
|
+
case "help":
|
|
3174
|
+
case "--help":
|
|
3175
|
+
case "-h":
|
|
3176
|
+
case undefined:
|
|
3177
|
+
showHelp();
|
|
3178
|
+
break;
|
|
3179
|
+
default:
|
|
3180
|
+
console.error(`${FAIL} Unknown command: ${command}`);
|
|
3181
|
+
showHelp();
|
|
3182
|
+
process.exit(1);
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3186
|
+
const isDirectExecution = (() => {
|
|
3187
|
+
const argv1 = process.argv[1];
|
|
3188
|
+
if (!argv1) return false;
|
|
3189
|
+
|
|
3190
|
+
try {
|
|
3191
|
+
const invokedPath = fs.realpathSync(argv1);
|
|
3192
|
+
const modulePath = fs.realpathSync(__filename);
|
|
3193
|
+
return invokedPath === modulePath;
|
|
3194
|
+
} catch {
|
|
3195
|
+
return path.resolve(argv1) === __filename;
|
|
3196
|
+
}
|
|
3197
|
+
})();
|
|
3198
|
+
|
|
3199
|
+
if (isDirectExecution) {
|
|
3200
|
+
await main();
|
|
2656
3201
|
}
|