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