agentsys 5.0.0 → 5.0.2

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 (202) hide show
  1. package/.claude-plugin/marketplace.json +13 -13
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +19 -2
  4. package/README.md +1 -0
  5. package/adapters/README.md +1 -1
  6. package/adapters/codex/skills/consult/SKILL.md +3 -2
  7. package/adapters/codex/skills/next-task/SKILL.md +8 -8
  8. package/adapters/opencode/agents/consult-agent.md +1 -1
  9. package/adapters/opencode/agents/delivery-validator.md +1 -1
  10. package/adapters/opencode/agents/implementation-agent.md +1 -1
  11. package/adapters/opencode/agents/worktree-manager.md +3 -3
  12. package/adapters/opencode/commands/consult.md +3 -2
  13. package/adapters/opencode/commands/next-task.md +7 -7
  14. package/adapters/opencode/skills/agnix/SKILL.md +1 -1
  15. package/adapters/opencode/skills/consult/SKILL.md +16 -4
  16. package/adapters/opencode/skills/deslop/SKILL.md +1 -1
  17. package/adapters/opencode/skills/discover-tasks/SKILL.md +2 -2
  18. package/adapters/opencode/skills/drift-analysis/SKILL.md +1 -1
  19. package/adapters/opencode/skills/enhance-agent-prompts/SKILL.md +1 -1
  20. package/adapters/opencode/skills/enhance-claude-memory/SKILL.md +1 -1
  21. package/adapters/opencode/skills/enhance-cross-file/SKILL.md +1 -1
  22. package/adapters/opencode/skills/enhance-docs/SKILL.md +1 -1
  23. package/adapters/opencode/skills/enhance-hooks/SKILL.md +1 -1
  24. package/adapters/opencode/skills/enhance-orchestrator/SKILL.md +1 -1
  25. package/adapters/opencode/skills/enhance-plugins/SKILL.md +1 -1
  26. package/adapters/opencode/skills/enhance-prompts/SKILL.md +1 -1
  27. package/adapters/opencode/skills/enhance-skills/SKILL.md +1 -1
  28. package/adapters/opencode/skills/learn/SKILL.md +1 -1
  29. package/adapters/opencode/skills/orchestrate-review/SKILL.md +1 -1
  30. package/adapters/opencode/skills/perf-analyzer/SKILL.md +1 -1
  31. package/adapters/opencode/skills/perf-baseline-manager/SKILL.md +1 -1
  32. package/adapters/opencode/skills/perf-benchmarker/SKILL.md +1 -1
  33. package/adapters/opencode/skills/perf-code-paths/SKILL.md +1 -1
  34. package/adapters/opencode/skills/perf-investigation-logger/SKILL.md +1 -1
  35. package/adapters/opencode/skills/perf-profiler/SKILL.md +1 -1
  36. package/adapters/opencode/skills/perf-theory-gatherer/SKILL.md +1 -1
  37. package/adapters/opencode/skills/perf-theory-tester/SKILL.md +1 -1
  38. package/adapters/opencode/skills/sync-docs/SKILL.md +1 -1
  39. package/adapters/opencode/skills/validate-delivery/SKILL.md +2 -2
  40. package/bin/cli.js +42 -8
  41. package/bin/dev-cli.js +16 -6
  42. package/lib/collectors/github.js +76 -12
  43. package/lib/perf/benchmark-runner.js +11 -6
  44. package/lib/perf/investigation-state.js +12 -13
  45. package/lib/perf/profiling-runner.js +23 -4
  46. package/lib/repo-map/concurrency.js +29 -0
  47. package/lib/repo-map/runner.js +218 -19
  48. package/lib/repo-map/updater.js +115 -27
  49. package/lib/state/workflow-state.js +31 -30
  50. package/lib/utils/command-parser.js +0 -0
  51. package/lib/utils/state-helpers.js +61 -0
  52. package/package.json +2 -1
  53. package/plugins/agnix/.claude-plugin/plugin.json +1 -1
  54. package/plugins/agnix/skills/agnix/SKILL.md +1 -1
  55. package/plugins/audit-project/.claude-plugin/plugin.json +1 -1
  56. package/plugins/audit-project/lib/collectors/github.js +76 -12
  57. package/plugins/audit-project/lib/perf/benchmark-runner.js +11 -6
  58. package/plugins/audit-project/lib/perf/investigation-state.js +12 -13
  59. package/plugins/audit-project/lib/perf/profiling-runner.js +23 -4
  60. package/plugins/audit-project/lib/repo-map/concurrency.js +29 -0
  61. package/plugins/audit-project/lib/repo-map/runner.js +218 -19
  62. package/plugins/audit-project/lib/repo-map/updater.js +115 -27
  63. package/plugins/audit-project/lib/state/workflow-state.js +31 -30
  64. package/plugins/audit-project/lib/utils/command-parser.js +0 -0
  65. package/plugins/audit-project/lib/utils/state-helpers.js +61 -0
  66. package/plugins/consult/.claude-plugin/plugin.json +1 -1
  67. package/plugins/consult/agents/consult-agent.md +1 -1
  68. package/plugins/consult/commands/consult.md +3 -2
  69. package/plugins/consult/skills/consult/SKILL.md +16 -4
  70. package/plugins/deslop/.claude-plugin/plugin.json +1 -1
  71. package/plugins/deslop/lib/collectors/github.js +76 -12
  72. package/plugins/deslop/lib/perf/benchmark-runner.js +11 -6
  73. package/plugins/deslop/lib/perf/investigation-state.js +12 -13
  74. package/plugins/deslop/lib/perf/profiling-runner.js +23 -4
  75. package/plugins/deslop/lib/repo-map/concurrency.js +29 -0
  76. package/plugins/deslop/lib/repo-map/runner.js +218 -19
  77. package/plugins/deslop/lib/repo-map/updater.js +115 -27
  78. package/plugins/deslop/lib/state/workflow-state.js +31 -30
  79. package/plugins/deslop/lib/utils/command-parser.js +0 -0
  80. package/plugins/deslop/lib/utils/state-helpers.js +61 -0
  81. package/plugins/deslop/skills/deslop/SKILL.md +1 -1
  82. package/plugins/drift-detect/.claude-plugin/plugin.json +1 -1
  83. package/plugins/drift-detect/lib/collectors/github.js +76 -12
  84. package/plugins/drift-detect/lib/perf/benchmark-runner.js +11 -6
  85. package/plugins/drift-detect/lib/perf/investigation-state.js +12 -13
  86. package/plugins/drift-detect/lib/perf/profiling-runner.js +23 -4
  87. package/plugins/drift-detect/lib/repo-map/concurrency.js +29 -0
  88. package/plugins/drift-detect/lib/repo-map/runner.js +218 -19
  89. package/plugins/drift-detect/lib/repo-map/updater.js +115 -27
  90. package/plugins/drift-detect/lib/state/workflow-state.js +31 -30
  91. package/plugins/drift-detect/lib/utils/command-parser.js +0 -0
  92. package/plugins/drift-detect/lib/utils/state-helpers.js +61 -0
  93. package/plugins/drift-detect/skills/drift-analysis/SKILL.md +1 -1
  94. package/plugins/enhance/.claude-plugin/plugin.json +1 -1
  95. package/plugins/enhance/lib/collectors/github.js +76 -12
  96. package/plugins/enhance/lib/perf/benchmark-runner.js +11 -6
  97. package/plugins/enhance/lib/perf/investigation-state.js +12 -13
  98. package/plugins/enhance/lib/perf/profiling-runner.js +23 -4
  99. package/plugins/enhance/lib/repo-map/concurrency.js +29 -0
  100. package/plugins/enhance/lib/repo-map/runner.js +218 -19
  101. package/plugins/enhance/lib/repo-map/updater.js +115 -27
  102. package/plugins/enhance/lib/state/workflow-state.js +31 -30
  103. package/plugins/enhance/lib/utils/command-parser.js +0 -0
  104. package/plugins/enhance/lib/utils/state-helpers.js +61 -0
  105. package/plugins/enhance/skills/enhance-agent-prompts/SKILL.md +1 -1
  106. package/plugins/enhance/skills/enhance-claude-memory/SKILL.md +1 -1
  107. package/plugins/enhance/skills/enhance-cross-file/SKILL.md +1 -1
  108. package/plugins/enhance/skills/enhance-docs/SKILL.md +1 -1
  109. package/plugins/enhance/skills/enhance-hooks/SKILL.md +1 -1
  110. package/plugins/enhance/skills/enhance-orchestrator/SKILL.md +1 -1
  111. package/plugins/enhance/skills/enhance-plugins/SKILL.md +1 -1
  112. package/plugins/enhance/skills/enhance-prompts/SKILL.md +1 -1
  113. package/plugins/enhance/skills/enhance-skills/SKILL.md +1 -1
  114. package/plugins/learn/.claude-plugin/plugin.json +1 -1
  115. package/plugins/learn/lib/collectors/github.js +76 -12
  116. package/plugins/learn/lib/perf/benchmark-runner.js +11 -6
  117. package/plugins/learn/lib/perf/investigation-state.js +12 -13
  118. package/plugins/learn/lib/perf/profiling-runner.js +23 -4
  119. package/plugins/learn/lib/repo-map/concurrency.js +29 -0
  120. package/plugins/learn/lib/repo-map/runner.js +218 -19
  121. package/plugins/learn/lib/repo-map/updater.js +115 -27
  122. package/plugins/learn/lib/state/workflow-state.js +31 -30
  123. package/plugins/learn/lib/utils/command-parser.js +0 -0
  124. package/plugins/learn/lib/utils/state-helpers.js +61 -0
  125. package/plugins/learn/skills/learn/SKILL.md +1 -1
  126. package/plugins/next-task/.claude-plugin/plugin.json +1 -1
  127. package/plugins/next-task/agents/delivery-validator.md +1 -1
  128. package/plugins/next-task/agents/implementation-agent.md +2 -2
  129. package/plugins/next-task/agents/worktree-manager.md +3 -3
  130. package/plugins/next-task/commands/next-task.md +8 -8
  131. package/plugins/next-task/hooks/hooks.json +1 -1
  132. package/plugins/next-task/lib/collectors/github.js +76 -12
  133. package/plugins/next-task/lib/perf/benchmark-runner.js +11 -6
  134. package/plugins/next-task/lib/perf/investigation-state.js +12 -13
  135. package/plugins/next-task/lib/perf/profiling-runner.js +23 -4
  136. package/plugins/next-task/lib/repo-map/concurrency.js +29 -0
  137. package/plugins/next-task/lib/repo-map/runner.js +218 -19
  138. package/plugins/next-task/lib/repo-map/updater.js +115 -27
  139. package/plugins/next-task/lib/state/workflow-state.js +31 -30
  140. package/plugins/next-task/lib/utils/command-parser.js +0 -0
  141. package/plugins/next-task/lib/utils/state-helpers.js +61 -0
  142. package/plugins/next-task/skills/discover-tasks/SKILL.md +2 -2
  143. package/plugins/next-task/skills/orchestrate-review/SKILL.md +1 -1
  144. package/plugins/next-task/skills/validate-delivery/SKILL.md +2 -2
  145. package/plugins/perf/.claude-plugin/plugin.json +1 -1
  146. package/plugins/perf/lib/collectors/github.js +76 -12
  147. package/plugins/perf/lib/perf/benchmark-runner.js +11 -6
  148. package/plugins/perf/lib/perf/investigation-state.js +12 -13
  149. package/plugins/perf/lib/perf/profiling-runner.js +23 -4
  150. package/plugins/perf/lib/repo-map/concurrency.js +29 -0
  151. package/plugins/perf/lib/repo-map/runner.js +218 -19
  152. package/plugins/perf/lib/repo-map/updater.js +115 -27
  153. package/plugins/perf/lib/state/workflow-state.js +31 -30
  154. package/plugins/perf/lib/utils/command-parser.js +0 -0
  155. package/plugins/perf/lib/utils/state-helpers.js +61 -0
  156. package/plugins/perf/skills/perf-analyzer/SKILL.md +1 -1
  157. package/plugins/perf/skills/perf-baseline-manager/SKILL.md +1 -1
  158. package/plugins/perf/skills/perf-benchmarker/SKILL.md +1 -1
  159. package/plugins/perf/skills/perf-code-paths/SKILL.md +1 -1
  160. package/plugins/perf/skills/perf-investigation-logger/SKILL.md +1 -1
  161. package/plugins/perf/skills/perf-profiler/SKILL.md +1 -1
  162. package/plugins/perf/skills/perf-theory-gatherer/SKILL.md +1 -1
  163. package/plugins/perf/skills/perf-theory-tester/SKILL.md +1 -1
  164. package/plugins/repo-map/.claude-plugin/plugin.json +1 -1
  165. package/plugins/repo-map/lib/collectors/github.js +76 -12
  166. package/plugins/repo-map/lib/perf/benchmark-runner.js +11 -6
  167. package/plugins/repo-map/lib/perf/investigation-state.js +12 -13
  168. package/plugins/repo-map/lib/perf/profiling-runner.js +23 -4
  169. package/plugins/repo-map/lib/repo-map/concurrency.js +29 -0
  170. package/plugins/repo-map/lib/repo-map/runner.js +218 -19
  171. package/plugins/repo-map/lib/repo-map/updater.js +115 -27
  172. package/plugins/repo-map/lib/state/workflow-state.js +31 -30
  173. package/plugins/repo-map/lib/utils/command-parser.js +0 -0
  174. package/plugins/repo-map/lib/utils/state-helpers.js +61 -0
  175. package/plugins/ship/.claude-plugin/plugin.json +1 -1
  176. package/plugins/ship/lib/collectors/github.js +76 -12
  177. package/plugins/ship/lib/perf/benchmark-runner.js +11 -6
  178. package/plugins/ship/lib/perf/investigation-state.js +12 -13
  179. package/plugins/ship/lib/perf/profiling-runner.js +23 -4
  180. package/plugins/ship/lib/repo-map/concurrency.js +29 -0
  181. package/plugins/ship/lib/repo-map/runner.js +218 -19
  182. package/plugins/ship/lib/repo-map/updater.js +115 -27
  183. package/plugins/ship/lib/state/workflow-state.js +31 -30
  184. package/plugins/ship/lib/utils/command-parser.js +0 -0
  185. package/plugins/ship/lib/utils/state-helpers.js +61 -0
  186. package/plugins/sync-docs/.claude-plugin/plugin.json +1 -1
  187. package/plugins/sync-docs/lib/collectors/github.js +76 -12
  188. package/plugins/sync-docs/lib/perf/benchmark-runner.js +11 -6
  189. package/plugins/sync-docs/lib/perf/investigation-state.js +12 -13
  190. package/plugins/sync-docs/lib/perf/profiling-runner.js +23 -4
  191. package/plugins/sync-docs/lib/repo-map/concurrency.js +29 -0
  192. package/plugins/sync-docs/lib/repo-map/runner.js +218 -19
  193. package/plugins/sync-docs/lib/repo-map/updater.js +115 -27
  194. package/plugins/sync-docs/lib/state/workflow-state.js +31 -30
  195. package/plugins/sync-docs/lib/utils/command-parser.js +0 -0
  196. package/plugins/sync-docs/lib/utils/state-helpers.js +61 -0
  197. package/plugins/sync-docs/skills/sync-docs/SKILL.md +1 -1
  198. package/scripts/bump-version.js +4 -1
  199. package/scripts/dev-install.js +9 -0
  200. package/scripts/validate-opencode-install.js +17 -4
  201. package/site/content.json +1 -1
  202. package/site/index.html +1 -1
@@ -16,11 +16,11 @@ const path = require('path');
16
16
  const crypto = require('crypto');
17
17
  const { getStateDir } = require('../platform/state-dir');
18
18
  const { writeJsonAtomic } = require('../utils/atomic-write');
19
+ const { isPlainObject, updatesApplied, sleepForRetry } = require('../utils/state-helpers');
19
20
 
20
21
  // File paths
21
22
  const TASKS_FILE = 'tasks.json';
22
23
  const FLOW_FILE = 'flow.json';
23
-
24
24
  /**
25
25
  * Validate and resolve path to prevent path traversal attacks
26
26
  * @param {string} basePath - Base directory path
@@ -223,7 +223,7 @@ function writeFlow(flow, worktreePath = process.cwd()) {
223
223
  * Uses optimistic locking with version check and retry
224
224
  */
225
225
  function updateFlow(updates, worktreePath = process.cwd()) {
226
- const MAX_RETRIES = 3;
226
+ const MAX_RETRIES = 5;
227
227
 
228
228
  for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
229
229
  const flow = readFlow(worktreePath) || {};
@@ -238,10 +238,7 @@ function updateFlow(updates, worktreePath = process.cwd()) {
238
238
  flow[key] = null;
239
239
  }
240
240
  // Deep merge if both source and target are non-null objects
241
- else if (
242
- value && typeof value === 'object' && !Array.isArray(value) &&
243
- flow[key] && typeof flow[key] === 'object' && !Array.isArray(flow[key])
244
- ) {
241
+ else if (isPlainObject(value) && isPlainObject(flow[key])) {
245
242
  flow[key] = { ...flow[key], ...value };
246
243
  }
247
244
  // Otherwise direct assignment
@@ -258,24 +255,26 @@ function updateFlow(updates, worktreePath = process.cwd()) {
258
255
 
259
256
  // Re-read to verify our write succeeded
260
257
  const afterWrite = readFlow(worktreePath);
261
- if (afterWrite && afterWrite._version === initialVersion + 1) {
258
+ if (afterWrite && afterWrite._version >= initialVersion + 1 && updatesApplied(afterWrite, updates)) {
262
259
  return true; // Success
263
260
  }
264
261
 
265
- // Version conflict - retry after brief delay
262
+ // Version conflict or overwrite - retry after brief delay
266
263
  if (attempt < MAX_RETRIES - 1) {
267
- // Small random delay to reduce collision probability
268
264
  const delay = Math.floor(Math.random() * 50) + 10;
269
- const start = Date.now();
270
- while (Date.now() - start < delay) {
271
- // Busy wait (synchronous delay)
272
- }
265
+ sleepForRetry(delay);
273
266
  }
274
267
  }
275
268
 
276
- // All retries exhausted
277
- console.error('[WARN] updateFlow: max retries exceeded, possible version conflict');
278
- return true; // Return true to not break callers, but log warning
269
+ // All retries exhausted. One final read can detect if another writer
270
+ // applied the same updates while we were retrying.
271
+ const latest = readFlow(worktreePath);
272
+ if (latest && updatesApplied(latest, updates)) {
273
+ return true;
274
+ }
275
+
276
+ console.error('[ERROR] updateFlow: failed to apply updates after max retries');
277
+ return false;
279
278
  }
280
279
 
281
280
  /**
@@ -420,8 +419,8 @@ function completePhase(result = null, worktreePath = process.cwd()) {
420
419
  }
421
420
  }
422
421
 
423
- updateFlow(updates, worktreePath);
424
- return readFlow(worktreePath);
422
+ const updated = updateFlow(updates, worktreePath);
423
+ return updated ? readFlow(worktreePath) : null;
425
424
  }
426
425
 
427
426
  /**
@@ -454,16 +453,17 @@ function failWorkflow(error, worktreePath = process.cwd()) {
454
453
  function completeWorkflow(worktreePath = process.cwd()) {
455
454
  const flow = readFlow(worktreePath);
456
455
 
457
- // Clear active task from main project if projectPath is stored
458
- if (flow && flow.projectPath) {
459
- clearActiveTask(flow.projectPath);
460
- }
461
-
462
- return updateFlow({
456
+ const updated = updateFlow({
463
457
  phase: 'complete',
464
458
  status: 'completed',
465
459
  completedAt: new Date().toISOString()
466
460
  }, worktreePath);
461
+
462
+ if (updated && flow && flow.projectPath) {
463
+ clearActiveTask(flow.projectPath);
464
+ }
465
+
466
+ return updated;
467
467
  }
468
468
 
469
469
  /**
@@ -473,16 +473,17 @@ function completeWorkflow(worktreePath = process.cwd()) {
473
473
  function abortWorkflow(reason, worktreePath = process.cwd()) {
474
474
  const flow = readFlow(worktreePath);
475
475
 
476
- // Clear active task from main project if projectPath is stored
477
- if (flow && flow.projectPath) {
478
- clearActiveTask(flow.projectPath);
479
- }
480
-
481
- return updateFlow({
476
+ const updated = updateFlow({
482
477
  status: 'aborted',
483
478
  abortReason: reason,
484
479
  abortedAt: new Date().toISOString()
485
480
  }, worktreePath);
481
+
482
+ if (updated && flow && flow.projectPath) {
483
+ clearActiveTask(flow.projectPath);
484
+ }
485
+
486
+ return updated;
486
487
  }
487
488
 
488
489
  // =============================================================================
Binary file
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ const { isDeepStrictEqual } = require('util');
4
+
5
+ const RETRY_SLEEP_STATE = typeof SharedArrayBuffer === 'function' && typeof Atomics === 'object' && typeof Atomics.wait === 'function'
6
+ ? new Int32Array(new SharedArrayBuffer(4))
7
+ : null;
8
+
9
+ function isPlainObject(value) {
10
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
11
+ }
12
+
13
+ function hasUpdatedSubset(target, subset) {
14
+ if (!isPlainObject(subset)) {
15
+ return isDeepStrictEqual(target, subset);
16
+ }
17
+ if (!isPlainObject(target)) {
18
+ return false;
19
+ }
20
+
21
+ for (const [key, value] of Object.entries(subset)) {
22
+ if (!hasUpdatedSubset(target[key], value)) {
23
+ return false;
24
+ }
25
+ }
26
+ return true;
27
+ }
28
+
29
+ function updatesApplied(state, updates) {
30
+ if (!state) return false;
31
+
32
+ for (const [key, value] of Object.entries(updates || {})) {
33
+ if (key === '_version') continue;
34
+ if (!hasUpdatedSubset(state[key], value)) {
35
+ return false;
36
+ }
37
+ }
38
+
39
+ return true;
40
+ }
41
+
42
+ function sleepForRetry(ms) {
43
+ if (!Number.isFinite(ms) || ms <= 0) {
44
+ return;
45
+ }
46
+
47
+ const delayMs = Math.floor(ms);
48
+ if (RETRY_SLEEP_STATE) {
49
+ try {
50
+ Atomics.wait(RETRY_SLEEP_STATE, 0, 0, delayMs);
51
+ } catch {
52
+ // Ignore environments where Atomics.wait exists but cannot be used.
53
+ }
54
+ }
55
+ }
56
+
57
+ module.exports = {
58
+ isPlainObject,
59
+ updatesApplied,
60
+ sleepForRetry
61
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentsys",
3
- "version": "5.0.0",
3
+ "version": "5.0.2",
4
4
  "description": "A modular runtime and orchestration system for AI agents - works with Claude Code, OpenCode, and Codex CLI",
5
5
  "main": "lib/platform/detect-platform.js",
6
6
  "type": "commonjs",
@@ -81,6 +81,7 @@
81
81
  "node": ">=18.0.0"
82
82
  },
83
83
  "dependencies": {
84
+ "agentsys": "^5.0.0",
84
85
  "js-yaml": "^4.1.1"
85
86
  },
86
87
  "devDependencies": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agnix",
3
- "version": "5.0.0",
3
+ "version": "5.0.2",
4
4
  "description": "Lint agent configurations before they break your workflow. Validates Skills, Hooks, MCP, Memory, Plugins.",
5
5
  "author": {
6
6
  "name": "Avi Fenesh",
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agnix
3
3
  description: "Use when user asks to 'lint agent configs', 'validate skills', 'check CLAUDE.md', 'validate hooks', 'lint MCP'. Validates agent configuration files against 155 rules across 10+ AI tools."
4
- version: 5.0.0
4
+ version: 5.0.2
5
5
  argument-hint: "[path] [--fix] [--strict] [--target=claude-code|cursor|codex]"
6
6
  ---
7
7
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "audit-project",
3
- "version": "5.0.0",
3
+ "version": "5.0.2",
4
4
  "description": "Multi-agent iterative code review until zero issues remain",
5
5
  "author": {
6
6
  "name": "Avi Fenesh",
@@ -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
+ };