@vibecheckai/cli 3.5.0 → 3.5.2
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/bin/registry.js +214 -237
- package/bin/runners/cli-utils.js +33 -2
- package/bin/runners/context/analyzer.js +52 -1
- package/bin/runners/context/generators/cursor.js +2 -49
- package/bin/runners/context/git-context.js +3 -1
- package/bin/runners/context/team-conventions.js +33 -7
- package/bin/runners/lib/analysis-core.js +25 -5
- package/bin/runners/lib/analyzers.js +431 -481
- package/bin/runners/lib/default-config.js +127 -0
- package/bin/runners/lib/doctor/modules/security.js +3 -1
- package/bin/runners/lib/engine/ast-cache.js +210 -0
- package/bin/runners/lib/engine/auth-extractor.js +211 -0
- package/bin/runners/lib/engine/billing-extractor.js +112 -0
- package/bin/runners/lib/engine/enforcement-extractor.js +100 -0
- package/bin/runners/lib/engine/env-extractor.js +207 -0
- package/bin/runners/lib/engine/express-extractor.js +208 -0
- package/bin/runners/lib/engine/extractors.js +849 -0
- package/bin/runners/lib/engine/index.js +207 -0
- package/bin/runners/lib/engine/repo-index.js +514 -0
- package/bin/runners/lib/engine/types.js +124 -0
- package/bin/runners/lib/engines/accessibility-engine.js +18 -218
- package/bin/runners/lib/engines/api-consistency-engine.js +30 -335
- package/bin/runners/lib/engines/cross-file-analysis-engine.js +27 -292
- package/bin/runners/lib/engines/empty-catch-engine.js +17 -127
- package/bin/runners/lib/engines/mock-data-engine.js +10 -53
- package/bin/runners/lib/engines/performance-issues-engine.js +36 -176
- package/bin/runners/lib/engines/security-vulnerabilities-engine.js +54 -382
- package/bin/runners/lib/engines/type-aware-engine.js +39 -263
- package/bin/runners/lib/engines/vibecheck-engines/index.js +13 -122
- package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +164 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +291 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +83 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +198 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +275 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +167 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +217 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +73 -373
- package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +140 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +164 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +234 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +217 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +78 -0
- package/bin/runners/lib/entitlements-v2.js +73 -97
- package/bin/runners/lib/error-handler.js +44 -3
- package/bin/runners/lib/error-messages.js +289 -0
- package/bin/runners/lib/evidence-pack.js +7 -1
- package/bin/runners/lib/finding-id.js +69 -0
- package/bin/runners/lib/finding-sorter.js +89 -0
- package/bin/runners/lib/html-proof-report.js +700 -350
- package/bin/runners/lib/missions/plan.js +6 -46
- package/bin/runners/lib/missions/templates.js +0 -232
- package/bin/runners/lib/next-action.js +560 -0
- package/bin/runners/lib/prerequisites.js +149 -0
- package/bin/runners/lib/route-detection.js +137 -68
- package/bin/runners/lib/scan-output.js +91 -76
- package/bin/runners/lib/scan-runner.js +135 -0
- package/bin/runners/lib/schemas/ajv-validator.js +464 -0
- package/bin/runners/lib/schemas/error-envelope.schema.json +105 -0
- package/bin/runners/lib/schemas/finding-v3.schema.json +151 -0
- package/bin/runners/lib/schemas/report-artifact.schema.json +120 -0
- package/bin/runners/lib/schemas/run-request.schema.json +108 -0
- package/bin/runners/lib/schemas/validator.js +27 -0
- package/bin/runners/lib/schemas/verdict.schema.json +140 -0
- package/bin/runners/lib/ship-output-enterprise.js +23 -23
- package/bin/runners/lib/ship-output.js +75 -31
- package/bin/runners/lib/terminal-ui.js +6 -113
- package/bin/runners/lib/truth.js +351 -10
- package/bin/runners/lib/unified-cli-output.js +430 -603
- package/bin/runners/lib/unified-output.js +13 -9
- package/bin/runners/runAIAgent.js +10 -5
- package/bin/runners/runAgent.js +0 -3
- package/bin/runners/runAllowlist.js +389 -0
- package/bin/runners/runApprove.js +0 -33
- package/bin/runners/runAuth.js +73 -45
- package/bin/runners/runCheckpoint.js +51 -11
- package/bin/runners/runClassify.js +85 -21
- package/bin/runners/runContext.js +0 -3
- package/bin/runners/runDoctor.js +41 -28
- package/bin/runners/runEvidencePack.js +362 -0
- package/bin/runners/runFirewall.js +0 -3
- package/bin/runners/runFirewallHook.js +0 -3
- package/bin/runners/runFix.js +66 -76
- package/bin/runners/runGuard.js +18 -411
- package/bin/runners/runInit.js +113 -30
- package/bin/runners/runLabs.js +424 -0
- package/bin/runners/runMcp.js +19 -25
- package/bin/runners/runPolish.js +64 -240
- package/bin/runners/runPromptFirewall.js +12 -5
- package/bin/runners/runProve.js +57 -22
- package/bin/runners/runQuickstart.js +531 -0
- package/bin/runners/runReality.js +59 -68
- package/bin/runners/runReport.js +38 -33
- package/bin/runners/runRuntime.js +8 -5
- package/bin/runners/runScan.js +1413 -190
- package/bin/runners/runShip.js +113 -719
- package/bin/runners/runTruth.js +0 -3
- package/bin/runners/runValidate.js +13 -9
- package/bin/runners/runWatch.js +23 -14
- package/bin/scan.js +6 -1
- package/bin/vibecheck.js +204 -185
- package/mcp-server/deprecation-middleware.js +282 -0
- package/mcp-server/handlers/index.ts +15 -0
- package/mcp-server/handlers/tool-handler.ts +554 -0
- package/mcp-server/index-v1.js +698 -0
- package/mcp-server/index.js +210 -238
- package/mcp-server/lib/cache-wrapper.cjs +383 -0
- package/mcp-server/lib/error-envelope.js +138 -0
- package/mcp-server/lib/executor.ts +499 -0
- package/mcp-server/lib/index.ts +19 -0
- package/mcp-server/lib/rate-limiter.js +166 -0
- package/mcp-server/lib/sandbox.test.ts +519 -0
- package/mcp-server/lib/sandbox.ts +395 -0
- package/mcp-server/lib/types.ts +267 -0
- package/mcp-server/package.json +12 -3
- package/mcp-server/registry/tool-registry.js +794 -0
- package/mcp-server/registry/tools.json +605 -0
- package/mcp-server/registry.test.ts +334 -0
- package/mcp-server/tests/tier-gating.test.js +297 -0
- package/mcp-server/tier-auth.js +378 -45
- package/mcp-server/tools-v3.js +353 -442
- package/mcp-server/tsconfig.json +37 -0
- package/mcp-server/vibecheck-2.0-tools.js +14 -1
- package/package.json +1 -1
- package/bin/runners/lib/agent-firewall/learning/learning-engine.js +0 -849
- package/bin/runners/lib/audit-logger.js +0 -532
- package/bin/runners/lib/authority/authorities/architecture.js +0 -364
- package/bin/runners/lib/authority/authorities/compliance.js +0 -341
- package/bin/runners/lib/authority/authorities/human.js +0 -343
- package/bin/runners/lib/authority/authorities/quality.js +0 -420
- package/bin/runners/lib/authority/authorities/security.js +0 -228
- package/bin/runners/lib/authority/index.js +0 -293
- package/bin/runners/lib/bundle/bundle-intelligence.js +0 -846
- package/bin/runners/lib/cli-charts.js +0 -368
- package/bin/runners/lib/cli-config-display.js +0 -405
- package/bin/runners/lib/cli-demo.js +0 -275
- package/bin/runners/lib/cli-errors.js +0 -438
- package/bin/runners/lib/cli-help-formatter.js +0 -439
- package/bin/runners/lib/cli-interactive-menu.js +0 -509
- package/bin/runners/lib/cli-prompts.js +0 -441
- package/bin/runners/lib/cli-scan-cards.js +0 -362
- package/bin/runners/lib/compliance-reporter.js +0 -710
- package/bin/runners/lib/conductor/index.js +0 -671
- package/bin/runners/lib/easy/README.md +0 -123
- package/bin/runners/lib/easy/index.js +0 -140
- package/bin/runners/lib/easy/interactive-wizard.js +0 -788
- package/bin/runners/lib/easy/one-click-firewall.js +0 -564
- package/bin/runners/lib/easy/zero-config-reality.js +0 -714
- package/bin/runners/lib/engines/async-patterns-engine.js +0 -444
- package/bin/runners/lib/engines/bundle-size-engine.js +0 -433
- package/bin/runners/lib/engines/confidence-scoring.js +0 -276
- package/bin/runners/lib/engines/context-detection.js +0 -264
- package/bin/runners/lib/engines/database-patterns-engine.js +0 -429
- package/bin/runners/lib/engines/duplicate-code-engine.js +0 -354
- package/bin/runners/lib/engines/env-variables-engine.js +0 -458
- package/bin/runners/lib/engines/error-handling-engine.js +0 -437
- package/bin/runners/lib/engines/false-positive-prevention.js +0 -630
- package/bin/runners/lib/engines/framework-adapters/index.js +0 -607
- package/bin/runners/lib/engines/framework-detection.js +0 -508
- package/bin/runners/lib/engines/import-order-engine.js +0 -429
- package/bin/runners/lib/engines/naming-conventions-engine.js +0 -544
- package/bin/runners/lib/engines/noise-reduction-engine.js +0 -452
- package/bin/runners/lib/engines/orchestrator.js +0 -334
- package/bin/runners/lib/engines/react-patterns-engine.js +0 -457
- package/bin/runners/lib/engines/vibecheck-engines/lib/ai-hallucination-engine.js +0 -806
- package/bin/runners/lib/engines/vibecheck-engines/lib/smart-fix-engine.js +0 -577
- package/bin/runners/lib/engines/vibecheck-engines/lib/vibe-score-engine.js +0 -543
- package/bin/runners/lib/engines/vibecheck-engines.js +0 -514
- package/bin/runners/lib/enhanced-features/index.js +0 -305
- package/bin/runners/lib/enhanced-output.js +0 -631
- package/bin/runners/lib/enterprise.js +0 -300
- package/bin/runners/lib/firewall/command-validator.js +0 -351
- package/bin/runners/lib/firewall/config.js +0 -341
- package/bin/runners/lib/firewall/content-validator.js +0 -519
- package/bin/runners/lib/firewall/index.js +0 -101
- package/bin/runners/lib/firewall/path-validator.js +0 -256
- package/bin/runners/lib/intelligence/cross-repo-intelligence.js +0 -817
- package/bin/runners/lib/mcp-utils.js +0 -425
- package/bin/runners/lib/output/index.js +0 -1022
- package/bin/runners/lib/policy-engine.js +0 -652
- package/bin/runners/lib/polish/autofix/accessibility-fixes.js +0 -333
- package/bin/runners/lib/polish/autofix/async-handlers.js +0 -273
- package/bin/runners/lib/polish/autofix/dead-code.js +0 -280
- package/bin/runners/lib/polish/autofix/imports-optimizer.js +0 -344
- package/bin/runners/lib/polish/autofix/index.js +0 -200
- package/bin/runners/lib/polish/autofix/remove-consoles.js +0 -209
- package/bin/runners/lib/polish/autofix/strengthen-types.js +0 -245
- package/bin/runners/lib/polish/backend-checks.js +0 -148
- package/bin/runners/lib/polish/documentation-checks.js +0 -111
- package/bin/runners/lib/polish/frontend-checks.js +0 -168
- package/bin/runners/lib/polish/index.js +0 -71
- package/bin/runners/lib/polish/infrastructure-checks.js +0 -131
- package/bin/runners/lib/polish/library-detection.js +0 -175
- package/bin/runners/lib/polish/performance-checks.js +0 -100
- package/bin/runners/lib/polish/security-checks.js +0 -148
- package/bin/runners/lib/polish/utils.js +0 -203
- package/bin/runners/lib/prompt-builder.js +0 -540
- package/bin/runners/lib/proof-certificate.js +0 -634
- package/bin/runners/lib/reality/accessibility-audit.js +0 -946
- package/bin/runners/lib/reality/api-contract-validator.js +0 -1012
- package/bin/runners/lib/reality/chaos-engineering.js +0 -1084
- package/bin/runners/lib/reality/performance-tracker.js +0 -1077
- package/bin/runners/lib/reality/scenario-generator.js +0 -1404
- package/bin/runners/lib/reality/visual-regression.js +0 -852
- package/bin/runners/lib/reality-profiler.js +0 -717
- package/bin/runners/lib/replay/flight-recorder-viewer.js +0 -1160
- package/bin/runners/lib/review/ai-code-review.js +0 -832
- package/bin/runners/lib/rules/custom-rule-engine.js +0 -985
- package/bin/runners/lib/sbom-generator.js +0 -641
- package/bin/runners/lib/scan-output-enhanced.js +0 -512
- package/bin/runners/lib/security/owasp-scanner.js +0 -939
- package/bin/runners/lib/validators/contract-validator.js +0 -283
- package/bin/runners/lib/validators/dead-export-detector.js +0 -279
- package/bin/runners/lib/validators/dep-audit.js +0 -245
- package/bin/runners/lib/validators/env-validator.js +0 -319
- package/bin/runners/lib/validators/index.js +0 -120
- package/bin/runners/lib/validators/license-checker.js +0 -252
- package/bin/runners/lib/validators/route-validator.js +0 -290
- package/bin/runners/runAuthority.js +0 -528
- package/bin/runners/runConductor.js +0 -772
- package/bin/runners/runContainer.js +0 -366
- package/bin/runners/runEasy.js +0 -410
- package/bin/runners/runIaC.js +0 -372
- package/bin/runners/runVibe.js +0 -791
- package/mcp-server/tools.js +0 -495
|
@@ -1,671 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Conductor - Multi-Agent Coordination System
|
|
3
|
-
*
|
|
4
|
-
* ═══════════════════════════════════════════════════════════════════════════════
|
|
5
|
-
* ENTERPRISE EDITION - Agent Orchestration
|
|
6
|
-
* ═══════════════════════════════════════════════════════════════════════════════
|
|
7
|
-
*
|
|
8
|
-
* Manages multiple AI agents working on the same codebase:
|
|
9
|
-
* - Agent registration and heartbeat tracking
|
|
10
|
-
* - File locking to prevent concurrent edits
|
|
11
|
-
* - Change proposals with conflict detection
|
|
12
|
-
* - Coordination history for auditing
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
"use strict";
|
|
16
|
-
|
|
17
|
-
const fs = require("fs");
|
|
18
|
-
const path = require("path");
|
|
19
|
-
const crypto = require("crypto");
|
|
20
|
-
|
|
21
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
22
|
-
// CONFIGURATION
|
|
23
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
24
|
-
|
|
25
|
-
const STATE_DIR = ".vibecheck";
|
|
26
|
-
const STATE_FILE = "conductor-state.json";
|
|
27
|
-
const LOCK_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes - stale lock threshold
|
|
28
|
-
const AGENT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes - agent considered inactive
|
|
29
|
-
|
|
30
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
31
|
-
// UUID GENERATION (no external dependency)
|
|
32
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
33
|
-
|
|
34
|
-
function generateUUID() {
|
|
35
|
-
// RFC 4122 v4 UUID
|
|
36
|
-
const bytes = crypto.randomBytes(16);
|
|
37
|
-
bytes[6] = (bytes[6] & 0x0f) | 0x40; // Version 4
|
|
38
|
-
bytes[8] = (bytes[8] & 0x3f) | 0x80; // Variant 10xx
|
|
39
|
-
|
|
40
|
-
const hex = bytes.toString("hex");
|
|
41
|
-
return [
|
|
42
|
-
hex.slice(0, 8),
|
|
43
|
-
hex.slice(8, 12),
|
|
44
|
-
hex.slice(12, 16),
|
|
45
|
-
hex.slice(16, 20),
|
|
46
|
-
hex.slice(20, 32)
|
|
47
|
-
].join("-");
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
51
|
-
// FILE UTILITIES
|
|
52
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
53
|
-
|
|
54
|
-
function ensureDir(dirPath) {
|
|
55
|
-
if (!fs.existsSync(dirPath)) {
|
|
56
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function readJSON(filePath) {
|
|
61
|
-
try {
|
|
62
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
63
|
-
return JSON.parse(content);
|
|
64
|
-
} catch {
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function writeJSON(filePath, data) {
|
|
70
|
-
const dir = path.dirname(filePath);
|
|
71
|
-
ensureDir(dir);
|
|
72
|
-
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
76
|
-
// CONDUCTOR CLASS
|
|
77
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
78
|
-
|
|
79
|
-
class Conductor {
|
|
80
|
-
constructor(projectRoot = process.cwd()) {
|
|
81
|
-
this.projectRoot = projectRoot;
|
|
82
|
-
this.stateFile = path.join(projectRoot, STATE_DIR, STATE_FILE);
|
|
83
|
-
this.state = null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
87
|
-
// STATE MANAGEMENT
|
|
88
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Load state from disk (or initialize fresh)
|
|
92
|
-
*/
|
|
93
|
-
async load() {
|
|
94
|
-
const existing = readJSON(this.stateFile);
|
|
95
|
-
|
|
96
|
-
if (existing) {
|
|
97
|
-
this.state = existing;
|
|
98
|
-
} else {
|
|
99
|
-
this.state = {
|
|
100
|
-
version: 1,
|
|
101
|
-
createdAt: new Date().toISOString(),
|
|
102
|
-
agents: {},
|
|
103
|
-
locks: {},
|
|
104
|
-
proposals: [],
|
|
105
|
-
history: []
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return this.state;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Persist state to disk
|
|
114
|
-
*/
|
|
115
|
-
async save() {
|
|
116
|
-
this.state.updatedAt = new Date().toISOString();
|
|
117
|
-
writeJSON(this.stateFile, this.state);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
121
|
-
// AGENT MANAGEMENT
|
|
122
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Register a new agent
|
|
126
|
-
* @param {Object} agentInfo - Agent metadata (name, type, capabilities)
|
|
127
|
-
* @returns {string} - Unique agent ID
|
|
128
|
-
*/
|
|
129
|
-
async registerAgent(agentInfo) {
|
|
130
|
-
const agentId = generateUUID();
|
|
131
|
-
const now = new Date().toISOString();
|
|
132
|
-
|
|
133
|
-
this.state.agents[agentId] = {
|
|
134
|
-
id: agentId,
|
|
135
|
-
name: agentInfo.name || "unnamed-agent",
|
|
136
|
-
type: agentInfo.type || "unknown",
|
|
137
|
-
capabilities: agentInfo.capabilities || [],
|
|
138
|
-
metadata: agentInfo.metadata || {},
|
|
139
|
-
registeredAt: now,
|
|
140
|
-
lastSeen: now,
|
|
141
|
-
status: "active"
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
// Record in history
|
|
145
|
-
this._recordEvent("agent_registered", {
|
|
146
|
-
agentId,
|
|
147
|
-
agentName: agentInfo.name,
|
|
148
|
-
agentType: agentInfo.type
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
await this.save();
|
|
152
|
-
return agentId;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Update agent's last seen timestamp
|
|
157
|
-
* @param {string} agentId
|
|
158
|
-
*/
|
|
159
|
-
async heartbeat(agentId) {
|
|
160
|
-
const agent = this.state.agents[agentId];
|
|
161
|
-
|
|
162
|
-
if (!agent) {
|
|
163
|
-
return { success: false, reason: "agent-not-found" };
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
agent.lastSeen = new Date().toISOString();
|
|
167
|
-
agent.status = "active";
|
|
168
|
-
await this.save();
|
|
169
|
-
|
|
170
|
-
return { success: true, lastSeen: agent.lastSeen };
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Unregister an agent (and release its locks)
|
|
175
|
-
* @param {string} agentId
|
|
176
|
-
*/
|
|
177
|
-
async unregisterAgent(agentId) {
|
|
178
|
-
const agent = this.state.agents[agentId];
|
|
179
|
-
|
|
180
|
-
if (!agent) {
|
|
181
|
-
return { success: false, reason: "agent-not-found" };
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Release all locks held by this agent
|
|
185
|
-
const releasedLocks = [];
|
|
186
|
-
for (const [filePath, lock] of Object.entries(this.state.locks)) {
|
|
187
|
-
if (lock.agentId === agentId) {
|
|
188
|
-
delete this.state.locks[filePath];
|
|
189
|
-
releasedLocks.push(filePath);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Mark agent as inactive (preserve for history)
|
|
194
|
-
agent.status = "unregistered";
|
|
195
|
-
agent.unregisteredAt = new Date().toISOString();
|
|
196
|
-
|
|
197
|
-
this._recordEvent("agent_unregistered", {
|
|
198
|
-
agentId,
|
|
199
|
-
releasedLocks
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
await this.save();
|
|
203
|
-
|
|
204
|
-
return { success: true, releasedLocks };
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Get agent info by ID
|
|
209
|
-
*/
|
|
210
|
-
getAgent(agentId) {
|
|
211
|
-
return this.state.agents[agentId] || null;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* List all agents (optionally filter active only)
|
|
216
|
-
*/
|
|
217
|
-
listAgents(activeOnly = false) {
|
|
218
|
-
const now = Date.now();
|
|
219
|
-
const agents = Object.values(this.state.agents);
|
|
220
|
-
|
|
221
|
-
if (!activeOnly) {
|
|
222
|
-
return agents;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
return agents.filter(agent => {
|
|
226
|
-
if (agent.status !== "active") return false;
|
|
227
|
-
const lastSeen = new Date(agent.lastSeen).getTime();
|
|
228
|
-
return now - lastSeen < AGENT_TIMEOUT_MS;
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
233
|
-
// FILE LOCKING
|
|
234
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Acquire a lock on a file
|
|
238
|
-
* @param {string} agentId - Agent requesting the lock
|
|
239
|
-
* @param {string} filePath - File to lock (relative to project root)
|
|
240
|
-
* @param {string} reason - Why the lock is needed
|
|
241
|
-
* @returns {Object} - { success, lockId } or { success: false, reason, holder }
|
|
242
|
-
*/
|
|
243
|
-
async acquireLock(agentId, filePath, reason = "") {
|
|
244
|
-
// Normalize file path
|
|
245
|
-
const normalizedPath = this._normalizePath(filePath);
|
|
246
|
-
const existing = this.state.locks[normalizedPath];
|
|
247
|
-
const now = Date.now();
|
|
248
|
-
|
|
249
|
-
// Check if already locked by another agent
|
|
250
|
-
if (existing && existing.agentId !== agentId) {
|
|
251
|
-
const lockAge = now - new Date(existing.acquiredAt).getTime();
|
|
252
|
-
|
|
253
|
-
// Check if lock is stale (> 5 min)
|
|
254
|
-
if (lockAge < LOCK_TIMEOUT_MS) {
|
|
255
|
-
return {
|
|
256
|
-
success: false,
|
|
257
|
-
reason: "locked",
|
|
258
|
-
holder: existing.agentId,
|
|
259
|
-
holderName: this.state.agents[existing.agentId]?.name || "unknown",
|
|
260
|
-
holderReason: existing.reason,
|
|
261
|
-
lockAge: lockAge,
|
|
262
|
-
expiresIn: LOCK_TIMEOUT_MS - lockAge
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Stale lock - record takeover in history
|
|
267
|
-
this._recordEvent("lock_expired", {
|
|
268
|
-
filePath: normalizedPath,
|
|
269
|
-
previousHolder: existing.agentId,
|
|
270
|
-
newHolder: agentId
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Grant the lock
|
|
275
|
-
this.state.locks[normalizedPath] = {
|
|
276
|
-
agentId,
|
|
277
|
-
agentName: this.state.agents[agentId]?.name || "unknown",
|
|
278
|
-
reason,
|
|
279
|
-
acquiredAt: new Date().toISOString(),
|
|
280
|
-
expiresAt: new Date(now + LOCK_TIMEOUT_MS).toISOString()
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
this._recordEvent("lock_acquired", {
|
|
284
|
-
agentId,
|
|
285
|
-
filePath: normalizedPath,
|
|
286
|
-
reason
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
await this.save();
|
|
290
|
-
|
|
291
|
-
return { success: true, lockId: normalizedPath };
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Release a lock on a file
|
|
296
|
-
* @param {string} agentId - Agent releasing the lock
|
|
297
|
-
* @param {string} filePath - File to unlock
|
|
298
|
-
*/
|
|
299
|
-
async releaseLock(agentId, filePath) {
|
|
300
|
-
const normalizedPath = this._normalizePath(filePath);
|
|
301
|
-
const lock = this.state.locks[normalizedPath];
|
|
302
|
-
|
|
303
|
-
if (!lock) {
|
|
304
|
-
return { success: false, reason: "not-locked" };
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
if (lock.agentId !== agentId) {
|
|
308
|
-
return {
|
|
309
|
-
success: false,
|
|
310
|
-
reason: "not-owner",
|
|
311
|
-
holder: lock.agentId
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
delete this.state.locks[normalizedPath];
|
|
316
|
-
|
|
317
|
-
this._recordEvent("lock_released", {
|
|
318
|
-
agentId,
|
|
319
|
-
filePath: normalizedPath
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
await this.save();
|
|
323
|
-
|
|
324
|
-
return { success: true };
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Check if a file is locked
|
|
329
|
-
*/
|
|
330
|
-
checkLock(filePath) {
|
|
331
|
-
const normalizedPath = this._normalizePath(filePath);
|
|
332
|
-
const lock = this.state.locks[normalizedPath];
|
|
333
|
-
|
|
334
|
-
if (!lock) {
|
|
335
|
-
return { locked: false };
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
const now = Date.now();
|
|
339
|
-
const lockAge = now - new Date(lock.acquiredAt).getTime();
|
|
340
|
-
const isStale = lockAge >= LOCK_TIMEOUT_MS;
|
|
341
|
-
|
|
342
|
-
return {
|
|
343
|
-
locked: !isStale,
|
|
344
|
-
stale: isStale,
|
|
345
|
-
holder: lock.agentId,
|
|
346
|
-
holderName: lock.agentName,
|
|
347
|
-
reason: lock.reason,
|
|
348
|
-
acquiredAt: lock.acquiredAt,
|
|
349
|
-
lockAge
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* List all active locks
|
|
355
|
-
*/
|
|
356
|
-
listLocks() {
|
|
357
|
-
const now = Date.now();
|
|
358
|
-
const locks = [];
|
|
359
|
-
|
|
360
|
-
for (const [filePath, lock] of Object.entries(this.state.locks)) {
|
|
361
|
-
const lockAge = now - new Date(lock.acquiredAt).getTime();
|
|
362
|
-
locks.push({
|
|
363
|
-
filePath,
|
|
364
|
-
...lock,
|
|
365
|
-
lockAge,
|
|
366
|
-
stale: lockAge >= LOCK_TIMEOUT_MS
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
return locks;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* Acquire locks on multiple files atomically
|
|
375
|
-
*/
|
|
376
|
-
async acquireLocks(agentId, files, reason = "") {
|
|
377
|
-
const results = [];
|
|
378
|
-
const acquired = [];
|
|
379
|
-
|
|
380
|
-
// Try to acquire all locks
|
|
381
|
-
for (const filePath of files) {
|
|
382
|
-
const result = await this.acquireLock(agentId, filePath, reason);
|
|
383
|
-
results.push({ filePath, ...result });
|
|
384
|
-
|
|
385
|
-
if (result.success) {
|
|
386
|
-
acquired.push(filePath);
|
|
387
|
-
} else {
|
|
388
|
-
// Rollback - release all acquired locks
|
|
389
|
-
for (const acquiredPath of acquired) {
|
|
390
|
-
await this.releaseLock(agentId, acquiredPath);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
return {
|
|
394
|
-
success: false,
|
|
395
|
-
reason: "partial-failure",
|
|
396
|
-
failedFile: filePath,
|
|
397
|
-
failedReason: result.reason,
|
|
398
|
-
holder: result.holder
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
return { success: true, locks: acquired };
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
407
|
-
// CHANGE PROPOSALS
|
|
408
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
409
|
-
|
|
410
|
-
/**
|
|
411
|
-
* Submit a change proposal
|
|
412
|
-
* @param {string} agentId - Agent submitting the proposal
|
|
413
|
-
* @param {Object} proposal - { description, files, changes, priority }
|
|
414
|
-
*/
|
|
415
|
-
async propose(agentId, proposal) {
|
|
416
|
-
const id = generateUUID();
|
|
417
|
-
const now = new Date().toISOString();
|
|
418
|
-
|
|
419
|
-
const fullProposal = {
|
|
420
|
-
id,
|
|
421
|
-
agentId,
|
|
422
|
-
agentName: this.state.agents[agentId]?.name || "unknown",
|
|
423
|
-
description: proposal.description || "",
|
|
424
|
-
files: proposal.files || [],
|
|
425
|
-
changes: proposal.changes || [],
|
|
426
|
-
priority: proposal.priority || "normal",
|
|
427
|
-
status: "pending",
|
|
428
|
-
createdAt: now,
|
|
429
|
-
updatedAt: now
|
|
430
|
-
};
|
|
431
|
-
|
|
432
|
-
// Check for conflicts with other pending proposals
|
|
433
|
-
const conflicts = this._findConflicts(proposal);
|
|
434
|
-
if (conflicts.length > 0) {
|
|
435
|
-
fullProposal.conflicts = conflicts;
|
|
436
|
-
fullProposal.status = "conflict";
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
this.state.proposals.push(fullProposal);
|
|
440
|
-
|
|
441
|
-
this._recordEvent("proposal_created", {
|
|
442
|
-
proposalId: id,
|
|
443
|
-
agentId,
|
|
444
|
-
files: proposal.files,
|
|
445
|
-
hasConflicts: conflicts.length > 0
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
await this.save();
|
|
449
|
-
|
|
450
|
-
return fullProposal;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
/**
|
|
454
|
-
* Find conflicts between a new proposal and existing pending proposals
|
|
455
|
-
*/
|
|
456
|
-
_findConflicts(proposal) {
|
|
457
|
-
const conflicts = [];
|
|
458
|
-
const proposalFiles = new Set(proposal.files || []);
|
|
459
|
-
|
|
460
|
-
for (const existing of this.state.proposals) {
|
|
461
|
-
// Only check pending proposals
|
|
462
|
-
if (existing.status !== "pending") continue;
|
|
463
|
-
|
|
464
|
-
// Check for file overlap
|
|
465
|
-
const existingFiles = new Set(existing.files || []);
|
|
466
|
-
const overlap = [...proposalFiles].filter(f => existingFiles.has(f));
|
|
467
|
-
|
|
468
|
-
if (overlap.length > 0) {
|
|
469
|
-
conflicts.push({
|
|
470
|
-
proposalId: existing.id,
|
|
471
|
-
agentId: existing.agentId,
|
|
472
|
-
agentName: existing.agentName,
|
|
473
|
-
description: existing.description,
|
|
474
|
-
files: overlap
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
return conflicts;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
/**
|
|
483
|
-
* Update proposal status
|
|
484
|
-
*/
|
|
485
|
-
async updateProposal(proposalId, status, resolution = null) {
|
|
486
|
-
const proposal = this.state.proposals.find(p => p.id === proposalId);
|
|
487
|
-
|
|
488
|
-
if (!proposal) {
|
|
489
|
-
return { success: false, reason: "proposal-not-found" };
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
const validStatuses = ["pending", "approved", "rejected", "conflict", "merged", "cancelled"];
|
|
493
|
-
if (!validStatuses.includes(status)) {
|
|
494
|
-
return { success: false, reason: "invalid-status" };
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
proposal.status = status;
|
|
498
|
-
proposal.updatedAt = new Date().toISOString();
|
|
499
|
-
|
|
500
|
-
if (resolution) {
|
|
501
|
-
proposal.resolution = resolution;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
this._recordEvent("proposal_updated", {
|
|
505
|
-
proposalId,
|
|
506
|
-
status,
|
|
507
|
-
resolution
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
await this.save();
|
|
511
|
-
|
|
512
|
-
return { success: true, proposal };
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
/**
|
|
516
|
-
* Get a proposal by ID
|
|
517
|
-
*/
|
|
518
|
-
getProposal(proposalId) {
|
|
519
|
-
return this.state.proposals.find(p => p.id === proposalId) || null;
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
/**
|
|
523
|
-
* List proposals with optional filtering
|
|
524
|
-
*/
|
|
525
|
-
listProposals(filter = {}) {
|
|
526
|
-
let proposals = [...this.state.proposals];
|
|
527
|
-
|
|
528
|
-
if (filter.status) {
|
|
529
|
-
proposals = proposals.filter(p => p.status === filter.status);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
if (filter.agentId) {
|
|
533
|
-
proposals = proposals.filter(p => p.agentId === filter.agentId);
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
if (filter.hasConflicts !== undefined) {
|
|
537
|
-
proposals = proposals.filter(p =>
|
|
538
|
-
filter.hasConflicts ? (p.conflicts?.length > 0) : (!p.conflicts || p.conflicts.length === 0)
|
|
539
|
-
);
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
return proposals;
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
546
|
-
// STATUS & REPORTING
|
|
547
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
548
|
-
|
|
549
|
-
/**
|
|
550
|
-
* Get overall conductor status
|
|
551
|
-
*/
|
|
552
|
-
getStatus() {
|
|
553
|
-
const now = Date.now();
|
|
554
|
-
const agents = Object.values(this.state.agents);
|
|
555
|
-
|
|
556
|
-
// Count active agents (seen within timeout)
|
|
557
|
-
const activeAgents = agents.filter(a => {
|
|
558
|
-
if (a.status !== "active") return false;
|
|
559
|
-
return now - new Date(a.lastSeen).getTime() < AGENT_TIMEOUT_MS;
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
// Count active (non-stale) locks
|
|
563
|
-
const activeLocks = Object.entries(this.state.locks).filter(([_, lock]) => {
|
|
564
|
-
return now - new Date(lock.acquiredAt).getTime() < LOCK_TIMEOUT_MS;
|
|
565
|
-
});
|
|
566
|
-
|
|
567
|
-
// Count proposals by status
|
|
568
|
-
const proposalsByStatus = {};
|
|
569
|
-
for (const p of this.state.proposals) {
|
|
570
|
-
proposalsByStatus[p.status] = (proposalsByStatus[p.status] || 0) + 1;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
return {
|
|
574
|
-
activeAgents: activeAgents.length,
|
|
575
|
-
totalAgents: agents.length,
|
|
576
|
-
activeLocks: activeLocks.length,
|
|
577
|
-
totalLocks: Object.keys(this.state.locks).length,
|
|
578
|
-
pendingProposals: proposalsByStatus.pending || 0,
|
|
579
|
-
conflictProposals: proposalsByStatus.conflict || 0,
|
|
580
|
-
totalProposals: this.state.proposals.length,
|
|
581
|
-
proposalsByStatus,
|
|
582
|
-
historyCount: this.state.history.length,
|
|
583
|
-
stateCreated: this.state.createdAt,
|
|
584
|
-
lastUpdated: this.state.updatedAt
|
|
585
|
-
};
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
/**
|
|
589
|
-
* Get recent history events
|
|
590
|
-
*/
|
|
591
|
-
getHistory(limit = 50) {
|
|
592
|
-
return this.state.history.slice(-limit).reverse();
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
596
|
-
// INTERNAL HELPERS
|
|
597
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
598
|
-
|
|
599
|
-
/**
|
|
600
|
-
* Normalize a file path for consistent lookups
|
|
601
|
-
*/
|
|
602
|
-
_normalizePath(filePath) {
|
|
603
|
-
// Convert to forward slashes, remove leading ./
|
|
604
|
-
return filePath
|
|
605
|
-
.replace(/\\/g, "/")
|
|
606
|
-
.replace(/^\.\//, "")
|
|
607
|
-
.replace(/\/+/g, "/");
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
/**
|
|
611
|
-
* Record an event in history
|
|
612
|
-
*/
|
|
613
|
-
_recordEvent(type, data) {
|
|
614
|
-
this.state.history.push({
|
|
615
|
-
type,
|
|
616
|
-
timestamp: new Date().toISOString(),
|
|
617
|
-
...data
|
|
618
|
-
});
|
|
619
|
-
|
|
620
|
-
// Keep history bounded (last 1000 events)
|
|
621
|
-
if (this.state.history.length > 1000) {
|
|
622
|
-
this.state.history = this.state.history.slice(-1000);
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
/**
|
|
627
|
-
* Clean up stale locks and inactive agents
|
|
628
|
-
*/
|
|
629
|
-
async cleanup() {
|
|
630
|
-
const now = Date.now();
|
|
631
|
-
let cleaned = { locks: 0, agents: 0 };
|
|
632
|
-
|
|
633
|
-
// Remove stale locks
|
|
634
|
-
for (const [filePath, lock] of Object.entries(this.state.locks)) {
|
|
635
|
-
const lockAge = now - new Date(lock.acquiredAt).getTime();
|
|
636
|
-
if (lockAge >= LOCK_TIMEOUT_MS) {
|
|
637
|
-
delete this.state.locks[filePath];
|
|
638
|
-
cleaned.locks++;
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
// Mark inactive agents
|
|
643
|
-
for (const agent of Object.values(this.state.agents)) {
|
|
644
|
-
if (agent.status === "active") {
|
|
645
|
-
const lastSeen = now - new Date(agent.lastSeen).getTime();
|
|
646
|
-
if (lastSeen >= AGENT_TIMEOUT_MS) {
|
|
647
|
-
agent.status = "inactive";
|
|
648
|
-
cleaned.agents++;
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
if (cleaned.locks > 0 || cleaned.agents > 0) {
|
|
654
|
-
this._recordEvent("cleanup", cleaned);
|
|
655
|
-
await this.save();
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
return cleaned;
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
663
|
-
// SINGLETON EXPORT
|
|
664
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
665
|
-
|
|
666
|
-
const conductor = new Conductor();
|
|
667
|
-
|
|
668
|
-
module.exports = {
|
|
669
|
-
Conductor,
|
|
670
|
-
conductor
|
|
671
|
-
};
|