pi-pier 0.2.7 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -7
- package/package.json +7 -2
- package/schemas/efficiency-config.schema.json +77 -0
- package/skills/pi-herdr-workflow/SKILL.md +12 -0
- package/src/ansi-text.ts +83 -0
- package/src/ask-multi.ts +260 -0
- package/src/ask-user.ts +60 -10
- package/src/bootstrap.ts +5 -2
- package/src/compact-coordinator.ts +454 -0
- package/src/compact-economics-core.ts +396 -0
- package/src/config-catalog-core.ts +452 -0
- package/src/config-command.ts +181 -0
- package/src/config-guide.ts +361 -0
- package/src/core/grid-shape.ts +4 -2
- package/src/core/heat-plan.ts +160 -0
- package/src/core/observation.ts +555 -0
- package/src/core/subagent.ts +187 -393
- package/src/core/terminal.ts +121 -47
- package/src/core/todo.ts +25 -11
- package/src/dashboard-command.ts +89 -0
- package/src/efficiency-config-core.ts +661 -0
- package/src/efficiency-config-io.ts +53 -0
- package/src/efficiency-store.ts +312 -0
- package/src/focus-poller.ts +244 -0
- package/src/gc-core.ts +85 -0
- package/src/git-adapter.ts +12 -3
- package/src/herdr-client.ts +292 -36
- package/src/index-locks.ts +27 -9
- package/src/index-master.ts +5 -1
- package/src/index-notices.ts +13 -2
- package/src/index-pipe.ts +5 -0
- package/src/index-worker.ts +3 -1
- package/src/index.ts +330 -34
- package/src/jev-client.ts +146 -0
- package/src/jev-core.ts +381 -0
- package/src/lock-core.ts +105 -16
- package/src/observation-core.ts +248 -0
- package/src/pier-options.ts +96 -0
- package/src/pipe-channel.ts +52 -7
- package/src/reducer-core.ts +358 -0
- package/src/reducer-invoker.ts +392 -0
- package/src/renderers.ts +198 -0
- package/src/role-loader.ts +2 -1
- package/src/role-manifest.ts +0 -4
- package/src/runtime-policy.ts +5 -8
- package/src/session-tail.ts +60 -5
- package/src/settle-wake-core.ts +3 -1
- package/src/slim-frame.ts +4 -2
- package/src/storage-layout.ts +23 -0
- package/src/subagent-core.ts +3 -1
- package/src/subagent-execute.ts +328 -0
- package/src/subagent-gc.ts +33 -13
- package/src/subagent-launch.ts +4 -0
- package/src/subagent-list-action.ts +69 -0
- package/src/subagent-output-action.ts +104 -0
- package/src/subagent-output-adapter.ts +27 -0
- package/src/subagent-output-core.ts +263 -0
- package/src/subagent-poll-loop.ts +59 -39
- package/src/subagent-poller.ts +46 -0
- package/src/subagent-ready.ts +89 -0
- package/src/subagent-registry.ts +70 -0
- package/src/subagent-resolution.ts +41 -0
- package/src/subagent-scope.ts +3 -1
- package/src/subagent-session-io.ts +115 -32
- package/src/subagent-spawn.ts +153 -8
- package/src/swallow.ts +75 -0
- package/src/terminal-core.ts +82 -10
- package/src/todo-core.ts +5 -4
- package/src/todo-reminder-core.ts +8 -1
- package/src/todos-service.ts +18 -6
- package/src/tool-error.ts +26 -0
- package/src/vocab.ts +7 -4
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D104 Config Catalog Core (pure).
|
|
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.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type ConfigPlaneId = 'efficiency' | 'roles' | 'pi' | 'boot' | 'env';
|
|
15
|
+
|
|
16
|
+
export type ConfigKind = 'boolean' | 'number' | 'string' | 'enum';
|
|
17
|
+
|
|
18
|
+
export interface ConfigKnob {
|
|
19
|
+
readonly plane: ConfigPlaneId;
|
|
20
|
+
/** Dotted path inside the plane's file, or the env var name for the `env` plane. */
|
|
21
|
+
readonly key: string;
|
|
22
|
+
readonly kind: ConfigKind;
|
|
23
|
+
readonly defaultValue?: string | number | boolean;
|
|
24
|
+
/** Highest-precedence env override for a file-plane knob. */
|
|
25
|
+
readonly envVar?: string;
|
|
26
|
+
/** Lower bound for numeric knobs (mirrors the runtime validator). */
|
|
27
|
+
readonly min?: number;
|
|
28
|
+
/** Historical spellings still accepted for an env knob (B10: canonical `PIER_*` first). */
|
|
29
|
+
readonly aliases?: readonly string[];
|
|
30
|
+
/** One-line effect/impact, shown by `show`. */
|
|
31
|
+
readonly impact: string;
|
|
32
|
+
/** Where to read more (docs/schema anchor). */
|
|
33
|
+
readonly docRef?: string;
|
|
34
|
+
/** True when pier only reads the value (owned by pi / herdr). */
|
|
35
|
+
readonly readOnly?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ConfigPlane {
|
|
39
|
+
readonly id: ConfigPlaneId;
|
|
40
|
+
readonly title: string;
|
|
41
|
+
readonly owner: 'pier' | 'pi' | 'workbench';
|
|
42
|
+
/** Human-facing path templates, in precedence order. */
|
|
43
|
+
readonly files: readonly string[];
|
|
44
|
+
/** What to do to change this plane. */
|
|
45
|
+
readonly editHint: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const CONFIG_PLANES: readonly ConfigPlane[] = Object.freeze([
|
|
49
|
+
{
|
|
50
|
+
id: 'efficiency',
|
|
51
|
+
title: 'Efficiency mechanisms (D100-D103)',
|
|
52
|
+
owner: 'pier',
|
|
53
|
+
files: ['<workspace>/.pi-herdr/config.json', '~/.pi/agent/herdr-pi/config.json', 'PI_HERDR_* env'],
|
|
54
|
+
editHint: 'Edit the workspace or user JSON (env wins); see docs/efficiency-trial.md',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 'roles',
|
|
58
|
+
title: 'Role profiles (D82/D11)',
|
|
59
|
+
owner: 'pier',
|
|
60
|
+
files: ['<workspace>/.pi-herdr/roles/<name>.json', '~/.pi/agent/herdr-pi/roles/<name>.json', 'builtin src/roles/'],
|
|
61
|
+
editHint: 'Add or edit a role JSON; builtin names (master/worker-default) cannot be overridden',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'pi',
|
|
65
|
+
title: "Pi's own settings (read-only here)",
|
|
66
|
+
owner: 'pi',
|
|
67
|
+
files: ['~/.pi/agent/settings.json', '<trusted-project>/.pi/settings.json'],
|
|
68
|
+
editHint: 'Use pi\'s /settings for everything except the OCC-relevant compaction.* keys',
|
|
69
|
+
},
|
|
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
|
+
{
|
|
78
|
+
id: 'env',
|
|
79
|
+
title: 'Runtime policy env (PIER_* / PI_HERDR_*)',
|
|
80
|
+
owner: 'pier',
|
|
81
|
+
files: ['process environment (per-process, no file)'],
|
|
82
|
+
editHint: 'Export the variable before starting pi; it applies to new processes only',
|
|
83
|
+
},
|
|
84
|
+
] as const);
|
|
85
|
+
|
|
86
|
+
/** Efficiency knobs: mirrors schemas/efficiency-config.schema.json (guarded by the drift test). */
|
|
87
|
+
const EFFICIENCY_KNOBS: readonly ConfigKnob[] = [
|
|
88
|
+
{ 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
|
+
{ plane: 'efficiency', key: 'onlineContextCompact.logEnabled', kind: 'boolean', defaultValue: false, envVar: 'PI_HERDR_COMPACT_LOG', impact: 'Write compact.jsonl decisions', docRef: 'docs/efficiency-trial.md' },
|
|
90
|
+
{ plane: 'efficiency', key: 'onlineContextCompact.cacheWriteReadRatio', kind: 'string', defaultValue: 'auto', envVar: 'PI_HERDR_CACHE_RATIO', impact: 'KV cache write/read cost ratio; auto = model cost, then input/cacheRead, then provider family (gemini 4 / grok+deepseek 10), then token-account 2.0 — never disables OCC', docRef: 'docs/efficiency-trial.md' },
|
|
91
|
+
{ plane: 'efficiency', key: 'onlineContextCompact.firstCompactionRequestScale', kind: 'number', defaultValue: 2.0, min: 1.0, impact: 'First-compaction horizon relaxation', docRef: 'docs/efficiency-trial.md' },
|
|
92
|
+
{ plane: 'efficiency', key: 'onlineContextCompact.subsequentCompactionMargin', kind: 'number', defaultValue: 1.5, min: 1.0, impact: 'Safety margin required for later compactions', docRef: 'docs/efficiency-trial.md' },
|
|
93
|
+
{ plane: 'efficiency', key: 'onlineContextCompact.keepRecentTokens', kind: 'number', defaultValue: 20000, min: 1000, impact: 'Native compaction tail window; inherited from pi unless set here', docRef: 'docs/configuration.md' },
|
|
94
|
+
{ plane: 'efficiency', key: 'observationPack.enabled', kind: 'boolean', defaultValue: false, envVar: 'PI_HERDR_OBS_PACK_ENABLE', impact: 'Project large tool outputs as placeholders (observationPack)', docRef: 'docs/efficiency-trial.md' },
|
|
95
|
+
{ plane: 'efficiency', key: 'observationPack.logEnabled', kind: 'boolean', defaultValue: false, envVar: 'PI_HERDR_OBS_PACK_LOG', impact: 'Write observation.jsonl packed/recall records', docRef: 'docs/efficiency-trial.md' },
|
|
96
|
+
{ plane: 'efficiency', key: 'observationPack.thresholdBytes', kind: 'number', defaultValue: 10240, min: 1024, impact: 'Minimum output size before packing applies', docRef: 'docs/efficiency-trial.md' },
|
|
97
|
+
{ plane: 'efficiency', key: 'observationPack.fullSends', kind: 'number', defaultValue: 2, min: 1, impact: 'Provider requests that keep the full text before packing', docRef: 'docs/efficiency-trial.md' },
|
|
98
|
+
{ plane: 'efficiency', key: 'observationPack.recallChunkBytes', kind: 'number', defaultValue: 16384, min: 1024, impact: 'obs_recall page size', docRef: 'docs/efficiency-trial.md' },
|
|
99
|
+
{ plane: 'efficiency', key: 'observationPack.excerptBytes', kind: 'number', defaultValue: 1024, min: 128, impact: 'Whole-line head/tail excerpt kept in the placeholder', docRef: 'docs/efficiency-trial.md' },
|
|
100
|
+
{ plane: 'efficiency', key: 'evidencePreservingReducer.enabled', kind: 'boolean', defaultValue: false, envVar: 'PI_HERDR_REDUCER_ENABLE', impact: 'Reduce long bash diagnostic logs to a verified receipt (EPR)', docRef: 'docs/efficiency-trial.md' },
|
|
101
|
+
{ plane: 'efficiency', key: 'evidencePreservingReducer.logEnabled', kind: 'boolean', defaultValue: false, envVar: 'PI_HERDR_REDUCER_LOG', impact: 'Write reducer.jsonl attempts and fallback reasons', docRef: 'docs/efficiency-trial.md' },
|
|
102
|
+
{ plane: 'efficiency', key: 'evidencePreservingReducer.model', kind: 'string', envVar: 'PI_HERDR_REDUCER_MODEL', impact: 'Cheap model used for reduction (provider/model); defaults to the session model', docRef: 'docs/efficiency-trial.md' },
|
|
103
|
+
{ plane: 'efficiency', key: 'evidencePreservingReducer.minBytes', kind: 'number', defaultValue: 4096, min: 512, impact: 'Minimum log size before reduction is attempted', docRef: 'docs/efficiency-trial.md' },
|
|
104
|
+
{ plane: 'efficiency', key: 'evidencePreservingReducer.maxChars', kind: 'number', defaultValue: 600000, min: 1000, impact: 'Skip reduction above this log size', docRef: 'docs/efficiency-trial.md' },
|
|
105
|
+
{ plane: 'efficiency', key: 'evidencePreservingReducer.maxOutputTokens', kind: 'number', defaultValue: 2048, min: 128, impact: 'Reduction output token budget', docRef: 'docs/efficiency-trial.md' },
|
|
106
|
+
{ plane: 'efficiency', key: 'evidencePreservingReducer.timeoutMs', kind: 'number', defaultValue: 5000, min: 500, impact: 'Synchronous reduction budget; timeout falls back to the full log', docRef: 'docs/efficiency-trial.md' },
|
|
107
|
+
{ plane: 'efficiency', key: 'evidencePreservingReducer.localOnly', kind: 'boolean', defaultValue: false, impact: 'Archive the raw log but never call a model', docRef: 'docs/efficiency-trial.md' },
|
|
108
|
+
{ plane: 'efficiency', key: 'jev.enabled', kind: 'boolean', defaultValue: false, envVar: 'PIER_JEV_ENABLE', impact: 'Jev decision layer (System One classification) master switch; every call site fails open', docRef: 'docs/rfc-jev-integration.md' },
|
|
109
|
+
{ plane: 'efficiency', key: 'jev.logEnabled', kind: 'boolean', defaultValue: false, envVar: 'PIER_JEV_LOG', impact: 'Write jev.jsonl question outcomes (no request/response bodies)', docRef: 'docs/rfc-jev-integration.md' },
|
|
110
|
+
{ plane: 'efficiency', key: 'jev.baseUrl', kind: 'string', envVar: 'PIER_JEV_BASE_URL', impact: 'API root override (relay/gateway); default https://api.typesafe.ai', docRef: 'docs/rfc-jev-integration.md' },
|
|
111
|
+
{ plane: 'efficiency', key: 'jev.model', kind: 'string', defaultValue: 'jev-1.13.0', envVar: 'PIER_JEV_MODEL', impact: 'Pinned versioned model id; aliases drift silently and skew tuned thresholds', docRef: 'docs/rfc-jev-integration.md' },
|
|
112
|
+
{ plane: 'efficiency', key: 'jev.timeoutMs', kind: 'number', defaultValue: 2000, min: 500, envVar: 'PIER_JEV_TIMEOUT_MS', impact: 'Total per-call budget via AbortController hard kill', docRef: 'docs/rfc-jev-integration.md' },
|
|
113
|
+
{ plane: 'efficiency', key: 'jev.minConfidence', kind: 'number', defaultValue: 0.6, min: 0, envVar: 'PIER_JEV_MIN_CONFIDENCE', impact: 'Minimum Choice/Score confidence to adopt an answer; below counts as unanswered', docRef: 'docs/rfc-jev-integration.md' },
|
|
114
|
+
{ 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
|
+
];
|
|
116
|
+
|
|
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
|
+
/** Pi-owned keys pier only reads. */
|
|
130
|
+
const PI_KNOBS: readonly ConfigKnob[] = [
|
|
131
|
+
{ 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
|
+
{ 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
|
+
];
|
|
134
|
+
|
|
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
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Dotted-path getter for parsed JSON layers. */
|
|
189
|
+
export function readDotted(obj: unknown, dotted: string): unknown {
|
|
190
|
+
if (!obj || typeof obj !== 'object') return undefined;
|
|
191
|
+
let cursor: unknown = obj;
|
|
192
|
+
for (const part of dotted.split('.')) {
|
|
193
|
+
if (!cursor || typeof cursor !== 'object' || Array.isArray(cursor)) return undefined;
|
|
194
|
+
cursor = (cursor as Record<string, unknown>)[part];
|
|
195
|
+
}
|
|
196
|
+
return cursor;
|
|
197
|
+
}
|
|
198
|
+
|
|
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
|
+
*/
|
|
203
|
+
const SECRET_LIKE = /(api[_-]?key|apikey|authorization|bearer|access[_-]?token|client[_-]?secret|password|credential)/i;
|
|
204
|
+
|
|
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
|
+
export function formatValue(value: unknown): string {
|
|
211
|
+
if (value === undefined) return '(unset)';
|
|
212
|
+
if (typeof value === 'string') return value === '' ? '""' : value;
|
|
213
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
214
|
+
if (value === null) return 'null';
|
|
215
|
+
return JSON.stringify(value);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Renders a knob value, masking secret-looking keys. */
|
|
219
|
+
export function redactValue(key: string, value: unknown): string {
|
|
220
|
+
if (isSecretLike(key) && value !== undefined && value !== '') return '***';
|
|
221
|
+
return formatValue(value);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export type ConfigSource = 'env' | 'workspace' | 'user' | 'default' | 'pi';
|
|
225
|
+
|
|
226
|
+
export interface ResolvedKnob {
|
|
227
|
+
readonly knob: ConfigKnob;
|
|
228
|
+
readonly value: string;
|
|
229
|
+
readonly source: ConfigSource;
|
|
230
|
+
/** Env var that actually supplied the value when it is an alias of `knob.key`. */
|
|
231
|
+
readonly via?: string;
|
|
232
|
+
/** Set when a workspace layer exists but was ignored (untrusted project). */
|
|
233
|
+
readonly note?: string;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export interface RawConfigLayers {
|
|
237
|
+
readonly env?: Record<string, string | undefined>;
|
|
238
|
+
/** Parsed workspace efficiency config (untrusted layers are reported, never honored). */
|
|
239
|
+
readonly workspace?: unknown;
|
|
240
|
+
readonly user?: unknown;
|
|
241
|
+
readonly workspaceTrusted?: boolean;
|
|
242
|
+
/** Result of loadPiNativeCompactionSettings (pi-owned values pier reads). */
|
|
243
|
+
readonly piSettings?: { enabled?: boolean; keepRecentTokens?: number };
|
|
244
|
+
}
|
|
245
|
+
|
|
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
|
+
}
|
|
255
|
+
|
|
256
|
+
const workspaceValue = readDotted(layers.workspace, knob.key);
|
|
257
|
+
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' };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (userValue !== undefined) return { knob, value: redactValue(knob.key, userValue), source: 'user' };
|
|
271
|
+
return { knob, value: redactValue(knob.key, knob.defaultValue), source: 'default' };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Resolves efficiency + pi knobs from parsed layers. */
|
|
275
|
+
export function resolveConfigKnobs(layers: RawConfigLayers): ResolvedKnob[] {
|
|
276
|
+
const out: ResolvedKnob[] = [];
|
|
277
|
+
for (const knob of EFFICIENCY_KNOBS) out.push(resolveFileKnob(knob, layers));
|
|
278
|
+
|
|
279
|
+
// Effective-value rule from loadEfficiencyConfigFromDisk: pi's compaction.enabled=false
|
|
280
|
+
// disables OCC unless PI_HERDR_COMPACT_ENABLE forces it on.
|
|
281
|
+
const occ = out.find((e) => e.knob.key === 'onlineContextCompact.enabled');
|
|
282
|
+
const envForced = (layers.env?.PI_HERDR_COMPACT_ENABLE ?? '') !== '';
|
|
283
|
+
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)' };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
for (const knob of PI_KNOBS) {
|
|
289
|
+
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' });
|
|
292
|
+
}
|
|
293
|
+
return out;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Resolves the env plane: only catalog keys are ever read. */
|
|
297
|
+
export function resolveEnvKnobs(env: Record<string, string | undefined> = {}): ResolvedKnob[] {
|
|
298
|
+
return ENV_KNOBS.map((knob) => {
|
|
299
|
+
// Same precedence as pierOption(): canonical first, then the historical spelling, empty = unset.
|
|
300
|
+
for (const name of [knob.key, ...(knob.aliases ?? [])]) {
|
|
301
|
+
const raw = env[name];
|
|
302
|
+
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
|
+
};
|
|
309
|
+
}
|
|
310
|
+
return { knob, value: redactValue(knob.key, knob.defaultValue), source: 'default' as const };
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/* ── checks ─────────────────────────────────────────────────────────────── */
|
|
315
|
+
|
|
316
|
+
export interface CheckReport {
|
|
317
|
+
readonly plane: ConfigPlaneId;
|
|
318
|
+
readonly ok: boolean;
|
|
319
|
+
readonly issues: readonly string[];
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Validates env knobs against catalog bounds (mirrors parseEnvInt's warn-and-default behaviour). */
|
|
323
|
+
export function checkEnvKnobs(env: Record<string, string | undefined> = {}): string[] {
|
|
324
|
+
const issues: string[] = [];
|
|
325
|
+
for (const knob of ENV_KNOBS) {
|
|
326
|
+
for (const name of [knob.key, ...(knob.aliases ?? [])]) {
|
|
327
|
+
const raw = env[name];
|
|
328
|
+
if (raw === undefined || raw === '') continue;
|
|
329
|
+
if (knob.kind !== 'number') continue;
|
|
330
|
+
const parsed = Number.parseInt(raw, 10);
|
|
331
|
+
const min = knob.min ?? 0;
|
|
332
|
+
if (!Number.isFinite(parsed) || parsed < min) {
|
|
333
|
+
issues.push(`${name}="${raw}" is not a valid integer >= ${min} (runtime falls back to ${formatValue(knob.defaultValue)})`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return issues;
|
|
338
|
+
}
|
|
339
|
+
|
|
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
|
+
function sourceLabel(entry: ResolvedKnob): string {
|
|
351
|
+
const base = entry.via ? `${SOURCE_TAG[entry.source]} via ${entry.via}` : SOURCE_TAG[entry.source];
|
|
352
|
+
return entry.note ? `${base} (${entry.note})` : base;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Short non-default summary of one plane: "3 changed (env 1 / workspace 1 / user 1)". */
|
|
356
|
+
export function summarizePlane(entries: readonly ResolvedKnob[]): string {
|
|
357
|
+
const changed = entries.filter((e) => e.source !== 'default');
|
|
358
|
+
if (changed.length === 0) return 'all defaults';
|
|
359
|
+
const bySource = new Map<ConfigSource, number>();
|
|
360
|
+
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(', ')})`;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Index lines for `/pier-config` (kept short: one line per plane). */
|
|
366
|
+
export function renderIndex(lines: {
|
|
367
|
+
efficiency: readonly ResolvedKnob[];
|
|
368
|
+
pi: readonly ResolvedKnob[];
|
|
369
|
+
env: readonly ResolvedKnob[];
|
|
370
|
+
roleSummary: string;
|
|
371
|
+
bootSummary: string;
|
|
372
|
+
}): 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;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** Full listing for one plane. */
|
|
387
|
+
export function renderPlane(plane: ConfigPlaneId, entries: readonly ResolvedKnob[]): string[] {
|
|
388
|
+
const out: string[] = [];
|
|
389
|
+
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}`);
|
|
396
|
+
}
|
|
397
|
+
void plane;
|
|
398
|
+
return out;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Validated-issue lines for `check`. */
|
|
402
|
+
export function renderCheck(reports: readonly CheckReport[]): string[] {
|
|
403
|
+
const out: string[] = [];
|
|
404
|
+
for (const report of reports) {
|
|
405
|
+
out.push(` ${report.ok ? 'ok ' : 'FAIL'} ${report.plane}`);
|
|
406
|
+
for (const issue of report.issues) out.push(` - ${issue}`);
|
|
407
|
+
}
|
|
408
|
+
return out;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export interface ReportMeta {
|
|
412
|
+
readonly generatedAt: string;
|
|
413
|
+
readonly cwd: string;
|
|
414
|
+
readonly workspaceTrusted: boolean;
|
|
415
|
+
readonly piVersion?: string;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** Machine-truth markdown report (`/pier-config doc`). */
|
|
419
|
+
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('');
|
|
430
|
+
for (const plane of CONFIG_PLANES) {
|
|
431
|
+
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('');
|
|
438
|
+
if (planeEntries.length === 0) {
|
|
439
|
+
lines.push('(no value-carrying knobs; per-file plane)');
|
|
440
|
+
lines.push('');
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
lines.push('| key | value | source | impact |');
|
|
444
|
+
lines.push('|---|---|---|---|');
|
|
445
|
+
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, '\\|')} |`);
|
|
448
|
+
}
|
|
449
|
+
lines.push('');
|
|
450
|
+
}
|
|
451
|
+
return lines.join('\n');
|
|
452
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D104 `/pier-config` command registration.
|
|
3
|
+
*
|
|
4
|
+
* Read-only by design: the command reports effective values with their provenance and
|
|
5
|
+
* hands the actual edit to the agent under the existing write-lock + diff-confirmation
|
|
6
|
+
* flow. It never writes configuration itself (only the opt-in `doc` report).
|
|
7
|
+
*
|
|
8
|
+
* Subcommands: (none) | show [plane|all] | check | doc [path]
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
13
|
+
import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
14
|
+
import { VERSION as PI_VERSION } from '@earendil-works/pi-coding-agent';
|
|
15
|
+
import { CONFIG_PLANES, type ConfigPlaneId } from './config-catalog-core.ts';
|
|
16
|
+
import {
|
|
17
|
+
CONFIG_GUIDANCE_PROMPT,
|
|
18
|
+
collectConfigSnapshot,
|
|
19
|
+
guideCheckLines,
|
|
20
|
+
guideIndexLines,
|
|
21
|
+
guidePlaneLines,
|
|
22
|
+
guideReportMarkdown,
|
|
23
|
+
type ConfigGuideDeps,
|
|
24
|
+
type ConfigGuideSnapshot,
|
|
25
|
+
} from './config-guide.ts';
|
|
26
|
+
import { formatOptionRows } from './pier-options.ts';
|
|
27
|
+
import { formatSwallowedErrors } from './swallow.ts';
|
|
28
|
+
import { HERDR_PROTOCOL_EXPECTED } from './herdr-client.ts';
|
|
29
|
+
|
|
30
|
+
export const CONFIG_COMMAND_NAME = 'pier-config';
|
|
31
|
+
export const CONFIG_GUIDE_CUSTOM_TYPE = 'pi-herdr.config-guide';
|
|
32
|
+
export const CONFIG_REPORT_FILENAME = 'config-report.md';
|
|
33
|
+
export const CONFIG_REPORT_DIR = '.pi-herdr';
|
|
34
|
+
|
|
35
|
+
const PLANE_IDS: readonly ConfigPlaneId[] = CONFIG_PLANES.map((p) => p.id);
|
|
36
|
+
const SUBCOMMANDS = ['show', 'check', 'doc', 'doctor'] as const;
|
|
37
|
+
|
|
38
|
+
export interface ConfigCommandDeps {
|
|
39
|
+
pi: ExtensionAPI;
|
|
40
|
+
/** Injectable for tests; defaults to collectConfigSnapshot. */
|
|
41
|
+
collect?: (deps: ConfigGuideDeps) => ConfigGuideSnapshot;
|
|
42
|
+
/** Overridable so tests can pin the report destination. */
|
|
43
|
+
reportDir?: (cwd: string) => string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isPlaneId(value: string): value is ConfigPlaneId {
|
|
47
|
+
return (PLANE_IDS as readonly string[]).includes(value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parseArgs(args: unknown): { sub: (typeof SUBCOMMANDS)[number] | 'index'; rest: string } {
|
|
51
|
+
const raw = typeof args === 'string'
|
|
52
|
+
? args.split(/\s+/).filter(Boolean)
|
|
53
|
+
: Array.isArray(args) ? args.map(String) : [];
|
|
54
|
+
const [first, ...rest] = raw;
|
|
55
|
+
const sub = (SUBCOMMANDS as readonly string[]).includes(first ?? '')
|
|
56
|
+
? (first as (typeof SUBCOMMANDS)[number])
|
|
57
|
+
: 'index';
|
|
58
|
+
// `show` accepts an optional plane; an unknown first token is treated as the plane selector for `show`.
|
|
59
|
+
const restJoined = sub === 'index' && first && first !== 'index' ? [first, ...rest].join(' ') : rest.join(' ');
|
|
60
|
+
return { sub, rest: restJoined };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Registers `/pier-config`. Safe to call once per extension load (pi replaces commands by name). */
|
|
64
|
+
export function installConfigCommand(deps: ConfigCommandDeps): void {
|
|
65
|
+
const { pi } = deps;
|
|
66
|
+
const collect = deps.collect ?? collectConfigSnapshot;
|
|
67
|
+
const reportDir = deps.reportDir ?? ((cwd: string) => join(cwd, CONFIG_REPORT_DIR));
|
|
68
|
+
|
|
69
|
+
pi.registerCommand(CONFIG_COMMAND_NAME, {
|
|
70
|
+
description:
|
|
71
|
+
'Show pier configuration (5 planes) with effective values and sources; `check` validates them; `doctor` lists option values and swallowed errors; no argument hands a guided change to the agent',
|
|
72
|
+
getArgumentCompletions: (prefix: string) => {
|
|
73
|
+
const tokens = (prefix ?? '').split(/\s+/);
|
|
74
|
+
const head = tokens[0] ?? '';
|
|
75
|
+
if (tokens.length <= 1) {
|
|
76
|
+
const candidates = ['show', 'check', 'doc', 'doctor', 'all', ...PLANE_IDS];
|
|
77
|
+
return candidates
|
|
78
|
+
.filter((c) => c.startsWith(head))
|
|
79
|
+
.map((c) => ({ value: c, label: c, description: c === 'doc' ? 'write a config report file' : undefined }));
|
|
80
|
+
}
|
|
81
|
+
if (head === 'show') {
|
|
82
|
+
const last = tokens[tokens.length - 1] ?? '';
|
|
83
|
+
return ['all', ...PLANE_IDS]
|
|
84
|
+
.filter((c) => c.startsWith(last))
|
|
85
|
+
.map((c) => ({ value: c, label: c }));
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
},
|
|
89
|
+
handler: async (args: string, ctx: ExtensionContext) => {
|
|
90
|
+
const ui = (ctx as { ui?: { notify?: (text: string, level?: string) => void } }).ui;
|
|
91
|
+
const emit = (text: string, level: 'info' | 'warning' | 'error' = 'info'): void => {
|
|
92
|
+
if (ui?.notify) ui.notify(text, level);
|
|
93
|
+
else console.log(text); // print/json and test modes have no UI surface
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const cwd = (ctx as { cwd?: string }).cwd ?? process.cwd();
|
|
97
|
+
const isProjectTrusted =
|
|
98
|
+
typeof (ctx as { isProjectTrusted?: () => boolean }).isProjectTrusted === 'function'
|
|
99
|
+
? (ctx as { isProjectTrusted: () => boolean }).isProjectTrusted()
|
|
100
|
+
: false;
|
|
101
|
+
|
|
102
|
+
let snapshot: ConfigGuideSnapshot;
|
|
103
|
+
try {
|
|
104
|
+
snapshot = collect({ cwd, isProjectTrusted });
|
|
105
|
+
} catch (err) {
|
|
106
|
+
emit(`pier-config: failed to read configuration (${err instanceof Error ? err.message : String(err)})`, 'error');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const { sub, rest } = parseArgs(args);
|
|
111
|
+
|
|
112
|
+
if (sub === 'check') {
|
|
113
|
+
emit([`pier config check (workspace trusted: ${snapshot.workspaceTrusted})`, ...guideCheckLines(snapshot)].join('\n'));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (sub === 'doc') {
|
|
118
|
+
const target = rest
|
|
119
|
+
? (isAbsolute(rest) ? rest : resolve(cwd, rest))
|
|
120
|
+
: join(reportDir(cwd), CONFIG_REPORT_FILENAME);
|
|
121
|
+
try {
|
|
122
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
123
|
+
writeFileSync(
|
|
124
|
+
target,
|
|
125
|
+
guideReportMarkdown(snapshot, { generatedAt: new Date().toISOString(), cwd, piVersion: PI_VERSION }),
|
|
126
|
+
'utf8',
|
|
127
|
+
);
|
|
128
|
+
emit(
|
|
129
|
+
`pier config report written: ${target}` +
|
|
130
|
+
(rest ? '' : ' (add `.pi-herdr/config-report.md` to .gitignore if you do not want it tracked)'),
|
|
131
|
+
);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
emit(`pier-config: could not write report (${err instanceof Error ? err.message : String(err)})`, 'error');
|
|
134
|
+
}
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (sub === 'doctor') {
|
|
139
|
+
// B9/B10: one place to see every pier option (canonical name, effective value, source) and the
|
|
140
|
+
// errors that were deliberately swallowed this session. Without it, a silently failing
|
|
141
|
+
// best-effort path stays invisible until something else breaks.
|
|
142
|
+
emit(
|
|
143
|
+
[
|
|
144
|
+
'pier doctor',
|
|
145
|
+
'',
|
|
146
|
+
`wire: herdr protocol ${HERDR_PROTOCOL_EXPECTED} (pinned by test/fixtures/herdr-contract.json)`,
|
|
147
|
+
'',
|
|
148
|
+
`options (canonical PIER_*, legacy PI_HERDR_* alias accepted):`,
|
|
149
|
+
...formatOptionRows(),
|
|
150
|
+
'',
|
|
151
|
+
formatSwallowedErrors(),
|
|
152
|
+
].join('\n'),
|
|
153
|
+
);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (sub === 'show') {
|
|
158
|
+
const plane = rest.trim();
|
|
159
|
+
const selector = plane === '' || plane === 'all' ? 'all' : isPlaneId(plane) ? plane : null;
|
|
160
|
+
if (selector === null) {
|
|
161
|
+
emit(`pier-config: unknown plane "${plane}" — expected one of ${PLANE_IDS.join(', ')} or all`, 'warning');
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
emit(guidePlaneLines(snapshot, selector).join('\n'));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Bare `/pier-config`: index + hand the guided change to the agent.
|
|
169
|
+
emit([...guideIndexLines(snapshot), '', 'asking the agent to guide the change...'].join('\n'));
|
|
170
|
+
try {
|
|
171
|
+
// Call on the receiver: the ExtensionAPI method must not be detached from `pi`.
|
|
172
|
+
(pi as { sendMessage?: (msg: unknown, opts: unknown) => void }).sendMessage?.(
|
|
173
|
+
{ customType: CONFIG_GUIDE_CUSTOM_TYPE, content: CONFIG_GUIDANCE_PROMPT, display: false },
|
|
174
|
+
{ triggerTurn: true },
|
|
175
|
+
);
|
|
176
|
+
} catch (err) {
|
|
177
|
+
emit(`pier-config: could not start the guided flow (${err instanceof Error ? err.message : String(err)})`, 'warning');
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
}
|