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.
- package/.claude-plugin/marketplace.json +13 -13
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +19 -2
- package/README.md +1 -0
- package/adapters/README.md +1 -1
- package/adapters/codex/skills/consult/SKILL.md +3 -2
- package/adapters/codex/skills/next-task/SKILL.md +8 -8
- package/adapters/opencode/agents/consult-agent.md +1 -1
- package/adapters/opencode/agents/delivery-validator.md +1 -1
- package/adapters/opencode/agents/implementation-agent.md +1 -1
- package/adapters/opencode/agents/worktree-manager.md +3 -3
- package/adapters/opencode/commands/consult.md +3 -2
- package/adapters/opencode/commands/next-task.md +7 -7
- package/adapters/opencode/skills/agnix/SKILL.md +1 -1
- package/adapters/opencode/skills/consult/SKILL.md +16 -4
- package/adapters/opencode/skills/deslop/SKILL.md +1 -1
- package/adapters/opencode/skills/discover-tasks/SKILL.md +2 -2
- package/adapters/opencode/skills/drift-analysis/SKILL.md +1 -1
- package/adapters/opencode/skills/enhance-agent-prompts/SKILL.md +1 -1
- package/adapters/opencode/skills/enhance-claude-memory/SKILL.md +1 -1
- package/adapters/opencode/skills/enhance-cross-file/SKILL.md +1 -1
- package/adapters/opencode/skills/enhance-docs/SKILL.md +1 -1
- package/adapters/opencode/skills/enhance-hooks/SKILL.md +1 -1
- package/adapters/opencode/skills/enhance-orchestrator/SKILL.md +1 -1
- package/adapters/opencode/skills/enhance-plugins/SKILL.md +1 -1
- package/adapters/opencode/skills/enhance-prompts/SKILL.md +1 -1
- package/adapters/opencode/skills/enhance-skills/SKILL.md +1 -1
- package/adapters/opencode/skills/learn/SKILL.md +1 -1
- package/adapters/opencode/skills/orchestrate-review/SKILL.md +1 -1
- package/adapters/opencode/skills/perf-analyzer/SKILL.md +1 -1
- package/adapters/opencode/skills/perf-baseline-manager/SKILL.md +1 -1
- package/adapters/opencode/skills/perf-benchmarker/SKILL.md +1 -1
- package/adapters/opencode/skills/perf-code-paths/SKILL.md +1 -1
- package/adapters/opencode/skills/perf-investigation-logger/SKILL.md +1 -1
- package/adapters/opencode/skills/perf-profiler/SKILL.md +1 -1
- package/adapters/opencode/skills/perf-theory-gatherer/SKILL.md +1 -1
- package/adapters/opencode/skills/perf-theory-tester/SKILL.md +1 -1
- package/adapters/opencode/skills/sync-docs/SKILL.md +1 -1
- package/adapters/opencode/skills/validate-delivery/SKILL.md +2 -2
- package/bin/cli.js +42 -8
- package/bin/dev-cli.js +16 -6
- package/lib/collectors/github.js +76 -12
- package/lib/perf/benchmark-runner.js +11 -6
- package/lib/perf/investigation-state.js +12 -13
- package/lib/perf/profiling-runner.js +23 -4
- package/lib/repo-map/concurrency.js +29 -0
- package/lib/repo-map/runner.js +218 -19
- package/lib/repo-map/updater.js +115 -27
- package/lib/state/workflow-state.js +31 -30
- package/lib/utils/command-parser.js +0 -0
- package/lib/utils/state-helpers.js +61 -0
- package/package.json +2 -1
- package/plugins/agnix/.claude-plugin/plugin.json +1 -1
- package/plugins/agnix/skills/agnix/SKILL.md +1 -1
- package/plugins/audit-project/.claude-plugin/plugin.json +1 -1
- package/plugins/audit-project/lib/collectors/github.js +76 -12
- package/plugins/audit-project/lib/perf/benchmark-runner.js +11 -6
- package/plugins/audit-project/lib/perf/investigation-state.js +12 -13
- package/plugins/audit-project/lib/perf/profiling-runner.js +23 -4
- package/plugins/audit-project/lib/repo-map/concurrency.js +29 -0
- package/plugins/audit-project/lib/repo-map/runner.js +218 -19
- package/plugins/audit-project/lib/repo-map/updater.js +115 -27
- package/plugins/audit-project/lib/state/workflow-state.js +31 -30
- package/plugins/audit-project/lib/utils/command-parser.js +0 -0
- package/plugins/audit-project/lib/utils/state-helpers.js +61 -0
- package/plugins/consult/.claude-plugin/plugin.json +1 -1
- package/plugins/consult/agents/consult-agent.md +1 -1
- package/plugins/consult/commands/consult.md +3 -2
- package/plugins/consult/skills/consult/SKILL.md +16 -4
- package/plugins/deslop/.claude-plugin/plugin.json +1 -1
- package/plugins/deslop/lib/collectors/github.js +76 -12
- package/plugins/deslop/lib/perf/benchmark-runner.js +11 -6
- package/plugins/deslop/lib/perf/investigation-state.js +12 -13
- package/plugins/deslop/lib/perf/profiling-runner.js +23 -4
- package/plugins/deslop/lib/repo-map/concurrency.js +29 -0
- package/plugins/deslop/lib/repo-map/runner.js +218 -19
- package/plugins/deslop/lib/repo-map/updater.js +115 -27
- package/plugins/deslop/lib/state/workflow-state.js +31 -30
- package/plugins/deslop/lib/utils/command-parser.js +0 -0
- package/plugins/deslop/lib/utils/state-helpers.js +61 -0
- package/plugins/deslop/skills/deslop/SKILL.md +1 -1
- package/plugins/drift-detect/.claude-plugin/plugin.json +1 -1
- package/plugins/drift-detect/lib/collectors/github.js +76 -12
- package/plugins/drift-detect/lib/perf/benchmark-runner.js +11 -6
- package/plugins/drift-detect/lib/perf/investigation-state.js +12 -13
- package/plugins/drift-detect/lib/perf/profiling-runner.js +23 -4
- package/plugins/drift-detect/lib/repo-map/concurrency.js +29 -0
- package/plugins/drift-detect/lib/repo-map/runner.js +218 -19
- package/plugins/drift-detect/lib/repo-map/updater.js +115 -27
- package/plugins/drift-detect/lib/state/workflow-state.js +31 -30
- package/plugins/drift-detect/lib/utils/command-parser.js +0 -0
- package/plugins/drift-detect/lib/utils/state-helpers.js +61 -0
- package/plugins/drift-detect/skills/drift-analysis/SKILL.md +1 -1
- package/plugins/enhance/.claude-plugin/plugin.json +1 -1
- package/plugins/enhance/lib/collectors/github.js +76 -12
- package/plugins/enhance/lib/perf/benchmark-runner.js +11 -6
- package/plugins/enhance/lib/perf/investigation-state.js +12 -13
- package/plugins/enhance/lib/perf/profiling-runner.js +23 -4
- package/plugins/enhance/lib/repo-map/concurrency.js +29 -0
- package/plugins/enhance/lib/repo-map/runner.js +218 -19
- package/plugins/enhance/lib/repo-map/updater.js +115 -27
- package/plugins/enhance/lib/state/workflow-state.js +31 -30
- package/plugins/enhance/lib/utils/command-parser.js +0 -0
- package/plugins/enhance/lib/utils/state-helpers.js +61 -0
- package/plugins/enhance/skills/enhance-agent-prompts/SKILL.md +1 -1
- package/plugins/enhance/skills/enhance-claude-memory/SKILL.md +1 -1
- package/plugins/enhance/skills/enhance-cross-file/SKILL.md +1 -1
- package/plugins/enhance/skills/enhance-docs/SKILL.md +1 -1
- package/plugins/enhance/skills/enhance-hooks/SKILL.md +1 -1
- package/plugins/enhance/skills/enhance-orchestrator/SKILL.md +1 -1
- package/plugins/enhance/skills/enhance-plugins/SKILL.md +1 -1
- package/plugins/enhance/skills/enhance-prompts/SKILL.md +1 -1
- package/plugins/enhance/skills/enhance-skills/SKILL.md +1 -1
- package/plugins/learn/.claude-plugin/plugin.json +1 -1
- package/plugins/learn/lib/collectors/github.js +76 -12
- package/plugins/learn/lib/perf/benchmark-runner.js +11 -6
- package/plugins/learn/lib/perf/investigation-state.js +12 -13
- package/plugins/learn/lib/perf/profiling-runner.js +23 -4
- package/plugins/learn/lib/repo-map/concurrency.js +29 -0
- package/plugins/learn/lib/repo-map/runner.js +218 -19
- package/plugins/learn/lib/repo-map/updater.js +115 -27
- package/plugins/learn/lib/state/workflow-state.js +31 -30
- package/plugins/learn/lib/utils/command-parser.js +0 -0
- package/plugins/learn/lib/utils/state-helpers.js +61 -0
- package/plugins/learn/skills/learn/SKILL.md +1 -1
- package/plugins/next-task/.claude-plugin/plugin.json +1 -1
- package/plugins/next-task/agents/delivery-validator.md +1 -1
- package/plugins/next-task/agents/implementation-agent.md +2 -2
- package/plugins/next-task/agents/worktree-manager.md +3 -3
- package/plugins/next-task/commands/next-task.md +8 -8
- package/plugins/next-task/hooks/hooks.json +1 -1
- package/plugins/next-task/lib/collectors/github.js +76 -12
- package/plugins/next-task/lib/perf/benchmark-runner.js +11 -6
- package/plugins/next-task/lib/perf/investigation-state.js +12 -13
- package/plugins/next-task/lib/perf/profiling-runner.js +23 -4
- package/plugins/next-task/lib/repo-map/concurrency.js +29 -0
- package/plugins/next-task/lib/repo-map/runner.js +218 -19
- package/plugins/next-task/lib/repo-map/updater.js +115 -27
- package/plugins/next-task/lib/state/workflow-state.js +31 -30
- package/plugins/next-task/lib/utils/command-parser.js +0 -0
- package/plugins/next-task/lib/utils/state-helpers.js +61 -0
- package/plugins/next-task/skills/discover-tasks/SKILL.md +2 -2
- package/plugins/next-task/skills/orchestrate-review/SKILL.md +1 -1
- package/plugins/next-task/skills/validate-delivery/SKILL.md +2 -2
- package/plugins/perf/.claude-plugin/plugin.json +1 -1
- package/plugins/perf/lib/collectors/github.js +76 -12
- package/plugins/perf/lib/perf/benchmark-runner.js +11 -6
- package/plugins/perf/lib/perf/investigation-state.js +12 -13
- package/plugins/perf/lib/perf/profiling-runner.js +23 -4
- package/plugins/perf/lib/repo-map/concurrency.js +29 -0
- package/plugins/perf/lib/repo-map/runner.js +218 -19
- package/plugins/perf/lib/repo-map/updater.js +115 -27
- package/plugins/perf/lib/state/workflow-state.js +31 -30
- package/plugins/perf/lib/utils/command-parser.js +0 -0
- package/plugins/perf/lib/utils/state-helpers.js +61 -0
- package/plugins/perf/skills/perf-analyzer/SKILL.md +1 -1
- package/plugins/perf/skills/perf-baseline-manager/SKILL.md +1 -1
- package/plugins/perf/skills/perf-benchmarker/SKILL.md +1 -1
- package/plugins/perf/skills/perf-code-paths/SKILL.md +1 -1
- package/plugins/perf/skills/perf-investigation-logger/SKILL.md +1 -1
- package/plugins/perf/skills/perf-profiler/SKILL.md +1 -1
- package/plugins/perf/skills/perf-theory-gatherer/SKILL.md +1 -1
- package/plugins/perf/skills/perf-theory-tester/SKILL.md +1 -1
- package/plugins/repo-map/.claude-plugin/plugin.json +1 -1
- package/plugins/repo-map/lib/collectors/github.js +76 -12
- package/plugins/repo-map/lib/perf/benchmark-runner.js +11 -6
- package/plugins/repo-map/lib/perf/investigation-state.js +12 -13
- package/plugins/repo-map/lib/perf/profiling-runner.js +23 -4
- package/plugins/repo-map/lib/repo-map/concurrency.js +29 -0
- package/plugins/repo-map/lib/repo-map/runner.js +218 -19
- package/plugins/repo-map/lib/repo-map/updater.js +115 -27
- package/plugins/repo-map/lib/state/workflow-state.js +31 -30
- package/plugins/repo-map/lib/utils/command-parser.js +0 -0
- package/plugins/repo-map/lib/utils/state-helpers.js +61 -0
- package/plugins/ship/.claude-plugin/plugin.json +1 -1
- package/plugins/ship/lib/collectors/github.js +76 -12
- package/plugins/ship/lib/perf/benchmark-runner.js +11 -6
- package/plugins/ship/lib/perf/investigation-state.js +12 -13
- package/plugins/ship/lib/perf/profiling-runner.js +23 -4
- package/plugins/ship/lib/repo-map/concurrency.js +29 -0
- package/plugins/ship/lib/repo-map/runner.js +218 -19
- package/plugins/ship/lib/repo-map/updater.js +115 -27
- package/plugins/ship/lib/state/workflow-state.js +31 -30
- package/plugins/ship/lib/utils/command-parser.js +0 -0
- package/plugins/ship/lib/utils/state-helpers.js +61 -0
- package/plugins/sync-docs/.claude-plugin/plugin.json +1 -1
- package/plugins/sync-docs/lib/collectors/github.js +76 -12
- package/plugins/sync-docs/lib/perf/benchmark-runner.js +11 -6
- package/plugins/sync-docs/lib/perf/investigation-state.js +12 -13
- package/plugins/sync-docs/lib/perf/profiling-runner.js +23 -4
- package/plugins/sync-docs/lib/repo-map/concurrency.js +29 -0
- package/plugins/sync-docs/lib/repo-map/runner.js +218 -19
- package/plugins/sync-docs/lib/repo-map/updater.js +115 -27
- package/plugins/sync-docs/lib/state/workflow-state.js +31 -30
- package/plugins/sync-docs/lib/utils/command-parser.js +0 -0
- package/plugins/sync-docs/lib/utils/state-helpers.js +61 -0
- package/plugins/sync-docs/skills/sync-docs/SKILL.md +1 -1
- package/scripts/bump-version.js +4 -1
- package/scripts/dev-install.js +9 -0
- package/scripts/validate-opencode-install.js +17 -4
- package/site/content.json +1 -1
- 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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
278
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
};
|
|
@@ -80,7 +80,7 @@ The results of the consultation are:
|
|
|
80
80
|
{response}
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
-
Set `continuable: true`
|
|
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
|
|
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.
|
|
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
|
-
**
|
|
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
|
-
**
|
|
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
|
|
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.
|
|
@@ -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
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
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 (
|
|
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
|
|
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 (
|
|
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
|
|
285
|
+
const milestonesResult = execGhWithResult([
|
|
242
286
|
'api', 'repos/{owner}/{repo}/milestones',
|
|
243
|
-
'--
|
|
287
|
+
'--paginate',
|
|
288
|
+
'--slurp'
|
|
244
289
|
], opts);
|
|
245
290
|
|
|
246
|
-
if (
|
|
247
|
-
|
|
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 {
|
|
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 =
|
|
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
|
|
81
|
-
const stdout = error.stdout ? error.stdout
|
|
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}): ${
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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('[
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
};
|