@zhushanwen/pi-system-prompt 1.1.6 → 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 +1 -1
- package/src/__tests__/system-prompt.test.ts +133 -39
- package/src/index.ts +56 -8
package/package.json
CHANGED
|
@@ -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
|
-
* -
|
|
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
|
*
|
|
@@ -40,6 +46,11 @@ const DATA_DIR = '/taiji-test/data'
|
|
|
40
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>()
|
|
@@ -136,62 +147,64 @@ afterEach(() => {
|
|
|
136
147
|
vi.restoreAllMocks()
|
|
137
148
|
})
|
|
138
149
|
|
|
139
|
-
describe('readJsonIfValid 解析边界(config 读取 → defaults
|
|
140
|
-
|
|
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')).
|
|
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')).
|
|
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')).
|
|
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')).
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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('
|
|
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
|
|
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(
|
|
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('
|
|
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
|
-
|
|
225
|
-
expect(
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
400
|
+
it('systemPrompt 非法类型(undefined)且无 append → 旧 quirk 锚定:返回仅含 capability 段的结果而非 undefined', () => {
|
|
315
401
|
// R3 复核锚定的返回值守卫 quirk(index.ts newPrompt === event.systemPrompt 比较):
|
|
316
|
-
// base 收敛 '',
|
|
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 注入 →
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
354
|
-
setupFs({ config:
|
|
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')).
|
|
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.
|
|
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
|
-
*
|
|
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`
|
|
@@ -17,8 +21,9 @@
|
|
|
17
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 →
|
|
21
|
-
* (the explicitly configured text wins
|
|
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.
|
|
@@ -68,6 +73,22 @@ 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. ) 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
|
*
|
|
@@ -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 →
|
|
232
|
-
* configured text wins last). Returns the new
|
|
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
|
|