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
@@ -19,7 +19,14 @@
19
19
 
20
20
  import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
21
21
  import { DynamicBorder, getSettingsListTheme } from "@mariozechner/pi-coding-agent";
22
- import { Container, type SelectItem, SelectList, type SettingItem, SettingsList, Text } from "@mariozechner/pi-tui";
22
+ import {
23
+ Container,
24
+ type SelectItem,
25
+ SelectList,
26
+ type SettingItem,
27
+ SettingsList,
28
+ Text,
29
+ } from "@mariozechner/pi-tui";
23
30
  import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync, unlinkSync } from "fs";
24
31
  import { join, dirname } from "path";
25
32
  import { parse as yamlParse } from "yaml";
@@ -40,7 +47,6 @@ import {
40
47
  } from "./config-loader.ts";
41
48
  import { loadPiSettingsPackages } from "./settings-loader.ts";
42
49
 
43
-
44
50
  // ── Types ────────────────────────────────────────────────────────────
45
51
 
46
52
  /** Source of a field's current value */
@@ -84,7 +90,6 @@ export interface SectionDef {
84
90
  readOnly?: boolean;
85
91
  }
86
92
 
87
-
88
93
  // ── Section & Field Definitions ──────────────────────────────────────
89
94
 
90
95
  /**
@@ -95,114 +100,401 @@ export const SECTIONS: SectionDef[] = [
95
100
  {
96
101
  name: "Orchestrator",
97
102
  fields: [
98
- { configPath: "orchestrator.orchestrator.maxLanes", label: "Max Lanes", control: "input", layer: "L1", fieldType: "number", description: "Maximum parallel execution lanes" },
99
- { configPath: "orchestrator.orchestrator.worktreeLocation", label: "Worktree Location", control: "toggle", layer: "L1", fieldType: "enum", values: ["sibling", "subdirectory"], description: "Where lane worktree directories are created" },
100
- { configPath: "orchestrator.orchestrator.worktreePrefix", label: "Worktree Prefix", control: "input", layer: "L1", fieldType: "string", description: "Prefix for worktree directory names" },
101
- { configPath: "orchestrator.orchestrator.batchIdFormat", label: "Batch ID Format", control: "toggle", layer: "L1", fieldType: "enum", values: ["timestamp", "sequential"], description: "Batch ID format for logs/branch naming" },
102
- { configPath: "orchestrator.orchestrator.sessionPrefix", label: "Session Prefix", control: "input", layer: "L1+L2", fieldType: "string", prefsKey: "sessionPrefix", description: "Prefix for orchestrator session names" },
103
- { configPath: "orchestrator.orchestrator.operatorId", label: "Operator ID", control: "input", layer: "L1+L2", fieldType: "string", prefsKey: "operatorId", description: "Operator identifier (empty = auto-detect)" },
104
- { configPath: "orchestrator.orchestrator.integration", label: "Integration", control: "picker", layer: "L1", fieldType: "enum", values: ["manual", "supervised", "auto"], description: "How completed batches are integrated. manual = user runs /orch-integrate. supervised = supervisor proposes plan, asks confirmation. auto = supervisor executes without asking." },
103
+ {
104
+ configPath: "orchestrator.orchestrator.maxLanes",
105
+ label: "Max Lanes",
106
+ control: "input",
107
+ layer: "L1",
108
+ fieldType: "number",
109
+ description: "Maximum parallel execution lanes",
110
+ },
111
+ {
112
+ configPath: "orchestrator.orchestrator.worktreeLocation",
113
+ label: "Worktree Location",
114
+ control: "toggle",
115
+ layer: "L1",
116
+ fieldType: "enum",
117
+ values: ["sibling", "subdirectory"],
118
+ description: "Where lane worktree directories are created",
119
+ },
120
+ {
121
+ configPath: "orchestrator.orchestrator.worktreePrefix",
122
+ label: "Worktree Prefix",
123
+ control: "input",
124
+ layer: "L1",
125
+ fieldType: "string",
126
+ description: "Prefix for worktree directory names",
127
+ },
128
+ {
129
+ configPath: "orchestrator.orchestrator.batchIdFormat",
130
+ label: "Batch ID Format",
131
+ control: "toggle",
132
+ layer: "L1",
133
+ fieldType: "enum",
134
+ values: ["timestamp", "sequential"],
135
+ description: "Batch ID format for logs/branch naming",
136
+ },
137
+ {
138
+ configPath: "orchestrator.orchestrator.sessionPrefix",
139
+ label: "Session Prefix",
140
+ control: "input",
141
+ layer: "L1+L2",
142
+ fieldType: "string",
143
+ prefsKey: "sessionPrefix",
144
+ description: "Prefix for orchestrator session names",
145
+ },
146
+ {
147
+ configPath: "orchestrator.orchestrator.operatorId",
148
+ label: "Operator ID",
149
+ control: "input",
150
+ layer: "L1+L2",
151
+ fieldType: "string",
152
+ prefsKey: "operatorId",
153
+ description: "Operator identifier (empty = auto-detect)",
154
+ },
155
+ {
156
+ configPath: "orchestrator.orchestrator.integration",
157
+ label: "Integration",
158
+ control: "picker",
159
+ layer: "L1",
160
+ fieldType: "enum",
161
+ values: ["manual", "supervised", "auto"],
162
+ description:
163
+ "How completed batches are integrated. manual = user runs /orch-integrate. supervised = supervisor proposes plan, asks confirmation. auto = supervisor executes without asking.",
164
+ },
105
165
  ],
106
166
  },
107
167
  {
108
168
  name: "Agent: Supervisor",
109
169
  fields: [
110
- { configPath: "orchestrator.supervisor.model", label: "Supervisor Model", control: "input", layer: "L1+L2", fieldType: "string", prefsKey: "supervisorModel", description: "Supervisor model (inherit = use session model)" },
111
- { configPath: "orchestrator.supervisor.autonomy", label: "Autonomy Level", control: "picker", layer: "L1", fieldType: "enum", values: ["interactive", "supervised", "autonomous"], description: "Recovery action confirmation behavior" },
170
+ {
171
+ configPath: "orchestrator.supervisor.model",
172
+ label: "Supervisor Model",
173
+ control: "input",
174
+ layer: "L1+L2",
175
+ fieldType: "string",
176
+ prefsKey: "supervisorModel",
177
+ description: "Supervisor model (inherit = use session model)",
178
+ },
179
+ {
180
+ configPath: "orchestrator.supervisor.autonomy",
181
+ label: "Autonomy Level",
182
+ control: "picker",
183
+ layer: "L1",
184
+ fieldType: "enum",
185
+ values: ["interactive", "supervised", "autonomous"],
186
+ description: "Recovery action confirmation behavior",
187
+ },
112
188
  ],
113
189
  },
114
190
  {
115
191
  name: "Agent: Worker",
116
192
  fields: [
117
- { configPath: "taskRunner.worker.model", label: "Worker Model", control: "input", layer: "L1+L2", fieldType: "string", prefsKey: "workerModel", description: "Worker model (inherit = use session model)" },
118
- { configPath: "taskRunner.worker.tools", label: "Worker Tools", control: "input", layer: "L1", fieldType: "string", description: "Worker tool allowlist" },
119
- { configPath: "taskRunner.worker.thinking", label: "Worker Thinking", control: "picker", layer: "L1", fieldType: "string", description: "Worker thinking mode" },
193
+ {
194
+ configPath: "taskRunner.worker.model",
195
+ label: "Worker Model",
196
+ control: "input",
197
+ layer: "L1+L2",
198
+ fieldType: "string",
199
+ prefsKey: "workerModel",
200
+ description: "Worker model (inherit = use session model)",
201
+ },
202
+ {
203
+ configPath: "taskRunner.worker.tools",
204
+ label: "Worker Tools",
205
+ control: "input",
206
+ layer: "L1",
207
+ fieldType: "string",
208
+ description: "Worker tool allowlist",
209
+ },
210
+ {
211
+ configPath: "taskRunner.worker.thinking",
212
+ label: "Worker Thinking",
213
+ control: "picker",
214
+ layer: "L1",
215
+ fieldType: "string",
216
+ description: "Worker thinking mode",
217
+ },
120
218
  ],
121
219
  },
122
220
  {
123
221
  name: "Agent: Reviewer",
124
222
  fields: [
125
- { configPath: "taskRunner.reviewer.model", label: "Reviewer Model", control: "input", layer: "L1+L2", fieldType: "string", prefsKey: "reviewerModel", description: "Reviewer model (inherit = use session model)" },
126
- { configPath: "taskRunner.reviewer.tools", label: "Reviewer Tools", control: "input", layer: "L1", fieldType: "string", description: "Reviewer tool allowlist" },
127
- { configPath: "taskRunner.reviewer.thinking", label: "Reviewer Thinking", control: "picker", layer: "L1", fieldType: "string", description: "Reviewer thinking mode" },
223
+ {
224
+ configPath: "taskRunner.reviewer.model",
225
+ label: "Reviewer Model",
226
+ control: "input",
227
+ layer: "L1+L2",
228
+ fieldType: "string",
229
+ prefsKey: "reviewerModel",
230
+ description: "Reviewer model (inherit = use session model)",
231
+ },
232
+ {
233
+ configPath: "taskRunner.reviewer.tools",
234
+ label: "Reviewer Tools",
235
+ control: "input",
236
+ layer: "L1",
237
+ fieldType: "string",
238
+ description: "Reviewer tool allowlist",
239
+ },
240
+ {
241
+ configPath: "taskRunner.reviewer.thinking",
242
+ label: "Reviewer Thinking",
243
+ control: "picker",
244
+ layer: "L1",
245
+ fieldType: "string",
246
+ description: "Reviewer thinking mode",
247
+ },
128
248
  ],
129
249
  },
130
250
  {
131
251
  name: "Agent: Merge",
132
252
  fields: [
133
- { configPath: "orchestrator.merge.model", label: "Merge Model", control: "input", layer: "L1+L2", fieldType: "string", prefsKey: "mergeModel", description: "Merge-agent model (inherit = use session model)" },
134
- { configPath: "orchestrator.merge.tools", label: "Merge Tools", control: "input", layer: "L1", fieldType: "string", description: "Merge-agent tool allowlist" },
135
- { configPath: "orchestrator.merge.thinking", label: "Merge Thinking", control: "picker", layer: "L1+L2", fieldType: "string", prefsKey: "mergeThinking", description: "Merge-agent thinking mode" },
136
- { configPath: "orchestrator.merge.order", label: "Merge Order", control: "toggle", layer: "L1", fieldType: "enum", values: ["fewest-files-first", "sequential"], description: "Lane merge ordering policy" },
137
- { configPath: "orchestrator.merge.timeoutMinutes", label: "Merge Timeout (minutes)", control: "input", layer: "L1", fieldType: "number", description: "Max time for merge agent to complete. Increase for large batches (default: 10)" },
253
+ {
254
+ configPath: "orchestrator.merge.model",
255
+ label: "Merge Model",
256
+ control: "input",
257
+ layer: "L1+L2",
258
+ fieldType: "string",
259
+ prefsKey: "mergeModel",
260
+ description: "Merge-agent model (inherit = use session model)",
261
+ },
262
+ {
263
+ configPath: "orchestrator.merge.tools",
264
+ label: "Merge Tools",
265
+ control: "input",
266
+ layer: "L1",
267
+ fieldType: "string",
268
+ description: "Merge-agent tool allowlist",
269
+ },
270
+ {
271
+ configPath: "orchestrator.merge.thinking",
272
+ label: "Merge Thinking",
273
+ control: "picker",
274
+ layer: "L1+L2",
275
+ fieldType: "string",
276
+ prefsKey: "mergeThinking",
277
+ description: "Merge-agent thinking mode",
278
+ },
279
+ {
280
+ configPath: "orchestrator.merge.order",
281
+ label: "Merge Order",
282
+ control: "toggle",
283
+ layer: "L1",
284
+ fieldType: "enum",
285
+ values: ["fewest-files-first", "sequential"],
286
+ description: "Lane merge ordering policy",
287
+ },
288
+ {
289
+ configPath: "orchestrator.merge.timeoutMinutes",
290
+ label: "Merge Timeout (minutes)",
291
+ control: "input",
292
+ layer: "L1",
293
+ fieldType: "number",
294
+ description: "Max time for merge agent to complete. Increase for large batches (default: 10)",
295
+ },
138
296
  ],
139
297
  },
140
298
  {
141
299
  name: "Agent Extensions",
142
- readOnly: true, // Dynamically handled — no fixed fields
300
+ readOnly: true, // Dynamically handled — no fixed fields
143
301
  fields: [],
144
302
  },
145
303
  {
146
304
  name: "Context Limits",
147
305
  fields: [
148
- { configPath: "taskRunner.context.workerContextWindow", label: "Context Window", control: "input", layer: "L1", fieldType: "number", description: "Worker context window size" },
149
- { configPath: "taskRunner.context.warnPercent", label: "Warn %", control: "input", layer: "L1", fieldType: "number", description: "Context utilization warn threshold (%)" },
150
- { configPath: "taskRunner.context.killPercent", label: "Kill %", control: "input", layer: "L1", fieldType: "number", description: "Context utilization hard-stop threshold (%)" },
151
- { configPath: "taskRunner.context.maxWorkerIterations", label: "Max Iterations", control: "input", layer: "L1", fieldType: "number", description: "Max worker iterations per step" },
152
- { configPath: "taskRunner.context.maxReviewCycles", label: "Max Review Cycles", control: "input", layer: "L1", fieldType: "number", description: "Max revise loops per review stage" },
153
- { configPath: "taskRunner.context.noProgressLimit", label: "No Progress Limit", control: "input", layer: "L1", fieldType: "number", description: "Max no-progress iterations before failure" },
154
- { configPath: "taskRunner.context.maxWorkerMinutes", label: "Max Worker Min (ctx)", control: "input", layer: "L1", fieldType: "number", optional: true, description: "Per-worker wall-clock cap (minutes, empty = no cap)" },
306
+ {
307
+ configPath: "taskRunner.context.workerContextWindow",
308
+ label: "Context Window",
309
+ control: "input",
310
+ layer: "L1",
311
+ fieldType: "number",
312
+ description: "Worker context window size",
313
+ },
314
+ {
315
+ configPath: "taskRunner.context.warnPercent",
316
+ label: "Warn %",
317
+ control: "input",
318
+ layer: "L1",
319
+ fieldType: "number",
320
+ description: "Context utilization warn threshold (%)",
321
+ },
322
+ {
323
+ configPath: "taskRunner.context.killPercent",
324
+ label: "Kill %",
325
+ control: "input",
326
+ layer: "L1",
327
+ fieldType: "number",
328
+ description: "Context utilization hard-stop threshold (%)",
329
+ },
330
+ {
331
+ configPath: "taskRunner.context.maxWorkerIterations",
332
+ label: "Max Iterations",
333
+ control: "input",
334
+ layer: "L1",
335
+ fieldType: "number",
336
+ description: "Max worker iterations per step",
337
+ },
338
+ {
339
+ configPath: "taskRunner.context.maxReviewCycles",
340
+ label: "Max Review Cycles",
341
+ control: "input",
342
+ layer: "L1",
343
+ fieldType: "number",
344
+ description: "Max revise loops per review stage",
345
+ },
346
+ {
347
+ configPath: "taskRunner.context.noProgressLimit",
348
+ label: "No Progress Limit",
349
+ control: "input",
350
+ layer: "L1",
351
+ fieldType: "number",
352
+ description: "Max no-progress iterations before failure",
353
+ },
354
+ {
355
+ configPath: "taskRunner.context.maxWorkerMinutes",
356
+ label: "Max Worker Min (ctx)",
357
+ control: "input",
358
+ layer: "L1",
359
+ fieldType: "number",
360
+ optional: true,
361
+ description: "Per-worker wall-clock cap (minutes, empty = no cap)",
362
+ },
155
363
  ],
156
364
  },
157
365
  {
158
366
  name: "Failure Policy",
159
367
  fields: [
160
- { configPath: "orchestrator.failure.onTaskFailure", label: "On Task Failure", control: "toggle", layer: "L1", fieldType: "enum", values: ["skip-dependents", "stop-wave", "stop-all"], description: "Batch behavior when a task fails" },
161
- { configPath: "orchestrator.failure.onMergeFailure", label: "On Merge Failure", control: "toggle", layer: "L1", fieldType: "enum", values: ["pause", "abort"], description: "Behavior when a merge step fails" },
162
- { configPath: "orchestrator.failure.stallTimeout", label: "Stall Timeout (min)", control: "input", layer: "L1", fieldType: "number", description: "Stall detection threshold (minutes)" },
163
- { configPath: "orchestrator.failure.maxWorkerMinutes", label: "Max Worker Min", control: "input", layer: "L1", fieldType: "number", description: "Max worker runtime budget per task (minutes)" },
164
- { configPath: "orchestrator.failure.abortGracePeriod", label: "Abort Grace (sec)", control: "input", layer: "L1", fieldType: "number", description: "Graceful abort wait time (seconds)" },
368
+ {
369
+ configPath: "orchestrator.failure.onTaskFailure",
370
+ label: "On Task Failure",
371
+ control: "toggle",
372
+ layer: "L1",
373
+ fieldType: "enum",
374
+ values: ["skip-dependents", "stop-wave", "stop-all"],
375
+ description: "Batch behavior when a task fails",
376
+ },
377
+ {
378
+ configPath: "orchestrator.failure.onMergeFailure",
379
+ label: "On Merge Failure",
380
+ control: "toggle",
381
+ layer: "L1",
382
+ fieldType: "enum",
383
+ values: ["pause", "abort"],
384
+ description: "Behavior when a merge step fails",
385
+ },
386
+ {
387
+ configPath: "orchestrator.failure.stallTimeout",
388
+ label: "Stall Timeout (min)",
389
+ control: "input",
390
+ layer: "L1",
391
+ fieldType: "number",
392
+ description: "Stall detection threshold (minutes)",
393
+ },
394
+ {
395
+ configPath: "orchestrator.failure.maxWorkerMinutes",
396
+ label: "Max Worker Min",
397
+ control: "input",
398
+ layer: "L1",
399
+ fieldType: "number",
400
+ description: "Max worker runtime budget per task (minutes)",
401
+ },
402
+ {
403
+ configPath: "orchestrator.failure.abortGracePeriod",
404
+ label: "Abort Grace (sec)",
405
+ control: "input",
406
+ layer: "L1",
407
+ fieldType: "number",
408
+ description: "Graceful abort wait time (seconds)",
409
+ },
165
410
  ],
166
411
  },
167
412
  {
168
413
  name: "Dependencies",
169
414
  fields: [
170
- { configPath: "orchestrator.dependencies.source", label: "Dep Source", control: "toggle", layer: "L1", fieldType: "enum", values: ["prompt", "agent"], description: "Dependency extraction source" },
171
- { configPath: "orchestrator.dependencies.cache", label: "Dep Cache", control: "toggle", layer: "L1", fieldType: "boolean", values: ["true", "false"], description: "Cache dependency analysis results" },
415
+ {
416
+ configPath: "orchestrator.dependencies.source",
417
+ label: "Dep Source",
418
+ control: "toggle",
419
+ layer: "L1",
420
+ fieldType: "enum",
421
+ values: ["prompt", "agent"],
422
+ description: "Dependency extraction source",
423
+ },
424
+ {
425
+ configPath: "orchestrator.dependencies.cache",
426
+ label: "Dep Cache",
427
+ control: "toggle",
428
+ layer: "L1",
429
+ fieldType: "boolean",
430
+ values: ["true", "false"],
431
+ description: "Cache dependency analysis results",
432
+ },
172
433
  ],
173
434
  },
174
435
  {
175
436
  name: "Assignment",
176
437
  fields: [
177
- { configPath: "orchestrator.assignment.strategy", label: "Strategy", control: "toggle", layer: "L1", fieldType: "enum", values: ["affinity-first", "round-robin", "load-balanced"], description: "Lane assignment strategy" },
438
+ {
439
+ configPath: "orchestrator.assignment.strategy",
440
+ label: "Strategy",
441
+ control: "toggle",
442
+ layer: "L1",
443
+ fieldType: "enum",
444
+ values: ["affinity-first", "round-robin", "load-balanced"],
445
+ description: "Lane assignment strategy",
446
+ },
178
447
  ],
179
448
  },
180
449
  {
181
450
  name: "Pre-Warm",
182
451
  fields: [
183
- { configPath: "orchestrator.preWarm.autoDetect", label: "Auto-Detect", control: "toggle", layer: "L1", fieldType: "boolean", values: ["true", "false"], description: "Enable automatic pre-warm command detection" },
452
+ {
453
+ configPath: "orchestrator.preWarm.autoDetect",
454
+ label: "Auto-Detect",
455
+ control: "toggle",
456
+ layer: "L1",
457
+ fieldType: "boolean",
458
+ values: ["true", "false"],
459
+ description: "Enable automatic pre-warm command detection",
460
+ },
184
461
  ],
185
462
  },
186
463
  {
187
464
  name: "Monitoring",
188
465
  fields: [
189
- { configPath: "orchestrator.monitoring.pollInterval", label: "Poll Interval (sec)", control: "input", layer: "L1", fieldType: "number", description: "Poll interval for lane/task monitoring (seconds)" },
466
+ {
467
+ configPath: "orchestrator.monitoring.pollInterval",
468
+ label: "Poll Interval (sec)",
469
+ control: "input",
470
+ layer: "L1",
471
+ fieldType: "number",
472
+ description: "Poll interval for lane/task monitoring (seconds)",
473
+ },
190
474
  ],
191
475
  },
192
476
  {
193
477
  name: "Global Preferences",
194
478
  fields: [
195
- { configPath: "preferences.dashboardPort", label: "Dashboard Port", control: "input", layer: "L2", fieldType: "number", prefsKey: "dashboardPort", optional: true, description: "Dashboard server port" },
479
+ {
480
+ configPath: "preferences.dashboardPort",
481
+ label: "Dashboard Port",
482
+ control: "input",
483
+ layer: "L2",
484
+ fieldType: "number",
485
+ prefsKey: "dashboardPort",
486
+ optional: true,
487
+ description: "Dashboard server port",
488
+ },
196
489
  ],
197
490
  },
198
491
  {
199
492
  name: "Advanced (JSON Only)",
200
493
  readOnly: true,
201
- fields: [], // Populated dynamically in getAdvancedItems()
494
+ fields: [], // Populated dynamically in getAdvancedItems()
202
495
  },
203
496
  ];
204
497
 
205
-
206
498
  // ── Raw Config Readers (Source Detection) ────────────────────────────
207
499
 
208
500
  /**
@@ -256,7 +548,9 @@ export function readRawYamlConfigs(configRoot: string): Record<string, any> | nu
256
548
  if (parsed && typeof parsed === "object") {
257
549
  result.taskRunner = convertYamlKeys(parsed, "taskRunner");
258
550
  }
259
- } catch { /* ignore */ }
551
+ } catch {
552
+ /* ignore */
553
+ }
260
554
  }
261
555
 
262
556
  if (hasOrch) {
@@ -266,7 +560,9 @@ export function readRawYamlConfigs(configRoot: string): Record<string, any> | nu
266
560
  if (parsed && typeof parsed === "object") {
267
561
  result.orchestrator = convertYamlKeys(parsed, "orchestrator");
268
562
  }
269
- } catch { /* ignore */ }
563
+ } catch {
564
+ /* ignore */
565
+ }
270
566
  }
271
567
 
272
568
  return Object.keys(result).length > 0 ? result : null;
@@ -338,7 +634,6 @@ function readRawPreferences(): Record<string, any> | null {
338
634
  }
339
635
  }
340
636
 
341
-
342
637
  // ── Write-Back ───────────────────────────────────────────────────────
343
638
 
344
639
  /**
@@ -425,8 +720,8 @@ export function writeProjectConfigField(
425
720
  } catch (e: any) {
426
721
  throw new Error(
427
722
  `Cannot write settings: ${jsonPath} contains malformed JSON. ` +
428
- `Please fix or delete the file and try again. ` +
429
- `(Parse error: ${e.message ?? "unknown"})`,
723
+ `Please fix or delete the file and try again. ` +
724
+ `(Parse error: ${e.message ?? "unknown"})`,
430
725
  );
431
726
  }
432
727
  } else {
@@ -449,7 +744,11 @@ export function writeProjectConfigField(
449
744
  renameSync(tmpPath, jsonPath);
450
745
  } catch {
451
746
  writeFileSync(jsonPath, json, "utf-8");
452
- try { if (existsSync(tmpPath)) unlinkSync(tmpPath); } catch { /* cleanup best-effort */ }
747
+ try {
748
+ if (existsSync(tmpPath)) unlinkSync(tmpPath);
749
+ } catch {
750
+ /* cleanup best-effort */
751
+ }
453
752
  }
454
753
  }
455
754
 
@@ -488,7 +787,11 @@ export function writeGlobalPreference(path: string, value: any): void {
488
787
  renameSync(tmpPath, prefsPath);
489
788
  } catch {
490
789
  writeFileSync(prefsPath, json, "utf-8");
491
- try { if (existsSync(tmpPath)) unlinkSync(tmpPath); } catch { /* cleanup best-effort */ }
790
+ try {
791
+ if (existsSync(tmpPath)) unlinkSync(tmpPath);
792
+ } catch {
793
+ /* cleanup best-effort */
794
+ }
492
795
  }
493
796
  }
494
797
 
@@ -559,7 +862,6 @@ export function resolveWriteAction(
559
862
  return defaultDest;
560
863
  }
561
864
 
562
-
563
865
  // ── Source Detection ─────────────────────────────────────────────────
564
866
 
565
867
  /**
@@ -596,7 +898,6 @@ export function detectFieldSource(
596
898
  return "global";
597
899
  }
598
900
 
599
-
600
901
  // ── Value Formatting ─────────────────────────────────────────────────
601
902
 
602
903
  /**
@@ -628,7 +929,6 @@ export function getFieldDisplayValue(
628
929
  return String(val);
629
930
  }
630
931
 
631
-
632
932
  // ── Validation ───────────────────────────────────────────────────────
633
933
 
634
934
  export interface ValidationResult {
@@ -683,7 +983,6 @@ export function validateFieldInput(field: FieldDef, input: string): ValidationRe
683
983
  }
684
984
  }
685
985
 
686
-
687
986
  // ── Advanced Section Items ───────────────────────────────────────────
688
987
 
689
988
  export interface AdvancedItem {
@@ -772,11 +1071,7 @@ export function getAdvancedItems(config: TaskplaneConfig): AdvancedItem[] {
772
1071
  * Known subsection objects (like `taskRunner.worker`, `orchestrator.merge`)
773
1072
  * are recursed into, not reported as leaves themselves.
774
1073
  */
775
- function walkConfig(
776
- obj: any,
777
- prefix: string,
778
- visitor: (path: string, value: any) => void,
779
- ): void {
1074
+ function walkConfig(obj: any, prefix: string, visitor: (path: string, value: any) => void): void {
780
1075
  if (obj === null || obj === undefined) return;
781
1076
 
782
1077
  for (const [key, value] of Object.entries(obj)) {
@@ -861,7 +1156,6 @@ function summarizeArray(arr: any[]): string {
861
1156
  return `${arr.length} items`;
862
1157
  }
863
1158
 
864
-
865
1159
  // ── TUI Rendering ────────────────────────────────────────────────────
866
1160
 
867
1161
  /**
@@ -890,7 +1184,10 @@ async function pickModel(ctx: ExtensionContext, currentModel: string): Promise<s
890
1184
  if (available.length === 0) {
891
1185
  ctx.ui.notify("No available models found in pi model registry", "warning");
892
1186
  // Fall back to manual input
893
- const manual = await ctx.ui.input("Model (provider/model-id, or empty for inherit)", currentModel || "");
1187
+ const manual = await ctx.ui.input(
1188
+ "Model (provider/model-id, or empty for inherit)",
1189
+ currentModel || "",
1190
+ );
894
1191
  if (manual === null || manual === undefined) return undefined;
895
1192
  return manual;
896
1193
  }
@@ -909,10 +1206,10 @@ async function pickModel(ctx: ExtensionContext, currentModel: string): Promise<s
909
1206
  ];
910
1207
 
911
1208
  const providerChoice = await selectScrollable(ctx, "Choose model provider", providerOptions);
912
- if (!providerChoice) return undefined; // Cancelled
1209
+ if (!providerChoice) return undefined; // Cancelled
913
1210
 
914
1211
  if (providerChoice.startsWith("inherit")) {
915
- return ""; // Empty string = inherit
1212
+ return ""; // Empty string = inherit
916
1213
  }
917
1214
 
918
1215
  // Extract provider name (strip " (N models)" suffix)
@@ -941,7 +1238,7 @@ async function pickModel(ctx: ExtensionContext, currentModel: string): Promise<s
941
1238
  }
942
1239
 
943
1240
  const modelChoice = await selectScrollable(ctx, `Choose model (${provider})`, modelOptions);
944
- if (!modelChoice) continue; // Cancelled → back to providers
1241
+ if (!modelChoice) continue; // Cancelled → back to providers
945
1242
  if (modelChoice === "← Back to providers") continue;
946
1243
 
947
1244
  const resolved = modelOptionMap.get(modelChoice);
@@ -962,7 +1259,9 @@ const THINKING_MODE_OPTIONS: Array<{ value: ThinkingModeValue; label: string }>
962
1259
  ];
963
1260
 
964
1261
  function normalizeThinkingMode(value: unknown): ThinkingModeValue {
965
- const cleaned = String(value ?? "").trim().toLowerCase();
1262
+ const cleaned = String(value ?? "")
1263
+ .trim()
1264
+ .toLowerCase();
966
1265
  if (!cleaned || cleaned === "inherit") return "";
967
1266
  if (cleaned === "on") return "high";
968
1267
  if (["off", "minimal", "low", "medium", "high", "xhigh"].includes(cleaned)) {
@@ -1014,15 +1313,17 @@ function resolveModelRecord(ctx: ExtensionContext, modelRef: string): any | unde
1014
1313
  if (slashIdx > 0) {
1015
1314
  const provider = trimmed.slice(0, slashIdx).toLowerCase();
1016
1315
  const id = trimmed.slice(slashIdx + 1).toLowerCase();
1017
- return available.find((m: any) =>
1018
- String(m?.provider ?? "").toLowerCase() === provider
1019
- && String(m?.id ?? "").toLowerCase() === id,
1316
+ return available.find(
1317
+ (m: any) =>
1318
+ String(m?.provider ?? "").toLowerCase() === provider &&
1319
+ String(m?.id ?? "").toLowerCase() === id,
1020
1320
  );
1021
1321
  }
1022
1322
 
1023
- return available.find((m: any) =>
1024
- String(m?.id ?? "").toLowerCase() === lower
1025
- || `${String(m?.provider ?? "").toLowerCase()}/${String(m?.id ?? "").toLowerCase()}` === lower,
1323
+ return available.find(
1324
+ (m: any) =>
1325
+ String(m?.id ?? "").toLowerCase() === lower ||
1326
+ `${String(m?.provider ?? "").toLowerCase()}/${String(m?.id ?? "").toLowerCase()}` === lower,
1026
1327
  );
1027
1328
  }
1028
1329
 
@@ -1046,12 +1347,9 @@ export function modelSupportsThinking(model: any): boolean {
1046
1347
  "reasoning_tokens",
1047
1348
  ];
1048
1349
 
1049
- const candidateObjects = [
1050
- model,
1051
- model.capabilities,
1052
- model.features,
1053
- model.metadata,
1054
- ].filter((entry) => entry && typeof entry === "object");
1350
+ const candidateObjects = [model, model.capabilities, model.features, model.metadata].filter(
1351
+ (entry) => entry && typeof entry === "object",
1352
+ );
1055
1353
 
1056
1354
  for (const candidate of candidateObjects) {
1057
1355
  for (const key of boolFlags) {
@@ -1145,7 +1443,9 @@ async function selectScrollable(
1145
1443
  container.addChild(selectList);
1146
1444
 
1147
1445
  container.addChild(new Text("", 0, 0));
1148
- container.addChild(new Text(theme.fg("dim", "↑↓ navigate • type to filter • enter select • esc back"), 1, 0));
1446
+ container.addChild(
1447
+ new Text(theme.fg("dim", "↑↓ navigate • type to filter • enter select • esc back"), 1, 0),
1448
+ );
1149
1449
  container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
1150
1450
 
1151
1451
  return {
@@ -1160,7 +1460,8 @@ async function selectScrollable(
1160
1460
 
1161
1461
  if (selectedValue === undefined) return undefined;
1162
1462
  const selectedIndex = Number(selectedValue);
1163
- if (!Number.isInteger(selectedIndex) || selectedIndex < 0 || selectedIndex >= options.length) return undefined;
1463
+ if (!Number.isInteger(selectedIndex) || selectedIndex < 0 || selectedIndex >= options.length)
1464
+ return undefined;
1164
1465
  return options[selectedIndex];
1165
1466
  }
1166
1467
 
@@ -1178,7 +1479,10 @@ export async function openSettingsTui(
1178
1479
  * Reload all config state from disk. Called after write-back to
1179
1480
  * refresh the TUI display.
1180
1481
  */
1181
- function loadConfigState(configRoot: string, pointerConfigRoot?: string): {
1482
+ function loadConfigState(
1483
+ configRoot: string,
1484
+ pointerConfigRoot?: string,
1485
+ ): {
1182
1486
  mergedConfig: TaskplaneConfig;
1183
1487
  prefs: GlobalPreferences;
1184
1488
  rawProject: Record<string, any> | null;
@@ -1211,11 +1515,12 @@ async function showSectionSelectorLoop(
1211
1515
  const sectionItems: SelectItem[] = SECTIONS.map((section, i) => ({
1212
1516
  value: String(i),
1213
1517
  label: section.name,
1214
- description: section.name === "Agent Extensions"
1215
- ? "Toggle extensions per agent type"
1216
- : section.readOnly
1217
- ? "Read-only collection/record fields"
1218
- : `${section.fields.length} setting${section.fields.length === 1 ? "" : "s"}`,
1518
+ description:
1519
+ section.name === "Agent Extensions"
1520
+ ? "Toggle extensions per agent type"
1521
+ : section.readOnly
1522
+ ? "Read-only collection/record fields"
1523
+ : `${section.fields.length} setting${section.fields.length === 1 ? "" : "s"}`,
1219
1524
  }));
1220
1525
 
1221
1526
  const selectedSection = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
@@ -1226,7 +1531,9 @@ async function showSectionSelectorLoop(
1226
1531
 
1227
1532
  // Title
1228
1533
  container.addChild(new Text(theme.fg("accent", theme.bold("⚙ Settings")), 1, 0));
1229
- container.addChild(new Text(theme.fg("dim", "Navigate sections to view and edit configuration"), 1, 0));
1534
+ container.addChild(
1535
+ new Text(theme.fg("dim", "Navigate sections to view and edit configuration"), 1, 0),
1536
+ );
1230
1537
  container.addChild(new Text("", 0, 0));
1231
1538
 
1232
1539
  // SelectList
@@ -1251,11 +1558,14 @@ async function showSectionSelectorLoop(
1251
1558
  return {
1252
1559
  render: (w: number) => container.render(w),
1253
1560
  invalidate: () => container.invalidate(),
1254
- handleInput: (data: string) => { selectList.handleInput(data); tui.requestRender(); },
1561
+ handleInput: (data: string) => {
1562
+ selectList.handleInput(data);
1563
+ tui.requestRender();
1564
+ },
1255
1565
  };
1256
1566
  });
1257
1567
 
1258
- if (selectedSection === null) return; // User pressed Esc
1568
+ if (selectedSection === null) return; // User pressed Esc
1259
1569
 
1260
1570
  const sectionIndex = parseInt(selectedSection, 10);
1261
1571
  const section = SECTIONS[sectionIndex];
@@ -1295,15 +1605,17 @@ async function showAdvancedSection(
1295
1605
 
1296
1606
  // Title
1297
1607
  container.addChild(new Text(theme.fg("accent", theme.bold("Advanced (JSON Only)")), 1, 0));
1298
- container.addChild(new Text(theme.fg("dim", "These fields can only be edited directly in the config file"), 1, 0));
1608
+ container.addChild(
1609
+ new Text(theme.fg("dim", "These fields can only be edited directly in the config file"), 1, 0),
1610
+ );
1299
1611
  container.addChild(new Text("", 0, 0));
1300
1612
 
1301
1613
  const settingsList = new SettingsList(
1302
1614
  settingsItems,
1303
1615
  Math.min(settingsItems.length + 2, 20),
1304
1616
  getSettingsListTheme(),
1305
- () => {}, // onChange — no-op (read-only)
1306
- () => done(undefined), // onCancel
1617
+ () => {}, // onChange — no-op (read-only)
1618
+ () => done(undefined), // onCancel
1307
1619
  );
1308
1620
  container.addChild(settingsList);
1309
1621
 
@@ -1317,7 +1629,10 @@ async function showAdvancedSection(
1317
1629
  return {
1318
1630
  render: (w: number) => container.render(w),
1319
1631
  invalidate: () => container.invalidate(),
1320
- handleInput: (data: string) => { settingsList.handleInput?.(data); tui.requestRender(); },
1632
+ handleInput: (data: string) => {
1633
+ settingsList.handleInput?.(data);
1634
+ tui.requestRender();
1635
+ },
1321
1636
  };
1322
1637
  });
1323
1638
  }
@@ -1351,14 +1666,18 @@ async function showExtensionsSection(
1351
1666
  container.addChild(new Text(theme.fg("accent", theme.bold("Agent Extensions")), 1, 0));
1352
1667
  container.addChild(new Text("", 0, 0));
1353
1668
  container.addChild(new Text(theme.fg("dim", "No third-party extensions found."), 1, 0));
1354
- container.addChild(new Text(theme.fg("dim", "Install extensions via pi settings to see them here."), 1, 0));
1669
+ container.addChild(
1670
+ new Text(theme.fg("dim", "Install extensions via pi settings to see them here."), 1, 0),
1671
+ );
1355
1672
  container.addChild(new Text("", 0, 0));
1356
1673
  container.addChild(new Text(theme.fg("dim", "esc back"), 1, 0));
1357
1674
  container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
1358
1675
  return {
1359
1676
  render: (w: number) => container.render(w),
1360
1677
  invalidate: () => container.invalidate(),
1361
- handleInput: (data: string) => { if (data === "\x1b" || data === "\x1b\x1b") done(undefined); },
1678
+ handleInput: (data: string) => {
1679
+ if (data === "\x1b" || data === "\x1b\x1b") done(undefined);
1680
+ },
1362
1681
  };
1363
1682
  });
1364
1683
  return;
@@ -1371,7 +1690,11 @@ async function showExtensionsSection(
1371
1690
 
1372
1691
  const agentTypes = [
1373
1692
  { name: "Worker", exclude: workerExclude, configPath: "taskRunner.worker.excludeExtensions" },
1374
- { name: "Reviewer", exclude: reviewerExclude, configPath: "taskRunner.reviewer.excludeExtensions" },
1693
+ {
1694
+ name: "Reviewer",
1695
+ exclude: reviewerExclude,
1696
+ configPath: "taskRunner.reviewer.excludeExtensions",
1697
+ },
1375
1698
  { name: "Merger", exclude: mergeExclude, configPath: "orchestrator.merge.excludeExtensions" },
1376
1699
  ];
1377
1700
 
@@ -1391,32 +1714,37 @@ async function showExtensionsSection(
1391
1714
  }
1392
1715
  }
1393
1716
 
1394
- const result = await ctx.ui.custom<{ id: string; value: string } | null>((tui, theme, _kb, done) => {
1395
- const container = new Container();
1396
- container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
1397
- container.addChild(new Text(theme.fg("accent", theme.bold("Agent Extensions")), 1, 0));
1398
- container.addChild(new Text(theme.fg("dim", "Toggle extensions on/off per agent type"), 1, 0));
1399
- container.addChild(new Text("", 0, 0));
1717
+ const result = await ctx.ui.custom<{ id: string; value: string } | null>(
1718
+ (tui, theme, _kb, done) => {
1719
+ const container = new Container();
1720
+ container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
1721
+ container.addChild(new Text(theme.fg("accent", theme.bold("Agent Extensions")), 1, 0));
1722
+ container.addChild(new Text(theme.fg("dim", "Toggle extensions on/off per agent type"), 1, 0));
1723
+ container.addChild(new Text("", 0, 0));
1400
1724
 
1401
- const settingsList = new SettingsList(
1402
- settingsItems,
1403
- Math.min(settingsItems.length + 2, 20),
1404
- getSettingsListTheme(),
1405
- (id, newValue) => done({ id, value: newValue }),
1406
- () => done(null),
1407
- );
1408
- container.addChild(settingsList);
1725
+ const settingsList = new SettingsList(
1726
+ settingsItems,
1727
+ Math.min(settingsItems.length + 2, 20),
1728
+ getSettingsListTheme(),
1729
+ (id, newValue) => done({ id, value: newValue }),
1730
+ () => done(null),
1731
+ );
1732
+ container.addChild(settingsList);
1409
1733
 
1410
- container.addChild(new Text("", 0, 0));
1411
- container.addChild(new Text(theme.fg("dim", "↑↓ navigate • space toggle • esc back"), 1, 0));
1412
- container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
1734
+ container.addChild(new Text("", 0, 0));
1735
+ container.addChild(new Text(theme.fg("dim", "↑↓ navigate • space toggle • esc back"), 1, 0));
1736
+ container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
1413
1737
 
1414
- return {
1415
- render: (w: number) => container.render(w),
1416
- invalidate: () => container.invalidate(),
1417
- handleInput: (data: string) => { settingsList.handleInput?.(data); tui.requestRender(); },
1418
- };
1419
- });
1738
+ return {
1739
+ render: (w: number) => container.render(w),
1740
+ invalidate: () => container.invalidate(),
1741
+ handleInput: (data: string) => {
1742
+ settingsList.handleInput?.(data);
1743
+ tui.requestRender();
1744
+ },
1745
+ };
1746
+ },
1747
+ );
1420
1748
 
1421
1749
  if (!result) return; // User pressed Esc
1422
1750
 
@@ -1428,7 +1756,8 @@ async function showExtensionsSection(
1428
1756
 
1429
1757
  // Read current exclusion array from merged effective config (handles YAML+JSON)
1430
1758
  const freshConfig = loadProjectConfig(configRoot, pointerConfigRoot);
1431
- const currentExcludeList: string[] = (getNestedValue(freshConfig, configPath) as string[] | undefined) ?? [];
1759
+ const currentExcludeList: string[] =
1760
+ (getNestedValue(freshConfig, configPath) as string[] | undefined) ?? [];
1432
1761
 
1433
1762
  let newExcludeList: string[];
1434
1763
  if (enabling) {
@@ -1444,7 +1773,11 @@ async function showExtensionsSection(
1444
1773
  try {
1445
1774
  writeProjectConfigField(configRoot, configPath, newExcludeList, pointerConfigRoot);
1446
1775
  if (onConfigChanged) {
1447
- try { onConfigChanged(); } catch { /* non-fatal */ }
1776
+ try {
1777
+ onConfigChanged();
1778
+ } catch {
1779
+ /* non-fatal */
1780
+ }
1448
1781
  }
1449
1782
  ctx.ui.notify(
1450
1783
  `${enabling ? "✅ Enabled" : "❌ Disabled"} ${pkg} for ${configPath.includes("worker") ? "Worker" : configPath.includes("reviewer") ? "Reviewer" : "Merger"}`,
@@ -1463,8 +1796,10 @@ async function showExtensionsSection(
1463
1796
  */
1464
1797
  function formatSourceBadge(source: FieldSource): string {
1465
1798
  switch (source) {
1466
- case "project": return "(project)";
1467
- case "global": return "(global)";
1799
+ case "project":
1800
+ return "(project)";
1801
+ case "global":
1802
+ return "(global)";
1468
1803
  }
1469
1804
  }
1470
1805
 
@@ -1487,18 +1822,28 @@ async function showSectionSettingsLoop(
1487
1822
  ): Promise<void> {
1488
1823
  while (true) {
1489
1824
  const state = loadConfigState(configRoot, pointerConfigRoot);
1490
- const result = await showSectionSettingsOnce(ctx, section, state.mergedConfig, state.prefs, state.rawProject, state.rawPrefs);
1825
+ const result = await showSectionSettingsOnce(
1826
+ ctx,
1827
+ section,
1828
+ state.mergedConfig,
1829
+ state.prefs,
1830
+ state.rawProject,
1831
+ state.rawPrefs,
1832
+ );
1491
1833
 
1492
- if (result === null) return; // User pressed Esc → back to sections
1834
+ if (result === null) return; // User pressed Esc → back to sections
1493
1835
 
1494
1836
  // Process the pending change
1495
1837
  const field = section.fields.find((f) => f.configPath === result.fieldId);
1496
- if (!field) continue; // Safety: field not found
1838
+ if (!field) continue; // Safety: field not found
1497
1839
 
1498
1840
  let previousModelValue = "";
1499
1841
 
1500
1842
  // Input/picker fields: the submenu returned a sentinel — open the editor picker.
1501
- if (result.rawValue === "__EDIT_REQUESTED__" && (field.control === "input" || field.control === "picker")) {
1843
+ if (
1844
+ result.rawValue === "__EDIT_REQUESTED__" &&
1845
+ (field.control === "input" || field.control === "picker")
1846
+ ) {
1502
1847
  const state = loadConfigState(configRoot, pointerConfigRoot);
1503
1848
  const currentDisplay = getFieldDisplayValue(field, state.mergedConfig, state.prefs);
1504
1849
  const currentClean = String(currentDisplay).replace(/\s+\((?:default|project|global)\)$/, "");
@@ -1508,31 +1853,30 @@ async function showSectionSettingsLoop(
1508
1853
  if (field.configPath.endsWith(".model")) {
1509
1854
  previousModelValue = normalizedCurrent;
1510
1855
  const selected = await pickModel(ctx, normalizedCurrent);
1511
- if (selected === undefined) continue; // Cancelled
1856
+ if (selected === undefined) continue; // Cancelled
1512
1857
  result.rawValue = selected;
1513
1858
  } else if (field.control === "picker" && field.configPath.endsWith(".thinking")) {
1514
1859
  const note = buildThinkingUnsupportedNoteForThinkingField(ctx, field, state.mergedConfig);
1515
1860
  if (note) ctx.ui.notify(note, "info");
1516
1861
  const selected = await pickThinkingMode(ctx, normalizedCurrent);
1517
- if (selected === undefined) continue; // Cancelled
1862
+ if (selected === undefined) continue; // Cancelled
1518
1863
  result.rawValue = selected;
1519
1864
  } else if (field.control === "picker" && field.values && field.values.length > 0) {
1520
1865
  // Enum picker: show scrollable list of allowed values
1521
- const options = field.values.map((v) =>
1522
- `${v}${v === normalizedCurrent ? " ✓ current" : ""}`
1523
- );
1866
+ const options = field.values.map((v) => `${v}${v === normalizedCurrent ? " ✓ current" : ""}`);
1524
1867
  const selected = await selectScrollable(ctx, field.label, options);
1525
- if (!selected) continue; // Cancelled
1868
+ if (!selected) continue; // Cancelled
1526
1869
  result.rawValue = selected.replace(/\s+✓ current$/, "");
1527
1870
  } else {
1528
- const placeholder = currentClean === "(not set)" || currentClean === "(inherit)" ? "" : currentClean;
1871
+ const placeholder =
1872
+ currentClean === "(not set)" || currentClean === "(inherit)" ? "" : currentClean;
1529
1873
 
1530
1874
  const newValue = await ctx.ui.input(
1531
1875
  `${field.label}${field.description ? ` — ${field.description}` : ""}`,
1532
1876
  placeholder,
1533
1877
  );
1534
1878
 
1535
- if (newValue === null || newValue === undefined) continue; // Cancelled
1879
+ if (newValue === null || newValue === undefined) continue; // Cancelled
1536
1880
 
1537
1881
  // Validate
1538
1882
  const validation = validateFieldInput(field, newValue);
@@ -1585,13 +1929,14 @@ async function showSectionSettingsLoop(
1585
1929
  }
1586
1930
  // Notify caller to reload in-memory config from disk
1587
1931
  if (onConfigChanged) {
1588
- try { onConfigChanged(); } catch { /* non-fatal */ }
1932
+ try {
1933
+ onConfigChanged();
1934
+ } catch {
1935
+ /* non-fatal */
1936
+ }
1589
1937
  }
1590
1938
 
1591
- ctx.ui.notify(
1592
- `✅ ${field.label} updated.`,
1593
- "info",
1594
- );
1939
+ ctx.ui.notify(`✅ ${field.label} updated.`, "info");
1595
1940
 
1596
1941
  const refreshedState = loadConfigState(configRoot, pointerConfigRoot);
1597
1942
  const suggestion = buildThinkingSuggestionForModelChange(
@@ -1657,17 +2002,13 @@ async function showSectionSettingsOnce(
1657
2002
  }));
1658
2003
  // Return SelectList directly — Container doesn't forward
1659
2004
  // handleInput to children, which would freeze the TUI.
1660
- const list = new SelectList(
1661
- selectItems,
1662
- Math.min(selectItems.length + 1, 10),
1663
- {
1664
- selectedPrefix: (t: string) => `\x1b[36m${t}\x1b[0m`,
1665
- selectedText: (t: string) => `\x1b[36m${t}\x1b[0m`,
1666
- description: (t: string) => `\x1b[2m${t}\x1b[0m`,
1667
- scrollInfo: (t: string) => `\x1b[2m${t}\x1b[0m`,
1668
- noMatch: (t: string) => `\x1b[33m${t}\x1b[0m`,
1669
- },
1670
- );
2005
+ const list = new SelectList(selectItems, Math.min(selectItems.length + 1, 10), {
2006
+ selectedPrefix: (t: string) => `\x1b[36m${t}\x1b[0m`,
2007
+ selectedText: (t: string) => `\x1b[36m${t}\x1b[0m`,
2008
+ description: (t: string) => `\x1b[2m${t}\x1b[0m`,
2009
+ scrollInfo: (t: string) => `\x1b[2m${t}\x1b[0m`,
2010
+ noMatch: (t: string) => `\x1b[33m${t}\x1b[0m`,
2011
+ });
1671
2012
  const currentIdx = field.values!.indexOf(displayValue);
1672
2013
  if (currentIdx >= 0) list.setSelectedIndex(currentIdx);
1673
2014
  list.onSelect = (selected) => done(selected.value);
@@ -1710,7 +2051,7 @@ async function showSectionSettingsOnce(
1710
2051
  // Exit TUI with the change so the caller can handle write-back
1711
2052
  done({ fieldId: id, rawValue: newValue });
1712
2053
  },
1713
- () => done(null), // onCancel → back to section selector
2054
+ () => done(null), // onCancel → back to section selector
1714
2055
  { enableSearch: settingsItems.length > 5 },
1715
2056
  );
1716
2057
  container.addChild(settingsList);
@@ -1723,10 +2064,9 @@ async function showSectionSettingsOnce(
1723
2064
 
1724
2065
  // Help text
1725
2066
  container.addChild(new Text("", 0, 0));
1726
- container.addChild(new Text(
1727
- theme.fg("dim", "↑↓ navigate • ←→/space cycle • enter edit • esc back"),
1728
- 1, 0,
1729
- ));
2067
+ container.addChild(
2068
+ new Text(theme.fg("dim", "↑↓ navigate • ←→/space cycle • enter edit • esc back"), 1, 0),
2069
+ );
1730
2070
 
1731
2071
  // Bottom border
1732
2072
  container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
@@ -1734,12 +2074,14 @@ async function showSectionSettingsOnce(
1734
2074
  return {
1735
2075
  render: (w: number) => container.render(w),
1736
2076
  invalidate: () => container.invalidate(),
1737
- handleInput: (data: string) => { settingsList.handleInput?.(data); tui.requestRender(); },
2077
+ handleInput: (data: string) => {
2078
+ settingsList.handleInput?.(data);
2079
+ tui.requestRender();
2080
+ },
1738
2081
  };
1739
2082
  });
1740
2083
  }
1741
2084
 
1742
-
1743
2085
  // ── Input Submenu ────────────────────────────────────────────────────
1744
2086
 
1745
2087
  /**
@@ -1761,7 +2103,7 @@ function createInputSubmenu(
1761
2103
  render(width: number): string[] {
1762
2104
  const lines: string[] = [];
1763
2105
  const prompt = ` Enter ${field.label}: `;
1764
- const inputDisplay = inputBuffer + "█"; // Simple cursor
2106
+ const inputDisplay = inputBuffer + "█"; // Simple cursor
1765
2107
  lines.push(truncateLine(prompt + inputDisplay, width));
1766
2108
 
1767
2109
  if (field.optional) {
@@ -1818,7 +2160,6 @@ function truncateLine(text: string, width: number): string {
1818
2160
  return text.substring(0, width - 3) + "...";
1819
2161
  }
1820
2162
 
1821
-
1822
2163
  // ── JSON-Only Footer ─────────────────────────────────────────────────
1823
2164
 
1824
2165
  /**
@@ -1826,17 +2167,17 @@ function truncateLine(text: string, width: number): string {
1826
2167
  * Used to dynamically discover JSON-only sibling fields.
1827
2168
  */
1828
2169
  const SECTION_CONFIG_PREFIXES: Record<string, string[]> = {
1829
- "Orchestrator": ["orchestrator.orchestrator"],
2170
+ Orchestrator: ["orchestrator.orchestrator"],
1830
2171
  "Agent: Supervisor": ["orchestrator.supervisor"],
1831
2172
  "Agent: Worker": ["taskRunner.worker"],
1832
2173
  "Agent: Reviewer": ["taskRunner.reviewer"],
1833
2174
  "Agent: Merge": ["orchestrator.merge"],
1834
2175
  "Context Limits": ["taskRunner.context"],
1835
2176
  "Failure Policy": ["orchestrator.failure"],
1836
- "Dependencies": ["orchestrator.dependencies"],
1837
- "Assignment": ["orchestrator.assignment"],
2177
+ Dependencies: ["orchestrator.dependencies"],
2178
+ Assignment: ["orchestrator.assignment"],
1838
2179
  "Pre-Warm": ["orchestrator.preWarm"],
1839
- "Monitoring": ["orchestrator.monitoring"],
2180
+ Monitoring: ["orchestrator.monitoring"],
1840
2181
  };
1841
2182
 
1842
2183
  /**