@zhushanwen/pi-system-prompt 1.1.5 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-system-prompt",
3
- "version": "1.1.5",
3
+ "version": "1.2.0",
4
4
  "description": "System prompt injection extension for Pi — reads config and appends to system prompt",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
7
- "xyz-agent": {
7
+ "taiji": {
8
8
  "role": "taiji"
9
9
  },
10
10
  "pi": {
@@ -32,7 +32,7 @@
32
32
  "vitest": "^4.1.8"
33
33
  },
34
34
  "dependencies": {
35
- "@zhushanwen/pi-extension-logger": "0.4.1"
35
+ "@zhushanwen/pi-extension-logger": "0.6.1"
36
36
  },
37
37
  "scripts": {
38
38
  "typecheck": "npx tsc --noEmit",
@@ -4,13 +4,19 @@
4
4
  * 覆盖(替换原 expect(true) 占位,R3 extension-api SUGGESTION #1):
5
5
  * - readJsonIfValid 解析边界:文件缺失 / 畸形 JSON / 顶层 array / 顶层原始值 → 全部收敛 defaults
6
6
  * - readSection 字段级防御:section 非对象、enabled 非 true、prompt 非字符串 / 空白 → 不注入
7
- * - before_agent_start 注入顺序:base → global instructions → append config(indexOf 链锁定,
7
+ * - capability 段三态锚点(设计 D6):v1 存量 json(无字段)→ 注入;显式布尔 false → 不注入;
8
+ * 损坏形态("enabled": "false" 字符串 / capability 非对象)→ 注入(与 readSection 解析方向相反)
9
+ * - before_agent_start 注入顺序:base → global instructions → capability → append(indexOf 链锁定,
8
10
  * swap 注入顺序两行的 mutant 会被顺序用例 kill)
9
11
  * - -nc / --no-context-files 守卫:global 注入跳过、append 不受影响
10
12
  * - global 候选选择:候选序优先、空白内容跳过继续找、目录缺失降级
11
13
  * - fail-safe:handler 全程 throw → return undefined + logger.error 可观测;logger 自身抛错的
12
14
  * 终极兜底 process.stderr.write;systemPrompt 非法类型的旧 quirk 锚定
13
15
  *
16
+ * 适配约定:capability 默认开(仅显式布尔 false 关闭)。聚焦 append/global 语义的既有用例
17
+ * 在 config 里显式 "capability":{"enabled":false} 隔离关注点;无法塞字段的用例(畸形 JSON /
18
+ * 文件缺失)期望值如实更新为含 capability 段。capability 自身行为由专项 describe 锚定。
19
+ *
14
20
  * mock 策略(参照 msg-id-mapper 测试模式):pi SDK import type 零运行时解析,
15
21
  * ExtensionAPI 用结构化桩;node:fs mock 后按路径分流(env 指向假目录,不碰真实文件系统)。
16
22
  *
@@ -36,10 +42,15 @@ vi.mock('node:fs', () => ({
36
42
  statSync: vi.fn(),
37
43
  }))
38
44
 
39
- const DATA_DIR = '/xyz-test/data'
40
- const GLOBAL_DIR = '/xyz-test/global-agents'
45
+ const DATA_DIR = '/taiji-test/data'
46
+ const GLOBAL_DIR = '/taiji-test/global-agents'
41
47
  const CONFIG_PATH = path.join(DATA_DIR, 'system-prompt.json')
42
48
 
49
+ /** capability 段 header 锚(文案变更须同步本锚,红 = 提醒评审段文本变化) */
50
+ const CAP_HEADER = '# TaiJi capabilities'
51
+ /** 既有用例隔离关注点用的「capability 显式关闭」config 片段 */
52
+ const CAP_OFF = '"capability": {"enabled": false}'
53
+
43
54
  /** hook 注册表桩(参照 msg-id-mapper harness 模式) */
44
55
  function createHarness(): { beforeAgentStart: (event: BeforeAgentStartEvent) => unknown } {
45
56
  const handlers = new Map<string, (...args: unknown[]) => unknown>()
@@ -114,15 +125,15 @@ function setupFs(setup: FsSetup = {}): void {
114
125
  })
115
126
  }
116
127
 
117
- const ENV_KEYS = ['XYZ_AGENT_DATA_DIR', 'XYZ_GLOBAL_AGENTS_DIR', 'PI_CODING_AGENT_DIR'] as const
128
+ const ENV_KEYS = ['TAIJI_AGENT_DATA_DIR', 'TAIJI_GLOBAL_AGENTS_DIR', 'PI_CODING_AGENT_DIR'] as const
118
129
  const savedEnv: Record<string, string | undefined> = {}
119
130
  const savedArgv = process.argv
120
131
 
121
132
  beforeEach(() => {
122
133
  vi.clearAllMocks() // 清跨用例的 fs mock 调用记录(「守卫不触发」类断言依赖零计数)
123
134
  for (const k of ENV_KEYS) savedEnv[k] = process.env[k]
124
- process.env.XYZ_AGENT_DATA_DIR = DATA_DIR
125
- process.env.XYZ_GLOBAL_AGENTS_DIR = GLOBAL_DIR
135
+ process.env.TAIJI_AGENT_DATA_DIR = DATA_DIR
136
+ process.env.TAIJI_GLOBAL_AGENTS_DIR = GLOBAL_DIR
126
137
  delete process.env.PI_CODING_AGENT_DIR
127
138
  setupFs()
128
139
  })
@@ -136,62 +147,64 @@ afterEach(() => {
136
147
  vi.restoreAllMocks()
137
148
  })
138
149
 
139
- describe('readJsonIfValid 解析边界(config 读取 → defaults 收敛)', () => {
140
- it('config 文件缺失(ENOENT)→ defaults → 无注入,返回 undefined', () => {
150
+ describe('readJsonIfValid 解析边界(config 读取 → defaults 收敛,capability 默认开)', () => {
151
+ // 本组 config 均无法塞字段(缺失/畸形/非对象),capability 走默认 true——
152
+ // defaults 的语义自 schema v2 起含「capability 段注入」,期望如实更新。
153
+ it('config 文件缺失(ENOENT)→ defaults → 仅注入 capability 段', () => {
141
154
  setupFs({ config: new Error("ENOENT: no such file or directory, open '" + CONFIG_PATH + "'") })
142
- expect(runHook('base prompt')).toBeUndefined()
155
+ expect(runHook('base prompt')).toEqual({ systemPrompt: expect.stringContaining(CAP_HEADER) })
143
156
  })
144
157
 
145
- it('config 畸形 JSON(parse throw)→ defaults → 无注入', () => {
158
+ it('config 畸形 JSON(parse throw)→ defaults → 仅注入 capability 段', () => {
146
159
  setupFs({ config: '{broken json' })
147
- expect(runHook('base prompt')).toBeUndefined()
160
+ expect(runHook('base prompt')).toEqual({ systemPrompt: expect.stringContaining(CAP_HEADER) })
148
161
  })
149
162
 
150
- it('config 顶层 array → isJsonObject 放行 quirk(数组不排除)→ 字段缺省收敛 defaults → 无注入', () => {
163
+ it('config 顶层 array → isJsonObject 放行 quirk(数组不排除)→ 字段缺省收敛 defaults → 仅 capability 段', () => {
151
164
  // R3 复核锚定的行为等价:顶层数组两版实现同走 typeof object 放行路径,无错误数据
152
165
  setupFs({ config: '[1, 2]' })
153
- expect(runHook('base prompt')).toBeUndefined()
166
+ expect(runHook('base prompt')).toEqual({ systemPrompt: expect.stringContaining(CAP_HEADER) })
154
167
  })
155
168
 
156
- it('config 顶层原始值(number / string / null 字面量)→ null → defaults → 无注入', () => {
169
+ it('config 顶层原始值(number / string / null 字面量)→ null → defaults → 仅 capability 段', () => {
157
170
  for (const bad of ['42', '"a string"', 'null', 'true']) {
158
171
  setupFs({ config: bad })
159
- expect(runHook('base prompt')).toBeUndefined()
172
+ expect(runHook('base prompt')).toEqual({ systemPrompt: expect.stringContaining(CAP_HEADER) })
160
173
  }
161
174
  })
162
175
  })
163
176
 
164
- describe('readSection 字段级防御(append section)', () => {
177
+ describe('readSection 字段级防御(append section;capability 显式关闭以隔离关注点)', () => {
165
178
  it('append section 非对象(null / 字符串)→ {enabled:false, prompt:""} → 不注入', () => {
166
179
  for (const section of ['null', '"just text"']) {
167
- setupFs({ config: `{"append": ${section}}` })
180
+ setupFs({ config: `{"append": ${section}, ${CAP_OFF}}` })
168
181
  expect(runHook('base prompt')).toBeUndefined()
169
182
  }
170
183
  })
171
184
 
172
185
  it('append.enabled 非 true(字符串 "true")→ 不视为开启 → 不注入', () => {
173
- setupFs({ config: '{"append": {"enabled": "true", "prompt": "extra"}}' })
186
+ setupFs({ config: `{"append": {"enabled": "true", "prompt": "extra"}, ${CAP_OFF}}` })
174
187
  expect(runHook('base prompt')).toBeUndefined()
175
188
  })
176
189
 
177
190
  it('append.prompt 非字符串(number)→ 缺省 "" → 不注入', () => {
178
- setupFs({ config: '{"append": {"enabled": true, "prompt": 123}}' })
191
+ setupFs({ config: `{"append": {"enabled": true, "prompt": 123}, ${CAP_OFF}}` })
179
192
  expect(runHook('base prompt')).toBeUndefined()
180
193
  })
181
194
 
182
195
  it('append.enabled true 但 prompt 纯空白 → trim 后为空 → 不注入', () => {
183
- setupFs({ config: '{"append": {"enabled": true, "prompt": " \\n\\t "}}' })
196
+ setupFs({ config: `{"append": {"enabled": true, "prompt": " \\n\\t "}, ${CAP_OFF}}` })
184
197
  expect(runHook('base prompt')).toBeUndefined()
185
198
  })
186
199
 
187
200
  it('append 合法(enabled true + 非空 prompt)→ 注入到 base 之后(\\n\\n 分隔)', () => {
188
- setupFs({ config: '{"append": {"enabled": true, "prompt": "APPEND-TEXT"}}' })
201
+ setupFs({ config: `{"append": {"enabled": true, "prompt": "APPEND-TEXT"}, ${CAP_OFF}}` })
189
202
  expect(runHook('base prompt')).toEqual({ systemPrompt: 'base prompt\n\nAPPEND-TEXT' })
190
203
  })
191
204
  })
192
205
 
193
- describe('before_agent_start 注入顺序(base → global → append)', () => {
194
- it('三段齐备 → base 在前、global 段居中、append 文本最后(indexOf 链锁定)', () => {
206
+ describe('before_agent_start 注入顺序(base → global → capability → append)', () => {
207
+ it('四段齐备 → base 最前、global 居中、capability 段次之、append 最后(indexOf 链锁定)', () => {
195
208
  setupFs({
196
209
  config: '{"append": {"enabled": true, "prompt": "APPEND-TEXT"}}',
197
210
  globalEntries: ['AGENTS.md'],
@@ -201,35 +214,106 @@ describe('before_agent_start 注入顺序(base → global → append)', () =
201
214
  expect(result).toEqual({ systemPrompt: expect.stringContaining('APPEND-TEXT') })
202
215
  const prompt = result!.systemPrompt as string
203
216
 
204
- // 顺序锚点:base 最前 → global header → global 内容 → append 文本最后
217
+ // 顺序锚点:base → global header → global 内容 → capability 段 → append 文本
205
218
  const iBase = prompt.indexOf('BASE-PROMPT')
206
219
  const iHeader = prompt.indexOf('# Global instructions')
207
220
  const iGlobal = prompt.indexOf('GLOBAL-CONTENT')
221
+ const iCap = prompt.indexOf(CAP_HEADER)
208
222
  const iAppend = prompt.indexOf('APPEND-TEXT')
209
223
  expect(iBase).toBeGreaterThanOrEqual(0)
210
224
  expect(iHeader).toBeGreaterThan(iBase)
211
225
  expect(iGlobal).toBeGreaterThan(iHeader)
212
- expect(iAppend).toBeGreaterThan(iGlobal)
226
+ expect(iCap).toBeGreaterThan(iGlobal)
227
+ expect(iAppend).toBeGreaterThan(iCap)
213
228
  expect(prompt.indexOf('APPEND-TEXT', iAppend + 1)).toBe(-1) // append 恰一次
229
+ expect(prompt.indexOf(CAP_HEADER, iCap + 1)).toBe(-1) // capability 段恰一次
214
230
  // global header 带真实注入路径(可追溯)
215
231
  expect(prompt).toContain(path.join(GLOBAL_DIR, 'AGENTS.md'))
216
232
  })
217
233
 
218
- it('global 无候选文件 → 只剩 base + append 两段', () => {
234
+ it('capability 段文案覆盖四点能力面(HTML 白名单 / 相对图片 / 相对链接双通道 / 远程图片不渲染)', () => {
235
+ setupFs({ config: '{"append": {"enabled": false, "prompt": ""}}' })
236
+ const prompt = runHook('BASE-PROMPT')!.systemPrompt as string
237
+ const capText = prompt.slice(prompt.indexOf(CAP_HEADER))
238
+ expect(capText).toContain('Inline HTML')
239
+ expect(capText).toContain('Relative image paths')
240
+ expect(capText).toContain('Relative links')
241
+ expect(capText).toContain('not rendered')
242
+ expect(capText).toContain('backticks')
243
+ })
244
+
245
+ it('capability 关闭(显式布尔 false)→ 段消失,其余段顺序不变', () => {
246
+ setupFs({
247
+ config: `{"append": {"enabled": true, "prompt": "APPEND-TEXT"}, ${CAP_OFF}}`,
248
+ globalEntries: ['AGENTS.md'],
249
+ globalFiles: { 'AGENTS.md': 'GLOBAL-CONTENT' },
250
+ })
251
+ const prompt = runHook('BASE-PROMPT')!.systemPrompt as string
252
+ expect(prompt).not.toContain(CAP_HEADER)
253
+ // 三段顺序保持 base → global → append(capability 摘除不动既有链路)
254
+ const iBase = prompt.indexOf('BASE-PROMPT')
255
+ const iGlobal = prompt.indexOf('GLOBAL-CONTENT')
256
+ const iAppend = prompt.indexOf('APPEND-TEXT')
257
+ expect(iBase).toBeGreaterThanOrEqual(0)
258
+ expect(iGlobal).toBeGreaterThan(iBase)
259
+ expect(iAppend).toBeGreaterThan(iGlobal)
260
+ })
261
+
262
+ it('global 无候选文件 → 只剩 base + capability + append 三段', () => {
219
263
  setupFs({
220
264
  config: '{"append": {"enabled": true, "prompt": "APPEND-TEXT"}}',
221
265
  globalEntries: [],
222
266
  })
223
267
  const result = runHook('BASE-PROMPT')
224
- expect(result).toEqual({ systemPrompt: 'BASE-PROMPT\n\nAPPEND-TEXT' })
225
- expect(result!.systemPrompt).not.toContain('# Global instructions')
268
+ const prompt = result!.systemPrompt as string
269
+ expect(prompt).not.toContain('# Global instructions')
270
+ expect(prompt).toContain(CAP_HEADER)
271
+ expect(prompt).toContain('APPEND-TEXT')
272
+ expect(prompt.indexOf(CAP_HEADER)).toBeGreaterThan(prompt.indexOf('BASE-PROMPT'))
273
+ expect(prompt.indexOf('APPEND-TEXT')).toBeGreaterThan(prompt.indexOf(CAP_HEADER))
274
+ })
275
+ })
276
+
277
+ describe('capability 段三态锚点(设计 D6:仅显式布尔 false 关闭,与 readSection 方向相反)', () => {
278
+ it('v1 存量 json(无 capability 字段)→ 默认开 → 注入', () => {
279
+ setupFs({ config: '{"version": 1, "replace": {"enabled": false, "prompt": ""}, "append": {"enabled": false, "prompt": ""}}' })
280
+ expect(runHook('base prompt')).toEqual({ systemPrompt: expect.stringContaining(CAP_HEADER) })
281
+ })
282
+
283
+ it('config 文件不存在 → 同 v1 语义 → 注入', () => {
284
+ setupFs()
285
+ expect(runHook('base prompt')).toEqual({ systemPrompt: expect.stringContaining(CAP_HEADER) })
286
+ })
287
+
288
+ it('显式 enabled: false(布尔)→ 关闭 → 不注入', () => {
289
+ setupFs({ config: `{"append": {"enabled": false, "prompt": ""}, ${CAP_OFF}}` })
290
+ expect(runHook('base prompt')).toBeUndefined()
291
+ })
292
+
293
+ it('显式 enabled: true → 注入', () => {
294
+ setupFs({ config: '{"append": {"enabled": false, "prompt": ""}, "capability": {"enabled": true}}' })
295
+ expect(runHook('base prompt')).toEqual({ systemPrompt: expect.stringContaining(CAP_HEADER) })
296
+ })
297
+
298
+ it('损坏形态:enabled 为字符串 "false"(非布尔)→ 不视为关闭 → 注入', () => {
299
+ setupFs({ config: '{"append": {"enabled": false, "prompt": ""}, "capability": {"enabled": "false"}}' })
300
+ expect(runHook('base prompt')).toEqual({ systemPrompt: expect.stringContaining(CAP_HEADER) })
301
+ })
302
+
303
+ it('损坏形态:capability 字段非对象(字符串 / null / 数组)→ 注入', () => {
304
+ for (const bad of ['"off"', 'null', '[1]']) {
305
+ setupFs({ config: `{"append": {"enabled": false, "prompt": ""}, "capability": ${bad}}` })
306
+ expect(runHook('base prompt')).toEqual({ systemPrompt: expect.stringContaining(CAP_HEADER) })
307
+ }
226
308
  })
227
309
  })
228
310
 
229
311
  describe('-nc / --no-context-files 守卫(contextFilesDisabled)', () => {
312
+ // capability 与守卫正交(内置段不受 --no-context-files 影响,该旗标只管上下文文件
313
+ // 发现),用例显式关 capability 隔离守卫语义。
230
314
  it('argv 含 --no-context-files → global 不注入,append 仍生效(用户显式退出不得溜回来)', () => {
231
315
  setupFs({
232
- config: '{"append": {"enabled": true, "prompt": "APPEND-TEXT"}}',
316
+ config: `{"append": {"enabled": true, "prompt": "APPEND-TEXT"}, ${CAP_OFF}}`,
233
317
  globalEntries: ['AGENTS.md'],
234
318
  globalFiles: { 'AGENTS.md': 'GLOBAL-CONTENT' },
235
319
  })
@@ -241,7 +325,7 @@ describe('-nc / --no-context-files 守卫(contextFilesDisabled)', () => {
241
325
 
242
326
  it('argv 含 -nc 短形式 → 同样跳过 global 注入(与 argv-mirror 两种形式一致)', () => {
243
327
  setupFs({
244
- config: '{"append": {"enabled": true, "prompt": "APPEND-TEXT"}}',
328
+ config: `{"append": {"enabled": true, "prompt": "APPEND-TEXT"}, ${CAP_OFF}}`,
245
329
  globalEntries: ['AGENTS.md'],
246
330
  globalFiles: { 'AGENTS.md': 'GLOBAL-CONTENT' },
247
331
  })
@@ -272,9 +356,11 @@ describe('global 候选文件选择(readGlobalAgentsFile)', () => {
272
356
  expect(result!.systemPrompt).toContain(path.join(GLOBAL_DIR, 'CLAUDE.md'))
273
357
  })
274
358
 
275
- it('global 目录不存在(readdirSync throw)→ 降级 null:不注入、不抛错', () => {
359
+ it('global 目录不存在(readdirSync throw)→ 降级 null:global 不注入、不抛错(capability 默认段仍在)', () => {
276
360
  setupFs({ globalEntries: new Error('ENOENT: no such directory') })
277
- expect(runHook('BASE-PROMPT')).toBeUndefined()
361
+ const result = runHook('BASE-PROMPT')
362
+ expect(result).toEqual({ systemPrompt: expect.stringContaining(CAP_HEADER) })
363
+ expect(result!.systemPrompt).not.toContain('# Global instructions')
278
364
  })
279
365
  })
280
366
 
@@ -311,20 +397,26 @@ describe('fail-safe(外层 catch return undefined,永不阻断 agent loop)
311
397
  )
312
398
  })
313
399
 
314
- it('systemPrompt 非法类型(undefined)且无注入 → 旧 quirk 锚定:返回 {systemPrompt:""} 而非 undefined', () => {
400
+ it('systemPrompt 非法类型(undefined)且无 append → 旧 quirk 锚定:返回仅含 capability 段的结果而非 undefined', () => {
315
401
  // R3 复核锚定的返回值守卫 quirk(index.ts newPrompt === event.systemPrompt 比较):
316
- // base 收敛 '','' !== undefined → 返回 {systemPrompt: ''}。与重构前行为一致(非回归)。
402
+ // base 收敛 '',capability 默认开 → newPrompt 非空且 !== undefined → 必然返回对象。
403
+ // 与重构前行为一致(非回归):quirk 在「有注入时必然返回对象」,schema v2 后 capability
404
+ // 默认开使该形态成为常态。
317
405
  const h = createHarness()
318
406
  const event = { type: 'before_agent_start', prompt: 'hi' } as unknown as BeforeAgentStartEvent
319
- expect(h.beforeAgentStart(event)).toEqual({ systemPrompt: '' })
407
+ expect(h.beforeAgentStart(event)).toEqual({ systemPrompt: expect.stringContaining(CAP_HEADER) })
320
408
  })
321
409
 
322
- it('systemPrompt 非法类型(undefined)但有 append 注入 → newPrompt = 空串 base + 分隔符 + append', () => {
410
+ it('systemPrompt 非法类型(undefined)但有 append 注入 → 空串 base + capability 段 + append 依序拼接', () => {
323
411
  setupFs({ config: '{"append": {"enabled": true, "prompt": "APPEND-TEXT"}}' })
324
412
  const h = createHarness()
325
413
  const event = { type: 'before_agent_start', prompt: 'hi' } as unknown as BeforeAgentStartEvent
326
- // base 收敛 '',拼接形态固定为 '' + '\n\n' + append(分隔符保留,与合法 base 一致)
327
- expect(h.beforeAgentStart(event)).toEqual({ systemPrompt: '\n\nAPPEND-TEXT' })
414
+ // base 收敛 '',拼接形态固定为 '' + '\n\n' + capability + '\n\n' + append(分隔符保留,与合法 base 一致)
415
+ const result = h.beforeAgentStart(event) as { systemPrompt: string }
416
+ expect(result.systemPrompt).toContain(CAP_HEADER)
417
+ expect(result.systemPrompt).toContain('APPEND-TEXT')
418
+ expect(result.systemPrompt.indexOf('APPEND-TEXT')).toBeGreaterThan(result.systemPrompt.indexOf(CAP_HEADER))
419
+ expect(result.systemPrompt.startsWith('\n\n')).toBe(true)
328
420
  })
329
421
  })
330
422
 
@@ -343,17 +435,19 @@ describe('cachedReadFileSync(mtime 级内容缓存,KV-cache 稳定性改造
343
435
  })
344
436
 
345
437
  it('文件改写(mtime 变)后下一轮 hook 读到新内容(变更即生效语义保留)', () => {
346
- setupFs({ config: '{"append": {"enabled": true, "prompt": "P1"}}' })
438
+ // CAP_OFF:本条锚精确串「base + P1」,capability 开启会让期望值带上整段常量文案,
439
+ // 显式关闭以保持断言可读;capability 的变更即生效由 readConfig 共用路径保证。
440
+ setupFs({ config: `{"append": {"enabled": true, "prompt": "P1"}, ${CAP_OFF}}` })
347
441
  expect(runHook('base')).toEqual({ systemPrompt: 'base\n\nP1' })
348
442
  // 模拟用户改写 append.prompt(setupFs 递增 mtime 纪元)
349
- setupFs({ config: '{"append": {"enabled": true, "prompt": "P2"}}' })
443
+ setupFs({ config: `{"append": {"enabled": true, "prompt": "P2"}, ${CAP_OFF}}` })
350
444
  expect(runHook('base')).toEqual({ systemPrompt: 'base\n\nP2' })
351
445
  })
352
446
 
353
- it('文件删除(stat throw)后缓存驱逐,注入降级为无 append', () => {
354
- setupFs({ config: '{"append": {"enabled": true, "prompt": "P1"}}' })
447
+ it('文件删除(stat throw)后缓存驱逐,append 注入降级消失,capability 回默认开', () => {
448
+ setupFs({ config: `{"append": {"enabled": true, "prompt": "P1"}, ${CAP_OFF}}` })
355
449
  expect(runHook('base')).toEqual({ systemPrompt: 'base\n\nP1' })
356
- setupFs() // config 恢复默认 ENOENT
357
- expect(runHook('base')).toBeUndefined()
450
+ setupFs() // config 恢复默认 ENOENT → defaults:append 无、capability 默认开
451
+ expect(runHook('base')).toEqual({ systemPrompt: expect.stringContaining(CAP_HEADER) })
358
452
  })
359
453
  })
package/src/index.ts CHANGED
@@ -4,9 +4,13 @@
4
4
  * Registers a `before_agent_start` hook that:
5
5
  * 1. Reads <dataDir>/system-prompt.json every turn (mtime-cached, see
6
6
  * `cachedReadFileSync`).
7
- * 2. When `append.enabled === true` and `append.prompt` is non-blank,
7
+ * 2. Appends the fixed taiji capability section unless explicitly disabled
8
+ * (`capability.enabled === false` — anything else, including a missing
9
+ * field, keeps it ON; deliberate inversion of the readSection fail-safe
10
+ * direction, see `readCapabilityEnabled`).
11
+ * 3. When `append.enabled === true` and `append.prompt` is non-blank,
8
12
  * appends the user's text to the event's systemPrompt.
9
- * 3. Reads the global instructions file `~/.agents/AGENTS.md` (candidates
13
+ * 4. Reads the global instructions file `~/.agents/AGENTS.md` (candidates
10
14
  * AGENTS.md / AGENTS.MD / CLAUDE.md / CLAUDE.MD) every turn and appends it
11
15
  * under a labeled header. Modeled on pi's native `loadContextFileFromDir`
12
16
  * but deliberately narrower: pi 0.84.4 also probes `AGENTS.override.md`
@@ -14,11 +18,12 @@
14
18
  * targets the global agents directory and never picks up override files.
15
19
  * Opt-in by file existence: no file → no injection. Skipped when
16
20
  * pi was spawned with `--no-context-files` (consistent with pi's native
17
- * context-file opt-out). `XYZ_GLOBAL_AGENTS_DIR` overrides the global
21
+ * context-file opt-out). `TAIJI_GLOBAL_AGENTS_DIR` overrides the global
18
22
  * directory (test hook / escape hatch).
19
23
  *
20
- * Injection order per turn: base prompt → global instructions → append config
21
- * (the explicitly configured text wins last).
24
+ * Injection order per turn: base prompt → global instructions → taiji
25
+ * capability section → append config (the explicitly configured text wins
26
+ * last).
22
27
  *
23
28
  * Fail-safe: any error in the handler is swallowed and `undefined` is returned
24
29
  * so the agent loop is never blocked.
@@ -30,7 +35,7 @@ import { readFileSync, readdirSync, statSync } from 'node:fs'
30
35
  import type { ExtensionAPI, BeforeAgentStartEvent } from '@earendil-works/pi-coding-agent'
31
36
  import { getLogger } from '@zhushanwen/pi-extension-logger'
32
37
 
33
- const logger = getLogger('xyz-system-prompt-extension')
38
+ const logger = getLogger('taiji-system-prompt-extension')
34
39
 
35
40
  const CONFIG_FILE = 'system-prompt.json'
36
41
 
@@ -68,11 +73,27 @@ function cachedReadFileSync(filePath: string): string | null {
68
73
  */
69
74
  const GLOBAL_AGENTS_CANDIDATES = ['AGENTS.md', 'AGENTS.MD', 'CLAUDE.md', 'CLAUDE.MD']
70
75
 
76
+ /**
77
+ * taiji capability 固定注入段(设计 D6):告知 AI 本渲染器的能力面,让新会话无需
78
+ * 用户手动教。常量放扩展源码内(版本化随 feature 走,非用户配置——用户只持有
79
+ * on/off 开关,不持有文案本身)。文案英文,与 pi system prompt 语言一致;四点 =
80
+ * 内联 HTML 白名单面 / 相对图片 cwd 解析 / 相对链接 cwd 解析(与反引号白名单路径
81
+ * 自动链接化同基准、校验面差异如实)/ 远程图片不渲染 + 反引号规约。
82
+ */
83
+ const TAIJI_CAPABILITY_SECTION = `# TaiJi capabilities
84
+
85
+ How TaiJi renders your markdown responses:
86
+
87
+ - Inline HTML is rendered with a GitHub-grade tag allowlist. Script/style elements and style/class attributes are stripped.
88
+ - Relative image paths (e.g. ![](docs/assets/img.png)) are resolved against the session working directory and displayed.
89
+ - Relative links (e.g. [plan](docs/plan.md)) use the same session working directory; clicking one opens the target file inside the app. Backtick file paths that TaiJi auto-links share the same cwd base. Difference in guarantees: auto-linked backtick paths are checked to exist, relative links may be dead — when citing a file you know exists, backticks are the safer form.
90
+ - Remote http(s) images are not rendered; reference a local file path instead. Always use backticks for inline code and type names in prose (bare angle-bracket names like Promise<void> are stripped).`
91
+
71
92
  /**
72
93
  * Resolve the data directory from the environment.
73
94
  *
74
95
  * Priority:
75
- * 1. `process.env.XYZ_AGENT_DATA_DIR` (explicit)
96
+ * 1. `process.env.TAIJI_AGENT_DATA_DIR` (explicit)
76
97
  * 2. `path.resolve(process.env.PI_CODING_AGENT_DIR ?? '', '..')`
77
98
  * (PI_CODING_AGENT_DIR == <dataDir>/agent, one level up == dataDir)
78
99
  *
@@ -80,7 +101,7 @@ const GLOBAL_AGENTS_CANDIDATES = ['AGENTS.md', 'AGENTS.MD', 'CLAUDE.md', 'CLAUDE
80
101
  * layout shape is handled — one level up. An OLD-layout value
81
102
  * (`<dataDir>/pi/agent`, pre-migration) would resolve to `<dataDir>/pi`,
82
103
  * accepted here on purpose: in every shipped scenario the runtime injects
83
- * XYZ_AGENT_DATA_DIR (priority 1 covers it), so priority 2 only serves
104
+ * TAIJI_AGENT_DATA_DIR (priority 1 covers it), so priority 2 only serves
84
105
  * standalone hosts already on the new layout. session-reader's
85
106
  * discovery/env.ts strips both shapes for its diagnostics; this resolver
86
107
  * deliberately stays single-shape (registered divergence, not a bug).
@@ -89,8 +110,8 @@ const GLOBAL_AGENTS_CANDIDATES = ['AGENTS.md', 'AGENTS.MD', 'CLAUDE.md', 'CLAUDE
89
110
  * take effect without reloading the extension.
90
111
  */
91
112
  function resolveDataDir(): string {
92
- if (process.env.XYZ_AGENT_DATA_DIR) {
93
- return process.env.XYZ_AGENT_DATA_DIR
113
+ if (process.env.TAIJI_AGENT_DATA_DIR) {
114
+ return process.env.TAIJI_AGENT_DATA_DIR
94
115
  }
95
116
  return path.resolve(process.env.PI_CODING_AGENT_DIR ?? '', '..')
96
117
  }
@@ -99,15 +120,15 @@ function resolveDataDir(): string {
99
120
  * Resolve the global agents directory.
100
121
  *
101
122
  * Priority:
102
- * 1. `process.env.XYZ_GLOBAL_AGENTS_DIR` (explicit override; tests / escape
123
+ * 1. `process.env.TAIJI_GLOBAL_AGENTS_DIR` (explicit override; tests / escape
103
124
  * hatch)
104
125
  * 2. `~/.agents` (the user-global agents dir that also hosts skills/templates)
105
126
  *
106
127
  * Re-read on every handler invocation so env changes take effect.
107
128
  */
108
129
  function resolveGlobalAgentsDir(): string {
109
- if (process.env.XYZ_GLOBAL_AGENTS_DIR) {
110
- return process.env.XYZ_GLOBAL_AGENTS_DIR
130
+ if (process.env.TAIJI_GLOBAL_AGENTS_DIR) {
131
+ return process.env.TAIJI_GLOBAL_AGENTS_DIR
111
132
  }
112
133
  return path.join(homedir(), '.agents')
113
134
  }
@@ -157,6 +178,7 @@ function readConfig(dataDir: string): {
157
178
  version: number
158
179
  replace: { enabled: boolean; prompt: string }
159
180
  append: { enabled: boolean; prompt: string }
181
+ capability: { enabled: boolean }
160
182
  } {
161
183
  const parsed = readJsonIfValid(path.join(dataDir, CONFIG_FILE))
162
184
  if (!parsed) {
@@ -164,6 +186,8 @@ function readConfig(dataDir: string): {
164
186
  version: 1,
165
187
  replace: { enabled: false, prompt: '' },
166
188
  append: { enabled: false, prompt: '' },
189
+ // capability 默认值与解析语义同向:缺 config → 开(见 readCapabilityEnabled)
190
+ capability: { enabled: true },
167
191
  }
168
192
  }
169
193
  // Merge defensively — every field has its own default.
@@ -173,9 +197,22 @@ function readConfig(dataDir: string): {
173
197
  version: typeof parsed.version === 'number' ? parsed.version : 1,
174
198
  replace: readSection(parsed.replace),
175
199
  append: readSection(parsed.append),
200
+ capability: { enabled: readCapabilityEnabled(parsed.capability) },
176
201
  }
177
202
  }
178
203
 
204
+ /**
205
+ * capability 段开关解析——方向与 readSection 刻意相反(设计 D6 防照抄锚点):
206
+ * 仅显式布尔 `false` 关闭;缺字段(v1 存量 json)/字段形态不对(如字符串
207
+ * "false")/capability 非对象/文件损坏(readConfig 前置收敛)→ true。
208
+ * 原因:replace/append 是用户显式配置(缺省关闭才安全),capability 是 taiji
209
+ * 内置告知(默认开是交付语义)——fail-safe 方向各自服务于所属字段的语义。
210
+ */
211
+ function readCapabilityEnabled(raw: unknown): boolean {
212
+ if (!isJsonObject(raw)) return true
213
+ return raw.enabled !== false
214
+ }
215
+
179
216
  /** Read a JSON file and return it as an object; missing / malformed / non-object → null. */
180
217
  function readJsonIfValid(filePath: string): Record<string, unknown> | null {
181
218
  try {
@@ -219,6 +256,17 @@ function withGlobalInstructions(prompt: string): string {
219
256
  return prompt + '\n\n# Global instructions (' + global.path + ')\n\n' + global.content
220
257
  }
221
258
 
259
+ /**
260
+ * Append the fixed taiji capability section(labeled header 与 global 注入段同构)。
261
+ * 生效粒度 = 下一 turn:本 hook 每 turn 读 config(mtime 缓存判变),改开关 → 写
262
+ * system-prompt.json → 下一 turn 读到新值——当前 turn 不受影响(机制既有,非新增)。
263
+ */
264
+ function withCapabilitySection(prompt: string): string {
265
+ const cfg = readConfig(resolveDataDir())
266
+ if (!cfg.capability.enabled) return prompt
267
+ return prompt + '\n\n' + TAIJI_CAPABILITY_SECTION
268
+ }
269
+
222
270
  /** Read the append config and apply it to the prompt (empty append → unchanged). */
223
271
  function withAppendPrompt(prompt: string): string {
224
272
  const cfg = readConfig(resolveDataDir())
@@ -228,13 +276,13 @@ function withAppendPrompt(prompt: string): string {
228
276
 
229
277
  /**
230
278
  * Build the injected system prompt. Injection order per turn:
231
- * base prompt → global instructions → append config (the explicitly
232
- * configured text wins last). Returns the new systemPrompt, or undefined
233
- * when nothing changed.
279
+ * base prompt → global instructions → taiji capability section → append
280
+ * config (the explicitly configured text wins last). Returns the new
281
+ * systemPrompt, or undefined when nothing changed.
234
282
  */
235
283
  function buildSystemPrompt(event: BeforeAgentStartEvent): { systemPrompt: string } | undefined {
236
284
  const basePrompt = typeof event.systemPrompt === 'string' ? event.systemPrompt : ''
237
- const newPrompt = withAppendPrompt(withGlobalInstructions(basePrompt))
285
+ const newPrompt = withAppendPrompt(withCapabilitySection(withGlobalInstructions(basePrompt)))
238
286
  return newPrompt === event.systemPrompt ? undefined : { systemPrompt: newPrompt }
239
287
  }
240
288
 
@@ -242,7 +290,7 @@ function buildSystemPrompt(event: BeforeAgentStartEvent): { systemPrompt: string
242
290
  * 落盘诊断,不泄露配置内容。
243
291
  *
244
292
  * 通道:extension-logger 的 error → appendEntry 写入 session JSONL(需 setPiHandle
245
- * 注入 pi handle 后生效);XYZ_AGENT_DEBUG=1 时另落文件日志。仅当 logger 自身抛错
293
+ * 注入 pi handle 后生效);TAIJI_AGENT_DEBUG=1 时另落文件日志。仅当 logger 自身抛错
246
294
  * 时才兜底 process.stderr.write(下方 catch),不外泄到 agent loop。
247
295
  */
248
296
  function logHookFailure(err: unknown): void {
@@ -252,7 +300,7 @@ function logHookFailure(err: unknown): void {
252
300
  } catch (nestedErr) {
253
301
  // best-effort:logger 抛错时的终极兜底 process.stderr.write——其内部吞错不会抛,仍不外泄到 agent loop。
254
302
  try {
255
- process.stderr.write(`[xyz-system-prompt-extension] logHookFailure also failed: ${String(nestedErr)}\n`)
303
+ process.stderr.write(`[taiji-system-prompt-extension] logHookFailure also failed: ${String(nestedErr)}\n`)
256
304
  } catch (finalErr) {
257
305
  /* 完全静默:两层兜底都失败时无处可写 */
258
306
  void finalErr