@codyswann/lisa 4.73.1 → 4.73.3
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/README.md +20 -0
- package/all/copy-overwrite/scripts/lib/history-secret-git.mjs +158 -5
- package/all/copy-overwrite/scripts/lib/npm-update-contract.mjs +25 -1
- package/all/copy-overwrite/scripts/lisa-enforcement-fallback.sh +137 -163
- package/all/copy-overwrite/scripts/lisa-enforcement-freshness.mjs +180 -0
- package/all/copy-overwrite/scripts/lisa-history-secrets.mjs +17 -3
- package/all/copy-overwrite/scripts/lisa-hooks/block-blind-automerge.sh +86 -7
- package/all/copy-overwrite/scripts/lisa-hooks/block-managed-file-edits.sh +7 -1
- package/all/copy-overwrite/scripts/lisa-hooks/block-no-verify.sh +62 -7
- package/all/copy-overwrite/scripts/lisa-rails-prepush.mjs +4 -1
- package/all/copy-overwrite/scripts/lisa-work-item.mjs +191 -5
- package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +5 -5
- package/all/deletions.json +19 -2
- package/dist/codex/scripts/block-no-verify.sh +62 -7
- package/dist/core/config.d.ts +16 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +88 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/lisa.d.ts +31 -0
- package/dist/core/lisa.d.ts.map +1 -1
- package/dist/core/lisa.js +69 -4
- package/dist/core/lisa.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/retire-unmodified.d.ts +66 -0
- package/dist/core/retire-unmodified.d.ts.map +1 -0
- package/dist/core/retire-unmodified.js +244 -0
- package/dist/core/retire-unmodified.js.map +1 -0
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +39 -21
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/block-managed-file-edits.sh +7 -1
- package/dist/opencode/plugin-templates/block-no-verify.sh +62 -7
- package/package.json +4 -4
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/hooks/auto-update.mjs +69 -1
- package/plugins/lisa/hooks/block-blind-automerge.sh +86 -7
- package/plugins/lisa/hooks/block-managed-file-edits.sh +7 -1
- package/plugins/lisa/hooks/block-no-verify.agy.sh +62 -7
- package/plugins/lisa/hooks/block-no-verify.sh +62 -7
- package/plugins/lisa-agy/hooks/block-blind-automerge.sh +86 -7
- package/plugins/lisa-agy/hooks/block-managed-file-edits.sh +7 -1
- package/plugins/lisa-agy/hooks/block-no-verify.agy.sh +62 -7
- 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-copilot/hooks/auto-update.mjs +69 -1
- package/plugins/lisa-copilot/hooks/block-blind-automerge.sh +86 -7
- package/plugins/lisa-copilot/hooks/block-managed-file-edits.sh +7 -1
- package/plugins/lisa-copilot/hooks/block-no-verify.sh +62 -7
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/auto-update.mjs +69 -1
- package/plugins/lisa-cursor/hooks/block-blind-automerge.sh +86 -7
- package/plugins/lisa-cursor/hooks/block-managed-file-edits.sh +7 -1
- package/plugins/lisa-cursor/hooks/block-no-verify.sh +62 -7
- 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-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- 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-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- 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/materialized-artifacts.json +1 -0
- package/plugins/src/base/hooks/auto-update.mjs +69 -1
- package/plugins/src/base/hooks/block-blind-automerge.sh +86 -7
- package/plugins/src/base/hooks/block-managed-file-edits.sh +7 -1
- package/plugins/src/base/hooks/block-no-verify.agy.sh +62 -7
- package/plugins/src/base/hooks/block-no-verify.sh +62 -7
- package/scripts/build-plugins.sh +7 -0
- package/scripts/check-shipped-surface-removals.mjs +22 -2
- package/scripts/lisa-enforcement-fallback.sh +137 -163
- package/scripts/lisa-enforcement-freshness.mjs +177 -0
- package/scripts/two-channel-couplings.json +1 -1
- package/typescript/copy-overwrite/.prettierignore +1 -0
|
@@ -103,8 +103,8 @@ plugin_tree="$repo_root/plugins/lisa/hooks"
|
|
|
103
103
|
#
|
|
104
104
|
# Enforcement resolves from THE CHECKOUT, never from npm. Publishing a guard
|
|
105
105
|
# fix and refreshing the marketplace does not reach the copy governing an agent
|
|
106
|
-
# working in a branch cut before that fix
|
|
107
|
-
# strongest refusal
|
|
106
|
+
# working in a branch cut before that fix. First-wins resolution chooses each
|
|
107
|
+
# guard; the strongest refusal aggregates DIFFERENT guards (#3205).
|
|
108
108
|
# One guard measured 22/22 on `main`, 22/22 in the installed clone, and 19/22
|
|
109
109
|
# on the copy actually in force on the machine the fix had been written on.
|
|
110
110
|
#
|
|
@@ -116,54 +116,29 @@ plugin_tree="$repo_root/plugins/lisa/hooks"
|
|
|
116
116
|
#
|
|
117
117
|
# Three constraints shape how the dating is done, and each rules something out:
|
|
118
118
|
#
|
|
119
|
-
# -
|
|
120
|
-
#
|
|
121
|
-
#
|
|
122
|
-
# lookup.
|
|
119
|
+
# - Already-noticed permitted calls perform no diagnostic reads or forks.
|
|
120
|
+
# Notices/refusals use one bounded optional helper per process, off that
|
|
121
|
+
# hot path, to validate JSON and compare selected entry-file bytes.
|
|
123
122
|
# - It must work offline, and there is no network lookup anywhere below. A
|
|
124
123
|
# guard that stalls or fails when the network does is a guard whoever it
|
|
125
124
|
# slows down switches off.
|
|
126
|
-
# -
|
|
127
|
-
#
|
|
128
|
-
#
|
|
125
|
+
# - Host content differences are not a claim of older/newer code. Historical
|
|
126
|
+
# receipt versions never date current bytes. Plugin-channel age remains a
|
|
127
|
+
# distinct version-backed diagnostic; missing evidence is explicit unknown.
|
|
129
128
|
#
|
|
130
129
|
# Bash 3.2 is the floor — macOS ships it as /bin/bash, and that is what the
|
|
131
130
|
# repository hook entry invokes — so there are no associative arrays
|
|
132
131
|
# here. There are exactly two possible trees, which is what makes plain
|
|
133
132
|
# variables enough.
|
|
134
133
|
|
|
135
|
-
#
|
|
136
|
-
#
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
#
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
# the top-level one. A file that does not state the key leaves the result
|
|
144
|
-
# empty, which is reported as an unknown vintage rather than guessed at.
|
|
145
|
-
read_json_version() {
|
|
146
|
-
json_version=""
|
|
147
|
-
[ -f "$1" ] || return 1
|
|
148
|
-
# Built as a variable and matched unquoted: in bash 3.2 a quoted portion of
|
|
149
|
-
# an `=~` pattern is literal text, so an inline pattern would stop being a
|
|
150
|
-
# regex on exactly the interpreter this has to work under.
|
|
151
|
-
#
|
|
152
|
-
# Anchored to at most three leading spaces so a NESTED key cannot answer for
|
|
153
|
-
# a top-level one. `node_modules/@codyswann/lisa/package.json` carries its
|
|
154
|
-
# `"version"` at line 118 of 255 — measured, not assumed — with a dependency
|
|
155
|
-
# block below it, and a package literally named `version` would otherwise
|
|
156
|
-
# match first and report its range as a Lisa version.
|
|
157
|
-
local pattern="^[[:space:]]{0,3}\"$2\"[[:space:]]*:[[:space:]]*\"([^\"]+)\""
|
|
158
|
-
local line
|
|
159
|
-
while IFS= read -r line || [ -n "$line" ]; do
|
|
160
|
-
if [[ "$line" =~ $pattern ]]; then
|
|
161
|
-
json_version="${BASH_REMATCH[1]}"
|
|
162
|
-
return 0
|
|
163
|
-
fi
|
|
164
|
-
done <"$1"
|
|
165
|
-
return 1
|
|
166
|
-
}
|
|
134
|
+
# The receipt records history, not the current contents of a selected guard.
|
|
135
|
+
# Read strict bounded JSON and actual selected host/template pairs only when a
|
|
136
|
+
# notice or refusal needs evidence. The optional Node helper is shipped beside
|
|
137
|
+
# this dispatcher; its absence cannot alter enforcement, only make evidence
|
|
138
|
+
# unknown. There is no network, directory scan or persistent content cache.
|
|
139
|
+
installed_version=""
|
|
140
|
+
last_applied_version=""
|
|
141
|
+
host_guard_states=()
|
|
167
142
|
|
|
168
143
|
# Numeric release fields of the last split_version call.
|
|
169
144
|
v1=0
|
|
@@ -234,54 +209,41 @@ note_version() {
|
|
|
234
209
|
fi
|
|
235
210
|
}
|
|
236
211
|
|
|
237
|
-
host_tree_version=""
|
|
238
212
|
plugin_tree_version=""
|
|
239
213
|
|
|
240
|
-
#
|
|
241
|
-
#
|
|
242
|
-
#
|
|
243
|
-
# said. On the overwhelmingly common path (a permitted call in a session that
|
|
244
|
-
# has already printed its notice) it never runs at all, so those three file
|
|
245
|
-
# reads leave the hot path entirely.
|
|
214
|
+
# Resolve once PER PROCESS at the existing lazy diagnostic boundary. A later
|
|
215
|
+
# refusal in the same session gets fresh content evidence in its new process;
|
|
216
|
+
# an already-noticed permitted call invokes neither Node nor a comparison.
|
|
246
217
|
resolve_vintages() {
|
|
247
218
|
[ "$vintages_resolved" -eq 0 ] || return 0
|
|
248
219
|
vintages_resolved=1
|
|
249
|
-
|
|
250
|
-
# The marketplace clone is the installed release in the literal sense — it is
|
|
251
|
-
# the copy `claude plugin` put on this machine, and it is a full checkout of
|
|
252
|
-
# Lisa, so it dates itself.
|
|
253
220
|
local config_dir="${CLAUDE_CONFIG_DIR-}"
|
|
254
221
|
[ -n "$config_dir" ] || config_dir="${HOME-}/.claude"
|
|
255
|
-
local
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
222
|
+
local helper="${BASH_SOURCE[0]%/*}/lisa-enforcement-freshness.mjs"
|
|
223
|
+
local key value state_index
|
|
224
|
+
local state_count=0
|
|
225
|
+
while [ "$state_count" -lt "$guard_count" ]; do
|
|
226
|
+
host_guard_states+=("unknown")
|
|
227
|
+
state_count=$((state_count + 1))
|
|
228
|
+
done
|
|
229
|
+
if [ -f "$helper" ] && [ -r "$helper" ] && command -v node >/dev/null 2>&1; then
|
|
230
|
+
while IFS=$'\t' read -r key value; do
|
|
231
|
+
case "$key" in
|
|
232
|
+
installed) installed_version="$value" ;;
|
|
233
|
+
applied) last_applied_version="$value" ;;
|
|
234
|
+
plugin) plugin_tree_version="$value" ;;
|
|
235
|
+
marketplace) note_version "$value" "$config_dir/plugins/marketplaces/lisa/plugins/lisa/.claude-plugin/plugin.json" ;;
|
|
236
|
+
channel) plugin_channel_version="$value" ;;
|
|
237
|
+
channel_path) plugin_channel_path="$value" ;;
|
|
238
|
+
guard[0-7])
|
|
239
|
+
state_index="${key#guard}"
|
|
240
|
+
case "$value" in matching|different|unknown) host_guard_states[$state_index]="$value" ;; esac
|
|
241
|
+
;;
|
|
242
|
+
esac
|
|
243
|
+
done < <(node "$helper" "$repo_root" "$config_dir" "${guard_evidence_names[@]}" 2>/dev/null)
|
|
277
244
|
fi
|
|
245
|
+
note_version "$installed_version" "$repo_root/node_modules/@codyswann/lisa/package.json"
|
|
278
246
|
note_version "$plugin_tree_version" "$plugin_tree"
|
|
279
|
-
|
|
280
|
-
# The channel this dispatcher races. Deliberately NOT folded into
|
|
281
|
-
# `note_version`: the newest-on-disk maximum answers "is this copy behind
|
|
282
|
-
# something local", and the plugin channel's age is a separate question
|
|
283
|
-
# about a copy that runs in parallel rather than instead.
|
|
284
|
-
resolve_plugin_channel || true
|
|
285
247
|
}
|
|
286
248
|
|
|
287
249
|
# ---------------------------------------------------------------------------
|
|
@@ -330,51 +292,8 @@ resolve_vintages() {
|
|
|
330
292
|
plugin_channel_version=""
|
|
331
293
|
plugin_channel_path=""
|
|
332
294
|
|
|
333
|
-
#
|
|
334
|
-
#
|
|
335
|
-
# `plugins/installed_plugins.json` records installs PER PROJECT DIRECTORY —
|
|
336
|
-
# which is exactly why one checkout sits versions behind another on the same
|
|
337
|
-
# disk — so the lookup is keyed on this repo root, not on the machine.
|
|
338
|
-
#
|
|
339
|
-
# A record is claimed only when its `installPath` sits under a `lisa/lisa/`
|
|
340
|
-
# marketplace cache. A locally-installed plugin does not match and is left
|
|
341
|
-
# UNRESOLVED on purpose: reporting "agree" about a copy that was never read is
|
|
342
|
-
# the failure this whole section exists to end, and an honest "I could not
|
|
343
|
-
# tell" is the safe direction.
|
|
344
|
-
#
|
|
345
|
-
# `grep` does the scanning because the record runs to tens of thousands of
|
|
346
|
-
# lines; the bash loop then sees a handful. Called only from resolve_vintages,
|
|
347
|
-
# so it inherits that latch and never runs on the silent allow path.
|
|
348
|
-
resolve_plugin_channel() {
|
|
349
|
-
local config_dir="${CLAUDE_CONFIG_DIR-}"
|
|
350
|
-
[ -n "$config_dir" ] || config_dir="${HOME-}/.claude"
|
|
351
|
-
local record="$config_dir/plugins/installed_plugins.json"
|
|
352
|
-
[ -f "$record" ] || return 1
|
|
353
|
-
|
|
354
|
-
local install_pattern="^[[:space:]]*\"installPath\"[[:space:]]*:[[:space:]]*\"(.*/lisa/lisa/[^\"]+)\""
|
|
355
|
-
local version_pattern="^[[:space:]]*\"version\"[[:space:]]*:[[:space:]]*\"([^\"]+)\""
|
|
356
|
-
local line
|
|
357
|
-
local candidate=""
|
|
358
|
-
while IFS= read -r line || [ -n "$line" ]; do
|
|
359
|
-
# `grep -A` separates non-adjacent groups with `--`. A candidate must not
|
|
360
|
-
# survive across that boundary or one plugin's installPath answers for
|
|
361
|
-
# another plugin's version.
|
|
362
|
-
if [ "$line" = "--" ]; then
|
|
363
|
-
candidate=""
|
|
364
|
-
continue
|
|
365
|
-
fi
|
|
366
|
-
if [[ "$line" =~ $install_pattern ]]; then
|
|
367
|
-
candidate="${BASH_REMATCH[1]}"
|
|
368
|
-
continue
|
|
369
|
-
fi
|
|
370
|
-
if [ -n "$candidate" ] && [[ "$line" =~ $version_pattern ]]; then
|
|
371
|
-
plugin_channel_path="$candidate"
|
|
372
|
-
plugin_channel_version="${BASH_REMATCH[1]}"
|
|
373
|
-
return 0
|
|
374
|
-
fi
|
|
375
|
-
done < <(grep -F -A4 "\"projectPath\": \"$repo_root\"," "$record" 2>/dev/null)
|
|
376
|
-
return 1
|
|
377
|
-
}
|
|
295
|
+
# The bounded helper reads the runtime record for exactly lisa@lisa and this
|
|
296
|
+
# project. It reports installed version/path, never whether hooks are live.
|
|
378
297
|
|
|
379
298
|
# Verdict of the last classify_channel_skew call: agree | skew | undetermined.
|
|
380
299
|
#
|
|
@@ -411,43 +330,57 @@ channel_skew_line=""
|
|
|
411
330
|
# of the channel the plugin manifest runs.
|
|
412
331
|
classify_channel_skew() {
|
|
413
332
|
local mine=""
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
333
|
+
local source=""
|
|
334
|
+
local tree
|
|
335
|
+
local channel_repair=""
|
|
336
|
+
local compared=0
|
|
337
|
+
channel_skew_line=""
|
|
338
|
+
channel_skew_verdict="undetermined"
|
|
339
|
+
for tree in host plugin; do
|
|
340
|
+
if [ "$tree" = host ] && [ "$host_tree_used" -eq 1 ]; then
|
|
341
|
+
mine="$installed_version"
|
|
342
|
+
source="installed Lisa package"
|
|
343
|
+
elif [ "$tree" = plugin ] && [ "$plugin_tree_used" -eq 1 ]; then
|
|
344
|
+
mine="$plugin_tree_version"
|
|
345
|
+
source="this checkout's plugin manifest"
|
|
346
|
+
else
|
|
347
|
+
continue
|
|
348
|
+
fi
|
|
349
|
+
if [ -z "$mine" ] || [ -z "$plugin_channel_version" ]; then
|
|
350
|
+
channel_skew_line="$channel_skew_line cross-channel vintage UNDETERMINED — $source could not be
|
|
351
|
+
compared with the plugin manifest's installed copies. Not agreement:
|
|
352
|
+
missing version evidence does not establish either channel's age or liveness.
|
|
426
353
|
"
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
354
|
+
elif [ "$mine" != "$plugin_channel_version" ]; then
|
|
355
|
+
channel_skew_verdict="skew"
|
|
356
|
+
if version_older "$plugin_channel_version" "$mine"; then
|
|
357
|
+
channel_repair="update the installed plugin for this project"
|
|
358
|
+
elif version_older "$mine" "$plugin_channel_version"; then
|
|
359
|
+
if [ "$tree" = host ]; then
|
|
360
|
+
channel_repair="update the installed Lisa package, then inspect guard/template content differences before choosing a template refresh"
|
|
361
|
+
else
|
|
362
|
+
channel_repair="$PLUGIN_REPAIR"
|
|
363
|
+
fi
|
|
364
|
+
else
|
|
365
|
+
channel_repair="inspect the differing installed channel versions; their release fields do not establish an older channel"
|
|
366
|
+
fi
|
|
367
|
+
channel_skew_line="$channel_skew_line cross-channel vintage SKEW — $source is lisa $mine; the plugin
|
|
440
368
|
installed for this project is lisa $plugin_channel_version at $plugin_channel_path.
|
|
441
|
-
|
|
442
|
-
|
|
369
|
+
These are installed channel versions, not a date for current host guard bytes.
|
|
370
|
+
When both fire on one tool call the agent sees the UNION of their verdicts:
|
|
443
371
|
YOU CANNOT RETIRE A REFUSAL BY SHIPPING A FIX.
|
|
444
|
-
A relaxation on
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
repair: refresh both — \`npx @codyswann/lisa apply\` for this checkout's
|
|
448
|
-
guards, and update the installed plugin for the manifest's copies.
|
|
372
|
+
A relaxation on one channel stays inert until the other catches up.
|
|
373
|
+
repair: $channel_repair;
|
|
374
|
+
host content differences, if any, are reported separately below.
|
|
449
375
|
"
|
|
450
|
-
|
|
376
|
+
compared=1
|
|
377
|
+
else
|
|
378
|
+
compared=1
|
|
379
|
+
fi
|
|
380
|
+
done
|
|
381
|
+
if [ "$compared" -eq 1 ] && [ -z "$channel_skew_line" ]; then
|
|
382
|
+
channel_skew_verdict="agree"
|
|
383
|
+
fi
|
|
451
384
|
}
|
|
452
385
|
|
|
453
386
|
# Description of the last describe_vintage call.
|
|
@@ -479,6 +412,30 @@ describe_vintage() {
|
|
|
479
412
|
fi
|
|
480
413
|
}
|
|
481
414
|
|
|
415
|
+
# Content state is per selected guard. Neither an equal package/receipt
|
|
416
|
+
# version nor some OTHER matching guard can prove this refusing copy current.
|
|
417
|
+
# Unknown evidence suggests inspection, not applying over already-matching code.
|
|
418
|
+
describe_host_guard() {
|
|
419
|
+
vintage_is_stale=1
|
|
420
|
+
local facts="installed lisa ${installed_version:-unknown}; last applied lisa ${last_applied_version:-unknown}"
|
|
421
|
+
case "${host_guard_states[$1]-unknown}" in
|
|
422
|
+
matching)
|
|
423
|
+
vintage_label="matches installed template; $facts"
|
|
424
|
+
vintage_is_stale=0
|
|
425
|
+
;;
|
|
426
|
+
different) vintage_label="DIFFERENT from installed template; $facts" ;;
|
|
427
|
+
*) vintage_label="host content unknown; $facts" ;;
|
|
428
|
+
esac
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
host_guard_repair() {
|
|
432
|
+
if [ "${host_guard_states[$1]-unknown}" = different ]; then
|
|
433
|
+
printf '%s' "$HOST_REPAIR"
|
|
434
|
+
else
|
|
435
|
+
printf 'inspect the selected guard and installed Lisa template evidence (missing, unreadable, invalid or beyond bounds); content freshness is unknown'
|
|
436
|
+
fi
|
|
437
|
+
}
|
|
438
|
+
|
|
482
439
|
# ---------------------------------------------------------------------------
|
|
483
440
|
# Resolution
|
|
484
441
|
#
|
|
@@ -490,6 +447,7 @@ guard_count=0
|
|
|
490
447
|
guard_names=()
|
|
491
448
|
guard_scripts=()
|
|
492
449
|
guard_trees=()
|
|
450
|
+
guard_evidence_names=()
|
|
493
451
|
missing=""
|
|
494
452
|
shadowed=""
|
|
495
453
|
host_tree_used=0
|
|
@@ -504,6 +462,7 @@ for guard in block-no-verify parity-safety-net block-shell-json-parsing \
|
|
|
504
462
|
# The TREE, not its version: vintages are resolved lazily, so what a guard
|
|
505
463
|
# records here is which tree to ask about it later.
|
|
506
464
|
guard_trees+=("host")
|
|
465
|
+
guard_evidence_names+=("$guard")
|
|
507
466
|
guard_count=$((guard_count + 1))
|
|
508
467
|
host_tree_used=1
|
|
509
468
|
# The shadowed copy never runs, and nothing used to say so. Two copies of
|
|
@@ -517,6 +476,7 @@ for guard in block-no-verify parity-safety-net block-shell-json-parsing \
|
|
|
517
476
|
guard_names+=("$guard")
|
|
518
477
|
guard_scripts+=("$plugin_tree/$guard.sh")
|
|
519
478
|
guard_trees+=("plugin")
|
|
479
|
+
guard_evidence_names+=("")
|
|
520
480
|
guard_count=$((guard_count + 1))
|
|
521
481
|
plugin_tree_used=1
|
|
522
482
|
else
|
|
@@ -780,7 +740,19 @@ note_tree_staleness() {
|
|
|
780
740
|
if [ "$notice_due" -eq 1 ]; then
|
|
781
741
|
resolve_vintages
|
|
782
742
|
if [ "$host_tree_used" -eq 1 ]; then
|
|
783
|
-
|
|
743
|
+
host_notice_index=0
|
|
744
|
+
while [ "$host_notice_index" -lt "$guard_count" ]; do
|
|
745
|
+
if [ "${guard_trees[$host_notice_index]}" = host ]; then
|
|
746
|
+
describe_host_guard "$host_notice_index"
|
|
747
|
+
stale_notice="$stale_notice ${guard_scripts[$host_notice_index]} — $vintage_label
|
|
748
|
+
"
|
|
749
|
+
if [ "$vintage_is_stale" -eq 1 ]; then
|
|
750
|
+
stale_notice="$stale_notice repair: $(host_guard_repair "$host_notice_index")
|
|
751
|
+
"
|
|
752
|
+
fi
|
|
753
|
+
fi
|
|
754
|
+
host_notice_index=$((host_notice_index + 1))
|
|
755
|
+
done
|
|
784
756
|
fi
|
|
785
757
|
if [ "$plugin_tree_used" -eq 1 ]; then
|
|
786
758
|
note_tree_staleness "$plugin_tree" "$plugin_tree_version" "$PLUGIN_REPAIR"
|
|
@@ -813,7 +785,7 @@ if [ "$notice_due" -eq 1 ]; then
|
|
|
813
785
|
if [ "$notice_should_print" -eq 1 ]; then
|
|
814
786
|
{
|
|
815
787
|
printf 'Lisa enforcement is running guards from this checkout, not from npm,\n'
|
|
816
|
-
printf '
|
|
788
|
+
printf 'selected host contents are compared with installed templates only for diagnostics.\n'
|
|
817
789
|
if [ -n "$stale_notice" ]; then
|
|
818
790
|
printf '%s' "$stale_notice"
|
|
819
791
|
fi
|
|
@@ -902,7 +874,7 @@ while [ "$index" -lt "$guard_count" ]; do
|
|
|
902
874
|
# vintage is certainly worth its three file reads.
|
|
903
875
|
resolve_vintages
|
|
904
876
|
if [ "${guard_trees[$index]}" = "host" ]; then
|
|
905
|
-
|
|
877
|
+
describe_host_guard "$index"
|
|
906
878
|
else
|
|
907
879
|
describe_vintage "$plugin_tree_version"
|
|
908
880
|
fi
|
|
@@ -943,13 +915,15 @@ while [ "$index" -lt "$guard_count" ]; do
|
|
|
943
915
|
printf '\nTHIS VERDICT MAY NOT REFLECT CURRENT SOURCE — the copy that produced it\n'
|
|
944
916
|
printf 'is not provably current (%s).\n' "$vintage_label"
|
|
945
917
|
if [ "${guard_trees[$index]}" = "host" ]; then
|
|
946
|
-
printf ' repair:
|
|
918
|
+
printf ' repair: '
|
|
919
|
+
host_guard_repair "$index"
|
|
920
|
+
printf '\n'
|
|
947
921
|
else
|
|
948
922
|
printf ' repair: %s\n' "$PLUGIN_REPAIR"
|
|
949
923
|
fi
|
|
950
924
|
printf 'Before filing a defect on this behaviour, read the guard on your\n'
|
|
951
925
|
printf 'integration branch. Re-running the command confirms nothing: it asks\n'
|
|
952
|
-
printf 'the same
|
|
926
|
+
printf 'the same selected copy again.\n'
|
|
953
927
|
} >&2
|
|
954
928
|
fi
|
|
955
929
|
fi
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// This file is managed by Lisa and IS replaced on each `lisa` run.
|
|
2
|
+
// Do not edit directly — durable changes belong upstream in Lisa.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Offline diagnostic evidence for the fallback, never an enforcement decision.
|
|
6
|
+
* Open descriptors bound reads even if a path changes after the size check.
|
|
7
|
+
* Missing Node/helper/evidence leaves Bash's initialized unknown verdicts.
|
|
8
|
+
* @module scripts/lisa-enforcement-freshness
|
|
9
|
+
*/
|
|
10
|
+
import {
|
|
11
|
+
closeSync,
|
|
12
|
+
constants,
|
|
13
|
+
fstatSync,
|
|
14
|
+
openSync,
|
|
15
|
+
readSync,
|
|
16
|
+
statSync,
|
|
17
|
+
} from "node:fs";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
|
|
20
|
+
const METADATA_LIMIT = 64 * 1024;
|
|
21
|
+
const RECORD_LIMIT = 4 * 1024 * 1024;
|
|
22
|
+
const GUARD_LIMIT = 1024 * 1024;
|
|
23
|
+
const GUARDS = new Set([
|
|
24
|
+
"block-no-verify",
|
|
25
|
+
"parity-safety-net",
|
|
26
|
+
"block-shell-json-parsing",
|
|
27
|
+
"block-instruction-file-edits",
|
|
28
|
+
"block-direct-issue-create",
|
|
29
|
+
"block-managed-file-edits",
|
|
30
|
+
"block-blind-automerge",
|
|
31
|
+
"worktree-binding-guard",
|
|
32
|
+
]);
|
|
33
|
+
const VERSION = /^\d{1,6}\.\d{1,6}\.\d{1,6}(?:[-+][A-Za-z0-9.-]{1,48})?$/u;
|
|
34
|
+
|
|
35
|
+
/** A finite regular, readable file, including when the caller is root. */
|
|
36
|
+
function readBounded(file, limit) {
|
|
37
|
+
let fd;
|
|
38
|
+
try {
|
|
39
|
+
// Nonblocking open also prevents a substituted FIFO from stalling a notice.
|
|
40
|
+
fd = openSync(file, constants.O_RDONLY | constants.O_NONBLOCK);
|
|
41
|
+
const before = fstatSync(fd);
|
|
42
|
+
if (!before.isFile() || !(before.mode & 0o444) || before.size > limit)
|
|
43
|
+
return null;
|
|
44
|
+
const buffer = Buffer.alloc(limit + 1);
|
|
45
|
+
let length = 0;
|
|
46
|
+
while (length < buffer.length) {
|
|
47
|
+
const count = readSync(fd, buffer, length, buffer.length - length, null);
|
|
48
|
+
if (!count) break;
|
|
49
|
+
length += count;
|
|
50
|
+
}
|
|
51
|
+
const after = fstatSync(fd);
|
|
52
|
+
const current = statSync(file);
|
|
53
|
+
if (
|
|
54
|
+
length > limit ||
|
|
55
|
+
length !== before.size ||
|
|
56
|
+
after.size !== before.size ||
|
|
57
|
+
after.mtimeMs !== before.mtimeMs ||
|
|
58
|
+
after.ctimeMs !== before.ctimeMs ||
|
|
59
|
+
after.mode !== before.mode ||
|
|
60
|
+
!current.isFile() ||
|
|
61
|
+
!(current.mode & 0o444) ||
|
|
62
|
+
current.size !== before.size ||
|
|
63
|
+
current.mtimeMs !== before.mtimeMs ||
|
|
64
|
+
current.ctimeMs !== before.ctimeMs ||
|
|
65
|
+
current.mode !== before.mode ||
|
|
66
|
+
current.dev !== before.dev ||
|
|
67
|
+
current.ino !== before.ino
|
|
68
|
+
)
|
|
69
|
+
return null;
|
|
70
|
+
return buffer.subarray(0, length);
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
} finally {
|
|
74
|
+
if (fd !== undefined) closeSync(fd);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Malformed, oversized, unreadable and non-object JSON all remain unknown. */
|
|
79
|
+
function json(file, limit = METADATA_LIMIT) {
|
|
80
|
+
try {
|
|
81
|
+
const bytes = readBounded(file, limit);
|
|
82
|
+
const value =
|
|
83
|
+
bytes === null
|
|
84
|
+
? null
|
|
85
|
+
: JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
86
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
87
|
+
? value
|
|
88
|
+
: null;
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function version(value) {
|
|
95
|
+
return typeof value === "string" && VERSION.test(value) ? value : "";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function emit(key, value) {
|
|
99
|
+
if (value) process.stdout.write(`${key}\t${value}\n`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const [root, config, ...names] = process.argv.slice(2);
|
|
103
|
+
if (
|
|
104
|
+
root &&
|
|
105
|
+
config &&
|
|
106
|
+
names.length <= GUARDS.size &&
|
|
107
|
+
names.every(name => name === "" || GUARDS.has(name)) &&
|
|
108
|
+
new Set(names.filter(Boolean)).size === names.filter(Boolean).length
|
|
109
|
+
) {
|
|
110
|
+
const installedRoot = path.join(root, "node_modules/@codyswann/lisa");
|
|
111
|
+
const installed = json(path.join(installedRoot, "package.json"));
|
|
112
|
+
const installedVersion =
|
|
113
|
+
installed?.name === "@codyswann/lisa" ? version(installed.version) : "";
|
|
114
|
+
emit("installed", installedVersion);
|
|
115
|
+
if (names.some(Boolean)) {
|
|
116
|
+
const receipt = json(path.join(root, ".lisa/apply-receipt.json"));
|
|
117
|
+
// Mirror readApplyReceipt's supported schema and required historical fields.
|
|
118
|
+
// Optional fields do not make older supported receipts unusable.
|
|
119
|
+
if (receipt?.schema_version === 1 && typeof receipt.applied_at === "string")
|
|
120
|
+
emit("applied", version(receipt.lisa_version));
|
|
121
|
+
}
|
|
122
|
+
const plugin = json(
|
|
123
|
+
path.join(root, "plugins/lisa/.claude-plugin/plugin.json")
|
|
124
|
+
);
|
|
125
|
+
emit("plugin", plugin?.name === "lisa" ? version(plugin.version) : "");
|
|
126
|
+
const marketplace = json(
|
|
127
|
+
path.join(
|
|
128
|
+
config,
|
|
129
|
+
"plugins/marketplaces/lisa/plugins/lisa/.claude-plugin/plugin.json"
|
|
130
|
+
)
|
|
131
|
+
);
|
|
132
|
+
emit(
|
|
133
|
+
"marketplace",
|
|
134
|
+
marketplace?.name === "lisa" ? version(marketplace.version) : ""
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// Eight pairs / sixteen MiB, plus one overflow-probe byte per file. Only
|
|
138
|
+
// host-selected names are nonempty; plugin-selected guards are not compared.
|
|
139
|
+
for (const [index, name] of names.entries()) {
|
|
140
|
+
let state = "unknown";
|
|
141
|
+
if (installedVersion && name) {
|
|
142
|
+
const host = readBounded(
|
|
143
|
+
path.join(root, "scripts/lisa-hooks", `${name}.sh`),
|
|
144
|
+
GUARD_LIMIT
|
|
145
|
+
);
|
|
146
|
+
const template = readBounded(
|
|
147
|
+
path.join(
|
|
148
|
+
installedRoot,
|
|
149
|
+
"all/copy-overwrite/scripts/lisa-hooks",
|
|
150
|
+
`${name}.sh`
|
|
151
|
+
),
|
|
152
|
+
GUARD_LIMIT
|
|
153
|
+
);
|
|
154
|
+
if (host !== null && template !== null)
|
|
155
|
+
state = host.equals(template) ? "matching" : "different";
|
|
156
|
+
}
|
|
157
|
+
emit(`guard${index}`, state);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const record = json(
|
|
161
|
+
path.join(config, "plugins/installed_plugins.json"),
|
|
162
|
+
RECORD_LIMIT
|
|
163
|
+
);
|
|
164
|
+
const entries = record?.plugins?.["lisa@lisa"];
|
|
165
|
+
if (Array.isArray(entries)) {
|
|
166
|
+
const entry = entries.find(
|
|
167
|
+
candidate =>
|
|
168
|
+
candidate?.projectPath === root &&
|
|
169
|
+
typeof candidate.installPath === "string" &&
|
|
170
|
+
candidate.installPath.length <= 4096 &&
|
|
171
|
+
!/[\x00-\x1f\x7f]/u.test(candidate.installPath) &&
|
|
172
|
+
candidate.installPath.includes("/lisa/lisa/") &&
|
|
173
|
+
version(candidate.version)
|
|
174
|
+
);
|
|
175
|
+
if (entry) {
|
|
176
|
+
emit("channel", version(entry.version));
|
|
177
|
+
emit("channel_path", entry.installPath);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -41,9 +41,23 @@ export const main = async (
|
|
|
41
41
|
const scannerIndex = args.indexOf("--scanner");
|
|
42
42
|
const scanner = scannerIndex < 0 ? undefined : args[scannerIndex + 1];
|
|
43
43
|
const mode = args[0];
|
|
44
|
+
// Git hands a pre-push hook the remote's name (or URL) and URL as $1/$2.
|
|
45
|
+
// Both are optional so an older wrapper that forwards neither keeps the
|
|
46
|
+
// complete-history scan; neither may look like an option.
|
|
47
|
+
const remoteArgs =
|
|
48
|
+
mode === "pre-push"
|
|
49
|
+
? args.slice(1, scannerIndex < 0 ? undefined : scannerIndex)
|
|
50
|
+
: [];
|
|
44
51
|
const accepted =
|
|
45
52
|
mode === "pre-push"
|
|
46
|
-
? [
|
|
53
|
+
? [
|
|
54
|
+
"pre-push",
|
|
55
|
+
...(remoteArgs.length <= 2 &&
|
|
56
|
+
remoteArgs.every(value => value !== "" && !value.startsWith("-"))
|
|
57
|
+
? remoteArgs
|
|
58
|
+
: []),
|
|
59
|
+
...(scanner ? ["--scanner", scanner] : []),
|
|
60
|
+
]
|
|
47
61
|
: [
|
|
48
62
|
"ci",
|
|
49
63
|
...(args[1] === "--event"
|
|
@@ -56,7 +70,7 @@ export const main = async (
|
|
|
56
70
|
!["pre-push", "ci"].includes(mode)
|
|
57
71
|
)
|
|
58
72
|
throw new HistorySecretError(
|
|
59
|
-
"Invalid scanner command. Use pre-push with actual stdin or ci --event <event-json> --event-name <push|pull_request>; optional --scanner <pinned-executable>."
|
|
73
|
+
"Invalid scanner command. Use pre-push [<remote> [<url>]] with actual stdin or ci --event <event-json> --event-name <push|pull_request>; optional --scanner <pinned-executable>."
|
|
60
74
|
);
|
|
61
75
|
const width = objectWidth(cwd);
|
|
62
76
|
let pairs;
|
|
@@ -87,7 +101,7 @@ export const main = async (
|
|
|
87
101
|
width
|
|
88
102
|
);
|
|
89
103
|
}
|
|
90
|
-
const commits = introducedCommits(pairs, cwd, width);
|
|
104
|
+
const commits = introducedCommits(pairs, cwd, width, remoteArgs);
|
|
91
105
|
if (commits.length === 0) {
|
|
92
106
|
console.log("No introduced history. Scanner was not run.");
|
|
93
107
|
return 0;
|