@codyswann/lisa 3.67.0 → 3.69.0

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 (95) hide show
  1. package/all/copy-overwrite/scripts/lisa-gates.mjs +276 -8
  2. package/dist/codex/scripts/format-on-edit.sh +28 -0
  3. package/dist/codex/scripts/lint-on-edit.sh +28 -0
  4. package/dist/codex/scripts/lisa-edit-gate.sh +152 -0
  5. package/dist/codex/scripts/rubocop-on-edit.sh +28 -0
  6. package/dist/codex/scripts/sg-scan-on-edit.sh +28 -0
  7. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  8. package/dist/core/lisa-owned-hash-ledger.js +2 -0
  9. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  10. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  11. package/dist/core/upstream-evidence-manifest.js +30 -6
  12. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  13. package/package.json +1 -1
  14. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  15. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  16. package/plugins/lisa-agy/plugin.json +1 -1
  17. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  18. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  19. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  20. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  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/hooks/lisa-edit-gate.sh +152 -0
  37. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  38. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-nestjs-copilot/hooks/lisa-edit-gate.sh +152 -0
  40. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-nestjs-cursor/hooks/lisa-edit-gate.sh +152 -0
  42. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  44. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  45. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  49. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  50. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  54. package/plugins/lisa-rails/hooks/lisa-edit-gate.sh +152 -0
  55. package/plugins/lisa-rails/hooks/rubocop-on-edit.sh +23 -0
  56. package/plugins/lisa-rails/hooks/sg-scan-on-edit.sh +23 -0
  57. package/plugins/lisa-rails-agy/plugin.json +1 -1
  58. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-rails-copilot/hooks/lisa-edit-gate.sh +152 -0
  60. package/plugins/lisa-rails-copilot/hooks/rubocop-on-edit.sh +23 -0
  61. package/plugins/lisa-rails-copilot/hooks/sg-scan-on-edit.sh +23 -0
  62. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-rails-cursor/hooks/lisa-edit-gate.sh +152 -0
  64. package/plugins/lisa-rails-cursor/hooks/rubocop-on-edit.sh +23 -0
  65. package/plugins/lisa-rails-cursor/hooks/sg-scan-on-edit.sh +23 -0
  66. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  68. package/plugins/lisa-typescript/hooks/format-on-edit.sh +23 -0
  69. package/plugins/lisa-typescript/hooks/lint-on-edit.sh +23 -0
  70. package/plugins/lisa-typescript/hooks/lisa-edit-gate.sh +152 -0
  71. package/plugins/lisa-typescript/hooks/sg-scan-on-edit.sh +23 -0
  72. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  73. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-typescript-copilot/hooks/format-on-edit.sh +23 -0
  75. package/plugins/lisa-typescript-copilot/hooks/lint-on-edit.sh +23 -0
  76. package/plugins/lisa-typescript-copilot/hooks/lisa-edit-gate.sh +152 -0
  77. package/plugins/lisa-typescript-copilot/hooks/sg-scan-on-edit.sh +23 -0
  78. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa-typescript-cursor/hooks/format-on-edit.sh +23 -0
  80. package/plugins/lisa-typescript-cursor/hooks/lint-on-edit.sh +23 -0
  81. package/plugins/lisa-typescript-cursor/hooks/lisa-edit-gate.sh +152 -0
  82. package/plugins/lisa-typescript-cursor/hooks/sg-scan-on-edit.sh +23 -0
  83. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  84. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  85. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  86. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  87. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  88. package/plugins/src/nestjs/hooks/lisa-edit-gate.sh +152 -0
  89. package/plugins/src/rails/hooks/lisa-edit-gate.sh +152 -0
  90. package/plugins/src/rails/hooks/rubocop-on-edit.sh +23 -0
  91. package/plugins/src/rails/hooks/sg-scan-on-edit.sh +23 -0
  92. package/plugins/src/typescript/hooks/format-on-edit.sh +23 -0
  93. package/plugins/src/typescript/hooks/lint-on-edit.sh +23 -0
  94. package/plugins/src/typescript/hooks/lisa-edit-gate.sh +152 -0
  95. package/plugins/src/typescript/hooks/sg-scan-on-edit.sh +23 -0
@@ -0,0 +1,152 @@
1
+ #!/bin/bash
2
+ # This file is managed by Lisa.
3
+ # Do not edit directly — changes will be overwritten on the next `lisa` run.
4
+ # =============================================================================
5
+ # Edit-time gate façade
6
+ # =============================================================================
7
+ # Sourced by every on-edit hook. Answers one question before the hook runs
8
+ # anything: has the project declared what proves this property at this moment,
9
+ # and if so what should run instead of Lisa's written-in tool?
10
+ #
11
+ # WHY THIS EXISTS. Edit time is the highest-frequency enforcement surface Lisa
12
+ # owns and was the one surface with no configurability at all. The scripts
13
+ # resolved a RUNNER (`./node_modules/.bin/oxlint`, else `bunx`/`npx`) and
14
+ # hardcoded the TOOL, which is the inversion the gate registry exists to fix.
15
+ # Worse, they exit 2 — refusing the edit — when the written-in binary or config
16
+ # filename is absent, so a project that lints correctly with something else had
17
+ # every agent write refused until it installed Lisa's choice.
18
+ #
19
+ # THE CONTRACT, and the two halves that matter equally:
20
+ #
21
+ # declared -> the project's task runs and the hook's written-in tool is
22
+ # never consulted, so a missing binary cannot refuse the edit.
23
+ # undeclared -> NOTHING changes. The resolver is not even invoked when no
24
+ # registry is installed, and the hook falls through to exactly
25
+ # the command it ran before. That is the overwhelming majority
26
+ # of projects, and it is the case a façade silently breaks.
27
+ #
28
+ # ALL OR NOTHING FOR A MULTI-PROPERTY SCRIPT. `lisa_edit_gate_tasks` takes every
29
+ # property its caller proves and resolves only when EVERY one is declared. The
30
+ # Rails hook runs `rubocop -a` and then a second pass for unfixable errors — its
31
+ # own header calls it both formatter and linter — so standing down on `code-style`
32
+ # alone would silently stop proving `format-conformance`. Same shape as
33
+ # `lisa_gate_covers test-correctness coverage-adequacy` in the pre-push hook.
34
+ #
35
+ # HOW THE DECLARED TASK LEARNS WHICH FILE WAS EDITED. It does not take an
36
+ # argument: `<runner> <task>` with no arguments is the invocation every other
37
+ # Lisa façade uses, and adding a positional here would make the declaration
38
+ # mean something different at this moment than at every other one. The path is
39
+ # exported as LISA_EDITED_FILE instead, so a project that wants a per-file run
40
+ # can scope its own task and one that does not gets a whole-project run.
41
+ # =============================================================================
42
+
43
+ # Where the gate registry may be installed, in resolution order.
44
+ lisa_edit_gate_registry() {
45
+ for _lisa_candidate in \
46
+ "node_modules/@codyswann/lisa/all/copy-overwrite/scripts/lisa-gates.mjs" \
47
+ "scripts/lisa-gates.mjs" \
48
+ "all/copy-overwrite/scripts/lisa-gates.mjs"
49
+ do
50
+ if [ -f "$_lisa_candidate" ]; then
51
+ printf '%s' "$_lisa_candidate"
52
+ unset _lisa_candidate
53
+ return 0
54
+ fi
55
+ done
56
+ unset _lisa_candidate
57
+ return 1
58
+ }
59
+
60
+ # lisa_edit_gate_tasks <moment> <gate-id>...
61
+ #
62
+ # Prints one `<runner> <task>` line per DISTINCT declared task and returns 0,
63
+ # but only when every named gate resolves to a runnable task at <moment>.
64
+ # Returns 1 and prints nothing otherwise — including when no registry is
65
+ # installed, when node is unavailable, and when the resolver itself fails.
66
+ #
67
+ # Fail-safe in the direction that keeps proving things: any doubt returns 1 and
68
+ # the caller runs its built-in. A resolver that could not answer must never read
69
+ # as "the project has this covered".
70
+ lisa_edit_gate_tasks() {
71
+ _lisa_moment="$1"
72
+ shift
73
+ [ -n "$_lisa_moment" ] || { unset _lisa_moment; return 1; }
74
+ [ "$#" -gt 0 ] || { unset _lisa_moment; return 1; }
75
+ # NO SETTINGS FILE, NO SPAWN. Checked before anything else because this is
76
+ # the path essentially every project is on, and it runs on EVERY agent
77
+ # edit: asking the resolver would cost a node process per keystroke-sized
78
+ # write to answer "nothing is declared", which the absent file already
79
+ # answers. The equivalence control compares which COMMAND runs and would
80
+ # not have caught the cost.
81
+ [ -f .lisa.config.json ] || { unset _lisa_moment; return 1; }
82
+ command -v node >/dev/null 2>&1 || { unset _lisa_moment; return 1; }
83
+
84
+ _lisa_registry="$(lisa_edit_gate_registry)" || {
85
+ unset _lisa_moment _lisa_registry
86
+ return 1
87
+ }
88
+
89
+ # EXPORTED, not prefixed. An assignment prefix reaches only the FIRST
90
+ # command of a pipeline, and the filter that reads it is the second — so
91
+ # the prefixed form resolved an empty want-list and every script fell
92
+ # silently back to its built-in, which is a façade that looks wired and is
93
+ # not.
94
+ LISA_GATE_IDS="$*"
95
+ export LISA_GATE_IDS
96
+ _lisa_out="$(
97
+ node "$_lisa_registry" list \
98
+ --moment="$_lisa_moment" --json 2>/dev/null | node -e '
99
+ let raw = "";
100
+ process.stdin.on("data", chunk => { raw += chunk }).on("end", () => {
101
+ let resolved;
102
+ try { resolved = JSON.parse(raw || "[]"); } catch { process.exit(1); }
103
+ const wanted = (process.env.LISA_GATE_IDS || "").split(" ").filter(Boolean);
104
+ const commands = [];
105
+ for (const id of wanted) {
106
+ const hit = resolved.find(gate => gate.id === id);
107
+ // `run` mode with a task is the only shape that PROVES anything here. An
108
+ // awaited gate proves nothing at edit time and an intercepted one is
109
+ // Lisa running it elsewhere; both must leave the built-in in charge.
110
+ if (!hit || hit.mode !== "run" || !hit.command) process.exit(1);
111
+ if (!commands.includes(hit.command)) commands.push(hit.command);
112
+ }
113
+ if (commands.length === 0) process.exit(1);
114
+ process.stdout.write(commands.join("\n"));
115
+ });
116
+ '
117
+ )" || {
118
+ unset _lisa_moment _lisa_registry _lisa_out
119
+ return 1
120
+ }
121
+
122
+ [ -n "$_lisa_out" ] || {
123
+ unset _lisa_moment _lisa_registry _lisa_out
124
+ return 1
125
+ }
126
+ printf '%s' "$_lisa_out"
127
+ unset _lisa_moment _lisa_registry _lisa_out
128
+ return 0
129
+ }
130
+
131
+ # lisa_edit_gate_run <edited-file> <commands>
132
+ #
133
+ # Runs each resolved command with LISA_EDITED_FILE exported. Exits 2 on the
134
+ # first failure, which is the same refusal the built-in path uses, so a
135
+ # declared task that finds a real problem still stops the agent.
136
+ lisa_edit_gate_run() {
137
+ _lisa_file="$1"
138
+ shift
139
+ LISA_EDITED_FILE="$_lisa_file"
140
+ export LISA_EDITED_FILE
141
+ printf '%s\n' "$*" | while IFS= read -r _lisa_command; do
142
+ [ -n "$_lisa_command" ] || continue
143
+ echo "Running declared gate task: $_lisa_command"
144
+ # Deliberate word split: the resolver validated both halves as plain
145
+ # words, so no shell metacharacter reaches argv.
146
+ # shellcheck disable=SC2086
147
+ $_lisa_command || exit 2
148
+ done
149
+ _lisa_status=$?
150
+ unset _lisa_file _lisa_command
151
+ return $_lisa_status
152
+ }
@@ -35,8 +35,31 @@ case "$RELATIVE_PATH" in
35
35
  *) exit 0 ;;
36
36
  esac
37
37
 
38
+ # Where this script lives, resolved BEFORE any `cd`. `$0` is the path the
39
+ # harness invoked, and the façade helper ships beside it.
40
+ LISA_HOOK_DIR="$(cd "$(dirname "$0")" 2>/dev/null && pwd)"
41
+
38
42
  cd "$CLAUDE_PROJECT_DIR" || exit 0
39
43
 
44
+ # ---------------------------------------------------------------------------
45
+ # Gate façade. The project's declaration decides BEFORE any tool is resolved.
46
+ # Full contract, and why an undeclared project sees no change at all, in
47
+ # lisa-edit-gate.sh beside this file.
48
+ #
49
+ # All-or-nothing across the properties this script proves: a single invocation
50
+ # that proves more than one stands down only when EVERY one is declared, or it
51
+ # would silently stop proving the others.
52
+ # ---------------------------------------------------------------------------
53
+ if [ -f "$LISA_HOOK_DIR/lisa-edit-gate.sh" ]; then
54
+ # shellcheck source=/dev/null
55
+ . "$LISA_HOOK_DIR/lisa-edit-gate.sh"
56
+ if LISA_GATE_COMMANDS="$(lisa_edit_gate_tasks post-tool structural-rules)"; then
57
+ lisa_edit_gate_run "$FILE_PATH" "$LISA_GATE_COMMANDS"
58
+ exit $?
59
+ fi
60
+ fi
61
+
62
+
40
63
  # Verify ast-grep configuration exists
41
64
  if [ ! -f "sgconfig.yml" ]; then
42
65
  exit 0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "3.67.0",
3
+ "version": "3.69.0",
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"
@@ -43,8 +43,31 @@ case "$FILE_EXT" in
43
43
  esac
44
44
 
45
45
  # Change to the project directory to ensure package manager commands work
46
+ # Where this script lives, resolved BEFORE any `cd`. `$0` is the path the
47
+ # harness invoked, and the façade helper ships beside it.
48
+ LISA_HOOK_DIR="$(cd "$(dirname "$0")" 2>/dev/null && pwd)"
49
+
46
50
  cd "$CLAUDE_PROJECT_DIR" || exit 0
47
51
 
52
+ # ---------------------------------------------------------------------------
53
+ # Gate façade. The project's declaration decides BEFORE any tool is resolved.
54
+ # Full contract, and why an undeclared project sees no change at all, in
55
+ # lisa-edit-gate.sh beside this file.
56
+ #
57
+ # All-or-nothing across the properties this script proves: a single invocation
58
+ # that proves more than one stands down only when EVERY one is declared, or it
59
+ # would silently stop proving the others.
60
+ # ---------------------------------------------------------------------------
61
+ if [ -f "$LISA_HOOK_DIR/lisa-edit-gate.sh" ]; then
62
+ # shellcheck source=/dev/null
63
+ . "$LISA_HOOK_DIR/lisa-edit-gate.sh"
64
+ if LISA_GATE_COMMANDS="$(lisa_edit_gate_tasks post-tool format-conformance)"; then
65
+ lisa_edit_gate_run "$FILE_PATH" "$LISA_GATE_COMMANDS"
66
+ exit $?
67
+ fi
68
+ fi
69
+
70
+
48
71
  # Resolve Prettier binary — prefer local node_modules/.bin, then package-manager exec
49
72
  if [ -x "./node_modules/.bin/prettier" ]; then
50
73
  PRETTIER_CMD="./node_modules/.bin/prettier"
@@ -45,8 +45,31 @@ case "$RELATIVE_PATH" in
45
45
  *) exit 0 ;;
46
46
  esac
47
47
 
48
+ # Where this script lives, resolved BEFORE any `cd`. `$0` is the path the
49
+ # harness invoked, and the façade helper ships beside it.
50
+ LISA_HOOK_DIR="$(cd "$(dirname "$0")" 2>/dev/null && pwd)"
51
+
48
52
  cd "$CLAUDE_PROJECT_DIR" || exit 0
49
53
 
54
+ # ---------------------------------------------------------------------------
55
+ # Gate façade. The project's declaration decides BEFORE any tool is resolved.
56
+ # Full contract, and why an undeclared project sees no change at all, in
57
+ # lisa-edit-gate.sh beside this file.
58
+ #
59
+ # All-or-nothing across the properties this script proves: a single invocation
60
+ # that proves more than one stands down only when EVERY one is declared, or it
61
+ # would silently stop proving the others.
62
+ # ---------------------------------------------------------------------------
63
+ if [ -f "$LISA_HOOK_DIR/lisa-edit-gate.sh" ]; then
64
+ # shellcheck source=/dev/null
65
+ . "$LISA_HOOK_DIR/lisa-edit-gate.sh"
66
+ if LISA_GATE_COMMANDS="$(lisa_edit_gate_tasks post-tool code-style)"; then
67
+ lisa_edit_gate_run "$FILE_PATH" "$LISA_GATE_COMMANDS"
68
+ exit $?
69
+ fi
70
+ fi
71
+
72
+
50
73
  # Resolve oxlint binary - prefer local node_modules/.bin
51
74
  if [ -x "./node_modules/.bin/oxlint" ]; then
52
75
  OXLINT_CMD="./node_modules/.bin/oxlint"
@@ -0,0 +1,152 @@
1
+ #!/bin/bash
2
+ # This file is managed by Lisa.
3
+ # Do not edit directly — changes will be overwritten on the next `lisa` run.
4
+ # =============================================================================
5
+ # Edit-time gate façade
6
+ # =============================================================================
7
+ # Sourced by every on-edit hook. Answers one question before the hook runs
8
+ # anything: has the project declared what proves this property at this moment,
9
+ # and if so what should run instead of Lisa's written-in tool?
10
+ #
11
+ # WHY THIS EXISTS. Edit time is the highest-frequency enforcement surface Lisa
12
+ # owns and was the one surface with no configurability at all. The scripts
13
+ # resolved a RUNNER (`./node_modules/.bin/oxlint`, else `bunx`/`npx`) and
14
+ # hardcoded the TOOL, which is the inversion the gate registry exists to fix.
15
+ # Worse, they exit 2 — refusing the edit — when the written-in binary or config
16
+ # filename is absent, so a project that lints correctly with something else had
17
+ # every agent write refused until it installed Lisa's choice.
18
+ #
19
+ # THE CONTRACT, and the two halves that matter equally:
20
+ #
21
+ # declared -> the project's task runs and the hook's written-in tool is
22
+ # never consulted, so a missing binary cannot refuse the edit.
23
+ # undeclared -> NOTHING changes. The resolver is not even invoked when no
24
+ # registry is installed, and the hook falls through to exactly
25
+ # the command it ran before. That is the overwhelming majority
26
+ # of projects, and it is the case a façade silently breaks.
27
+ #
28
+ # ALL OR NOTHING FOR A MULTI-PROPERTY SCRIPT. `lisa_edit_gate_tasks` takes every
29
+ # property its caller proves and resolves only when EVERY one is declared. The
30
+ # Rails hook runs `rubocop -a` and then a second pass for unfixable errors — its
31
+ # own header calls it both formatter and linter — so standing down on `code-style`
32
+ # alone would silently stop proving `format-conformance`. Same shape as
33
+ # `lisa_gate_covers test-correctness coverage-adequacy` in the pre-push hook.
34
+ #
35
+ # HOW THE DECLARED TASK LEARNS WHICH FILE WAS EDITED. It does not take an
36
+ # argument: `<runner> <task>` with no arguments is the invocation every other
37
+ # Lisa façade uses, and adding a positional here would make the declaration
38
+ # mean something different at this moment than at every other one. The path is
39
+ # exported as LISA_EDITED_FILE instead, so a project that wants a per-file run
40
+ # can scope its own task and one that does not gets a whole-project run.
41
+ # =============================================================================
42
+
43
+ # Where the gate registry may be installed, in resolution order.
44
+ lisa_edit_gate_registry() {
45
+ for _lisa_candidate in \
46
+ "node_modules/@codyswann/lisa/all/copy-overwrite/scripts/lisa-gates.mjs" \
47
+ "scripts/lisa-gates.mjs" \
48
+ "all/copy-overwrite/scripts/lisa-gates.mjs"
49
+ do
50
+ if [ -f "$_lisa_candidate" ]; then
51
+ printf '%s' "$_lisa_candidate"
52
+ unset _lisa_candidate
53
+ return 0
54
+ fi
55
+ done
56
+ unset _lisa_candidate
57
+ return 1
58
+ }
59
+
60
+ # lisa_edit_gate_tasks <moment> <gate-id>...
61
+ #
62
+ # Prints one `<runner> <task>` line per DISTINCT declared task and returns 0,
63
+ # but only when every named gate resolves to a runnable task at <moment>.
64
+ # Returns 1 and prints nothing otherwise — including when no registry is
65
+ # installed, when node is unavailable, and when the resolver itself fails.
66
+ #
67
+ # Fail-safe in the direction that keeps proving things: any doubt returns 1 and
68
+ # the caller runs its built-in. A resolver that could not answer must never read
69
+ # as "the project has this covered".
70
+ lisa_edit_gate_tasks() {
71
+ _lisa_moment="$1"
72
+ shift
73
+ [ -n "$_lisa_moment" ] || { unset _lisa_moment; return 1; }
74
+ [ "$#" -gt 0 ] || { unset _lisa_moment; return 1; }
75
+ # NO SETTINGS FILE, NO SPAWN. Checked before anything else because this is
76
+ # the path essentially every project is on, and it runs on EVERY agent
77
+ # edit: asking the resolver would cost a node process per keystroke-sized
78
+ # write to answer "nothing is declared", which the absent file already
79
+ # answers. The equivalence control compares which COMMAND runs and would
80
+ # not have caught the cost.
81
+ [ -f .lisa.config.json ] || { unset _lisa_moment; return 1; }
82
+ command -v node >/dev/null 2>&1 || { unset _lisa_moment; return 1; }
83
+
84
+ _lisa_registry="$(lisa_edit_gate_registry)" || {
85
+ unset _lisa_moment _lisa_registry
86
+ return 1
87
+ }
88
+
89
+ # EXPORTED, not prefixed. An assignment prefix reaches only the FIRST
90
+ # command of a pipeline, and the filter that reads it is the second — so
91
+ # the prefixed form resolved an empty want-list and every script fell
92
+ # silently back to its built-in, which is a façade that looks wired and is
93
+ # not.
94
+ LISA_GATE_IDS="$*"
95
+ export LISA_GATE_IDS
96
+ _lisa_out="$(
97
+ node "$_lisa_registry" list \
98
+ --moment="$_lisa_moment" --json 2>/dev/null | node -e '
99
+ let raw = "";
100
+ process.stdin.on("data", chunk => { raw += chunk }).on("end", () => {
101
+ let resolved;
102
+ try { resolved = JSON.parse(raw || "[]"); } catch { process.exit(1); }
103
+ const wanted = (process.env.LISA_GATE_IDS || "").split(" ").filter(Boolean);
104
+ const commands = [];
105
+ for (const id of wanted) {
106
+ const hit = resolved.find(gate => gate.id === id);
107
+ // `run` mode with a task is the only shape that PROVES anything here. An
108
+ // awaited gate proves nothing at edit time and an intercepted one is
109
+ // Lisa running it elsewhere; both must leave the built-in in charge.
110
+ if (!hit || hit.mode !== "run" || !hit.command) process.exit(1);
111
+ if (!commands.includes(hit.command)) commands.push(hit.command);
112
+ }
113
+ if (commands.length === 0) process.exit(1);
114
+ process.stdout.write(commands.join("\n"));
115
+ });
116
+ '
117
+ )" || {
118
+ unset _lisa_moment _lisa_registry _lisa_out
119
+ return 1
120
+ }
121
+
122
+ [ -n "$_lisa_out" ] || {
123
+ unset _lisa_moment _lisa_registry _lisa_out
124
+ return 1
125
+ }
126
+ printf '%s' "$_lisa_out"
127
+ unset _lisa_moment _lisa_registry _lisa_out
128
+ return 0
129
+ }
130
+
131
+ # lisa_edit_gate_run <edited-file> <commands>
132
+ #
133
+ # Runs each resolved command with LISA_EDITED_FILE exported. Exits 2 on the
134
+ # first failure, which is the same refusal the built-in path uses, so a
135
+ # declared task that finds a real problem still stops the agent.
136
+ lisa_edit_gate_run() {
137
+ _lisa_file="$1"
138
+ shift
139
+ LISA_EDITED_FILE="$_lisa_file"
140
+ export LISA_EDITED_FILE
141
+ printf '%s\n' "$*" | while IFS= read -r _lisa_command; do
142
+ [ -n "$_lisa_command" ] || continue
143
+ echo "Running declared gate task: $_lisa_command"
144
+ # Deliberate word split: the resolver validated both halves as plain
145
+ # words, so no shell metacharacter reaches argv.
146
+ # shellcheck disable=SC2086
147
+ $_lisa_command || exit 2
148
+ done
149
+ _lisa_status=$?
150
+ unset _lisa_file _lisa_command
151
+ return $_lisa_status
152
+ }
@@ -35,8 +35,31 @@ case "$RELATIVE_PATH" in
35
35
  *) exit 0 ;;
36
36
  esac
37
37
 
38
+ # Where this script lives, resolved BEFORE any `cd`. `$0` is the path the
39
+ # harness invoked, and the façade helper ships beside it.
40
+ LISA_HOOK_DIR="$(cd "$(dirname "$0")" 2>/dev/null && pwd)"
41
+
38
42
  cd "$CLAUDE_PROJECT_DIR" || exit 0
39
43
 
44
+ # ---------------------------------------------------------------------------
45
+ # Gate façade. The project's declaration decides BEFORE any tool is resolved.
46
+ # Full contract, and why an undeclared project sees no change at all, in
47
+ # lisa-edit-gate.sh beside this file.
48
+ #
49
+ # All-or-nothing across the properties this script proves: a single invocation
50
+ # that proves more than one stands down only when EVERY one is declared, or it
51
+ # would silently stop proving the others.
52
+ # ---------------------------------------------------------------------------
53
+ if [ -f "$LISA_HOOK_DIR/lisa-edit-gate.sh" ]; then
54
+ # shellcheck source=/dev/null
55
+ . "$LISA_HOOK_DIR/lisa-edit-gate.sh"
56
+ if LISA_GATE_COMMANDS="$(lisa_edit_gate_tasks post-tool structural-rules)"; then
57
+ lisa_edit_gate_run "$FILE_PATH" "$LISA_GATE_COMMANDS"
58
+ exit $?
59
+ fi
60
+ fi
61
+
62
+
40
63
  # Verify ast-grep configuration exists
41
64
  if [ ! -f "sgconfig.yml" ]; then
42
65
  exit 0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "3.67.0",
3
+ "version": "3.69.0",
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.67.0",
3
+ "version": "3.69.0",
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.67.0",
3
+ "version": "3.69.0",
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.67.0",
3
+ "version": "3.69.0",
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.67.0",
3
+ "version": "3.69.0",
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"
@@ -0,0 +1,152 @@
1
+ #!/bin/bash
2
+ # This file is managed by Lisa.
3
+ # Do not edit directly — changes will be overwritten on the next `lisa` run.
4
+ # =============================================================================
5
+ # Edit-time gate façade
6
+ # =============================================================================
7
+ # Sourced by every on-edit hook. Answers one question before the hook runs
8
+ # anything: has the project declared what proves this property at this moment,
9
+ # and if so what should run instead of Lisa's written-in tool?
10
+ #
11
+ # WHY THIS EXISTS. Edit time is the highest-frequency enforcement surface Lisa
12
+ # owns and was the one surface with no configurability at all. The scripts
13
+ # resolved a RUNNER (`./node_modules/.bin/oxlint`, else `bunx`/`npx`) and
14
+ # hardcoded the TOOL, which is the inversion the gate registry exists to fix.
15
+ # Worse, they exit 2 — refusing the edit — when the written-in binary or config
16
+ # filename is absent, so a project that lints correctly with something else had
17
+ # every agent write refused until it installed Lisa's choice.
18
+ #
19
+ # THE CONTRACT, and the two halves that matter equally:
20
+ #
21
+ # declared -> the project's task runs and the hook's written-in tool is
22
+ # never consulted, so a missing binary cannot refuse the edit.
23
+ # undeclared -> NOTHING changes. The resolver is not even invoked when no
24
+ # registry is installed, and the hook falls through to exactly
25
+ # the command it ran before. That is the overwhelming majority
26
+ # of projects, and it is the case a façade silently breaks.
27
+ #
28
+ # ALL OR NOTHING FOR A MULTI-PROPERTY SCRIPT. `lisa_edit_gate_tasks` takes every
29
+ # property its caller proves and resolves only when EVERY one is declared. The
30
+ # Rails hook runs `rubocop -a` and then a second pass for unfixable errors — its
31
+ # own header calls it both formatter and linter — so standing down on `code-style`
32
+ # alone would silently stop proving `format-conformance`. Same shape as
33
+ # `lisa_gate_covers test-correctness coverage-adequacy` in the pre-push hook.
34
+ #
35
+ # HOW THE DECLARED TASK LEARNS WHICH FILE WAS EDITED. It does not take an
36
+ # argument: `<runner> <task>` with no arguments is the invocation every other
37
+ # Lisa façade uses, and adding a positional here would make the declaration
38
+ # mean something different at this moment than at every other one. The path is
39
+ # exported as LISA_EDITED_FILE instead, so a project that wants a per-file run
40
+ # can scope its own task and one that does not gets a whole-project run.
41
+ # =============================================================================
42
+
43
+ # Where the gate registry may be installed, in resolution order.
44
+ lisa_edit_gate_registry() {
45
+ for _lisa_candidate in \
46
+ "node_modules/@codyswann/lisa/all/copy-overwrite/scripts/lisa-gates.mjs" \
47
+ "scripts/lisa-gates.mjs" \
48
+ "all/copy-overwrite/scripts/lisa-gates.mjs"
49
+ do
50
+ if [ -f "$_lisa_candidate" ]; then
51
+ printf '%s' "$_lisa_candidate"
52
+ unset _lisa_candidate
53
+ return 0
54
+ fi
55
+ done
56
+ unset _lisa_candidate
57
+ return 1
58
+ }
59
+
60
+ # lisa_edit_gate_tasks <moment> <gate-id>...
61
+ #
62
+ # Prints one `<runner> <task>` line per DISTINCT declared task and returns 0,
63
+ # but only when every named gate resolves to a runnable task at <moment>.
64
+ # Returns 1 and prints nothing otherwise — including when no registry is
65
+ # installed, when node is unavailable, and when the resolver itself fails.
66
+ #
67
+ # Fail-safe in the direction that keeps proving things: any doubt returns 1 and
68
+ # the caller runs its built-in. A resolver that could not answer must never read
69
+ # as "the project has this covered".
70
+ lisa_edit_gate_tasks() {
71
+ _lisa_moment="$1"
72
+ shift
73
+ [ -n "$_lisa_moment" ] || { unset _lisa_moment; return 1; }
74
+ [ "$#" -gt 0 ] || { unset _lisa_moment; return 1; }
75
+ # NO SETTINGS FILE, NO SPAWN. Checked before anything else because this is
76
+ # the path essentially every project is on, and it runs on EVERY agent
77
+ # edit: asking the resolver would cost a node process per keystroke-sized
78
+ # write to answer "nothing is declared", which the absent file already
79
+ # answers. The equivalence control compares which COMMAND runs and would
80
+ # not have caught the cost.
81
+ [ -f .lisa.config.json ] || { unset _lisa_moment; return 1; }
82
+ command -v node >/dev/null 2>&1 || { unset _lisa_moment; return 1; }
83
+
84
+ _lisa_registry="$(lisa_edit_gate_registry)" || {
85
+ unset _lisa_moment _lisa_registry
86
+ return 1
87
+ }
88
+
89
+ # EXPORTED, not prefixed. An assignment prefix reaches only the FIRST
90
+ # command of a pipeline, and the filter that reads it is the second — so
91
+ # the prefixed form resolved an empty want-list and every script fell
92
+ # silently back to its built-in, which is a façade that looks wired and is
93
+ # not.
94
+ LISA_GATE_IDS="$*"
95
+ export LISA_GATE_IDS
96
+ _lisa_out="$(
97
+ node "$_lisa_registry" list \
98
+ --moment="$_lisa_moment" --json 2>/dev/null | node -e '
99
+ let raw = "";
100
+ process.stdin.on("data", chunk => { raw += chunk }).on("end", () => {
101
+ let resolved;
102
+ try { resolved = JSON.parse(raw || "[]"); } catch { process.exit(1); }
103
+ const wanted = (process.env.LISA_GATE_IDS || "").split(" ").filter(Boolean);
104
+ const commands = [];
105
+ for (const id of wanted) {
106
+ const hit = resolved.find(gate => gate.id === id);
107
+ // `run` mode with a task is the only shape that PROVES anything here. An
108
+ // awaited gate proves nothing at edit time and an intercepted one is
109
+ // Lisa running it elsewhere; both must leave the built-in in charge.
110
+ if (!hit || hit.mode !== "run" || !hit.command) process.exit(1);
111
+ if (!commands.includes(hit.command)) commands.push(hit.command);
112
+ }
113
+ if (commands.length === 0) process.exit(1);
114
+ process.stdout.write(commands.join("\n"));
115
+ });
116
+ '
117
+ )" || {
118
+ unset _lisa_moment _lisa_registry _lisa_out
119
+ return 1
120
+ }
121
+
122
+ [ -n "$_lisa_out" ] || {
123
+ unset _lisa_moment _lisa_registry _lisa_out
124
+ return 1
125
+ }
126
+ printf '%s' "$_lisa_out"
127
+ unset _lisa_moment _lisa_registry _lisa_out
128
+ return 0
129
+ }
130
+
131
+ # lisa_edit_gate_run <edited-file> <commands>
132
+ #
133
+ # Runs each resolved command with LISA_EDITED_FILE exported. Exits 2 on the
134
+ # first failure, which is the same refusal the built-in path uses, so a
135
+ # declared task that finds a real problem still stops the agent.
136
+ lisa_edit_gate_run() {
137
+ _lisa_file="$1"
138
+ shift
139
+ LISA_EDITED_FILE="$_lisa_file"
140
+ export LISA_EDITED_FILE
141
+ printf '%s\n' "$*" | while IFS= read -r _lisa_command; do
142
+ [ -n "$_lisa_command" ] || continue
143
+ echo "Running declared gate task: $_lisa_command"
144
+ # Deliberate word split: the resolver validated both halves as plain
145
+ # words, so no shell metacharacter reaches argv.
146
+ # shellcheck disable=SC2086
147
+ $_lisa_command || exit 2
148
+ done
149
+ _lisa_status=$?
150
+ unset _lisa_file _lisa_command
151
+ return $_lisa_status
152
+ }