@codyswann/lisa 4.22.6 → 4.22.7

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 (62) hide show
  1. package/all/copy-overwrite/scripts/check-release-package-identity.mjs +319 -0
  2. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  3. package/dist/core/lisa-owned-hash-ledger.js +6 -0
  4. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  5. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  6. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  7. package/dist/core/upstream-evidence-manifest.js +8 -2
  8. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  9. package/package.json +3 -2
  10. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  11. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  12. package/plugins/lisa-agy/plugin.json +1 -1
  13. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  14. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  15. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  16. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  17. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  18. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  19. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  20. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  22. package/plugins/lisa-expo-agy/plugin.json +1 -1
  23. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  27. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  28. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  32. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  33. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  37. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  38. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  42. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  43. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  47. package/plugins/lisa-rails-agy/plugin.json +1 -1
  48. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  52. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  53. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  57. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  58. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  60. package/scripts/check-release-package-identity.mjs +319 -0
  61. package/scripts/two-channel-couplings.json +17 -4
  62. package/typescript/copy-overwrite/.prettierignore +1 -0
@@ -0,0 +1,319 @@
1
+ #!/usr/bin/env node
2
+ // This file is managed by Lisa and IS replaced on each `lisa` run.
3
+ // Do not edit directly — durable changes belong upstream in Lisa.
4
+
5
+ /**
6
+ * Fail closed when a release checkout or packed npm candidate does not bind to
7
+ * one immutable version and commit.
8
+ *
9
+ * Release workflows have two separate state transitions: standard-version
10
+ * writes and pushes the release commit, then npm builds and publishes a tagged
11
+ * checkout. A tag aimed at the pre-release merge let the second transition
12
+ * compile stale generated source and only afterwards rewrite package.json. This
13
+ * checker makes both boundaries executable: `checkout` proves tag/HEAD/commit
14
+ * identity before build, and `pack` proves the exact tarball passed to
15
+ * `npm publish` contains matching package metadata and generated certificate.
16
+ *
17
+ * Projects without Lisa's nightly certificate still receive the generic
18
+ * version/commit/tarball checks. When the source certificate exists, its built
19
+ * and packed counterpart becomes mandatory.
20
+ * @module scripts/check-release-package-identity
21
+ */
22
+ import { createHash } from "node:crypto";
23
+ import {
24
+ appendFileSync,
25
+ existsSync,
26
+ mkdirSync,
27
+ readFileSync,
28
+ readdirSync,
29
+ } from "node:fs";
30
+ import path from "node:path";
31
+ import process from "node:process";
32
+
33
+ import { boundedExecFileSync } from "./lib/bounded-spawn.mjs";
34
+ import { invokedAsScript } from "./lib/invoked-as-script.mjs";
35
+
36
+ const RELEASE_COMMIT_PATTERN = /^[a-f0-9]{40}$/u;
37
+ const SOURCE_CERTIFICATE = "src/core/nightly-e2e-guard-behavior-certificate.ts";
38
+ const PACKED_CERTIFICATE =
39
+ "package/dist/core/nightly-e2e-guard-behavior-certificate.js";
40
+
41
+ /** Raise one consistently named fail-closed identity error. */
42
+ function mismatch(message) {
43
+ throw new Error(`Release package identity mismatch: ${message}`);
44
+ }
45
+
46
+ /** Return a required CLI option. */
47
+ function requiredOption(argv, name) {
48
+ const index = argv.indexOf(name);
49
+ const value = index >= 0 ? argv[index + 1] : undefined;
50
+ if (!value || value.startsWith("--")) mismatch(`missing required ${name}`);
51
+ return value;
52
+ }
53
+
54
+ /** Resolve one git revision to its immutable commit. */
55
+ function gitCommit(root, revision) {
56
+ try {
57
+ return String(
58
+ boundedExecFileSync("git", ["rev-parse", `${revision}^{commit}`], {
59
+ cwd: root,
60
+ encoding: "utf8",
61
+ })
62
+ ).trim();
63
+ } catch {
64
+ mismatch(`could not resolve git revision ${revision}`);
65
+ }
66
+ }
67
+
68
+ /** Parse one JSON file with a release-specific diagnostic. */
69
+ function readJson(filePath, label) {
70
+ try {
71
+ return JSON.parse(readFileSync(filePath, "utf8"));
72
+ } catch {
73
+ mismatch(`${label} is missing or invalid JSON (${filePath})`);
74
+ }
75
+ }
76
+
77
+ /** SHA-256 for an immutable file or member. */
78
+ function sha256(bytes) {
79
+ return createHash("sha256").update(bytes).digest("hex");
80
+ }
81
+
82
+ /** Read one tar member without extracting paths into the checkout. */
83
+ function tarMember(tarballPath, member) {
84
+ try {
85
+ return Buffer.from(
86
+ boundedExecFileSync("tar", ["-xOf", tarballPath, member])
87
+ );
88
+ } catch {
89
+ mismatch(`packed candidate does not contain ${member}`);
90
+ }
91
+ }
92
+
93
+ /** Extract the one workspace package version certified by generated bytes. */
94
+ function certificateVersion(bytes, label) {
95
+ const match = String(bytes).match(
96
+ /workspace package @codyswann\/lisa@([^\s"'()]+)\s*\(/u
97
+ );
98
+ if (!match?.[1]) mismatch(`${label} has no workspace package provenance`);
99
+ return match[1];
100
+ }
101
+
102
+ /** Require a valid immutable release commit spelling. */
103
+ export function assertReleaseCommit(releaseCommit) {
104
+ if (!RELEASE_COMMIT_PATTERN.test(releaseCommit)) {
105
+ mismatch(
106
+ `release commit must be 40 lowercase hexadecimal characters; got ${JSON.stringify(releaseCommit)}`
107
+ );
108
+ }
109
+ return releaseCommit;
110
+ }
111
+
112
+ /**
113
+ * Prove that the checked-out tag and HEAD name the supplied release commit.
114
+ * @param {{root: string, releaseCommit: string, tag: string}} input Identity input
115
+ * @returns {{headCommit: string, tagCommit: string, releaseCommit: string}}
116
+ */
117
+ export function assertCheckoutIdentity({ root, releaseCommit, tag }) {
118
+ assertReleaseCommit(releaseCommit);
119
+ const headCommit = gitCommit(root, "HEAD");
120
+ if (headCommit !== releaseCommit) {
121
+ mismatch(
122
+ `HEAD ${headCommit} does not match release commit ${releaseCommit}`
123
+ );
124
+ }
125
+ const tagCommit = gitCommit(root, tag);
126
+ if (tagCommit !== releaseCommit) {
127
+ mismatch(`tag ${tag} resolves ${tagCommit}, expected ${releaseCommit}`);
128
+ }
129
+ return Object.freeze({ headCommit, tagCommit, releaseCommit });
130
+ }
131
+
132
+ /**
133
+ * Pack and validate the exact candidate later passed to npm publish.
134
+ * @param {{root: string, version: string, releaseCommit: string, packDestination: string}} input Candidate input
135
+ * @returns {{version: string, releaseCommit: string, certificateVersion: string|null, tarballPath: string, tarballSha256: string, certificateMemberSha256: string|null}}
136
+ */
137
+ export function packAndValidateReleaseCandidate({
138
+ root,
139
+ version,
140
+ releaseCommit,
141
+ packDestination,
142
+ }) {
143
+ assertReleaseCommit(releaseCommit);
144
+ const packageJson = readJson(path.join(root, "package.json"), "package.json");
145
+ if (packageJson.version !== version) {
146
+ mismatch(
147
+ `package.json version ${JSON.stringify(packageJson.version)} expected ${version}`
148
+ );
149
+ }
150
+ if (packageJson.lisaReleaseCommit !== releaseCommit) {
151
+ mismatch(
152
+ `package.json lisaReleaseCommit ${JSON.stringify(packageJson.lisaReleaseCommit)} expected ${releaseCommit}`
153
+ );
154
+ }
155
+ if (packageJson.gitHead !== releaseCommit) {
156
+ mismatch(
157
+ `package.json gitHead ${JSON.stringify(packageJson.gitHead)} expected ${releaseCommit}`
158
+ );
159
+ }
160
+
161
+ const sourceCertificatePath = path.join(root, SOURCE_CERTIFICATE);
162
+ const hasCertificate = existsSync(sourceCertificatePath);
163
+ if (hasCertificate) {
164
+ const sourceVersion = certificateVersion(
165
+ readFileSync(sourceCertificatePath),
166
+ "source certificate"
167
+ );
168
+ if (sourceVersion !== version) {
169
+ mismatch(
170
+ `source certificate names ${sourceVersion}, expected ${version}`
171
+ );
172
+ }
173
+ }
174
+
175
+ mkdirSync(packDestination, { recursive: true });
176
+ if (readdirSync(packDestination).length !== 0) {
177
+ mismatch(`pack destination must be empty (${packDestination})`);
178
+ }
179
+ let packOutput;
180
+ try {
181
+ packOutput = String(
182
+ boundedExecFileSync(
183
+ "npm",
184
+ [
185
+ "pack",
186
+ "--ignore-scripts",
187
+ "--silent",
188
+ "--pack-destination",
189
+ packDestination,
190
+ ],
191
+ { cwd: root, encoding: "utf8", timeout: 120_000 }
192
+ )
193
+ );
194
+ } catch (error) {
195
+ mismatch(
196
+ `npm pack failed before publication (${error instanceof Error ? error.message : String(error)})`
197
+ );
198
+ }
199
+ const candidates = readdirSync(packDestination).filter(file =>
200
+ file.endsWith(".tgz")
201
+ );
202
+ if (candidates.length !== 1) {
203
+ mismatch("npm pack did not report exactly one candidate tarball");
204
+ }
205
+ const reportedCandidate = packOutput
206
+ .split(/\r?\n/u)
207
+ .map(line => line.trim())
208
+ .filter(Boolean)
209
+ .at(-1);
210
+ if (path.basename(reportedCandidate ?? "") !== candidates[0]) {
211
+ mismatch(
212
+ `npm pack reported ${JSON.stringify(reportedCandidate)}, wrote ${JSON.stringify(candidates[0])}`
213
+ );
214
+ }
215
+
216
+ const tarballPath = path.resolve(packDestination, candidates[0]);
217
+ const tarballBytes = readFileSync(tarballPath);
218
+ const packedPackage = JSON.parse(
219
+ String(tarMember(tarballPath, "package/package.json"))
220
+ );
221
+ if (packedPackage.version !== version) {
222
+ mismatch(
223
+ `packed package version ${JSON.stringify(packedPackage.version)} expected ${version}`
224
+ );
225
+ }
226
+ if (packedPackage.lisaReleaseCommit !== releaseCommit) {
227
+ mismatch(
228
+ `packed lisaReleaseCommit ${JSON.stringify(packedPackage.lisaReleaseCommit)} expected ${releaseCommit}`
229
+ );
230
+ }
231
+ if (packedPackage.gitHead !== releaseCommit) {
232
+ mismatch(
233
+ `packed gitHead ${JSON.stringify(packedPackage.gitHead)} expected ${releaseCommit}`
234
+ );
235
+ }
236
+
237
+ let packedCertificateVersion = null;
238
+ let certificateMemberSha256 = null;
239
+ if (hasCertificate) {
240
+ const member = tarMember(tarballPath, PACKED_CERTIFICATE);
241
+ packedCertificateVersion = certificateVersion(member, "packed certificate");
242
+ if (packedCertificateVersion !== version) {
243
+ mismatch(
244
+ `packed certificate names ${packedCertificateVersion}, expected ${version} at release commit ${releaseCommit} (${PACKED_CERTIFICATE})`
245
+ );
246
+ }
247
+ certificateMemberSha256 = sha256(member);
248
+ }
249
+
250
+ return Object.freeze({
251
+ version,
252
+ releaseCommit,
253
+ certificateVersion: packedCertificateVersion,
254
+ tarballPath,
255
+ tarballSha256: sha256(tarballBytes),
256
+ certificateMemberSha256,
257
+ });
258
+ }
259
+
260
+ /** Append validated tarball identity to a GitHub Actions step output. */
261
+ function writeGithubOutput(result, outputPath) {
262
+ if (!outputPath) return;
263
+ if (result.tarballPath.includes("\n") || result.tarballPath.includes("\r")) {
264
+ mismatch("candidate tarball path contains a line break");
265
+ }
266
+ appendFileSync(
267
+ outputPath,
268
+ [
269
+ `tarball=${result.tarballPath}`,
270
+ `tarball_sha256=${result.tarballSha256}`,
271
+ `certificate_member_sha256=${result.certificateMemberSha256 ?? "not-applicable"}`,
272
+ "",
273
+ ].join("\n")
274
+ );
275
+ }
276
+
277
+ /** Execute the checkout or pack CLI boundary. */
278
+ export function runReleasePackageIdentityCli(
279
+ argv,
280
+ { root = process.cwd(), githubOutput = process.env.GITHUB_OUTPUT } = {}
281
+ ) {
282
+ const [mode, ...options] = argv;
283
+ if (mode === "checkout") {
284
+ const result = assertCheckoutIdentity({
285
+ root,
286
+ releaseCommit: requiredOption(options, "--release-commit"),
287
+ tag: requiredOption(options, "--tag"),
288
+ });
289
+ process.stdout.write(
290
+ `RELEASE_CHECKOUT_IDENTITY_OK ${JSON.stringify(result)}\n`
291
+ );
292
+ return 0;
293
+ }
294
+ if (mode === "pack") {
295
+ const result = packAndValidateReleaseCandidate({
296
+ root,
297
+ version: requiredOption(options, "--version"),
298
+ releaseCommit: requiredOption(options, "--release-commit"),
299
+ packDestination: requiredOption(options, "--pack-destination"),
300
+ });
301
+ writeGithubOutput(result, githubOutput);
302
+ process.stdout.write(
303
+ `RELEASE_PACKAGE_IDENTITY_OK ${JSON.stringify(result)}\n`
304
+ );
305
+ return 0;
306
+ }
307
+ mismatch("expected mode checkout or pack");
308
+ }
309
+
310
+ if (invokedAsScript(import.meta.url)) {
311
+ try {
312
+ process.exitCode = runReleasePackageIdentityCli(process.argv.slice(2));
313
+ } catch (error) {
314
+ process.stderr.write(
315
+ `${error instanceof Error ? error.message : String(error)}\n`
316
+ );
317
+ process.exitCode = 1;
318
+ }
319
+ }
@@ -5,13 +5,13 @@
5
5
  },
6
6
  "inspected": {
7
7
  "workflows": 23,
8
- "steps": 636,
9
- "couplings": 26,
10
- "inventory": 56
8
+ "steps": 639,
9
+ "couplings": 27,
10
+ "inventory": 57
11
11
  },
12
12
  "counts": {
13
13
  "package-backed": 10,
14
- "apply-lagged": 14,
14
+ "apply-lagged": 15,
15
15
  "never-delivered": 0,
16
16
  "undelivered": 2
17
17
  },
@@ -68,6 +68,19 @@
68
68
  ],
69
69
  "detail": "`nightly-e2e-report.yml` is delivered at `@main`, so this step is live in every consumer on their next run. It reads `scripts/check-nightly-e2e-health.mjs` from the CALLER's tree with no package-relative candidate, and Lisa delivers that path on the `lisa apply` channel (typescript/copy-overwrite). The two halves land in the wrong order: this step is live everywhere immediately, the file it needs is live only where somebody applied. The read is unguarded, so an absent path fails the job loudly. Visible, which makes it the better of the two failures."
70
70
  },
71
+ {
72
+ "key": "publish-to-npm.yml::scripts/check-release-package-identity.mjs",
73
+ "workflow": "publish-to-npm.yml",
74
+ "path": "scripts/check-release-package-identity.mjs",
75
+ "channel": "apply",
76
+ "verdict": "apply-lagged",
77
+ "remedy": "run-lisa-apply",
78
+ "guarded": false,
79
+ "lanes": [
80
+ "all/copy-overwrite"
81
+ ],
82
+ "detail": "`publish-to-npm.yml` is delivered at `@main`, so this step is live in every consumer on their next run. It reads `scripts/check-release-package-identity.mjs` from the CALLER's tree with no package-relative candidate, and Lisa delivers that path on the `lisa apply` channel (all/copy-overwrite). The two halves land in the wrong order: this step is live everywhere immediately, the file it needs is live only where somebody applied. The read is unguarded, so an absent path fails the job loudly. Visible, which makes it the better of the two failures."
83
+ },
71
84
  {
72
85
  "key": "quality-rails.yml::scripts/check-threshold-ratchet.mjs",
73
86
  "workflow": "quality-rails.yml",
@@ -68,6 +68,7 @@ scripts/check-conflict-markers.mjs
68
68
  scripts/check-e2e-coverage.mjs
69
69
  scripts/check-lighthouse-details.mjs
70
70
  scripts/check-nightly-e2e-health.mjs
71
+ scripts/check-release-package-identity.mjs
71
72
  scripts/check-skipped-required-checks.mjs
72
73
  scripts/check-state-classification.mjs
73
74
  scripts/check-threshold-ratchet.mjs