@codyswann/lisa 2.85.2 → 2.88.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.
Files changed (28) hide show
  1. package/package.json +1 -1
  2. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  3. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  4. package/plugins/lisa/commands/automation-status.md +5 -0
  5. package/plugins/lisa/scripts/automation-status-expected-fleet.mjs +444 -0
  6. package/plugins/lisa/scripts/automation-status-report.mjs +170 -0
  7. package/plugins/lisa/skills/automation-status/SKILL.md +75 -0
  8. package/plugins/lisa/skills/automation-status/agents/openai.yaml +4 -0
  9. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  10. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  11. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  12. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  13. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  14. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  15. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  16. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  17. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  18. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  19. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  20. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  21. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  23. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  25. package/plugins/src/base/commands/automation-status.md +5 -0
  26. package/plugins/src/base/scripts/automation-status-expected-fleet.mjs +444 -0
  27. package/plugins/src/base/scripts/automation-status-report.mjs +170 -0
  28. package/plugins/src/base/skills/automation-status/SKILL.md +75 -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.2",
85
+ "version": "2.88.0",
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.85.2",
3
+ "version": "2.88.0",
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.85.2",
3
+ "version": "2.88.0",
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,5 @@
1
+ ---
2
+ description: "Inspect the current project's Lisa automation fleet and report whether the expected recurring jobs exist, match Lisa's setup contract, and show healthy recent activity. Read-only by default."
3
+ ---
4
+
5
+ Use the /lisa:automation-status skill to inspect the current project's expected Lisa automations, compare them with the runtime's scheduler metadata, and report fleet health, drift, staleness, unsupported jobs, and remediation hints. $ARGUMENTS
@@ -0,0 +1,444 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Shared automation-status expected-fleet helpers.
4
+ *
5
+ * This module resolves the same naming, queue arguments, cadence, and
6
+ * exploratory support decisions documented by `/lisa:setup-automations`, so
7
+ * automation-status and later runtime adapters do not invent a second source of
8
+ * truth.
9
+ */
10
+
11
+ export const AUTOMATION_EXPECTED_CADENCES = {
12
+ "intake-repair": {
13
+ human: "every 60 minutes",
14
+ rrule: "FREQ=HOURLY;INTERVAL=1",
15
+ },
16
+ "intake-prd": {
17
+ human: "every 60 minutes",
18
+ rrule: "FREQ=HOURLY;INTERVAL=1",
19
+ },
20
+ "intake-tickets": {
21
+ human: "every 10 minutes",
22
+ rrule: "FREQ=MINUTELY;INTERVAL=10",
23
+ },
24
+ "exploratory-bugs": {
25
+ human: "once a day",
26
+ rrule: "FREQ=DAILY;INTERVAL=1",
27
+ },
28
+ "exploratory-prds": {
29
+ human: "once a day",
30
+ rrule: "FREQ=DAILY;INTERVAL=1",
31
+ },
32
+ };
33
+
34
+ export const EXPLORATORY_QA_STACK_PRIORITY = ["expo", "rails", "harper-fabric"];
35
+
36
+ const GITHUB_REMOTE_PATTERNS = [
37
+ /github\.com[:/](?<owner>[^/]+)\/(?<repo>[^/.]+?)(?:\.git)?$/,
38
+ /^git@github\.com:(?<owner>[^/]+)\/(?<repo>[^/.]+?)(?:\.git)?$/,
39
+ ];
40
+
41
+ /**
42
+ * @typedef {{
43
+ * readonly id: string
44
+ * readonly automationId: string
45
+ * readonly expectedCadence: string
46
+ * readonly expectedRRule: string
47
+ * readonly expectedCommand: string
48
+ * readonly group: "core" | "exploratory"
49
+ * }} ExpectedAutomationEntry
50
+ *
51
+ * @typedef {{
52
+ * readonly id: string
53
+ * readonly automationId: string
54
+ * readonly group: "core" | "exploratory"
55
+ * readonly reason: string
56
+ * readonly expectedCadence: string
57
+ * readonly expectedRRule: string
58
+ * }} UnsupportedAutomationEntry
59
+ */
60
+
61
+ /**
62
+ * Resolve the stable project identifier and automation prefix used by
63
+ * `/lisa:setup-automations`.
64
+ *
65
+ * @param {{
66
+ * readonly config?: Record<string, any>
67
+ * readonly gitRemoteUrl?: string
68
+ * }} input
69
+ * @returns {{ readonly owner: string, readonly repo: string, readonly project: string, readonly automationPrefix: string }}
70
+ */
71
+ export function resolveAutomationProjectIdentity(input = {}) {
72
+ const githubRef = resolveGithubRepoRef(
73
+ input.config ?? {},
74
+ input.gitRemoteUrl
75
+ );
76
+
77
+ if (!githubRef) {
78
+ throw new Error(
79
+ "Unable to resolve repo identity for automation naming. Configure github.org/github.repo or provide a GitHub origin remote."
80
+ );
81
+ }
82
+
83
+ const project = slugifyProjectToken(`${githubRef.owner}-${githubRef.repo}`);
84
+
85
+ return {
86
+ ...githubRef,
87
+ project,
88
+ automationPrefix: `lisa-auto-${project}-`,
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Resolve the expected Lisa automation fleet for the current repo.
94
+ *
95
+ * @param {{
96
+ * readonly config?: Record<string, any>
97
+ * readonly gitRemoteUrl?: string
98
+ * readonly detectedTypes?: readonly string[]
99
+ * readonly autoStartPrds?: boolean | string
100
+ * readonly autoStartTickets?: boolean | string
101
+ * }} input
102
+ * @returns {{
103
+ * readonly owner: string
104
+ * readonly repo: string
105
+ * readonly project: string
106
+ * readonly automationPrefix: string
107
+ * readonly expected: readonly ExpectedAutomationEntry[]
108
+ * readonly unsupported: readonly UnsupportedAutomationEntry[]
109
+ * }}
110
+ */
111
+ export function resolveExpectedAutomationFleet(input = {}) {
112
+ const config = input.config ?? {};
113
+ const identity = resolveAutomationProjectIdentity(input);
114
+ const autoStartPrds = normalizeBooleanFlag(input.autoStartPrds);
115
+ const autoStartTickets = normalizeBooleanFlag(input.autoStartTickets);
116
+ const detectedTypes = input.detectedTypes ?? [];
117
+
118
+ const tracker = config.tracker;
119
+ const source = resolvePrdSource(config);
120
+ const prdQueue = resolvePrdQueueArgument(config, source);
121
+ const buildQueue = resolveBuildQueueArgument(config, tracker);
122
+ const repairQueue = resolveRepairQueueArgument(config, source, tracker);
123
+
124
+ const expected = [
125
+ createExpectedEntry(
126
+ identity,
127
+ "intake-repair",
128
+ `/lisa:repair-intake ${repairQueue}`,
129
+ "core"
130
+ ),
131
+ createExpectedEntry(
132
+ identity,
133
+ "intake-prd",
134
+ `/lisa:intake ${prdQueue}`,
135
+ "core"
136
+ ),
137
+ createExpectedEntry(
138
+ identity,
139
+ "intake-tickets",
140
+ `/lisa:intake ${buildQueue}`,
141
+ "core"
142
+ ),
143
+ createExpectedEntry(
144
+ identity,
145
+ "exploratory-prds",
146
+ `/lisa:project-ideation prd_ready=${String(autoStartPrds)}`,
147
+ "exploratory"
148
+ ),
149
+ ];
150
+
151
+ const exploratoryStack = resolveExploratoryQaStack(detectedTypes);
152
+ const unsupported = [];
153
+
154
+ if (exploratoryStack) {
155
+ expected.push(
156
+ createExpectedEntry(
157
+ identity,
158
+ "exploratory-bugs",
159
+ `/lisa-${exploratoryStack}:exploratory-qa ready=${String(autoStartTickets)}`,
160
+ "exploratory"
161
+ )
162
+ );
163
+ } else {
164
+ unsupported.push(
165
+ createUnsupportedEntry(
166
+ identity,
167
+ "exploratory-bugs",
168
+ "This repository does not ship an exploratory-qa command surface."
169
+ )
170
+ );
171
+ }
172
+
173
+ return {
174
+ ...identity,
175
+ expected,
176
+ unsupported,
177
+ };
178
+ }
179
+
180
+ /**
181
+ * Return the supported exploratory-qa surface for the detected host stacks.
182
+ *
183
+ * @param {readonly string[]} detectedTypes
184
+ * @returns {string | null}
185
+ */
186
+ export function resolveExploratoryQaStack(detectedTypes = []) {
187
+ for (const stack of EXPLORATORY_QA_STACK_PRIORITY) {
188
+ if (detectedTypes.includes(stack)) {
189
+ return stack;
190
+ }
191
+ }
192
+ return null;
193
+ }
194
+
195
+ /**
196
+ * @param {{ readonly automationPrefix: string }} identity
197
+ * @param {string} id
198
+ * @param {string} expectedCommand
199
+ * @param {"core" | "exploratory"} group
200
+ * @returns {ExpectedAutomationEntry}
201
+ */
202
+ function createExpectedEntry(identity, id, expectedCommand, group) {
203
+ const cadence = AUTOMATION_EXPECTED_CADENCES[id];
204
+ return {
205
+ id,
206
+ automationId: `${identity.automationPrefix}${id}`,
207
+ expectedCadence: cadence.human,
208
+ expectedRRule: cadence.rrule,
209
+ expectedCommand,
210
+ group,
211
+ };
212
+ }
213
+
214
+ /**
215
+ * @param {{ readonly automationPrefix: string }} identity
216
+ * @param {string} id
217
+ * @param {string} reason
218
+ * @returns {UnsupportedAutomationEntry}
219
+ */
220
+ function createUnsupportedEntry(identity, id, reason) {
221
+ const cadence = AUTOMATION_EXPECTED_CADENCES[id];
222
+ return {
223
+ id,
224
+ automationId: `${identity.automationPrefix}${id}`,
225
+ expectedCadence: cadence.human,
226
+ expectedRRule: cadence.rrule,
227
+ group: "exploratory",
228
+ reason,
229
+ };
230
+ }
231
+
232
+ /**
233
+ * @param {Record<string, any>} config
234
+ * @param {string | undefined} source
235
+ * @returns {string}
236
+ */
237
+ function resolvePrdQueueArgument(config, source) {
238
+ switch (source) {
239
+ case "github":
240
+ requireGithubRepo(config);
241
+ return "github intake_mode=prd";
242
+ case "linear":
243
+ requireLinearWorkspace(config);
244
+ return "linear";
245
+ case "notion": {
246
+ const databaseId = config.notion?.prdDatabaseId;
247
+ if (!databaseId) {
248
+ throw new Error(
249
+ "Unable to resolve the PRD queue: notion.prdDatabaseId is required when source=notion."
250
+ );
251
+ }
252
+ return databaseId;
253
+ }
254
+ case "confluence": {
255
+ const parentPageId = config.confluence?.parentPageId;
256
+ const spaceKey = config.confluence?.spaceKey;
257
+ if (!parentPageId && !spaceKey) {
258
+ throw new Error(
259
+ "Unable to resolve the PRD queue: confluence.parentPageId or confluence.spaceKey is required when source=confluence."
260
+ );
261
+ }
262
+ return parentPageId ?? spaceKey;
263
+ }
264
+ case "jira": {
265
+ const project = config.jira?.project;
266
+ if (!project) {
267
+ throw new Error(
268
+ "Unable to resolve the PRD queue: jira.project is required when source=jira."
269
+ );
270
+ }
271
+ return project;
272
+ }
273
+ default:
274
+ throw new Error(
275
+ "Unable to resolve the PRD queue from config. Set source or use tracker=github self-host with github.org/github.repo."
276
+ );
277
+ }
278
+ }
279
+
280
+ /**
281
+ * @param {Record<string, any>} config
282
+ * @param {string | undefined} tracker
283
+ * @returns {string}
284
+ */
285
+ function resolveBuildQueueArgument(config, tracker) {
286
+ switch (tracker) {
287
+ case "github":
288
+ requireGithubRepo(config);
289
+ return "github intake_mode=build";
290
+ case "linear":
291
+ requireLinearWorkspace(config);
292
+ return "linear";
293
+ case "jira": {
294
+ const project = config.jira?.project;
295
+ if (!project) {
296
+ throw new Error(
297
+ "Unable to resolve the build queue: jira.project is required when tracker=jira."
298
+ );
299
+ }
300
+ return project;
301
+ }
302
+ default:
303
+ throw new Error(
304
+ "Unable to resolve the build queue from config. tracker must be github, linear, or jira."
305
+ );
306
+ }
307
+ }
308
+
309
+ /**
310
+ * @param {Record<string, any>} config
311
+ * @param {string | undefined} source
312
+ * @param {string | undefined} tracker
313
+ * @returns {string}
314
+ */
315
+ function resolveRepairQueueArgument(config, source, tracker) {
316
+ if (tracker === "github" && source === "github") {
317
+ requireGithubRepo(config);
318
+ return "github intake_mode=both";
319
+ }
320
+
321
+ if (tracker === "linear" && source === "linear") {
322
+ requireLinearWorkspace(config);
323
+ return "linear";
324
+ }
325
+
326
+ if (tracker === "jira" && source === "jira") {
327
+ const project = config.jira?.project;
328
+ if (!project) {
329
+ throw new Error(
330
+ "Unable to resolve the repair queue: jira.project is required when tracker=jira and source=jira."
331
+ );
332
+ }
333
+ return project;
334
+ }
335
+
336
+ if (tracker === "github" && source === undefined) {
337
+ requireGithubRepo(config);
338
+ return "github intake_mode=both";
339
+ }
340
+
341
+ throw new Error(
342
+ `Unable to resolve a single repair-intake queue for tracker=${String(tracker)} and source=${String(source)} without guessing.`
343
+ );
344
+ }
345
+
346
+ /**
347
+ * @param {Record<string, any>} config
348
+ * @returns {string | undefined}
349
+ */
350
+ function resolvePrdSource(config) {
351
+ if (typeof config.source === "string" && config.source.length > 0) {
352
+ return config.source;
353
+ }
354
+
355
+ if (
356
+ config.tracker === "github" &&
357
+ config.github?.org &&
358
+ config.github?.repo
359
+ ) {
360
+ return "github";
361
+ }
362
+
363
+ return undefined;
364
+ }
365
+
366
+ /**
367
+ * @param {Record<string, any>} config
368
+ * @param {string | undefined} gitRemoteUrl
369
+ * @returns {{ readonly owner: string, readonly repo: string } | null}
370
+ */
371
+ function resolveGithubRepoRef(config, gitRemoteUrl) {
372
+ const owner = config.github?.org;
373
+ const repo = config.github?.repo;
374
+
375
+ if (owner && repo) {
376
+ return { owner, repo };
377
+ }
378
+
379
+ if (!gitRemoteUrl) {
380
+ return null;
381
+ }
382
+
383
+ for (const pattern of GITHUB_REMOTE_PATTERNS) {
384
+ const match = gitRemoteUrl.match(pattern);
385
+ if (match?.groups?.owner && match.groups.repo) {
386
+ return {
387
+ owner: match.groups.owner,
388
+ repo: match.groups.repo,
389
+ };
390
+ }
391
+ }
392
+
393
+ return null;
394
+ }
395
+
396
+ /**
397
+ * @param {Record<string, any>} config
398
+ */
399
+ function requireGithubRepo(config) {
400
+ if (!config.github?.org || !config.github?.repo) {
401
+ throw new Error(
402
+ "Unable to resolve the GitHub queue: github.org and github.repo are required."
403
+ );
404
+ }
405
+ }
406
+
407
+ /**
408
+ * @param {Record<string, any>} config
409
+ */
410
+ function requireLinearWorkspace(config) {
411
+ if (!config.linear?.workspace) {
412
+ throw new Error(
413
+ "Unable to resolve the Linear queue: linear.workspace is required."
414
+ );
415
+ }
416
+ }
417
+
418
+ /**
419
+ * @param {boolean | string | undefined} value
420
+ * @returns {boolean}
421
+ */
422
+ function normalizeBooleanFlag(value) {
423
+ if (typeof value === "boolean") {
424
+ return value;
425
+ }
426
+
427
+ if (typeof value === "string") {
428
+ return value.toLowerCase() === "true";
429
+ }
430
+
431
+ return false;
432
+ }
433
+
434
+ /**
435
+ * @param {string} value
436
+ * @returns {string}
437
+ */
438
+ function slugifyProjectToken(value) {
439
+ return value
440
+ .trim()
441
+ .toLowerCase()
442
+ .replace(/[^a-z0-9]+/g, "-")
443
+ .replace(/^-+|-+$/g, "");
444
+ }
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Shared automation-status report helpers for the base Lisa operator surface.
4
+ *
5
+ * Keep this dependency-free so the grouped fleet rendering contract can ship in
6
+ * plugin distributions and downstream repos before the runtime adapters land.
7
+ */
8
+
9
+ export const AUTOMATION_HEALTH_STATUSES = [
10
+ "HEALTHY",
11
+ "MISSING",
12
+ "UNSUPPORTED",
13
+ "DRIFTED",
14
+ "STALE",
15
+ "FAILING",
16
+ ];
17
+
18
+ export const AUTOMATION_FLEET_VERDICTS = [
19
+ "HEALTHY",
20
+ "PARTIAL_SUPPORT",
21
+ "ATTENTION_NEEDED",
22
+ ];
23
+
24
+ /**
25
+ * @typedef {"HEALTHY" | "MISSING" | "UNSUPPORTED" | "DRIFTED" | "STALE" | "FAILING"} AutomationHealthStatus
26
+ * @typedef {"HEALTHY" | "PARTIAL_SUPPORT" | "ATTENTION_NEEDED"} AutomationFleetVerdict
27
+ *
28
+ * @typedef {{
29
+ * readonly id: string
30
+ * readonly status: AutomationHealthStatus
31
+ * readonly summary: string
32
+ * readonly expectedCadence?: string
33
+ * readonly expectedCommand?: string
34
+ * readonly observed?: string
35
+ * readonly remediation?: string
36
+ * }} AutomationStatusItem
37
+ *
38
+ * @typedef {{
39
+ * readonly id: string
40
+ * readonly title: string
41
+ * readonly items: readonly AutomationStatusItem[]
42
+ * }} AutomationStatusGroup
43
+ *
44
+ * @typedef {{
45
+ * readonly runtime?: string
46
+ * readonly generatedAt?: string
47
+ * readonly groups: readonly AutomationStatusGroup[]
48
+ * }} AutomationStatusReportInput
49
+ */
50
+
51
+ /**
52
+ * @param {readonly AutomationStatusGroup[]} groups
53
+ * @returns {AutomationFleetVerdict}
54
+ */
55
+ export function computeAutomationFleetVerdict(groups) {
56
+ const items = groups.flatMap(group => group.items);
57
+ if (
58
+ items.some(item =>
59
+ ["MISSING", "DRIFTED", "STALE", "FAILING"].includes(item.status)
60
+ )
61
+ ) {
62
+ return "ATTENTION_NEEDED";
63
+ }
64
+ if (items.some(item => item.status === "UNSUPPORTED")) {
65
+ return "PARTIAL_SUPPORT";
66
+ }
67
+ return "HEALTHY";
68
+ }
69
+
70
+ /**
71
+ * @param {readonly AutomationStatusGroup[]} groups
72
+ * @returns {Record<AutomationHealthStatus, number>}
73
+ */
74
+ export function countAutomationHealthStatuses(groups) {
75
+ return groups
76
+ .flatMap(group => group.items)
77
+ .reduce(
78
+ (counts, item) => ({
79
+ ...counts,
80
+ [item.status]: counts[item.status] + 1,
81
+ }),
82
+ {
83
+ HEALTHY: 0,
84
+ MISSING: 0,
85
+ UNSUPPORTED: 0,
86
+ DRIFTED: 0,
87
+ STALE: 0,
88
+ FAILING: 0,
89
+ }
90
+ );
91
+ }
92
+
93
+ /**
94
+ * @param {AutomationStatusReportInput} input
95
+ * @returns {{ readonly verdict: AutomationFleetVerdict, readonly counts: Record<AutomationHealthStatus, number>, readonly text: string }}
96
+ */
97
+ export function renderAutomationStatusReport(input) {
98
+ const groups = input.groups.map(normalizeGroup);
99
+ const verdict = computeAutomationFleetVerdict(groups);
100
+ const counts = countAutomationHealthStatuses(groups);
101
+ const lines = [
102
+ `Overall verdict: ${verdict}`,
103
+ `Counts: ${AUTOMATION_HEALTH_STATUSES.map(status => `${counts[status]} ${status}`).join(", ")}`,
104
+ ];
105
+
106
+ if (input.runtime) {
107
+ lines.push(`Runtime inspected: ${input.runtime}`);
108
+ }
109
+
110
+ if (input.generatedAt) {
111
+ lines.push(`Generated at: ${input.generatedAt}`);
112
+ }
113
+
114
+ for (const group of groups) {
115
+ lines.push("", `${group.id}. ${group.title}`);
116
+ if (group.items.length === 0) {
117
+ lines.push(
118
+ "- UNSUPPORTED empty-group: no automations expected in this group"
119
+ );
120
+ continue;
121
+ }
122
+
123
+ for (const item of group.items) {
124
+ lines.push(`- ${item.status} ${item.id}: ${item.summary}`);
125
+ if (item.expectedCadence || item.expectedCommand) {
126
+ const cadence = item.expectedCadence ?? "cadence unavailable";
127
+ const command = item.expectedCommand ?? "command unavailable";
128
+ lines.push(` Expected: ${cadence} -> ${command}`);
129
+ }
130
+ if (item.observed) {
131
+ lines.push(` Observed: ${item.observed}`);
132
+ }
133
+ if (item.remediation) {
134
+ lines.push(` Remediation: ${item.remediation}`);
135
+ }
136
+ }
137
+ }
138
+
139
+ return {
140
+ verdict,
141
+ counts,
142
+ text: `${lines.join("\n")}\n`,
143
+ };
144
+ }
145
+
146
+ /**
147
+ * @param {AutomationStatusGroup} group
148
+ * @returns {AutomationStatusGroup}
149
+ */
150
+ function normalizeGroup(group) {
151
+ return {
152
+ ...group,
153
+ items: group.items.map(normalizeItem),
154
+ };
155
+ }
156
+
157
+ /**
158
+ * @param {AutomationStatusItem} item
159
+ * @returns {AutomationStatusItem}
160
+ */
161
+ function normalizeItem(item) {
162
+ const normalizedStatus = AUTOMATION_HEALTH_STATUSES.includes(item.status)
163
+ ? item.status
164
+ : "FAILING";
165
+
166
+ return {
167
+ ...item,
168
+ status: normalizedStatus,
169
+ };
170
+ }