@codyswann/lisa 3.48.3 → 3.48.5

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 (64) hide show
  1. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  2. package/dist/core/upstream-evidence-manifest.js +7 -5
  3. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  4. package/expo/package-lisa/package.lisa.json +1 -0
  5. package/nestjs/package-lisa/package.lisa.json +1 -0
  6. package/package.json +1 -1
  7. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  8. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  9. package/plugins/lisa/.codex-plugin/skills/lisa-setup-github-repo/SKILL.md +1 -0
  10. package/plugins/lisa/skills/lisa-setup-github-repo/SKILL.md +1 -0
  11. package/plugins/lisa-agy/plugin.json +1 -1
  12. package/plugins/lisa-agy/skills/lisa-setup-github-repo/SKILL.md +1 -0
  13. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  14. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  15. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  16. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  17. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  18. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  19. package/plugins/lisa-copilot/skills/lisa-setup-github-repo/SKILL.md +1 -0
  20. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-cursor/skills/lisa-setup-github-repo/SKILL.md +1 -0
  22. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  24. package/plugins/lisa-expo-agy/plugin.json +1 -1
  25. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  29. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  30. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  34. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  35. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  39. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  40. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  44. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  45. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  49. package/plugins/lisa-rails-agy/plugin.json +1 -1
  50. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  54. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  55. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  59. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  60. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  62. package/plugins/src/base/skills/lisa-setup-github-repo/SKILL.md +1 -0
  63. package/scripts/lisa-github-rulesets.sh +97 -14
  64. package/typescript/copy-contents/.husky/pre-push +32 -2
@@ -39,6 +39,11 @@ YES_MODE=false
39
39
  VERBOSE=false
40
40
  PROJECT_PATH=""
41
41
 
42
+ # Rulesets that already matched and were therefore not sent, and the payload
43
+ # GitHub last accepted (set by apply_with_integration_fallback).
44
+ UNCHANGED_COUNT=0
45
+ APPLIED_PAYLOAD=""
46
+
42
47
  # Project type hierarchy (child -> parent)
43
48
  # Using a function to avoid associative array issues with set -u
44
49
  get_parent_type() {
@@ -428,22 +433,14 @@ add_config_required_checks() {
428
433
  }
429
434
 
430
435
  preserve_live_required_checks() {
431
- local repo="$1"
432
- local existing_id="$2"
433
- local json="$3"
434
- local ruleset_name="$4"
436
+ local live="$1"
437
+ local json="$2"
435
438
 
436
- if [[ -z "$existing_id" ]]; then
439
+ if [[ -z "$live" ]]; then
437
440
  echo "$json"
438
441
  return 0
439
442
  fi
440
443
 
441
- local live
442
- if ! live=$(gh api -X GET "repos/$repo/rulesets/$existing_id" 2>/dev/null); then
443
- log_warning "Could not read existing ruleset '$ruleset_name' details — refusing to silently replace required checks" >&2
444
- return 1
445
- fi
446
-
447
444
  echo "$json" | jq --argjson live "$live" '
448
445
  def required_checks:
449
446
  (.rules // [])
@@ -475,6 +472,73 @@ preserve_live_required_checks() {
475
472
  end'
476
473
  }
477
474
 
475
+ # The contexts the outgoing payload requires that the live ruleset does not.
476
+ # This is the drift a template change opens: a context added to a template never
477
+ # reaches an already-configured repository on its own (#2641), so the operator
478
+ # needs the reconciliation to SAY which checks it just made blocking rather than
479
+ # to report an opaque "applied".
480
+ ruleset_added_contexts() {
481
+ local live="$1"
482
+ local payload="$2"
483
+
484
+ jq -r -n --argjson live "${live:-null}" --argjson want "$payload" '
485
+ def contexts:
486
+ [ (.rules // [])[]
487
+ | select(.type == "required_status_checks")
488
+ | (.parameters.required_status_checks // [])[]
489
+ | .context ];
490
+
491
+ (if $live == null then [] else ($live | contexts) end) as $have
492
+ | ($want | contexts)
493
+ | map(select(. as $context | ($have | index($context)) | not))
494
+ | .[]'
495
+ }
496
+
497
+ # True when everything the payload asks for is ALREADY true of the live ruleset.
498
+ #
499
+ # Deliberately a subset test rather than an equality test: GitHub echoes back
500
+ # parameters it filled in with its own defaults (`required_reviewers`,
501
+ # `require_extra_approval_for_unattributed_changes`, ...) which no template
502
+ # names. Comparing whole documents would report drift on every run and make
503
+ # "nothing to do" unreachable — the AC's second scenario would then never hold
504
+ # even on a repository that needs no change.
505
+ ruleset_is_current() {
506
+ local live="$1"
507
+ local payload="$2"
508
+
509
+ [[ -n "$live" ]] || return 1
510
+
511
+ jq -e -n \
512
+ --argjson live "$(strip_readonly_fields "$live")" \
513
+ --argjson want "$payload" '
514
+ def covers($have; $need):
515
+ if ($need | type) == "object" then
516
+ ($have | type) == "object"
517
+ and ([$need | keys_unsorted[]]
518
+ | all(. as $key | ($have | has($key)) and covers($have[$key]; $need[$key])))
519
+ elif ($need | type) == "array" then
520
+ ($have | type) == "array"
521
+ and ($have | length) == ($need | length)
522
+ and ([range(0; $need | length)]
523
+ | all(. as $index | covers($have[$index]; $need[$index])))
524
+ else $have == $need
525
+ end;
526
+ covers($live; $want)' > /dev/null
527
+ }
528
+
529
+ # Print one line per context this run makes blocking. Reads the payload that
530
+ # GitHub actually ACCEPTED, so a context dropped by the integration fallback is
531
+ # never reported as added.
532
+ report_added_contexts() {
533
+ local live="$1"
534
+ local payload="$2"
535
+
536
+ local context
537
+ while IFS= read -r context; do
538
+ [[ -n "$context" ]] && log_info " + now required: $context"
539
+ done < <(ruleset_added_contexts "$live" "$payload")
540
+ }
541
+
478
542
  apply_ruleset() {
479
543
  local repo="$1"
480
544
  local template_file="$2"
@@ -499,10 +563,22 @@ apply_ruleset() {
499
563
  return 0
500
564
  fi
501
565
 
502
- local existing_id
566
+ local existing_id live=""
503
567
  existing_id=$(find_ruleset_by_name "$existing_rulesets" "$ruleset_name")
504
568
  if [[ -n "$existing_id" ]]; then
505
- clean_template=$(preserve_live_required_checks "$repo" "$existing_id" "$clean_template" "$ruleset_name")
569
+ if ! live=$(gh api -X GET "repos/$repo/rulesets/$existing_id" 2>/dev/null); then
570
+ log_warning "Could not read existing ruleset '$ruleset_name' details — refusing to silently replace required checks"
571
+ return 1
572
+ fi
573
+ clean_template=$(preserve_live_required_checks "$live" "$clean_template")
574
+ fi
575
+
576
+ # Idempotence: a run that would change nothing must say so and send nothing,
577
+ # so a second run is visibly a no-op rather than an indistinguishable write.
578
+ if ruleset_is_current "$live" "$clean_template"; then
579
+ log_success "Ruleset '$ruleset_name' already matches the template — nothing to do"
580
+ UNCHANGED_COUNT=$((UNCHANGED_COUNT + 1))
581
+ return 0
506
582
  fi
507
583
 
508
584
  if [[ "$DRY_RUN" == "true" ]]; then
@@ -511,6 +587,7 @@ apply_ruleset() {
511
587
  else
512
588
  log_info "[DRY RUN] Would create ruleset '$ruleset_name'"
513
589
  fi
590
+ report_added_contexts "$live" "$clean_template"
514
591
  log_verbose "Template: $template_file"
515
592
  return 0
516
593
  fi
@@ -521,7 +598,9 @@ apply_ruleset() {
521
598
  log_info "Creating ruleset '$ruleset_name'..."
522
599
  fi
523
600
 
601
+ APPLIED_PAYLOAD=""
524
602
  if apply_with_integration_fallback "$repo" "$ruleset_name" "$clean_template" "$existing_id"; then
603
+ report_added_contexts "$live" "${APPLIED_PAYLOAD:-$clean_template}"
525
604
  return 0
526
605
  fi
527
606
  log_error "Failed to apply ruleset '$ruleset_name'"
@@ -546,6 +625,7 @@ apply_with_integration_fallback() {
546
625
  # Capture stdout AND stderr — gh prints the API error body (which carries
547
626
  # the "Invalid integration ids" detail) on stdout, not stderr.
548
627
  send_ruleset() {
628
+ APPLIED_PAYLOAD="$1"
549
629
  echo "$1" > "$temp_file"
550
630
  if [[ -n "$existing_id" ]]; then
551
631
  error_output=$(gh api -X PUT "repos/$repo/rulesets/$existing_id" --input "$temp_file" 2>&1)
@@ -772,7 +852,10 @@ main() {
772
852
  if [[ "$DRY_RUN" == "true" ]]; then
773
853
  log_info "Dry run complete. ${#templates[@]} ruleset(s) would be applied."
774
854
  else
775
- log_success "Applied $success_count ruleset(s)"
855
+ log_success "Applied $((success_count - UNCHANGED_COUNT)) ruleset(s), $UNCHANGED_COUNT already current"
856
+ if [[ $UNCHANGED_COUNT -eq $success_count ]]; then
857
+ log_info "Nothing to do — every ruleset already matches its template"
858
+ fi
776
859
  if [[ $fail_count -gt 0 ]]; then
777
860
  log_warning "$fail_count ruleset(s) failed"
778
861
  exit 1
@@ -499,11 +499,41 @@ fi
499
499
  # Run unit tests with coverage. `test:cov` proves two properties in one run —
500
500
  # the suite passes AND the coverage thresholds hold — so it stands down only
501
501
  # when both have a gate declared.
502
+ #
503
+ # WHICH coverage script, and why it decides the cost of every push (#2827).
504
+ # `test:cov` is force-pinned to `vitest run --coverage` with no integration
505
+ # exclusion, so it collects the whole `tests/integration/**` tree — and the
506
+ # integration step below then collects the same tree a second time. Every push
507
+ # paid twice for the slowest population in the repository, and every push had
508
+ # two independent chances for a spawn-heavy integration suite to block it on
509
+ # unchanged code.
510
+ #
511
+ # This is the path that matters, because it is the path host projects take.
512
+ # Nothing seeds a `gates` block into a consumer's `.lisa.config.json`, so the
513
+ # gate runner above exits NO_GATES for them and control arrives here. Fixing
514
+ # only the declared path would have fixed only the Lisa repository itself.
515
+ #
516
+ # `test:cov:unit` is the same run scoped to the unit tree, so the coverage step
517
+ # and the integration step stop overlapping. It is force-pinned by the
518
+ # TypeScript template, so any project on a current Lisa has it. A project whose
519
+ # package.json predates it falls back to `test:cov` and behaves exactly as it
520
+ # did before — slower, but never with the integration tree silently dropped,
521
+ # which is the failure direction that would actually cost something.
522
+ #
523
+ # Resolved with node rather than jq: this hook already refuses to run at all
524
+ # without node, whereas jq may legitimately be absent — the audit block above
525
+ # says so in as many words. Deciding this with jq would hand every jq-less
526
+ # machine the slow path without a word.
527
+ COVERAGE_SCRIPT="test:cov"
528
+ if node -e 'const s=require("./package.json").scripts||{};process.exit(s["test:cov:unit"]?0:1)' >/dev/null 2>&1; then
529
+ COVERAGE_SCRIPT="test:cov:unit"
530
+ fi
531
+
502
532
  if lisa_gate_covers test-correctness coverage-adequacy; then
503
533
  echo "ℹ️ Covered by the test-correctness and coverage-adequacy gates; the built-in run stands down."
504
534
  else
505
- echo "🧪 Running unit tests with coverage..."
506
- $RUNNER test:cov
535
+ echo "🧪 Running unit tests with coverage ($COVERAGE_SCRIPT)..."
536
+ $RUNNER "$COVERAGE_SCRIPT"
507
537
  if [ $? -ne 0 ]; then
508
538
  echo "❌ Unit tests or coverage thresholds failed. Please fix before pushing."
509
539
  exit 1