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.
Files changed (41) hide show
  1. package/bin/gitignore-patterns.mjs +11 -8
  2. package/bin/rpc-wrapper.mjs +410 -357
  3. package/bin/taskplane.mjs +533 -250
  4. package/extensions/reviewer-extension.ts +17 -11
  5. package/extensions/taskplane/abort.ts +50 -18
  6. package/extensions/taskplane/agent-bridge-extension.ts +232 -105
  7. package/extensions/taskplane/agent-host.ts +224 -97
  8. package/extensions/taskplane/cleanup.ts +71 -42
  9. package/extensions/taskplane/config-loader.ts +142 -58
  10. package/extensions/taskplane/config-schema.ts +6 -13
  11. package/extensions/taskplane/config.ts +10 -2
  12. package/extensions/taskplane/diagnostic-reports.ts +59 -47
  13. package/extensions/taskplane/diagnostics.ts +13 -13
  14. package/extensions/taskplane/discovery.ts +35 -61
  15. package/extensions/taskplane/engine-worker.ts +53 -46
  16. package/extensions/taskplane/engine.ts +1760 -602
  17. package/extensions/taskplane/execution.ts +426 -206
  18. package/extensions/taskplane/extension.ts +1073 -598
  19. package/extensions/taskplane/formatting.ts +136 -124
  20. package/extensions/taskplane/git.ts +0 -2
  21. package/extensions/taskplane/lane-runner.ts +542 -311
  22. package/extensions/taskplane/mailbox.ts +57 -49
  23. package/extensions/taskplane/merge.ts +662 -383
  24. package/extensions/taskplane/messages.ts +109 -51
  25. package/extensions/taskplane/migrations.ts +1 -1
  26. package/extensions/taskplane/path-resolver.ts +8 -9
  27. package/extensions/taskplane/persistence.ts +425 -262
  28. package/extensions/taskplane/process-registry.ts +36 -7
  29. package/extensions/taskplane/quality-gate.ts +107 -55
  30. package/extensions/taskplane/resume.ts +774 -267
  31. package/extensions/taskplane/sessions.ts +1 -1
  32. package/extensions/taskplane/settings-tui.ts +505 -164
  33. package/extensions/taskplane/sidecar-telemetry.ts +25 -10
  34. package/extensions/taskplane/supervisor.ts +477 -270
  35. package/extensions/taskplane/task-executor-core.ts +178 -53
  36. package/extensions/taskplane/types.ts +186 -108
  37. package/extensions/taskplane/verification.ts +27 -22
  38. package/extensions/taskplane/waves.ts +59 -43
  39. package/extensions/taskplane/workspace.ts +14 -12
  40. package/extensions/taskplane/worktree.ts +218 -196
  41. 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(stateRoot: string, batchId: string): PostIntegrateCleanupResult {
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 (entry.includes(batchId) && (
120
- (entry.startsWith("merge-result-") && entry.endsWith(".json")) ||
121
- (entry.startsWith("merge-request-") && entry.endsWith(".txt"))
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(`Failed to delete mailbox directory ${mailboxBatchDir}: ${(err as Error).message}`);
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(`Failed to delete context-snapshots directory ${snapshotBatchDir}: ${(err as Error).message}`);
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 = result.telemetryFilesDeleted + result.mergeFilesDeleted + result.promptFilesDeleted + result.mailboxDirsDeleted + result.snapshotDirsDeleted;
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(join(stateRoot, ".pi", "telemetry"), (name) =>
291
- name.endsWith(".jsonl") ||
292
- name.endsWith("-exit.json") ||
293
- (name.startsWith("lane-prompt-") && name.endsWith(".txt")),
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(join(stateRoot, ".pi"), (name) =>
298
- (name.startsWith("merge-result-") && name.endsWith(".json")) ||
299
- (name.startsWith("merge-request-") && name.endsWith(".txt")),
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(join(stateRoot, ".pi"), (name) =>
304
- name.startsWith("worker-conversation-") && name.endsWith(".jsonl"),
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(join(stateRoot, ".pi"), (name) =>
309
- name.startsWith("lane-state-") && name.endsWith(".json"),
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(`Failed to read ${label} directory ${parentDir}: ${(err as Error).message}`);
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 (result.staleFilesDeleted === 0 && result.staleDirsDeleted === 0 && result.warnings.length === 0) {
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(join(piDir, "telemetry"), (name) =>
625
- name.endsWith(".jsonl") ||
626
- name.endsWith("-exit.json") ||
627
- (name.startsWith("lane-prompt-") && name.endsWith(".txt")),
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(piDir, (name) =>
632
- (name.startsWith("merge-result-") && name.endsWith(".json")) ||
633
- (name.startsWith("merge-request-") && name.endsWith(".txt")),
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(`🧹 Prior batch cleanup: removed ${result.itemsDeleted} artifact(s) from previous batch(es)`);
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 (!result.sweep.skipped && (result.sweep.staleFilesDeleted > 0 || result.sweep.staleDirsDeleted > 0)) {
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) segments.push(`${result.sweep.staleFilesDeleted} stale artifact(s)`);
730
- if (result.sweep.staleDirsDeleted > 0) segments.push(`${result.sweep.staleDirsDeleted} stale mailbox dir(s)`);
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(`[taskplane] Auto-migrated global preference: orchestrator.orchestrator.spawnMode "tmux" → "subprocess"`);
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(`[taskplane] Auto-migrated global preference: taskRunner.worker.spawnMode "tmux" → "subprocess"`);
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(`[taskplane] Warning: could not persist preferences migration to disk: ${err instanceof Error ? err.message : err}`);
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 { _projectMigrationDone = false; }
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) result.standardsOverrides = convertRecordSection(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) result.assignment.sizeWeights = preserveRecord(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 = typeof rawRouting.defaultRepo === "string" ? rawRouting.defaultRepo.trim() : "";
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 = typeof rawRouting.taskPacketRepo === "string" ? rawRouting.taskPacketRepo.trim() : "";
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): Partial<TaskplaneConfig> | null {
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
- `Expected configVersion: ${CONFIG_VERSION}.`,
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
- `only supports configVersion ${CONFIG_VERSION}. Please upgrade Taskplane.`,
509
+ `only supports configVersion ${CONFIG_VERSION}. Please upgrade Taskplane.`,
502
510
  );
503
511
  }
504
512
 
505
- const overrides: Partial<TaskplaneConfig> = {};
506
- if (parsed.taskRunner && typeof parsed.taskRunner === "object" && !Array.isArray(parsed.taskRunner)) {
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 (parsed.orchestrator && typeof parsed.orchestrator === "object" && !Array.isArray(parsed.orchestrator)) {
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 (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || Object.keys(parsed).length === 0) {
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 ?? "").trim().toLowerCase();
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(rawInitDefaults: unknown): GlobalPreferences["initAgentDefaults"] | undefined {
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) extracted.workerThinking = normalizePreferenceThinkingMode(raw.workerThinking);
754
- if (raw.reviewerThinking !== undefined) extracted.reviewerThinking = normalizePreferenceThinkingMode(raw.reviewerThinking);
755
- if (raw.mergeThinking !== undefined) extracted.mergeThinking = normalizePreferenceThinkingMode(raw.mergeThinking);
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(raw: Record<string, any>, prefsPath: string): GlobalPreferences {
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(config: TaskplaneConfig, prefs: GlobalPreferences): TaskplaneConfig {
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) => { config.orchestrator.orchestrator.operatorId = v; });
832
- applyStr(prefs.sessionPrefix, (v) => { config.orchestrator.orchestrator.sessionPrefix = v; });
833
- applyStr(prefs.workerModel, (v) => { config.taskRunner.worker.model = v; });
834
- applyStr(prefs.reviewerModel, (v) => { config.taskRunner.reviewer.model = v; });
835
- applyStr(prefs.mergeModel, (v) => { config.orchestrator.merge.model = v; });
836
- applyStr(prefs.mergeThinking, (v) => { config.orchestrator.merge.thinking = v; });
837
- applyStr(prefs.supervisorModel, (v) => { config.orchestrator.supervisor.model = v; });
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(`[taskplane] Auto-migrated runtime global preference: orchestrator.orchestrator.spawnMode "tmux" → "subprocess"`);
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(`[taskplane] Auto-migrated runtime global preference: taskRunner.worker.spawnMode "tmux" → "subprocess"`);
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(config: TaskplaneConfig, overrides: Partial<TaskplaneConfig>): void {
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(config.orchestrator as Record<string, any>, overrides.orchestrator as Record<string, any>);
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
- function migrateProjectOverrides(overrides: Partial<TaskplaneConfig>, configRoot: string): boolean {
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
- const orchestratorCore = overrides.orchestrator?.orchestrator as Record<string, unknown> | undefined;
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(`[taskplane] Auto-migrated: orchestrator.orchestrator.spawnMode "tmux" → "subprocess"`);
1042
+ console.error(
1043
+ `[taskplane] Auto-migrated: orchestrator.orchestrator.spawnMode "tmux" → "subprocess"`,
1044
+ );
973
1045
  migrated = true;
974
1046
  }
975
1047
 
976
- const workerConfig = overrides.taskRunner?.worker as Record<string, unknown> | undefined;
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(`[taskplane] Warning: could not persist config migration to disk: ${err instanceof Error ? err.message : err}`);
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
- export function loadProjectOverrides(configRoot: string): Partial<TaskplaneConfig> {
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: Partial<TaskplaneConfig> = {};
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(config: TaskplaneConfig): import("./types.ts").OrchestratorConfig {
1175
+ export function toOrchestratorConfig(
1176
+ config: TaskplaneConfig,
1177
+ ): import("./types.ts").OrchestratorConfig {
1094
1178
  const o = config.orchestrator;
1095
1179
  return {
1096
1180
  orchestrator: {