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.
Files changed (174) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +199 -0
  3. package/agents/reviewer.agent.md +50 -0
  4. package/agents/worker.agent.md +41 -0
  5. package/config/default-agents.yaml +56 -0
  6. package/config/pm-agent.yaml +36 -0
  7. package/config/quality-gates.yaml +88 -0
  8. package/config/repair-agent.yaml +34 -0
  9. package/config/semgrep-rules/complexity-mismatch.yaml +6 -0
  10. package/config/semgrep-rules/exception-swallowing.yaml +13 -0
  11. package/config/semgrep-rules/hardcoded-answer.yaml +6 -0
  12. package/config/semgrep-rules/mock-mutation.yaml +6 -0
  13. package/config/semgrep-rules/test-modification.yaml +6 -0
  14. package/config/user-agents.yaml +23 -0
  15. package/dist/src/adapters/adapter-factory.js +29 -0
  16. package/dist/src/adapters/agent-adapter.js +32 -0
  17. package/dist/src/adapters/claude-code-adapter.js +126 -0
  18. package/dist/src/adapters/claude-code-teams.js +119 -0
  19. package/dist/src/adapters/codex-adapter.js +118 -0
  20. package/dist/src/adapters/copilot-adapter.js +224 -0
  21. package/dist/src/adapters/fatal-error-classifier.js +88 -0
  22. package/dist/src/adapters/index.js +44 -0
  23. package/dist/src/adapters/persistent-session.js +204 -0
  24. package/dist/src/adapters/process-supervisor.js +199 -0
  25. package/dist/src/agents-exporter.js +398 -0
  26. package/dist/src/analytics-log.js +161 -0
  27. package/dist/src/baseline-scanner.js +65 -0
  28. package/dist/src/bootstrap-evidence.js +88 -0
  29. package/dist/src/bootstrap-orchestrator.js +205 -0
  30. package/dist/src/bootstrap-types.js +5 -0
  31. package/dist/src/branch-merger.js +427 -0
  32. package/dist/src/cli/attest-handlers.js +44 -0
  33. package/dist/src/cli/cost-prompt.js +51 -0
  34. package/dist/src/cli/demo-handlers.js +274 -0
  35. package/dist/src/cli/flags.js +152 -0
  36. package/dist/src/cli/index.js +57 -0
  37. package/dist/src/cli/live-status.js +186 -0
  38. package/dist/src/cli/misc-handlers.js +242 -0
  39. package/dist/src/cli/plan-handlers.js +350 -0
  40. package/dist/src/cli/share-handlers.js +163 -0
  41. package/dist/src/cli/status-handlers.js +457 -0
  42. package/dist/src/cli/swarm-handlers.js +638 -0
  43. package/dist/src/cli/usage.js +72 -0
  44. package/dist/src/cli.js +217 -0
  45. package/dist/src/commit-pattern-detector.js +143 -0
  46. package/dist/src/commit-quality-analyzer.js +38 -0
  47. package/dist/src/config-loader.js +339 -0
  48. package/dist/src/context-broker.js +358 -0
  49. package/dist/src/copilot-cli-wrapper.js +258 -0
  50. package/dist/src/cost-estimator.js +241 -0
  51. package/dist/src/defaults.js +18 -0
  52. package/dist/src/demo-mode.js +180 -0
  53. package/dist/src/deployment-handler.js +78 -0
  54. package/dist/src/deployment-manager.js +228 -0
  55. package/dist/src/execution-queue.js +196 -0
  56. package/dist/src/external-tool-manager.js +184 -0
  57. package/dist/src/gate-prompt-builder.js +117 -0
  58. package/dist/src/gate-remediation.js +173 -0
  59. package/dist/src/github-issues-ingester.js +152 -0
  60. package/dist/src/github-mcp-integrator.js +164 -0
  61. package/dist/src/hook-generator.js +229 -0
  62. package/dist/src/index.js +48 -0
  63. package/dist/src/knowledge-base.js +300 -0
  64. package/dist/src/logger.js +111 -0
  65. package/dist/src/meta-analyzer.js +208 -0
  66. package/dist/src/metrics-collector.js +209 -0
  67. package/dist/src/metrics-types.js +5 -0
  68. package/dist/src/multi-repo-coordinator.js +179 -0
  69. package/dist/src/orchestrator/async-meta-analysis.js +84 -0
  70. package/dist/src/orchestrator/fatal-run-error.js +43 -0
  71. package/dist/src/orchestrator/final-gates-remediation.js +258 -0
  72. package/dist/src/orchestrator/git-state-utils.js +119 -0
  73. package/dist/src/orchestrator/pause-controller.js +55 -0
  74. package/dist/src/orchestrator/replan-runner.js +258 -0
  75. package/dist/src/orchestrator/step-executor.js +391 -0
  76. package/dist/src/orchestrator/wave-scheduler-loop.js +204 -0
  77. package/dist/src/owasp-mapper.js +153 -0
  78. package/dist/src/owasp-report-renderer.js +39 -0
  79. package/dist/src/plan-files.js +142 -0
  80. package/dist/src/plan-generator.js +1207 -0
  81. package/dist/src/pm-agent.js +296 -0
  82. package/dist/src/post-run-reporter.js +203 -0
  83. package/dist/src/pr-automation.js +103 -0
  84. package/dist/src/pr-manager.js +318 -0
  85. package/dist/src/presenter/index.js +209 -0
  86. package/dist/src/prompt-builder.js +173 -0
  87. package/dist/src/quality-gates/config-loader.js +196 -0
  88. package/dist/src/quality-gates/default-config.js +243 -0
  89. package/dist/src/quality-gates/file-utils.js +152 -0
  90. package/dist/src/quality-gates/gate-runner.js +171 -0
  91. package/dist/src/quality-gates/gates/accessibility.js +355 -0
  92. package/dist/src/quality-gates/gates/duplicate-blocks.js +144 -0
  93. package/dist/src/quality-gates/gates/hardcoded-config.js +52 -0
  94. package/dist/src/quality-gates/gates/readme-claims.js +62 -0
  95. package/dist/src/quality-gates/gates/runtime-checks.js +265 -0
  96. package/dist/src/quality-gates/gates/scaffold-defaults.js +108 -0
  97. package/dist/src/quality-gates/gates/test-coverage.js +240 -0
  98. package/dist/src/quality-gates/gates/test-file-protection.js +85 -0
  99. package/dist/src/quality-gates/gates/test-isolation.js +74 -0
  100. package/dist/src/quality-gates/index.js +17 -0
  101. package/dist/src/quality-gates/registry.js +205 -0
  102. package/dist/src/quality-gates/types.js +2 -0
  103. package/dist/src/quick-fix-mode.js +302 -0
  104. package/dist/src/recipe-loader.js +122 -0
  105. package/dist/src/repair-agent.js +396 -0
  106. package/dist/src/repo-analyzer.js +342 -0
  107. package/dist/src/report-generator.js +146 -0
  108. package/dist/src/report-renderer.js +90 -0
  109. package/dist/src/requirement-filter.js +52 -0
  110. package/dist/src/sarif-formatter.js +140 -0
  111. package/dist/src/scheduling/dependency-analyzer.js +178 -0
  112. package/dist/src/scheduling/work-stealing-queue.js +123 -0
  113. package/dist/src/secret-redactor.js +90 -0
  114. package/dist/src/session-executor.js +587 -0
  115. package/dist/src/session-manager.js +221 -0
  116. package/dist/src/share/transcript-verification.js +128 -0
  117. package/dist/src/share-parser.js +477 -0
  118. package/dist/src/spinner.js +146 -0
  119. package/dist/src/step-runner.js +365 -0
  120. package/dist/src/swarm-orchestrator.js +686 -0
  121. package/dist/src/task-classifier.js +134 -0
  122. package/dist/src/test-command-discovery.js +123 -0
  123. package/dist/src/text-similarity.js +24 -0
  124. package/dist/src/tier-maps.js +158 -0
  125. package/dist/src/types.js +5 -0
  126. package/dist/src/url-shortener.js +135 -0
  127. package/dist/src/verification/attestation.js +166 -0
  128. package/dist/src/verification/cheat-detector.js +240 -0
  129. package/dist/src/verification/command-runner.js +70 -0
  130. package/dist/src/verification/composite-score.js +157 -0
  131. package/dist/src/verification/cosign-attestation.js +137 -0
  132. package/dist/src/verification/diff-analysis.js +89 -0
  133. package/dist/src/verification/differential-gate.js +173 -0
  134. package/dist/src/verification/index.js +41 -0
  135. package/dist/src/verification/mutation-gate.js +242 -0
  136. package/dist/src/verification/property-gate.js +201 -0
  137. package/dist/src/verification/test-synthesizer.js +243 -0
  138. package/dist/src/verifier/outcome-checks.js +396 -0
  139. package/dist/src/verifier/transcript-checks.js +207 -0
  140. package/dist/src/verifier/verification-reporters.js +148 -0
  141. package/dist/src/verifier-engine.js +491 -0
  142. package/dist/src/wave-resizer.js +131 -0
  143. package/dist/src/wave-scheduler.js +44 -0
  144. package/dist/src/worktree-manager.js +368 -0
  145. package/dist/src/worktree-reserved-paths.js +97 -0
  146. package/package.json +80 -0
  147. package/plugin/agents/.gitkeep +0 -0
  148. package/plugin/agents/backend_master.agent.md +72 -0
  149. package/plugin/agents/devops_pro.agent.md +45 -0
  150. package/plugin/agents/frontend_expert.agent.md +59 -0
  151. package/plugin/agents/integrator_finalizer.agent.md +44 -0
  152. package/plugin/agents/meta_reviewer.agent.md +44 -0
  153. package/plugin/agents/security_auditor.agent.md +43 -0
  154. package/plugin/agents/tester_elite.agent.md +43 -0
  155. package/plugin/hooks/hooks.json +19 -0
  156. package/plugin/hooks/post-tool-use.json +9 -0
  157. package/plugin/hooks/pre-tool-use.json +9 -0
  158. package/plugin/plugin.json +47 -0
  159. package/plugin/skills/swarm-gates/SKILL.md +53 -0
  160. package/plugin/skills/swarm-orchestrate/SKILL.md +34 -0
  161. package/plugin/skills/swarm-verify/SKILL.md +44 -0
  162. package/templates/README.md +35 -0
  163. package/templates/cli-tool.json +31 -0
  164. package/templates/fullstack.json +52 -0
  165. package/templates/library.json +38 -0
  166. package/templates/react-app.json +45 -0
  167. package/templates/recipes/add-api-docs.json +35 -0
  168. package/templates/recipes/add-auth.json +47 -0
  169. package/templates/recipes/add-ci.json +39 -0
  170. package/templates/recipes/add-tests.json +39 -0
  171. package/templates/recipes/migrate-to-ts.json +42 -0
  172. package/templates/recipes/refactor-modularize.json +44 -0
  173. package/templates/recipes/security-audit.json +35 -0
  174. 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;