@codyswann/lisa 4.14.0 → 4.15.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/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +7 -2
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/health/deterministic.d.ts +3 -0
- package/dist/health/deterministic.d.ts.map +1 -1
- package/dist/health/deterministic.js +6 -0
- package/dist/health/deterministic.js.map +1 -1
- package/dist/health/ruleset-inspection.d.ts +14 -0
- package/dist/health/ruleset-inspection.d.ts.map +1 -1
- package/dist/health/ruleset-inspection.js +19 -8
- package/dist/health/ruleset-inspection.js.map +1 -1
- package/dist/health/ruleset-reach-inspection.d.ts +68 -0
- package/dist/health/ruleset-reach-inspection.d.ts.map +1 -0
- package/dist/health/ruleset-reach-inspection.js +209 -0
- package/dist/health/ruleset-reach-inspection.js.map +1 -0
- 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/.codex-plugin/skills/lisa-doctor/SKILL.md +31 -0
- package/plugins/lisa/skills/lisa-doctor/SKILL.md +31 -0
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-doctor/SKILL.md +31 -0
- 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/skills/lisa-doctor/SKILL.md +31 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/skills/lisa-doctor/SKILL.md +31 -0
- 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/src/base/skills/lisa-doctor/SKILL.md +31 -0
- package/scripts/lisa-github-rulesets.sh +149 -29
- package/scripts/lisa-ruleset-reach.mjs +512 -0
|
@@ -629,6 +629,37 @@ choice at all — the value is whatever GitHub's default happens to be. Declarin
|
|
|
629
629
|
and makes it visible to the reconciler; declaring nothing keeps the payload byte-for-byte what it
|
|
630
630
|
was, parameter absent.
|
|
631
631
|
|
|
632
|
+
### A ruleset that governs no branch
|
|
633
|
+
|
|
634
|
+
Reconciliation asks whether the required LIST matches. It never asks whether the ruleset carrying
|
|
635
|
+
that list matches any ref the repository actually has, and GitHub does not ask either: a
|
|
636
|
+
`conditions.ref_name.include` entry is a pattern, not a reference, so `refs/heads/dev` is accepted
|
|
637
|
+
and stays live on a repository whose only branch is `main`. Everything that ruleset requires is then
|
|
638
|
+
required nowhere — the gate still runs and posts its context, and no pull request waits on it.
|
|
639
|
+
|
|
640
|
+
Every surface read that as healthy. `compareRulesets` in `lisa health` compares the include list
|
|
641
|
+
against Lisa's template by string equality, and both say `refs/heads/dev`, therefore no drift; the
|
|
642
|
+
repo-map row parses includes but matches the literal `~DEFAULT_BRANCH` and ORs across rows, so one
|
|
643
|
+
governing ruleset makes the whole answer green. Nothing enumerated the repository's branches.
|
|
644
|
+
|
|
645
|
+
The `github.ruleset-reach` check in `lisa health` now does, and so does
|
|
646
|
+
`scripts/lisa-github-rulesets.sh` at the end of every apply, dry run included. Both load one shipped
|
|
647
|
+
detector, `scripts/lisa-ruleset-reach.mjs`, so the two surfaces cannot disagree about which rulesets
|
|
648
|
+
govern nothing. Not in doctor: doctor makes no network call, and which branches exist is a live
|
|
649
|
+
read — the same split that puts the template half of the declaration comparison in doctor and the
|
|
650
|
+
live half in health.
|
|
651
|
+
|
|
652
|
+
Three answers, never two. A ruleset that matches at least one branch is silent. One that matches
|
|
653
|
+
none is named with the patterns that matched nothing. One this run could not classify — an
|
|
654
|
+
unreadable branch list, a `~DEFAULT_BRANCH` whose default branch could not be resolved, an fnmatch
|
|
655
|
+
bracket expression the detector does not model — is `undetermined` and says so, because an unread
|
|
656
|
+
branch list is not an empty repository and reporting it as one would name every ruleset present.
|
|
657
|
+
|
|
658
|
+
**Report only, and neither obvious repair is Lisa's to make.** Creating the missing branch
|
|
659
|
+
manufactures the very ref the ruleset was written to protect; disabling the ruleset gives up a
|
|
660
|
+
protection somebody chose. An automated actor may narrow a control, never loosen one. Point the
|
|
661
|
+
ruleset at a branch the repository has, or retire it deliberately.
|
|
662
|
+
|
|
632
663
|
## Secrets configuration
|
|
633
664
|
|
|
634
665
|
Run the secrets health checks through the skill that owns the contract, rather than reimplementing
|
|
@@ -933,26 +933,32 @@ collect_templates() {
|
|
|
933
933
|
# a repository may legitimately require a status posted by a third-party app or
|
|
934
934
|
# by a job its own CI defines, and a sweep that flagged every externally
|
|
935
935
|
# produced context would be noise an operator learns to ignore.
|
|
936
|
-
|
|
936
|
+
# Fetch every ruleset's DETAIL payload once, for every report-only sweep.
|
|
937
|
+
#
|
|
938
|
+
# The LIST endpoint answers with summaries: id, name, enforcement — and no
|
|
939
|
+
# `rules` and no `conditions`. A sweep run over the list would therefore find
|
|
940
|
+
# nothing on every repository, always, and report it as clean. Each ruleset's
|
|
941
|
+
# required checks and ref conditions only exist on its detail payload, so each
|
|
942
|
+
# one is fetched, and a detail this run could not read is stated rather than
|
|
943
|
+
# skipped.
|
|
944
|
+
#
|
|
945
|
+
# Fetched once and shared. Two sweeps now read the same details — one for
|
|
946
|
+
# retired required contexts, one for branch reach — and fetching twice would
|
|
947
|
+
# double a bounded N+1 of API calls for an answer that cannot differ between
|
|
948
|
+
# them, then report the same unreadable ruleset to the operator twice.
|
|
949
|
+
#
|
|
950
|
+
# Writes the JSON array to $3 rather than stdout, because the warnings it emits
|
|
951
|
+
# for an unreadable ruleset are themselves stdout and would otherwise land in
|
|
952
|
+
# the middle of the payload.
|
|
953
|
+
fetch_ruleset_details() {
|
|
937
954
|
local repo="$1"
|
|
938
955
|
local rulesets="$2"
|
|
939
|
-
local
|
|
940
|
-
local retired
|
|
956
|
+
local outfile="$3"
|
|
941
957
|
|
|
942
|
-
#
|
|
943
|
-
#
|
|
944
|
-
#
|
|
945
|
-
|
|
946
|
-
log_warning "Could not read $registry — retired required contexts were NOT checked. This is not a clean result."
|
|
947
|
-
return 0
|
|
948
|
-
fi
|
|
949
|
-
if ! retired=$(node "$registry" retired-contexts 2>/dev/null); then
|
|
950
|
-
log_warning "Could not derive the retired-context list — retired required contexts were NOT checked. This is not a clean result."
|
|
951
|
-
return 0
|
|
952
|
-
fi
|
|
953
|
-
|
|
954
|
-
# Likewise for the other side of the comparison: zero rulesets read and a
|
|
955
|
-
# repository with nothing required look identical from here.
|
|
958
|
+
# Zero rulesets read and a repository with nothing required look identical
|
|
959
|
+
# from here, and the empty one is the more likely: a token without the
|
|
960
|
+
# ruleset scope, an organization-level ruleset this reader cannot see, and a
|
|
961
|
+
# repository with no protection all arrive as zero rows.
|
|
956
962
|
local -a ids=()
|
|
957
963
|
if [[ -n "$rulesets" ]]; then
|
|
958
964
|
while IFS= read -r id; do
|
|
@@ -960,24 +966,43 @@ report_retired_contexts() {
|
|
|
960
966
|
done < <(echo "$rulesets" | jq -r '.[].id // empty')
|
|
961
967
|
fi
|
|
962
968
|
if [[ ${#ids[@]} -eq 0 ]]; then
|
|
963
|
-
log_warning "No rulesets were readable — retired required contexts were NOT checked. This is not a clean result."
|
|
964
|
-
return
|
|
969
|
+
log_warning "No rulesets were readable — retired required contexts and ruleset branch reach were NOT checked. This is not a clean result."
|
|
970
|
+
return 1
|
|
965
971
|
fi
|
|
966
972
|
|
|
967
|
-
# The LIST endpoint answers with summaries: id, name, enforcement — and no
|
|
968
|
-
# `rules`. A sweep run over the list would therefore find nothing on every
|
|
969
|
-
# repository, always, and report it as clean. Each ruleset's required checks
|
|
970
|
-
# only exist on its detail payload, so each one is fetched, and a detail this
|
|
971
|
-
# run could not read is stated rather than skipped.
|
|
972
973
|
local details="[]"
|
|
973
974
|
local id detail
|
|
974
975
|
for id in "${ids[@]}"; do
|
|
975
976
|
if ! detail=$(gh api -X GET "repos/$repo/rulesets/$id" 2>/dev/null); then
|
|
976
|
-
log_warning "Ruleset $id could not be read — retired required contexts were NOT checked for it. This is not a clean result."
|
|
977
|
+
log_warning "Ruleset $id could not be read — retired required contexts and branch reach were NOT checked for it. This is not a clean result."
|
|
977
978
|
continue
|
|
978
979
|
fi
|
|
979
980
|
details=$(jq -c -n --argjson all "$details" --argjson one "$detail" '$all + [$one]')
|
|
980
981
|
done
|
|
982
|
+
printf '%s' "$details" > "$outfile"
|
|
983
|
+
return 0
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
report_retired_contexts() {
|
|
987
|
+
local details_file="$1"
|
|
988
|
+
local registry="$LISA_ROOT/all/copy-overwrite/scripts/lisa-gates.mjs"
|
|
989
|
+
local retired
|
|
990
|
+
|
|
991
|
+
# An unread retirement list is NOT "no retired contexts". Saying nothing here
|
|
992
|
+
# would report a repository clean on the strength of a read that never
|
|
993
|
+
# happened, which is the same defect one layer up.
|
|
994
|
+
if [[ ! -f "$registry" ]]; then
|
|
995
|
+
log_warning "Could not read $registry — retired required contexts were NOT checked. This is not a clean result."
|
|
996
|
+
return 0
|
|
997
|
+
fi
|
|
998
|
+
if ! retired=$(node "$registry" retired-contexts 2>/dev/null); then
|
|
999
|
+
log_warning "Could not derive the retired-context list — retired required contexts were NOT checked. This is not a clean result."
|
|
1000
|
+
return 0
|
|
1001
|
+
fi
|
|
1002
|
+
|
|
1003
|
+
[[ -s "$details_file" ]] || return 0
|
|
1004
|
+
local details
|
|
1005
|
+
details=$(cat "$details_file")
|
|
981
1006
|
|
|
982
1007
|
# Matched on the retired LABEL as the final context segment, never on the
|
|
983
1008
|
# whole context string. A check run's reported name is the `/`-joined chain
|
|
@@ -1034,6 +1059,94 @@ report_retired_contexts() {
|
|
|
1034
1059
|
echo ""
|
|
1035
1060
|
}
|
|
1036
1061
|
|
|
1062
|
+
##############################################################################
|
|
1063
|
+
# Ruleset Branch Reach
|
|
1064
|
+
##############################################################################
|
|
1065
|
+
|
|
1066
|
+
# Report every ruleset whose include patterns match NO branch this repository
|
|
1067
|
+
# has. CodySwannGT/lisa#2781.
|
|
1068
|
+
#
|
|
1069
|
+
# GitHub accepts an include entry naming a branch that does not exist — the
|
|
1070
|
+
# entries are patterns, not references, and nothing on either side validates
|
|
1071
|
+
# them. So a ruleset can be live, active, and governing nothing, and every
|
|
1072
|
+
# surface reads it as healthy: `compareRulesets` in `src/health` holds the
|
|
1073
|
+
# template against the live ruleset by string equality, and both say
|
|
1074
|
+
# `refs/heads/dev`, therefore not drifted, therefore fine.
|
|
1075
|
+
#
|
|
1076
|
+
# The cost is paid by whoever merges next. A required context is only required
|
|
1077
|
+
# on the refs the ruleset matches, so a gate whose ruleset matches nothing is a
|
|
1078
|
+
# gate that reports and blocks no one — and the operator learns which of those
|
|
1079
|
+
# two it is at the moment they are blocked, or never. This moves it earlier.
|
|
1080
|
+
#
|
|
1081
|
+
# REPORT ONLY, like the sweep above and for a stronger reason. The two obvious
|
|
1082
|
+
# repairs — create the missing branch, or disable the ruleset — are both an
|
|
1083
|
+
# automated actor LOOSENING a control nobody asked it to loosen: the first
|
|
1084
|
+
# manufactures the very ref the ruleset exists to protect, the second gives up
|
|
1085
|
+
# a protection somebody chose. Neither is this script's decision.
|
|
1086
|
+
report_zero_reach_rulesets() {
|
|
1087
|
+
local repo="$1"
|
|
1088
|
+
local details_file="$2"
|
|
1089
|
+
local detector="$LISA_ROOT/scripts/lisa-ruleset-reach.mjs"
|
|
1090
|
+
|
|
1091
|
+
if [[ ! -f "$detector" ]]; then
|
|
1092
|
+
log_warning "Could not read $detector — ruleset branch reach was NOT checked. This is not a clean result."
|
|
1093
|
+
return 0
|
|
1094
|
+
fi
|
|
1095
|
+
[[ -s "$details_file" ]] || return 0
|
|
1096
|
+
|
|
1097
|
+
# An unread branch list is NOT an empty repository, and the difference is the
|
|
1098
|
+
# whole point: comparing include patterns against zero branches would report
|
|
1099
|
+
# EVERY ruleset as governing nothing, which is noise an operator learns to
|
|
1100
|
+
# ignore and is how a real one stops being read.
|
|
1101
|
+
local branch_pages branch_names
|
|
1102
|
+
if ! branch_pages=$(gh api --paginate "repos/$repo/branches?per_page=100" 2>/dev/null); then
|
|
1103
|
+
log_warning "Could not list this repository's branches — ruleset branch reach was NOT checked. This is not a clean result."
|
|
1104
|
+
return 0
|
|
1105
|
+
fi
|
|
1106
|
+
# `--paginate` emits one JSON array PER PAGE rather than one merged array, so
|
|
1107
|
+
# this filter is applied to a stream of arrays; jq reads each top-level value
|
|
1108
|
+
# in turn, which is exactly the shape needed. A filter written for a single
|
|
1109
|
+
# array would silently read page one only.
|
|
1110
|
+
if ! branch_names=$(jq -r '.[] | .name? // empty' <<< "$branch_pages" 2>/dev/null); then
|
|
1111
|
+
log_warning "Could not read this repository's branch list — ruleset branch reach was NOT checked. This is not a clean result."
|
|
1112
|
+
return 0
|
|
1113
|
+
fi
|
|
1114
|
+
if [[ -z "$branch_names" ]]; then
|
|
1115
|
+
log_warning "No branch was readable — ruleset branch reach was NOT checked. This is not a clean result."
|
|
1116
|
+
return 0
|
|
1117
|
+
fi
|
|
1118
|
+
|
|
1119
|
+
# Read separately, and NOT defaulted when unreadable. `~DEFAULT_BRANCH` is
|
|
1120
|
+
# the include entry most rulesets rely on, and guessing which branch it names
|
|
1121
|
+
# would decide the whole verdict from a guess. The detector answers
|
|
1122
|
+
# `undetermined` for a ruleset it cannot resolve it for.
|
|
1123
|
+
local default_branch=""
|
|
1124
|
+
default_branch=$(gh api "repos/$repo" --jq '.default_branch' 2>/dev/null) || default_branch=""
|
|
1125
|
+
|
|
1126
|
+
local branches_file report
|
|
1127
|
+
branches_file=$(mktemp)
|
|
1128
|
+
printf '%s\n' "$branch_names" > "$branches_file"
|
|
1129
|
+
# A detector that could not run is NOT a repository with nothing to report.
|
|
1130
|
+
# Reading its failure as silence is the same defect one layer up, sited on
|
|
1131
|
+
# the detector built to catch it.
|
|
1132
|
+
if ! report=$(node "$detector" \
|
|
1133
|
+
--rulesets="$details_file" \
|
|
1134
|
+
--branches="$branches_file" \
|
|
1135
|
+
--default-branch="$default_branch" 2>/dev/null); then
|
|
1136
|
+
rm -f "$branches_file"
|
|
1137
|
+
log_warning "The ruleset reach detector could not run — ruleset branch reach was NOT checked. This is not a clean result."
|
|
1138
|
+
return 0
|
|
1139
|
+
fi
|
|
1140
|
+
rm -f "$branches_file"
|
|
1141
|
+
|
|
1142
|
+
if [[ -z "$report" ]]; then
|
|
1143
|
+
log_verbose "Every ruleset in this repository governs at least one branch."
|
|
1144
|
+
return 0
|
|
1145
|
+
fi
|
|
1146
|
+
echo ""
|
|
1147
|
+
echo "$report"
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1037
1150
|
main() {
|
|
1038
1151
|
# Parse arguments
|
|
1039
1152
|
while [[ $# -gt 0 ]]; do
|
|
@@ -1171,10 +1284,17 @@ main() {
|
|
|
1171
1284
|
fi
|
|
1172
1285
|
done
|
|
1173
1286
|
|
|
1174
|
-
#
|
|
1175
|
-
#
|
|
1176
|
-
# exactly the run that most
|
|
1177
|
-
|
|
1287
|
+
# Both sweeps run whatever the apply did, dry-run included, and both read the
|
|
1288
|
+
# SAME detail payloads. What they report lives in rulesets the apply above
|
|
1289
|
+
# never touched, so an apply that "succeeded" is exactly the run that most
|
|
1290
|
+
# needs to say this.
|
|
1291
|
+
local ruleset_details
|
|
1292
|
+
ruleset_details=$(mktemp)
|
|
1293
|
+
if fetch_ruleset_details "$repo" "$existing_rulesets" "$ruleset_details"; then
|
|
1294
|
+
report_retired_contexts "$ruleset_details"
|
|
1295
|
+
report_zero_reach_rulesets "$repo" "$ruleset_details"
|
|
1296
|
+
fi
|
|
1297
|
+
rm -f "$ruleset_details"
|
|
1178
1298
|
|
|
1179
1299
|
echo ""
|
|
1180
1300
|
if [[ "$DRY_RUN" == "true" ]]; then
|