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
|
@@ -64,7 +64,10 @@ export interface PostIntegrateCleanupResult {
|
|
|
64
64
|
* @param batchId - Batch ID to scope deletion
|
|
65
65
|
* @returns Cleanup result with counts and warnings
|
|
66
66
|
*/
|
|
67
|
-
export function cleanupPostIntegrate(
|
|
67
|
+
export function cleanupPostIntegrate(
|
|
68
|
+
stateRoot: string,
|
|
69
|
+
batchId: string,
|
|
70
|
+
): PostIntegrateCleanupResult {
|
|
68
71
|
const result: PostIntegrateCleanupResult = {
|
|
69
72
|
telemetryFilesDeleted: 0,
|
|
70
73
|
mergeFilesDeleted: 0,
|
|
@@ -116,10 +119,11 @@ export function cleanupPostIntegrate(stateRoot: string, batchId: string): PostIn
|
|
|
116
119
|
try {
|
|
117
120
|
const entries = readdirSync(piDir);
|
|
118
121
|
for (const entry of entries) {
|
|
119
|
-
if (
|
|
120
|
-
|
|
121
|
-
(entry.startsWith("merge-
|
|
122
|
-
|
|
122
|
+
if (
|
|
123
|
+
entry.includes(batchId) &&
|
|
124
|
+
((entry.startsWith("merge-result-") && entry.endsWith(".json")) ||
|
|
125
|
+
(entry.startsWith("merge-request-") && entry.endsWith(".txt")))
|
|
126
|
+
) {
|
|
123
127
|
try {
|
|
124
128
|
unlinkSync(join(piDir, entry));
|
|
125
129
|
result.mergeFilesDeleted++;
|
|
@@ -140,7 +144,9 @@ export function cleanupPostIntegrate(stateRoot: string, batchId: string): PostIn
|
|
|
140
144
|
rmSync(mailboxBatchDir, { recursive: true, force: true });
|
|
141
145
|
result.mailboxDirsDeleted = 1;
|
|
142
146
|
} catch (err: unknown) {
|
|
143
|
-
result.warnings.push(
|
|
147
|
+
result.warnings.push(
|
|
148
|
+
`Failed to delete mailbox directory ${mailboxBatchDir}: ${(err as Error).message}`,
|
|
149
|
+
);
|
|
144
150
|
}
|
|
145
151
|
}
|
|
146
152
|
|
|
@@ -151,7 +157,9 @@ export function cleanupPostIntegrate(stateRoot: string, batchId: string): PostIn
|
|
|
151
157
|
rmSync(snapshotBatchDir, { recursive: true, force: true });
|
|
152
158
|
result.snapshotDirsDeleted = 1;
|
|
153
159
|
} catch (err: unknown) {
|
|
154
|
-
result.warnings.push(
|
|
160
|
+
result.warnings.push(
|
|
161
|
+
`Failed to delete context-snapshots directory ${snapshotBatchDir}: ${(err as Error).message}`,
|
|
162
|
+
);
|
|
155
163
|
}
|
|
156
164
|
}
|
|
157
165
|
|
|
@@ -163,7 +171,12 @@ export function cleanupPostIntegrate(stateRoot: string, batchId: string): PostIn
|
|
|
163
171
|
*/
|
|
164
172
|
export function formatPostIntegrateCleanup(result: PostIntegrateCleanupResult): string {
|
|
165
173
|
const parts: string[] = [];
|
|
166
|
-
const totalDeleted =
|
|
174
|
+
const totalDeleted =
|
|
175
|
+
result.telemetryFilesDeleted +
|
|
176
|
+
result.mergeFilesDeleted +
|
|
177
|
+
result.promptFilesDeleted +
|
|
178
|
+
result.mailboxDirsDeleted +
|
|
179
|
+
result.snapshotDirsDeleted;
|
|
167
180
|
|
|
168
181
|
if (totalDeleted > 0) {
|
|
169
182
|
const segments: string[] = [];
|
|
@@ -287,26 +300,32 @@ export function sweepStaleArtifacts(
|
|
|
287
300
|
};
|
|
288
301
|
|
|
289
302
|
// Sweep telemetry files
|
|
290
|
-
sweepDir(
|
|
291
|
-
|
|
292
|
-
name
|
|
293
|
-
|
|
303
|
+
sweepDir(
|
|
304
|
+
join(stateRoot, ".pi", "telemetry"),
|
|
305
|
+
(name) =>
|
|
306
|
+
name.endsWith(".jsonl") ||
|
|
307
|
+
name.endsWith("-exit.json") ||
|
|
308
|
+
(name.startsWith("lane-prompt-") && name.endsWith(".txt")),
|
|
294
309
|
);
|
|
295
310
|
|
|
296
311
|
// Sweep merge result/request files
|
|
297
|
-
sweepDir(
|
|
298
|
-
(
|
|
299
|
-
(name
|
|
312
|
+
sweepDir(
|
|
313
|
+
join(stateRoot, ".pi"),
|
|
314
|
+
(name) =>
|
|
315
|
+
(name.startsWith("merge-result-") && name.endsWith(".json")) ||
|
|
316
|
+
(name.startsWith("merge-request-") && name.endsWith(".txt")),
|
|
300
317
|
);
|
|
301
318
|
|
|
302
319
|
// Sweep stale worker conversation logs (.pi/worker-conversation-*.jsonl)
|
|
303
|
-
sweepDir(
|
|
304
|
-
|
|
320
|
+
sweepDir(
|
|
321
|
+
join(stateRoot, ".pi"),
|
|
322
|
+
(name) => name.startsWith("worker-conversation-") && name.endsWith(".jsonl"),
|
|
305
323
|
);
|
|
306
324
|
|
|
307
325
|
// Sweep stale lane state files (.pi/lane-state-*.json)
|
|
308
|
-
sweepDir(
|
|
309
|
-
|
|
326
|
+
sweepDir(
|
|
327
|
+
join(stateRoot, ".pi"),
|
|
328
|
+
(name) => name.startsWith("lane-state-") && name.endsWith(".json"),
|
|
310
329
|
);
|
|
311
330
|
|
|
312
331
|
// Sweep stale batch directories under a parent (mailbox, context-snapshots, verification)
|
|
@@ -328,7 +347,9 @@ export function sweepStaleArtifacts(
|
|
|
328
347
|
}
|
|
329
348
|
}
|
|
330
349
|
} catch (err: unknown) {
|
|
331
|
-
result.warnings.push(
|
|
350
|
+
result.warnings.push(
|
|
351
|
+
`Failed to read ${label} directory ${parentDir}: ${(err as Error).message}`,
|
|
352
|
+
);
|
|
332
353
|
}
|
|
333
354
|
};
|
|
334
355
|
|
|
@@ -351,7 +372,11 @@ export function formatPreflightSweep(result: PreflightSweepResult): string {
|
|
|
351
372
|
if (result.skipped) {
|
|
352
373
|
return `ℹ️ Preflight sweep skipped: ${result.skipReason}`;
|
|
353
374
|
}
|
|
354
|
-
if (
|
|
375
|
+
if (
|
|
376
|
+
result.staleFilesDeleted === 0 &&
|
|
377
|
+
result.staleDirsDeleted === 0 &&
|
|
378
|
+
result.warnings.length === 0
|
|
379
|
+
) {
|
|
355
380
|
return ""; // Nothing to report
|
|
356
381
|
}
|
|
357
382
|
const parts: string[] = [];
|
|
@@ -621,27 +646,27 @@ export function cleanupPriorBatchArtifacts(
|
|
|
621
646
|
};
|
|
622
647
|
|
|
623
648
|
// Clean telemetry files from prior batches
|
|
624
|
-
cleanDir(
|
|
625
|
-
|
|
626
|
-
name
|
|
627
|
-
|
|
649
|
+
cleanDir(
|
|
650
|
+
join(piDir, "telemetry"),
|
|
651
|
+
(name) =>
|
|
652
|
+
name.endsWith(".jsonl") ||
|
|
653
|
+
name.endsWith("-exit.json") ||
|
|
654
|
+
(name.startsWith("lane-prompt-") && name.endsWith(".txt")),
|
|
628
655
|
);
|
|
629
656
|
|
|
630
657
|
// Clean merge result/request files from prior batches
|
|
631
|
-
cleanDir(
|
|
632
|
-
|
|
633
|
-
(name
|
|
658
|
+
cleanDir(
|
|
659
|
+
piDir,
|
|
660
|
+
(name) =>
|
|
661
|
+
(name.startsWith("merge-result-") && name.endsWith(".json")) ||
|
|
662
|
+
(name.startsWith("merge-request-") && name.endsWith(".txt")),
|
|
634
663
|
);
|
|
635
664
|
|
|
636
665
|
// Clean worker conversation logs from prior batches
|
|
637
|
-
cleanDir(piDir, (name) =>
|
|
638
|
-
name.startsWith("worker-conversation-") && name.endsWith(".jsonl"),
|
|
639
|
-
);
|
|
666
|
+
cleanDir(piDir, (name) => name.startsWith("worker-conversation-") && name.endsWith(".jsonl"));
|
|
640
667
|
|
|
641
668
|
// Clean lane state files from prior batches
|
|
642
|
-
cleanDir(piDir, (name) =>
|
|
643
|
-
name.startsWith("lane-state-") && name.endsWith(".json"),
|
|
644
|
-
);
|
|
669
|
+
cleanDir(piDir, (name) => name.startsWith("lane-state-") && name.endsWith(".json"));
|
|
645
670
|
|
|
646
671
|
// Clean batch-scoped directories (mailbox, context-snapshots)
|
|
647
672
|
const cleanBatchDirs = (parentDir: string): void => {
|
|
@@ -678,7 +703,9 @@ export function formatPriorBatchCleanup(result: PriorBatchCleanupResult): string
|
|
|
678
703
|
if (result.itemsDeleted === 0 && result.warnings.length === 0) return "";
|
|
679
704
|
const parts: string[] = [];
|
|
680
705
|
if (result.itemsDeleted > 0) {
|
|
681
|
-
parts.push(
|
|
706
|
+
parts.push(
|
|
707
|
+
`🧹 Prior batch cleanup: removed ${result.itemsDeleted} artifact(s) from previous batch(es)`,
|
|
708
|
+
);
|
|
682
709
|
}
|
|
683
710
|
for (const warning of result.warnings) {
|
|
684
711
|
parts.push(` ⚠️ ${warning}`);
|
|
@@ -706,10 +733,7 @@ export interface PreflightCleanupResult {
|
|
|
706
733
|
* @param deps - Sweep dependencies (active batch check)
|
|
707
734
|
* @returns Combined cleanup result
|
|
708
735
|
*/
|
|
709
|
-
export function runPreflightCleanup(
|
|
710
|
-
stateRoot: string,
|
|
711
|
-
deps: SweepDeps,
|
|
712
|
-
): PreflightCleanupResult {
|
|
736
|
+
export function runPreflightCleanup(stateRoot: string, deps: SweepDeps): PreflightCleanupResult {
|
|
713
737
|
const sweep = sweepStaleArtifacts(stateRoot, deps);
|
|
714
738
|
const rotation = rotateSupervisorLogs(stateRoot);
|
|
715
739
|
return { sweep, rotation };
|
|
@@ -724,10 +748,15 @@ export function formatPreflightCleanup(result: PreflightCleanupResult): string {
|
|
|
724
748
|
const parts: string[] = [];
|
|
725
749
|
|
|
726
750
|
// Layer 2: age-based sweep
|
|
727
|
-
if (
|
|
751
|
+
if (
|
|
752
|
+
!result.sweep.skipped &&
|
|
753
|
+
(result.sweep.staleFilesDeleted > 0 || result.sweep.staleDirsDeleted > 0)
|
|
754
|
+
) {
|
|
728
755
|
const segments: string[] = [];
|
|
729
|
-
if (result.sweep.staleFilesDeleted > 0)
|
|
730
|
-
|
|
756
|
+
if (result.sweep.staleFilesDeleted > 0)
|
|
757
|
+
segments.push(`${result.sweep.staleFilesDeleted} stale artifact(s)`);
|
|
758
|
+
if (result.sweep.staleDirsDeleted > 0)
|
|
759
|
+
segments.push(`${result.sweep.staleDirsDeleted} stale mailbox dir(s)`);
|
|
731
760
|
parts.push(`removed ${segments.join(" and ")} (>3 days old)`);
|
|
732
761
|
}
|
|
733
762
|
|
|
@@ -43,9 +43,9 @@ import type {
|
|
|
43
43
|
OrchestratorSection,
|
|
44
44
|
WorkspaceSectionConfig,
|
|
45
45
|
GlobalPreferences,
|
|
46
|
+
DeepPartial,
|
|
46
47
|
} from "./config-schema.ts";
|
|
47
48
|
|
|
48
|
-
|
|
49
49
|
// ── Error Types ──────────────────────────────────────────────────────
|
|
50
50
|
|
|
51
51
|
/**
|
|
@@ -72,7 +72,6 @@ export class ConfigLoadError extends Error {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
|
|
76
75
|
// ── Deep Clone Helper ────────────────────────────────────────────────
|
|
77
76
|
|
|
78
77
|
/** Deep clone a config object to avoid cross-call mutation. */
|
|
@@ -80,7 +79,6 @@ function deepClone<T>(obj: T): T {
|
|
|
80
79
|
return JSON.parse(JSON.stringify(obj));
|
|
81
80
|
}
|
|
82
81
|
|
|
83
|
-
|
|
84
82
|
// ── Deep Merge Helper ────────────────────────────────────────────────
|
|
85
83
|
|
|
86
84
|
/**
|
|
@@ -176,12 +174,16 @@ function migrateGlobalPreferences(raw: Record<string, any>, prefsPath: string):
|
|
|
176
174
|
}
|
|
177
175
|
if (raw.orchestrator?.orchestrator?.spawnMode === "tmux") {
|
|
178
176
|
raw.orchestrator.orchestrator.spawnMode = "subprocess";
|
|
179
|
-
console.error(
|
|
177
|
+
console.error(
|
|
178
|
+
`[taskplane] Auto-migrated global preference: orchestrator.orchestrator.spawnMode "tmux" → "subprocess"`,
|
|
179
|
+
);
|
|
180
180
|
migrated = true;
|
|
181
181
|
}
|
|
182
182
|
if (raw.taskRunner?.worker?.spawnMode === "tmux") {
|
|
183
183
|
raw.taskRunner.worker.spawnMode = "subprocess";
|
|
184
|
-
console.error(
|
|
184
|
+
console.error(
|
|
185
|
+
`[taskplane] Auto-migrated global preference: taskRunner.worker.spawnMode "tmux" → "subprocess"`,
|
|
186
|
+
);
|
|
185
187
|
migrated = true;
|
|
186
188
|
}
|
|
187
189
|
if (migrated) {
|
|
@@ -191,15 +193,18 @@ function migrateGlobalPreferences(raw: Record<string, any>, prefsPath: string):
|
|
|
191
193
|
renameSync(tmpPath, prefsPath);
|
|
192
194
|
console.error(`[taskplane] Preferences file updated: ${prefsPath}`);
|
|
193
195
|
} catch (err) {
|
|
194
|
-
console.error(
|
|
196
|
+
console.error(
|
|
197
|
+
`[taskplane] Warning: could not persist preferences migration to disk: ${err instanceof Error ? err.message : err}`,
|
|
198
|
+
);
|
|
195
199
|
}
|
|
196
200
|
}
|
|
197
201
|
return migrated;
|
|
198
202
|
}
|
|
199
203
|
|
|
200
204
|
/** Reset migration guard (for testing). @internal */
|
|
201
|
-
export function _resetMigrationGuard(): void {
|
|
202
|
-
|
|
205
|
+
export function _resetMigrationGuard(): void {
|
|
206
|
+
_projectMigrationDone = false;
|
|
207
|
+
}
|
|
203
208
|
|
|
204
209
|
// ── YAML snake_case → camelCase Mapping ──────────────────────────────
|
|
205
210
|
|
|
@@ -300,7 +305,8 @@ function mapTaskRunnerYaml(raw: any): Partial<TaskRunnerSection> {
|
|
|
300
305
|
|
|
301
306
|
// Record sections with structural inner keys
|
|
302
307
|
if (raw.task_areas) result.taskAreas = convertRecordSection(raw.task_areas);
|
|
303
|
-
if (raw.standards_overrides)
|
|
308
|
+
if (raw.standards_overrides)
|
|
309
|
+
result.standardsOverrides = convertRecordSection(raw.standards_overrides);
|
|
304
310
|
|
|
305
311
|
// Flat record sections (keys are identifiers, values are strings)
|
|
306
312
|
if (raw.reference_docs) result.referenceDocs = preserveRecord(raw.reference_docs);
|
|
@@ -341,7 +347,8 @@ function mapOrchestratorYaml(raw: any): Partial<OrchestratorSection> {
|
|
|
341
347
|
if (raw.assignment) {
|
|
342
348
|
result.assignment = {};
|
|
343
349
|
if (raw.assignment.strategy !== undefined) result.assignment.strategy = raw.assignment.strategy;
|
|
344
|
-
if (raw.assignment.size_weights)
|
|
350
|
+
if (raw.assignment.size_weights)
|
|
351
|
+
result.assignment.sizeWeights = preserveRecord(raw.assignment.size_weights);
|
|
345
352
|
}
|
|
346
353
|
|
|
347
354
|
// pre_warm: auto_detect is structural, commands is user-defined, always is array
|
|
@@ -398,9 +405,11 @@ function normalizeWorkspaceSection(
|
|
|
398
405
|
};
|
|
399
406
|
}
|
|
400
407
|
|
|
401
|
-
const defaultRepo =
|
|
408
|
+
const defaultRepo =
|
|
409
|
+
typeof rawRouting.defaultRepo === "string" ? rawRouting.defaultRepo.trim() : "";
|
|
402
410
|
const tasksRoot = typeof rawRouting.tasksRoot === "string" ? rawRouting.tasksRoot.trim() : "";
|
|
403
|
-
let taskPacketRepo =
|
|
411
|
+
let taskPacketRepo =
|
|
412
|
+
typeof rawRouting.taskPacketRepo === "string" ? rawRouting.taskPacketRepo.trim() : "";
|
|
404
413
|
|
|
405
414
|
if (!taskPacketRepo && defaultRepo) {
|
|
406
415
|
taskPacketRepo = defaultRepo;
|
|
@@ -426,7 +435,6 @@ function normalizeWorkspaceSection(
|
|
|
426
435
|
};
|
|
427
436
|
}
|
|
428
437
|
|
|
429
|
-
|
|
430
438
|
// ── Config File Path Resolution ──────────────────────────────────────
|
|
431
439
|
|
|
432
440
|
/**
|
|
@@ -464,7 +472,7 @@ function resolveConfigFilePath(configRoot: string, filename: string): string {
|
|
|
464
472
|
* Returns the parsed config or null if the file doesn't exist.
|
|
465
473
|
* Throws ConfigLoadError for malformed JSON or unsupported versions.
|
|
466
474
|
*/
|
|
467
|
-
function loadJsonConfig(configRoot: string):
|
|
475
|
+
function loadJsonConfig(configRoot: string): DeepPartial<TaskplaneConfig> | null {
|
|
468
476
|
const jsonPath = resolveConfigFilePath(configRoot, PROJECT_CONFIG_FILENAME);
|
|
469
477
|
if (!existsSync(jsonPath)) return null;
|
|
470
478
|
|
|
@@ -490,7 +498,7 @@ function loadJsonConfig(configRoot: string): Partial<TaskplaneConfig> | null {
|
|
|
490
498
|
throw new ConfigLoadError(
|
|
491
499
|
"CONFIG_VERSION_MISSING",
|
|
492
500
|
`${jsonPath} is missing required field "configVersion". ` +
|
|
493
|
-
|
|
501
|
+
`Expected configVersion: ${CONFIG_VERSION}.`,
|
|
494
502
|
);
|
|
495
503
|
}
|
|
496
504
|
|
|
@@ -498,15 +506,23 @@ function loadJsonConfig(configRoot: string): Partial<TaskplaneConfig> | null {
|
|
|
498
506
|
throw new ConfigLoadError(
|
|
499
507
|
"CONFIG_VERSION_UNSUPPORTED",
|
|
500
508
|
`${jsonPath} has configVersion ${parsed.configVersion}, but this version of Taskplane ` +
|
|
501
|
-
|
|
509
|
+
`only supports configVersion ${CONFIG_VERSION}. Please upgrade Taskplane.`,
|
|
502
510
|
);
|
|
503
511
|
}
|
|
504
512
|
|
|
505
|
-
const overrides:
|
|
506
|
-
if (
|
|
513
|
+
const overrides: DeepPartial<TaskplaneConfig> = {};
|
|
514
|
+
if (
|
|
515
|
+
parsed.taskRunner &&
|
|
516
|
+
typeof parsed.taskRunner === "object" &&
|
|
517
|
+
!Array.isArray(parsed.taskRunner)
|
|
518
|
+
) {
|
|
507
519
|
overrides.taskRunner = deepClone(parsed.taskRunner);
|
|
508
520
|
}
|
|
509
|
-
if (
|
|
521
|
+
if (
|
|
522
|
+
parsed.orchestrator &&
|
|
523
|
+
typeof parsed.orchestrator === "object" &&
|
|
524
|
+
!Array.isArray(parsed.orchestrator)
|
|
525
|
+
) {
|
|
510
526
|
overrides.orchestrator = deepClone(parsed.orchestrator);
|
|
511
527
|
}
|
|
512
528
|
if (parsed.workspace) {
|
|
@@ -519,7 +535,6 @@ function loadJsonConfig(configRoot: string): Partial<TaskplaneConfig> | null {
|
|
|
519
535
|
return overrides;
|
|
520
536
|
}
|
|
521
537
|
|
|
522
|
-
|
|
523
538
|
// ── YAML Loading ─────────────────────────────────────────────────────
|
|
524
539
|
|
|
525
540
|
/**
|
|
@@ -612,7 +627,6 @@ function loadWorkspaceYaml(configRoot: string): WorkspaceSectionConfig | undefin
|
|
|
612
627
|
}
|
|
613
628
|
}
|
|
614
629
|
|
|
615
|
-
|
|
616
630
|
// ── Global Preferences (Layer 2) ─────────────────────────────────────
|
|
617
631
|
|
|
618
632
|
/**
|
|
@@ -703,7 +717,12 @@ export function loadGlobalPreferencesWithMeta(): GlobalPreferencesLoadResult {
|
|
|
703
717
|
};
|
|
704
718
|
}
|
|
705
719
|
|
|
706
|
-
if (
|
|
720
|
+
if (
|
|
721
|
+
!parsed ||
|
|
722
|
+
typeof parsed !== "object" ||
|
|
723
|
+
Array.isArray(parsed) ||
|
|
724
|
+
Object.keys(parsed).length === 0
|
|
725
|
+
) {
|
|
707
726
|
return {
|
|
708
727
|
preferences: bootstrapGlobalPreferencesFile(prefsPath),
|
|
709
728
|
wasBootstrapped: true,
|
|
@@ -730,7 +749,9 @@ export function loadGlobalPreferences(): GlobalPreferences {
|
|
|
730
749
|
* Unknown keys are silently dropped — this is the Layer 2 boundary guardrail.
|
|
731
750
|
*/
|
|
732
751
|
function normalizePreferenceThinkingMode(value: unknown): string {
|
|
733
|
-
const cleaned = String(value ?? "")
|
|
752
|
+
const cleaned = String(value ?? "")
|
|
753
|
+
.trim()
|
|
754
|
+
.toLowerCase();
|
|
734
755
|
if (!cleaned || cleaned === "inherit") return "";
|
|
735
756
|
if (cleaned === "on") return "high";
|
|
736
757
|
if (["off", "minimal", "low", "medium", "high", "xhigh"].includes(cleaned)) {
|
|
@@ -739,7 +760,9 @@ function normalizePreferenceThinkingMode(value: unknown): string {
|
|
|
739
760
|
return "";
|
|
740
761
|
}
|
|
741
762
|
|
|
742
|
-
function extractInitAgentDefaults(
|
|
763
|
+
function extractInitAgentDefaults(
|
|
764
|
+
rawInitDefaults: unknown,
|
|
765
|
+
): GlobalPreferences["initAgentDefaults"] | undefined {
|
|
743
766
|
if (!rawInitDefaults || typeof rawInitDefaults !== "object" || Array.isArray(rawInitDefaults)) {
|
|
744
767
|
return undefined;
|
|
745
768
|
}
|
|
@@ -750,9 +773,12 @@ function extractInitAgentDefaults(rawInitDefaults: unknown): GlobalPreferences["
|
|
|
750
773
|
if (typeof raw.workerModel === "string") extracted.workerModel = raw.workerModel;
|
|
751
774
|
if (typeof raw.reviewerModel === "string") extracted.reviewerModel = raw.reviewerModel;
|
|
752
775
|
if (typeof raw.mergeModel === "string") extracted.mergeModel = raw.mergeModel;
|
|
753
|
-
if (raw.workerThinking !== undefined)
|
|
754
|
-
|
|
755
|
-
if (raw.
|
|
776
|
+
if (raw.workerThinking !== undefined)
|
|
777
|
+
extracted.workerThinking = normalizePreferenceThinkingMode(raw.workerThinking);
|
|
778
|
+
if (raw.reviewerThinking !== undefined)
|
|
779
|
+
extracted.reviewerThinking = normalizePreferenceThinkingMode(raw.reviewerThinking);
|
|
780
|
+
if (raw.mergeThinking !== undefined)
|
|
781
|
+
extracted.mergeThinking = normalizePreferenceThinkingMode(raw.mergeThinking);
|
|
756
782
|
|
|
757
783
|
return Object.keys(extracted).length > 0 ? extracted : undefined;
|
|
758
784
|
}
|
|
@@ -764,7 +790,10 @@ function extractConfigOverrideSection(rawSection: unknown): Record<string, any>
|
|
|
764
790
|
return deepClone(rawSection as Record<string, any>);
|
|
765
791
|
}
|
|
766
792
|
|
|
767
|
-
function extractAllowlistedPreferences(
|
|
793
|
+
function extractAllowlistedPreferences(
|
|
794
|
+
raw: Record<string, any>,
|
|
795
|
+
prefsPath: string,
|
|
796
|
+
): GlobalPreferences {
|
|
768
797
|
migrateGlobalPreferences(raw, prefsPath);
|
|
769
798
|
|
|
770
799
|
const prefs: GlobalPreferences = {};
|
|
@@ -821,27 +850,46 @@ function extractAllowlistedPreferences(raw: Record<string, any>, prefsPath: stri
|
|
|
821
850
|
* Preferences-only fields (`dashboardPort`, `initAgentDefaults`) are preserved
|
|
822
851
|
* in `GlobalPreferences` but intentionally not merged into runtime config.
|
|
823
852
|
*/
|
|
824
|
-
export function applyGlobalPreferences(
|
|
853
|
+
export function applyGlobalPreferences(
|
|
854
|
+
config: TaskplaneConfig,
|
|
855
|
+
prefs: GlobalPreferences,
|
|
856
|
+
): TaskplaneConfig {
|
|
825
857
|
// Helper: only apply non-empty string values
|
|
826
858
|
const applyStr = (val: string | undefined, setter: (v: string) => void) => {
|
|
827
859
|
if (val !== undefined && val !== "") setter(val);
|
|
828
860
|
};
|
|
829
861
|
|
|
830
862
|
// 1) Legacy flat aliases
|
|
831
|
-
applyStr(prefs.operatorId, (v) => {
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
applyStr(prefs.
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
applyStr(prefs.
|
|
863
|
+
applyStr(prefs.operatorId, (v) => {
|
|
864
|
+
config.orchestrator.orchestrator.operatorId = v;
|
|
865
|
+
});
|
|
866
|
+
applyStr(prefs.sessionPrefix, (v) => {
|
|
867
|
+
config.orchestrator.orchestrator.sessionPrefix = v;
|
|
868
|
+
});
|
|
869
|
+
applyStr(prefs.workerModel, (v) => {
|
|
870
|
+
config.taskRunner.worker.model = v;
|
|
871
|
+
});
|
|
872
|
+
applyStr(prefs.reviewerModel, (v) => {
|
|
873
|
+
config.taskRunner.reviewer.model = v;
|
|
874
|
+
});
|
|
875
|
+
applyStr(prefs.mergeModel, (v) => {
|
|
876
|
+
config.orchestrator.merge.model = v;
|
|
877
|
+
});
|
|
878
|
+
applyStr(prefs.mergeThinking, (v) => {
|
|
879
|
+
config.orchestrator.merge.thinking = v;
|
|
880
|
+
});
|
|
881
|
+
applyStr(prefs.supervisorModel, (v) => {
|
|
882
|
+
config.orchestrator.supervisor.model = v;
|
|
883
|
+
});
|
|
838
884
|
|
|
839
885
|
// spawnMode: enum — apply if defined (not a string-empty check)
|
|
886
|
+
// TP-195: dropped dead `prefs.spawnMode === "tmux"` migration check.
|
|
887
|
+
// `prefs.spawnMode` is typed as `"subprocess"` only (see
|
|
888
|
+
// `GlobalPreferences.spawnMode` in config-schema.ts). Raw input is
|
|
889
|
+
// migrated upstream at line ~169 BEFORE assignment to the typed
|
|
890
|
+
// `prefs` object, so by this point the value is already "subprocess"
|
|
891
|
+
// or undefined — the comparison can never be true. Behavior-neutral.
|
|
840
892
|
if (prefs.spawnMode !== undefined) {
|
|
841
|
-
if (prefs.spawnMode === "tmux") {
|
|
842
|
-
prefs.spawnMode = "subprocess";
|
|
843
|
-
console.error(`[taskplane] Auto-migrated runtime preference: spawnMode "tmux" → "subprocess"`);
|
|
844
|
-
}
|
|
845
893
|
config.orchestrator.orchestrator.spawnMode = prefs.spawnMode;
|
|
846
894
|
}
|
|
847
895
|
|
|
@@ -862,11 +910,15 @@ export function applyGlobalPreferences(config: TaskplaneConfig, prefs: GlobalPre
|
|
|
862
910
|
// Runtime safety: nested legacy values may arrive through config-shaped overrides.
|
|
863
911
|
if ((config.orchestrator.orchestrator as Record<string, any>).spawnMode === "tmux") {
|
|
864
912
|
config.orchestrator.orchestrator.spawnMode = "subprocess";
|
|
865
|
-
console.error(
|
|
913
|
+
console.error(
|
|
914
|
+
`[taskplane] Auto-migrated runtime global preference: orchestrator.orchestrator.spawnMode "tmux" → "subprocess"`,
|
|
915
|
+
);
|
|
866
916
|
}
|
|
867
917
|
if ((config.taskRunner.worker as Record<string, any>).spawnMode === "tmux") {
|
|
868
918
|
config.taskRunner.worker.spawnMode = "subprocess";
|
|
869
|
-
console.error(
|
|
919
|
+
console.error(
|
|
920
|
+
`[taskplane] Auto-migrated runtime global preference: taskRunner.worker.spawnMode "tmux" → "subprocess"`,
|
|
921
|
+
);
|
|
870
922
|
}
|
|
871
923
|
|
|
872
924
|
return config;
|
|
@@ -891,11 +943,7 @@ export function hasConfigFiles(root: string): boolean {
|
|
|
891
943
|
// coordination file, not a project config). Without this distinction,
|
|
892
944
|
// workspace root's .pi/taskplane-workspace.yaml causes resolveConfigRoot
|
|
893
945
|
// to short-circuit before checking the pointer-resolved config root (#424).
|
|
894
|
-
const files = [
|
|
895
|
-
PROJECT_CONFIG_FILENAME,
|
|
896
|
-
"task-runner.yaml",
|
|
897
|
-
"task-orchestrator.yaml",
|
|
898
|
-
];
|
|
946
|
+
const files = [PROJECT_CONFIG_FILENAME, "task-runner.yaml", "task-orchestrator.yaml"];
|
|
899
947
|
for (const f of files) {
|
|
900
948
|
if (existsSync(join(root, ".pi", f)) || existsSync(join(root, f))) return true;
|
|
901
949
|
}
|
|
@@ -937,12 +985,18 @@ export function resolveConfigRoot(cwd: string, pointerConfigRoot?: string): stri
|
|
|
937
985
|
return cwd;
|
|
938
986
|
}
|
|
939
987
|
|
|
940
|
-
function mergeProjectOverrides(
|
|
988
|
+
function mergeProjectOverrides(
|
|
989
|
+
config: TaskplaneConfig,
|
|
990
|
+
overrides: DeepPartial<TaskplaneConfig>,
|
|
991
|
+
): void {
|
|
941
992
|
if (overrides.taskRunner) {
|
|
942
993
|
deepMerge(config.taskRunner as Record<string, any>, overrides.taskRunner as Record<string, any>);
|
|
943
994
|
}
|
|
944
995
|
if (overrides.orchestrator) {
|
|
945
|
-
deepMerge(
|
|
996
|
+
deepMerge(
|
|
997
|
+
config.orchestrator as Record<string, any>,
|
|
998
|
+
overrides.orchestrator as Record<string, any>,
|
|
999
|
+
);
|
|
946
1000
|
}
|
|
947
1001
|
if (overrides.workspace) {
|
|
948
1002
|
if (!config.workspace || typeof config.workspace !== "object") {
|
|
@@ -952,11 +1006,27 @@ function mergeProjectOverrides(config: TaskplaneConfig, overrides: Partial<Taskp
|
|
|
952
1006
|
}
|
|
953
1007
|
}
|
|
954
1008
|
|
|
955
|
-
|
|
1009
|
+
// TP-195: switched parameter to `DeepPartial<TaskplaneConfig>` to match the
|
|
1010
|
+
// nested-section partial shape produced by `loadProjectOverrides` (the YAML
|
|
1011
|
+
// loaders return `Partial<TaskRunnerSection>` etc., which `Partial<TaskplaneConfig>`
|
|
1012
|
+
// rejects — it makes top-level fields optional but inner sections stay full).
|
|
1013
|
+
function migrateProjectOverrides(
|
|
1014
|
+
overrides: DeepPartial<TaskplaneConfig>,
|
|
1015
|
+
configRoot: string,
|
|
1016
|
+
): boolean {
|
|
956
1017
|
if (_projectMigrationDone) return false;
|
|
957
1018
|
|
|
958
1019
|
let migrated = false;
|
|
959
|
-
|
|
1020
|
+
// TP-195: 2-step `as unknown as` widening. The structurally-typed
|
|
1021
|
+
// `OrchestratorCoreConfig` is being treated as a property bag for
|
|
1022
|
+
// migration purposes (legacy `tmuxPrefix` -> `sessionPrefix`,
|
|
1023
|
+
// `spawnMode "tmux"` -> `"subprocess"`). Both source and target
|
|
1024
|
+
// types are object-shaped at runtime; the cast is structurally
|
|
1025
|
+
// legitimate, just outside the narrow set of conversions TS allows
|
|
1026
|
+
// in a single step.
|
|
1027
|
+
const orchestratorCore = overrides.orchestrator?.orchestrator as unknown as
|
|
1028
|
+
| Record<string, unknown>
|
|
1029
|
+
| undefined;
|
|
960
1030
|
if (orchestratorCore && hasOwn(orchestratorCore, "tmuxPrefix")) {
|
|
961
1031
|
const currentPrefix = orchestratorCore.sessionPrefix;
|
|
962
1032
|
const isDefault = currentPrefix === undefined || currentPrefix === "orch";
|
|
@@ -969,11 +1039,17 @@ function migrateProjectOverrides(overrides: Partial<TaskplaneConfig>, configRoot
|
|
|
969
1039
|
}
|
|
970
1040
|
if (orchestratorCore?.spawnMode === "tmux") {
|
|
971
1041
|
(orchestratorCore as any).spawnMode = "subprocess";
|
|
972
|
-
console.error(
|
|
1042
|
+
console.error(
|
|
1043
|
+
`[taskplane] Auto-migrated: orchestrator.orchestrator.spawnMode "tmux" → "subprocess"`,
|
|
1044
|
+
);
|
|
973
1045
|
migrated = true;
|
|
974
1046
|
}
|
|
975
1047
|
|
|
976
|
-
|
|
1048
|
+
// TP-195: 2-step `as unknown as` widening (same rationale as the
|
|
1049
|
+
// orchestratorCore cast above).
|
|
1050
|
+
const workerConfig = overrides.taskRunner?.worker as unknown as
|
|
1051
|
+
| Record<string, unknown>
|
|
1052
|
+
| undefined;
|
|
977
1053
|
if (workerConfig?.spawnMode === "tmux") {
|
|
978
1054
|
(workerConfig as any).spawnMode = "subprocess";
|
|
979
1055
|
console.error(`[taskplane] Auto-migrated: taskRunner.worker.spawnMode "tmux" → "subprocess"`);
|
|
@@ -1005,7 +1081,9 @@ function migrateProjectOverrides(overrides: Partial<TaskplaneConfig>, configRoot
|
|
|
1005
1081
|
console.error(`[taskplane] Config file updated: ${jsonPath}`);
|
|
1006
1082
|
}
|
|
1007
1083
|
} catch (err) {
|
|
1008
|
-
console.error(
|
|
1084
|
+
console.error(
|
|
1085
|
+
`[taskplane] Warning: could not persist config migration to disk: ${err instanceof Error ? err.message : err}`,
|
|
1086
|
+
);
|
|
1009
1087
|
}
|
|
1010
1088
|
}
|
|
1011
1089
|
|
|
@@ -1013,7 +1091,12 @@ function migrateProjectOverrides(overrides: Partial<TaskplaneConfig>, configRoot
|
|
|
1013
1091
|
return migrated;
|
|
1014
1092
|
}
|
|
1015
1093
|
|
|
1016
|
-
|
|
1094
|
+
// TP-195: return type widened from `Partial<TaskplaneConfig>` to
|
|
1095
|
+
// `DeepPartial<TaskplaneConfig>` so the nested `Partial<TaskRunnerSection>` /
|
|
1096
|
+
// `Partial<OrchestratorSection>` returned by the YAML loaders are
|
|
1097
|
+
// assignable. `Partial<TaskplaneConfig>` only relaxes top-level optionality
|
|
1098
|
+
// while keeping inner sections fully required.
|
|
1099
|
+
export function loadProjectOverrides(configRoot: string): DeepPartial<TaskplaneConfig> {
|
|
1017
1100
|
const jsonOverrides = loadJsonConfig(configRoot);
|
|
1018
1101
|
if (jsonOverrides !== null) {
|
|
1019
1102
|
return jsonOverrides;
|
|
@@ -1023,7 +1106,7 @@ export function loadProjectOverrides(configRoot: string): Partial<TaskplaneConfi
|
|
|
1023
1106
|
const orchestrator = loadOrchestratorYaml(configRoot);
|
|
1024
1107
|
const workspace = loadWorkspaceYaml(configRoot);
|
|
1025
1108
|
|
|
1026
|
-
const overrides:
|
|
1109
|
+
const overrides: DeepPartial<TaskplaneConfig> = {};
|
|
1027
1110
|
if (Object.keys(taskRunner).length > 0) overrides.taskRunner = taskRunner;
|
|
1028
1111
|
if (Object.keys(orchestrator).length > 0) overrides.orchestrator = orchestrator;
|
|
1029
1112
|
if (workspace) overrides.workspace = workspace;
|
|
@@ -1077,7 +1160,6 @@ export function loadLayer1Config(cwd: string, pointerConfigRoot?: string): Taskp
|
|
|
1077
1160
|
return config;
|
|
1078
1161
|
}
|
|
1079
1162
|
|
|
1080
|
-
|
|
1081
1163
|
// ── Backward-Compatible Adapters ─────────────────────────────────────
|
|
1082
1164
|
|
|
1083
1165
|
// The following adapter functions convert the unified camelCase config
|
|
@@ -1090,7 +1172,9 @@ export function loadLayer1Config(cwd: string, pointerConfigRoot?: string): Taskp
|
|
|
1090
1172
|
* to preserve record/dictionary keys verbatim (e.g., sizeWeights S/M/L,
|
|
1091
1173
|
* preWarm.commands keys, etc.).
|
|
1092
1174
|
*/
|
|
1093
|
-
export function toOrchestratorConfig(
|
|
1175
|
+
export function toOrchestratorConfig(
|
|
1176
|
+
config: TaskplaneConfig,
|
|
1177
|
+
): import("./types.ts").OrchestratorConfig {
|
|
1094
1178
|
const o = config.orchestrator;
|
|
1095
1179
|
return {
|
|
1096
1180
|
orchestrator: {
|