@zhuan-ai/zhuanspec 2.16.4 → 2.16.5

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.
@@ -405,6 +405,55 @@ function generateProposalChangeSummary(toolInput) {
405
405
  // Truncate to 120 chars
406
406
  return firstLine.length > 120 ? firstLine.substring(0, 117) + '...' : firstLine;
407
407
  }
408
+ function parseApplyPatch(command) {
409
+ const result = { files: [], totalLinesAdded: 0, totalLinesRemoved: 0 };
410
+ if (!command)
411
+ return result;
412
+ const lines = command.split('\n');
413
+ let currentFile = null;
414
+ for (const line of lines) {
415
+ if (line.startsWith('*** Add File: ') || line.startsWith('*** Update File: ')) {
416
+ if (currentFile)
417
+ result.files.push(currentFile);
418
+ const filePath = line.replace(/^\*\*\* (?:Add|Update) File:\s*/, '').trim();
419
+ currentFile = { path: filePath, linesAdded: 0, linesRemoved: 0 };
420
+ }
421
+ else if (line.startsWith('*** Delete File: ')) {
422
+ if (currentFile)
423
+ result.files.push(currentFile);
424
+ const filePath = line.replace(/^\*\*\* Delete File:\s*/, '').trim();
425
+ currentFile = { path: filePath, linesAdded: 0, linesRemoved: 0 };
426
+ }
427
+ else if (line === '*** End Patch' || line === '*** Begin Patch') {
428
+ if (currentFile) {
429
+ result.files.push(currentFile);
430
+ currentFile = null;
431
+ }
432
+ }
433
+ else if (line.startsWith('*** ')) {
434
+ // Other patch directives (e.g., *** Rename File:) — push current and skip
435
+ if (currentFile) {
436
+ result.files.push(currentFile);
437
+ currentFile = null;
438
+ }
439
+ }
440
+ else if (currentFile) {
441
+ if (line.startsWith('+'))
442
+ currentFile.linesAdded++;
443
+ else if (line.startsWith('-'))
444
+ currentFile.linesRemoved++;
445
+ // Context lines (space prefix or @@ markers) are ignored for counting
446
+ }
447
+ }
448
+ if (currentFile)
449
+ result.files.push(currentFile);
450
+ // Calculate totals
451
+ for (const f of result.files) {
452
+ result.totalLinesAdded += f.linesAdded;
453
+ result.totalLinesRemoved += f.linesRemoved;
454
+ }
455
+ return result;
456
+ }
408
457
  async function runRecordProgress(filePath, toolName, success, stdinData) {
409
458
  const cwd = resolveZhuanSpecRoot();
410
459
  // Priority: filePath-derived > progress.json scan > environment variables
@@ -620,8 +669,31 @@ async function runRecordProgress(filePath, toolName, success, stdinData) {
620
669
  progress.currentNode = process.env.ZHUANSPEC_CURRENT_NODE || phase;
621
670
  progress.currentTask = currentTask || progress.currentTask;
622
671
  progress.toolCalls.push(toolCall);
623
- const WRITE_TOOLS = new Set(['Write', 'Edit', 'MultiEdit', 'NotebookEdit']);
624
- if (filePath && WRITE_TOOLS.has(toolName) && !progress.filesModified.includes(filePath)) {
672
+ // === Codex apply_patch 支持 ===
673
+ // Codex 使用 apply_patch 工具,file_path 为空,实际内容在 command 字段的 patch 格式中
674
+ let patchResult = null;
675
+ if (toolName === 'apply_patch') {
676
+ const command = stdinData.tool_input?.command || '';
677
+ patchResult = parseApplyPatch(command);
678
+ // 用第一个文件路径作为 filePath(用于 accuracy 追踪和 change detection)
679
+ if (patchResult.files.length > 0 && !filePath) {
680
+ filePath = patchResult.files[0].path;
681
+ }
682
+ }
683
+ const WRITE_TOOLS = new Set(['Write', 'Edit', 'MultiEdit', 'NotebookEdit', 'apply_patch']);
684
+ if (toolName === 'apply_patch' && patchResult) {
685
+ // apply_patch 可修改多文件,逐一加入 filesModified
686
+ const zhuanspecDirPrefix = path.resolve(process.cwd(), 'zhuanspec') + path.sep;
687
+ for (const f of patchResult.files) {
688
+ if (!f.path || progress.filesModified.includes(f.path))
689
+ continue;
690
+ const absF = path.isAbsolute(f.path) ? f.path : path.resolve(process.cwd(), f.path);
691
+ if (!absF.startsWith(zhuanspecDirPrefix)) {
692
+ progress.filesModified.push(f.path);
693
+ }
694
+ }
695
+ }
696
+ else if (filePath && WRITE_TOOLS.has(toolName) && !progress.filesModified.includes(filePath)) {
625
697
  // Exclude ZhuanSpec internal files (spec/metrics/doc files inside zhuanspec/ dir)
626
698
  const absFilePath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
627
699
  const zhuanspecDirPrefix = path.resolve(process.cwd(), 'zhuanspec') + path.sep;
@@ -631,7 +703,7 @@ async function runRecordProgress(filePath, toolName, success, stdinData) {
631
703
  }
632
704
  // Detect proposal file changes and record them
633
705
  const changePath = path.join(zhuanspecDir, 'changes', changeId);
634
- const WRITE_EDIT_TOOLS = new Set(['Write', 'Edit', 'MultiEdit']);
706
+ const WRITE_EDIT_TOOLS = new Set(['Write', 'Edit', 'MultiEdit', 'apply_patch']);
635
707
  if (filePath && WRITE_EDIT_TOOLS.has(toolName) && isProposalFile(filePath, changePath)) {
636
708
  const relativeProposalPath = path.relative(changePath, path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath));
637
709
  // 提案文件变更行数:按工具类型取不同字段,避免 Edit/MultiEdit 全记为 0
@@ -652,6 +724,16 @@ async function runRecordProgress(filePath, toolName, success, stdinData) {
652
724
  contentLines += Math.max(ns, os);
653
725
  }
654
726
  }
727
+ else if (toolName === 'apply_patch' && patchResult) {
728
+ // apply_patch 的提案文件变更行数:用 patch 中涉及该提案文件的行数
729
+ const absChangePath = path.isAbsolute(changePath) ? changePath : path.resolve(cwd, changePath);
730
+ for (const f of patchResult.files) {
731
+ const absF = path.isAbsolute(f.path) ? f.path : path.resolve(cwd, f.path);
732
+ if (absF.startsWith(absChangePath + path.sep)) {
733
+ contentLines += f.linesAdded + f.linesRemoved;
734
+ }
735
+ }
736
+ }
655
737
  const record = {
656
738
  changeRecordId: `pc-${Date.now()}`,
657
739
  timestamp,
@@ -725,6 +807,11 @@ async function runRecordProgress(filePath, toolName, success, stdinData) {
725
807
  }
726
808
  contentSource = toolInputEdits.length > 0 ? 'edits[]' : 'none';
727
809
  }
810
+ else if (toolName === 'apply_patch' && patchResult) {
811
+ // Codex apply_patch:统计 patch 中所有 +/- 行
812
+ changeLines = patchResult.totalLinesAdded + patchResult.totalLinesRemoved;
813
+ contentSource = patchResult.files.length > 0 ? 'command' : 'none';
814
+ }
728
815
  let estimatedLines = 0;
729
816
  // === P0 兜底:post-baseline-fallback ===
730
817
  // 正常链路:UserPromptSubmit 的 user-input hook 会在 phaseBaseline 已打标时写入
@@ -185,17 +185,19 @@ export async function userInputHook(content, responseTimeMs) {
185
185
  logDebug(cwd, { event: 'skip', reason: 'content-too-short', changeId, phase, phaseSource, contentLen });
186
186
  return { continue: true };
187
187
  }
188
- // Skip slash/bang commands (e.g. /zhuanspec:proposal, /compact, /clear, !ls).
189
- // 这类 prompt 是命令/workflow 触发,非“用户对 AI 产出的纠偏/濄清/补充”,
188
+ // Skip slash/bang/dollar commands:
189
+ // Claude Code: /zhuanspec:proposal, /compact, /clear, !ls
190
+ // Codex: $zhuanspec-proposal, $zhuanspec-techDesign 等 skill 触发
191
+ // 这类 prompt 是命令/workflow 触发,非"用户对 AI 产出的纠偏/澄清/补充",
190
192
  // 不应计入 user_inputs.json,更不应放进纠偏溯源池影响 accuracyRate。
191
193
  // 注意:Claude Code 在 slash 命令前会注入 NAK(U+0015) 等 C0 控制字符做标记,
192
194
  // 普通 trim() 不会剥控制字符,必须显式剥掉前缀的 C0/DEL 再做正则判定,
193
195
  // 否则像 "\u0015/zhuanspec:proposal" 这种命令会漏过过滤被记成普通用户输入。
194
196
  const trimmedContent = content.trim().replace(/^[\x00-\x1F\x7F]+/, '');
195
- if (/^[/!][A-Za-z0-9_:/@.\-]+(\s|$)/.test(trimmedContent) || /^\/[\w:\-]+$/.test(trimmedContent)) {
197
+ if (/^[/!$][A-Za-z0-9_:/@.\-]+(\s|$)/.test(trimmedContent) || /^\/[\w:\-]+$/.test(trimmedContent)) {
196
198
  logDebug(cwd, {
197
199
  event: 'skip',
198
- reason: 'slash-command',
200
+ reason: 'skill-command',
199
201
  changeId,
200
202
  phase,
201
203
  phaseSource,
@@ -50,7 +50,7 @@ const CODEX_DEFAULT_HOOKS = [
50
50
  },
51
51
  {
52
52
  event: 'PostToolUse',
53
- matcher: 'Write|Edit|Read|Grep|Glob|Bash|Skill|Agent|mcp__.*',
53
+ matcher: 'Write|Edit|apply_patch|Read|Grep|Glob|Bash|Skill|Agent|mcp__.*',
54
54
  command: 'ZHUANSPEC_HOST=codex zhuanspec-hook record-progress --json',
55
55
  statusMessage: '📝 Recording progress...',
56
56
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhuan-ai/zhuanspec",
3
- "version": "2.16.4",
3
+ "version": "2.16.5",
4
4
  "description": "AI-native system for spec-driven development",
5
5
  "keywords": [
6
6
  "zhuanspec",
@@ -39,6 +39,26 @@
39
39
  "!dist/**/__tests__",
40
40
  "!dist/**/*.map"
41
41
  ],
42
+ "scripts": {
43
+ "lint": "eslint src/",
44
+ "build": "node build.js",
45
+ "dev": "tsc --watch",
46
+ "dev:cli": "pnpm build && node bin/zhuanspec.js",
47
+ "test": "vitest run",
48
+ "test:watch": "vitest",
49
+ "test:ui": "vitest --ui",
50
+ "test:coverage": "vitest --coverage",
51
+ "test:postinstall": "node scripts/postinstall.js",
52
+ "prepare": "npm run build",
53
+ "prepublishOnly": "npm run build",
54
+ "postinstall": "node scripts/postinstall.js",
55
+ "check:pack-version": "node scripts/pack-version-check.mjs",
56
+ "diagnose:cursor": "node scripts/diagnose-cursor-commands.js",
57
+ "release": "pnpm run release:ci",
58
+ "release:ci": "pnpm run check:pack-version && pnpm exec changeset publish",
59
+ "release:local": "pnpm exec changeset version && pnpm run check:pack-version && pnpm exec changeset publish",
60
+ "changeset": "changeset"
61
+ },
42
62
  "engines": {
43
63
  "node": ">=20.19.0"
44
64
  },
@@ -60,23 +80,5 @@
60
80
  "ora": "^8.2.0",
61
81
  "yaml": "^2.8.2",
62
82
  "zod": "^4.0.17"
63
- },
64
- "scripts": {
65
- "lint": "eslint src/",
66
- "build": "node build.js",
67
- "dev": "tsc --watch",
68
- "dev:cli": "pnpm build && node bin/zhuanspec.js",
69
- "test": "vitest run",
70
- "test:watch": "vitest",
71
- "test:ui": "vitest --ui",
72
- "test:coverage": "vitest --coverage",
73
- "test:postinstall": "node scripts/postinstall.js",
74
- "postinstall": "node scripts/postinstall.js",
75
- "check:pack-version": "node scripts/pack-version-check.mjs",
76
- "diagnose:cursor": "node scripts/diagnose-cursor-commands.js",
77
- "release": "pnpm run release:ci",
78
- "release:ci": "pnpm run check:pack-version && pnpm exec changeset publish",
79
- "release:local": "pnpm exec changeset version && pnpm run check:pack-version && pnpm exec changeset publish",
80
- "changeset": "changeset"
81
83
  }
82
- }
84
+ }