@codyswann/lisa 2.104.3 → 2.104.5
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-claude-adapter.mjs +34 -7
- package/plugins/lisa/scripts/automation-status-codex-adapter.mjs +39 -14
- package/plugins/lisa/scripts/automation-status-contract-drift.mjs +65 -1
- package/plugins/lisa/scripts/queue-status-build-readers.mjs +2 -1
- package/plugins/lisa/scripts/queue-status-prd-readers.mjs +2 -1
- 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-claude-adapter.mjs +34 -7
- package/plugins/src/base/scripts/automation-status-codex-adapter.mjs +39 -14
- package/plugins/src/base/scripts/automation-status-contract-drift.mjs +65 -1
- package/plugins/src/base/scripts/queue-status-build-readers.mjs +2 -1
- package/plugins/src/base/scripts/queue-status-prd-readers.mjs +2 -1
package/package.json
CHANGED
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"lodash": ">=4.18.1"
|
|
83
83
|
},
|
|
84
84
|
"name": "@codyswann/lisa",
|
|
85
|
-
"version": "2.104.
|
|
85
|
+
"version": "2.104.5",
|
|
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": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Claude does not expose last-run or failure metadata.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { compareAutomationFleet } from "./automation-status-contract-drift.mjs";
|
|
13
13
|
|
|
14
14
|
const CLAUDE_RUNTIME_LABEL = "Claude /schedule";
|
|
15
15
|
const CLAUDE_ACTIVE_STATUSES = new Set([
|
|
@@ -80,11 +80,13 @@ export function inspectClaudeAutomationFleet(input) {
|
|
|
80
80
|
["exploratory", []],
|
|
81
81
|
]);
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
const comparisons = compareAutomationFleet({
|
|
84
|
+
expectedAutomations: expectedFleet.expected,
|
|
85
|
+
observedAutomations,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
for (const [index, expected] of expectedFleet.expected.entries()) {
|
|
89
|
+
const comparison = comparisons[index];
|
|
88
90
|
expectedGroups.get(expected.group)?.push(
|
|
89
91
|
createObservedStatusItem({
|
|
90
92
|
expected,
|
|
@@ -175,6 +177,31 @@ export function deriveClaudeObservedCommand(command) {
|
|
|
175
177
|
return undefined;
|
|
176
178
|
}
|
|
177
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Extract the cadence argument from a Claude `/schedule` command string.
|
|
182
|
+
* Supports quoted (double-quote, single-quote, backtick) and unquoted cadence
|
|
183
|
+
* values, returning the first matched capture group via {@link firstString}.
|
|
184
|
+
*
|
|
185
|
+
* @param {string | undefined} command - The command string to parse
|
|
186
|
+
* @returns {string | undefined} The extracted cadence, or undefined if not found
|
|
187
|
+
*/
|
|
188
|
+
function extractClaudeScheduleCadence(command) {
|
|
189
|
+
if (!command) {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const scheduleLine = command
|
|
194
|
+
.trim()
|
|
195
|
+
.match(/^\/schedule\s+(?:"([^"]+)"|'([^']+)'|`([^`]+)`|(\S+))/m);
|
|
196
|
+
|
|
197
|
+
return firstString(
|
|
198
|
+
scheduleLine?.[1],
|
|
199
|
+
scheduleLine?.[2],
|
|
200
|
+
scheduleLine?.[3],
|
|
201
|
+
scheduleLine?.[4]
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
178
205
|
function createObservedStatusItem(input) {
|
|
179
206
|
const expected = input.expected;
|
|
180
207
|
const comparison = input.comparison;
|
|
@@ -405,7 +432,7 @@ function normalizeClaudeScheduleTextEntry(block) {
|
|
|
405
432
|
|
|
406
433
|
const cadenceSource =
|
|
407
434
|
extractField(block, /^(?:Cadence|Schedule):\s*(.+)$/im) ??
|
|
408
|
-
block
|
|
435
|
+
extractClaudeScheduleCadence(block);
|
|
409
436
|
const commandSource =
|
|
410
437
|
extractField(block, /^(?:Command|Prompt):\s*(.+)$/im) ??
|
|
411
438
|
extractField(
|
|
@@ -14,9 +14,10 @@ import fs from "node:fs/promises";
|
|
|
14
14
|
import os from "node:os";
|
|
15
15
|
import path from "node:path";
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import { compareAutomationFleet } from "./automation-status-contract-drift.mjs";
|
|
18
18
|
|
|
19
19
|
const CODEx_RUNTIME_LABEL = "Codex automations";
|
|
20
|
+
const RUN_TIMESTAMP_PATTERN = /20\d{2}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?Z/;
|
|
20
21
|
const RUN_FAILURE_PATTERN =
|
|
21
22
|
/\b(failed|failure|errored|error|exception|crash(?:ed)?)\b/i;
|
|
22
23
|
const NEGATED_FAILURE_PATTERN =
|
|
@@ -82,11 +83,13 @@ export async function inspectCodexAutomationFleet(input) {
|
|
|
82
83
|
["exploratory", []],
|
|
83
84
|
]);
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
const comparisons = compareAutomationFleet({
|
|
87
|
+
expectedAutomations: expectedFleet.expected,
|
|
88
|
+
observedAutomations,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
for (const [index, expected] of expectedFleet.expected.entries()) {
|
|
92
|
+
const comparison = comparisons[index];
|
|
90
93
|
expectedGroups.get(expected.group)?.push(
|
|
91
94
|
createObservedStatusItem({
|
|
92
95
|
expected,
|
|
@@ -211,28 +214,50 @@ export function parseCodexAutomationMemory(memoryContent) {
|
|
|
211
214
|
};
|
|
212
215
|
}
|
|
213
216
|
|
|
214
|
-
const timestampMatch = memoryContent.match(
|
|
215
|
-
/20\d{2}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?Z/
|
|
216
|
-
);
|
|
217
217
|
const lines = memoryContent.split(/\r?\n/);
|
|
218
|
+
const latestBlock = findLatestAutomationMemoryBlock(lines);
|
|
218
219
|
const summaryLine =
|
|
219
|
-
lines
|
|
220
|
+
latestBlock.lines
|
|
220
221
|
.find(line => line.startsWith("- "))
|
|
221
222
|
?.replace(/^- /, "")
|
|
222
223
|
.trim() ?? null;
|
|
223
224
|
|
|
224
|
-
const
|
|
225
|
+
const latestBlockText = latestBlock.lines.join("\n");
|
|
225
226
|
const lastRunFailed =
|
|
226
|
-
RUN_FAILURE_PATTERN.test(
|
|
227
|
-
!NEGATED_FAILURE_PATTERN.test(
|
|
227
|
+
RUN_FAILURE_PATTERN.test(latestBlockText) &&
|
|
228
|
+
!NEGATED_FAILURE_PATTERN.test(latestBlockText);
|
|
228
229
|
|
|
229
230
|
return {
|
|
230
|
-
lastRunAt:
|
|
231
|
+
lastRunAt: latestBlock.timestamp,
|
|
231
232
|
lastRunSummary: summaryLine,
|
|
232
233
|
lastRunFailed,
|
|
233
234
|
};
|
|
234
235
|
}
|
|
235
236
|
|
|
237
|
+
function findLatestAutomationMemoryBlock(lines) {
|
|
238
|
+
const timestampLines = lines
|
|
239
|
+
.map((line, index) => ({
|
|
240
|
+
index,
|
|
241
|
+
timestamp: line.match(RUN_TIMESTAMP_PATTERN)?.[0] ?? null,
|
|
242
|
+
}))
|
|
243
|
+
.filter(entry => entry.timestamp);
|
|
244
|
+
|
|
245
|
+
if (timestampLines.length === 0) {
|
|
246
|
+
return {
|
|
247
|
+
timestamp: null,
|
|
248
|
+
lines,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const latest = timestampLines.at(-1);
|
|
253
|
+
const next = timestampLines.find(entry => entry.index > latest.index);
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
timestamp: latest.timestamp,
|
|
257
|
+
lines: lines.slice(latest.index, next?.index),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
236
261
|
async function readCodexAutomation(automationDir) {
|
|
237
262
|
const tomlPath = path.join(automationDir, "automation.toml");
|
|
238
263
|
const memoryPath = path.join(automationDir, "memory.md");
|
|
@@ -39,6 +39,41 @@ const DRIFT_LABELS = {
|
|
|
39
39
|
* }} AutomationContractComparison
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Compare the expected automation fleet against observed scheduler entries,
|
|
44
|
+
* consuming each observed automation at most once.
|
|
45
|
+
*
|
|
46
|
+
* @param {{
|
|
47
|
+
* readonly expectedAutomations: readonly ExpectedAutomationContract[]
|
|
48
|
+
* readonly observedAutomations?: readonly ObservedAutomationContract[]
|
|
49
|
+
* }} input
|
|
50
|
+
* @returns {readonly AutomationContractComparison[]}
|
|
51
|
+
*/
|
|
52
|
+
export function compareAutomationFleet(input) {
|
|
53
|
+
const remainingObserved = [...(input.observedAutomations ?? [])];
|
|
54
|
+
|
|
55
|
+
return input.expectedAutomations.map(expected => {
|
|
56
|
+
const observed = findObservedAutomationMatch(
|
|
57
|
+
expected,
|
|
58
|
+
remainingObserved,
|
|
59
|
+
input.expectedAutomations
|
|
60
|
+
);
|
|
61
|
+
const comparison = compareAutomationContract({
|
|
62
|
+
expected,
|
|
63
|
+
observedAutomation: observed,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (observed) {
|
|
67
|
+
const index = remainingObserved.indexOf(observed);
|
|
68
|
+
if (index >= 0) {
|
|
69
|
+
remainingObserved.splice(index, 1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return comparison;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
42
77
|
/**
|
|
43
78
|
* Find the best observed scheduler entry for an expected automation contract.
|
|
44
79
|
*
|
|
@@ -50,11 +85,13 @@ const DRIFT_LABELS = {
|
|
|
50
85
|
*
|
|
51
86
|
* @param {ExpectedAutomationContract} expected
|
|
52
87
|
* @param {readonly ObservedAutomationContract[]} observedAutomations
|
|
88
|
+
* @param {readonly ExpectedAutomationContract[]} expectedAutomations
|
|
53
89
|
* @returns {ObservedAutomationContract | null}
|
|
54
90
|
*/
|
|
55
91
|
export function findObservedAutomationMatch(
|
|
56
92
|
expected,
|
|
57
|
-
observedAutomations = []
|
|
93
|
+
observedAutomations = [],
|
|
94
|
+
expectedAutomations = [expected]
|
|
58
95
|
) {
|
|
59
96
|
const exactId = observedAutomations.find(
|
|
60
97
|
observed => observed.automationId === expected.automationId
|
|
@@ -99,6 +136,15 @@ export function findObservedAutomationMatch(
|
|
|
99
136
|
return exactCommand;
|
|
100
137
|
}
|
|
101
138
|
|
|
139
|
+
if (
|
|
140
|
+
isSharedExpectedCommandToken(
|
|
141
|
+
expectedCommand.commandToken,
|
|
142
|
+
expectedAutomations
|
|
143
|
+
)
|
|
144
|
+
) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
102
148
|
return (
|
|
103
149
|
observedAutomations.find(observed => {
|
|
104
150
|
const observedCommand = normalizeAutomationCommand(
|
|
@@ -161,6 +207,24 @@ export function compareAutomationContract(input) {
|
|
|
161
207
|
};
|
|
162
208
|
}
|
|
163
209
|
|
|
210
|
+
/**
|
|
211
|
+
* @param {string} commandToken
|
|
212
|
+
* @param {readonly ExpectedAutomationContract[]} expectedAutomations
|
|
213
|
+
* @returns {boolean}
|
|
214
|
+
*/
|
|
215
|
+
function isSharedExpectedCommandToken(commandToken, expectedAutomations) {
|
|
216
|
+
if (!commandToken) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
expectedAutomations.filter(expected => {
|
|
222
|
+
const normalized = normalizeAutomationCommand(expected.expectedCommand);
|
|
223
|
+
return normalized.commandToken === commandToken;
|
|
224
|
+
}).length > 1
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
164
228
|
/**
|
|
165
229
|
* @param {ExpectedAutomationContract} expected
|
|
166
230
|
* @param {ObservedAutomationContract} observed
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { classifyQueueHealth } from "./queue-health-classification.mjs";
|
|
12
|
+
import { resolveBuildLifecycleRoles } from "./queue-contract-resolution.mjs";
|
|
12
13
|
|
|
13
14
|
export const BUILD_LIFECYCLE_ORDER = [
|
|
14
15
|
"ready",
|
|
@@ -60,7 +61,7 @@ const HIGHLIGHT_COPY = {
|
|
|
60
61
|
* }} input
|
|
61
62
|
*/
|
|
62
63
|
export function readGithubBuildQueueSnapshot(input = {}) {
|
|
63
|
-
const roles = input.roles ?? {};
|
|
64
|
+
const roles = input.roles ?? resolveBuildLifecycleRoles({}, "github").roles;
|
|
64
65
|
const normalizedItems = (input.issues ?? [])
|
|
65
66
|
.map(issue => normalizeGithubBuildIssue(issue, roles))
|
|
66
67
|
.filter(Boolean);
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { classifyQueueHealth } from "./queue-health-classification.mjs";
|
|
12
|
+
import { resolvePrdLifecycleRoles } from "./queue-contract-resolution.mjs";
|
|
12
13
|
|
|
13
14
|
export const PRD_LIFECYCLE_ORDER = [
|
|
14
15
|
"draft",
|
|
@@ -67,7 +68,7 @@ const HIGHLIGHT_COPY = {
|
|
|
67
68
|
* }} input
|
|
68
69
|
*/
|
|
69
70
|
export function readGithubPrdQueueSnapshot(input = {}) {
|
|
70
|
-
const roles = input.roles ?? {};
|
|
71
|
+
const roles = input.roles ?? resolvePrdLifecycleRoles({}, "github").roles;
|
|
71
72
|
const normalizedItems = (input.issues ?? [])
|
|
72
73
|
.map(issue => normalizeGithubPrdIssue(issue, roles))
|
|
73
74
|
.filter(Boolean);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "2.104.
|
|
3
|
+
"version": "2.104.5",
|
|
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.104.
|
|
3
|
+
"version": "2.104.5",
|
|
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"
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Claude does not expose last-run or failure metadata.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { compareAutomationFleet } from "./automation-status-contract-drift.mjs";
|
|
13
13
|
|
|
14
14
|
const CLAUDE_RUNTIME_LABEL = "Claude /schedule";
|
|
15
15
|
const CLAUDE_ACTIVE_STATUSES = new Set([
|
|
@@ -80,11 +80,13 @@ export function inspectClaudeAutomationFleet(input) {
|
|
|
80
80
|
["exploratory", []],
|
|
81
81
|
]);
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
const comparisons = compareAutomationFleet({
|
|
84
|
+
expectedAutomations: expectedFleet.expected,
|
|
85
|
+
observedAutomations,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
for (const [index, expected] of expectedFleet.expected.entries()) {
|
|
89
|
+
const comparison = comparisons[index];
|
|
88
90
|
expectedGroups.get(expected.group)?.push(
|
|
89
91
|
createObservedStatusItem({
|
|
90
92
|
expected,
|
|
@@ -175,6 +177,31 @@ export function deriveClaudeObservedCommand(command) {
|
|
|
175
177
|
return undefined;
|
|
176
178
|
}
|
|
177
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Extract the cadence argument from a Claude `/schedule` command string.
|
|
182
|
+
* Supports quoted (double-quote, single-quote, backtick) and unquoted cadence
|
|
183
|
+
* values, returning the first matched capture group via {@link firstString}.
|
|
184
|
+
*
|
|
185
|
+
* @param {string | undefined} command - The command string to parse
|
|
186
|
+
* @returns {string | undefined} The extracted cadence, or undefined if not found
|
|
187
|
+
*/
|
|
188
|
+
function extractClaudeScheduleCadence(command) {
|
|
189
|
+
if (!command) {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const scheduleLine = command
|
|
194
|
+
.trim()
|
|
195
|
+
.match(/^\/schedule\s+(?:"([^"]+)"|'([^']+)'|`([^`]+)`|(\S+))/m);
|
|
196
|
+
|
|
197
|
+
return firstString(
|
|
198
|
+
scheduleLine?.[1],
|
|
199
|
+
scheduleLine?.[2],
|
|
200
|
+
scheduleLine?.[3],
|
|
201
|
+
scheduleLine?.[4]
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
178
205
|
function createObservedStatusItem(input) {
|
|
179
206
|
const expected = input.expected;
|
|
180
207
|
const comparison = input.comparison;
|
|
@@ -405,7 +432,7 @@ function normalizeClaudeScheduleTextEntry(block) {
|
|
|
405
432
|
|
|
406
433
|
const cadenceSource =
|
|
407
434
|
extractField(block, /^(?:Cadence|Schedule):\s*(.+)$/im) ??
|
|
408
|
-
block
|
|
435
|
+
extractClaudeScheduleCadence(block);
|
|
409
436
|
const commandSource =
|
|
410
437
|
extractField(block, /^(?:Command|Prompt):\s*(.+)$/im) ??
|
|
411
438
|
extractField(
|
|
@@ -14,9 +14,10 @@ import fs from "node:fs/promises";
|
|
|
14
14
|
import os from "node:os";
|
|
15
15
|
import path from "node:path";
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import { compareAutomationFleet } from "./automation-status-contract-drift.mjs";
|
|
18
18
|
|
|
19
19
|
const CODEx_RUNTIME_LABEL = "Codex automations";
|
|
20
|
+
const RUN_TIMESTAMP_PATTERN = /20\d{2}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?Z/;
|
|
20
21
|
const RUN_FAILURE_PATTERN =
|
|
21
22
|
/\b(failed|failure|errored|error|exception|crash(?:ed)?)\b/i;
|
|
22
23
|
const NEGATED_FAILURE_PATTERN =
|
|
@@ -82,11 +83,13 @@ export async function inspectCodexAutomationFleet(input) {
|
|
|
82
83
|
["exploratory", []],
|
|
83
84
|
]);
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
const comparisons = compareAutomationFleet({
|
|
87
|
+
expectedAutomations: expectedFleet.expected,
|
|
88
|
+
observedAutomations,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
for (const [index, expected] of expectedFleet.expected.entries()) {
|
|
92
|
+
const comparison = comparisons[index];
|
|
90
93
|
expectedGroups.get(expected.group)?.push(
|
|
91
94
|
createObservedStatusItem({
|
|
92
95
|
expected,
|
|
@@ -211,28 +214,50 @@ export function parseCodexAutomationMemory(memoryContent) {
|
|
|
211
214
|
};
|
|
212
215
|
}
|
|
213
216
|
|
|
214
|
-
const timestampMatch = memoryContent.match(
|
|
215
|
-
/20\d{2}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?Z/
|
|
216
|
-
);
|
|
217
217
|
const lines = memoryContent.split(/\r?\n/);
|
|
218
|
+
const latestBlock = findLatestAutomationMemoryBlock(lines);
|
|
218
219
|
const summaryLine =
|
|
219
|
-
lines
|
|
220
|
+
latestBlock.lines
|
|
220
221
|
.find(line => line.startsWith("- "))
|
|
221
222
|
?.replace(/^- /, "")
|
|
222
223
|
.trim() ?? null;
|
|
223
224
|
|
|
224
|
-
const
|
|
225
|
+
const latestBlockText = latestBlock.lines.join("\n");
|
|
225
226
|
const lastRunFailed =
|
|
226
|
-
RUN_FAILURE_PATTERN.test(
|
|
227
|
-
!NEGATED_FAILURE_PATTERN.test(
|
|
227
|
+
RUN_FAILURE_PATTERN.test(latestBlockText) &&
|
|
228
|
+
!NEGATED_FAILURE_PATTERN.test(latestBlockText);
|
|
228
229
|
|
|
229
230
|
return {
|
|
230
|
-
lastRunAt:
|
|
231
|
+
lastRunAt: latestBlock.timestamp,
|
|
231
232
|
lastRunSummary: summaryLine,
|
|
232
233
|
lastRunFailed,
|
|
233
234
|
};
|
|
234
235
|
}
|
|
235
236
|
|
|
237
|
+
function findLatestAutomationMemoryBlock(lines) {
|
|
238
|
+
const timestampLines = lines
|
|
239
|
+
.map((line, index) => ({
|
|
240
|
+
index,
|
|
241
|
+
timestamp: line.match(RUN_TIMESTAMP_PATTERN)?.[0] ?? null,
|
|
242
|
+
}))
|
|
243
|
+
.filter(entry => entry.timestamp);
|
|
244
|
+
|
|
245
|
+
if (timestampLines.length === 0) {
|
|
246
|
+
return {
|
|
247
|
+
timestamp: null,
|
|
248
|
+
lines,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const latest = timestampLines.at(-1);
|
|
253
|
+
const next = timestampLines.find(entry => entry.index > latest.index);
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
timestamp: latest.timestamp,
|
|
257
|
+
lines: lines.slice(latest.index, next?.index),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
236
261
|
async function readCodexAutomation(automationDir) {
|
|
237
262
|
const tomlPath = path.join(automationDir, "automation.toml");
|
|
238
263
|
const memoryPath = path.join(automationDir, "memory.md");
|
|
@@ -39,6 +39,41 @@ const DRIFT_LABELS = {
|
|
|
39
39
|
* }} AutomationContractComparison
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Compare the expected automation fleet against observed scheduler entries,
|
|
44
|
+
* consuming each observed automation at most once.
|
|
45
|
+
*
|
|
46
|
+
* @param {{
|
|
47
|
+
* readonly expectedAutomations: readonly ExpectedAutomationContract[]
|
|
48
|
+
* readonly observedAutomations?: readonly ObservedAutomationContract[]
|
|
49
|
+
* }} input
|
|
50
|
+
* @returns {readonly AutomationContractComparison[]}
|
|
51
|
+
*/
|
|
52
|
+
export function compareAutomationFleet(input) {
|
|
53
|
+
const remainingObserved = [...(input.observedAutomations ?? [])];
|
|
54
|
+
|
|
55
|
+
return input.expectedAutomations.map(expected => {
|
|
56
|
+
const observed = findObservedAutomationMatch(
|
|
57
|
+
expected,
|
|
58
|
+
remainingObserved,
|
|
59
|
+
input.expectedAutomations
|
|
60
|
+
);
|
|
61
|
+
const comparison = compareAutomationContract({
|
|
62
|
+
expected,
|
|
63
|
+
observedAutomation: observed,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (observed) {
|
|
67
|
+
const index = remainingObserved.indexOf(observed);
|
|
68
|
+
if (index >= 0) {
|
|
69
|
+
remainingObserved.splice(index, 1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return comparison;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
42
77
|
/**
|
|
43
78
|
* Find the best observed scheduler entry for an expected automation contract.
|
|
44
79
|
*
|
|
@@ -50,11 +85,13 @@ const DRIFT_LABELS = {
|
|
|
50
85
|
*
|
|
51
86
|
* @param {ExpectedAutomationContract} expected
|
|
52
87
|
* @param {readonly ObservedAutomationContract[]} observedAutomations
|
|
88
|
+
* @param {readonly ExpectedAutomationContract[]} expectedAutomations
|
|
53
89
|
* @returns {ObservedAutomationContract | null}
|
|
54
90
|
*/
|
|
55
91
|
export function findObservedAutomationMatch(
|
|
56
92
|
expected,
|
|
57
|
-
observedAutomations = []
|
|
93
|
+
observedAutomations = [],
|
|
94
|
+
expectedAutomations = [expected]
|
|
58
95
|
) {
|
|
59
96
|
const exactId = observedAutomations.find(
|
|
60
97
|
observed => observed.automationId === expected.automationId
|
|
@@ -99,6 +136,15 @@ export function findObservedAutomationMatch(
|
|
|
99
136
|
return exactCommand;
|
|
100
137
|
}
|
|
101
138
|
|
|
139
|
+
if (
|
|
140
|
+
isSharedExpectedCommandToken(
|
|
141
|
+
expectedCommand.commandToken,
|
|
142
|
+
expectedAutomations
|
|
143
|
+
)
|
|
144
|
+
) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
102
148
|
return (
|
|
103
149
|
observedAutomations.find(observed => {
|
|
104
150
|
const observedCommand = normalizeAutomationCommand(
|
|
@@ -161,6 +207,24 @@ export function compareAutomationContract(input) {
|
|
|
161
207
|
};
|
|
162
208
|
}
|
|
163
209
|
|
|
210
|
+
/**
|
|
211
|
+
* @param {string} commandToken
|
|
212
|
+
* @param {readonly ExpectedAutomationContract[]} expectedAutomations
|
|
213
|
+
* @returns {boolean}
|
|
214
|
+
*/
|
|
215
|
+
function isSharedExpectedCommandToken(commandToken, expectedAutomations) {
|
|
216
|
+
if (!commandToken) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
expectedAutomations.filter(expected => {
|
|
222
|
+
const normalized = normalizeAutomationCommand(expected.expectedCommand);
|
|
223
|
+
return normalized.commandToken === commandToken;
|
|
224
|
+
}).length > 1
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
164
228
|
/**
|
|
165
229
|
* @param {ExpectedAutomationContract} expected
|
|
166
230
|
* @param {ObservedAutomationContract} observed
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { classifyQueueHealth } from "./queue-health-classification.mjs";
|
|
12
|
+
import { resolveBuildLifecycleRoles } from "./queue-contract-resolution.mjs";
|
|
12
13
|
|
|
13
14
|
export const BUILD_LIFECYCLE_ORDER = [
|
|
14
15
|
"ready",
|
|
@@ -60,7 +61,7 @@ const HIGHLIGHT_COPY = {
|
|
|
60
61
|
* }} input
|
|
61
62
|
*/
|
|
62
63
|
export function readGithubBuildQueueSnapshot(input = {}) {
|
|
63
|
-
const roles = input.roles ?? {};
|
|
64
|
+
const roles = input.roles ?? resolveBuildLifecycleRoles({}, "github").roles;
|
|
64
65
|
const normalizedItems = (input.issues ?? [])
|
|
65
66
|
.map(issue => normalizeGithubBuildIssue(issue, roles))
|
|
66
67
|
.filter(Boolean);
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { classifyQueueHealth } from "./queue-health-classification.mjs";
|
|
12
|
+
import { resolvePrdLifecycleRoles } from "./queue-contract-resolution.mjs";
|
|
12
13
|
|
|
13
14
|
export const PRD_LIFECYCLE_ORDER = [
|
|
14
15
|
"draft",
|
|
@@ -67,7 +68,7 @@ const HIGHLIGHT_COPY = {
|
|
|
67
68
|
* }} input
|
|
68
69
|
*/
|
|
69
70
|
export function readGithubPrdQueueSnapshot(input = {}) {
|
|
70
|
-
const roles = input.roles ?? {};
|
|
71
|
+
const roles = input.roles ?? resolvePrdLifecycleRoles({}, "github").roles;
|
|
71
72
|
const normalizedItems = (input.issues ?? [])
|
|
72
73
|
.map(issue => normalizeGithubPrdIssue(issue, roles))
|
|
73
74
|
.filter(Boolean);
|