@codyswann/lisa 2.252.0 → 2.254.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/rules/eager/automation-runbook-contract.md +4 -1
- package/plugins/lisa/rules/reference/automation-runbook-contract.md +16 -7
- package/plugins/lisa/scripts/automation-run-record.mjs +262 -0
- 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-run-record.mjs +262 -0
- 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/rules/eager/automation-runbook-contract.md +4 -1
- package/plugins/lisa-copilot/rules/reference/automation-runbook-contract.md +16 -7
- package/plugins/lisa-copilot/scripts/automation-run-record.mjs +262 -0
- 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/rules/automation-runbook-contract-reference.mdc +16 -7
- package/plugins/lisa-cursor/rules/automation-runbook-contract.mdc +4 -1
- package/plugins/lisa-cursor/scripts/automation-run-record.mjs +262 -0
- 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/rules/eager/automation-runbook-contract.md +4 -1
- package/plugins/src/base/rules/reference/automation-runbook-contract.md +16 -7
- package/plugins/src/base/scripts/automation-run-record.mjs +262 -0
- 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
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Dependency-free run outcome recorder for registered Lisa automation loops.
|
|
4
|
+
*
|
|
5
|
+
* The file is local scheduler state, not project knowledge: one bounded JSONL
|
|
6
|
+
* file per loop under `.lisa/automations/runs/`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
|
|
12
|
+
export const AUTOMATION_RUN_OUTCOMES = [
|
|
13
|
+
"nothing-needed",
|
|
14
|
+
"candidate-proposed",
|
|
15
|
+
"change-proved",
|
|
16
|
+
"approval-requested",
|
|
17
|
+
"recovery-required",
|
|
18
|
+
"policy-obsolete",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_AUTOMATION_RUN_HISTORY_MAX_ENTRIES = 50;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {{
|
|
25
|
+
* readonly ts: string
|
|
26
|
+
* readonly loop_id: string
|
|
27
|
+
* readonly outcome: string
|
|
28
|
+
* readonly summary: string
|
|
29
|
+
* readonly runbook: string
|
|
30
|
+
* readonly refs: readonly string[]
|
|
31
|
+
* readonly run_id: string
|
|
32
|
+
* }} AutomationRunRecord
|
|
33
|
+
*
|
|
34
|
+
* @typedef {{
|
|
35
|
+
* readonly projectRoot?: string
|
|
36
|
+
* readonly loopId: string
|
|
37
|
+
* readonly outcome: string
|
|
38
|
+
* readonly summary: string
|
|
39
|
+
* readonly runbook: string
|
|
40
|
+
* readonly refs?: readonly string[]
|
|
41
|
+
* readonly runId?: string
|
|
42
|
+
* readonly ts?: string | Date
|
|
43
|
+
* readonly maxEntries?: number
|
|
44
|
+
* }} RecordAutomationRunInput
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Record one automation-loop outcome, suppressing duplicate re-appends for the
|
|
49
|
+
* same `run_id` and trimming the file to the configured history bound.
|
|
50
|
+
*
|
|
51
|
+
* @param {RecordAutomationRunInput} input
|
|
52
|
+
* @returns {Promise<{ readonly path: string, readonly record: AutomationRunRecord, readonly records: readonly AutomationRunRecord[], readonly appended: boolean, readonly skippedCorruptLines: number, readonly maxEntries: number }>}
|
|
53
|
+
*/
|
|
54
|
+
export async function recordAutomationRun(input) {
|
|
55
|
+
const projectRoot = path.resolve(input.projectRoot ?? process.cwd());
|
|
56
|
+
const maxEntries =
|
|
57
|
+
input.maxEntries ??
|
|
58
|
+
(await resolveAutomationRunHistoryMaxEntries(projectRoot));
|
|
59
|
+
const record = buildAutomationRunRecord(input);
|
|
60
|
+
const filePath = automationRunRecordPath(projectRoot, record.loop_id);
|
|
61
|
+
const readResult = await readAutomationRunRecords(filePath);
|
|
62
|
+
|
|
63
|
+
if (readResult.records.some(existing => existing.run_id === record.run_id)) {
|
|
64
|
+
return {
|
|
65
|
+
path: filePath,
|
|
66
|
+
record,
|
|
67
|
+
records: readResult.records,
|
|
68
|
+
appended: false,
|
|
69
|
+
skippedCorruptLines: readResult.skippedCorruptLines,
|
|
70
|
+
maxEntries,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const nextRecords = [...readResult.records, record].slice(-maxEntries);
|
|
75
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
76
|
+
await writeJsonlAtomically(filePath, nextRecords);
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
path: filePath,
|
|
80
|
+
record,
|
|
81
|
+
records: nextRecords,
|
|
82
|
+
appended: true,
|
|
83
|
+
skippedCorruptLines: readResult.skippedCorruptLines,
|
|
84
|
+
maxEntries,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @param {string} projectRoot
|
|
90
|
+
* @returns {Promise<number>}
|
|
91
|
+
*/
|
|
92
|
+
export async function resolveAutomationRunHistoryMaxEntries(projectRoot) {
|
|
93
|
+
const globalConfig = await readJsonIfPresent(
|
|
94
|
+
path.join(projectRoot, ".lisa.config.json")
|
|
95
|
+
);
|
|
96
|
+
const localConfig = await readJsonIfPresent(
|
|
97
|
+
path.join(projectRoot, ".lisa.config.local.json")
|
|
98
|
+
);
|
|
99
|
+
const configured =
|
|
100
|
+
localConfig?.automations?.runHistory?.maxEntries ??
|
|
101
|
+
globalConfig?.automations?.runHistory?.maxEntries;
|
|
102
|
+
|
|
103
|
+
if (Number.isInteger(configured) && configured > 0) {
|
|
104
|
+
return configured;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return DEFAULT_AUTOMATION_RUN_HISTORY_MAX_ENTRIES;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {string} projectRoot
|
|
112
|
+
* @param {string} loopId
|
|
113
|
+
* @returns {string}
|
|
114
|
+
*/
|
|
115
|
+
export function automationRunRecordPath(projectRoot, loopId) {
|
|
116
|
+
return path.join(
|
|
117
|
+
path.resolve(projectRoot),
|
|
118
|
+
".lisa",
|
|
119
|
+
"automations",
|
|
120
|
+
"runs",
|
|
121
|
+
`${normalizeLoopId(loopId)}.jsonl`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @param {string} filePath
|
|
127
|
+
* @returns {Promise<{ readonly records: readonly AutomationRunRecord[], readonly skippedCorruptLines: number }>}
|
|
128
|
+
*/
|
|
129
|
+
export async function readAutomationRunRecords(filePath) {
|
|
130
|
+
let content = "";
|
|
131
|
+
try {
|
|
132
|
+
content = await readFile(filePath, "utf8");
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (error?.code === "ENOENT") {
|
|
135
|
+
return { records: [], skippedCorruptLines: 0 };
|
|
136
|
+
}
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const records = [];
|
|
141
|
+
let skippedCorruptLines = 0;
|
|
142
|
+
for (const line of content.split(/\n/)) {
|
|
143
|
+
if (!line.trim()) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
const parsed = JSON.parse(line);
|
|
148
|
+
records.push(validateStoredRecord(parsed));
|
|
149
|
+
} catch {
|
|
150
|
+
skippedCorruptLines += 1;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { records, skippedCorruptLines };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @param {RecordAutomationRunInput} input
|
|
159
|
+
* @returns {AutomationRunRecord}
|
|
160
|
+
*/
|
|
161
|
+
function buildAutomationRunRecord(input) {
|
|
162
|
+
const loopId = normalizeLoopId(input.loopId);
|
|
163
|
+
const summary = String(input.summary ?? "").trim();
|
|
164
|
+
const runbook = String(input.runbook ?? "").trim();
|
|
165
|
+
const refs = Array.isArray(input.refs)
|
|
166
|
+
? input.refs.map(ref => String(ref))
|
|
167
|
+
: [];
|
|
168
|
+
const ts =
|
|
169
|
+
input.ts instanceof Date
|
|
170
|
+
? input.ts.toISOString()
|
|
171
|
+
: input.ts
|
|
172
|
+
? new Date(input.ts).toISOString()
|
|
173
|
+
: new Date().toISOString();
|
|
174
|
+
const runId = String(input.runId ?? `${loopId}:${ts}`).trim();
|
|
175
|
+
|
|
176
|
+
if (!AUTOMATION_RUN_OUTCOMES.includes(input.outcome)) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`Invalid automation run outcome "${input.outcome}". Valid outcomes: ${AUTOMATION_RUN_OUTCOMES.join(", ")}.`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
if (!summary) {
|
|
182
|
+
throw new Error("Automation run summary is required.");
|
|
183
|
+
}
|
|
184
|
+
if (!runbook) {
|
|
185
|
+
throw new Error("Automation runbook path is required.");
|
|
186
|
+
}
|
|
187
|
+
if (!runId) {
|
|
188
|
+
throw new Error("Automation run_id is required.");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
ts,
|
|
193
|
+
loop_id: loopId,
|
|
194
|
+
outcome: input.outcome,
|
|
195
|
+
summary,
|
|
196
|
+
runbook,
|
|
197
|
+
refs,
|
|
198
|
+
run_id: runId,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* @param {unknown} value
|
|
204
|
+
* @returns {AutomationRunRecord}
|
|
205
|
+
*/
|
|
206
|
+
function validateStoredRecord(value) {
|
|
207
|
+
if (!value || typeof value !== "object") {
|
|
208
|
+
throw new Error("Automation run record must be an object.");
|
|
209
|
+
}
|
|
210
|
+
return buildAutomationRunRecord({
|
|
211
|
+
ts: String(value.ts ?? ""),
|
|
212
|
+
loopId: String(value.loop_id ?? ""),
|
|
213
|
+
outcome: String(value.outcome ?? ""),
|
|
214
|
+
summary: String(value.summary ?? ""),
|
|
215
|
+
runbook: String(value.runbook ?? ""),
|
|
216
|
+
refs: Array.isArray(value.refs) ? value.refs.map(ref => String(ref)) : [],
|
|
217
|
+
runId: String(value.run_id ?? ""),
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* @param {string} loopId
|
|
223
|
+
* @returns {string}
|
|
224
|
+
*/
|
|
225
|
+
function normalizeLoopId(loopId) {
|
|
226
|
+
const normalized = String(loopId ?? "").trim();
|
|
227
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/i.test(normalized)) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
"Automation loop_id must be a non-empty slug containing only letters, numbers, dots, underscores, and hyphens."
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
return normalized;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* @param {string} filePath
|
|
237
|
+
* @returns {Promise<unknown | undefined>}
|
|
238
|
+
*/
|
|
239
|
+
async function readJsonIfPresent(filePath) {
|
|
240
|
+
try {
|
|
241
|
+
return JSON.parse(await readFile(filePath, "utf8"));
|
|
242
|
+
} catch (error) {
|
|
243
|
+
if (error?.code === "ENOENT") {
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
if (error instanceof SyntaxError) {
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* @param {string} filePath
|
|
255
|
+
* @param {readonly AutomationRunRecord[]} records
|
|
256
|
+
*/
|
|
257
|
+
async function writeJsonlAtomically(filePath, records) {
|
|
258
|
+
const content = `${records.map(record => JSON.stringify(record)).join("\n")}\n`;
|
|
259
|
+
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
260
|
+
await writeFile(tempPath, content, "utf8");
|
|
261
|
+
await rename(tempPath, filePath);
|
|
262
|
+
}
|
|
@@ -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
|
|