pi-pier 0.3.0 → 0.3.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 (96) hide show
  1. package/README.md +12 -11
  2. package/package.json +2 -2
  3. package/schemas/role-manifest.schema.json +7 -1
  4. package/skills/pi-herdr-workflow/SKILL.md +4 -5
  5. package/src/ansi-text.ts +62 -11
  6. package/src/ask-dialog.ts +308 -0
  7. package/src/ask-user.ts +113 -112
  8. package/src/bootstrap.ts +8 -12
  9. package/src/compact-coordinator.ts +278 -261
  10. package/src/compact-economics-core.ts +77 -145
  11. package/src/config-catalog-core.ts +77 -197
  12. package/src/config-command.ts +271 -64
  13. package/src/dashboard-command.ts +38 -19
  14. package/src/efficiency-config-core.ts +287 -484
  15. package/src/efficiency-store.ts +16 -29
  16. package/src/focus-poller.ts +14 -35
  17. package/src/gc-core.ts +10 -36
  18. package/src/herdr-client.ts +82 -208
  19. package/src/history-store.ts +36 -23
  20. package/src/index-gates.ts +124 -0
  21. package/src/index-locks.ts +2 -7
  22. package/src/index-master.ts +24 -18
  23. package/src/index-notices.ts +28 -15
  24. package/src/index-pipe.ts +17 -12
  25. package/src/index-roles.ts +306 -0
  26. package/src/index-runtime.ts +2 -6
  27. package/src/index-worker.ts +4 -8
  28. package/src/index.ts +305 -564
  29. package/src/jev-client.ts +12 -17
  30. package/src/jev-core.ts +137 -87
  31. package/src/ledger.ts +13 -21
  32. package/src/lock-core.ts +28 -56
  33. package/src/manifest-compose.ts +35 -20
  34. package/src/observation-core.ts +52 -100
  35. package/src/pane-title.ts +19 -38
  36. package/src/pi-surface.ts +34 -22
  37. package/src/pier-options.ts +54 -50
  38. package/src/pipe-channel.ts +27 -59
  39. package/src/platform-paths.ts +19 -52
  40. package/src/{core → plugins}/grid-shape.ts +8 -10
  41. package/src/{core → plugins}/heat-plan.ts +20 -22
  42. package/src/plugins/observation.ts +475 -0
  43. package/src/plugins/subagent.ts +728 -0
  44. package/src/{core → plugins}/terminal.ts +167 -127
  45. package/src/{core → plugins}/todo.ts +61 -100
  46. package/src/progress-core.ts +16 -53
  47. package/src/reconcile-core.ts +24 -33
  48. package/src/reducer-core.ts +24 -60
  49. package/src/reducer-invoker.ts +266 -239
  50. package/src/renderers.ts +76 -87
  51. package/src/role-loader.ts +33 -28
  52. package/src/role-manifest.ts +23 -28
  53. package/src/role-state.ts +119 -0
  54. package/src/routing-telemetry.ts +136 -0
  55. package/src/runtime-policy.ts +25 -40
  56. package/src/session-tail.ts +65 -41
  57. package/src/settle-wake-core.ts +52 -21
  58. package/src/slim-frame.ts +25 -122
  59. package/src/stale-core.ts +17 -24
  60. package/src/storage-layout.ts +14 -43
  61. package/src/subagent-core.ts +253 -118
  62. package/src/subagent-gc.ts +109 -67
  63. package/src/subagent-poller.ts +367 -24
  64. package/src/subagent-session.ts +516 -0
  65. package/src/subagent-spawn.ts +530 -52
  66. package/src/swallow.ts +7 -17
  67. package/src/terminal-core.ts +125 -106
  68. package/src/todo-core.ts +57 -109
  69. package/src/todo-read-hook.ts +23 -39
  70. package/src/todo-reminder-core.ts +26 -51
  71. package/src/todo-window.ts +11 -12
  72. package/src/todos-service.ts +15 -35
  73. package/src/tool-error.ts +6 -13
  74. package/src/tool-gate.ts +38 -21
  75. package/src/vocab.ts +52 -25
  76. package/src/ask-multi.ts +0 -260
  77. package/src/config-guide.ts +0 -361
  78. package/src/core/observation.ts +0 -555
  79. package/src/core/subagent.ts +0 -633
  80. package/src/efficiency-config-io.ts +0 -53
  81. package/src/git-adapter.ts +0 -113
  82. package/src/notice-buffer.ts +0 -23
  83. package/src/subagent-execute.ts +0 -328
  84. package/src/subagent-git-io.ts +0 -80
  85. package/src/subagent-launch.ts +0 -114
  86. package/src/subagent-list-action.ts +0 -69
  87. package/src/subagent-output-action.ts +0 -104
  88. package/src/subagent-output-adapter.ts +0 -27
  89. package/src/subagent-output-core.ts +0 -263
  90. package/src/subagent-poll-loop.ts +0 -305
  91. package/src/subagent-port.ts +0 -27
  92. package/src/subagent-ready.ts +0 -89
  93. package/src/subagent-registry.ts +0 -70
  94. package/src/subagent-resolution.ts +0 -41
  95. package/src/subagent-scope.ts +0 -41
  96. package/src/subagent-session-io.ts +0 -230
@@ -1,361 +0,0 @@
1
- /**
2
- * D104 Config Guide adapter.
3
- *
4
- * Thin I/O layer over config-catalog-core: reads the real files/env for the five
5
- * configuration planes, aggregates validation issues, and renders the strings used by
6
- * `/pier-config show|check|doc`. All paths are injectable so tests never touch the
7
- * developer's real home directory.
8
- *
9
- * Fail-open: every read is wrapped; a broken plane degrades to a reported issue rather
10
- * than throwing into pi's command pipeline.
11
- */
12
-
13
- import { existsSync, readFileSync, readdirSync } from 'node:fs';
14
- import { homedir } from 'node:os';
15
- import { dirname, join, resolve } from 'node:path';
16
- import { fileURLToPath } from 'node:url';
17
- import {
18
- CONFIG_PLANES,
19
- checkEnvKnobs,
20
- formatValue,
21
- readDotted,
22
- renderCheck,
23
- renderIndex,
24
- renderPlane,
25
- renderReport,
26
- resolveConfigKnobs,
27
- resolveEnvKnobs,
28
- type CheckReport,
29
- type ConfigPlaneId,
30
- type ResolvedKnob,
31
- } from './config-catalog-core.ts';
32
- import {
33
- defaultUserEfficiencyConfigFile,
34
- defaultWorkspaceEfficiencyConfigFile,
35
- validateEfficiencyConfig,
36
- } from './efficiency-config-core.ts';
37
- import { loadPiNativeCompactionSettings } from './efficiency-config-io.ts';
38
- import { RESERVED_ROLE_NAMES, loadRoleConfig, roleLayers } from './role-loader.ts';
39
-
40
- export const CONFIG_GUIDANCE_PROMPT = [
41
- '[PIER-CONFIG] The user wants to inspect or change pier configuration. Follow this workflow strictly:',
42
- '',
43
- '1. Ground yourself first: run `/pier-config show all` (or read the files) and use the reported EFFECTIVE VALUE and SOURCE',
44
- ' (env > workspace > user > default). Only entries whose source is `default` are worth changing; an ignored workspace file',
45
- ' (untrusted project) must be reported as such instead of edited blindly.',
46
- '2. Route by plane:',
47
- ' - efficiency (D100-D103): read `docs/efficiency-trial.md` before proposing values.',
48
- ' - roles: read `docs/sidebar-role-config.md` and `schemas/role-manifest.schema.json`; builtin role names cannot be overridden.',
49
- " - pi (settings.json): recommend pi's own `/settings`; the only OCC-relevant keys here are `compaction.enabled` and",
50
- ' `compaction.keepRecentTokens` (read-only from our side).',
51
- ' - boot (boot-config.json): manual edits are risky; prefer `npx pier-setup@latest update --force`, and only patch paths when asked.',
52
- ' - env (PIER_* / PI_HERDR_*): affects new processes only; state that explicitly.',
53
- '3. Explain before changing: 2-3 sentences on what the knob controls, its cost/benefit (tokens, latency, safety, blast radius),',
54
- ' and 2-3 recommended values for common scenarios. Then ask what the user actually wants to achieve.',
55
- '4. Before writing: show a precise diff (file path, current -> proposed value) and the activation path',
56
- ' (hot / needs `/reload` / needs a new session or process restart). Wait for explicit confirmation. Change ONE plane at a time.',
57
- '5. Apply with the normal `edit`/`write` tools (write locks apply), then run `/pier-config check` and report the result back.',
58
- '6. If a change cannot take effect in the current process, say so and tell the user exactly what to restart.',
59
- '',
60
- 'Forbidden: dumping `process.env`; writing secrets or tokens into any config file; editing multiple planes before confirmation;',
61
- 'touching `.pi-herdr/` of an untrusted project.',
62
- ].join('\n');
63
-
64
- export interface ConfigPlaneFile {
65
- readonly path: string;
66
- readonly label: string;
67
- readonly exists: boolean;
68
- readonly ignored?: boolean;
69
- }
70
-
71
- export interface ConfigGuideDeps {
72
- cwd?: string;
73
- env?: Record<string, string | undefined>;
74
- isProjectTrusted?: boolean;
75
- /** Pi agent dir holding settings.json (defaults to ~/.pi/agent; mirrors PI_CODING_AGENT_DIR). */
76
- agentDir?: string;
77
- /** User-level efficiency config path (defaults to ~/.pi/agent/herdr-pi/config.json). */
78
- userConfigPath?: string;
79
- /** herdr plugin config dir holding the user-mode boot-config.json. */
80
- herdrPluginConfigDir?: string;
81
- /** Repository root used to probe the dev-mode boot-config.json. */
82
- repoRoot?: string;
83
- }
84
-
85
- export interface ConfigGuideSnapshot {
86
- readonly entries: readonly ResolvedKnob[];
87
- readonly reports: readonly CheckReport[];
88
- readonly files: Readonly<Record<ConfigPlaneId, readonly ConfigPlaneFile[]>>;
89
- readonly workspaceTrusted: boolean;
90
- readonly roleSummary: string;
91
- readonly bootSummary: string;
92
- }
93
-
94
- function defaultAgentDir(env: Record<string, string | undefined>): string {
95
- return env.PI_CODING_AGENT_DIR ?? join(homedir(), '.pi', 'agent');
96
- }
97
-
98
- /** Repo root of this checkout (works in-repo; in node_modules the probe simply finds nothing). */
99
- function defaultRepoRoot(): string {
100
- try {
101
- const here = dirname(fileURLToPath(import.meta.url));
102
- return resolve(here, '..', '..', '..');
103
- } catch {
104
- return process.cwd();
105
- }
106
- }
107
-
108
- /** Standard herdr plugin config dirs: XDG config on macOS/Linux, LOCALAPPDATA on Windows. */
109
- export function defaultHerdrPluginConfigDirs(env: Record<string, string | undefined> = process.env): string[] {
110
- const out: string[] = [];
111
- const xdg = env.XDG_CONFIG_HOME && env.XDG_CONFIG_HOME.trim() !== '' ? env.XDG_CONFIG_HOME : join(homedir(), '.config');
112
- out.push(join(xdg, 'herdr', 'plugins', 'config', 'pier.workbench'));
113
- if (env.LOCALAPPDATA && env.LOCALAPPDATA.trim() !== '') {
114
- out.push(join(env.LOCALAPPDATA, 'herdr', 'plugins', 'config', 'pier.workbench'));
115
- }
116
- return out;
117
- }
118
-
119
- function readJsonLayer(path: string): { value: unknown; issue?: string } {
120
- if (!existsSync(path)) return { value: undefined };
121
- try {
122
- return { value: JSON.parse(readFileSync(path, 'utf8')) };
123
- } catch (err) {
124
- return { value: undefined, issue: `${path}: invalid JSON (${err instanceof Error ? err.message : String(err)})` };
125
- }
126
- }
127
-
128
- function listJsonFiles(dir: string): string[] {
129
- try {
130
- return readdirSync(dir).filter((f) => f.endsWith('.json')).sort();
131
- } catch {
132
- return [];
133
- }
134
- }
135
-
136
- /** Collects the whole snapshot for the guide/command. */
137
- export function collectConfigSnapshot(deps: ConfigGuideDeps = {}): ConfigGuideSnapshot {
138
- const env = deps.env ?? process.env;
139
- const cwd = deps.cwd ?? process.cwd();
140
- const workspaceTrusted = deps.isProjectTrusted ?? false;
141
- const agentDir = deps.agentDir ?? defaultAgentDir(env);
142
- const userConfigPath = deps.userConfigPath ?? defaultUserEfficiencyConfigFile();
143
- const workspaceConfigPath = defaultWorkspaceEfficiencyConfigFile(cwd);
144
- const repoRoot = deps.repoRoot ?? defaultRepoRoot();
145
- const herdrPluginConfigDir = deps.herdrPluginConfigDir ?? env.HERDR_PLUGIN_CONFIG_DIR;
146
-
147
- const reports: CheckReport[] = [];
148
- const files: Record<ConfigPlaneId, ConfigPlaneFile[]> = {
149
- efficiency: [],
150
- roles: [],
151
- pi: [],
152
- boot: [],
153
- env: [],
154
- };
155
-
156
- /* ── efficiency ───────────────────────────────────────────────────────── */
157
- const workspaceLayer = readJsonLayer(workspaceConfigPath);
158
- const userLayer = readJsonLayer(userConfigPath);
159
- const efficiencyIssues: string[] = [];
160
- if (workspaceLayer.issue) efficiencyIssues.push(workspaceLayer.issue);
161
- if (userLayer.issue) efficiencyIssues.push(userLayer.issue);
162
-
163
- if (workspaceLayer.value !== undefined) {
164
- if (!workspaceTrusted) {
165
- efficiencyIssues.push(`${workspaceConfigPath}: ignored — the project is not trusted (values below come from user/default)`);
166
- } else {
167
- const validated = validateEfficiencyConfig(workspaceLayer.value);
168
- for (const issue of validated.issues) efficiencyIssues.push(`${workspaceConfigPath}: ${issue}`);
169
- }
170
- }
171
- if (userLayer.value !== undefined) {
172
- const validated = validateEfficiencyConfig(userLayer.value);
173
- for (const issue of validated.issues) efficiencyIssues.push(`${userConfigPath}: ${issue}`);
174
- }
175
-
176
- files.efficiency.push(
177
- { path: workspaceConfigPath, label: 'workspace', exists: workspaceLayer.value !== undefined, ignored: !workspaceTrusted },
178
- { path: userConfigPath, label: 'user', exists: userLayer.value !== undefined },
179
- );
180
-
181
- /* ── pi settings ──────────────────────────────────────────────────────── */
182
- let piSettings: { enabled?: boolean; keepRecentTokens?: number } = {};
183
- const piSettingsPath = join(agentDir, 'settings.json');
184
- try {
185
- piSettings = loadPiNativeCompactionSettings({ cwd, isProjectTrusted: workspaceTrusted, agentDir });
186
- } catch {
187
- piSettings = {};
188
- }
189
- const piIssues: string[] = [];
190
- if (!existsSync(piSettingsPath)) {
191
- piIssues.push(`${piSettingsPath}: not found (pi defaults apply; use pi's /settings to create it)`);
192
- }
193
- files.pi.push({ path: piSettingsPath, label: 'agent', exists: existsSync(piSettingsPath) });
194
-
195
- /* ── env ──────────────────────────────────────────────────────────────── */
196
- const envIssues = checkEnvKnobs(env);
197
- files.env.push({ path: '(process environment)', label: 'env', exists: true });
198
-
199
- /* ── roles ────────────────────────────────────────────────────────────── */
200
- const layerDirs = roleLayers({ baseDir: cwd });
201
- const roleIssues: string[] = [];
202
- const roleNames = new Set<string>();
203
- const customLayerNames = new Set<string>();
204
- const layerCounts: string[] = [];
205
- for (const layer of layerDirs) {
206
- const names = listJsonFiles(layer.dir);
207
- const kind: ConfigPlaneFile['label'] = layer.label.startsWith('workspace')
208
- ? 'workspace'
209
- : layer.label.startsWith('user') ? 'user' : 'builtin';
210
- files.roles.push({ path: layer.dir, label: kind, exists: names.length > 0 });
211
- layerCounts.push(`${kind} ${names.length}`);
212
- for (const file of names) {
213
- const name = file.replace(/\.json$/, '');
214
- roleNames.add(name);
215
- if (kind !== 'builtin') customLayerNames.add(name);
216
- }
217
- }
218
- for (const name of [...roleNames].sort()) {
219
- try {
220
- loadRoleConfig(name, { baseDir: cwd });
221
- } catch (err) {
222
- const issues = (err as { issues?: readonly string[] }).issues ?? [];
223
- roleIssues.push(`role "${name}": ${err instanceof Error ? err.message : String(err)}${issues.length ? ` — ${issues.join('; ')}` : ''}`);
224
- }
225
- }
226
- // Built-in names come from the bundled layer by design; only custom layers are suspicious.
227
- const reservedPresent = [...customLayerNames].filter((n) => RESERVED_ROLE_NAMES.includes(n));
228
- if (reservedPresent.length > 0) {
229
- roleIssues.push(`reserved role name(s) shadowed in a custom layer (ignored for built-ins): ${reservedPresent.join(', ')}`);
230
- }
231
- const roleSummary = `${roleNames.size} role(s): ${layerCounts.join(' / ')}`;
232
-
233
- /* ── boot-config ──────────────────────────────────────────────────────── */
234
- const bootIssues: string[] = [];
235
- const bootCandidates: Array<{ path: string; label: string }> = [];
236
- if (herdrPluginConfigDir) bootCandidates.push({ path: join(herdrPluginConfigDir, 'boot-config.json'), label: 'herdr plugin config-dir' });
237
- // herdr does not always export HERDR_PLUGIN_CONFIG_DIR into the pi process, so production also
238
- // probes the standard plugin config locations (the same file `install.mjs` writes in user mode).
239
- // Tests inject env/herdrPluginConfigDir and therefore stay hermetic.
240
- const probeDefaultDirs = deps.herdrPluginConfigDir === undefined && deps.env === undefined;
241
- if (probeDefaultDirs) {
242
- for (const dir of defaultHerdrPluginConfigDirs(env)) {
243
- bootCandidates.push({ path: join(dir, 'boot-config.json'), label: 'herdr plugin config (default)' });
244
- }
245
- }
246
- bootCandidates.push({ path: join(repoRoot, 'packages', 'pier-workbench', 'scripts', 'boot-config.json'), label: 'dev (repo)' });
247
- let bootFound: { path: string; label: string } | null = null;
248
- for (const candidate of bootCandidates) {
249
- const exists = existsSync(candidate.path);
250
- files.boot.push({ path: candidate.path, label: candidate.label, exists });
251
- if (exists && !bootFound) bootFound = candidate;
252
- }
253
- if (!bootFound) {
254
- bootIssues.push(`boot-config.json not found in: ${bootCandidates.map((c) => c.path).join(' , ')} (run \`npx pier-setup@latest install\`)`);
255
- } else {
256
- const parsed = readJsonLayer(bootFound.path);
257
- if (parsed.issue) {
258
- bootIssues.push(parsed.issue);
259
- } else {
260
- for (const key of ['piNode', 'piCli', 'extPath'] as const) {
261
- const value = readDotted(parsed.value, key);
262
- if (typeof value !== 'string' || value === '') {
263
- bootIssues.push(`${bootFound.path}: missing required key "${key}"`);
264
- continue;
265
- }
266
- // A stale absolute path is the most common post-reinstall breakage (e.g. an npm
267
- // install path that no longer exists while pi still loads the extension elsewhere).
268
- if (!existsSync(value)) {
269
- bootIssues.push(
270
- `${bootFound.path}: "${key}" points to a path that does not exist: ${value} (stale after a reinstall? re-run \`npx pier-setup@latest update --force\`)`,
271
- );
272
- }
273
- }
274
- }
275
- }
276
- const bootSummary = bootFound ? `present (${bootFound.label})` : 'missing (run pier-setup)';
277
-
278
- /* ── resolution + reports ─────────────────────────────────────────────── */
279
- const entries = [
280
- ...resolveConfigKnobs({ env, workspace: workspaceLayer.value, user: userLayer.value, workspaceTrusted, piSettings }),
281
- ...resolveEnvKnobs(env),
282
- ];
283
-
284
- reports.push({ plane: 'efficiency', ok: efficiencyIssues.length === 0, issues: efficiencyIssues });
285
- reports.push({ plane: 'roles', ok: roleIssues.length === 0, issues: roleIssues });
286
- reports.push({ plane: 'pi', ok: true, issues: piIssues });
287
- reports.push({ plane: 'boot', ok: bootIssues.length === 0, issues: bootIssues });
288
- reports.push({ plane: 'env', ok: envIssues.length === 0, issues: envIssues });
289
-
290
- return { entries, reports, files, workspaceTrusted, roleSummary, bootSummary };
291
- }
292
-
293
- /* ── rendering used by the command ───────────────────────────────────────── */
294
-
295
- export function guideIndexLines(snapshot: ConfigGuideSnapshot): string[] {
296
- const pick = (plane: ConfigPlaneId, prefix: string): readonly ResolvedKnob[] =>
297
- snapshot.entries.filter((e) => e.knob.plane === plane && e.knob.key.startsWith(prefix));
298
- return renderIndex({
299
- efficiency: pick('efficiency', ''),
300
- pi: pick('pi', ''),
301
- env: pick('env', ''),
302
- roleSummary: snapshot.roleSummary,
303
- bootSummary: snapshot.bootSummary,
304
- });
305
- }
306
-
307
- export function guidePlaneLines(snapshot: ConfigGuideSnapshot, plane: ConfigPlaneId | 'all'): string[] {
308
- const planes = plane === 'all' ? CONFIG_PLANES.map((p) => p.id) : [plane];
309
- const out: string[] = [];
310
- for (const id of planes) {
311
- const meta = CONFIG_PLANES.find((p) => p.id === id);
312
- if (!meta) continue;
313
- out.push(`${id} — ${meta.title}`);
314
- for (const file of snapshot.files[id]) {
315
- const status = file.exists ? (file.ignored ? 'present (IGNORED: untrusted project)' : 'present') : 'absent';
316
- out.push(` file [${file.label}] ${file.path} — ${status}`);
317
- }
318
- out.push(` hint: ${meta.editHint}`);
319
- const entries = snapshot.entries.filter((e) => e.knob.plane === id);
320
- if (entries.length > 0) out.push(...renderPlane(id, entries));
321
- else out.push(` (per-file plane: ${entrySummaryForPlane(snapshot, id)})`);
322
- out.push('');
323
- }
324
- return out;
325
- }
326
-
327
- function entrySummaryForPlane(snapshot: ConfigGuideSnapshot, plane: ConfigPlaneId): string {
328
- const report = snapshot.reports.find((r) => r.plane === plane);
329
- if (!report) return 'no data';
330
- return report.ok ? 'no issues' : `${report.issues.length} issue(s) — see /pier-config check`;
331
- }
332
-
333
- export function guideCheckLines(snapshot: ConfigGuideSnapshot): string[] {
334
- const lines = renderCheck(snapshot.reports);
335
- const anyFail = snapshot.reports.some((r) => !r.ok);
336
- lines.push(anyFail ? ' → fix the FAIL planes above (values are still shown by `show`)' : ' → all planes look consistent');
337
- return lines;
338
- }
339
-
340
- export function guideReportMarkdown(snapshot: ConfigGuideSnapshot, meta: { generatedAt: string; cwd: string; piVersion?: string }): string {
341
- const body = renderReport(snapshot.entries, {
342
- generatedAt: meta.generatedAt,
343
- cwd: meta.cwd,
344
- workspaceTrusted: snapshot.workspaceTrusted,
345
- piVersion: meta.piVersion,
346
- });
347
- const filesSection = ['', '## Files observed', ''];
348
- for (const plane of CONFIG_PLANES) {
349
- for (const file of snapshot.files[plane.id]) {
350
- filesSection.push(`- [${plane.id}] ${file.label}: \`${file.path}\` — ${file.exists ? (file.ignored ? 'present, ignored (untrusted project)' : 'present') : 'absent'}`);
351
- }
352
- }
353
- const checkSection = ['', '## Checks', '', ...guideCheckLines(snapshot).map((l) => `- ${l.trim()}`)];
354
- return `${body}\n${filesSection.join('\n')}\n${checkSection.join('\n')}\n`;
355
- }
356
-
357
- /** Small helper used by tests and `show` for reading a single knob. */
358
- export function knobValue(snapshot: ConfigGuideSnapshot, plane: ConfigPlaneId, key: string): string {
359
- const entry = snapshot.entries.find((e) => e.knob.plane === plane && e.knob.key === key);
360
- return entry ? `${entry.value} [${entry.source}]` : formatValue(undefined);
361
- }