@codyswann/lisa 3.33.4 → 3.33.6

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 (108) 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/dist/configs/eslint/base.d.ts +21 -0
  5. package/dist/configs/eslint/base.d.ts.map +1 -1
  6. package/dist/configs/eslint/base.js +78 -2
  7. package/dist/configs/eslint/base.js.map +1 -1
  8. package/dist/configs/eslint/cdk.d.ts.map +1 -1
  9. package/dist/configs/eslint/cdk.js +5 -1
  10. package/dist/configs/eslint/cdk.js.map +1 -1
  11. package/dist/configs/eslint/expo.d.ts.map +1 -1
  12. package/dist/configs/eslint/expo.js +5 -1
  13. package/dist/configs/eslint/expo.js.map +1 -1
  14. package/dist/configs/eslint/harper-fabric.d.ts.map +1 -1
  15. package/dist/configs/eslint/harper-fabric.js +6 -1
  16. package/dist/configs/eslint/harper-fabric.js.map +1 -1
  17. package/dist/configs/eslint/nestjs.d.ts.map +1 -1
  18. package/dist/configs/eslint/nestjs.js +5 -1
  19. package/dist/configs/eslint/nestjs.js.map +1 -1
  20. package/dist/configs/eslint/phaser.d.ts.map +1 -1
  21. package/dist/configs/eslint/phaser.js +6 -1
  22. package/dist/configs/eslint/phaser.js.map +1 -1
  23. package/dist/configs/eslint/typescript.d.ts +2 -2
  24. package/dist/configs/eslint/typescript.d.ts.map +1 -1
  25. package/dist/configs/eslint/typescript.js +6 -2
  26. package/dist/configs/eslint/typescript.js.map +1 -1
  27. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  28. package/dist/core/lisa-owned-hash-ledger.js +11 -0
  29. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  30. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  31. package/dist/core/upstream-evidence-manifest.js +29 -28
  32. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  33. package/expo/copy-overwrite/scripts/bdd/baseline.mjs +11 -4
  34. package/expo/copy-overwrite/scripts/bdd/discover.mjs +12 -6
  35. package/expo/copy-overwrite/scripts/bdd/waivers.mjs +7 -4
  36. package/expo/copy-overwrite/scripts/bdd-matrix.mjs +1 -0
  37. package/expo/copy-overwrite/scripts/check-bdd-coverage.mjs +26 -17
  38. package/expo/copy-overwrite/scripts/check-e2e-coverage.mjs +12 -8
  39. package/package.json +1 -1
  40. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  42. package/plugins/lisa-agy/plugin.json +1 -1
  43. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  45. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  46. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  52. package/plugins/lisa-expo-agy/plugin.json +1 -1
  53. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  57. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  58. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  62. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  63. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  67. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  68. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  72. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  73. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  77. package/plugins/lisa-rails-agy/plugin.json +1 -1
  78. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  82. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  83. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  84. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  86. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  87. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  88. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  89. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  90. package/scripts/check-conflict-markers.mjs +1 -1
  91. package/scripts/check-required-check-promotions.mjs +8 -4
  92. package/scripts/detect-stale-workflow-inputs.mjs +1 -3
  93. package/scripts/fix-namespace-test-assertions.mjs +1 -1
  94. package/scripts/generate-agy-plugin-artifacts.mjs +12 -7
  95. package/scripts/generate-codex-plugin-artifacts.mjs +14 -9
  96. package/scripts/generate-copilot-plugin-artifacts.mjs +1 -1
  97. package/scripts/generate-cursor-plugin-artifacts.mjs +2 -2
  98. package/scripts/generate-upstream-evidence-manifest.mjs +1 -1
  99. package/scripts/lisa-assert-eas-profile.mjs +3 -3
  100. package/scripts/plugin-parity-drift.mjs +29 -13
  101. package/scripts/plugin-routing-validate.mjs +8 -2
  102. package/scripts/update-node-version.ts +7 -3
  103. package/scripts/verify-health-agentic-built.mjs +27 -20
  104. package/scripts/verify-health-consumer-built.mjs +7 -3
  105. package/scripts/verify-health-deterministic-built.mjs +24 -19
  106. package/typescript/copy-overwrite/eslint.ignore.config.json +0 -2
  107. package/typescript/copy-overwrite/scripts/check-nightly-e2e-health.mjs +9 -4
  108. package/typescript/copy-overwrite/scripts/lisa-mutation.mjs +1 -3
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "3.33.4",
3
+ "version": "3.33.6",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "3.33.4",
3
+ "version": "3.33.6",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "3.33.4",
3
+ "version": "3.33.6",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "3.33.4",
3
+ "version": "3.33.6",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "3.33.4",
3
+ "version": "3.33.6",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "3.33.4",
3
+ "version": "3.33.6",
4
4
  "description": "Ruby on Rails-specific skills and hooks for RuboCop and ast-grep scanning on edit.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "3.33.4",
3
+ "version": "3.33.6",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "3.33.4",
3
+ "version": "3.33.6",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "3.33.4",
3
+ "version": "3.33.6",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "3.33.4",
3
+ "version": "3.33.6",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "3.33.4",
3
+ "version": "3.33.6",
4
4
  "description": "TypeScript-specific hooks for formatting, linting, and ast-grep scanning on edit.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "3.33.4",
3
+ "version": "3.33.6",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "3.33.4",
3
+ "version": "3.33.6",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "3.33.4",
3
+ "version": "3.33.6",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "3.33.4",
3
+ "version": "3.33.6",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "3.33.4",
3
+ "version": "3.33.6",
4
4
  "description": "Distributable LLM Wiki kernel — ingest, query, lint, and maintain a git-native markdown knowledge base across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "3.33.4",
3
+ "version": "3.33.6",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "3.33.4",
3
+ "version": "3.33.6",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "3.33.4",
3
+ "version": "3.33.6",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -299,7 +299,7 @@ export function main(argv, io = {}) {
299
299
  scanned: files.length,
300
300
  });
301
301
  out.write(
302
- (opts.json ? JSON.stringify(report, null, 2) : humanReport(report)) + "\n"
302
+ `${opts.json ? JSON.stringify(report, null, 2) : humanReport(report)}\n`
303
303
  );
304
304
  return results.length === 0 ? 0 : 1;
305
305
  }
@@ -140,6 +140,10 @@ import path from "node:path";
140
140
  import { fileURLToPath } from "node:url";
141
141
  import yaml from "js-yaml";
142
142
 
143
+ // Literals named once — each was repeated enough times that a typo in one
144
+ // copy would diverge silently.
145
+ const HEADROOM_EVIDENCE_MISSING = "headroom-evidence-missing";
146
+
143
147
  /** Integration id GitHub Actions reports status checks under. */
144
148
  export const ACTIONS_INTEGRATION_ID = 15_368;
145
149
 
@@ -343,7 +347,7 @@ function budgetProblems(budget) {
343
347
  if (typeof subject !== "string" || typeof measuredOn !== "string") {
344
348
  return [
345
349
  {
346
- rule: "headroom-evidence-missing",
350
+ rule: HEADROOM_EVIDENCE_MISSING,
347
351
  detail:
348
352
  "every headroom.budgets[] entry needs a subject and a measured_on_subject",
349
353
  },
@@ -391,7 +395,7 @@ function provenanceProblems(observedOn) {
391
395
  }
392
396
  return [
393
397
  {
394
- rule: "headroom-evidence-missing",
398
+ rule: HEADROOM_EVIDENCE_MISSING,
395
399
  detail:
396
400
  'headroom.observed_on must be "pass": the run that produced observed_worst_ms must have COMPLETED within its budget, because a duration reported alongside a timeout measures contention, not cost',
397
401
  },
@@ -414,7 +418,7 @@ function ratioProblems(budgetMs, observedMs) {
414
418
  ) {
415
419
  return [
416
420
  {
417
- rule: "headroom-evidence-missing",
421
+ rule: HEADROOM_EVIDENCE_MISSING,
418
422
  detail:
419
423
  "headroom needs a positive budget_ms and a positive observed_worst_ms",
420
424
  },
@@ -481,7 +485,7 @@ export function headroomProblems(headroom) {
481
485
  for (const [field, detail] of prose) {
482
486
  const value = headroom[field];
483
487
  if (typeof value !== "string" || value.trim() === "") {
484
- problems.push({ rule: "headroom-evidence-missing", detail });
488
+ problems.push({ rule: HEADROOM_EVIDENCE_MISSING, detail });
485
489
  }
486
490
  }
487
491
  if (problems.length > 0) return problems;
@@ -231,9 +231,7 @@ function humanTable(report) {
231
231
  * @returns {void}
232
232
  */
233
233
  function emitReport(out, report, json) {
234
- out.write(
235
- (json ? JSON.stringify(report, null, 2) : humanTable(report)) + "\n"
236
- );
234
+ out.write(`${json ? JSON.stringify(report, null, 2) : humanTable(report)}\n`);
237
235
  }
238
236
 
239
237
  /**
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * Fix test assertions and a few skill references after lisa-* skill / lisa: command namespace migration.
4
4
  */
5
- import { readFileSync, writeFileSync, readdirSync, statSync } from "node:fs";
5
+ import { readFileSync, writeFileSync } from "node:fs";
6
6
  import path from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
 
@@ -44,6 +44,11 @@ import { fileURLToPath } from "node:url";
44
44
  import { nestCommandsUnderLisa } from "./lib/nest-plugin-commands.mjs";
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 CLAUDE_PLUGIN_DIR = ".claude-plugin";
50
+ const PLUGIN_MANIFEST = "plugin.json";
51
+
47
52
  const REPO_ROOT = path.resolve(
48
53
  path.dirname(fileURLToPath(import.meta.url)),
49
54
  ".."
@@ -115,7 +120,7 @@ function copyDir(src, dst, keep = () => true) {
115
120
  * @param {string} version Version string for the manifest.
116
121
  */
117
122
  export function generateAgyVariant(srcDir, outDir, version) {
118
- const claudeManifest = path.join(srcDir, ".claude-plugin", "plugin.json");
123
+ const claudeManifest = path.join(srcDir, CLAUDE_PLUGIN_DIR, PLUGIN_MANIFEST);
119
124
  if (!fs.existsSync(claudeManifest)) return;
120
125
 
121
126
  const policy = readPolicy();
@@ -148,8 +153,8 @@ export function generateAgyVariant(srcDir, outDir, version) {
148
153
  }
149
154
  // Skip the .claude-plugin manifest — we'll write a bare plugin.json instead.
150
155
  if (
151
- relPath === path.join(".claude-plugin", "plugin.json") ||
152
- relPath === ".claude-plugin"
156
+ relPath === path.join(CLAUDE_PLUGIN_DIR, PLUGIN_MANIFEST) ||
157
+ relPath === CLAUDE_PLUGIN_DIR
153
158
  ) {
154
159
  return false;
155
160
  }
@@ -181,11 +186,11 @@ export function generateAgyVariant(srcDir, outDir, version) {
181
186
  delete manifest.hooks;
182
187
  delete manifest.mcpServers;
183
188
 
184
- const bareManifestPath = path.join(outDir, "plugin.json");
185
- fs.writeFileSync(bareManifestPath, JSON.stringify(manifest, null, 2) + "\n");
189
+ const bareManifestPath = path.join(outDir, PLUGIN_MANIFEST);
190
+ fs.writeFileSync(bareManifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
186
191
 
187
192
  // 3. Ensure no .claude-plugin/ directory survives.
188
- const ghostDir = path.join(outDir, ".claude-plugin");
193
+ const ghostDir = path.join(outDir, CLAUDE_PLUGIN_DIR);
189
194
  if (fs.existsSync(ghostDir)) {
190
195
  fs.rmSync(ghostDir, { recursive: true, force: true });
191
196
  }
@@ -328,7 +333,7 @@ function emitAgyPluginHooks(srcDir, outDir, sourceHooks, installDirName) {
328
333
  );
329
334
  fs.writeFileSync(
330
335
  path.join(outDir, "hooks.json"),
331
- JSON.stringify(hooksConfig, null, 2) + "\n"
336
+ `${JSON.stringify(hooksConfig, null, 2)}\n`
332
337
  );
333
338
 
334
339
  // Copy the agy-protocol scripts into the variant's hooks/ subdir.
@@ -38,6 +38,11 @@ import fs from "node:fs";
38
38
  import path from "node:path";
39
39
  import { fileURLToPath } from "node:url";
40
40
 
41
+ // Literals named once — each was repeated enough times that a typo in one
42
+ // copy would diverge silently.
43
+ const CODEX_PLUGIN_DIR = ".codex-plugin";
44
+ const HOOKS_MANIFEST = "hooks.json";
45
+
41
46
  const REPO_ROOT = path.resolve(
42
47
  path.dirname(fileURLToPath(import.meta.url)),
43
48
  ".."
@@ -469,7 +474,7 @@ function writeSkillAgentsInRoot(skillsDir) {
469
474
  function codexSkillRoots(pluginDir) {
470
475
  return [
471
476
  path.join(pluginDir, "skills"),
472
- path.join(pluginDir, ".codex-plugin", "skills"),
477
+ path.join(pluginDir, CODEX_PLUGIN_DIR, "skills"),
473
478
  ].filter(skillRoot => fs.existsSync(skillRoot));
474
479
  }
475
480
 
@@ -482,7 +487,7 @@ function codexSkillRoots(pluginDir) {
482
487
  * @returns {readonly string[]} Generated skill names.
483
488
  */
484
489
  export function emitCommandSkills(pluginDir) {
485
- const generatedRoot = path.join(pluginDir, ".codex-plugin", "skills");
490
+ const generatedRoot = path.join(pluginDir, CODEX_PLUGIN_DIR, "skills");
486
491
  const commandsRoot = path.join(pluginDir, "commands");
487
492
  if (!fs.existsSync(commandsRoot)) {
488
493
  return [];
@@ -536,7 +541,7 @@ export function emitCommandSkills(pluginDir) {
536
541
  */
537
542
  export function emitCodexSkillVariants(pluginDir) {
538
543
  const authoredRoot = path.join(pluginDir, "skills");
539
- const codexRoot = path.join(pluginDir, ".codex-plugin", "skills");
544
+ const codexRoot = path.join(pluginDir, CODEX_PLUGIN_DIR, "skills");
540
545
  fs.rmSync(codexRoot, { force: true, recursive: true });
541
546
  if (!fs.existsSync(authoredRoot)) return [];
542
547
 
@@ -799,14 +804,14 @@ function main() {
799
804
  * @param {object} claudeManifest Parsed contents of .claude-plugin/plugin.json.
800
805
  */
801
806
  export function emitCodexHooks(pluginDir, claudeManifest) {
802
- const codexPluginDir = path.join(pluginDir, ".codex-plugin");
803
- const hooksJsonPath = path.join(codexPluginDir, "hooks.json");
807
+ const codexPluginDir = path.join(pluginDir, CODEX_PLUGIN_DIR);
808
+ const hooksJsonPath = path.join(codexPluginDir, HOOKS_MANIFEST);
804
809
  // Purge the older layouts so a rebuilt plugin never ships a stale/duplicate
805
810
  // file:
806
811
  // - <plugin-root>/hooks/hooks.json (2.121–2.124; broke Claude startup)
807
812
  // - <plugin-root>/.codex-plugin/hooks/ (pre-2.121 copied-scripts dir)
808
813
  // The shared .sh scripts at <plugin-root>/hooks/ are left untouched.
809
- fs.rmSync(path.join(pluginDir, "hooks", "hooks.json"), { force: true });
814
+ fs.rmSync(path.join(pluginDir, "hooks", HOOKS_MANIFEST), { force: true });
810
815
  fs.rmSync(path.join(codexPluginDir, "hooks"), {
811
816
  force: true,
812
817
  recursive: true,
@@ -861,7 +866,7 @@ function writeCodexManifest(pluginDir, claudeManifest, pluginName, version) {
861
866
  },
862
867
  };
863
868
 
864
- const manifestDir = path.join(pluginDir, ".codex-plugin");
869
+ const manifestDir = path.join(pluginDir, CODEX_PLUGIN_DIR);
865
870
  fs.mkdirSync(manifestDir, { recursive: true });
866
871
  fs.writeFileSync(
867
872
  path.join(manifestDir, "plugin.json"),
@@ -871,7 +876,7 @@ function writeCodexManifest(pluginDir, claudeManifest, pluginName, version) {
871
876
 
872
877
  export function componentPointers(pluginDir) {
873
878
  return {
874
- ...(fs.existsSync(path.join(pluginDir, ".codex-plugin", "skills"))
879
+ ...(fs.existsSync(path.join(pluginDir, CODEX_PLUGIN_DIR, "skills"))
875
880
  ? { skills: "./.codex-plugin/skills/" }
876
881
  : fs.existsSync(path.join(pluginDir, "skills"))
877
882
  ? { skills: "./skills/" }
@@ -879,7 +884,7 @@ export function componentPointers(pluginDir) {
879
884
  ...(fs.existsSync(path.join(pluginDir, ".mcp.json"))
880
885
  ? { mcpServers: "./.mcp.json" }
881
886
  : {}),
882
- ...(fs.existsSync(path.join(pluginDir, ".codex-plugin", "hooks.json"))
887
+ ...(fs.existsSync(path.join(pluginDir, CODEX_PLUGIN_DIR, HOOKS_MANIFEST))
883
888
  ? { hooks: "./.codex-plugin/hooks.json" }
884
889
  : {}),
885
890
  };
@@ -247,7 +247,7 @@ export function generateCopilotVariant(srcDir, outDir, version) {
247
247
  }
248
248
  }
249
249
 
250
- fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
250
+ fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
251
251
 
252
252
  // 4. Filter the hooks/ directory.
253
253
  const hooksDir = path.join(outDir, "hooks");
@@ -336,7 +336,7 @@ export function generateCursorVariant(srcDir, outDir, version) {
336
336
  manifest.version = version;
337
337
  const cursorHooks = buildCursorHooksJson(manifest.hooks ?? {});
338
338
  delete manifest.hooks;
339
- fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
339
+ fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
340
340
 
341
341
  // 2a. Emit hooks/hooks.json in Cursor's native shape when any hooks survive.
342
342
  if (cursorHooks) {
@@ -344,7 +344,7 @@ export function generateCursorVariant(srcDir, outDir, version) {
344
344
  fs.mkdirSync(cursorHooksDir, { recursive: true });
345
345
  fs.writeFileSync(
346
346
  path.join(cursorHooksDir, "hooks.json"),
347
- JSON.stringify(cursorHooks, null, 2) + "\n"
347
+ `${JSON.stringify(cursorHooks, null, 2)}\n`
348
348
  );
349
349
  }
350
350
 
@@ -123,7 +123,7 @@ const existingOutput = existsSync(outputPath)
123
123
  : "";
124
124
  const existingPublicCommits = [
125
125
  ...existingOutput.matchAll(
126
- /^\s+(?:"([0-9][a-f0-9]{39})"|([a-f][a-f0-9]{39})):\s+true,$/gmu
126
+ /^\s+(?:"(\d[a-f0-9]{39})"|([a-f][a-f0-9]{39})):\s+true,$/gmu
127
127
  ),
128
128
  ]
129
129
  .map(match => match[1] ?? match[2] ?? "")
@@ -88,9 +88,9 @@ export function resolveProfile(profiles, name) {
88
88
  if (!profile) {
89
89
  const known = Object.keys(profiles ?? {});
90
90
  throw new Error(
91
- `eas.json has no build profile called "${current}"` +
92
- (current === name ? "" : ` (reached by extending from "${name}")`) +
93
- `. It defines: ${known.length ? known.join(", ") : "none"}.`
91
+ `eas.json has no build profile called "${current}"${
92
+ current === name ? "" : ` (reached by extending from "${name}")`
93
+ }. It defines: ${known.length ? known.join(", ") : "none"}.`
94
94
  );
95
95
  }
96
96
  seen.add(current);
@@ -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
  /**