pi-pier 0.3.0 → 0.4.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 (103) hide show
  1. package/README.md +16 -12
  2. package/package.json +3 -3
  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 +10 -13
  9. package/src/compact-coordinator.ts +279 -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/cordis-services.ts +64 -0
  14. package/src/dashboard-command.ts +77 -33
  15. package/src/delegation-core.ts +115 -0
  16. package/src/efficiency-config-core.ts +286 -486
  17. package/src/efficiency-store.ts +16 -29
  18. package/src/gc-core.ts +10 -36
  19. package/src/guards.ts +6 -0
  20. package/src/herdr-client.ts +202 -233
  21. package/src/history-store.ts +38 -23
  22. package/src/index-gates.ts +118 -0
  23. package/src/index-locks.ts +2 -7
  24. package/src/index-master.ts +31 -29
  25. package/src/index-notices.ts +28 -15
  26. package/src/index-pipe.ts +31 -12
  27. package/src/index-roles.ts +306 -0
  28. package/src/index-runtime.ts +16 -6
  29. package/src/index-worker.ts +8 -18
  30. package/src/index.ts +561 -570
  31. package/src/jev-client.ts +12 -17
  32. package/src/jev-core.ts +138 -90
  33. package/src/ledger.ts +13 -21
  34. package/src/lock-core.ts +28 -56
  35. package/src/manifest-compose.ts +35 -20
  36. package/src/observation-core.ts +52 -100
  37. package/src/pane-title.ts +19 -38
  38. package/src/panel-open.ts +47 -0
  39. package/src/pi-surface.ts +34 -22
  40. package/src/pier-options.ts +53 -50
  41. package/src/pipe-channel.ts +44 -59
  42. package/src/platform-paths.ts +19 -52
  43. package/src/{core → plugins}/grid-shape.ts +8 -10
  44. package/src/{core → plugins}/heat-plan.ts +20 -22
  45. package/src/plugins/observation.ts +475 -0
  46. package/src/plugins/subagent.ts +766 -0
  47. package/src/{core → plugins}/terminal.ts +198 -139
  48. package/src/{core → plugins}/todo.ts +73 -134
  49. package/src/progress-core.ts +16 -53
  50. package/src/reconcile-core.ts +24 -33
  51. package/src/reducer-core.ts +26 -64
  52. package/src/reducer-invoker.ts +266 -239
  53. package/src/renderers.ts +108 -84
  54. package/src/role-loader.ts +33 -28
  55. package/src/role-manifest.ts +25 -31
  56. package/src/role-state.ts +119 -0
  57. package/src/roles/worker-default.json +3 -3
  58. package/src/routing-telemetry.ts +136 -0
  59. package/src/runtime-policy.ts +25 -40
  60. package/src/session-tail.ts +65 -41
  61. package/src/settle-wake-core.ts +61 -22
  62. package/src/slim-frame.ts +25 -122
  63. package/src/stale-core.ts +17 -24
  64. package/src/storage-layout.ts +14 -43
  65. package/src/subagent-core.ts +423 -120
  66. package/src/subagent-gc.ts +109 -67
  67. package/src/subagent-poller.ts +383 -24
  68. package/src/subagent-session.ts +516 -0
  69. package/src/subagent-spawn.ts +577 -52
  70. package/src/swallow.ts +7 -17
  71. package/src/terminal-core.ts +187 -134
  72. package/src/todo-core.ts +57 -109
  73. package/src/todo-read-hook.ts +46 -39
  74. package/src/todo-reminder-core.ts +26 -51
  75. package/src/todo-window.ts +11 -12
  76. package/src/todos-service.ts +15 -35
  77. package/src/tool-error.ts +6 -13
  78. package/src/tool-gate.ts +38 -21
  79. package/src/tree-view.ts +162 -0
  80. package/src/vocab.ts +52 -25
  81. package/src/worker-marker.ts +76 -0
  82. package/src/ask-multi.ts +0 -260
  83. package/src/config-guide.ts +0 -361
  84. package/src/core/observation.ts +0 -555
  85. package/src/core/subagent.ts +0 -633
  86. package/src/efficiency-config-io.ts +0 -53
  87. package/src/focus-poller.ts +0 -244
  88. package/src/git-adapter.ts +0 -113
  89. package/src/notice-buffer.ts +0 -23
  90. package/src/subagent-execute.ts +0 -328
  91. package/src/subagent-git-io.ts +0 -80
  92. package/src/subagent-launch.ts +0 -114
  93. package/src/subagent-list-action.ts +0 -69
  94. package/src/subagent-output-action.ts +0 -104
  95. package/src/subagent-output-adapter.ts +0 -27
  96. package/src/subagent-output-core.ts +0 -263
  97. package/src/subagent-poll-loop.ts +0 -305
  98. package/src/subagent-port.ts +0 -27
  99. package/src/subagent-ready.ts +0 -89
  100. package/src/subagent-registry.ts +0 -70
  101. package/src/subagent-resolution.ts +0 -41
  102. package/src/subagent-scope.ts +0 -41
  103. package/src/subagent-session-io.ts +0 -230
@@ -1,17 +1,15 @@
1
1
  /**
2
- * D104 Config Catalog Core (pure).
2
+ * D104 config catalog core (pure).
3
3
  *
4
- * Single source of truth for the five pier configuration planes: knob descriptors,
5
- * effective-value provenance (env > workspace > user > default) and text rendering
6
- * for `/pier-config show|check|doc`.
7
- *
8
- * No I/O here; file/env reading lives in config-guide.ts. The catalog is guarded
9
- * against drift by test/config-catalog-core.test.ts, which diffs it against
10
- * schemas/efficiency-config.schema.json, schemas/role-manifest.schema.json and the
11
- * env keys actually read by runtime-policy.ts / efficiency-config-core.ts.
4
+ * Single source of truth for the four pier configuration planes: knob descriptors, effective-value
5
+ * provenance (env > workspace > user > default) and rendering for `/pier-config show|check|doc`. The
6
+ * `env` plane derives from the `PIER_OPTIONS` registry, so it cannot disagree with the runtime readers;
7
+ * no I/O here (config-command.ts does the reading) and test/config-catalog.test.ts guards the mirror.
12
8
  */
13
9
 
14
- export type ConfigPlaneId = 'efficiency' | 'roles' | 'pi' | 'boot' | 'env';
10
+ import { PIER_OPTIONS } from './pier-options.ts';
11
+
12
+ export type ConfigPlaneId = 'efficiency' | 'roles' | 'pi' | 'env';
15
13
 
16
14
  export type ConfigKind = 'boolean' | 'number' | 'string' | 'enum';
17
15
 
@@ -27,9 +25,7 @@ export interface ConfigKnob {
27
25
  readonly min?: number;
28
26
  /** Historical spellings still accepted for an env knob (B10: canonical `PIER_*` first). */
29
27
  readonly aliases?: readonly string[];
30
- /** One-line effect/impact, shown by `show`. */
31
28
  readonly impact: string;
32
- /** Where to read more (docs/schema anchor). */
33
29
  readonly docRef?: string;
34
30
  /** True when pier only reads the value (owned by pi / herdr). */
35
31
  readonly readOnly?: boolean;
@@ -38,10 +34,9 @@ export interface ConfigKnob {
38
34
  export interface ConfigPlane {
39
35
  readonly id: ConfigPlaneId;
40
36
  readonly title: string;
41
- readonly owner: 'pier' | 'pi' | 'workbench';
37
+ readonly owner: 'pier' | 'pi';
42
38
  /** Human-facing path templates, in precedence order. */
43
39
  readonly files: readonly string[];
44
- /** What to do to change this plane. */
45
40
  readonly editHint: string;
46
41
  }
47
42
 
@@ -67,13 +62,6 @@ export const CONFIG_PLANES: readonly ConfigPlane[] = Object.freeze([
67
62
  files: ['~/.pi/agent/settings.json', '<trusted-project>/.pi/settings.json'],
68
63
  editHint: 'Use pi\'s /settings for everything except the OCC-relevant compaction.* keys',
69
64
  },
70
- {
71
- id: 'boot',
72
- title: 'Workbench boot-config',
73
- owner: 'workbench',
74
- files: ['$HERDR_PLUGIN_CONFIG_DIR/boot-config.json', 'packages/pier-workbench/scripts/boot-config.json'],
75
- editHint: 'Prefer `npx pier-setup@latest update --force` over hand-editing paths',
76
- },
77
65
  {
78
66
  id: 'env',
79
67
  title: 'Runtime policy env (PIER_* / PI_HERDR_*)',
@@ -83,7 +71,7 @@ export const CONFIG_PLANES: readonly ConfigPlane[] = Object.freeze([
83
71
  },
84
72
  ] as const);
85
73
 
86
- /** Efficiency knobs: mirrors schemas/efficiency-config.schema.json (guarded by the drift test). */
74
+ /** Efficiency knobs: mirrors schemas/efficiency-config.schema.json (guarded by the catalog test). */
87
75
  const EFFICIENCY_KNOBS: readonly ConfigKnob[] = [
88
76
  { plane: 'efficiency', key: 'onlineContextCompact.enabled', kind: 'boolean', defaultValue: false, envVar: 'PI_HERDR_COMPACT_ENABLE', impact: 'Todo-driven online compaction (OCC) master switch', docRef: 'docs/efficiency-trial.md' },
89
77
  { plane: 'efficiency', key: 'onlineContextCompact.logEnabled', kind: 'boolean', defaultValue: false, envVar: 'PI_HERDR_COMPACT_LOG', impact: 'Write compact.jsonl decisions', docRef: 'docs/efficiency-trial.md' },
@@ -114,80 +102,29 @@ const EFFICIENCY_KNOBS: readonly ConfigKnob[] = [
114
102
  { plane: 'efficiency', key: 'jev.apiKey', kind: 'string', envVar: 'PIER_JEV_API_KEY', impact: 'API key; PIER_JEV_API_KEY env > config value > TYPESAFE_API_KEY env', docRef: 'docs/rfc-jev-integration.md' },
115
103
  ];
116
104
 
117
- /** Role-manifest keys (mirrors schemas/role-manifest.schema.json). Roles are per-file, so these are inventory keys. */
118
- const ROLE_KNOBS: readonly ConfigKnob[] = [
119
- { plane: 'roles', key: 'role', kind: 'string', impact: 'Role name (must match the file name)', docRef: 'docs/sidebar-role-config.md' },
120
- { plane: 'roles', key: 'version', kind: 'string', impact: 'Role profile version string', docRef: 'docs/sidebar-role-config.md' },
121
- { plane: 'roles', key: 'model', kind: 'string', impact: 'Dispatch routing: provider/model for panes spawned with this role', docRef: 'docs/sidebar-role-config.md' },
122
- { plane: 'roles', key: 'description', kind: 'string', impact: 'Human description shown in role listings', docRef: 'docs/sidebar-role-config.md' },
123
- { plane: 'roles', key: 'manifest.tools', kind: 'string', impact: 'Visible tool names for the role', docRef: 'docs/sidebar-role-config.md' },
124
- { plane: 'roles', key: 'manifest.rules', kind: 'string', impact: 'Permission map: tool name → allow|ask|deny', docRef: 'docs/sidebar-role-config.md' },
125
- { plane: 'roles', key: 'manifest.unknownTools', kind: 'enum', impact: 'Stance for tools missing from manifest.tools: allow|deny', docRef: 'docs/sidebar-role-config.md' },
126
- { plane: 'roles', key: 'services.todos', kind: 'string', impact: 'Todo service mode for the role: serial|parallel', docRef: 'docs/sidebar-role-config.md' },
127
- ];
128
-
129
105
  /** Pi-owned keys pier only reads. */
130
106
  const PI_KNOBS: readonly ConfigKnob[] = [
131
107
  { plane: 'pi', key: 'compaction.enabled', kind: 'boolean', impact: 'When false, OCC is disabled too (unless PI_HERDR_COMPACT_ENABLE=1)', docRef: 'docs/efficiency-trial.md', readOnly: true },
132
108
  { plane: 'pi', key: 'compaction.keepRecentTokens', kind: 'number', impact: 'Inherited as the OCC retention window when not set in the efficiency config', docRef: 'docs/efficiency-trial.md', readOnly: true },
133
109
  ];
134
110
 
135
- /** Workbench boot-config keys (template: packages/pier-workbench/scripts/boot-config.example.json). */
136
- const BOOT_KNOBS: readonly ConfigKnob[] = [
137
- { plane: 'boot', key: 'mainTabLabel', kind: 'string', impact: 'Label of the main tab the workbench keeps', readOnly: true },
138
- { plane: 'boot', key: 'piNode', kind: 'string', impact: 'Absolute node binary used to launch pi', readOnly: true },
139
- { plane: 'boot', key: 'piCli', kind: 'string', impact: 'Absolute path to pi\'s cli.js', readOnly: true },
140
- { plane: 'boot', key: 'extPath', kind: 'string', impact: 'Absolute path to the pier extension entry (index.ts)', readOnly: true },
141
- { plane: 'boot', key: 'workbenchPluginId', kind: 'string', impact: 'herdr plugin id the boot script drives', readOnly: true },
142
- { plane: 'boot', key: 'hmrDev', kind: 'boolean', impact: 'Enable hot-reload wiring for local development', readOnly: true },
143
- ];
144
-
145
- /**
146
- * Runtime policy / behaviour env knobs. Bounds mirror runtime-policy.ts (parseEnvInt) and
147
- * the readers in terminal-core.ts / todo-reminder-core.ts / pi-surface bootstrap.
148
- */
149
- const ENV_KNOBS: readonly ConfigKnob[] = [
150
- { plane: 'env', key: 'PIER_SUBAGENT_TIMEOUT_MS', aliases: ['PI_HERDR_SUBAGENT_TIMEOUT_MS'], kind: 'number', defaultValue: 600000, min: 1000, impact: 'Subagent inactivity budget before forced termination' },
151
- { plane: 'env', key: 'PIER_GC_TICK_MS', aliases: ['PI_HERDR_GC_TICK_MS'], kind: 'number', defaultValue: 30000, min: 1000, impact: 'Subagent GC ticker interval' },
152
- { plane: 'env', key: 'PIER_POLL_INTERVAL_MS', aliases: ['PI_HERDR_POLL_INTERVAL_MS'], kind: 'number', defaultValue: 30000, min: 1000, impact: 'Subagent state observation poll interval' },
153
- { plane: 'env', key: 'PIER_SETTLEMENT_WINDOW_MS', aliases: ['PI_HERDR_SETTLEMENT_WINDOW_MS'], kind: 'number', defaultValue: 60000, min: 0, impact: 'Settlement notice window / machine-inject grace / takeover idle' },
154
- { plane: 'env', key: 'PIER_OBSERVATION_WINDOW_MS', aliases: ['PI_HERDR_OBSERVATION_WINDOW_MS'], kind: 'number', defaultValue: 30000, min: 0, impact: 'Post-settle observation window before auto-consume' },
155
- { plane: 'env', key: 'PIER_FOREGROUND_PATIENCE_MS', aliases: ['PI_HERDR_FOREGROUND_PATIENCE_MS'], kind: 'number', defaultValue: 300000, min: 0, impact: 'Foreground patience before demoting a subagent to background' },
156
- { plane: 'env', key: 'PIER_SESSION_TTL_SECONDS', aliases: ['PI_HERDR_SESSION_TTL_SECONDS'], kind: 'number', defaultValue: 600, min: 0, impact: 'Session retention after subagent exit before GC' },
157
- { plane: 'env', key: 'PIER_GIT_TIMEOUT_MS', aliases: ['PI_HERDR_GIT_TIMEOUT_MS'], kind: 'number', defaultValue: 10000, min: 1, impact: 'git worktree/diff/cleanup execution timeout' },
158
- { plane: 'env', key: 'PIER_READY_TIMEOUT_MS', aliases: ['PI_HERDR_READY_TIMEOUT_MS'], kind: 'number', defaultValue: 90000, min: 1000, impact: 'Subagent pane pipe readiness wait (backoff; a dead pane fails fast)' },
159
- { plane: 'env', key: 'PIER_ISOLATE_SWEEP_ORPHANS', aliases: ['PI_HERDR_ISOLATE_SWEEP_ORPHANS'], kind: 'string', impact: 'Opt-in sweeping of orphaned isolate worktrees' },
160
- { plane: 'env', key: 'PIER_FOCUS_POLL_MS', kind: 'number', defaultValue: 1500, min: 0, impact: 'Pane-focus sampling cadence for the workbench heat layout (0 disables). Default 1500ms on Herdr <0.9.1; 0 (event-first) on 0.9.1+' },
161
- { plane: 'env', key: 'PIER_TERMINAL_PROMPT', aliases: ['PI_HERDR_TERMINAL_PROMPT'], kind: 'string', impact: 'Terminal readiness prompt strategy: bash|zsh|powershell|pwsh (default: $SHELL)' },
162
- // B10: canonical names are `PIER_*`; the `PI_HERDR_*` spelling stays accepted (aliases).
163
- { plane: 'env', key: 'PIER_TODO_GRACE_MS', aliases: ['PI_HERDR_TODO_GRACE_MS'], kind: 'number', defaultValue: 30000, min: 1, impact: 'Delay before the unfinished-todo reminder fires (0/NaN falls back to the default)' },
164
- { plane: 'env', key: 'PIER_TERM_IDLE_MS', aliases: ['PI_HERDR_TERM_IDLE_MS'], kind: 'number', defaultValue: 1800000, min: 1, impact: 'Terminal idle threshold before a nudge is due (0/NaN falls back to the default)' },
165
- { plane: 'env', key: 'PIER_TERM_GRACE_MS', aliases: ['PI_HERDR_TERM_GRACE_MS'], kind: 'number', defaultValue: 30000, min: 1, impact: 'Extra grace after terminal idle before reading (0/NaN falls back to the default)' },
166
- { plane: 'env', key: 'PIER_TERM_READ_MAX', aliases: ['PI_HERDR_TERM_READ_MAX'], kind: 'number', defaultValue: 8000, min: 1, impact: 'Maximum terminal characters read per operation (0/NaN falls back to the default)' },
167
- { plane: 'env', key: 'PIER_TRACE', aliases: ['PI_HERDR_TRACE'], kind: 'string', impact: 'Write pier diagnostics to stderr (or to this file when it is a path)' },
168
- { plane: 'env', key: 'PIER_SLIM_FRAME', aliases: ['PI_HERDR_SLIM_FRAME'], kind: 'string', impact: 'Force the slim transcript frame on/off' },
169
- { plane: 'env', key: 'PIER_HMR', aliases: ['PI_HERDR_HMR'], kind: 'string', impact: 'Force hot-reload wiring on/off' },
170
- ];
171
-
172
- export const CONFIG_KNOBS: readonly ConfigKnob[] = Object.freeze([
173
- ...EFFICIENCY_KNOBS,
174
- ...ROLE_KNOBS,
175
- ...PI_KNOBS,
176
- ...BOOT_KNOBS,
177
- ...ENV_KNOBS,
178
- ]);
179
-
180
- export function catalogKeysForPlane(plane: ConfigPlaneId): string[] {
181
- return CONFIG_KNOBS.filter((k) => k.plane === plane).map((k) => k.key);
182
- }
183
-
184
- export function planeById(id: ConfigPlaneId): ConfigPlane | undefined {
185
- return CONFIG_PLANES.find((p) => p.id === id);
186
- }
111
+ /** Runtime policy / behaviour env knobs, derived from the `PIER_OPTIONS` registry. */
112
+ const ENV_KNOBS: readonly ConfigKnob[] = PIER_OPTIONS.map((option) => ({
113
+ plane: 'env' as const,
114
+ key: option.name,
115
+ kind: option.min === undefined ? ('string' as const) : ('number' as const),
116
+ defaultValue: option.min === undefined
117
+ ? (option.fallback === '' ? undefined : option.fallback)
118
+ : Number(option.fallback),
119
+ aliases: option.legacy ? [option.legacy] : undefined,
120
+ min: option.min,
121
+ impact: option.description,
122
+ }));
123
+
124
+ /** Every knob the catalog can resolve (roles is a per-file plane: its schema is the reference). */
125
+ export const CONFIG_KNOBS: readonly ConfigKnob[] = Object.freeze([...EFFICIENCY_KNOBS, ...PI_KNOBS, ...ENV_KNOBS]);
187
126
 
188
- /** Dotted-path getter for parsed JSON layers. */
189
127
  export function readDotted(obj: unknown, dotted: string): unknown {
190
- if (!obj || typeof obj !== 'object') return undefined;
191
128
  let cursor: unknown = obj;
192
129
  for (const part of dotted.split('.')) {
193
130
  if (!cursor || typeof cursor !== 'object' || Array.isArray(cursor)) return undefined;
@@ -196,17 +133,10 @@ export function readDotted(obj: unknown, dotted: string): unknown {
196
133
  return cursor;
197
134
  }
198
135
 
199
- /**
200
- * Secret-shaped KEY names (not values). Deliberately narrower than a bare `token` match so
201
- * that legitimate keys like `compaction.keepRecentTokens` are still shown verbatim.
202
- */
136
+ /** Secret-shaped KEY names (not values). Deliberately narrower than a bare `token` match so that
137
+ * legitimate keys like `compaction.keepRecentTokens` are still shown verbatim. */
203
138
  const SECRET_LIKE = /(api[_-]?key|apikey|authorization|bearer|access[_-]?token|client[_-]?secret|password|credential)/i;
204
139
 
205
- /** True when a value should never be echoed verbatim in command output. */
206
- export function isSecretLike(key: string): boolean {
207
- return SECRET_LIKE.test(key);
208
- }
209
-
210
140
  export function formatValue(value: unknown): string {
211
141
  if (value === undefined) return '(unset)';
212
142
  if (typeof value === 'string') return value === '' ? '""' : value;
@@ -215,9 +145,8 @@ export function formatValue(value: unknown): string {
215
145
  return JSON.stringify(value);
216
146
  }
217
147
 
218
- /** Renders a knob value, masking secret-looking keys. */
219
148
  export function redactValue(key: string, value: unknown): string {
220
- if (isSecretLike(key) && value !== undefined && value !== '') return '***';
149
+ if (SECRET_LIKE.test(key) && value !== undefined && value !== '') return '***';
221
150
  return formatValue(value);
222
151
  }
223
152
 
@@ -227,7 +156,6 @@ export interface ResolvedKnob {
227
156
  readonly knob: ConfigKnob;
228
157
  readonly value: string;
229
158
  readonly source: ConfigSource;
230
- /** Env var that actually supplied the value when it is an alias of `knob.key`. */
231
159
  readonly via?: string;
232
160
  /** Set when a workspace layer exists but was ignored (untrusted project). */
233
161
  readonly note?: string;
@@ -243,52 +171,38 @@ export interface RawConfigLayers {
243
171
  readonly piSettings?: { enabled?: boolean; keepRecentTokens?: number };
244
172
  }
245
173
 
246
- function resolveFileKnob(
247
- knob: ConfigKnob,
248
- layers: RawConfigLayers,
249
- ): ResolvedKnob {
250
- const env = layers.env ?? {};
251
- const envRaw = knob.envVar ? env[knob.envVar] : undefined;
252
- if (envRaw !== undefined && envRaw !== '') {
253
- return { knob, value: redactValue(knob.key, envRaw), source: 'env' };
254
- }
174
+ function resolveFileKnob(knob: ConfigKnob, layers: RawConfigLayers): ResolvedKnob {
175
+ const envRaw = knob.envVar ? (layers.env ?? {})[knob.envVar] : undefined;
176
+ if (envRaw !== undefined && envRaw !== '') return { knob, value: redactValue(knob.key, envRaw), source: 'env' };
255
177
 
256
178
  const workspaceValue = readDotted(layers.workspace, knob.key);
257
179
  const userValue = readDotted(layers.user, knob.key);
258
- if (layers.workspace !== undefined && !layers.workspaceTrusted) {
259
- // Untrusted workspace layer exists but is ignored by the loader: surface both facts.
260
- if (workspaceValue !== undefined) {
261
- if (userValue !== undefined) {
262
- return { knob, value: redactValue(knob.key, userValue), source: 'user', note: 'workspace layer ignored (untrusted project)' };
263
- }
264
- return { knob, value: redactValue(knob.key, knob.defaultValue), source: 'default', note: 'workspace value ignored (untrusted project)' };
265
- }
266
- } else if (workspaceValue !== undefined) {
267
- return { knob, value: redactValue(knob.key, workspaceValue), source: 'workspace' };
180
+ if (workspaceValue !== undefined) {
181
+ if (layers.workspaceTrusted) return { knob, value: redactValue(knob.key, workspaceValue), source: 'workspace' };
182
+ return userValue === undefined
183
+ ? { knob, value: redactValue(knob.key, knob.defaultValue), source: 'default', note: 'workspace value ignored (untrusted project)' }
184
+ : { knob, value: redactValue(knob.key, userValue), source: 'user', note: 'workspace layer ignored (untrusted project)' };
268
185
  }
269
-
270
186
  if (userValue !== undefined) return { knob, value: redactValue(knob.key, userValue), source: 'user' };
271
187
  return { knob, value: redactValue(knob.key, knob.defaultValue), source: 'default' };
272
188
  }
273
189
 
274
- /** Resolves efficiency + pi knobs from parsed layers. */
275
190
  export function resolveConfigKnobs(layers: RawConfigLayers): ResolvedKnob[] {
276
- const out: ResolvedKnob[] = [];
277
- for (const knob of EFFICIENCY_KNOBS) out.push(resolveFileKnob(knob, layers));
191
+ const out = EFFICIENCY_KNOBS.map((knob) => resolveFileKnob(knob, layers));
278
192
 
279
193
  // Effective-value rule from loadEfficiencyConfigFromDisk: pi's compaction.enabled=false
280
194
  // disables OCC unless PI_HERDR_COMPACT_ENABLE forces it on.
281
195
  const occ = out.find((e) => e.knob.key === 'onlineContextCompact.enabled');
282
196
  const envForced = (layers.env?.PI_HERDR_COMPACT_ENABLE ?? '') !== '';
283
197
  if (occ && occ.value === 'true' && layers.piSettings?.enabled === false && !envForced) {
284
- const index = out.indexOf(occ);
285
- out[index] = { ...occ, value: 'false', note: 'pi compaction.enabled=false disables OCC (set PI_HERDR_COMPACT_ENABLE=1 to force)' };
198
+ out[out.indexOf(occ)] = { ...occ, value: 'false', note: 'pi compaction.enabled=false disables OCC (set PI_HERDR_COMPACT_ENABLE=1 to force)' };
286
199
  }
287
200
 
288
201
  for (const knob of PI_KNOBS) {
289
202
  const raw = knob.key === 'compaction.enabled' ? layers.piSettings?.enabled : layers.piSettings?.keepRecentTokens;
290
- if (raw !== undefined) out.push({ knob, value: redactValue(knob.key, raw), source: 'pi' });
291
- else out.push({ knob, value: formatValue(undefined), source: 'pi', note: 'not set in pi settings' });
203
+ out.push(raw === undefined
204
+ ? { knob, value: formatValue(undefined), source: 'pi', note: 'not set in pi settings' }
205
+ : { knob, value: redactValue(knob.key, raw), source: 'pi' });
292
206
  }
293
207
  return out;
294
208
  }
@@ -300,33 +214,25 @@ export function resolveEnvKnobs(env: Record<string, string | undefined> = {}): R
300
214
  for (const name of [knob.key, ...(knob.aliases ?? [])]) {
301
215
  const raw = env[name];
302
216
  if (raw === undefined || raw === '') continue;
303
- return {
304
- knob,
305
- value: redactValue(knob.key, raw),
306
- source: 'env' as const,
307
- ...(name === knob.key ? {} : { via: name }),
308
- };
217
+ return { knob, value: redactValue(knob.key, raw), source: 'env' as const, ...(name === knob.key ? {} : { via: name }) };
309
218
  }
310
219
  return { knob, value: redactValue(knob.key, knob.defaultValue), source: 'default' as const };
311
220
  });
312
221
  }
313
222
 
314
- /* ── checks ─────────────────────────────────────────────────────────────── */
315
-
316
223
  export interface CheckReport {
317
224
  readonly plane: ConfigPlaneId;
318
225
  readonly ok: boolean;
319
226
  readonly issues: readonly string[];
320
227
  }
321
228
 
322
- /** Validates env knobs against catalog bounds (mirrors parseEnvInt's warn-and-default behaviour). */
323
229
  export function checkEnvKnobs(env: Record<string, string | undefined> = {}): string[] {
324
230
  const issues: string[] = [];
325
231
  for (const knob of ENV_KNOBS) {
232
+ if (knob.kind !== 'number') continue;
326
233
  for (const name of [knob.key, ...(knob.aliases ?? [])]) {
327
234
  const raw = env[name];
328
235
  if (raw === undefined || raw === '') continue;
329
- if (knob.kind !== 'number') continue;
330
236
  const parsed = Number.parseInt(raw, 10);
331
237
  const min = knob.min ?? 0;
332
238
  if (!Number.isFinite(parsed) || parsed < min) {
@@ -337,68 +243,49 @@ export function checkEnvKnobs(env: Record<string, string | undefined> = {}): str
337
243
  return issues;
338
244
  }
339
245
 
340
- /* ── rendering ──────────────────────────────────────────────────────────── */
341
-
342
- const SOURCE_TAG: Record<ConfigSource, string> = {
343
- env: 'env',
344
- workspace: 'workspace',
345
- user: 'user',
346
- default: 'default',
347
- pi: 'pi',
348
- };
349
-
350
246
  function sourceLabel(entry: ResolvedKnob): string {
351
- const base = entry.via ? `${SOURCE_TAG[entry.source]} via ${entry.via}` : SOURCE_TAG[entry.source];
247
+ const base = entry.via ? `${entry.source} via ${entry.via}` : entry.source;
352
248
  return entry.note ? `${base} (${entry.note})` : base;
353
249
  }
354
250
 
355
- /** Short non-default summary of one plane: "3 changed (env 1 / workspace 1 / user 1)". */
356
251
  export function summarizePlane(entries: readonly ResolvedKnob[]): string {
357
252
  const changed = entries.filter((e) => e.source !== 'default');
358
253
  if (changed.length === 0) return 'all defaults';
359
254
  const bySource = new Map<ConfigSource, number>();
360
255
  for (const e of changed) bySource.set(e.source, (bySource.get(e.source) ?? 0) + 1);
361
- const parts = [...bySource.entries()].map(([s, n]) => `${SOURCE_TAG[s]} ${n}`);
362
- return `${changed.length} set (${parts.join(', ')})`;
256
+ return `${changed.length} set (${[...bySource].map(([s, n]) => `${s} ${n}`).join(', ')})`;
363
257
  }
364
258
 
365
- /** Index lines for `/pier-config` (kept short: one line per plane). */
366
259
  export function renderIndex(lines: {
367
260
  efficiency: readonly ResolvedKnob[];
368
261
  pi: readonly ResolvedKnob[];
369
262
  env: readonly ResolvedKnob[];
370
263
  roleSummary: string;
371
- bootSummary: string;
372
264
  }): string[] {
373
- const enabled = (prefix: string): number =>
374
- lines.efficiency.filter((e) => e.knob.key === `${prefix}.enabled` && e.value === 'true').length;
375
- const out: string[] = [];
376
- out.push(`pier config — 5 planes, env > workspace > user > default`);
377
- out.push(` efficiency ${summarizePlane(lines.efficiency)} — OCC ${enabled('onlineContextCompact') ? 'on' : 'off'} / OBS ${enabled('observationPack') ? 'on' : 'off'} / EPR ${enabled('evidencePreservingReducer') ? 'on' : 'off'}`);
378
- out.push(` roles ${lines.roleSummary}`);
379
- out.push(` pi ${summarizePlane(lines.pi)} — OCC reads compaction.* only`);
380
- out.push(` boot ${lines.bootSummary}`);
381
- out.push(` env ${summarizePlane(lines.env)}`);
382
- out.push(` show: /pier-config show <efficiency|roles|pi|boot|env|all> check: /pier-config check report: /pier-config doc`);
383
- return out;
265
+ const mechanism = (prefix: string): string =>
266
+ lines.efficiency.some((e) => e.knob.key === `${prefix}.enabled` && e.value === 'true') ? 'on' : 'off';
267
+ return [
268
+ 'pier config — 4 planes, env > workspace > user > default',
269
+ ` efficiency ${summarizePlane(lines.efficiency)} — OCC ${mechanism('onlineContextCompact')} / OBS ${mechanism('observationPack')} / EPR ${mechanism('evidencePreservingReducer')}`,
270
+ ` roles ${lines.roleSummary}`,
271
+ ` pi ${summarizePlane(lines.pi)} — OCC reads compaction.* only`,
272
+ ` env ${summarizePlane(lines.env)}`,
273
+ ' show: /pier-config show <efficiency|roles|pi|env|all> check: /pier-config check report: /pier-config doc',
274
+ ];
384
275
  }
385
276
 
386
- /** Full listing for one plane. */
387
- export function renderPlane(plane: ConfigPlaneId, entries: readonly ResolvedKnob[]): string[] {
277
+ export function renderPlane(entries: readonly ResolvedKnob[]): string[] {
388
278
  const out: string[] = [];
389
279
  for (const entry of entries) {
390
- const key = entry.knob.envVar && entry.knob.plane === 'efficiency' && entry.knob.envVar
391
- ? `${entry.knob.key}${entry.source === 'env' ? ' (from ' + entry.knob.envVar + ')' : ''}`
392
- : entry.knob.key;
393
- const flags = [entry.knob.readOnly ? 'read-only' : '', entry.knob.docRef ?? ''].filter(Boolean).join('; ');
394
- out.push(` ${key} = ${entry.value} [${sourceLabel(entry)}]${flags ? ` (${flags})` : ''}`);
395
- if (entry.knob.impact) out.push(` ${entry.knob.impact}`);
280
+ const { knob } = entry;
281
+ const from = knob.envVar && entry.source === 'env' ? ` (from ${knob.envVar})` : '';
282
+ const flags = [knob.readOnly ? 'read-only' : '', knob.docRef ?? ''].filter(Boolean).join('; ');
283
+ out.push(` ${knob.key}${from} = ${entry.value} [${sourceLabel(entry)}]${flags ? ` (${flags})` : ''}`);
284
+ out.push(` ${knob.impact}`);
396
285
  }
397
- void plane;
398
286
  return out;
399
287
  }
400
288
 
401
- /** Validated-issue lines for `check`. */
402
289
  export function renderCheck(reports: readonly CheckReport[]): string[] {
403
290
  const out: string[] = [];
404
291
  for (const report of reports) {
@@ -415,36 +302,29 @@ export interface ReportMeta {
415
302
  readonly piVersion?: string;
416
303
  }
417
304
 
418
- /** Machine-truth markdown report (`/pier-config doc`). */
419
305
  export function renderReport(entries: readonly ResolvedKnob[], meta: ReportMeta): string {
420
- const lines: string[] = [];
421
- lines.push('# pier config report');
422
- lines.push('');
423
- lines.push(`- generated: ${meta.generatedAt}`);
424
- lines.push(`- cwd: ${meta.cwd}`);
425
- lines.push(`- workspace trusted: ${meta.workspaceTrusted}`);
426
- if (meta.piVersion) lines.push(`- pi: ${meta.piVersion}`);
427
- lines.push('');
428
- lines.push('Precedence: env > workspace (trusted) > user > default.');
429
- lines.push('');
306
+ const lines = [
307
+ '# pier config report',
308
+ '',
309
+ `- generated: ${meta.generatedAt}`,
310
+ `- cwd: ${meta.cwd}`,
311
+ `- workspace trusted: ${meta.workspaceTrusted}`,
312
+ ...(meta.piVersion ? [`- pi: ${meta.piVersion}`] : []),
313
+ '',
314
+ 'Precedence: env > workspace (trusted) > user > default.',
315
+ '',
316
+ ];
317
+ const escape = (text: string): string => text.replace(/\|/g, '\\|');
430
318
  for (const plane of CONFIG_PLANES) {
431
319
  const planeEntries = entries.filter((e) => e.knob.plane === plane.id);
432
- lines.push(`## ${plane.id} — ${plane.title}`);
433
- lines.push('');
434
- lines.push(`Files: ${plane.files.map((f) => `\`${f}\``).join(' , ')}`);
435
- lines.push('');
436
- lines.push(`> ${plane.editHint}`);
437
- lines.push('');
320
+ lines.push(`## ${plane.id} — ${plane.title}`, '', `Files: ${plane.files.map((f) => `\`${f}\``).join(' , ')}`, '', `> ${plane.editHint}`, '');
438
321
  if (planeEntries.length === 0) {
439
- lines.push('(no value-carrying knobs; per-file plane)');
440
- lines.push('');
322
+ lines.push('(no value-carrying knobs; per-file plane)', '');
441
323
  continue;
442
324
  }
443
- lines.push('| key | value | source | impact |');
444
- lines.push('|---|---|---|---|');
325
+ lines.push('| key | value | source | impact |', '|---|---|---|---|');
445
326
  for (const entry of planeEntries) {
446
- const value = entry.value.replace(/\|/g, '\\|');
447
- lines.push(`| \`${entry.knob.key}\` | ${value} | ${sourceLabel(entry)} | ${entry.knob.impact.replace(/\|/g, '\\|')} |`);
327
+ lines.push(`| \`${entry.knob.key}\` | ${escape(entry.value)} | ${sourceLabel(entry)} | ${escape(entry.knob.impact)} |`);
448
328
  }
449
329
  lines.push('');
450
330
  }