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