@codyswann/lisa 3.33.5 → 3.33.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 (145) hide show
  1. package/all/copy-overwrite/scripts/check-state-classification.mjs +18 -4
  2. package/all/copy-overwrite/scripts/lisa-gates.mjs +47 -46
  3. package/all/copy-overwrite/scripts/lisa-run-gates.mjs +3 -2
  4. package/all/copy-overwrite/scripts/lisa-work-item.mjs +284 -2
  5. package/dist/configs/eslint/base.d.ts +21 -0
  6. package/dist/configs/eslint/base.d.ts.map +1 -1
  7. package/dist/configs/eslint/base.js +78 -2
  8. package/dist/configs/eslint/base.js.map +1 -1
  9. package/dist/configs/eslint/cdk.d.ts.map +1 -1
  10. package/dist/configs/eslint/cdk.js +5 -1
  11. package/dist/configs/eslint/cdk.js.map +1 -1
  12. package/dist/configs/eslint/expo.d.ts.map +1 -1
  13. package/dist/configs/eslint/expo.js +5 -1
  14. package/dist/configs/eslint/expo.js.map +1 -1
  15. package/dist/configs/eslint/harper-fabric.d.ts.map +1 -1
  16. package/dist/configs/eslint/harper-fabric.js +6 -1
  17. package/dist/configs/eslint/harper-fabric.js.map +1 -1
  18. package/dist/configs/eslint/nestjs.d.ts.map +1 -1
  19. package/dist/configs/eslint/nestjs.js +5 -1
  20. package/dist/configs/eslint/nestjs.js.map +1 -1
  21. package/dist/configs/eslint/phaser.d.ts.map +1 -1
  22. package/dist/configs/eslint/phaser.js +6 -1
  23. package/dist/configs/eslint/phaser.js.map +1 -1
  24. package/dist/configs/eslint/typescript.d.ts +2 -2
  25. package/dist/configs/eslint/typescript.d.ts.map +1 -1
  26. package/dist/configs/eslint/typescript.js +6 -2
  27. package/dist/configs/eslint/typescript.js.map +1 -1
  28. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  29. package/dist/core/lisa-owned-hash-ledger.js +12 -0
  30. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  31. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  32. package/dist/core/upstream-evidence-manifest.js +36 -35
  33. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  34. package/expo/copy-overwrite/scripts/bdd/baseline.mjs +11 -4
  35. package/expo/copy-overwrite/scripts/bdd/discover.mjs +12 -6
  36. package/expo/copy-overwrite/scripts/bdd/waivers.mjs +7 -4
  37. package/expo/copy-overwrite/scripts/bdd-matrix.mjs +1 -0
  38. package/expo/copy-overwrite/scripts/check-bdd-coverage.mjs +26 -17
  39. package/expo/copy-overwrite/scripts/check-e2e-coverage.mjs +12 -8
  40. package/package.json +1 -1
  41. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  43. package/plugins/lisa/.codex-plugin/skills/lisa-git-submit-pr/SKILL.md +11 -4
  44. package/plugins/lisa/.codex-plugin/skills/lisa-github-sync/SKILL.md +7 -2
  45. package/plugins/lisa/.codex-plugin/skills/lisa-implement/SKILL.md +1 -1
  46. package/plugins/lisa/.codex-plugin/skills/lisa-jira-sync/SKILL.md +7 -2
  47. package/plugins/lisa/.codex-plugin/skills/lisa-linear-sync/SKILL.md +7 -2
  48. package/plugins/lisa/.codex-plugin/skills/lisa-tracker-sync/SKILL.md +7 -2
  49. package/plugins/lisa/skills/lisa-git-submit-pr/SKILL.md +11 -4
  50. package/plugins/lisa/skills/lisa-github-sync/SKILL.md +7 -2
  51. package/plugins/lisa/skills/lisa-implement/SKILL.md +1 -1
  52. package/plugins/lisa/skills/lisa-jira-sync/SKILL.md +7 -2
  53. package/plugins/lisa/skills/lisa-linear-sync/SKILL.md +7 -2
  54. package/plugins/lisa/skills/lisa-tracker-sync/SKILL.md +7 -2
  55. package/plugins/lisa-agy/plugin.json +1 -1
  56. package/plugins/lisa-agy/skills/lisa-git-submit-pr/SKILL.md +11 -4
  57. package/plugins/lisa-agy/skills/lisa-github-sync/SKILL.md +7 -2
  58. package/plugins/lisa-agy/skills/lisa-implement/SKILL.md +1 -1
  59. package/plugins/lisa-agy/skills/lisa-jira-sync/SKILL.md +7 -2
  60. package/plugins/lisa-agy/skills/lisa-linear-sync/SKILL.md +7 -2
  61. package/plugins/lisa-agy/skills/lisa-tracker-sync/SKILL.md +7 -2
  62. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  64. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  65. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-copilot/skills/lisa-git-submit-pr/SKILL.md +11 -4
  69. package/plugins/lisa-copilot/skills/lisa-github-sync/SKILL.md +7 -2
  70. package/plugins/lisa-copilot/skills/lisa-implement/SKILL.md +1 -1
  71. package/plugins/lisa-copilot/skills/lisa-jira-sync/SKILL.md +7 -2
  72. package/plugins/lisa-copilot/skills/lisa-linear-sync/SKILL.md +7 -2
  73. package/plugins/lisa-copilot/skills/lisa-tracker-sync/SKILL.md +7 -2
  74. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-cursor/skills/lisa-git-submit-pr/SKILL.md +11 -4
  76. package/plugins/lisa-cursor/skills/lisa-github-sync/SKILL.md +7 -2
  77. package/plugins/lisa-cursor/skills/lisa-implement/SKILL.md +1 -1
  78. package/plugins/lisa-cursor/skills/lisa-jira-sync/SKILL.md +7 -2
  79. package/plugins/lisa-cursor/skills/lisa-linear-sync/SKILL.md +7 -2
  80. package/plugins/lisa-cursor/skills/lisa-tracker-sync/SKILL.md +7 -2
  81. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  82. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  83. package/plugins/lisa-expo-agy/plugin.json +1 -1
  84. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  86. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  87. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  88. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  89. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  90. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  91. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  92. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  93. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  94. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  95. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  96. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  97. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  98. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  99. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  100. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  101. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  102. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  103. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  104. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  105. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  106. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  107. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  108. package/plugins/lisa-rails-agy/plugin.json +1 -1
  109. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  110. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  111. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  112. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  113. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  114. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  115. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  116. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  117. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  118. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  119. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  120. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  121. package/plugins/src/base/skills/lisa-git-submit-pr/SKILL.md +11 -4
  122. package/plugins/src/base/skills/lisa-github-sync/SKILL.md +7 -2
  123. package/plugins/src/base/skills/lisa-implement/SKILL.md +1 -1
  124. package/plugins/src/base/skills/lisa-jira-sync/SKILL.md +7 -2
  125. package/plugins/src/base/skills/lisa-linear-sync/SKILL.md +7 -2
  126. package/plugins/src/base/skills/lisa-tracker-sync/SKILL.md +7 -2
  127. package/scripts/check-conflict-markers.mjs +1 -1
  128. package/scripts/check-required-check-promotions.mjs +8 -4
  129. package/scripts/detect-stale-workflow-inputs.mjs +1 -3
  130. package/scripts/fix-namespace-test-assertions.mjs +1 -1
  131. package/scripts/generate-agy-plugin-artifacts.mjs +12 -7
  132. package/scripts/generate-codex-plugin-artifacts.mjs +14 -9
  133. package/scripts/generate-copilot-plugin-artifacts.mjs +1 -1
  134. package/scripts/generate-cursor-plugin-artifacts.mjs +2 -2
  135. package/scripts/generate-upstream-evidence-manifest.mjs +1 -1
  136. package/scripts/lisa-assert-eas-profile.mjs +3 -3
  137. package/scripts/plugin-parity-drift.mjs +29 -13
  138. package/scripts/plugin-routing-validate.mjs +8 -2
  139. package/scripts/update-node-version.ts +7 -3
  140. package/scripts/verify-health-agentic-built.mjs +27 -20
  141. package/scripts/verify-health-consumer-built.mjs +7 -3
  142. package/scripts/verify-health-deterministic-built.mjs +24 -19
  143. package/typescript/copy-overwrite/eslint.ignore.config.json +0 -2
  144. package/typescript/copy-overwrite/scripts/check-nightly-e2e-health.mjs +9 -4
  145. package/typescript/copy-overwrite/scripts/lisa-mutation.mjs +1 -3
@@ -44,17 +44,31 @@ import { fileURLToPath } from "node:url";
44
44
 
45
45
  import { invokedAsScript } from "./lib/invoked-as-script.mjs";
46
46
 
47
+ // Literals named once — each was repeated enough times that a typo in one
48
+ // copy would diverge silently.
49
+ const NOT_INSTALLED = "not-installed";
50
+
47
51
  const REPO_ROOT = path.resolve(
48
52
  path.dirname(fileURLToPath(import.meta.url)),
49
53
  ".."
50
54
  );
51
55
 
52
56
  /**
53
- * Semver 2.0.0 grammar. Build metadata (`+...`) is accepted but ignored in
54
- * comparison; prerelease (`-...`) is accepted and sorts below its release.
57
+ * Semver 2.0.0 grammar, one clause per name. Build metadata (`+...`) is
58
+ * accepted but ignored in comparison; prerelease (`-...`) is accepted and sorts
59
+ * below its release.
60
+ *
61
+ * Assembled from fragments rather than written as one literal because the
62
+ * literal was unreadable — this is the semver.org grammar verbatim, and the
63
+ * composed `.source` is byte-identical to the literal it replaced.
55
64
  */
56
- const SEMVER_RE =
57
- /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
65
+ const SEMVER_NUMERIC = "0|[1-9]\\d*";
66
+ const SEMVER_PRERELEASE_ID = `(?:${SEMVER_NUMERIC}|\\d*[A-Za-z-][0-9A-Za-z-]*)`;
67
+ const SEMVER_PRERELEASE = `(?:-(${SEMVER_PRERELEASE_ID}(?:\\.${SEMVER_PRERELEASE_ID})*))?`;
68
+ const SEMVER_BUILD = "(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?";
69
+ const SEMVER_RE = new RegExp(
70
+ `^(${SEMVER_NUMERIC})\\.(${SEMVER_NUMERIC})\\.(${SEMVER_NUMERIC})${SEMVER_PRERELEASE}${SEMVER_BUILD}$`
71
+ );
58
72
 
59
73
  /** A plugin name / marketplace token: `1*(ALPHA / DIGIT / "-" / "_")`. */
60
74
  const TOKEN_RE = /^[A-Za-z0-9_-]+$/;
@@ -283,11 +297,11 @@ export function resolveCurrentVersion(cacheRoot, name, marketplace) {
283
297
  // enforces this, but resolveCurrentVersion is a public export that no longer
284
298
  // co-locates with its validating caller.
285
299
  if (!TOKEN_RE.test(name) || !TOKEN_RE.test(marketplace)) {
286
- return { status: "not-installed", version: null };
300
+ return { status: NOT_INSTALLED, version: null };
287
301
  }
288
302
  const dir = path.join(cacheRoot, marketplace, name);
289
303
  if (!isDirectory(dir)) {
290
- return { status: "not-installed", version: null };
304
+ return { status: NOT_INSTALLED, version: null };
291
305
  }
292
306
  const versions = [];
293
307
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
@@ -308,8 +322,12 @@ export function resolveCurrentVersion(cacheRoot, name, marketplace) {
308
322
  if (versions.length === 0) {
309
323
  return { status: "unresolved", version: null };
310
324
  }
311
- const max = versions.reduce((acc, v) =>
312
- compareSemver(v, acc) > 0 ? v : acc
325
+ // `versions` is non-empty (guarded above), so seeding with the first element
326
+ // is exactly what the no-seed form did — and it cannot throw if that guard is
327
+ // ever moved.
328
+ const max = versions.reduce(
329
+ (acc, v) => (compareSemver(v, acc) > 0 ? v : acc),
330
+ versions[0]
313
331
  );
314
332
  return { status: "ok", version: max };
315
333
  }
@@ -323,8 +341,8 @@ export function resolveCurrentVersion(cacheRoot, name, marketplace) {
323
341
  * @returns {"ok" | "stale" | "ahead" | "not-installed" | "unresolved"} the status.
324
342
  */
325
343
  export function classify(pinnedVersion, cur) {
326
- if (cur.status === "not-installed") {
327
- return "not-installed";
344
+ if (cur.status === NOT_INSTALLED) {
345
+ return NOT_INSTALLED;
328
346
  }
329
347
  // Defense-in-depth: any non-`ok` resolver state, or an `ok` state without a
330
348
  // string version, is treated as `unresolved` so compareSemver is never called
@@ -550,9 +568,7 @@ function collectSyncedSkills(roots) {
550
568
  * @returns {void}
551
569
  */
552
570
  function emitReport(out, report, json) {
553
- out.write(
554
- (json ? JSON.stringify(report, null, 2) : humanTable(report)) + "\n"
555
- );
571
+ out.write(`${json ? JSON.stringify(report, null, 2) : humanTable(report)}\n`);
556
572
  }
557
573
 
558
574
  /**
@@ -152,7 +152,13 @@ export function cacheMaxVersion(cacheRoot, name, marketplace) {
152
152
  if (versions.length === 0) {
153
153
  return null;
154
154
  }
155
- return versions.reduce((acc, v) => (compareSemver(v, acc) > 0 ? v : acc));
155
+ // `versions` is non-empty (guarded above), so seeding with the first element
156
+ // is exactly what the no-seed form did — and it cannot throw if that guard is
157
+ // ever moved.
158
+ return versions.reduce(
159
+ (acc, v) => (compareSemver(v, acc) > 0 ? v : acc),
160
+ versions[0]
161
+ );
156
162
  }
157
163
 
158
164
  /**
@@ -553,7 +559,7 @@ function humanReport(report) {
553
559
  */
554
560
  function emitReport(out, report, json) {
555
561
  out.write(
556
- (json ? JSON.stringify(report, null, 2) : humanReport(report)) + "\n"
562
+ `${json ? JSON.stringify(report, null, 2) : humanReport(report)}\n`
557
563
  );
558
564
  }
559
565
 
@@ -18,6 +18,10 @@
18
18
  import * as fs from "node:fs";
19
19
  import * as path from "node:path";
20
20
 
21
+ // Literals named once — each was repeated enough times that a typo in one
22
+ // copy would diverge silently.
23
+ const VERSION_REPLACEMENT = "$1{{version}}$2";
24
+
21
25
  const PROJECT_ROOT = path.resolve(import.meta.dirname, "..");
22
26
 
23
27
  /**
@@ -34,12 +38,12 @@ const WORKFLOW_PATTERNS: readonly ReplacementPattern[] = [
34
38
  // node_version: '22.21.1' (workflow input defaults)
35
39
  {
36
40
  pattern: /(node_version:\s*['"])[\d.]+(['"])/g,
37
- replacement: "$1{{version}}$2",
41
+ replacement: VERSION_REPLACEMENT,
38
42
  },
39
43
  // node-version: '22.21.1' (actions/setup-node)
40
44
  {
41
45
  pattern: /(node-version:\s*['"])[\d.]+(['"])/g,
42
- replacement: "$1{{version}}$2",
46
+ replacement: VERSION_REPLACEMENT,
43
47
  },
44
48
  // node-version: '22.x' (major version pattern)
45
49
  {
@@ -50,7 +54,7 @@ const WORKFLOW_PATTERNS: readonly ReplacementPattern[] = [
50
54
 
51
55
  const PACKAGE_JSON_PATTERN: ReplacementPattern = {
52
56
  pattern: /("node":\s*["'])[\d.]+(["'])/g,
53
- replacement: "$1{{version}}$2",
57
+ replacement: VERSION_REPLACEMENT,
54
58
  };
55
59
 
56
60
  /**
@@ -17,6 +17,14 @@ import {
17
17
  import { tmpdir } from "node:os";
18
18
  import path from "node:path";
19
19
 
20
+ // Literals named once — each was repeated enough times that a typo in one
21
+ // copy would diverge silently.
22
+ const TEMPLATES_MANAGED = "templates.managed";
23
+ const AGENTIC_CI_SKIP_LINT = "agentic.ci.skip.lint";
24
+ const ESLINT_CONFIG_TS = "eslint.config.ts";
25
+ const AGENTIC_INTENTIONAL_DRIFT = "agentic.intentional-drift";
26
+ const HARPER_APP = "harper-app";
27
+
20
28
  let assertions = 0;
21
29
  const check = (condition, message) => {
22
30
  assertions += 1;
@@ -241,8 +249,7 @@ function artifactEvaluator(request, signal) {
241
249
  const mutationDisabled =
242
250
  request.config.quality.mutation.gate.enabled === false;
243
251
  const managedDrift = request.deterministicFindings.find(
244
- finding =>
245
- finding.check === "templates.managed" && finding.status === "fail"
252
+ finding => finding.check === TEMPLATES_MANAGED && finding.status === "fail"
246
253
  );
247
254
 
248
255
  if (mutationDisabled) {
@@ -268,19 +275,19 @@ function artifactEvaluator(request, signal) {
268
275
  !/\b(?:reason|justification)\b/iu.test(workflow.content)
269
276
  ) {
270
277
  judgments.push({
271
- check: "agentic.ci.skip.lint",
278
+ check: AGENTIC_CI_SKIP_LINT,
272
279
  reason: `${workflow.path} skips lint without a recorded justification.`,
273
280
  });
274
281
  }
275
282
  }
276
283
  if (
277
- managedDrift?.reason.includes("eslint.config.ts") === true &&
284
+ managedDrift?.reason.includes(ESLINT_CONFIG_TS) === true &&
278
285
  /\b(?:reason|justification)\b/iu.test(override?.content ?? "") &&
279
286
  managedArtifact?.content.includes("no-explicit-any") === true &&
280
287
  /["']off["']/u.test(managedArtifact.content)
281
288
  ) {
282
289
  judgments.push({
283
- check: "agentic.intentional-drift",
290
+ check: AGENTIC_INTENTIONAL_DRIFT,
284
291
  reason:
285
292
  "The bounded managed ESLint drift disables no-explicit-any despite the recorded temporary justification.",
286
293
  });
@@ -316,13 +323,13 @@ try {
316
323
  2
317
324
  )}\n`
318
325
  );
319
- await mkdir(path.join(project, "harper-app"), { recursive: true });
326
+ await mkdir(path.join(project, HARPER_APP), { recursive: true });
320
327
  await writeFile(
321
- path.join(project, "harper-app", "config.yaml"),
328
+ path.join(project, HARPER_APP, "config.yaml"),
322
329
  "graphqlSchema: ./schema.graphql\njsResource: true\nstatic: ./web\n"
323
330
  );
324
331
  await writeFile(
325
- path.join(project, "harper-app", "schema.graphql"),
332
+ path.join(project, HARPER_APP, "schema.graphql"),
326
333
  "type Query { health: Boolean! }\n"
327
334
  );
328
335
  git("init", "--initial-branch=main");
@@ -525,7 +532,7 @@ try {
525
532
  check(
526
533
  unreasonedSkip.findings.some(
527
534
  finding =>
528
- finding.check === "agentic.ci.skip.lint" &&
535
+ finding.check === AGENTIC_CI_SKIP_LINT &&
529
536
  finding.reason.includes("ci.yml")
530
537
  ),
531
538
  "unreasoned skipped job produces a named warning"
@@ -545,22 +552,22 @@ try {
545
552
  );
546
553
  check(
547
554
  !reasonedSkip.findings.some(
548
- finding => finding.check === "agentic.ci.skip.lint"
555
+ finding => finding.check === AGENTIC_CI_SKIP_LINT
549
556
  ),
550
557
  "recorded skip justification clears the job warning"
551
558
  );
552
559
  printStateDump("skipped-job-reason-clears", {
553
560
  undocumented: unreasonedSkip.findings
554
- .filter(finding => finding.check === "agentic.ci.skip.lint")
561
+ .filter(finding => finding.check === AGENTIC_CI_SKIP_LINT)
555
562
  .map(findingState),
556
563
  documented: reasonedSkip.findings
557
- .filter(finding => finding.check === "agentic.ci.skip.lint")
564
+ .filter(finding => finding.check === AGENTIC_CI_SKIP_LINT)
558
565
  .map(findingState),
559
566
  });
560
567
  console.log("[EVIDENCE: state-dump: skipped-job-reason-clears]");
561
568
  await writeFile(ciPath, ciOriginal);
562
569
 
563
- const managedEslintPath = path.join(project, "eslint.config.ts");
570
+ const managedEslintPath = path.join(project, ESLINT_CONFIG_TS);
564
571
  const managedEslintOriginal = await readFile(managedEslintPath);
565
572
  const managedJustification =
566
573
  '// justification: temporary vendor migration\nexport default [{ rules: { "@typescript-eslint/no-explicit-any": "off" } }];\n';
@@ -599,7 +606,7 @@ try {
599
606
  );
600
607
  equal(
601
608
  observedManagedArtifacts.map(artifact => artifact?.path),
602
- ["eslint.config.ts", "eslint.config.ts"],
609
+ [ESLINT_CONFIG_TS, ESLINT_CONFIG_TS],
603
610
  "both drift judgments inspect the bounded managed ESLint artifact"
604
611
  );
605
612
  equal(
@@ -613,18 +620,18 @@ try {
613
620
  check(
614
621
  benignDrift.findings.some(
615
622
  finding =>
616
- finding.check === "templates.managed" && finding.status === "fail"
623
+ finding.check === TEMPLATES_MANAGED && finding.status === "fail"
617
624
  ),
618
625
  "benign agentic judgment cannot erase deterministic managed drift"
619
626
  );
620
627
  check(
621
628
  !benignDrift.findings.some(
622
- finding => finding.check === "agentic.intentional-drift"
629
+ finding => finding.check === AGENTIC_INTENTIONAL_DRIFT
623
630
  ),
624
631
  "benign managed content produces no drift judgment"
625
632
  );
626
633
  const suspiciousDriftWarning = suspiciousDrift.findings.find(
627
- finding => finding.check === "agentic.intentional-drift"
634
+ finding => finding.check === AGENTIC_INTENTIONAL_DRIFT
628
635
  );
629
636
  equal(
630
637
  suspiciousDriftWarning?.status,
@@ -643,7 +650,7 @@ try {
643
650
  .update(benignManagedContent)
644
651
  .digest("hex"),
645
652
  warnings: benignDrift.findings
646
- .filter(finding => finding.check === "agentic.intentional-drift")
653
+ .filter(finding => finding.check === AGENTIC_INTENTIONAL_DRIFT)
647
654
  .map(findingState),
648
655
  },
649
656
  suspicious: {
@@ -651,7 +658,7 @@ try {
651
658
  .update(suspiciousManagedContent)
652
659
  .digest("hex"),
653
660
  warnings: suspiciousDrift.findings
654
- .filter(finding => finding.check === "agentic.intentional-drift")
661
+ .filter(finding => finding.check === AGENTIC_INTENTIONAL_DRIFT)
655
662
  .map(findingState),
656
663
  },
657
664
  });
@@ -678,7 +685,7 @@ try {
678
685
  name: "malformed",
679
686
  evaluator: async () => ({
680
687
  status: "completed",
681
- judgments: [{ check: "templates.managed", reason: "collision" }],
688
+ judgments: [{ check: TEMPLATES_MANAGED, reason: "collision" }],
682
689
  }),
683
690
  timeoutMs: 1_000,
684
691
  },
@@ -14,6 +14,10 @@ import {
14
14
  import { tmpdir } from "node:os";
15
15
  import path from "node:path";
16
16
 
17
+ // Literals named once — each was repeated enough times that a typo in one
18
+ // copy would diverge silently.
19
+ const LATEST_JSON = "latest.json";
20
+
17
21
  let assertions = 0;
18
22
  const check = (condition, message) => {
19
23
  assertions += 1;
@@ -64,7 +68,7 @@ function runCli(project, argv, input) {
64
68
 
65
69
  /** Return whether the canonical result exists without throwing on absence. */
66
70
  async function resultExists(project) {
67
- return lstat(path.join(project, ".lisa", "health", "latest.json"))
71
+ return lstat(path.join(project, ".lisa", "health", LATEST_JSON))
68
72
  .then(() => true)
69
73
  .catch(() => false);
70
74
  }
@@ -91,14 +95,14 @@ async function prepare(project) {
91
95
 
92
96
  /** Validate one final CLI result against its exact persisted representation. */
93
97
  async function validateFinal(project, stdout, expectedMode) {
94
- const resultPath = path.join(project, ".lisa", "health", "latest.json");
98
+ const resultPath = path.join(project, ".lisa", "health", LATEST_JSON);
95
99
  const persisted = await readFile(resultPath, "utf8");
96
100
  equal(stdout, persisted, `${expectedMode} stdout equals persisted bytes`);
97
101
  const result = JSON.parse(stdout);
98
102
  equal(result.mode, expectedMode, `final result is ${expectedMode}`);
99
103
  equal(
100
104
  (await readdir(path.dirname(resultPath))).sort(),
101
- ["latest.json"],
105
+ [LATEST_JSON],
102
106
  "one final result remains with no temporary or lock artifact"
103
107
  );
104
108
  return result;
@@ -18,6 +18,14 @@ import {
18
18
  import { tmpdir } from "node:os";
19
19
  import path from "node:path";
20
20
 
21
+ // Literals named once — each was repeated enough times that a typo in one
22
+ // copy would diverge silently.
23
+ const PACKAGE_JSON = "package.json";
24
+ const LISA_CONFIG_JSON = ".lisa.config.json";
25
+ const HARPER_APP = "harper-app";
26
+ const TEMPLATES_MANAGED = "templates.managed";
27
+ const PRETTIERIGNORE = ".prettierignore";
28
+
21
29
  let assertions = 0;
22
30
  const check = (condition, message) => {
23
31
  assertions += 1;
@@ -149,7 +157,7 @@ try {
149
157
  await mkdir(isolatedBin, { recursive: true });
150
158
  await symlink(gitExecutable, path.join(isolatedBin, "git"));
151
159
  await writeFile(
152
- path.join(project, "package.json"),
160
+ path.join(project, PACKAGE_JSON),
153
161
  `${JSON.stringify(
154
162
  {
155
163
  name: "deterministic-health-fixture",
@@ -161,7 +169,7 @@ try {
161
169
  )}\n`
162
170
  );
163
171
  await writeFile(
164
- path.join(project, ".lisa.config.json"),
172
+ path.join(project, LISA_CONFIG_JSON),
165
173
  `${JSON.stringify(
166
174
  {
167
175
  tracker: "github",
@@ -172,13 +180,13 @@ try {
172
180
  2
173
181
  )}\n`
174
182
  );
175
- await mkdir(path.join(project, "harper-app"), { recursive: true });
183
+ await mkdir(path.join(project, HARPER_APP), { recursive: true });
176
184
  await writeFile(
177
- path.join(project, "harper-app", "config.yaml"),
185
+ path.join(project, HARPER_APP, "config.yaml"),
178
186
  "graphqlSchema: ./schema.graphql\njsResource: true\nstatic: ./web\n"
179
187
  );
180
188
  await writeFile(
181
- path.join(project, "harper-app", "schema.graphql"),
189
+ path.join(project, HARPER_APP, "schema.graphql"),
182
190
  "type Query { health: Boolean! }\n"
183
191
  );
184
192
  git("init", "--initial-branch=main");
@@ -220,7 +228,7 @@ try {
220
228
  }
221
229
  );
222
230
  const version = JSON.parse(
223
- await readFile(path.join(root, "package.json"), "utf8")
231
+ await readFile(path.join(root, PACKAGE_JSON), "utf8")
224
232
  ).version;
225
233
  await writeFile(
226
234
  path.join(project, ".claude", ".lisa-plugins-synced"),
@@ -256,13 +264,12 @@ try {
256
264
  equal(clean.summary.counts.fail, 0, "clean fixture has zero failures");
257
265
  equal(clean.summary.verdict, "in band", "clean fixture is in band");
258
266
  equal(
259
- clean.findings.find(finding => finding.check === "templates.managed")
260
- .status,
267
+ clean.findings.find(finding => finding.check === TEMPLATES_MANAGED).status,
261
268
  "pass",
262
269
  "the real TypeScript plus Harper child template stack composes cleanly"
263
270
  );
264
271
  check(
265
- (await readFile(path.join(project, ".prettierignore"), "utf8")).includes(
272
+ (await readFile(path.join(project, PRETTIERIGNORE), "utf8")).includes(
266
273
  "AI GUARDRAILS HARPER-FABRIC"
267
274
  ),
268
275
  "the real child copy-contents block survives its parent overwrite"
@@ -298,14 +305,14 @@ try {
298
305
  );
299
306
  await writeFile(callerPath, callerOriginal);
300
307
 
301
- const prettierPath = path.join(project, ".prettierignore");
308
+ const prettierPath = path.join(project, PRETTIERIGNORE);
302
309
  const prettierOriginal = await readFile(prettierPath);
303
310
  await writeFile(path.join(project, ".lisaignore"), ".prettierignore\n");
304
311
  await writeFile(prettierPath, "project-owned while ignored\n");
305
312
  const ignoredTemplate = await assertPureCollection(health, options);
306
313
  equal(
307
314
  ignoredTemplate.findings.find(
308
- finding => finding.check === "templates.managed"
315
+ finding => finding.check === TEMPLATES_MANAGED
309
316
  ).status,
310
317
  "pass",
311
318
  "ignored templates are outside Lisa ownership"
@@ -337,9 +344,8 @@ try {
337
344
  await writeFile(learningsPath, customLearnings);
338
345
  const learningsOwned = await assertPureCollection(health, options);
339
346
  equal(
340
- learningsOwned.findings.find(
341
- finding => finding.check === "templates.managed"
342
- ).status,
347
+ learningsOwned.findings.find(finding => finding.check === TEMPLATES_MANAGED)
348
+ .status,
343
349
  "pass",
344
350
  "project learnings remain host-owned after their initial seed"
345
351
  );
@@ -396,8 +402,7 @@ try {
396
402
  );
397
403
  const drift = await assertPureCollection(health, options);
398
404
  const managedDrift = drift.findings.filter(
399
- finding =>
400
- finding.check === "templates.managed" && finding.status === "fail"
405
+ finding => finding.check === TEMPLATES_MANAGED && finding.status === "fail"
401
406
  );
402
407
  equal(
403
408
  managedDrift.length,
@@ -422,7 +427,7 @@ try {
422
427
  console.log("[EVIDENCE: clean-project-in-band]");
423
428
  console.log("[EVIDENCE: managed-file-restoration-in-band]");
424
429
 
425
- const configPath = path.join(project, ".lisa.config.json");
430
+ const configPath = path.join(project, LISA_CONFIG_JSON);
426
431
  const config = JSON.parse(await readFile(configPath, "utf8"));
427
432
  delete config.github.repo;
428
433
  await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`);
@@ -458,10 +463,10 @@ try {
458
463
  const unsafeProject = path.join(workspace, "unsafe-host");
459
464
  await mkdir(unsafeProject, { recursive: true });
460
465
  await writeFile(
461
- path.join(unsafeProject, ".lisa.config.json"),
466
+ path.join(unsafeProject, LISA_CONFIG_JSON),
462
467
  '{"tracker":"github","harness":"codex"}\n'
463
468
  );
464
- execFileSync("mkfifo", [path.join(unsafeProject, "package.json")]);
469
+ execFileSync("mkfifo", [path.join(unsafeProject, PACKAGE_JSON)]);
465
470
  const unsafeStarted = Date.now();
466
471
  const unsafe = await health.runDeterministicHealth(unsafeProject, {
467
472
  lisaRoot: root,
@@ -72,8 +72,6 @@
72
72
  "*.d.ts",
73
73
  "esbuild.plugins.js",
74
74
 
75
- "projects/**/scripts/**",
76
- "scripts/**",
77
75
 
78
76
  "lib/**/*.js",
79
77
  "src/lib/**/*.js",
@@ -426,7 +426,9 @@ function validateMatch(match, where) {
426
426
  try {
427
427
  // No `g` flag, ever: a sticky lastIndex makes repeated .test() calls
428
428
  // return alternating answers, which reads as an intermittent gate.
429
- void new RegExp(pattern);
429
+ // Compiling IS the assertion — an invalid pattern throws right here — so
430
+ // the result is deliberately unused.
431
+ const _compiled = new RegExp(pattern);
430
432
  } catch (error) {
431
433
  throw new GateConfigError(
432
434
  `${where}: \`match.pattern\` does not compile: ${error instanceof Error ? error.message : String(error)}`
@@ -1550,6 +1552,9 @@ function sleep(ms) {
1550
1552
  return new Promise(resolve => setTimeout(resolve, ms));
1551
1553
  }
1552
1554
 
1555
+ /** GitHub's remaining-quota response header, read from three call sites. */
1556
+ const RATELIMIT_REMAINING_HEADER = "x-ratelimit-remaining";
1557
+
1553
1558
  /**
1554
1559
  * How long to wait before retrying a throttled response, bounded.
1555
1560
  *
@@ -1563,7 +1568,7 @@ export function retryDelayMs(response, attempt, maxSeconds) {
1563
1568
  if (Number.isFinite(retryAfter) && retryAfter > 0) {
1564
1569
  return Math.min(retryAfter, maxSeconds) * 1000;
1565
1570
  }
1566
- const remaining = response.headers?.get?.("x-ratelimit-remaining");
1571
+ const remaining = response.headers?.get?.(RATELIMIT_REMAINING_HEADER);
1567
1572
  const reset = Number(response.headers?.get?.("x-ratelimit-reset"));
1568
1573
  if (remaining === "0" && Number.isFinite(reset)) {
1569
1574
  const seconds = Math.max(0, reset - Math.floor(Date.now() / 1000));
@@ -1613,7 +1618,7 @@ export async function apiGet(api, path, wait = sleep) {
1613
1618
  // 401/403 without rate-limit headers is an auth problem: retrying cannot
1614
1619
  // fix a token that is not allowed to read run history, so fail immediately
1615
1620
  // with a message that names the fix.
1616
- const remaining = response.headers?.get?.("x-ratelimit-remaining");
1621
+ const remaining = response.headers?.get?.(RATELIMIT_REMAINING_HEADER);
1617
1622
  const throttled =
1618
1623
  response.status === 429 || (response.status === 403 && remaining === "0");
1619
1624
  if ((response.status === 401 || response.status === 403) && !throttled) {
@@ -1782,7 +1787,7 @@ export async function apiWrite(api, method, path, payload, wait = sleep) {
1782
1787
  continue;
1783
1788
  }
1784
1789
  if (response.ok) return await response.json();
1785
- const remaining = response.headers?.get?.("x-ratelimit-remaining");
1790
+ const remaining = response.headers?.get?.(RATELIMIT_REMAINING_HEADER);
1786
1791
  const throttled =
1787
1792
  response.status === 429 || (response.status === 403 && remaining === "0");
1788
1793
  if ((response.status === 401 || response.status === 403) && !throttled) {
@@ -120,9 +120,7 @@ try {
120
120
 
121
121
  if (changed.length === 0) {
122
122
  console.log(
123
- "⚪ Mutation gate: no changed source files vs " +
124
- since +
125
- ". Nothing to mutate."
123
+ `⚪ Mutation gate: no changed source files vs ${since}. Nothing to mutate.`
126
124
  );
127
125
  process.exit(0);
128
126
  }