@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.
- package/all/copy-overwrite/scripts/lisa-gates.mjs +276 -8
- package/dist/codex/scripts/format-on-edit.sh +28 -0
- package/dist/codex/scripts/lint-on-edit.sh +28 -0
- package/dist/codex/scripts/lisa-edit-gate.sh +152 -0
- package/dist/codex/scripts/rubocop-on-edit.sh +28 -0
- package/dist/codex/scripts/sg-scan-on-edit.sh +28 -0
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +2 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +30 -6
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/hooks/lisa-edit-gate.sh +152 -0
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/hooks/lisa-edit-gate.sh +152 -0
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/hooks/lisa-edit-gate.sh +152 -0
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/hooks/lisa-edit-gate.sh +152 -0
- package/plugins/lisa-rails/hooks/rubocop-on-edit.sh +23 -0
- package/plugins/lisa-rails/hooks/sg-scan-on-edit.sh +23 -0
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/hooks/lisa-edit-gate.sh +152 -0
- package/plugins/lisa-rails-copilot/hooks/rubocop-on-edit.sh +23 -0
- package/plugins/lisa-rails-copilot/hooks/sg-scan-on-edit.sh +23 -0
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/hooks/lisa-edit-gate.sh +152 -0
- package/plugins/lisa-rails-cursor/hooks/rubocop-on-edit.sh +23 -0
- package/plugins/lisa-rails-cursor/hooks/sg-scan-on-edit.sh +23 -0
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/hooks/format-on-edit.sh +23 -0
- package/plugins/lisa-typescript/hooks/lint-on-edit.sh +23 -0
- package/plugins/lisa-typescript/hooks/lisa-edit-gate.sh +152 -0
- package/plugins/lisa-typescript/hooks/sg-scan-on-edit.sh +23 -0
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/hooks/format-on-edit.sh +23 -0
- package/plugins/lisa-typescript-copilot/hooks/lint-on-edit.sh +23 -0
- package/plugins/lisa-typescript-copilot/hooks/lisa-edit-gate.sh +152 -0
- package/plugins/lisa-typescript-copilot/hooks/sg-scan-on-edit.sh +23 -0
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/hooks/format-on-edit.sh +23 -0
- package/plugins/lisa-typescript-cursor/hooks/lint-on-edit.sh +23 -0
- package/plugins/lisa-typescript-cursor/hooks/lisa-edit-gate.sh +152 -0
- package/plugins/lisa-typescript-cursor/hooks/sg-scan-on-edit.sh +23 -0
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/nestjs/hooks/lisa-edit-gate.sh +152 -0
- package/plugins/src/rails/hooks/lisa-edit-gate.sh +152 -0
- package/plugins/src/rails/hooks/rubocop-on-edit.sh +23 -0
- package/plugins/src/rails/hooks/sg-scan-on-edit.sh +23 -0
- package/plugins/src/typescript/hooks/format-on-edit.sh +23 -0
- package/plugins/src/typescript/hooks/lint-on-edit.sh +23 -0
- package/plugins/src/typescript/hooks/lisa-edit-gate.sh +152 -0
- 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
|
+
}
|
|
@@ -44,8 +44,31 @@ case "$RELATIVE_PATH" in
|
|
|
44
44
|
*) exit 0 ;;
|
|
45
45
|
esac
|
|
46
46
|
|
|
47
|
+
# Where this script lives, resolved BEFORE any `cd`. `$0` is the path the
|
|
48
|
+
# harness invoked, and the façade helper ships beside it.
|
|
49
|
+
LISA_HOOK_DIR="$(cd "$(dirname "$0")" 2>/dev/null && pwd)"
|
|
50
|
+
|
|
47
51
|
cd "$CLAUDE_PROJECT_DIR" || exit 0
|
|
48
52
|
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# Gate façade. The project's declaration decides BEFORE any tool is resolved.
|
|
55
|
+
# Full contract, and why an undeclared project sees no change at all, in
|
|
56
|
+
# lisa-edit-gate.sh beside this file.
|
|
57
|
+
#
|
|
58
|
+
# All-or-nothing across the properties this script proves: a single invocation
|
|
59
|
+
# that proves more than one stands down only when EVERY one is declared, or it
|
|
60
|
+
# would silently stop proving the others.
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
if [ -f "$LISA_HOOK_DIR/lisa-edit-gate.sh" ]; then
|
|
63
|
+
# shellcheck source=/dev/null
|
|
64
|
+
. "$LISA_HOOK_DIR/lisa-edit-gate.sh"
|
|
65
|
+
if LISA_GATE_COMMANDS="$(lisa_edit_gate_tasks post-tool code-style format-conformance)"; then
|
|
66
|
+
lisa_edit_gate_run "$FILE_PATH" "$LISA_GATE_COMMANDS"
|
|
67
|
+
exit $?
|
|
68
|
+
fi
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
|
|
49
72
|
# Verify this is a Ruby project with Bundler
|
|
50
73
|
if [ ! -f "Gemfile" ]; then
|
|
51
74
|
exit 0
|
|
@@ -39,8 +39,31 @@ case "$RELATIVE_PATH" in
|
|
|
39
39
|
*) exit 0 ;;
|
|
40
40
|
esac
|
|
41
41
|
|
|
42
|
+
# Where this script lives, resolved BEFORE any `cd`. `$0` is the path the
|
|
43
|
+
# harness invoked, and the façade helper ships beside it.
|
|
44
|
+
LISA_HOOK_DIR="$(cd "$(dirname "$0")" 2>/dev/null && pwd)"
|
|
45
|
+
|
|
42
46
|
cd "$CLAUDE_PROJECT_DIR" || exit 0
|
|
43
47
|
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# Gate façade. The project's declaration decides BEFORE any tool is resolved.
|
|
50
|
+
# Full contract, and why an undeclared project sees no change at all, in
|
|
51
|
+
# lisa-edit-gate.sh beside this file.
|
|
52
|
+
#
|
|
53
|
+
# All-or-nothing across the properties this script proves: a single invocation
|
|
54
|
+
# that proves more than one stands down only when EVERY one is declared, or it
|
|
55
|
+
# would silently stop proving the others.
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
if [ -f "$LISA_HOOK_DIR/lisa-edit-gate.sh" ]; then
|
|
58
|
+
# shellcheck source=/dev/null
|
|
59
|
+
. "$LISA_HOOK_DIR/lisa-edit-gate.sh"
|
|
60
|
+
if LISA_GATE_COMMANDS="$(lisa_edit_gate_tasks post-tool structural-rules)"; then
|
|
61
|
+
lisa_edit_gate_run "$FILE_PATH" "$LISA_GATE_COMMANDS"
|
|
62
|
+
exit $?
|
|
63
|
+
fi
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
|
|
44
67
|
# Verify ast-grep configuration exists
|
|
45
68
|
if [ ! -f "sgconfig.yml" ]; then
|
|
46
69
|
exit 0
|
|
@@ -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
|