role-os 2.2.0 → 2.3.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.
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Domain Detection — Repo type detection and swarm manifest generation.
3
+ *
4
+ * Detects the repo type from directory structure and package metadata,
5
+ * then generates a swarm-manifest.json with non-overlapping domain assignments.
6
+ */
7
+
8
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
9
+ import { join, relative } from "node:path";
10
+
11
+ // ── Repo type detection ───────────────────────────────────────────────────────
12
+
13
+ const REPO_TYPE_SIGNALS = {
14
+ desktop: {
15
+ files: ["src-tauri/tauri.conf.json", "src-tauri/Cargo.toml"],
16
+ deps: ["@tauri-apps/api", "electron", "electron-builder"],
17
+ dirs: ["src-tauri"],
18
+ },
19
+ web: {
20
+ files: ["next.config.js", "next.config.mjs", "nuxt.config.ts", "vite.config.ts", "vite.config.js", "astro.config.mjs"],
21
+ deps: ["react-dom", "next", "nuxt", "vue", "@angular/core", "svelte", "astro"],
22
+ dirs: ["pages", "app", "views"],
23
+ },
24
+ mcp: {
25
+ files: [],
26
+ deps: ["@modelcontextprotocol/sdk"],
27
+ dirs: [],
28
+ },
29
+ cli: {
30
+ files: [],
31
+ deps: [],
32
+ dirs: [],
33
+ // CLI is the default fallback
34
+ },
35
+ };
36
+
37
+ /**
38
+ * Detect the repo type from directory structure and package metadata.
39
+ * @param {string} cwd - Repository root directory
40
+ * @returns {"desktop"|"web"|"mcp"|"cli"|"monorepo"}
41
+ */
42
+ export function detectRepoType(cwd) {
43
+ // Check for monorepo signals first
44
+ const hasWorkspaces = hasPackageWorkspaces(cwd);
45
+ const hasLerna = existsSync(join(cwd, "lerna.json"));
46
+ if (hasWorkspaces || hasLerna) return "monorepo";
47
+
48
+ const pkg = readPackageJson(cwd);
49
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
50
+
51
+ // Check each type in priority order
52
+ for (const type of ["desktop", "web", "mcp"]) {
53
+ const signals = REPO_TYPE_SIGNALS[type];
54
+
55
+ // Check files
56
+ for (const f of signals.files) {
57
+ if (existsSync(join(cwd, f))) return type;
58
+ }
59
+
60
+ // Check dependencies
61
+ for (const dep of signals.deps) {
62
+ if (allDeps[dep]) return type;
63
+ }
64
+
65
+ // Check directories
66
+ for (const dir of signals.dirs) {
67
+ if (existsSync(join(cwd, dir)) && statSync(join(cwd, dir)).isDirectory()) return type;
68
+ }
69
+ }
70
+
71
+ // Check for Rust CLI (Cargo.toml without Tauri)
72
+ if (existsSync(join(cwd, "Cargo.toml")) && !existsSync(join(cwd, "src-tauri"))) return "cli";
73
+
74
+ // Check for Python CLI
75
+ if (existsSync(join(cwd, "pyproject.toml")) || existsSync(join(cwd, "setup.py"))) return "cli";
76
+
77
+ // Check for Go
78
+ if (existsSync(join(cwd, "go.mod"))) return "cli";
79
+
80
+ return "cli";
81
+ }
82
+
83
+ // ── Domain assignment ─────────────────────────────────────────────────────────
84
+
85
+ /**
86
+ * Default domain templates per repo type.
87
+ * Each domain defines glob patterns for file ownership.
88
+ */
89
+ const DOMAIN_TEMPLATES = {
90
+ cli: [
91
+ { id: "backend", role: "Swarm Backend Agent", patterns: ["src/**", "lib/**", "bin/**", "*.mjs", "*.js", "*.ts", "*.py", "*.rs", "*.go"] },
92
+ { id: "tests", role: "Swarm Tests Agent", patterns: ["test/**", "tests/**", "spec/**", "__tests__/**", "*.test.*", "*.spec.*"] },
93
+ { id: "infra", role: "Swarm Infra Agent", patterns: [".github/**", "*.md", "*.json", "*.yaml", "*.yml", "*.toml", ".eslintrc*", ".prettierrc*", "Makefile", "Dockerfile"] },
94
+ ],
95
+ web: [
96
+ { id: "backend", role: "Swarm Backend Agent", patterns: ["src/server/**", "src/api/**", "src/lib/**", "server/**", "api/**"] },
97
+ { id: "frontend", role: "Swarm Frontend Agent", patterns: ["src/components/**", "src/pages/**", "src/app/**", "src/views/**", "src/styles/**", "public/**", "*.css", "*.html"] },
98
+ { id: "tests", role: "Swarm Tests Agent", patterns: ["test/**", "tests/**", "spec/**", "__tests__/**", "*.test.*", "*.spec.*", "e2e/**", "cypress/**"] },
99
+ { id: "infra", role: "Swarm Infra Agent", patterns: [".github/**", "*.md", "*.json", "*.yaml", "*.yml", "*.toml", ".eslintrc*", ".prettierrc*", "Makefile", "Dockerfile", "*.config.*"] },
100
+ ],
101
+ desktop: [
102
+ { id: "backend", role: "Swarm Backend Agent", patterns: ["src-tauri/**", "src/lib/**", "src/server/**"] },
103
+ { id: "bridge", role: "Swarm Bridge Agent", patterns: ["src/bridge/**", "src/ipc/**", "src/commands/**"] },
104
+ { id: "frontend", role: "Swarm Frontend Agent", patterns: ["src/components/**", "src/pages/**", "src/app/**", "src/views/**", "src/styles/**", "public/**"] },
105
+ { id: "tests", role: "Swarm Tests Agent", patterns: ["test/**", "tests/**", "spec/**", "__tests__/**", "*.test.*", "*.spec.*"] },
106
+ { id: "infra", role: "Swarm Infra Agent", patterns: [".github/**", "*.md", "*.json", "*.yaml", "*.yml", "*.toml", ".eslintrc*", ".prettierrc*", "Makefile", "Dockerfile"] },
107
+ ],
108
+ mcp: [
109
+ { id: "backend", role: "Swarm Backend Agent", patterns: ["src/**", "lib/**", "*.mjs", "*.js", "*.ts"] },
110
+ { id: "tests", role: "Swarm Tests Agent", patterns: ["test/**", "tests/**", "spec/**", "__tests__/**", "*.test.*", "*.spec.*"] },
111
+ { id: "infra", role: "Swarm Infra Agent", patterns: [".github/**", "*.md", "*.json", "*.yaml", "*.yml", "*.toml", ".eslintrc*", ".prettierrc*"] },
112
+ ],
113
+ };
114
+
115
+ /**
116
+ * Generate a swarm manifest for the given repo.
117
+ * @param {string} cwd - Repository root directory
118
+ * @param {object} [options]
119
+ * @param {string} [options.repoType] - Override auto-detected repo type
120
+ * @returns {object} Swarm manifest
121
+ */
122
+ export function generateSwarmManifest(cwd, options = {}) {
123
+ const repoType = options.repoType || detectRepoType(cwd);
124
+ const templates = DOMAIN_TEMPLATES[repoType] || DOMAIN_TEMPLATES.cli;
125
+
126
+ // Build domains from templates
127
+ const domains = templates.map((tmpl, idx) => ({
128
+ id: tmpl.id,
129
+ role: tmpl.role,
130
+ patterns: tmpl.patterns,
131
+ agentSlot: idx,
132
+ }));
133
+
134
+ // Read repo metadata
135
+ const pkg = readPackageJson(cwd);
136
+ const repoName = pkg.name || cwd.split(/[/\\]/).pop();
137
+
138
+ return {
139
+ version: "1.0",
140
+ repo: repoName,
141
+ repoType,
142
+ domains,
143
+ stages: ["health-a", "health-b", "health-c", "feature"],
144
+ exclusiveOwnership: {
145
+ mode: "strict",
146
+ maxAgentsPerWave: domains.length,
147
+ },
148
+ };
149
+ }
150
+
151
+ /**
152
+ * Validate a swarm manifest for correctness.
153
+ * @param {object} manifest
154
+ * @returns {{ valid: boolean, issues: string[] }}
155
+ */
156
+ export function validateSwarmManifest(manifest) {
157
+ const issues = [];
158
+
159
+ if (!manifest) {
160
+ issues.push("Manifest is null or undefined");
161
+ return { valid: false, issues };
162
+ }
163
+
164
+ if (!manifest.version) issues.push("Missing version");
165
+ if (!manifest.repo) issues.push("Missing repo");
166
+ if (!Array.isArray(manifest.domains)) {
167
+ issues.push("Missing or invalid domains array");
168
+ return { valid: false, issues };
169
+ }
170
+
171
+ if (manifest.domains.length === 0) {
172
+ issues.push("No domains defined — need at least 1");
173
+ }
174
+
175
+ if (manifest.domains.length > 10) {
176
+ issues.push(`Too many domains (${manifest.domains.length}) — max 10`);
177
+ }
178
+
179
+ // Check for empty domains
180
+ for (const domain of manifest.domains) {
181
+ if (!domain.id) issues.push("Domain missing id");
182
+ if (!domain.role) issues.push(`Domain ${domain.id || "?"} missing role`);
183
+ if (!Array.isArray(domain.patterns) || domain.patterns.length === 0) {
184
+ issues.push(`Domain ${domain.id || "?"} has no patterns`);
185
+ }
186
+ }
187
+
188
+ // Check for duplicate domain IDs
189
+ const ids = manifest.domains.map(d => d.id);
190
+ const uniqueIds = new Set(ids);
191
+ if (uniqueIds.size !== ids.length) {
192
+ issues.push("Duplicate domain IDs found");
193
+ }
194
+
195
+ // Check for pattern overlaps (simple heuristic — exact pattern match)
196
+ const allPatterns = [];
197
+ for (const domain of manifest.domains) {
198
+ for (const pattern of domain.patterns || []) {
199
+ const existing = allPatterns.find(p => p.pattern === pattern);
200
+ if (existing) {
201
+ issues.push(`Pattern "${pattern}" claimed by both ${existing.domain} and ${domain.id}`);
202
+ }
203
+ allPatterns.push({ pattern, domain: domain.id });
204
+ }
205
+ }
206
+
207
+ // Check stages
208
+ if (!Array.isArray(manifest.stages) || manifest.stages.length === 0) {
209
+ issues.push("Missing or empty stages array");
210
+ }
211
+
212
+ return { valid: issues.length === 0, issues };
213
+ }
214
+
215
+ // ── Helpers ───────────────────────────────────────────────────────────────────
216
+
217
+ function readPackageJson(cwd) {
218
+ const pkgPath = join(cwd, "package.json");
219
+ if (!existsSync(pkgPath)) return {};
220
+ try {
221
+ return JSON.parse(readFileSync(pkgPath, "utf8"));
222
+ } catch {
223
+ return {};
224
+ }
225
+ }
226
+
227
+ function hasPackageWorkspaces(cwd) {
228
+ const pkg = readPackageJson(cwd);
229
+ return Array.isArray(pkg.workspaces) || (pkg.workspaces && pkg.workspaces.packages);
230
+ }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Evidence Persistence Bridge — Optional connection to dogfood-labs.
3
+ *
4
+ * Converts swarm wave results into dogfood submission format and audit DB
5
+ * payloads. The core swarm mission works without this — it's activated by
6
+ * the --persist-evidence flag on `roleos swarm`.
7
+ *
8
+ * This mirrors the logic from dogfood-labs/tools/swarm/persist-results.js
9
+ * but produces the payloads without requiring dogfood-labs to be present.
10
+ */
11
+
12
+ // ── Surface mapping ─────────────────────────────────────────────────────────
13
+
14
+ /**
15
+ * Map a domain ID to a product surface string for dogfood submissions.
16
+ * @param {string} domainId - e.g. "backend", "frontend", "tests"
17
+ * @returns {string}
18
+ */
19
+ export function surfaceFromDomain(domainId) {
20
+ const map = {
21
+ backend: "cli",
22
+ bridge: "cli",
23
+ tests: "cli",
24
+ infra: "cli",
25
+ frontend: "web",
26
+ };
27
+ return map[domainId] || "cli";
28
+ }
29
+
30
+ // ── Verdict derivation ──────────────────────────────────────────────────────
31
+
32
+ /**
33
+ * Derive a verdict from findings.
34
+ * @param {{ severity: string, status?: string }[]} findings
35
+ * @returns {"pass"|"partial"|"fail"}
36
+ */
37
+ export function deriveVerdict(findings) {
38
+ if (!findings || findings.length === 0) return "pass";
39
+
40
+ const open = findings.filter(f => f.status !== "fixed" && f.status !== "accepted_risk");
41
+ const hasCritical = open.some(f => f.severity === "critical");
42
+ const hasHigh = open.some(f => f.severity === "high");
43
+
44
+ if (hasCritical) return "fail";
45
+ if (hasHigh) return "partial";
46
+ return "pass";
47
+ }
48
+
49
+ // ── Scenario results ────────────────────────────────────────────────────────
50
+
51
+ /**
52
+ * Build per-domain scenario results from wave reports.
53
+ * @param {object[]} waveReports - Array of { domain, stage, findings, remediations }
54
+ * @returns {object[]}
55
+ */
56
+ export function buildScenarioResults(waveReports) {
57
+ // Group by domain
58
+ const byDomain = {};
59
+ for (const wr of waveReports) {
60
+ if (!byDomain[wr.domain]) byDomain[wr.domain] = [];
61
+ byDomain[wr.domain].push(wr);
62
+ }
63
+
64
+ return Object.entries(byDomain).map(([domain, reports]) => {
65
+ const allFindings = reports.flatMap(r => r.findings || []);
66
+ const allRemediations = reports.flatMap(r => r.remediations || []);
67
+
68
+ return {
69
+ scenario_id: `swarm-${domain}`,
70
+ product_surface: surfaceFromDomain(domain),
71
+ verdict: deriveVerdict(allFindings),
72
+ step_results: [
73
+ { step: "audit", status: allFindings.length > 0 ? "pass" : "pass" },
74
+ { step: "remediate", status: allRemediations.length > 0 ? "pass" : "skip" },
75
+ ],
76
+ evidence: {
77
+ total_findings: allFindings.length,
78
+ open_findings: allFindings.filter(f => f.status !== "fixed").length,
79
+ fixed: allFindings.filter(f => f.status === "fixed").length,
80
+ severities: {
81
+ critical: allFindings.filter(f => f.severity === "critical").length,
82
+ high: allFindings.filter(f => f.severity === "high").length,
83
+ medium: allFindings.filter(f => f.severity === "medium").length,
84
+ low: allFindings.filter(f => f.severity === "low").length,
85
+ },
86
+ },
87
+ };
88
+ });
89
+ }
90
+
91
+ // ── Overall verdict ─────────────────────────────────────────────────────────
92
+
93
+ /**
94
+ * Compute overall verdict from scenario results.
95
+ * @param {object[]} scenarioResults
96
+ * @returns {"pass"|"partial"|"fail"}
97
+ */
98
+ export function computeOverallVerdict(scenarioResults) {
99
+ if (scenarioResults.some(s => s.verdict === "fail")) return "fail";
100
+ if (scenarioResults.some(s => s.verdict === "partial")) return "partial";
101
+ return "pass";
102
+ }
103
+
104
+ // ── Dogfood submission payload ──────────────────────────────────────────────
105
+
106
+ /**
107
+ * Build a dogfood-labs-compatible submission payload.
108
+ * @param {object} manifest - Swarm manifest
109
+ * @param {object[]} waveReports - All wave reports from the run
110
+ * @param {object} meta - { commitSha, branch, startedAt, completedAt }
111
+ * @returns {object} Dogfood submission payload
112
+ */
113
+ export function buildSubmission(manifest, waveReports, meta = {}) {
114
+ const scenarios = buildScenarioResults(waveReports);
115
+ const overall = computeOverallVerdict(scenarios);
116
+
117
+ return {
118
+ repo: manifest.repo || "unknown",
119
+ product_surface: surfaceFromDomain(manifest.domains?.[0]?.id || "backend"),
120
+ execution_mode: "automated",
121
+ overall_verdict: overall,
122
+ scenario_results: scenarios,
123
+ metadata: {
124
+ commit_sha: meta.commitSha || "unknown",
125
+ branch: meta.branch || "unknown",
126
+ started_at: meta.startedAt || new Date().toISOString(),
127
+ completed_at: meta.completedAt || new Date().toISOString(),
128
+ tool: "role-os-swarm",
129
+ tool_version: "1.0.0",
130
+ },
131
+ };
132
+ }
133
+
134
+ // ── Audit DB payload ────────────────────────────────────────────────────────
135
+
136
+ /**
137
+ * Build an audit-DB-compatible payload from wave reports.
138
+ * @param {object} manifest - Swarm manifest
139
+ * @param {object[]} waveReports - All wave reports
140
+ * @param {object} meta - { commitSha, branch }
141
+ * @returns {object}
142
+ */
143
+ export function buildAuditPayload(manifest, waveReports, meta = {}) {
144
+ const allFindings = waveReports.flatMap(r => r.findings || []);
145
+ const fixed = allFindings.filter(f => f.status === "fixed").length;
146
+
147
+ const severities = {
148
+ critical: allFindings.filter(f => f.severity === "critical").length,
149
+ high: allFindings.filter(f => f.severity === "high").length,
150
+ medium: allFindings.filter(f => f.severity === "medium").length,
151
+ low: allFindings.filter(f => f.severity === "low").length,
152
+ };
153
+
154
+ const scenarios = buildScenarioResults(waveReports);
155
+ const overall = computeOverallVerdict(scenarios);
156
+
157
+ return {
158
+ run: {
159
+ slug: `swarm-${manifest.repo || "unknown"}`,
160
+ commit_sha: meta.commitSha || "unknown",
161
+ scope_level: "full",
162
+ overall_status: fixed === allFindings.length ? "pass" : overall,
163
+ overall_posture: overall,
164
+ blocking_release: severities.critical > 0 || severities.high > 0,
165
+ },
166
+ findings: allFindings,
167
+ metrics: {
168
+ total_findings: allFindings.length,
169
+ ...severities,
170
+ fixed,
171
+ pass_rate: allFindings.length > 0 ? Math.round((fixed / allFindings.length) * 100) : 100,
172
+ },
173
+ };
174
+ }