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,339 @@
|
|
|
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.ConfigLoader = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const yaml = __importStar(require("js-yaml"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const logger_1 = require("./logger");
|
|
41
|
+
const logger = (0, logger_1.getLogger)('config-loader');
|
|
42
|
+
class ConfigLoader {
|
|
43
|
+
configDir;
|
|
44
|
+
customAgentsDir;
|
|
45
|
+
constructor(configDir) {
|
|
46
|
+
const cwd = process.cwd();
|
|
47
|
+
const packageRoot = this.findPackageRoot(__dirname);
|
|
48
|
+
this.configDir = configDir || this.resolveConfigDir(cwd, packageRoot);
|
|
49
|
+
this.customAgentsDir = this.resolveCustomAgentsDir(cwd, packageRoot);
|
|
50
|
+
logger.debug(`agent config dir: ${this.configDir}`);
|
|
51
|
+
logger.debug(`custom agents dir: ${this.customAgentsDir}`);
|
|
52
|
+
}
|
|
53
|
+
findPackageRoot(startDir) {
|
|
54
|
+
// walk upwards until we find a package.json
|
|
55
|
+
let current = startDir;
|
|
56
|
+
for (let i = 0; i < 8; i++) {
|
|
57
|
+
const candidate = path.join(current, 'package.json');
|
|
58
|
+
if (fs.existsSync(candidate)) {
|
|
59
|
+
return current;
|
|
60
|
+
}
|
|
61
|
+
const parent = path.dirname(current);
|
|
62
|
+
if (parent === current) {
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
current = parent;
|
|
66
|
+
}
|
|
67
|
+
// fallback: best effort
|
|
68
|
+
return startDir;
|
|
69
|
+
}
|
|
70
|
+
resolveConfigDir(cwd, packageRoot) {
|
|
71
|
+
const cwdConfig = path.join(cwd, 'config');
|
|
72
|
+
if (fs.existsSync(path.join(cwdConfig, 'default-agents.yaml'))) {
|
|
73
|
+
return cwdConfig;
|
|
74
|
+
}
|
|
75
|
+
const pkgConfig = path.join(packageRoot, 'config');
|
|
76
|
+
if (fs.existsSync(path.join(pkgConfig, 'default-agents.yaml'))) {
|
|
77
|
+
return pkgConfig;
|
|
78
|
+
}
|
|
79
|
+
return cwdConfig;
|
|
80
|
+
}
|
|
81
|
+
resolveCustomAgentsDir(cwd, packageRoot) {
|
|
82
|
+
const cwdAgents = path.join(cwd, '.github', 'agents');
|
|
83
|
+
if (fs.existsSync(cwdAgents)) {
|
|
84
|
+
return cwdAgents;
|
|
85
|
+
}
|
|
86
|
+
const pkgAgents = path.join(packageRoot, '.github', 'agents');
|
|
87
|
+
if (fs.existsSync(pkgAgents)) {
|
|
88
|
+
return pkgAgents;
|
|
89
|
+
}
|
|
90
|
+
return cwdAgents;
|
|
91
|
+
}
|
|
92
|
+
loadDefaultAgents() {
|
|
93
|
+
const filePath = path.join(this.configDir, 'default-agents.yaml');
|
|
94
|
+
return this.loadAgentFile(filePath);
|
|
95
|
+
}
|
|
96
|
+
loadUserAgents() {
|
|
97
|
+
const filePath = path.join(this.configDir, 'user-agents.yaml');
|
|
98
|
+
return this.loadAgentFile(filePath);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Load all agents: custom .agent.md files + YAML configs
|
|
102
|
+
* Custom agents take precedence over YAML
|
|
103
|
+
*/
|
|
104
|
+
loadAllAgents() {
|
|
105
|
+
const agentMap = new Map();
|
|
106
|
+
// Load YAML configs first (legacy/fallback)
|
|
107
|
+
try {
|
|
108
|
+
const defaultAgents = this.loadDefaultAgents();
|
|
109
|
+
logger.debug(`loaded default agents from ${path.join(this.configDir, 'default-agents.yaml')}`);
|
|
110
|
+
defaultAgents.agents.forEach(agent => {
|
|
111
|
+
agentMap.set(agent.name, agent);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
const err = error;
|
|
116
|
+
logger.warn(`Failed to load default agents: ${err.message}`);
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const userAgents = this.loadUserAgents();
|
|
120
|
+
logger.debug(`loaded user agents from ${path.join(this.configDir, 'user-agents.yaml')}`);
|
|
121
|
+
userAgents.agents.forEach(agent => {
|
|
122
|
+
agentMap.set(agent.name, agent);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// User agents YAML (config/user-agents.yaml) is optional; missing file is normal
|
|
127
|
+
}
|
|
128
|
+
// Load custom .agent.md files (override YAML if name matches)
|
|
129
|
+
const customAgents = this.loadCustomAgents();
|
|
130
|
+
customAgents.forEach(agent => {
|
|
131
|
+
agentMap.set(agent.name, agent);
|
|
132
|
+
});
|
|
133
|
+
return Array.from(agentMap.values());
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Load custom agents from .github/agents/*.agent.md
|
|
137
|
+
*/
|
|
138
|
+
loadCustomAgents() {
|
|
139
|
+
if (!fs.existsSync(this.customAgentsDir)) {
|
|
140
|
+
logger.debug(`no custom agents directory at ${this.customAgentsDir}`);
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
const agentFiles = fs.readdirSync(this.customAgentsDir)
|
|
144
|
+
.filter(file => file.endsWith('.agent.md'));
|
|
145
|
+
const agents = [];
|
|
146
|
+
for (const file of agentFiles) {
|
|
147
|
+
try {
|
|
148
|
+
const agentPath = path.join(this.customAgentsDir, file);
|
|
149
|
+
const agent = this.parseCustomAgentFile(agentPath);
|
|
150
|
+
if (agent) {
|
|
151
|
+
agents.push(agent);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
const err = error;
|
|
156
|
+
logger.warn(`Failed to load custom agent ${file}: ${err.message}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return agents;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Parse a .agent.md custom agent file
|
|
163
|
+
*/
|
|
164
|
+
parseCustomAgentFile(filePath) {
|
|
165
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
166
|
+
// Extract frontmatter (YAML between --- markers)
|
|
167
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
168
|
+
if (!frontmatterMatch || !frontmatterMatch[1]) {
|
|
169
|
+
logger.warn(`No frontmatter found in ${filePath}`);
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
const frontmatter = yaml.load(frontmatterMatch[1]);
|
|
173
|
+
// Extract markdown content after frontmatter
|
|
174
|
+
const markdownContent = content.substring(frontmatterMatch[0].length).trim();
|
|
175
|
+
// Parse sections from markdown
|
|
176
|
+
const scope = this.extractMarkdownSection(markdownContent, 'Scope');
|
|
177
|
+
const boundaries = this.extractMarkdownSection(markdownContent, 'Boundaries');
|
|
178
|
+
const doneDefinition = this.extractMarkdownSection(markdownContent, 'Done Definition');
|
|
179
|
+
const refusalRules = this.extractMarkdownSection(markdownContent, 'Refusal Rules');
|
|
180
|
+
// Convert custom agent to AgentProfile format
|
|
181
|
+
const agent = {
|
|
182
|
+
name: frontmatter.name,
|
|
183
|
+
purpose: frontmatter.description,
|
|
184
|
+
scope: scope.length > 0 ? scope : ['See .agent.md file for details'],
|
|
185
|
+
boundaries: boundaries.length > 0 ? boundaries : ['See .agent.md file for details'],
|
|
186
|
+
done_definition: doneDefinition.length > 0 ? doneDefinition : ['See .agent.md file for details'],
|
|
187
|
+
refusal_rules: refusalRules.length > 0 ? refusalRules : ['Follow agent guidelines'],
|
|
188
|
+
output_contract: {
|
|
189
|
+
transcript: `proof/step-{N}-${frontmatter.name.replace(/_/g, '-')}.md`,
|
|
190
|
+
artifacts: []
|
|
191
|
+
},
|
|
192
|
+
customAgentPath: filePath,
|
|
193
|
+
description: frontmatter.description,
|
|
194
|
+
...(frontmatter.tools && { tools: frontmatter.tools }),
|
|
195
|
+
...(frontmatter.metadata && { metadata: frontmatter.metadata })
|
|
196
|
+
};
|
|
197
|
+
return agent;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Extract bullet point list from markdown section
|
|
201
|
+
*/
|
|
202
|
+
extractMarkdownSection(markdown, sectionHeader) {
|
|
203
|
+
const items = [];
|
|
204
|
+
// Find section header (## Scope or ## Boundaries, etc.)
|
|
205
|
+
const headerRegex = new RegExp(`##\\s+${sectionHeader}[^\\n]*\\n`, 'i');
|
|
206
|
+
const match = markdown.match(headerRegex);
|
|
207
|
+
if (!match || match.index === undefined) {
|
|
208
|
+
return items;
|
|
209
|
+
}
|
|
210
|
+
// Extract content until next ## header or end
|
|
211
|
+
const startIndex = match.index + match[0].length;
|
|
212
|
+
const remainingContent = markdown.substring(startIndex);
|
|
213
|
+
const nextHeaderMatch = remainingContent.match(/\n##\s+/);
|
|
214
|
+
const sectionContent = nextHeaderMatch
|
|
215
|
+
? remainingContent.substring(0, nextHeaderMatch.index)
|
|
216
|
+
: remainingContent;
|
|
217
|
+
// Extract bullet points (lines starting with - or *)
|
|
218
|
+
const lines = sectionContent.split('\n');
|
|
219
|
+
for (const line of lines) {
|
|
220
|
+
const bulletMatch = line.match(/^[-*]\s+(.+)$/);
|
|
221
|
+
if (bulletMatch && bulletMatch[1]) {
|
|
222
|
+
items.push(bulletMatch[1].trim());
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return items;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Get agent name for --agent flag (converts names to match .agent.md naming)
|
|
229
|
+
*/
|
|
230
|
+
getAgentCLIName(agentName) {
|
|
231
|
+
// If agent has customAgentPath, use the name from frontmatter
|
|
232
|
+
const agent = this.getAgentByName(agentName);
|
|
233
|
+
if (agent?.customAgentPath) {
|
|
234
|
+
return agent.name; // already in correct format from frontmatter
|
|
235
|
+
}
|
|
236
|
+
// Otherwise normalize to lowercase (e.g. 'Worker' -> 'worker')
|
|
237
|
+
return agentName
|
|
238
|
+
.replace(/([A-Z])/g, '_$1')
|
|
239
|
+
.toLowerCase()
|
|
240
|
+
.replace(/^_/, '');
|
|
241
|
+
}
|
|
242
|
+
loadAgentFile(filePath) {
|
|
243
|
+
if (!fs.existsSync(filePath)) {
|
|
244
|
+
throw new Error(`Config file not found: ${filePath}`);
|
|
245
|
+
}
|
|
246
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
247
|
+
const config = yaml.load(content);
|
|
248
|
+
this.validateConfig(config, filePath);
|
|
249
|
+
return config;
|
|
250
|
+
}
|
|
251
|
+
validateConfig(config, filePath) {
|
|
252
|
+
if (!config || typeof config !== 'object') {
|
|
253
|
+
throw new Error(`Invalid config format in ${filePath}`);
|
|
254
|
+
}
|
|
255
|
+
if (!Array.isArray(config.agents)) {
|
|
256
|
+
throw new Error(`Missing 'agents' array in ${filePath}`);
|
|
257
|
+
}
|
|
258
|
+
config.agents.forEach((agent, index) => {
|
|
259
|
+
this.validateAgent(agent, `${filePath}[${index}]`);
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
validateAgent(agent, context) {
|
|
263
|
+
const requiredFields = [
|
|
264
|
+
'name',
|
|
265
|
+
'purpose',
|
|
266
|
+
'scope',
|
|
267
|
+
'boundaries',
|
|
268
|
+
'done_definition',
|
|
269
|
+
'output_contract',
|
|
270
|
+
'refusal_rules'
|
|
271
|
+
];
|
|
272
|
+
for (const field of requiredFields) {
|
|
273
|
+
if (!(field in agent)) {
|
|
274
|
+
throw new Error(`Missing required field '${field}' in agent at ${context}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// Validate name is non-empty string
|
|
278
|
+
if (typeof agent.name !== 'string' || agent.name.trim() === '') {
|
|
279
|
+
throw new Error(`Agent name must be non-empty string at ${context}`);
|
|
280
|
+
}
|
|
281
|
+
// Validate purpose is non-empty string
|
|
282
|
+
if (typeof agent.purpose !== 'string' || agent.purpose.trim() === '') {
|
|
283
|
+
throw new Error(`Agent purpose must be non-empty string at ${context}`);
|
|
284
|
+
}
|
|
285
|
+
// Validate arrays
|
|
286
|
+
const arrayFields = ['scope', 'boundaries', 'done_definition', 'refusal_rules'];
|
|
287
|
+
for (const field of arrayFields) {
|
|
288
|
+
if (!Array.isArray(agent[field])) {
|
|
289
|
+
throw new Error(`Agent field '${field}' must be an array at ${context}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// Validate output_contract
|
|
293
|
+
if (!agent.output_contract || typeof agent.output_contract !== 'object') {
|
|
294
|
+
throw new Error(`Agent output_contract must be an object at ${context}`);
|
|
295
|
+
}
|
|
296
|
+
if (typeof agent.output_contract.transcript !== 'string') {
|
|
297
|
+
throw new Error(`Agent output_contract.transcript must be a string at ${context}`);
|
|
298
|
+
}
|
|
299
|
+
if (!Array.isArray(agent.output_contract.artifacts)) {
|
|
300
|
+
throw new Error(`Agent output_contract.artifacts must be an array at ${context}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
getAgentByName(name) {
|
|
304
|
+
const allAgents = this.loadAllAgents();
|
|
305
|
+
// Exact match first, then normalized match (handles PascalCase vs snake_case)
|
|
306
|
+
return allAgents.find(agent => agent.name === name)
|
|
307
|
+
|| allAgents.find(agent => ConfigLoader.normalizeAgentName(agent.name) === ConfigLoader.normalizeAgentName(name));
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Build an agent map keyed by name, with lowercase aliases for mixed-case names.
|
|
311
|
+
* Plans use lowercase ('worker', 'reviewer'); YAML may use mixed case.
|
|
312
|
+
* Both must resolve to the same agent profile.
|
|
313
|
+
*/
|
|
314
|
+
buildAgentMap() {
|
|
315
|
+
const agents = this.loadAllAgents();
|
|
316
|
+
const map = new Map();
|
|
317
|
+
for (const agent of agents) {
|
|
318
|
+
map.set(agent.name, agent);
|
|
319
|
+
const snakeName = ConfigLoader.normalizeAgentName(agent.name);
|
|
320
|
+
if (snakeName !== agent.name && !map.has(snakeName)) {
|
|
321
|
+
map.set(snakeName, agent);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return map;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Normalize agent name for consistent lookups.
|
|
328
|
+
* 'Worker' -> 'worker', 'worker' -> 'worker'
|
|
329
|
+
*/
|
|
330
|
+
static normalizeAgentName(name) {
|
|
331
|
+
return name
|
|
332
|
+
.replace(/([A-Z])/g, '_$1')
|
|
333
|
+
.toLowerCase()
|
|
334
|
+
.replace(/^_/, '')
|
|
335
|
+
.replace(/__+/g, '_');
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
exports.ConfigLoader = ConfigLoader;
|
|
339
|
+
exports.default = ConfigLoader;
|
|
@@ -0,0 +1,358 @@
|
|
|
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.ContextBroker = void 0;
|
|
37
|
+
const crypto_1 = require("crypto");
|
|
38
|
+
const events_1 = require("events");
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const defaults_1 = require("./defaults");
|
|
42
|
+
const logger_1 = require("./logger");
|
|
43
|
+
const logger = (0, logger_1.getLogger)('context-broker');
|
|
44
|
+
/**
|
|
45
|
+
* Context Broker - manages shared state and git locking for parallel agents
|
|
46
|
+
* Uses EventEmitter for zero-latency dependency notification instead of file polling
|
|
47
|
+
*/
|
|
48
|
+
class ContextBroker extends events_1.EventEmitter {
|
|
49
|
+
contextDir;
|
|
50
|
+
lockDir;
|
|
51
|
+
contextFile;
|
|
52
|
+
maxLockWaitMs = defaults_1.DEFAULT_CONTEXT_LOCK_WAIT_MS;
|
|
53
|
+
constructor(runDir) {
|
|
54
|
+
super();
|
|
55
|
+
// Plans with many steps create concurrent per-step listeners for dependency tracking
|
|
56
|
+
this.setMaxListeners(50);
|
|
57
|
+
this.contextDir = path.join(runDir, '.context');
|
|
58
|
+
this.lockDir = path.join(runDir, '.locks');
|
|
59
|
+
this.contextFile = path.join(this.contextDir, 'shared-context.json');
|
|
60
|
+
this.ensureDirectories();
|
|
61
|
+
}
|
|
62
|
+
ensureDirectories() {
|
|
63
|
+
if (!fs.existsSync(this.contextDir)) {
|
|
64
|
+
fs.mkdirSync(this.contextDir, { recursive: true });
|
|
65
|
+
}
|
|
66
|
+
if (!fs.existsSync(this.lockDir)) {
|
|
67
|
+
fs.mkdirSync(this.lockDir, { recursive: true });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Acquire a git lock for safe concurrent operations
|
|
72
|
+
* Returns lock ID if successful, null if timeout
|
|
73
|
+
*/
|
|
74
|
+
async acquireGitLock(agentName, operation) {
|
|
75
|
+
const lockId = (0, crypto_1.randomBytes)(16).toString('hex');
|
|
76
|
+
const lockFile = path.join(this.lockDir, 'git.lock');
|
|
77
|
+
const lock = {
|
|
78
|
+
lockId,
|
|
79
|
+
agentName,
|
|
80
|
+
operation,
|
|
81
|
+
acquiredAt: new Date().toISOString()
|
|
82
|
+
};
|
|
83
|
+
const startTime = Date.now();
|
|
84
|
+
// spin-wait for lock with timeout
|
|
85
|
+
while (Date.now() - startTime < this.maxLockWaitMs) {
|
|
86
|
+
// Re-ensure lock directory exists (may have been removed by rollback/cleanup)
|
|
87
|
+
if (!fs.existsSync(this.lockDir)) {
|
|
88
|
+
fs.mkdirSync(this.lockDir, { recursive: true });
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
// atomic file creation (fails if exists)
|
|
92
|
+
fs.writeFileSync(lockFile, JSON.stringify(lock, null, 2), { flag: 'wx' });
|
|
93
|
+
return lockId;
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
const error = err;
|
|
97
|
+
if (error.code === 'ENOENT') {
|
|
98
|
+
// Directory was removed between check and write, retry
|
|
99
|
+
fs.mkdirSync(this.lockDir, { recursive: true });
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (error.code === 'EEXIST') {
|
|
103
|
+
// lock exists, check if stale (> 5 minutes old)
|
|
104
|
+
try {
|
|
105
|
+
const existingLock = JSON.parse(fs.readFileSync(lockFile, 'utf8'));
|
|
106
|
+
const lockAge = Date.now() - new Date(existingLock.acquiredAt).getTime();
|
|
107
|
+
if (lockAge > defaults_1.DEFAULT_CONTEXT_STALE_LOCK_MS) {
|
|
108
|
+
// stale lock, force remove
|
|
109
|
+
fs.unlinkSync(lockFile);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Lock file contains invalid JSON (corrupted write); safe to remove and retry
|
|
115
|
+
fs.unlinkSync(lockFile);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
// wait and retry
|
|
119
|
+
await this.sleep(100);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return null; // timeout
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Release a git lock
|
|
129
|
+
*/
|
|
130
|
+
releaseGitLock(lockId) {
|
|
131
|
+
const lockFile = path.join(this.lockDir, 'git.lock');
|
|
132
|
+
try {
|
|
133
|
+
const lock = JSON.parse(fs.readFileSync(lockFile, 'utf8'));
|
|
134
|
+
if (lock.lockId === lockId) {
|
|
135
|
+
fs.unlinkSync(lockFile);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
logger.warn(`Lock mismatch: expected ${lockId}, got ${lock.lockId}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
const error = err;
|
|
143
|
+
if (error.code !== 'ENOENT') {
|
|
144
|
+
logger.error('Failed to release lock:', error);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Force-remove all lock files regardless of age.
|
|
150
|
+
* Use before critical phases (e.g. final merge) to clear stale locks
|
|
151
|
+
* from cancelled runs.
|
|
152
|
+
*/
|
|
153
|
+
forceReleaseStaleLocks() {
|
|
154
|
+
// Re-ensure lock directory exists (may have been removed by rollback/cleanup)
|
|
155
|
+
if (!fs.existsSync(this.lockDir)) {
|
|
156
|
+
fs.mkdirSync(this.lockDir, { recursive: true });
|
|
157
|
+
}
|
|
158
|
+
const lockFile = path.join(this.lockDir, 'git.lock');
|
|
159
|
+
try {
|
|
160
|
+
if (fs.existsSync(lockFile)) {
|
|
161
|
+
fs.unlinkSync(lockFile);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// Lock file may have been removed by a concurrent release; harmless
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Add context from a completed step.
|
|
170
|
+
* Uses an exclusive file lock around the read-modify-write to prevent
|
|
171
|
+
* concurrent writers from losing entries. In single-process Node.js the
|
|
172
|
+
* synchronous I/O already serializes, but the lock protects against
|
|
173
|
+
* future multi-process architectures.
|
|
174
|
+
*/
|
|
175
|
+
addStepContext(entry) {
|
|
176
|
+
this.ensureDirectories();
|
|
177
|
+
const lockFile = path.join(this.lockDir, 'context.lock');
|
|
178
|
+
this.acquireFileLockSync(lockFile);
|
|
179
|
+
try {
|
|
180
|
+
const contexts = this.getAllContext();
|
|
181
|
+
contexts.push(entry);
|
|
182
|
+
fs.writeFileSync(this.contextFile, JSON.stringify(contexts, null, 2), 'utf8');
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
this.releaseFileLockSync(lockFile);
|
|
186
|
+
}
|
|
187
|
+
this.emit('step-completed', entry.stepNumber);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Synchronous exclusive file lock via atomic wx creation.
|
|
191
|
+
* Stale locks (>5s) are force-removed on retry.
|
|
192
|
+
*/
|
|
193
|
+
acquireFileLockSync(lockFile) {
|
|
194
|
+
const maxAttempts = 50;
|
|
195
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
196
|
+
try {
|
|
197
|
+
fs.writeFileSync(lockFile, JSON.stringify({ pid: process.pid, ts: Date.now() }), { flag: 'wx' });
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
const error = err;
|
|
202
|
+
if (error.code === 'EEXIST') {
|
|
203
|
+
try {
|
|
204
|
+
const stat = fs.statSync(lockFile);
|
|
205
|
+
if (Date.now() - stat.mtimeMs > defaults_1.DEFAULT_SIGKILL_DELAY_MS) {
|
|
206
|
+
fs.unlinkSync(lockFile);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// Lock removed between check and stat; retry
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
// Single-process Node.js: synchronous ops cannot interleave,
|
|
215
|
+
// so this continue only fires in multi-process scenarios
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (error.code === 'ENOENT') {
|
|
219
|
+
this.ensureDirectories();
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
throw new Error(`Failed to acquire context lock after ${maxAttempts} attempts: ${lockFile}`);
|
|
226
|
+
}
|
|
227
|
+
releaseFileLockSync(lockFile) {
|
|
228
|
+
try {
|
|
229
|
+
fs.unlinkSync(lockFile);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// Lock file already removed or directory cleaned; harmless
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Get all shared context entries
|
|
237
|
+
*/
|
|
238
|
+
getAllContext() {
|
|
239
|
+
if (!fs.existsSync(this.contextFile)) {
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
const content = fs.readFileSync(this.contextFile, 'utf8');
|
|
244
|
+
return JSON.parse(content);
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
248
|
+
logger.warn(`[context-broker] Failed to parse ${this.contextFile}: ${msg}`);
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Get context for specific steps (useful for dependency resolution).
|
|
254
|
+
* When strictIsolation is true, only entries with verified transcript
|
|
255
|
+
* evidence are returned -- rejects anything backed by shared mutable state.
|
|
256
|
+
*/
|
|
257
|
+
getContextForSteps(stepNumbers, strictIsolation = false) {
|
|
258
|
+
const allContext = this.getAllContext();
|
|
259
|
+
const byStep = allContext.filter(entry => stepNumbers.includes(entry.stepNumber));
|
|
260
|
+
if (!strictIsolation) {
|
|
261
|
+
return byStep;
|
|
262
|
+
}
|
|
263
|
+
// strict mode: only entries with a non-empty transcript path
|
|
264
|
+
return byStep.filter(entry => entry.data.transcript && entry.data.transcript.length > 0);
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Get summary of changes from dependent steps.
|
|
268
|
+
* Strict isolation mode filters to transcript-backed entries only.
|
|
269
|
+
*/
|
|
270
|
+
getDependencyContext(dependencies, strictIsolation = false) {
|
|
271
|
+
if (dependencies.length === 0) {
|
|
272
|
+
return 'No dependencies - you are starting fresh.';
|
|
273
|
+
}
|
|
274
|
+
const entries = this.getContextForSteps(dependencies, strictIsolation);
|
|
275
|
+
if (entries.length === 0) {
|
|
276
|
+
return `Dependencies: Steps ${dependencies.join(', ')} (context not yet available)`;
|
|
277
|
+
}
|
|
278
|
+
const lines = [];
|
|
279
|
+
lines.push(`Context from ${entries.length} dependent step(s):`);
|
|
280
|
+
entries.forEach(entry => {
|
|
281
|
+
lines.push(`\nStep ${entry.stepNumber} (${entry.agentName}):`);
|
|
282
|
+
lines.push(` - ${entry.data.outputsSummary}`);
|
|
283
|
+
if (entry.data.filesChanged.length > 0) {
|
|
284
|
+
lines.push(` - Files modified: ${entry.data.filesChanged.slice(0, 5).join(', ')}${entry.data.filesChanged.length > 5 ? '...' : ''}`);
|
|
285
|
+
}
|
|
286
|
+
if (entry.data.branchName) {
|
|
287
|
+
lines.push(` - Branch: ${entry.data.branchName}`);
|
|
288
|
+
}
|
|
289
|
+
if (entry.data.commitShas && entry.data.commitShas.length > 0) {
|
|
290
|
+
lines.push(` - Commits: ${entry.data.commitShas.length}`);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
return lines.join('\n');
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Check if all dependencies are satisfied
|
|
297
|
+
*/
|
|
298
|
+
areDependenciesSatisfied(dependencies) {
|
|
299
|
+
if (dependencies.length === 0) {
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
const completedSteps = new Set(this.getAllContext().map(entry => entry.stepNumber));
|
|
303
|
+
return dependencies.every(dep => completedSteps.has(dep));
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Wait for dependencies to be satisfied.
|
|
307
|
+
* Uses EventEmitter notifications instead of file polling for zero-latency response.
|
|
308
|
+
* Falls back to a timeout to prevent indefinite hangs.
|
|
309
|
+
*/
|
|
310
|
+
async waitForDependencies(dependencies, timeoutMs = defaults_1.DEFAULT_DEPENDENCY_WAIT_MS) {
|
|
311
|
+
if (this.areDependenciesSatisfied(dependencies)) {
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
return new Promise((resolve) => {
|
|
315
|
+
const remaining = new Set(dependencies.filter(dep => {
|
|
316
|
+
const completedSteps = new Set(this.getAllContext().map(e => e.stepNumber));
|
|
317
|
+
return !completedSteps.has(dep);
|
|
318
|
+
}));
|
|
319
|
+
if (remaining.size === 0) {
|
|
320
|
+
resolve(true);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const timer = setTimeout(() => {
|
|
324
|
+
this.removeListener('step-completed', onStep);
|
|
325
|
+
resolve(false);
|
|
326
|
+
}, timeoutMs);
|
|
327
|
+
const onStep = (stepNumber) => {
|
|
328
|
+
remaining.delete(stepNumber);
|
|
329
|
+
if (remaining.size === 0) {
|
|
330
|
+
clearTimeout(timer);
|
|
331
|
+
this.removeListener('step-completed', onStep);
|
|
332
|
+
resolve(true);
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
this.on('step-completed', onStep);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Clear all context (useful for testing or fresh runs)
|
|
340
|
+
*/
|
|
341
|
+
clearContext() {
|
|
342
|
+
if (fs.existsSync(this.contextFile)) {
|
|
343
|
+
fs.unlinkSync(this.contextFile);
|
|
344
|
+
}
|
|
345
|
+
// clean up any stale locks
|
|
346
|
+
if (fs.existsSync(this.lockDir)) {
|
|
347
|
+
const lockFiles = fs.readdirSync(this.lockDir);
|
|
348
|
+
lockFiles.forEach(file => {
|
|
349
|
+
fs.unlinkSync(path.join(this.lockDir, file));
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
sleep(ms) {
|
|
354
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
exports.ContextBroker = ContextBroker;
|
|
358
|
+
exports.default = ContextBroker;
|