speclock 5.2.5 → 5.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/README.md +144 -24
- package/package.json +242 -67
- package/src/cli/index.js +137 -7
- package/src/core/auth.js +341 -341
- package/src/core/compliance.js +1 -1
- package/src/core/engine.js +63 -1
- package/src/core/lock-author.js +487 -487
- package/src/core/replay.js +236 -0
- package/src/core/rules-sync.js +548 -0
- package/src/core/semantics.js +33 -0
- package/src/core/templates.js +69 -0
- package/src/dashboard/index.html +2 -2
- package/src/mcp/http-server.js +3 -3
- package/src/mcp/server.js +130 -1
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpecLock Incident Replay — Session activity log
|
|
3
|
+
* Shows exactly what AI agents tried and what SpecLock caught.
|
|
4
|
+
*
|
|
5
|
+
* Developed by Sandeep Roy (https://github.com/sgroy10)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readBrain, readEvents } from "./storage.js";
|
|
9
|
+
import { ensureInit } from "./memory.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get a formatted replay of a session's events.
|
|
13
|
+
*
|
|
14
|
+
* @param {string} root - Project root
|
|
15
|
+
* @param {Object} options
|
|
16
|
+
* @param {string} [options.sessionId] - Specific session ID to replay. If omitted, replays last session.
|
|
17
|
+
* @param {number} [options.limit] - Max events to show (default: 50)
|
|
18
|
+
* @returns {Object} Replay data with events, stats, and formatted output
|
|
19
|
+
*/
|
|
20
|
+
export function getReplay(root, options = {}) {
|
|
21
|
+
const brain = ensureInit(root);
|
|
22
|
+
const allSessions = [
|
|
23
|
+
...(brain.sessions.current ? [brain.sessions.current] : []),
|
|
24
|
+
...brain.sessions.history,
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
if (allSessions.length === 0) {
|
|
28
|
+
return {
|
|
29
|
+
found: false,
|
|
30
|
+
error: "No sessions recorded yet. Start a session with speclock_session_briefing.",
|
|
31
|
+
events: [],
|
|
32
|
+
stats: { total: 0, allowed: 0, warned: 0, blocked: 0 },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Find the target session
|
|
37
|
+
let session;
|
|
38
|
+
if (options.sessionId) {
|
|
39
|
+
session = allSessions.find((s) => s.id === options.sessionId);
|
|
40
|
+
if (!session) {
|
|
41
|
+
return {
|
|
42
|
+
found: false,
|
|
43
|
+
error: `Session ${options.sessionId} not found. Available: ${allSessions.slice(0, 5).map((s) => s.id).join(", ")}`,
|
|
44
|
+
events: [],
|
|
45
|
+
stats: { total: 0, allowed: 0, warned: 0, blocked: 0 },
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
// Default to current or most recent
|
|
50
|
+
session = allSessions[0];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Get events for this session
|
|
54
|
+
const sinceTime = session.startedAt;
|
|
55
|
+
const untilTime = session.endedAt || new Date().toISOString();
|
|
56
|
+
const limit = options.limit || 50;
|
|
57
|
+
|
|
58
|
+
let events = readEvents(root, { since: sinceTime });
|
|
59
|
+
|
|
60
|
+
// Filter to events within this session's time window
|
|
61
|
+
if (session.endedAt) {
|
|
62
|
+
events = events.filter((e) => e.at <= session.endedAt);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Reverse to chronological order
|
|
66
|
+
events = events.reverse().slice(0, limit);
|
|
67
|
+
|
|
68
|
+
// Categorize events
|
|
69
|
+
const stats = {
|
|
70
|
+
total: events.length,
|
|
71
|
+
allowed: 0,
|
|
72
|
+
warned: 0,
|
|
73
|
+
blocked: 0,
|
|
74
|
+
changes: 0,
|
|
75
|
+
locks_added: 0,
|
|
76
|
+
locks_removed: 0,
|
|
77
|
+
decisions: 0,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const categorized = events.map((evt) => {
|
|
81
|
+
const entry = {
|
|
82
|
+
time: evt.at.substring(11, 19),
|
|
83
|
+
fullTime: evt.at,
|
|
84
|
+
type: evt.type,
|
|
85
|
+
summary: evt.summary || "",
|
|
86
|
+
files: evt.files || [],
|
|
87
|
+
icon: " ",
|
|
88
|
+
level: "INFO",
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
switch (evt.type) {
|
|
92
|
+
case "conflict_blocked":
|
|
93
|
+
entry.icon = "BLOCK";
|
|
94
|
+
entry.level = "BLOCK";
|
|
95
|
+
stats.blocked++;
|
|
96
|
+
break;
|
|
97
|
+
case "conflict_warned":
|
|
98
|
+
entry.icon = "WARN";
|
|
99
|
+
entry.level = "WARN";
|
|
100
|
+
stats.warned++;
|
|
101
|
+
break;
|
|
102
|
+
case "conflict_checked":
|
|
103
|
+
entry.icon = "ALLOW";
|
|
104
|
+
entry.level = "ALLOW";
|
|
105
|
+
stats.allowed++;
|
|
106
|
+
break;
|
|
107
|
+
case "lock_added":
|
|
108
|
+
entry.icon = "LOCK";
|
|
109
|
+
entry.level = "LOCK";
|
|
110
|
+
stats.locks_added++;
|
|
111
|
+
break;
|
|
112
|
+
case "lock_removed":
|
|
113
|
+
entry.icon = "UNLOCK";
|
|
114
|
+
entry.level = "UNLOCK";
|
|
115
|
+
stats.locks_removed++;
|
|
116
|
+
break;
|
|
117
|
+
case "change_logged":
|
|
118
|
+
entry.icon = "CHANGE";
|
|
119
|
+
entry.level = "CHANGE";
|
|
120
|
+
stats.changes++;
|
|
121
|
+
break;
|
|
122
|
+
case "decision_added":
|
|
123
|
+
entry.icon = "DEC";
|
|
124
|
+
entry.level = "DECISION";
|
|
125
|
+
stats.decisions++;
|
|
126
|
+
break;
|
|
127
|
+
case "session_started":
|
|
128
|
+
entry.icon = "START";
|
|
129
|
+
entry.level = "SESSION";
|
|
130
|
+
break;
|
|
131
|
+
case "session_ended":
|
|
132
|
+
entry.icon = "END";
|
|
133
|
+
entry.level = "SESSION";
|
|
134
|
+
break;
|
|
135
|
+
case "revert_detected":
|
|
136
|
+
entry.icon = "REVERT";
|
|
137
|
+
entry.level = "REVERT";
|
|
138
|
+
break;
|
|
139
|
+
case "override_applied":
|
|
140
|
+
entry.icon = "OVERRIDE";
|
|
141
|
+
entry.level = "OVERRIDE";
|
|
142
|
+
break;
|
|
143
|
+
case "context_generated":
|
|
144
|
+
entry.icon = "CTX";
|
|
145
|
+
entry.level = "INFO";
|
|
146
|
+
break;
|
|
147
|
+
default:
|
|
148
|
+
entry.icon = "INFO";
|
|
149
|
+
entry.level = "INFO";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return entry;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Duration
|
|
156
|
+
const startMs = new Date(session.startedAt).getTime();
|
|
157
|
+
const endMs = session.endedAt
|
|
158
|
+
? new Date(session.endedAt).getTime()
|
|
159
|
+
: Date.now();
|
|
160
|
+
const durationMin = Math.round((endMs - startMs) / 60000);
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
found: true,
|
|
164
|
+
session: {
|
|
165
|
+
id: session.id,
|
|
166
|
+
tool: session.toolUsed,
|
|
167
|
+
startedAt: session.startedAt,
|
|
168
|
+
endedAt: session.endedAt || "(active)",
|
|
169
|
+
duration: `${durationMin} min`,
|
|
170
|
+
summary: session.summary || "(in progress)",
|
|
171
|
+
},
|
|
172
|
+
events: categorized,
|
|
173
|
+
stats,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* List available sessions for replay.
|
|
179
|
+
*/
|
|
180
|
+
export function listSessions(root, limit = 10) {
|
|
181
|
+
const brain = ensureInit(root);
|
|
182
|
+
const sessions = [
|
|
183
|
+
...(brain.sessions.current ? [{ ...brain.sessions.current, isCurrent: true }] : []),
|
|
184
|
+
...brain.sessions.history.slice(0, limit),
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
total: (brain.sessions.history?.length || 0) + (brain.sessions.current ? 1 : 0),
|
|
189
|
+
sessions: sessions.map((s) => ({
|
|
190
|
+
id: s.id,
|
|
191
|
+
tool: s.toolUsed,
|
|
192
|
+
startedAt: s.startedAt,
|
|
193
|
+
endedAt: s.endedAt || "(active)",
|
|
194
|
+
summary: s.summary || "(no summary)",
|
|
195
|
+
events: s.eventsInSession || 0,
|
|
196
|
+
isCurrent: s.isCurrent || false,
|
|
197
|
+
})),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Format replay data as a readable string for CLI output.
|
|
203
|
+
*/
|
|
204
|
+
export function formatReplay(replay) {
|
|
205
|
+
if (!replay.found) {
|
|
206
|
+
return replay.error;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const lines = [];
|
|
210
|
+
const s = replay.session;
|
|
211
|
+
|
|
212
|
+
lines.push(`Session: ${s.id} (${s.tool}, ${s.duration})`);
|
|
213
|
+
lines.push(`Started: ${s.startedAt.substring(0, 19).replace("T", " ")}`);
|
|
214
|
+
lines.push(`Ended: ${typeof s.endedAt === "string" && s.endedAt !== "(active)" ? s.endedAt.substring(0, 19).replace("T", " ") : s.endedAt}`);
|
|
215
|
+
lines.push("-".repeat(60));
|
|
216
|
+
|
|
217
|
+
for (const evt of replay.events) {
|
|
218
|
+
const fileStr = evt.files.length > 0 ? ` (${evt.files.slice(0, 3).join(", ")})` : "";
|
|
219
|
+
const pad = evt.icon.length < 6 ? " ".repeat(6 - evt.icon.length) : " ";
|
|
220
|
+
lines.push(`${evt.time} [${evt.icon}]${pad}${evt.summary}${fileStr}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
lines.push("");
|
|
224
|
+
lines.push("-".repeat(60));
|
|
225
|
+
const st = replay.stats;
|
|
226
|
+
const parts = [];
|
|
227
|
+
if (st.allowed > 0) parts.push(`${st.allowed} allowed`);
|
|
228
|
+
if (st.warned > 0) parts.push(`${st.warned} warned`);
|
|
229
|
+
if (st.blocked > 0) parts.push(`${st.blocked} BLOCKED`);
|
|
230
|
+
if (st.changes > 0) parts.push(`${st.changes} changes`);
|
|
231
|
+
if (st.locks_added > 0) parts.push(`${st.locks_added} locks added`);
|
|
232
|
+
|
|
233
|
+
lines.push(`Score: ${st.total} events | ${parts.join(" | ") || "no activity"}`);
|
|
234
|
+
|
|
235
|
+
return lines.join("\n");
|
|
236
|
+
}
|