@boyingliu01/xp-gate 0.19.2 → 0.20.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.
Files changed (223) hide show
  1. package/adapters/gate-4.sh +83 -18
  2. package/adapters/gate-8.sh +49 -5
  3. package/adapters/iac.sh +1 -1
  4. package/adapters/typescript.sh +15 -60
  5. package/bin/xp-gate.js +15 -16
  6. package/gate-10.sh +35 -2
  7. package/gate-4.sh +110 -21
  8. package/gate-8.sh +49 -5
  9. package/hooks/gate-10.sh +35 -2
  10. package/hooks/gate-4.sh +110 -21
  11. package/hooks/gate-8.sh +49 -5
  12. package/hooks/lib/jscpd-run.sh +63 -0
  13. package/hooks/lib/sprint-gate-report.sh +58 -0
  14. package/hooks/lib/test-failure.sh +152 -0
  15. package/hooks/lib/typecheck.sh +141 -0
  16. package/hooks/lib/validate-code-walkthrough.cjs +43 -5
  17. package/hooks/post-merge +100 -0
  18. package/hooks/pre-commit +401 -155
  19. package/hooks/pre-push +75 -19
  20. package/lib/__tests__/bootstrap.test.js +23 -41
  21. package/lib/__tests__/check-version.test.js +8 -1
  22. package/lib/__tests__/detect-deps.test.js +16 -0
  23. package/lib/__tests__/doctor-drift-round2.test.js +212 -0
  24. package/lib/__tests__/doctor-executed-surface.test.js +209 -0
  25. package/lib/__tests__/doctor-hook-drift.test.js +329 -0
  26. package/lib/__tests__/doctor-machine-mutation.test.js +116 -0
  27. package/lib/__tests__/doctor-module-drift.test.js +206 -0
  28. package/lib/__tests__/doctor-module-resolution-parity.test.js +314 -0
  29. package/lib/__tests__/doctor-path-equivalence.test.js +59 -0
  30. package/lib/__tests__/doctor-repair-boundary.test.js +52 -0
  31. package/lib/__tests__/doctor-tui-skill-freshness.test.js +105 -0
  32. package/lib/__tests__/doctor.test.js +155 -0
  33. package/lib/__tests__/gate-runner.test.js +19 -3
  34. package/lib/__tests__/helpers/lib-sandbox.cjs +55 -0
  35. package/lib/__tests__/init.test.js +44 -17
  36. package/lib/__tests__/install-skill-full-dir.test.js +151 -0
  37. package/lib/__tests__/install-skill.test.js +51 -132
  38. package/lib/__tests__/language-tools-detect.test.js +158 -0
  39. package/lib/__tests__/phase-transition.test.js +15 -1
  40. package/lib/__tests__/qoder-delphi-agents.test.js +285 -0
  41. package/lib/__tests__/shared-paths.test.js +20 -0
  42. package/lib/__tests__/shared-utils.test.js +69 -0
  43. package/lib/__tests__/skill-flags.test.js +48 -0
  44. package/lib/__tests__/ui-detector.test.ts +6 -2
  45. package/lib/__tests__/uninstall.test.js +20 -0
  46. package/lib/__tests__/update-hooks.test.js +12 -0
  47. package/lib/__tests__/update-skill.test.js +7 -4
  48. package/lib/detect-deps.js +3 -20
  49. package/lib/doctor-tui.js +22 -5
  50. package/lib/doctor.js +1178 -31
  51. package/lib/init.js +184 -33
  52. package/lib/install-cmd.js +7 -4
  53. package/lib/install-skill.js +38 -95
  54. package/lib/language-tools.js +83 -4
  55. package/lib/mattpocock-license-verdict.json +1 -1
  56. package/lib/shared-paths.js +5 -2
  57. package/lib/shared-utils.js +47 -3
  58. package/lib/uninstall.js +12 -0
  59. package/lib/update-hooks.js +4 -4
  60. package/lib/update-skill.js +6 -7
  61. package/mock-policy/AGENTS.md +4 -4
  62. package/mutation/AGENTS.md +4 -4
  63. package/mutation/__tests__/gate-m.test.ts +66 -14
  64. package/mutation/__tests__/mutmut-runner.test.ts +91 -42
  65. package/mutation/__tests__/test-filter-parity.test.ts +37 -0
  66. package/mutation/gate-m.ts +21 -8
  67. package/package.json +1 -1
  68. package/plugins/claude-code/.claude-plugin/plugin.json +2 -2
  69. package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  70. package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
  71. package/plugins/claude-code/skills/delphi-review/SKILL.md +98 -14
  72. package/plugins/claude-code/skills/delphi-review/references/code-walkthrough.md +36 -3
  73. package/plugins/claude-code/skills/delphi-review/references/requirements.md +6 -6
  74. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
  75. package/plugins/claude-code/skills/sprint-flow/references/phase-2-design.md +3 -3
  76. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
  77. package/plugins/dsh/README.md +169 -4
  78. package/plugins/dsh/dsh-overlay.patch.yml +67 -0
  79. package/plugins/dsh/package.json +1 -1
  80. package/plugins/dsh/scripts/prepack.cjs +24 -19
  81. package/plugins/dsh/setup-dsh.ps1 +160 -0
  82. package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  83. package/plugins/dsh/skills/delphi-review/AGENTS.md +4 -4
  84. package/plugins/dsh/skills/delphi-review/SKILL.md +98 -14
  85. package/plugins/dsh/skills/delphi-review/references/code-walkthrough.md +36 -3
  86. package/plugins/dsh/skills/delphi-review/references/requirements.md +6 -6
  87. package/plugins/dsh/skills/sprint-flow/AGENTS.md +4 -4
  88. package/plugins/dsh/skills/sprint-flow/references/phase-2-design.md +3 -3
  89. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +4 -4
  90. package/plugins/dsh/src/command.test.ts +66 -2
  91. package/plugins/dsh/src/command.ts +36 -14
  92. package/plugins/dsh/src/contract.test.ts +31 -1
  93. package/plugins/dsh/src/delphi-config.test.ts +190 -0
  94. package/plugins/dsh/src/delphi-config.ts +223 -0
  95. package/plugins/dsh/src/delphi-run.test.ts +181 -0
  96. package/plugins/dsh/src/delphi-run.ts +129 -0
  97. package/plugins/dsh/src/delphi.test.ts +222 -0
  98. package/plugins/dsh/src/delphi.ts +247 -0
  99. package/plugins/dsh/src/dialect.test.ts +59 -0
  100. package/plugins/dsh/src/dialect.ts +40 -0
  101. package/plugins/dsh/src/index.ts +70 -4
  102. package/plugins/dsh/src/skills.test.ts +26 -19
  103. package/plugins/opencode/package.json +1 -1
  104. package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  105. package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
  106. package/plugins/opencode/skills/delphi-review/SKILL.md +98 -14
  107. package/plugins/opencode/skills/delphi-review/references/code-walkthrough.md +36 -3
  108. package/plugins/opencode/skills/delphi-review/references/requirements.md +6 -6
  109. package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
  110. package/plugins/opencode/skills/sprint-flow/references/phase-2-design.md +3 -3
  111. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
  112. package/plugins/qoder/agents/delphi-architecture.md +1 -1
  113. package/plugins/qoder/agents/delphi-feasibility.md +1 -1
  114. package/plugins/qoder/agents/delphi-technical.md +1 -1
  115. package/plugins/qoder/plugin.json +2 -2
  116. package/plugins/qoder/skills/admin-template-guidelines/SKILL.md +6 -797
  117. package/plugins/qoder/skills/admin-template-guidelines/references/rule-1.md +91 -0
  118. package/plugins/qoder/skills/admin-template-guidelines/references/rule-2.md +155 -0
  119. package/plugins/qoder/skills/admin-template-guidelines/references/rule-3.md +70 -0
  120. package/plugins/qoder/skills/admin-template-guidelines/references/rule-4.md +154 -0
  121. package/plugins/qoder/skills/admin-template-guidelines/references/rule-5.md +126 -0
  122. package/plugins/qoder/skills/admin-template-guidelines/references/rule-6.md +180 -0
  123. package/plugins/qoder/skills/batch-grill-me/SKILL.md +15 -0
  124. package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  125. package/plugins/qoder/skills/clipboard-vision/describe-clipboard.ps1 +115 -0
  126. package/plugins/qoder/skills/clipboard-vision/describe-clipboard.sh +161 -0
  127. package/plugins/qoder/skills/delphi-review/.delphi-config.json.example +61 -28
  128. package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
  129. package/plugins/qoder/skills/delphi-review/INSTALL.md +97 -98
  130. package/plugins/qoder/skills/delphi-review/SKILL.md +170 -63
  131. package/plugins/qoder/skills/delphi-review/hooks.md +19 -0
  132. package/plugins/qoder/skills/delphi-review/opencode.json.delphi.example +6 -10
  133. package/plugins/qoder/skills/delphi-review/references/code-walkthrough.md +36 -3
  134. package/plugins/qoder/skills/delphi-review/references/orchestrator-dispatch.md +64 -0
  135. package/plugins/qoder/skills/delphi-review/references/requirements.md +402 -0
  136. package/plugins/qoder/skills/delphi-review/references/round-templates.md +134 -0
  137. package/plugins/qoder/skills/delphi-review/scripts/verify-consensus.sh +43 -0
  138. package/plugins/qoder/skills/delphi-review/tools.md +29 -0
  139. package/plugins/qoder/skills/domain-modeling/ADR-FORMAT.md +64 -0
  140. package/plugins/qoder/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  141. package/plugins/qoder/skills/domain-modeling/SKILL.md +74 -0
  142. package/plugins/qoder/skills/grill-with-docs/SKILL.md +7 -0
  143. package/plugins/qoder/skills/grilling/SKILL.md +12 -0
  144. package/plugins/qoder/skills/ralph-loop/SKILL.md +87 -428
  145. package/plugins/qoder/skills/ralph-loop/references/components.md +113 -0
  146. package/plugins/qoder/skills/ralph-loop/references/workflow-details.md +99 -0
  147. package/plugins/qoder/skills/sprint-flow/AGENTS.md +5 -5
  148. package/plugins/qoder/skills/sprint-flow/SKILL.md +255 -454
  149. package/plugins/qoder/skills/sprint-flow/__tests__/sprint-flow.test.cjs +457 -0
  150. package/plugins/qoder/skills/sprint-flow/hooks.md +18 -0
  151. package/plugins/qoder/skills/sprint-flow/references/components/memory.md +1 -1
  152. package/plugins/qoder/skills/sprint-flow/references/components/middleware.md +1 -1
  153. package/plugins/qoder/skills/sprint-flow/references/components/skill-invocations.md +21 -20
  154. package/plugins/qoder/skills/sprint-flow/references/components/tool-descriptions.md +22 -23
  155. package/plugins/qoder/skills/sprint-flow/references/force-levels.md +0 -1
  156. package/plugins/qoder/skills/sprint-flow/references/orchestration-rules.md +34 -27
  157. package/plugins/qoder/skills/sprint-flow/references/phase-1-prep.md +1 -1
  158. package/plugins/qoder/skills/sprint-flow/references/phase-2-design.md +160 -123
  159. package/plugins/qoder/skills/sprint-flow/references/phase-3-build.md +203 -2
  160. package/plugins/qoder/skills/sprint-flow/references/phase-4-verify.md +107 -34
  161. package/plugins/qoder/skills/sprint-flow/references/phase-5-ship.md +194 -12
  162. package/plugins/qoder/skills/sprint-flow/references/phase-6-close.md +139 -7
  163. package/plugins/qoder/skills/sprint-flow/references/phase-overview.md +486 -0
  164. package/plugins/qoder/skills/sprint-flow/scripts/verify-sprint-state.sh +38 -0
  165. package/plugins/qoder/skills/sprint-flow/tools.md +29 -0
  166. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
  167. package/plugins/qoder/skills/test-specification-alignment/SKILL.md +104 -601
  168. package/plugins/qoder/skills/to-issues/SKILL.md +1 -1
  169. package/principles/AGENTS.md +9 -8
  170. package/principles/__tests__/adapter-metrics.test.ts +292 -0
  171. package/principles/__tests__/analyzer.test.ts +29 -6
  172. package/principles/__tests__/baseline-prune-timeout.test.ts +71 -0
  173. package/principles/__tests__/baseline-prune.test.ts +338 -0
  174. package/principles/__tests__/boy-scout-init-baseline.test.ts +167 -0
  175. package/principles/__tests__/boy-scout-integration.test.ts +97 -6
  176. package/principles/__tests__/boy-scout.test.ts +253 -7
  177. package/principles/__tests__/config-enforcement.test.ts +469 -0
  178. package/principles/__tests__/config-git-timeout.test.ts +66 -0
  179. package/principles/__tests__/direct-execution.test.ts +187 -0
  180. package/principles/__tests__/index.test.ts +16 -3
  181. package/principles/__tests__/warnings-baseline-budgets.test.ts +74 -0
  182. package/principles/adapters/typescript.ts +613 -13
  183. package/principles/analyzer.ts +22 -0
  184. package/principles/baseline-prune.ts +217 -0
  185. package/principles/boy-scout.ts +481 -51
  186. package/principles/config.ts +362 -54
  187. package/principles/direct-execution.ts +60 -0
  188. package/principles/index.ts +40 -17
  189. package/principles/rules/__tests__/clean-code/many-exports.test.ts +16 -10
  190. package/principles/rules/clean-code/code-duplication.ts +8 -8
  191. package/principles/rules/clean-code/deep-nesting.ts +8 -8
  192. package/principles/rules/clean-code/god-class.ts +8 -8
  193. package/principles/rules/clean-code/large-file.ts +15 -11
  194. package/principles/rules/clean-code/long-function.ts +8 -8
  195. package/principles/rules/clean-code/magic-numbers.ts +10 -7
  196. package/principles/rules/clean-code/many-exports.ts +7 -7
  197. package/principles/rules/clean-code/missing-error-handling.ts +5 -5
  198. package/principles/rules/clean-code/too-many-params.ts +8 -8
  199. package/principles/rules/clean-code/unused-imports.ts +5 -5
  200. package/principles/rules/solid/dip.ts +18 -8
  201. package/principles/rules/solid/isp.ts +9 -8
  202. package/principles/rules/solid/lsp.ts +6 -6
  203. package/principles/rules/solid/ocp.ts +6 -5
  204. package/principles/rules/solid/srp.ts +7 -8
  205. package/principles/types.ts +15 -0
  206. package/scripts/__tests__/sync-package-content.test.js +28 -0
  207. package/scripts/delphi-external-review.cjs +193 -10
  208. package/scripts/sync-package-content.js +7 -4
  209. package/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  210. package/skills/delphi-review/AGENTS.md +4 -4
  211. package/skills/delphi-review/SKILL.md +98 -14
  212. package/skills/delphi-review/references/code-walkthrough.md +36 -3
  213. package/skills/delphi-review/references/requirements.md +6 -6
  214. package/skills/sprint-flow/AGENTS.md +4 -4
  215. package/skills/sprint-flow/references/phase-2-design.md +3 -3
  216. package/skills/test-specification-alignment/AGENTS.md +4 -4
  217. package/lib/__tests__/download-skill.test.js +0 -281
  218. package/lib/download-skill.js +0 -96
  219. package/plugins/qoder/skills/ralph-loop/references/components/memory.md +0 -55
  220. package/plugins/qoder/skills/ralph-loop/references/components/middleware.md +0 -54
  221. package/plugins/qoder/skills/ralph-loop/references/components/skill-invocations.md +0 -39
  222. package/plugins/qoder/skills/ralph-loop/references/components/system-prompt.md +0 -24
  223. package/plugins/qoder/skills/ralph-loop/references/components/tool-descriptions.md +0 -32
@@ -34,7 +34,7 @@ function readXpGateConfig() {
34
34
  }
35
35
 
36
36
  function copyHooks(srcDir, destDir) {
37
- ['pre-commit', 'pre-push'].forEach(hook => {
37
+ ['pre-commit', 'pre-push', 'post-merge'].forEach(hook => {
38
38
  const src = path.join(srcDir, 'hooks', hook);
39
39
  const dest = path.join(destDir, hook);
40
40
  if (fs.existsSync(src)) {
@@ -64,7 +64,7 @@ function copyAdapters(srcDir, destDir) {
64
64
  }
65
65
  });
66
66
  }
67
- const githooksDir = path.resolve(srcDir, '..', '..', '..', 'githooks');
67
+ const githooksDir = path.resolve(srcDir, '..', '..', 'githooks');
68
68
  if (fs.existsSync(githooksDir)) {
69
69
  fs.readdirSync(githooksDir).forEach(f => {
70
70
  if ((f.startsWith('gate-') || f === 'sprint-gate.sh') && f.endsWith('.sh')) {
@@ -72,6 +72,50 @@ function copyAdapters(srcDir, destDir) {
72
72
  }
73
73
  });
74
74
  }
75
+ // Gate scripts ship at the package root — sync-package-content.js copies them
76
+ // from githooks/ during prepack. Installed environments have no repo-level
77
+ // githooks/, so the package root is the only reliable source.
78
+ if (fs.existsSync(srcDir)) {
79
+ fs.readdirSync(srcDir).forEach(f => {
80
+ if ((f.startsWith('gate-') || f === 'sprint-gate.sh') && f.endsWith('.sh')) {
81
+ fs.copyFileSync(path.join(srcDir, f), path.join(destDir, f));
82
+ }
83
+ });
84
+ }
85
+ }
86
+
87
+ /**
88
+ * The flags each skill command consumes. A flag outside that command's list used
89
+ * to be dropped silently, so `xp-gate install-skill <name> --offline` — the form
90
+ * the CLI documented until the bundle-only installer (#416) removed it — exited 0
91
+ * having done nothing with the flag. Same defect class as the one #488 fixed for
92
+ * `doctor --sync-hooks`: an ignored option must be answered loudly. A flag that
93
+ * parses but is never destructured by the handler is ignored too, so the table is
94
+ * keyed per command (Round 2 architecture MI-04).
95
+ */
96
+ const SKILL_FLAGS_BY_COMMAND = {
97
+ 'install-skill': ['--verbose', '--force'],
98
+ 'update-skill': ['--verbose', '--all', '--check'],
99
+ 'uninstall-skill': ['--force'],
100
+ };
101
+
102
+ /**
103
+ * @param {string[]} args - arguments after the skill name
104
+ * @param {string} command - the subcommand whose flags are accepted
105
+ * @returns {{options: Object.<string, boolean>, unknown: string[]}}
106
+ */
107
+ function parseSkillFlags(args, command) {
108
+ const options = { verbose: false, force: false, all: false, check: false };
109
+ const allowed = SKILL_FLAGS_BY_COMMAND[command] || [];
110
+ const unknown = [];
111
+ for (const arg of args) {
112
+ if (!allowed.includes(arg)) {
113
+ unknown.push(arg);
114
+ continue;
115
+ }
116
+ options[arg.slice(2)] = true;
117
+ }
118
+ return { options, unknown };
75
119
  }
76
120
 
77
- module.exports = { copyDirRecursive, readXpGateConfig, copyHooks, copyAdapters };
121
+ module.exports = { copyDirRecursive, readXpGateConfig, copyHooks, copyAdapters, SKILL_FLAGS_BY_COMMAND, parseSkillFlags };
package/lib/uninstall.js CHANGED
@@ -19,6 +19,7 @@ const BACKUP_DIR = path.join(CONFIG_DIR, '.uninstall-backup');
19
19
  const SIGNATURES = {
20
20
  'pre-commit': 'OpenCode Quality Gates - Pre-Commit Hook',
21
21
  'pre-push': 'Pre-push Hook - Code Walkthrough Result Validator',
22
+ 'post-merge': 'XP-Gate VERSION sync hook',
22
23
  'adapter-common.sh': 'detect_project_lang()'
23
24
  };
24
25
 
@@ -201,6 +202,14 @@ function buildPlan(mode) {
201
202
  manifestKey: '.git/hooks/pre-push',
202
203
  critical: true
203
204
  });
205
+ plan.push({
206
+ type: 'file',
207
+ path: path.join(hooksDir, 'post-merge'),
208
+ signature: SIGNATURES['post-merge'],
209
+ label: '.git/hooks/post-merge',
210
+ manifestKey: '.git/hooks/post-merge',
211
+ critical: true
212
+ });
204
213
  plan.push({
205
214
  type: 'file',
206
215
  path: path.join(githooksDir, 'adapter-common.sh'),
@@ -489,6 +498,9 @@ async function uninstall(args) {
489
498
 
490
499
  // Print summary
491
500
  console.log('\nUninstall complete');
501
+ console.log('Note: Qoder Delphi expert agents (.qoder/agents/delphi-*.md) are user-owned files');
502
+ console.log(' that init never overwrites, so they were left in place — delete them by hand');
503
+ console.log(' (then restart the Qoder session) if you want them gone.');
492
504
  if (hadErrors) {
493
505
  console.log('Some items could not be removed. Run doctor for diagnostics.');
494
506
  }
@@ -38,8 +38,8 @@ function getProjectHooksDir() {
38
38
  function detectLocalModifications(srcDir, hooksDestDir, adaptersDestDir) {
39
39
  const modified = [];
40
40
 
41
- // Check hook files: pre-commit, pre-push
42
- ['pre-commit', 'pre-push'].forEach(hook => {
41
+ // Check hook files: pre-commit, pre-push, post-merge
42
+ ['pre-commit', 'pre-push', 'post-merge'].forEach(hook => {
43
43
  const srcPath = path.join(srcDir, 'hooks', hook);
44
44
  const destPath = path.join(hooksDestDir, hook);
45
45
  if (fs.existsSync(destPath) && fs.existsSync(srcPath)) {
@@ -150,7 +150,7 @@ function atomicCopyFile(src, dest, dryRun, noBackup, label) {
150
150
  }
151
151
 
152
152
  /**
153
- * Copy hook files (pre-commit, pre-push) from package to destination.
153
+ * Copy hook files (pre-commit, pre-push, post-merge) from package to destination.
154
154
  * @param {string} srcDir - Package root directory
155
155
  * @param {string} destDir - Destination hooks directory
156
156
  * @param {boolean} dryRun
@@ -158,7 +158,7 @@ function atomicCopyFile(src, dest, dryRun, noBackup, label) {
158
158
  */
159
159
  function copyHooks(srcDir, destDir, dryRun, noBackup) {
160
160
  const hooksSrcDir = path.join(srcDir, 'hooks');
161
- ['pre-commit', 'pre-push'].forEach(hook => {
161
+ ['pre-commit', 'pre-push', 'post-merge'].forEach(hook => {
162
162
  const src = path.join(hooksSrcDir, hook);
163
163
  const dest = path.join(destDir, hook);
164
164
  atomicCopyFile(src, dest, dryRun, noBackup, hook);
@@ -6,7 +6,6 @@ const os = require('os');
6
6
  const HOME = process.env.HOME || process.env.USERPROFILE || os.homedir();
7
7
 
8
8
  const CONFIG_DIR = path.join(HOME, '.config', 'xp-gate');
9
- const SKILLS_DIR = path.join(HOME, '.config', 'opencode', 'skills');
10
9
 
11
10
  function handleCheckMode() {
12
11
  console.log('Checking for updates...');
@@ -59,12 +58,12 @@ async function updateSkill(name, options = {}) {
59
58
 
60
59
  async function updateSingleSkill(name, verbose) {
61
60
  console.log(`Updating ${name}...`);
62
-
63
- const targetDir = path.join(SKILLS_DIR, name);
64
- if (fs.existsSync(targetDir)) {
65
- fs.rmSync(targetDir, { recursive: true });
66
- }
67
-
61
+
62
+ // Everything destructive lives behind installSkill()'s validation: it resolves
63
+ // the platform skills directory, backs the existing copy up, and only then
64
+ // replaces it. Deleting here first (#416) wiped the wrong copy on Qoder and
65
+ // Claude Code, and turned an update of a skill the package no longer carries
66
+ // into an unrecoverable loss.
68
67
  const { installSkill } = require('./install-skill.js');
69
68
  const result = await installSkill(name, { force: true, verbose });
70
69
 
@@ -1,9 +1,9 @@
1
1
  # SRC/MOCK-POLICY KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-09-22
4
- **Commit:** 3d1d1bf
5
- **Branch:** main
6
- **Version:** 0.19.2.0
3
+ **Generated:** 2026-10-07
4
+ **Commit:** ff55f35b
5
+ **Branch:** fix/batch-defects-20261007
6
+ **Version:** 0.20.0.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Mock layering policy enforcement — Gate M3 of pre-push hook. Ensures integration tests use real implementations for internal dependencies, mock external dependencies, and annotate pending mocks with removal plans. Combines project scope scanning, mock decision engine, and per-file validation into a single pipeline.
@@ -1,9 +1,9 @@
1
1
  # SRC/MUTATION KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-09-22
4
- **Commit:** 3d1d1bf
5
- **Branch:** main
6
- **Version:** 0.19.2.0
3
+ **Generated:** 2026-10-07
4
+ **Commit:** ff55f35b
5
+ **Branch:** fix/batch-defects-20261007
6
+ **Version:** 0.20.0.0
7
7
 
8
8
  ## OVERVIEW
9
9
  **Gate M** (incremental mutation testing) + **Gate M2** helpers (test-layer detection used by `src/mock-policy/`). Pre-push quality gate. TypeScript-only; uses Stryker.
@@ -7,8 +7,10 @@
7
7
  import { describe, it, expect, vi } from 'vitest';
8
8
  import fs from 'fs/promises';
9
9
 
10
- // We need to test parseArgs and filterSourceFiles from gate-m
11
- // Import the module and test its public interface
10
+ // The REAL filter is under test: the previous local duplicate diverged from
11
+ // gate-m.ts (which is multi-language via registered runners) and could not
12
+ // catch #480 for that exact reason.
13
+ import { filterSourceFiles } from '../gate-m';
12
14
  import { detectAITestCharacteristics } from '../detect-ai-test';
13
15
 
14
16
  /* jscpd:disable */
@@ -56,16 +58,6 @@ function parseArgs(args: string[]): {
56
58
  }
57
59
  /* jscpd:enable */
58
60
 
59
- function filterSourceFiles(files: string[]): string[] {
60
- return files.filter(file => {
61
- if (!file.endsWith('.ts')) return false;
62
- if (file.endsWith('.test.ts')) return false;
63
- if (file.endsWith('.d.ts')) return false;
64
- if (file.includes('/adapters/')) return false;
65
- return true;
66
- });
67
- }
68
-
69
61
  vi.mock('fs/promises');
70
62
 
71
63
  describe('gate-m.ts - Mutation Testing Gate', () => {
@@ -126,11 +118,71 @@ describe('gate-m.ts - Mutation Testing Gate', () => {
126
118
  expect(result).toEqual(['src/foo.ts']);
127
119
  });
128
120
 
129
- it('should filter out non-TypeScript files', () => {
121
+ it('should filter out files with no registered runner', () => {
130
122
  const files = ['src/foo.ts', 'src/bar.js', 'src/baz.py'];
131
123
  const result = filterSourceFiles(files);
132
124
 
133
- expect(result).toEqual(['src/foo.ts']);
125
+ // .ts (stryker) and .py (mutmut) have registered runners; .js has none.
126
+ expect(result).toEqual(['src/foo.ts', 'src/baz.py']);
127
+ });
128
+
129
+ it('should filter out test-tree helpers not named *.test.* (#480)', () => {
130
+ const files = [
131
+ 'tests/e2e/helpers/e2e-server.ts',
132
+ 'test/fixtures/setup.py',
133
+ 'src/__tests__/harness.ts',
134
+ 'tests\\e2e\\helpers\\e2e-server.ts',
135
+ ];
136
+ const result = filterSourceFiles(files);
137
+
138
+ expect(result).toEqual([]);
139
+
140
+ // The same exclusion inside a list that also carries production source:
141
+ // `a/b/tests/` is a tree segment, and src/foo.ts must survive next to it.
142
+ expect(filterSourceFiles(['src/foo.ts', 'a/b/tests/fixture-factory.ts'])).toEqual(['src/foo.ts']);
143
+ });
144
+
145
+ // Round 2 feasibility FC-03: only the test-tree regex read `normalized`, the
146
+ // `.test.`/`_test.`, Java/Kotlin and /adapters/ checks read the raw path, so a
147
+ // backslash-delimited path was judged by two different views of one string.
148
+ it('should judge every exclusion against one separator view of the path (#480)', () => {
149
+ for (const files of [
150
+ ['src\\adapters\\router.ts'],
151
+ ['src\\FooTest.java'],
152
+ ['src/FooTest.java'],
153
+ ['src/PaymentTests.kt'],
154
+ ['src/BillingSpec.kts'],
155
+ ['src\\helper_test.go'],
156
+ ['src\\__tests__\\harness.ts'],
157
+ ['src\\legacy.d.ts'],
158
+ ]) {
159
+ expect(filterSourceFiles(files)).toEqual([]);
160
+ }
161
+ });
162
+
163
+ // The same class as #480: pre-push excludes *.spec.* from the mutation
164
+ // candidate list, gate-m.ts did not, so the two definitions of "not
165
+ // production code" already disagreed on spec-named test files.
166
+ it('should filter out *.spec.* test files, as pre-push already does (#480)', () => {
167
+ expect(filterSourceFiles(['src/login.spec.ts'])).toEqual([]);
168
+ expect(filterSourceFiles(['src\\login.spec.ts'])).toEqual([]);
169
+ });
170
+
171
+ it('should not exclude production paths that merely contain "test" as a substring (#480)', () => {
172
+ // Round 2 technical MN-02 asked whether the tree segment is anchored: it is,
173
+ // so only a whole path segment named test/tests/__tests__ excludes.
174
+ const files = [
175
+ 'src/latest/foo.ts',
176
+ 'src/protest/foo.ts',
177
+ 'src/test-utils/foo.ts',
178
+ 'src/mytests/foo.ts',
179
+ 'testing/reporter.ts',
180
+ 'src/test-data.ts',
181
+ 'tests-helper.ts',
182
+ ];
183
+ const result = filterSourceFiles(files);
184
+
185
+ expect(result).toEqual(files);
134
186
  });
135
187
 
136
188
  it('should return empty array when no source files', () => {
@@ -32,6 +32,61 @@ describe('MutmutRunner', () => {
32
32
  let MutmutRunner: typeof import('../runners/mutmut-runner').MutmutRunner;
33
33
  let tmpDir: string;
34
34
 
35
+ /**
36
+ * Wait until the runner has actually spawned the child process.
37
+ *
38
+ * `run()` awaits `isAvailable()` when the platform is win32 (the route is
39
+ * resolved via a version probe), so `spawn` is NOT called synchronously with
40
+ * `run()`. Calling `stdoutCb` right after `run()` therefore raced on Windows:
41
+ * the callback had not been captured yet and the test threw
42
+ * `stdoutCb is not a function`. Linux skipped the await entirely, which is why
43
+ * CI stayed green while the same suite failed locally.
44
+ */
45
+ async function waitForSpawn(): Promise<void> {
46
+ for (let i = 0; i < 50 && !vi.mocked(spawn).mock.calls.length; i++) {
47
+ await new Promise(resolve => setTimeout(resolve, 0));
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Build a fake child process whose `stdout`/`close` callbacks are captured so a
53
+ * test can drive them by hand. Shared because every spawn-based test needs the
54
+ * same shape, and inlining it in each one pushed those callbacks past the
55
+ * function-length rule.
56
+ */
57
+ function mockChildProcess(capture: {
58
+ stdout?: (cb: (d: Buffer) => void) => void;
59
+ stderr?: (cb: (d: Buffer) => void) => void;
60
+ close?: (cb: (code: number | null) => void) => void;
61
+ /** Run when `close` is subscribed, instead of the test driving it by hand. */
62
+ onClose?: (emit: { stderr: (d: Buffer) => void; close: (code: number) => void }) => void;
63
+ }) {
64
+ let stderrCb: ((d: Buffer) => void) | null = null;
65
+ let closeCb: ((code: number | null) => void) | null = null;
66
+ return {
67
+ stdout: {
68
+ on: vi.fn((_event: string, cb: (d: Buffer) => void) => capture.stdout?.(cb)),
69
+ },
70
+ stderr: {
71
+ on: vi.fn((_event: string, cb: (d: Buffer) => void) => {
72
+ stderrCb = cb;
73
+ capture.stderr?.(cb);
74
+ }),
75
+ },
76
+ on: vi.fn((event: string, cb: (code: number | null) => void) => {
77
+ if (event !== 'close') return;
78
+ closeCb = cb;
79
+ capture.close?.(cb);
80
+ capture.onClose?.({
81
+ stderr: d => stderrCb?.(d),
82
+ close: code => closeCb?.(code),
83
+ });
84
+ }),
85
+ kill: vi.fn(),
86
+ killed: false,
87
+ } as unknown as ChildProcess;
88
+ }
89
+
35
90
  beforeEach(async () => {
36
91
  vi.clearAllMocks();
37
92
  vi.mocked(execSync).mockReset();
@@ -140,20 +195,12 @@ describe('MutmutRunner', () => {
140
195
  let stdoutCb: ((d: Buffer) => void) | null = null;
141
196
  let closeCb: ((code: number | null) => void) | null = null;
142
197
 
143
- const mockChild = {
144
- stdout: {
145
- on: vi.fn((_event: string, cb: (d: Buffer) => void) => {
146
- stdoutCb = cb;
147
- }),
148
- },
149
- stderr: { on: vi.fn() },
150
- on: vi.fn((event: string, cb: (code: number | null) => void) => {
151
- if (event === 'close') closeCb = cb;
198
+ vi.mocked(spawn).mockReturnValue(
199
+ mockChildProcess({
200
+ stdout: cb => { stdoutCb = cb; },
201
+ close: cb => { closeCb = cb; },
152
202
  }),
153
- kill: vi.fn(),
154
- killed: false,
155
- };
156
- vi.mocked(spawn).mockReturnValue(mockChild as unknown as ChildProcess);
203
+ );
157
204
 
158
205
  // No existing pyproject.toml
159
206
  vi.mocked(existsSync).mockReturnValue(false);
@@ -165,9 +212,11 @@ describe('MutmutRunner', () => {
165
212
  cwd: tmpDir,
166
213
  });
167
214
 
168
- // Simulate stdout with emoji progress
215
+ // Simulate stdout with emoji progress. One wait covers both callbacks --
216
+ // they are captured by the same spawn invocation.
169
217
  const progressOutput =
170
218
  '38/38 🎉 9 🫥 29 ⏰ 0 🤔 0 🙁 0 🔇 0 🧙 0\n';
219
+ await waitForSpawn();
171
220
  stdoutCb!(Buffer.from(progressOutput));
172
221
  closeCb!(0);
173
222
 
@@ -211,6 +260,7 @@ describe('MutmutRunner', () => {
211
260
  timeoutMs: 60000,
212
261
  cwd: 'C:\\work\\project',
213
262
  });
263
+ await waitForSpawn();
214
264
  closeCb!(0);
215
265
  await promise;
216
266
 
@@ -249,6 +299,7 @@ describe('MutmutRunner', () => {
249
299
  timeoutMs: 60000,
250
300
  cwd: 'C:\\work\\project',
251
301
  });
302
+ await waitForSpawn();
252
303
  closeCb!(0);
253
304
  await promise;
254
305
 
@@ -285,6 +336,7 @@ describe('MutmutRunner', () => {
285
336
  timeoutMs: 60000,
286
337
  cwd: 'C:\\work dir\\safe; echo injected',
287
338
  });
339
+ await waitForSpawn();
288
340
  closeCb!(0);
289
341
  await promise;
290
342
 
@@ -320,6 +372,7 @@ describe('MutmutRunner', () => {
320
372
  const runner = new MutmutRunner('win32');
321
373
  expect(await runner.isAvailable()).toBe(true);
322
374
  const promise = runner.run({ files: ['src/main.py'], timeoutMs: 60000, cwd: windowsPath });
375
+ await waitForSpawn();
323
376
  closeCb!(0);
324
377
  await promise;
325
378
 
@@ -368,6 +421,7 @@ describe('MutmutRunner', () => {
368
421
  nativeAvailable = false;
369
422
  expect(await runner.isAvailable()).toBe(true);
370
423
  const promise = runner.run({ files: ['src/main.py'], timeoutMs: 60000, cwd: 'C:\\repo' });
424
+ await waitForSpawn();
371
425
  closeCb!(0);
372
426
  await promise;
373
427
 
@@ -489,6 +543,7 @@ describe('MutmutRunner', () => {
489
543
  expect(await runner.isAvailable()).toBe(true);
490
544
  const promise = runner.run({ files: ['src/main.py'], timeoutMs: 100, cwd: 'C:\\repo' });
491
545
  await vi.advanceTimersByTimeAsync(100);
546
+ await waitForSpawn();
492
547
  closeCb!(1);
493
548
  const outcome = await promise;
494
549
 
@@ -622,26 +677,18 @@ describe('MutmutRunner', () => {
622
677
  });
623
678
 
624
679
  it('should return error message when exit code is non-zero', async () => {
625
- let stderrCb: ((d: Buffer) => void) | null = null;
626
- const mockChild = {
627
- stdout: { on: vi.fn() },
628
- stderr: {
629
- on: vi.fn((_event: string, cb: (d: Buffer) => void) => {
630
- stderrCb = cb;
631
- }),
632
- },
633
- on: vi.fn((event: string, cb: (code: number | null) => void) => {
634
- if (event === 'close') {
680
+ // The stderr line and the non-zero exit must arrive together, so the
681
+ // factory emits both as soon as the runner subscribes to `close`.
682
+ vi.mocked(spawn).mockReturnValue(
683
+ mockChildProcess({
684
+ onClose: emit => {
635
685
  setTimeout(() => {
636
- if (stderrCb) stderrCb(Buffer.from('mutation failed'));
637
- cb(1);
686
+ emit.stderr(Buffer.from('mutation failed'));
687
+ emit.close(1);
638
688
  }, 10);
639
- }
689
+ },
640
690
  }),
641
- kill: vi.fn(),
642
- killed: false,
643
- };
644
- vi.mocked(spawn).mockReturnValue(mockChild as unknown as ChildProcess);
691
+ );
645
692
  vi.mocked(existsSync).mockReturnValue(false);
646
693
 
647
694
  const runner = new MutmutRunner();
@@ -685,6 +732,7 @@ describe('MutmutRunner', () => {
685
732
  });
686
733
 
687
734
  const output = '20/20 🎉 10 🫥 5 ⏰ 2 🤔 1 🙁 2 🔇 0 🧙 0\n';
735
+ await waitForSpawn();
688
736
  stdoutCb!(Buffer.from(output));
689
737
  closeCb!(0);
690
738
 
@@ -724,9 +772,11 @@ describe('MutmutRunner', () => {
724
772
  cwd: tmpDir,
725
773
  });
726
774
 
775
+ await waitForSpawn();
776
+
727
777
  stdoutCb!(Buffer.from('Some random output without emoji\n'));
778
+ await waitForSpawn();
728
779
  closeCb!(0);
729
-
730
780
  const result = await promise;
731
781
  expect(result.report).toBeNull();
732
782
  expect(result.timedOut).toBe(false);
@@ -760,6 +810,7 @@ describe('MutmutRunner', () => {
760
810
  });
761
811
 
762
812
  const output = '0/0 🎉 0 🫥 0 ⏰ 0 🤔 0 🙁 0 🔇 0 🧙 0\n';
813
+ await waitForSpawn();
763
814
  stdoutCb!(Buffer.from(output));
764
815
  closeCb!(0);
765
816
 
@@ -789,6 +840,7 @@ describe('MutmutRunner', () => {
789
840
  timeoutMs: 60000,
790
841
  cwd: tmpDir,
791
842
  });
843
+ await waitForSpawn();
792
844
  closeCb!(0);
793
845
  await promise;
794
846
 
@@ -839,6 +891,7 @@ describe('MutmutRunner', () => {
839
891
  timeoutMs: 60000,
840
892
  cwd: tmpDir,
841
893
  });
894
+ await waitForSpawn();
842
895
  closeCb!(0);
843
896
  await promise;
844
897
 
@@ -888,6 +941,7 @@ describe('MutmutRunner', () => {
888
941
  timeoutMs: 60000,
889
942
  cwd: tmpDir,
890
943
  });
944
+ await waitForSpawn();
891
945
  closeCb!(0);
892
946
  await promise;
893
947
 
@@ -905,16 +959,9 @@ describe('MutmutRunner', () => {
905
959
  describe('cleanup', () => {
906
960
  it('should restore backup after run completes', async () => {
907
961
  let closeCb: ((code: number | null) => void) | null = null;
908
- const mockChild = {
909
- stdout: { on: vi.fn() },
910
- stderr: { on: vi.fn() },
911
- on: vi.fn((event: string, cb: (code: number | null) => void) => {
912
- if (event === 'close') closeCb = cb;
913
- }),
914
- kill: vi.fn(),
915
- killed: false,
916
- };
917
- vi.mocked(spawn).mockReturnValue(mockChild as unknown as ChildProcess);
962
+ vi.mocked(spawn).mockReturnValue(
963
+ mockChildProcess({ close: cb => { closeCb = cb; } }),
964
+ );
918
965
 
919
966
  vi.mocked(existsSync).mockImplementation((path) => {
920
967
  const p = path.toString();
@@ -939,6 +986,7 @@ describe('MutmutRunner', () => {
939
986
  timeoutMs: 60000,
940
987
  cwd: tmpDir,
941
988
  });
989
+ await waitForSpawn();
942
990
  closeCb!(0);
943
991
  await promise;
944
992
 
@@ -989,6 +1037,7 @@ describe('MutmutRunner', () => {
989
1037
  timeoutMs: 60000,
990
1038
  cwd: tmpDir,
991
1039
  });
1040
+ await waitForSpawn();
992
1041
  closeCb!(0);
993
1042
  await promise;
994
1043
 
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @test REQ-480
3
+ * @intent "测试基础设施" 的定义在 githooks/pre-push(bash)与 src/mutation/gate-m.ts
4
+ * (TypeScript)各写了一遍,没有共享常量的自然机制;一侧演化后 Gate M 与
5
+ * pre-push 会对同一变更集给出不一致裁决(Round 2 架构 MA-02 / 可行性 FC-03)。
6
+ * 跨语言无法共用函数,退而求其次:把两处文本钉成同一份定义。
7
+ * @covers AC-480-04
8
+ */
9
+ import { describe, it, expect } from 'vitest';
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+
13
+ const repoRoot = path.resolve(__dirname, '..', '..', '..');
14
+ const prePush = fs.readFileSync(path.join(repoRoot, 'githooks', 'pre-push'), 'utf8');
15
+ const gateM = fs.readFileSync(path.join(repoRoot, 'src', 'mutation', 'gate-m.ts'), 'utf8');
16
+
17
+ const prePushFilter =
18
+ prePush.split('\n').find((line) => line.includes('CHANGED_SOURCE_FILES=')) || '';
19
+
20
+ describe('test-path filter parity between pre-push and gate-m (#480)', () => {
21
+ it('compares a real filter line, not an empty string', () => {
22
+ expect(prePushFilter).toContain('grep -v');
23
+ expect(gateM).toContain('export function filterSourceFiles');
24
+ });
25
+
26
+ it('both sides exclude the same test-tree segments', () => {
27
+ expect(prePushFilter).toContain('(tests?|__tests__)/');
28
+ expect(gateM).toContain(String.raw`(^|\/)(tests?|__tests__)\/`);
29
+ });
30
+
31
+ it('both sides exclude declaration files and spec-named tests', () => {
32
+ expect(prePushFilter).toContain(String.raw`\.d\.ts$`);
33
+ expect(prePushFilter).toContain(String.raw`\.spec\.`);
34
+ expect(gateM).toContain("endsWith('.d.ts')");
35
+ expect(gateM).toContain("includes('.spec.')");
36
+ });
37
+ });
@@ -79,19 +79,32 @@ function parseTimeout(options: GateMOptions, args: string[], i: number): void {
79
79
  // ── File filtering: excludes tests, declarations, adapters ──
80
80
  // Delegate per-language filtering to a helper.
81
81
 
82
- function filterSourceFiles(files: string[]): string[] {
82
+ export function filterSourceFiles(files: string[]): string[] {
83
83
  return files.filter(file => {
84
84
  const ext = path.extname(file);
85
+ // One separator view for every pattern below: the path arrives from the
86
+ // adapter layer, which on Windows yields backslash-delimited relative paths.
87
+ const normalized = file.replace(/\\/g, '/');
85
88
  // Skip test files
86
- if (file.includes('.test.') || file.includes('_test.')) return false;
87
- // Skip Java/Kotlin test files: FooTest.java, FooSpec.kt, etc.
88
- if (/[A-Z]Test\.(java|kt|kts)$/.test(file)) return false;
89
- if (/[A-Z]Tests\.(java|kt|kts)$/.test(file)) return false;
90
- if (/[A-Z](IT|Spec)\.(java|kt|kts)$/.test(file)) return false;
89
+ if (normalized.includes('.test.') || normalized.includes('_test.')) return false;
90
+ // Skip spec-named test files: the pre-push filter already excludes them, and
91
+ // a mutant inside a *.spec.ts is scored against a production budget (#480).
92
+ if (normalized.includes('.spec.')) return false;
93
+ // Skip test-tree directories: helpers/fixtures/factories there are test
94
+ // infrastructure judged by the suites that run them, not by mutation
95
+ // scoring budgeted for production code (#480)
96
+ if (/(^|\/)(tests?|__tests__)\//.test(normalized)) return false;
97
+ // Skip Java/Kotlin test files: FooTest.java, FooSpec.kt, etc. Case-sensitive
98
+ // "Test"/"Spec"/"IT" suffix matching is what the Surefire/Failsafe conventions
99
+ // actually produce -- `[A-Z]Test\.only` caught MyTest.java but not FooTest.java,
100
+ // so most test classes stayed in the mutation candidate list.
101
+ if (/Test\.(java|kt|kts)$/.test(normalized)) return false;
102
+ if (/Tests\.(java|kt|kts)$/.test(normalized)) return false;
103
+ if (/(IT|Spec)\.(java|kt|kts)$/.test(normalized)) return false;
91
104
  // Skip declaration files
92
- if (file.endsWith('.d.ts')) return false;
105
+ if (normalized.endsWith('.d.ts')) return false;
93
106
  // Skip adapter files
94
- if (file.includes('/adapters/')) return false;
107
+ if (normalized.includes('/adapters/')) return false;
95
108
  // Skip files with no registered runner
96
109
  const runner = resolveRunner(ext);
97
110
  return runner !== undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boyingliu01/xp-gate",
3
- "version": "0.19.2",
3
+ "version": "0.20.0",
4
4
  "description": "AI-driven development workflow: 6 quality gates + Delphi review + Sprint Flow",
5
5
  "bin": {
6
6
  "xp-gate": "./bin/xp-gate.js"
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "xp-gate",
3
- "version": "0.19.2",
3
+ "version": "0.20.0",
4
4
  "displayName": "XP-Gate",
5
- "description": "Extreme Programming quality gates + AI workflow skills for Claude Code. Includes 10 quality gates (Gate 0-9), Sprint Flow (11 phases), and Delphi multi-expert review (>=90% consensus).",
5
+ "description": "Extreme Programming quality gates + AI workflow skills for Claude Code. Includes 12 pre-commit gates (Gate 0-11), 8 pre-push gates, Sprint Flow (6 phases), and Delphi multi-expert review (>=90% consensus).",
6
6
  "author": {
7
7
  "name": "boyingliu01"
8
8
  },