@codyswann/lisa 4.50.8 → 4.51.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.
Files changed (68) hide show
  1. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  2. package/dist/core/reusable-workflow-load-adapter.d.ts +78 -0
  3. package/dist/core/reusable-workflow-load-adapter.d.ts.map +1 -0
  4. package/dist/core/reusable-workflow-load-adapter.js +125 -0
  5. package/dist/core/reusable-workflow-load-adapter.js.map +1 -0
  6. package/dist/core/reusable-workflow-load-failure.d.ts +144 -0
  7. package/dist/core/reusable-workflow-load-failure.d.ts.map +1 -0
  8. package/dist/core/reusable-workflow-load-failure.js +184 -0
  9. package/dist/core/reusable-workflow-load-failure.js.map +1 -0
  10. package/dist/core/reusable-workflow-load-scan.d.ts +92 -0
  11. package/dist/core/reusable-workflow-load-scan.d.ts.map +1 -0
  12. package/dist/core/reusable-workflow-load-scan.js +108 -0
  13. package/dist/core/reusable-workflow-load-scan.js.map +1 -0
  14. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  15. package/dist/core/upstream-evidence-manifest.js +10 -0
  16. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  17. package/package.json +5 -4
  18. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  19. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  20. package/plugins/lisa-agy/plugin.json +1 -1
  21. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  23. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  24. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  30. package/plugins/lisa-expo-agy/plugin.json +1 -1
  31. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  35. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  36. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  40. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  41. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  45. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  46. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  50. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  51. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  55. package/plugins/lisa-rails-agy/plugin.json +1 -1
  56. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  60. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  61. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  65. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  66. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  68. package/scripts/check-workflow-load-failures.ts +171 -0
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Scan this repository's recent run history for reusable workflows that failed
3
+ * to LOAD (CodySwannGT/lisa#3581).
4
+ *
5
+ * ## Why this runs on a schedule and not at a gate
6
+ *
7
+ * The failure is invisible at the moments a gate covers. On a pull request it
8
+ * is already caught — `check-skipped-required-checks --pr=<n>` raises
9
+ * `absent_required_check`, and a ruleset-required context that never reports
10
+ * blocks the merge anyway. The incident this closes happened somewhere no gate
11
+ * looks: a `push` handler failed five times in a row, ~40 minutes after an
12
+ * upstream commit landed, with nothing changed on the consumer side and no red
13
+ * job to open. A control that only runs where the failure cannot happen is not
14
+ * a control, so the moment is the design decision here, not the mechanism.
15
+ *
16
+ * The sibling precedent is `lifecycle-drift-sweep.yml`, which exists because a
17
+ * report nobody scheduled and nobody read is indistinguishable from no report.
18
+ * This follows it: scheduled, and failing the job on drift so the finding lands
19
+ * in the existing failure-to-issue path rather than a job summary nobody opens.
20
+ *
21
+ * ## Two ways to exit non-zero, and they mean different things
22
+ *
23
+ * Findings are one. **A window this scan could not cover is the other**, and it
24
+ * is the more important of the two: the implementation this replaces read one
25
+ * page, filtered by timestamp, and reported that a hundred runs were inspected
26
+ * across a window that contained four known load failures. "Found nothing" and
27
+ * "could not look" must never render as the same green.
28
+ * @module scripts/check-workflow-load-failures
29
+ */
30
+ import { execFile } from "node:child_process";
31
+ import { readdir, readFile } from "node:fs/promises";
32
+ import path from "node:path";
33
+ import { promisify } from "node:util";
34
+
35
+ import { callerDeclaresReusableWorkflow } from "../src/core/reusable-workflow-load-failure.js";
36
+ import {
37
+ createRunPageFetcher,
38
+ type GithubRequest,
39
+ } from "../src/core/reusable-workflow-load-adapter.js";
40
+ import {
41
+ scanForLoadFailures,
42
+ type ScanResult,
43
+ } from "../src/core/reusable-workflow-load-scan.js";
44
+
45
+ const run = promisify(execFile);
46
+
47
+ /** Where caller workflows live, relative to the repository root. */
48
+ const WORKFLOW_DIR = ".github/workflows";
49
+
50
+ /** How far back to scan, in hours. */
51
+ const WINDOW_HOURS = 26;
52
+
53
+ /**
54
+ * Page cap.
55
+ *
56
+ * Hitting it is NOT coverage — the scan reports the window as uncovered and
57
+ * this script exits non-zero, because a cap is a budget somebody set and not
58
+ * evidence about the runs beyond it.
59
+ */
60
+ const MAX_PAGES = 20;
61
+
62
+ /**
63
+ * Build the population test by reading the callers' own source.
64
+ *
65
+ * Derived, never enumerated: there is no list of known callers here, because a
66
+ * roster silently excludes every workflow nobody remembered to add and the
67
+ * exclusion looks exactly like a pass.
68
+ * @param root - Repository root
69
+ * @returns A predicate over a run's caller `path`
70
+ */
71
+ export async function buildPopulationTest(
72
+ root: string
73
+ ): Promise<(runPath: string) => boolean> {
74
+ const dir = path.join(root, WORKFLOW_DIR);
75
+ const names = await readdir(dir).catch(() => [] as string[]);
76
+ const declaring = await Promise.all(
77
+ names
78
+ .filter(name => name.endsWith(".yml") || name.endsWith(".yaml"))
79
+ .map(async name => {
80
+ const source = await readFile(path.join(dir, name), "utf8").catch(
81
+ () => ""
82
+ );
83
+ return callerDeclaresReusableWorkflow(source) ? name : "";
84
+ })
85
+ );
86
+ const declared = new Set(declaring.filter(name => name !== ""));
87
+ return (runPath: string): boolean => declared.has(path.basename(runPath));
88
+ }
89
+
90
+ /**
91
+ * A request function backed by the `gh` CLI.
92
+ * @param repo - Repository in `owner/name` form, used only for error text
93
+ * @returns A request function returning decoded JSON
94
+ */
95
+ export function ghRequest(repo: string): GithubRequest {
96
+ return async (apiPath: string): Promise<unknown> => {
97
+ const { stdout } = await run("gh", ["api", apiPath], {
98
+ maxBuffer: 32 * 1024 * 1024,
99
+ });
100
+ try {
101
+ return JSON.parse(stdout) as unknown;
102
+ } catch {
103
+ throw new Error(
104
+ `check-workflow-load-failures: \`gh api ${apiPath}\` on ${repo} returned a body that is not JSON. Treating that as "nothing found" would be a pass produced by a broken read, so this is an error.`
105
+ );
106
+ }
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Render the scan's answer for an operator.
112
+ * @param result - What the scan found, and whether it could see
113
+ * @returns Operator-readable lines
114
+ */
115
+ export function describe(result: ScanResult): string {
116
+ if (!result.covered) {
117
+ return `check-workflow-load-failures: INCOMPLETE. The scan ${result.reason} It inspected ${result.inspected} run(s), but that is not the same as having covered the window, so this is an error rather than a pass.`;
118
+ }
119
+ if (result.loadFailures.length === 0) {
120
+ return `check-workflow-load-failures: OK. ${result.inspected} run(s) inspected across the last ${WINDOW_HOURS}h; every caller that declared a reusable workflow resolved one.`;
121
+ }
122
+ const lines = result.loadFailures.map(
123
+ finding =>
124
+ ` - run ${finding.id} (${finding.path}) declared a reusable workflow and resolved none`
125
+ );
126
+ return [
127
+ `check-workflow-load-failures: ${result.loadFailures.length} run(s) failed to LOAD a reusable workflow.`,
128
+ ...lines,
129
+ "",
130
+ "A load failure creates NO jobs, so there is no red job to open and no annotation naming the line. Check the upstream workflow's most recent commit for a syntax or schema error.",
131
+ ].join("\n");
132
+ }
133
+
134
+ /**
135
+ * Scan, report, and choose an exit code.
136
+ * @param repo - Repository in `owner/name` form
137
+ * @param root - Repository root
138
+ * @returns 0 when the window was covered and clean, 1 otherwise
139
+ */
140
+ export async function main(repo: string, root: string): Promise<number> {
141
+ const windowStart = new Date(
142
+ Date.now() - WINDOW_HOURS * 60 * 60 * 1000
143
+ ).toISOString();
144
+
145
+ const result = await scanForLoadFailures({
146
+ fetchPage: createRunPageFetcher({ repo, request: ghRequest(repo) }),
147
+ windowStart,
148
+ inPopulation: await buildPopulationTest(root),
149
+ maxPages: MAX_PAGES,
150
+ });
151
+
152
+ console.log(describe(result));
153
+ return result.covered && result.loadFailures.length === 0 ? 0 : 1;
154
+ }
155
+
156
+ /* c8 ignore start -- entry point, exercised by the workflow rather than a test */
157
+ if (
158
+ process.argv[1] !== undefined &&
159
+ import.meta.url.endsWith(path.basename(process.argv[1]))
160
+ ) {
161
+ const repo = process.env["GITHUB_REPOSITORY"] ?? "CodySwannGT/lisa";
162
+ main(repo, process.cwd())
163
+ .then(code => {
164
+ process.exitCode = code;
165
+ })
166
+ .catch((error: unknown) => {
167
+ console.error(String(error));
168
+ process.exitCode = 1;
169
+ });
170
+ }
171
+ /* c8 ignore stop */