@codyswann/lisa 2.87.0 → 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.
- 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-expected-fleet.mjs +444 -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-expected-fleet.mjs +444 -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.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": {
|
|
@@ -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
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.88.0",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.88.0",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, across Claude and Codex.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -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
|
+
}
|