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
  // =============================================================================
@@ -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
+ };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "consult",
3
- "version": "5.0.0",
3
+ "version": "5.0.2",
4
4
  "description": "Cross-tool AI consultation: get second opinions from Gemini, Codex, Claude, OpenCode, or Copilot CLI",
5
5
  "author": {
6
6
  "name": "Avi Fenesh",
@@ -80,7 +80,7 @@ The results of the consultation are:
80
80
  {response}
81
81
  ```
82
82
 
83
- Set `continuable: true` only for Claude and Gemini (tools with session resumption support).
83
+ Set `continuable: true` for Claude, Gemini, Codex, and OpenCode (tools with continuation support). Codex continuation is context-based (prior Q&A prepended to prompt) since `codex resume` is TUI-only. Claude, Gemini, and OpenCode support native session resume flags. Only Copilot is non-continuable.
84
84
 
85
85
  ### 5. Save Session State
86
86
 
@@ -212,9 +212,10 @@ The results of the consultation are:
212
212
  {response}
213
213
  ```
214
214
 
215
- For continuable tools (Claude/Gemini), also display: `Session: {session_id} - use /consult --continue to resume`
215
+ For continuable tools with a session_id (Claude, Gemini, OpenCode), display: `Session: {session_id} - use /consult --continue to resume`
216
+ For Codex (context-based continuation, no session_id), display: `Use /consult --continue to continue this conversation (prior context will be prepended)`
216
217
 
217
- Save session state for continuable tools (Claude, Gemini) to `{AI_STATE_DIR}/consult/last-session.json`.
218
+ Save session state for continuable tools (Claude, Gemini, Codex, OpenCode) to `{AI_STATE_DIR}/consult/last-session.json`.
218
219
 
219
220
  Platform state directory:
220
221
  - Claude Code: `.claude/`
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: consult
3
3
  description: "Cross-tool AI consultation. Use when user asks to 'consult gemini', 'ask codex', 'get second opinion', 'cross-check with claude', 'consult another AI', 'ask opencode', 'copilot opinion', or wants a second opinion from a different AI tool."
4
- version: 5.0.0
4
+ version: 5.0.2
5
5
  argument-hint: "[question] [--tool] [--effort] [--model] [--context] [--continue]"
6
6
  ---
7
7
 
@@ -76,6 +76,7 @@ Models: gemini-2.5-flash, gemini-2.5-pro, gemini-3-flash-preview, gemini-3-pro-p
76
76
 
77
77
  ```
78
78
  Command: codex -q "QUESTION" --json -m "MODEL" -a suggest -c model_reasoning_effort="LEVEL"
79
+ Session resume: codex resume "SESSION_ID"
79
80
  ```
80
81
 
81
82
  Models: gpt-5.1-codex-mini, gpt-5-codex, gpt-5.1-codex, gpt-5.2-codex, gpt-5.3-codex, gpt-5.1-codex-max
@@ -88,12 +89,14 @@ Models: gpt-5.1-codex-mini, gpt-5-codex, gpt-5.1-codex, gpt-5.2-codex, gpt-5.3-c
88
89
  | max | gpt-5.3-codex | xhigh |
89
90
 
90
91
  **Parse output**: `JSON.parse(stdout).message` or raw text
91
- **Continuable**: No
92
+ **Session ID**: Codex prints a resume hint at session end (e.g., `codex resume SESSION_ID`). Extract the session ID from stdout or from `JSON.parse(stdout).session_id` if available.
93
+ **Continuable**: Yes, but interactive only. Sessions are stored as JSONL rollout files at `~/.codex/sessions/`. The `codex resume SESSION_ID` subcommand resumes in TUI mode -- it does not support `-q` for non-interactive use. For non-interactive continuation, prepend prior conversation context to the new question text instead.
92
94
 
93
95
  ### OpenCode
94
96
 
95
97
  ```
96
98
  Command: opencode run "QUESTION" --format json --model "MODEL" --variant "VARIANT"
99
+ Session resume: opencode run "QUESTION" --format json --model "MODEL" --variant "VARIANT" --continue (most recent) or --session "SESSION_ID"
97
100
  With thinking: add --thinking flag
98
101
  ```
99
102
 
@@ -107,7 +110,8 @@ Models: 75+ via providers (format: provider/model). Top picks: claude-sonnet-4-5
107
110
  | max | (user-selected or default) | high + --thinking |
108
111
 
109
112
  **Parse output**: Parse JSON events from stdout, extract final text response
110
- **Continuable**: No
113
+ **Session ID**: Extract from JSON output if available, or use `--continue` to auto-resume the most recent session.
114
+ **Continuable**: Yes (via `--continue` or `--session`). Sessions are stored in a SQLite database in the OpenCode data directory. Use `--session SESSION_ID` for a specific session, or `--continue` for the most recent.
111
115
 
112
116
  ### Copilot
113
117
 
@@ -149,7 +153,10 @@ If `--model` is specified, use it directly. Otherwise, use the effort-based mode
149
153
 
150
154
  Use the command template from the provider's configuration section. Substitute QUESTION, MODEL, TURNS, LEVEL, and VARIANT with resolved values.
151
155
 
152
- If continuing a session (Claude or Gemini): append `--resume SESSION_ID`.
156
+ If continuing a session:
157
+ - **Claude or Gemini**: append `--resume SESSION_ID` to the command.
158
+ - **Codex**: no non-interactive resume flag exists. Prepend the prior Q&A context to the new question text before passing to `codex -q`.
159
+ - **OpenCode**: append `--session SESSION_ID` to the command. If no session_id is saved, use `--continue` instead (resumes most recent session).
153
160
  If OpenCode at max effort: append `--thinking`.
154
161
 
155
162
  ### Step 3: Context Packaging
@@ -174,9 +181,14 @@ User-provided question text MUST NOT be interpolated into shell command strings.
174
181
  | Provider | Safe command pattern |
175
182
  |----------|---------------------|
176
183
  | Claude | `claude -p - --output-format json --model "MODEL" --max-turns TURNS --allowedTools "Read,Glob,Grep" < "{AI_STATE_DIR}/consult/question.tmp"` |
184
+ | Claude (resume) | `claude -p - --output-format json --model "MODEL" --max-turns TURNS --allowedTools "Read,Glob,Grep" --resume "SESSION_ID" < "{AI_STATE_DIR}/consult/question.tmp"` |
177
185
  | Gemini | `gemini -p - --output-format json -m "MODEL" < "{AI_STATE_DIR}/consult/question.tmp"` |
186
+ | Gemini (resume) | `gemini -p - --output-format json -m "MODEL" --resume "SESSION_ID" < "{AI_STATE_DIR}/consult/question.tmp"` |
178
187
  | Codex | `codex -q "$(cat "{AI_STATE_DIR}/consult/question.tmp")" --json -m "MODEL" -a suggest` (Codex lacks stdin mode -- cat reads from platform-controlled path, not user input) |
188
+ | Codex (continue) | No non-interactive resume. Prepend prior context to question, then use standard Codex command above. |
179
189
  | OpenCode | `opencode run - --format json --model "MODEL" --variant "VARIANT" < "{AI_STATE_DIR}/consult/question.tmp"` |
190
+ | OpenCode (resume by ID) | `opencode run - --format json --model "MODEL" --variant "VARIANT" --session "SESSION_ID" < "{AI_STATE_DIR}/consult/question.tmp"` |
191
+ | OpenCode (resume latest) | `opencode run - --format json --model "MODEL" --variant "VARIANT" --continue < "{AI_STATE_DIR}/consult/question.tmp"` |
180
192
  | Copilot | `copilot -p - < "{AI_STATE_DIR}/consult/question.tmp"` |
181
193
 
182
194
  3. **Delete the temp file** after the command completes (success or failure). Always clean up to prevent accumulation.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deslop",
3
- "version": "5.0.0",
3
+ "version": "5.0.2",
4
4
  "description": "AI slop cleanup with minimal diffs and behavior preservation",
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
+ };