agentsys 5.0.1 → 5.0.3

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 (185) hide show
  1. package/.claude-plugin/marketplace.json +13 -13
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +10 -0
  4. package/README.md +1 -0
  5. package/adapters/codex/skills/consult/SKILL.md +2 -2
  6. package/adapters/opencode/agents/consult-agent.md +1 -1
  7. package/adapters/opencode/commands/consult.md +2 -2
  8. package/adapters/opencode/skills/agnix/SKILL.md +1 -1
  9. package/adapters/opencode/skills/consult/SKILL.md +22 -6
  10. package/adapters/opencode/skills/deslop/SKILL.md +1 -1
  11. package/adapters/opencode/skills/discover-tasks/SKILL.md +1 -1
  12. package/adapters/opencode/skills/drift-analysis/SKILL.md +1 -1
  13. package/adapters/opencode/skills/enhance-agent-prompts/SKILL.md +1 -1
  14. package/adapters/opencode/skills/enhance-claude-memory/SKILL.md +1 -1
  15. package/adapters/opencode/skills/enhance-cross-file/SKILL.md +1 -1
  16. package/adapters/opencode/skills/enhance-docs/SKILL.md +1 -1
  17. package/adapters/opencode/skills/enhance-hooks/SKILL.md +1 -1
  18. package/adapters/opencode/skills/enhance-orchestrator/SKILL.md +1 -1
  19. package/adapters/opencode/skills/enhance-plugins/SKILL.md +1 -1
  20. package/adapters/opencode/skills/enhance-prompts/SKILL.md +1 -1
  21. package/adapters/opencode/skills/enhance-skills/SKILL.md +1 -1
  22. package/adapters/opencode/skills/learn/SKILL.md +1 -1
  23. package/adapters/opencode/skills/perf-analyzer/SKILL.md +1 -1
  24. package/adapters/opencode/skills/perf-baseline-manager/SKILL.md +1 -1
  25. package/adapters/opencode/skills/perf-benchmarker/SKILL.md +1 -1
  26. package/adapters/opencode/skills/perf-code-paths/SKILL.md +1 -1
  27. package/adapters/opencode/skills/perf-investigation-logger/SKILL.md +1 -1
  28. package/adapters/opencode/skills/perf-profiler/SKILL.md +1 -1
  29. package/adapters/opencode/skills/perf-theory-gatherer/SKILL.md +1 -1
  30. package/adapters/opencode/skills/perf-theory-tester/SKILL.md +1 -1
  31. package/adapters/opencode/skills/sync-docs/SKILL.md +1 -1
  32. package/adapters/opencode/skills/validate-delivery/SKILL.md +1 -1
  33. package/bin/cli.js +25 -6
  34. package/bin/dev-cli.js +16 -6
  35. package/lib/collectors/github.js +76 -12
  36. package/lib/perf/benchmark-runner.js +11 -6
  37. package/lib/perf/investigation-state.js +12 -13
  38. package/lib/perf/profiling-runner.js +23 -4
  39. package/lib/repo-map/concurrency.js +29 -0
  40. package/lib/repo-map/runner.js +218 -19
  41. package/lib/repo-map/updater.js +115 -27
  42. package/lib/state/workflow-state.js +31 -30
  43. package/lib/utils/command-parser.js +0 -0
  44. package/lib/utils/state-helpers.js +61 -0
  45. package/package.json +1 -1
  46. package/plugins/agnix/.claude-plugin/plugin.json +1 -1
  47. package/plugins/agnix/skills/agnix/SKILL.md +1 -1
  48. package/plugins/audit-project/.claude-plugin/plugin.json +1 -1
  49. package/plugins/audit-project/lib/collectors/github.js +76 -12
  50. package/plugins/audit-project/lib/perf/benchmark-runner.js +11 -6
  51. package/plugins/audit-project/lib/perf/investigation-state.js +12 -13
  52. package/plugins/audit-project/lib/perf/profiling-runner.js +23 -4
  53. package/plugins/audit-project/lib/repo-map/concurrency.js +29 -0
  54. package/plugins/audit-project/lib/repo-map/runner.js +218 -19
  55. package/plugins/audit-project/lib/repo-map/updater.js +115 -27
  56. package/plugins/audit-project/lib/state/workflow-state.js +31 -30
  57. package/plugins/audit-project/lib/utils/command-parser.js +0 -0
  58. package/plugins/audit-project/lib/utils/state-helpers.js +61 -0
  59. package/plugins/consult/.claude-plugin/plugin.json +1 -1
  60. package/plugins/consult/agents/consult-agent.md +1 -1
  61. package/plugins/consult/commands/consult.md +2 -2
  62. package/plugins/consult/skills/consult/SKILL.md +22 -6
  63. package/plugins/deslop/.claude-plugin/plugin.json +1 -1
  64. package/plugins/deslop/lib/collectors/github.js +76 -12
  65. package/plugins/deslop/lib/perf/benchmark-runner.js +11 -6
  66. package/plugins/deslop/lib/perf/investigation-state.js +12 -13
  67. package/plugins/deslop/lib/perf/profiling-runner.js +23 -4
  68. package/plugins/deslop/lib/repo-map/concurrency.js +29 -0
  69. package/plugins/deslop/lib/repo-map/runner.js +218 -19
  70. package/plugins/deslop/lib/repo-map/updater.js +115 -27
  71. package/plugins/deslop/lib/state/workflow-state.js +31 -30
  72. package/plugins/deslop/lib/utils/command-parser.js +0 -0
  73. package/plugins/deslop/lib/utils/state-helpers.js +61 -0
  74. package/plugins/deslop/skills/deslop/SKILL.md +1 -1
  75. package/plugins/drift-detect/.claude-plugin/plugin.json +1 -1
  76. package/plugins/drift-detect/lib/collectors/github.js +76 -12
  77. package/plugins/drift-detect/lib/perf/benchmark-runner.js +11 -6
  78. package/plugins/drift-detect/lib/perf/investigation-state.js +12 -13
  79. package/plugins/drift-detect/lib/perf/profiling-runner.js +23 -4
  80. package/plugins/drift-detect/lib/repo-map/concurrency.js +29 -0
  81. package/plugins/drift-detect/lib/repo-map/runner.js +218 -19
  82. package/plugins/drift-detect/lib/repo-map/updater.js +115 -27
  83. package/plugins/drift-detect/lib/state/workflow-state.js +31 -30
  84. package/plugins/drift-detect/lib/utils/command-parser.js +0 -0
  85. package/plugins/drift-detect/lib/utils/state-helpers.js +61 -0
  86. package/plugins/drift-detect/skills/drift-analysis/SKILL.md +1 -1
  87. package/plugins/enhance/.claude-plugin/plugin.json +1 -1
  88. package/plugins/enhance/lib/collectors/github.js +76 -12
  89. package/plugins/enhance/lib/perf/benchmark-runner.js +11 -6
  90. package/plugins/enhance/lib/perf/investigation-state.js +12 -13
  91. package/plugins/enhance/lib/perf/profiling-runner.js +23 -4
  92. package/plugins/enhance/lib/repo-map/concurrency.js +29 -0
  93. package/plugins/enhance/lib/repo-map/runner.js +218 -19
  94. package/plugins/enhance/lib/repo-map/updater.js +115 -27
  95. package/plugins/enhance/lib/state/workflow-state.js +31 -30
  96. package/plugins/enhance/lib/utils/command-parser.js +0 -0
  97. package/plugins/enhance/lib/utils/state-helpers.js +61 -0
  98. package/plugins/enhance/skills/enhance-agent-prompts/SKILL.md +1 -1
  99. package/plugins/enhance/skills/enhance-claude-memory/SKILL.md +1 -1
  100. package/plugins/enhance/skills/enhance-cross-file/SKILL.md +1 -1
  101. package/plugins/enhance/skills/enhance-docs/SKILL.md +1 -1
  102. package/plugins/enhance/skills/enhance-hooks/SKILL.md +1 -1
  103. package/plugins/enhance/skills/enhance-orchestrator/SKILL.md +1 -1
  104. package/plugins/enhance/skills/enhance-plugins/SKILL.md +1 -1
  105. package/plugins/enhance/skills/enhance-prompts/SKILL.md +1 -1
  106. package/plugins/enhance/skills/enhance-skills/SKILL.md +1 -1
  107. package/plugins/learn/.claude-plugin/plugin.json +1 -1
  108. package/plugins/learn/lib/collectors/github.js +76 -12
  109. package/plugins/learn/lib/perf/benchmark-runner.js +11 -6
  110. package/plugins/learn/lib/perf/investigation-state.js +12 -13
  111. package/plugins/learn/lib/perf/profiling-runner.js +23 -4
  112. package/plugins/learn/lib/repo-map/concurrency.js +29 -0
  113. package/plugins/learn/lib/repo-map/runner.js +218 -19
  114. package/plugins/learn/lib/repo-map/updater.js +115 -27
  115. package/plugins/learn/lib/state/workflow-state.js +31 -30
  116. package/plugins/learn/lib/utils/command-parser.js +0 -0
  117. package/plugins/learn/lib/utils/state-helpers.js +61 -0
  118. package/plugins/learn/skills/learn/SKILL.md +1 -1
  119. package/plugins/next-task/.claude-plugin/plugin.json +1 -1
  120. package/plugins/next-task/lib/collectors/github.js +76 -12
  121. package/plugins/next-task/lib/perf/benchmark-runner.js +11 -6
  122. package/plugins/next-task/lib/perf/investigation-state.js +12 -13
  123. package/plugins/next-task/lib/perf/profiling-runner.js +23 -4
  124. package/plugins/next-task/lib/repo-map/concurrency.js +29 -0
  125. package/plugins/next-task/lib/repo-map/runner.js +218 -19
  126. package/plugins/next-task/lib/repo-map/updater.js +115 -27
  127. package/plugins/next-task/lib/state/workflow-state.js +31 -30
  128. package/plugins/next-task/lib/utils/command-parser.js +0 -0
  129. package/plugins/next-task/lib/utils/state-helpers.js +61 -0
  130. package/plugins/next-task/skills/discover-tasks/SKILL.md +1 -1
  131. package/plugins/next-task/skills/validate-delivery/SKILL.md +1 -1
  132. package/plugins/perf/.claude-plugin/plugin.json +1 -1
  133. package/plugins/perf/lib/collectors/github.js +76 -12
  134. package/plugins/perf/lib/perf/benchmark-runner.js +11 -6
  135. package/plugins/perf/lib/perf/investigation-state.js +12 -13
  136. package/plugins/perf/lib/perf/profiling-runner.js +23 -4
  137. package/plugins/perf/lib/repo-map/concurrency.js +29 -0
  138. package/plugins/perf/lib/repo-map/runner.js +218 -19
  139. package/plugins/perf/lib/repo-map/updater.js +115 -27
  140. package/plugins/perf/lib/state/workflow-state.js +31 -30
  141. package/plugins/perf/lib/utils/command-parser.js +0 -0
  142. package/plugins/perf/lib/utils/state-helpers.js +61 -0
  143. package/plugins/perf/skills/perf-analyzer/SKILL.md +1 -1
  144. package/plugins/perf/skills/perf-baseline-manager/SKILL.md +1 -1
  145. package/plugins/perf/skills/perf-benchmarker/SKILL.md +1 -1
  146. package/plugins/perf/skills/perf-code-paths/SKILL.md +1 -1
  147. package/plugins/perf/skills/perf-investigation-logger/SKILL.md +1 -1
  148. package/plugins/perf/skills/perf-profiler/SKILL.md +1 -1
  149. package/plugins/perf/skills/perf-theory-gatherer/SKILL.md +1 -1
  150. package/plugins/perf/skills/perf-theory-tester/SKILL.md +1 -1
  151. package/plugins/repo-map/.claude-plugin/plugin.json +1 -1
  152. package/plugins/repo-map/lib/collectors/github.js +76 -12
  153. package/plugins/repo-map/lib/perf/benchmark-runner.js +11 -6
  154. package/plugins/repo-map/lib/perf/investigation-state.js +12 -13
  155. package/plugins/repo-map/lib/perf/profiling-runner.js +23 -4
  156. package/plugins/repo-map/lib/repo-map/concurrency.js +29 -0
  157. package/plugins/repo-map/lib/repo-map/runner.js +218 -19
  158. package/plugins/repo-map/lib/repo-map/updater.js +115 -27
  159. package/plugins/repo-map/lib/state/workflow-state.js +31 -30
  160. package/plugins/repo-map/lib/utils/command-parser.js +0 -0
  161. package/plugins/repo-map/lib/utils/state-helpers.js +61 -0
  162. package/plugins/ship/.claude-plugin/plugin.json +1 -1
  163. package/plugins/ship/lib/collectors/github.js +76 -12
  164. package/plugins/ship/lib/perf/benchmark-runner.js +11 -6
  165. package/plugins/ship/lib/perf/investigation-state.js +12 -13
  166. package/plugins/ship/lib/perf/profiling-runner.js +23 -4
  167. package/plugins/ship/lib/repo-map/concurrency.js +29 -0
  168. package/plugins/ship/lib/repo-map/runner.js +218 -19
  169. package/plugins/ship/lib/repo-map/updater.js +115 -27
  170. package/plugins/ship/lib/state/workflow-state.js +31 -30
  171. package/plugins/ship/lib/utils/command-parser.js +0 -0
  172. package/plugins/ship/lib/utils/state-helpers.js +61 -0
  173. package/plugins/sync-docs/.claude-plugin/plugin.json +1 -1
  174. package/plugins/sync-docs/lib/collectors/github.js +76 -12
  175. package/plugins/sync-docs/lib/perf/benchmark-runner.js +11 -6
  176. package/plugins/sync-docs/lib/perf/investigation-state.js +12 -13
  177. package/plugins/sync-docs/lib/perf/profiling-runner.js +23 -4
  178. package/plugins/sync-docs/lib/repo-map/concurrency.js +29 -0
  179. package/plugins/sync-docs/lib/repo-map/runner.js +218 -19
  180. package/plugins/sync-docs/lib/repo-map/updater.js +115 -27
  181. package/plugins/sync-docs/lib/state/workflow-state.js +31 -30
  182. package/plugins/sync-docs/lib/utils/command-parser.js +0 -0
  183. package/plugins/sync-docs/lib/utils/state-helpers.js +61 -0
  184. package/plugins/sync-docs/skills/sync-docs/SKILL.md +1 -1
  185. package/site/content.json +1 -1
@@ -14,6 +14,7 @@ const { execFileSync } = require('child_process');
14
14
  const DEFAULT_OPTIONS = {
15
15
  issueLimit: 100,
16
16
  prLimit: 50,
17
+ milestoneLimit: 100,
17
18
  timeout: 10000,
18
19
  cwd: process.cwd()
19
20
  };
@@ -25,16 +26,41 @@ const DEFAULT_OPTIONS = {
25
26
  * @returns {Object|null} Parsed JSON result or null
26
27
  */
27
28
  function execGh(args, options = {}) {
29
+ const result = execGhWithResult(args, options);
30
+ return result.ok ? result.data : null;
31
+ }
32
+
33
+ function execGhWithResult(args, options = {}) {
28
34
  try {
29
- const result = execFileSync('gh', args, {
35
+ const output = execFileSync('gh', args, {
30
36
  encoding: 'utf8',
31
37
  stdio: 'pipe',
32
38
  timeout: options.timeout || DEFAULT_OPTIONS.timeout,
33
39
  cwd: options.cwd || DEFAULT_OPTIONS.cwd
34
40
  });
35
- return JSON.parse(result);
36
- } catch {
37
- return null;
41
+
42
+ try {
43
+ return { ok: true, data: JSON.parse(output) };
44
+ } catch (error) {
45
+ return {
46
+ ok: false,
47
+ error: {
48
+ type: 'parse',
49
+ message: `Failed to parse gh output as JSON: ${error.message}`,
50
+ raw: output.slice(0, 500)
51
+ }
52
+ };
53
+ }
54
+ } catch (error) {
55
+ return {
56
+ ok: false,
57
+ error: {
58
+ type: error.killed ? 'timeout' : 'process',
59
+ message: error.message,
60
+ exitCode: error.status ?? null,
61
+ stderr: error.stderr ? String(error.stderr).trim() : ''
62
+ }
63
+ };
38
64
  }
39
65
  }
40
66
 
@@ -192,10 +218,18 @@ function scanGitHubState(options = {}) {
192
218
 
193
219
  const result = {
194
220
  available: false,
221
+ partial: false,
222
+ errors: [],
195
223
  summary: { issueCount: 0, prCount: 0, milestoneCount: 0 },
196
224
  issues: [],
197
225
  prs: [],
198
226
  milestones: [],
227
+ overdueMilestones: [],
228
+ pagination: {
229
+ issues: { requestedLimit: opts.issueLimit, fetchedCount: 0, hasMore: false },
230
+ prs: { requestedLimit: opts.prLimit, fetchedCount: 0, hasMore: false },
231
+ milestones: { requestedLimit: opts.milestoneLimit, fetchedCount: 0, hasMore: false }
232
+ },
199
233
  categorized: { bugs: [], features: [], security: [], enhancements: [], other: [] },
200
234
  stale: [],
201
235
  themes: []
@@ -209,44 +243,74 @@ function scanGitHubState(options = {}) {
209
243
  result.available = true;
210
244
 
211
245
  // Fetch open issues
212
- const issues = execGh([
246
+ const issuesResult = execGhWithResult([
213
247
  'issue', 'list',
214
248
  '--state', 'open',
215
249
  '--json', 'number,title,labels,milestone,createdAt,updatedAt,body',
216
250
  '--limit', String(opts.issueLimit)
217
251
  ], opts);
218
252
 
219
- if (issues) {
253
+ if (issuesResult.ok && Array.isArray(issuesResult.data)) {
254
+ const issues = issuesResult.data;
220
255
  result.issues = issues.map(summarizeIssue);
221
256
  result.summary.issueCount = issues.length;
257
+ result.pagination.issues.fetchedCount = issues.length;
258
+ result.pagination.issues.hasMore = opts.issueLimit > 0 && issues.length >= opts.issueLimit;
222
259
  categorizeIssues(result, issues);
223
260
  findStaleItems(result, issues, 90);
224
261
  extractThemes(result, issues);
262
+ } else if (!issuesResult.ok) {
263
+ result.errors.push({ source: 'issues', ...issuesResult.error });
225
264
  }
226
265
 
227
266
  // Fetch open PRs with files changed
228
- const prs = execGh([
267
+ const prsResult = execGhWithResult([
229
268
  'pr', 'list',
230
269
  '--state', 'open',
231
270
  '--json', 'number,title,labels,isDraft,createdAt,updatedAt,body,files',
232
271
  '--limit', String(opts.prLimit)
233
272
  ], opts);
234
273
 
235
- if (prs) {
274
+ if (prsResult.ok && Array.isArray(prsResult.data)) {
275
+ const prs = prsResult.data;
236
276
  result.prs = prs.map(summarizePR);
237
277
  result.summary.prCount = prs.length;
278
+ result.pagination.prs.fetchedCount = prs.length;
279
+ result.pagination.prs.hasMore = opts.prLimit > 0 && prs.length >= opts.prLimit;
280
+ } else if (!prsResult.ok) {
281
+ result.errors.push({ source: 'prs', ...prsResult.error });
238
282
  }
239
283
 
240
284
  // Fetch milestones
241
- const milestones = execGh([
285
+ const milestonesResult = execGhWithResult([
242
286
  'api', 'repos/{owner}/{repo}/milestones',
243
- '--jq', '.[].{title,state,due_on,open_issues,closed_issues}'
287
+ '--paginate',
288
+ '--slurp'
244
289
  ], opts);
245
290
 
246
- if (milestones) {
247
- result.milestones = Array.isArray(milestones) ? milestones : [milestones];
291
+ if (milestonesResult.ok && Array.isArray(milestonesResult.data)) {
292
+ const pages = milestonesResult.data;
293
+ const allMilestones = pages.flatMap(page => Array.isArray(page) ? page : []);
294
+ const mappedMilestones = allMilestones.map((milestone) => ({
295
+ title: milestone.title,
296
+ state: milestone.state,
297
+ due_on: milestone.due_on,
298
+ open_issues: milestone.open_issues,
299
+ closed_issues: milestone.closed_issues
300
+ }));
301
+
302
+ result.pagination.milestones.fetchedCount = mappedMilestones.length;
303
+ result.pagination.milestones.hasMore = opts.milestoneLimit > 0 && mappedMilestones.length > opts.milestoneLimit;
304
+ result.milestones = mappedMilestones.slice(0, opts.milestoneLimit);
248
305
  result.summary.milestoneCount = result.milestones.length;
249
306
  findOverdueMilestones(result);
307
+ } else if (!milestonesResult.ok) {
308
+ result.errors.push({ source: 'milestones', ...milestonesResult.error });
309
+ }
310
+
311
+ result.partial = result.errors.length > 0;
312
+ if (result.partial && !result.error) {
313
+ result.error = 'Partial GitHub data collected';
250
314
  }
251
315
 
252
316
  return result;
@@ -4,8 +4,9 @@
4
4
  * @module lib/perf/benchmark-runner
5
5
  */
6
6
 
7
- const { execSync } = require('child_process');
7
+ const { execFileSync } = require('child_process');
8
8
  const { validateBaseline } = require('./schemas');
9
+ const { parseCommand, resolveExecutableForPlatform } = require('../utils/command-parser');
9
10
 
10
11
  const DEFAULT_MIN_DURATION = 60;
11
12
  const BINARY_SEARCH_MIN_DURATION = 30;
@@ -55,6 +56,8 @@ function runBenchmark(command, options = {}) {
55
56
  throw new Error('Benchmark command must be a non-empty string');
56
57
  }
57
58
 
59
+ const parsedCommand = parseCommand(command, 'Benchmark command');
60
+ const executable = resolveExecutableForPlatform(parsedCommand.executable);
58
61
  const normalized = normalizeBenchmarkOptions(options);
59
62
  const setDurationEnv = options.setDurationEnv !== false;
60
63
  const env = {
@@ -71,18 +74,20 @@ function runBenchmark(command, options = {}) {
71
74
  const start = Date.now();
72
75
  let output;
73
76
  try {
74
- output = execSync(command, {
77
+ output = execFileSync(executable, parsedCommand.args, {
75
78
  stdio: 'pipe',
76
79
  encoding: 'utf8',
77
- env
80
+ env,
81
+ windowsHide: true,
82
+ cwd: options.cwd || process.cwd()
78
83
  });
79
84
  } catch (error) {
80
- const stderr = error.stderr ? error.stderr.toString().trim() : '';
81
- const stdout = error.stdout ? error.stdout.toString().trim() : '';
85
+ const stderr = error.stderr ? String(error.stderr).trim() : '';
86
+ const stdout = error.stdout ? String(error.stdout).trim() : '';
82
87
  const exitCode = error.status ?? 'unknown';
83
88
  const details = stderr || stdout || error.message || 'No error details available';
84
89
  throw new Error(
85
- `Benchmark command failed (exit code ${exitCode}): ${command}\n` +
90
+ `Benchmark command failed (exit code ${exitCode}): ${parsedCommand.display}\n` +
86
91
  `Details: ${details}`
87
92
  );
88
93
  }
@@ -14,12 +14,12 @@ const crypto = require('crypto');
14
14
  const { getStateDir } = require('../platform/state-dir');
15
15
  const { validateInvestigationState, assertValid } = require('./schemas');
16
16
  const { writeJsonAtomic, writeFileAtomic } = require('../utils/atomic-write');
17
+ const { isPlainObject, updatesApplied, sleepForRetry } = require('../utils/state-helpers');
17
18
 
18
19
  const SCHEMA_VERSION = 1;
19
20
  const INVESTIGATION_FILE = 'investigation.json';
20
21
  const LOG_DIR = 'investigations';
21
22
  const BASELINE_DIR = 'baselines';
22
-
23
23
  const PHASES = [
24
24
  'setup',
25
25
  'baseline',
@@ -196,10 +196,12 @@ function writeInvestigation(state, basePath = process.cwd()) {
196
196
  * @returns {object|null}
197
197
  */
198
198
  function updateInvestigation(updates, basePath = process.cwd()) {
199
- const MAX_RETRIES = 3;
199
+ const MAX_RETRIES = 5;
200
+ let fallbackState = null;
200
201
 
201
202
  for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
202
203
  const current = readInvestigation(basePath) || {};
204
+ fallbackState = current;
203
205
  const initialVersion = current._version || 0;
204
206
  const nextState = { ...current };
205
207
 
@@ -209,10 +211,7 @@ function updateInvestigation(updates, basePath = process.cwd()) {
209
211
 
210
212
  if (value === null) {
211
213
  nextState[key] = null;
212
- } else if (
213
- value && typeof value === 'object' && !Array.isArray(value) &&
214
- nextState[key] && typeof nextState[key] === 'object' && !Array.isArray(nextState[key])
215
- ) {
214
+ } else if (isPlainObject(value) && isPlainObject(nextState[key])) {
216
215
  nextState[key] = { ...nextState[key], ...value };
217
216
  } else {
218
217
  nextState[key] = value;
@@ -226,23 +225,23 @@ function updateInvestigation(updates, basePath = process.cwd()) {
226
225
 
227
226
  // Re-read to verify our write succeeded
228
227
  const afterWrite = readInvestigation(basePath);
229
- if (afterWrite && afterWrite._version === initialVersion + 1) {
228
+ if (afterWrite) {
229
+ fallbackState = afterWrite;
230
+ }
231
+ if (afterWrite && afterWrite._version >= initialVersion + 1 && updatesApplied(afterWrite, updates)) {
230
232
  return afterWrite; // Success
231
233
  }
232
234
 
233
235
  // Version conflict - retry after brief delay
234
236
  if (attempt < MAX_RETRIES - 1) {
235
237
  const delay = Math.floor(Math.random() * 50) + 10;
236
- const start = Date.now();
237
- while (Date.now() - start < delay) {
238
- // Busy wait (synchronous delay)
239
- }
238
+ sleepForRetry(delay);
240
239
  }
241
240
  }
242
241
 
243
242
  // All retries exhausted
244
- console.error('[WARN] updateInvestigation: max retries exceeded, possible version conflict');
245
- return readInvestigation(basePath);
243
+ console.error('[ERROR] updateInvestigation: failed to apply updates after max retries');
244
+ return readInvestigation(basePath) || fallbackState || { ...updates };
246
245
  }
247
246
 
248
247
  /**
@@ -4,8 +4,9 @@
4
4
  * @module lib/perf/profiling-runner
5
5
  */
6
6
 
7
- const { execSync } = require('child_process');
7
+ const { execFileSync } = require('child_process');
8
8
  const profilers = require('./profilers');
9
+ const { parseCommand, resolveExecutableForPlatform } = require('../utils/command-parser');
9
10
 
10
11
  /**
11
12
  * Run a profiling command and return artifacts/hotspots metadata.
@@ -16,6 +17,9 @@ const profilers = require('./profilers');
16
17
  */
17
18
  function runProfiling(options = {}) {
18
19
  const repoPath = options.repoPath || process.cwd();
20
+ const timeoutMs = Number.isFinite(options.timeoutMs)
21
+ ? Math.max(1, Math.floor(options.timeoutMs))
22
+ : null;
19
23
  const profiler = profilers.selectProfiler(repoPath);
20
24
 
21
25
  if (!profiler || typeof profiler.buildCommand !== 'function') {
@@ -27,14 +31,29 @@ function runProfiling(options = {}) {
27
31
  output: options.output,
28
32
  ...(options.profileOptions || {})
29
33
  });
34
+ const parsedCommand = parseCommand(command, 'Profiling command');
35
+ const executable = resolveExecutableForPlatform(parsedCommand.executable);
30
36
  const env = {
31
37
  ...process.env,
32
38
  ...(options.env || {})
33
39
  };
34
40
  try {
35
- execSync(command, { stdio: 'pipe', env });
41
+ const execOptions = {
42
+ stdio: 'pipe',
43
+ env,
44
+ cwd: repoPath,
45
+ windowsHide: true
46
+ };
47
+ if (timeoutMs !== null) {
48
+ execOptions.timeout = timeoutMs;
49
+ }
50
+
51
+ execFileSync(executable, parsedCommand.args, execOptions);
36
52
  } catch (error) {
37
- return { ok: false, error: error.message };
53
+ const stderr = error.stderr ? String(error.stderr).trim() : '';
54
+ const stdout = error.stdout ? String(error.stdout).trim() : '';
55
+ const details = stderr || stdout || error.message;
56
+ return { ok: false, error: `Profiling command failed: ${details}` };
38
57
  }
39
58
 
40
59
  const parsed = typeof profiler.parseOutput === 'function'
@@ -43,7 +62,7 @@ function runProfiling(options = {}) {
43
62
 
44
63
  const result = {
45
64
  tool: profiler.id,
46
- command,
65
+ command: parsedCommand.display,
47
66
  hotspots: parsed.hotspots || [],
48
67
  artifacts: parsed.artifacts || []
49
68
  };
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ async function runWithConcurrency(items, limit, worker) {
4
+ if (!Array.isArray(items) || items.length === 0) {
5
+ return [];
6
+ }
7
+
8
+ const maxConcurrency = Math.max(1, Math.min(items.length, Math.floor(limit) || 1));
9
+ const results = new Array(items.length);
10
+ let cursor = 0;
11
+
12
+ async function runWorker() {
13
+ while (true) {
14
+ const index = cursor;
15
+ cursor += 1;
16
+ if (index >= items.length) {
17
+ return;
18
+ }
19
+ results[index] = await worker(items[index], index);
20
+ }
21
+ }
22
+
23
+ await Promise.all(Array.from({ length: maxConcurrency }, () => runWorker()));
24
+ return results;
25
+ }
26
+
27
+ module.exports = {
28
+ runWithConcurrency
29
+ };