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.
- package/README.md +12 -11
- package/package.json +2 -2
- package/schemas/role-manifest.schema.json +7 -1
- package/skills/pi-herdr-workflow/SKILL.md +4 -5
- package/src/ansi-text.ts +62 -11
- package/src/ask-dialog.ts +308 -0
- package/src/ask-user.ts +113 -112
- package/src/bootstrap.ts +8 -12
- package/src/compact-coordinator.ts +278 -261
- package/src/compact-economics-core.ts +77 -145
- package/src/config-catalog-core.ts +77 -197
- package/src/config-command.ts +271 -64
- package/src/dashboard-command.ts +38 -19
- package/src/efficiency-config-core.ts +287 -484
- package/src/efficiency-store.ts +16 -29
- package/src/focus-poller.ts +14 -35
- package/src/gc-core.ts +10 -36
- package/src/herdr-client.ts +82 -208
- package/src/history-store.ts +36 -23
- package/src/index-gates.ts +124 -0
- package/src/index-locks.ts +2 -7
- package/src/index-master.ts +24 -18
- package/src/index-notices.ts +28 -15
- package/src/index-pipe.ts +17 -12
- package/src/index-roles.ts +306 -0
- package/src/index-runtime.ts +2 -6
- package/src/index-worker.ts +4 -8
- package/src/index.ts +305 -564
- package/src/jev-client.ts +12 -17
- package/src/jev-core.ts +137 -87
- package/src/ledger.ts +13 -21
- package/src/lock-core.ts +28 -56
- package/src/manifest-compose.ts +35 -20
- package/src/observation-core.ts +52 -100
- package/src/pane-title.ts +19 -38
- package/src/pi-surface.ts +34 -22
- package/src/pier-options.ts +54 -50
- package/src/pipe-channel.ts +27 -59
- package/src/platform-paths.ts +19 -52
- package/src/{core → plugins}/grid-shape.ts +8 -10
- package/src/{core → plugins}/heat-plan.ts +20 -22
- package/src/plugins/observation.ts +475 -0
- package/src/plugins/subagent.ts +728 -0
- package/src/{core → plugins}/terminal.ts +167 -127
- package/src/{core → plugins}/todo.ts +61 -100
- package/src/progress-core.ts +16 -53
- package/src/reconcile-core.ts +24 -33
- package/src/reducer-core.ts +24 -60
- package/src/reducer-invoker.ts +266 -239
- package/src/renderers.ts +76 -87
- package/src/role-loader.ts +33 -28
- package/src/role-manifest.ts +23 -28
- package/src/role-state.ts +119 -0
- package/src/routing-telemetry.ts +136 -0
- package/src/runtime-policy.ts +25 -40
- package/src/session-tail.ts +65 -41
- package/src/settle-wake-core.ts +52 -21
- package/src/slim-frame.ts +25 -122
- package/src/stale-core.ts +17 -24
- package/src/storage-layout.ts +14 -43
- package/src/subagent-core.ts +253 -118
- package/src/subagent-gc.ts +109 -67
- package/src/subagent-poller.ts +367 -24
- package/src/subagent-session.ts +516 -0
- package/src/subagent-spawn.ts +530 -52
- package/src/swallow.ts +7 -17
- package/src/terminal-core.ts +125 -106
- package/src/todo-core.ts +57 -109
- package/src/todo-read-hook.ts +23 -39
- package/src/todo-reminder-core.ts +26 -51
- package/src/todo-window.ts +11 -12
- package/src/todos-service.ts +15 -35
- package/src/tool-error.ts +6 -13
- package/src/tool-gate.ts +38 -21
- package/src/vocab.ts +52 -25
- package/src/ask-multi.ts +0 -260
- package/src/config-guide.ts +0 -361
- package/src/core/observation.ts +0 -555
- package/src/core/subagent.ts +0 -633
- package/src/efficiency-config-io.ts +0 -53
- package/src/git-adapter.ts +0 -113
- package/src/notice-buffer.ts +0 -23
- package/src/subagent-execute.ts +0 -328
- package/src/subagent-git-io.ts +0 -80
- package/src/subagent-launch.ts +0 -114
- package/src/subagent-list-action.ts +0 -69
- package/src/subagent-output-action.ts +0 -104
- package/src/subagent-output-adapter.ts +0 -27
- package/src/subagent-output-core.ts +0 -263
- package/src/subagent-poll-loop.ts +0 -305
- package/src/subagent-port.ts +0 -27
- package/src/subagent-ready.ts +0 -89
- package/src/subagent-registry.ts +0 -70
- package/src/subagent-resolution.ts +0 -41
- package/src/subagent-scope.ts +0 -41
- package/src/subagent-session-io.ts +0 -230
package/src/config-command.ts
CHANGED
|
@@ -1,39 +1,256 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* D104 `/pier-config
|
|
2
|
+
* D104 `/pier-config`: probes the four configuration planes (real files + process env), renders
|
|
3
|
+
* `show|check|doc|doctor`, and registers the command.
|
|
3
4
|
*
|
|
4
|
-
* Read-only by design:
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* Subcommands: (none) | show [plane|all] | check | doc [path]
|
|
5
|
+
* Read-only by design: effective values are reported with their provenance and the actual edit goes
|
|
6
|
+
* to the agent under the write-lock + diff-confirmation flow; only the opt-in `doc` report writes a
|
|
7
|
+
* file. Fail-open: every read is wrapped, so a broken plane degrades to a reported issue instead of
|
|
8
|
+
* throwing into pi's command pipeline. All paths are injectable, so tests never touch a real home dir.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { homedir } from 'node:os';
|
|
12
13
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
13
|
-
import type
|
|
14
|
-
import {
|
|
15
|
-
|
|
14
|
+
import { VERSION as piVersion, type ExtensionAPI, type ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
15
|
+
import {
|
|
16
|
+
CONFIG_PLANES,
|
|
17
|
+
checkEnvKnobs,
|
|
18
|
+
renderCheck,
|
|
19
|
+
renderIndex,
|
|
20
|
+
renderPlane,
|
|
21
|
+
renderReport,
|
|
22
|
+
resolveConfigKnobs,
|
|
23
|
+
resolveEnvKnobs,
|
|
24
|
+
type CheckReport,
|
|
25
|
+
type ConfigPlaneId,
|
|
26
|
+
type ResolvedKnob,
|
|
27
|
+
} from './config-catalog-core.ts';
|
|
16
28
|
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
type ConfigGuideDeps,
|
|
24
|
-
type ConfigGuideSnapshot,
|
|
25
|
-
} from './config-guide.ts';
|
|
29
|
+
defaultUserEfficiencyConfigFile,
|
|
30
|
+
defaultWorkspaceEfficiencyConfigFile,
|
|
31
|
+
loadPiNativeCompactionSettings,
|
|
32
|
+
validateEfficiencyConfig,
|
|
33
|
+
} from './efficiency-config-core.ts';
|
|
34
|
+
import { RESERVED_ROLE_NAMES, listRoleNames, loadRoleConfig, roleLayers } from './role-loader.ts';
|
|
26
35
|
import { formatOptionRows } from './pier-options.ts';
|
|
27
36
|
import { formatSwallowedErrors } from './swallow.ts';
|
|
28
37
|
import { HERDR_PROTOCOL_EXPECTED } from './herdr-client.ts';
|
|
29
38
|
|
|
30
|
-
export const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
39
|
+
export const CONFIG_GUIDANCE_PROMPT = [
|
|
40
|
+
'[PIER-CONFIG] The user wants to inspect or change pier configuration. Follow this workflow strictly:',
|
|
41
|
+
'',
|
|
42
|
+
'1. Ground yourself first: run `/pier-config show all` (or read the files) and use the reported EFFECTIVE VALUE and SOURCE',
|
|
43
|
+
' (env > workspace > user > default). Only entries whose source is `default` are worth changing; an ignored workspace file',
|
|
44
|
+
' (untrusted project) must be reported as such instead of edited blindly.',
|
|
45
|
+
'2. Route by plane:',
|
|
46
|
+
' - efficiency (D100-D103): read `docs/efficiency-trial.md` before proposing values.',
|
|
47
|
+
' - roles: read `docs/sidebar-role-config.md` and `schemas/role-manifest.schema.json`; builtin role names cannot be overridden.',
|
|
48
|
+
" - pi (settings.json): recommend pi's own `/settings`; the only OCC-relevant keys here are `compaction.enabled` and",
|
|
49
|
+
' `compaction.keepRecentTokens` (read-only from our side).',
|
|
50
|
+
' - env (PIER_* / PI_HERDR_*): affects new processes only; state that explicitly.',
|
|
51
|
+
'3. Explain before changing: 2-3 sentences on what the knob controls, its cost/benefit (tokens, latency, safety, blast radius),',
|
|
52
|
+
' and 2-3 recommended values for common scenarios. Then ask what the user actually wants to achieve.',
|
|
53
|
+
'4. Before writing: show a precise diff (file path, current -> proposed value) and the activation path',
|
|
54
|
+
' (hot / needs `/reload` / needs a new session or process restart). Wait for explicit confirmation. Change ONE plane at a time.',
|
|
55
|
+
'5. Apply with the normal `edit`/`write` tools (write locks apply), then run `/pier-config check` and report the result back.',
|
|
56
|
+
'6. If a change cannot take effect in the current process, say so and tell the user exactly what to restart.',
|
|
57
|
+
'',
|
|
58
|
+
'Forbidden: dumping `process.env`; writing secrets or tokens into any config file; editing multiple planes before confirmation;',
|
|
59
|
+
'touching `.pi-herdr/` of an untrusted project.',
|
|
60
|
+
].join('\n');
|
|
34
61
|
|
|
35
|
-
|
|
36
|
-
|
|
62
|
+
export interface ConfigPlaneFile {
|
|
63
|
+
readonly path: string;
|
|
64
|
+
readonly label: string;
|
|
65
|
+
readonly exists: boolean;
|
|
66
|
+
readonly ignored?: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface ConfigGuideDeps {
|
|
70
|
+
cwd?: string;
|
|
71
|
+
env?: Record<string, string | undefined>;
|
|
72
|
+
isProjectTrusted?: boolean;
|
|
73
|
+
/** Pi agent dir holding settings.json (defaults to ~/.pi/agent; mirrors PI_CODING_AGENT_DIR). */
|
|
74
|
+
agentDir?: string;
|
|
75
|
+
/** User-level efficiency config path (defaults to ~/.pi/agent/herdr-pi/config.json). */
|
|
76
|
+
userConfigPath?: string;
|
|
77
|
+
/** User-level roles dir override (defaults to ~/.pi/agent/herdr-pi/roles). */
|
|
78
|
+
rolesUserDir?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface ConfigGuideSnapshot {
|
|
82
|
+
readonly entries: readonly ResolvedKnob[];
|
|
83
|
+
readonly reports: readonly CheckReport[];
|
|
84
|
+
readonly files: Readonly<Record<ConfigPlaneId, readonly ConfigPlaneFile[]>>;
|
|
85
|
+
readonly workspaceTrusted: boolean;
|
|
86
|
+
readonly roleSummary: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
function readJsonLayer(path: string): { value: unknown; issue?: string } {
|
|
91
|
+
if (!existsSync(path)) return { value: undefined };
|
|
92
|
+
try {
|
|
93
|
+
return { value: JSON.parse(readFileSync(path, 'utf8')) };
|
|
94
|
+
} catch (err) {
|
|
95
|
+
return { value: undefined, issue: `${path}: invalid JSON (${err instanceof Error ? err.message : String(err)})` };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function listJsonFiles(dir: string): string[] {
|
|
100
|
+
try {
|
|
101
|
+
return readdirSync(dir).filter((f) => f.endsWith('.json')).sort();
|
|
102
|
+
} catch {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function collectConfigSnapshot(deps: ConfigGuideDeps = {}): ConfigGuideSnapshot {
|
|
108
|
+
const env = deps.env ?? process.env;
|
|
109
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
110
|
+
const workspaceTrusted = deps.isProjectTrusted ?? false;
|
|
111
|
+
const agentDir = deps.agentDir ?? env.PI_CODING_AGENT_DIR ?? join(homedir(), '.pi', 'agent');
|
|
112
|
+
const userConfigPath = deps.userConfigPath ?? defaultUserEfficiencyConfigFile();
|
|
113
|
+
const workspaceConfigPath = defaultWorkspaceEfficiencyConfigFile(cwd);
|
|
114
|
+
|
|
115
|
+
const reports: CheckReport[] = [];
|
|
116
|
+
const files: Record<ConfigPlaneId, ConfigPlaneFile[]> = { efficiency: [], roles: [], pi: [], env: [] };
|
|
117
|
+
|
|
118
|
+
/* efficiency: workspace layer (trusted projects only) + user layer, JSON issues reported as-is */
|
|
119
|
+
const workspaceLayer = readJsonLayer(workspaceConfigPath);
|
|
120
|
+
const userLayer = readJsonLayer(userConfigPath);
|
|
121
|
+
const efficiencyIssues: string[] = [];
|
|
122
|
+
for (const layer of [workspaceLayer, userLayer]) if (layer.issue) efficiencyIssues.push(layer.issue);
|
|
123
|
+
if (workspaceLayer.value !== undefined) {
|
|
124
|
+
if (workspaceTrusted) {
|
|
125
|
+
for (const issue of validateEfficiencyConfig(workspaceLayer.value).issues) efficiencyIssues.push(`${workspaceConfigPath}: ${issue}`);
|
|
126
|
+
} else {
|
|
127
|
+
efficiencyIssues.push(`${workspaceConfigPath}: ignored — the project is not trusted (values below come from user/default)`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (userLayer.value !== undefined) {
|
|
131
|
+
for (const issue of validateEfficiencyConfig(userLayer.value).issues) efficiencyIssues.push(`${userConfigPath}: ${issue}`);
|
|
132
|
+
}
|
|
133
|
+
files.efficiency.push(
|
|
134
|
+
{ path: workspaceConfigPath, label: 'workspace', exists: workspaceLayer.value !== undefined, ignored: !workspaceTrusted },
|
|
135
|
+
{ path: userConfigPath, label: 'user', exists: userLayer.value !== undefined },
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
/* pi settings (read-only here; only compaction.* matters to pier) */
|
|
139
|
+
const piSettingsPath = join(agentDir, 'settings.json');
|
|
140
|
+
const piSettingsExists = existsSync(piSettingsPath);
|
|
141
|
+
const piSettings = loadPiNativeCompactionSettings({ cwd, isProjectTrusted: workspaceTrusted, agentDir });
|
|
142
|
+
const piIssues = piSettingsExists
|
|
143
|
+
? []
|
|
144
|
+
: [`${piSettingsPath}: not found (pi defaults apply; use pi's /settings to create it)`];
|
|
145
|
+
files.pi.push({ path: piSettingsPath, label: 'agent', exists: piSettingsExists });
|
|
146
|
+
|
|
147
|
+
const envIssues = checkEnvKnobs(env);
|
|
148
|
+
files.env.push({ path: '(process environment)', label: 'env', exists: true });
|
|
149
|
+
|
|
150
|
+
/* roles: every layer's JSON must load; builtin names are intentionally shadow-proof */
|
|
151
|
+
const roleIssues: string[] = [];
|
|
152
|
+
const roleNames = new Set<string>();
|
|
153
|
+
const layerCounts: string[] = [];
|
|
154
|
+
for (const layer of roleLayers({ baseDir: cwd, userDir: deps.rolesUserDir })) {
|
|
155
|
+
const names = listJsonFiles(layer.dir);
|
|
156
|
+
const label: ConfigPlaneFile['label'] = layer.label.startsWith('workspace')
|
|
157
|
+
? 'workspace'
|
|
158
|
+
: layer.label.startsWith('user') ? 'user' : 'builtin';
|
|
159
|
+
files.roles.push({ path: layer.dir, label, exists: names.length > 0 });
|
|
160
|
+
layerCounts.push(`${label} ${names.length}`);
|
|
161
|
+
for (const file of names) roleNames.add(file.replace(/\.json$/, ''));
|
|
162
|
+
}
|
|
163
|
+
for (const name of [...roleNames].sort()) {
|
|
164
|
+
try {
|
|
165
|
+
loadRoleConfig(name, { baseDir: cwd });
|
|
166
|
+
} catch (err) {
|
|
167
|
+
const issues = (err as { issues?: readonly string[] }).issues ?? [];
|
|
168
|
+
roleIssues.push(`role "${name}": ${err instanceof Error ? err.message : String(err)}${issues.length ? ` — ${issues.join('; ')}` : ''}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const shadowed = listRoleNames(cwd, deps.rolesUserDir).filter((name) => RESERVED_ROLE_NAMES.includes(name));
|
|
172
|
+
if (shadowed.length > 0) {
|
|
173
|
+
roleIssues.push(`reserved role name(s) shadowed in a custom layer (ignored for built-ins): ${shadowed.join(', ')}`);
|
|
174
|
+
}
|
|
175
|
+
const roleSummary = `${roleNames.size} role(s): ${layerCounts.join(' / ')}`;
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
reports.push(
|
|
179
|
+
{ plane: 'efficiency', ok: efficiencyIssues.length === 0, issues: efficiencyIssues },
|
|
180
|
+
{ plane: 'roles', ok: roleIssues.length === 0, issues: roleIssues },
|
|
181
|
+
{ plane: 'pi', ok: true, issues: piIssues },
|
|
182
|
+
{ plane: 'env', ok: envIssues.length === 0, issues: envIssues },
|
|
183
|
+
);
|
|
184
|
+
const entries = [
|
|
185
|
+
...resolveConfigKnobs({ env, workspace: workspaceLayer.value, user: userLayer.value, workspaceTrusted, piSettings }),
|
|
186
|
+
...resolveEnvKnobs(env),
|
|
187
|
+
];
|
|
188
|
+
return { entries, reports, files, workspaceTrusted, roleSummary };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
export type GuideView = 'index' | 'check' | ConfigPlaneId | 'all';
|
|
193
|
+
|
|
194
|
+
/** Renders one `/pier-config` view: the plane index, one/all plane listings, or the check report. */
|
|
195
|
+
export function renderGuide(snapshot: ConfigGuideSnapshot, view: GuideView): string[] {
|
|
196
|
+
if (view === 'index') {
|
|
197
|
+
const plane = (id: ConfigPlaneId): readonly ResolvedKnob[] => snapshot.entries.filter((e) => e.knob.plane === id);
|
|
198
|
+
return renderIndex({
|
|
199
|
+
efficiency: plane('efficiency'),
|
|
200
|
+
pi: plane('pi'),
|
|
201
|
+
env: plane('env'),
|
|
202
|
+
roleSummary: snapshot.roleSummary,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
if (view === 'check') {
|
|
206
|
+
const anyFail = snapshot.reports.some((r) => !r.ok);
|
|
207
|
+
return [
|
|
208
|
+
...renderCheck(snapshot.reports),
|
|
209
|
+
anyFail ? ' → fix the FAIL planes above (values are still shown by `show`)' : ' → all planes look consistent',
|
|
210
|
+
];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const out: string[] = [];
|
|
214
|
+
for (const meta of CONFIG_PLANES) {
|
|
215
|
+
if (view !== 'all' && meta.id !== view) continue;
|
|
216
|
+
out.push(`${meta.id} — ${meta.title}`);
|
|
217
|
+
for (const file of snapshot.files[meta.id]) {
|
|
218
|
+
const status = file.exists ? (file.ignored ? 'present (IGNORED: untrusted project)' : 'present') : 'absent';
|
|
219
|
+
out.push(` file [${file.label}] ${file.path} — ${status}`);
|
|
220
|
+
}
|
|
221
|
+
out.push(` hint: ${meta.editHint}`);
|
|
222
|
+
const entries = snapshot.entries.filter((e) => e.knob.plane === meta.id);
|
|
223
|
+
if (entries.length > 0) {
|
|
224
|
+
out.push(...renderPlane(entries));
|
|
225
|
+
} else {
|
|
226
|
+
const report = snapshot.reports.find((r) => r.plane === meta.id);
|
|
227
|
+
out.push(` (per-file plane: ${!report ? 'no data' : report.ok ? 'no issues' : `${report.issues.length} issue(s) — see /pier-config check`})`);
|
|
228
|
+
}
|
|
229
|
+
out.push('');
|
|
230
|
+
}
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function guideReportMarkdown(
|
|
235
|
+
snapshot: ConfigGuideSnapshot,
|
|
236
|
+
meta: { generatedAt: string; cwd: string; piVersion?: string },
|
|
237
|
+
): string {
|
|
238
|
+
const body = renderReport(snapshot.entries, {
|
|
239
|
+
generatedAt: meta.generatedAt,
|
|
240
|
+
cwd: meta.cwd,
|
|
241
|
+
workspaceTrusted: snapshot.workspaceTrusted,
|
|
242
|
+
piVersion: meta.piVersion,
|
|
243
|
+
});
|
|
244
|
+
const filesSection = ['', '## Files observed', ''];
|
|
245
|
+
for (const plane of CONFIG_PLANES) {
|
|
246
|
+
for (const file of snapshot.files[plane.id]) {
|
|
247
|
+
const status = file.exists ? (file.ignored ? 'present, ignored (untrusted project)' : 'present') : 'absent';
|
|
248
|
+
filesSection.push(`- [${plane.id}] ${file.label}: \`${file.path}\` — ${status}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const checkSection = ['', '## Checks', '', ...renderGuide(snapshot, 'check').map((l) => `- ${l.trim()}`)];
|
|
252
|
+
return `${body}\n${filesSection.join('\n')}\n${checkSection.join('\n')}\n`;
|
|
253
|
+
}
|
|
37
254
|
|
|
38
255
|
export interface ConfigCommandDeps {
|
|
39
256
|
pi: ExtensionAPI;
|
|
@@ -43,9 +260,8 @@ export interface ConfigCommandDeps {
|
|
|
43
260
|
reportDir?: (cwd: string) => string;
|
|
44
261
|
}
|
|
45
262
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
263
|
+
const PLANE_IDS: readonly ConfigPlaneId[] = CONFIG_PLANES.map((p) => p.id);
|
|
264
|
+
const SUBCOMMANDS = ['show', 'check', 'doc', 'doctor'] as const;
|
|
49
265
|
|
|
50
266
|
function parseArgs(args: unknown): { sub: (typeof SUBCOMMANDS)[number] | 'index'; rest: string } {
|
|
51
267
|
const raw = typeof args === 'string'
|
|
@@ -64,25 +280,22 @@ function parseArgs(args: unknown): { sub: (typeof SUBCOMMANDS)[number] | 'index'
|
|
|
64
280
|
export function installConfigCommand(deps: ConfigCommandDeps): void {
|
|
65
281
|
const { pi } = deps;
|
|
66
282
|
const collect = deps.collect ?? collectConfigSnapshot;
|
|
67
|
-
const reportDir = deps.reportDir ?? ((cwd: string) => join(cwd,
|
|
283
|
+
const reportDir = deps.reportDir ?? ((cwd: string) => join(cwd, '.pi-herdr'));
|
|
68
284
|
|
|
69
|
-
pi.registerCommand(
|
|
285
|
+
pi.registerCommand('pier-config', {
|
|
70
286
|
description:
|
|
71
|
-
'Show pier configuration (
|
|
287
|
+
'Show pier configuration (4 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
288
|
getArgumentCompletions: (prefix: string) => {
|
|
73
289
|
const tokens = (prefix ?? '').split(/\s+/);
|
|
74
290
|
const head = tokens[0] ?? '';
|
|
75
291
|
if (tokens.length <= 1) {
|
|
76
|
-
|
|
77
|
-
return candidates
|
|
292
|
+
return ['show', 'check', 'doc', 'doctor', 'all', ...PLANE_IDS]
|
|
78
293
|
.filter((c) => c.startsWith(head))
|
|
79
294
|
.map((c) => ({ value: c, label: c, description: c === 'doc' ? 'write a config report file' : undefined }));
|
|
80
295
|
}
|
|
81
296
|
if (head === 'show') {
|
|
82
297
|
const last = tokens[tokens.length - 1] ?? '';
|
|
83
|
-
return ['all', ...PLANE_IDS]
|
|
84
|
-
.filter((c) => c.startsWith(last))
|
|
85
|
-
.map((c) => ({ value: c, label: c }));
|
|
298
|
+
return ['all', ...PLANE_IDS].filter((c) => c.startsWith(last)).map((c) => ({ value: c, label: c }));
|
|
86
299
|
}
|
|
87
300
|
return null;
|
|
88
301
|
},
|
|
@@ -110,24 +323,22 @@ export function installConfigCommand(deps: ConfigCommandDeps): void {
|
|
|
110
323
|
const { sub, rest } = parseArgs(args);
|
|
111
324
|
|
|
112
325
|
if (sub === 'check') {
|
|
113
|
-
emit([`pier config check (workspace trusted: ${snapshot.workspaceTrusted})`, ...
|
|
326
|
+
emit([`pier config check (workspace trusted: ${snapshot.workspaceTrusted})`, ...renderGuide(snapshot, 'check')].join('\n'));
|
|
114
327
|
return;
|
|
115
328
|
}
|
|
116
329
|
|
|
117
330
|
if (sub === 'doc') {
|
|
118
|
-
const target = rest
|
|
119
|
-
? (isAbsolute(rest) ? rest : resolve(cwd, rest))
|
|
120
|
-
: join(reportDir(cwd), CONFIG_REPORT_FILENAME);
|
|
331
|
+
const target = rest ? (isAbsolute(rest) ? rest : resolve(cwd, rest)) : join(reportDir(cwd), 'config-report.md');
|
|
121
332
|
try {
|
|
122
333
|
mkdirSync(dirname(target), { recursive: true });
|
|
123
334
|
writeFileSync(
|
|
124
335
|
target,
|
|
125
|
-
guideReportMarkdown(snapshot, { generatedAt: new Date().toISOString(), cwd, piVersion
|
|
336
|
+
guideReportMarkdown(snapshot, { generatedAt: new Date().toISOString(), cwd, piVersion }),
|
|
126
337
|
'utf8',
|
|
127
338
|
);
|
|
128
339
|
emit(
|
|
129
|
-
`pier config report written: ${target}`
|
|
130
|
-
(rest ? '' : ' (add `.pi-herdr/config-report.md` to .gitignore if you do not want it tracked)'),
|
|
340
|
+
`pier config report written: ${target}`
|
|
341
|
+
+ (rest ? '' : ' (add `.pi-herdr/config-report.md` to .gitignore if you do not want it tracked)'),
|
|
131
342
|
);
|
|
132
343
|
} catch (err) {
|
|
133
344
|
emit(`pier-config: could not write report (${err instanceof Error ? err.message : String(err)})`, 'error');
|
|
@@ -136,41 +347,37 @@ export function installConfigCommand(deps: ConfigCommandDeps): void {
|
|
|
136
347
|
}
|
|
137
348
|
|
|
138
349
|
if (sub === 'doctor') {
|
|
139
|
-
// B9/B10: one place
|
|
140
|
-
// errors
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
formatSwallowedErrors(),
|
|
152
|
-
].join('\n'),
|
|
153
|
-
);
|
|
350
|
+
// B9/B10: one place for every pier option (canonical name, effective value, source) and the
|
|
351
|
+
// errors deliberately swallowed this session — otherwise a failing best-effort path stays invisible.
|
|
352
|
+
emit([
|
|
353
|
+
'pier doctor',
|
|
354
|
+
'',
|
|
355
|
+
`wire: herdr protocol ${HERDR_PROTOCOL_EXPECTED} (pinned by test/fixtures/herdr-contract.json)`,
|
|
356
|
+
'',
|
|
357
|
+
'options (canonical PIER_*, legacy PI_HERDR_* alias accepted):',
|
|
358
|
+
...formatOptionRows(),
|
|
359
|
+
'',
|
|
360
|
+
formatSwallowedErrors(),
|
|
361
|
+
].join('\n'));
|
|
154
362
|
return;
|
|
155
363
|
}
|
|
156
364
|
|
|
157
365
|
if (sub === 'show') {
|
|
158
366
|
const plane = rest.trim();
|
|
159
|
-
const selector = plane === '' || plane === 'all' ? 'all' :
|
|
160
|
-
if (selector
|
|
367
|
+
const selector = plane === '' || plane === 'all' ? 'all' : PLANE_IDS.find((id) => id === plane);
|
|
368
|
+
if (!selector) {
|
|
161
369
|
emit(`pier-config: unknown plane "${plane}" — expected one of ${PLANE_IDS.join(', ')} or all`, 'warning');
|
|
162
370
|
return;
|
|
163
371
|
}
|
|
164
|
-
emit(
|
|
372
|
+
emit(renderGuide(snapshot, selector).join('\n'));
|
|
165
373
|
return;
|
|
166
374
|
}
|
|
167
375
|
|
|
168
|
-
|
|
169
|
-
emit([...guideIndexLines(snapshot), '', 'asking the agent to guide the change...'].join('\n'));
|
|
376
|
+
emit([...renderGuide(snapshot, 'index'), '', 'asking the agent to guide the change...'].join('\n'));
|
|
170
377
|
try {
|
|
171
378
|
// Call on the receiver: the ExtensionAPI method must not be detached from `pi`.
|
|
172
379
|
(pi as { sendMessage?: (msg: unknown, opts: unknown) => void }).sendMessage?.(
|
|
173
|
-
{ customType:
|
|
380
|
+
{ customType: 'pi-herdr.config-guide', content: CONFIG_GUIDANCE_PROMPT, display: false },
|
|
174
381
|
{ triggerTurn: true },
|
|
175
382
|
);
|
|
176
383
|
} catch (err) {
|
package/src/dashboard-command.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { HerdrClientLike, HerdrEnv } from './herdr-client.ts';
|
|
2
|
-
import type
|
|
2
|
+
import { isTodoStatus, type TodoItem, type TodoStatus } from './vocab.ts';
|
|
3
3
|
|
|
4
4
|
export interface DashboardCommandDeps {
|
|
5
5
|
pi: {
|
|
@@ -11,6 +11,25 @@ export interface DashboardCommandDeps {
|
|
|
11
11
|
getHeldLocks?: () => readonly string[];
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/** SUMMARY_ORDER fixes the summary-line order; LIVE_ROWS adds the detail line below it.
|
|
15
|
+
* `abandoned` is counted in the total but never listed. */
|
|
16
|
+
const SUMMARY_ORDER = [
|
|
17
|
+
{ status: 'completed', label: 'done' },
|
|
18
|
+
{ status: 'in_progress', label: 'working' },
|
|
19
|
+
{ status: 'blocked', label: 'blocked' },
|
|
20
|
+
{ status: 'pending', label: 'pending' },
|
|
21
|
+
] as const;
|
|
22
|
+
|
|
23
|
+
const LIVE_ROWS = [
|
|
24
|
+
{ status: 'in_progress', label: 'Active', glyph: '▶', line: (todo: TodoItem) => todo.content },
|
|
25
|
+
{
|
|
26
|
+
status: 'blocked',
|
|
27
|
+
label: 'Blocked',
|
|
28
|
+
glyph: '■',
|
|
29
|
+
line: (todo: TodoItem) => `${todo.content}${todo.blocker ? ` (${todo.blocker})` : ''}`,
|
|
30
|
+
},
|
|
31
|
+
] as const;
|
|
32
|
+
|
|
14
33
|
export function formatStandaloneDashboard(opts: {
|
|
15
34
|
todos: readonly TodoItem[];
|
|
16
35
|
locks: readonly string[];
|
|
@@ -22,26 +41,27 @@ export function formatStandaloneDashboard(opts: {
|
|
|
22
41
|
lines.push('Mode: Standalone (Non-Herdr / Local Session)');
|
|
23
42
|
lines.push('--------------------------------------------------------------------------------');
|
|
24
43
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
44
|
+
if (opts.todos.length === 0) {
|
|
45
|
+
lines.push('Todos: (none)');
|
|
46
|
+
} else {
|
|
47
|
+
const groups: Record<TodoStatus, TodoItem[]> = {
|
|
48
|
+
pending: [], in_progress: [], completed: [], blocked: [], abandoned: [],
|
|
49
|
+
};
|
|
50
|
+
// Corrupt snapshots can carry any status; a prototype key would make `groups[...].push` throw.
|
|
51
|
+
const todos = opts.todos.filter((todo) => isTodoStatus(todo.status));
|
|
52
|
+
for (const todo of todos) groups[todo.status].push(todo);
|
|
53
|
+
|
|
31
54
|
lines.push(
|
|
32
|
-
`Todos: ${todos.length} total (${
|
|
55
|
+
`Todos: ${todos.length} total (${SUMMARY_ORDER.map((row) => `${groups[row.status].length} ${row.label}`).join(', ')})`,
|
|
33
56
|
);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (blocked.length > 0) {
|
|
38
|
-
lines.push(`Blocked: ■ ${blocked.map((t) => `${t.content}${t.blocker ? ` (${t.blocker})` : ''}`).join(', ')}`);
|
|
57
|
+
for (const row of LIVE_ROWS) {
|
|
58
|
+
const items = groups[row.status];
|
|
59
|
+
if (items.length > 0) lines.push(`${row.label}: ${row.glyph} ${items.map(row.line).join(', ')}`);
|
|
39
60
|
}
|
|
40
|
-
|
|
41
|
-
|
|
61
|
+
// Recent completions only carry signal when nothing is in flight.
|
|
62
|
+
if (groups.in_progress.length === 0 && groups.blocked.length === 0 && groups.completed.length > 0) {
|
|
63
|
+
lines.push(`Completed: ✓ ${groups.completed.slice(-3).map((todo) => todo.content).join(', ')}`);
|
|
42
64
|
}
|
|
43
|
-
} else {
|
|
44
|
-
lines.push('Todos: (none)');
|
|
45
65
|
}
|
|
46
66
|
|
|
47
67
|
const locks = opts.locks;
|
|
@@ -58,7 +78,6 @@ export function installDashboardCommand(deps: DashboardCommandDeps): void {
|
|
|
58
78
|
handler: async (_args, ctx) => {
|
|
59
79
|
const ui = (ctx as { ui?: { notify?: (text: string, level?: string) => void } }).ui;
|
|
60
80
|
|
|
61
|
-
// Level 1 / Level 2: Herdr environment
|
|
62
81
|
if (deps.client.available && deps.env) {
|
|
63
82
|
try {
|
|
64
83
|
const res = await deps.client.openPluginPane({
|
|
@@ -74,7 +93,7 @@ export function installDashboardCommand(deps: DashboardCommandDeps): void {
|
|
|
74
93
|
}
|
|
75
94
|
return;
|
|
76
95
|
} catch {
|
|
77
|
-
//
|
|
96
|
+
// Plugin missing or socket error: fall through to the Level 3 local view.
|
|
78
97
|
}
|
|
79
98
|
}
|
|
80
99
|
|