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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-pier",
3
- "version": "0.4.2",
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 缓存写入与读取成本比。auto 表示从 ctx.model.cost 自动推导"
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": "压缩摘要请求的 thinking 等级上限(只降不升,结束后恢复会话等级)。inherit 表示沿用会话等级"
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": "原生压缩保留区大小(同时用于 archiveTokens 估算与 nativeCompactionFeasible 预检)。缺省自动继承 pi 的 compaction.keepRecentTokens 设置(默认 20000)。"
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": "专用的轻量 Reducer 模型 (provider/model)。缺省使用当前模型并在日志中提示经济性"
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": "同步拦截超时上限,默认 5 秒避免阻塞" },
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 决策层(RFC docs/rfc-jev-integration.md):可选的 System One 分类调用,逐点 fail-open",
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": "是否启用 jev 决策层(还需配置 apiKey 才会真正发起调用)" },
72
- "logEnabled": { "type": "boolean", "default": false, "description": "是否写 efficiency-logs/jev.jsonl(不记录请求/响应 body)" },
73
- "baseUrl": { "type": "string", "description": "API 根地址覆盖(中转/网关);缺省 https://api.typesafe.ai" },
74
- "model": { "type": "string", "default": "jev-1.13.0", "description": "钉住的版本化模型 id;别名会静默漂移导致校准阈值失真" },
75
- "timeoutMs": { "type": "integer", "default": 2000, "minimum": 500, "description": "单次调用总预算(AbortController 硬杀,SDK 无总预算模式)" },
76
- "minConfidence": { "type": "number", "default": 0.6, "minimum": 0, "maximum": 1, "description": "Choice/Score 答案被采纳的最低置信度,低于则按未回答处理" },
77
- "apiKey": { "type": "string", "description": "API key。解析优先级:PIER_JEV_API_KEY 环境变量 > 此处 > TYPESAFE_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(D76 v2)",
5
- "description": "档2 role 档案契约:version(P2 语义版本)+ roleType(P3 planner/executor/hybrid)+ 三态 rules(P1 allow/ask/deny)。运行时校验为 src/role-manifest.ts 手写零依赖实现,本文件是合同文档。",
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": "语义版本 x.y.z" },
10
- "model": { "type": "string", "pattern": "^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$", "description": "按角色路由模型 provider/model(WS-D10;可省略 = 进程默认)" },
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 行为约束(RFC §4.6):工具集表达不了的“怎么用”约束,每轮作为 pier-role prompt section 注入;空数组请省略"
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": "基线工具;必含 todo_write 与 ask_user_question(协调工具,运行时校验强制)"
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": "三态权限;* 为通配符默认;可省略 = {\"*\":\"allow\"}"
32
+ "description": "Tri-state permissions; * is the wildcard default; optional = {\"*\":\"allow\"}"
33
33
  },
34
34
  "unknownTools": {
35
35
  "enum": ["allow", "deny"],
36
- "description": "D82 未知工具姿态(缺省 deny):allow = 用户安装的扩展默认可见可用(master/worker);deny = 清单外一律不可见。与 rules 的 * 通配分属两轴(可见性 vs 强制力)"
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} 必须是 boolean`;
159
+ return `${path} must be a boolean`;
160
160
  case 'int':
161
- return `${path} 必须是 >= ${field.min} 的整数`;
161
+ return `${path} must be an integer >= ${field.min}`;
162
162
  case 'number':
163
163
  return field.max === undefined
164
- ? `${path} 必须是 >= ${field.minText ?? field.min} 的数字`
165
- : `${path} 必须是 ${field.min} 到 ${field.max} 之间的数字`;
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} 必须是 "auto" 或非负有限数`;
167
+ return `${path} must be "auto" or a non-negative finite number`;
168
168
  case 'enum':
169
- return `${path} 必须是 ${(field.values ?? []).join(' | ')} 之一`;
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 必须是 JSON 对象'] };
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(`未知顶层配置项: "${key}"`);
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 必须是 1 (当前: ${JSON.stringify(raw.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} 必须是 JSON 对象`);
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} 未知配置项: "${key}"`);
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' ? '(需 0 到 1)'
341
- : override.kind === 'int' ? `(需 >= ${override.min} 的整数)`
340
+ override.kind === 'unit' ? ' (must be 0 to 1)'
341
+ : override.kind === 'int' ? ` (must be an integer >= ${override.min})`
342
342
  : '';
343
- warn(`无效环境变量 ${override.env}="${raw}"${requirement},忽略`);
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('工作区能效配置未被信任(isProjectTrusted=false),已安全忽略工作区配置');
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(`配置存在校验问题,已安全回退默认值: ${validated.issues.join('; ')}`);
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('无法解析工作区能效配置文件', wsPath, error));
444
- const userConfig = readJsonConfig(userPath, (error) => warnFile('无法解析用户能效配置文件', userPath, error));
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,
@@ -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 = `…另有 ${hidden} 条结算未逐条展示。全量结果看 history 台账(路径公式见 subagent resume 工具描述);在跑代理用 subagent list 查看。`;
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 = `注意:仍有 ${running.length} 个后台 subagent 在运行:${brief}。若你的任务依赖它们,请等待其结算(subagent list 查看状态);若不等待,请说明放弃原因。`;
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') {
@@ -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: 基线工具不能为空,至少需包含 todo_write 和 ask_user_question',
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
- `裁剪后 manifest 为空(deny-all trap?)。基线: ${baseline.join(', ') || '(空)'};` +
66
- `模型建议: ${sources.modelSuggested.join(', ') || '(无)'};` +
67
- `规则: ${JSON.stringify(rules)}`,
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
 
@@ -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
  });
@@ -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
  }
@@ -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 不存在或名字非法([a-z0-9-]+): ${JSON.stringify(name)}`);
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}" 不存在(读 ${fileName} 失败: ${typeof code === 'string' ? code : 'error'})`);
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}" 是内置保留名,不能在 ${layer.label} 层定义(内置档案优先;请删除该档案或改名)`,
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}" 不存在(查找层:${layers.map((l) => l.label).join(' → ')})`,
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}"(${layerLabel})不是合法 JSON: ${(err as Error).message}`);
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}"(${layerLabel})校验失败:\n${result.issues.map((i) => ` - ${i}`).join('\n')}`,
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 档案名不符:${layerLabel} 的 ${fileName} 内 role="${result.value.role}"`,
163
- [`role 字段 "${result.value.role}" 与请求的 "${name}" 不一致`],
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;
@@ -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 档案必须是 JSON 对象'] };
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(`未知顶层键 "${k}"(契约外字段,检查拼写)`);
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 必须是 [a-z0-9-]+ 字符串,收到 ${JSON.stringify(input.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 必须是 x.y.z 三段数字(如 1.0.0),收到 ${JSON.stringify(input.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 必须是 provider/model 形态(如 opencode-go/muse-spark-1.2-contributor),收到 ${JSON.stringify(input.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('guidelines 最多 20 条(prompt 预算保护)');
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(`manifest 内未知键 "${k}"`);
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 必须是 allow/deny,收到 ${JSON.stringify(m.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 必须是非空字符串数组(工具名 [a-z0-9_-])');
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 重复项 "${t}"`);
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(`基线非空约束:manifest.tools 必须包含 "${need}"(协调工具)`);
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 键 "${k}" 不是合法工具名([a-z0-9_-] 或 *)`);
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}"] 必须是 ${ACTIONS.join('/')} 之一,收到 ${JSON.stringify(v)}`);
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 必须是 serial/parallel,收到 ${JSON.stringify(t.mode)}`);
144
+ issues.push(`services.todos.mode must be serial/parallel, got ${JSON.stringify(t.mode)}`);
145
145
  }
146
146
  }
147
147
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "role": "master",
3
3
  "version": "1.4.0",
4
- "description": "主控角色(WS-D7 自应用;D83 镜像源):核心工具(todos/协调/subagent/terminal)+ pi 内建 + web_search;D82 unknownTools=allow。v1.4.0: 工具合并 terminal 6→1, subagent 5→1",
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": "默认worker角色(D83 继承制):镜像 master.tools 减 subagent/terminal(禁嵌套、资源归属);排除族显式 deny——在 D82 unknownTools=allow 姿态下仍持禁令;todos serial。v1.6.0: 删除 report_progress(一周 0 调用,D125);v1.5.0: Phase 2 显式放行 report_progress;v1.4.0: 工具合并后更新 deny 列表",
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 ? `(${clip(cur.blocker, 24)})` : '';
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 `■ 等人: ${clip(cur.blocker || cur.content, 40)} ${counts}`;
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 ? [`…另 ${hidden} 个子代理 · /pier-tree`] : [])];
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(本 pane)· ${opts.selfItems.length} 项`);
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)}` : '· todo 未同步';
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('提示: /todos 编辑自己的清单 · subagent list 交互子代理 · /dashboard ops 面板');
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