@zhushanwen/pi-system-prompt 1.3.2 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-system-prompt",
3
- "version": "1.3.2",
3
+ "version": "1.4.1",
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",
@@ -36,6 +36,6 @@
36
36
  },
37
37
  "scripts": {
38
38
  "typecheck": "npx tsc --noEmit",
39
- "test": "vitest run"
39
+ "test": "vitest run --pool=threads"
40
40
  }
41
41
  }
@@ -8,7 +8,7 @@
8
8
  * 损坏形态("enabled": "false" 字符串 / capability 非对象)→ 注入(与 readSection 解析方向相反)
9
9
  * - before_agent_start 注入顺序:base → global instructions → capability → append(indexOf 链锁定,
10
10
  * swap 注入顺序两行的 mutant 会被顺序用例 kill)
11
- * - -nc / --no-context-files 守卫:global 注入跳过、append 不受影响
11
+ * - -nc / --no-context-files 检查:global 注入跳过、append 不受影响
12
12
  * - global 候选选择:候选序优先、空白内容跳过继续找、目录缺失降级
13
13
  * - fail-safe:handler 全程 throw → return undefined + logger.error 可观测;logger 自身抛错的
14
14
  * 终极兜底 process.stderr.write;systemPrompt 非法类型的旧 quirk 锚定
@@ -17,7 +17,7 @@
17
17
  * 在 config 里显式 "capability":{"enabled":false} 隔离关注点;无法塞字段的用例(畸形 JSON /
18
18
  * 文件缺失)期望值如实更新为含 capability 段。capability 自身行为由专项 describe 锚定。
19
19
  *
20
- * mock 策略(参照 msg-id-mapper 测试模式):pi SDK import type 零运行时解析,
20
+ * mock 策略(参照 host-internals msg-id-mapper 测试模式):pi SDK import type 零运行时解析,
21
21
  * ExtensionAPI 用结构化桩;node:fs mock 后按路径分流(env 指向假目录,不碰真实文件系统)。
22
22
  *
23
23
  * 运行:cd extensions/taiji/system-prompt && npx vitest run
@@ -38,6 +38,7 @@ import createExtension, {
38
38
  CAPABILITY_INLINE_TAG_FAMILIES,
39
39
  CAPABILITY_PRESENTATION_ATTRS,
40
40
  SESSION_ARTIFACTS_DIR_SEGMENT,
41
+ SESSION_STORE_DIR_SEGMENT,
41
42
  resolveSessionArtifactsDir,
42
43
  } from '../index'
43
44
  import type { ExtensionAPI, BeforeAgentStartEvent } from '@earendil-works/pi-coding-agent'
@@ -57,7 +58,7 @@ const CAP_HEADER = '# TaiJi capabilities'
57
58
  /** 既有用例隔离关注点用的「capability 显式关闭」config 片段 */
58
59
  const CAP_OFF = '"capability": {"enabled": false}'
59
60
 
60
- /** hook 注册表桩(参照 msg-id-mapper harness 模式)——handler 第二参 ctx 透传(路径注入测试用) */
61
+ /** hook 注册表桩(参照 host-internals msg-id-mapper harness 模式)——handler 第二参 ctx 透传(路径注入测试用) */
61
62
  function createHarness(): { beforeAgentStart: (event: BeforeAgentStartEvent, ctx?: unknown) => unknown } {
62
63
  const handlers = new Map<string, (...args: unknown[]) => unknown>()
63
64
  const pi = {
@@ -136,7 +137,7 @@ const savedEnv: Record<string, string | undefined> = {}
136
137
  const savedArgv = process.argv
137
138
 
138
139
  beforeEach(() => {
139
- vi.clearAllMocks() // 清跨用例的 fs mock 调用记录(「守卫不触发」类断言依赖零计数)
140
+ vi.clearAllMocks() // 清跨用例的 fs mock 调用记录(「检查不触发」类断言依赖零计数)
140
141
  for (const k of ENV_KEYS) savedEnv[k] = process.env[k]
141
142
  process.env.TAIJI_AGENT_DATA_DIR = DATA_DIR
142
143
  process.env.TAIJI_GLOBAL_AGENTS_DIR = GLOBAL_DIR
@@ -237,7 +238,7 @@ describe('before_agent_start 注入顺序(base → global → capability → a
237
238
  expect(prompt).toContain(path.join(GLOBAL_DIR, 'AGENTS.md'))
238
239
  })
239
240
 
240
- it('capability 段文案覆盖四点能力面(HTML 白名单 / 相对图片 / 相对链接双通道 / 远程图片不渲染)', () => {
241
+ it('capability 段文案覆盖四点能力侧(HTML 白名单 / 相对图片 / 相对链接双通道 / 远程图片不渲染)', () => {
241
242
  setupFs({ config: '{"append": {"enabled": false, "prompt": ""}}' })
242
243
  const prompt = runHook('BASE-PROMPT')!.systemPrompt as string
243
244
  const capText = prompt.slice(prompt.indexOf(CAP_HEADER))
@@ -314,9 +315,9 @@ describe('capability 段三态锚点(设计 D6:仅显式布尔 false 关闭
314
315
  })
315
316
  })
316
317
 
317
- describe('-nc / --no-context-files 守卫(contextFilesDisabled)', () => {
318
- // capability 与守卫正交(内置段不受 --no-context-files 影响,该旗标只管上下文文件
319
- // 发现),用例显式关 capability 隔离守卫语义。
318
+ describe('-nc / --no-context-files 检查(contextFilesDisabled)', () => {
319
+ // capability 与检查正交(内置段不受 --no-context-files 影响,该旗标只管上下文文件
320
+ // 发现),用例显式关 capability 隔离检查语义。
320
321
  it('argv 含 --no-context-files → global 不注入,append 仍生效(用户显式退出不得溜回来)', () => {
321
322
  setupFs({
322
323
  config: `{"append": {"enabled": true, "prompt": "APPEND-TEXT"}, ${CAP_OFF}}`,
@@ -325,11 +326,11 @@ describe('-nc / --no-context-files 守卫(contextFilesDisabled)', () => {
325
326
  })
326
327
  process.argv = ['node', 'pi', '--no-context-files']
327
328
  expect(runHook('BASE-PROMPT')).toEqual({ systemPrompt: 'BASE-PROMPT\n\nAPPEND-TEXT' })
328
- // 守卫在读 global 文件之前:readdirSync 不应被调用(global 目录完全不被触碰)
329
+ // 检查在读 global 文件之前:readdirSync 不应被调用(global 目录完全不被触碰)
329
330
  expect(readdirSync).not.toHaveBeenCalled()
330
331
  })
331
332
 
332
- it('argv 含 -nc 短形式 → 同样跳过 global 注入(pi CLI 等价短形式,守卫两种形式都命中)', () => {
333
+ it('argv 含 -nc 短形式 → 同样跳过 global 注入(pi CLI 等价短形式,检查两种形式都命中)', () => {
333
334
  setupFs({
334
335
  config: `{"append": {"enabled": true, "prompt": "APPEND-TEXT"}, ${CAP_OFF}}`,
335
336
  globalEntries: ['AGENTS.md'],
@@ -385,26 +386,8 @@ describe('fail-safe(外层 catch return undefined,永不阻断 agent loop)
385
386
  )
386
387
  })
387
388
 
388
- it('logger 自身抛错的终极兜底 → stderr 兜底,仍 return undefined', () => {
389
- const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true)
390
- loggerMock.error.mockImplementation(() => {
391
- throw new Error('logger gone')
392
- })
393
- const h = createHarness()
394
- const event = { type: 'before_agent_start', prompt: 'hi' } as unknown as BeforeAgentStartEvent
395
- Object.defineProperty(event, 'systemPrompt', {
396
- get() {
397
- throw new Error('getter boom')
398
- },
399
- })
400
- expect(h.beforeAgentStart(event)).toBeUndefined()
401
- expect(stderrSpy).toHaveBeenCalledWith(
402
- expect.stringContaining('logHookFailure also failed'),
403
- )
404
- })
405
-
406
389
  it('systemPrompt 非法类型(undefined)且无 append → 旧 quirk 锚定:返回仅含 capability 段的结果而非 undefined', () => {
407
- // R3 复核锚定的返回值守卫 quirk(index.ts newPrompt === event.systemPrompt 比较):
390
+ // R3 复核锚定的返回值检查 quirk(index.ts newPrompt === event.systemPrompt 比较):
408
391
  // base 收敛 '',capability 默认开 → newPrompt 非空且 !== undefined → 必然返回对象。
409
392
  // 与重构前行为一致(非回归):quirk 在「有注入时必然返回对象」,schema v2 后 capability
410
393
  // 默认开使该形态成为常态。
@@ -529,17 +512,20 @@ describe('capability 段 ①② 渲染锁定(文案 = 常量渲染产物,手
529
512
  })
530
513
  })
531
514
 
532
- describe('产物目录镜像常量与推导 helper(设计 D1/D7,供 u-artifacts 公式对拍消费)', () => {
533
- it('段名字面量为 artifacts', () => {
515
+ describe('产物目录镜像常量与推导 helper(session 存储树布局,公式对拍机检消费)', () => {
516
+ it('段名字面量:树根段 ext-sessions + 域段 artifacts(与 shared SESSION_STORE_DIR_SEGMENT / getSessionArtifactsDir 对拍)', () => {
517
+ expect(SESSION_STORE_DIR_SEGMENT).toBe('ext-sessions')
534
518
  expect(SESSION_ARTIFACTS_DIR_SEGMENT).toBe('artifacts')
535
519
  })
536
520
 
537
- it('合法 pi sid → <dataDir>/artifacts/<sessionId>', () => {
521
+ it('合法 pi sid → <dataDir>/ext-sessions/<sessionId>/artifacts', () => {
538
522
  expect(resolveSessionArtifactsDir('0198ab12-cdef-7000-8000-1234567890ab')).toBe(
539
- path.join(DATA_DIR, 'artifacts', '0198ab12-cdef-7000-8000-1234567890ab'),
523
+ path.join(DATA_DIR, 'ext-sessions', '0198ab12-cdef-7000-8000-1234567890ab', 'artifacts'),
540
524
  )
541
525
  // 允许 `.` 与 `_`(与 isPiSessionId 同域)
542
- expect(resolveSessionArtifactsDir('a.b-c_d')).toBe(path.join(DATA_DIR, 'artifacts', 'a.b-c_d'))
526
+ expect(resolveSessionArtifactsDir('a.b-c_d')).toBe(
527
+ path.join(DATA_DIR, 'ext-sessions', 'a.b-c_d', 'artifacts'),
528
+ )
543
529
  })
544
530
 
545
531
  it('非法 sessionId(含冒号 / 空串 / 首尾非字母数字)→ throw(防路径穿越)', () => {
@@ -551,7 +537,9 @@ describe('产物目录镜像常量与推导 helper(设计 D1/D7,供 u-artifa
551
537
  it('resolveDataDir 回退(无 TAIJI_AGENT_DATA_DIR 时取 PI_CODING_AGENT_DIR 上级)', () => {
552
538
  delete process.env.TAIJI_AGENT_DATA_DIR
553
539
  process.env.PI_CODING_AGENT_DIR = path.join('/tmp/fallback-data', 'agent')
554
- expect(resolveSessionArtifactsDir('abc')).toBe(path.join('/tmp/fallback-data', 'artifacts', 'abc'))
540
+ expect(resolveSessionArtifactsDir('abc')).toBe(
541
+ path.join('/tmp/fallback-data', 'ext-sessions', 'abc', 'artifacts'),
542
+ )
555
543
  })
556
544
  })
557
545
 
@@ -576,9 +564,9 @@ describe('capability 段 ③④(HTML 交付约定 + 预览约束)与产物
576
564
 
577
565
  it('ctx 会话 id 在 → ③ 段含产物目录绝对路径(与会话 id 匹配)', () => {
578
566
  const text = capabilityTextWithCtx(ctxWithSessionId(() => SID))
579
- // 路径 = <dataDir>/artifacts/<sid>(与 shared getSessionArtifactsDir 同公式)
567
+ // 路径 = <dataDir>/ext-sessions/<sid>/artifacts(与 shared getSessionArtifactsDir 同公式)
580
568
  expect(text).toContain(resolveSessionArtifactsDir(SID))
581
- expect(text).toContain(path.join(DATA_DIR, 'artifacts', SID))
569
+ expect(text).toContain(path.join(DATA_DIR, 'ext-sessions', SID, 'artifacts'))
582
570
  // ③ 段交付约定与 ④ 段预览约束齐备
583
571
  expect(text).toContain('info string is html-preview')
584
572
  expect(text).toContain("that file's absolute path")
@@ -605,7 +593,7 @@ describe('capability 段 ③④(HTML 交付约定 + 预览约束)与产物
605
593
  expect(text).toContain('blocked by the browser CORS policy')
606
594
  expect(text).toContain('CDN links will not load')
607
595
  // 不出现半截/伪造路径
608
- expect(text).not.toContain(path.join(DATA_DIR, 'artifacts'))
596
+ expect(text).not.toContain(path.join(DATA_DIR, 'ext-sessions'))
609
597
  }
610
598
  })
611
599
 
@@ -616,7 +604,7 @@ describe('capability 段 ③④(HTML 交付约定 + 预览约束)与产物
616
604
  const text = capabilityTextWithCtx(ctx)
617
605
  expect(text).toContain('(unavailable this turn)')
618
606
  expect(text).toContain('info string is html-preview')
619
- expect(text).not.toContain(path.join(DATA_DIR, 'artifacts'))
607
+ expect(text).not.toContain(path.join(DATA_DIR, 'ext-sessions'))
620
608
  }
621
609
  })
622
610
 
package/src/index.ts CHANGED
@@ -214,7 +214,18 @@ function resolveDataDir(): string {
214
214
  }
215
215
 
216
216
  /**
217
- * 会话产物目录镜像常量(设计 D1/D7):`<dataDir>/artifacts/<sessionId>/`。
217
+ * session 存储树根段镜像常量(session-store-layout-inversion D1):
218
+ * `<dataDir>/ext-sessions/<sessionId>/...` 树根段。
219
+ *
220
+ * shared 侧单点声明 = `packages/shared/src/paths.ts` 的 `SESSION_STORE_DIR_SEGMENT` /
221
+ * `getSessionStoreDir`;本包不 import `@taiji/shared`(包边界 + C-proc-26/C-proc-09
222
+ * 门禁组合,见下方产物目录镜像说明),以同名字面量镜像,一致性由
223
+ * `scripts/check-artifact-dir-formula-sync.mjs` 对拍机检守护。
224
+ */
225
+ export const SESSION_STORE_DIR_SEGMENT = 'ext-sessions'
226
+
227
+ /**
228
+ * 会话产物目录镜像常量(session 存储树域 2):`<dataDir>/ext-sessions/<sessionId>/artifacts/`。
218
229
  *
219
230
  * shared 侧单点声明 = `packages/shared/src/paths.ts` 的 `getSessionArtifactsDir`;本包
220
231
  * 不 import `@taiji/shared`(① 包边界——extensions 包全体不依赖 @taiji/shared;② 运行时
@@ -226,22 +237,22 @@ function resolveDataDir(): string {
226
237
  export const SESSION_ARTIFACTS_DIR_SEGMENT = 'artifacts'
227
238
 
228
239
  /**
229
- * 会话产物目录绝对路径(镜像 shared `getSessionArtifactsDir` 公式)。
240
+ * 会话产物目录绝对路径(镜像 shared `getSessionArtifactsDir` 公式:
241
+ * `<dataDir>/ext-sessions/<sessionId>/artifacts`)。
230
242
  *
231
243
  * sessionId 校验与 pi `assertValidSessionId` / runtime `isPiSessionId` 同域:首尾字母
232
- * 数字,中间允许 `[A-Za-z0-9._-]`(允许 `.` / `_`,禁 `:`)——不用 `getImageCacheDir`
233
- * 的窄集 `/^[A-Za-z0-9_-]+$/`(合法含 `.` 的 pi sid 会被窄集误拒)。非法 sessionId
244
+ * 数字,中间允许 `[A-Za-z0-9._-]`(允许 `.` / `_`,禁 `:`)。非法 sessionId
234
245
  * 直接 throw(防路径穿越)。
235
246
  *
236
247
  * @param sessionId 会话 id(子目录分区,须与 `isPiSessionId` 同域)
237
248
  * @throws Error 当 sessionId 含路径分隔符、冒号、空串或首尾非字母数字
238
249
  */
239
250
  export function resolveSessionArtifactsDir(sessionId: string): string {
240
- // 校验与 pi-paths.ts 的 isPiSessionId 同域(字面量与 shared getSessionArtifactsDir 一致)
251
+ // 校验与 pi-paths.ts 的 isPiSessionId 同域(字面量与 shared PI_SESSION_ID_PATTERN 一致)
241
252
  if (!/^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/.test(sessionId)) {
242
253
  throw new Error(`invalid sessionId (path traversal blocked): ${sessionId}`)
243
254
  }
244
- return path.join(resolveDataDir(), SESSION_ARTIFACTS_DIR_SEGMENT, sessionId)
255
+ return path.join(resolveDataDir(), SESSION_STORE_DIR_SEGMENT, sessionId, SESSION_ARTIFACTS_DIR_SEGMENT)
245
256
  }
246
257
 
247
258
  /**
@@ -392,8 +403,7 @@ function readSection(raw: unknown): { enabled: boolean; prompt: string } {
392
403
  /**
393
404
  * pi 是否以 --no-context-files / -nc 启动。用户显式退出 AGENTS.md
394
405
  * 自动发现时,全局文件不得从这条通路溜回来。pi CLI 把 -nc 视为 --no-context-files
395
- * 的等价短形式(cli/args.ts),两种形式都必须命中守卫——与镜像侧
396
- * (argv-mirror.ts 同样解析两种形式)保持一致。
406
+ * 的等价短形式(cli/args.ts),本守卫单侧同时覆盖两种 argv 形态。
397
407
  */
398
408
  function contextFilesDisabled(): boolean {
399
409
  return process.argv.includes('--no-context-files') || process.argv.includes('-nc')
@@ -442,22 +452,15 @@ function buildSystemPrompt(event: BeforeAgentStartEvent, ctx?: ExtensionContext)
442
452
  * 落盘诊断,不泄露配置内容。
443
453
  *
444
454
  * 通道:extension-logger 的 error → appendEntry 写入 session JSONL(需 setPiHandle
445
- * 注入 pi handle 后生效);TAIJI_AGENT_DEBUG=1 时另落文件日志。仅当 logger 自身抛错
446
- * 时才兜底 process.stderr.write(下方 catch),不外泄到 agent loop。
455
+ * 注入 pi handle 后生效);TAIJI_AGENT_DEBUG=1 时另落文件日志。logger 全程吞错
456
+ * (appendEntry 与 fileLog 通道内部各自整体 try/catch,实装见 extension-logger
457
+ * error 方法),不会抛错到本调用方——旧的两层嵌套兜底(stderr 终极兜底 + 静默
458
+ * catch)已删,恒不命中。若未来 logger 改为可抛实现,异常由外层 handler 的
459
+ * try/catch 兜住(同样不外泄到 agent loop)。
447
460
  */
448
461
  function logHookFailure(err: unknown): void {
449
- try {
450
- const msg = err instanceof Error ? `${err.name}: ${err.message}` : String(err)
451
- logger.error(`before_agent_start hook failed: ${msg}`)
452
- } catch (nestedErr) {
453
- // best-effort:logger 抛错时的终极兜底 process.stderr.write——其内部吞错不会抛,仍不外泄到 agent loop。
454
- try {
455
- process.stderr.write(`[taiji-system-prompt-extension] logHookFailure also failed: ${String(nestedErr)}\n`)
456
- } catch (finalErr) {
457
- /* 完全静默:两层兜底都失败时无处可写 */
458
- void finalErr
459
- }
460
- }
462
+ const msg = err instanceof Error ? `${err.name}: ${err.message}` : String(err)
463
+ logger.error(`before_agent_start hook failed: ${msg}`)
461
464
  }
462
465
 
463
466
  export default function (pi: ExtensionAPI): void {