taskplane 0.24.21 → 0.24.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/taskplane.mjs +239 -103
- package/extensions/task-runner.ts +2 -110
- package/extensions/taskplane/config-loader.ts +306 -269
- package/extensions/taskplane/config-schema.ts +49 -32
- package/extensions/taskplane/merge.ts +1 -1
- package/extensions/taskplane/settings-tui.ts +149 -172
- package/extensions/taskplane/supervisor.ts +2 -2
- package/extensions/taskplane/types.ts +1 -1
- package/package.json +1 -1
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Unified config loader for taskplane-config.json with YAML fallback
|
|
3
|
-
* and user preferences (Layer 2) merge.
|
|
2
|
+
* Unified config loader for taskplane-config.json with YAML fallback.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
* 1.
|
|
7
|
-
* 2.
|
|
8
|
-
* 3.
|
|
9
|
-
* 4. JSON absent + one/both YAML files present → read YAML, map to unified shape
|
|
10
|
-
* 5. None present → return cloned defaults
|
|
4
|
+
* Effective precedence:
|
|
5
|
+
* 1. Schema defaults (internal)
|
|
6
|
+
* 2. Global preferences (`~/.pi/agent/taskplane/preferences.json`)
|
|
7
|
+
* 3. Project overrides (`taskplane-config.json` or YAML fallback)
|
|
11
8
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
9
|
+
* Project config is treated as sparse overrides. Missing project fields
|
|
10
|
+
* fall through to global preferences, then schema defaults.
|
|
11
|
+
*
|
|
12
|
+
* Global preferences parsing is allowlist-based. Unknown top-level keys are
|
|
13
|
+
* ignored, and malformed preferences fall back to defaults silently.
|
|
17
14
|
*
|
|
18
15
|
* Path resolution:
|
|
19
16
|
* Resolves config paths relative to `configRoot`. Callers should pass
|
|
@@ -33,18 +30,17 @@ import {
|
|
|
33
30
|
CONFIG_VERSION,
|
|
34
31
|
PROJECT_CONFIG_FILENAME,
|
|
35
32
|
DEFAULT_PROJECT_CONFIG,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
USER_PREFERENCES_SUBDIR,
|
|
33
|
+
DEFAULT_GLOBAL_PREFERENCES,
|
|
34
|
+
DEFAULT_BOOTSTRAP_GLOBAL_PREFERENCES,
|
|
35
|
+
GLOBAL_PREFERENCES_FILENAME,
|
|
36
|
+
GLOBAL_PREFERENCES_SUBDIR,
|
|
41
37
|
} from "./config-schema.ts";
|
|
42
38
|
import type {
|
|
43
39
|
TaskplaneConfig,
|
|
44
40
|
TaskRunnerSection,
|
|
45
41
|
OrchestratorSection,
|
|
46
42
|
WorkspaceSectionConfig,
|
|
47
|
-
|
|
43
|
+
GlobalPreferences,
|
|
48
44
|
} from "./config-schema.ts";
|
|
49
45
|
|
|
50
46
|
|
|
@@ -154,102 +150,36 @@ function normalizeInheritanceAliases(config: TaskplaneConfig): void {
|
|
|
154
150
|
let _projectMigrationDone = false;
|
|
155
151
|
|
|
156
152
|
/**
|
|
157
|
-
* Auto-migrate legacy TMUX fields in
|
|
158
|
-
*
|
|
159
|
-
* Precedence: if both `sessionPrefix` and `tmuxPrefix` exist, the new
|
|
160
|
-
* key (`sessionPrefix`) wins. `tmuxPrefix` is only used when `sessionPrefix`
|
|
161
|
-
* is absent. This matches the principle that explicit new-format config
|
|
162
|
-
* takes priority over legacy fields.
|
|
163
|
-
*
|
|
164
|
-
* Writes back to disk atomically (tmp + rename) on first migration.
|
|
165
|
-
* Idempotent — safe to call multiple times per load cycle (skips after first).
|
|
166
|
-
*
|
|
167
|
-
* @returns true if any migrations were applied
|
|
168
|
-
*/
|
|
169
|
-
function migrateProjectConfig(config: TaskplaneConfig, configRoot: string): boolean {
|
|
170
|
-
if (_projectMigrationDone) return false;
|
|
171
|
-
|
|
172
|
-
let migrated = false;
|
|
173
|
-
const orchestratorCore = config.orchestrator?.orchestrator as Record<string, unknown> | undefined;
|
|
174
|
-
if (orchestratorCore && hasOwn(orchestratorCore, "tmuxPrefix")) {
|
|
175
|
-
// Use tmuxPrefix if sessionPrefix is absent, undefined, or still the default.
|
|
176
|
-
// An explicit non-default sessionPrefix takes priority over legacy tmuxPrefix.
|
|
177
|
-
const currentPrefix = orchestratorCore.sessionPrefix;
|
|
178
|
-
const isDefault = currentPrefix === undefined || currentPrefix === "orch";
|
|
179
|
-
if (isDefault) {
|
|
180
|
-
(orchestratorCore as any).sessionPrefix = orchestratorCore.tmuxPrefix;
|
|
181
|
-
}
|
|
182
|
-
delete orchestratorCore.tmuxPrefix;
|
|
183
|
-
console.error(`[taskplane] Auto-migrated: orchestrator.orchestrator.tmuxPrefix → sessionPrefix`);
|
|
184
|
-
migrated = true;
|
|
185
|
-
}
|
|
186
|
-
if (orchestratorCore?.spawnMode === "tmux") {
|
|
187
|
-
(orchestratorCore as any).spawnMode = "subprocess";
|
|
188
|
-
console.error(`[taskplane] Auto-migrated: orchestrator.orchestrator.spawnMode "tmux" → "subprocess"`);
|
|
189
|
-
migrated = true;
|
|
190
|
-
}
|
|
191
|
-
const workerConfig = config.taskRunner?.worker as Record<string, unknown> | undefined;
|
|
192
|
-
if (workerConfig?.spawnMode === "tmux") {
|
|
193
|
-
(workerConfig as any).spawnMode = "subprocess";
|
|
194
|
-
console.error(`[taskplane] Auto-migrated: taskRunner.worker.spawnMode "tmux" → "subprocess"`);
|
|
195
|
-
migrated = true;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (migrated) {
|
|
199
|
-
// Write back atomically (tmp + rename) to prevent corruption
|
|
200
|
-
try {
|
|
201
|
-
const jsonPath = join(configRoot, ".pi", "taskplane-config.json");
|
|
202
|
-
if (existsSync(jsonPath)) {
|
|
203
|
-
const raw = JSON.parse(readFileSync(jsonPath, "utf-8"));
|
|
204
|
-
// Apply same renames to the raw JSON (consistent precedence)
|
|
205
|
-
if (raw.orchestrator?.orchestrator?.tmuxPrefix !== undefined) {
|
|
206
|
-
const rawPrefix = raw.orchestrator.orchestrator.sessionPrefix;
|
|
207
|
-
if (rawPrefix === undefined || rawPrefix === "orch") {
|
|
208
|
-
raw.orchestrator.orchestrator.sessionPrefix = raw.orchestrator.orchestrator.tmuxPrefix;
|
|
209
|
-
}
|
|
210
|
-
delete raw.orchestrator.orchestrator.tmuxPrefix;
|
|
211
|
-
}
|
|
212
|
-
if (raw.orchestrator?.orchestrator?.spawnMode === "tmux") {
|
|
213
|
-
raw.orchestrator.orchestrator.spawnMode = "subprocess";
|
|
214
|
-
}
|
|
215
|
-
if (raw.taskRunner?.worker?.spawnMode === "tmux") {
|
|
216
|
-
raw.taskRunner.worker.spawnMode = "subprocess";
|
|
217
|
-
}
|
|
218
|
-
const tmpPath = jsonPath + ".migration-tmp";
|
|
219
|
-
writeFileSync(tmpPath, JSON.stringify(raw, null, 2) + "\n");
|
|
220
|
-
renameSync(tmpPath, jsonPath);
|
|
221
|
-
console.error(`[taskplane] Config file updated: ${jsonPath}`);
|
|
222
|
-
}
|
|
223
|
-
} catch (err) {
|
|
224
|
-
console.error(`[taskplane] Warning: could not persist config migration to disk: ${err instanceof Error ? err.message : err}`);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
_projectMigrationDone = true;
|
|
229
|
-
return migrated;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Auto-migrate legacy TMUX fields in user preferences.
|
|
153
|
+
* Auto-migrate legacy TMUX fields in global preferences.
|
|
234
154
|
*
|
|
235
155
|
* Same precedence: new key wins if both exist.
|
|
236
156
|
* Writes back atomically (tmp + rename).
|
|
237
157
|
*
|
|
238
158
|
* @returns true if any migrations were applied
|
|
239
159
|
*/
|
|
240
|
-
function
|
|
160
|
+
function migrateGlobalPreferences(raw: Record<string, any>, prefsPath: string): boolean {
|
|
241
161
|
let migrated = false;
|
|
242
162
|
if (hasOwn(raw, "tmuxPrefix")) {
|
|
243
163
|
if (!hasOwn(raw, "sessionPrefix") || raw.sessionPrefix === undefined) {
|
|
244
164
|
raw.sessionPrefix = raw.tmuxPrefix;
|
|
245
165
|
}
|
|
246
166
|
delete raw.tmuxPrefix;
|
|
247
|
-
console.error(`[taskplane] Auto-migrated
|
|
167
|
+
console.error(`[taskplane] Auto-migrated global preference: tmuxPrefix → sessionPrefix`);
|
|
248
168
|
migrated = true;
|
|
249
169
|
}
|
|
250
170
|
if (raw.spawnMode === "tmux") {
|
|
251
171
|
raw.spawnMode = "subprocess";
|
|
252
|
-
console.error(`[taskplane] Auto-migrated
|
|
172
|
+
console.error(`[taskplane] Auto-migrated global preference: spawnMode "tmux" → "subprocess"`);
|
|
173
|
+
migrated = true;
|
|
174
|
+
}
|
|
175
|
+
if (raw.orchestrator?.orchestrator?.spawnMode === "tmux") {
|
|
176
|
+
raw.orchestrator.orchestrator.spawnMode = "subprocess";
|
|
177
|
+
console.error(`[taskplane] Auto-migrated global preference: orchestrator.orchestrator.spawnMode "tmux" → "subprocess"`);
|
|
178
|
+
migrated = true;
|
|
179
|
+
}
|
|
180
|
+
if (raw.taskRunner?.worker?.spawnMode === "tmux") {
|
|
181
|
+
raw.taskRunner.worker.spawnMode = "subprocess";
|
|
182
|
+
console.error(`[taskplane] Auto-migrated global preference: taskRunner.worker.spawnMode "tmux" → "subprocess"`);
|
|
253
183
|
migrated = true;
|
|
254
184
|
}
|
|
255
185
|
if (migrated) {
|
|
@@ -532,7 +462,7 @@ function resolveConfigFilePath(configRoot: string, filename: string): string {
|
|
|
532
462
|
* Returns the parsed config or null if the file doesn't exist.
|
|
533
463
|
* Throws ConfigLoadError for malformed JSON or unsupported versions.
|
|
534
464
|
*/
|
|
535
|
-
function loadJsonConfig(configRoot: string): TaskplaneConfig | null {
|
|
465
|
+
function loadJsonConfig(configRoot: string): Partial<TaskplaneConfig> | null {
|
|
536
466
|
const jsonPath = resolveConfigFilePath(configRoot, PROJECT_CONFIG_FILENAME);
|
|
537
467
|
if (!existsSync(jsonPath)) return null;
|
|
538
468
|
|
|
@@ -570,22 +500,21 @@ function loadJsonConfig(configRoot: string): TaskplaneConfig | null {
|
|
|
570
500
|
);
|
|
571
501
|
}
|
|
572
502
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
deepMerge(config.taskRunner, parsed.taskRunner);
|
|
503
|
+
const overrides: Partial<TaskplaneConfig> = {};
|
|
504
|
+
if (parsed.taskRunner && typeof parsed.taskRunner === "object" && !Array.isArray(parsed.taskRunner)) {
|
|
505
|
+
overrides.taskRunner = deepClone(parsed.taskRunner);
|
|
577
506
|
}
|
|
578
|
-
if (parsed.orchestrator) {
|
|
579
|
-
|
|
507
|
+
if (parsed.orchestrator && typeof parsed.orchestrator === "object" && !Array.isArray(parsed.orchestrator)) {
|
|
508
|
+
overrides.orchestrator = deepClone(parsed.orchestrator);
|
|
580
509
|
}
|
|
581
510
|
if (parsed.workspace) {
|
|
582
511
|
const normalizedWorkspace = normalizeWorkspaceSection(parsed.workspace, jsonPath);
|
|
583
512
|
if (normalizedWorkspace) {
|
|
584
|
-
|
|
513
|
+
overrides.workspace = normalizedWorkspace;
|
|
585
514
|
}
|
|
586
515
|
}
|
|
587
516
|
|
|
588
|
-
return
|
|
517
|
+
return overrides;
|
|
589
518
|
}
|
|
590
519
|
|
|
591
520
|
|
|
@@ -598,28 +527,24 @@ function loadJsonConfig(configRoot: string): TaskplaneConfig | null {
|
|
|
598
527
|
* flat layout (`<root>/task-runner.yaml`) — see `resolveConfigFilePath`.
|
|
599
528
|
* Maps snake_case YAML keys to the camelCase TaskRunnerSection shape.
|
|
600
529
|
* Uses section-aware mapping that preserves user-defined record keys.
|
|
601
|
-
* Returns
|
|
530
|
+
* Returns sparse overrides (empty object when missing/malformed).
|
|
602
531
|
*/
|
|
603
|
-
function loadTaskRunnerYaml(configRoot: string): TaskRunnerSection {
|
|
532
|
+
function loadTaskRunnerYaml(configRoot: string): Partial<TaskRunnerSection> {
|
|
604
533
|
const yamlPath = resolveConfigFilePath(configRoot, "task-runner.yaml");
|
|
605
|
-
if (!existsSync(yamlPath)) return
|
|
534
|
+
if (!existsSync(yamlPath)) return {};
|
|
606
535
|
|
|
607
536
|
try {
|
|
608
537
|
const raw = readFileSync(yamlPath, "utf-8");
|
|
609
538
|
const loaded = yamlParse(raw) as any;
|
|
610
|
-
if (!loaded || typeof loaded !== "object") return
|
|
539
|
+
if (!loaded || typeof loaded !== "object") return {};
|
|
611
540
|
|
|
612
541
|
// Section-aware mapping: structural keys → camelCase, record keys → preserved
|
|
613
542
|
const mapped = mapTaskRunnerYaml(loaded);
|
|
614
543
|
|
|
615
|
-
// Deep merge with cloned defaults
|
|
616
|
-
const section = deepClone(DEFAULT_TASK_RUNNER_SECTION);
|
|
617
|
-
deepMerge(section, mapped);
|
|
618
|
-
|
|
619
544
|
// Post-process taskAreas: trim repoId, drop whitespace-only values
|
|
620
545
|
// (matches legacy loadTaskRunnerConfig behavior from config.ts)
|
|
621
|
-
if (
|
|
622
|
-
for (const area of Object.values(
|
|
546
|
+
if (mapped.taskAreas) {
|
|
547
|
+
for (const area of Object.values(mapped.taskAreas)) {
|
|
623
548
|
if (area.repoId !== undefined) {
|
|
624
549
|
const trimmed = typeof area.repoId === "string" ? area.repoId.trim() : "";
|
|
625
550
|
if (trimmed) {
|
|
@@ -631,9 +556,9 @@ function loadTaskRunnerYaml(configRoot: string): TaskRunnerSection {
|
|
|
631
556
|
}
|
|
632
557
|
}
|
|
633
558
|
|
|
634
|
-
return
|
|
559
|
+
return mapped;
|
|
635
560
|
} catch {
|
|
636
|
-
return
|
|
561
|
+
return {};
|
|
637
562
|
}
|
|
638
563
|
}
|
|
639
564
|
|
|
@@ -644,27 +569,21 @@ function loadTaskRunnerYaml(configRoot: string): TaskRunnerSection {
|
|
|
644
569
|
* flat layout (`<root>/task-orchestrator.yaml`) — see `resolveConfigFilePath`.
|
|
645
570
|
* Maps snake_case YAML keys to the camelCase OrchestratorSection shape.
|
|
646
571
|
* Uses section-aware mapping that preserves user-defined record keys.
|
|
647
|
-
* Returns
|
|
572
|
+
* Returns sparse overrides (empty object when missing/malformed).
|
|
648
573
|
*/
|
|
649
|
-
function loadOrchestratorYaml(configRoot: string): OrchestratorSection {
|
|
574
|
+
function loadOrchestratorYaml(configRoot: string): Partial<OrchestratorSection> {
|
|
650
575
|
const yamlPath = resolveConfigFilePath(configRoot, "task-orchestrator.yaml");
|
|
651
|
-
if (!existsSync(yamlPath)) return
|
|
576
|
+
if (!existsSync(yamlPath)) return {};
|
|
652
577
|
|
|
653
578
|
try {
|
|
654
579
|
const raw = readFileSync(yamlPath, "utf-8");
|
|
655
580
|
const loaded = yamlParse(raw) as any;
|
|
656
|
-
if (!loaded || typeof loaded !== "object") return
|
|
581
|
+
if (!loaded || typeof loaded !== "object") return {};
|
|
657
582
|
|
|
658
583
|
// Section-aware mapping: structural keys → camelCase, record keys → preserved
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
// Deep merge with cloned defaults
|
|
662
|
-
const section = deepClone(DEFAULT_ORCHESTRATOR_SECTION);
|
|
663
|
-
deepMerge(section, mapped);
|
|
664
|
-
|
|
665
|
-
return section;
|
|
584
|
+
return mapOrchestratorYaml(loaded);
|
|
666
585
|
} catch {
|
|
667
|
-
return
|
|
586
|
+
return {};
|
|
668
587
|
}
|
|
669
588
|
}
|
|
670
589
|
|
|
@@ -692,10 +611,10 @@ function loadWorkspaceYaml(configRoot: string): WorkspaceSectionConfig | undefin
|
|
|
692
611
|
}
|
|
693
612
|
|
|
694
613
|
|
|
695
|
-
// ──
|
|
614
|
+
// ── Global Preferences (Layer 2) ─────────────────────────────────────
|
|
696
615
|
|
|
697
616
|
/**
|
|
698
|
-
* Resolve the absolute path to the
|
|
617
|
+
* Resolve the absolute path to the global preferences file.
|
|
699
618
|
*
|
|
700
619
|
* Resolution order:
|
|
701
620
|
* 1. `PI_CODING_AGENT_DIR` env → `<value>/taskplane/preferences.json`
|
|
@@ -704,61 +623,104 @@ function loadWorkspaceYaml(configRoot: string): WorkspaceSectionConfig | undefin
|
|
|
704
623
|
* Uses `os.homedir()` for cross-platform home resolution
|
|
705
624
|
* (USERPROFILE on Windows, HOME on Unix) and `path.join()` for separators.
|
|
706
625
|
*/
|
|
707
|
-
export function
|
|
626
|
+
export function resolveGlobalPreferencesPath(): string {
|
|
708
627
|
const agentDir = process.env.PI_CODING_AGENT_DIR;
|
|
709
628
|
if (agentDir) {
|
|
710
|
-
return join(agentDir,
|
|
629
|
+
return join(agentDir, GLOBAL_PREFERENCES_SUBDIR, GLOBAL_PREFERENCES_FILENAME);
|
|
630
|
+
}
|
|
631
|
+
return join(homedir(), ".pi", "agent", GLOBAL_PREFERENCES_SUBDIR, GLOBAL_PREFERENCES_FILENAME);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/** Result envelope for global preferences loading. */
|
|
635
|
+
export interface GlobalPreferencesLoadResult {
|
|
636
|
+
preferences: GlobalPreferences;
|
|
637
|
+
wasBootstrapped: boolean;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/** Persist preferences JSON atomically (temp file + rename). */
|
|
641
|
+
function writePreferencesAtomically(prefsPath: string, prefs: GlobalPreferences): void {
|
|
642
|
+
const tmpPath = `${prefsPath}.tmp-${process.pid}-${Date.now()}`;
|
|
643
|
+
writeFileSync(tmpPath, JSON.stringify(prefs, null, 2) + "\n", "utf-8");
|
|
644
|
+
renameSync(tmpPath, prefsPath);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Write first-install bootstrap preferences to disk and return the in-memory seed.
|
|
649
|
+
*/
|
|
650
|
+
function bootstrapGlobalPreferencesFile(prefsPath: string): GlobalPreferences {
|
|
651
|
+
const bootstrapPrefs = deepClone(DEFAULT_BOOTSTRAP_GLOBAL_PREFERENCES);
|
|
652
|
+
try {
|
|
653
|
+
const dir = join(prefsPath, "..");
|
|
654
|
+
mkdirSync(dir, { recursive: true });
|
|
655
|
+
writePreferencesAtomically(prefsPath, bootstrapPrefs);
|
|
656
|
+
} catch {
|
|
657
|
+
// Best-effort; if we can't create, still return bootstrap defaults in-memory.
|
|
711
658
|
}
|
|
712
|
-
return
|
|
659
|
+
return bootstrapPrefs;
|
|
713
660
|
}
|
|
714
661
|
|
|
715
662
|
/**
|
|
716
|
-
* Load
|
|
663
|
+
* Load global preferences plus bootstrap metadata.
|
|
717
664
|
*
|
|
718
665
|
* Behavior:
|
|
719
|
-
* - If file doesn't exist:
|
|
720
|
-
* - If file is malformed
|
|
721
|
-
* - Unknown keys are silently ignored (
|
|
722
|
-
* - Returns a fresh UserPreferences object on each call
|
|
723
|
-
*
|
|
724
|
-
* @returns Parsed UserPreferences (only recognized fields)
|
|
666
|
+
* - If file doesn't exist: bootstrap preferences on disk and mark bootstrapped
|
|
667
|
+
* - If file is empty/malformed/invalid: re-bootstrap preferences and mark bootstrapped
|
|
668
|
+
* - Unknown keys are silently ignored (allowlist extraction)
|
|
725
669
|
*/
|
|
726
|
-
export function
|
|
727
|
-
const prefsPath =
|
|
670
|
+
export function loadGlobalPreferencesWithMeta(): GlobalPreferencesLoadResult {
|
|
671
|
+
const prefsPath = resolveGlobalPreferencesPath();
|
|
728
672
|
|
|
729
673
|
if (!existsSync(prefsPath)) {
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
writeFileSync(prefsPath, JSON.stringify(DEFAULT_USER_PREFERENCES, null, 2) + "\n", "utf-8");
|
|
735
|
-
} catch {
|
|
736
|
-
// Best-effort; if we can't create, just return defaults
|
|
737
|
-
}
|
|
738
|
-
return { ...DEFAULT_USER_PREFERENCES };
|
|
674
|
+
return {
|
|
675
|
+
preferences: bootstrapGlobalPreferencesFile(prefsPath),
|
|
676
|
+
wasBootstrapped: true,
|
|
677
|
+
};
|
|
739
678
|
}
|
|
740
679
|
|
|
741
680
|
let raw: string;
|
|
742
681
|
try {
|
|
743
682
|
raw = readFileSync(prefsPath, "utf-8");
|
|
744
683
|
} catch {
|
|
745
|
-
return {
|
|
684
|
+
return { preferences: deepClone(DEFAULT_GLOBAL_PREFERENCES), wasBootstrapped: false };
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (!raw.trim()) {
|
|
688
|
+
return {
|
|
689
|
+
preferences: bootstrapGlobalPreferencesFile(prefsPath),
|
|
690
|
+
wasBootstrapped: true,
|
|
691
|
+
};
|
|
746
692
|
}
|
|
747
693
|
|
|
748
694
|
let parsed: any;
|
|
749
695
|
try {
|
|
750
696
|
parsed = JSON.parse(raw);
|
|
751
697
|
} catch {
|
|
752
|
-
|
|
753
|
-
|
|
698
|
+
return {
|
|
699
|
+
preferences: bootstrapGlobalPreferencesFile(prefsPath),
|
|
700
|
+
wasBootstrapped: true,
|
|
701
|
+
};
|
|
754
702
|
}
|
|
755
703
|
|
|
756
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
757
|
-
return {
|
|
704
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || Object.keys(parsed).length === 0) {
|
|
705
|
+
return {
|
|
706
|
+
preferences: bootstrapGlobalPreferencesFile(prefsPath),
|
|
707
|
+
wasBootstrapped: true,
|
|
708
|
+
};
|
|
758
709
|
}
|
|
759
710
|
|
|
760
|
-
|
|
761
|
-
|
|
711
|
+
return {
|
|
712
|
+
preferences: extractAllowlistedPreferences(parsed, prefsPath),
|
|
713
|
+
wasBootstrapped: false,
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Load global preferences from `~/.pi/agent/taskplane/preferences.json`.
|
|
719
|
+
*
|
|
720
|
+
* @returns Parsed GlobalPreferences (only recognized fields)
|
|
721
|
+
*/
|
|
722
|
+
export function loadGlobalPreferences(): GlobalPreferences {
|
|
723
|
+
return loadGlobalPreferencesWithMeta().preferences;
|
|
762
724
|
}
|
|
763
725
|
|
|
764
726
|
/**
|
|
@@ -767,18 +729,21 @@ export function loadUserPreferences(): UserPreferences {
|
|
|
767
729
|
*/
|
|
768
730
|
function normalizePreferenceThinkingMode(value: unknown): string {
|
|
769
731
|
const cleaned = String(value ?? "").trim().toLowerCase();
|
|
770
|
-
if (cleaned === "
|
|
771
|
-
if (cleaned === "
|
|
732
|
+
if (!cleaned || cleaned === "inherit") return "";
|
|
733
|
+
if (cleaned === "on") return "high";
|
|
734
|
+
if (["off", "minimal", "low", "medium", "high", "xhigh"].includes(cleaned)) {
|
|
735
|
+
return cleaned;
|
|
736
|
+
}
|
|
772
737
|
return "";
|
|
773
738
|
}
|
|
774
739
|
|
|
775
|
-
function extractInitAgentDefaults(rawInitDefaults: unknown):
|
|
740
|
+
function extractInitAgentDefaults(rawInitDefaults: unknown): GlobalPreferences["initAgentDefaults"] | undefined {
|
|
776
741
|
if (!rawInitDefaults || typeof rawInitDefaults !== "object" || Array.isArray(rawInitDefaults)) {
|
|
777
742
|
return undefined;
|
|
778
743
|
}
|
|
779
744
|
|
|
780
745
|
const raw = rawInitDefaults as Record<string, unknown>;
|
|
781
|
-
const extracted: NonNullable<
|
|
746
|
+
const extracted: NonNullable<GlobalPreferences["initAgentDefaults"]> = {};
|
|
782
747
|
|
|
783
748
|
if (typeof raw.workerModel === "string") extracted.workerModel = raw.workerModel;
|
|
784
749
|
if (typeof raw.reviewerModel === "string") extracted.reviewerModel = raw.reviewerModel;
|
|
@@ -790,11 +755,34 @@ function extractInitAgentDefaults(rawInitDefaults: unknown): UserPreferences["in
|
|
|
790
755
|
return Object.keys(extracted).length > 0 ? extracted : undefined;
|
|
791
756
|
}
|
|
792
757
|
|
|
793
|
-
function
|
|
794
|
-
|
|
758
|
+
function extractConfigOverrideSection(rawSection: unknown): Record<string, any> | undefined {
|
|
759
|
+
if (!rawSection || typeof rawSection !== "object" || Array.isArray(rawSection)) {
|
|
760
|
+
return undefined;
|
|
761
|
+
}
|
|
762
|
+
return deepClone(rawSection as Record<string, any>);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function extractAllowlistedPreferences(raw: Record<string, any>, prefsPath: string): GlobalPreferences {
|
|
766
|
+
migrateGlobalPreferences(raw, prefsPath);
|
|
795
767
|
|
|
796
|
-
const prefs:
|
|
768
|
+
const prefs: GlobalPreferences = {};
|
|
797
769
|
|
|
770
|
+
const taskRunnerOverrides = extractConfigOverrideSection(raw.taskRunner);
|
|
771
|
+
if (taskRunnerOverrides) {
|
|
772
|
+
prefs.taskRunner = taskRunnerOverrides as GlobalPreferences["taskRunner"];
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const orchestratorOverrides = extractConfigOverrideSection(raw.orchestrator);
|
|
776
|
+
if (orchestratorOverrides) {
|
|
777
|
+
prefs.orchestrator = orchestratorOverrides as GlobalPreferences["orchestrator"];
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const workspaceOverrides = extractConfigOverrideSection(raw.workspace);
|
|
781
|
+
if (workspaceOverrides) {
|
|
782
|
+
prefs.workspace = workspaceOverrides as GlobalPreferences["workspace"];
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Legacy flat aliases (backward compatibility for existing preferences.json files)
|
|
798
786
|
if (typeof raw.operatorId === "string") prefs.operatorId = raw.operatorId;
|
|
799
787
|
if (typeof raw.sessionPrefix === "string") {
|
|
800
788
|
prefs.sessionPrefix = raw.sessionPrefix;
|
|
@@ -807,6 +795,8 @@ function extractAllowlistedPreferences(raw: Record<string, any>, prefsPath: stri
|
|
|
807
795
|
if (typeof raw.mergeModel === "string") prefs.mergeModel = raw.mergeModel;
|
|
808
796
|
if (typeof raw.mergeThinking === "string") prefs.mergeThinking = raw.mergeThinking;
|
|
809
797
|
if (typeof raw.supervisorModel === "string") prefs.supervisorModel = raw.supervisorModel;
|
|
798
|
+
|
|
799
|
+
// Preferences-only fields (intentionally not merged into runtime config)
|
|
810
800
|
if (typeof raw.dashboardPort === "number" && Number.isFinite(raw.dashboardPort)) {
|
|
811
801
|
prefs.dashboardPort = raw.dashboardPort;
|
|
812
802
|
}
|
|
@@ -819,34 +809,23 @@ function extractAllowlistedPreferences(raw: Record<string, any>, prefsPath: stri
|
|
|
819
809
|
}
|
|
820
810
|
|
|
821
811
|
/**
|
|
822
|
-
* Apply
|
|
812
|
+
* Apply global preferences (Layer 2) onto a project config (Layer 1).
|
|
823
813
|
*
|
|
824
|
-
*
|
|
825
|
-
*
|
|
814
|
+
* Merge order inside Layer 2:
|
|
815
|
+
* 1. Legacy flat aliases (for backward compatibility)
|
|
816
|
+
* 2. Config-shaped nested overrides (`taskRunner` / `orchestrator` / `workspace`)
|
|
817
|
+
* Nested overrides intentionally win when both styles are present.
|
|
826
818
|
*
|
|
827
|
-
*
|
|
828
|
-
*
|
|
829
|
-
* Empty-string preference values are treated as "not set" and do NOT
|
|
830
|
-
* override the project config value. This lets users clear a preference
|
|
831
|
-
* by deleting the field or setting it to "".
|
|
832
|
-
*
|
|
833
|
-
* Mapping table:
|
|
834
|
-
* prefs.operatorId → config.orchestrator.orchestrator.operatorId
|
|
835
|
-
* prefs.sessionPrefix → config.orchestrator.orchestrator.sessionPrefix
|
|
836
|
-
* prefs.spawnMode → config.orchestrator.orchestrator.spawnMode
|
|
837
|
-
* prefs.workerModel → config.taskRunner.worker.model
|
|
838
|
-
* prefs.reviewerModel → config.taskRunner.reviewer.model
|
|
839
|
-
* prefs.mergeModel → config.orchestrator.merge.model
|
|
840
|
-
* prefs.supervisorModel → config.orchestrator.supervisor.model
|
|
841
|
-
* prefs.dashboardPort → (no config target yet — stored only)
|
|
842
|
-
* prefs.initAgentDefaults → (preferences-only; consumed by CLI init flow)
|
|
819
|
+
* Preferences-only fields (`dashboardPort`, `initAgentDefaults`) are preserved
|
|
820
|
+
* in `GlobalPreferences` but intentionally not merged into runtime config.
|
|
843
821
|
*/
|
|
844
|
-
export function
|
|
822
|
+
export function applyGlobalPreferences(config: TaskplaneConfig, prefs: GlobalPreferences): TaskplaneConfig {
|
|
845
823
|
// Helper: only apply non-empty string values
|
|
846
824
|
const applyStr = (val: string | undefined, setter: (v: string) => void) => {
|
|
847
825
|
if (val !== undefined && val !== "") setter(val);
|
|
848
826
|
};
|
|
849
827
|
|
|
828
|
+
// 1) Legacy flat aliases
|
|
850
829
|
applyStr(prefs.operatorId, (v) => { config.orchestrator.orchestrator.operatorId = v; });
|
|
851
830
|
applyStr(prefs.sessionPrefix, (v) => { config.orchestrator.orchestrator.sessionPrefix = v; });
|
|
852
831
|
applyStr(prefs.workerModel, (v) => { config.taskRunner.worker.model = v; });
|
|
@@ -864,8 +843,29 @@ export function applyUserPreferences(config: TaskplaneConfig, prefs: UserPrefere
|
|
|
864
843
|
config.orchestrator.orchestrator.spawnMode = prefs.spawnMode;
|
|
865
844
|
}
|
|
866
845
|
|
|
867
|
-
//
|
|
868
|
-
|
|
846
|
+
// 2) Config-shaped nested overrides
|
|
847
|
+
if (prefs.taskRunner) {
|
|
848
|
+
deepMerge(config.taskRunner as Record<string, any>, prefs.taskRunner as Record<string, any>);
|
|
849
|
+
}
|
|
850
|
+
if (prefs.orchestrator) {
|
|
851
|
+
deepMerge(config.orchestrator as Record<string, any>, prefs.orchestrator as Record<string, any>);
|
|
852
|
+
}
|
|
853
|
+
if (prefs.workspace) {
|
|
854
|
+
if (!config.workspace || typeof config.workspace !== "object") {
|
|
855
|
+
config.workspace = {} as TaskplaneConfig["workspace"];
|
|
856
|
+
}
|
|
857
|
+
deepMerge(config.workspace as Record<string, any>, prefs.workspace as Record<string, any>);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Runtime safety: nested legacy values may arrive through config-shaped overrides.
|
|
861
|
+
if ((config.orchestrator.orchestrator as Record<string, any>).spawnMode === "tmux") {
|
|
862
|
+
config.orchestrator.orchestrator.spawnMode = "subprocess";
|
|
863
|
+
console.error(`[taskplane] Auto-migrated runtime global preference: orchestrator.orchestrator.spawnMode "tmux" → "subprocess"`);
|
|
864
|
+
}
|
|
865
|
+
if ((config.taskRunner.worker as Record<string, any>).spawnMode === "tmux") {
|
|
866
|
+
config.taskRunner.worker.spawnMode = "subprocess";
|
|
867
|
+
console.error(`[taskplane] Auto-migrated runtime global preference: taskRunner.worker.spawnMode "tmux" → "subprocess"`);
|
|
868
|
+
}
|
|
869
869
|
|
|
870
870
|
return config;
|
|
871
871
|
}
|
|
@@ -932,105 +932,142 @@ export function resolveConfigRoot(cwd: string, pointerConfigRoot?: string): stri
|
|
|
932
932
|
return cwd;
|
|
933
933
|
}
|
|
934
934
|
|
|
935
|
+
function mergeProjectOverrides(config: TaskplaneConfig, overrides: Partial<TaskplaneConfig>): void {
|
|
936
|
+
if (overrides.taskRunner) {
|
|
937
|
+
deepMerge(config.taskRunner as Record<string, any>, overrides.taskRunner as Record<string, any>);
|
|
938
|
+
}
|
|
939
|
+
if (overrides.orchestrator) {
|
|
940
|
+
deepMerge(config.orchestrator as Record<string, any>, overrides.orchestrator as Record<string, any>);
|
|
941
|
+
}
|
|
942
|
+
if (overrides.workspace) {
|
|
943
|
+
if (!config.workspace || typeof config.workspace !== "object") {
|
|
944
|
+
config.workspace = {} as TaskplaneConfig["workspace"];
|
|
945
|
+
}
|
|
946
|
+
deepMerge(config.workspace as Record<string, any>, overrides.workspace as Record<string, any>);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function migrateProjectOverrides(overrides: Partial<TaskplaneConfig>, configRoot: string): boolean {
|
|
951
|
+
if (_projectMigrationDone) return false;
|
|
952
|
+
|
|
953
|
+
let migrated = false;
|
|
954
|
+
const orchestratorCore = overrides.orchestrator?.orchestrator as Record<string, unknown> | undefined;
|
|
955
|
+
if (orchestratorCore && hasOwn(orchestratorCore, "tmuxPrefix")) {
|
|
956
|
+
const currentPrefix = orchestratorCore.sessionPrefix;
|
|
957
|
+
const isDefault = currentPrefix === undefined || currentPrefix === "orch";
|
|
958
|
+
if (isDefault) {
|
|
959
|
+
(orchestratorCore as any).sessionPrefix = orchestratorCore.tmuxPrefix;
|
|
960
|
+
}
|
|
961
|
+
delete orchestratorCore.tmuxPrefix;
|
|
962
|
+
console.error(`[taskplane] Auto-migrated: orchestrator.orchestrator.tmuxPrefix → sessionPrefix`);
|
|
963
|
+
migrated = true;
|
|
964
|
+
}
|
|
965
|
+
if (orchestratorCore?.spawnMode === "tmux") {
|
|
966
|
+
(orchestratorCore as any).spawnMode = "subprocess";
|
|
967
|
+
console.error(`[taskplane] Auto-migrated: orchestrator.orchestrator.spawnMode "tmux" → "subprocess"`);
|
|
968
|
+
migrated = true;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
const workerConfig = overrides.taskRunner?.worker as Record<string, unknown> | undefined;
|
|
972
|
+
if (workerConfig?.spawnMode === "tmux") {
|
|
973
|
+
(workerConfig as any).spawnMode = "subprocess";
|
|
974
|
+
console.error(`[taskplane] Auto-migrated: taskRunner.worker.spawnMode "tmux" → "subprocess"`);
|
|
975
|
+
migrated = true;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
if (migrated) {
|
|
979
|
+
try {
|
|
980
|
+
const jsonPath = resolveConfigFilePath(configRoot, PROJECT_CONFIG_FILENAME);
|
|
981
|
+
if (existsSync(jsonPath)) {
|
|
982
|
+
const raw = JSON.parse(readFileSync(jsonPath, "utf-8"));
|
|
983
|
+
if (raw.orchestrator?.orchestrator?.tmuxPrefix !== undefined) {
|
|
984
|
+
const rawPrefix = raw.orchestrator.orchestrator.sessionPrefix;
|
|
985
|
+
if (rawPrefix === undefined || rawPrefix === "orch") {
|
|
986
|
+
raw.orchestrator.orchestrator.sessionPrefix = raw.orchestrator.orchestrator.tmuxPrefix;
|
|
987
|
+
}
|
|
988
|
+
delete raw.orchestrator.orchestrator.tmuxPrefix;
|
|
989
|
+
}
|
|
990
|
+
if (raw.orchestrator?.orchestrator?.spawnMode === "tmux") {
|
|
991
|
+
raw.orchestrator.orchestrator.spawnMode = "subprocess";
|
|
992
|
+
}
|
|
993
|
+
if (raw.taskRunner?.worker?.spawnMode === "tmux") {
|
|
994
|
+
raw.taskRunner.worker.spawnMode = "subprocess";
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
const tmpPath = jsonPath + ".migration-tmp";
|
|
998
|
+
writeFileSync(tmpPath, JSON.stringify(raw, null, 2) + "\n");
|
|
999
|
+
renameSync(tmpPath, jsonPath);
|
|
1000
|
+
console.error(`[taskplane] Config file updated: ${jsonPath}`);
|
|
1001
|
+
}
|
|
1002
|
+
} catch (err) {
|
|
1003
|
+
console.error(`[taskplane] Warning: could not persist config migration to disk: ${err instanceof Error ? err.message : err}`);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
_projectMigrationDone = true;
|
|
1008
|
+
return migrated;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
export function loadProjectOverrides(configRoot: string): Partial<TaskplaneConfig> {
|
|
1012
|
+
const jsonOverrides = loadJsonConfig(configRoot);
|
|
1013
|
+
if (jsonOverrides !== null) {
|
|
1014
|
+
return jsonOverrides;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const taskRunner = loadTaskRunnerYaml(configRoot);
|
|
1018
|
+
const orchestrator = loadOrchestratorYaml(configRoot);
|
|
1019
|
+
const workspace = loadWorkspaceYaml(configRoot);
|
|
1020
|
+
|
|
1021
|
+
const overrides: Partial<TaskplaneConfig> = {};
|
|
1022
|
+
if (Object.keys(taskRunner).length > 0) overrides.taskRunner = taskRunner;
|
|
1023
|
+
if (Object.keys(orchestrator).length > 0) overrides.orchestrator = orchestrator;
|
|
1024
|
+
if (workspace) overrides.workspace = workspace;
|
|
1025
|
+
return overrides;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
935
1028
|
/**
|
|
936
1029
|
* Load the unified project configuration.
|
|
937
1030
|
*
|
|
938
1031
|
* Precedence (layered):
|
|
939
|
-
*
|
|
940
|
-
*
|
|
941
|
-
*
|
|
942
|
-
* (+ optional `.pi/taskplane-workspace.yaml` workspace section mapping)
|
|
943
|
-
* 3. Defaults — if no config files exist
|
|
944
|
-
*
|
|
945
|
-
* Layer 2 — User preferences (applied on top of Layer 1):
|
|
946
|
-
* Reads `~/.pi/agent/taskplane/preferences.json` and overrides only
|
|
947
|
-
* allowlisted user-scoped fields. See `applyUserPreferences()` for
|
|
948
|
-
* the field mapping.
|
|
1032
|
+
* 1. Schema defaults
|
|
1033
|
+
* 2. Global preferences (`~/.pi/agent/taskplane/preferences.json`)
|
|
1034
|
+
* 3. Project overrides (`taskplane-config.json` or YAML fallback)
|
|
949
1035
|
*
|
|
950
|
-
*
|
|
951
|
-
*
|
|
952
|
-
* 2. pointerConfigRoot has config files → use it (pointer redirect, workspace mode)
|
|
953
|
-
* 3. TASKPLANE_WORKSPACE_ROOT has config files → use it (legacy fallback)
|
|
954
|
-
* 4. Fall back to cwd (loaders will return defaults)
|
|
955
|
-
*
|
|
956
|
-
* @param cwd - Current working directory (project root or worktree)
|
|
957
|
-
* @param pointerConfigRoot - Resolved config root from pointer file (optional).
|
|
958
|
-
* Callers in workspace mode should resolve the pointer via `resolvePointer()`
|
|
959
|
-
* and pass `result.configRoot` here. In repo mode, omit or pass undefined.
|
|
960
|
-
* @returns Unified TaskplaneConfig — always a fresh deep-cloned object
|
|
961
|
-
* @throws ConfigLoadError if JSON exists but is malformed or has unsupported version
|
|
1036
|
+
* Project config is treated as sparse overrides. Missing fields in project
|
|
1037
|
+
* config fall through to global preferences, then schema defaults.
|
|
962
1038
|
*/
|
|
963
1039
|
export function loadProjectConfig(cwd: string, pointerConfigRoot?: string): TaskplaneConfig {
|
|
964
1040
|
const configRoot = resolveConfigRoot(cwd, pointerConfigRoot);
|
|
1041
|
+
const config = deepClone(DEFAULT_PROJECT_CONFIG);
|
|
965
1042
|
|
|
966
|
-
// Layer
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
// Try JSON first
|
|
970
|
-
const jsonConfig = loadJsonConfig(configRoot);
|
|
971
|
-
if (jsonConfig !== null) {
|
|
972
|
-
config = jsonConfig;
|
|
973
|
-
} else {
|
|
974
|
-
// Fall back to YAML
|
|
975
|
-
const taskRunner = loadTaskRunnerYaml(configRoot);
|
|
976
|
-
const orchestrator = loadOrchestratorYaml(configRoot);
|
|
977
|
-
const workspace = loadWorkspaceYaml(configRoot);
|
|
978
|
-
config = {
|
|
979
|
-
configVersion: CONFIG_VERSION,
|
|
980
|
-
taskRunner,
|
|
981
|
-
orchestrator,
|
|
982
|
-
...(workspace ? { workspace } : {}),
|
|
983
|
-
};
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
_projectMigrationDone = false; // Reset guard for each top-level load
|
|
987
|
-
migrateProjectConfig(config, configRoot);
|
|
1043
|
+
// Layer 2 baseline: global preferences on top of defaults
|
|
1044
|
+
const prefs = loadGlobalPreferences();
|
|
1045
|
+
applyGlobalPreferences(config, prefs);
|
|
988
1046
|
|
|
989
|
-
// Layer
|
|
990
|
-
const
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1047
|
+
// Layer 1 project overrides: sparse config merged on top
|
|
1048
|
+
const overrides = loadProjectOverrides(configRoot);
|
|
1049
|
+
_projectMigrationDone = false;
|
|
1050
|
+
migrateProjectOverrides(overrides, configRoot);
|
|
1051
|
+
mergeProjectOverrides(config, overrides);
|
|
994
1052
|
|
|
995
1053
|
normalizeInheritanceAliases(config);
|
|
996
1054
|
return config;
|
|
997
1055
|
}
|
|
998
1056
|
|
|
999
1057
|
/**
|
|
1000
|
-
* Load
|
|
1001
|
-
*
|
|
1002
|
-
*
|
|
1003
|
-
* Layer 2 user preferences. Used by the settings TUI write-back to
|
|
1004
|
-
* bootstrap a JSON config file from YAML-only projects without
|
|
1005
|
-
* accidentally embedding user preferences into the project config.
|
|
1006
|
-
*
|
|
1007
|
-
* @param cwd - Current working directory (project root or worktree)
|
|
1008
|
-
* @param pointerConfigRoot - Optional pointer-resolved config root (workspace mode)
|
|
1009
|
-
* @returns Layer 1 TaskplaneConfig — always a fresh deep-cloned object
|
|
1010
|
-
* @throws ConfigLoadError if JSON exists but is malformed or has unsupported version
|
|
1058
|
+
* Load project overrides merged with schema defaults, without applying
|
|
1059
|
+
* global preferences. Used by settings write-back code paths that must
|
|
1060
|
+
* avoid embedding global baseline values into project config.
|
|
1011
1061
|
*/
|
|
1012
1062
|
export function loadLayer1Config(cwd: string, pointerConfigRoot?: string): TaskplaneConfig {
|
|
1013
1063
|
const configRoot = resolveConfigRoot(cwd, pointerConfigRoot);
|
|
1064
|
+
const config = deepClone(DEFAULT_PROJECT_CONFIG);
|
|
1065
|
+
const overrides = loadProjectOverrides(configRoot);
|
|
1014
1066
|
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
migrateProjectConfig(jsonConfig, configRoot);
|
|
1019
|
-
normalizeInheritanceAliases(jsonConfig);
|
|
1020
|
-
return jsonConfig;
|
|
1021
|
-
}
|
|
1067
|
+
_projectMigrationDone = false;
|
|
1068
|
+
migrateProjectOverrides(overrides, configRoot);
|
|
1069
|
+
mergeProjectOverrides(config, overrides);
|
|
1022
1070
|
|
|
1023
|
-
// Fall back to YAML
|
|
1024
|
-
const taskRunner = loadTaskRunnerYaml(configRoot);
|
|
1025
|
-
const orchestrator = loadOrchestratorYaml(configRoot);
|
|
1026
|
-
const workspace = loadWorkspaceYaml(configRoot);
|
|
1027
|
-
const config: TaskplaneConfig = {
|
|
1028
|
-
configVersion: CONFIG_VERSION,
|
|
1029
|
-
taskRunner,
|
|
1030
|
-
orchestrator,
|
|
1031
|
-
...(workspace ? { workspace } : {}),
|
|
1032
|
-
};
|
|
1033
|
-
migrateProjectConfig(config, configRoot);
|
|
1034
1071
|
normalizeInheritanceAliases(config);
|
|
1035
1072
|
return config;
|
|
1036
1073
|
}
|