@codyswann/lisa 2.93.0 → 2.95.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 (25) 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/scripts/automation-status-expected-fleet.mjs +7 -132
  5. package/plugins/lisa/scripts/queue-contract-resolution.mjs +458 -0
  6. package/plugins/lisa/scripts/queue-health-classification.mjs +157 -0
  7. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  8. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  9. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  10. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  11. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  12. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  13. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  14. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  15. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  16. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  17. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  18. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  19. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  20. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  21. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  23. package/plugins/src/base/scripts/automation-status-expected-fleet.mjs +7 -132
  24. package/plugins/src/base/scripts/queue-contract-resolution.mjs +458 -0
  25. package/plugins/src/base/scripts/queue-health-classification.mjs +157 -0
@@ -0,0 +1,458 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Shared queue-contract resolution helpers for queue-facing Lisa operator
4
+ * surfaces. These helpers intentionally mirror the same config-resolution
5
+ * defaults that `intake`, `repair-intake`, and future queue-status runtime
6
+ * adapters need, so repo/source/tracker detection does not drift.
7
+ */
8
+
9
+ const GITHUB_REMOTE_PATTERNS = [
10
+ /github\.com[:/](?<owner>[^/]+)\/(?<repo>[^/.]+?)(?:\.git)?$/,
11
+ /^git@github\.com:(?<owner>[^/]+)\/(?<repo>[^/.]+?)(?:\.git)?$/,
12
+ ];
13
+
14
+ const DEFAULT_GITHUB_BUILD_DONE = {
15
+ dev: "status:on-dev",
16
+ staging: "status:on-stg",
17
+ production: "status:done",
18
+ };
19
+
20
+ const DEFAULT_JIRA_BUILD_DONE = {
21
+ dev: "On Dev",
22
+ staging: "On Stg",
23
+ production: "Done",
24
+ };
25
+
26
+ const DEFAULT_GITHUB_LINEAR_PRD_ROLES = {
27
+ draft: "prd-draft",
28
+ ready: "prd-ready",
29
+ in_review: "prd-in-review",
30
+ blocked: "prd-blocked",
31
+ ticketed: "prd-ticketed",
32
+ shipped: "prd-shipped",
33
+ verified: "prd-verified",
34
+ sentinel: "prd-intake-feedback",
35
+ };
36
+
37
+ const DEFAULT_NOTION_PRD_ROLES = {
38
+ draft: "Draft",
39
+ ready: "Ready",
40
+ in_review: "In Review",
41
+ blocked: "Blocked",
42
+ ticketed: "Ticketed",
43
+ shipped: "Shipped",
44
+ verified: "Verified",
45
+ };
46
+
47
+ const DEFAULT_CONFLUENCE_PARENT_ROLES = {
48
+ draft: null,
49
+ ready: null,
50
+ in_review: null,
51
+ blocked: null,
52
+ ticketed: null,
53
+ shipped: null,
54
+ verified: null,
55
+ };
56
+
57
+ /**
58
+ * Resolve the current repo short name per config-resolution's repo-scoping
59
+ * ladder: explicit `.repo`, then `github.repo`, then the origin remote basename.
60
+ *
61
+ * @param {{
62
+ * readonly config?: Record<string, any>
63
+ * readonly gitRemoteUrl?: string
64
+ * }} input
65
+ * @returns {string | null}
66
+ */
67
+ export function resolveCurrentRepo(input = {}) {
68
+ const config = input.config ?? {};
69
+
70
+ if (typeof config.repo === "string" && config.repo.trim().length > 0) {
71
+ return config.repo.trim();
72
+ }
73
+
74
+ const githubRef = resolveGithubRepoRef(config, input.gitRemoteUrl);
75
+ if (githubRef?.repo) {
76
+ return githubRef.repo;
77
+ }
78
+
79
+ return resolveRepoNameFromRemote(input.gitRemoteUrl);
80
+ }
81
+
82
+ /**
83
+ * Resolve the repo's configured build tracker.
84
+ *
85
+ * @param {Record<string, any>} config
86
+ * @returns {string}
87
+ */
88
+ export function resolveBuildTracker(config = {}) {
89
+ if (typeof config.tracker === "string" && config.tracker.trim().length > 0) {
90
+ return config.tracker.trim();
91
+ }
92
+
93
+ throw new Error(
94
+ "Unable to resolve the build tracker from config. tracker must be github, linear, or jira."
95
+ );
96
+ }
97
+
98
+ /**
99
+ * Resolve the repo's configured PRD source. Self-hosted GitHub falls back to
100
+ * `github` when `tracker=github` and a GitHub repo identity is configured.
101
+ *
102
+ * @param {Record<string, any>} config
103
+ * @returns {string}
104
+ */
105
+ export function resolvePrdSource(config = {}) {
106
+ if (typeof config.source === "string" && config.source.trim().length > 0) {
107
+ return config.source.trim();
108
+ }
109
+
110
+ if (
111
+ config.tracker === "github" &&
112
+ config.github?.org &&
113
+ config.github?.repo
114
+ ) {
115
+ return "github";
116
+ }
117
+
118
+ throw new Error(
119
+ "Unable to resolve the PRD source from config. Set source explicitly or use tracker=github self-host with github.org/github.repo."
120
+ );
121
+ }
122
+
123
+ /**
124
+ * Resolve the PRD queue argument shape Lisa batch skills expect.
125
+ *
126
+ * @param {Record<string, any>} config
127
+ * @param {string} [source]
128
+ * @returns {string}
129
+ */
130
+ export function resolvePrdQueueArgument(
131
+ config = {},
132
+ source = resolvePrdSource(config)
133
+ ) {
134
+ switch (source) {
135
+ case "github":
136
+ requireGithubRepo(config);
137
+ return "github intake_mode=prd";
138
+ case "linear":
139
+ requireLinearWorkspace(config);
140
+ return "linear";
141
+ case "notion": {
142
+ const databaseId = config.notion?.prdDatabaseId;
143
+ if (!databaseId) {
144
+ throw new Error(
145
+ "Unable to resolve the PRD queue: notion.prdDatabaseId is required when source=notion."
146
+ );
147
+ }
148
+ return databaseId;
149
+ }
150
+ case "confluence": {
151
+ const parentPageId = config.confluence?.parentPageId;
152
+ const spaceKey = config.confluence?.spaceKey;
153
+ if (!parentPageId && !spaceKey) {
154
+ throw new Error(
155
+ "Unable to resolve the PRD queue: confluence.parentPageId or confluence.spaceKey is required when source=confluence."
156
+ );
157
+ }
158
+ return parentPageId ?? spaceKey;
159
+ }
160
+ default:
161
+ throw new Error(
162
+ `Unable to resolve the PRD queue from config. source=${String(source)} is not a supported Lisa PRD source.`
163
+ );
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Resolve the build queue argument shape Lisa batch skills expect.
169
+ *
170
+ * @param {Record<string, any>} config
171
+ * @param {string} [tracker]
172
+ * @returns {string}
173
+ */
174
+ export function resolveBuildQueueArgument(
175
+ config = {},
176
+ tracker = resolveBuildTracker(config)
177
+ ) {
178
+ switch (tracker) {
179
+ case "github":
180
+ requireGithubRepo(config);
181
+ return "github intake_mode=build";
182
+ case "linear":
183
+ requireLinearWorkspace(config);
184
+ return "linear";
185
+ case "jira": {
186
+ const project = config.jira?.project;
187
+ if (!project) {
188
+ throw new Error(
189
+ "Unable to resolve the build queue: jira.project is required when tracker=jira."
190
+ );
191
+ }
192
+ return project;
193
+ }
194
+ default:
195
+ throw new Error(
196
+ "Unable to resolve the build queue from config. tracker must be github, linear, or jira."
197
+ );
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Resolve the PRD lifecycle roles for the configured source vendor.
203
+ *
204
+ * @param {Record<string, any>} config
205
+ * @param {string} [source]
206
+ * @returns {Record<string, any>}
207
+ */
208
+ export function resolvePrdLifecycleRoles(
209
+ config = {},
210
+ source = resolvePrdSource(config)
211
+ ) {
212
+ switch (source) {
213
+ case "github":
214
+ return {
215
+ vendor: "github",
216
+ kind: "labels",
217
+ roles: resolveObjectRoles(
218
+ config.github?.labels?.prd,
219
+ DEFAULT_GITHUB_LINEAR_PRD_ROLES
220
+ ),
221
+ rollup: {
222
+ closeOnShipped: Boolean(
223
+ config.github?.labels?.prd?.rollup?.closeOnShipped ?? false
224
+ ),
225
+ },
226
+ };
227
+ case "linear":
228
+ return {
229
+ vendor: "linear",
230
+ kind: "labels",
231
+ roles: resolveObjectRoles(
232
+ config.linear?.labels?.prd,
233
+ DEFAULT_GITHUB_LINEAR_PRD_ROLES
234
+ ),
235
+ rollup: {
236
+ closeOnShipped: Boolean(
237
+ config.linear?.labels?.prd?.rollup?.closeOnShipped ?? false
238
+ ),
239
+ },
240
+ };
241
+ case "notion":
242
+ return {
243
+ vendor: "notion",
244
+ kind: "status",
245
+ statusProperty: config.notion?.statusProperty || "Status",
246
+ roles: resolveObjectRoles(
247
+ config.notion?.values,
248
+ DEFAULT_NOTION_PRD_ROLES
249
+ ),
250
+ rollup: {
251
+ closeOnShipped: Boolean(
252
+ config.notion?.rollup?.closeOnShipped ?? false
253
+ ),
254
+ },
255
+ };
256
+ case "confluence":
257
+ return {
258
+ vendor: "confluence",
259
+ kind: "parent-pages",
260
+ roles: resolveObjectRoles(
261
+ config.confluence?.parents,
262
+ DEFAULT_CONFLUENCE_PARENT_ROLES
263
+ ),
264
+ rollup: {
265
+ closeOnShipped: Boolean(
266
+ config.confluence?.rollup?.closeOnShipped ?? false
267
+ ),
268
+ },
269
+ };
270
+ default:
271
+ throw new Error(
272
+ `Unable to resolve PRD lifecycle roles. source=${String(source)} is not a supported Lisa PRD source.`
273
+ );
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Resolve the build lifecycle roles for the configured tracker vendor.
279
+ *
280
+ * @param {Record<string, any>} config
281
+ * @param {string} [tracker]
282
+ * @returns {Record<string, any>}
283
+ */
284
+ export function resolveBuildLifecycleRoles(
285
+ config = {},
286
+ tracker = resolveBuildTracker(config)
287
+ ) {
288
+ switch (tracker) {
289
+ case "github":
290
+ return {
291
+ vendor: "github",
292
+ kind: "labels",
293
+ roles: {
294
+ ready: config.github?.labels?.build?.ready || "status:ready",
295
+ claimed:
296
+ config.github?.labels?.build?.claimed || "status:in-progress",
297
+ blocked: config.github?.labels?.build?.blocked || "status:blocked",
298
+ done:
299
+ config.github?.labels?.build?.done ||
300
+ structuredClone(DEFAULT_GITHUB_BUILD_DONE),
301
+ },
302
+ };
303
+ case "linear":
304
+ return {
305
+ vendor: "linear",
306
+ kind: "labels",
307
+ roles: {
308
+ ready: config.linear?.labels?.build?.ready || "status:ready",
309
+ claimed:
310
+ config.linear?.labels?.build?.claimed || "status:in-progress",
311
+ review: config.linear?.labels?.build?.review || "status:code-review",
312
+ blocked: config.linear?.labels?.build?.blocked || "status:blocked",
313
+ done:
314
+ config.linear?.labels?.build?.done ||
315
+ structuredClone(DEFAULT_GITHUB_BUILD_DONE),
316
+ },
317
+ };
318
+ case "jira":
319
+ return {
320
+ vendor: "jira",
321
+ kind: "workflow",
322
+ roles: {
323
+ ready: config.jira?.workflow?.ready || "Ready",
324
+ claimed: config.jira?.workflow?.claimed || "In Progress",
325
+ review: config.jira?.workflow?.review || "Code Review",
326
+ blocked: config.jira?.workflow?.blocked || "Blocked",
327
+ done:
328
+ config.jira?.workflow?.done ||
329
+ structuredClone(DEFAULT_JIRA_BUILD_DONE),
330
+ },
331
+ };
332
+ default:
333
+ throw new Error(
334
+ `Unable to resolve build lifecycle roles. tracker=${String(tracker)} is not a supported Lisa build tracker.`
335
+ );
336
+ }
337
+ }
338
+
339
+ /**
340
+ * Resolve the repo-scoped queue contract queue-status should report against.
341
+ *
342
+ * @param {{
343
+ * readonly config?: Record<string, any>
344
+ * readonly gitRemoteUrl?: string
345
+ * }} input
346
+ * @returns {{
347
+ * readonly currentRepo: string | null
348
+ * readonly source: string
349
+ * readonly tracker: string
350
+ * readonly prdQueue: { readonly argument: string } & Record<string, any>
351
+ * readonly buildQueue: { readonly argument: string } & Record<string, any>
352
+ * }}
353
+ */
354
+ export function resolveQueueContract(input = {}) {
355
+ const config = input.config ?? {};
356
+ const source = resolvePrdSource(config);
357
+ const tracker = resolveBuildTracker(config);
358
+
359
+ return {
360
+ currentRepo: resolveCurrentRepo(input),
361
+ source,
362
+ tracker,
363
+ prdQueue: {
364
+ argument: resolvePrdQueueArgument(config, source),
365
+ ...resolvePrdLifecycleRoles(config, source),
366
+ },
367
+ buildQueue: {
368
+ argument: resolveBuildQueueArgument(config, tracker),
369
+ ...resolveBuildLifecycleRoles(config, tracker),
370
+ },
371
+ };
372
+ }
373
+
374
+ /**
375
+ * @param {Record<string, any> | undefined} values
376
+ * @param {Record<string, any>} defaults
377
+ * @returns {Record<string, any>}
378
+ */
379
+ function resolveObjectRoles(values, defaults) {
380
+ return {
381
+ ...defaults,
382
+ ...(values ?? {}),
383
+ };
384
+ }
385
+
386
+ /**
387
+ * @param {Record<string, any>} config
388
+ * @param {string | undefined} gitRemoteUrl
389
+ * @returns {{ readonly owner: string, readonly repo: string } | null}
390
+ */
391
+ export function resolveGithubRepoRef(config = {}, gitRemoteUrl) {
392
+ const owner = config.github?.org;
393
+ const repo = config.github?.repo;
394
+
395
+ if (owner && repo) {
396
+ return { owner, repo };
397
+ }
398
+
399
+ if (!gitRemoteUrl) {
400
+ return null;
401
+ }
402
+
403
+ for (const pattern of GITHUB_REMOTE_PATTERNS) {
404
+ const match = gitRemoteUrl.match(pattern);
405
+ if (match?.groups?.owner && match.groups.repo) {
406
+ return {
407
+ owner: match.groups.owner,
408
+ repo: match.groups.repo,
409
+ };
410
+ }
411
+ }
412
+
413
+ return null;
414
+ }
415
+
416
+ /**
417
+ * @param {string | undefined} gitRemoteUrl
418
+ * @returns {string | null}
419
+ */
420
+ function resolveRepoNameFromRemote(gitRemoteUrl) {
421
+ if (!gitRemoteUrl || typeof gitRemoteUrl !== "string") {
422
+ return null;
423
+ }
424
+
425
+ const trimmed = gitRemoteUrl.trim();
426
+ if (!trimmed) {
427
+ return null;
428
+ }
429
+
430
+ const basename = trimmed.split(/[/:]/).pop();
431
+ if (!basename) {
432
+ return null;
433
+ }
434
+
435
+ return basename.replace(/\.git$/i, "") || null;
436
+ }
437
+
438
+ /**
439
+ * @param {Record<string, any>} config
440
+ */
441
+ function requireGithubRepo(config) {
442
+ if (!config.github?.org || !config.github?.repo) {
443
+ throw new Error(
444
+ "Unable to resolve the GitHub queue: github.org and github.repo are required."
445
+ );
446
+ }
447
+ }
448
+
449
+ /**
450
+ * @param {Record<string, any>} config
451
+ */
452
+ function requireLinearWorkspace(config) {
453
+ if (!config.linear?.workspace) {
454
+ throw new Error(
455
+ "Unable to resolve the Linear queue: linear.workspace is required."
456
+ );
457
+ }
458
+ }
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Shared queue-health classification helpers for `/lisa:queue-status`.
4
+ *
5
+ * Queue readers normalize vendor-specific lifecycle data into the small set of
6
+ * signals below so queue-status can distinguish quiet, healthy, stuck, and
7
+ * misconfigured queues without inventing a second lifecycle vocabulary.
8
+ */
9
+
10
+ export const QUEUE_HEALTH_VERDICTS = [
11
+ "IDLE",
12
+ "HEALTHY",
13
+ "ATTENTION_NEEDED",
14
+ "MISCONFIGURED",
15
+ ];
16
+
17
+ /**
18
+ * @typedef {"IDLE" | "HEALTHY" | "ATTENTION_NEEDED" | "MISCONFIGURED"} QueueHealthVerdict
19
+ *
20
+ * @typedef {{
21
+ * readonly queueResolved?: boolean
22
+ * readonly namespaceAdopted?: boolean
23
+ * readonly readyCount?: number
24
+ * readonly activeCount?: number
25
+ * readonly blockedCount?: number
26
+ * readonly stalledCount?: number
27
+ * readonly resolutionError?: string | null
28
+ * }} QueueHealthInput
29
+ */
30
+
31
+ /**
32
+ * Classify a queue using the same high-level concepts intake and repair-intake
33
+ * already rely on:
34
+ * - queue must resolve from config;
35
+ * - lifecycle namespace must be adopted/present;
36
+ * - blocked or stalled work means operator attention is needed;
37
+ * - otherwise ready or active work is healthy;
38
+ * - otherwise the queue is truly idle.
39
+ *
40
+ * @param {QueueHealthInput} input
41
+ * @returns {{
42
+ * readonly verdict: QueueHealthVerdict
43
+ * readonly reasons: readonly string[]
44
+ * readonly counts: Readonly<{
45
+ * ready: number
46
+ * active: number
47
+ * blocked: number
48
+ * stalled: number
49
+ * attentionNeeded: number
50
+ * }>
51
+ * }}
52
+ */
53
+ export function classifyQueueHealth(input = {}) {
54
+ const counts = {
55
+ ready: normalizeCount(input.readyCount),
56
+ active: normalizeCount(input.activeCount),
57
+ blocked: normalizeCount(input.blockedCount),
58
+ stalled: normalizeCount(input.stalledCount),
59
+ };
60
+ const attentionNeeded = counts.blocked + counts.stalled;
61
+
62
+ if (input.queueResolved === false || hasContent(input.resolutionError)) {
63
+ return {
64
+ verdict: "MISCONFIGURED",
65
+ reasons: ["queue-unresolved"],
66
+ counts: {
67
+ ...counts,
68
+ attentionNeeded,
69
+ },
70
+ };
71
+ }
72
+
73
+ if (input.namespaceAdopted === false) {
74
+ return {
75
+ verdict: "MISCONFIGURED",
76
+ reasons: ["lifecycle-namespace-absent"],
77
+ counts: {
78
+ ...counts,
79
+ attentionNeeded,
80
+ },
81
+ };
82
+ }
83
+
84
+ if (attentionNeeded > 0) {
85
+ return {
86
+ verdict: "ATTENTION_NEEDED",
87
+ reasons: [
88
+ counts.blocked > 0 ? "blocked-work-present" : null,
89
+ counts.stalled > 0 ? "stalled-work-present" : null,
90
+ ].filter(Boolean),
91
+ counts: {
92
+ ...counts,
93
+ attentionNeeded,
94
+ },
95
+ };
96
+ }
97
+
98
+ if (counts.ready > 0 || counts.active > 0) {
99
+ return {
100
+ verdict: "HEALTHY",
101
+ reasons: [
102
+ counts.ready > 0 ? "ready-work-present" : null,
103
+ counts.active > 0 ? "active-work-in-flight" : null,
104
+ ].filter(Boolean),
105
+ counts: {
106
+ ...counts,
107
+ attentionNeeded,
108
+ },
109
+ };
110
+ }
111
+
112
+ return {
113
+ verdict: "IDLE",
114
+ reasons: ["no-actionable-work"],
115
+ counts: {
116
+ ...counts,
117
+ attentionNeeded,
118
+ },
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Combine individual queue verdicts into the overall queue-status verdict.
124
+ *
125
+ * @param {readonly { verdict: QueueHealthVerdict }[]} sections
126
+ * @returns {QueueHealthVerdict}
127
+ */
128
+ export function computeOverallQueueVerdict(sections) {
129
+ const verdicts = sections.map(section => section.verdict);
130
+
131
+ if (verdicts.includes("MISCONFIGURED")) {
132
+ return "MISCONFIGURED";
133
+ }
134
+ if (verdicts.includes("ATTENTION_NEEDED")) {
135
+ return "ATTENTION_NEEDED";
136
+ }
137
+ if (verdicts.includes("HEALTHY")) {
138
+ return "HEALTHY";
139
+ }
140
+ return "IDLE";
141
+ }
142
+
143
+ /**
144
+ * @param {number | null | undefined} value
145
+ * @returns {number}
146
+ */
147
+ function normalizeCount(value) {
148
+ return Number.isFinite(value) && value > 0 ? Math.trunc(value) : 0;
149
+ }
150
+
151
+ /**
152
+ * @param {string | null | undefined} value
153
+ * @returns {boolean}
154
+ */
155
+ function hasContent(value) {
156
+ return typeof value === "string" && value.trim().length > 0;
157
+ }