@codyswann/lisa 3.11.4 → 3.11.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 (70) hide show
  1. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  2. package/dist/core/upstream-evidence-manifest.js +9 -8
  3. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  4. package/package.json +1 -1
  5. package/phaser/copy-overwrite/ast-grep/rule-tests/.gitkeep +3 -3
  6. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  7. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  8. package/plugins/lisa/.codex-plugin/skills/lisa-parity-safety-net-rules/SKILL.md +18 -3
  9. package/plugins/lisa/hooks/threshold-ratchet.mjs +173 -9
  10. package/plugins/lisa/skills/lisa-parity-safety-net-rules/SKILL.md +18 -3
  11. package/plugins/lisa-agy/plugin.json +1 -1
  12. package/plugins/lisa-agy/skills/lisa-parity-safety-net-rules/SKILL.md +18 -3
  13. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  14. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  15. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  16. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  17. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  18. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  19. package/plugins/lisa-copilot/hooks/threshold-ratchet.mjs +173 -9
  20. package/plugins/lisa-copilot/skills/lisa-parity-safety-net-rules/SKILL.md +18 -3
  21. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-cursor/hooks/threshold-ratchet.mjs +173 -9
  23. package/plugins/lisa-cursor/skills/lisa-parity-safety-net-rules/SKILL.md +18 -3
  24. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  26. package/plugins/lisa-expo-agy/plugin.json +1 -1
  27. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  31. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  32. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  36. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  37. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  41. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  42. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  46. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  47. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  51. package/plugins/lisa-rails-agy/plugin.json +1 -1
  52. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  56. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  57. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  61. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  62. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  64. package/plugins/src/base/hooks/threshold-ratchet.mjs +173 -9
  65. package/plugins/src/base/skills/lisa-parity-safety-net-rules/SKILL.md +18 -3
  66. package/rails/copy-overwrite/ast-grep/rule-tests/.gitkeep +3 -3
  67. package/rails/copy-overwrite/scripts/check-threshold-ratchet.mjs +173 -9
  68. package/scripts/check-required-check-promotions.mjs +93 -0
  69. package/typescript/copy-overwrite/ast-grep/rule-tests/.gitkeep +3 -3
  70. package/typescript/copy-overwrite/scripts/check-threshold-ratchet.mjs +173 -9
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "3.11.4",
3
+ "version": "3.11.7",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "3.11.4",
3
+ "version": "3.11.7",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "3.11.4",
3
+ "version": "3.11.7",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "3.11.4",
3
+ "version": "3.11.7",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "3.11.4",
3
+ "version": "3.11.7",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "3.11.4",
3
+ "version": "3.11.7",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "3.11.4",
3
+ "version": "3.11.7",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "3.11.4",
3
+ "version": "3.11.7",
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.11.4",
3
+ "version": "3.11.7",
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.11.4",
3
+ "version": "3.11.7",
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.11.4",
3
+ "version": "3.11.7",
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.11.4",
3
+ "version": "3.11.7",
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.11.4",
3
+ "version": "3.11.7",
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.11.4",
3
+ "version": "3.11.7",
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.11.4",
3
+ "version": "3.11.7",
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.11.4",
3
+ "version": "3.11.7",
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.11.4",
3
+ "version": "3.11.7",
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.11.4",
3
+ "version": "3.11.7",
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.11.4",
3
+ "version": "3.11.7",
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.11.4",
3
+ "version": "3.11.7",
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.11.4",
3
+ "version": "3.11.7",
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.11.4",
3
+ "version": "3.11.7",
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.11.4",
3
+ "version": "3.11.7",
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.11.4",
3
+ "version": "3.11.7",
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.11.4",
3
+ "version": "3.11.7",
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.11.4",
3
+ "version": "3.11.7",
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.11.4",
3
+ "version": "3.11.7",
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"
@@ -24,6 +24,13 @@
24
24
  * itself an exception in the same change that weakens a gate. `key: "*"`
25
25
  * allows every key in the file.
26
26
  *
27
+ * One exception, and only one: a PROMOTION between deploy-chain branches
28
+ * (`--base` + `--head`, both named in `deploy.branches`, head upstream of
29
+ * base, head fully containing base). There the allow list is read from the
30
+ * head, because the change under review is the baseline plus history that has
31
+ * already passed this same gate. See `isPromotion` for why that is the only
32
+ * discriminator that holds.
33
+ *
27
34
  * Extraction lives in threshold-ratchet-families.mjs; comparison rules in
28
35
  * threshold-ratchet-compare.mjs. Zero dependencies.
29
36
  */
@@ -131,6 +138,148 @@ function resolvePlan(mode, root, baseRef, onlyFiles) {
131
138
  };
132
139
  }
133
140
 
141
+ /**
142
+ * Strip a remote prefix so `origin/staging` and `staging` compare equal to a
143
+ * branch name declared in `.lisa.config.json`.
144
+ * @param {string} ref Git ref, possibly remote-qualified
145
+ * @returns {string} Bare branch name
146
+ */
147
+ function bareBranch(ref) {
148
+ return ref.replace(/^refs\/heads\//u, "").replace(/^origin\//u, "");
149
+ }
150
+
151
+ /**
152
+ * The deploy chain, earliest environment first, from `deploy.branches`.
153
+ *
154
+ * Declaration order IS the chain order — that is already how Lisa reads it
155
+ * (dev → staging → production), and it is what makes "upstream of" decidable.
156
+ * @param {unknown} config Parsed `.lisa.config.json`
157
+ * @returns {string[]} Branch names in chain order
158
+ */
159
+ function deployChain(config) {
160
+ const branches = config?.deploy?.branches;
161
+ if (!branches || typeof branches !== "object") return [];
162
+ return Object.values(branches).filter(b => typeof b === "string" && b !== "");
163
+ }
164
+
165
+ /**
166
+ * Whether this is a promotion of one deploy-chain branch into the next.
167
+ *
168
+ * A promotion carries approved history into a branch that is behind, so the
169
+ * exemptions it brings with it are not new — each one already faced this gate
170
+ * on the upstream branch. Reading the allow list from the baseline there
171
+ * reports every one of them as newly added, and the documented remedy is
172
+ * circular: recording them means adding `thresholdRatchet.allow` entries,
173
+ * which is itself the Tier 3 change being blocked. That deadlocked the whole
174
+ * promotion lane (#2531).
175
+ *
176
+ * The discriminator is branch IDENTITY, not ancestry. "The head contains the
177
+ * base" is true of any ordinary topic branch that is up to date with its base
178
+ * — and a repository with a strict up-to-date branch-protection rule REQUIRES
179
+ * that of every PR — so ancestry alone would hand self-approval to exactly the
180
+ * changes Tier 3 exists to stop. Being a deploy-chain branch cannot be
181
+ * arranged by a topic branch: those branches are protected, so everything on
182
+ * them arrived through a reviewed PR that passed this same ratchet.
183
+ *
184
+ * Ancestry is still required, as a second condition rather than the only one:
185
+ * a head that has diverged from its base is not "the baseline plus approved
186
+ * history", and the strict reading should stand.
187
+ *
188
+ * The chain is read from the BASELINE config, so a change cannot declare
189
+ * itself a promotion by adding `deploy.branches` entries in the same commit.
190
+ * @param {string} root Repo root
191
+ * @param {unknown} baselineConfig `.lisa.config.json` at the baseline
192
+ * @param {string | undefined} baseRef Ref being merged into
193
+ * @param {string | undefined} headRef Ref being merged from
194
+ * @returns {boolean} True when the allow list may be read from the head
195
+ */
196
+ function isPromotion(root, baselineConfig, baseRef, headRef) {
197
+ if (!baseRef || !headRef) return false;
198
+ const chain = deployChain(baselineConfig);
199
+ const basePosition = chain.indexOf(bareBranch(baseRef));
200
+ const headPosition = chain.indexOf(bareBranch(headRef));
201
+ if (basePosition === -1 || headPosition === -1) return false;
202
+ if (headPosition >= basePosition) return false;
203
+ // Empty string on success, null when git exits non-zero or the ref is bogus.
204
+ if (git(["merge-base", "--is-ancestor", baseRef, headRef], root) !== null) {
205
+ return true;
206
+ }
207
+ // Both ARE deploy-chain branches, so this is a promotion that has diverged —
208
+ // typically a hotfix that landed on the base and was never synced down. The
209
+ // strict reading is correct here, but silence would leave an operator
210
+ // guessing why this promotion behaves differently from the last one.
211
+ process.stderr.write(
212
+ `threshold-ratchet: ${bareBranch(headRef)} does not contain ` +
213
+ `${bareBranch(baseRef)}, so this promotion is not the baseline plus ` +
214
+ `approved history and the allow list is read from the baseline. Sync ` +
215
+ `${bareBranch(baseRef)} down into ${bareBranch(headRef)} first.\n`
216
+ );
217
+ return false;
218
+ }
219
+
220
+ /**
221
+ * Resolve the allow list and say where it came from.
222
+ * @param {string} root Repo root
223
+ * @param {string} baselineRef Ref the comparison baselines against
224
+ * @param {"hook"|"staged"|"base"} mode Comparison mode
225
+ * @param {string | undefined} baseRef Base ref (base mode only)
226
+ * @param {string | undefined} headRef Head ref (base mode only)
227
+ * @returns {{ entries: object[], promotion: boolean, note: string | null }}
228
+ * Entries, whether this is a promotion, and an audit line to print when the
229
+ * entries came from anywhere but the baseline
230
+ */
231
+ function resolveAllowList(root, baselineRef, mode, baseRef, headRef) {
232
+ const baselineConfig = parseJson(
233
+ git(["show", `${baselineRef}:.lisa.config.json`], root)
234
+ );
235
+ if (mode !== "base" || !isPromotion(root, baselineConfig, baseRef, headRef)) {
236
+ return {
237
+ entries: extractAllowEntries(baselineConfig),
238
+ promotion: false,
239
+ note: null,
240
+ };
241
+ }
242
+ return {
243
+ entries: extractAllowEntries(
244
+ parseJson(git(["show", "HEAD:.lisa.config.json"], root))
245
+ ),
246
+ promotion: true,
247
+ note:
248
+ `threshold-ratchet: promotion ${bareBranch(headRef)} → ` +
249
+ `${bareBranch(baseRef)}; both are deploy-chain branches declared at ` +
250
+ `${baselineRef} and the head fully contains the base, so the allow list ` +
251
+ `is read from the head. Exemptions below were approved upstream, not by ` +
252
+ `this change.`,
253
+ };
254
+ }
255
+
256
+ /**
257
+ * Split off the allow-entry additions a promotion is carrying forward.
258
+ *
259
+ * `applyAllowList` never drops an `allow-added` finding, from either side —
260
+ * an exception must not approve its own creation. That is right for an
261
+ * ordinary PR and wrong for a promotion, where the entry is not being created:
262
+ * it already exists on the upstream branch, where its creation faced this same
263
+ * unconditional block and needed a human to clear it. Without this the fix
264
+ * would be cosmetic — a promotion carrying an approved exemption also carries
265
+ * the `.lisa.config.json` diff that records it, so it would still be blocked
266
+ * by the finding for the record of its own approval.
267
+ *
268
+ * Only `allow-added` is carried. An actual threshold weakening in the same
269
+ * promotion still has to be covered by an allow entry.
270
+ * @param {Array<{ type: string }>} findings All findings from the change
271
+ * @param {boolean} promotion Whether the change is a recognised promotion
272
+ * @returns {{ carried: object[], rest: object[] }} Findings excused as already
273
+ * approved upstream, and findings still subject to the allow list
274
+ */
275
+ function partitionCarriedEntries(findings, promotion) {
276
+ if (!promotion) return { carried: [], rest: findings };
277
+ return {
278
+ carried: findings.filter(f => f.type === "allow-added"),
279
+ rest: findings.filter(f => f.type !== "allow-added"),
280
+ };
281
+ }
282
+
134
283
  /**
135
284
  * Decide the exit code when the ratchet cannot determine what changed.
136
285
  *
@@ -161,9 +310,11 @@ function undeterminable(mode, reason) {
161
310
  * @param {"hook"|"staged"|"base"} mode Comparison mode
162
311
  * @param {string | undefined} [baseRef] Base ref (base mode only)
163
312
  * @param {string[] | undefined} [onlyFiles] Restrict to these paths (hook mode)
313
+ * @param {string | undefined} [headRef] Head ref (base mode only), used solely
314
+ * to recognise a promotion between deploy-chain branches
164
315
  * @returns {number} Process exit code (2 for hook mode, 1 otherwise; 0 clean)
165
316
  */
166
- function run(mode, baseRef, onlyFiles) {
317
+ function run(mode, baseRef, onlyFiles, headRef) {
167
318
  const root = git(["rev-parse", "--show-toplevel"])?.trim();
168
319
  if (!root) return undeterminable(mode, "not a git repository");
169
320
  const plan = resolvePlan(mode, root, baseRef, onlyFiles);
@@ -205,13 +356,21 @@ function run(mode, baseRef, onlyFiles) {
205
356
  );
206
357
  if (findings.length === 0) return 0;
207
358
 
208
- const baselineConfig = parseJson(
209
- git(["show", `${plan.baselineRef}:.lisa.config.json`], root)
210
- );
211
- const { blocked, allowed } = applyAllowList(
212
- findings,
213
- extractAllowEntries(baselineConfig)
359
+ const allow = resolveAllowList(
360
+ root,
361
+ plan.baselineRef,
362
+ mode,
363
+ baseRef,
364
+ headRef
214
365
  );
366
+ const split = partitionCarriedEntries(findings, allow.promotion);
367
+ const { blocked, allowed } = applyAllowList(split.rest, allow.entries);
368
+ if (allow.note) process.stdout.write(`${allow.note}\n`);
369
+ for (const finding of split.carried) {
370
+ process.stdout.write(
371
+ `threshold-ratchet: carried forward by this promotion, approved upstream — ${finding.message}\n`
372
+ );
373
+ }
215
374
  for (const finding of allowed) {
216
375
  process.stdout.write(
217
376
  `threshold-ratchet: allowed by .lisa.config.json exception — ${finding.message}\n`
@@ -257,11 +416,16 @@ function runHookMode() {
257
416
  */
258
417
  function main() {
259
418
  const args = process.argv.slice(2);
419
+ const headIndex = args.indexOf("--head");
420
+ const headRef = headIndex === -1 ? undefined : args[headIndex + 1];
260
421
  if (args[0] === "--staged") return run("staged");
261
- if (args[0] === "--base") return run("base", args[1]);
422
+ // `--head` is optional and additive: a caller that omits it gets exactly the
423
+ // behavior that shipped before promotions were recognised, so an older
424
+ // workflow driving a newer script stays strict rather than silently relaxing.
425
+ if (args[0] === "--base") return run("base", args[1], undefined, headRef);
262
426
  if (args[0] === "--hook") return runHookMode();
263
427
  process.stderr.write(
264
- "usage: threshold-ratchet.mjs --hook | --staged | --base <ref>\n"
428
+ "usage: threshold-ratchet.mjs --hook | --staged | --base <ref> [--head <ref>]\n"
265
429
  );
266
430
  return 0;
267
431
  }
@@ -2,7 +2,7 @@
2
2
  name: lisa-parity-safety-net-rules
3
3
  description: "View, set, and verify the custom guard rules enforced by Lisa's safety-net PreToolUse Bash hook (parity-safety-net.sh). The consolidated cross-agent equivalent of the upstream safety-net plugin's set-custom-rules + verify-custom-rules skills — manages a project-local list of extended-regex patterns that block destructive shell commands, on Codex, agy, Copilot, Cursor, and Claude."
4
4
  allowed-tools: ["Read", "Edit", "Write", "Bash"]
5
- synced-from: safety-net@cc-marketplace@2.0.3
5
+ synced-from: safety-net@cc-marketplace@2.0.4
6
6
  ---
7
7
 
8
8
  # Parity Safety-Net Rules
@@ -24,7 +24,7 @@ project-specific rules on top of those built-ins.
24
24
  > against Lisa conventions — it does **not** port or invoke upstream plugin
25
25
  > code.
26
26
  >
27
- > **Drift tracking.** Pinned to `safety-net@cc-marketplace@2.0.3`.
27
+ > **Drift tracking.** Pinned to `safety-net@cc-marketplace@2.0.4`.
28
28
  > `scripts/plugin-parity-drift.mjs` compares this pin against the upstream
29
29
  > version in the plugin cache and flags staleness. **Do not port or copy upstream
30
30
  > plugin code.**
@@ -34,7 +34,22 @@ project-specific rules on top of those built-ins.
34
34
  > rule-management skill or Lisa's project-local ERE rule contract, and Lisa does
35
35
  > not currently ship hooks for those runtimes, so no behavior is absorbed here.
36
36
  >
37
- > **Known gap at the 2.0.3 pin.** Upstream 2.0.0 rebuilt its engine and added two
37
+ > **2.0.4 review.** Method, so the next person can re-run it rather than trust
38
+ > it: hash the sorted (relative path, content) manifest of every rule-bearing
39
+ > directory in the plugin cache under both versions, then diff the trees
40
+ > excluding `dist/`, `.git/`, and `node_modules/`. Result — `src/` (230 files),
41
+ > `skills/` (1), and `hooks/` (1) are byte-identical across 2.0.3 and 2.0.4
42
+ > (`src` digest `7e346e205fbd1aa7` both sides). The only non-generated
43
+ > differences are the `version` string in `package.json`, `kimi.plugin.json`,
44
+ > and `.claude-plugin/plugin.json`, plus a test-harness fix in
45
+ > `tests/cli/cli-statusline.test.ts` that tolerates EPIPE when an oversized
46
+ > payload is flushed at the bounded reader's cap. Everything else is rebuilt
47
+ > `dist/` output. No rule, guard family, or skill surface moved, so nothing is
48
+ > absorbed here — and, critically, **2.0.4 does not close the gap recorded
49
+ > below**, which therefore carries forward unchanged rather than being dropped
50
+ > at the new pin.
51
+ >
52
+ > **Known gap at the 2.0.4 pin.** Upstream 2.0.0 rebuilt its engine and added two
38
53
  > guard families this hook does **not** mirror: `secret.*` (blocks reading or
39
54
  > copying SSH keys, `.env` files, cloud credentials, and coding-CLI credential
40
55
  > stores) and `rm.git-metadata` (blocks deleting the `.git` control plane). Both
@@ -1,8 +1,8 @@
1
1
  # Test cases for the ast-grep rules in ../rules/.
2
2
  #
3
- # These run. `ast-grep test` is wired to the `sg:test` package script and to
4
- # the AST Grep Scan job in Lisa's quality workflow, so a wrong assertion here
5
- # fails CI. The job counts the YAML files in this directory first: an empty
3
+ # These run. The AST Grep Scan job in Lisa's quality workflow invokes the
4
+ # `ast-grep` binary directly, so a wrong assertion here fails CI whether or not
5
+ # this project has run `lisa apply` since the step shipped. The job counts the YAML files in this directory first: an empty
6
6
  # directory is reported as a skipped step with a warning, never as a pass,
7
7
  # because `ast-grep test` exits 0 when it finds nothing to run.
8
8
  #