@codyswann/lisa 4.13.2 → 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.
Files changed (83) hide show
  1. package/dist/core/lisa-owned-hash-ledger.d.ts +17 -0
  2. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  3. package/dist/core/lisa-owned-hash-ledger.js +601 -0
  4. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  5. package/dist/core/lisa-owned-provenance.d.ts +30 -0
  6. package/dist/core/lisa-owned-provenance.d.ts.map +1 -1
  7. package/dist/core/lisa-owned-provenance.js +28 -1
  8. package/dist/core/lisa-owned-provenance.js.map +1 -1
  9. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  10. package/dist/core/upstream-evidence-manifest.js +9 -3
  11. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  12. package/dist/health/deterministic.d.ts +3 -0
  13. package/dist/health/deterministic.d.ts.map +1 -1
  14. package/dist/health/deterministic.js +6 -0
  15. package/dist/health/deterministic.js.map +1 -1
  16. package/dist/health/ruleset-inspection.d.ts +14 -0
  17. package/dist/health/ruleset-inspection.d.ts.map +1 -1
  18. package/dist/health/ruleset-inspection.js +19 -8
  19. package/dist/health/ruleset-inspection.js.map +1 -1
  20. package/dist/health/ruleset-reach-inspection.d.ts +68 -0
  21. package/dist/health/ruleset-reach-inspection.d.ts.map +1 -0
  22. package/dist/health/ruleset-reach-inspection.js +209 -0
  23. package/dist/health/ruleset-reach-inspection.js.map +1 -0
  24. package/package.json +1 -1
  25. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  27. package/plugins/lisa/.codex-plugin/skills/lisa-doctor/SKILL.md +31 -0
  28. package/plugins/lisa/skills/lisa-doctor/SKILL.md +31 -0
  29. package/plugins/lisa-agy/plugin.json +1 -1
  30. package/plugins/lisa-agy/skills/lisa-doctor/SKILL.md +31 -0
  31. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  33. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  34. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-copilot/skills/lisa-doctor/SKILL.md +31 -0
  38. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-cursor/skills/lisa-doctor/SKILL.md +31 -0
  40. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  42. package/plugins/lisa-expo-agy/plugin.json +1 -1
  43. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  47. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  48. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  52. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  53. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  57. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  58. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  62. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  63. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  67. package/plugins/lisa-rails-agy/plugin.json +1 -1
  68. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  72. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  73. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  77. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  78. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  80. package/plugins/src/base/skills/lisa-doctor/SKILL.md +31 -0
  81. package/scripts/generate-lisa-owned-hash-ledger.mjs +455 -64
  82. package/scripts/lisa-github-rulesets.sh +149 -29
  83. package/scripts/lisa-ruleset-reach.mjs +512 -0
@@ -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
- report_retired_contexts() {
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 registry="$LISA_ROOT/all/copy-overwrite/scripts/lisa-gates.mjs"
940
- local retired
956
+ local outfile="$3"
941
957
 
942
- # An unread retirement list is NOT "no retired contexts". Saying nothing here
943
- # would report a repository clean on the strength of a read that never
944
- # happened, which is the same defect one layer up.
945
- if [[ ! -f "$registry" ]]; then
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 0
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
- # Runs whatever the apply did, dry-run included. The retired name lives in a
1175
- # ruleset the apply above never touched, so an apply that "succeeded" is
1176
- # exactly the run that most needs to say this.
1177
- report_retired_contexts "$repo" "$existing_rulesets"
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