@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 CHANGED
@@ -82,7 +82,7 @@
82
82
  "lodash": ">=4.18.1"
83
83
  },
84
84
  "name": "@codyswann/lisa",
85
- "version": "2.90.0",
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": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.90.0",
3
+ "version": "2.91.1",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.90.0",
3
+ "version": "2.91.1",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -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
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.90.0",
3
+ "version": "2.91.1",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.90.0",
3
+ "version": "2.91.1",
4
4
  "description": "AWS CDK-specific Lisa plugin.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "2.90.0",
3
+ "version": "2.91.1",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "2.90.0",
3
+ "version": "2.91.1",
4
4
  "description": "Expo and React Native-specific skills, agents, rules, and MCP servers.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "2.90.0",
3
+ "version": "2.91.1",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"