speclock 4.2.0 → 4.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/package.json +2 -2
- package/src/cli/index.js +1 -1
- package/src/core/compliance.js +1 -1
- package/src/core/conflict.js +109 -20
- package/src/core/engine.js +3 -0
- package/src/core/semantics.js +28 -11
- package/src/core/telemetry.js +1 -1
- package/src/dashboard/index.html +2 -2
- package/src/mcp/http-server.js +1 -1
- package/src/mcp/server.js +103 -87
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "speclock",
|
|
3
|
-
"version": "4.
|
|
4
|
-
"description": "AI constraint engine with Policy-as-Code DSL, OAuth/OIDC SSO, admin dashboard, telemetry, API key auth, RBAC, AES-256-GCM encryption, hard enforcement, semantic pre-commit, HMAC audit chain, SOC 2/HIPAA compliance.
|
|
3
|
+
"version": "4.3.0",
|
|
4
|
+
"description": "AI constraint engine with Gemini LLM universal detection, Policy-as-Code DSL, OAuth/OIDC SSO, admin dashboard, telemetry, API key auth, RBAC, AES-256-GCM encryption, hard enforcement, semantic pre-commit, HMAC audit chain, SOC 2/HIPAA compliance. Cross-platform: MCP + direct API. 31 MCP tools + CLI. Enterprise platform.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/mcp/server.js",
|
|
7
7
|
"bin": {
|
package/src/cli/index.js
CHANGED
|
@@ -116,7 +116,7 @@ function refreshContext(root) {
|
|
|
116
116
|
|
|
117
117
|
function printHelp() {
|
|
118
118
|
console.log(`
|
|
119
|
-
SpecLock
|
|
119
|
+
SpecLock v4.3.0 — AI Constraint Engine (Gemini LLM + Policy-as-Code + SSO + Dashboard + Telemetry + Auth + RBAC + Encryption)
|
|
120
120
|
Developed by Sandeep Roy (github.com/sgroy10)
|
|
121
121
|
|
|
122
122
|
Usage: speclock <command> [options]
|
package/src/core/compliance.js
CHANGED
package/src/core/conflict.js
CHANGED
|
@@ -56,13 +56,95 @@ const GUARD_TAG = "SPECLOCK-GUARD";
|
|
|
56
56
|
|
|
57
57
|
// --- Core functions ---
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Detect if the first argument is a file-system path (brain mode)
|
|
61
|
+
* or natural text (direct mode for cross-platform usage).
|
|
62
|
+
*/
|
|
63
|
+
function isDirectoryPath(str) {
|
|
64
|
+
if (!str || typeof str !== "string") return false;
|
|
65
|
+
// Absolute paths: /foo, C:\foo, \\server
|
|
66
|
+
if (str.startsWith("/") || str.startsWith("\\") || /^[A-Z]:/i.test(str)) return true;
|
|
67
|
+
// Relative path with separator or current dir
|
|
68
|
+
if (str === "." || str === ".." || str.includes("/") || str.includes("\\")) return true;
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Direct-mode conflict check: action text + lock text(s) directly.
|
|
74
|
+
* No brain.json needed. Works on any platform.
|
|
75
|
+
* @param {string} actionText - The proposed action
|
|
76
|
+
* @param {string|string[]} locks - Lock text or array of lock texts
|
|
77
|
+
* @returns {Object} Same shape as brain-mode checkConflict
|
|
78
|
+
*/
|
|
79
|
+
function checkConflictDirect(actionText, locks) {
|
|
80
|
+
const lockList = Array.isArray(locks) ? locks : [locks];
|
|
81
|
+
const conflicting = [];
|
|
82
|
+
let maxNonConflictScore = 0;
|
|
83
|
+
|
|
84
|
+
for (const lockText of lockList) {
|
|
85
|
+
const result = analyzeConflict(actionText, lockText);
|
|
86
|
+
if (result.isConflict) {
|
|
87
|
+
conflicting.push({
|
|
88
|
+
id: "direct",
|
|
89
|
+
text: lockText,
|
|
90
|
+
matchedKeywords: [],
|
|
91
|
+
confidence: result.confidence,
|
|
92
|
+
level: result.level,
|
|
93
|
+
reasons: result.reasons,
|
|
94
|
+
});
|
|
95
|
+
} else if (result.confidence > maxNonConflictScore) {
|
|
96
|
+
maxNonConflictScore = result.confidence;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (conflicting.length === 0) {
|
|
101
|
+
return {
|
|
102
|
+
hasConflict: false,
|
|
103
|
+
conflictingLocks: [],
|
|
104
|
+
_maxNonConflictScore: maxNonConflictScore,
|
|
105
|
+
analysis: `Checked against ${lockList.length} lock(s). No conflicts detected.`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
conflicting.sort((a, b) => b.confidence - a.confidence);
|
|
110
|
+
const details = conflicting
|
|
111
|
+
.map(
|
|
112
|
+
(c) =>
|
|
113
|
+
`- [${c.level}] "${c.text}" (confidence: ${c.confidence}%)\n Reasons: ${c.reasons.join("; ")}`
|
|
114
|
+
)
|
|
115
|
+
.join("\n");
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
hasConflict: true,
|
|
119
|
+
conflictingLocks: conflicting,
|
|
120
|
+
_maxNonConflictScore: maxNonConflictScore,
|
|
121
|
+
analysis: `Potential conflict with ${conflicting.length} lock(s):\n${details}\nReview before proceeding.`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Check conflicts. Supports TWO calling patterns:
|
|
127
|
+
* 1. Brain mode: checkConflict(rootDir, proposedAction)
|
|
128
|
+
* 2. Direct mode: checkConflict(actionText, lockText)
|
|
129
|
+
* checkConflict(actionText, [lock1, lock2, ...])
|
|
130
|
+
* Automatically detects which mode based on the first argument.
|
|
131
|
+
*/
|
|
132
|
+
export function checkConflict(rootOrAction, proposedActionOrLock) {
|
|
133
|
+
// Direct mode: first arg is not a directory path → treat as (action, lock)
|
|
134
|
+
if (!isDirectoryPath(rootOrAction)) {
|
|
135
|
+
return checkConflictDirect(rootOrAction, proposedActionOrLock);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Brain mode: first arg is a directory path → read locks from brain.json
|
|
139
|
+
const root = rootOrAction;
|
|
140
|
+
const proposedAction = proposedActionOrLock;
|
|
60
141
|
const brain = ensureInit(root);
|
|
61
|
-
const activeLocks = brain.specLock
|
|
142
|
+
const activeLocks = (brain.specLock?.items || []).filter((l) => l.active !== false);
|
|
62
143
|
if (activeLocks.length === 0) {
|
|
63
144
|
return {
|
|
64
145
|
hasConflict: false,
|
|
65
146
|
conflictingLocks: [],
|
|
147
|
+
_maxNonConflictScore: 0,
|
|
66
148
|
analysis: "No active locks. No constraints to check against.",
|
|
67
149
|
};
|
|
68
150
|
}
|
|
@@ -124,25 +206,17 @@ export function checkConflict(root, proposedAction) {
|
|
|
124
206
|
|
|
125
207
|
/**
|
|
126
208
|
* Async conflict check with LLM fallback for grey-zone cases.
|
|
209
|
+
* Supports both brain mode and direct mode (same as checkConflict).
|
|
127
210
|
* Strategy: Run heuristic first (fast, free, offline).
|
|
128
211
|
* - Score > 70% on ALL conflicts → trust heuristic (skip LLM)
|
|
129
|
-
* -
|
|
130
|
-
* - Score 1–70% on ANY lock → GREY ZONE → call LLM for universal domain coverage
|
|
131
|
-
* This catches vocabulary gaps where the heuristic has partial/no signal
|
|
132
|
-
* but an LLM (which knows every domain) would detect the conflict.
|
|
212
|
+
* - Everything else → call LLM for universal domain coverage
|
|
133
213
|
*/
|
|
134
|
-
export async function checkConflictAsync(
|
|
135
|
-
// 1. Always run the fast heuristic first
|
|
136
|
-
const heuristicResult = checkConflict(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
? Math.max(...heuristicResult.conflictingLocks.map((c) => c.confidence))
|
|
141
|
-
: 0;
|
|
142
|
-
const maxNonConflictScore = heuristicResult._maxNonConflictScore || 0;
|
|
143
|
-
const maxScore = Math.max(maxConflictScore, maxNonConflictScore);
|
|
144
|
-
|
|
145
|
-
// 3. Fast path: all conflicts are HIGH (>70%) → heuristic is certain, skip LLM
|
|
214
|
+
export async function checkConflictAsync(rootOrAction, proposedActionOrLock) {
|
|
215
|
+
// 1. Always run the fast heuristic first (handles both brain + direct mode)
|
|
216
|
+
const heuristicResult = checkConflict(rootOrAction, proposedActionOrLock);
|
|
217
|
+
const isDirect = !isDirectoryPath(rootOrAction);
|
|
218
|
+
|
|
219
|
+
// 2. Fast path: all conflicts are HIGH (>70%) → heuristic is certain, skip LLM
|
|
146
220
|
if (
|
|
147
221
|
heuristicResult.hasConflict &&
|
|
148
222
|
heuristicResult.conflictingLocks.every((c) => c.confidence > 70)
|
|
@@ -150,12 +224,27 @@ export async function checkConflictAsync(root, proposedAction) {
|
|
|
150
224
|
return heuristicResult;
|
|
151
225
|
}
|
|
152
226
|
|
|
153
|
-
//
|
|
227
|
+
// 3. Call LLM for everything else — including score 0.
|
|
154
228
|
// Score 0 means "heuristic vocabulary doesn't cover this domain",
|
|
155
229
|
// which is EXACTLY when an LLM (which knows every domain) adds value.
|
|
156
230
|
try {
|
|
157
231
|
const { llmCheckConflict } = await import("./llm-checker.js");
|
|
158
|
-
|
|
232
|
+
// In direct mode, build activeLocks from the lock text(s) passed directly
|
|
233
|
+
let llmResult;
|
|
234
|
+
if (isDirect) {
|
|
235
|
+
const lockTexts = Array.isArray(proposedActionOrLock)
|
|
236
|
+
? proposedActionOrLock
|
|
237
|
+
: [proposedActionOrLock];
|
|
238
|
+
const activeLocks = lockTexts.map((t, i) => ({
|
|
239
|
+
id: `direct-${i}`,
|
|
240
|
+
text: t,
|
|
241
|
+
active: true,
|
|
242
|
+
}));
|
|
243
|
+
llmResult = await llmCheckConflict(null, rootOrAction, activeLocks);
|
|
244
|
+
} else {
|
|
245
|
+
llmResult = await llmCheckConflict(rootOrAction, proposedActionOrLock);
|
|
246
|
+
}
|
|
247
|
+
|
|
159
248
|
if (llmResult) {
|
|
160
249
|
// Keep HIGH heuristic conflicts (>70%) — they're already certain
|
|
161
250
|
const highConfidence = heuristicResult.conflictingLocks.filter(
|
package/src/core/engine.js
CHANGED
package/src/core/semantics.js
CHANGED
|
@@ -379,8 +379,7 @@ export const CONCEPT_MAP = {
|
|
|
379
379
|
"anti-fraud"],
|
|
380
380
|
"posting": ["transaction", "ledger entry", "journal entry", "record"],
|
|
381
381
|
"reconciliation": ["balance", "ledger", "account", "transaction", "audit"],
|
|
382
|
-
"checkout": ["
|
|
383
|
-
"payment processing", "order"],
|
|
382
|
+
"checkout": ["cart", "purchase", "order"],
|
|
384
383
|
"revenue": ["payment", "billing", "income", "sales", "earnings",
|
|
385
384
|
"transaction"],
|
|
386
385
|
"invoice": ["billing", "payment", "charge", "transaction", "accounts receivable"],
|
|
@@ -425,9 +424,9 @@ export const CONCEPT_MAP = {
|
|
|
425
424
|
|
|
426
425
|
// E-commerce
|
|
427
426
|
"cart": ["checkout", "purchase", "shopping cart"],
|
|
428
|
-
"payment processing":["payment", "
|
|
427
|
+
"payment processing":["payment", "billing", "transaction",
|
|
429
428
|
"stripe", "payment gateway"],
|
|
430
|
-
"payment gateway": ["payment processing", "stripe", "paypal",
|
|
429
|
+
"payment gateway": ["payment processing", "stripe", "paypal",
|
|
431
430
|
"billing", "transaction"],
|
|
432
431
|
"product": ["item", "sku", "catalog", "merchandise", "product listing"],
|
|
433
432
|
"price": ["pricing", "cost", "amount", "rate", "charge"],
|
|
@@ -1386,17 +1385,35 @@ function _compareSubjectsInline(actionText, lockText) {
|
|
|
1386
1385
|
};
|
|
1387
1386
|
}
|
|
1388
1387
|
|
|
1388
|
+
// --- Performance cache: avoid re-computing action-side analysis across multiple locks ---
|
|
1389
|
+
const _actionCache = new Map();
|
|
1390
|
+
const _ACTION_CACHE_MAX = 50;
|
|
1391
|
+
|
|
1392
|
+
function _getCachedAction(text) {
|
|
1393
|
+
const cached = _actionCache.get(text);
|
|
1394
|
+
if (cached) return cached;
|
|
1395
|
+
const tokens = tokenize(text);
|
|
1396
|
+
const expanded = expandSemantics(tokens.all);
|
|
1397
|
+
const intent = classifyIntent(text);
|
|
1398
|
+
const temporal = detectTemporalModifier(text);
|
|
1399
|
+
const entry = { tokens, expanded, intent, temporal };
|
|
1400
|
+
if (_actionCache.size >= _ACTION_CACHE_MAX) {
|
|
1401
|
+
_actionCache.delete(_actionCache.keys().next().value);
|
|
1402
|
+
}
|
|
1403
|
+
_actionCache.set(text, entry);
|
|
1404
|
+
return entry;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1389
1407
|
export function scoreConflict({ actionText, lockText }) {
|
|
1390
|
-
const
|
|
1391
|
-
const
|
|
1408
|
+
const actionCached = _getCachedAction(actionText);
|
|
1409
|
+
const actionTokens = actionCached.tokens;
|
|
1410
|
+
const actionExpanded = actionCached.expanded;
|
|
1411
|
+
const actionIntent = actionCached.intent;
|
|
1412
|
+
const hasTemporalMod = actionCached.temporal;
|
|
1392
1413
|
|
|
1393
|
-
const
|
|
1414
|
+
const lockTokens = tokenize(lockText);
|
|
1394
1415
|
const lockExpanded = expandSemantics(lockTokens.all);
|
|
1395
|
-
|
|
1396
|
-
const actionIntent = classifyIntent(actionText);
|
|
1397
1416
|
const lockIntent = classifyIntent(lockText);
|
|
1398
|
-
|
|
1399
|
-
const hasTemporalMod = detectTemporalModifier(actionText);
|
|
1400
1417
|
const lockIsProhibitive = isProhibitiveLock(lockText);
|
|
1401
1418
|
|
|
1402
1419
|
let score = 0;
|
package/src/core/telemetry.js
CHANGED
|
@@ -257,7 +257,7 @@ export async function flushToRemote(root) {
|
|
|
257
257
|
// Build anonymized payload
|
|
258
258
|
const payload = {
|
|
259
259
|
instanceId: summary.instanceId,
|
|
260
|
-
version: "3.
|
|
260
|
+
version: "4.3.0",
|
|
261
261
|
totalCalls: summary.totalCalls,
|
|
262
262
|
avgResponseMs: summary.avgResponseMs,
|
|
263
263
|
conflicts: summary.conflicts,
|
package/src/dashboard/index.html
CHANGED
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
<div class="header">
|
|
90
90
|
<div>
|
|
91
91
|
<h1><span>SpecLock</span> Dashboard</h1>
|
|
92
|
-
<div class="meta">
|
|
92
|
+
<div class="meta">v4.3.0 — AI Constraint Engine</div>
|
|
93
93
|
</div>
|
|
94
94
|
<div style="display:flex;align-items:center;gap:12px;">
|
|
95
95
|
<span id="health-badge" class="status-badge healthy">Loading...</span>
|
|
@@ -182,7 +182,7 @@
|
|
|
182
182
|
</div>
|
|
183
183
|
|
|
184
184
|
<div style="text-align:center;padding:24px;color:var(--muted);font-size:12px;">
|
|
185
|
-
SpecLock
|
|
185
|
+
SpecLock v4.3.0 — Developed by Sandeep Roy — <a href="https://github.com/sgroy10/speclock" style="color:var(--accent)">GitHub</a>
|
|
186
186
|
</div>
|
|
187
187
|
|
|
188
188
|
<script>
|
package/src/mcp/http-server.js
CHANGED
|
@@ -91,7 +91,7 @@ import { fileURLToPath } from "url";
|
|
|
91
91
|
import _path from "path";
|
|
92
92
|
|
|
93
93
|
const PROJECT_ROOT = process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
|
|
94
|
-
const VERSION = "3.
|
|
94
|
+
const VERSION = "4.3.0";
|
|
95
95
|
const AUTHOR = "Sandeep Roy";
|
|
96
96
|
const START_TIME = Date.now();
|
|
97
97
|
|
package/src/mcp/server.js
CHANGED
|
@@ -100,7 +100,7 @@ const PROJECT_ROOT =
|
|
|
100
100
|
args.project || process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
|
|
101
101
|
|
|
102
102
|
// --- MCP Server ---
|
|
103
|
-
const VERSION = "3.
|
|
103
|
+
const VERSION = "4.3.0";
|
|
104
104
|
const AUTHOR = "Sandeep Roy";
|
|
105
105
|
|
|
106
106
|
const server = new McpServer(
|
|
@@ -521,48 +521,54 @@ server.tool(
|
|
|
521
521
|
.describe("Which AI tool is being used"),
|
|
522
522
|
},
|
|
523
523
|
async ({ toolName }) => {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
// Last session summary
|
|
535
|
-
if (briefing.lastSession) {
|
|
536
|
-
parts.push("## Last Session");
|
|
537
|
-
parts.push(`- Tool: **${briefing.lastSession.toolUsed}**`);
|
|
538
|
-
parts.push(`- Ended: ${briefing.lastSession.endedAt || "unknown"}`);
|
|
539
|
-
if (briefing.lastSession.summary)
|
|
540
|
-
parts.push(`- Summary: ${briefing.lastSession.summary}`);
|
|
541
|
-
parts.push(
|
|
542
|
-
`- Events: ${briefing.lastSession.eventsInSession || 0}`
|
|
543
|
-
);
|
|
544
|
-
parts.push(
|
|
545
|
-
`- Changes since then: ${briefing.changesSinceLastSession}`
|
|
546
|
-
);
|
|
524
|
+
try {
|
|
525
|
+
const briefing = getSessionBriefing(PROJECT_ROOT, toolName);
|
|
526
|
+
const contextMd = generateContext(PROJECT_ROOT);
|
|
527
|
+
|
|
528
|
+
const parts = [];
|
|
529
|
+
|
|
530
|
+
// Session info
|
|
531
|
+
parts.push(`# SpecLock Session Briefing`);
|
|
532
|
+
parts.push(`Session started (${toolName}). ID: ${briefing.session?.id || "new"}`);
|
|
547
533
|
parts.push("");
|
|
548
|
-
}
|
|
549
534
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
parts.push(`- ${
|
|
535
|
+
// Last session summary
|
|
536
|
+
if (briefing.lastSession) {
|
|
537
|
+
parts.push("## Last Session");
|
|
538
|
+
parts.push(`- Tool: **${briefing.lastSession.toolUsed || "unknown"}**`);
|
|
539
|
+
parts.push(`- Ended: ${briefing.lastSession.endedAt || "unknown"}`);
|
|
540
|
+
if (briefing.lastSession.summary)
|
|
541
|
+
parts.push(`- Summary: ${briefing.lastSession.summary}`);
|
|
542
|
+
parts.push(
|
|
543
|
+
`- Events: ${briefing.lastSession.eventsInSession || 0}`
|
|
544
|
+
);
|
|
545
|
+
parts.push(
|
|
546
|
+
`- Changes since then: ${briefing.changesSinceLastSession || 0}`
|
|
547
|
+
);
|
|
548
|
+
parts.push("");
|
|
555
549
|
}
|
|
556
|
-
parts.push("");
|
|
557
|
-
}
|
|
558
550
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
551
|
+
// Warnings
|
|
552
|
+
if (briefing.warnings?.length > 0) {
|
|
553
|
+
parts.push("## Warnings");
|
|
554
|
+
for (const w of briefing.warnings) {
|
|
555
|
+
parts.push(`- ${w}`);
|
|
556
|
+
}
|
|
557
|
+
parts.push("");
|
|
558
|
+
}
|
|
562
559
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
560
|
+
// Full context
|
|
561
|
+
parts.push("---");
|
|
562
|
+
parts.push(contextMd);
|
|
563
|
+
|
|
564
|
+
return {
|
|
565
|
+
content: [{ type: "text", text: parts.join("\n") }],
|
|
566
|
+
};
|
|
567
|
+
} catch (err) {
|
|
568
|
+
return {
|
|
569
|
+
content: [{ type: "text", text: `# SpecLock Session Briefing\n\nError loading session: ${err.message}\n\nTry running speclock_init first.\n\n---\n*SpecLock v${VERSION}*` }],
|
|
570
|
+
};
|
|
571
|
+
}
|
|
566
572
|
}
|
|
567
573
|
);
|
|
568
574
|
|
|
@@ -780,68 +786,78 @@ server.tool(
|
|
|
780
786
|
"Get a health check of the SpecLock setup including completeness score, missing recommended items, and multi-agent session timeline.",
|
|
781
787
|
{},
|
|
782
788
|
async () => {
|
|
783
|
-
|
|
784
|
-
|
|
789
|
+
try {
|
|
790
|
+
const brain = ensureInit(PROJECT_ROOT);
|
|
791
|
+
const activeLocks = (brain.specLock?.items || []).filter((l) => l.active !== false);
|
|
785
792
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
793
|
+
// Calculate health score
|
|
794
|
+
let score = 0;
|
|
795
|
+
const checks = [];
|
|
789
796
|
|
|
790
|
-
|
|
791
|
-
|
|
797
|
+
if (brain.goal?.text) { score += 20; checks.push("[PASS] Goal is set"); }
|
|
798
|
+
else checks.push("[MISS] No project goal set");
|
|
792
799
|
|
|
793
|
-
|
|
794
|
-
|
|
800
|
+
if (activeLocks.length > 0) { score += 25; checks.push(`[PASS] ${activeLocks.length} active lock(s)`); }
|
|
801
|
+
else checks.push("[MISS] No SpecLock constraints defined");
|
|
795
802
|
|
|
796
|
-
|
|
797
|
-
|
|
803
|
+
if ((brain.decisions || []).length > 0) { score += 15; checks.push(`[PASS] ${brain.decisions.length} decision(s) recorded`); }
|
|
804
|
+
else checks.push("[MISS] No decisions recorded");
|
|
798
805
|
|
|
799
|
-
|
|
800
|
-
|
|
806
|
+
if ((brain.notes || []).length > 0) { score += 10; checks.push(`[PASS] ${brain.notes.length} note(s)`); }
|
|
807
|
+
else checks.push("[MISS] No notes added");
|
|
801
808
|
|
|
802
|
-
|
|
803
|
-
|
|
809
|
+
const sessionHistory = brain.sessions?.history || [];
|
|
810
|
+
if (sessionHistory.length > 0) { score += 15; checks.push(`[PASS] ${sessionHistory.length} session(s) in history`); }
|
|
811
|
+
else checks.push("[MISS] No session history yet");
|
|
804
812
|
|
|
805
|
-
|
|
806
|
-
|
|
813
|
+
const recentChanges = brain.state?.recentChanges || [];
|
|
814
|
+
if (recentChanges.length > 0) { score += 10; checks.push(`[PASS] ${recentChanges.length} change(s) tracked`); }
|
|
815
|
+
else checks.push("[MISS] No changes tracked");
|
|
807
816
|
|
|
808
|
-
|
|
809
|
-
|
|
817
|
+
if (brain.facts?.deploy?.provider && brain.facts.deploy.provider !== "unknown") { score += 5; checks.push("[PASS] Deploy facts configured"); }
|
|
818
|
+
else checks.push("[MISS] Deploy facts not configured");
|
|
810
819
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
820
|
+
// Multi-agent timeline
|
|
821
|
+
const agentMap = {};
|
|
822
|
+
for (const session of sessionHistory) {
|
|
823
|
+
const tool = session.toolUsed || "unknown";
|
|
824
|
+
if (!agentMap[tool]) agentMap[tool] = { count: 0, lastUsed: "", summaries: [] };
|
|
825
|
+
agentMap[tool].count++;
|
|
826
|
+
if (!agentMap[tool].lastUsed || (session.endedAt && session.endedAt > agentMap[tool].lastUsed)) {
|
|
827
|
+
agentMap[tool].lastUsed = session.endedAt || session.startedAt || "";
|
|
828
|
+
}
|
|
829
|
+
if (session.summary && agentMap[tool].summaries.length < 3) {
|
|
830
|
+
agentMap[tool].summaries.push(session.summary.substring(0, 80));
|
|
831
|
+
}
|
|
822
832
|
}
|
|
823
|
-
}
|
|
824
833
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
+
let agentTimeline = "";
|
|
835
|
+
if (Object.keys(agentMap).length > 0) {
|
|
836
|
+
agentTimeline = "\n\n## Multi-Agent Timeline\n" +
|
|
837
|
+
Object.entries(agentMap)
|
|
838
|
+
.map(([tool, info]) =>
|
|
839
|
+
`- **${tool}**: ${info.count} session(s), last active ${info.lastUsed ? info.lastUsed.substring(0, 16) : "unknown"}\n Recent: ${info.summaries.length > 0 ? info.summaries.map(s => `"${s}"`).join(", ") : "(no summaries)"}`
|
|
840
|
+
)
|
|
841
|
+
.join("\n");
|
|
842
|
+
}
|
|
834
843
|
|
|
835
|
-
|
|
844
|
+
const grade = score >= 80 ? "A" : score >= 60 ? "B" : score >= 40 ? "C" : score >= 20 ? "D" : "F";
|
|
845
|
+
const evtCount = brain.events?.count || 0;
|
|
846
|
+
const revertCount = (brain.state?.reverts || []).length;
|
|
836
847
|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
848
|
+
return {
|
|
849
|
+
content: [
|
|
850
|
+
{
|
|
851
|
+
type: "text",
|
|
852
|
+
text: `## SpecLock Health Check\n\nScore: **${score}/100** (Grade: ${grade})\nEvents: ${evtCount} | Reverts: ${revertCount}\n\n### Checks\n${checks.join("\n")}${agentTimeline}\n\n---\n*SpecLock v${VERSION} — Developed by ${AUTHOR}*`,
|
|
853
|
+
},
|
|
854
|
+
],
|
|
855
|
+
};
|
|
856
|
+
} catch (err) {
|
|
857
|
+
return {
|
|
858
|
+
content: [{ type: "text", text: `## SpecLock Health Check\n\nError: ${err.message}\n\nTry running speclock_init first to initialize the project.\n\n---\n*SpecLock v${VERSION}*` }],
|
|
859
|
+
};
|
|
860
|
+
}
|
|
845
861
|
}
|
|
846
862
|
);
|
|
847
863
|
|