pi-pier 0.4.2 → 0.4.3
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/package.json +1 -1
- package/schemas/efficiency-config.schema.json +20 -20
- package/schemas/role-manifest.schema.json +9 -9
- package/src/efficiency-config-core.ts +19 -19
- package/src/index-notices.ts +1 -1
- package/src/index.ts +2 -2
- package/src/manifest-compose.ts +4 -4
- package/src/plugins/todo.ts +1 -1
- package/src/reducer-invoker.ts +1 -1
- package/src/role-loader.ts +8 -8
- package/src/role-manifest.ts +22 -22
- package/src/roles/master.json +1 -1
- package/src/roles/worker-default.json +1 -1
- package/src/tree-view.ts +10 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-pier",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "pier: pi extension that fuses pi agent sessions with the herdr pane/tab orchestrator — todo_write loop closure + interactive subagents in isolated panes",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -8,15 +8,15 @@
|
|
|
8
8
|
"onlineContextCompact": {
|
|
9
9
|
"type": "object",
|
|
10
10
|
"properties": {
|
|
11
|
-
"enabled": { "type": "boolean", "default": false, "description": "
|
|
12
|
-
"logEnabled": { "type": "boolean", "default": false, "description": "
|
|
11
|
+
"enabled": { "type": "boolean", "default": false, "description": "Enable todo-driven online context compaction" },
|
|
12
|
+
"logEnabled": { "type": "boolean", "default": false, "description": "Log online compaction decisions and savings" },
|
|
13
13
|
"cacheWriteReadRatio": {
|
|
14
14
|
"oneOf": [
|
|
15
15
|
{ "type": "number", "minimum": 0 },
|
|
16
16
|
{ "type": "string", "enum": ["auto"] }
|
|
17
17
|
],
|
|
18
18
|
"default": "auto",
|
|
19
|
-
"description": "KV
|
|
19
|
+
"description": "KV cache write-to-read cost ratio. auto derives it from ctx.model.cost"
|
|
20
20
|
},
|
|
21
21
|
"firstCompactionRequestScale": { "type": "number", "default": 2.0, "minimum": 1.0 },
|
|
22
22
|
"subsequentCompactionMargin": { "type": "number", "default": 1.5, "minimum": 1.0 },
|
|
@@ -24,13 +24,13 @@
|
|
|
24
24
|
"type": "string",
|
|
25
25
|
"enum": ["inherit", "off", "minimal", "low", "medium", "high", "xhigh", "max"],
|
|
26
26
|
"default": "low",
|
|
27
|
-
"description": "
|
|
27
|
+
"description": "Thinking level cap for compaction summary requests (only lowers, never raises; the session level is restored afterwards). inherit keeps the session level"
|
|
28
28
|
},
|
|
29
29
|
"keepRecentTokens": {
|
|
30
30
|
"type": "integer",
|
|
31
31
|
"default": 20000,
|
|
32
32
|
"minimum": 1000,
|
|
33
|
-
"description": "
|
|
33
|
+
"description": "Native compaction keep-recent window size (also used for archiveTokens estimation and the nativeCompactionFeasible precheck). Defaults to inheriting pi's compaction.keepRecentTokens setting (20000 when unset)."
|
|
34
34
|
}
|
|
35
35
|
},
|
|
36
36
|
"additionalProperties": false
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"observationPack": {
|
|
39
39
|
"type": "object",
|
|
40
40
|
"properties": {
|
|
41
|
-
"enabled": { "type": "boolean", "default": false, "description": "
|
|
42
|
-
"logEnabled": { "type": "boolean", "default": false, "description": "
|
|
41
|
+
"enabled": { "type": "boolean", "default": false, "description": "Enable hot/cold-tiered packing of large tool outputs" },
|
|
42
|
+
"logEnabled": { "type": "boolean", "default": false, "description": "Log observation packing and recall" },
|
|
43
43
|
"thresholdBytes": { "type": "integer", "default": 10240, "minimum": 1024 },
|
|
44
44
|
"fullSends": { "type": "integer", "default": 2, "minimum": 1 },
|
|
45
45
|
"recallChunkBytes": { "type": "integer", "default": 16384, "minimum": 1024 },
|
|
@@ -50,31 +50,31 @@
|
|
|
50
50
|
"evidencePreservingReducer": {
|
|
51
51
|
"type": "object",
|
|
52
52
|
"properties": {
|
|
53
|
-
"enabled": { "type": "boolean", "default": false, "description": "
|
|
54
|
-
"logEnabled": { "type": "boolean", "default": false, "description": "
|
|
53
|
+
"enabled": { "type": "boolean", "default": false, "description": "Enable fidelity-preserving refinement of long test logs" },
|
|
54
|
+
"logEnabled": { "type": "boolean", "default": false, "description": "Log refinement comparisons, durations and fallbacks" },
|
|
55
55
|
"model": {
|
|
56
56
|
"type": "string",
|
|
57
|
-
"description": "
|
|
57
|
+
"description": "Dedicated lightweight Reducer model (provider/model). Defaults to the current model and logs an economy hint"
|
|
58
58
|
},
|
|
59
59
|
"minBytes": { "type": "integer", "default": 4096, "minimum": 512 },
|
|
60
60
|
"maxChars": { "type": "integer", "default": 600000 },
|
|
61
61
|
"maxOutputTokens": { "type": "integer", "default": 2048 },
|
|
62
|
-
"timeoutMs": { "type": "integer", "default": 5000, "description": "
|
|
63
|
-
"localOnly": { "type": "boolean", "default": false, "description": "
|
|
62
|
+
"timeoutMs": { "type": "integer", "default": 5000, "description": "Upper bound for the synchronous interception timeout; defaults to 5s to avoid blocking" },
|
|
63
|
+
"localOnly": { "type": "boolean", "default": false, "description": "Archive long logs locally only and skip model refinement" }
|
|
64
64
|
},
|
|
65
65
|
"additionalProperties": false
|
|
66
66
|
},
|
|
67
67
|
"jev": {
|
|
68
68
|
"type": "object",
|
|
69
|
-
"description": "jev
|
|
69
|
+
"description": "jev decision layer (RFC docs/rfc-jev-integration.md): optional System One classification calls, fail-open at each point",
|
|
70
70
|
"properties": {
|
|
71
|
-
"enabled": { "type": "boolean", "default": false, "description": "
|
|
72
|
-
"logEnabled": { "type": "boolean", "default": false, "description": "
|
|
73
|
-
"baseUrl": { "type": "string", "description": "API
|
|
74
|
-
"model": { "type": "string", "default": "jev-1.13.0", "description": "
|
|
75
|
-
"timeoutMs": { "type": "integer", "default": 2000, "minimum": 500, "description": "
|
|
76
|
-
"minConfidence": { "type": "number", "default": 0.6, "minimum": 0, "maximum": 1, "description": "Choice/Score
|
|
77
|
-
"apiKey": { "type": "string", "description": "API key
|
|
71
|
+
"enabled": { "type": "boolean", "default": false, "description": "Enable the jev decision layer (calls are only made once apiKey is configured)" },
|
|
72
|
+
"logEnabled": { "type": "boolean", "default": false, "description": "Write efficiency-logs/jev.jsonl (no request/response bodies)" },
|
|
73
|
+
"baseUrl": { "type": "string", "description": "API base URL override (proxy/gateway); defaults to https://api.typesafe.ai" },
|
|
74
|
+
"model": { "type": "string", "default": "jev-1.13.0", "description": "Pinned versioned model id; aliases drift silently and skew calibration thresholds" },
|
|
75
|
+
"timeoutMs": { "type": "integer", "default": 2000, "minimum": 500, "description": "Total budget per call (hard kill via AbortController; the SDK has no total-budget mode)" },
|
|
76
|
+
"minConfidence": { "type": "number", "default": 0.6, "minimum": 0, "maximum": 1, "description": "Minimum confidence for accepting a Choice/Score answer; below it the answer is treated as unanswered" },
|
|
77
|
+
"apiKey": { "type": "string", "description": "API key. Resolution order: PIER_JEV_API_KEY env var > this field > TYPESAFE_API_KEY env var" }
|
|
78
78
|
},
|
|
79
79
|
"additionalProperties": false
|
|
80
80
|
}
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://herdr-pi.local/schemas/role-manifest.schema.json",
|
|
4
|
-
"title": "herdr-pi role manifest
|
|
5
|
-
"description": "
|
|
4
|
+
"title": "herdr-pi role manifest (D76 v2)",
|
|
5
|
+
"description": "Tier-2 role manifest contract: version (P2 semantic version) + roleType (P3 planner/executor/hybrid) + tri-state rules (P1 allow/ask/deny). Runtime validation is the hand-written zero-dependency implementation in src/role-manifest.ts; this file is the contract document.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"properties": {
|
|
8
|
-
"role": { "type": "string", "pattern": "^[a-z0-9-]+$", "description": "
|
|
9
|
-
"version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+$", "description": "
|
|
10
|
-
"model": { "type": "string", "pattern": "^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$", "description": "
|
|
8
|
+
"role": { "type": "string", "pattern": "^[a-z0-9-]+$", "description": "Role name; must match the file name" },
|
|
9
|
+
"version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+$", "description": "Semantic version x.y.z" },
|
|
10
|
+
"model": { "type": "string", "pattern": "^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$", "description": "Per-role model routing, provider/model (WS-D10; optional = process default)" },
|
|
11
11
|
"guidelines": {
|
|
12
12
|
"type": "array",
|
|
13
13
|
"minItems": 1,
|
|
14
14
|
"maxItems": 20,
|
|
15
15
|
"items": { "type": "string", "minLength": 1 },
|
|
16
|
-
"description": "P0 per-role
|
|
16
|
+
"description": "P0 per-role behavior constraints (RFC §4.6): constraints on \"how to use\" tools that the tool set cannot express, injected each turn as the pier-role prompt section; omit instead of passing an empty array"
|
|
17
17
|
},
|
|
18
18
|
"manifest": {
|
|
19
19
|
"type": "object",
|
|
@@ -23,17 +23,17 @@
|
|
|
23
23
|
"minItems": 1,
|
|
24
24
|
"items": { "type": "string", "pattern": "^[a-z0-9_-]+$" },
|
|
25
25
|
"uniqueItems": true,
|
|
26
|
-
"description": "
|
|
26
|
+
"description": "Baseline tools; must include todo_write and ask_user_question (coordination tools, enforced by runtime validation)"
|
|
27
27
|
},
|
|
28
28
|
"rules": {
|
|
29
29
|
"type": "object",
|
|
30
30
|
"propertyNames": { "pattern": "^[a-z0-9_*-]+$" },
|
|
31
31
|
"additionalProperties": { "enum": ["allow", "ask", "deny"] },
|
|
32
|
-
"description": "
|
|
32
|
+
"description": "Tri-state permissions; * is the wildcard default; optional = {\"*\":\"allow\"}"
|
|
33
33
|
},
|
|
34
34
|
"unknownTools": {
|
|
35
35
|
"enum": ["allow", "deny"],
|
|
36
|
-
"description": "D82
|
|
36
|
+
"description": "D82 unknown-tool stance (default deny): allow = user-installed extensions are visible and usable by default (master/worker); deny = anything outside the list is invisible. Orthogonal to the rules * wildcard (visibility vs enforcement)"
|
|
37
37
|
}
|
|
38
38
|
},
|
|
39
39
|
"required": ["tools"],
|
|
@@ -156,19 +156,19 @@ function issueFor(section: string, field: FieldSpec): string {
|
|
|
156
156
|
const path = `${section}.${field.key}`;
|
|
157
157
|
switch (field.kind) {
|
|
158
158
|
case 'bool':
|
|
159
|
-
return `${path}
|
|
159
|
+
return `${path} must be a boolean`;
|
|
160
160
|
case 'int':
|
|
161
|
-
return `${path}
|
|
161
|
+
return `${path} must be an integer >= ${field.min}`;
|
|
162
162
|
case 'number':
|
|
163
163
|
return field.max === undefined
|
|
164
|
-
? `${path}
|
|
165
|
-
: `${path}
|
|
164
|
+
? `${path} must be a number >= ${field.minText ?? field.min}`
|
|
165
|
+
: `${path} must be a number between ${field.min} and ${field.max}`;
|
|
166
166
|
case 'ratio':
|
|
167
|
-
return `${path}
|
|
167
|
+
return `${path} must be "auto" or a non-negative finite number`;
|
|
168
168
|
case 'enum':
|
|
169
|
-
return `${path}
|
|
169
|
+
return `${path} must be one of ${(field.values ?? []).join(' | ')}`;
|
|
170
170
|
default:
|
|
171
|
-
return `${path}
|
|
171
|
+
return `${path} must be a non-empty string`;
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
|
|
@@ -229,15 +229,15 @@ export function validateEfficiencyConfig(raw: unknown): ValidateConfigResult {
|
|
|
229
229
|
const config = defaults();
|
|
230
230
|
if (!isPlainObject(raw)) {
|
|
231
231
|
for (const section of SECTIONS) config[section.name].enabled = false;
|
|
232
|
-
return { ok: false, config, issues: ['efficiency config
|
|
232
|
+
return { ok: false, config, issues: ['efficiency config must be a JSON object'] };
|
|
233
233
|
}
|
|
234
234
|
|
|
235
235
|
const issues: string[] = [];
|
|
236
236
|
for (const key of Object.keys(raw)) {
|
|
237
|
-
if (key !== 'version' && !SECTIONS.some((s) => s.name === key)) issues.push(
|
|
237
|
+
if (key !== 'version' && !SECTIONS.some((s) => s.name === key)) issues.push(`unknown top-level config key: "${key}"`);
|
|
238
238
|
}
|
|
239
239
|
if (raw.version !== undefined && raw.version !== 1) {
|
|
240
|
-
issues.push(`version
|
|
240
|
+
issues.push(`version must be 1 (got: ${JSON.stringify(raw.version)})`);
|
|
241
241
|
}
|
|
242
242
|
const topLevelInvalid = issues.length > 0;
|
|
243
243
|
|
|
@@ -247,10 +247,10 @@ export function validateEfficiencyConfig(raw: unknown): ValidateConfigResult {
|
|
|
247
247
|
const rawSection = raw[section.name];
|
|
248
248
|
if (rawSection !== undefined) {
|
|
249
249
|
if (!isPlainObject(rawSection)) {
|
|
250
|
-
issues.push(`${section.name}
|
|
250
|
+
issues.push(`${section.name} must be a JSON object`);
|
|
251
251
|
} else {
|
|
252
252
|
for (const key of Object.keys(rawSection)) {
|
|
253
|
-
if (!section.fields.some((f) => f.key === key)) issues.push(`${section.name}
|
|
253
|
+
if (!section.fields.some((f) => f.key === key)) issues.push(`${section.name} unknown config key: "${key}"`);
|
|
254
254
|
}
|
|
255
255
|
for (const field of section.fields) {
|
|
256
256
|
if (!applyField(target, field, rawSection[field.key])) issues.push(issueFor(section.name, field));
|
|
@@ -337,10 +337,10 @@ function applyEnvOverrides(
|
|
|
337
337
|
continue;
|
|
338
338
|
}
|
|
339
339
|
const requirement =
|
|
340
|
-
override.kind === 'unit' ? '
|
|
341
|
-
: override.kind === 'int' ?
|
|
340
|
+
override.kind === 'unit' ? ' (must be 0 to 1)'
|
|
341
|
+
: override.kind === 'int' ? ` (must be an integer >= ${override.min})`
|
|
342
342
|
: '';
|
|
343
|
-
warn(
|
|
343
|
+
warn(`invalid environment variable ${override.env}="${raw}"${requirement}, ignored`);
|
|
344
344
|
}
|
|
345
345
|
}
|
|
346
346
|
|
|
@@ -361,13 +361,13 @@ export function resolveEfficiencyConfig(opts: ResolveConfigOptions = {}): Effici
|
|
|
361
361
|
let baseRaw: unknown = opts.userConfig;
|
|
362
362
|
if (opts.workspaceConfig !== undefined) {
|
|
363
363
|
if (isTrusted) baseRaw = opts.workspaceConfig;
|
|
364
|
-
else warn('
|
|
364
|
+
else warn('workspace efficiency config is not trusted (isProjectTrusted=false); workspace config safely ignored');
|
|
365
365
|
}
|
|
366
366
|
|
|
367
367
|
let resolved = defaults();
|
|
368
368
|
if (baseRaw !== undefined) {
|
|
369
369
|
const validated = validateEfficiencyConfig(baseRaw);
|
|
370
|
-
if (!validated.ok) warn(
|
|
370
|
+
if (!validated.ok) warn(`config has validation issues; safely fell back to defaults: ${validated.issues.join('; ')}`);
|
|
371
371
|
resolved = validated.config;
|
|
372
372
|
}
|
|
373
373
|
|
|
@@ -440,8 +440,8 @@ export function loadEfficiencyConfigFromDisk(opts: {
|
|
|
440
440
|
const warnFile = (label: string, filePath: string, error: unknown): void => {
|
|
441
441
|
opts.onWarning?.(`${label} ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
442
442
|
};
|
|
443
|
-
const workspaceConfig = readJsonConfig(wsPath, (error) => warnFile('
|
|
444
|
-
const userConfig = readJsonConfig(userPath, (error) => warnFile('
|
|
443
|
+
const workspaceConfig = readJsonConfig(wsPath, (error) => warnFile('cannot parse workspace efficiency config file', wsPath, error));
|
|
444
|
+
const userConfig = readJsonConfig(userPath, (error) => warnFile('cannot parse user efficiency config file', userPath, error));
|
|
445
445
|
const resolved = resolveEfficiencyConfig({
|
|
446
446
|
workspaceConfig,
|
|
447
447
|
userConfig,
|
package/src/index-notices.ts
CHANGED
|
@@ -14,7 +14,7 @@ export function collapseNotices(contents: readonly string[], max = NOTICE_MAX_SH
|
|
|
14
14
|
if (contents.length === 0) return null;
|
|
15
15
|
if (contents.length <= max) return contents.join('\n\n');
|
|
16
16
|
const hidden = contents.length - max;
|
|
17
|
-
const tail =
|
|
17
|
+
const tail = `…${hidden} more settlement(s) not shown individually. See the history ledger for full results (path formula in the subagent resume tool description); use subagent list for running agents.`;
|
|
18
18
|
return [...contents.slice(0, max), tail].join('\n\n');
|
|
19
19
|
}
|
|
20
20
|
|
package/src/index.ts
CHANGED
|
@@ -667,8 +667,8 @@ export default async function (pi: ExtensionAPI) {
|
|
|
667
667
|
// D96: master settled while background subagents still run → remind (worker port is unbound).
|
|
668
668
|
if (plan.wake && plan.notice && !isSubagent) {
|
|
669
669
|
const running = subagentPort.current?.listRunningSubs() ?? [];
|
|
670
|
-
const brief = running.map((s) => `${s.paneId} (${s.description})`).join('
|
|
671
|
-
const notice =
|
|
670
|
+
const brief = running.map((s) => `${s.paneId} (${s.description})`).join(', ');
|
|
671
|
+
const notice = `Note: ${running.length} background subagent(s) still running: ${brief}. If your task depends on them, wait for their settlement (subagent list to check status); if you will not wait, state why you are abandoning them.`;
|
|
672
672
|
void sendUserMessageAs(notice, 'followUp');
|
|
673
673
|
}
|
|
674
674
|
if (plan.compact && ctx && typeof ctx === 'object') {
|
package/src/manifest-compose.ts
CHANGED
|
@@ -36,7 +36,7 @@ export function composeManifest(sources: ManifestSources): ComposedManifest {
|
|
|
36
36
|
if (baseline.length === 0) {
|
|
37
37
|
throw new ManifestError(
|
|
38
38
|
'INVALID_ROLE_CONFIG',
|
|
39
|
-
'INVALID_ROLE_CONFIG:
|
|
39
|
+
'INVALID_ROLE_CONFIG: baseline tools must not be empty; todo_write and ask_user_question are required',
|
|
40
40
|
);
|
|
41
41
|
}
|
|
42
42
|
const candidates = new Set<string>([
|
|
@@ -62,9 +62,9 @@ export function composeManifest(sources: ManifestSources): ComposedManifest {
|
|
|
62
62
|
if (tools.length === 0) {
|
|
63
63
|
throw new ManifestError(
|
|
64
64
|
'EMPTY_MANIFEST',
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
`Trimmed manifest is empty (deny-all trap?). Baseline: ${baseline.join(', ') || '(empty)'}; ` +
|
|
66
|
+
`model suggestions: ${sources.modelSuggested.join(', ') || '(none)'}; ` +
|
|
67
|
+
`rules: ${JSON.stringify(rules)}`,
|
|
68
68
|
);
|
|
69
69
|
}
|
|
70
70
|
|
package/src/plugins/todo.ts
CHANGED
|
@@ -376,7 +376,7 @@ export default function todoPlugin(ctx: Context): void {
|
|
|
376
376
|
}
|
|
377
377
|
const age = archivedAgeMs();
|
|
378
378
|
const head = formatTodoSummary(todos.items)
|
|
379
|
-
+ (age != null ? ` · archived ${formatAge(age)}
|
|
379
|
+
+ (age != null ? ` · archived ${formatAge(age)} (no longer injected)` : '');
|
|
380
380
|
ui?.notify?.([head, ...body].join('\n'), 'info');
|
|
381
381
|
},
|
|
382
382
|
});
|
package/src/reducer-invoker.ts
CHANGED
|
@@ -105,7 +105,7 @@ function resolveCandidate(
|
|
|
105
105
|
if (!isTrusted) {
|
|
106
106
|
if (!untrustedWarningEmitted) {
|
|
107
107
|
untrustedWarningEmitted = true;
|
|
108
|
-
console.warn('[pi-herdr] EPR
|
|
108
|
+
console.warn('[pi-herdr] EPR refinement is safely disabled in untrusted projects');
|
|
109
109
|
}
|
|
110
110
|
return undefined;
|
|
111
111
|
}
|
package/src/role-loader.ts
CHANGED
|
@@ -98,7 +98,7 @@ export function listRoleNames(baseDir?: string, userDir?: string): string[] {
|
|
|
98
98
|
|
|
99
99
|
export function loadRoleConfig(name: string, opts?: LoadRoleOptions): RoleManifest {
|
|
100
100
|
if (typeof name !== 'string' || !ROLE_NAME_RE.test(name)) {
|
|
101
|
-
throw new RoleLoaderError('ROLE_NOT_FOUND', `role
|
|
101
|
+
throw new RoleLoaderError('ROLE_NOT_FOUND', `role not found or invalid name ([a-z0-9-]+): ${JSON.stringify(name)}`);
|
|
102
102
|
}
|
|
103
103
|
const fileName = `${name}.json`;
|
|
104
104
|
|
|
@@ -108,7 +108,7 @@ export function loadRoleConfig(name: string, opts?: LoadRoleOptions): RoleManife
|
|
|
108
108
|
text = opts.read(fileName);
|
|
109
109
|
} catch (err) {
|
|
110
110
|
const code = (err as { code?: unknown })?.code;
|
|
111
|
-
throw new RoleLoaderError('ROLE_NOT_FOUND', `role "${name}"
|
|
111
|
+
throw new RoleLoaderError('ROLE_NOT_FOUND', `role "${name}" not found (failed to read ${fileName}: ${typeof code === 'string' ? code : 'error'})`);
|
|
112
112
|
}
|
|
113
113
|
return parseAndValidate(name, fileName, text, 'builtin (src/roles/)');
|
|
114
114
|
}
|
|
@@ -123,7 +123,7 @@ export function loadRoleConfig(name: string, opts?: LoadRoleOptions): RoleManife
|
|
|
123
123
|
if (layerRead(layer.dir, fileName) != null) {
|
|
124
124
|
throw new RoleLoaderError(
|
|
125
125
|
'ROLE_RESERVED',
|
|
126
|
-
`role "${name}"
|
|
126
|
+
`role "${name}" is a reserved built-in name and cannot be defined in the ${layer.label} layer (built-in manifests take precedence; delete that file or rename it)`,
|
|
127
127
|
);
|
|
128
128
|
}
|
|
129
129
|
}
|
|
@@ -136,7 +136,7 @@ export function loadRoleConfig(name: string, opts?: LoadRoleOptions): RoleManife
|
|
|
136
136
|
}
|
|
137
137
|
throw new RoleLoaderError(
|
|
138
138
|
'ROLE_NOT_FOUND',
|
|
139
|
-
`role "${name}"
|
|
139
|
+
`role "${name}" not found (searched layers: ${layers.map((l) => l.label).join(' → ')})`,
|
|
140
140
|
);
|
|
141
141
|
}
|
|
142
142
|
|
|
@@ -145,22 +145,22 @@ function parseAndValidate(name: string, fileName: string, text: string, layerLab
|
|
|
145
145
|
try {
|
|
146
146
|
parsed = JSON.parse(text);
|
|
147
147
|
} catch (err) {
|
|
148
|
-
throw new RoleLoaderError('INVALID_ROLE_CONFIG', `role "${name}"
|
|
148
|
+
throw new RoleLoaderError('INVALID_ROLE_CONFIG', `role "${name}" (${layerLabel}) is not valid JSON: ${(err as Error).message}`);
|
|
149
149
|
}
|
|
150
150
|
const result = validateRoleManifest(parsed);
|
|
151
151
|
// `ok === false` (instead of `!ok`) is required to narrow without strictNullChecks.
|
|
152
152
|
if (result.ok === false) {
|
|
153
153
|
throw new RoleLoaderError(
|
|
154
154
|
'INVALID_ROLE_CONFIG',
|
|
155
|
-
`role "${name}"
|
|
155
|
+
`role "${name}" (${layerLabel}) failed validation:\n${result.issues.map((i) => ` - ${i}`).join('\n')}`,
|
|
156
156
|
result.issues,
|
|
157
157
|
);
|
|
158
158
|
}
|
|
159
159
|
if (result.value.role !== name) {
|
|
160
160
|
throw new RoleLoaderError(
|
|
161
161
|
'INVALID_ROLE_CONFIG',
|
|
162
|
-
`role
|
|
163
|
-
[`role
|
|
162
|
+
`role manifest name mismatch: role="${result.value.role}" in ${fileName} of ${layerLabel}`,
|
|
163
|
+
[`role field "${result.value.role}" does not match the requested "${name}"`],
|
|
164
164
|
);
|
|
165
165
|
}
|
|
166
166
|
return result.value;
|
package/src/role-manifest.ts
CHANGED
|
@@ -53,79 +53,79 @@ const ACTIONS: readonly PermissionAction[] = ['allow', 'ask', 'deny'];
|
|
|
53
53
|
export function validateRoleManifest(input: unknown): ValidateResult {
|
|
54
54
|
const issues: string[] = [];
|
|
55
55
|
if (!isPlainObject(input)) {
|
|
56
|
-
return { ok: false, code: 'INVALID_ROLE_CONFIG', issues: ['role
|
|
56
|
+
return { ok: false, code: 'INVALID_ROLE_CONFIG', issues: ['role manifest must be a JSON object'] };
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
const KNOWN_TOP = new Set(['role', 'version', 'model', 'description', 'guidelines', 'manifest', 'services']);
|
|
60
60
|
for (const k of Object.keys(input)) {
|
|
61
|
-
if (!KNOWN_TOP.has(k)) issues.push(
|
|
61
|
+
if (!KNOWN_TOP.has(k)) issues.push(`unknown top-level key "${k}" (not in the contract; check spelling)`);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
if (typeof input.role !== 'string' || !ROLE_NAME_RE.test(input.role)) {
|
|
65
|
-
issues.push(`role
|
|
65
|
+
issues.push(`role must be a [a-z0-9-]+ string, got ${JSON.stringify(input.role)}`);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
if (typeof input.version !== 'string' || !SEMVER_RE.test(input.version)) {
|
|
69
|
-
issues.push(`version
|
|
69
|
+
issues.push(`version must be three dot-separated numbers x.y.z (e.g. 1.0.0), got ${JSON.stringify(input.version)}`);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
if (input.model !== undefined) {
|
|
73
73
|
if (typeof input.model !== 'string' || !/^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/.test(input.model)) {
|
|
74
|
-
issues.push(`model
|
|
74
|
+
issues.push(`model must be in provider/model form (e.g. opencode-go/muse-spark-1.2-contributor), got ${JSON.stringify(input.model)}`);
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
if (input.description !== undefined && typeof input.description !== 'string') {
|
|
79
|
-
issues.push('description
|
|
79
|
+
issues.push('description must be a string');
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
if (input.guidelines !== undefined) {
|
|
83
83
|
if (!Array.isArray(input.guidelines)) {
|
|
84
|
-
issues.push('guidelines
|
|
84
|
+
issues.push('guidelines must be an array of strings');
|
|
85
85
|
} else if (input.guidelines.length === 0) {
|
|
86
|
-
issues.push('guidelines
|
|
86
|
+
issues.push('omit guidelines entirely instead of passing an empty array');
|
|
87
87
|
} else if (!input.guidelines.every((g) => typeof g === 'string' && g.trim() !== '')) {
|
|
88
|
-
issues.push('guidelines
|
|
88
|
+
issues.push('every guidelines entry must be a non-empty string');
|
|
89
89
|
} else if (input.guidelines.length > 20) {
|
|
90
|
-
issues.push('
|
|
90
|
+
issues.push('at most 20 guidelines (prompt budget protection)');
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
const m = input.manifest;
|
|
95
95
|
if (!isPlainObject(m)) {
|
|
96
|
-
issues.push('manifest
|
|
96
|
+
issues.push('manifest must be an object');
|
|
97
97
|
} else {
|
|
98
98
|
const KNOWN_MANIFEST = new Set(['tools', 'rules', 'unknownTools']);
|
|
99
99
|
for (const k of Object.keys(m)) {
|
|
100
|
-
if (!KNOWN_MANIFEST.has(k)) issues.push(`
|
|
100
|
+
if (!KNOWN_MANIFEST.has(k)) issues.push(`unknown key "${k}" in manifest`);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
if (m.unknownTools !== undefined && m.unknownTools !== 'allow' && m.unknownTools !== 'deny') {
|
|
104
|
-
issues.push(`manifest.unknownTools
|
|
104
|
+
issues.push(`manifest.unknownTools must be allow/deny, got ${JSON.stringify(m.unknownTools)}`);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
const tools = m.tools;
|
|
108
108
|
if (!Array.isArray(tools) || !tools.every((t) => typeof t === 'string' && TOOL_KEY_RE.test(t))) {
|
|
109
|
-
issues.push('manifest.tools
|
|
109
|
+
issues.push('manifest.tools must be a non-empty array of strings (tool names [a-z0-9_-])');
|
|
110
110
|
} else {
|
|
111
111
|
const seen = new Set<string>();
|
|
112
112
|
for (const t of tools) {
|
|
113
|
-
if (seen.has(t)) issues.push(`manifest.tools
|
|
113
|
+
if (seen.has(t)) issues.push(`duplicate entry in manifest.tools "${t}"`);
|
|
114
114
|
seen.add(t);
|
|
115
115
|
}
|
|
116
116
|
for (const need of COORDINATION_TOOLS) {
|
|
117
|
-
if (!seen.has(need)) issues.push(
|
|
117
|
+
if (!seen.has(need)) issues.push(`non-empty baseline constraint: manifest.tools must include "${need}" (coordination tool)`);
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
if (m.rules !== undefined) {
|
|
122
122
|
if (!isPlainObject(m.rules)) {
|
|
123
|
-
issues.push('manifest.rules
|
|
123
|
+
issues.push('manifest.rules must be an object');
|
|
124
124
|
} else {
|
|
125
125
|
for (const [k, v] of Object.entries(m.rules)) {
|
|
126
|
-
if (!TOOL_KEY_RE.test(k)) issues.push(`rules
|
|
126
|
+
if (!TOOL_KEY_RE.test(k)) issues.push(`rules key "${k}" is not a valid tool name ([a-z0-9_-] or *)`);
|
|
127
127
|
if (!ACTIONS.includes(v as PermissionAction)) {
|
|
128
|
-
issues.push(`rules["${k}"]
|
|
128
|
+
issues.push(`rules["${k}"] must be one of ${ACTIONS.join('/')}, got ${JSON.stringify(v)}`);
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
}
|
|
@@ -134,14 +134,14 @@ export function validateRoleManifest(input: unknown): ValidateResult {
|
|
|
134
134
|
|
|
135
135
|
if (input.services !== undefined) {
|
|
136
136
|
if (!isPlainObject(input.services)) {
|
|
137
|
-
issues.push('services
|
|
137
|
+
issues.push('services must be an object');
|
|
138
138
|
} else if (input.services.todos !== undefined) {
|
|
139
139
|
const t = input.services.todos;
|
|
140
140
|
if (!isPlainObject(t)) {
|
|
141
|
-
issues.push('services.todos
|
|
141
|
+
issues.push('services.todos must be an object');
|
|
142
142
|
} else {
|
|
143
143
|
if (t.mode !== undefined && t.mode !== 'serial' && t.mode !== 'parallel') {
|
|
144
|
-
issues.push(`services.todos.mode
|
|
144
|
+
issues.push(`services.todos.mode must be serial/parallel, got ${JSON.stringify(t.mode)}`);
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
}
|
package/src/roles/master.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"role": "master",
|
|
3
3
|
"version": "1.4.0",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "Master role (WS-D7 self-application; D83 mirror source): core tools (todos/coordination/subagent/terminal) + pi built-ins + web_search; D82 unknownTools=allow. v1.4.0: tool consolidation terminal 6→1, subagent 5→1",
|
|
5
5
|
"manifest": {
|
|
6
6
|
"unknownTools": "allow",
|
|
7
7
|
"tools": [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"role": "worker-default",
|
|
3
3
|
"version": "1.6.0",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "Default worker role (D83 inheritance): mirrors master.tools minus subagent/terminal (no nesting, resource ownership); excluded families explicitly denied — still banned under the D82 unknownTools=allow stance; todos serial. v1.6.0: removed report_progress (0 calls in a week, D125); v1.5.0: Phase 2 explicitly allowed report_progress; v1.4.0: refreshed the deny list after tool consolidation",
|
|
5
5
|
"manifest": {
|
|
6
6
|
"unknownTools": "allow",
|
|
7
7
|
"tools": [
|
package/src/tree-view.ts
CHANGED
|
@@ -66,7 +66,7 @@ export function workerLine(row: WorkerWidgetRow): string {
|
|
|
66
66
|
const counts = countsOf(items);
|
|
67
67
|
if (!cur) return `↳ ${ref} ${counts}`;
|
|
68
68
|
if (cur.status === 'blocked') {
|
|
69
|
-
const why = cur.blocker && cur.blocker !== cur.content ?
|
|
69
|
+
const why = cur.blocker && cur.blocker !== cur.content ? ` (${clip(cur.blocker, 24)})` : '';
|
|
70
70
|
return `↳ ${ref} ■ ${clip(cur.content, 40)}${why} ${counts}`;
|
|
71
71
|
}
|
|
72
72
|
return `↳ ${ref} ▶ ${clip(cur.content)} ${counts}`;
|
|
@@ -85,7 +85,7 @@ export function selfLine(items: readonly TodoItem[]): string | null {
|
|
|
85
85
|
const cur = currentOf(items);
|
|
86
86
|
if (!cur) return null;
|
|
87
87
|
const counts = countsOf(items);
|
|
88
|
-
if (cur.status === 'blocked') return `■
|
|
88
|
+
if (cur.status === 'blocked') return `■ waiting on: ${clip(cur.blocker || cur.content, 40)} ${counts}`;
|
|
89
89
|
return `▶ ${clip(cur.content)} ${counts}`;
|
|
90
90
|
}
|
|
91
91
|
|
|
@@ -100,8 +100,8 @@ export function unifiedWidgetLines(
|
|
|
100
100
|
// Human gate open: the question owns the fixed area (unchanged discipline from the old widget).
|
|
101
101
|
if ((opts?.blockedDepth ?? 0) > 0) {
|
|
102
102
|
return workers.length === 0 || items.length === 0
|
|
103
|
-
? [`${formatTodoSummary(items)} · /todos
|
|
104
|
-
: [`${formatTodoSummary(items)} · ↳×${workers.length} · /todos
|
|
103
|
+
? [`${formatTodoSummary(items)} · /todos for all`]
|
|
104
|
+
: [`${formatTodoSummary(items)} · ↳×${workers.length} · /todos for all`];
|
|
105
105
|
}
|
|
106
106
|
if (items.length === 0 && workers.length === 0) return [];
|
|
107
107
|
// Archived plans keep their degraded two-line form; workers stay visible underneath.
|
|
@@ -114,7 +114,7 @@ export function unifiedWidgetLines(
|
|
|
114
114
|
const budget = WIDGET_MAX_LINES - head.length - 1;
|
|
115
115
|
const shown = rows.slice(0, Math.max(budget, 0));
|
|
116
116
|
const hidden = rows.length - shown.length;
|
|
117
|
-
return [...head, ...shown, ...(hidden > 0 ? [
|
|
117
|
+
return [...head, ...shown, ...(hidden > 0 ? [`…+${hidden} more subagents · /pier-tree`] : [])];
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
/** Full delegation tree for /pier-tree: self todos in full + per-worker full lists + settled nodes. */
|
|
@@ -126,15 +126,15 @@ export function formatDelegationTree(opts: {
|
|
|
126
126
|
}): string[] {
|
|
127
127
|
const now = opts.now ?? Date.now();
|
|
128
128
|
const lines: string[] = ['==================== PIER DELEGATION TREE ===================='];
|
|
129
|
-
lines.push(`● master
|
|
129
|
+
lines.push(`● master (this pane) · ${opts.selfItems.length} items`);
|
|
130
130
|
lines.push(...renderTodoGroups(opts.selfItems).map((l) => ` ${l}`));
|
|
131
131
|
if (opts.workers.length > 0 || (opts.settled?.length ?? 0) > 0) lines.push(' │');
|
|
132
132
|
for (const w of opts.workers) {
|
|
133
133
|
const ref = w.slug ?? w.paneId;
|
|
134
134
|
const items = w.todos?.items;
|
|
135
135
|
const headSuffix = items && items.length > 0
|
|
136
|
-
? `· ${items.length}
|
|
137
|
-
: w.todos?.summary ? `· ${clip(w.todos.summary, 40)}` : '·
|
|
136
|
+
? `· ${items.length} items`
|
|
137
|
+
: w.todos?.summary ? `· ${clip(w.todos.summary, 40)}` : '· todos not synced';
|
|
138
138
|
lines.push(` ├─ ↳ ${ref} (${w.paneId} · running${w.kind && w.kind !== 'task' ? ` · ${w.kind}` : ''}) ${headSuffix}`);
|
|
139
139
|
if (items && items.length > 0) {
|
|
140
140
|
lines.push(...renderTodoGroups(toTodoItems(items)).map((l) => ` │ ${l}`));
|
|
@@ -143,10 +143,10 @@ export function formatDelegationTree(opts: {
|
|
|
143
143
|
for (const s of opts.settled ?? []) {
|
|
144
144
|
const ref = s.slug ?? s.paneId;
|
|
145
145
|
const age = s.settledAt ? ` · settled ${formatAge(Math.max(now - s.settledAt, 0))} ago` : ' · settled';
|
|
146
|
-
lines.push(` └─ ✓ ${ref} (${s.paneId}${age}) —
|
|
146
|
+
lines.push(` └─ ✓ ${ref} (${s.paneId}${age}) — result delivered in the settlement notice`);
|
|
147
147
|
}
|
|
148
148
|
lines.push('----------------------------------------------------------------');
|
|
149
|
-
lines.push('
|
|
149
|
+
lines.push('Hint: /todos edit your own list · subagent list interact with subagents · /dashboard ops panel');
|
|
150
150
|
return lines;
|
|
151
151
|
}
|
|
152
152
|
|