role-os 2.2.1 → 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.
package/src/mission.mjs CHANGED
@@ -327,6 +327,132 @@ export const MISSIONS = {
327
327
  dispatchDefaults: { model: "sonnet", maxTurns: 25, maxBudgetUsd: 3.0 },
328
328
  trialEvidence: "New mission — no trial evidence yet. Architecture designed 2026-03-27.",
329
329
  },
330
+
331
+ // ── Dogfood Swarm (Multi-Pass Health + Feature Convergence) ─────────────────
332
+ "dogfood-swarm": {
333
+ name: "Dogfood Swarm",
334
+ description: "Three-stage health pass (bug/security → proactive → humanization) then iterative feature pass with exclusive file ownership, build gates, and user checkpoints. Moves a repo from 'works' to 'production-ready.' Domain agent count scales with repo structure.",
335
+ pack: "swarm",
336
+ entryPath: "Generate swarm manifest → Save-point tag → Health-A wave (5 agents parallel, loop until 0 CRITICAL+HIGH) → Health-B wave (proactive, user review) → Health-C wave (humanization, loop) → Feature wave (user approval gate, loop) → Synthesizer → Critic verdict",
337
+ roleChain: [
338
+ "Swarm Coordinator", // ×1 — orchestrates all stages, enforces gates
339
+ "Swarm Backend Agent", // ×1 — exclusive ownership of backend files
340
+ "Swarm Bridge Agent", // ×1 — exclusive ownership of bridge/integration files
341
+ "Swarm Tests Agent", // ×1 — exclusive ownership of test files
342
+ "Swarm Infra Agent", // ×1 — exclusive ownership of CI/config/docs
343
+ "Swarm Frontend Agent", // ×1 — exclusive ownership of frontend files
344
+ "Swarm Synthesizer", // ×1 — final verification report
345
+ "Critic Reviewer", // ×1 — final acceptance
346
+ ],
347
+ // Dynamic dispatch contract:
348
+ // swarm-manifest.json defines domains[] with non-overlapping file paths.
349
+ // Each domain maps to one of the 5 domain agent roles.
350
+ // Domains are instantiated per stage (health-a, health-b, health-c, feature).
351
+ // Coordinator gates between stages evaluate exit conditions.
352
+ dynamicDispatch: {
353
+ scalingRoles: ["Swarm Backend Agent", "Swarm Bridge Agent", "Swarm Tests Agent", "Swarm Infra Agent", "Swarm Frontend Agent"],
354
+ manifestSource: "swarm-manifest.json",
355
+ domainAgentPer: "domains",
356
+ coordinatorAfter: "each-stage",
357
+ synthesisAfter: "all-stages",
358
+ },
359
+ // Wave loops — iterative convergence (new primitive, unique to swarm)
360
+ waveLoops: [
361
+ {
362
+ stage: "health-a",
363
+ lens: "Bug/Security Fix — audit for bugs, security, quality, types, test coverage, doc accuracy",
364
+ exitCondition: "0 CRITICAL + 0 HIGH findings open",
365
+ maxIterations: 4,
366
+ buildGate: true,
367
+ userApproval: false,
368
+ },
369
+ {
370
+ stage: "health-b",
371
+ lens: "Proactive Health — defensive coding, observability, graceful degradation, future-proofing",
372
+ exitCondition: "user approves proactive findings",
373
+ maxIterations: 2,
374
+ buildGate: true,
375
+ userApproval: true,
376
+ },
377
+ {
378
+ stage: "health-c",
379
+ lens: "Humanization — error messages that help users fix problems, reconnection/retry feedback, responsive layouts, loading states, state persistence, accessibility",
380
+ exitCondition: "0 CRITICAL + 0 HIGH humanization findings open",
381
+ maxIterations: 3,
382
+ buildGate: true,
383
+ userApproval: false,
384
+ },
385
+ {
386
+ stage: "feature",
387
+ lens: "Feature Audit — missing capabilities, feature gaps, UX, production readiness",
388
+ exitCondition: "user approves feature audit + 0 CRITICAL feature gaps",
389
+ maxIterations: 5,
390
+ buildGate: true,
391
+ userApproval: true,
392
+ },
393
+ ],
394
+ // Exclusive ownership — domain file boundaries (new primitive, unique to swarm)
395
+ exclusiveOwnership: {
396
+ mode: "strict",
397
+ manifestSource: "swarm-manifest.json",
398
+ maxAgentsPerWave: 5,
399
+ },
400
+ artifactFlow: [
401
+ // Health-A: Bug/Security audit + remediate (loops)
402
+ { role: "Swarm Backend Agent", produces: "wave-report", consumedBy: "Swarm Coordinator", stage: "health-a" },
403
+ { role: "Swarm Bridge Agent", produces: "wave-report", consumedBy: "Swarm Coordinator", stage: "health-a" },
404
+ { role: "Swarm Tests Agent", produces: "wave-report", consumedBy: "Swarm Coordinator", stage: "health-a" },
405
+ { role: "Swarm Infra Agent", produces: "wave-report", consumedBy: "Swarm Coordinator", stage: "health-a" },
406
+ { role: "Swarm Frontend Agent", produces: "wave-report", consumedBy: "Swarm Coordinator", stage: "health-a" },
407
+ { role: "Swarm Coordinator", produces: "swarm-gate", consumedBy: "Swarm Backend Agent", stage: "health-a" },
408
+
409
+ // Health-B: Proactive hardening (user review gate)
410
+ { role: "Swarm Backend Agent", produces: "wave-report", consumedBy: "Swarm Coordinator", stage: "health-b" },
411
+ { role: "Swarm Bridge Agent", produces: "wave-report", consumedBy: "Swarm Coordinator", stage: "health-b" },
412
+ { role: "Swarm Tests Agent", produces: "wave-report", consumedBy: "Swarm Coordinator", stage: "health-b" },
413
+ { role: "Swarm Infra Agent", produces: "wave-report", consumedBy: "Swarm Coordinator", stage: "health-b" },
414
+ { role: "Swarm Frontend Agent", produces: "wave-report", consumedBy: "Swarm Coordinator", stage: "health-b" },
415
+ { role: "Swarm Coordinator", produces: "swarm-gate", consumedBy: "Swarm Backend Agent", stage: "health-b" },
416
+
417
+ // Health-C: Humanization (UX emphasis, loops)
418
+ { role: "Swarm Backend Agent", produces: "wave-report", consumedBy: "Swarm Coordinator", stage: "health-c" },
419
+ { role: "Swarm Bridge Agent", produces: "wave-report", consumedBy: "Swarm Coordinator", stage: "health-c" },
420
+ { role: "Swarm Tests Agent", produces: "wave-report", consumedBy: "Swarm Coordinator", stage: "health-c" },
421
+ { role: "Swarm Infra Agent", produces: "wave-report", consumedBy: "Swarm Coordinator", stage: "health-c" },
422
+ { role: "Swarm Frontend Agent", produces: "wave-report", consumedBy: "Swarm Coordinator", stage: "health-c" },
423
+ { role: "Swarm Coordinator", produces: "swarm-gate", consumedBy: "Swarm Backend Agent", stage: "health-c" },
424
+
425
+ // Feature: Audit → user approval → execute (loops)
426
+ { role: "Swarm Backend Agent", produces: "wave-report", consumedBy: "Swarm Coordinator", stage: "feature" },
427
+ { role: "Swarm Bridge Agent", produces: "wave-report", consumedBy: "Swarm Coordinator", stage: "feature" },
428
+ { role: "Swarm Tests Agent", produces: "wave-report", consumedBy: "Swarm Coordinator", stage: "feature" },
429
+ { role: "Swarm Infra Agent", produces: "wave-report", consumedBy: "Swarm Coordinator", stage: "feature" },
430
+ { role: "Swarm Frontend Agent", produces: "wave-report", consumedBy: "Swarm Coordinator", stage: "feature" },
431
+ { role: "Swarm Coordinator", produces: "swarm-gate", consumedBy: "Swarm Synthesizer", stage: "feature" },
432
+
433
+ // Final: Synthesize + Critic verdict
434
+ { role: "Swarm Synthesizer", produces: "swarm-final-report", consumedBy: "Critic Reviewer", stage: "final" },
435
+ { role: "Critic Reviewer", produces: "review-verdict", consumedBy: null, stage: "final" },
436
+ ],
437
+ escalationBranches: [
438
+ { trigger: "build gate fails after remediation", from: "Swarm Coordinator", to: "Swarm Coordinator", action: "halt stage, report which agent's changes broke the build" },
439
+ { trigger: "domain agent touches files outside its assignment", from: "Swarm Coordinator", to: "Swarm Coordinator", action: "reject wave-report, re-run agent with strict boundary warning" },
440
+ { trigger: "finding spans multiple domains", from: "Swarm Coordinator", to: "Swarm Coordinator", action: "assign to the domain with most file overlap, note cross-domain in finding" },
441
+ { trigger: "health pass stuck at max iterations", from: "Swarm Coordinator", to: "Swarm Synthesizer", action: "synthesize with partial health — document remaining CRITICAL/HIGH" },
442
+ { trigger: "feature audit finds no gaps", from: "Swarm Coordinator", to: "Swarm Synthesizer", action: "skip feature execution, advance to final synthesis" },
443
+ { trigger: "user rejects feature audit", from: "Swarm Coordinator", to: "Swarm Coordinator", action: "re-scope feature audit with user feedback, re-run" },
444
+ ],
445
+ honestPartial: "One or more health stages complete but feature pass blocked or incomplete. Per-stage findings are individually valid and actionable. Manifest and wave reports exist even if synthesis does not. Build gate status is known.",
446
+ stopConditions: [
447
+ "All four stages converge — Synthesizer produces final report, Critic accepts",
448
+ "Health pass stuck after max iterations — synthesize with partial health findings",
449
+ "Feature pass stuck after max iterations — synthesize with partial feature progress",
450
+ "Build gate fails repeatedly — stop and report infrastructure issue",
451
+ "User halts swarm — synthesize from completed stages",
452
+ ],
453
+ dispatchDefaults: { model: "sonnet", maxTurns: 40, maxBudgetUsd: 6.0 },
454
+ trialEvidence: "Proven on claude-collaborate (2026-03-28): 35→129 tests, 106 health findings fixed, v1.1.0 shipped. Protocol v2.0.",
455
+ },
330
456
  };
331
457
 
332
458
  // ── Mission catalog ─────────────────────────────────────────────────────────
@@ -395,6 +521,10 @@ export function suggestMission(taskDescription) {
395
521
  signals: ["deep audit", "component audit", "decompose and audit", "audit components", "structural audit", "deep review", "code audit", "repo deep dive"],
396
522
  weight: 1.2,
397
523
  },
524
+ "dogfood-swarm": {
525
+ signals: ["dogfood swarm", "swarm", "health pass", "multi-pass", "convergence", "full quality pass", "production ready", "wave-based audit", "swarm this repo"],
526
+ weight: 1.3,
527
+ },
398
528
  };
399
529
 
400
530
  let bestKey = null;
package/src/packs.mjs CHANGED
@@ -323,6 +323,42 @@ export const TEAM_PACKS = {
323
323
  { notForSignals: ["handbook", "documentation", "restructure docs"], suggestInstead: "docs", reason: "This is docs work, not a brainstorm" },
324
324
  ],
325
325
  },
326
+
327
+ // ── Dogfood Swarm (Multi-Pass Health + Feature Convergence) ─────────────────
328
+ swarm: {
329
+ name: "Dogfood Swarm (Multi-Pass Convergence)",
330
+ description: "Three-stage health pass (bug/security → proactive → humanization) then iterative feature pass, all with exclusive file ownership, build gates, and user checkpoints. Moves a repo from 'works' to 'production-ready.'",
331
+ roles: [
332
+ "Swarm Coordinator",
333
+ "Swarm Backend Agent",
334
+ "Swarm Bridge Agent",
335
+ "Swarm Tests Agent",
336
+ "Swarm Infra Agent",
337
+ "Swarm Frontend Agent",
338
+ "Swarm Synthesizer",
339
+ "Critic Reviewer",
340
+ ],
341
+ orchestratorRequired: false,
342
+ optionalRoles: [],
343
+ chainOrder: "Coordinator → [5 domain agents parallel] → Coordinator gate (repeat per stage: health-a, health-b, health-c, feature) → Synthesizer → Critic Reviewer",
344
+ requiredArtifacts: ["swarm-gate", "wave-report", "swarm-final-report", "review-verdict"],
345
+ stopConditions: [
346
+ "All four stages converge — Synthesizer produces final report, Critic accepts",
347
+ "Health pass stuck after max iterations — synthesize with partial health findings",
348
+ "Feature pass stuck after max iterations — synthesize with partial feature progress",
349
+ "Build gate fails repeatedly — stop and report infrastructure issue",
350
+ ],
351
+ escalationOwner: "Swarm Coordinator",
352
+ dispatchDefaults: { model: "sonnet", maxTurns: 40, maxBudgetUsd: 6.0 },
353
+ trialEvidence: "Proven on claude-collaborate (2026-03-28): 35→129 tests, 106 health findings fixed, v1.1.0 shipped. Protocol v2.0.",
354
+ mismatchGuards: [
355
+ { notForSignals: ["fix bug", "single bug", "one crash", "quick fix"], suggestInstead: "bugfix", reason: "This is a single bug to fix, not a full swarm" },
356
+ { notForSignals: ["brainstorm", "explore ideas", "concept", "ideate"], suggestInstead: "brainstorm", reason: "This is exploration, not convergence work" },
357
+ { notForSignals: ["research", "competitive analysis", "user research"], suggestInstead: "research", reason: "This is research, not repo health work" },
358
+ { notForSignals: ["launch", "announce", "release notes", "go-to-market"], suggestInstead: "launch", reason: "This is launch work, not repo health" },
359
+ { notForSignals: ["handbook", "documentation only", "restructure docs"], suggestInstead: "docs", reason: "This is docs work — swarm is for full repo convergence" },
360
+ ],
361
+ },
326
362
  };
327
363
 
328
364
  // ── Pack selection ────────────────────────────────────────────────────────────
@@ -337,6 +373,7 @@ const PACK_KEYWORDS = {
337
373
  treatment: ["treatment", "polish", "cleanup", "repo audit", "shipcheck", "full treatment"],
338
374
  brainstorm: ["brainstorm", "explore", "ideate", "divergent", "opportunity", "creative directions", "concept exploration", "what could", "possibilities"],
339
375
  "deep-audit": ["deep audit", "component audit", "repo audit deep", "decompose and audit", "audit components", "code audit", "structural audit", "deep review"],
376
+ swarm: ["swarm", "dogfood", "health pass", "multi-pass", "convergence", "wave", "full quality", "production ready", "dogfood swarm"],
340
377
  };
341
378
 
342
379
  /**
package/src/route.mjs CHANGED
@@ -374,6 +374,57 @@ export const ROLE_CATALOG = [
374
374
  excludeWhen: ["component audit still running", "no findings to synthesize"],
375
375
  deliverableAffinity: ["Review"],
376
376
  },
377
+
378
+ // ── Dogfood Swarm (Multi-Pass Health + Feature Convergence) ─────────────────
379
+ {
380
+ name: "Swarm Coordinator", pack: "swarm", phase: 0,
381
+ keywords: ["swarm", "wave", "gate", "convergence", "health pass", "dogfood"],
382
+ triggers: ["dogfood swarm", "swarm coordinator", "wave orchestration", "health gate"],
383
+ excludeWhen: ["single bug fix", "brainstorm only", "docs only"],
384
+ deliverableAffinity: ["Review"],
385
+ },
386
+ {
387
+ name: "Swarm Backend Agent", pack: "swarm", phase: 3,
388
+ keywords: ["backend", "server", "core logic", "api", "database", "service"],
389
+ triggers: ["swarm backend", "backend audit and fix", "server health"],
390
+ excludeWhen: ["frontend only", "docs only", "test only"],
391
+ deliverableAffinity: ["Code", "Review"],
392
+ },
393
+ {
394
+ name: "Swarm Bridge Agent", pack: "swarm", phase: 3,
395
+ keywords: ["bridge", "integration", "websocket", "middleware", "adapter", "protocol"],
396
+ triggers: ["swarm bridge", "bridge audit and fix", "integration health"],
397
+ excludeWhen: ["no secondary services", "single module repo"],
398
+ deliverableAffinity: ["Code", "Review"],
399
+ },
400
+ {
401
+ name: "Swarm Tests Agent", pack: "swarm", phase: 3,
402
+ keywords: ["test", "coverage", "fixture", "mock", "assertion", "spec"],
403
+ triggers: ["swarm tests", "test audit and fix", "test health"],
404
+ excludeWhen: ["no test suite", "implementation only"],
405
+ deliverableAffinity: ["Test", "Review"],
406
+ },
407
+ {
408
+ name: "Swarm Infra Agent", pack: "swarm", phase: 3,
409
+ keywords: ["ci", "workflow", "config", "docs", "readme", "changelog", "infrastructure"],
410
+ triggers: ["swarm infra", "infra audit and fix", "ci health", "docs health"],
411
+ excludeWhen: ["code only", "no ci"],
412
+ deliverableAffinity: ["Review"],
413
+ },
414
+ {
415
+ name: "Swarm Frontend Agent", pack: "swarm", phase: 3,
416
+ keywords: ["frontend", "ui", "component", "css", "html", "react", "view"],
417
+ triggers: ["swarm frontend", "frontend audit and fix", "ui health"],
418
+ excludeWhen: ["no frontend", "cli only", "backend only"],
419
+ deliverableAffinity: ["Code", "Review"],
420
+ },
421
+ {
422
+ name: "Swarm Synthesizer", pack: "swarm", phase: 5,
423
+ keywords: ["synthesis", "final report", "verification", "summary", "recommendation"],
424
+ triggers: ["swarm synthesis", "swarm final report", "swarm verification"],
425
+ excludeWhen: ["swarm still running", "no wave results"],
426
+ deliverableAffinity: ["Review"],
427
+ },
377
428
  ];
378
429
 
379
430
  // ── Deliverable type → role affinity ──────────────────────────────────────────
package/src/run-cmd.mjs CHANGED
@@ -109,13 +109,16 @@ export async function runCommand(args) {
109
109
  // Strip flags from task description
110
110
  const taskText = args.filter(a => !a.startsWith("--")).join(" ");
111
111
 
112
- const run = createPersistentRun(taskText, cwd, opts);
112
+ const run = await createPersistentRun(taskText, cwd, opts);
113
113
 
114
114
  console.log(`Created run: ${run.id}`);
115
115
  console.log(`Entry: ${run.entryLevel.toUpperCase()}`);
116
116
  if (run.missionKey) console.log(`Mission: ${run.missionKey}`);
117
117
  if (run.packKey) console.log(`Pack: ${run.packKey}`);
118
118
  console.log(`Steps: ${run.steps.length}`);
119
+ if (run.knowledge) {
120
+ console.log(`Knowledge: ${run.knowledge.status} (${run.knowledge.retrieval_bundle?.selected?.length ?? 0} chunks)`);
121
+ }
119
122
  console.log("");
120
123
 
121
124
  // Auto-start the first step
package/src/run.mjs CHANGED
@@ -19,6 +19,7 @@ import { getMission } from "./mission.mjs";
19
19
  import { TEAM_PACKS, getPack } from "./packs.mjs";
20
20
  import { ROLE_CATALOG } from "./route.mjs";
21
21
  import { ROLE_ARTIFACT_CONTRACTS, validateArtifact, getHandoffContract } from "./artifacts.mjs";
22
+ import { retrieveForDispatch, isKnowledgeConfigured } from "./knowledge/index.mjs";
22
23
 
23
24
  // ── Run directory ────────────────────────────────────────────────────────────
24
25
 
@@ -88,7 +89,7 @@ let _counter = 0;
88
89
  * @param {string} [opts.forcePack] - force a specific pack key
89
90
  * @returns {PersistentRun}
90
91
  */
91
- export function createPersistentRun(taskDescription, cwd, opts = {}) {
92
+ export async function createPersistentRun(taskDescription, cwd, opts = {}) {
92
93
  if (!taskDescription || !taskDescription.trim()) {
93
94
  throw new Error("Task description required");
94
95
  }
@@ -125,6 +126,26 @@ export function createPersistentRun(taskDescription, cwd, opts = {}) {
125
126
 
126
127
  const id = `run-${Date.now()}-${++_counter}`;
127
128
 
129
+ // Knowledge retrieval — automatic when corpus is configured
130
+ let knowledge = null;
131
+ if (isKnowledgeConfigured()) {
132
+ // Retrieve for the primary role in the chain (first step's role)
133
+ const primaryRole = steps[0]?.role;
134
+ if (primaryRole) {
135
+ try {
136
+ const roleId = primaryRole.toLowerCase().replace(/\s+/g, "-");
137
+ const result = await retrieveForDispatch({
138
+ roleId,
139
+ taskText: taskDescription.trim(),
140
+ });
141
+ knowledge = { retrieval_bundle: result.bundle, status: result.status };
142
+ } catch (e) {
143
+ // Retrieval failure is non-fatal — run proceeds without knowledge
144
+ knowledge = null;
145
+ }
146
+ }
147
+ }
148
+
128
149
  const run = {
129
150
  id,
130
151
  taskDescription: taskDescription.trim(),
@@ -140,6 +161,7 @@ export function createPersistentRun(taskDescription, cwd, opts = {}) {
140
161
  pausedAt: null,
141
162
  completedAt: null,
142
163
  completionReport: null,
164
+ knowledge,
143
165
  };
144
166
 
145
167
  // Persist
@@ -789,6 +811,13 @@ export function generateReport(run) {
789
811
  artifactChain: artifacts,
790
812
  escalationCount: run.escalations.length,
791
813
  interventionCount: run.interventions.length,
814
+ knowledge: run.knowledge ? {
815
+ status: run.knowledge.status,
816
+ selected_count: run.knowledge.retrieval_bundle?.selected?.length ?? 0,
817
+ trust_posture: run.knowledge.retrieval_bundle?.provenance?.trust_posture ?? "unknown",
818
+ freshness_posture: run.knowledge.retrieval_bundle?.provenance?.freshness_posture ?? "unknown",
819
+ warning_codes: (run.knowledge.retrieval_bundle?.warnings ?? []).map((w) => w.code),
820
+ } : null,
792
821
  honestPartial: (isPartial || isFailed) ? honestPartial : null,
793
822
  verdict: isComplete
794
823
  ? "Run completed — all steps passed."
@@ -835,6 +864,18 @@ export function formatReport(report) {
835
864
  lines.push(` ${icon} ${step.index}. ${step.role}${artifact}${note}`);
836
865
  }
837
866
 
867
+ // Knowledge posture (Phase 5)
868
+ if (report.knowledge) {
869
+ lines.push("");
870
+ lines.push("## Knowledge");
871
+ lines.push(` Status: ${report.knowledge.status}`);
872
+ lines.push(` Evidence: ${report.knowledge.selected_count} chunks selected`);
873
+ lines.push(` Trust: ${report.knowledge.trust_posture} | Freshness: ${report.knowledge.freshness_posture}`);
874
+ if (report.knowledge.warning_codes.length > 0) {
875
+ lines.push(` Warnings: ${report.knowledge.warning_codes.join(", ")}`);
876
+ }
877
+ }
878
+
838
879
  if (report.honestPartial) {
839
880
  lines.push("");
840
881
  lines.push("## Honest Partial");
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Build Gate — Detects build system and runs lint/typecheck/test verification.
3
+ *
4
+ * After every swarm wave, the build gate runs to ensure changes didn't break anything.
5
+ * Auto-detects the build system from project files and runs appropriate commands.
6
+ */
7
+
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { execSync } from "node:child_process";
11
+
12
+ // ── Build system detection ──────────────────────────────────────────────────
13
+
14
+ /**
15
+ * Detect the build system and available verification commands.
16
+ * @param {string} cwd - Repository root directory
17
+ * @returns {{ type: string, lintCmd: string|null, typecheckCmd: string|null, testCmd: string|null }}
18
+ */
19
+ export function detectBuildSystem(cwd) {
20
+ // Node.js (package.json)
21
+ const pkgPath = join(cwd, "package.json");
22
+ if (existsSync(pkgPath)) {
23
+ try {
24
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
25
+ const scripts = pkg.scripts || {};
26
+ return {
27
+ type: "node",
28
+ lintCmd: scripts.lint ? "npm run lint" : null,
29
+ typecheckCmd: scripts.typecheck ? "npm run typecheck" : (scripts["type-check"] ? "npm run type-check" : null),
30
+ testCmd: scripts.test ? "npm test" : null,
31
+ };
32
+ } catch {
33
+ // Fall through
34
+ }
35
+ }
36
+
37
+ // Rust (Cargo.toml)
38
+ if (existsSync(join(cwd, "Cargo.toml"))) {
39
+ return {
40
+ type: "rust",
41
+ lintCmd: "cargo clippy --all-targets -- -D warnings",
42
+ typecheckCmd: "cargo check",
43
+ testCmd: "cargo test",
44
+ };
45
+ }
46
+
47
+ // Python (pyproject.toml or setup.py)
48
+ if (existsSync(join(cwd, "pyproject.toml")) || existsSync(join(cwd, "setup.py"))) {
49
+ const hasRuff = existsSync(join(cwd, "pyproject.toml")) &&
50
+ readFileSync(join(cwd, "pyproject.toml"), "utf8").includes("[tool.ruff]");
51
+ return {
52
+ type: "python",
53
+ lintCmd: hasRuff ? "ruff check ." : null,
54
+ typecheckCmd: null,
55
+ testCmd: "pytest",
56
+ };
57
+ }
58
+
59
+ // Go (go.mod)
60
+ if (existsSync(join(cwd, "go.mod"))) {
61
+ return {
62
+ type: "go",
63
+ lintCmd: "golangci-lint run",
64
+ typecheckCmd: "go vet ./...",
65
+ testCmd: "go test ./...",
66
+ };
67
+ }
68
+
69
+ return { type: "unknown", lintCmd: null, typecheckCmd: null, testCmd: null };
70
+ }
71
+
72
+ // ── Build gate execution ────────────────────────────────────────────────────
73
+
74
+ /**
75
+ * Run the build gate: lint → typecheck → test.
76
+ * @param {string} cwd - Repository root directory
77
+ * @param {object} [options]
78
+ * @param {object} [options.buildSystem] - Override auto-detected build system
79
+ * @param {number} [options.timeout] - Per-command timeout in ms (default: 120000)
80
+ * @returns {{ pass: boolean, lint: StepResult, typecheck: StepResult, test: StepResult, duration: number }}
81
+ *
82
+ * @typedef {{ status: "pass"|"fail"|"skip", output: string, duration: number }} StepResult
83
+ */
84
+ export function runBuildGate(cwd, options = {}) {
85
+ const bs = options.buildSystem || detectBuildSystem(cwd);
86
+ const timeout = options.timeout || 120_000;
87
+ const start = Date.now();
88
+
89
+ const lint = runStep(bs.lintCmd, cwd, timeout);
90
+ const typecheck = runStep(bs.typecheckCmd, cwd, timeout);
91
+ const test = runStep(bs.testCmd, cwd, timeout);
92
+
93
+ const pass = lint.status !== "fail" && typecheck.status !== "fail" && test.status !== "fail";
94
+
95
+ return {
96
+ pass,
97
+ lint,
98
+ typecheck,
99
+ test,
100
+ duration: Date.now() - start,
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Run a single build step.
106
+ * @param {string|null} cmd
107
+ * @param {string} cwd
108
+ * @param {number} timeout
109
+ * @returns {StepResult}
110
+ */
111
+ function runStep(cmd, cwd, timeout) {
112
+ if (!cmd) return { status: "skip", output: "", duration: 0 };
113
+
114
+ const start = Date.now();
115
+ try {
116
+ const output = execSync(cmd, {
117
+ cwd,
118
+ timeout,
119
+ encoding: "utf8",
120
+ stdio: ["pipe", "pipe", "pipe"],
121
+ });
122
+ return { status: "pass", output: output.slice(0, 2000), duration: Date.now() - start };
123
+ } catch (err) {
124
+ const output = (err.stdout || "") + "\n" + (err.stderr || "");
125
+ return { status: "fail", output: output.slice(0, 2000), duration: Date.now() - start };
126
+ }
127
+ }