@codyswann/lisa 3.16.0 → 3.17.1

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 (119) hide show
  1. package/all/copy-overwrite/scripts/lisa-gates.mjs +1197 -0
  2. package/all/copy-overwrite/scripts/lisa-reconcile-policy.mjs +1188 -0
  3. package/all/copy-overwrite/scripts/lisa-run-gates.mjs +597 -0
  4. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  5. package/dist/core/lisa-owned-hash-ledger.js +25 -0
  6. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  7. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  8. package/dist/core/upstream-evidence-manifest.js +73 -8
  9. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  10. package/package.json +6 -2
  11. package/plugins/lisa/.claude-plugin/plugin.json +10 -1
  12. package/plugins/lisa/.codex-plugin/hooks.json +9 -0
  13. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  14. package/plugins/lisa/.codex-plugin/skills/lisa-doctor/SKILL.md +108 -2
  15. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/preflight-secrets.mjs +324 -0
  16. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/routing-floor.mjs +153 -0
  17. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/surfaces.mjs +55 -7
  18. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/validate-config.mjs +89 -17
  19. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +242 -0
  20. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/tool-floor.mjs +94 -0
  21. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +14 -32
  22. package/plugins/lisa/hooks/secrets-preflight.sh +72 -0
  23. package/plugins/lisa/skills/lisa-doctor/SKILL.md +108 -2
  24. package/plugins/lisa/skills/lisa-secrets-access/scripts/preflight-secrets.mjs +324 -0
  25. package/plugins/lisa/skills/lisa-secrets-access/scripts/routing-floor.mjs +153 -0
  26. package/plugins/lisa/skills/lisa-secrets-access/scripts/surfaces.mjs +55 -7
  27. package/plugins/lisa/skills/lisa-secrets-access/scripts/validate-config.mjs +89 -17
  28. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +242 -0
  29. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/tool-floor.mjs +94 -0
  30. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +14 -32
  31. package/plugins/lisa-agy/plugin.json +1 -1
  32. package/plugins/lisa-agy/skills/lisa-doctor/SKILL.md +108 -2
  33. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/preflight-secrets.mjs +324 -0
  34. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/routing-floor.mjs +153 -0
  35. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/surfaces.mjs +55 -7
  36. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/validate-config.mjs +89 -17
  37. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +242 -0
  38. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/tool-floor.mjs +94 -0
  39. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +14 -32
  40. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  42. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  43. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-copilot/.claude-plugin/plugin.json +10 -1
  46. package/plugins/lisa-copilot/hooks/secrets-preflight.sh +72 -0
  47. package/plugins/lisa-copilot/skills/lisa-doctor/SKILL.md +108 -2
  48. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/preflight-secrets.mjs +324 -0
  49. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/routing-floor.mjs +153 -0
  50. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/surfaces.mjs +55 -7
  51. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/validate-config.mjs +89 -17
  52. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +242 -0
  53. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/tool-floor.mjs +94 -0
  54. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +14 -32
  55. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-cursor/hooks/hooks.json +3 -0
  57. package/plugins/lisa-cursor/hooks/secrets-preflight.sh +72 -0
  58. package/plugins/lisa-cursor/skills/lisa-doctor/SKILL.md +108 -2
  59. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/preflight-secrets.mjs +324 -0
  60. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/routing-floor.mjs +153 -0
  61. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/surfaces.mjs +55 -7
  62. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/validate-config.mjs +89 -17
  63. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +242 -0
  64. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/tool-floor.mjs +94 -0
  65. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +14 -32
  66. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  68. package/plugins/lisa-expo-agy/plugin.json +1 -1
  69. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  73. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  74. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  78. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  79. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  82. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  83. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  84. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  86. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  87. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  88. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  89. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  90. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  91. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  92. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  93. package/plugins/lisa-rails-agy/plugin.json +1 -1
  94. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  95. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  96. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  97. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  98. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  99. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  100. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  101. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  102. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  103. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  104. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  105. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  106. package/plugins/src/base/.claude-plugin/plugin.json +9 -0
  107. package/plugins/src/base/hooks/secrets-preflight.sh +72 -0
  108. package/plugins/src/base/skills/lisa-doctor/SKILL.md +108 -2
  109. package/plugins/src/base/skills/lisa-secrets-access/scripts/preflight-secrets.mjs +324 -0
  110. package/plugins/src/base/skills/lisa-secrets-access/scripts/routing-floor.mjs +153 -0
  111. package/plugins/src/base/skills/lisa-secrets-access/scripts/surfaces.mjs +55 -7
  112. package/plugins/src/base/skills/lisa-secrets-access/scripts/validate-config.mjs +89 -17
  113. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +242 -0
  114. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/tool-floor.mjs +94 -0
  115. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +14 -32
  116. package/scripts/generate-lisa-owned-hash-ledger.mjs +10 -1
  117. package/scripts/lib/per-agent-hook-filter.mjs +18 -0
  118. package/typescript/copy-contents/.husky/pre-commit +130 -21
  119. package/typescript/copy-contents/.husky/pre-push +202 -22
@@ -43,9 +43,99 @@ else
43
43
  fi
44
44
  fi
45
45
 
46
+ # ---------------------------------------------------------------------------
47
+ # Gate-driven checks.
48
+ #
49
+ # A gate is a PROPERTY the project must hold; `.lisa.config.json` names the task
50
+ # that proves it. `gitleaks` is not a gate — credential leakage is, and gitleaks
51
+ # is one way to prove it. When a `gates` block exists, that registry decides
52
+ # what runs at the `commit` moment, not the hardcoded list further down.
53
+ #
54
+ # BACKWARD COMPATIBILITY — read before changing any of this. Most installed
55
+ # projects have NO `gates` block. The exit codes are the switch:
56
+ #
57
+ # exit 0 → the declared gates at this moment ran and none of them blocked.
58
+ # exit 1 → a required gate FAILED. Block the commit.
59
+ # exit 78 → there is no `gates` block at all. Nothing is covered.
60
+ # other → the runner itself could not run (config unreadable, crash).
61
+ # Nothing was proved. Say so loudly and cover nothing.
62
+ #
63
+ # ONE STEP AT A TIME, NOT ALL OR NOTHING. Exit 0 says the declared gates
64
+ # passed. It does not say the registry covers every property the steps below
65
+ # prove — so it cannot be the thing that skips them wholesale. A block that
66
+ # declares `code-style` and says nothing about `credential-leakage` would then
67
+ # delete the secret scan by omission, which is the failure this whole subsystem
68
+ # exists to prevent: a control returning success for an input it never
69
+ # examined.
70
+ #
71
+ # So the runner writes the properties it covers into $LISA_GATE_COVERAGE, one
72
+ # gate id per line, and every built-in step below stands down only against its
73
+ # OWN property. A half-declared block therefore loses nothing: the registry
74
+ # runs what it declares and the built-ins run the rest.
75
+ #
76
+ # Fail-safe by construction: no coverage file, an empty one, a runner that
77
+ # could not run, or no exact match, all mean the built-in step runs.
78
+ # ---------------------------------------------------------------------------
79
+ GATE_RUNNER="scripts/lisa-run-gates.mjs"
80
+ if [ ! -f "$GATE_RUNNER" ]; then
81
+ GATE_RUNNER="all/copy-overwrite/scripts/lisa-run-gates.mjs"
82
+ fi
83
+ LISA_GATE_COVERAGE=""
84
+ if [ -f "$GATE_RUNNER" ] && command -v node >/dev/null 2>&1; then
85
+ LISA_GATE_COVERAGE="$(mktemp "${TMPDIR:-/tmp}/lisa-gate-coverage.XXXXXXXX")" || LISA_GATE_COVERAGE=""
86
+ trap 'rm -f "$LISA_GATE_COVERAGE"' EXIT
87
+ if [ -n "$LISA_GATE_COVERAGE" ]; then
88
+ node "$GATE_RUNNER" --moment=commit --coverage="$LISA_GATE_COVERAGE"
89
+ else
90
+ # No temp file, so nothing can be covered and every built-in step runs.
91
+ node "$GATE_RUNNER" --moment=commit
92
+ fi
93
+ GATE_STATUS=$?
94
+ if [ $GATE_STATUS -eq 1 ]; then
95
+ echo ""
96
+ echo "❌ Commit blocked: a required gate failed. See the FAILED line above."
97
+ echo ""
98
+ exit 1
99
+ fi
100
+ if [ $GATE_STATUS -ne 0 ]; then
101
+ # Anything other than a clean run: discard whatever is in the file. A
102
+ # runner that crashed midway may have written coverage before it failed,
103
+ # and coverage from a run that did not finish is not evidence.
104
+ if [ -n "$LISA_GATE_COVERAGE" ]; then
105
+ : > "$LISA_GATE_COVERAGE"
106
+ fi
107
+ if [ $GATE_STATUS -ne 78 ]; then
108
+ echo ""
109
+ echo "⚠️ The gate runner could not run (exit $GATE_STATUS)."
110
+ echo " Nothing was proved by the gate registry — this is NOT a pass."
111
+ echo " Running the built-in checks instead."
112
+ echo ""
113
+ fi
114
+ fi
115
+ else
116
+ echo "ℹ️ Gate runner unavailable; running the built-in checks."
117
+ fi
118
+
119
+ # Whether a declared gate covers every property named, so the built-in step
120
+ # that proves them can stand down. Exact whole-line matching: a gate id that is
121
+ # a prefix of another must never satisfy it.
122
+ lisa_gate_covers() {
123
+ [ -n "$LISA_GATE_COVERAGE" ] || return 1
124
+ [ -s "$LISA_GATE_COVERAGE" ] || return 1
125
+ for _lisa_gate in "$@"; do
126
+ grep -Fqx -- "$_lisa_gate" "$LISA_GATE_COVERAGE" || return 1
127
+ done
128
+ return 0
129
+ }
130
+
131
+ # BEGIN: built-in checks — the pre-registry path, kept step for step so a
132
+ # project without a `gates` block behaves exactly as it did before.
133
+
46
134
  # Check for Gitleaks and run secret detection
47
135
  echo "🔐 Checking for secrets with Gitleaks..."
48
- if command -v gitleaks >/dev/null 2>&1; then
136
+ if lisa_gate_covers credential-leakage; then
137
+ echo "ℹ️ Covered by the credential-leakage gate; the built-in scan stands down."
138
+ elif command -v gitleaks >/dev/null 2>&1; then
49
139
  # Run gitleaks on staged files. Lisa keeps the host-owned allowlist in
50
140
  # .gitleaksignore and shared managed entries in .gitleaksignore.local.
51
141
  GITLEAKS_IGNORE_ARGS=""
@@ -109,10 +199,49 @@ fi
109
199
  # exit 1
110
200
  # fi
111
201
 
202
+ # Run lint-staged for incremental lint and format checks. It proves three
203
+ # properties in one pass — oxlint/eslint, prettier, and `ast-grep scan` — so it
204
+ # stands down only when a gate is declared for every one of them.
205
+ if lisa_gate_covers code-style format-conformance structural-rules; then
206
+ echo "ℹ️ Lint, format and structural rules are covered by gates; lint-staged stands down."
207
+ else
208
+ echo "🚀 Running lint-staged..."
209
+ $EXECUTOR lint-staged --config .lintstagedrc.json
210
+ LINT_STAGED_STATUS=$?
211
+ if [ $LINT_STAGED_STATUS -ne 0 ]; then
212
+ exit $LINT_STAGED_STATUS
213
+ fi
214
+ fi
215
+
216
+ # Generated artifacts must vouch for the bytes committed beside them. Runs AFTER
217
+ # lint-staged on purpose: reformatting a staged template moves its bytes, so a
218
+ # check placed above would sign off on bytes that no longer exist. Absent in host
219
+ # projects, which have the hook but none of Lisa's generators.
220
+ if lisa_gate_covers artifact-freshness; then
221
+ echo "ℹ️ Covered by the artifact-freshness gate; the built-in check stands down."
222
+ elif [ -f "scripts/check-derived-artifacts.mjs" ] && command -v node >/dev/null 2>&1; then
223
+ echo "🧾 Checking generated artifacts..."
224
+ node scripts/check-derived-artifacts.mjs --staged
225
+ if [ $? -ne 0 ]; then
226
+ exit 1
227
+ fi
228
+ fi
229
+
230
+ # END: built-in checks
231
+
112
232
  # Threshold ratchet: quality thresholds may tighten, never weaken. The staged
113
233
  # change is compared against HEAD by the same deterministic comparator the
114
234
  # agent-time hooks and CI run; human-approved exceptions live in
115
235
  # .lisa.config.json under thresholdRatchet.allow.
236
+ #
237
+ # OUTSIDE the gate-registry handover, deliberately. Every other built-in step
238
+ # here has a registry gate that can replace it, so the runner refuses to take
239
+ # the moment until the block declares one — see BUILTIN_FLOOR and
240
+ # CONDITIONAL_FLOOR in lisa-run-gates.mjs. This one has no such counterpart:
241
+ # `threshold-monotonicity` is push-onward and compares against HEAD^, while
242
+ # this compares the STAGED change. Leaving it inside the handover would let a
243
+ # complete gates block delete it at commit with nothing put in its place, which
244
+ # is precisely the deletion-by-omission the floor exists to prevent.
116
245
  if [ -f "scripts/check-threshold-ratchet.mjs" ] && command -v node >/dev/null 2>&1; then
117
246
  echo "📐 Checking threshold ratchet..."
118
247
  node scripts/check-threshold-ratchet.mjs --staged
@@ -125,24 +254,4 @@ if [ -f "scripts/check-threshold-ratchet.mjs" ] && command -v node >/dev/null 2>
125
254
  exit 1
126
255
  fi
127
256
  fi
128
-
129
- # Run lint-staged for incremental lint and format checks
130
- echo "🚀 Running lint-staged..."
131
- $EXECUTOR lint-staged --config .lintstagedrc.json
132
- LINT_STAGED_STATUS=$?
133
- if [ $LINT_STAGED_STATUS -ne 0 ]; then
134
- exit $LINT_STAGED_STATUS
135
- fi
136
-
137
- # Generated artifacts must vouch for the bytes committed beside them. Runs AFTER
138
- # lint-staged on purpose: reformatting a staged template moves its bytes, so a
139
- # check placed above would sign off on bytes that no longer exist. Absent in host
140
- # projects, which have the hook but none of Lisa's generators.
141
- if [ -f "scripts/check-derived-artifacts.mjs" ] && command -v node >/dev/null 2>&1; then
142
- echo "🧾 Checking generated artifacts..."
143
- node scripts/check-derived-artifacts.mjs --staged
144
- if [ $? -ne 0 ]; then
145
- exit 1
146
- fi
147
- fi
148
257
  # END: AI GUARDRAILS
@@ -38,21 +38,100 @@ fi
38
38
 
39
39
  echo "📦 Using package manager: $PACKAGE_MANAGER"
40
40
 
41
+ # ---------------------------------------------------------------------------
42
+ # Gate-driven checks. See the matching block in .husky/pre-commit for the full
43
+ # contract; the exit-code routing is identical, at the `push` moment.
44
+ #
45
+ # exit 0 → the declared gates at this moment ran and none of them blocked.
46
+ # exit 1 → a required gate FAILED. Block the push.
47
+ # exit 78 → there is no `gates` block at all. Nothing is covered.
48
+ # other → the runner itself could not run. Nothing was proved. Say so
49
+ # loudly and cover nothing.
50
+ #
51
+ # ONE STEP AT A TIME, NOT ALL OR NOTHING. Exit 0 says the declared gates
52
+ # passed, not that the registry covers every property the steps below prove, so
53
+ # it cannot be the thing that skips them wholesale. The runner writes the
54
+ # properties it covers into $LISA_GATE_COVERAGE and each step stands down only
55
+ # against its own. Fail-safe: no file, an empty file, a runner that could not
56
+ # run, or no exact match all mean the built-in step runs.
57
+ #
58
+ # The project-specific extension slots at the bottom of this file run either
59
+ # way — they are project behaviour, not registry gates.
60
+ # ---------------------------------------------------------------------------
61
+ GATE_RUNNER="scripts/lisa-run-gates.mjs"
62
+ if [ ! -f "$GATE_RUNNER" ]; then
63
+ GATE_RUNNER="all/copy-overwrite/scripts/lisa-run-gates.mjs"
64
+ fi
65
+ LISA_GATE_COVERAGE=""
66
+ if [ -f "$GATE_RUNNER" ] && command -v node >/dev/null 2>&1; then
67
+ LISA_GATE_COVERAGE="$(mktemp "${TMPDIR:-/tmp}/lisa-gate-coverage.XXXXXXXX")" || LISA_GATE_COVERAGE=""
68
+ trap 'rm -f "$LISA_GATE_COVERAGE"' EXIT
69
+ if [ -n "$LISA_GATE_COVERAGE" ]; then
70
+ node "$GATE_RUNNER" --moment=push --coverage="$LISA_GATE_COVERAGE"
71
+ else
72
+ # No temp file, so nothing can be covered and every built-in step runs.
73
+ node "$GATE_RUNNER" --moment=push
74
+ fi
75
+ GATE_STATUS=$?
76
+ if [ $GATE_STATUS -eq 1 ]; then
77
+ echo ""
78
+ echo "❌ Push blocked: a required gate failed. See the FAILED line above."
79
+ echo ""
80
+ exit 1
81
+ fi
82
+ if [ $GATE_STATUS -ne 0 ]; then
83
+ # Coverage from a run that did not finish is not evidence.
84
+ if [ -n "$LISA_GATE_COVERAGE" ]; then
85
+ : > "$LISA_GATE_COVERAGE"
86
+ fi
87
+ if [ $GATE_STATUS -ne 78 ]; then
88
+ echo ""
89
+ echo "⚠️ The gate runner could not run (exit $GATE_STATUS)."
90
+ echo " Nothing was proved by the gate registry — this is NOT a pass."
91
+ echo " Running the built-in checks instead."
92
+ echo ""
93
+ fi
94
+ fi
95
+ else
96
+ echo "ℹ️ Gate runner unavailable; running the built-in checks."
97
+ fi
98
+
99
+ # Whether a declared gate covers every property named, so the built-in step
100
+ # that proves them can stand down. Exact whole-line matching: a gate id that is
101
+ # a prefix of another must never satisfy it.
102
+ lisa_gate_covers() {
103
+ [ -n "$LISA_GATE_COVERAGE" ] || return 1
104
+ [ -s "$LISA_GATE_COVERAGE" ] || return 1
105
+ for _lisa_gate in "$@"; do
106
+ grep -Fqx -- "$_lisa_gate" "$LISA_GATE_COVERAGE" || return 1
107
+ done
108
+ return 0
109
+ }
110
+
111
+ # BEGIN: built-in checks — the pre-registry path, kept step for step so a
112
+ # project without a `gates` block behaves exactly as it did before.
113
+
41
114
  # Run the whole-project type check once before code leaves the machine. It is
42
115
  # intentionally not a pre-commit gate because TypeScript cannot check only the
43
116
  # staged files and rebuilding the full program makes small commits too slow.
44
- echo "🔍 Running type check..."
45
- $RUNNER typecheck
46
- if [ $? -ne 0 ]; then
47
- echo " Type check failed. Please fix TypeScript errors before pushing."
48
- exit 1
117
+ if lisa_gate_covers type-correctness; then
118
+ echo "ℹ️ Covered by the type-correctness gate; the built-in type check stands down."
119
+ else
120
+ echo "🔍 Running type check..."
121
+ $RUNNER typecheck
122
+ if [ $? -ne 0 ]; then
123
+ echo "❌ Type check failed. Please fix TypeScript errors before pushing."
124
+ exit 1
125
+ fi
49
126
  fi
50
127
 
51
128
  # Run security audit
52
129
  echo "🔒 Running security audit..."
53
130
 
54
131
  # jq is required for all audit paths (JSON config reading, npm/yarn filtering)
55
- if ! command -v jq >/dev/null 2>&1; then
132
+ if lisa_gate_covers dependency-vulnerability; then
133
+ echo "ℹ️ Covered by the dependency-vulnerability gate; the built-in audit stands down."
134
+ elif ! command -v jq >/dev/null 2>&1; then
56
135
  echo ""
57
136
  echo "⚠️ WARNING: jq is not installed - required for security audit"
58
137
  echo ""
@@ -179,7 +258,79 @@ else
179
258
  # devDependencies too and the gate fails on dev-only CVEs that never ship —
180
259
  # the SE-5221 false positive. bun honours `--production` even though
181
260
  # `bun audit --help` omits it from the flag list.
182
- AUDIT_JSON=$(bun audit --production --json 2>/dev/null || true)
261
+ # Normalize bun's transport before parsing, or this gate FAILS OPEN.
262
+ #
263
+ # Measured: bun can return gzip-compressed JSON on stdout while reporting a
264
+ # parse error on stderr. The previous single-line form captured that binary
265
+ # into AUDIT_JSON, the jq filter below then failed, UNFIXED_HIGH came back
266
+ # empty, UNFIXED_COUNT resolved to empty, `-gt 0` was false, and the hook
267
+ # printed "No high or critical vulnerabilities found" and allowed the push.
268
+ # A security gate reporting success having parsed nothing.
269
+ #
270
+ # Lisa's own .husky/pre-push was patched for this; this shipped copy was
271
+ # not, so every host project on this template carried the fail-open. Keep
272
+ # the two in step — two copies of one check cannot be kept aligned by
273
+ # intention, which is exactly how this drifted.
274
+ # Every path out of this block either parses advisory data or blocks the
275
+ # push. The gzip branch below handles the transport shape that was measured;
276
+ # it cannot be the only one handled, because "some other unparseable shape"
277
+ # is precisely the case nobody has seen yet. An audit whose payload was
278
+ # never parsed has audited nothing, and printing "no vulnerabilities found"
279
+ # from that state is the fail-open this whole block exists to remove.
280
+ #
281
+ # This hook does not run under `set -e`, so an unguarded mktemp would leave
282
+ # the path empty, the redirect would fail, and `[ ! -s "" ]` would be true —
283
+ # a clean verdict from a file that was never written. Same rule the
284
+ # pre-commit gitleaks scan states: a degraded security check is worse than a
285
+ # blocked push.
286
+ if ! AUDIT_OUTPUT="$(mktemp "${TMPDIR:-/tmp}/bun-audit.XXXXXXXX")" || [ -z "$AUDIT_OUTPUT" ]; then
287
+ echo "❌ Push blocked: could not create a temporary file for the security audit." >&2
288
+ echo " Check that TMPDIR (${TMPDIR:-/tmp}) exists and is writable." >&2
289
+ exit 1
290
+ fi
291
+ if ! AUDIT_STDERR="$(mktemp "${TMPDIR:-/tmp}/bun-audit-err.XXXXXXXX")" || [ -z "$AUDIT_STDERR" ]; then
292
+ rm -f "$AUDIT_OUTPUT"
293
+ echo "❌ Push blocked: could not create a temporary file for the security audit." >&2
294
+ echo " Check that TMPDIR (${TMPDIR:-/tmp}) exists and is writable." >&2
295
+ exit 1
296
+ fi
297
+ # bun's own diagnostics are kept rather than discarded. "Lockfile not found"
298
+ # is the difference between a broken audit and a clean one, and throwing it
299
+ # away is how a failed command comes to read as a measured zero.
300
+ bun audit --production --json > "$AUDIT_OUTPUT" 2>"$AUDIT_STDERR" || true
301
+ if [ ! -s "$AUDIT_OUTPUT" ]; then
302
+ echo "❌ Push blocked: bun audit produced no output, so nothing was audited." >&2
303
+ echo " This is NOT a clean result — a clean audit prints JSON ({} when" >&2
304
+ echo " there are no advisories). Empty output means the audit did not run." >&2
305
+ if [ -s "$AUDIT_STDERR" ]; then
306
+ echo " bun said:" >&2
307
+ sed 's/^/ /' "$AUDIT_STDERR" >&2
308
+ fi
309
+ rm -f "$AUDIT_OUTPUT" "$AUDIT_STDERR"
310
+ exit 1
311
+ elif jq empty "$AUDIT_OUTPUT" >/dev/null 2>&1; then
312
+ AUDIT_JSON=$(cat "$AUDIT_OUTPUT")
313
+ else
314
+ DECOMPRESSED_AUDIT_OUTPUT="$(mktemp "${TMPDIR:-/tmp}/bun-audit-gz.XXXXXXXX")"
315
+ if [ -n "$DECOMPRESSED_AUDIT_OUTPUT" ]; then
316
+ gzip -dc "$AUDIT_OUTPUT" > "$DECOMPRESSED_AUDIT_OUTPUT" 2>/dev/null || true
317
+ fi
318
+ # `-s` before `jq empty`: an EMPTY file passes `jq empty` (no input is
319
+ # not invalid input), so a gunzip that failed and wrote nothing would be
320
+ # accepted as valid JSON and set AUDIT_JSON to the empty string.
321
+ if [ -s "$DECOMPRESSED_AUDIT_OUTPUT" ] && jq empty "$DECOMPRESSED_AUDIT_OUTPUT" >/dev/null 2>&1; then
322
+ AUDIT_JSON=$(cat "$DECOMPRESSED_AUDIT_OUTPUT")
323
+ rm -f "$DECOMPRESSED_AUDIT_OUTPUT"
324
+ else
325
+ rm -f "$DECOMPRESSED_AUDIT_OUTPUT" "$AUDIT_OUTPUT" "$AUDIT_STDERR"
326
+ echo "❌ Push blocked: bun audit output is neither JSON nor gzipped JSON," >&2
327
+ echo " so no advisory data was parsed and nothing was audited." >&2
328
+ echo " This is NOT a clean result. Re-run 'bun audit --production --json'" >&2
329
+ echo " to see what it emitted." >&2
330
+ exit 1
331
+ fi
332
+ fi
333
+ rm -f "$AUDIT_OUTPUT" "$AUDIT_STDERR"
183
334
  UNFIXED_HIGH=$(echo "$AUDIT_JSON" | jq -r --arg ids "$AUDIT_EXCLUSIONS" '
184
335
  ($ids | split(" ") | map(select(length > 0))) as $ex
185
336
  | [ .[]? | .[]?
@@ -188,7 +339,18 @@ else
188
339
  | select(($ex | index($g)) | not)
189
340
  | $g ]
190
341
  | unique')
191
- UNFIXED_COUNT=$(echo "$UNFIXED_HIGH" | jq -r 'length' 2>/dev/null || echo 0)
342
+ # A count that could not be computed is not a count of zero. The old `|| echo
343
+ # 0` turned a jq filter that errored — an audit whose JSON shape this hook
344
+ # does not understand — into "no vulnerabilities found", which is the same
345
+ # fail-open as the transport hole above, one step further down.
346
+ UNFIXED_COUNT=$(echo "$UNFIXED_HIGH" | jq -r 'length' 2>/dev/null || echo "")
347
+ if [ -z "$UNFIXED_COUNT" ]; then
348
+ echo "❌ Push blocked: the audit payload parsed, but the advisory filter" >&2
349
+ echo " produced no count, so no advisory was evaluated." >&2
350
+ echo " This is NOT a clean result. Re-run 'bun audit --production --json'" >&2
351
+ echo " to see the shape it returned." >&2
352
+ exit 1
353
+ fi
192
354
  if [ "$UNFIXED_COUNT" -gt 0 ]; then
193
355
  echo "⚠️ Security audit failed. Unresolved high/critical advisories in production dependencies:"
194
356
  echo "$UNFIXED_HIGH" | jq -r '.[]'
@@ -200,7 +362,9 @@ else
200
362
  fi
201
363
 
202
364
  # Run slow lint rules - only if script exists
203
- if jq -e '.scripts["lint:slow"]' package.json >/dev/null 2>&1; then
365
+ if lisa_gate_covers code-style-slow; then
366
+ echo "ℹ️ Covered by the code-style-slow gate; the built-in slow lint stands down."
367
+ elif jq -e '.scripts["lint:slow"]' package.json >/dev/null 2>&1; then
204
368
  echo "🐢 Running slow lint rules..."
205
369
  $RUNNER lint:slow
206
370
  if [ $? -ne 0 ]; then
@@ -223,7 +387,9 @@ else
223
387
  KNIP_SCRIPT=""
224
388
  fi
225
389
 
226
- if [ -n "$KNIP_SCRIPT" ]; then
390
+ if lisa_gate_covers dead-code; then
391
+ echo "ℹ️ Covered by the dead-code gate; the built-in knip run stands down."
392
+ elif [ -n "$KNIP_SCRIPT" ]; then
227
393
  echo "🗑️ Running dead code detection (knip)..."
228
394
  $RUNNER "$KNIP_SCRIPT"
229
395
  if [ $? -ne 0 ]; then
@@ -237,20 +403,30 @@ else
237
403
  echo "ℹ️ Skipping dead code detection (knip not configured)"
238
404
  fi
239
405
 
240
- # Run unit tests with coverage
241
- echo "🧪 Running unit tests with coverage..."
242
- $RUNNER test:cov
243
- if [ $? -ne 0 ]; then
244
- echo " Unit tests or coverage thresholds failed. Please fix before pushing."
245
- exit 1
406
+ # Run unit tests with coverage. `test:cov` proves two properties in one run —
407
+ # the suite passes AND the coverage thresholds hold — so it stands down only
408
+ # when both have a gate declared.
409
+ if lisa_gate_covers test-correctness coverage-adequacy; then
410
+ echo "ℹ️ Covered by the test-correctness and coverage-adequacy gates; the built-in run stands down."
411
+ else
412
+ echo "🧪 Running unit tests with coverage..."
413
+ $RUNNER test:cov
414
+ if [ $? -ne 0 ]; then
415
+ echo "❌ Unit tests or coverage thresholds failed. Please fix before pushing."
416
+ exit 1
417
+ fi
246
418
  fi
247
419
 
248
420
  # Run integration tests
249
- echo "🧪 Running integration tests..."
250
- $RUNNER test:integration
251
- if [ $? -ne 0 ]; then
252
- echo " Integration tests failed. Please fix failing tests before pushing."
253
- exit 1
421
+ if lisa_gate_covers test-integration; then
422
+ echo "ℹ️ Covered by the test-integration gate; the built-in run stands down."
423
+ else
424
+ echo "🧪 Running integration tests..."
425
+ $RUNNER test:integration
426
+ if [ $? -ne 0 ]; then
427
+ echo "❌ Integration tests failed. Please fix failing tests before pushing."
428
+ exit 1
429
+ fi
254
430
  fi
255
431
 
256
432
  # Run mutation-testing gate - only if script exists.
@@ -258,7 +434,9 @@ fi
258
434
  # (mutation.gate.json: "enabled": false), so this adds no cost to projects that
259
435
  # have not opted in. When enabled, it runs Stryker diff-only on changed files
260
436
  # and fails the push when the mutation score is below thresholds.break.
261
- if jq -e '.scripts["test:mutation"]' package.json >/dev/null 2>&1; then
437
+ if lisa_gate_covers test-meaningfulness; then
438
+ echo "ℹ️ Covered by the test-meaningfulness gate; the built-in run stands down."
439
+ elif jq -e '.scripts["test:mutation"]' package.json >/dev/null 2>&1; then
262
440
  echo "🧬 Running mutation-testing gate..."
263
441
  $RUNNER test:mutation
264
442
  if [ $? -ne 0 ]; then
@@ -337,6 +515,8 @@ fi
337
515
  # fi
338
516
  # fi
339
517
 
518
+ # END: built-in checks
519
+
340
520
  # Managed verification (UAT) extension slot (see .husky/pre-push.verify).
341
521
  # Opt-in project types (e.g. phaser) ship this snippet via Lisa; it mirrors the
342
522
  # CI verification_coverage gate locally. It is absent for types that have not