agentsys 5.0.3 → 5.1.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 (264) hide show
  1. package/.claude-plugin/marketplace.json +21 -14
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/AGENTS.md +2 -1
  4. package/CHANGELOG.md +18 -0
  5. package/README.md +7 -6
  6. package/adapters/codex/skills/agnix/SKILL.md +0 -1
  7. package/adapters/codex/skills/audit-project/SKILL.md +0 -1
  8. package/adapters/codex/skills/audit-project-agents/SKILL.md +0 -1
  9. package/adapters/codex/skills/audit-project-github/SKILL.md +0 -1
  10. package/adapters/codex/skills/consult/SKILL.md +132 -57
  11. package/adapters/codex/skills/debate/SKILL.md +214 -0
  12. package/adapters/codex/skills/delivery-approval/SKILL.md +0 -1
  13. package/adapters/codex/skills/deslop/SKILL.md +0 -1
  14. package/adapters/codex/skills/drift-detect/SKILL.md +0 -1
  15. package/adapters/codex/skills/enhance/SKILL.md +0 -1
  16. package/adapters/codex/skills/learn/SKILL.md +0 -1
  17. package/adapters/codex/skills/next-task/SKILL.md +0 -1
  18. package/adapters/codex/skills/perf/SKILL.md +0 -1
  19. package/adapters/codex/skills/repo-map/SKILL.md +0 -1
  20. package/adapters/codex/skills/ship/SKILL.md +0 -1
  21. package/adapters/codex/skills/ship-ci-review-loop/SKILL.md +0 -1
  22. package/adapters/codex/skills/ship-deployment/SKILL.md +0 -1
  23. package/adapters/codex/skills/ship-error-handling/SKILL.md +0 -1
  24. package/adapters/codex/skills/sync-docs/SKILL.md +0 -1
  25. package/adapters/opencode/agents/agent-enhancer.md +0 -1
  26. package/adapters/opencode/agents/agnix-agent.md +0 -1
  27. package/adapters/opencode/agents/ci-fixer.md +0 -1
  28. package/adapters/opencode/agents/ci-monitor.md +0 -1
  29. package/adapters/opencode/agents/claudemd-enhancer.md +0 -1
  30. package/adapters/opencode/agents/consult-agent.md +122 -30
  31. package/adapters/opencode/agents/cross-file-enhancer.md +0 -1
  32. package/adapters/opencode/agents/debate-orchestrator.md +169 -0
  33. package/adapters/opencode/agents/delivery-validator.md +0 -1
  34. package/adapters/opencode/agents/deslop-agent.md +0 -1
  35. package/adapters/opencode/agents/docs-enhancer.md +0 -1
  36. package/adapters/opencode/agents/exploration-agent.md +0 -1
  37. package/adapters/opencode/agents/hooks-enhancer.md +0 -1
  38. package/adapters/opencode/agents/implementation-agent.md +0 -1
  39. package/adapters/opencode/agents/learn-agent.md +0 -1
  40. package/adapters/opencode/agents/map-validator.md +0 -1
  41. package/adapters/opencode/agents/perf-analyzer.md +0 -1
  42. package/adapters/opencode/agents/perf-code-paths.md +0 -1
  43. package/adapters/opencode/agents/perf-investigation-logger.md +0 -1
  44. package/adapters/opencode/agents/perf-orchestrator.md +0 -1
  45. package/adapters/opencode/agents/perf-theory-gatherer.md +0 -1
  46. package/adapters/opencode/agents/perf-theory-tester.md +0 -1
  47. package/adapters/opencode/agents/plan-synthesizer.md +0 -1
  48. package/adapters/opencode/agents/planning-agent.md +0 -1
  49. package/adapters/opencode/agents/plugin-enhancer.md +0 -1
  50. package/adapters/opencode/agents/prompt-enhancer.md +0 -1
  51. package/adapters/opencode/agents/simple-fixer.md +0 -1
  52. package/adapters/opencode/agents/skills-enhancer.md +0 -1
  53. package/adapters/opencode/agents/sync-docs-agent.md +0 -1
  54. package/adapters/opencode/agents/task-discoverer.md +0 -1
  55. package/adapters/opencode/agents/test-coverage-checker.md +0 -1
  56. package/adapters/opencode/agents/worktree-manager.md +0 -1
  57. package/adapters/opencode/commands/agnix.md +0 -1
  58. package/adapters/opencode/commands/audit-project-agents.md +0 -1
  59. package/adapters/opencode/commands/audit-project-github.md +0 -1
  60. package/adapters/opencode/commands/audit-project.md +0 -1
  61. package/adapters/opencode/commands/consult.md +133 -57
  62. package/adapters/opencode/commands/debate.md +224 -0
  63. package/adapters/opencode/commands/delivery-approval.md +0 -1
  64. package/adapters/opencode/commands/deslop.md +0 -1
  65. package/adapters/opencode/commands/drift-detect.md +0 -1
  66. package/adapters/opencode/commands/enhance.md +0 -1
  67. package/adapters/opencode/commands/learn.md +0 -1
  68. package/adapters/opencode/commands/next-task.md +0 -1
  69. package/adapters/opencode/commands/perf.md +0 -1
  70. package/adapters/opencode/commands/repo-map.md +0 -1
  71. package/adapters/opencode/commands/ship-ci-review-loop.md +0 -1
  72. package/adapters/opencode/commands/ship-deployment.md +0 -1
  73. package/adapters/opencode/commands/ship-error-handling.md +0 -1
  74. package/adapters/opencode/commands/ship.md +0 -1
  75. package/adapters/opencode/commands/sync-docs.md +0 -1
  76. package/adapters/opencode/skills/agnix/SKILL.md +1 -2
  77. package/adapters/opencode/skills/consult/SKILL.md +33 -23
  78. package/adapters/opencode/skills/debate/SKILL.md +245 -0
  79. package/adapters/opencode/skills/deslop/SKILL.md +1 -2
  80. package/adapters/opencode/skills/discover-tasks/SKILL.md +1 -2
  81. package/adapters/opencode/skills/drift-analysis/SKILL.md +1 -2
  82. package/adapters/opencode/skills/enhance-agent-prompts/SKILL.md +1 -2
  83. package/adapters/opencode/skills/enhance-claude-memory/SKILL.md +1 -2
  84. package/adapters/opencode/skills/enhance-cross-file/SKILL.md +1 -2
  85. package/adapters/opencode/skills/enhance-docs/SKILL.md +1 -2
  86. package/adapters/opencode/skills/enhance-hooks/SKILL.md +1 -2
  87. package/adapters/opencode/skills/enhance-orchestrator/SKILL.md +1 -2
  88. package/adapters/opencode/skills/enhance-plugins/SKILL.md +1 -2
  89. package/adapters/opencode/skills/enhance-prompts/SKILL.md +1 -2
  90. package/adapters/opencode/skills/enhance-skills/SKILL.md +1 -2
  91. package/adapters/opencode/skills/learn/SKILL.md +1 -2
  92. package/adapters/opencode/skills/orchestrate-review/SKILL.md +0 -1
  93. package/adapters/opencode/skills/perf-analyzer/SKILL.md +1 -2
  94. package/adapters/opencode/skills/perf-baseline-manager/SKILL.md +1 -2
  95. package/adapters/opencode/skills/perf-benchmarker/SKILL.md +1 -2
  96. package/adapters/opencode/skills/perf-code-paths/SKILL.md +1 -2
  97. package/adapters/opencode/skills/perf-investigation-logger/SKILL.md +1 -2
  98. package/adapters/opencode/skills/perf-profiler/SKILL.md +1 -2
  99. package/adapters/opencode/skills/perf-theory-gatherer/SKILL.md +1 -2
  100. package/adapters/opencode/skills/perf-theory-tester/SKILL.md +1 -2
  101. package/adapters/opencode/skills/repo-mapping/SKILL.md +1 -2
  102. package/adapters/opencode/skills/sync-docs/SKILL.md +1 -2
  103. package/adapters/opencode/skills/validate-delivery/SKILL.md +1 -2
  104. package/lib/adapter-transforms.js +24 -4
  105. package/package.json +1 -1
  106. package/plugins/agnix/.claude-plugin/plugin.json +1 -1
  107. package/plugins/agnix/skills/agnix/SKILL.md +1 -1
  108. package/plugins/audit-project/.claude-plugin/plugin.json +1 -1
  109. package/plugins/audit-project/lib/adapter-transforms.js +24 -4
  110. package/plugins/consult/.claude-plugin/plugin.json +1 -1
  111. package/plugins/consult/agents/consult-agent.md +122 -29
  112. package/plugins/consult/commands/consult.md +135 -58
  113. package/plugins/consult/skills/consult/SKILL.md +31 -20
  114. package/plugins/debate/.claude-plugin/plugin.json +21 -0
  115. package/plugins/debate/agents/debate-orchestrator.md +175 -0
  116. package/plugins/debate/commands/debate.md +221 -0
  117. package/plugins/debate/lib/adapter-transforms.js +298 -0
  118. package/plugins/debate/lib/collectors/codebase.js +392 -0
  119. package/plugins/debate/lib/collectors/docs-patterns.js +713 -0
  120. package/plugins/debate/lib/collectors/documentation.js +219 -0
  121. package/plugins/debate/lib/collectors/github.js +330 -0
  122. package/plugins/debate/lib/collectors/index.js +126 -0
  123. package/plugins/debate/lib/config/index.js +14 -0
  124. package/plugins/debate/lib/cross-platform/index.js +539 -0
  125. package/plugins/debate/lib/discovery/index.js +352 -0
  126. package/plugins/debate/lib/drift-detect/collectors.js +37 -0
  127. package/plugins/debate/lib/enhance/agent-analyzer.js +421 -0
  128. package/plugins/debate/lib/enhance/agent-patterns.js +571 -0
  129. package/plugins/debate/lib/enhance/auto-suppression.js +622 -0
  130. package/plugins/debate/lib/enhance/benchmark.js +417 -0
  131. package/plugins/debate/lib/enhance/cross-file-analyzer.js +930 -0
  132. package/plugins/debate/lib/enhance/cross-file-patterns.js +370 -0
  133. package/plugins/debate/lib/enhance/docs-analyzer.js +325 -0
  134. package/plugins/debate/lib/enhance/docs-patterns.js +671 -0
  135. package/plugins/debate/lib/enhance/fixer.js +721 -0
  136. package/plugins/debate/lib/enhance/hook-analyzer.js +135 -0
  137. package/plugins/debate/lib/enhance/hook-patterns.js +40 -0
  138. package/plugins/debate/lib/enhance/index.js +127 -0
  139. package/plugins/debate/lib/enhance/plugin-analyzer.js +402 -0
  140. package/plugins/debate/lib/enhance/plugin-patterns.js +326 -0
  141. package/plugins/debate/lib/enhance/projectmemory-analyzer.js +551 -0
  142. package/plugins/debate/lib/enhance/projectmemory-patterns.js +617 -0
  143. package/plugins/debate/lib/enhance/prompt-analyzer.js +457 -0
  144. package/plugins/debate/lib/enhance/prompt-patterns.js +1484 -0
  145. package/plugins/debate/lib/enhance/reporter.js +1348 -0
  146. package/plugins/debate/lib/enhance/security-patterns.js +284 -0
  147. package/plugins/debate/lib/enhance/skill-analyzer.js +182 -0
  148. package/plugins/debate/lib/enhance/skill-patterns.js +147 -0
  149. package/plugins/debate/lib/enhance/suppression.js +352 -0
  150. package/plugins/debate/lib/enhance/tool-patterns.js +373 -0
  151. package/plugins/debate/lib/index.js +270 -0
  152. package/plugins/debate/lib/patterns/cli-enhancers.js +611 -0
  153. package/plugins/debate/lib/patterns/pipeline.js +948 -0
  154. package/plugins/debate/lib/patterns/review-patterns.js +558 -0
  155. package/plugins/debate/lib/patterns/slop-analyzers.js +2305 -0
  156. package/plugins/debate/lib/patterns/slop-patterns.js +1187 -0
  157. package/plugins/debate/lib/perf/analyzer/index.js +22 -0
  158. package/plugins/debate/lib/perf/argument-parser.js +105 -0
  159. package/plugins/debate/lib/perf/baseline-comparator.js +50 -0
  160. package/plugins/debate/lib/perf/baseline-store.js +127 -0
  161. package/plugins/debate/lib/perf/benchmark-runner.js +404 -0
  162. package/plugins/debate/lib/perf/breaking-point-finder.js +52 -0
  163. package/plugins/debate/lib/perf/breaking-point-runner.js +60 -0
  164. package/plugins/debate/lib/perf/checkpoint.js +123 -0
  165. package/plugins/debate/lib/perf/code-paths.js +86 -0
  166. package/plugins/debate/lib/perf/consolidation.js +37 -0
  167. package/plugins/debate/lib/perf/constraint-runner.js +71 -0
  168. package/plugins/debate/lib/perf/experiment-runner.js +32 -0
  169. package/plugins/debate/lib/perf/index.js +41 -0
  170. package/plugins/debate/lib/perf/investigation-state.js +874 -0
  171. package/plugins/debate/lib/perf/optimization-runner.js +79 -0
  172. package/plugins/debate/lib/perf/profilers/go.js +22 -0
  173. package/plugins/debate/lib/perf/profilers/index.js +46 -0
  174. package/plugins/debate/lib/perf/profilers/java.js +23 -0
  175. package/plugins/debate/lib/perf/profilers/node.js +27 -0
  176. package/plugins/debate/lib/perf/profilers/python.js +23 -0
  177. package/plugins/debate/lib/perf/profilers/rust.js +23 -0
  178. package/plugins/debate/lib/perf/profiling-runner.js +75 -0
  179. package/plugins/debate/lib/perf/schemas.js +140 -0
  180. package/plugins/debate/lib/platform/detect-platform.js +413 -0
  181. package/plugins/debate/lib/platform/detection-configs.js +93 -0
  182. package/plugins/debate/lib/platform/state-dir.js +132 -0
  183. package/plugins/debate/lib/platform/verify-tools.js +182 -0
  184. package/plugins/debate/lib/repo-map/cache.js +152 -0
  185. package/plugins/debate/lib/repo-map/concurrency.js +29 -0
  186. package/plugins/debate/lib/repo-map/index.js +222 -0
  187. package/plugins/debate/lib/repo-map/installer.js +212 -0
  188. package/plugins/debate/lib/repo-map/queries/go.js +27 -0
  189. package/plugins/debate/lib/repo-map/queries/index.js +100 -0
  190. package/plugins/debate/lib/repo-map/queries/java.js +38 -0
  191. package/plugins/debate/lib/repo-map/queries/javascript.js +55 -0
  192. package/plugins/debate/lib/repo-map/queries/python.js +24 -0
  193. package/plugins/debate/lib/repo-map/queries/rust.js +73 -0
  194. package/plugins/debate/lib/repo-map/queries/typescript.js +38 -0
  195. package/plugins/debate/lib/repo-map/runner.js +1364 -0
  196. package/plugins/debate/lib/repo-map/updater.js +562 -0
  197. package/plugins/debate/lib/repo-map/usage-analyzer.js +407 -0
  198. package/plugins/debate/lib/schemas/plugin-manifest.schema.json +57 -0
  199. package/plugins/debate/lib/schemas/validator.js +247 -0
  200. package/plugins/debate/lib/sources/custom-handler.js +199 -0
  201. package/plugins/debate/lib/sources/policy-questions.js +246 -0
  202. package/plugins/debate/lib/sources/source-cache.js +165 -0
  203. package/plugins/debate/lib/state/workflow-state.js +576 -0
  204. package/plugins/debate/lib/types/agent-frontmatter.d.ts +134 -0
  205. package/plugins/debate/lib/types/command-frontmatter.d.ts +107 -0
  206. package/plugins/debate/lib/types/hook-frontmatter.d.ts +115 -0
  207. package/plugins/debate/lib/types/index.d.ts +84 -0
  208. package/plugins/debate/lib/types/plugin-manifest.d.ts +102 -0
  209. package/plugins/debate/lib/types/skill-frontmatter.d.ts +89 -0
  210. package/plugins/debate/lib/utils/atomic-write.js +94 -0
  211. package/plugins/debate/lib/utils/cache-manager.js +159 -0
  212. package/plugins/debate/lib/utils/command-parser.js +0 -0
  213. package/plugins/debate/lib/utils/context-optimizer.js +300 -0
  214. package/plugins/debate/lib/utils/deprecation.js +37 -0
  215. package/plugins/debate/lib/utils/shell-escape.js +88 -0
  216. package/plugins/debate/lib/utils/state-helpers.js +61 -0
  217. package/plugins/debate/skills/debate/SKILL.md +264 -0
  218. package/plugins/deslop/.claude-plugin/plugin.json +1 -1
  219. package/plugins/deslop/lib/adapter-transforms.js +24 -4
  220. package/plugins/deslop/skills/deslop/SKILL.md +1 -1
  221. package/plugins/drift-detect/.claude-plugin/plugin.json +1 -1
  222. package/plugins/drift-detect/lib/adapter-transforms.js +24 -4
  223. package/plugins/drift-detect/skills/drift-analysis/SKILL.md +1 -1
  224. package/plugins/enhance/.claude-plugin/plugin.json +1 -1
  225. package/plugins/enhance/lib/adapter-transforms.js +24 -4
  226. package/plugins/enhance/skills/enhance-agent-prompts/SKILL.md +1 -1
  227. package/plugins/enhance/skills/enhance-claude-memory/SKILL.md +1 -1
  228. package/plugins/enhance/skills/enhance-cross-file/SKILL.md +1 -1
  229. package/plugins/enhance/skills/enhance-docs/SKILL.md +1 -1
  230. package/plugins/enhance/skills/enhance-hooks/SKILL.md +1 -1
  231. package/plugins/enhance/skills/enhance-orchestrator/SKILL.md +1 -1
  232. package/plugins/enhance/skills/enhance-plugins/SKILL.md +1 -1
  233. package/plugins/enhance/skills/enhance-prompts/SKILL.md +1 -1
  234. package/plugins/enhance/skills/enhance-skills/SKILL.md +1 -1
  235. package/plugins/learn/.claude-plugin/plugin.json +1 -1
  236. package/plugins/learn/agents/learn-agent.md +1 -1
  237. package/plugins/learn/lib/adapter-transforms.js +24 -4
  238. package/plugins/learn/skills/learn/SKILL.md +1 -1
  239. package/plugins/next-task/.claude-plugin/plugin.json +1 -1
  240. package/plugins/next-task/agents/exploration-agent.md +1 -1
  241. package/plugins/next-task/lib/adapter-transforms.js +24 -4
  242. package/plugins/next-task/skills/discover-tasks/SKILL.md +1 -1
  243. package/plugins/next-task/skills/validate-delivery/SKILL.md +1 -1
  244. package/plugins/perf/.claude-plugin/plugin.json +1 -1
  245. package/plugins/perf/lib/adapter-transforms.js +24 -4
  246. package/plugins/perf/skills/perf-analyzer/SKILL.md +1 -1
  247. package/plugins/perf/skills/perf-baseline-manager/SKILL.md +1 -1
  248. package/plugins/perf/skills/perf-benchmarker/SKILL.md +1 -1
  249. package/plugins/perf/skills/perf-code-paths/SKILL.md +1 -1
  250. package/plugins/perf/skills/perf-investigation-logger/SKILL.md +1 -1
  251. package/plugins/perf/skills/perf-profiler/SKILL.md +1 -1
  252. package/plugins/perf/skills/perf-theory-gatherer/SKILL.md +1 -1
  253. package/plugins/perf/skills/perf-theory-tester/SKILL.md +1 -1
  254. package/plugins/repo-map/.claude-plugin/plugin.json +1 -1
  255. package/plugins/repo-map/lib/adapter-transforms.js +24 -4
  256. package/plugins/ship/.claude-plugin/plugin.json +1 -1
  257. package/plugins/ship/lib/adapter-transforms.js +24 -4
  258. package/plugins/sync-docs/.claude-plugin/plugin.json +1 -1
  259. package/plugins/sync-docs/lib/adapter-transforms.js +24 -4
  260. package/plugins/sync-docs/skills/sync-docs/SKILL.md +1 -1
  261. package/scripts/gen-adapters.js +6 -7
  262. package/scripts/generate-docs.js +4 -2
  263. package/scripts/plugins.txt +1 -0
  264. package/site/content.json +6 -6
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Perf analysis helpers.
3
+ *
4
+ * @module lib/perf/analyzer
5
+ */
6
+
7
+ /**
8
+ * Build a compact summary of perf findings.
9
+ * @param {object} input
10
+ * @returns {object}
11
+ */
12
+ function summarize(input = {}) {
13
+ return {
14
+ summary: input.summary || '',
15
+ recommendations: input.recommendations || [],
16
+ risks: input.risks || []
17
+ };
18
+ }
19
+
20
+ module.exports = {
21
+ summarize
22
+ };
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Argument parsing helper for /perf.
3
+ *
4
+ * @module lib/perf/argument-parser
5
+ */
6
+
7
+ const GREEDY_FLAGS = new Set(['--quote', '--change', '--scenario', '--command', '--rationale']);
8
+
9
+ function parseArgv(tokens) {
10
+ const args = [];
11
+ if (!Array.isArray(tokens)) return args;
12
+
13
+ for (let i = 0; i < tokens.length; i++) {
14
+ const token = tokens[i];
15
+ if (!token) continue;
16
+
17
+ if (token.startsWith('--')) {
18
+ args.push(token);
19
+ if (i + 1 >= tokens.length) {
20
+ continue;
21
+ }
22
+ if (GREEDY_FLAGS.has(token)) {
23
+ const valueTokens = [];
24
+ while (i + 1 < tokens.length && !String(tokens[i + 1]).startsWith('--')) {
25
+ valueTokens.push(tokens[++i]);
26
+ }
27
+ if (valueTokens.length > 0) {
28
+ args.push(valueTokens.join(' '));
29
+ }
30
+ continue;
31
+ }
32
+ if (!String(tokens[i + 1]).startsWith('--')) {
33
+ args.push(tokens[++i]);
34
+ }
35
+ continue;
36
+ }
37
+
38
+ args.push(token);
39
+ }
40
+
41
+ return args;
42
+ }
43
+
44
+ function parseArguments(raw) {
45
+ if (Array.isArray(raw)) {
46
+ return parseArgv(raw);
47
+ }
48
+ if (!raw || typeof raw !== 'string') return [];
49
+
50
+ const args = [];
51
+ let current = '';
52
+ let quote = null;
53
+ let escaped = false;
54
+
55
+ for (let i = 0; i < raw.length; i++) {
56
+ const ch = raw[i];
57
+
58
+ if (escaped) {
59
+ current += ch;
60
+ escaped = false;
61
+ continue;
62
+ }
63
+
64
+ if (ch === '\\') {
65
+ if (quote) {
66
+ escaped = true;
67
+ continue;
68
+ }
69
+ }
70
+
71
+ if (quote) {
72
+ if (ch === quote) {
73
+ quote = null;
74
+ } else {
75
+ current += ch;
76
+ }
77
+ continue;
78
+ }
79
+
80
+ if (ch === '"' || ch === "'") {
81
+ quote = ch;
82
+ continue;
83
+ }
84
+
85
+ if (/\s/.test(ch)) {
86
+ if (current) {
87
+ args.push(current);
88
+ current = '';
89
+ }
90
+ continue;
91
+ }
92
+
93
+ current += ch;
94
+ }
95
+
96
+ if (current) {
97
+ args.push(current);
98
+ }
99
+
100
+ return args;
101
+ }
102
+
103
+ module.exports = {
104
+ parseArguments
105
+ };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Baseline comparison helpers
3
+ *
4
+ * @module lib/perf/baseline-comparator
5
+ */
6
+
7
+ /**
8
+ * Compute delta between baseline and current metrics.
9
+ * Supports flat numeric values under baseline.metrics/current.metrics.
10
+ *
11
+ * @param {object} baseline
12
+ * @param {object} current
13
+ * @returns {object}
14
+ */
15
+ function compareBaselines(baseline, current) {
16
+ const baselineMetrics = baseline?.metrics || {};
17
+ const currentMetrics = current?.metrics || {};
18
+ const keys = new Set([
19
+ ...Object.keys(baselineMetrics),
20
+ ...Object.keys(currentMetrics)
21
+ ]);
22
+
23
+ const deltas = {};
24
+ for (const key of keys) {
25
+ const baseValue = baselineMetrics[key];
26
+ const currentValue = currentMetrics[key];
27
+
28
+ if (typeof baseValue === 'number' && typeof currentValue === 'number') {
29
+ const delta = currentValue - baseValue;
30
+ const percent = baseValue === 0 ? null : delta / baseValue;
31
+ deltas[key] = { baseline: baseValue, current: currentValue, delta, percent };
32
+ } else {
33
+ deltas[key] = {
34
+ baseline: baseValue ?? null,
35
+ current: currentValue ?? null,
36
+ delta: null,
37
+ percent: null
38
+ };
39
+ }
40
+ }
41
+
42
+ return {
43
+ comparedAt: new Date().toISOString(),
44
+ metrics: deltas
45
+ };
46
+ }
47
+
48
+ module.exports = {
49
+ compareBaselines
50
+ };
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Baseline storage utilities for /perf
3
+ *
4
+ * Stores baselines under:
5
+ * - {state-dir}/perf/baselines/{version}.json
6
+ *
7
+ * @module lib/perf/baseline-store
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const { getStateDir } = require('../platform/state-dir');
13
+ const { validateBaseline, assertValid } = require('./schemas');
14
+
15
+ const BASELINE_DIR = 'baselines';
16
+
17
+ function assertSafeBaselineVersion(version) {
18
+ if (!version || typeof version !== 'string') {
19
+ throw new Error('Baseline version is required');
20
+ }
21
+ if (version.includes('..') || version.includes('/') || version.includes('\\') || version.includes('\0')) {
22
+ throw new Error('Baseline version contains invalid characters');
23
+ }
24
+ if (!/^[a-zA-Z0-9._+-]+$/.test(version)) {
25
+ throw new Error('Baseline version contains invalid characters');
26
+ }
27
+ return version;
28
+ }
29
+
30
+ /**
31
+ * Get baseline directory path
32
+ * @param {string} basePath
33
+ * @returns {string}
34
+ */
35
+ function getBaselineDir(basePath = process.cwd()) {
36
+ return path.join(basePath, getStateDir(basePath), 'perf', BASELINE_DIR);
37
+ }
38
+
39
+ /**
40
+ * Ensure baseline directory exists
41
+ * @param {string} basePath
42
+ * @returns {string}
43
+ */
44
+ function ensureBaselineDir(basePath = process.cwd()) {
45
+ const dir = getBaselineDir(basePath);
46
+ if (!fs.existsSync(dir)) {
47
+ fs.mkdirSync(dir, { recursive: true });
48
+ }
49
+ return dir;
50
+ }
51
+
52
+ /**
53
+ * Build baseline file path
54
+ * @param {string} version
55
+ * @param {string} basePath
56
+ * @returns {string}
57
+ */
58
+ function getBaselinePath(version, basePath = process.cwd()) {
59
+ const safeVersion = assertSafeBaselineVersion(version);
60
+ return path.join(ensureBaselineDir(basePath), `${safeVersion}.json`);
61
+ }
62
+
63
+ /**
64
+ * List baseline versions
65
+ * @param {string} basePath
66
+ * @returns {string[]}
67
+ */
68
+ function listBaselines(basePath = process.cwd()) {
69
+ const dir = ensureBaselineDir(basePath);
70
+ return fs.readdirSync(dir)
71
+ .filter(file => file.endsWith('.json'))
72
+ .map(file => path.basename(file, '.json'))
73
+ .sort();
74
+ }
75
+
76
+ /**
77
+ * Read baseline file
78
+ * @param {string} version
79
+ * @param {string} basePath
80
+ * @returns {object|null}
81
+ */
82
+ function readBaseline(version, basePath = process.cwd()) {
83
+ const baselinePath = getBaselinePath(version, basePath);
84
+ if (!fs.existsSync(baselinePath)) {
85
+ return null;
86
+ }
87
+ try {
88
+ const parsed = JSON.parse(fs.readFileSync(baselinePath, 'utf8'));
89
+ const validation = validateBaseline(parsed);
90
+ if (!validation.ok) {
91
+ console.error(`[CRITICAL] Invalid baseline file at ${baselinePath}: ${validation.errors.join(', ')}`);
92
+ return null;
93
+ }
94
+ return parsed;
95
+ } catch (error) {
96
+ console.error(`[CRITICAL] Corrupted baseline file at ${baselinePath}: ${error.message}`);
97
+ return null;
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Write baseline file (overwrites existing)
103
+ * @param {string} version
104
+ * @param {object} baseline
105
+ * @param {string} basePath
106
+ * @returns {boolean}
107
+ */
108
+ function writeBaseline(version, baseline, basePath = process.cwd()) {
109
+ const baselinePath = getBaselinePath(version, basePath);
110
+ const payload = {
111
+ version,
112
+ recordedAt: new Date().toISOString(),
113
+ ...baseline
114
+ };
115
+ assertValid(validateBaseline(payload), 'Invalid baseline payload');
116
+ fs.writeFileSync(baselinePath, JSON.stringify(payload, null, 2), 'utf8');
117
+ return true;
118
+ }
119
+
120
+ module.exports = {
121
+ getBaselineDir,
122
+ ensureBaselineDir,
123
+ getBaselinePath,
124
+ listBaselines,
125
+ readBaseline,
126
+ writeBaseline
127
+ };
@@ -0,0 +1,404 @@
1
+ /**
2
+ * Sequential benchmark runner utilities.
3
+ *
4
+ * @module lib/perf/benchmark-runner
5
+ */
6
+
7
+ const { execFileSync } = require('child_process');
8
+ const { validateBaseline } = require('./schemas');
9
+ const { parseCommand, resolveExecutableForPlatform } = require('../utils/command-parser');
10
+
11
+ const DEFAULT_MIN_DURATION = 60;
12
+ const BINARY_SEARCH_MIN_DURATION = 30;
13
+ const DEFAULT_DURATION_SLACK_SECONDS = 1;
14
+
15
+ function parseDuration(value) {
16
+ const parsed = Number(value);
17
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
18
+ }
19
+
20
+ /**
21
+ * Normalize benchmark options and enforce minimum durations.
22
+ * @param {object} options
23
+ * @returns {object}
24
+ */
25
+ function normalizeBenchmarkOptions(options = {}) {
26
+ const mode = options.mode || 'full';
27
+ const defaultMin = mode === 'binary-search'
28
+ ? BINARY_SEARCH_MIN_DURATION
29
+ : DEFAULT_MIN_DURATION;
30
+ const requestedDuration = parseDuration(options.duration);
31
+ const requestedMin = parseDuration(options.minDuration);
32
+ const minDuration = requestedMin ?? requestedDuration ?? defaultMin;
33
+ const duration = Math.max(requestedDuration ?? minDuration, minDuration);
34
+
35
+ return {
36
+ ...options,
37
+ mode,
38
+ duration,
39
+ warmup: options.warmup || 10,
40
+ allowShort: options.allowShort === true
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Run a benchmark command synchronously (sequential only).
46
+ * @param {string} command
47
+ * @param {object} options
48
+ * @param {number} [options.duration]
49
+ * @param {number} [options.minDuration]
50
+ * @param {boolean} [options.setDurationEnv]
51
+ * @param {string} [options.runMode]
52
+ * @returns {{ success: boolean, output: string }}
53
+ */
54
+ function runBenchmark(command, options = {}) {
55
+ if (!command || typeof command !== 'string') {
56
+ throw new Error('Benchmark command must be a non-empty string');
57
+ }
58
+
59
+ const parsedCommand = parseCommand(command, 'Benchmark command');
60
+ const executable = resolveExecutableForPlatform(parsedCommand.executable);
61
+ const normalized = normalizeBenchmarkOptions(options);
62
+ const setDurationEnv = options.setDurationEnv !== false;
63
+ const env = {
64
+ ...process.env,
65
+ ...normalized.env
66
+ };
67
+ if (setDurationEnv) {
68
+ env.PERF_RUN_DURATION = String(normalized.duration);
69
+ }
70
+ if (options.runMode) {
71
+ env.PERF_RUN_MODE = options.runMode;
72
+ }
73
+
74
+ const start = Date.now();
75
+ let output;
76
+ try {
77
+ output = execFileSync(executable, parsedCommand.args, {
78
+ stdio: 'pipe',
79
+ encoding: 'utf8',
80
+ env,
81
+ windowsHide: true,
82
+ cwd: options.cwd || process.cwd()
83
+ });
84
+ } catch (error) {
85
+ const stderr = error.stderr ? String(error.stderr).trim() : '';
86
+ const stdout = error.stdout ? String(error.stdout).trim() : '';
87
+ const exitCode = error.status ?? 'unknown';
88
+ const details = stderr || stdout || error.message || 'No error details available';
89
+ throw new Error(
90
+ `Benchmark command failed (exit code ${exitCode}): ${parsedCommand.display}\n` +
91
+ `Details: ${details}`
92
+ );
93
+ }
94
+ const elapsedSeconds = (Date.now() - start) / 1000;
95
+
96
+ const allowShort = normalized.allowShort || process.env.PERF_ALLOW_SHORT === '1';
97
+ if (!allowShort && setDurationEnv && elapsedSeconds + DEFAULT_DURATION_SLACK_SECONDS < normalized.duration) {
98
+ throw new Error(`Benchmark finished too quickly (${elapsedSeconds.toFixed(2)}s < ${normalized.duration}s)`);
99
+ }
100
+
101
+ return {
102
+ success: true,
103
+ output,
104
+ duration: normalized.duration,
105
+ warmup: normalized.warmup,
106
+ mode: normalized.mode,
107
+ elapsedSeconds
108
+ };
109
+ }
110
+
111
+ function parseLineMetrics(output) {
112
+ const lines = output.split(/\r?\n/);
113
+ const metrics = {};
114
+ let sawMarker = false;
115
+
116
+ for (const line of lines) {
117
+ const markerIndex = line.indexOf('PERF_METRICS');
118
+ if (markerIndex === -1) continue;
119
+
120
+ sawMarker = true;
121
+ const rest = line.slice(markerIndex + 'PERF_METRICS'.length).trim();
122
+ if (!rest) continue;
123
+
124
+ const tokens = rest.split(/\s+/).filter(Boolean);
125
+ let scenario = null;
126
+ const lineMetrics = {};
127
+
128
+ for (const token of tokens) {
129
+ const eqIndex = token.indexOf('=');
130
+ if (eqIndex === -1) continue;
131
+
132
+ const key = token.slice(0, eqIndex).trim();
133
+ const rawValue = token.slice(eqIndex + 1).trim();
134
+ if (!key) continue;
135
+
136
+ if (key === 'scenario') {
137
+ scenario = rawValue;
138
+ continue;
139
+ }
140
+
141
+ const value = Number(rawValue);
142
+ if (!Number.isFinite(value)) {
143
+ return { ok: false, error: `Metric ${key} must be a number` };
144
+ }
145
+
146
+ lineMetrics[key] = value;
147
+ }
148
+
149
+ if (Object.keys(lineMetrics).length === 0) {
150
+ continue;
151
+ }
152
+
153
+ if (scenario) {
154
+ if (!metrics.scenarios) {
155
+ metrics.scenarios = {};
156
+ }
157
+ metrics.scenarios[scenario] = {
158
+ ...(metrics.scenarios[scenario] || {}),
159
+ ...lineMetrics
160
+ };
161
+ } else {
162
+ Object.assign(metrics, lineMetrics);
163
+ }
164
+ }
165
+
166
+ if (!sawMarker) {
167
+ return { ok: false, error: 'Metrics markers not found' };
168
+ }
169
+
170
+ return { ok: true, metrics };
171
+ }
172
+
173
+ /**
174
+ * Parse metrics from benchmark output using PERF_METRICS markers.
175
+ * @param {string} output
176
+ * @returns {{ ok: boolean, metrics?: object, error?: string }}
177
+ */
178
+ function parseMetrics(output) {
179
+ if (typeof output !== 'string') {
180
+ return { ok: false, error: 'Output must be a string' };
181
+ }
182
+
183
+ const startMarker = 'PERF_METRICS_START';
184
+ const endMarker = 'PERF_METRICS_END';
185
+ const startIndex = output.indexOf(startMarker);
186
+ const endIndex = output.indexOf(endMarker);
187
+
188
+ if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
189
+ const jsonStart = startIndex + startMarker.length;
190
+ const raw = output.slice(jsonStart, endIndex).trim();
191
+
192
+ try {
193
+ const parsed = JSON.parse(raw);
194
+ const validation = validateBaseline({
195
+ version: 'temp',
196
+ recordedAt: new Date().toISOString(),
197
+ command: 'temp',
198
+ metrics: parsed
199
+ });
200
+ if (!validation.ok) {
201
+ return { ok: false, error: `Invalid metrics: ${validation.errors.join(', ')}` };
202
+ }
203
+ return { ok: true, metrics: parsed };
204
+ } catch (error) {
205
+ return { ok: false, error: `Failed to parse metrics JSON: ${error.message}` };
206
+ }
207
+ }
208
+
209
+ const lineParsed = parseLineMetrics(output);
210
+ if (!lineParsed.ok) {
211
+ return lineParsed;
212
+ }
213
+
214
+ const validation = validateBaseline({
215
+ version: 'temp',
216
+ recordedAt: new Date().toISOString(),
217
+ command: 'temp',
218
+ metrics: lineParsed.metrics
219
+ });
220
+ if (!validation.ok) {
221
+ return { ok: false, error: `Invalid metrics: ${validation.errors.join(', ')}` };
222
+ }
223
+ return { ok: true, metrics: lineParsed.metrics };
224
+ }
225
+
226
+ function flattenMetrics(metrics) {
227
+ if (!metrics || typeof metrics !== 'object' || Array.isArray(metrics)) {
228
+ throw new Error('metrics must be an object');
229
+ }
230
+ const flat = {};
231
+
232
+ for (const [key, value] of Object.entries(metrics)) {
233
+ if (key === 'scenarios') {
234
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
235
+ throw new Error('metrics.scenarios must be an object');
236
+ }
237
+ for (const [scenarioName, scenarioMetrics] of Object.entries(value)) {
238
+ if (!scenarioMetrics || typeof scenarioMetrics !== 'object' || Array.isArray(scenarioMetrics)) {
239
+ throw new Error(`metrics.scenarios.${scenarioName} must be an object`);
240
+ }
241
+ for (const [metricName, metricValue] of Object.entries(scenarioMetrics)) {
242
+ if (typeof metricValue !== 'number' || Number.isNaN(metricValue)) {
243
+ throw new Error(`metric ${scenarioName}.${metricName} must be a number`);
244
+ }
245
+ flat[`scenarios.${scenarioName}.${metricName}`] = metricValue;
246
+ }
247
+ }
248
+ continue;
249
+ }
250
+
251
+ if (typeof value !== 'number' || Number.isNaN(value)) {
252
+ throw new Error(`metric ${key} must be a number`);
253
+ }
254
+ flat[key] = value;
255
+ }
256
+
257
+ return flat;
258
+ }
259
+
260
+ function unflattenMetrics(flat) {
261
+ const metrics = {};
262
+ for (const [key, value] of Object.entries(flat)) {
263
+ if (!key.startsWith('scenarios.')) {
264
+ metrics[key] = value;
265
+ continue;
266
+ }
267
+ const parts = key.split('.');
268
+ if (parts.length < 3) {
269
+ throw new Error(`invalid scenario metric key: ${key}`);
270
+ }
271
+ const scenarioName = parts[1];
272
+ const metricName = parts.slice(2).join('.');
273
+ if (!metrics.scenarios) {
274
+ metrics.scenarios = {};
275
+ }
276
+ if (!metrics.scenarios[scenarioName]) {
277
+ metrics.scenarios[scenarioName] = {};
278
+ }
279
+ metrics.scenarios[scenarioName][metricName] = value;
280
+ }
281
+ return metrics;
282
+ }
283
+
284
+ function aggregateValues(values, aggregate) {
285
+ const normalized = (aggregate || 'median').toLowerCase();
286
+ const sorted = [...values].sort((a, b) => a - b);
287
+
288
+ switch (normalized) {
289
+ case 'median': {
290
+ const mid = Math.floor(sorted.length / 2);
291
+ if (sorted.length % 2 === 0) {
292
+ return (sorted[mid - 1] + sorted[mid]) / 2;
293
+ }
294
+ return sorted[mid];
295
+ }
296
+ case 'mean': {
297
+ const sum = sorted.reduce((acc, value) => acc + value, 0);
298
+ return sum / sorted.length;
299
+ }
300
+ case 'min':
301
+ return sorted[0];
302
+ case 'max':
303
+ return sorted[sorted.length - 1];
304
+ default:
305
+ throw new Error(`Unsupported aggregate: ${aggregate}`);
306
+ }
307
+ }
308
+
309
+ function aggregateMetrics(samples, aggregate = 'median') {
310
+ if (!Array.isArray(samples) || samples.length === 0) {
311
+ throw new Error('samples must be a non-empty array');
312
+ }
313
+
314
+ const flattened = samples.map(flattenMetrics);
315
+ const keys = Object.keys(flattened[0]).sort();
316
+
317
+ for (const sample of flattened) {
318
+ const sampleKeys = Object.keys(sample).sort();
319
+ if (sampleKeys.length !== keys.length) {
320
+ throw new Error('Metric sets differ across runs');
321
+ }
322
+ for (let i = 0; i < keys.length; i++) {
323
+ if (keys[i] !== sampleKeys[i]) {
324
+ throw new Error('Metric sets differ across runs');
325
+ }
326
+ }
327
+ }
328
+
329
+ const aggregated = {};
330
+ for (const key of keys) {
331
+ const values = flattened.map(sample => sample[key]);
332
+ aggregated[key] = aggregateValues(values, aggregate);
333
+ }
334
+
335
+ return unflattenMetrics(aggregated);
336
+ }
337
+
338
+ function resolveRuns(options) {
339
+ if (!options || options.runs == null) return 1;
340
+ const runs = Number(options.runs);
341
+ if (!Number.isFinite(runs) || runs < 1) {
342
+ throw new Error('runs must be a positive number');
343
+ }
344
+ return Math.floor(runs);
345
+ }
346
+
347
+ /**
348
+ * Run benchmark multiple times and aggregate metrics.
349
+ * @param {string} command
350
+ * @param {object} options
351
+ * @returns {{ metrics: object, samples: object[], runs: number, aggregate: string }}
352
+ */
353
+ function runBenchmarkSeries(command, options = {}) {
354
+ const runs = resolveRuns(options);
355
+ const aggregate = options.aggregate || (runs > 1 ? 'median' : 'median');
356
+ const runMode = options.runMode || (runs > 1 ? 'oneshot' : 'duration');
357
+ const env = {
358
+ ...options.env,
359
+ PERF_RUN_MODE: runMode
360
+ };
361
+ const allowShort = options.allowShort === true || runMode === 'oneshot';
362
+ const setDurationEnv = runMode !== 'oneshot' && options.setDurationEnv !== false;
363
+
364
+ const samples = [];
365
+ for (let i = 0; i < runs; i++) {
366
+ let result;
367
+ try {
368
+ result = runBenchmark(command, {
369
+ ...options,
370
+ env,
371
+ allowShort,
372
+ setDurationEnv,
373
+ runMode
374
+ });
375
+ } catch (error) {
376
+ throw new Error(
377
+ `Benchmark run ${i + 1}/${runs} failed: ${error.message}`
378
+ );
379
+ }
380
+ const parsed = parseMetrics(result.output);
381
+ if (!parsed.ok) {
382
+ throw new Error(`Metrics parse failed on run ${i + 1}/${runs}: ${parsed.error}`);
383
+ }
384
+ samples.push(parsed.metrics);
385
+ }
386
+
387
+ const metrics = samples.length === 1 ? samples[0] : aggregateMetrics(samples, aggregate);
388
+ return {
389
+ metrics,
390
+ samples,
391
+ runs,
392
+ aggregate
393
+ };
394
+ }
395
+
396
+ module.exports = {
397
+ DEFAULT_MIN_DURATION,
398
+ BINARY_SEARCH_MIN_DURATION,
399
+ normalizeBenchmarkOptions,
400
+ runBenchmark,
401
+ runBenchmarkSeries,
402
+ aggregateMetrics,
403
+ parseMetrics
404
+ };