@codyswann/lisa 2.90.0 → 2.91.1
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-claude-adapter.mjs +645 -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-claude-adapter.mjs +645 -0
package/package.json
CHANGED
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"lodash": ">=4.18.1"
|
|
83
83
|
},
|
|
84
84
|
"name": "@codyswann/lisa",
|
|
85
|
-
"version": "2.
|
|
85
|
+
"version": "2.91.1",
|
|
86
86
|
"description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
|
|
87
87
|
"main": "dist/index.js",
|
|
88
88
|
"exports": {
|
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Shared Claude runtime adapter for `/lisa:automation-status`.
|
|
4
|
+
*
|
|
5
|
+
* Claude exposes scheduler state through `/schedule`, but the exact listing
|
|
6
|
+
* surface can vary between structured metadata and human-readable listings.
|
|
7
|
+
* This adapter accepts either shape, normalizes command/cadence/status data
|
|
8
|
+
* into the shared automation-status contract, and degrades explicitly when
|
|
9
|
+
* Claude does not expose last-run or failure metadata.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { compareAutomationContract } from "./automation-status-contract-drift.mjs";
|
|
13
|
+
|
|
14
|
+
const CLAUDE_RUNTIME_LABEL = "Claude /schedule";
|
|
15
|
+
const CLAUDE_ACTIVE_STATUSES = new Set([
|
|
16
|
+
"ACTIVE",
|
|
17
|
+
"ENABLED",
|
|
18
|
+
"RUNNING",
|
|
19
|
+
"SCHEDULED",
|
|
20
|
+
]);
|
|
21
|
+
const RUN_FAILURE_PATTERN =
|
|
22
|
+
/\b(failed|failure|errored|error|exception|crash(?:ed)?)\b/i;
|
|
23
|
+
const NEGATED_FAILURE_PATTERN =
|
|
24
|
+
/\b(no|without)\s+(?:recent\s+)?fail(?:ure|ed)\b/i;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {import("./automation-status-expected-fleet.mjs").resolveExpectedAutomationFleet extends (...args: any[]) => infer T ? T : never} ExpectedFleet
|
|
28
|
+
*
|
|
29
|
+
* @typedef {{
|
|
30
|
+
* readonly automationId: string
|
|
31
|
+
* readonly status?: string
|
|
32
|
+
* readonly observedCadence?: string
|
|
33
|
+
* readonly observedRRule?: string
|
|
34
|
+
* readonly observedCommand?: string
|
|
35
|
+
* readonly lastRunAt?: string | null
|
|
36
|
+
* readonly lastRunSummary?: string | null
|
|
37
|
+
* readonly lastRunFailed?: boolean | null
|
|
38
|
+
* readonly rawObserved?: string
|
|
39
|
+
* }} ObservedClaudeAutomation
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Inspect the current repo's Claude schedule fleet and map it to the shared
|
|
44
|
+
* automation-status report contract.
|
|
45
|
+
*
|
|
46
|
+
* @param {{
|
|
47
|
+
* readonly expectedFleet: ExpectedFleet
|
|
48
|
+
* readonly scheduleListing?: string | readonly unknown[] | Record<string, unknown> | null
|
|
49
|
+
* readonly now?: string | Date
|
|
50
|
+
* }} input
|
|
51
|
+
* @returns {{
|
|
52
|
+
* readonly runtime: string
|
|
53
|
+
* readonly generatedAt: string
|
|
54
|
+
* readonly groups: readonly {
|
|
55
|
+
* readonly id: string
|
|
56
|
+
* readonly title: string
|
|
57
|
+
* readonly items: readonly {
|
|
58
|
+
* readonly id: string
|
|
59
|
+
* readonly status: "HEALTHY" | "MISSING" | "UNSUPPORTED" | "DRIFTED" | "STALE" | "FAILING"
|
|
60
|
+
* readonly summary: string
|
|
61
|
+
* readonly expectedCadence?: string
|
|
62
|
+
* readonly expectedCommand?: string
|
|
63
|
+
* readonly observed?: string
|
|
64
|
+
* readonly remediation?: string
|
|
65
|
+
* }[]
|
|
66
|
+
* }[]
|
|
67
|
+
* readonly observedAutomations: readonly ObservedClaudeAutomation[]
|
|
68
|
+
* }}}
|
|
69
|
+
*/
|
|
70
|
+
export function inspectClaudeAutomationFleet(input) {
|
|
71
|
+
const expectedFleet = input.expectedFleet;
|
|
72
|
+
const now = normalizeDate(input.now);
|
|
73
|
+
const observedAutomations = listClaudeAutomations({
|
|
74
|
+
scheduleListing: input.scheduleListing,
|
|
75
|
+
automationPrefix: expectedFleet.automationPrefix,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const expectedGroups = new Map([
|
|
79
|
+
["core", []],
|
|
80
|
+
["exploratory", []],
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
for (const expected of expectedFleet.expected) {
|
|
84
|
+
const comparison = compareAutomationContract({
|
|
85
|
+
expected,
|
|
86
|
+
observedAutomations,
|
|
87
|
+
});
|
|
88
|
+
expectedGroups.get(expected.group)?.push(
|
|
89
|
+
createObservedStatusItem({
|
|
90
|
+
expected,
|
|
91
|
+
comparison,
|
|
92
|
+
now,
|
|
93
|
+
})
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const unsupported of expectedFleet.unsupported) {
|
|
98
|
+
expectedGroups.get(unsupported.group)?.push({
|
|
99
|
+
id: unsupported.automationId,
|
|
100
|
+
status: "UNSUPPORTED",
|
|
101
|
+
summary: unsupported.reason,
|
|
102
|
+
expectedCadence: unsupported.expectedCadence,
|
|
103
|
+
observed: "No automation is expected for this repo/runtime combination.",
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
runtime: `${CLAUDE_RUNTIME_LABEL} listing`,
|
|
109
|
+
generatedAt: now.toISOString(),
|
|
110
|
+
groups: [
|
|
111
|
+
{
|
|
112
|
+
id: "1",
|
|
113
|
+
title: "Core automations",
|
|
114
|
+
items: expectedGroups.get("core") ?? [],
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: "2",
|
|
118
|
+
title: "Exploratory automations",
|
|
119
|
+
items: expectedGroups.get("exploratory") ?? [],
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
observedAutomations,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Read repo-scoped Claude `/schedule` entries from either structured metadata
|
|
128
|
+
* or a human-readable listing string.
|
|
129
|
+
*
|
|
130
|
+
* @param {{
|
|
131
|
+
* readonly scheduleListing?: string | readonly unknown[] | Record<string, unknown> | null
|
|
132
|
+
* readonly automationPrefix: string
|
|
133
|
+
* }} input
|
|
134
|
+
* @returns {readonly ObservedClaudeAutomation[]}
|
|
135
|
+
*/
|
|
136
|
+
export function listClaudeAutomations(input) {
|
|
137
|
+
return coerceClaudeScheduleEntries(input.scheduleListing)
|
|
138
|
+
.map(entry => normalizeClaudeScheduleEntry(entry))
|
|
139
|
+
.filter(Boolean)
|
|
140
|
+
.filter(entry => entry.automationId.startsWith(input.automationPrefix))
|
|
141
|
+
.toSorted((left, right) =>
|
|
142
|
+
left.automationId.localeCompare(right.automationId)
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Normalize a Claude `/schedule` command line back into the Lisa slash-command
|
|
148
|
+
* surface expected by the shared drift classifier.
|
|
149
|
+
*
|
|
150
|
+
* @param {string | undefined} command
|
|
151
|
+
* @returns {string | undefined}
|
|
152
|
+
*/
|
|
153
|
+
export function deriveClaudeObservedCommand(command) {
|
|
154
|
+
if (!command) {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const trimmed = command.trim();
|
|
159
|
+
const scheduleWrapped = trimmed.match(
|
|
160
|
+
/^\/schedule\s+(?:"[^"]+"|'[^']+'|`[^`]+`|\S+)\s+(.+)$/
|
|
161
|
+
);
|
|
162
|
+
if (scheduleWrapped?.[1]) {
|
|
163
|
+
return scheduleWrapped[1].trim();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const commandLabel = trimmed.match(/^Command:\s*(.+)$/im);
|
|
167
|
+
if (commandLabel?.[1]) {
|
|
168
|
+
return deriveClaudeObservedCommand(commandLabel[1]);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (trimmed.startsWith("/lisa:") || trimmed.startsWith("/lisa-")) {
|
|
172
|
+
return trimmed;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function createObservedStatusItem(input) {
|
|
179
|
+
const expected = input.expected;
|
|
180
|
+
const comparison = input.comparison;
|
|
181
|
+
const observed = comparison.observedAutomation;
|
|
182
|
+
const runSignal = classifyAutomationRunSignal({
|
|
183
|
+
expected,
|
|
184
|
+
observedAutomation: observed,
|
|
185
|
+
now: input.now,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const observedDetails = [comparison.observed];
|
|
189
|
+
if (observed?.status) {
|
|
190
|
+
observedDetails.push(`Scheduler status: ${observed.status}`);
|
|
191
|
+
}
|
|
192
|
+
if (observed?.lastRunAt) {
|
|
193
|
+
observedDetails.push(`Last run: ${observed.lastRunAt}`);
|
|
194
|
+
} else if (observed) {
|
|
195
|
+
observedDetails.push(
|
|
196
|
+
"Last-run metadata unavailable from Claude /schedule."
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
if (observed?.lastRunSummary) {
|
|
200
|
+
observedDetails.push(`Latest summary: ${observed.lastRunSummary}`);
|
|
201
|
+
} else if (observed?.lastRunFailed == null) {
|
|
202
|
+
observedDetails.push("Failure metadata unavailable from Claude /schedule.");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const status =
|
|
206
|
+
runSignal?.status ??
|
|
207
|
+
/** @type {"HEALTHY" | "MISSING" | "DRIFTED"} */ (comparison.status);
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
id: expected.automationId,
|
|
211
|
+
status,
|
|
212
|
+
summary: composeAutomationSummary({
|
|
213
|
+
comparison,
|
|
214
|
+
runSignal,
|
|
215
|
+
}),
|
|
216
|
+
expectedCadence: expected.expectedCadence,
|
|
217
|
+
expectedCommand: expected.expectedCommand,
|
|
218
|
+
observed: observedDetails.join(" "),
|
|
219
|
+
remediation: runSignal?.remediation ?? comparison.remediation,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function composeAutomationSummary(input) {
|
|
224
|
+
const comparisonSummary = input.comparison.summary;
|
|
225
|
+
if (!input.runSignal) {
|
|
226
|
+
return comparisonSummary;
|
|
227
|
+
}
|
|
228
|
+
if (input.comparison.status === "HEALTHY") {
|
|
229
|
+
return input.runSignal.summary;
|
|
230
|
+
}
|
|
231
|
+
return `${input.runSignal.summary}; ${comparisonSummary}`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function classifyAutomationRunSignal(input) {
|
|
235
|
+
const observed = input.observedAutomation;
|
|
236
|
+
if (!observed) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (
|
|
241
|
+
observed.status &&
|
|
242
|
+
!CLAUDE_ACTIVE_STATUSES.has(observed.status.toUpperCase())
|
|
243
|
+
) {
|
|
244
|
+
return {
|
|
245
|
+
status: "FAILING",
|
|
246
|
+
summary: `scheduler entry is ${observed.status.toLowerCase()}`,
|
|
247
|
+
remediation: "Inspect `/schedule` and re-enable the routine if needed.",
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (observed.lastRunFailed === true) {
|
|
252
|
+
return {
|
|
253
|
+
status: "FAILING",
|
|
254
|
+
summary: "latest recorded run failed",
|
|
255
|
+
remediation:
|
|
256
|
+
"Inspect the latest Claude routine output, fix the failing job, then allow the next scheduled run to proceed.",
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!observed.lastRunAt) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const cadenceMs =
|
|
265
|
+
rruleToIntervalMs(observed.observedRRule) ??
|
|
266
|
+
cadenceLabelToIntervalMs(
|
|
267
|
+
observed.observedCadence ?? input.expected.expectedCadence
|
|
268
|
+
);
|
|
269
|
+
if (!cadenceMs) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const lastRunAt = Date.parse(observed.lastRunAt);
|
|
274
|
+
if (Number.isNaN(lastRunAt)) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const staleAfterMs = cadenceMs * 3;
|
|
279
|
+
if (input.now.getTime() - lastRunAt > staleAfterMs) {
|
|
280
|
+
return {
|
|
281
|
+
status: "STALE",
|
|
282
|
+
summary: "last recorded run is stale for the expected cadence",
|
|
283
|
+
remediation:
|
|
284
|
+
"Inspect why the Claude routine has not run recently, then refresh it from `/lisa:setup-automations` or the `/schedule` surface.",
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function coerceClaudeScheduleEntries(scheduleListing) {
|
|
292
|
+
if (!scheduleListing) {
|
|
293
|
+
return [];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (typeof scheduleListing === "string") {
|
|
297
|
+
const parsedJson = tryParseJson(scheduleListing);
|
|
298
|
+
if (parsedJson) {
|
|
299
|
+
return coerceClaudeScheduleEntries(parsedJson);
|
|
300
|
+
}
|
|
301
|
+
return splitClaudeScheduleListing(scheduleListing);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (Array.isArray(scheduleListing)) {
|
|
305
|
+
return scheduleListing;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (typeof scheduleListing === "object") {
|
|
309
|
+
const nested =
|
|
310
|
+
scheduleListing.entries ??
|
|
311
|
+
scheduleListing.tasks ??
|
|
312
|
+
scheduleListing.routines ??
|
|
313
|
+
scheduleListing.items ??
|
|
314
|
+
scheduleListing.data;
|
|
315
|
+
if (Array.isArray(nested)) {
|
|
316
|
+
return nested;
|
|
317
|
+
}
|
|
318
|
+
return [scheduleListing];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return [];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function splitClaudeScheduleListing(listing) {
|
|
325
|
+
return listing
|
|
326
|
+
.split(/\n\s*\n/g)
|
|
327
|
+
.map(block => block.trim())
|
|
328
|
+
.filter(Boolean);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function normalizeClaudeScheduleEntry(entry) {
|
|
332
|
+
if (typeof entry === "string") {
|
|
333
|
+
return normalizeClaudeScheduleTextEntry(entry);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (!entry || typeof entry !== "object") {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const automationId = firstString(
|
|
341
|
+
entry.automationId,
|
|
342
|
+
entry.id,
|
|
343
|
+
entry.name,
|
|
344
|
+
entry.title
|
|
345
|
+
);
|
|
346
|
+
if (!automationId) {
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const commandSource = firstString(
|
|
351
|
+
entry.command,
|
|
352
|
+
entry.prompt,
|
|
353
|
+
entry.run,
|
|
354
|
+
entry.task
|
|
355
|
+
);
|
|
356
|
+
const cadenceSource = firstString(
|
|
357
|
+
entry.cadence,
|
|
358
|
+
entry.schedule,
|
|
359
|
+
entry.rrule,
|
|
360
|
+
entry.interval
|
|
361
|
+
);
|
|
362
|
+
const lastRunAt = normalizeTimestamp(
|
|
363
|
+
firstString(
|
|
364
|
+
entry.lastRunAt,
|
|
365
|
+
entry.last_run_at,
|
|
366
|
+
entry.lastRun,
|
|
367
|
+
entry.last_run,
|
|
368
|
+
entry.latestRunAt
|
|
369
|
+
)
|
|
370
|
+
);
|
|
371
|
+
const lastRunSummary = firstString(
|
|
372
|
+
entry.lastRunSummary,
|
|
373
|
+
entry.last_run_summary,
|
|
374
|
+
entry.lastResult,
|
|
375
|
+
entry.last_result,
|
|
376
|
+
entry.latestResult,
|
|
377
|
+
entry.latest_result
|
|
378
|
+
);
|
|
379
|
+
const lastRunFailed = deriveRunFailure({
|
|
380
|
+
failed: entry.lastRunFailed ?? entry.last_run_failed,
|
|
381
|
+
summary: lastRunSummary,
|
|
382
|
+
details: firstString(entry.lastError, entry.last_error),
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
automationId,
|
|
387
|
+
status: firstString(entry.status, entry.state),
|
|
388
|
+
observedCadence: humanizeClaudeCadence(cadenceSource),
|
|
389
|
+
observedRRule: normalizeClaudeRRule(cadenceSource),
|
|
390
|
+
observedCommand: deriveClaudeObservedCommand(commandSource),
|
|
391
|
+
lastRunAt,
|
|
392
|
+
lastRunSummary: lastRunSummary ?? null,
|
|
393
|
+
lastRunFailed,
|
|
394
|
+
rawObserved: stringifyObserved(entry),
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function normalizeClaudeScheduleTextEntry(block) {
|
|
399
|
+
const automationId =
|
|
400
|
+
extractField(block, /^(?:ID|Name|Routine|Task):\s*(.+)$/im) ??
|
|
401
|
+
block.match(/\blisa-auto-[a-z0-9-]+\b/i)?.[0];
|
|
402
|
+
if (!automationId) {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const cadenceSource =
|
|
407
|
+
extractField(block, /^(?:Cadence|Schedule):\s*(.+)$/im) ??
|
|
408
|
+
block.match(/^\/schedule\s+(?:"[^"]+"|'[^']+'|`[^`]+`|\S+)/m)?.[0];
|
|
409
|
+
const commandSource =
|
|
410
|
+
extractField(block, /^(?:Command|Prompt):\s*(.+)$/im) ??
|
|
411
|
+
extractField(
|
|
412
|
+
block,
|
|
413
|
+
/^\/schedule\s+(?:"[^"]+"|'[^']+'|`[^`]+`|\S+)\s+(.+)$/im
|
|
414
|
+
);
|
|
415
|
+
const lastRunSummary = extractField(
|
|
416
|
+
block,
|
|
417
|
+
/^(?:Last result|Latest result|Result|Outcome):\s*(.+)$/im
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
automationId,
|
|
422
|
+
status: extractField(block, /^(?:Status|State):\s*(.+)$/im),
|
|
423
|
+
observedCadence: humanizeClaudeCadence(cadenceSource),
|
|
424
|
+
observedRRule: normalizeClaudeRRule(cadenceSource),
|
|
425
|
+
observedCommand: deriveClaudeObservedCommand(commandSource),
|
|
426
|
+
lastRunAt: normalizeTimestamp(
|
|
427
|
+
extractField(block, /^(?:Last run|Latest run|Last executed):\s*(.+)$/im)
|
|
428
|
+
),
|
|
429
|
+
lastRunSummary: lastRunSummary ?? null,
|
|
430
|
+
lastRunFailed: deriveRunFailure({
|
|
431
|
+
summary: lastRunSummary,
|
|
432
|
+
}),
|
|
433
|
+
rawObserved: block.replace(/\s+/g, " ").trim(),
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function normalizeClaudeRRule(value) {
|
|
438
|
+
if (!value) {
|
|
439
|
+
return undefined;
|
|
440
|
+
}
|
|
441
|
+
if (value.startsWith("FREQ=")) {
|
|
442
|
+
return value;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const cadence = value.toLowerCase();
|
|
446
|
+
if (
|
|
447
|
+
cadence === "hourly" ||
|
|
448
|
+
cadence === "every 60 minutes" ||
|
|
449
|
+
cadence === "every hour"
|
|
450
|
+
) {
|
|
451
|
+
return "FREQ=HOURLY;INTERVAL=1";
|
|
452
|
+
}
|
|
453
|
+
if (cadence === "every 10 minutes") {
|
|
454
|
+
return "FREQ=MINUTELY;INTERVAL=10";
|
|
455
|
+
}
|
|
456
|
+
if (
|
|
457
|
+
cadence === "once a day" ||
|
|
458
|
+
cadence === "every day" ||
|
|
459
|
+
cadence === "daily"
|
|
460
|
+
) {
|
|
461
|
+
return "FREQ=DAILY;INTERVAL=1";
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const everyMinutes = cadence.match(/every (\d+) minutes?/);
|
|
465
|
+
if (everyMinutes?.[1]) {
|
|
466
|
+
return `FREQ=MINUTELY;INTERVAL=${everyMinutes[1]}`;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const everyHours = cadence.match(/every (\d+) hours?/);
|
|
470
|
+
if (everyHours?.[1]) {
|
|
471
|
+
return `FREQ=HOURLY;INTERVAL=${everyHours[1]}`;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const everyDays = cadence.match(/every (\d+) days?/);
|
|
475
|
+
if (everyDays?.[1]) {
|
|
476
|
+
return `FREQ=DAILY;INTERVAL=${everyDays[1]}`;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return undefined;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function humanizeClaudeCadence(value) {
|
|
483
|
+
if (!value) {
|
|
484
|
+
return undefined;
|
|
485
|
+
}
|
|
486
|
+
if (value.startsWith("FREQ=")) {
|
|
487
|
+
return humanizeRRule(value);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const normalized = value
|
|
491
|
+
.replace(/^Schedule:\s*/i, "")
|
|
492
|
+
.trim()
|
|
493
|
+
.toLowerCase();
|
|
494
|
+
if (normalized === "hourly") {
|
|
495
|
+
return "every 60 minutes";
|
|
496
|
+
}
|
|
497
|
+
if (normalized === "daily") {
|
|
498
|
+
return "once a day";
|
|
499
|
+
}
|
|
500
|
+
if (normalized === "every day") {
|
|
501
|
+
return "once a day";
|
|
502
|
+
}
|
|
503
|
+
return normalized;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function humanizeRRule(rrule) {
|
|
507
|
+
if (rrule === "FREQ=HOURLY;INTERVAL=1") {
|
|
508
|
+
return "every 60 minutes";
|
|
509
|
+
}
|
|
510
|
+
if (rrule === "FREQ=MINUTELY;INTERVAL=10") {
|
|
511
|
+
return "every 10 minutes";
|
|
512
|
+
}
|
|
513
|
+
if (rrule === "FREQ=DAILY;INTERVAL=1") {
|
|
514
|
+
return "once a day";
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const minutely = rrule.match(/^FREQ=MINUTELY;INTERVAL=(\d+)$/);
|
|
518
|
+
if (minutely?.[1]) {
|
|
519
|
+
return `every ${minutely[1]} minutes`;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const hourly = rrule.match(/^FREQ=HOURLY;INTERVAL=(\d+)$/);
|
|
523
|
+
if (hourly?.[1]) {
|
|
524
|
+
return `every ${Number(hourly[1]) * 60} minutes`;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const daily = rrule.match(/^FREQ=DAILY;INTERVAL=(\d+)$/);
|
|
528
|
+
if (daily?.[1]) {
|
|
529
|
+
return Number(daily[1]) === 1 ? "once a day" : `every ${daily[1]} days`;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return rrule;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function cadenceLabelToIntervalMs(label) {
|
|
536
|
+
if (!label) {
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const everyMinutes = label.match(/^every (\d+) minutes$/);
|
|
541
|
+
if (everyMinutes?.[1]) {
|
|
542
|
+
return Number(everyMinutes[1]) * 60_000;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const oncePerDay = new Set(["once a day", "daily"]);
|
|
546
|
+
if (oncePerDay.has(label)) {
|
|
547
|
+
return 24 * 60 * 60_000;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function rruleToIntervalMs(rrule) {
|
|
554
|
+
if (!rrule) {
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const minutely = rrule.match(/^FREQ=MINUTELY;INTERVAL=(\d+)$/);
|
|
559
|
+
if (minutely?.[1]) {
|
|
560
|
+
return Number(minutely[1]) * 60_000;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const hourly = rrule.match(/^FREQ=HOURLY;INTERVAL=(\d+)$/);
|
|
564
|
+
if (hourly?.[1]) {
|
|
565
|
+
return Number(hourly[1]) * 60 * 60_000;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const daily = rrule.match(/^FREQ=DAILY;INTERVAL=(\d+)$/);
|
|
569
|
+
if (daily?.[1]) {
|
|
570
|
+
return Number(daily[1]) * 24 * 60 * 60_000;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function deriveRunFailure(input) {
|
|
577
|
+
if (typeof input.failed === "boolean") {
|
|
578
|
+
return input.failed;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const signal = [input.summary, input.details].filter(Boolean).join("\n");
|
|
582
|
+
if (!signal) {
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return (
|
|
587
|
+
RUN_FAILURE_PATTERN.test(signal) && !NEGATED_FAILURE_PATTERN.test(signal)
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function normalizeTimestamp(value) {
|
|
592
|
+
if (!value) {
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const isoMatch = value.match(
|
|
597
|
+
/20\d{2}-\d\d-\d\d[T ]\d\d:\d\d:\d\d(?:\.\d+)?(?:Z|[+-]\d\d:\d\d)?/
|
|
598
|
+
);
|
|
599
|
+
if (isoMatch?.[0]) {
|
|
600
|
+
const isoValue = isoMatch[0].replace(" ", "T");
|
|
601
|
+
return Number.isNaN(Date.parse(isoValue)) ? null : isoValue;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function stringifyObserved(value) {
|
|
608
|
+
try {
|
|
609
|
+
return JSON.stringify(value);
|
|
610
|
+
} catch {
|
|
611
|
+
return String(value);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function extractField(value, pattern) {
|
|
616
|
+
const match = value.match(pattern);
|
|
617
|
+
return match?.[1]?.trim();
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function firstString(...values) {
|
|
621
|
+
for (const value of values) {
|
|
622
|
+
if (typeof value === "string" && value.trim()) {
|
|
623
|
+
return value.trim();
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return undefined;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function tryParseJson(value) {
|
|
630
|
+
try {
|
|
631
|
+
return JSON.parse(value);
|
|
632
|
+
} catch {
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function normalizeDate(value) {
|
|
638
|
+
if (value instanceof Date) {
|
|
639
|
+
return value;
|
|
640
|
+
}
|
|
641
|
+
if (typeof value === "string") {
|
|
642
|
+
return new Date(value);
|
|
643
|
+
}
|
|
644
|
+
return new Date();
|
|
645
|
+
}
|