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,152 @@
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.parse_gitignore_dirs = parse_gitignore_dirs;
37
+ exports.list_project_files = list_project_files;
38
+ exports.maybe_read_text = maybe_read_text;
39
+ exports.compile_regexes = compile_regexes;
40
+ exports.is_excluded_by_regex = is_excluded_by_regex;
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ function to_posix(relPath) {
44
+ return relPath.split(path.sep).join('/');
45
+ }
46
+ /**
47
+ * Parse a .gitignore file into directory names and path-prefix patterns.
48
+ * Only handles the patterns quality gates care about: bare directory names
49
+ * (e.g. "venv", "__pycache__") and directory prefixes ("venv/", "lib/").
50
+ * Does not implement full gitignore globbing; the goal is to catch the
51
+ * obvious third-party trees that should never be scanned.
52
+ */
53
+ function parse_gitignore_dirs(projectRoot) {
54
+ const dirs = new Set();
55
+ const gitignorePath = path.join(projectRoot, '.gitignore');
56
+ if (!fs.existsSync(gitignorePath)) {
57
+ return dirs;
58
+ }
59
+ let content;
60
+ try {
61
+ content = fs.readFileSync(gitignorePath, 'utf8');
62
+ }
63
+ catch {
64
+ return dirs;
65
+ }
66
+ for (const raw of content.split('\n')) {
67
+ const line = raw.trim();
68
+ if (!line || line.startsWith('#')) {
69
+ continue;
70
+ }
71
+ // Negation patterns (e.g. "!important/") are intentionally skipped;
72
+ // they would need full gitignore semantics to handle correctly
73
+ if (line.startsWith('!')) {
74
+ continue;
75
+ }
76
+ // Strip trailing slash if present: "venv/" -> "venv"
77
+ const cleaned = line.endsWith('/') ? line.slice(0, -1) : line;
78
+ // Only treat simple names (no globs, no path separators) as directory excludes.
79
+ // Patterns like "*.pyc" or "build/output" are too complex for this fast path.
80
+ if (cleaned && !cleaned.includes('*') && !cleaned.includes('?') && !cleaned.includes('/')) {
81
+ dirs.add(cleaned);
82
+ }
83
+ }
84
+ return dirs;
85
+ }
86
+ function list_project_files(projectRoot, excludeDirNames) {
87
+ const files = [];
88
+ const exclude = new Set(excludeDirNames);
89
+ // Merge directory names from .gitignore so project-specific exclusions
90
+ // (like "venv", "lib", ".eggs") are honored without hardcoding every variant
91
+ const gitignoreDirs = parse_gitignore_dirs(projectRoot);
92
+ for (const dir of gitignoreDirs) {
93
+ exclude.add(dir);
94
+ }
95
+ function walk(absDir) {
96
+ const entries = fs.readdirSync(absDir, { withFileTypes: true });
97
+ for (const entry of entries) {
98
+ if (entry.name === '.DS_Store') {
99
+ continue;
100
+ }
101
+ const absPath = path.join(absDir, entry.name);
102
+ const relPath = to_posix(path.relative(projectRoot, absPath));
103
+ if (entry.isDirectory()) {
104
+ if (exclude.has(entry.name)) {
105
+ continue;
106
+ }
107
+ walk(absPath);
108
+ continue;
109
+ }
110
+ if (!entry.isFile()) {
111
+ continue;
112
+ }
113
+ let sizeBytes;
114
+ try {
115
+ sizeBytes = fs.statSync(absPath).size;
116
+ }
117
+ catch {
118
+ // ignore unreadable
119
+ continue;
120
+ }
121
+ files.push({
122
+ path: absPath,
123
+ relativePath: relPath,
124
+ sizeBytes
125
+ });
126
+ }
127
+ }
128
+ walk(projectRoot);
129
+ return files;
130
+ }
131
+ function maybe_read_text(file, maxBytes) {
132
+ if (file.sizeBytes > maxBytes) {
133
+ return undefined;
134
+ }
135
+ try {
136
+ const buf = fs.readFileSync(file.path);
137
+ // crude binary sniff
138
+ if (buf.includes(0)) {
139
+ return undefined;
140
+ }
141
+ return buf.toString('utf8');
142
+ }
143
+ catch {
144
+ return undefined;
145
+ }
146
+ }
147
+ function compile_regexes(regexes) {
148
+ return regexes.map(r => new RegExp(r, 'i'));
149
+ }
150
+ function is_excluded_by_regex(relPath, regexes) {
151
+ return regexes.some(r => r.test(relPath));
152
+ }
@@ -0,0 +1,171 @@
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.run_quality_gates = run_quality_gates;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const file_utils_1 = require("./file-utils");
40
+ const registry_1 = require("./registry");
41
+ function format_issue(issue) {
42
+ const loc = issue.filePath
43
+ ? issue.line
44
+ ? `${issue.filePath}:${issue.line}`
45
+ : issue.filePath
46
+ : 'unknown';
47
+ return `- ${loc}: ${issue.message}`;
48
+ }
49
+ async function run_quality_gates(projectRoot, config, outDir, baselineFiles, baseCommit, skippedRequirementIds, skippedGateKeys) {
50
+ const start = Date.now();
51
+ if (!config.enabled) {
52
+ return {
53
+ passed: true,
54
+ advisory: true,
55
+ results: [{
56
+ id: 'quality-gates',
57
+ title: 'Quality gates disabled',
58
+ status: 'skip',
59
+ durationMs: 0,
60
+ issues: []
61
+ }],
62
+ totalDurationMs: 0
63
+ };
64
+ }
65
+ const files = (0, file_utils_1.list_project_files)(projectRoot, config.excludeDirNames);
66
+ // attach text for small-ish files once (so multiple gates can reuse)
67
+ const ctx = {
68
+ projectRoot,
69
+ files: files.map(f => {
70
+ const text = (0, file_utils_1.maybe_read_text)(f, config.maxFileSizeBytes);
71
+ return text === undefined ? { ...f } : { ...f, text };
72
+ }),
73
+ baselineFiles,
74
+ skippedRequirementIds,
75
+ baseCommit,
76
+ };
77
+ const gateResults = [];
78
+ for (const gate of (0, registry_1.getRegisteredGates)()) {
79
+ const gateConfig = config.gates[gate.key];
80
+ if (!gateConfig?.enabled)
81
+ continue;
82
+ // Target-mode skip: self-improvement gates the caller has opted out of
83
+ // for this run (orchestrator passes SELF_IMPROVEMENT_GATE_KEYS when
84
+ // targetMode === true — running against an external repo whose
85
+ // conventions aren't ours to enforce). Universal gates (hardcodedConfig,
86
+ // testFileProtection) are never in this set. See registry.ts.
87
+ if (skippedGateKeys?.has(gate.key)) {
88
+ // Match the kebab-case id convention that the fired gates emit
89
+ // (e.g. 'duplicateBlocks' registry key → 'duplicate-blocks' id).
90
+ // Downstream consumers look gates up by id, so skipped entries need
91
+ // the same id shape as run entries for the same gate.
92
+ const id = gate.key.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
93
+ gateResults.push({
94
+ id,
95
+ title: gate.title,
96
+ status: 'skip',
97
+ durationMs: 0,
98
+ issues: [],
99
+ });
100
+ continue;
101
+ }
102
+ gateResults.push(await gate.run(ctx, gateConfig, config.maxFileSizeBytes));
103
+ }
104
+ // Downgrade gate failures when the entire gate covers requirements that are
105
+ // in the skip tier for the current task type. This prevents API projects from
106
+ // failing on accessibility checks, or CLI projects from failing on security headers.
107
+ if (ctx.skippedRequirementIds && ctx.skippedRequirementIds.size > 0) {
108
+ const GATE_TO_REQUIREMENT_IDS = {
109
+ 'accessibility': ['aria-attributes', 'aria-interactive', 'responsive-layout', 'skip-link', 'dark-mode', 'focus-visible', 'semantic-html', 'keyboard-navigation', 'responsive-breakpoint'],
110
+ };
111
+ for (const result of gateResults) {
112
+ if (result.status !== 'fail')
113
+ continue;
114
+ const mappedIds = GATE_TO_REQUIREMENT_IDS[result.id];
115
+ if (!mappedIds)
116
+ continue;
117
+ const allSkipped = mappedIds.every(id => ctx.skippedRequirementIds.has(id));
118
+ if (allSkipped) {
119
+ result.status = 'skip';
120
+ result.issues = [];
121
+ }
122
+ }
123
+ }
124
+ const hardPassed = true;
125
+ const totalDurationMs = Date.now() - start;
126
+ if (outDir) {
127
+ fs.mkdirSync(outDir, { recursive: true });
128
+ const jsonPath = path.join(outDir, 'quality-gates.json');
129
+ fs.writeFileSync(jsonPath, JSON.stringify({
130
+ projectRoot,
131
+ passed: hardPassed,
132
+ advisory: true,
133
+ totalDurationMs,
134
+ results: gateResults
135
+ }, null, 2), 'utf8');
136
+ const mdPath = path.join(outDir, 'quality-gates.md');
137
+ const mdLines = [];
138
+ mdLines.push('# Quality Gates Report');
139
+ mdLines.push('');
140
+ mdLines.push(`Root: ${projectRoot}`);
141
+ mdLines.push('Status: ✅ PASSED (advisory findings only)');
142
+ mdLines.push(`Duration: ${totalDurationMs}ms`);
143
+ mdLines.push('');
144
+ for (const gr of gateResults) {
145
+ const icon = gr.status === 'pass' ? '✅' : gr.status === 'skip' ? '⏭️' : '❌';
146
+ mdLines.push(`## ${icon} ${gr.title} (${gr.id})`);
147
+ mdLines.push('');
148
+ if (gr.issues.length === 0) {
149
+ mdLines.push('No issues found.');
150
+ mdLines.push('');
151
+ continue;
152
+ }
153
+ for (const issue of gr.issues) {
154
+ mdLines.push(format_issue(issue));
155
+ if (issue.excerpt) {
156
+ mdLines.push('');
157
+ mdLines.push('```');
158
+ mdLines.push(issue.excerpt);
159
+ mdLines.push('```');
160
+ }
161
+ if (issue.hint) {
162
+ mdLines.push('');
163
+ mdLines.push(`hint: ${issue.hint}`);
164
+ }
165
+ mdLines.push('');
166
+ }
167
+ }
168
+ fs.writeFileSync(mdPath, mdLines.join('\n'), 'utf8');
169
+ }
170
+ return { passed: hardPassed, advisory: true, results: gateResults, totalDurationMs };
171
+ }
@@ -0,0 +1,355 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.run_accessibility_gate = run_accessibility_gate;
4
+ const file_utils_1 = require("../file-utils");
5
+ // Matches <h1> through <h6> tags in HTML content
6
+ const HEADING_REGEX = /<h([1-6])\b/gi;
7
+ async function run_accessibility_gate(ctx, config, maxFileSizeBytes) {
8
+ const start = Date.now();
9
+ const issues = [];
10
+ // Only check files created by agents; pre-existing project files are the owner's concern
11
+ const baseline = ctx.baselineFiles;
12
+ const isAgentFile = (f) => !baseline || !baseline.has(f.relativePath);
13
+ const htmlFiles = ctx.files.filter(f => /\.html?$/i.test(f.relativePath) && isAgentFile(f));
14
+ const jsxFiles = ctx.files.filter(f => /\.(jsx|tsx)$/i.test(f.relativePath) && isAgentFile(f));
15
+ const cssFiles = ctx.files.filter(f => /\.css$/i.test(f.relativePath) && isAgentFile(f));
16
+ // 1. Check lang attribute on <html> in all .html files
17
+ for (const file of htmlFiles) {
18
+ const text = file.text ?? (0, file_utils_1.maybe_read_text)(file, maxFileSizeBytes);
19
+ if (!text)
20
+ continue;
21
+ if (/<html\b/i.test(text) && !/<html[^>]+lang\s*=/i.test(text)) {
22
+ issues.push({
23
+ message: `<html> element missing lang attribute`,
24
+ filePath: file.relativePath,
25
+ hint: 'add lang="en" (or appropriate language code) to the <html> tag'
26
+ });
27
+ }
28
+ }
29
+ // 2. Check for skip-to-content link
30
+ if (config.requireSkipLink && htmlFiles.length > 0) {
31
+ const hasSkipLink = htmlFiles.some(f => {
32
+ const text = f.text ?? (0, file_utils_1.maybe_read_text)(f, maxFileSizeBytes);
33
+ if (!text)
34
+ return false;
35
+ return /skip-link|skip.to.content|#main-content/i.test(text);
36
+ });
37
+ // Also check JSX/TSX components for skip links
38
+ const hasSkipLinkInJsx = jsxFiles.some(f => {
39
+ const text = f.text ?? (0, file_utils_1.maybe_read_text)(f, maxFileSizeBytes);
40
+ if (!text)
41
+ return false;
42
+ return /skip-link|skip.to.content|#main-content/i.test(text);
43
+ });
44
+ if (!hasSkipLink && !hasSkipLinkInJsx) {
45
+ issues.push({
46
+ message: 'no skip-to-content link found in HTML or JSX files',
47
+ hint: 'add <a href="#main-content" class="skip-link">Skip to content</a> as the first focusable element'
48
+ });
49
+ }
50
+ }
51
+ // 3. Check heading hierarchy
52
+ if (config.requireHeadingHierarchy) {
53
+ const allSourceFiles = [...htmlFiles, ...jsxFiles];
54
+ for (const file of allSourceFiles) {
55
+ const text = file.text ?? (0, file_utils_1.maybe_read_text)(file, maxFileSizeBytes);
56
+ if (!text)
57
+ continue;
58
+ const headings = [];
59
+ let match;
60
+ const headingRegex = new RegExp(HEADING_REGEX.source, 'gi');
61
+ while ((match = headingRegex.exec(text)) !== null) {
62
+ headings.push(parseInt(match[1], 10));
63
+ }
64
+ if (headings.length === 0)
65
+ continue;
66
+ // First heading should be h1
67
+ if (headings[0] !== 1) {
68
+ issues.push({
69
+ message: `first heading is <h${headings[0]}>, expected <h1>`,
70
+ filePath: file.relativePath,
71
+ hint: 'page should start with an <h1> element'
72
+ });
73
+ }
74
+ // No skipped levels: h1 -> h3 without h2 in between
75
+ for (let i = 1; i < headings.length; i++) {
76
+ if (headings[i] > headings[i - 1] + 1) {
77
+ issues.push({
78
+ message: `heading level skips from <h${headings[i - 1]}> to <h${headings[i]}>`,
79
+ filePath: file.relativePath,
80
+ hint: `add an intermediate <h${headings[i - 1] + 1}> heading before <h${headings[i]}>`
81
+ });
82
+ break; // one issue per file is enough
83
+ }
84
+ }
85
+ }
86
+ }
87
+ // 4. Check aria-label on interactive/landmark elements
88
+ if (config.requireAriaLabels) {
89
+ for (const file of jsxFiles) {
90
+ const text = file.text ?? (0, file_utils_1.maybe_read_text)(file, maxFileSizeBytes);
91
+ if (!text)
92
+ continue;
93
+ // <nav> without aria-label
94
+ if (/<nav\b/i.test(text) && !/<nav[^>]+aria-label/i.test(text)) {
95
+ issues.push({
96
+ message: '<nav> element missing aria-label',
97
+ filePath: file.relativePath,
98
+ hint: 'add aria-label="Main navigation" or similar'
99
+ });
100
+ }
101
+ // <button> without aria-label and without visible text children
102
+ // Only flag buttons that look like icon-only buttons (contain emoji or svg but no plain text)
103
+ const buttonRegex = /<button\b([^>]*)>([\s\S]*?)<\/button>/gi;
104
+ let btnMatch;
105
+ while ((btnMatch = buttonRegex.exec(text)) !== null) {
106
+ const attrs = btnMatch[1];
107
+ const content = btnMatch[2];
108
+ const hasAriaLabel = /aria-label/i.test(attrs);
109
+ // Button has meaningful text if it contains word characters outside tags
110
+ const strippedContent = content.replace(/<[^>]+>/g, '').trim();
111
+ const hasTextContent = /\w{2,}/.test(strippedContent);
112
+ if (!hasAriaLabel && !hasTextContent) {
113
+ issues.push({
114
+ message: 'icon-only <button> missing aria-label',
115
+ filePath: file.relativePath,
116
+ excerpt: content.slice(0, 40),
117
+ hint: 'add aria-label describing the button action'
118
+ });
119
+ break; // one per file
120
+ }
121
+ }
122
+ }
123
+ }
124
+ // 5. Check for :focus-visible in CSS
125
+ if (config.requireFocusStyles) {
126
+ const hasFocusVisible = cssFiles.some(f => {
127
+ const text = f.text ?? (0, file_utils_1.maybe_read_text)(f, maxFileSizeBytes);
128
+ if (!text)
129
+ return false;
130
+ return /:focus-visible/.test(text);
131
+ });
132
+ // Also check inline styles in JSX for focus handling
133
+ const hasFocusInJsx = jsxFiles.some(f => {
134
+ const text = f.text ?? (0, file_utils_1.maybe_read_text)(f, maxFileSizeBytes);
135
+ if (!text)
136
+ return false;
137
+ return /focus-visible|onFocus|:focus/.test(text);
138
+ });
139
+ if (!hasFocusVisible && !hasFocusInJsx && cssFiles.length > 0) {
140
+ issues.push({
141
+ message: 'no :focus-visible styles found in any CSS file',
142
+ hint: 'add :focus-visible { outline: 2px solid <color>; outline-offset: 2px; } for interactive elements'
143
+ });
144
+ }
145
+ // Check for outline: none/0 without replacement focus style.
146
+ // "0" must be standalone (followed by ; or whitespace-then-;), not a decimal prefix like 0.1875rem.
147
+ const outlineDisabledRe = /outline\s*:\s*(none|0(?!\.\d))\s*(;|!|$)/m;
148
+ const outlineReplacementRe = /outline\s*:\s*\d+(\.\d+)?\s*(px|rem|em)\b/;
149
+ for (const file of cssFiles) {
150
+ const text = file.text ?? (0, file_utils_1.maybe_read_text)(file, maxFileSizeBytes);
151
+ if (!text)
152
+ continue;
153
+ if (outlineDisabledRe.test(text) && !outlineReplacementRe.test(text.replace(outlineDisabledRe, ''))) {
154
+ issues.push({
155
+ message: 'outline: none found without replacement focus style',
156
+ filePath: file.relativePath,
157
+ hint: 'removing outline disables keyboard focus visibility; add an alternative focus indicator'
158
+ });
159
+ }
160
+ }
161
+ }
162
+ // 6. Check prefers-reduced-motion when animations are present
163
+ if (config.requireReducedMotion) {
164
+ const animationSources = [...cssFiles, ...htmlFiles];
165
+ for (const file of animationSources) {
166
+ const text = file.text ?? (0, file_utils_1.maybe_read_text)(file, maxFileSizeBytes);
167
+ if (!text)
168
+ continue;
169
+ const hasAnimations = /@keyframes\b/.test(text) || /animation\s*:/.test(text) || /transition\s*:/.test(text);
170
+ const hasReducedMotionQuery = /prefers-reduced-motion/.test(text);
171
+ if (hasAnimations && !hasReducedMotionQuery) {
172
+ issues.push({
173
+ message: 'CSS includes animations but no prefers-reduced-motion media query',
174
+ filePath: file.relativePath,
175
+ hint: 'add @media (prefers-reduced-motion: reduce) { /* disable or simplify animations */ }'
176
+ });
177
+ }
178
+ }
179
+ }
180
+ // 7. Check for phantom asset references (src/href pointing to nonexistent local files)
181
+ if (config.requireNoPhantomAssets) {
182
+ const knownFiles = new Set(ctx.files.map(f => f.relativePath));
183
+ for (const file of htmlFiles) {
184
+ const text = file.text ?? (0, file_utils_1.maybe_read_text)(file, maxFileSizeBytes);
185
+ if (!text)
186
+ continue;
187
+ // Match src="..." and href="..." that look like local paths (not URLs, not anchors, not data URIs)
188
+ const refRegex = /(?:src|href)\s*=\s*["']([^"']+)["']/gi;
189
+ // Resolve references against the HTML file's own directory so a nested
190
+ // page (e.g. web/index.html) correctly points at web/src/styles.css.
191
+ const slashIdx = file.relativePath.lastIndexOf('/');
192
+ const htmlDir = slashIdx >= 0 ? file.relativePath.substring(0, slashIdx) : '';
193
+ let refMatch;
194
+ while ((refMatch = refRegex.exec(text)) !== null) {
195
+ const ref = refMatch[1];
196
+ // Skip external URLs, anchors, data URIs, template expressions
197
+ if (/^(https?:|mailto:|#|data:|javascript:|\{|%|\/)/.test(ref))
198
+ continue;
199
+ // Resolve ./ and ../ segments against the HTML file's directory.
200
+ let remaining = ref.replace(/^\.\//g, '');
201
+ let dir = htmlDir;
202
+ while (remaining.startsWith('../')) {
203
+ remaining = remaining.substring(3);
204
+ const parent = dir.lastIndexOf('/');
205
+ dir = parent >= 0 ? dir.substring(0, parent) : '';
206
+ }
207
+ const resolved = dir ? `${dir}/${remaining}` : remaining;
208
+ // Fall back to the bare reference for flat projects where HTML sits
209
+ // alongside its assets without a shared parent directory.
210
+ if (!knownFiles.has(resolved) && !knownFiles.has(remaining)) {
211
+ issues.push({
212
+ message: `references "${ref}" which does not exist in the project`,
213
+ filePath: file.relativePath,
214
+ hint: 'remove the reference or create the missing file'
215
+ });
216
+ }
217
+ }
218
+ }
219
+ }
220
+ // 8. Check for required meta tags in HTML head
221
+ if (config.requireMetaTags) {
222
+ for (const file of htmlFiles) {
223
+ const text = file.text ?? (0, file_utils_1.maybe_read_text)(file, maxFileSizeBytes);
224
+ if (!text)
225
+ continue;
226
+ if (!/<html\b/i.test(text))
227
+ continue;
228
+ if (!/<meta[^>]+name\s*=\s*["']description["'][^>]*>/i.test(text)) {
229
+ issues.push({
230
+ message: 'missing <meta name="description"> in HTML head',
231
+ filePath: file.relativePath,
232
+ hint: 'add <meta name="description" content="..."> with a real project description'
233
+ });
234
+ }
235
+ if (!/<meta[^>]+name\s*=\s*["']theme-color["'][^>]*>/i.test(text)) {
236
+ issues.push({
237
+ message: 'missing <meta name="theme-color"> in HTML head',
238
+ filePath: file.relativePath,
239
+ hint: 'add <meta name="theme-color" content="#..."> matching the primary brand color'
240
+ });
241
+ }
242
+ }
243
+ }
244
+ // 9. Check for responsive CSS (viewport meta tag and responsive units or media queries)
245
+ if (config.requireResponsiveCSS && htmlFiles.length > 0) {
246
+ const hasViewportMeta = htmlFiles.some(f => {
247
+ const text = f.text ?? (0, file_utils_1.maybe_read_text)(f, maxFileSizeBytes);
248
+ return text ? /<meta[^>]+name\s*=\s*["']viewport["'][^>]*>/i.test(text) : false;
249
+ });
250
+ if (!hasViewportMeta) {
251
+ issues.push({
252
+ message: 'no <meta name="viewport"> found in any HTML file',
253
+ hint: 'add <meta name="viewport" content="width=device-width, initial-scale=1.0"> for responsive layout'
254
+ });
255
+ }
256
+ // At least one CSS file should use a responsive technique
257
+ if (cssFiles.length > 0) {
258
+ const hasResponsive = cssFiles.some(f => {
259
+ const text = f.text ?? (0, file_utils_1.maybe_read_text)(f, maxFileSizeBytes);
260
+ if (!text)
261
+ return false;
262
+ return /@media\b/.test(text) || /\bclamp\(/.test(text) || /\b(vw|vh|vmin|vmax|rem|%)\b/.test(text);
263
+ });
264
+ if (!hasResponsive) {
265
+ issues.push({
266
+ message: 'CSS uses no responsive techniques (media queries, clamp(), relative units)',
267
+ hint: 'use relative units (rem, %), clamp(), or @media queries for responsive layout'
268
+ });
269
+ }
270
+ }
271
+ }
272
+ // 10. Check for color scheme support (CSS custom properties + dark mode query)
273
+ if (config.requireColorScheme && cssFiles.length > 0) {
274
+ const hasCustomProperties = cssFiles.some(f => {
275
+ const text = f.text ?? (0, file_utils_1.maybe_read_text)(f, maxFileSizeBytes);
276
+ return text ? /--[\w-]+\s*:/.test(text) : false;
277
+ });
278
+ const hasDarkModeQuery = cssFiles.some(f => {
279
+ const text = f.text ?? (0, file_utils_1.maybe_read_text)(f, maxFileSizeBytes);
280
+ return text ? /prefers-color-scheme\s*:\s*dark/.test(text) : false;
281
+ });
282
+ if (!hasCustomProperties) {
283
+ issues.push({
284
+ message: 'no CSS custom properties defined; colors should use --var references for theming',
285
+ hint: 'define colors, spacing, and font sizes as custom properties on :root'
286
+ });
287
+ }
288
+ if (!hasDarkModeQuery) {
289
+ issues.push({
290
+ message: 'no @media (prefers-color-scheme: dark) found for dark mode support',
291
+ hint: 'add a dark mode media query that overrides :root custom properties'
292
+ });
293
+ }
294
+ }
295
+ // 11. Check semantic HTML: flag div-heavy markup without semantic elements
296
+ if (config.requireSemanticHTML) {
297
+ for (const file of htmlFiles) {
298
+ const text = file.text ?? (0, file_utils_1.maybe_read_text)(file, maxFileSizeBytes);
299
+ if (!text)
300
+ continue;
301
+ if (!/<html\b/i.test(text))
302
+ continue;
303
+ const hasMain = /<main\b/i.test(text);
304
+ const hasNav = /<nav\b/i.test(text);
305
+ const hasHeader = /<header\b/i.test(text);
306
+ const hasSemanticElement = hasMain || hasNav || hasHeader;
307
+ const divCount = (text.match(/<div\b/gi) || []).length;
308
+ // Flag pages that use divs but no semantic landmarks at all
309
+ if (divCount > 3 && !hasSemanticElement) {
310
+ issues.push({
311
+ message: `${divCount} <div> elements but no semantic landmarks (main, nav, header)`,
312
+ filePath: file.relativePath,
313
+ hint: 'replace wrapper divs with semantic elements: <main>, <nav>, <header>, <footer>'
314
+ });
315
+ }
316
+ }
317
+ }
318
+ // 12. Check that <img> tags have alt attributes
319
+ if (config.requireImgAlt) {
320
+ const allSourceFiles = [...htmlFiles, ...jsxFiles];
321
+ for (const file of allSourceFiles) {
322
+ const text = file.text ?? (0, file_utils_1.maybe_read_text)(file, maxFileSizeBytes);
323
+ if (!text)
324
+ continue;
325
+ const imgRegex = /<img\b([^>]*)>/gi;
326
+ let imgMatch;
327
+ while ((imgMatch = imgRegex.exec(text)) !== null) {
328
+ const attrs = imgMatch[1];
329
+ if (!/\balt\s*=/i.test(attrs)) {
330
+ issues.push({
331
+ message: '<img> element missing alt attribute',
332
+ filePath: file.relativePath,
333
+ hint: 'add alt="descriptive text" or alt="" for decorative images'
334
+ });
335
+ break; // one per file
336
+ }
337
+ }
338
+ }
339
+ }
340
+ const durationMs = Date.now() - start;
341
+ const status = issues.length > 0 ? 'fail' : 'pass';
342
+ return {
343
+ id: 'accessibility',
344
+ title: 'Accessibility checks',
345
+ status,
346
+ durationMs,
347
+ issues,
348
+ stats: {
349
+ htmlFiles: htmlFiles.length,
350
+ jsxFiles: jsxFiles.length,
351
+ cssFiles: cssFiles.length,
352
+ issues: issues.length
353
+ }
354
+ };
355
+ }