swarm-orchestrator 7.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +199 -0
- package/agents/reviewer.agent.md +50 -0
- package/agents/worker.agent.md +41 -0
- package/config/default-agents.yaml +56 -0
- package/config/pm-agent.yaml +36 -0
- package/config/quality-gates.yaml +88 -0
- package/config/repair-agent.yaml +34 -0
- package/config/semgrep-rules/complexity-mismatch.yaml +6 -0
- package/config/semgrep-rules/exception-swallowing.yaml +13 -0
- package/config/semgrep-rules/hardcoded-answer.yaml +6 -0
- package/config/semgrep-rules/mock-mutation.yaml +6 -0
- package/config/semgrep-rules/test-modification.yaml +6 -0
- package/config/user-agents.yaml +23 -0
- package/dist/src/adapters/adapter-factory.js +29 -0
- package/dist/src/adapters/agent-adapter.js +32 -0
- package/dist/src/adapters/claude-code-adapter.js +126 -0
- package/dist/src/adapters/claude-code-teams.js +119 -0
- package/dist/src/adapters/codex-adapter.js +118 -0
- package/dist/src/adapters/copilot-adapter.js +224 -0
- package/dist/src/adapters/fatal-error-classifier.js +88 -0
- package/dist/src/adapters/index.js +44 -0
- package/dist/src/adapters/persistent-session.js +204 -0
- package/dist/src/adapters/process-supervisor.js +199 -0
- package/dist/src/agents-exporter.js +398 -0
- package/dist/src/analytics-log.js +161 -0
- package/dist/src/baseline-scanner.js +65 -0
- package/dist/src/bootstrap-evidence.js +88 -0
- package/dist/src/bootstrap-orchestrator.js +205 -0
- package/dist/src/bootstrap-types.js +5 -0
- package/dist/src/branch-merger.js +427 -0
- package/dist/src/cli/attest-handlers.js +44 -0
- package/dist/src/cli/cost-prompt.js +51 -0
- package/dist/src/cli/demo-handlers.js +274 -0
- package/dist/src/cli/flags.js +152 -0
- package/dist/src/cli/index.js +57 -0
- package/dist/src/cli/live-status.js +186 -0
- package/dist/src/cli/misc-handlers.js +242 -0
- package/dist/src/cli/plan-handlers.js +350 -0
- package/dist/src/cli/share-handlers.js +163 -0
- package/dist/src/cli/status-handlers.js +457 -0
- package/dist/src/cli/swarm-handlers.js +638 -0
- package/dist/src/cli/usage.js +72 -0
- package/dist/src/cli.js +217 -0
- package/dist/src/commit-pattern-detector.js +143 -0
- package/dist/src/commit-quality-analyzer.js +38 -0
- package/dist/src/config-loader.js +339 -0
- package/dist/src/context-broker.js +358 -0
- package/dist/src/copilot-cli-wrapper.js +258 -0
- package/dist/src/cost-estimator.js +241 -0
- package/dist/src/defaults.js +18 -0
- package/dist/src/demo-mode.js +180 -0
- package/dist/src/deployment-handler.js +78 -0
- package/dist/src/deployment-manager.js +228 -0
- package/dist/src/execution-queue.js +196 -0
- package/dist/src/external-tool-manager.js +184 -0
- package/dist/src/gate-prompt-builder.js +117 -0
- package/dist/src/gate-remediation.js +173 -0
- package/dist/src/github-issues-ingester.js +152 -0
- package/dist/src/github-mcp-integrator.js +164 -0
- package/dist/src/hook-generator.js +229 -0
- package/dist/src/index.js +48 -0
- package/dist/src/knowledge-base.js +300 -0
- package/dist/src/logger.js +111 -0
- package/dist/src/meta-analyzer.js +208 -0
- package/dist/src/metrics-collector.js +209 -0
- package/dist/src/metrics-types.js +5 -0
- package/dist/src/multi-repo-coordinator.js +179 -0
- package/dist/src/orchestrator/async-meta-analysis.js +84 -0
- package/dist/src/orchestrator/fatal-run-error.js +43 -0
- package/dist/src/orchestrator/final-gates-remediation.js +258 -0
- package/dist/src/orchestrator/git-state-utils.js +119 -0
- package/dist/src/orchestrator/pause-controller.js +55 -0
- package/dist/src/orchestrator/replan-runner.js +258 -0
- package/dist/src/orchestrator/step-executor.js +391 -0
- package/dist/src/orchestrator/wave-scheduler-loop.js +204 -0
- package/dist/src/owasp-mapper.js +153 -0
- package/dist/src/owasp-report-renderer.js +39 -0
- package/dist/src/plan-files.js +142 -0
- package/dist/src/plan-generator.js +1207 -0
- package/dist/src/pm-agent.js +296 -0
- package/dist/src/post-run-reporter.js +203 -0
- package/dist/src/pr-automation.js +103 -0
- package/dist/src/pr-manager.js +318 -0
- package/dist/src/presenter/index.js +209 -0
- package/dist/src/prompt-builder.js +173 -0
- package/dist/src/quality-gates/config-loader.js +196 -0
- package/dist/src/quality-gates/default-config.js +243 -0
- package/dist/src/quality-gates/file-utils.js +152 -0
- package/dist/src/quality-gates/gate-runner.js +171 -0
- package/dist/src/quality-gates/gates/accessibility.js +355 -0
- package/dist/src/quality-gates/gates/duplicate-blocks.js +144 -0
- package/dist/src/quality-gates/gates/hardcoded-config.js +52 -0
- package/dist/src/quality-gates/gates/readme-claims.js +62 -0
- package/dist/src/quality-gates/gates/runtime-checks.js +265 -0
- package/dist/src/quality-gates/gates/scaffold-defaults.js +108 -0
- package/dist/src/quality-gates/gates/test-coverage.js +240 -0
- package/dist/src/quality-gates/gates/test-file-protection.js +85 -0
- package/dist/src/quality-gates/gates/test-isolation.js +74 -0
- package/dist/src/quality-gates/index.js +17 -0
- package/dist/src/quality-gates/registry.js +205 -0
- package/dist/src/quality-gates/types.js +2 -0
- package/dist/src/quick-fix-mode.js +302 -0
- package/dist/src/recipe-loader.js +122 -0
- package/dist/src/repair-agent.js +396 -0
- package/dist/src/repo-analyzer.js +342 -0
- package/dist/src/report-generator.js +146 -0
- package/dist/src/report-renderer.js +90 -0
- package/dist/src/requirement-filter.js +52 -0
- package/dist/src/sarif-formatter.js +140 -0
- package/dist/src/scheduling/dependency-analyzer.js +178 -0
- package/dist/src/scheduling/work-stealing-queue.js +123 -0
- package/dist/src/secret-redactor.js +90 -0
- package/dist/src/session-executor.js +587 -0
- package/dist/src/session-manager.js +221 -0
- package/dist/src/share/transcript-verification.js +128 -0
- package/dist/src/share-parser.js +477 -0
- package/dist/src/spinner.js +146 -0
- package/dist/src/step-runner.js +365 -0
- package/dist/src/swarm-orchestrator.js +686 -0
- package/dist/src/task-classifier.js +134 -0
- package/dist/src/test-command-discovery.js +123 -0
- package/dist/src/text-similarity.js +24 -0
- package/dist/src/tier-maps.js +158 -0
- package/dist/src/types.js +5 -0
- package/dist/src/url-shortener.js +135 -0
- package/dist/src/verification/attestation.js +166 -0
- package/dist/src/verification/cheat-detector.js +240 -0
- package/dist/src/verification/command-runner.js +70 -0
- package/dist/src/verification/composite-score.js +157 -0
- package/dist/src/verification/cosign-attestation.js +137 -0
- package/dist/src/verification/diff-analysis.js +89 -0
- package/dist/src/verification/differential-gate.js +173 -0
- package/dist/src/verification/index.js +41 -0
- package/dist/src/verification/mutation-gate.js +242 -0
- package/dist/src/verification/property-gate.js +201 -0
- package/dist/src/verification/test-synthesizer.js +243 -0
- package/dist/src/verifier/outcome-checks.js +396 -0
- package/dist/src/verifier/transcript-checks.js +207 -0
- package/dist/src/verifier/verification-reporters.js +148 -0
- package/dist/src/verifier-engine.js +491 -0
- package/dist/src/wave-resizer.js +131 -0
- package/dist/src/wave-scheduler.js +44 -0
- package/dist/src/worktree-manager.js +368 -0
- package/dist/src/worktree-reserved-paths.js +97 -0
- package/package.json +80 -0
- package/plugin/agents/.gitkeep +0 -0
- package/plugin/agents/backend_master.agent.md +72 -0
- package/plugin/agents/devops_pro.agent.md +45 -0
- package/plugin/agents/frontend_expert.agent.md +59 -0
- package/plugin/agents/integrator_finalizer.agent.md +44 -0
- package/plugin/agents/meta_reviewer.agent.md +44 -0
- package/plugin/agents/security_auditor.agent.md +43 -0
- package/plugin/agents/tester_elite.agent.md +43 -0
- package/plugin/hooks/hooks.json +19 -0
- package/plugin/hooks/post-tool-use.json +9 -0
- package/plugin/hooks/pre-tool-use.json +9 -0
- package/plugin/plugin.json +47 -0
- package/plugin/skills/swarm-gates/SKILL.md +53 -0
- package/plugin/skills/swarm-orchestrate/SKILL.md +34 -0
- package/plugin/skills/swarm-verify/SKILL.md +44 -0
- package/templates/README.md +35 -0
- package/templates/cli-tool.json +31 -0
- package/templates/fullstack.json +52 -0
- package/templates/library.json +38 -0
- package/templates/react-app.json +45 -0
- package/templates/recipes/add-api-docs.json +35 -0
- package/templates/recipes/add-auth.json +47 -0
- package/templates/recipes/add-ci.json +39 -0
- package/templates/recipes/add-tests.json +39 -0
- package/templates/recipes/migrate-to-ts.json +42 -0
- package/templates/recipes/refactor-modularize.json +44 -0
- package/templates/recipes/security-audit.json +35 -0
- package/templates/rest-api.json +38 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AdaptiveConcurrencyManager = exports.WaveResizer = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* dynamic wave resizer - splits waves when rate limits hit
|
|
6
|
+
* or merges small waves for efficiency
|
|
7
|
+
*/
|
|
8
|
+
class WaveResizer {
|
|
9
|
+
resizeHistory = [];
|
|
10
|
+
/**
|
|
11
|
+
* split wave into smaller chunks
|
|
12
|
+
*/
|
|
13
|
+
splitWave(wave, chunkSize, reason) {
|
|
14
|
+
const splitWaves = [];
|
|
15
|
+
for (let i = 0; i < wave.length; i += chunkSize) {
|
|
16
|
+
splitWaves.push(wave.slice(i, i + chunkSize));
|
|
17
|
+
}
|
|
18
|
+
const event = {
|
|
19
|
+
originalWave: wave,
|
|
20
|
+
splitWaves,
|
|
21
|
+
reason,
|
|
22
|
+
timestamp: new Date().toISOString()
|
|
23
|
+
};
|
|
24
|
+
this.resizeHistory.push(event);
|
|
25
|
+
return splitWaves;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* merge small waves together if safe
|
|
29
|
+
*/
|
|
30
|
+
mergeWaves(waves, dependencies) {
|
|
31
|
+
const merged = [];
|
|
32
|
+
let currentMerge = [];
|
|
33
|
+
for (const wave of waves) {
|
|
34
|
+
// check if we can safely merge this wave with current
|
|
35
|
+
const canMerge = wave.every(step => {
|
|
36
|
+
const deps = dependencies.get(step) || [];
|
|
37
|
+
return deps.every(dep => currentMerge.includes(dep) === false ||
|
|
38
|
+
merged.flat().includes(dep));
|
|
39
|
+
});
|
|
40
|
+
if (canMerge && currentMerge.length + wave.length <= 5) {
|
|
41
|
+
currentMerge.push(...wave);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
if (currentMerge.length > 0) {
|
|
45
|
+
merged.push(currentMerge);
|
|
46
|
+
}
|
|
47
|
+
currentMerge = [...wave];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (currentMerge.length > 0) {
|
|
51
|
+
merged.push(currentMerge);
|
|
52
|
+
}
|
|
53
|
+
return merged;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* calculate optimal chunk size based on recent failures
|
|
57
|
+
*/
|
|
58
|
+
calculateOptimalChunkSize(waveSize, recentFailures, queueStats) {
|
|
59
|
+
// start conservative after failures
|
|
60
|
+
if (recentFailures >= 3) {
|
|
61
|
+
return 1; // one at a time
|
|
62
|
+
}
|
|
63
|
+
else if (recentFailures >= 1) {
|
|
64
|
+
return Math.max(1, Math.floor(waveSize / 3));
|
|
65
|
+
}
|
|
66
|
+
// normal operation - respect max concurrency
|
|
67
|
+
return Math.min(waveSize, queueStats.maxConcurrency);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* get resize history
|
|
71
|
+
*/
|
|
72
|
+
getResizeHistory() {
|
|
73
|
+
return [...this.resizeHistory];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
exports.WaveResizer = WaveResizer;
|
|
77
|
+
/**
|
|
78
|
+
* adaptive concurrency manager - adjusts limits based on observed behavior
|
|
79
|
+
* Starts at maxConcurrency (aggressive) and only backs off on failures.
|
|
80
|
+
*/
|
|
81
|
+
class AdaptiveConcurrencyManager {
|
|
82
|
+
currentLimit;
|
|
83
|
+
minLimit = 1;
|
|
84
|
+
maxLimit;
|
|
85
|
+
consecutiveSuccesses = 0;
|
|
86
|
+
consecutiveFailures = 0;
|
|
87
|
+
constructor(initialLimit = 3, maxLimit = 10) {
|
|
88
|
+
// Start aggressive: use the configured limit immediately
|
|
89
|
+
this.currentLimit = initialLimit;
|
|
90
|
+
this.maxLimit = maxLimit;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* record successful execution
|
|
94
|
+
*/
|
|
95
|
+
recordSuccess() {
|
|
96
|
+
this.consecutiveSuccesses++;
|
|
97
|
+
this.consecutiveFailures = 0;
|
|
98
|
+
// Recover toward maxLimit after backoff (2 successes to step up)
|
|
99
|
+
if (this.consecutiveSuccesses >= 2 && this.currentLimit < this.maxLimit) {
|
|
100
|
+
this.currentLimit = Math.min(this.currentLimit + 1, this.maxLimit);
|
|
101
|
+
this.consecutiveSuccesses = 0;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* record failure (rate limit or error)
|
|
106
|
+
*/
|
|
107
|
+
recordFailure(reason) {
|
|
108
|
+
this.consecutiveFailures++;
|
|
109
|
+
this.consecutiveSuccesses = 0;
|
|
110
|
+
// aggressive backoff on rate limits
|
|
111
|
+
if (reason === 'rate_limit') {
|
|
112
|
+
this.currentLimit = Math.max(this.minLimit, Math.floor(this.currentLimit / 2));
|
|
113
|
+
}
|
|
114
|
+
else if (this.consecutiveFailures >= 3) {
|
|
115
|
+
this.currentLimit = Math.max(this.minLimit, this.currentLimit - 1);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* get current recommended limit
|
|
120
|
+
*/
|
|
121
|
+
getCurrentLimit() {
|
|
122
|
+
return this.currentLimit;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* force set limit
|
|
126
|
+
*/
|
|
127
|
+
setLimit(limit) {
|
|
128
|
+
this.currentLimit = Math.max(this.minLimit, Math.min(limit, this.maxLimit));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
exports.AdaptiveConcurrencyManager = AdaptiveConcurrencyManager;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildDependencyGraph = buildDependencyGraph;
|
|
4
|
+
exports.identifyExecutionWaves = identifyExecutionWaves;
|
|
5
|
+
/**
|
|
6
|
+
* Build a dependency graph from an execution plan.
|
|
7
|
+
* Returns a map of step number → array of dependency step numbers.
|
|
8
|
+
*/
|
|
9
|
+
function buildDependencyGraph(plan) {
|
|
10
|
+
const graph = new Map();
|
|
11
|
+
plan.steps.forEach(step => {
|
|
12
|
+
graph.set(step.stepNumber, step.dependencies);
|
|
13
|
+
});
|
|
14
|
+
return graph;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Identify waves of parallel execution (topological sort by levels).
|
|
18
|
+
* Each wave contains steps whose dependencies have all been completed
|
|
19
|
+
* in prior waves.
|
|
20
|
+
*/
|
|
21
|
+
function identifyExecutionWaves(graph) {
|
|
22
|
+
const waves = [];
|
|
23
|
+
const completed = new Set();
|
|
24
|
+
const allSteps = Array.from(graph.keys());
|
|
25
|
+
while (completed.size < allSteps.length) {
|
|
26
|
+
const currentWave = [];
|
|
27
|
+
// find steps whose dependencies are all completed
|
|
28
|
+
for (const step of allSteps) {
|
|
29
|
+
if (completed.has(step))
|
|
30
|
+
continue;
|
|
31
|
+
const deps = graph.get(step) || [];
|
|
32
|
+
const allDepsCompleted = deps.every(dep => completed.has(dep));
|
|
33
|
+
if (allDepsCompleted) {
|
|
34
|
+
currentWave.push(step);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (currentWave.length === 0) {
|
|
38
|
+
throw new Error('Circular dependency detected or graph issue');
|
|
39
|
+
}
|
|
40
|
+
waves.push(currentWave);
|
|
41
|
+
currentWave.forEach(step => completed.add(step));
|
|
42
|
+
}
|
|
43
|
+
return waves;
|
|
44
|
+
}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.WorktreeManager = void 0;
|
|
37
|
+
const child_process_1 = require("child_process");
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const defaults_1 = require("./defaults");
|
|
41
|
+
const logger_1 = require("./logger");
|
|
42
|
+
const logger = (0, logger_1.getLogger)('worktree-manager');
|
|
43
|
+
/**
|
|
44
|
+
* Manages git worktrees, branches, and initial repo setup for parallel agent isolation.
|
|
45
|
+
* Extracted from SwarmOrchestrator to keep git operations in a single focused module.
|
|
46
|
+
*/
|
|
47
|
+
class WorktreeManager {
|
|
48
|
+
workingDir;
|
|
49
|
+
constructor(workingDir) {
|
|
50
|
+
this.workingDir = workingDir;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Create an isolated git worktree for a step's agent to work in.
|
|
54
|
+
* When repoDir differs from workingDir, the worktree forks from
|
|
55
|
+
* the target repo's history (e.g. bootstrap targeting an external project).
|
|
56
|
+
*/
|
|
57
|
+
async createAgentWorktree(branchName, fromBranch, runDir, stepNumber, repoDir) {
|
|
58
|
+
let gitDir = repoDir || this.workingDir;
|
|
59
|
+
const worktreePath = path.join(runDir, 'worktrees', `step-${stepNumber}`);
|
|
60
|
+
const worktreesDir = path.dirname(worktreePath);
|
|
61
|
+
if (!fs.existsSync(worktreesDir)) {
|
|
62
|
+
fs.mkdirSync(worktreesDir, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
// Guard: verify the resolved git root matches the intended target directory.
|
|
65
|
+
// Without this, a parent repo (e.g. /home/user/.git) silently captures all
|
|
66
|
+
// git operations and worktrees fork from the wrong history.
|
|
67
|
+
gitDir = this.ensureOwnGitRepo(gitDir);
|
|
68
|
+
// Ensure the target repo has at least one commit (handles freshly-init'd repos)
|
|
69
|
+
try {
|
|
70
|
+
(0, child_process_1.execSync)('git rev-parse HEAD', { cwd: gitDir, stdio: 'pipe' });
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
try {
|
|
74
|
+
(0, child_process_1.execSync)('git add -A', { cwd: gitDir, stdio: 'pipe' });
|
|
75
|
+
(0, child_process_1.execSync)('git commit --allow-empty -m "chore: initialize repository"', { cwd: gitDir, stdio: 'pipe' });
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
try {
|
|
79
|
+
(0, child_process_1.execSync)('git commit --allow-empty -m "chore: initialize repository"', { cwd: gitDir, stdio: 'pipe' });
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Truly cannot commit; worktree add will fail with a clear error
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Resolve fromBranch: the target repo may not have the same branch name.
|
|
87
|
+
// Also handle detached-HEAD repos (SWE-bench checks out a specific commit).
|
|
88
|
+
let resolvedFromBranch = fromBranch;
|
|
89
|
+
try {
|
|
90
|
+
const targetBranch = (0, child_process_1.execSync)('git branch --show-current', { cwd: gitDir, encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
91
|
+
if (targetBranch) {
|
|
92
|
+
resolvedFromBranch = targetBranch;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
// Detached HEAD — use the commit SHA directly as the start point.
|
|
96
|
+
// `git rev-parse --abbrev-ref HEAD` returns literal "HEAD" in detached state,
|
|
97
|
+
// which is valid as a start point, but using the full SHA is more explicit.
|
|
98
|
+
resolvedFromBranch = (0, child_process_1.execSync)('git rev-parse HEAD', { cwd: gitDir, encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// Keep the original fromBranch as best-effort fallback
|
|
103
|
+
}
|
|
104
|
+
// Create the branch without checkout
|
|
105
|
+
try {
|
|
106
|
+
(0, child_process_1.execSync)(`git branch ${branchName} ${resolvedFromBranch}`, { cwd: gitDir, stdio: 'pipe' });
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Branch already exists from a prior retry; safe to reuse
|
|
110
|
+
}
|
|
111
|
+
// Clean up existing worktree if present (e.g. from a retry)
|
|
112
|
+
if (fs.existsSync(worktreePath)) {
|
|
113
|
+
try {
|
|
114
|
+
(0, child_process_1.execSync)(`git worktree remove "${worktreePath}" --force`, { cwd: gitDir, stdio: 'pipe' });
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
try {
|
|
118
|
+
(0, child_process_1.execSync)('git worktree prune', { cwd: gitDir, stdio: 'pipe' });
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// Prune is best-effort cleanup
|
|
122
|
+
}
|
|
123
|
+
fs.rmSync(worktreePath, { recursive: true, force: true });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
const proc = (0, child_process_1.spawn)('git', ['worktree', 'add', worktreePath, branchName], {
|
|
128
|
+
cwd: gitDir
|
|
129
|
+
});
|
|
130
|
+
let stderr = '';
|
|
131
|
+
if (proc.stderr) {
|
|
132
|
+
proc.stderr.on('data', (data) => {
|
|
133
|
+
stderr += data.toString();
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
proc.on('close', (code) => {
|
|
137
|
+
if (code === 0) {
|
|
138
|
+
resolve(worktreePath);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
reject(new Error(`Failed to create worktree for step ${stepNumber}: ${stderr}`));
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Remove a git worktree. Non-throwing: always resolves even on failure
|
|
148
|
+
* since worktree cleanup is best-effort post-merge.
|
|
149
|
+
*/
|
|
150
|
+
async removeAgentWorktree(worktreePath) {
|
|
151
|
+
return new Promise((resolve) => {
|
|
152
|
+
const proc = (0, child_process_1.spawn)('git', ['worktree', 'remove', worktreePath, '--force'], {
|
|
153
|
+
cwd: this.workingDir
|
|
154
|
+
});
|
|
155
|
+
proc.on('close', () => {
|
|
156
|
+
resolve();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Create a new git branch (legacy path; kept for replan retry branches).
|
|
162
|
+
* Stashes uncommitted changes before switching to avoid conflicts from
|
|
163
|
+
* parallel agents.
|
|
164
|
+
*/
|
|
165
|
+
async createAgentBranch(branchName, fromBranch) {
|
|
166
|
+
try {
|
|
167
|
+
(0, child_process_1.execSync)('git stash --include-untracked', { cwd: this.workingDir, stdio: 'pipe' });
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
// Stash fails when working tree is clean; expected during normal execution
|
|
171
|
+
}
|
|
172
|
+
return new Promise((resolve, reject) => {
|
|
173
|
+
const proc = (0, child_process_1.spawn)('git', ['checkout', '-b', branchName, fromBranch], {
|
|
174
|
+
cwd: this.workingDir
|
|
175
|
+
});
|
|
176
|
+
let stderr = '';
|
|
177
|
+
if (proc.stderr) {
|
|
178
|
+
proc.stderr.on('data', (data) => {
|
|
179
|
+
stderr += data.toString();
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
proc.on('close', (code) => {
|
|
183
|
+
if (code === 0) {
|
|
184
|
+
resolve();
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
reject(new Error(`Failed to create branch ${branchName}: ${stderr}`));
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Switch to a git branch with a 15s timeout safety net.
|
|
194
|
+
*/
|
|
195
|
+
async switchBranch(branchName) {
|
|
196
|
+
return new Promise((resolve, reject) => {
|
|
197
|
+
const proc = (0, child_process_1.spawn)('git', ['checkout', branchName], {
|
|
198
|
+
cwd: this.workingDir,
|
|
199
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
200
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }
|
|
201
|
+
});
|
|
202
|
+
const timeout = setTimeout(() => {
|
|
203
|
+
proc.kill('SIGKILL');
|
|
204
|
+
reject(new Error(`Timeout switching to branch ${branchName}`));
|
|
205
|
+
}, defaults_1.DEFAULT_BRANCH_SWITCH_TIMEOUT_MS);
|
|
206
|
+
proc.on('close', (code) => {
|
|
207
|
+
clearTimeout(timeout);
|
|
208
|
+
if (code === 0) {
|
|
209
|
+
resolve();
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
reject(new Error(`Failed to switch to branch ${branchName}`));
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
proc.on('error', (err) => {
|
|
216
|
+
clearTimeout(timeout);
|
|
217
|
+
reject(err);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
getCurrentBranch() {
|
|
222
|
+
const result = (0, child_process_1.execSync)('git branch --show-current', {
|
|
223
|
+
cwd: this.workingDir,
|
|
224
|
+
encoding: 'utf8'
|
|
225
|
+
});
|
|
226
|
+
return result.trim() || 'main';
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Resolve the repo's default branch — the one the orchestrator treats as the
|
|
230
|
+
* integration target for merges and rebases.
|
|
231
|
+
*
|
|
232
|
+
* Resolution order:
|
|
233
|
+
* 1. `git symbolic-ref refs/remotes/origin/HEAD` — the repo's upstream default
|
|
234
|
+
* (returns `refs/remotes/origin/master` or `refs/remotes/origin/main` etc.)
|
|
235
|
+
* 2. `git branch --show-current` — whatever branch is currently checked out
|
|
236
|
+
* 3. The literal "main" as a last resort for brand-new empty repos where
|
|
237
|
+
* neither of the above resolves.
|
|
238
|
+
*
|
|
239
|
+
* This is what replaced the old `getCurrentBranch() || 'main'` shortcut. The
|
|
240
|
+
* shortcut broke on any repo whose default isn't `main` (sympy, scikit-learn,
|
|
241
|
+
* numpy pre-2022, astropy pre-2022, and anything else that predates the
|
|
242
|
+
* main-rename wave) and on detached-HEAD checkouts like SWE-bench's, where
|
|
243
|
+
* `git branch --show-current` returns empty.
|
|
244
|
+
*/
|
|
245
|
+
resolveDefaultBranch() {
|
|
246
|
+
try {
|
|
247
|
+
const head = (0, child_process_1.execSync)('git symbolic-ref refs/remotes/origin/HEAD', {
|
|
248
|
+
cwd: this.workingDir,
|
|
249
|
+
encoding: 'utf8',
|
|
250
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
251
|
+
}).trim();
|
|
252
|
+
// format: refs/remotes/origin/<branch>
|
|
253
|
+
const prefix = 'refs/remotes/origin/';
|
|
254
|
+
if (head.startsWith(prefix)) {
|
|
255
|
+
return head.slice(prefix.length);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
// origin/HEAD not set — repo has no remote or never fetched it; fall through
|
|
260
|
+
}
|
|
261
|
+
try {
|
|
262
|
+
const current = (0, child_process_1.execSync)('git branch --show-current', {
|
|
263
|
+
cwd: this.workingDir,
|
|
264
|
+
encoding: 'utf8',
|
|
265
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
266
|
+
}).trim();
|
|
267
|
+
if (current)
|
|
268
|
+
return current;
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
// current-branch lookup failed; fall through to default
|
|
272
|
+
}
|
|
273
|
+
return 'main';
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Ensure repo has at least one commit (required for branch creation).
|
|
277
|
+
* Creates an initial commit with a .gitignore if repo is empty.
|
|
278
|
+
* Also verifies the working directory is its own git root first.
|
|
279
|
+
*/
|
|
280
|
+
ensureInitialCommit() {
|
|
281
|
+
// Validate git root before any operations; prevents silently
|
|
282
|
+
// committing into a parent repo like /home/user/.git
|
|
283
|
+
this.ensureOwnGitRepo(this.workingDir);
|
|
284
|
+
try {
|
|
285
|
+
(0, child_process_1.execSync)('git rev-parse HEAD', {
|
|
286
|
+
cwd: this.workingDir,
|
|
287
|
+
encoding: 'utf8',
|
|
288
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
289
|
+
});
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
// rev-parse fails when repo has no commits; fall through to create one
|
|
294
|
+
}
|
|
295
|
+
logger.info(' \u{1F4DD} Empty repo detected, creating initial commit...');
|
|
296
|
+
const gitignorePath = path.join(this.workingDir, '.gitignore');
|
|
297
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
298
|
+
fs.writeFileSync(gitignorePath, `# Swarm orchestrator artifacts
|
|
299
|
+
plans/
|
|
300
|
+
runs/
|
|
301
|
+
proof/
|
|
302
|
+
.quickfix/
|
|
303
|
+
.context/
|
|
304
|
+
.locks/
|
|
305
|
+
node_modules/
|
|
306
|
+
`);
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
309
|
+
(0, child_process_1.execSync)('git add .gitignore', { cwd: this.workingDir, stdio: 'pipe' });
|
|
310
|
+
(0, child_process_1.execSync)('git commit -m "chore: initialize repository"', { cwd: this.workingDir, stdio: 'pipe' });
|
|
311
|
+
logger.info(' \u2705 Initial commit created');
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
try {
|
|
315
|
+
(0, child_process_1.execSync)('git add -A', { cwd: this.workingDir, stdio: 'pipe' });
|
|
316
|
+
(0, child_process_1.execSync)('git commit --allow-empty -m "chore: initialize repository"', { cwd: this.workingDir, stdio: 'pipe' });
|
|
317
|
+
logger.info(' \u2705 Initial commit created (empty)');
|
|
318
|
+
}
|
|
319
|
+
catch (innerErr) {
|
|
320
|
+
const msg = innerErr instanceof Error ? innerErr.message : String(innerErr);
|
|
321
|
+
logger.warn(`[init] Could not create initial commit: ${msg}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Verify that targetDir is its own git repository root, not a subdirectory
|
|
327
|
+
* of a parent repo. If git resolves to a different root (e.g. /home/user),
|
|
328
|
+
* initialize a dedicated repo at targetDir so worktrees fork from the
|
|
329
|
+
* correct history instead of the parent's.
|
|
330
|
+
*/
|
|
331
|
+
ensureOwnGitRepo(targetDir) {
|
|
332
|
+
const resolved = path.resolve(targetDir);
|
|
333
|
+
try {
|
|
334
|
+
const gitRoot = (0, child_process_1.execSync)('git rev-parse --show-toplevel', {
|
|
335
|
+
cwd: resolved, encoding: 'utf8', stdio: 'pipe'
|
|
336
|
+
}).trim();
|
|
337
|
+
if (path.resolve(gitRoot) === resolved) {
|
|
338
|
+
return resolved;
|
|
339
|
+
}
|
|
340
|
+
// Git resolved to a parent directory; targetDir is not its own repo.
|
|
341
|
+
// Initialize a fresh repo so worktrees use the right history.
|
|
342
|
+
logger.info(` ⚠️ ${resolved} is inside parent repo ${gitRoot}; initializing dedicated git repo`);
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
// No git repo found at all; initialize one
|
|
346
|
+
logger.info(` 📂 No git repo at ${resolved}; initializing`);
|
|
347
|
+
}
|
|
348
|
+
(0, child_process_1.execSync)('git init', { cwd: resolved, stdio: 'pipe' });
|
|
349
|
+
(0, child_process_1.execSync)('git config user.email "swarm-orchestrator@local"', { cwd: resolved, stdio: 'pipe' });
|
|
350
|
+
(0, child_process_1.execSync)('git config user.name "Swarm Orchestrator"', { cwd: resolved, stdio: 'pipe' });
|
|
351
|
+
(0, child_process_1.execSync)('git add -A', { cwd: resolved, stdio: 'pipe' });
|
|
352
|
+
try {
|
|
353
|
+
(0, child_process_1.execSync)('git commit -m "initialize project for swarm orchestration"', {
|
|
354
|
+
cwd: resolved, stdio: 'pipe',
|
|
355
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
(0, child_process_1.execSync)('git commit --allow-empty -m "initialize project for swarm orchestration"', {
|
|
360
|
+
cwd: resolved, stdio: 'pipe',
|
|
361
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
return resolved;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
exports.WorktreeManager = WorktreeManager;
|
|
368
|
+
exports.default = WorktreeManager;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Reserved paths for per-step commit-level exclusion (issue #27 / PR 2).
|
|
4
|
+
*
|
|
5
|
+
* Mirrors benchmarks/swe-bench/evaluation-scripts/worktree_reserved_paths.py
|
|
6
|
+
* one-for-one so that commit-time exclusion (TS side) stays in parity with
|
|
7
|
+
* capture-time exclusion (Python side). If you update one, update the other.
|
|
8
|
+
*
|
|
9
|
+
* Three categories tracked separately so audit decisions stay legible:
|
|
10
|
+
*
|
|
11
|
+
* ORCHESTRATOR_RESERVED_PATHS — paths this codebase writes to during normal
|
|
12
|
+
* operation. Scraped from src/**\/*.ts. Update when a new code path starts
|
|
13
|
+
* writing to a new top-level directory inside the worktree.
|
|
14
|
+
*
|
|
15
|
+
* BUILD_ARTIFACT_RESERVED_PATHS — paths universal build tooling writes
|
|
16
|
+
* (package managers, compilers, test runners, venv tools). Not
|
|
17
|
+
* orchestrator-specific; reasonable to exclude from any commit regardless.
|
|
18
|
+
*
|
|
19
|
+
* FILE_GLOB_EXCLUDES — pure file-glob patterns (not directory prefixes).
|
|
20
|
+
* Appended separately from the directory dual-form expansion below.
|
|
21
|
+
*
|
|
22
|
+
* The consumer is gitPathspecExcludes(), which is called at the per-step
|
|
23
|
+
* commit site in swarm-orchestrator.ts. See the parity test in
|
|
24
|
+
* test/worktree-reserved-paths.test.ts for the cross-language invariant.
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.FILE_GLOB_EXCLUDES = exports.BUILD_ARTIFACT_RESERVED_PATHS = exports.ORCHESTRATOR_RESERVED_PATHS = void 0;
|
|
28
|
+
exports.gitPathspecExcludes = gitPathspecExcludes;
|
|
29
|
+
exports.ORCHESTRATOR_RESERVED_PATHS = [
|
|
30
|
+
// runDir tree — holds everything orchestrator-scoped:
|
|
31
|
+
// .context/shared-context.json (per-step manifest)
|
|
32
|
+
// .locks/ (git serialization locks)
|
|
33
|
+
// worktrees/step-N/ (per-step isolated worktrees)
|
|
34
|
+
// steps/step-N/share.md (transcripts)
|
|
35
|
+
// session-state.json, metrics.json, cost-attribution.json
|
|
36
|
+
// quality-gates/ (gate output)
|
|
37
|
+
// Excluding "runs" excludes every nested path; no need to list sub-dirs.
|
|
38
|
+
'runs',
|
|
39
|
+
// quick-fix-mode session scratch (src/quick-fix-mode.ts)
|
|
40
|
+
'.quickfix',
|
|
41
|
+
// CLI plan commands write plan-*.json here
|
|
42
|
+
// (src/plan-files.ts)
|
|
43
|
+
'plans',
|
|
44
|
+
];
|
|
45
|
+
exports.BUILD_ARTIFACT_RESERVED_PATHS = [
|
|
46
|
+
// Node
|
|
47
|
+
'node_modules',
|
|
48
|
+
// Python
|
|
49
|
+
'__pycache__',
|
|
50
|
+
'.venv',
|
|
51
|
+
'venv',
|
|
52
|
+
'env',
|
|
53
|
+
// JS build tools
|
|
54
|
+
'.next',
|
|
55
|
+
'.turbo',
|
|
56
|
+
'.cache',
|
|
57
|
+
// Generic build output
|
|
58
|
+
'dist',
|
|
59
|
+
'build',
|
|
60
|
+
// Test coverage
|
|
61
|
+
'coverage',
|
|
62
|
+
];
|
|
63
|
+
exports.FILE_GLOB_EXCLUDES = [
|
|
64
|
+
'**/*.pyc',
|
|
65
|
+
'**/*.pyo',
|
|
66
|
+
'**/*.egg-info',
|
|
67
|
+
'**/*.egg-info/**',
|
|
68
|
+
// .copilot-instructions.md — written to the repo root by prompt-builder.ts
|
|
69
|
+
// and committed before step-1 runs. Orchestrator scaffolding, not agent work.
|
|
70
|
+
// Must be excluded from SWE-bench patches or `git apply` fails in /testbed.
|
|
71
|
+
// Mirrors Python _FILE_GLOB_EXCLUDES entry added in smoke8 post-mortem fix.
|
|
72
|
+
'.copilot-instructions.md',
|
|
73
|
+
];
|
|
74
|
+
/**
|
|
75
|
+
* Return git pathspec :(exclude) directives covering every reserved path.
|
|
76
|
+
*
|
|
77
|
+
* For each directory entry, emits both the top-level form (`:(exclude)runs`)
|
|
78
|
+
* and a tree glob (`:(exclude)runs/**`). Git requires both when the exclude
|
|
79
|
+
* should match the directory itself AND everything under it; the top-level
|
|
80
|
+
* form alone can miss deeply nested files on some git versions.
|
|
81
|
+
*
|
|
82
|
+
* Emission count: 14 directories × 2 (dual-form) + 4 file globs = 32 args.
|
|
83
|
+
* Matches Python's git_pathspec_excludes() output one-for-one.
|
|
84
|
+
*
|
|
85
|
+
* Consumers pass this list as trailing arguments to `git add` after a
|
|
86
|
+
* positive pathspec (typically `--`). Note: git add for commits does NOT
|
|
87
|
+
* need the `.` positive pathspec that capture uses; `git add -A` already
|
|
88
|
+
* stages the whole tree and the excludes suppress the reserved paths.
|
|
89
|
+
*/
|
|
90
|
+
function gitPathspecExcludes() {
|
|
91
|
+
const directoryExcludes = [
|
|
92
|
+
...exports.ORCHESTRATOR_RESERVED_PATHS,
|
|
93
|
+
...exports.BUILD_ARTIFACT_RESERVED_PATHS,
|
|
94
|
+
].flatMap(dir => [`:(exclude)${dir}`, `:(exclude)${dir}/**`]);
|
|
95
|
+
const globExcludes = exports.FILE_GLOB_EXCLUDES.map(g => `:(exclude)${g}`);
|
|
96
|
+
return [...directoryExcludes, ...globExcludes];
|
|
97
|
+
}
|