@codyswann/lisa 2.252.0 → 2.253.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/.codex-plugin/skills/lisa-automation-status/SKILL.md +14 -8
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-automations/SKILL.md +116 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-tear-down-automations/SKILL.md +25 -5
- package/plugins/lisa/scripts/automation-status-claude-adapter.mjs +36 -18
- package/plugins/lisa/scripts/automation-status-codex-adapter.mjs +50 -27
- package/plugins/lisa/scripts/automation-status-expected-fleet.mjs +149 -6
- package/plugins/lisa/skills/lisa-automation-status/SKILL.md +14 -8
- package/plugins/lisa/skills/lisa-setup-automations/SKILL.md +116 -1
- package/plugins/lisa/skills/lisa-tear-down-automations/SKILL.md +26 -6
- package/plugins/lisa/skills/lisa-tear-down-automations/agents/openai.yaml +2 -2
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/scripts/automation-status-claude-adapter.mjs +36 -18
- package/plugins/lisa-agy/scripts/automation-status-codex-adapter.mjs +50 -27
- package/plugins/lisa-agy/scripts/automation-status-expected-fleet.mjs +149 -6
- package/plugins/lisa-agy/skills/lisa-automation-status/SKILL.md +14 -8
- package/plugins/lisa-agy/skills/lisa-setup-automations/SKILL.md +116 -1
- package/plugins/lisa-agy/skills/lisa-tear-down-automations/SKILL.md +26 -6
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/scripts/automation-status-claude-adapter.mjs +36 -18
- package/plugins/lisa-copilot/scripts/automation-status-codex-adapter.mjs +50 -27
- package/plugins/lisa-copilot/scripts/automation-status-expected-fleet.mjs +149 -6
- package/plugins/lisa-copilot/skills/lisa-automation-status/SKILL.md +14 -8
- package/plugins/lisa-copilot/skills/lisa-setup-automations/SKILL.md +116 -1
- package/plugins/lisa-copilot/skills/lisa-tear-down-automations/SKILL.md +26 -6
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/scripts/automation-status-claude-adapter.mjs +36 -18
- package/plugins/lisa-cursor/scripts/automation-status-codex-adapter.mjs +50 -27
- package/plugins/lisa-cursor/scripts/automation-status-expected-fleet.mjs +149 -6
- package/plugins/lisa-cursor/skills/lisa-automation-status/SKILL.md +14 -8
- package/plugins/lisa-cursor/skills/lisa-setup-automations/SKILL.md +116 -1
- package/plugins/lisa-cursor/skills/lisa-tear-down-automations/SKILL.md +26 -6
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-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-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-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-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-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-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-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-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-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-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-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/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/scripts/automation-status-claude-adapter.mjs +36 -18
- package/plugins/src/base/scripts/automation-status-codex-adapter.mjs +50 -27
- package/plugins/src/base/scripts/automation-status-expected-fleet.mjs +149 -6
- package/plugins/src/base/skills/lisa-automation-status/SKILL.md +14 -8
- package/plugins/src/base/skills/lisa-setup-automations/SKILL.md +116 -1
- package/plugins/src/base/skills/lisa-tear-down-automations/SKILL.md +26 -6
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: lisa-tear-down-automations
|
|
3
|
-
description: "Remove every recurring Lisa automation that /setup-automations
|
|
3
|
+
description: "Remove every recurring Lisa automation that /setup-automations registered for this project — whatever is actually registered under the lisa-auto-<project>-* prefix, including the opt-in learnings-audit gardener — using the CURRENT runtime's native scheduler — Codex automations or, on Claude, /schedule. This skill is a declarative specification: it identifies WHICH automations to remove; it does not run teardown scripts. Carries no fixed list of loops: the registration set is the roster. Removes only this project's Lisa automations — never other projects' automations or non-Lisa ones. Leaves checked-in runbook files on disk. The inverse of /setup-automations."
|
|
4
4
|
allowed-tools: ["Skill", "Bash", "Read"]
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -21,14 +21,34 @@ removes them with its **native** scheduling mechanism.
|
|
|
21
21
|
|
|
22
22
|
## Scope (remove only what setup created)
|
|
23
23
|
|
|
24
|
-
- Remove
|
|
25
|
-
stable `lisa-auto-<project>-` name prefix
|
|
26
|
-
`
|
|
24
|
+
- Remove **every** automation `/setup-automations` registered for the current project — the whole
|
|
25
|
+
set found under the stable `lisa-auto-<project>-` name prefix, whatever it currently contains.
|
|
26
|
+
**Membership is registration, not a roster** (`automation-runbook-contract`): sweep the prefix and
|
|
27
|
+
remove what is there. Do **not** work from a fixed list of loop names — a list drifts the moment a
|
|
28
|
+
loop is added, which is exactly how the opt-in gardener came to be orphaned.
|
|
29
|
+
- This explicitly includes the opt-in **`learnings-audit`** gardener when it is registered:
|
|
30
|
+
`/setup-automations learnings-audit=true` registers it under the same prefix, so teardown removes
|
|
31
|
+
it with the rest. A conditionally-skipped loop (e.g. `exploratory-bugs` on a stack without
|
|
32
|
+
`exploratory-qa`) simply is not in the sweep.
|
|
27
33
|
- **Never** remove automations for a different project, or any non-Lisa automation (e.g. unrelated
|
|
28
34
|
crawlers/ingestors). Match strictly on the `lisa-auto-<project>-` prefix for THIS project; when in
|
|
29
35
|
doubt about an automation's ownership, leave it and report it rather than deleting it.
|
|
30
|
-
- **Idempotent** — an automation that is already absent is a no-op, not an error.
|
|
36
|
+
- **Idempotent** — an automation that is already absent is a no-op, not an error. Re-running when
|
|
37
|
+
the prefix sweep finds nothing is a clean, successful no-op.
|
|
38
|
+
- **Leave the runbooks alone.** The checked-in `.lisa/automations/<loop-id>.runbook.md` files that
|
|
39
|
+
`/setup-automations` scaffolded are project knowledge and the historical record of what these
|
|
40
|
+
loops did. Teardown removes scheduler registrations only; it never deletes, edits, or moves a
|
|
41
|
+
runbook file. An operator who wants them gone removes them deliberately, in git.
|
|
31
42
|
|
|
32
43
|
## Report
|
|
33
44
|
|
|
34
|
-
List each automation removed
|
|
45
|
+
List each automation removed by name. For "already absent", compare against the one source of truth
|
|
46
|
+
— the fleet `scripts/automation-status-expected-fleet.mjs` (`resolveExpectedAutomationFleet`)
|
|
47
|
+
resolves for this project — and name anything it expects that the sweep did not find; that is a
|
|
48
|
+
no-op, not an error. Do not invent an expected set of your own.
|
|
49
|
+
|
|
50
|
+
Then state, in the operator's words, that the runbook files under `.lisa/automations/` were left on
|
|
51
|
+
disk **and why**: they are the written record of what those jobs did, kept on purpose, and if you
|
|
52
|
+
do not want them you delete them yourself in git. Finally, confirm that nothing outside this
|
|
53
|
+
project's `lisa-auto-<project>-` prefix was touched. Write it so a non-technical operator can
|
|
54
|
+
confirm what happened without reading code.
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { compareAutomationFleet } from "./automation-status-contract-drift.mjs";
|
|
13
|
+
import {
|
|
14
|
+
assignToAutomationGroup,
|
|
15
|
+
createAutomationGroupBins,
|
|
16
|
+
renderAutomationGroups,
|
|
17
|
+
} from "./automation-status-expected-fleet.mjs";
|
|
13
18
|
|
|
14
19
|
const CLAUDE_RUNTIME_LABEL = "Claude /schedule";
|
|
15
20
|
const CLAUDE_ACTIVE_STATUSES = new Set([
|
|
@@ -75,10 +80,7 @@ export function inspectClaudeAutomationFleet(input) {
|
|
|
75
80
|
automationPrefix: expectedFleet.automationPrefix,
|
|
76
81
|
});
|
|
77
82
|
|
|
78
|
-
const expectedGroups =
|
|
79
|
-
["core", []],
|
|
80
|
-
["exploratory", []],
|
|
81
|
-
]);
|
|
83
|
+
const expectedGroups = createAutomationGroupBins();
|
|
82
84
|
|
|
83
85
|
const comparisons = compareAutomationFleet({
|
|
84
86
|
expectedAutomations: expectedFleet.expected,
|
|
@@ -87,7 +89,9 @@ export function inspectClaudeAutomationFleet(input) {
|
|
|
87
89
|
|
|
88
90
|
for (const [index, expected] of expectedFleet.expected.entries()) {
|
|
89
91
|
const comparison = comparisons[index];
|
|
90
|
-
|
|
92
|
+
assignToAutomationGroup(
|
|
93
|
+
expectedGroups,
|
|
94
|
+
expected.group,
|
|
91
95
|
createObservedStatusItem({
|
|
92
96
|
expected,
|
|
93
97
|
comparison,
|
|
@@ -97,7 +101,7 @@ export function inspectClaudeAutomationFleet(input) {
|
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
for (const unsupported of expectedFleet.unsupported) {
|
|
100
|
-
expectedGroups
|
|
104
|
+
assignToAutomationGroup(expectedGroups, unsupported.group, {
|
|
101
105
|
id: unsupported.automationId,
|
|
102
106
|
status: "UNSUPPORTED",
|
|
103
107
|
summary: unsupported.reason,
|
|
@@ -109,18 +113,7 @@ export function inspectClaudeAutomationFleet(input) {
|
|
|
109
113
|
return {
|
|
110
114
|
runtime: `${CLAUDE_RUNTIME_LABEL} listing`,
|
|
111
115
|
generatedAt: now.toISOString(),
|
|
112
|
-
groups:
|
|
113
|
-
{
|
|
114
|
-
id: "1",
|
|
115
|
-
title: "Core automations",
|
|
116
|
-
items: expectedGroups.get("core") ?? [],
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
id: "2",
|
|
120
|
-
title: "Exploratory automations",
|
|
121
|
-
items: expectedGroups.get("exploratory") ?? [],
|
|
122
|
-
},
|
|
123
|
-
],
|
|
116
|
+
groups: renderAutomationGroups(expectedGroups),
|
|
124
117
|
observedAutomations,
|
|
125
118
|
};
|
|
126
119
|
}
|
|
@@ -487,6 +480,13 @@ function normalizeClaudeRRule(value) {
|
|
|
487
480
|
) {
|
|
488
481
|
return "FREQ=DAILY;INTERVAL=1";
|
|
489
482
|
}
|
|
483
|
+
if (
|
|
484
|
+
cadence === "once a week" ||
|
|
485
|
+
cadence === "every week" ||
|
|
486
|
+
cadence === "weekly"
|
|
487
|
+
) {
|
|
488
|
+
return "FREQ=WEEKLY;INTERVAL=1";
|
|
489
|
+
}
|
|
490
490
|
|
|
491
491
|
const everyMinutes = cadence.match(/every (\d+) minutes?/);
|
|
492
492
|
if (everyMinutes?.[1]) {
|
|
@@ -503,6 +503,11 @@ function normalizeClaudeRRule(value) {
|
|
|
503
503
|
return `FREQ=DAILY;INTERVAL=${everyDays[1]}`;
|
|
504
504
|
}
|
|
505
505
|
|
|
506
|
+
const everyWeeks = cadence.match(/every (\d+) weeks?/);
|
|
507
|
+
if (everyWeeks?.[1]) {
|
|
508
|
+
return `FREQ=WEEKLY;INTERVAL=${everyWeeks[1]}`;
|
|
509
|
+
}
|
|
510
|
+
|
|
506
511
|
return undefined;
|
|
507
512
|
}
|
|
508
513
|
|
|
@@ -527,6 +532,9 @@ function humanizeClaudeCadence(value) {
|
|
|
527
532
|
if (normalized === "every day") {
|
|
528
533
|
return "once a day";
|
|
529
534
|
}
|
|
535
|
+
if (normalized === "weekly" || normalized === "every week") {
|
|
536
|
+
return "once a week";
|
|
537
|
+
}
|
|
530
538
|
return normalized;
|
|
531
539
|
}
|
|
532
540
|
|
|
@@ -556,6 +564,11 @@ function humanizeRRule(rrule) {
|
|
|
556
564
|
return Number(daily[1]) === 1 ? "once a day" : `every ${daily[1]} days`;
|
|
557
565
|
}
|
|
558
566
|
|
|
567
|
+
const weekly = rrule.match(/^FREQ=WEEKLY;INTERVAL=(\d+)$/);
|
|
568
|
+
if (weekly?.[1]) {
|
|
569
|
+
return Number(weekly[1]) === 1 ? "once a week" : `every ${weekly[1]} weeks`;
|
|
570
|
+
}
|
|
571
|
+
|
|
559
572
|
return rrule;
|
|
560
573
|
}
|
|
561
574
|
|
|
@@ -574,6 +587,11 @@ function cadenceLabelToIntervalMs(label) {
|
|
|
574
587
|
return 24 * 60 * 60_000;
|
|
575
588
|
}
|
|
576
589
|
|
|
590
|
+
const oncePerWeek = new Set(["once a week", "weekly"]);
|
|
591
|
+
if (oncePerWeek.has(label)) {
|
|
592
|
+
return 7 * 24 * 60 * 60_000;
|
|
593
|
+
}
|
|
594
|
+
|
|
577
595
|
return null;
|
|
578
596
|
}
|
|
579
597
|
|
|
@@ -17,6 +17,11 @@ import { execFile } from "node:child_process";
|
|
|
17
17
|
import { promisify } from "node:util";
|
|
18
18
|
|
|
19
19
|
import { compareAutomationFleet } from "./automation-status-contract-drift.mjs";
|
|
20
|
+
import {
|
|
21
|
+
assignToAutomationGroup,
|
|
22
|
+
createAutomationGroupBins,
|
|
23
|
+
renderAutomationGroups,
|
|
24
|
+
} from "./automation-status-expected-fleet.mjs";
|
|
20
25
|
|
|
21
26
|
const CODEx_RUNTIME_LABEL = "Codex automations";
|
|
22
27
|
const RUN_TIMESTAMP_PATTERN = /20\d{2}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?Z/;
|
|
@@ -156,10 +161,7 @@ export async function inspectCodexAutomationFleet(input) {
|
|
|
156
161
|
automationPrefix: expectedFleet.automationPrefix,
|
|
157
162
|
});
|
|
158
163
|
|
|
159
|
-
const expectedGroups =
|
|
160
|
-
["core", []],
|
|
161
|
-
["exploratory", []],
|
|
162
|
-
]);
|
|
164
|
+
const expectedGroups = createAutomationGroupBins();
|
|
163
165
|
|
|
164
166
|
const comparisons = compareAutomationFleet({
|
|
165
167
|
expectedAutomations: expectedFleet.expected,
|
|
@@ -168,7 +170,9 @@ export async function inspectCodexAutomationFleet(input) {
|
|
|
168
170
|
|
|
169
171
|
for (const [index, expected] of expectedFleet.expected.entries()) {
|
|
170
172
|
const comparison = comparisons[index];
|
|
171
|
-
|
|
173
|
+
assignToAutomationGroup(
|
|
174
|
+
expectedGroups,
|
|
175
|
+
expected.group,
|
|
172
176
|
createObservedStatusItem({
|
|
173
177
|
expected,
|
|
174
178
|
comparison,
|
|
@@ -178,7 +182,7 @@ export async function inspectCodexAutomationFleet(input) {
|
|
|
178
182
|
}
|
|
179
183
|
|
|
180
184
|
for (const unsupported of expectedFleet.unsupported) {
|
|
181
|
-
expectedGroups
|
|
185
|
+
assignToAutomationGroup(expectedGroups, unsupported.group, {
|
|
182
186
|
id: unsupported.automationId,
|
|
183
187
|
status: "UNSUPPORTED",
|
|
184
188
|
summary: unsupported.reason,
|
|
@@ -190,18 +194,7 @@ export async function inspectCodexAutomationFleet(input) {
|
|
|
190
194
|
return {
|
|
191
195
|
runtime: `${CODEx_RUNTIME_LABEL} (backing-store metadata)`,
|
|
192
196
|
generatedAt: now.toISOString(),
|
|
193
|
-
groups:
|
|
194
|
-
{
|
|
195
|
-
id: "1",
|
|
196
|
-
title: "Core automations",
|
|
197
|
-
items: expectedGroups.get("core") ?? [],
|
|
198
|
-
},
|
|
199
|
-
{
|
|
200
|
-
id: "2",
|
|
201
|
-
title: "Exploratory automations",
|
|
202
|
-
items: expectedGroups.get("exploratory") ?? [],
|
|
203
|
-
},
|
|
204
|
-
],
|
|
197
|
+
groups: renderAutomationGroups(expectedGroups),
|
|
205
198
|
observedAutomations,
|
|
206
199
|
};
|
|
207
200
|
}
|
|
@@ -249,21 +242,35 @@ export function deriveCodexObservedCommand(prompt) {
|
|
|
249
242
|
return undefined;
|
|
250
243
|
}
|
|
251
244
|
|
|
245
|
+
// The "with arguments" clause is optional: no-argument loops (monitor, the
|
|
246
|
+
// gardener) register without one. Requiring it made every such loop derive an
|
|
247
|
+
// undefined command and report DRIFTED forever, un-fixable by re-running setup.
|
|
252
248
|
const lisaSkillMatch = prompt.match(
|
|
253
|
-
/Use the Lisa ([a-z0-9:-]+) skill with arguments `([^`]
|
|
249
|
+
/Use the Lisa ([a-z0-9:-]+) skill(?: with arguments `([^`]*)`)?/i
|
|
254
250
|
);
|
|
255
|
-
if (lisaSkillMatch?.[1]
|
|
256
|
-
return `/lisa:${lisaSkillMatch[1]} ${lisaSkillMatch[2]}`.trim();
|
|
251
|
+
if (lisaSkillMatch?.[1]) {
|
|
252
|
+
return `/lisa:${lisaSkillMatch[1]} ${lisaSkillMatch[2] ?? ""}`.trim();
|
|
257
253
|
}
|
|
258
254
|
|
|
259
255
|
const aliasSkillMatch = prompt.match(
|
|
260
|
-
/Use the `\$([a-z0-9:-]+)` skill with arguments `([^`]
|
|
256
|
+
/Use the `\$([a-z0-9:-]+)` skill(?: with arguments `([^`]*)`)?/i
|
|
261
257
|
);
|
|
262
|
-
if (aliasSkillMatch?.[1]
|
|
263
|
-
return `${canonicalizeCodexSkillAlias(aliasSkillMatch[1])} ${aliasSkillMatch[2]}`.trim();
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
|
|
258
|
+
if (aliasSkillMatch?.[1]) {
|
|
259
|
+
return `${canonicalizeCodexSkillAlias(aliasSkillMatch[1])} ${aliasSkillMatch[2] ?? ""}`.trim();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// A registration whose prompt carries the literal Lisa command on its own
|
|
263
|
+
// line — the shape `/lisa:setup-automations` bakes so a command that is not a
|
|
264
|
+
// plain skill name (`/lisa:learnings:audit`) round-trips exactly. Mirrors the
|
|
265
|
+
// Claude adapter, which has always accepted a bare `/lisa:` command. Codex
|
|
266
|
+
// stores prompts as single-line TOML, so a line break arrives either real or
|
|
267
|
+
// as the two-character escape `\n`; both delimit a line here.
|
|
268
|
+
const literalCommand = prompt
|
|
269
|
+
.split(/\r?\n|\\n/)
|
|
270
|
+
.map(segment => segment.trim())
|
|
271
|
+
.find(segment => /^\/lisa[:-]\S/.test(segment));
|
|
272
|
+
|
|
273
|
+
return literalCommand;
|
|
267
274
|
}
|
|
268
275
|
|
|
269
276
|
function canonicalizeCodexSkillAlias(alias) {
|
|
@@ -583,6 +590,13 @@ function humanizeAutomationCadence(rrule) {
|
|
|
583
590
|
: `every ${everyDays[1]} days`;
|
|
584
591
|
}
|
|
585
592
|
|
|
593
|
+
const everyWeeks = rrule.match(/^FREQ=WEEKLY;INTERVAL=(\d+)$/);
|
|
594
|
+
if (everyWeeks?.[1]) {
|
|
595
|
+
return Number(everyWeeks[1]) === 1
|
|
596
|
+
? "once a week"
|
|
597
|
+
: `every ${everyWeeks[1]} weeks`;
|
|
598
|
+
}
|
|
599
|
+
|
|
586
600
|
return rrule;
|
|
587
601
|
}
|
|
588
602
|
|
|
@@ -606,6 +620,11 @@ function rruleToIntervalMs(rrule) {
|
|
|
606
620
|
return Number(daily[1]) * 24 * 60 * 60_000;
|
|
607
621
|
}
|
|
608
622
|
|
|
623
|
+
const weekly = rrule.match(/^FREQ=WEEKLY;INTERVAL=(\d+)$/);
|
|
624
|
+
if (weekly?.[1]) {
|
|
625
|
+
return Number(weekly[1]) * 7 * 24 * 60 * 60_000;
|
|
626
|
+
}
|
|
627
|
+
|
|
609
628
|
return null;
|
|
610
629
|
}
|
|
611
630
|
|
|
@@ -623,6 +642,10 @@ function cadenceLabelToIntervalMs(label) {
|
|
|
623
642
|
return 24 * 60 * 60_000;
|
|
624
643
|
}
|
|
625
644
|
|
|
645
|
+
if (label === "once a week") {
|
|
646
|
+
return 7 * 24 * 60 * 60_000;
|
|
647
|
+
}
|
|
648
|
+
|
|
626
649
|
return null;
|
|
627
650
|
}
|
|
628
651
|
|
|
@@ -37,10 +37,86 @@ export const AUTOMATION_EXPECTED_CADENCES = {
|
|
|
37
37
|
human: "once a day",
|
|
38
38
|
rrule: "FREQ=DAILY;INTERVAL=1",
|
|
39
39
|
},
|
|
40
|
+
monitor: {
|
|
41
|
+
human: "once a day",
|
|
42
|
+
rrule: "FREQ=DAILY;INTERVAL=1",
|
|
43
|
+
},
|
|
44
|
+
"learnings-audit": {
|
|
45
|
+
human: "once a week",
|
|
46
|
+
rrule: "FREQ=WEEKLY;INTERVAL=1",
|
|
47
|
+
},
|
|
40
48
|
};
|
|
41
49
|
|
|
42
50
|
export const EXPLORATORY_QA_STACK_PRIORITY = ["expo", "rails", "harper-fabric"];
|
|
43
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Directory (repo-relative) holding the checked-in per-loop runbooks scaffolded
|
|
54
|
+
* by `/lisa:setup-automations` per the `automation-runbook-contract` rule.
|
|
55
|
+
*/
|
|
56
|
+
export const AUTOMATION_RUNBOOK_DIRECTORY = ".lisa/automations";
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Every fleet group, in render order, with the operator-facing title each one
|
|
60
|
+
* carries in the status report. This is the single source of truth for group
|
|
61
|
+
* membership: adapters bin entries with {@link createAutomationGroupBins} and
|
|
62
|
+
* render with {@link renderAutomationGroups}, so adding a group here surfaces it
|
|
63
|
+
* everywhere instead of silently dropping its entries.
|
|
64
|
+
*/
|
|
65
|
+
export const AUTOMATION_FLEET_GROUP_TITLES = {
|
|
66
|
+
core: "Core automations",
|
|
67
|
+
exploratory: "Exploratory automations",
|
|
68
|
+
"opt-in": "Opt-in automations",
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Create one empty bin per known fleet group.
|
|
73
|
+
*
|
|
74
|
+
* @returns {Map<string, object[]>}
|
|
75
|
+
*/
|
|
76
|
+
export function createAutomationGroupBins() {
|
|
77
|
+
return new Map(
|
|
78
|
+
Object.keys(AUTOMATION_FLEET_GROUP_TITLES).map(group => [group, []])
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Add a rendered status item to its group bin.
|
|
84
|
+
*
|
|
85
|
+
* Throws on an unknown group rather than dropping the item: a silently skipped
|
|
86
|
+
* entry is indistinguishable from a healthy fleet, which is exactly how the
|
|
87
|
+
* opt-in gardener went missing from both runtime adapters.
|
|
88
|
+
*
|
|
89
|
+
* @param {Map<string, object[]>} bins
|
|
90
|
+
* @param {string} group
|
|
91
|
+
* @param {object} item
|
|
92
|
+
* @returns {void}
|
|
93
|
+
*/
|
|
94
|
+
export function assignToAutomationGroup(bins, group, item) {
|
|
95
|
+
const bin = bins.get(group);
|
|
96
|
+
if (!bin) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`Unknown automation fleet group "${group}" for ${item?.id ?? "an automation"}. Add it to AUTOMATION_FLEET_GROUP_TITLES so the status report can render it.`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
bin.push(item);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Render the group bins as the numbered report groups, in declaration order.
|
|
106
|
+
*
|
|
107
|
+
* @param {Map<string, object[]>} bins
|
|
108
|
+
* @returns {readonly { readonly id: string, readonly title: string, readonly items: readonly object[] }[]}
|
|
109
|
+
*/
|
|
110
|
+
export function renderAutomationGroups(bins) {
|
|
111
|
+
return Object.entries(AUTOMATION_FLEET_GROUP_TITLES).map(
|
|
112
|
+
([group, title], index) => ({
|
|
113
|
+
id: String(index + 1),
|
|
114
|
+
title,
|
|
115
|
+
items: bins.get(group) ?? [],
|
|
116
|
+
})
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
44
120
|
/**
|
|
45
121
|
* @typedef {{
|
|
46
122
|
* readonly id: string
|
|
@@ -48,19 +124,52 @@ export const EXPLORATORY_QA_STACK_PRIORITY = ["expo", "rails", "harper-fabric"];
|
|
|
48
124
|
* readonly expectedCadence: string
|
|
49
125
|
* readonly expectedRRule: string
|
|
50
126
|
* readonly expectedCommand: string
|
|
51
|
-
* readonly group: "core" | "exploratory"
|
|
127
|
+
* readonly group: "core" | "exploratory" | "opt-in"
|
|
128
|
+
* readonly runbookPath: string
|
|
52
129
|
* }} ExpectedAutomationEntry
|
|
53
130
|
*
|
|
54
131
|
* @typedef {{
|
|
55
132
|
* readonly id: string
|
|
56
133
|
* readonly automationId: string
|
|
57
|
-
* readonly group: "core" | "exploratory"
|
|
134
|
+
* readonly group: "core" | "exploratory" | "opt-in"
|
|
58
135
|
* readonly reason: string
|
|
59
136
|
* readonly expectedCadence: string
|
|
60
137
|
* readonly expectedRRule: string
|
|
138
|
+
* readonly runbookPath: string
|
|
61
139
|
* }} UnsupportedAutomationEntry
|
|
62
140
|
*/
|
|
63
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Infer whether this project opted into the weekly gardener, from what is
|
|
144
|
+
* actually registered on the scheduler.
|
|
145
|
+
*
|
|
146
|
+
* Membership is registration, not configuration: there is no config key to
|
|
147
|
+
* read, so a read-only caller decides by looking for the registration itself.
|
|
148
|
+
* Pass the result as `learningsAudit` to {@link resolveExpectedAutomationFleet}
|
|
149
|
+
* so an opted-in project compares its gardener like any other loop instead of
|
|
150
|
+
* reporting it `UNSUPPORTED` forever.
|
|
151
|
+
*
|
|
152
|
+
* @param {{
|
|
153
|
+
* readonly automationPrefix: string
|
|
154
|
+
* readonly observedAutomationIds?: readonly string[]
|
|
155
|
+
* }} input
|
|
156
|
+
* @returns {boolean}
|
|
157
|
+
*/
|
|
158
|
+
export function inferLearningsAuditRegistration(input) {
|
|
159
|
+
const target = `${input.automationPrefix}learnings-audit`;
|
|
160
|
+
return (input.observedAutomationIds ?? []).includes(target);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Resolve the repo-relative runbook path for a loop id.
|
|
165
|
+
*
|
|
166
|
+
* @param {string} id
|
|
167
|
+
* @returns {string}
|
|
168
|
+
*/
|
|
169
|
+
export function resolveAutomationRunbookPath(id) {
|
|
170
|
+
return `${AUTOMATION_RUNBOOK_DIRECTORY}/${id}.runbook.md`;
|
|
171
|
+
}
|
|
172
|
+
|
|
64
173
|
/**
|
|
65
174
|
* Resolve the stable project identifier and automation prefix used by
|
|
66
175
|
* `/lisa:setup-automations`.
|
|
@@ -101,7 +210,15 @@ export function resolveAutomationProjectIdentity(input = {}) {
|
|
|
101
210
|
* readonly detectedTypes?: readonly string[]
|
|
102
211
|
* readonly autoStartPrds?: boolean | string
|
|
103
212
|
* readonly autoStartTickets?: boolean | string
|
|
213
|
+
* readonly learningsAudit?: boolean | string
|
|
104
214
|
* }} input
|
|
215
|
+
*
|
|
216
|
+
* `learningsAudit` has no config home by design — the gardener is opted into at
|
|
217
|
+
* registration time, and membership is registration, not configuration. A
|
|
218
|
+
* read-only caller (the status surface) therefore INFERS it: pass `true` when a
|
|
219
|
+
* `<automationPrefix>learnings-audit` entry is observed on the runtime
|
|
220
|
+
* scheduler. Without that inference an opted-in project would report the
|
|
221
|
+
* gardener `UNSUPPORTED` forever instead of comparing it like any other loop.
|
|
105
222
|
* @returns {{
|
|
106
223
|
* readonly owner: string
|
|
107
224
|
* readonly repo: string
|
|
@@ -116,6 +233,7 @@ export function resolveExpectedAutomationFleet(input = {}) {
|
|
|
116
233
|
const identity = resolveAutomationProjectIdentity(input);
|
|
117
234
|
const autoStartPrds = normalizeBooleanFlag(input.autoStartPrds);
|
|
118
235
|
const autoStartTickets = normalizeBooleanFlag(input.autoStartTickets);
|
|
236
|
+
const learningsAudit = normalizeBooleanFlag(input.learningsAudit);
|
|
119
237
|
const detectedTypes = input.detectedTypes ?? [];
|
|
120
238
|
|
|
121
239
|
const tracker = config.tracker;
|
|
@@ -166,6 +284,7 @@ export function resolveExpectedAutomationFleet(input = {}) {
|
|
|
166
284
|
`/lisa:intake ${buildQueue}`,
|
|
167
285
|
"core"
|
|
168
286
|
),
|
|
287
|
+
createExpectedEntry(identity, "monitor", "/lisa:monitor", "core"),
|
|
169
288
|
createExpectedEntry(
|
|
170
289
|
identity,
|
|
171
290
|
"exploratory-prds",
|
|
@@ -191,7 +310,28 @@ export function resolveExpectedAutomationFleet(input = {}) {
|
|
|
191
310
|
createUnsupportedEntry(
|
|
192
311
|
identity,
|
|
193
312
|
"exploratory-bugs",
|
|
194
|
-
"This
|
|
313
|
+
"This project ships no exploratory-qa command, so there is nothing for this loop to run. No action needed unless the project adds one.",
|
|
314
|
+
"exploratory"
|
|
315
|
+
)
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (learningsAudit) {
|
|
320
|
+
expected.push(
|
|
321
|
+
createExpectedEntry(
|
|
322
|
+
identity,
|
|
323
|
+
"learnings-audit",
|
|
324
|
+
"/lisa:learnings:audit",
|
|
325
|
+
"opt-in"
|
|
326
|
+
)
|
|
327
|
+
);
|
|
328
|
+
} else {
|
|
329
|
+
unsupported.push(
|
|
330
|
+
createUnsupportedEntry(
|
|
331
|
+
identity,
|
|
332
|
+
"learnings-audit",
|
|
333
|
+
"The weekly gardener loop is opt-in and this project has not opted in, so nobody is auditing its knowledge surfaces. Run /lisa:setup-automations learnings-audit=true to enable it.",
|
|
334
|
+
"opt-in"
|
|
195
335
|
)
|
|
196
336
|
);
|
|
197
337
|
}
|
|
@@ -222,7 +362,7 @@ export function resolveExploratoryQaStack(detectedTypes = []) {
|
|
|
222
362
|
* @param {{ readonly automationPrefix: string }} identity
|
|
223
363
|
* @param {string} id
|
|
224
364
|
* @param {string} expectedCommand
|
|
225
|
-
* @param {"core" | "exploratory"} group
|
|
365
|
+
* @param {"core" | "exploratory" | "opt-in"} group
|
|
226
366
|
* @returns {ExpectedAutomationEntry}
|
|
227
367
|
*/
|
|
228
368
|
function createExpectedEntry(identity, id, expectedCommand, group) {
|
|
@@ -234,6 +374,7 @@ function createExpectedEntry(identity, id, expectedCommand, group) {
|
|
|
234
374
|
expectedRRule: cadence.rrule,
|
|
235
375
|
expectedCommand,
|
|
236
376
|
group,
|
|
377
|
+
runbookPath: resolveAutomationRunbookPath(id),
|
|
237
378
|
};
|
|
238
379
|
}
|
|
239
380
|
|
|
@@ -241,17 +382,19 @@ function createExpectedEntry(identity, id, expectedCommand, group) {
|
|
|
241
382
|
* @param {{ readonly automationPrefix: string }} identity
|
|
242
383
|
* @param {string} id
|
|
243
384
|
* @param {string} reason
|
|
385
|
+
* @param {"core" | "exploratory" | "opt-in"} group
|
|
244
386
|
* @returns {UnsupportedAutomationEntry}
|
|
245
387
|
*/
|
|
246
|
-
function createUnsupportedEntry(identity, id, reason) {
|
|
388
|
+
function createUnsupportedEntry(identity, id, reason, group) {
|
|
247
389
|
const cadence = AUTOMATION_EXPECTED_CADENCES[id];
|
|
248
390
|
return {
|
|
249
391
|
id,
|
|
250
392
|
automationId: `${identity.automationPrefix}${id}`,
|
|
251
393
|
expectedCadence: cadence.human,
|
|
252
394
|
expectedRRule: cadence.rrule,
|
|
253
|
-
group
|
|
395
|
+
group,
|
|
254
396
|
reason,
|
|
397
|
+
runbookPath: resolveAutomationRunbookPath(id),
|
|
255
398
|
};
|
|
256
399
|
}
|
|
257
400
|
|
|
@@ -16,14 +16,20 @@ Do **not** ask for confirmation once invoked. This skill inspects scheduler stat
|
|
|
16
16
|
|
|
17
17
|
## Scope
|
|
18
18
|
|
|
19
|
-
Inspect only the Lisa automation fleet for the current project
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
- `
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
Inspect only the Lisa automation fleet for the current project. Derive that fleet from
|
|
20
|
+
`scripts/automation-status-expected-fleet.mjs` (`resolveExpectedAutomationFleet`), which resolves
|
|
21
|
+
exactly what `setup-automations` registers for this repo — including the stack-guarded
|
|
22
|
+
`exploratory-bugs` and the opt-in `learnings-audit` gardener. **Membership is registration, not a
|
|
23
|
+
roster** (`automation-runbook-contract`): carry no fixed list of loop names here, so a loop added to
|
|
24
|
+
or removed from the registration set flows through without editing this skill.
|
|
25
|
+
|
|
26
|
+
The gardener is opted into at registration time and has no config key, so **infer it from the
|
|
27
|
+
scheduler**: list the project's automations first, then call
|
|
28
|
+
`inferLearningsAuditRegistration({ automationPrefix, observedAutomationIds })` and pass the result
|
|
29
|
+
as `learningsAudit` when resolving the fleet. A `lisa-auto-<project>-learnings-audit` entry on the
|
|
30
|
+
scheduler means the project opted in, so the gardener is compared like any other loop; no such entry
|
|
31
|
+
means it is reported `UNSUPPORTED` (opted out), never `MISSING`. This skill stays read-only — it
|
|
32
|
+
infers, it never writes the flag anywhere.
|
|
27
33
|
|
|
28
34
|
Resolve the expected project identifier, fleet naming prefix, queue arguments, cadence, and stack-support rules from the same contract used by `setup-automations` and `tear-down-automations`. Do **not** invent a second source of truth for fleet naming or queue resolution.
|
|
29
35
|
|