@codyswann/lisa 2.95.0 → 2.97.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/queue-status-build-readers.mjs +454 -0
- package/plugins/lisa/scripts/queue-status-prd-readers.mjs +359 -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/queue-status-build-readers.mjs +454 -0
- package/plugins/src/base/scripts/queue-status-prd-readers.mjs +359 -0
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Shared build-side queue readers for `/lisa:queue-status`.
|
|
4
|
+
*
|
|
5
|
+
* These helpers normalize vendor-specific build lifecycle items into a common
|
|
6
|
+
* snapshot shape so queue-status can report lifecycle counts, actionable
|
|
7
|
+
* highlights, and repair-intake signals without drifting from Lisa's build
|
|
8
|
+
* lifecycle contract.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { classifyQueueHealth } from "./queue-health-classification.mjs";
|
|
12
|
+
|
|
13
|
+
export const BUILD_LIFECYCLE_ORDER = [
|
|
14
|
+
"ready",
|
|
15
|
+
"claimed",
|
|
16
|
+
"review",
|
|
17
|
+
"blocked",
|
|
18
|
+
"done",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const ACTIONABLE_ROLE_ORDER = ["blocked", "ready", "claimed", "review"];
|
|
22
|
+
|
|
23
|
+
const HIGHLIGHT_COPY = {
|
|
24
|
+
blocked: {
|
|
25
|
+
summary: "Oldest blocked build item",
|
|
26
|
+
nextStep: "Run /lisa:repair-intake <queue> after clearing the blocker.",
|
|
27
|
+
},
|
|
28
|
+
stalled: {
|
|
29
|
+
summary: "Oldest stalled build item likely actionable for repair-intake",
|
|
30
|
+
nextStep:
|
|
31
|
+
"Run /lisa:repair-intake <queue> to re-evaluate the stalled build item.",
|
|
32
|
+
},
|
|
33
|
+
ready: {
|
|
34
|
+
summary: "Oldest ready build item awaiting intake",
|
|
35
|
+
nextStep: "Run /lisa:intake <queue> to claim the next build issue.",
|
|
36
|
+
},
|
|
37
|
+
claimed: {
|
|
38
|
+
summary: "Oldest claimed build item still in flight",
|
|
39
|
+
nextStep:
|
|
40
|
+
"Inspect the active implementation path before escalating to /lisa:repair-intake <queue>.",
|
|
41
|
+
},
|
|
42
|
+
review: {
|
|
43
|
+
summary: "Oldest build item waiting in review",
|
|
44
|
+
nextStep:
|
|
45
|
+
"Check the linked PR or review handoff before re-running /lisa:intake <queue>.",
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Read a GitHub-backed build queue snapshot from issue payloads.
|
|
51
|
+
*
|
|
52
|
+
* @param {{
|
|
53
|
+
* readonly issues?: readonly Record<string, any>[]
|
|
54
|
+
* readonly roles?: Record<string, any>
|
|
55
|
+
* readonly namespaceAdopted?: boolean
|
|
56
|
+
* readonly queueResolved?: boolean
|
|
57
|
+
* readonly queueArgument?: string
|
|
58
|
+
* readonly resolutionError?: string | null
|
|
59
|
+
* }} input
|
|
60
|
+
*/
|
|
61
|
+
export function readGithubBuildQueueSnapshot(input = {}) {
|
|
62
|
+
const roles = input.roles ?? {};
|
|
63
|
+
const normalizedItems = (input.issues ?? [])
|
|
64
|
+
.map(issue => normalizeGithubBuildIssue(issue, roles))
|
|
65
|
+
.filter(Boolean);
|
|
66
|
+
|
|
67
|
+
return createBuildQueueSnapshot({
|
|
68
|
+
tracker: "github",
|
|
69
|
+
items: normalizedItems,
|
|
70
|
+
roles,
|
|
71
|
+
namespaceAdopted: input.namespaceAdopted,
|
|
72
|
+
queueResolved: input.queueResolved,
|
|
73
|
+
queueArgument: input.queueArgument,
|
|
74
|
+
resolutionError: input.resolutionError,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Build a vendor-agnostic build queue snapshot from normalized lifecycle items.
|
|
80
|
+
*
|
|
81
|
+
* @param {{
|
|
82
|
+
* readonly tracker?: string
|
|
83
|
+
* readonly items?: readonly Record<string, any>[]
|
|
84
|
+
* readonly roles?: Record<string, any>
|
|
85
|
+
* readonly namespaceAdopted?: boolean
|
|
86
|
+
* readonly queueResolved?: boolean
|
|
87
|
+
* readonly queueArgument?: string
|
|
88
|
+
* readonly resolutionError?: string | null
|
|
89
|
+
* }} input
|
|
90
|
+
*/
|
|
91
|
+
export function createBuildQueueSnapshot(input = {}) {
|
|
92
|
+
const roles = normalizeRoles(input.roles);
|
|
93
|
+
const items = normalizeItems(input.items);
|
|
94
|
+
const counts = buildLifecycleCounts(items);
|
|
95
|
+
const repairSignals = buildRepairSignals(items, input.queueArgument);
|
|
96
|
+
const highlights = buildActionableHighlights(
|
|
97
|
+
items,
|
|
98
|
+
repairSignals,
|
|
99
|
+
input.queueArgument
|
|
100
|
+
);
|
|
101
|
+
const queueResolved =
|
|
102
|
+
input.queueResolved ?? typeof input.resolutionError !== "string";
|
|
103
|
+
const namespaceAdopted =
|
|
104
|
+
input.namespaceAdopted ?? inferNamespaceAdopted(items, roles);
|
|
105
|
+
|
|
106
|
+
const health = classifyQueueHealth({
|
|
107
|
+
queueResolved,
|
|
108
|
+
namespaceAdopted,
|
|
109
|
+
readyCount: counts.ready,
|
|
110
|
+
activeCount: counts.claimed + counts.review,
|
|
111
|
+
blockedCount: counts.blocked,
|
|
112
|
+
stalledCount: repairSignals.stalled.length,
|
|
113
|
+
resolutionError: input.resolutionError ?? null,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
tracker: input.tracker ?? "unknown",
|
|
118
|
+
queueResolved,
|
|
119
|
+
namespaceAdopted,
|
|
120
|
+
roles,
|
|
121
|
+
counts,
|
|
122
|
+
highlights,
|
|
123
|
+
repairSignals,
|
|
124
|
+
health,
|
|
125
|
+
resolutionError: input.resolutionError ?? null,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @param {Record<string, any>} issue
|
|
131
|
+
* @param {Record<string, any>} roles
|
|
132
|
+
* @returns {Record<string, any> | null}
|
|
133
|
+
*/
|
|
134
|
+
function normalizeGithubBuildIssue(issue, roles) {
|
|
135
|
+
if (!issue || typeof issue !== "object") {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const role = resolveGithubBuildRole(issue.labels, roles);
|
|
140
|
+
|
|
141
|
+
return normalizeItem({
|
|
142
|
+
id: String(issue.id ?? issue.number ?? issue.url ?? issue.title ?? ""),
|
|
143
|
+
ref:
|
|
144
|
+
issue.number !== undefined && issue.number !== null
|
|
145
|
+
? `#${issue.number}`
|
|
146
|
+
: String(issue.url ?? issue.title ?? ""),
|
|
147
|
+
title: String(issue.title ?? "").trim(),
|
|
148
|
+
url: typeof issue.url === "string" ? issue.url : null,
|
|
149
|
+
createdAt: issue.createdAt ?? null,
|
|
150
|
+
updatedAt: issue.updatedAt ?? null,
|
|
151
|
+
role,
|
|
152
|
+
stalled: Boolean(issue.stalled),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @param {readonly any[] | undefined} labels
|
|
158
|
+
* @param {Record<string, any>} roles
|
|
159
|
+
* @returns {string | null}
|
|
160
|
+
*/
|
|
161
|
+
function resolveGithubBuildRole(labels, roles) {
|
|
162
|
+
if (!Array.isArray(labels)) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const labelNames = new Set(
|
|
167
|
+
labels
|
|
168
|
+
.map(label =>
|
|
169
|
+
typeof label === "string"
|
|
170
|
+
? label
|
|
171
|
+
: typeof label?.name === "string"
|
|
172
|
+
? label.name
|
|
173
|
+
: null
|
|
174
|
+
)
|
|
175
|
+
.filter(Boolean)
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
for (const role of BUILD_LIFECYCLE_ORDER) {
|
|
179
|
+
if (role === "done") {
|
|
180
|
+
if (resolveDoneRoleNames(roles).some(name => labelNames.has(name))) {
|
|
181
|
+
return "done";
|
|
182
|
+
}
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const configuredName = roles[role];
|
|
187
|
+
if (configuredName && labelNames.has(configuredName)) {
|
|
188
|
+
return role;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* @param {Record<string, any> | undefined} roles
|
|
197
|
+
* @returns {Record<string, any>}
|
|
198
|
+
*/
|
|
199
|
+
function normalizeRoles(roles) {
|
|
200
|
+
return {
|
|
201
|
+
ready:
|
|
202
|
+
typeof roles?.ready === "string" && roles.ready.trim().length > 0
|
|
203
|
+
? roles.ready.trim()
|
|
204
|
+
: "ready",
|
|
205
|
+
claimed:
|
|
206
|
+
typeof roles?.claimed === "string" && roles.claimed.trim().length > 0
|
|
207
|
+
? roles.claimed.trim()
|
|
208
|
+
: "claimed",
|
|
209
|
+
review:
|
|
210
|
+
typeof roles?.review === "string" && roles.review.trim().length > 0
|
|
211
|
+
? roles.review.trim()
|
|
212
|
+
: "review",
|
|
213
|
+
blocked:
|
|
214
|
+
typeof roles?.blocked === "string" && roles.blocked.trim().length > 0
|
|
215
|
+
? roles.blocked.trim()
|
|
216
|
+
: "blocked",
|
|
217
|
+
done: normalizeDoneRoles(roles?.done),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* @param {Record<string, any> | undefined} done
|
|
223
|
+
* @returns {Record<string, string>}
|
|
224
|
+
*/
|
|
225
|
+
function normalizeDoneRoles(done) {
|
|
226
|
+
if (typeof done === "string" && done.trim().length > 0) {
|
|
227
|
+
return { default: done.trim() };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const normalized = {};
|
|
231
|
+
for (const [key, value] of Object.entries(done ?? {})) {
|
|
232
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
233
|
+
normalized[key] = value.trim();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return Object.keys(normalized).length > 0 ? normalized : { default: "done" };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* @param {Record<string, any>} roles
|
|
242
|
+
* @returns {readonly string[]}
|
|
243
|
+
*/
|
|
244
|
+
function resolveDoneRoleNames(roles) {
|
|
245
|
+
return Object.values(normalizeDoneRoles(roles?.done));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* @param {readonly Record<string, any>[] | undefined} items
|
|
250
|
+
* @returns {readonly Record<string, any>[]}
|
|
251
|
+
*/
|
|
252
|
+
function normalizeItems(items) {
|
|
253
|
+
return (items ?? []).map(normalizeItem).filter(Boolean);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* @param {Record<string, any>} item
|
|
258
|
+
* @returns {Record<string, any> | null}
|
|
259
|
+
*/
|
|
260
|
+
function normalizeItem(item) {
|
|
261
|
+
if (!item || typeof item !== "object") {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const role =
|
|
266
|
+
typeof item.role === "string" && BUILD_LIFECYCLE_ORDER.includes(item.role)
|
|
267
|
+
? item.role
|
|
268
|
+
: null;
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
id: String(item.id ?? item.ref ?? item.title ?? ""),
|
|
272
|
+
ref: String(item.ref ?? item.id ?? item.title ?? ""),
|
|
273
|
+
title: String(item.title ?? "").trim(),
|
|
274
|
+
url: typeof item.url === "string" ? item.url : null,
|
|
275
|
+
role,
|
|
276
|
+
createdAt: normalizeTimestamp(item.createdAt),
|
|
277
|
+
updatedAt: normalizeTimestamp(item.updatedAt),
|
|
278
|
+
stalled: Boolean(item.stalled),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* @param {readonly Record<string, any>[]} items
|
|
284
|
+
*/
|
|
285
|
+
function buildLifecycleCounts(items) {
|
|
286
|
+
const counts = Object.fromEntries(
|
|
287
|
+
BUILD_LIFECYCLE_ORDER.map(role => [role, 0])
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
for (const item of items) {
|
|
291
|
+
if (item.role && counts[item.role] !== undefined) {
|
|
292
|
+
counts[item.role] += 1;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return counts;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* @param {readonly Record<string, any>[]} items
|
|
301
|
+
* @param {string | undefined} queueArgument
|
|
302
|
+
*/
|
|
303
|
+
function buildRepairSignals(items, queueArgument) {
|
|
304
|
+
const blocked = items
|
|
305
|
+
.filter(item => item.role === "blocked")
|
|
306
|
+
.sort(compareQueueItemsByCreatedAt)
|
|
307
|
+
.map(toRepairSignalItem);
|
|
308
|
+
const stalled = items
|
|
309
|
+
.filter(item => item.stalled)
|
|
310
|
+
.sort(compareQueueItemsByCreatedAt)
|
|
311
|
+
.map(toRepairSignalItem);
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
actionable: blocked.length > 0 || stalled.length > 0,
|
|
315
|
+
blocked,
|
|
316
|
+
stalled,
|
|
317
|
+
suggestedCommand:
|
|
318
|
+
blocked.length > 0 || stalled.length > 0
|
|
319
|
+
? expandHighlightNextStep(
|
|
320
|
+
"Run /lisa:repair-intake <queue> to inspect the most actionable stuck build work.",
|
|
321
|
+
queueArgument
|
|
322
|
+
)
|
|
323
|
+
: null,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* @param {readonly Record<string, any>[]} items
|
|
329
|
+
* @param {Record<string, any>} repairSignals
|
|
330
|
+
* @param {string | undefined} queueArgument
|
|
331
|
+
*/
|
|
332
|
+
function buildActionableHighlights(items, repairSignals, queueArgument) {
|
|
333
|
+
const highlights = [];
|
|
334
|
+
|
|
335
|
+
const oldestStalled = repairSignals.stalled[0];
|
|
336
|
+
if (oldestStalled) {
|
|
337
|
+
highlights.push({
|
|
338
|
+
role: "stalled",
|
|
339
|
+
ref: oldestStalled.ref,
|
|
340
|
+
title: oldestStalled.title,
|
|
341
|
+
url: oldestStalled.url,
|
|
342
|
+
createdAt: oldestStalled.createdAt,
|
|
343
|
+
summary: HIGHLIGHT_COPY.stalled.summary,
|
|
344
|
+
nextStep: expandHighlightNextStep(
|
|
345
|
+
HIGHLIGHT_COPY.stalled.nextStep,
|
|
346
|
+
queueArgument
|
|
347
|
+
),
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
for (const role of ACTIONABLE_ROLE_ORDER) {
|
|
352
|
+
const oldest = findOldestItemForRole(items, role);
|
|
353
|
+
if (!oldest) {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const copy = HIGHLIGHT_COPY[role];
|
|
358
|
+
highlights.push({
|
|
359
|
+
role,
|
|
360
|
+
ref: oldest.ref,
|
|
361
|
+
title: oldest.title,
|
|
362
|
+
url: oldest.url,
|
|
363
|
+
createdAt: oldest.createdAt,
|
|
364
|
+
summary: copy.summary,
|
|
365
|
+
nextStep: expandHighlightNextStep(copy.nextStep, queueArgument),
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return highlights;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* @param {readonly Record<string, any>[]} items
|
|
374
|
+
* @param {string} role
|
|
375
|
+
*/
|
|
376
|
+
function findOldestItemForRole(items, role) {
|
|
377
|
+
return (
|
|
378
|
+
items
|
|
379
|
+
.filter(item => item.role === role)
|
|
380
|
+
.sort(compareQueueItemsByCreatedAt)[0] ?? null
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* @param {readonly Record<string, any>[]} items
|
|
386
|
+
* @param {Record<string, any>} roles
|
|
387
|
+
* @returns {boolean}
|
|
388
|
+
*/
|
|
389
|
+
function inferNamespaceAdopted(items, roles) {
|
|
390
|
+
if (items.some(item => item.role)) {
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return (
|
|
395
|
+
["ready", "claimed", "review", "blocked"].some(
|
|
396
|
+
role => typeof roles[role] === "string" && roles[role].trim().length > 0
|
|
397
|
+
) || resolveDoneRoleNames(roles).length > 0
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* @param {Record<string, any>} item
|
|
403
|
+
* @returns {Record<string, any>}
|
|
404
|
+
*/
|
|
405
|
+
function toRepairSignalItem(item) {
|
|
406
|
+
return {
|
|
407
|
+
ref: item.ref,
|
|
408
|
+
title: item.title,
|
|
409
|
+
url: item.url,
|
|
410
|
+
createdAt: item.createdAt,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* @param {string} template
|
|
416
|
+
* @param {string | undefined} queueArgument
|
|
417
|
+
* @returns {string}
|
|
418
|
+
*/
|
|
419
|
+
function expandHighlightNextStep(template, queueArgument) {
|
|
420
|
+
return template.replace("<queue>", queueArgument ?? "queue");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* @param {Record<string, any>} left
|
|
425
|
+
* @param {Record<string, any>} right
|
|
426
|
+
* @returns {number}
|
|
427
|
+
*/
|
|
428
|
+
function compareQueueItemsByCreatedAt(left, right) {
|
|
429
|
+
const leftMs = left.createdAt
|
|
430
|
+
? Date.parse(left.createdAt)
|
|
431
|
+
: Number.POSITIVE_INFINITY;
|
|
432
|
+
const rightMs = right.createdAt
|
|
433
|
+
? Date.parse(right.createdAt)
|
|
434
|
+
: Number.POSITIVE_INFINITY;
|
|
435
|
+
|
|
436
|
+
if (leftMs !== rightMs) {
|
|
437
|
+
return leftMs - rightMs;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return String(left.ref).localeCompare(String(right.ref));
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* @param {string | null | undefined} value
|
|
445
|
+
* @returns {string | null}
|
|
446
|
+
*/
|
|
447
|
+
function normalizeTimestamp(value) {
|
|
448
|
+
if (typeof value !== "string") {
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const trimmed = value.trim();
|
|
453
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
454
|
+
}
|