Merge "Prevent diff view nav links from overflowing the header"
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index a1fa859..306ba33 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -7908,7 +7908,7 @@
 Notify handling that defines to whom email notifications should be sent
 after the change is created. +
 Allowed values are `NONE`, `OWNER`, `OWNER_REVIEWERS` and `ALL`. +
-If not set, the default is `ALL`.
+If not set, the default is `OWNER` for WIP changes and `ALL` otherwise.
 |`notify_details`     |optional|
 Additional information about whom to notify about the change creation
 as a map of link:user-notify.html#recipient-types[recipient type] to
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 5d40e2d..fadbad9 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -724,6 +724,22 @@
 Valid relations are >=, >, \<=, <, or no relation, which will match if the number of unresolved
 comments is exactly equal.
 
+[[unmet_requirement]]
+unmet_requirement:'SUBMIT_REQUIREMENT_NAME'::
++
+Matches changes where the given submit requirement is evaluated and unmet (its result is UNSATISFIED, ERROR, or TIMEOUT). The requirement name comparison is case-insensitive.
+
+[[unsatisfied_requirement_count]]
+unsatisfied_requirement_count:'RELATION''NUMBER'::
++
+True if the number of unsatisfied submit requirements satisfies the given relation for the given number.
++
+For example, unsatisfied_requirement_count:>0 will be true for any change which has at least one unsatisfied
+submit requirement while unsatisfied_requirement_count:0 will be true for any change which has all submit requirements resolved.
++
+Valid relations are >=, >, \<=, <, or no relation, which will match if the number of unsatisfied
+submit requirements is exactly equal.
+
 == Argument Quoting
 
 Operator values that are not bare words (roughly A-Z, a-z, 0-9, @,
diff --git a/java/com/google/gerrit/entities/converter/ChangeInputProtoConverter.java b/java/com/google/gerrit/entities/converter/ChangeInputProtoConverter.java
index 7270af8..053c077 100644
--- a/java/com/google/gerrit/entities/converter/ChangeInputProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/ChangeInputProtoConverter.java
@@ -98,7 +98,9 @@
     if (changeInput.author != null) {
       builder.setAuthor(accountInputConverter.toProto(changeInput.author));
     }
-    builder.setNotify(Entities.NotifyHandling.forNumber(changeInput.notify.getValue()));
+    if (changeInput.notify != null) {
+      builder.setNotify(Entities.NotifyHandling.forNumber(changeInput.notify.getValue()));
+    }
 
     List<ListChangesOption> responseFormatOptions = changeInput.responseFormatOptions;
     if (responseFormatOptions != null) {
@@ -166,7 +168,9 @@
       }
     }
 
-    changeInput.notify = NotifyHandling.valueOf(proto.getNotify().name());
+    if (proto.hasNotify()) {
+      changeInput.notify = NotifyHandling.valueOf(proto.getNotify().name());
+    }
 
     if (proto.getNotifyDetailsCount() > 0) {
       changeInput.notifyDetails = new HashMap<>();
diff --git a/java/com/google/gerrit/extensions/common/ChangeInput.java b/java/com/google/gerrit/extensions/common/ChangeInput.java
index 2e2b9ca..07289a2 100644
--- a/java/com/google/gerrit/extensions/common/ChangeInput.java
+++ b/java/com/google/gerrit/extensions/common/ChangeInput.java
@@ -62,8 +62,12 @@
     this.subject = subject;
   }
 
-  /** Who to send email notifications to after change is created. */
-  public NotifyHandling notify = NotifyHandling.ALL;
+  /**
+   * Who to send email notifications to after change is created. If not specified, defaults to
+   * {@link NotifyHandling#OWNER} if the change is created as work-in-progress, or {@link
+   * NotifyHandling#ALL} otherwise.
+   */
+  public NotifyHandling notify;
 
   public Map<RecipientType, NotifyInfo> notifyDetails;
 }
diff --git a/java/com/google/gerrit/server/change/AddReviewersOp.java b/java/com/google/gerrit/server/change/AddReviewersOp.java
index 239fa7a..43598e4 100644
--- a/java/com/google/gerrit/server/change/AddReviewersOp.java
+++ b/java/com/google/gerrit/server/change/AddReviewersOp.java
@@ -82,6 +82,7 @@
   private final Collection<Address> addresses;
   private final ReviewerState state;
   private final boolean forGroup;
+  private boolean allowDowngradeToCc = true;
 
   // Unlike addedCCs, addedReviewers is a PatchSetApproval because the ReviewerResult returned
   // via the REST API is supposed to include vote information.
@@ -118,6 +119,10 @@
     this.forGroup = forGroup;
   }
 
+  public void setAllowDowngradeToCc(boolean allowDowngradeToCc) {
+    this.allowDowngradeToCc = allowDowngradeToCc;
+  }
+
   @Override
   public boolean updateChange(ChangeContext ctx) throws RestApiException, IOException {
     change = ctx.getChange();
@@ -130,7 +135,10 @@
       if (state == CC) {
         addedCCs =
             approvalsUtil.addCcs(
-                ctx.getNotes(), ctx.getUpdate(change.currentPatchSetId()), accountIds, forGroup);
+                ctx.getNotes(),
+                ctx.getUpdate(change.currentPatchSetId()),
+                accountIds,
+                forGroup || !allowDowngradeToCc);
       } else {
         addedReviewers =
             approvalsUtil.addReviewers(
diff --git a/java/com/google/gerrit/server/change/ReviewerModifier.java b/java/com/google/gerrit/server/change/ReviewerModifier.java
index 92dd9db..8fdb888 100644
--- a/java/com/google/gerrit/server/change/ReviewerModifier.java
+++ b/java/com/google/gerrit/server/change/ReviewerModifier.java
@@ -130,6 +130,13 @@
 
     /** Whether the visibility check for the reviewer account should be skipped. */
     public boolean skipVisibilityCheck = false;
+
+    /**
+     * Whether an existing REVIEWER should be downgraded to CC if the input state is CC.
+     *
+     * <p>If false, and the account is already a REVIEWER, the state will remain REVIEWER.
+     */
+    public boolean allowDowngradeToCc = true;
   }
 
   public static InternalReviewerInput newReviewerInput(
@@ -161,6 +168,10 @@
     in.state = CC;
     in.notify = notify;
     in.otherFailureBehavior = FailureBehavior.IGNORE_ALL;
+    // Automatic addition of author/committer as CC is an implicit system action on push.
+    // Preserve existing REVIEWER status so implicit auto-CC does not demote explicitly assigned
+    // reviewers.
+    in.allowDowngradeToCc = false;
     return Optional.of(in);
   }
 
@@ -527,6 +538,9 @@
                 this.reviewersByEmail,
                 state(),
                 forGroup);
+        if (input instanceof InternalReviewerInput internalInput) {
+          ((AddReviewersOp) op).setAllowDowngradeToCc(internalInput.allowDowngradeToCc);
+        }
       }
       this.exactMatchFound = exactMatchFound;
     }
diff --git a/java/com/google/gerrit/server/git/WorkQueue.java b/java/com/google/gerrit/server/git/WorkQueue.java
index 6209854..2b3bdba 100644
--- a/java/com/google/gerrit/server/git/WorkQueue.java
+++ b/java/com/google/gerrit/server/git/WorkQueue.java
@@ -995,11 +995,11 @@
     private void setThreadName(String oldThreadName) {
       try {
         Thread.currentThread().setName(oldThreadName + "[" + this + "]");
-      } catch (Exception e) {
+      } catch (RuntimeException e) {
         logger.atWarning().withCause(e).log("Cannot describe task");
         try {
           Thread.currentThread().setName(oldThreadName + "[" + runnable.getClass().getName() + "]");
-        } catch (Exception e2) {
+        } catch (RuntimeException e2) {
           logger.atWarning().withCause(e2).log("Cannot get runnable class name");
           Thread.currentThread().setName(oldThreadName + "[unknown task]");
         }
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 6c11800..9737d4d 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -2639,7 +2639,8 @@
 
       if (idList.isEmpty()) {
         messages.add(
-            new ValidationMessage("warning: pushing without Change-Id is deprecated", false));
+            new ValidationMessage(
+                "pushing without Change-Id is deprecated", ValidationMessage.Type.WARNING));
         break;
       }
     }
@@ -3566,11 +3567,12 @@
           if (messageEq && parentsEq && authorEq) {
             addMessage(
                 String.format(
-                    "warning: no changes between prior commit %s and new commit %s",
-                    abbreviateName(priorCommit, reader), abbreviateName(newCommit, reader)));
+                    "no changes between prior commit %s and new commit %s",
+                    abbreviateName(priorCommit, reader), abbreviateName(newCommit, reader)),
+                ValidationMessage.Type.WARNING);
           } else {
             StringBuilder msg = new StringBuilder();
-            msg.append("warning: ").append(abbreviateName(newCommit, reader));
+            msg.append("commit ").append(abbreviateName(newCommit, reader));
             msg.append(":");
             msg.append(" no files changed");
             if (!authorEq) {
@@ -3582,7 +3584,7 @@
             if (!parentsEq) {
               msg.append(", was rebased");
             }
-            addMessage(msg.toString());
+            addMessage(msg.toString(), ValidationMessage.Type.WARNING);
           }
         }
       }
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index e26165a..f7f1ce1 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -1674,6 +1674,51 @@
       STORED_SUBMIT_REQUIREMENTS_SPEC =
           STORED_SUBMIT_REQUIREMENTS_FIELD.storedOnly("full_submit_requirements");
 
+  /**
+   * Names of submit requirements that are not fulfilled for a change (i.e., whose evaluation status
+   * is {@link com.google.gerrit.entities.SubmitRequirementResult.Status#UNSATISFIED}, {@link
+   * com.google.gerrit.entities.SubmitRequirementResult.Status#ERROR}, or {@link
+   * com.google.gerrit.entities.SubmitRequirementResult.Status#TIMEOUT}).
+   *
+   * <p>Note that if evaluating a submit requirement results in an ERROR or TIMEOUT, it is
+   * considered unfulfilled and its name will be included in this field.
+   */
+  public static final IndexedField<ChangeData, Iterable<String>> UNMET_REQUIREMENT_FIELD =
+      IndexedField.<ChangeData>iterableStringBuilder("UnmetRequirement")
+          .build(
+              cd ->
+                  cd.submitRequirementsIncludingLegacy().values().stream()
+                      .filter(sr -> !sr.fulfilled())
+                      .map(sr -> sr.submitRequirement().name().toLowerCase(Locale.US))
+                      .collect(toImmutableSet()));
+
+  public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec UNMET_REQUIREMENT_SPEC =
+      UNMET_REQUIREMENT_FIELD.exact("unmet_requirement");
+
+  /**
+   * The number of submit requirements that are not fulfilled for a change (i.e., whose evaluation
+   * status is {@link com.google.gerrit.entities.SubmitRequirementResult.Status#UNSATISFIED}, {@link
+   * com.google.gerrit.entities.SubmitRequirementResult.Status#ERROR}, or {@link
+   * com.google.gerrit.entities.SubmitRequirementResult.Status#TIMEOUT}).
+   *
+   * <p>Note that if evaluating a submit requirement results in an ERROR or TIMEOUT, it is
+   * considered unfulfilled and is counted here.
+   */
+  public static final IndexedField<ChangeData, Integer> UNSATISFIED_REQUIREMENT_COUNT_FIELD =
+      IndexedField.<ChangeData>integerBuilder("UnsatisfiedRequirementCount")
+          .stored()
+          .build(
+              cd ->
+                  (int)
+                      cd.submitRequirementsIncludingLegacy().values().stream()
+                          .filter(sr -> !sr.fulfilled())
+                          .count(),
+              (cd, field) -> cd.setUnsatisfiedRequirementCount(field));
+
+  public static final IndexedField<ChangeData, Integer>.SearchSpec
+      UNSATISFIED_REQUIREMENT_COUNT_SPEC =
+          UNSATISFIED_REQUIREMENT_COUNT_FIELD.integerRange("unsatisfied_requirement_count");
+
   private static void parseSubmitRequirements(
       Iterable<Cache.SubmitRequirementResultProto> values, ChangeData out) {
     out.setSubmitRequirements(
diff --git a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index aaa7535..809dd0a 100644
--- a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -274,6 +274,7 @@
   @Deprecated static final Schema<ChangeData> V87 = schema(V86);
 
   /** Add REVIEWERS_COUNT_FIELD */
+  @Deprecated
   static final Schema<ChangeData> V88 =
       new Schema.Builder<ChangeData>()
           .add(V87)
@@ -281,6 +282,16 @@
           .addSearchSpecs(ChangeField.REVIEWER_COUNT_SPEC)
           .build();
 
+  /** Add met and unmet requirement tracking fields */
+  static final Schema<ChangeData> V89 =
+      new Schema.Builder<ChangeData>()
+          .add(V88)
+          .addIndexedFields(
+              ChangeField.UNMET_REQUIREMENT_FIELD, ChangeField.UNSATISFIED_REQUIREMENT_COUNT_FIELD)
+          .addSearchSpecs(
+              ChangeField.UNMET_REQUIREMENT_SPEC, ChangeField.UNSATISFIED_REQUIREMENT_COUNT_SPEC)
+          .build();
+
   /**
    * Name of the change index to be used when contacting index backends or loading configurations.
    */
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index a3b1e3e..6dee3eb 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -443,6 +443,7 @@
       Maps.newLinkedHashMapWithExpectedSize(1);
 
   private Map<SubmitRequirement, SubmitRequirementResult> submitRequirements;
+  private Integer unsatisfiedRequirementCount;
 
   private StorageConstraint storageConstraint = StorageConstraint.NOTEDB_ONLY;
   private Change change;
@@ -1307,6 +1308,14 @@
         projectConfigReqs, legacyReqs, this);
   }
 
+  public Integer unsatisfiedRequirementCount() {
+    return unsatisfiedRequirementCount;
+  }
+
+  public void setUnsatisfiedRequirementCount(Integer count) {
+    this.unsatisfiedRequirementCount = count;
+  }
+
   /**
    * Get all evaluated submit requirements for this change, including those from parent projects.
    * For closed changes, submit requirements are read from the change notes. For active changes,
diff --git a/java/com/google/gerrit/server/query/change/ChangePredicates.java b/java/com/google/gerrit/server/query/change/ChangePredicates.java
index fdaa061..6331323 100644
--- a/java/com/google/gerrit/server/query/change/ChangePredicates.java
+++ b/java/com/google/gerrit/server/query/change/ChangePredicates.java
@@ -242,6 +242,12 @@
         ChangeField.HASHTAG_SPEC, HashtagsUtil.cleanupHashtag(hashtag).toLowerCase(Locale.US));
   }
 
+  /** Returns a predicate that matches changes that have an unmet submit requirement. */
+  public static Predicate<ChangeData> unmetRequirement(String requirementName) {
+    return new ChangeIndexPredicate(
+        ChangeField.UNMET_REQUIREMENT_SPEC, requirementName.toLowerCase(Locale.US));
+  }
+
   /** Returns a predicate that matches changes tagged with the provided {@code hashtag}. */
   public static Predicate<ChangeData> fuzzyHashtag(String hashtag) {
     // Use toLowerCase without locale to match behavior in ChangeField.
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 6311c65..68530ef 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -1460,6 +1460,21 @@
   }
 
   @Operator
+  public Predicate<ChangeData> unmet_requirement(String requirementName)
+      throws QueryParseException {
+    checkFieldAvailable(ChangeField.UNMET_REQUIREMENT_SPEC, "unmet_requirement");
+    return ChangePredicates.unmetRequirement(requirementName);
+  }
+
+  @Operator
+  public Predicate<ChangeData> unsatisfied_requirement_count(String value)
+      throws QueryParseException {
+    checkFieldAvailable(
+        ChangeField.UNSATISFIED_REQUIREMENT_COUNT_SPEC, "unsatisfied_requirement_count");
+    return new UnsatisfiedRequirementCountPredicate(value);
+  }
+
+  @Operator
   public Predicate<ChangeData> cc(String who)
       throws QueryParseException, IOException, ConfigInvalidException {
     return reviewerByState(who, ReviewerStateInternal.CC, false);
diff --git a/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java
index 26ccb2c..4ca53c5 100644
--- a/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java
@@ -131,6 +131,21 @@
   }
 
   @Override
+  public Predicate<ChangeData> unmet_requirement(String requirementName)
+      throws QueryParseException {
+    throw new QueryParseException(
+        "Operator 'unmet_requirement' cannot be used in submit requirement expressions.");
+  }
+
+  @Override
+  public Predicate<ChangeData> unsatisfied_requirement_count(String value)
+      throws QueryParseException {
+    throw new QueryParseException(
+        "Operator 'unsatisfied_requirement_count' cannot be used in submit requirement"
+            + " expressions.");
+  }
+
+  @Override
   public Predicate<ChangeData> has(String value) throws QueryParseException {
     if (value.toLowerCase(Locale.US).startsWith(SUBMODULE_UPDATE_HAS_ARG)) {
       List<String> args = SUBMODULE_UPDATE_SPLITTER.splitToList(value);
diff --git a/java/com/google/gerrit/server/query/change/UnsatisfiedRequirementCountPredicate.java b/java/com/google/gerrit/server/query/change/UnsatisfiedRequirementCountPredicate.java
new file mode 100644
index 0000000..827558e
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/UnsatisfiedRequirementCountPredicate.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2026 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.server.index.change.ChangeField;
+
+public class UnsatisfiedRequirementCountPredicate extends IntegerRangeChangePredicate {
+  public UnsatisfiedRequirementCountPredicate(String value) throws QueryParseException {
+    super(ChangeField.UNSATISFIED_REQUIREMENT_COUNT_SPEC, value);
+  }
+
+  @Override
+  protected Integer getValueInt(ChangeData changeData) {
+    Integer count = changeData.unsatisfiedRequirementCount();
+    if (count != null) {
+      return count;
+    }
+    return ChangeField.UNSATISFIED_REQUIREMENT_COUNT_SPEC.get(changeData);
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/change/CreateChange.java b/java/com/google/gerrit/server/restapi/change/CreateChange.java
index fa4296b..9205a46 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateChange.java
@@ -559,11 +559,18 @@
           ins.setCustomKeyedValues(customKeyedValues.build());
         }
 
+        // Default to NotifyHandling.OWNER for WIP changes to avoid emailing project watchers
+        // before the change is marked ready for review (matching ReceiveCommits and CommitUtil).
+        // Private changes do not change default notify handling; they are filtered by visibility
+        // ACLs.
+        boolean isWorkInProgress =
+            Boolean.TRUE.equals(input.workInProgress) || !c.getFilesWithGitConflicts().isEmpty();
+        NotifyHandling defaultNotify = isWorkInProgress ? NotifyHandling.OWNER : NotifyHandling.ALL;
         try (BatchUpdate bu = updateFactory.create(projectState.getNameKey(), me, now)) {
           bu.setRepository(git, rw, oi);
           bu.setNotify(
               notifyResolver.resolve(
-                  firstNonNull(input.notify, NotifyHandling.ALL), input.notifyDetails));
+                  firstNonNull(input.notify, defaultNotify), input.notifyDetails));
           bu.insertChange(ins);
           bu.execute();
         }
diff --git a/java/com/google/gerrit/server/schema/MigrateLabelFunctionsToSubmitRequirement.java b/java/com/google/gerrit/server/schema/MigrateLabelFunctionsToSubmitRequirement.java
index 06c2037..305e476 100644
--- a/java/com/google/gerrit/server/schema/MigrateLabelFunctionsToSubmitRequirement.java
+++ b/java/com/google/gerrit/server/schema/MigrateLabelFunctionsToSubmitRequirement.java
@@ -40,6 +40,7 @@
 import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
@@ -329,12 +330,29 @@
               String.join(
                   " OR ",
                   attributes.refPatterns().stream()
-                      .map(b -> "branch:\\\"" + b + "\\\"")
+                      .map(MigrateLabelFunctionsToSubmitRequirement::toApplicableIfExpression)
                       .collect(Collectors.toList()))));
     }
     return builder.build();
   }
 
+  private static String toApplicableIfExpression(String branchRef) {
+    // Reqex -> migrate as it is.
+    if (branchRef.startsWith("^")) {
+      return "branch:" + branchRef;
+    }
+    // Wildcard -> needs to converted into gerrit regex.
+    if (branchRef.endsWith("/*")) {
+      String prefix = branchRef.substring(0, branchRef.length() - 1);
+      String regex = "^" + Pattern.quote(prefix) + ".*";
+      return "branch:" + regex;
+    }
+    // If branch with " -> need to escape "
+    branchRef = branchRef.replace("\"", "\\\"");
+    // Other cases e.g. branch with # or " -> needs to be quoted
+    return "branch:\"" + branchRef + "\"";
+  }
+
   private static boolean isBlockingOrRequiredLabel(String function) {
     return function.equals("AnyWithBlock")
         || function.equals("MaxWithBlock")
diff --git a/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementIT.java b/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementIT.java
index 5bf1a40..b8d06ef 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementIT.java
@@ -2783,6 +2783,58 @@
   }
 
   @Test
+  public void submitRequirement_disallowsUnsatisfiedRequirementCountInExpression()
+      throws Exception {
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+
+    configSubmitRequirement(
+        project,
+        SubmitRequirement.builder()
+            .setName("Wrong-Req")
+            .setSubmittabilityExpression(
+                SubmitRequirementExpression.create("unsatisfied_requirement_count:0"))
+            .setAllowOverrideInChildProjects(false)
+            .build());
+
+    ChangeInfo change = gApi.changes().id(changeId).get();
+    SubmitRequirementResultInfo srResult =
+        change.submitRequirements.stream()
+            .filter(sr -> sr.name.equals("Wrong-Req"))
+            .collect(MoreCollectors.onlyElement());
+    assertThat(srResult.status).isEqualTo(Status.ERROR);
+    assertThat(srResult.submittabilityExpressionResult.errorMessage)
+        .isEqualTo(
+            "Operator 'unsatisfied_requirement_count' cannot be used in submit requirement"
+                + " expressions.");
+  }
+
+  @Test
+  public void submitRequirement_disallowsUnmetRequirementInExpression() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+
+    configSubmitRequirement(
+        project,
+        SubmitRequirement.builder()
+            .setName("Wrong-Req")
+            .setSubmittabilityExpression(
+                SubmitRequirementExpression.create("unmet_requirement:Code-Review"))
+            .setAllowOverrideInChildProjects(false)
+            .build());
+
+    ChangeInfo change = gApi.changes().id(changeId).get();
+    SubmitRequirementResultInfo srResult =
+        change.submitRequirements.stream()
+            .filter(sr -> sr.name.equals("Wrong-Req"))
+            .collect(MoreCollectors.onlyElement());
+    assertThat(srResult.status).isEqualTo(Status.ERROR);
+    assertThat(srResult.submittabilityExpressionResult.errorMessage)
+        .isEqualTo(
+            "Operator 'unmet_requirement' cannot be used in submit requirement expressions.");
+  }
+
+  @Test
   public void submitRequirements_forcedByDirectSubmission() throws Exception {
     projectOperations
         .project(project)
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 3e82712..9f2610f 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -37,6 +37,8 @@
 import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_ACCOUNTS;
 import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
 import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
+import static com.google.gerrit.extensions.client.ReviewerState.CC;
+import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
 import static com.google.gerrit.extensions.common.testing.EditInfoSubject.assertThat;
 import static com.google.gerrit.server.git.receive.ReceiveConstants.PUSH_OPTION_SKIP_VALIDATION;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
@@ -499,7 +501,7 @@
         .committer(new PersonIdent(admin.newIdent(), testRepo.getInstant()))
         .create();
     PushResult result = pushHead(testRepo, "refs/for/master");
-    assertThat(result.getMessages()).contains("warning: pushing without Change-Id is deprecated");
+    assertThat(result.getMessages()).contains("WARNING: pushing without Change-Id is deprecated");
   }
 
   @Test
@@ -805,6 +807,30 @@
   }
 
   @Test
+  public void authorRemainsReviewerOnNewPatchSet() throws Exception {
+    PushOneCommit.Result r = pushTo("refs/for/master");
+    String changeId = r.getChangeId();
+
+    TestAccount user = accountCreator.user1();
+    gApi.changes().id(changeId).addReviewer(user.email());
+
+    PushOneCommit push =
+        pushFactory.create(user.newIdent(), testRepo, "Subject", "file.txt", "content", changeId);
+    r = push.to("refs/for/master");
+    r.assertOkStatus();
+
+    ChangeInfo changeInfo = gApi.changes().id(changeId).get();
+    Collection<AccountInfo> reviewers = changeInfo.reviewers.get(REVIEWER);
+    assertThat(reviewers).isNotNull();
+    assertThat(reviewers.stream().anyMatch(a -> a._accountId == user.id().get())).isTrue();
+
+    Collection<AccountInfo> ccs = changeInfo.reviewers.get(CC);
+    if (ccs != null) {
+      assertThat(ccs.stream().anyMatch(a -> a._accountId == user.id().get())).isFalse();
+    }
+  }
+
+  @Test
   public void pushForMasterWithReviewerByEmail() throws Exception {
     ConfigInput conf = new ConfigInput();
     conf.enableReviewerByEmail = InheritableBoolean.TRUE;
@@ -3027,7 +3053,7 @@
     assertPushOk(pr, r);
     assertThat(pr.getMessages())
         .contains(
-            "warning: no changes between prior commit "
+            "WARNING: no changes between prior commit "
                 + abbreviateName(c)
                 + " and new commit "
                 + abbreviateName(amended));
@@ -3056,7 +3082,8 @@
     pr = pushHead(testRepo, r, false);
     assertPushOk(pr, r);
     assertThat(pr.getMessages())
-        .contains("warning: " + abbreviateName(amended) + ": no files changed, message updated");
+        .contains(
+            "WARNING: commit " + abbreviateName(amended) + ": no files changed, message updated");
   }
 
   @Test
@@ -3080,7 +3107,8 @@
     pr = pushHead(testRepo, r, false);
     assertPushOk(pr, r);
     assertThat(pr.getMessages())
-        .contains("warning: " + abbreviateName(amended) + ": no files changed, author changed");
+        .contains(
+            "WARNING: commit " + abbreviateName(amended) + ": no files changed, author changed");
   }
 
   @Test
@@ -3107,7 +3135,7 @@
     pr = pushHead(testRepo, r, false);
     assertPushOk(pr, r);
     assertThat(pr.getMessages())
-        .contains("warning: " + abbreviateName(amended) + ": no files changed, was rebased");
+        .contains("WARNING: commit " + abbreviateName(amended) + ": no files changed, was rebased");
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/pgm/MigrateLabelFunctionsToSubmitRequirementIT.java b/javatests/com/google/gerrit/acceptance/pgm/MigrateLabelFunctionsToSubmitRequirementIT.java
index 5a6c5c5..e695254 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/MigrateLabelFunctionsToSubmitRequirementIT.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/MigrateLabelFunctionsToSubmitRequirementIT.java
@@ -277,7 +277,7 @@
 
     assertExistentSr(
         /* srName */ "Foo",
-        /* applicabilityExpression= */ "branch:\\\"refs/heads/master\\\"",
+        /* applicabilityExpression= */ "branch:\"refs/heads/master\"",
         /* submittabilityExpression= */ "label:Foo=MAX AND -label:Foo=MIN",
         /* canOverride= */ true);
     assertLabelFunction("Foo", "NoBlock");
@@ -299,8 +299,52 @@
 
     assertExistentSr(
         /* srName */ "Foo",
-        /* applicabilityExpression= */ "branch:\\\"refs/heads/master\\\" "
-            + "OR branch:\\\"refs/heads/develop\\\"",
+        /* applicabilityExpression= */ "branch:\"refs/heads/master\" "
+            + "OR branch:\"refs/heads/develop\"",
+        /* submittabilityExpression= */ "label:Foo=MAX AND -label:Foo=MIN",
+        /* canOverride= */ true);
+    assertLabelFunction("Foo", "NoBlock");
+  }
+
+  @Test
+  public void migrateBlockingLabel_withQuotesInBranchNameAttribute() throws Exception {
+    createLabelWithBranch(
+        "Foo",
+        "MaxWithBlock",
+        /* ignoreSelfApproval= */ false,
+        ImmutableList.of("refs/heads/gerr\"it"));
+
+    assertNonExistentSr(/* srName= */ "Foo");
+
+    TestUpdateUI updateUI = runMigration(/* expectedResult= */ Status.MIGRATED);
+    assertThat(updateUI.newlyCreatedSrs).isEqualTo(1);
+    assertThat(updateUI.existingSrsMismatchingWithMigration).isEqualTo(0);
+
+    assertExistentSr(
+        /* srName= */ "Foo",
+        /* applicabilityExpression= */ "branch:\"refs/heads/gerr\\\"it\"",
+        /* submittabilityExpression= */ "label:Foo=MAX AND -label:Foo=MIN",
+        /* canOverride= */ true);
+    assertLabelFunction("Foo", "NoBlock");
+  }
+
+  @Test
+  public void migrateBlockingLabel_withHashInBranchNameAttribute() throws Exception {
+    createLabelWithBranch(
+        "Foo",
+        "MaxWithBlock",
+        /* ignoreSelfApproval= */ false,
+        ImmutableList.of("refs/heads/gerr#it"));
+
+    assertNonExistentSr(/* srName= */ "Foo");
+
+    TestUpdateUI updateUI = runMigration(/* expectedResult= */ Status.MIGRATED);
+    assertThat(updateUI.newlyCreatedSrs).isEqualTo(1);
+    assertThat(updateUI.existingSrsMismatchingWithMigration).isEqualTo(0);
+
+    assertExistentSr(
+        /* srName= */ "Foo",
+        /* applicabilityExpression= */ "branch:\"refs/heads/gerr#it\"",
         /* submittabilityExpression= */ "label:Foo=MAX AND -label:Foo=MIN",
         /* canOverride= */ true);
     assertLabelFunction("Foo", "NoBlock");
@@ -321,8 +365,30 @@
     assertThat(updateUI.existingSrsMismatchingWithMigration).isEqualTo(0);
 
     assertExistentSr(
-        /* srName */ "Foo",
-        /* applicabilityExpression= */ "branch:\\\"^refs/heads/main-.*\\\"",
+        /* srName= */ "Foo",
+        /* applicabilityExpression= */ "branch:^refs/heads/main-.*",
+        /* submittabilityExpression= */ "label:Foo=MAX AND -label:Foo=MIN",
+        /* canOverride= */ true);
+    assertLabelFunction("Foo", "NoBlock");
+  }
+
+  @Test
+  public void migrateBlockingLabel_withWildcardBranchAttribute() throws Exception {
+    createLabelWithBranch(
+        "Foo",
+        "MaxWithBlock",
+        /* ignoreSelfApproval= */ false,
+        ImmutableList.of("refs/heads/release/*"));
+
+    assertNonExistentSr(/* srName= */ "Foo");
+
+    TestUpdateUI updateUI = runMigration(/* expectedResult= */ Status.MIGRATED);
+    assertThat(updateUI.newlyCreatedSrs).isEqualTo(1);
+    assertThat(updateUI.existingSrsMismatchingWithMigration).isEqualTo(0);
+
+    assertExistentSr(
+        /* srName= */ "Foo",
+        /* applicabilityExpression= */ "branch:^\\Qrefs/heads/release/\\E.*",
         /* submittabilityExpression= */ "label:Foo=MAX AND -label:Foo=MIN",
         /* canOverride= */ true);
     assertLabelFunction("Foo", "NoBlock");
@@ -344,8 +410,8 @@
 
     assertExistentSr(
         /* srName */ "Foo",
-        /* applicabilityExpression= */ "branch:\\\"refs/heads/master\\\" "
-            + "OR branch:\\\"^refs/heads/main-.*\\\"",
+        /* applicabilityExpression= */ "branch:\"refs/heads/master\" "
+            + "OR branch:^refs/heads/main-.*",
         /* submittabilityExpression= */ "label:Foo=MAX AND -label:Foo=MIN",
         /* canOverride= */ true);
     assertLabelFunction("Foo", "NoBlock");
diff --git a/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java b/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
index 5accd00..2f85c88 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
@@ -27,11 +27,17 @@
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.Address;
+import com.google.gerrit.entities.BooleanProjectConfig;
 import com.google.gerrit.entities.NotifyConfig;
 import com.google.gerrit.entities.NotifyConfig.NotifyType;
 import com.google.gerrit.entities.Permission;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.ReviewerInput;
+import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.extensions.client.ReviewerState;
+import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.testing.FakeEmailSender.Message;
 import com.google.inject.Inject;
@@ -716,4 +722,143 @@
     // assert that there was no email notification for user
     assertThat(sender.getMessages()).isEmpty();
   }
+
+  // Unlike private changes (which notify users with VIEW_PRIVATE_CHANGES permission as tested
+  // in watchProjectNotifyOnPrivateChange), WIP changes are visible to all readers but suppress
+  // notifications by defaulting notify handling to NotifyHandling.OWNER unless overridden.
+  @Test
+  public void watchProjectNoNotificationForWipChange_createdViaRest() throws Exception {
+    String watchedProject = projectOperations.newProject().create().get();
+    requestScopeOperations.setApiUser(user.id());
+    watch(watchedProject);
+
+    requestScopeOperations.setApiUser(admin.id());
+    ChangeInput input = new ChangeInput();
+    input.project = watchedProject;
+    input.branch = "master";
+    input.subject = "wip change";
+    input.workInProgress = true;
+    gApi.changes().create(input);
+
+    assertThat(sender.getMessages()).isEmpty();
+  }
+
+  @Test
+  public void watchProjectNoNotificationForWipChange_pushedWithoutNotify() throws Exception {
+    String watchedProject = projectOperations.newProject().create().get();
+    requestScopeOperations.setApiUser(user.id());
+    watch(watchedProject);
+
+    requestScopeOperations.setApiUser(admin.id());
+    TestRepository<InMemoryRepository> watchedRepo =
+        cloneProject(Project.nameKey(watchedProject), admin);
+    PushOneCommit.Result r =
+        pushFactory
+            .create(admin.newIdent(), watchedRepo, "wip change", "a", "a1")
+            .to("refs/for/master%wip");
+    r.assertOkStatus();
+
+    assertThat(sender.getMessages()).isEmpty();
+  }
+
+  @Test
+  public void watchProjectNotificationForWipChange_createdViaRestWithNotifyAll() throws Exception {
+    String watchedProject = projectOperations.newProject().create().get();
+    requestScopeOperations.setApiUser(user.id());
+    watch(watchedProject);
+
+    requestScopeOperations.setApiUser(admin.id());
+    ChangeInput input = new ChangeInput();
+    input.project = watchedProject;
+    input.branch = "master";
+    input.subject = "wip change";
+    input.workInProgress = true;
+    input.notify = NotifyHandling.ALL;
+    gApi.changes().create(input);
+
+    assertThat(sender.getMessages()).hasSize(1);
+    assertThat(sender.getMessages().get(0).rcpt()).containsExactly(user.getNameEmail());
+  }
+
+  @Test
+  public void watchProjectNoNotificationForWipChange_projectWipByDefault() throws Exception {
+    String watchedProject = projectOperations.newProject().create().get();
+    try (ProjectConfigUpdate u = updateProject(Project.nameKey(watchedProject))) {
+      u.getConfig()
+          .updateProject(
+              b ->
+                  b.setBooleanConfig(
+                      BooleanProjectConfig.WORK_IN_PROGRESS_BY_DEFAULT, InheritableBoolean.TRUE));
+      u.save();
+    }
+    requestScopeOperations.setApiUser(user.id());
+    watch(watchedProject);
+
+    requestScopeOperations.setApiUser(admin.id());
+    ChangeInput input = new ChangeInput();
+    input.project = watchedProject;
+    input.branch = "master";
+    input.subject = "default wip change";
+    gApi.changes().create(input);
+
+    assertThat(sender.getMessages()).isEmpty();
+  }
+
+  @Test
+  public void watchProjectNoNotificationForWipChange_addedAsReviewerOrCc() throws Exception {
+    String watchedProject = projectOperations.newProject().create().get();
+    requestScopeOperations.setApiUser(user.id());
+    watch(watchedProject);
+
+    requestScopeOperations.setApiUser(admin.id());
+    ChangeInput input = new ChangeInput();
+    input.project = watchedProject;
+    input.branch = "master";
+    input.subject = "wip change";
+    input.workInProgress = true;
+    String changeId = gApi.changes().create(input).get().id;
+    assertThat(sender.getMessages()).isEmpty();
+
+    // Adding user as a reviewer on the WIP change does not send watch/reviewer notifications
+    gApi.changes().id(changeId).addReviewer(user.email());
+    assertThat(sender.getMessages()).isEmpty();
+
+    // Adding user2 as CC on the WIP change also does not send notifications
+    TestAccount user2 = accountCreator.user2();
+    requestScopeOperations.setApiUser(user2.id());
+    watch(watchedProject);
+    requestScopeOperations.setApiUser(admin.id());
+    ReviewerInput ccInput = new ReviewerInput();
+    ccInput.reviewer = user2.email();
+    ccInput.state = ReviewerState.CC;
+    gApi.changes().id(changeId).addReviewer(ccInput);
+    assertThat(sender.getMessages()).isEmpty();
+  }
+
+  @Test
+  public void watchProjectNoNotificationForWipChange_newPatchsetWhenReviewer() throws Exception {
+    String watchedProject = projectOperations.newProject().create().get();
+    requestScopeOperations.setApiUser(user.id());
+    watch(watchedProject);
+
+    requestScopeOperations.setApiUser(admin.id());
+    TestRepository<InMemoryRepository> watchedRepo =
+        cloneProject(Project.nameKey(watchedProject), admin);
+    PushOneCommit.Result r =
+        pushFactory
+            .create(admin.newIdent(), watchedRepo, "wip change", "a", "a1")
+            .to("refs/for/master%wip");
+    r.assertOkStatus();
+    gApi.changes().id(r.getChangeId()).addReviewer(user.email());
+    assertThat(sender.getMessages()).isEmpty();
+
+    // Pushing a new patchset on the WIP change does not notify user (who is both watcher and
+    // reviewer)
+    PushOneCommit.Result r2 =
+        pushFactory
+            .create(admin.newIdent(), watchedRepo, "wip change", "a", "a2", r.getChangeId())
+            .to("refs/for/master%wip");
+    r2.assertOkStatus();
+    assertThat(sender.getMessages()).isEmpty();
+  }
 }
diff --git a/javatests/com/google/gerrit/entities/converter/ChangeInputProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/ChangeInputProtoConverterTest.java
index e64a9e5..05ffa4d 100644
--- a/javatests/com/google/gerrit/entities/converter/ChangeInputProtoConverterTest.java
+++ b/javatests/com/google/gerrit/entities/converter/ChangeInputProtoConverterTest.java
@@ -177,7 +177,6 @@
             .setProject("test-project")
             .setBranch("test-branch")
             .setSubject("test-subject")
-            .setNotify(Entities.NotifyHandling.ALL)
             .build();
     assertThat(proto).isEqualTo(expectedProto);
   }
diff --git a/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java b/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java
index 53431d1..6fcbf5d 100644
--- a/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java
+++ b/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java
@@ -105,6 +105,29 @@
   }
 
   @Test
+  public void unmetRequirementField() {
+    SubmitRequirementResult sr1 =
+        submitRequirementResult(
+            "Code-Review", "label:CR=+1", SubmitRequirementExpressionResult.Status.PASS);
+    SubmitRequirementResult sr2 =
+        submitRequirementResult(
+            "Verified", "label:V=+1", SubmitRequirementExpressionResult.Status.FAIL);
+
+    assertThat(sr1.fulfilled()).isTrue();
+    assertThat(sr2.fulfilled()).isFalse();
+
+    ChangeData cd = org.mockito.Mockito.mock(ChangeData.class);
+    org.mockito.Mockito.when(cd.submitRequirementsIncludingLegacy())
+        .thenReturn(
+            com.google.common.collect.ImmutableMap.of(
+                sr1.submitRequirement(), sr1,
+                sr2.submitRequirement(), sr2));
+
+    assertThat(ChangeField.UNMET_REQUIREMENT_FIELD.get(cd)).containsExactly("verified");
+    assertThat(ChangeField.UNSATISFIED_REQUIREMENT_COUNT_FIELD.get(cd)).isEqualTo(1);
+  }
+
+  @Test
   public void storedSubmitRecords() {
     assertStoredRecordRoundTrip(record(SubmitRecord.Status.CLOSED));
 
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 8d1e3dd..059be1b 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -4559,6 +4559,35 @@
   }
 
   @Test
+  public void bySubmitRequirement_unmet() throws Exception {
+    assume().that(getSchema().hasField(ChangeField.UNMET_REQUIREMENT_SPEC)).isTrue();
+    Project.NameKey project = Project.nameKey("repo");
+    repo = createAndOpenProject(project);
+    Change change1 = insert(project, newChange(repo));
+    assertQuery("unmet_requirement:Code-Review", change1);
+
+    approve(change1);
+    assertQuery("unmet_requirement:Code-Review");
+  }
+
+  @Test
+  public void bySubmitRequirement_unsatisfiedCount() throws Exception {
+    assume().that(getSchema().hasField(ChangeField.UNSATISFIED_REQUIREMENT_COUNT_SPEC)).isTrue();
+    Project.NameKey project = Project.nameKey("repo");
+    repo = createAndOpenProject(project);
+    Change change1 = insert(project, newChange(repo));
+    assertQuery("unsatisfied_requirement_count:>0", change1);
+    assertQuery("unsatisfied_requirement_count:1", change1);
+    assertQuery("unsatisfied_requirement_count:<=1", change1);
+    assertQuery("unmet_requirement:Code-Review", change1);
+
+    approve(change1);
+    assertQuery("unsatisfied_requirement_count:0", change1);
+    assertQuery("unsatisfied_requirement_count:<1", change1);
+    assertQuery("unmet_requirement:Code-Review");
+  }
+
+  @Test
   public void byUrlEncodedProject() throws Exception {
     Project.NameKey project = Project.nameKey("repo+foo");
     repo = createAndOpenProject(project);
diff --git a/tools/BUILD b/tools/BUILD
index 8b3429d..539aad5 100644
--- a/tools/BUILD
+++ b/tools/BUILD
@@ -306,6 +306,13 @@
         "-Xep:NonOverridingEquals:ERROR",
         "-Xep:NonRuntimeAnnotation:ERROR",
         "-Xep:NullOptional:ERROR",
+        # Demoted to a warning: false positive on Guava methods like
+        # Iterables.getFirst(iterable, null) when javac cannot read the
+        # @Nullable bound of <T extends @Nullable Object> from class
+        # files. Bazel's remotejdk_21 (Zulu 21.0.9) lacks the
+        # JDK-8341779 backport; JDK 25 is unaffected. See the commit
+        # message introducing this line for the full reference trail.
+        "-Xep:NullArgumentForNonNullParameter:WARN",
         "-Xep:NullTernary:ERROR",
         "-Xep:NullableConstructor:ERROR",
         "-Xep:NullablePrimitive:ERROR",
diff --git a/tools/js/eslint.bzl b/tools/js/eslint.bzl
index 737d01f..473330d 100644
--- a/tools/js/eslint.bzl
+++ b/tools/js/eslint.bzl
@@ -33,6 +33,7 @@
         ],
         extensions = [".ts"],
         plugins = [
+            "//:node_modules/@typescript-eslint/eslint-plugin",
             "//:node_modules/eslint-config-google",
             "//:node_modules/eslint-plugin-html",
             "//:node_modules/eslint-plugin-import",