@codyswann/lisa 2.87.0 → 2.89.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 +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/scripts/automation-status-contract-drift.mjs +328 -0
- package/plugins/lisa/scripts/automation-status-expected-fleet.mjs +444 -0
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/src/base/scripts/automation-status-contract-drift.mjs +328 -0
- package/plugins/src/base/scripts/automation-status-expected-fleet.mjs +444 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Shared automation-status contract drift helpers.
|
|
4
|
+
*
|
|
5
|
+
* Runtime adapters resolve the expected Lisa automation fleet, list the live
|
|
6
|
+
* scheduler entries, then use this module to find the best observed match for
|
|
7
|
+
* each expected automation and classify any contract drift.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const DRIFT_LABELS = {
|
|
11
|
+
name: "name",
|
|
12
|
+
cadence: "cadence",
|
|
13
|
+
command: "command",
|
|
14
|
+
queue_arguments: "queue arguments",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {{
|
|
19
|
+
* readonly automationId: string
|
|
20
|
+
* readonly expectedCadence?: string
|
|
21
|
+
* readonly expectedRRule?: string
|
|
22
|
+
* readonly expectedCommand: string
|
|
23
|
+
* }} ExpectedAutomationContract
|
|
24
|
+
*
|
|
25
|
+
* @typedef {{
|
|
26
|
+
* readonly automationId: string
|
|
27
|
+
* readonly observedCadence?: string
|
|
28
|
+
* readonly observedRRule?: string
|
|
29
|
+
* readonly observedCommand?: string
|
|
30
|
+
* }} ObservedAutomationContract
|
|
31
|
+
*
|
|
32
|
+
* @typedef {{
|
|
33
|
+
* readonly status: "HEALTHY" | "MISSING" | "DRIFTED"
|
|
34
|
+
* readonly summary: string
|
|
35
|
+
* readonly observed: string
|
|
36
|
+
* readonly remediation?: string
|
|
37
|
+
* readonly driftKinds: readonly ("name" | "cadence" | "command" | "queue_arguments")[]
|
|
38
|
+
* readonly observedAutomation: ObservedAutomationContract | null
|
|
39
|
+
* }} AutomationContractComparison
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Find the best observed scheduler entry for an expected automation contract.
|
|
44
|
+
*
|
|
45
|
+
* Match order is:
|
|
46
|
+
* 1. Exact automation id
|
|
47
|
+
* 2. Exact command shape and cadence
|
|
48
|
+
* 3. Exact command shape
|
|
49
|
+
* 4. Same command entrypoint
|
|
50
|
+
*
|
|
51
|
+
* @param {ExpectedAutomationContract} expected
|
|
52
|
+
* @param {readonly ObservedAutomationContract[]} observedAutomations
|
|
53
|
+
* @returns {ObservedAutomationContract | null}
|
|
54
|
+
*/
|
|
55
|
+
export function findObservedAutomationMatch(
|
|
56
|
+
expected,
|
|
57
|
+
observedAutomations = []
|
|
58
|
+
) {
|
|
59
|
+
const exactId = observedAutomations.find(
|
|
60
|
+
observed => observed.automationId === expected.automationId
|
|
61
|
+
);
|
|
62
|
+
if (exactId) {
|
|
63
|
+
return exactId;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const expectedCommand = normalizeAutomationCommand(expected.expectedCommand);
|
|
67
|
+
const expectedCadence = normalizeCadenceSignature({
|
|
68
|
+
cadence: expected.expectedCadence,
|
|
69
|
+
rrule: expected.expectedRRule,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const exactContract = observedAutomations.find(observed => {
|
|
73
|
+
const observedCommand = normalizeAutomationCommand(
|
|
74
|
+
observed.observedCommand
|
|
75
|
+
);
|
|
76
|
+
const observedCadence = normalizeCadenceSignature({
|
|
77
|
+
cadence: observed.observedCadence,
|
|
78
|
+
rrule: observed.observedRRule,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
observedCommand.commandSignature === expectedCommand.commandSignature &&
|
|
83
|
+
observedCadence === expectedCadence
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
if (exactContract) {
|
|
87
|
+
return exactContract;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const exactCommand = observedAutomations.find(observed => {
|
|
91
|
+
const observedCommand = normalizeAutomationCommand(
|
|
92
|
+
observed.observedCommand
|
|
93
|
+
);
|
|
94
|
+
return (
|
|
95
|
+
observedCommand.commandSignature === expectedCommand.commandSignature
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
if (exactCommand) {
|
|
99
|
+
return exactCommand;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
observedAutomations.find(observed => {
|
|
104
|
+
const observedCommand = normalizeAutomationCommand(
|
|
105
|
+
observed.observedCommand
|
|
106
|
+
);
|
|
107
|
+
return observedCommand.commandToken === expectedCommand.commandToken;
|
|
108
|
+
}) ?? null
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Compare one expected automation contract against an observed scheduler entry.
|
|
114
|
+
*
|
|
115
|
+
* @param {{
|
|
116
|
+
* readonly expected: ExpectedAutomationContract
|
|
117
|
+
* readonly observedAutomations?: readonly ObservedAutomationContract[]
|
|
118
|
+
* readonly observedAutomation?: ObservedAutomationContract | null
|
|
119
|
+
* }} input
|
|
120
|
+
* @returns {AutomationContractComparison}
|
|
121
|
+
*/
|
|
122
|
+
export function compareAutomationContract(input) {
|
|
123
|
+
const expected = input.expected;
|
|
124
|
+
const observed =
|
|
125
|
+
input.observedAutomation ??
|
|
126
|
+
findObservedAutomationMatch(expected, input.observedAutomations ?? []);
|
|
127
|
+
|
|
128
|
+
if (!observed) {
|
|
129
|
+
return {
|
|
130
|
+
status: "MISSING",
|
|
131
|
+
summary: "expected automation is missing",
|
|
132
|
+
observed: "No live automation matched the expected Lisa contract.",
|
|
133
|
+
remediation:
|
|
134
|
+
"Re-run `/lisa:setup-automations` or recreate the missing scheduler entry.",
|
|
135
|
+
driftKinds: [],
|
|
136
|
+
observedAutomation: null,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const driftKinds = detectDriftKinds(expected, observed);
|
|
141
|
+
const observedSummary = describeObservedAutomation(observed);
|
|
142
|
+
|
|
143
|
+
if (driftKinds.length === 0) {
|
|
144
|
+
return {
|
|
145
|
+
status: "HEALTHY",
|
|
146
|
+
summary: "expected automation exists and matches the contract",
|
|
147
|
+
observed: observedSummary,
|
|
148
|
+
driftKinds,
|
|
149
|
+
observedAutomation: observed,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
status: "DRIFTED",
|
|
155
|
+
summary: formatDriftSummary(driftKinds),
|
|
156
|
+
observed: observedSummary,
|
|
157
|
+
remediation:
|
|
158
|
+
"Re-run `/lisa:setup-automations` or update the scheduler entry to the expected command and cadence.",
|
|
159
|
+
driftKinds,
|
|
160
|
+
observedAutomation: observed,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @param {ExpectedAutomationContract} expected
|
|
166
|
+
* @param {ObservedAutomationContract} observed
|
|
167
|
+
* @returns {readonly ("name" | "cadence" | "command" | "queue_arguments")[]}
|
|
168
|
+
*/
|
|
169
|
+
function detectDriftKinds(expected, observed) {
|
|
170
|
+
const driftKinds = [];
|
|
171
|
+
|
|
172
|
+
if (observed.automationId !== expected.automationId) {
|
|
173
|
+
driftKinds.push("name");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const expectedCadence = normalizeCadenceSignature({
|
|
177
|
+
cadence: expected.expectedCadence,
|
|
178
|
+
rrule: expected.expectedRRule,
|
|
179
|
+
});
|
|
180
|
+
const observedCadence = normalizeCadenceSignature({
|
|
181
|
+
cadence: observed.observedCadence,
|
|
182
|
+
rrule: observed.observedRRule,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
if (expectedCadence !== observedCadence) {
|
|
186
|
+
driftKinds.push("cadence");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const expectedCommand = normalizeAutomationCommand(expected.expectedCommand);
|
|
190
|
+
const observedCommand = normalizeAutomationCommand(observed.observedCommand);
|
|
191
|
+
|
|
192
|
+
if (expectedCommand.commandToken !== observedCommand.commandToken) {
|
|
193
|
+
driftKinds.push("command");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (expectedCommand.queueSignature !== observedCommand.queueSignature) {
|
|
197
|
+
driftKinds.push("queue_arguments");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return driftKinds;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* @param {ObservedAutomationContract} observed
|
|
205
|
+
* @returns {string}
|
|
206
|
+
*/
|
|
207
|
+
function describeObservedAutomation(observed) {
|
|
208
|
+
const name = observed.automationId || "unnamed automation";
|
|
209
|
+
const cadence =
|
|
210
|
+
observed.observedCadence ?? observed.observedRRule ?? "cadence unavailable";
|
|
211
|
+
const command = observed.observedCommand ?? "command unavailable";
|
|
212
|
+
return `${name} runs ${cadence} -> ${command}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* @param {readonly ("name" | "cadence" | "command" | "queue_arguments")[]} driftKinds
|
|
217
|
+
* @returns {string}
|
|
218
|
+
*/
|
|
219
|
+
function formatDriftKinds(driftKinds) {
|
|
220
|
+
const labels = driftKinds.map(kind => DRIFT_LABELS[kind]);
|
|
221
|
+
if (labels.length === 0) {
|
|
222
|
+
return "contract details";
|
|
223
|
+
}
|
|
224
|
+
if (labels.length === 1) {
|
|
225
|
+
return labels[0];
|
|
226
|
+
}
|
|
227
|
+
if (labels.length === 2) {
|
|
228
|
+
return `${labels[0]} and ${labels[1]}`;
|
|
229
|
+
}
|
|
230
|
+
return `${labels.slice(0, -1).join(", ")}, and ${labels.at(-1)}`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* @param {readonly ("name" | "cadence" | "command" | "queue_arguments")[]} driftKinds
|
|
235
|
+
* @returns {string}
|
|
236
|
+
*/
|
|
237
|
+
function formatDriftSummary(driftKinds) {
|
|
238
|
+
const subject = formatDriftKinds(driftKinds);
|
|
239
|
+
return driftKinds.length === 1
|
|
240
|
+
? `${subject} no longer matches setup`
|
|
241
|
+
: `${subject} no longer match setup`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* @param {string | undefined} command
|
|
246
|
+
* @returns {{ readonly commandToken: string, readonly commandSignature: string, readonly queueSignature: string }}
|
|
247
|
+
*/
|
|
248
|
+
function normalizeAutomationCommand(command) {
|
|
249
|
+
const tokens = tokenizeCommand(command);
|
|
250
|
+
const [commandToken = "", ...queueTokens] = tokens;
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
commandToken,
|
|
254
|
+
commandSignature: serializeCommandTokens(tokens),
|
|
255
|
+
queueSignature: serializeQueueTokens(queueTokens),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* @param {{ readonly cadence?: string, readonly rrule?: string }} input
|
|
261
|
+
* @returns {string}
|
|
262
|
+
*/
|
|
263
|
+
function normalizeCadenceSignature(input) {
|
|
264
|
+
if (typeof input.rrule === "string" && input.rrule.trim().length > 0) {
|
|
265
|
+
return input.rrule.trim().toUpperCase();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (typeof input.cadence === "string" && input.cadence.trim().length > 0) {
|
|
269
|
+
return input.cadence.trim().toLowerCase().replace(/\s+/g, " ");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return "";
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* @param {readonly string[]} tokens
|
|
277
|
+
* @returns {string}
|
|
278
|
+
*/
|
|
279
|
+
function serializeCommandTokens(tokens) {
|
|
280
|
+
return tokens.join("\u0000");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Positional queue args stay ordered, while key=value arguments are sorted by
|
|
285
|
+
* key so semantically-equivalent scheduler strings do not false-positive.
|
|
286
|
+
*
|
|
287
|
+
* @param {readonly string[]} queueTokens
|
|
288
|
+
* @returns {string}
|
|
289
|
+
*/
|
|
290
|
+
function serializeQueueTokens(queueTokens) {
|
|
291
|
+
const positional = [];
|
|
292
|
+
const keyed = [];
|
|
293
|
+
|
|
294
|
+
for (const token of queueTokens) {
|
|
295
|
+
if (token.includes("=")) {
|
|
296
|
+
const [key, ...valueParts] = token.split("=");
|
|
297
|
+
keyed.push(`${key}=${valueParts.join("=")}`);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
positional.push(token);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
keyed.sort((left, right) => left.localeCompare(right));
|
|
304
|
+
return [...positional, ...keyed].join("\u0000");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Tokenize simple scheduler commands while preserving quoted values.
|
|
309
|
+
*
|
|
310
|
+
* @param {string | undefined} command
|
|
311
|
+
* @returns {readonly string[]}
|
|
312
|
+
*/
|
|
313
|
+
function tokenizeCommand(command) {
|
|
314
|
+
if (typeof command !== "string" || command.trim().length === 0) {
|
|
315
|
+
return [];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const tokens = [];
|
|
319
|
+
const pattern = /"([^"]*)"|'([^']*)'|[^\s]+/g;
|
|
320
|
+
let match = pattern.exec(command);
|
|
321
|
+
|
|
322
|
+
while (match) {
|
|
323
|
+
tokens.push(match[1] ?? match[2] ?? match[0]);
|
|
324
|
+
match = pattern.exec(command);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return tokens;
|
|
328
|
+
}
|