@edspencer/paddock 0.71.2 → 0.72.1

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 (106) hide show
  1. package/package.json +1 -1
  2. package/packages/server/dist/app.js +27 -0
  3. package/packages/server/dist/cli/args.d.ts +56 -5
  4. package/packages/server/dist/cli/args.js +193 -31
  5. package/packages/server/dist/cli/config.d.ts +157 -0
  6. package/packages/server/dist/cli/config.js +462 -0
  7. package/packages/server/dist/cli/paddock.js +53 -3
  8. package/packages/server/dist/config.d.ts +36 -1
  9. package/packages/server/dist/config.js +59 -44
  10. package/packages/server/dist/herdctl.d.ts +45 -0
  11. package/packages/server/dist/herdctl.js +58 -4
  12. package/packages/server/dist/instance-config.d.ts +149 -21
  13. package/packages/server/dist/instance-config.js +228 -5
  14. package/packages/server/dist/last-activity.d.ts +21 -0
  15. package/packages/server/dist/last-activity.js +7 -2
  16. package/packages/server/dist/profiles.d.ts +154 -0
  17. package/packages/server/dist/profiles.js +214 -0
  18. package/packages/server/dist/routes/transcripts.d.ts +61 -0
  19. package/packages/server/dist/routes/transcripts.js +676 -0
  20. package/packages/server/dist/routes.d.ts +3 -0
  21. package/packages/server/dist/routes.js +2 -0
  22. package/packages/server/dist/transcript-move.d.ts +62 -0
  23. package/packages/server/dist/transcript-move.js +179 -0
  24. package/packages/server/dist/transcripts-migration-execute.d.ts +144 -0
  25. package/packages/server/dist/transcripts-migration-execute.js +630 -0
  26. package/packages/server/dist/transcripts-migration.d.ts +336 -0
  27. package/packages/server/dist/transcripts-migration.js +917 -0
  28. package/packages/server/dist/transcripts.d.ts +46 -2
  29. package/packages/server/dist/transcripts.js +126 -3
  30. package/packages/server/package.json +1 -1
  31. package/packages/web/dist/assets/DiscoverPage-DhrXvgkl.js +2 -0
  32. package/packages/web/dist/assets/DiscoverView-yPLCp_YE.js +2 -0
  33. package/packages/web/dist/assets/InstanceConfigPage-aOgiBFLq.js +2 -0
  34. package/packages/web/dist/assets/{MarkdownRenderer-DA5CpdGI.js → MarkdownRenderer-BjrB0TDO.js} +1 -1
  35. package/packages/web/dist/assets/ProjectMenu-PhwXTmgn.js +2 -0
  36. package/packages/web/dist/assets/{ProjectRedirect-k10uM_63.js → ProjectRedirect-N4g7WrZm.js} +1 -1
  37. package/packages/web/dist/assets/ProjectView-B-7AIt--.js +13 -0
  38. package/packages/web/dist/assets/{ProjectsGrid-DrzZ7R8M.js → ProjectsGrid-DZF5VHJF.js} +1 -1
  39. package/packages/web/dist/assets/RootHome-C9wEtWHu.js +2 -0
  40. package/packages/web/dist/assets/Surfaces-BidCkENw.js +2 -0
  41. package/packages/web/dist/assets/{arc-BKAVFR26.js → arc-BctfAqLY.js} +1 -1
  42. package/packages/web/dist/assets/architectureDiagram-3BPJPVTR-BJgwSqOO.js +37 -0
  43. package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-Eu4BV7Ea.js → blockDiagram-GPEHLZMM-BD2skmST.js} +1 -1
  44. package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-DR8PZaoa.js → c4Diagram-AAUBKEIU-DckMrhMv.js} +1 -1
  45. package/packages/web/dist/assets/channel-CsZOz8jm.js +2 -0
  46. package/packages/web/dist/assets/{chunk-2J33WTMH-CgvIIrI1.js → chunk-2J33WTMH-CQDUH7rp.js} +1 -1
  47. package/packages/web/dist/assets/{chunk-4BX2VUAB-CyJWrZqN.js → chunk-4BX2VUAB-Bae1J961.js} +1 -1
  48. package/packages/web/dist/assets/{chunk-55IACEB6-DXEnZN1d.js → chunk-55IACEB6-CXxS5wOa.js} +1 -1
  49. package/packages/web/dist/assets/{chunk-727SXJPM-Co89QTmp.js → chunk-727SXJPM-DSNFkPB3.js} +1 -1
  50. package/packages/web/dist/assets/{chunk-AQP2D5EJ-BRxTzfev.js → chunk-AQP2D5EJ-B2mqFyB7.js} +1 -1
  51. package/packages/web/dist/assets/{chunk-FMBD7UC4-DUj18CvF.js → chunk-FMBD7UC4-BfqEy3lu.js} +1 -1
  52. package/packages/web/dist/assets/{chunk-ND2GUHAM-BQMMJobN.js → chunk-ND2GUHAM-Bm6CBVK7.js} +1 -1
  53. package/packages/web/dist/assets/{chunk-QZHKN3VN-COx8LZCe.js → chunk-QZHKN3VN-NmnLtgYc.js} +1 -1
  54. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DdALkhE9.js +2 -0
  55. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DdALkhE9.js +2 -0
  56. package/packages/web/dist/assets/{core-DnpcT0s6.js → core-YKeFSK1q.js} +1 -1
  57. package/packages/web/dist/assets/cose-bilkent-S5V4N54A-qf_mXpa9.js +2 -0
  58. package/packages/web/dist/assets/{dagre-BM42HDAG-CuaYJexd.js → dagre-BM42HDAG-C31v-j3s.js} +1 -1
  59. package/packages/web/dist/assets/{diagram-2AECGRRQ-C9_anxrA.js → diagram-2AECGRRQ-C_TiPHdS.js} +1 -1
  60. package/packages/web/dist/assets/{diagram-5GNKFQAL-Ckf3kFlY.js → diagram-5GNKFQAL-BC8gUFK-.js} +1 -1
  61. package/packages/web/dist/assets/{diagram-KO2AKTUF-BrpUuY5x.js → diagram-KO2AKTUF-vO_QYWxi.js} +1 -1
  62. package/packages/web/dist/assets/{diagram-LMA3HP47-B9lyXZXW.js → diagram-LMA3HP47-vxJ-YZOg.js} +1 -1
  63. package/packages/web/dist/assets/{diagram-OG6HWLK6-DT7ro32m.js → diagram-OG6HWLK6-BNIfJG6l.js} +1 -1
  64. package/packages/web/dist/assets/{erDiagram-TEJ5UH35-ZZn9bHQh.js → erDiagram-TEJ5UH35-B37HsOFM.js} +1 -1
  65. package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-BxQ82PY_.js → flowDiagram-I6XJVG4X-DqA5X81H.js} +1 -1
  66. package/packages/web/dist/assets/ganttDiagram-6RSMTGT7-BMP0zpIg.js +293 -0
  67. package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-BlwODdH9.js → gitGraphDiagram-PVQCEYII-P0nXaCzx.js} +1 -1
  68. package/packages/web/dist/assets/index-BkY_Nc_i.css +1 -0
  69. package/packages/web/dist/assets/index-C3MdluUb.js +71 -0
  70. package/packages/web/dist/assets/{infoDiagram-5YYISTIA-nanSBFyG.js → infoDiagram-5YYISTIA-DpKYQ7VK.js} +1 -1
  71. package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-yZHr_s28.js → ishikawaDiagram-YF4QCWOH-BZeRMg3C.js} +1 -1
  72. package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-BKjDqQEQ.js → journeyDiagram-JHISSGLW-Cg2O0qzv.js} +1 -1
  73. package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-QusvHqc6.js → kanban-definition-UN3LZRKU-DBwVxUvI.js} +1 -1
  74. package/packages/web/dist/assets/{linear-4xRoD38Z.js → linear-R8qRwYqx.js} +1 -1
  75. package/packages/web/dist/assets/{mermaid.core-CeulYvDv.js → mermaid.core-jctTs2en.js} +4 -4
  76. package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-DSjYt2Kp.js → mindmap-definition-RKZ34NQL-CtmsNfpb.js} +1 -1
  77. package/packages/web/dist/assets/{pieDiagram-4H26LBE5-Ci0gO3CA.js → pieDiagram-4H26LBE5-C-UiLFDv.js} +1 -1
  78. package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-y2qHkaHP.js → quadrantDiagram-W4KKPZXB-BS4SWa01.js} +1 -1
  79. package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-D9hiWbad.js → requirementDiagram-4Y6WPE33-LPPAC6Q1.js} +1 -1
  80. package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-DA3Mvoze.js → sankeyDiagram-5OEKKPKP-tjF8KyS3.js} +1 -1
  81. package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-DWACrljw.js → sequenceDiagram-3UESZ5HK-kmddkdk2.js} +1 -1
  82. package/packages/web/dist/assets/{stateDiagram-AJRCARHV-CFGEhoTb.js → stateDiagram-AJRCARHV-B07mykTY.js} +1 -1
  83. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-r6WdX0dw.js +2 -0
  84. package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-Cjygpd9e.js → timeline-definition-PNZ67QCA-DrUgi0cT.js} +1 -1
  85. package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-zQnLaxgv.js → vennDiagram-CIIHVFJN-Dz6p0m7l.js} +1 -1
  86. package/packages/web/dist/assets/{wardley-L42UT6IY-DGmt-Lo2.js → wardley-L42UT6IY-DM16lA5x.js} +1 -1
  87. package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-Dj3vDukN.js → wardleyDiagram-YWT4CUSO-CCFQkFGB.js} +1 -1
  88. package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-C0bGbwbL.js → xychartDiagram-2RQKCTM6-DiUI5Dos.js} +1 -1
  89. package/packages/web/dist/index.html +2 -2
  90. package/packages/web/dist/sw.js +1 -1
  91. package/packages/web/dist/assets/Chip-Cp32dDmz.js +0 -2
  92. package/packages/web/dist/assets/DiscoverPage-Cs439wTf.js +0 -2
  93. package/packages/web/dist/assets/DiscoverView-DoGAlUXT.js +0 -2
  94. package/packages/web/dist/assets/InstanceConfigPage-BDypeo_r.js +0 -2
  95. package/packages/web/dist/assets/ProjectMenu-DXXXtXeh.js +0 -2
  96. package/packages/web/dist/assets/ProjectView-BxjcMlSO.js +0 -13
  97. package/packages/web/dist/assets/RootHome-rWodzOC8.js +0 -2
  98. package/packages/web/dist/assets/architectureDiagram-3BPJPVTR-UzIlxaCQ.js +0 -37
  99. package/packages/web/dist/assets/channel-C-sodtn7.js +0 -2
  100. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-D2eBPVdq.js +0 -2
  101. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-D2eBPVdq.js +0 -2
  102. package/packages/web/dist/assets/cose-bilkent-S5V4N54A-B-4NadKI.js +0 -2
  103. package/packages/web/dist/assets/ganttDiagram-6RSMTGT7-BxliloOH.js +0 -293
  104. package/packages/web/dist/assets/index-Cx9y6c3f.js +0 -71
  105. package/packages/web/dist/assets/index-bC5uCujx.css +0 -1
  106. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CLfbDXLX.js +0 -2
@@ -24,12 +24,13 @@ import { fileURLToPath } from "node:url";
24
24
  import YAML from "yaml";
25
25
  import { DEFAULT_DRIVE_MODE, isKnownDriveMode, isKnownModel, } from "./models.js";
26
26
  import { SCHEMA_VERSION_KEY, configSchemaRefusal } from "./schema-version.js";
27
- import { DEFAULT_MAX_SPAWN_DEPTH, isValidMaxSpawnDepth } from "./spawn-capability.js";
28
- import { DEFAULT_TRANSCRIPTS_MODE, isKnownTranscriptsMode, } from "./transcripts.js";
29
- import { DEFAULT_CREDENTIALS_MODE, isKnownCredentialsMode, } from "./claude-credentials.js";
30
- import { DEFAULT_INSTRUCTIONS_MODE, isKnownInstructionsMode, } from "./claude-instructions.js";
31
- import { DEFAULT_HOOKS_MODE, isKnownHooksMode } from "./claude-settings.js";
32
- import { DEFAULT_MCP_SERVERS_MODE, isKnownMcpServersMode, } from "./claude-mcp.js";
27
+ import { isValidMaxSpawnDepth } from "./spawn-capability.js";
28
+ import { posture as postureFor, resolveProfileName, } from "./profiles.js";
29
+ import { isKnownTranscriptsMode } from "./transcripts.js";
30
+ import { isKnownCredentialsMode } from "./claude-credentials.js";
31
+ import { isKnownInstructionsMode } from "./claude-instructions.js";
32
+ import { isKnownHooksMode } from "./claude-settings.js";
33
+ import { isKnownMcpServersMode, } from "./claude-mcp.js";
33
34
  import { resolveDeclaredMcpServers } from "./mcp-servers.js";
34
35
  import { DEFAULT_ENVIRONMENT_PROMPT } from "./environment-prompt.js";
35
36
  import { DEFAULT_RECOVERY } from "./recovery-config.js";
@@ -254,7 +255,7 @@ function loadBrandConfig(file = {}) {
254
255
  export function resolveDefaultWebDist(moduleUrl) {
255
256
  return path.resolve(path.dirname(fileURLToPath(moduleUrl)), "../../web/dist");
256
257
  }
257
- export function loadPaddockConfig() {
258
+ export function loadPaddockConfig(opts = {}) {
258
259
  // The optional YAML instance-config file provides the BASE layer; env vars
259
260
  // override it (precedence file < env). The file is located under the data dir,
260
261
  // so resolve a BOOTSTRAP data dir from env/default first to find it — the file
@@ -264,11 +265,13 @@ export function loadPaddockConfig() {
264
265
  // Ensure the data root exists first so symlinks (e.g. /tmp -> /private/tmp on
265
266
  // macOS) resolve consistently for every derived path below.
266
267
  const dataRoot = abs(envOr("PADDOCK_DATA_DIR", fileOr(file.dataDir, "./data")));
267
- try {
268
- fs.mkdirSync(dataRoot, { recursive: true });
269
- }
270
- catch {
271
- /* best-effort; downstream mkdirs will surface real errors */
268
+ if (opts.createDataDir !== false) {
269
+ try {
270
+ fs.mkdirSync(dataRoot, { recursive: true });
271
+ }
272
+ catch {
273
+ /* best-effort; downstream mkdirs will surface real errors */
274
+ }
272
275
  }
273
276
  // working_directory of keeper agents MUST be canonical so session discovery
274
277
  // (which encodes the real path) can find Claude transcripts.
@@ -283,7 +286,15 @@ export function loadPaddockConfig() {
283
286
  const resolvedClaudeHome = resolveClaudeHome(dataDir, fileOpt(file.claudeHome));
284
287
  const legacyClaudeHome = userClaudeHome();
285
288
  const defaultWebDist = resolveDefaultWebDist(import.meta.url);
289
+ // The posture profile (#878), resolved BEFORE the levers it supplies defaults
290
+ // for. It is not a config layer of its own: `p` is handed to each posture
291
+ // loader as the fallback that used to be a hardcoded literal, so an individual
292
+ // file key or env var still wins over it. See `profiles.ts` for why an
293
+ // individual FILE key deliberately beats `PADDOCK_PROFILE` in the env.
294
+ const profile = resolveProfileName(file.profile);
295
+ const p = postureFor(profile);
286
296
  return Object.freeze({
297
+ profile,
287
298
  port: Number(envOr("PORT", fileOr(file.port, "7233"))),
288
299
  // Safe by default (#435): bind loopback unless explicitly told otherwise, so
289
300
  // a fresh source/tarball run is network-closed. Non-loopback + no auth is then
@@ -300,7 +311,7 @@ export function loadPaddockConfig() {
300
311
  webDist: abs(envOr("PADDOCK_WEB_DIST", fileOr(file.webDist, defaultWebDist))),
301
312
  claudeHome: resolvedClaudeHome,
302
313
  legacyClaudeHome,
303
- claude: loadClaudeConfig(file.claude),
314
+ claude: loadClaudeConfig(file.claude, p),
304
315
  auth: loadAuthConfig(file.auth),
305
316
  transcription: loadTranscriptionConfig(file.transcription),
306
317
  brand: loadBrandConfig(file.brand),
@@ -308,15 +319,19 @@ export function loadPaddockConfig() {
308
319
  driveMode: loadDriveMode(file.driveMode),
309
320
  nativeSystemPrompt: loadNativeSystemPrompt(file.nativeSystemPrompt),
310
321
  environmentPrompt: loadEnvironmentPrompt(file.environmentPrompt),
311
- selfMcpEnabled: loadSelfMcpEnabled(file.selfMcpEnabled),
312
- selfMcpWriteEnabled: loadSelfMcpEnabled(file.selfMcpEnabled) && loadSelfMcpWriteEnabled(file.selfMcpWriteEnabled),
322
+ selfMcpEnabled: loadSelfMcpEnabled(file.selfMcpEnabled, p.selfMcpEnabled),
323
+ selfMcpWriteEnabled: loadSelfMcpEnabled(file.selfMcpEnabled, p.selfMcpEnabled) &&
324
+ loadSelfMcpWriteEnabled(file.selfMcpWriteEnabled, p.selfMcpWriteEnabled),
313
325
  // Projects tool implies write implies read — the tool is appended in the write
314
326
  // block, so both outer gates must be on for it to mean anything (issue #467).
315
- selfMcpProjectsEnabled: loadSelfMcpEnabled(file.selfMcpEnabled) &&
316
- loadSelfMcpWriteEnabled(file.selfMcpWriteEnabled) &&
317
- loadSelfMcpProjectsEnabled(file.selfMcpProjectsEnabled),
318
- maxSpawnDepth: loadMaxSpawnDepth(file.maxSpawnDepth),
319
- scheduleMutationEnabled: loadScheduleMutationEnabled(file.scheduleMutationEnabled),
327
+ // The cascade is why a profile's three self-MCP values are never read in
328
+ // isolation: `profile: yolo` plus an explicit `selfMcpEnabled: false`
329
+ // correctly collapses write and projects too.
330
+ selfMcpProjectsEnabled: loadSelfMcpEnabled(file.selfMcpEnabled, p.selfMcpEnabled) &&
331
+ loadSelfMcpWriteEnabled(file.selfMcpWriteEnabled, p.selfMcpWriteEnabled) &&
332
+ loadSelfMcpProjectsEnabled(file.selfMcpProjectsEnabled, p.selfMcpProjectsEnabled),
333
+ maxSpawnDepth: loadMaxSpawnDepth(file.maxSpawnDepth, p.maxSpawnDepth),
334
+ scheduleMutationEnabled: loadScheduleMutationEnabled(file.scheduleMutationEnabled, p.scheduleMutationEnabled),
320
335
  ...(() => {
321
336
  // #312 M1: resolved against the real environment (token material is only
322
337
  // ever an `env:` reference). Diagnostics ride along for app.ts to log.
@@ -330,12 +345,12 @@ export function loadPaddockConfig() {
330
345
  const { servers, errors, warnings } = resolveDeclaredMcpServers(file.mcpServers, process.env);
331
346
  return { mcpServers: servers, mcpServersDiagnostics: { errors, warnings } };
332
347
  })(),
333
- hooksMcpEnabled: loadHooksMcpEnabled(file.hooksMcpEnabled),
348
+ hooksMcpEnabled: loadHooksMcpEnabled(file.hooksMcpEnabled, p.hooksMcpEnabled),
334
349
  recovery: loadRecoveryConfig(file.recovery),
335
350
  attachments: loadAttachmentsConfig(file.attachments),
336
351
  curation: loadCurationConfig(file.curation),
337
352
  logLevel: envOr("LOG_LEVEL", fileOr(file.logLevel, "info")),
338
- browserMcp: loadBrowserMcp(file.browserMcp),
353
+ browserMcp: loadBrowserMcp(file.browserMcp, p.browserMcp),
339
354
  openapi: loadOpenApiConfig(file.openapi),
340
355
  sweepMinIntervalMs: loadSweepMinIntervalMs(file.sweepMinIntervalMs),
341
356
  gitAuthor: {
@@ -383,11 +398,13 @@ function loadSweepMinIntervalMs(file) {
383
398
  * behaviour); only when the env var is UNSET does the config file provide the
384
399
  * base, using the 1/true/yes convention shared by the other boolean knobs.
385
400
  */
386
- function loadBrowserMcp(file) {
401
+ function loadBrowserMcp(file, fallback) {
387
402
  const env = process.env.PADDOCK_BROWSER_MCP;
388
403
  if (env !== undefined)
389
404
  return env === "1";
390
405
  const raw = fileOpt(file)?.toLowerCase();
406
+ if (raw === undefined)
407
+ return fallback;
391
408
  return raw === "1" || raw === "true" || raw === "yes";
392
409
  }
393
410
  /**
@@ -414,12 +431,12 @@ function loadOpenApiConfig(file) {
414
431
  * missing/blank/out-of-range value falls back to the default rather than failing
415
432
  * startup. A per-project override still wins at dispatch.
416
433
  */
417
- function loadMaxSpawnDepth(file) {
434
+ function loadMaxSpawnDepth(file, fallback) {
418
435
  const raw = envOpt("PADDOCK_MAX_SPAWN_DEPTH") ?? fileOpt(file);
419
436
  if (raw === undefined)
420
- return DEFAULT_MAX_SPAWN_DEPTH;
437
+ return fallback;
421
438
  const n = Number(raw);
422
- return isValidMaxSpawnDepth(n) ? n : DEFAULT_MAX_SPAWN_DEPTH;
439
+ return isValidMaxSpawnDepth(n) ? n : fallback;
423
440
  }
424
441
  /**
425
442
  * Resolve a boolean recovery knob (issue #301) with the shared 1/true/yes
@@ -503,8 +520,8 @@ function loadAttachmentsConfig(file) {
503
520
  * operator opts in with `PADDOCK_SCHEDULE_MUTATION=1` (or the config file). Accepts
504
521
  * 1/true/yes. Static `project.yaml` schedules are armed regardless of this flag.
505
522
  */
506
- function loadScheduleMutationEnabled(file) {
507
- const raw = envOr("PADDOCK_SCHEDULE_MUTATION", fileOr(file, "false")).toLowerCase();
523
+ function loadScheduleMutationEnabled(file, fallback) {
524
+ const raw = envOr("PADDOCK_SCHEDULE_MUTATION", fileOr(file, String(fallback))).toLowerCase();
508
525
  return raw === "1" || raw === "true" || raw === "yes";
509
526
  }
510
527
  /**
@@ -514,8 +531,8 @@ function loadScheduleMutationEnabled(file) {
514
531
  * override still wins at dispatch. Accepts 1/true/yes. Only meaningful when the
515
532
  * self-MCP write tools are also enabled (the hook tools live on that server).
516
533
  */
517
- function loadHooksMcpEnabled(file) {
518
- const raw = envOr("PADDOCK_HOOKS_MCP", fileOr(file, "false")).toLowerCase();
534
+ function loadHooksMcpEnabled(file, fallback) {
535
+ const raw = envOr("PADDOCK_HOOKS_MCP", fileOr(file, String(fallback))).toLowerCase();
519
536
  return raw === "1" || raw === "true" || raw === "yes";
520
537
  }
521
538
  /**
@@ -524,8 +541,8 @@ function loadHooksMcpEnabled(file) {
524
541
  * is also on (write implies read — enforced at the call site above). Accepts
525
542
  * 1/true/yes.
526
543
  */
527
- function loadSelfMcpWriteEnabled(file) {
528
- const raw = envOr("PADDOCK_SELF_MCP_WRITE", fileOr(file, "false")).toLowerCase();
544
+ function loadSelfMcpWriteEnabled(file, fallback) {
545
+ const raw = envOr("PADDOCK_SELF_MCP_WRITE", fileOr(file, String(fallback))).toLowerCase();
529
546
  return raw === "1" || raw === "true" || raw === "yes";
530
547
  }
531
548
  /**
@@ -534,8 +551,8 @@ function loadSelfMcpWriteEnabled(file) {
534
551
  * `PADDOCK_SELF_MCP` and `PADDOCK_SELF_MCP_WRITE` are also on (enforced at the call
535
552
  * site above). Accepts 1/true/yes.
536
553
  */
537
- function loadSelfMcpProjectsEnabled(file) {
538
- const raw = envOr("PADDOCK_SELF_MCP_PROJECTS", fileOr(file, "false")).toLowerCase();
554
+ function loadSelfMcpProjectsEnabled(file, fallback) {
555
+ const raw = envOr("PADDOCK_SELF_MCP_PROJECTS", fileOr(file, String(fallback))).toLowerCase();
539
556
  return raw === "1" || raw === "true" || raw === "yes";
540
557
  }
541
558
  /**
@@ -543,8 +560,8 @@ function loadSelfMcpProjectsEnabled(file) {
543
560
  * Defaults to OFF so a plain instance never advertises it; opt in per instance
544
561
  * with `PADDOCK_SELF_MCP=1`. Accepts 1/true/yes.
545
562
  */
546
- function loadSelfMcpEnabled(file) {
547
- const raw = envOr("PADDOCK_SELF_MCP", fileOr(file, "false")).toLowerCase();
563
+ function loadSelfMcpEnabled(file, fallback) {
564
+ const raw = envOr("PADDOCK_SELF_MCP", fileOr(file, String(fallback))).toLowerCase();
548
565
  return raw === "1" || raw === "true" || raw === "yes";
549
566
  }
550
567
  /**
@@ -660,20 +677,18 @@ export function userClaudeHome() {
660
677
  * reason — an MCP server is a process paddock spawns, so a typo that attaches
661
678
  * nothing is better than one that attaches the user's whole set unasked.
662
679
  */
663
- function loadClaudeConfig(file = {}) {
680
+ function loadClaudeConfig(file = {}, p) {
664
681
  const transcripts = (envOpt("PADDOCK_CLAUDE_TRANSCRIPTS") ?? fileOpt(file.transcripts))?.toLowerCase();
665
682
  const credentials = (envOpt("PADDOCK_CLAUDE_CREDENTIALS") ?? fileOpt(file.credentials))?.toLowerCase();
666
683
  const instructions = (envOpt("PADDOCK_CLAUDE_INSTRUCTIONS") ?? fileOpt(file.instructions))?.toLowerCase();
667
684
  const hooks = (envOpt("PADDOCK_CLAUDE_HOOKS") ?? fileOpt(file.hooks))?.toLowerCase();
668
685
  const mcpServers = (envOpt("PADDOCK_CLAUDE_MCP_SERVERS") ?? fileOpt(file.mcpServers))?.toLowerCase();
669
686
  return {
670
- transcripts: transcripts && isKnownTranscriptsMode(transcripts) ? transcripts : DEFAULT_TRANSCRIPTS_MODE,
671
- credentials: credentials && isKnownCredentialsMode(credentials) ? credentials : DEFAULT_CREDENTIALS_MODE,
672
- instructions: instructions && isKnownInstructionsMode(instructions)
673
- ? instructions
674
- : DEFAULT_INSTRUCTIONS_MODE,
675
- hooks: hooks && isKnownHooksMode(hooks) ? hooks : DEFAULT_HOOKS_MODE,
676
- mcpServers: mcpServers && isKnownMcpServersMode(mcpServers) ? mcpServers : DEFAULT_MCP_SERVERS_MODE,
687
+ transcripts: transcripts && isKnownTranscriptsMode(transcripts) ? transcripts : p.transcripts,
688
+ credentials: credentials && isKnownCredentialsMode(credentials) ? credentials : p.credentials,
689
+ instructions: instructions && isKnownInstructionsMode(instructions) ? instructions : p.instructions,
690
+ hooks: hooks && isKnownHooksMode(hooks) ? hooks : p.hooks,
691
+ mcpServers: mcpServers && isKnownMcpServersMode(mcpServers) ? mcpServers : p.mcpServers,
677
692
  };
678
693
  }
679
694
  /**
@@ -49,6 +49,7 @@
49
49
  import { FleetManager, type DiscoveredSession, type ChatMessage, type SDKMessage, type TriggerResult, type AgentInfo, type FleetStatus, type SlashCommand, type InjectedMcpServerDef, type SessionWakeHandler, type ResolveInjectedMcpServers, type ScheduleTriggerHandler, type ScheduleInfo, type JobMetadata, type AdoptionPlacementMode, type AdoptableSession, type AdoptSkippedSession } from "@herdctl/core";
50
50
  import type { PaddockConfig } from "./config.js";
51
51
  import type { Project } from "./projects.js";
52
+ import { type UnplantedChatsLink } from "./transcripts.js";
52
53
  import { type PaddockTrigger } from "./trigger-config.js";
53
54
  import { type McpSources } from "./claude-mcp.js";
54
55
  import { type HostPluginSource } from "./claude-plugins.js";
@@ -190,6 +191,22 @@ export declare class HerdctlService {
190
191
  * fix's residue gate — see {@link residueDepthFor} + `./resume-drain.ts`).
191
192
  */
192
193
  private agentWorkingDirs;
194
+ /**
195
+ * Each agent's `.chats` HOST dir — `project.dir`, which for a repo-backed
196
+ * project is NOT `workingDir` (#187). Recorded so {@link deleteSession} can ask
197
+ * whether this project's store is still a planted symlink before it runs an
198
+ * `rm` through it (#708).
199
+ */
200
+ private agentChatsHostDirs;
201
+ /**
202
+ * Stale `.chats` symlinks this service unplanted while bringing the on-disk
203
+ * layout in line with `claude.transcripts` (#708) — i.e. an instance that used
204
+ * to run `host` and now runs `own`. Drained by `app.ts` into the boot log:
205
+ * the transcripts written during the `host` period are still in the user's own
206
+ * Claude home and are no longer in paddock's chat list, so the operator needs
207
+ * the paths to decide what to import. Empty on every normal boot.
208
+ */
209
+ readonly unplantedChatsLinks: UnplantedChatsLink[];
193
210
  /**
194
211
  * Live session-mode turns keyed by the synthetic turn id we hand back as the
195
212
  * `jobId` (Paddock#111). `openChatSession` creates no herdctl job record, so
@@ -557,6 +574,26 @@ export declare class HerdctlService {
557
574
  *
558
575
  * The chat leaves paddock's list either way — which is what the user asked for
559
576
  * — so `retained` is how a caller tells "gone" from "no longer shown here".
577
+ *
578
+ * ## The `own` branch's one precondition (#708)
579
+ *
580
+ * `manager.deleteSession` `rm`s `<claudeHome>/projects/<enc>/<id>.jsonl`, which
581
+ * paddock has symlinked at `<project.dir>/.chats`. That is only paddock's store
582
+ * if `.chats` is a real directory. On an instance flipped `host` → `own` it was
583
+ * a leftover symlink into the user's own Claude home, so the `own` branch —
584
+ * chosen precisely because "these transcripts are ours to delete" — resolved
585
+ * through it and unlinked the user's real terminal history.
586
+ *
587
+ * `ensureProjectChats` clears that symlink on every boot now, so this should
588
+ * never fire. It is here anyway because that healing is best-effort by
589
+ * construction: the whole module swallows its own errors so a layout problem
590
+ * can never block chat, and the single operation that must not run against an
591
+ * unhealed layout is this `rm`. Checking is one `lstat` that never follows the
592
+ * link, so the guard itself cannot touch `~/.claude` either.
593
+ *
594
+ * Refusing reports the same `{removed: false, retained: true}` the `host`
595
+ * branch does, for the same reason: nothing was deleted, so do not claim it
596
+ * was. The next boot heals the layout and the delete then works normally.
560
597
  */
561
598
  deleteSession(agentName: string, sessionId: string): Promise<ChatDeletion>;
562
599
  /**
@@ -983,6 +1020,14 @@ export declare class HerdctlService {
983
1020
  * errors, and a missing symlink only means Claude keeps its default location.
984
1021
  */
985
1022
  private ensureSweeperHome;
1023
+ /**
1024
+ * {@link ensureProjectChats} for every call site, so the stale-`.chats`-symlink
1025
+ * findings it reports (#708) are accumulated in one place rather than dropped
1026
+ * by three independent callers. Sweeper homes are included deliberately: they
1027
+ * inherited the same stale link, so they wrote their transcripts into the
1028
+ * user's real Claude home too.
1029
+ */
1030
+ private ensureChats;
986
1031
  private triggerAgentConfig;
987
1032
  private ensureConfigFile;
988
1033
  }
@@ -53,7 +53,7 @@ import { promises as fs } from "node:fs";
53
53
  import { randomUUID } from "node:crypto";
54
54
  import path from "node:path";
55
55
  import { DEFAULT_MODEL } from "./models.js";
56
- import { ensureProjectChats, projectChatsDir } from "./transcripts.js";
56
+ import { chatsDirIsPlanted, ensureProjectChats, projectChatsDir, } from "./transcripts.js";
57
57
  import { triggerRunsOnOwnAgent, isCuratorTrigger, } from "./trigger-config.js";
58
58
  import { buildAgentConfig, buildSweeperConfig, sweeperWorkingDir, buildTriggerConfig, ensureConfigFile as writeBootConfigFile, } from "./herdctl-agent-config.js";
59
59
  import { EMPTY_MCP_SOURCES } from "./claude-mcp.js";
@@ -254,6 +254,22 @@ export class HerdctlService {
254
254
  * fix's residue gate — see {@link residueDepthFor} + `./resume-drain.ts`).
255
255
  */
256
256
  agentWorkingDirs = new Map();
257
+ /**
258
+ * Each agent's `.chats` HOST dir — `project.dir`, which for a repo-backed
259
+ * project is NOT `workingDir` (#187). Recorded so {@link deleteSession} can ask
260
+ * whether this project's store is still a planted symlink before it runs an
261
+ * `rm` through it (#708).
262
+ */
263
+ agentChatsHostDirs = new Map();
264
+ /**
265
+ * Stale `.chats` symlinks this service unplanted while bringing the on-disk
266
+ * layout in line with `claude.transcripts` (#708) — i.e. an instance that used
267
+ * to run `host` and now runs `own`. Drained by `app.ts` into the boot log:
268
+ * the transcripts written during the `host` period are still in the user's own
269
+ * Claude home and are no longer in paddock's chat list, so the operator needs
270
+ * the paths to decide what to import. Empty on every normal boot.
271
+ */
272
+ unplantedChatsLinks = [];
257
273
  /**
258
274
  * Live session-mode turns keyed by the synthetic turn id we hand back as the
259
275
  * `jobId` (Paddock#111). `openChatSession` creates no herdctl job record, so
@@ -420,7 +436,7 @@ export class HerdctlService {
420
436
  // at the .chats store in the metadata dir — so repo-backed transcripts stay
421
437
  // out of the external checkout's working tree (issue #187). For a notebook
422
438
  // project workingDir === dir, so this is the classic behavior.
423
- await ensureProjectChats(project.workingDir, project.dir, this.claudeHomeTarget);
439
+ await this.ensureChats(project.workingDir, project.dir);
424
440
  await this.ensureSweeperHome(project);
425
441
  await this.fleet.addAgent(this.keeperAgentConfig(project), { replace: true });
426
442
  await this.fleet.addAgent(this.sweeperAgentConfig(project), { replace: true });
@@ -430,6 +446,7 @@ export class HerdctlService {
430
446
  await this.registerTriggerAgents(project);
431
447
  this.agentModels.set(keeperAgentName(project.slug), project.model ?? DEFAULT_MODEL);
432
448
  this.agentWorkingDirs.set(keeperAgentName(project.slug), project.workingDir);
449
+ this.agentChatsHostDirs.set(keeperAgentName(project.slug), project.dir);
433
450
  }
434
451
  }
435
452
  /** Start the scheduler (keeper agents currently have no schedules, but this
@@ -494,7 +511,7 @@ export class HerdctlService {
494
511
  async ensureProjectAgent(project) {
495
512
  if (!this.fleet)
496
513
  return;
497
- await ensureProjectChats(project.workingDir, project.dir, this.claudeHomeTarget);
514
+ await this.ensureChats(project.workingDir, project.dir);
498
515
  await this.ensureSweeperHome(project);
499
516
  await this.fleet.addAgent(this.keeperAgentConfig(project), { replace: true });
500
517
  await this.fleet.addAgent(this.sweeperAgentConfig(project), { replace: true });
@@ -507,6 +524,7 @@ export class HerdctlService {
507
524
  // default), so a model change via PATCH takes effect here too.
508
525
  this.agentModels.set(keeperAgentName(project.slug), project.model ?? DEFAULT_MODEL);
509
526
  this.agentWorkingDirs.set(keeperAgentName(project.slug), project.workingDir);
527
+ this.agentChatsHostDirs.set(keeperAgentName(project.slug), project.dir);
510
528
  }
511
529
  /**
512
530
  * Register (or replace) every trigger a project declares that runs on its OWN scoped
@@ -922,9 +940,33 @@ export class HerdctlService {
922
940
  *
923
941
  * The chat leaves paddock's list either way — which is what the user asked for
924
942
  * — so `retained` is how a caller tells "gone" from "no longer shown here".
943
+ *
944
+ * ## The `own` branch's one precondition (#708)
945
+ *
946
+ * `manager.deleteSession` `rm`s `<claudeHome>/projects/<enc>/<id>.jsonl`, which
947
+ * paddock has symlinked at `<project.dir>/.chats`. That is only paddock's store
948
+ * if `.chats` is a real directory. On an instance flipped `host` → `own` it was
949
+ * a leftover symlink into the user's own Claude home, so the `own` branch —
950
+ * chosen precisely because "these transcripts are ours to delete" — resolved
951
+ * through it and unlinked the user's real terminal history.
952
+ *
953
+ * `ensureProjectChats` clears that symlink on every boot now, so this should
954
+ * never fire. It is here anyway because that healing is best-effort by
955
+ * construction: the whole module swallows its own errors so a layout problem
956
+ * can never block chat, and the single operation that must not run against an
957
+ * unhealed layout is this `rm`. Checking is one `lstat` that never follows the
958
+ * link, so the guard itself cannot touch `~/.claude` either.
959
+ *
960
+ * Refusing reports the same `{removed: false, retained: true}` the `host`
961
+ * branch does, for the same reason: nothing was deleted, so do not claim it
962
+ * was. The next boot heals the layout and the delete then works normally.
925
963
  */
926
964
  async deleteSession(agentName, sessionId) {
927
965
  if (this.cfg.claude.transcripts === "own") {
966
+ const chatsHostDir = this.agentChatsHostDirs.get(agentName);
967
+ if (chatsHostDir !== undefined && (await chatsDirIsPlanted(chatsHostDir))) {
968
+ return { removed: false, retained: true };
969
+ }
928
970
  const removed = await this.manager.deleteSession(agentName, sessionId);
929
971
  await this.dropAgentSessionPointer(agentName, sessionId);
930
972
  return { removed, retained: false };
@@ -1831,7 +1873,19 @@ export class HerdctlService {
1831
1873
  */
1832
1874
  async ensureSweeperHome(project) {
1833
1875
  const dir = sweeperWorkingDir(this.cfg, project.slug);
1834
- await ensureProjectChats(dir, dir, this.claudeHomeTarget);
1876
+ await this.ensureChats(dir, dir);
1877
+ }
1878
+ /**
1879
+ * {@link ensureProjectChats} for every call site, so the stale-`.chats`-symlink
1880
+ * findings it reports (#708) are accumulated in one place rather than dropped
1881
+ * by three independent callers. Sweeper homes are included deliberately: they
1882
+ * inherited the same stale link, so they wrote their transcripts into the
1883
+ * user's real Claude home too.
1884
+ */
1885
+ async ensureChats(workingDir, chatsHostDir) {
1886
+ const unplanted = await ensureProjectChats(workingDir, chatsHostDir, this.claudeHomeTarget);
1887
+ if (unplanted)
1888
+ this.unplantedChatsLinks.push(unplanted);
1835
1889
  }
1836
1890
  triggerAgentConfig(project, triggerName, trigger) {
1837
1891
  return buildTriggerConfig(this.cfg, project, triggerName, trigger);
@@ -1,3 +1,5 @@
1
+ import { type ProfileName } from "./profiles.js";
2
+ import { type TranscriptsMode } from "./transcripts.js";
1
3
  import { type PaddockConfig } from "./config.js";
2
4
  /** Groups the Settings screen renders, in display order. */
3
5
  export declare const GROUPS: {
@@ -91,6 +93,14 @@ export interface InstanceConfigFieldDto {
91
93
  envOverridden: boolean;
92
94
  /** The env var shadowing this field (only when `envOverridden`). */
93
95
  envVar?: string;
96
+ /**
97
+ * The instance's posture profile is what put `value` here (#878): this field
98
+ * is one of the profile's levers, and neither the config file nor an env var
99
+ * overrode it. False on every non-posture field, and — importantly — false on
100
+ * a posture field the environment or the file set, so the UI never credits the
101
+ * profile for a value it did not choose. See {@link isFromProfile}.
102
+ */
103
+ fromProfile: boolean;
94
104
  }
95
105
  export interface InstanceConfigGroupDto {
96
106
  id: string;
@@ -129,33 +139,29 @@ export interface InstanceConfigDto {
129
139
  * than trusting a cached value: the point is to notice a write we did not make.
130
140
  */
131
141
  export declare function instanceConfigVersion(configPath: string): string | null;
142
+ /**
143
+ * The transcripts mode a RESTART would resolve — read from the config FILE,
144
+ * falling back to the profile's default when the file is silent about it.
145
+ *
146
+ * NOT `cfg.claude.transcripts`, which is what THIS process froze at boot. The
147
+ * two differ for the whole window between a successful `own → host` migration
148
+ * (#882) and the restart it asks for, and that window is precisely when a
149
+ * second POST arrives: without this the idempotency check would see `own`,
150
+ * decide nothing had happened, and re-run a migration that already had.
151
+ *
152
+ * The environment is deliberately not consulted. `PADDOCK_CLAUDE_TRANSCRIPTS`
153
+ * beats the file, so when it is set the true answer to "what would a restart
154
+ * resolve" is the env var — but a caller in that situation is already refusing
155
+ * with `env_shadowed`, and folding it in here would make a shadowed instance
156
+ * look permanently migrated.
157
+ */
158
+ export declare function pendingTranscriptsMode(cfg: PaddockConfig): TranscriptsMode;
132
159
  /**
133
160
  * Resolve the path a PUT writes to: an explicit `PADDOCK_CONFIG` env var wins
134
161
  * (the same rule {@link import("./config.js").loadConfigFile} reads it back
135
162
  * from), else `<dataDir>/paddock.config.yaml`.
136
163
  */
137
164
  export declare function instanceConfigPath(cfg: PaddockConfig): string;
138
- /**
139
- * Build the grouped GET DTO from the resolved (frozen) config, the live env AND
140
- * the config file on disk. Each field reports:
141
- *
142
- * - `value` — EFFECTIVE now, out of the frozen boot config;
143
- * - `pendingValue` — what a restart would resolve, out of the file this instant;
144
- * - `pendingRestart` — those two differ.
145
- *
146
- * Reading the file is the load-bearing part (#722). While this built the DTO
147
- * from the frozen config alone, `GET` could not observe ANY write — including
148
- * the one the caller had just made — so a successful save appeared to revert,
149
- * two tabs clobbered each other with nothing able to notice, and
150
- * `restartRequired` had no choice but to be a hardcoded `false`.
151
- *
152
- * Pending values are computed for EDITABLE, non-env-shadowed fields only:
153
- * - an env-shadowed field resolves to the same env value after a restart, so
154
- * the file cannot make it diverge;
155
- * - the read-only `advanced` bindings are normalised at boot (`dataDir` and
156
- * friends are canonicalised, `port` is `Number()`-ed), so comparing them
157
- * against raw file text would manufacture divergence that isn't there.
158
- */
159
165
  export declare function buildInstanceConfig(cfg: PaddockConfig): InstanceConfigDto;
160
166
  /** A rejected PUT: which field failed and why (surfaced as a 400). */
161
167
  export declare class InstanceConfigError extends Error {
@@ -191,4 +197,126 @@ export declare function writeInstanceConfig(configPath: string, pairs: {
191
197
  key: string;
192
198
  value: unknown;
193
199
  }[]): void;
200
+ /**
201
+ * WHICH LAYER supplied a field's effective value.
202
+ *
203
+ * Paddock resolves config as `profile < file < env` beneath a static code
204
+ * default, and #878's argument for a `config show --resolved` is that naming the
205
+ * layer is the honest answer to "I want it all written down" — unlike a
206
+ * hand-maintained file, a printed resolution cannot drift from what the process
207
+ * actually did.
208
+ *
209
+ * `profile` and `default` are deliberately separate, and that distinction is the
210
+ * whole point of the command for the twelve posture keys. Those keys no longer
211
+ * HAVE a code default of their own: the profile supplies it (see
212
+ * {@link defaultFor}). Collapsing the two would leave an operator unable to tell
213
+ * "Paddock has always shipped this" from "your profile chose this, and picking a
214
+ * different one would change it".
215
+ */
216
+ export type ConfigSource = "env" | "file" | "profile" | "default";
217
+ /** One field's effective value and where it came from. */
218
+ export interface ResolvedConfigField {
219
+ key: string;
220
+ group: string;
221
+ label: string;
222
+ help?: string;
223
+ type: FieldType;
224
+ /**
225
+ * The EFFECTIVE value, normalised the way {@link buildInstanceConfig} does it:
226
+ * a field the frozen config leaves unset reads as the fallback it implies (the
227
+ * profile's, or the code default), never as a bare `null`. Printing `null` for
228
+ * `models` — which resolves to "offer the whole catalog" — would be a lie
229
+ * dressed as precision.
230
+ */
231
+ value: unknown;
232
+ source: ConfigSource;
233
+ /** `env` → the variable name; `profile` → the profile name; `file` → its path. */
234
+ origin?: string;
235
+ sensitive: boolean;
236
+ /**
237
+ * Whether `paddock config eject` may materialise this key into the config file
238
+ * (#878). Decided here rather than in the CLI because it is a property of the
239
+ * field catalog, and a lever added to {@link FIELDS} tomorrow should land on
240
+ * the right side of it without anyone remembering the eject command exists.
241
+ *
242
+ * The rule is `(editable || posture key) && !sensitive`, which draws the line
243
+ * at **what an ejected file can honestly promise to be**:
244
+ *
245
+ * - **Posture and editable keys are in.** These are the settings a config file
246
+ * is for — the twelve levers a profile governs, plus everything the Settings
247
+ * screen already round-trips through this same file. Writing them changes no
248
+ * effective value, only where it comes from.
249
+ * - **Process/filesystem bindings are out** — `port`, `host`, `dataDir`,
250
+ * `projectsRoot`, `stateDir`, `herdctlConfigPath`, `webDist`. Their resolved
251
+ * values are absolute, canonicalised, machine-specific paths; `dataDir` in
252
+ * particular resolves to the directory the file itself lives in, so ejecting
253
+ * it writes a self-reference that survives a move and silently points the
254
+ * copy back at the original. An ejected config should stay portable and
255
+ * stay mountable, and freezing this instance's port into it is how a second
256
+ * instance started from the same file collides on boot.
257
+ * - **`sensitive` fields are out**, unconditionally and with no flag to
258
+ * override. `transcription.endpoint` is an operator-supplied URL that can
259
+ * read `https://user:token@host`; bulk-materialising a possibly
260
+ * credential-bearing value into a file on disk is not a thing to make one
261
+ * keystroke away. Writing that key by hand stays available and is a
262
+ * deliberate act, which is the right shape for it.
263
+ *
264
+ * Excluding sensitive fields costs nothing in fidelity, which is worth stating
265
+ * because it is not obvious: a sensitive field's value can only have come from
266
+ * the file (in which case it is already there and eject leaves it alone) or
267
+ * from the environment (which eject skips anyway) or from its default (in which
268
+ * case there is nothing to write). So there is no case where skipping one makes
269
+ * the ejected file resolve differently.
270
+ */
271
+ ejectable: boolean;
272
+ /**
273
+ * The config file names this key, but a higher layer won and this value is
274
+ * inert. Worth surfacing rather than silently ignoring: an operator who edited
275
+ * the file and saw nothing change is exactly the person running this command.
276
+ */
277
+ shadowedFileValue?: unknown;
278
+ }
279
+ /** The resolved profile, and which layer named it. */
280
+ export interface ResolvedProfileInfo {
281
+ name: ProfileName;
282
+ source: "env" | "file" | "default";
283
+ /** The variable name, or the config file path. Absent on `default`. */
284
+ origin?: string;
285
+ /**
286
+ * A profile name that WAS set and is not one of the three. `resolveProfileName`
287
+ * falls back to the default rather than failing the boot, which is the right
288
+ * call for a boot and a terrible one for a diagnostic — so the typo is reported
289
+ * here instead of vanishing.
290
+ */
291
+ unrecognised?: {
292
+ value: string;
293
+ origin: string;
294
+ };
295
+ }
296
+ /** Everything `paddock config show` needs, with no formatting decisions taken. */
297
+ export interface ResolvedConfigReport {
298
+ dataDir: string;
299
+ configPath: string;
300
+ configFileExists: boolean;
301
+ /** The file exists but could not be read/parsed (the loader is the strict reader). */
302
+ configFileError?: string;
303
+ profile: ResolvedProfileInfo;
304
+ fields: ResolvedConfigField[];
305
+ }
306
+ /**
307
+ * Resolve every field in {@link FIELDS} against a loaded config, reporting the
308
+ * layer each value came from.
309
+ *
310
+ * Built on the same descriptor table the Config screen uses rather than a
311
+ * parallel list, so a lever added for the UI is printed by the CLI for free and
312
+ * the two can never disagree about what a default is. What it does NOT reuse is
313
+ * `buildInstanceConfig`'s DTO: that answers "what would a restart change?", which
314
+ * needs the file compared against the process. This answers "where did each of
315
+ * these values come from?", which needs the file compared against the LAYERS.
316
+ *
317
+ * No value here is a secret — {@link FIELDS} excludes them by construction — but
318
+ * `sensitive` rides along so a caller can decide what to put on a terminal that
319
+ * ends up pasted into an issue.
320
+ */
321
+ export declare function resolveConfigReport(cfg: PaddockConfig): ResolvedConfigReport;
194
322
  export {};