Merge branch 'stable-3.5'

* stable-3.5:
  Allow configuring the root project and branch
  Add support for names-factory provided by a plugins
  Update .gitignore to include intellij specific files
  Rename TaskAttributeFactory to TaskPluginDefinedInfoFactory
  Harden shell commands
  Migrate configuration to <plugin-name>.config from gerrit.config
  Revert "test: Remove complicated JSON->shell dict logic"
  test: docker: Update to Alpine 3.16
  test: Run py 2to3
  test: lib_helper: Remove inline python
  test: Consistently apply json_pp to expected
  test: Remove complicated JSON->shell dict logic
  test: Use jq more for shell json parsing
  UI: Move tasks to a primary tab
  UI: Fix html for task chips

Change-Id: Ic36c9499b91ee223a274a4dab9c2ad239118239f
diff --git a/.gitignore b/.gitignore
index c13cf52..c42173e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,12 +2,14 @@
 /eclipse-out
 /target
 /.classpath
+/.idea
+/.ijwb
 /.project
 /.settings/org.maven.ide.eclipse.prefs
 /.settings/org.eclipse.m2e.core.prefs
 /.settings/org.eclipse.core.resources.prefs
 /.settings/org.eclipse.jdt.core.prefs
 /package-lock.json
-/task.iml
+**/*.iml
 /node_modules/
 /test/docker/gerrit/artifacts
diff --git a/BUILD b/BUILD
index 8782740..0a0773f 100644
--- a/BUILD
+++ b/BUILD
@@ -11,6 +11,7 @@
 load("@rules_antlr//antlr:antlr4.bzl", "antlr")
 
 plugin_name = "task"
+test_factory_provider_plugin_name = "names-factory-provider"
 
 java_plugin(
     name = "auto-value-plugin",
@@ -75,12 +76,23 @@
     deps = PLUGIN_TEST_DEPS + PLUGIN_DEPS + [plugin_name],
 )
 
+gerrit_plugin(
+    name = test_factory_provider_plugin_name,
+    dir_name = plugin_name,
+    srcs = ["src/main/java/com/googlesource/gerrit/plugins/task/extensions/PluginProvidedTaskNamesFactory.java"] + glob(["src/test/java/**/names_factory_provider/*.java"]),
+    manifest_entries = [
+        "Gerrit-PluginName: " + test_factory_provider_plugin_name,
+        "Gerrit-Module: com.googlesource.gerrit.plugins.names_factory_provider.Module",
+        "Implementation-Title: Names Factory Provider",
+    ],
+)
+
 sh_test(
     name = "docker-tests",
     size = "medium",
     srcs = ["test/docker/run.sh"],
-    args = ["--task-plugin-jar", "$(location :task)"],
-    data = [plugin_name] + glob(["test/**"]) + glob(["src/main/resources/Documentation/*"]),
+    args = ["--task-plugin-jar", "$(location :task)", "--names-factory-provider-plugin-jar", "$(location :names-factory-provider)"],
+    data = [plugin_name, test_factory_provider_plugin_name] + glob(["test/**"]) + glob(["src/main/resources/Documentation/*"]),
     local = True,
     tags = ["docker"],
 )
diff --git a/gr-task-plugin/gr-task-chip.js b/gr-task-plugin/gr-task-chip.js
index 8e46bd0..a0c5b94 100644
--- a/gr-task-plugin/gr-task-chip.js
+++ b/gr-task-plugin/gr-task-chip.js
@@ -18,7 +18,7 @@
 import './gr-task-plugin.js';
 import {htmlTemplate} from './gr-task-chip_html.js';
 
-class GrTaskChip extends Polymer.Element {
+export class GrTaskChip extends Polymer.Element {
   static get is() {
     return 'gr-task-chip';
   }
@@ -37,23 +37,24 @@
     };
   }
 
-  _setTasksTabActive() {
-    // TODO: Identify a better way as current implementation is fragile
-    const endPointDecorators = document.querySelector('gr-app')
+  static getPrimaryTabs() {
+    return document.querySelector('gr-app')
         .shadowRoot.querySelector('gr-app-element')
         .shadowRoot.querySelector('main')
         .querySelector('gr-change-view')
         .shadowRoot.querySelector('#mainContent')
-        .getElementsByTagName('gr-endpoint-decorator');
-    if (endPointDecorators) {
-      for (let i = 0; i <= endPointDecorators.length; i++) {
-        const el = endPointDecorators[i]
-            ?.shadowRoot?.querySelector('gr-task-plugin');
-        if (el) {
-          el.shadowRoot.querySelector('paper-tabs')
-              .querySelector('paper-tab').scrollIntoView();
-          break;
-        }
+        .querySelector('#primaryTabs');
+  }
+
+  _setTasksTabActive() {
+    // TODO: Identify a better way as current implementation is fragile
+    const paperTabs = GrTaskChip.getPrimaryTabs();
+    const tabs = paperTabs.querySelectorAll('paper-tab');
+    for (let i=0; i <= tabs.length; i++) {
+      if (tabs[i].dataset['name'] === 'change-view-tab-header-task') {
+        paperTabs.selected = i;
+        tabs[i].scrollIntoView({block: 'center'});
+        break;
       }
     }
   }
diff --git a/gr-task-plugin/gr-task-plugin.js b/gr-task-plugin/gr-task-plugin.js
index 2ac355d..2bff130 100644
--- a/gr-task-plugin/gr-task-plugin.js
+++ b/gr-task-plugin/gr-task-plugin.js
@@ -82,30 +82,16 @@
         notify: true,
         value: 0,
       },
-      _invalid_count: {
-        type: Number,
-        notify: true,
-        value: 0,
-      },
-      _waiting_count: {
-        type: Number,
-        notify: true,
-        value: 0,
-      },
-      _duplicate_count: {
-        type: Number,
-        notify: true,
-        value: 0,
-      },
-      _pass_count: {
-        type: Number,
-        notify: true,
-        value: 0,
-      },
+
       _isPending: {
         type: Boolean,
         value: true,
       },
+
+      _tasks_info: {
+        type: Object,
+        observer: '_tasksInfoChanged',
+      },
     };
   }
 
@@ -113,52 +99,36 @@
     return show_all === 'true';
   }
 
-  connectedCallback() {
-    super.connectedCallback();
-
+  ready() {
+    super.ready();
+    if (!this.change) {
+      return;
+    }
+    document.addEventListener(`response-tasks-${this.change._number}`, e => {
+      this._tasks_info = e.detail.tasks_info;
+      this._isPending = e.detail.is_loading;
+    });
     this._getTasks();
   }
 
+  _tasksInfoChanged(newValue, oldValue) {
+    if (this._tasks_info) {
+      this._tasks = this._addTasks(this._tasks_info.roots);
+    }
+  }
+
   _is_hidden(_isPending, _tasks) {
     return (!_isPending && !_tasks.length);
   }
 
-  _getTasks() {
-    if (!this.change) {
-      return;
+  async _getTasks() {
+    while (this._isPending) {
+      document.dispatchEvent(
+          new CustomEvent(`request-tasks-${this.change._number}`, {
+            composed: true, bubbles: true,
+          }));
+      await new Promise(r => setTimeout(r, 100));
     }
-
-    this._isPending = true;
-    const endpoint =
-        `/changes/?q=change:${this.change._number}&--task--applicable`;
-
-    return this.plugin.restApi().get(endpoint).then(response => {
-      this._isPending = false;
-      if (response && response.length === 1) {
-        const cinfo = response[0];
-        if (cinfo.plugins) {
-          const taskPluginInfo = cinfo.plugins.find(
-              pluginInfo => pluginInfo.name === 'task');
-
-          if (taskPluginInfo) {
-            this._tasks = this._addTasks(taskPluginInfo.roots);
-          }
-        }
-        document.dispatchEvent(new CustomEvent('tasks-loaded', {
-          detail: {
-            ready_count: this._ready_count,
-            fail_count: this._fail_count,
-            invalid_count: this._invalid_count,
-            waiting_count: this._waiting_count,
-            duplicate_count: this._duplicate_count,
-            pass_count: this._pass_count,
-          },
-          composed: true, bubbles: true,
-        }));
-      }
-    }).catch(e => {
-      this._isPending = false;
-    });
   }
 
   _computeIcon(task) {
@@ -223,18 +193,6 @@
       case 'READY':
         this._ready_count++;
         break;
-      case 'INVALID':
-        this._invalid_count++;
-        break;
-      case 'WAITING':
-        this._waiting_count++;
-        break;
-      case 'DUPLICATE':
-        this._duplicate_count++;
-        break;
-      case 'PASS':
-        this._pass_count++;
-        break;
     }
   }
 
diff --git a/gr-task-plugin/gr-task-plugin_html.js b/gr-task-plugin/gr-task-plugin_html.js
index 6ab5fb9..e0ea9cd 100644
--- a/gr-task-plugin/gr-task-plugin_html.js
+++ b/gr-task-plugin/gr-task-plugin_html.js
@@ -80,14 +80,6 @@
 </style>
 
 <div id="tasks" hidden$="[[_is_hidden(_isPending, _tasks)]]">
-  <paper-tabs id="secondaryTabs" selected="0">
-    <paper-tab
-      data-name$="Tasks"
-      class="Tasks"
-    >
-      Tasks
-    </paper-tab>
-  </paper-tabs>
   <section class="TasksList">
     <div hidden$="[[!_isPending]]" class="task-list-item">Loading...</div>
     <div hidden$="[[_isPending]]" class="task-list-item">
@@ -119,6 +111,7 @@
   <div hidden$="[[!_expand_all]]" style="padding-bottom: 12px">
     <ul style="list-style-type:none;">
       <gr-task-plugin-tasks
+          hidden$="[[_isPending]]"
           tasks="[[_tasks]]"
           show_all$="[[_show_all]]"> </gr-task-plugin-tasks>
     </ul>
diff --git a/gr-task-plugin/gr-task-summary.js b/gr-task-plugin/gr-task-summary.js
index 49c858d..95ad3f3 100644
--- a/gr-task-plugin/gr-task-summary.js
+++ b/gr-task-plugin/gr-task-summary.js
@@ -16,7 +16,7 @@
  */
 
 import {htmlTemplate} from './gr-task-summary_html.js';
-import './gr-task-chip.js';
+import {GrTaskChip} from './gr-task-chip.js';
 
 class GrTaskSummary extends Polymer.Element {
   static get is() {
@@ -29,6 +29,10 @@
 
   static get properties() {
     return {
+      change: {
+        type: Object,
+      },
+
       ready_count: {
         type: Number,
         notify: true,
@@ -69,23 +73,93 @@
         type: Boolean,
         value: true,
       },
+
+      tasks_info: {
+        type: Object,
+      },
     };
   }
 
   /** @override */
   ready() {
     super.ready();
-    document.addEventListener('tasks-loaded', e => {
-      this.ready_count = e.detail.ready_count;
-      this.fail_count = e.detail.fail_count;
-      this.invalid_count = e.detail.invalid_count;
-      this.waiting_count = e.detail.waiting_count;
-      this.duplicate_count = e.detail.duplicate_count;
-      this.pass_count = e.detail.pass_count;
-      this.is_loading = false;
+    this._fetch_tasks();
+
+    document.addEventListener(`request-tasks-${this.change._number}`, e => {
+      document.dispatchEvent(
+          new CustomEvent(`response-tasks-${this.change._number}`, {
+            detail: {
+              tasks_info: this.tasks_info,
+              is_loading: this.is_loading,
+            },
+            composed: true, bubbles: true,
+          }));
     });
   }
 
+  _fetch_tasks() {
+    const endpoint =
+        `/changes/?q=change:${this.change._number}&--task--applicable`;
+    return this.plugin.restApi().get(endpoint).then(response => {
+      if (response && response.length === 1) {
+        const cinfo = response[0];
+        if (cinfo.plugins) {
+          this.tasks_info = cinfo.plugins.find(
+              pluginInfo => pluginInfo.name === 'task');
+          this._compute_counts(this.tasks_info.roots);
+        }
+      }
+    }).finally(e => {
+      this.is_loading = false;
+      if (!this._can_show_summary(
+          this.is_loading, this.ready_count,
+          this.fail_count, this.invalid_count,
+          this.waiting_count, this.duplicate_count,
+          this.pass_count)) {
+        this._hide_tasks_tab();
+      }
+    });
+  }
+
+  _compute_counts(tasks) {
+    if (!tasks) return [];
+    tasks.forEach(task => {
+      switch (task.status) {
+        case 'FAIL':
+          this.fail_count++;
+          break;
+        case 'READY':
+          this.ready_count++;
+          break;
+        case 'INVALID':
+          this.invalid_count++;
+          break;
+        case 'WAITING':
+          this.waiting_count++;
+          break;
+        case 'DUPLICATE':
+          this.duplicate_count++;
+          break;
+        case 'PASS':
+          this.pass_count++;
+          break;
+      }
+      this._compute_counts(task.sub_tasks);
+    });
+  }
+
+  _hide_tasks_tab() {
+    const paperTabs = GrTaskChip.getPrimaryTabs();
+    const tabs = paperTabs.querySelectorAll('paper-tab');
+    for (let i=0; i <= tabs.length; i++) {
+      if (tabs[i].dataset['name'] === 'change-view-tab-header-task') {
+        tabs[i].setAttribute('hidden', true);
+        paperTabs.selected = 0;
+        break;
+      }
+    }
+  }
+
   _can_show_summary(is_loading, ready_count,
       fail_count, invalid_count,
       waiting_count, duplicate_count,
@@ -98,4 +172,4 @@
   }
 }
 
-customElements.define(GrTaskSummary.is, GrTaskSummary);
\ No newline at end of file
+customElements.define(GrTaskSummary.is, GrTaskSummary);
diff --git a/gr-task-plugin/gr-task-summary_html.js b/gr-task-plugin/gr-task-summary_html.js
index 4667060..df59590 100644
--- a/gr-task-plugin/gr-task-summary_html.js
+++ b/gr-task-plugin/gr-task-summary_html.js
@@ -79,8 +79,8 @@
             <gr-task-chip chip_style="ready" hidden$="[[!ready_count]]">[[ready_count]] needed</gr-task-chip>
             <gr-task-chip chip_style="success" hidden$="[[!pass_count]]">[[pass_count]] passed</gr-task-chip>
             <gr-task-chip chip_style="duplicate" hidden$="[[!duplicate_count]]">[[duplicate_count]] duplicate</gr-task-chip>
+          </td>
         </template>
-        </td>
       </tr>
     </table>
   </div>
diff --git a/gr-task-plugin/gr-task-tab-header.js b/gr-task-plugin/gr-task-tab-header.js
new file mode 100644
index 0000000..1bc6bbe
--- /dev/null
+++ b/gr-task-plugin/gr-task-tab-header.js
@@ -0,0 +1,31 @@
+/**
+ * @license
+ * Copyright (C) 2024 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.
+ */
+
+import {htmlTemplate} from './gr-task-tab-header_html.js';
+import './gr-task-plugin.js';
+
+class GrTaskTabHeader extends Polymer.Element {
+  static get is() {
+    return 'gr-task-tab-header';
+  }
+
+  static get template() {
+    return htmlTemplate;
+  }
+}
+
+customElements.define(GrTaskTabHeader.is, GrTaskTabHeader);
\ No newline at end of file
diff --git a/gr-task-plugin/gr-task-tab-header_html.js b/gr-task-plugin/gr-task-tab-header_html.js
new file mode 100644
index 0000000..66be4a6
--- /dev/null
+++ b/gr-task-plugin/gr-task-tab-header_html.js
@@ -0,0 +1,20 @@
+/**
+ * @license
+ * Copyright (C) 2024 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.
+ */
+
+export const htmlTemplate = Polymer.html`
+  <div class="gr-task-tab-header">Tasks</div>
+`;
\ No newline at end of file
diff --git a/gr-task-plugin/plugin.js b/gr-task-plugin/plugin.js
index 2219f12..2448ea1 100644
--- a/gr-task-plugin/plugin.js
+++ b/gr-task-plugin/plugin.js
@@ -17,10 +17,18 @@
 
 import './gr-task-plugin.js';
 import './gr-task-summary.js';
+import './gr-task-tab-header.js';
 
 Gerrit.install(plugin => {
+  plugin.registerDynamicCustomComponent(
+      'change-view-tab-header',
+      'gr-task-tab-header'
+  );
+  plugin.registerDynamicCustomComponent(
+      'change-view-tab-content',
+      'gr-task-plugin'
+  );
   plugin.registerCustomComponent(
-      'change-view-integration', 'gr-task-plugin');
-  plugin.registerCustomComponent(
-      'commit-container', 'gr-task-summary');
+      'commit-container',
+      'gr-task-summary');
 });
diff --git a/src/main/antlr4/com/googlesource/gerrit/plugins/task/TaskReference.g4 b/src/main/antlr4/com/googlesource/gerrit/plugins/task/TaskReference.g4
index 3869643..dc46655 100644
--- a/src/main/antlr4/com/googlesource/gerrit/plugins/task/TaskReference.g4
+++ b/src/main/antlr4/com/googlesource/gerrit/plugins/task/TaskReference.g4
@@ -98,7 +98,7 @@
   ;
 
 file_path
- : ALL_PROJECTS_ROOT
+ : ROOT_PROJECT
  | FWD_SLASH absolute TASK_DELIMETER
  | user absolute? TASK_DELIMETER
  | group_name absolute? TASK_DELIMETER
@@ -175,7 +175,7 @@
  : '^'
  ;
 
-ALL_PROJECTS_ROOT
+ROOT_PROJECT
  : FWD_SLASH FWD_SLASH TASK_DELIMETER
  ;
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java b/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java
index 3451836..f70a5dd 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.config.CapabilityDefinition;
 import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.webui.JavaScriptPlugin;
 import com.google.gerrit.extensions.webui.WebUiPlugin;
@@ -27,6 +28,7 @@
 import com.google.gerrit.sshd.commands.Query;
 import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.task.cli.PatchSetArgument;
+import com.googlesource.gerrit.plugins.task.extensions.PluginProvidedTaskNamesFactory;
 import java.util.ArrayList;
 import java.util.List;
 import org.kohsuke.args4j.Option;
@@ -46,7 +48,7 @@
 
       bind(ChangePluginDefinedInfoFactory.class)
           .annotatedWith(Exports.named("task"))
-          .to(TaskAttributeFactory.class);
+          .to(TaskPluginDefinedInfoFactory.class);
 
       install(new IsTrueOperator.Module());
 
@@ -55,6 +57,8 @@
       bind(DynamicBean.class).annotatedWith(Exports.named(QueryChanges.class)).to(MyOptions.class);
       DynamicSet.bind(binder(), WebUiPlugin.class)
           .toInstance(new JavaScriptPlugin("gr-task-plugin.js"));
+
+      DynamicMap.mapOf(binder(), PluginProvidedTaskNamesFactory.class);
     }
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/PredicateCache.java b/src/main/java/com/googlesource/gerrit/plugins/task/PredicateCache.java
index 32641bd..cd61c3a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/PredicateCache.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/PredicateCache.java
@@ -14,7 +14,6 @@
 
 package com.googlesource.gerrit.plugins.task;
 
-import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.index.SchemaFieldDefs.SchemaField;
 import com.google.gerrit.index.query.AndPredicate;
 import com.google.gerrit.index.query.NotPredicate;
@@ -22,7 +21,6 @@
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.query.change.BranchSetIndexPredicate;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -34,10 +32,6 @@
 import com.googlesource.gerrit.plugins.task.statistics.HitHashMap;
 import com.googlesource.gerrit.plugins.task.statistics.StopWatch;
 import com.googlesource.gerrit.plugins.task.util.ThrowingProvider;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Set;
-import org.eclipse.jgit.lib.Config;
 
 public class PredicateCache {
   public static class Statistics {
@@ -45,7 +39,7 @@
   }
 
   protected final SubmitRequirementChangeQueryBuilder srcqb;
-  protected final Set<String> cacheableByBranchPredicateClassNames;
+  protected final TaskPluginConfiguration config;
   protected final CurrentUser user;
   protected final HitHashMap<String, ThrowingProvider<Predicate<ChangeData>, QueryParseException>>
       predicatesByQuery = new HitHashMap<>();
@@ -54,16 +48,10 @@
 
   @Inject
   public PredicateCache(
-      @GerritServerConfig Config config,
-      @PluginName String pluginName,
-      CurrentUser user,
-      SubmitRequirementChangeQueryBuilder srcqb) {
+      TaskPluginConfiguration config, CurrentUser user, SubmitRequirementChangeQueryBuilder srcqb) {
+    this.config = config;
     this.user = user;
     this.srcqb = srcqb;
-    cacheableByBranchPredicateClassNames =
-        new HashSet<>(
-            Arrays.asList(
-                config.getStringList(pluginName, "cacheable-predicates", "byBranch-className")));
   }
 
   public void initStatistics(int summaryCount) {
@@ -133,6 +121,8 @@
         return true;
       }
     }
-    return cacheableByBranchPredicateClassNames.contains(predicate.getClass().getName());
+    return config
+        .getCacheableByBranchPredicateClassNames()
+        .contains(predicate.getClass().getName());
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
index 53897b5..1859b0d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
@@ -33,7 +33,8 @@
 public class TaskConfig extends AbstractVersionedMetaData {
   public enum NamesFactoryType {
     CHANGE,
-    STATIC;
+    STATIC,
+    PLUGIN;
 
     public static NamesFactoryType getNamesFactoryType(String str) {
       for (NamesFactoryType type : NamesFactoryType.values()) {
@@ -164,12 +165,19 @@
     public String changes;
     public List<String> names;
     public String type;
+    public String plugin;
+    public String provider;
+    public List<String> args;
 
     public NamesFactory(SubSectionKey s) {
       super(s);
       changes = getString(s, KEY_CHANGES, null);
       names = getStringList(s, KEY_NAME);
       type = getString(s, KEY_TYPE, null);
+
+      plugin = getString(s, KEY_PLUGIN);
+      provider = getString(s, KEY_PROVIDER);
+      args = getStringList(s, KEY_ARG);
     }
   }
 
@@ -194,6 +202,7 @@
   public static final String SECTION_TASK = TaskKey.CONFIG_SECTION;
   public static final String SECTION_TASKS_FACTORY = TaskKey.CONFIG_TASKS_FACTORY;
   public static final String KEY_APPLICABLE = "applicable";
+  public static final String KEY_ARG = "arg";
   public static final String KEY_CHANGES = "changes";
   public static final String KEY_DUPLICATE_KEY = "duplicate-key";
   public static final String KEY_EXPORT_PREFIX = "export-";
@@ -204,8 +213,10 @@
   public static final String KEY_NAME = "name";
   public static final String KEY_NAMES_FACTORY = "names-factory";
   public static final String KEY_PASS = "pass";
+  public static final String KEY_PLUGIN = "plugin";
   public static final String KEY_PRELOAD_TASK = "preload-task";
   public static final String KEY_PROPERTIES_PREFIX = "set-";
+  public static final String KEY_PROVIDER = "provider";
   public static final String KEY_READY_HINT = "ready-hint";
   public static final String KEY_SUBTASK = "subtask";
   public static final String KEY_SUBTASKS_EXTERNAL = "subtasks-external";
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfigCache.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfigCache.java
index 648c729..8f7d1f6 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfigCache.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfigCache.java
@@ -17,7 +17,6 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Project;
-import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.AllProjectsName;
@@ -42,6 +41,7 @@
 
   protected final CurrentUser user;
   protected final AllProjectsName allProjects;
+  protected final TaskPluginConfiguration config;
 
   protected final Map<BranchNameKey, PatchSetArgument> psaMasquerades = new HashMap<>();
   protected final Map<FileKey, TaskConfig> taskCfgByFile = new HashMap<>();
@@ -51,25 +51,23 @@
       AllProjectsNameProvider allProjectsNameProvider,
       GitRepositoryManager gitMgr,
       PermissionBackend permissionBackend,
-      CurrentUser user) {
+      CurrentUser user,
+      TaskPluginConfiguration config) {
     this.allProjects = allProjectsNameProvider.get();
     this.gitMgr = gitMgr;
     this.permissionBackend = permissionBackend;
     this.user = user;
+    this.config = config;
   }
 
   public TaskConfig getRootConfig() throws ConfigInvalidException, IOException {
-    return getTaskConfig(FileKey.create(getRootBranch(), TaskFileConstants.TASK_CFG));
+    return getTaskConfig(FileKey.create(config.getRootConfigBranch(), TaskFileConstants.TASK_CFG));
   }
 
   public void masquerade(PatchSetArgument psa) {
     psaMasquerades.put(psa.change.getDest(), psa);
   }
 
-  protected BranchNameKey getRootBranch() {
-    return BranchNameKey.create(allProjects, RefNames.REFS_CONFIG);
-  }
-
   public TaskConfig getTaskConfig(FileKey key) throws ConfigInvalidException, IOException {
     TaskConfig cfg = taskCfgByFile.get(key);
     if (cfg == null) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskKey.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskKey.java
index bd0b683..609160e 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskKey.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskKey.java
@@ -69,18 +69,21 @@
     protected String file;
     protected String task;
     protected GroupCache groupCache;
+    protected TaskPluginConfiguration config;
 
     Builder(
         FileKey relativeTo,
         AllProjectsName allProjectsName,
         AllUsersName allUsersName,
         AccountCache accountCache,
-        GroupCache groupCache) {
+        GroupCache groupCache,
+        TaskPluginConfiguration config) {
       this.relativeTo = relativeTo;
       this.allProjectsName = allProjectsName;
       this.allUsersName = allUsersName;
       this.accountCache = accountCache;
       this.groupCache = groupCache;
+      this.config = config;
     }
 
     public TaskKey buildTaskKey() {
@@ -167,8 +170,8 @@
                       .getGroupUUID()));
     }
 
-    public void setReferringAllProjectsTask() {
-      branch = BranchNameKey.create(allProjectsName, RefNames.REFS_CONFIG);
+    public void setReferringRootConfigBranchTask() {
+      branch = config.getRootConfigBranch();
     }
 
     protected void throwIfInvalidPath() throws ConfigInvalidException {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskPluginConfiguration.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskPluginConfiguration.java
new file mode 100644
index 0000000..496d005
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskPluginConfiguration.java
@@ -0,0 +1,94 @@
+// Copyright (C) 2024 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.googlesource.gerrit.plugins.task;
+
+import static com.google.common.base.MoreObjects.firstNonNull;
+
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AllProjectsNameProvider;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import org.eclipse.jgit.lib.Config;
+
+@Singleton
+public class TaskPluginConfiguration {
+  private static final String CACHEABLE_PREDICATES = "cacheablePredicates";
+  private static final String CACHEABLE_PREDICATES_SECTION = "byBranch";
+  private static final String CACHEABLE_PREDICATES_KEY = "className";
+  private static final String DEPRECATED_CACHEABLE_PREDICATES = "cacheable-predicates";
+  private static final String DEPRECATED_CACHEABLE_PREDICATES_KEY = "byBranch-className";
+  private static final String ROOT_CONFIG = "rootConfig";
+  private static final String ROOT_CONFIG_PROJECT_KEY = "project";
+  private static final String ROOT_CONFIG_BRANCH_KEY = "branch";
+  private final String plugin;
+  private final Config gerritConfig;
+  private final Config pluginConfig;
+  private final AllProjectsName allProjectsName;
+  private final Set<String> cacheableByBranchPredicateClassNames;
+  private final BranchNameKey rootConfigBranch;
+
+  @Inject
+  public TaskPluginConfiguration(
+      @PluginName String plugin,
+      @GerritServerConfig Config gerritConfig,
+      PluginConfigFactory pluginConfigFactory,
+      AllProjectsNameProvider allProjectsNameProvider) {
+    this.plugin = plugin;
+    this.gerritConfig = gerritConfig;
+    this.pluginConfig = pluginConfigFactory.getGlobalPluginConfig(plugin);
+    this.allProjectsName = allProjectsNameProvider.get();
+    cacheableByBranchPredicateClassNames =
+        new HashSet<>(Arrays.asList(readCacheableByBranchPredicateClassNames()));
+    rootConfigBranch = readRootConfigBranch();
+  }
+
+  public Set<String> getCacheableByBranchPredicateClassNames() {
+    return cacheableByBranchPredicateClassNames;
+  }
+
+  public BranchNameKey getRootConfigBranch() {
+    return rootConfigBranch;
+  }
+
+  private String[] readCacheableByBranchPredicateClassNames() {
+    String[] fromPluginConfig =
+        pluginConfig.getStringList(
+            CACHEABLE_PREDICATES, CACHEABLE_PREDICATES_SECTION, CACHEABLE_PREDICATES_KEY);
+    if (fromPluginConfig.length > 0) {
+      return fromPluginConfig;
+    }
+    // Read from gerrit config for backward compatibility. This can be removed once all known users
+    // have migrated to plugin config.
+    return gerritConfig.getStringList(
+        plugin, DEPRECATED_CACHEABLE_PREDICATES, DEPRECATED_CACHEABLE_PREDICATES_KEY);
+  }
+
+  private BranchNameKey readRootConfigBranch() {
+    String project = pluginConfig.getString(ROOT_CONFIG, null, ROOT_CONFIG_PROJECT_KEY);
+    String branch = pluginConfig.getString(ROOT_CONFIG, null, ROOT_CONFIG_BRANCH_KEY);
+    return BranchNameKey.create(
+        project != null ? Project.NameKey.parse(project) : allProjectsName,
+        firstNonNull(branch, RefNames.REFS_CONFIG));
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskPluginDefinedInfoFactory.java
similarity index 98%
rename from src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
rename to src/main/java/com/googlesource/gerrit/plugins/task/TaskPluginDefinedInfoFactory.java
index ca1ae2c..2e3dfa8 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskPluginDefinedInfoFactory.java
@@ -38,7 +38,7 @@
 import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
-public class TaskAttributeFactory implements ChangePluginDefinedInfoFactory {
+public class TaskPluginDefinedInfoFactory implements ChangePluginDefinedInfoFactory {
   public static final TaskPath MISSING_VIEW_PATH_CAPABILITY =
       new TaskPath(
           String.format(
@@ -111,7 +111,7 @@
   protected Statistics statistics;
 
   @Inject
-  public TaskAttributeFactory(
+  public TaskPluginDefinedInfoFactory(
       String pluginName,
       TaskTree.Factory taskTreeFactory,
       PredicateCache predicateCache,
@@ -262,7 +262,7 @@
     }
 
     protected TaskAttribute invalid() {
-      TaskAttribute invalid = TaskAttributeFactory.invalid();
+      TaskAttribute invalid = TaskPluginDefinedInfoFactory.invalid();
       if (task.isVisible) {
         invalid.name = task.name();
       }
@@ -362,7 +362,7 @@
       for (Node subNode :
           options.onlyApplicable ? node.getApplicableSubNodes() : node.getSubNodes()) {
         if (subNode instanceof Node.Invalid) {
-          subTasks.add(TaskAttributeFactory.invalid());
+          subTasks.add(TaskPluginDefinedInfoFactory.invalid());
         } else {
           new AttributeFactory(subNode).create().ifPresent(t -> subTasks.add(t));
         }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskReference.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskReference.java
index a7824ac..1ccf673 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskReference.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskReference.java
@@ -48,6 +48,7 @@
       AllUsersNameProvider allUsersNameProvider,
       AccountCache accountCache,
       GroupCache groupCache,
+      TaskPluginConfiguration config,
       @Assisted FileKey relativeTo,
       @Assisted String reference) {
     this(
@@ -56,7 +57,8 @@
             allProjectsNameProvider.get(),
             allUsersNameProvider.get(),
             accountCache,
-            groupCache),
+            groupCache,
+            config),
         reference);
   }
 
@@ -133,8 +135,8 @@
 
     @Override
     public void enterFile_path(TaskReferenceParser.File_pathContext ctx) {
-      if (ctx.ALL_PROJECTS_ROOT() != null || (ctx.FWD_SLASH() != null && ctx.absolute() != null)) {
-        builder.setReferringAllProjectsTask();
+      if (ctx.ROOT_PROJECT() != null || (ctx.FWD_SLASH() != null && ctx.absolute() != null)) {
+        builder.setReferringRootConfigBranchTask();
       }
 
       if (ctx.absolute() == null && ctx.relative() == null) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
index 572c545..bca5ccc 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
@@ -21,10 +21,12 @@
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.DynamicOptions;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.config.AllUsersNameProvider;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -38,6 +40,7 @@
 import com.googlesource.gerrit.plugins.task.TaskConfig.NamesFactoryType;
 import com.googlesource.gerrit.plugins.task.TaskConfig.Task;
 import com.googlesource.gerrit.plugins.task.TaskConfig.TasksFactory;
+import com.googlesource.gerrit.plugins.task.extensions.PluginProvidedTaskNamesFactory;
 import com.googlesource.gerrit.plugins.task.properties.Properties;
 import com.googlesource.gerrit.plugins.task.statistics.HitHashMap;
 import com.googlesource.gerrit.plugins.task.statistics.HitHashMapOfCollection;
@@ -96,6 +99,7 @@
   protected final Preloader preloader;
   protected final TaskConfigCache taskConfigCache;
   protected final TaskExpression.Factory taskExpressionFactory;
+  protected final DynamicMap<DynamicOptions.DynamicBean> dynamicBeans;
   protected final NodeList root = new NodeList();
   protected final Provider<ChangeQueryBuilder> changeQueryBuilderProvider;
   protected final Provider<ChangeQueryProcessor> changeQueryProcessorProvider;
@@ -120,7 +124,8 @@
       PredicateCache predicateCache,
       TaskExpression.Factory taskExpressionFactory,
       Preloader.Factory preloaderFactory,
-      @Assisted TaskConfigCache taskConfigCache) {
+      @Assisted TaskConfigCache taskConfigCache,
+      DynamicMap<DynamicOptions.DynamicBean> dynamicBeans) {
     this.accountResolver = accountResolver;
     this.allUsers = allUsers;
     this.user = user != null ? user : anonymousUser;
@@ -131,6 +136,7 @@
     this.taskConfigCache = taskConfigCache;
     this.taskExpressionFactory = taskExpressionFactory;
     this.preloader = preloaderFactory.create(taskConfigCache);
+    this.dynamicBeans = dynamicBeans;
   }
 
   public List<Node> getRootNodes(ChangeData changeData)
@@ -438,6 +444,9 @@
                 case CHANGE:
                   addChangeTypeTasks(tasksFactory, namesFactory);
                   continue;
+                case PLUGIN:
+                  addPluginTypeTasks(tasksFactory, namesFactory);
+                  continue;
               }
             }
           }
@@ -480,6 +489,32 @@
         addInvalidNode();
       }
 
+      protected void addPluginTypeTasks(TasksFactory tasksFactory, NamesFactory namesFactory)
+          throws IOException {
+        if (namesFactory.plugin != null && namesFactory.provider != null) {
+          List<String> names;
+          try {
+            PluginProvidedTaskNamesFactory providedTaskNamesFactory =
+                PluginProvidedTaskNamesFactory.getProxyInstance(
+                    dynamicBeans, namesFactory.plugin, namesFactory.provider);
+            names = providedTaskNamesFactory.getNames(getChangeData(), namesFactory.args);
+          } catch (Exception e) {
+            log.atSevere().withCause(e).log("Failed to get plugin provided task names");
+            addInvalidNode();
+            return;
+          }
+          for (String name : names) {
+            try {
+              addPreloaded(preloader.preload(task.config.new Task(tasksFactory, name)));
+            } catch (ConfigInvalidException e) {
+              addInvalidNode();
+            }
+          }
+          return;
+        }
+        addInvalidNode();
+      }
+
       public void addPreloaded(List<Task> defs) {
         nodes.addAll(factory.createFromPreloaded(defs));
       }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/extensions/PluginProvidedTaskNamesFactory.java b/src/main/java/com/googlesource/gerrit/plugins/task/extensions/PluginProvidedTaskNamesFactory.java
new file mode 100644
index 0000000..949db10
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/extensions/PluginProvidedTaskNamesFactory.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2024 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.googlesource.gerrit.plugins.task.extensions;
+
+import com.google.common.reflect.Reflection;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.server.DynamicOptions.DynamicBean;
+import com.google.gerrit.server.query.change.ChangeData;
+import java.lang.reflect.Method;
+import java.util.List;
+
+public interface PluginProvidedTaskNamesFactory extends DynamicBean {
+  static PluginProvidedTaskNamesFactory getProxyInstance(
+      DynamicMap<DynamicBean> dynamicBeans, String pluginName, String exportName)
+      throws IllegalArgumentException {
+    Object bean = dynamicBeans.get(pluginName, exportName);
+    if (bean == null) {
+      throw new IllegalArgumentException(
+          String.format(
+              "provider '%s' not found. Is plugin '%s' installed?", exportName, pluginName));
+    }
+    return getProxyInstance(PluginProvidedTaskNamesFactory.class, bean);
+  }
+
+  static <T> T getProxyInstance(Class<T> classz, Object bean)
+      throws ClassCastException, IllegalArgumentException {
+    return Reflection.newProxy(
+        classz,
+        (Object proxy, Method method, Object[] args) ->
+            bean.getClass()
+                .getMethod(method.getName(), method.getParameterTypes())
+                .invoke(bean, args));
+  }
+
+  List<String> getNames(ChangeData change, List<String> args) throws Exception;
+}
diff --git a/src/main/resources/Documentation/config-gerrit.md b/src/main/resources/Documentation/config-gerrit.md
deleted file mode 100644
index 6f0b0a5..0000000
--- a/src/main/resources/Documentation/config-gerrit.md
+++ /dev/null
@@ -1,25 +0,0 @@
-# Admin User Guide - Configuration
-
-## File `etc/gerrit.config`
-
-The file `'$site_path'/etc/gerrit.config` is a Git-style config file
-that controls many host specific settings for Gerrit.
-
-### Section @PLUGIN@ "cacheable-predicates"
-
-The @PLUGIN@.cacheable-predicates section configures Change Predicate
-optimizations which the @PLUGIN@ plugin may use when evaluating tasks.
-
-#### @PLUGIN@.cacheable-predicates.byBranch-className
-
-The value set with this key specifies a fully qualified class name
-of a Predicate which can be assumed to always return the same match
-result to all Changes destined for the same project/branch
-combinations. This key may be specified more than once.
-
-Example:
-
-```
-[@PLUGIN@ "cacheable-predicates"]
-        byBranch-className = com.google.gerrit.server.query.change.BranchSetPredicate
-```
\ No newline at end of file
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
new file mode 100644
index 0000000..5f13e18
--- /dev/null
+++ b/src/main/resources/Documentation/config.md
@@ -0,0 +1,45 @@
+# Admin User Guide - Configuration
+
+## File `etc/@PLUGIN@.config`
+
+The file `'$site_path'/etc/@PLUGIN@.config` is a Git-style config file
+that controls settings for @PLUGIN@ plugin.
+
+### Section "cacheablePredicates"
+
+The cacheablePredicates section configures Change Predicate
+optimizations which the @PLUGIN@ plugin may use when evaluating tasks.
+
+#### cacheablePredicates.byBranch.className
+
+The value set with this key specifies a fully qualified class name
+of a Predicate which can be assumed to always return the same match
+result to all Changes destined for the same project/branch
+combinations. This key may be specified more than once.
+
+Example:
+
+```
+[cacheablePredicates "byBranch"]
+        className = com.google.gerrit.server.query.change.BranchSetPredicate
+```
+
+### Section "rootConfig"
+
+The rootConfig section can be used to configure the project and branch containing the root task.config.
+
+#### rootConfig.project
+
+The plugin will fetch the root task.config from the project set for this key. Defaults to `All-Projects`.
+
+#### rootConfig.branch
+
+The plugin will fetch the root task.config from the branch set for this key. Defaults to `refs/meta/config`.
+
+Example:
+
+```
+[rootConfig]
+        project = task/configuration
+        branch = refs/heads/master
+```
diff --git a/src/main/resources/Documentation/design/task_plugin_config.md b/src/main/resources/Documentation/design/task_plugin_config.md
new file mode 100644
index 0000000..edbe2a1
--- /dev/null
+++ b/src/main/resources/Documentation/design/task_plugin_config.md
@@ -0,0 +1,100 @@
+Task Plugin Config
+==================
+
+***
+
+**Requirements**
+----------------
+1. Task config devs should be able to collaboratively develop and test config using their own accounts
+    1. Must support collaboration across task config devs in various teams
+    2. Devs can preview task config changes before submitting them (using --preview)
+    3. Admins/devs can find the config where a specific task is defined (using --include-paths)
+2. Gerrit users should be able to view applicable tasks on their changes even when those tasks come from various team specific configs
+3. Task config devs should be able to configure tasks whose definition and applicability are visible only to a desired set of users
+    1. Users outside that set should not see invalid tasks if the tasks are not applicable on their changes
+4. Task config devs should be able to create automated jobs to
+    1. Verify the correctness of their task config changes
+    2. Merge task config updates after their desired gating criteria is met
+    3. Create/update task config changes
+    4. Only validate the roots they are interested in
+5. Task config devs can define gating criteria for task configs they own
+    1.  Gating criteria for task configs can be defined using task config (nice to have)
+    2.  Can block submit until dependent change (group query/destinations update for example) is submitted
+
+***
+
+The next two sections highlight how meeting some of the above-listed
+requirements is difficult with the default configuration and compares that
+configuration versus a custom root project and branch configuration.
+
+**Task config root in All-Projects:refs/meta/config (default)**
+---------------------------------------------------------------
+#### Cons:
+  1. refs/meta/config has restricted permissions usually, so admins have to get involved to get config updates merged. Hard to satisfy requirement [4.2](#requirements)
+  2. Permissions on refs/meta/config have to be relaxed so that task config devs can read/push on that ref which may expose other configs on that ref
+  3. Having config in All-Projects leads to customized labels, submit requirements and code-owners config being inherited by other projects we didn't intend to apply to
+  4. Requires special setup for fetching/pushing to refs/meta/config
+  5. Relies on group refs for requirements [3, 4 ,5](#requirements). See [Solutions for requirement [3]](#solutions-for-requirement-3)
+#### Pros:
+  1. Consistent with other plugin configs
+  2. Common location for task config devs to share/find task config
+  3. Admins control task roots
+
+
+**Task config root in a custom project:branch**
+-----------------------------------------------
+#### Cons:
+  1. Not consistent with other plugin configs
+  2. Location of task config might be hard to find by task config devs
+  3. Relies on group refs for requirement [3](#requirements). See [Solutions for requirement [3]](#solutions-for-requirement-3)
+  4. Admins potentially have less oversight on task roots
+  5. Cannot have project configuration such as labels, submit requirements, code-owners per team. Independent project:branch per team needs to be used if this is needed.
+#### Pros:
+  1. Admins do only one time setup for project permissions
+  2. Permissions on the ref containing task configs can be setup as desired by the custom project:branch owner (might not include all task config devs)
+  3. Can customize labels, create new submit requirements and enable other plugins(code-owners for example) without any impact on other projects
+  4. Reduces dependency on moving team config to independent projects/branches
+
+
+***
+
+This section details the known solutions to requirement [3] whilst highlighting advantages and limitations of each solution.
+
+**Solutions for requirement [3]**
+---------------------------------
+### **Team task config on group refs**
+#### Cons:
+  1. Group refs are only visible to group owners and upstream does not want to change this. Service accounts and collaborators would need to become group owners in order to do anything useful with the team's group-based task config.
+  2. Requires internal Gerrit group
+  3. Requires special setup for fetching/pushing to refs/groups/..
+#### Pros:
+  1. Admins do a one-time setup to allow everyone to push/review/submit to group refs
+  2. Gerrit groups already have a concept of visibility based on membership
+
+### **Team task config on custom group refs (refs/heads/group-owned/..) in All-Users**
+#### Cons:
+  1. Not consistent with other plugin configs
+  2. Location of task config might be hard to find by task config devs
+  3. Admins have to set up permissions on the new refs/heads/group-owned/.. ref. See implementation gaps.
+  4. There is a risk that any project customization can interfere with core's use of All-Users
+  5. These refs are not an existing gerrit concept and might be hard to get acceptance from upstream
+#### Pros:
+  1. Permissions on the ref containing task configs can be setup as desired by the task config devs
+  2. Can customize labels, create new submit requirements and enable other plugins(code-owners for example) without any impact on other projects
+  3. Gerrit groups already have a concept of visibility based on membership
+#### Implementation gap:
+  1. Plugin currently does not support task expressions referring to a refs/heads/group-owned/..
+  2. No support for automatically configuring permissions on refs/heads/group-owned/.. or otherwise tying the ref to the group
+
+### **Team task config on custom project branch**
+#### Cons:
+  1. Not consistent with other plugin configs (but might be similar if refs/meta/config is used as the branch)
+  2. Location of task config might be hard to find by task config devs
+  3. Admins have to setup new projects for each team if existing projects are not used
+#### Pros:
+  1. Admins do only one time setup for project permissions. Might already be done if it is an existing project.
+  2. Permissions on the ref containing task configs can be setup as desired by the task config devs
+  3. Can customize labels, create new submit requirements and enable other plugins(code-owners for example) without any impact on other projects
+#### Implementation gap:
+  1. Plugin currently does not support task expressions referring to a custom project:branch
+
diff --git a/src/main/resources/Documentation/task.md b/src/main/resources/Documentation/task.md
index 75ee189..4cf8c71 100644
--- a/src/main/resources/Documentation/task.md
+++ b/src/main/resources/Documentation/task.md
@@ -72,12 +72,13 @@
 
 Tasks
 -----
-Tasks can either be root tasks, or subtasks. Tasks are defined in the
-`All-Projects` project, on the `refs/meta/config` branch, in a file named
-`task.config`. This file uses the gitconfig format to define tasks. The
-special "True" keyword may be used as any query definition to indicate
-an always matching query. The following keys may be defined in any
-task section:
+Tasks can either be root tasks, or subtasks. Tasks are expected to be
+defined in the `All-Projects` project, on the `refs/meta/config` branch
+(the root project and branch are [configurable](config.html#section-rootconfig)),
+in a file named `task.config`. This file uses the gitconfig format to define
+tasks. The special "True" keyword may be used as any query definition
+to indicate an always matching query. The following keys may be defined
+in any task section:
 
 `applicable`
 
@@ -350,37 +351,23 @@
 Names-Factory
 -------------
 A names-factory section defines a collection of name keys which are used to
-generate the names for task definitions.  The section should contain a "type"
-key that specifies the type.
+generate the names for task definitions. A names-factory section is referenced
+by a names-factory key in a "tasks-factory" section. This section should contain
+a `type` key that specifies the type.
 
-A names-factory section is referenced by a names-factory key in a "tasks-factory"
-section.  A sample task.config which defines a names-factory section might look like
-this:
+`type`
 
-```
-[names-factory "static names factory list"]
-    name = my a task
-    name = my b task
-    type = static
-```
+: This key is mandatory and defines the type of the names-factory section. The
+type must be one of: `static`, `change`, or `plugin`.
 
-The following keys may be defined in any names-factory section:
+**static type**
 
-`changes`
-
-: This key defines a query that is used to fetch change numbers which will be used
-as the names of the task(s).
-
-Example:
-```
-    changes = change:1 OR change:2
-```
+: One or more `name` key(s) are required.
 
 `name`
 
-: This key defines the name of the tasks.  This key may be used several times
-in order to define more than one task. The name key can only be used along with
-names-factory of type `static`.
+: This key defines the name of the tasks. It can be used several times in order
+to define more than one task.
 
 Example:
 ```
@@ -388,19 +375,89 @@
     name = 12345
 ```
 
-`type`
+Here is an example which defines a names-factory of `static` type.
 
-: This key defines the type of the names-factory section.  The type
-can be either `static` or `change`. For names-factory of type `static`,
-`name` key(s) should be defined where as names-factory of type `change`
-needs a `change` key to be defined.
+```
+[names-factory "static names factory"]
+    type = static
+    name = task A
+    name = task B
+```
+
+**change type**
+
+: The `changes` key is required.
+
+`changes`
+
+: This key defines a query that is used to fetch change numbers which will be
+used as the names of the task(s).
 
 Example:
 ```
-    type = static
-    type = change
+    changes = change:1 OR change:2
 ```
 
+Here is an example which defines a names-factory of `change` type.
+
+```
+[names-factory "changes names factory"]
+    type = change
+    changes = topic:sample AND status:open
+```
+
+**plugin type**
+
+: The `plugin` and `provider` keys are required, whereas any `arg` key(s)
+are optional. The provider class should implement the `PluginProvidedTaskNamesFactory`
+interface. The collection of name strings returned by the `getNames()` method will
+be used to generate the names for task definitions.
+
+`arg`
+
+: This key defines an argument that will be passed down to the provider method
+`getNames()`. This key may be used several times in order to define a list of
+arguments.
+
+Example:
+```
+    arg = foo
+```
+
+`plugin`
+
+: This key defines the name of a plugin. The plugin is expected to register
+(bind) a class which implements the `PluginProvidedTaskNamesFactory` interface.
+
+Example:
+```
+    plugin = foobar
+```
+
+`provider`
+
+: This key defines the exported name used to bind the class which implements the
+`PluginProvidedTaskNamesFactory` interface provided by the plugin.
+
+Example:
+```
+    provider = foobar_provider
+```
+
+Here is an example which defines a names-factory of `plugin` type.
+
+```
+[names-factory "plugin names factory"]
+    type = plugin
+    plugin = names_factory_provider
+    provider = foobar_provider
+    arg = myarg1
+    arg = myarg2
+```
+
+A plugin `names_factory_provider` created exclusively for use in the test framework
+can also be viewed as a reference.
+
 External Entries
 ----------------
 A name for external task files on other projects and branches may be given
diff --git a/src/main/resources/Documentation/task_expression.md b/src/main/resources/Documentation/task_expression.md
index c084a39..61f46d0 100644
--- a/src/main/resources/Documentation/task_expression.md
+++ b/src/main/resources/Documentation/task_expression.md
@@ -154,10 +154,12 @@
     ...
 ```
 
-To reference a task from root task.config on the All-Projects.git, prefix the task name with `//^`
-and to reference a task from task dir on the All-Projects.git, use
-`//<relative path from task dir>^<task_name>`. It doesn't matter which project, ref and file one
-is referencing from while using this syntax.
+To reference a task from the root task.config on the root project and branch
+(defaults to `All-Projects` and `refs/meta/config` and is
+[configurable](config.html#section-rootconfig)), prefix the task name with
+`//^` and to reference a task from the task dir on the root project and branch,
+use `//<relative path from task dir>^<task_name>`. It doesn't matter which
+project, ref and file one is referencing from while using this syntax.
 
 Example:
 
diff --git a/src/main/resources/Documentation/test/paths.md b/src/main/resources/Documentation/test/paths.md
index 8f43cb0..54dca08 100644
--- a/src/main/resources/Documentation/test/paths.md
+++ b/src/main/resources/Documentation/test/paths.md
@@ -1,5 +1,7 @@
-`task.config` file in project `All-Projects` on ref `refs/meta/config`.
+The config below is expected to be in the `task.config` file in project
+`{root-cfg-prj}` on ref `{root-cfg-branch}`.
 
+file: `{root-cfg-prj}:{root-cfg-branch}:task.config`
 ```
 [root "Root Task PATHS"]
   subtask = subtask pass
@@ -13,10 +15,10 @@
    "hasPass" : false,
    "name" : "Root Task PATHS",
    "path" : {
-      "ref" : "refs/meta/config",
+      "ref" : "{root-cfg-branch}",
       "file" : "task.config",
       "name" : "Root Task PATHS",
-      "project" : "All-Projects",
+      "project" : "{root-cfg-prj}",
       "type" : "root"
    },
    "status" : "PASS",
@@ -26,10 +28,10 @@
          "hasPass" : true,
          "name" : "subtask pass",
          "path" : {
-            "ref" : "refs/meta/config",
+            "ref" : "{root-cfg-branch}",
             "file" : "task.config",
             "name" : "subtask pass",
-            "project" : "All-Projects",
+            "project" : "{root-cfg-prj}",
             "type" : "task"
          },
          "status" : "PASS"
@@ -46,10 +48,10 @@
    "hasPass" : false,
    "name" : "Root other FILE",
    "path" : {
-      "ref" : "refs/meta/config",
+      "ref" : "{root-cfg-branch}",
       "file" : "task.config",
       "name" : "Root other FILE",
-      "project" : "All-Projects",
+      "project" : "{root-cfg-prj}",
       "type" : "root"
    },
    "status" : "WAITING",
@@ -59,10 +61,10 @@
          "hasPass" : true,
          "name" : "file task/common.config PASS",
          "path" : {
-            "ref" : "refs/meta/config",
+            "ref" : "{root-cfg-branch}",
             "file" : "task/common.config",
             "name" : "file task/common.config PASS",
-            "project" : "All-Projects",
+            "project" : "{root-cfg-prj}",
             "type" : "task"
          },
          "status" : "PASS"
@@ -72,10 +74,10 @@
          "hasPass" : true,
          "name" : "file task/common.config FAIL",
          "path" : {
-            "ref" : "refs/meta/config",
+            "ref" : "{root-cfg-branch}",
             "file" : "task/common.config",
             "name" : "file task/common.config FAIL",
-            "project" : "All-Projects",
+            "project" : "{root-cfg-prj}",
             "type" : "task"
          },
          "status" : "FAIL"
@@ -99,10 +101,10 @@
    "hasPass" : false,
    "name" : "Root tasks-factory",
    "path" : {
-      "ref" : "refs/meta/config",
+      "ref" : "{root-cfg-branch}",
       "file" : "task.config",
       "name" : "Root tasks-factory",
-      "project" : "All-Projects",
+      "project" : "{root-cfg-prj}",
       "type" : "root"
    },
    "status" : "WAITING",
@@ -112,10 +114,10 @@
          "hasPass" : false,
          "name" : "my a task",
          "path" : {
-            "ref" : "refs/meta/config",
+            "ref" : "{root-cfg-branch}",
             "file" : "task.config",
             "name" : "my a task",
-            "project" : "All-Projects",
+            "project" : "{root-cfg-prj}",
             "tasksFactory" : "tasks-factory example",
             "type" : "tasks-factory"
          },
@@ -126,10 +128,10 @@
          "hasPass" : false,
          "name" : "my b task",
          "path" : {
-            "ref" : "refs/meta/config",
+            "ref" : "{root-cfg-branch}",
             "file" : "task.config",
             "name" : "my b task",
-            "project" : "All-Projects",
+            "project" : "{root-cfg-prj}",
             "tasksFactory" : "tasks-factory example",
             "type" : "tasks-factory"
          },
@@ -150,10 +152,10 @@
    "hasPass" : false,
    "name" : "Root other PROJECT",
    "path" : {
-      "ref" : "refs/meta/config",
+      "ref" : "{root-cfg-branch}",
       "file" : "task.config",
       "name" : "Root other PROJECT",
-      "project" : "All-Projects",
+      "project" : "{root-cfg-prj}",
       "type" : "root"
    },
    "status" : "WAITING",
@@ -189,8 +191,11 @@
    ]
 }
 ```
-`task.config` file in project `All-Projects` on ref `refs/meta/config`.
 
+The config below is expected to be in the `task.config` file in project
+`{root-cfg-prj}` on ref `{root-cfg-branch}`.
+
+file: `{root-cfg-prj}:{root-cfg-branch}:task.config`
 ```
 [root "Root Capability Error"]
     applicable = is:open
diff --git a/src/main/resources/Documentation/test/task-preview/new_root_with_original_with_external_secret_ref.md b/src/main/resources/Documentation/test/task-preview/new_root_with_original_with_external_secret_ref.md
index 3d1d7cc..950d87c 100644
--- a/src/main/resources/Documentation/test/task-preview/new_root_with_original_with_external_secret_ref.md
+++ b/src/main/resources/Documentation/test/task-preview/new_root_with_original_with_external_secret_ref.md
@@ -1,6 +1,6 @@
 # --task-preview a new root, original root with subtasks-external pointing to secret user ref.
 
-file: `All-Projects.git:refs/meta/config:task.config`
+file: `{root-cfg-prj}:{root-cfg-branch}:task.config`
 ```
  [root "Root with SECRET external"]
      applicable = is:open
diff --git a/src/main/resources/Documentation/test/task-preview/non-secret_ref_with_external_secret_ref.md b/src/main/resources/Documentation/test/task-preview/non-secret_ref_with_external_secret_ref.md
index d192050..b5f04d7 100644
--- a/src/main/resources/Documentation/test/task-preview/non-secret_ref_with_external_secret_ref.md
+++ b/src/main/resources/Documentation/test/task-preview/non-secret_ref_with_external_secret_ref.md
@@ -1,6 +1,6 @@
 # --task-preview a non-secret user ref with subtasks-external pointing to secret user ref.
 
-file: `All-Projects.git:refs/meta/config:task.config`
+file: `{root-cfg-prj}:{root-cfg-branch}:task.config`
 ```
 [root "Root for NON-SECRET external Preview with SECRET external"]
     applicable = "is:open"
diff --git a/src/main/resources/Documentation/test/task-preview/non_root_with_subtask_from_root_task.md b/src/main/resources/Documentation/test/task-preview/non_root_with_subtask_from_root_task.md
index a0f11eb..afb80b0 100644
--- a/src/main/resources/Documentation/test/task-preview/non_root_with_subtask_from_root_task.md
+++ b/src/main/resources/Documentation/test/task-preview/non_root_with_subtask_from_root_task.md
@@ -1,6 +1,6 @@
 # --task-preview non-root file with subtask pointing root task
 
-file: `All-Projects.git:refs/meta/config:task.config`
+file: `{root-cfg-prj}:{root-cfg-branch}:task.config`
 ```
 [root "Points to subFile task with rootFile task preview"]
     applicable = is:open
@@ -12,7 +12,7 @@
     pass = True
 ```
 
-file: `All-Projects.git:refs/meta/config:task/foo/bar/baz.config`
+file: `{root-cfg-prj}:{root-cfg-branch}:task/foo/bar/baz.config`
 ```
  [task "Preview pointing to rootFile task"]
      applicable = is:open
diff --git a/src/main/resources/Documentation/test/task-preview/root_with_external_non-secret_ref_with_external_secret_ref.md b/src/main/resources/Documentation/test/task-preview/root_with_external_non-secret_ref_with_external_secret_ref.md
index c0add08..45a069d 100644
--- a/src/main/resources/Documentation/test/task-preview/root_with_external_non-secret_ref_with_external_secret_ref.md
+++ b/src/main/resources/Documentation/test/task-preview/root_with_external_non-secret_ref_with_external_secret_ref.md
@@ -1,6 +1,6 @@
 # --task-preview root file with subtasks-external pointing to a non-secret user ref with subtasks-external pointing to a secret user ref.
 
-file: `All-Projects.git:refs/meta/config:task.config`
+file: `{root-cfg-prj}:{root-cfg-branch}:task.config`
 ```
  [root "Root Preview NON-SECRET external with SECRET external"]
      applicable = "is:open"
diff --git a/src/main/resources/Documentation/test/task-preview/root_with_external_secret_ref.md b/src/main/resources/Documentation/test/task-preview/root_with_external_secret_ref.md
index 4d81b12..d41bc8b 100644
--- a/src/main/resources/Documentation/test/task-preview/root_with_external_secret_ref.md
+++ b/src/main/resources/Documentation/test/task-preview/root_with_external_secret_ref.md
@@ -1,6 +1,6 @@
 # --task-preview root file with subtasks-external pointing to secret user ref
 
-file: `All-Projects.git:refs/meta/config:task.config`
+file: `{root-cfg-prj}:{root-cfg-branch}:task.config`
 ```
  [root "Root Preview SECRET external"]
      applicable = is:open
diff --git a/src/main/resources/Documentation/test/task-preview/subtask_using_group_syntax/root_with_subtask_non-secret_ref_with_subtask_secret_ref.md b/src/main/resources/Documentation/test/task-preview/subtask_using_group_syntax/root_with_subtask_non-secret_ref_with_subtask_secret_ref.md
index 504d5a8..883dc0d 100644
--- a/src/main/resources/Documentation/test/task-preview/subtask_using_group_syntax/root_with_subtask_non-secret_ref_with_subtask_secret_ref.md
+++ b/src/main/resources/Documentation/test/task-preview/subtask_using_group_syntax/root_with_subtask_non-secret_ref_with_subtask_secret_ref.md
@@ -1,6 +1,6 @@
 # --task-preview root file with subtask pointing to a non-secret group ref with subtask pointing to a secret group ref.
 
-file: `All-Projects.git:refs/meta/config:task.config`
+file: `{root-cfg-prj}:{root-cfg-branch}:task.config`
 ```
  [root "Root Preview NON-SECRET group subtask with SECRET group subtask"]
      applicable = "is:open"
diff --git a/src/main/resources/Documentation/test/task-preview/subtask_using_group_syntax/root_with_subtask_secret_ref.md b/src/main/resources/Documentation/test/task-preview/subtask_using_group_syntax/root_with_subtask_secret_ref.md
index 9a1932c..6a879cb 100644
--- a/src/main/resources/Documentation/test/task-preview/subtask_using_group_syntax/root_with_subtask_secret_ref.md
+++ b/src/main/resources/Documentation/test/task-preview/subtask_using_group_syntax/root_with_subtask_secret_ref.md
@@ -1,6 +1,6 @@
 # --task-preview root file with subtask pointing to secret group ref
 
-file: `All-Projects.git:refs/meta/config:task.config`
+file: `{root-cfg-prj}:{root-cfg-branch}:task.config`
 ```
  [root "Root Preview SECRET external group"]
      applicable = is:open
diff --git a/src/main/resources/Documentation/test/task-preview/subtask_using_user_syntax/root_with_subtask_non-secret_ref_with_subtask_secret_ref.md b/src/main/resources/Documentation/test/task-preview/subtask_using_user_syntax/root_with_subtask_non-secret_ref_with_subtask_secret_ref.md
index fad2f1d..a9469eb 100644
--- a/src/main/resources/Documentation/test/task-preview/subtask_using_user_syntax/root_with_subtask_non-secret_ref_with_subtask_secret_ref.md
+++ b/src/main/resources/Documentation/test/task-preview/subtask_using_user_syntax/root_with_subtask_non-secret_ref_with_subtask_secret_ref.md
@@ -1,6 +1,6 @@
 # --task-preview root file with subtask pointing to a non-secret user ref with subtask pointing to a secret user ref.
 
-file: `All-Projects.git:refs/meta/config:task.config`
+file: `{root-cfg-prj}:{root-cfg-branch}:task.config`
 ```
  [root "Root Preview NON-SECRET subtask with SECRET subtask"]
      applicable = "is:open"
diff --git a/src/main/resources/Documentation/test/task-preview/subtask_using_user_syntax/root_with_subtask_secret_ref.md b/src/main/resources/Documentation/test/task-preview/subtask_using_user_syntax/root_with_subtask_secret_ref.md
index d2b3349..007f53a 100644
--- a/src/main/resources/Documentation/test/task-preview/subtask_using_user_syntax/root_with_subtask_secret_ref.md
+++ b/src/main/resources/Documentation/test/task-preview/subtask_using_user_syntax/root_with_subtask_secret_ref.md
@@ -1,6 +1,6 @@
 # --task-preview root file with subtask pointing to secret user ref
 
-file: `All-Projects.git:refs/meta/config:task.config`
+file: `{root-cfg-prj}:{root-cfg-branch}:task.config`
 ```
  [root "Root Preview SECRET external"]
      applicable = is:open
diff --git a/src/main/resources/Documentation/test/task_states.md b/src/main/resources/Documentation/test/task_states.md
index 7a6a8d0..54ef4fb 100644
--- a/src/main/resources/Documentation/test/task_states.md
+++ b/src/main/resources/Documentation/test/task_states.md
@@ -15,9 +15,9 @@
 ```
 
 The config below is expected to be in the `task.config` file in project
-`All-Projects` on ref `refs/meta/config`.
+`{root-cfg-prj}` on ref `{root-cfg-branch}`.
 
-file: `All-Projects:refs/meta/config:task.config`
+file: `{root-cfg-prj}:{root-cfg-branch}:task.config`
 ```
 [root "Root N/A"]
   applicable = is:closed # Assumes test query is "is:open"
@@ -784,6 +784,42 @@
    ]
 }
 
+[root "Root tasks-factory PLUGIN"]
+  applicable = status:new
+  subtasks-factory = tasks-factory plugin
+
+[tasks-factory "tasks-factory plugin"]
+  names-factory = names-factory plugin list
+  fail = True
+
+[names-factory "names-factory plugin list"]
+  type = plugin
+  plugin = names-factory-provider
+  provider = foobar_provider
+  arg = baz
+  arg = qux
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root tasks-factory PLUGIN",
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "foobar-test-baz",
+         "status" : "FAIL"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "foobar-test-qux",
+         "status" : "FAIL"
+      }
+   ]
+}
+
 [root "Root tasks-factory static (empty name)"]
   subtasks-factory = tasks-factory static (empty name)
   # Grouping task since it has no pass criteria, not output since it has no subtasks
@@ -1373,6 +1409,36 @@
    ]
 }
 
+[root "Root Properties tasks-factory PLUGIN"]
+  subtasks-factory = tasks-factory PLUGIN Properties
+
+[tasks-factory "tasks-factory PLUGIN Properties"]
+  set-welcome-message = Welcome to the party
+  names-factory = names-factory plugin provided list
+  fail-hint = ${welcome-message} Name(${_name}) Change Number(${_change_number}) Change Id(${_change_id}) Change Project(${_change_project}) Change Branch(${_change_branch}) Change Status(${_change_status}) Change Topic(${_change_topic})
+  fail = True
+
+[names-factory "names-factory plugin provided list"]
+  type = plugin
+  plugin = names-factory-provider
+  provider = foobar_provider
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root Properties tasks-factory PLUGIN",
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "hint" : "Welcome to the party Name(foobar-test) Change Number(_change_number) Change Id(_change_id) Change Project(_change_project) Change Branch(_change_branch) Change Status(_change_status) Change Topic(_change_topic)",
+         "name" : "foobar-test",
+         "status" : "FAIL"
+      }
+   ]
+}
+
 [root "Root tasks-factory _name Property Reference"]
   subtasks-factory = Properties tasks-factory _name Property Reference
 
@@ -2372,18 +2438,18 @@
    ]
 }
 
-[root "Root Reference tasks from All-Projects"]
+[root "Root Reference tasks from {root-cfg-prj}"]
   applicable = is:open
   subtask = //^Subtask PASS
-  subtask = @testuser/dir/relative.config^Import All-Projects root task
-  subtask = @testuser/dir/relative.config^Import All-Projects non-root task
-  subtask = %{non_secret_group_name_without_space}/dir/relative.config^Import All-Projects root task - groups
-  subtask = %{non_secret_group_name_without_space}/dir/relative.config^Import All-Projects non-root task - groups
+  subtask = @testuser/dir/relative.config^Import {root-cfg-prj} root task
+  subtask = @testuser/dir/relative.config^Import {root-cfg-prj} non-root task
+  subtask = %{non_secret_group_name_without_space}/dir/relative.config^Import {root-cfg-prj} root task - groups
+  subtask = %{non_secret_group_name_without_space}/dir/relative.config^Import {root-cfg-prj} non-root task - groups
 
 {
    "applicable" : true,
    "hasPass" : false,
-   "name" : "Root Reference tasks from All-Projects",
+   "name" : "Root Reference tasks from {root-cfg-prj}",
    "status" : "PASS",
    "subTasks" : [
       {
@@ -2395,7 +2461,7 @@
       {
          "applicable" : true,
          "hasPass" : false,
-         "name" : "Import All-Projects root task",
+         "name" : "Import {root-cfg-prj} root task",
          "status" : "PASS",
          "subTasks" : [
             {
@@ -2409,7 +2475,7 @@
       {
          "applicable" : true,
          "hasPass" : false,
-         "name" : "Import All-Projects non-root task",
+         "name" : "Import {root-cfg-prj} non-root task",
          "status" : "PASS",
          "subTasks" : [
             {
@@ -2423,7 +2489,7 @@
       {
          "applicable" : true,
          "hasPass" : false,
-         "name" : "Import All-Projects root task - groups",
+         "name" : "Import {root-cfg-prj} root task - groups",
          "status" : "PASS",
          "subTasks" : [
             {
@@ -2437,7 +2503,7 @@
       {
          "applicable" : true,
          "hasPass" : false,
-         "name" : "Import All-Projects non-root task - groups",
+         "name" : "Import {root-cfg-prj} non-root task - groups",
          "status" : "PASS",
          "subTasks" : [
             {
@@ -2674,7 +2740,7 @@
 }
 
 [root "Root Preload from user ref which has subtask in different file"]
-  preload-task = @testuser/dir/relative.config^Import All-Projects root task
+  preload-task = @testuser/dir/relative.config^Import {root-cfg-prj} root task
 
 {
    "applicable" : true,
@@ -3334,7 +3400,7 @@
 
 ```
 
-file: `All-Projects:refs/meta/config:task/common.config`
+file: `{root-cfg-prj}:{root-cfg-branch}:task/common.config`
 ```
 [task "file task/common.config PASS"]
   applicable = is:open
@@ -3345,7 +3411,7 @@
   fail = is:open
 ```
 
-file: `All-Projects:refs/meta/config:task/relative.config`
+file: `{root-cfg-prj}:{root-cfg-branch}:task/relative.config`
 ```
 [task "Root Import task from subdir using relative syntax"]
     subtask = dir/common.config^Sample relative task in sub dir
@@ -3355,7 +3421,7 @@
     pass = True
 ```
 
-file: `All-Projects:refs/meta/config:task/dir/common.config`
+file: `{root-cfg-prj}:{root-cfg-branch}:task/dir/common.config`
 ```
 [task "Sample relative task in sub dir"]
     applicable = is:open
@@ -3422,7 +3488,7 @@
     preload-task = %{non_secret_group_name_without_space}/foo/bar.config^Absolute Task 1
 ```
 
-file: `All-Projects:refs/meta/config:task/invalids.config`
+file: `{root-cfg-prj}:{root-cfg-branch}:task/invalids.config`
 ```
 [task "No PASS criteria"]
   fail-hint = Invalid without Pass criteria and without subtasks
@@ -3641,11 +3707,11 @@
   applicable = is:open
   pass = True
 
-[task "Import All-Projects root task"]
+[task "Import {root-cfg-prj} root task"]
   applicable = is:open
   subtask = //^Subtask PASS
 
-[task "Import All-Projects non-root task"]
+[task "Import {root-cfg-prj} non-root task"]
   applicable = is:open
   subtask = //dir/common.config^Sample relative task in sub dir
 ```
@@ -3666,11 +3732,11 @@
 
 file: `All-Users:refs/groups/{sharded_non_secret_group_uuid_without_space}:task/dir/relative.config`
 ```
-[task "Import All-Projects root task - groups"]
+[task "Import {root-cfg-prj} root task - groups"]
   applicable = is:open
   subtask = //^Subtask PASS
 
-[task "Import All-Projects non-root task - groups"]
+[task "Import {root-cfg-prj} non-root task - groups"]
   applicable = is:open
   subtask = //dir/common.config^Sample relative task in sub dir
 ```
diff --git a/src/test/java/com/googlesource/gerrit/plugins/names_factory_provider/FoobarProvider.java b/src/test/java/com/googlesource/gerrit/plugins/names_factory_provider/FoobarProvider.java
new file mode 100644
index 0000000..0ea2535
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/names_factory_provider/FoobarProvider.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2024 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.googlesource.gerrit.plugins.names_factory_provider;
+
+import com.google.gerrit.server.query.change.ChangeData;
+import com.googlesource.gerrit.plugins.task.extensions.PluginProvidedTaskNamesFactory;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class FoobarProvider implements PluginProvidedTaskNamesFactory {
+  public static final String DELIMITER = "-";
+
+  @Override
+  public List<String> getNames(ChangeData changeData, List<String> args) throws Exception {
+    String name = String.join(DELIMITER, "foobar", changeData.project().get());
+    if (args == null || args.isEmpty()) {
+      return List.of(name);
+    }
+    return new ArrayList<>(
+        args.stream().map(x -> String.join(DELIMITER, name, x)).collect(Collectors.toList()));
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/names_factory_provider/Module.java b/src/test/java/com/googlesource/gerrit/plugins/names_factory_provider/Module.java
new file mode 100644
index 0000000..174a981
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/names_factory_provider/Module.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2024 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.googlesource.gerrit.plugins.names_factory_provider;
+
+import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.server.DynamicOptions;
+import com.google.inject.AbstractModule;
+
+public class Module extends AbstractModule {
+  @Override
+  protected void configure() {
+    bind(DynamicOptions.DynamicBean.class)
+        .annotatedWith(Exports.named("foobar_provider"))
+        .to(FoobarProvider.class);
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/task/TaskExpressionTest.java b/src/test/java/com/googlesource/gerrit/plugins/task/TaskExpressionTest.java
index 8ea0009..7afbeab 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/task/TaskExpressionTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/task/TaskExpressionTest.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllUsersName;
+import com.google.inject.Inject;
 import java.util.Iterator;
 import java.util.NoSuchElementException;
 import junit.framework.TestCase;
@@ -54,6 +55,8 @@
   public static String REL_WORLD_PEACE_CFG = "world/peace.config";
   public static String ABS_PEACE_CFG = "/peace.config";
 
+  @Inject private TaskPluginConfiguration config;
+
   public void testBlank() {
     TaskExpression exp = getTaskExpression("");
     Iterator<TaskKey> it = exp.iterator();
@@ -245,7 +248,8 @@
                         new AllProjectsName("All-Projects"),
                         new AllUsersName("All-Users"),
                         accountCache,
-                        groupCache),
+                        groupCache,
+                        config),
                     (String) invocation.getArguments()[1]));
     return new TaskExpression(factory, file, expression);
   }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/task/TaskReferenceTest.java b/src/test/java/com/googlesource/gerrit/plugins/task/TaskReferenceTest.java
index f94fcb2..c6b41d1 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/task/TaskReferenceTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/task/TaskReferenceTest.java
@@ -25,16 +25,20 @@
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AllProjectsNameProvider;
 import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.PluginConfigFactory;
 import java.time.Instant;
 import java.util.NoSuchElementException;
 import java.util.Optional;
 import junit.framework.TestCase;
 import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
 import org.junit.Test;
 import org.mockito.Mockito;
 
 public class TaskReferenceTest extends TestCase {
+  private static final String PLUGIN = "task";
   private static final String ALL_USERS = "All-Users";
   private static final String ALL_PROJECTS = "All-Projects";
   public static String SIMPLE = "simple";
@@ -251,6 +255,8 @@
   protected static TaskKey getTaskFromReference(FileKey file, String expression) {
     AccountCache accountCache = Mockito.mock(AccountCache.class);
     GroupCache groupCache = Mockito.mock(GroupCache.class);
+    PluginConfigFactory pluginConfigFactory = Mockito.mock(PluginConfigFactory.class);
+    AllProjectsNameProvider allProjectsNameProvider = Mockito.mock(AllProjectsNameProvider.class);
     Mockito.when(accountCache.getByUsername(TEST_USER))
         .thenReturn(Optional.of(AccountState.forAccount(TEST_USER_ACCOUNT)));
     Mockito.when(groupCache.get(TEST_GROUP1_NAME)).thenReturn(Optional.of(TEST_GROUP1));
@@ -259,7 +265,8 @@
         .thenReturn(Optional.of(TEST_GROUP1));
     Mockito.when(groupCache.get(AccountGroup.uuid(TEST_GROUP2_UUID)))
         .thenReturn(Optional.of(TEST_GROUP2));
-
+    Mockito.when(allProjectsNameProvider.get()).thenReturn(new AllProjectsName(ALL_PROJECTS));
+    Mockito.when(pluginConfigFactory.getGlobalPluginConfig(PLUGIN)).thenReturn(new Config());
     try {
       return new TaskReference(
               new TaskKey.Builder(
@@ -267,7 +274,9 @@
                   new AllProjectsName(ALL_PROJECTS),
                   new AllUsersName(ALL_USERS),
                   accountCache,
-                  groupCache),
+                  groupCache,
+                  new TaskPluginConfiguration(
+                      PLUGIN, new Config(), pluginConfigFactory, allProjectsNameProvider)),
               expression)
           .getTaskKey();
     } catch (ConfigInvalidException e) {
diff --git a/test/check_task_statuses.sh b/test/check_task_statuses.sh
index 30e2ece..9a5735b 100755
--- a/test/check_task_statuses.sh
+++ b/test/check_task_statuses.sh
@@ -38,7 +38,7 @@
             q_setup setup_repo "$project_dir" "$REMOTE_USERS" "$ref"
         fi
 
-        mkdir -p "$(dirname "$project_dir/$file")"
+        mkdir -p -- "$(dirname -- "$project_dir/$file")"
         md_marker_content "$DOC_STATES" "$marker" | replace_user \
             | testdoc_2_cfg > "$project_dir/$file"
 
@@ -72,6 +72,8 @@
 
     --help|-h                         help text
     --server|-s                       gerrit host
+    --root-config-project             project containing the root task config
+    --root-config-branch              branch containing the root task config
     --non-secret-user                 user who don't have permission
                                       to view other user refs.
     --untrusted-user                  user who doesn't have permission
@@ -94,23 +96,14 @@
 DOCS=$MYDIR/.././src/main/resources/Documentation/test
 OUT=$MYDIR/../target/tests
 
-ALL=$OUT/All-Projects
-ALL_TASKS=$ALL/task
-
-USERS=$OUT/All-Users
-USER_TASKS=$USERS/task
-
-EXPECTED=$OUT/expected
-ACTUAL=$OUT/actual
-
-ROOT_CFG=$ALL/task.config
-
 # --- Args ----
 
 while (( "$#" )) ; do
     case "$1" in
         --help|-h)                        usage ;;
         --server|-s)                      shift ; SERVER=$1 ;;
+        --root-config-project)            shift ; ROOT_CONFIG_PRJ=$1 ;;
+        --root-config-branch)             shift ; ROOT_CONFIG_BRANCH=$1 ;;
         --non-secret-user)                shift ; NON_SECRET_USER=$1 ;;
         --untrusted-user)                 shift ; UNTRUSTED_USER=$1 ;;
         --non-secret-group-without-space) shift ; GROUP_NAME_WITHOUT_SPACE=$1 ;;
@@ -126,19 +119,31 @@
 [ -z "$GROUP_NAME_WITHOUT_SPACE" ] && usage "You must specify --non-secret-group-without-space"
 [ -z "$GROUP_NAME_WITH_SPACE" ] && usage "You must specify --non-secret-group-with-space"
 [ -z "$GERRIT_GIT_DIR" ] && usage "GERRIT_GIT_DIR environment variable not set"
+[ -z "$ROOT_CONFIG_PRJ" ] && ROOT_CONFIG_PRJ=All-Projects
+[ -z "$ROOT_CONFIG_BRANCH" ] && ROOT_CONFIG_BRANCH=refs/meta/config
 
 
 PORT=29418
 HTTP_PORT=8080
 PROJECT=test
 BRANCH=master
-REMOTE_ALL=ssh://$SERVER:$PORT/All-Projects
+REMOTE_ALL=ssh://$SERVER:$PORT/$ROOT_CONFIG_PRJ
 REMOTE_USERS=ssh://$SERVER:$PORT/All-Users
 REMOTE_TEST=ssh://$SERVER:$PORT/$PROJECT
-
-REF_ALL=refs/meta/config
+REF_ALL=$ROOT_CONFIG_BRANCH
 REF_USERS=refs/users/self
 
+ALL=$OUT/$ROOT_CONFIG_PRJ
+ALL_TASKS=$ALL/task
+
+USERS=$OUT/All-Users
+USER_TASKS=$USERS/task
+
+EXPECTED=$OUT/expected
+ACTUAL=$OUT/actual
+
+ROOT_CFG=$ALL/task.config
+
 CONFIG=$ROOT_CFG
 
 declare -A USER_REFS
@@ -151,7 +156,7 @@
 GROUP_EXPANDED_BY_PLACEHOLDER["{sharded_non_secret_group_uuid_without_space}"]="$(get_sharded_group_uuid "$GROUP_NAME_WITHOUT_SPACE")"
 GROUP_EXPANDED_BY_PLACEHOLDER["{sharded_non_secret_group_uuid_with_space}"]="$(get_sharded_group_uuid "$GROUP_NAME_WITH_SPACE")"
 
-mkdir -p "$OUT" "$ALL_TASKS" "$USER_TASKS"
+mkdir -p -- "$OUT" "$ALL_TASKS" "$USER_TASKS"
 
 q_setup setup_repo "$ALL" "$REMOTE_ALL" "$REF_ALL"
 q_setup setup_repo "$USERS" "$REMOTE_USERS" "$REF_USERS" --initial-commit
@@ -204,22 +209,28 @@
 no_all_no_visible2_json=$(echo "$all2_pjson" | remove_suites "all" "visible")
 
 echo "$no_all_visible_json" | strip_non_applicable | \
-    grep -v "\"applicable\" :" > "$EXPECTED".applicable
+    grep -v "\"applicable\" :" | ensure json_pp > "$EXPECTED".applicable
 
 echo "$no_all_no_visible_json" | strip_non_applicable | \
-    grep -v "\"applicable\" :" > "$EXPECTED".applicable-visibility
+    grep -v "\"applicable\" :" | \
+    ensure json_pp > "$EXPECTED".applicable-visibility
 
 echo "$no_all_visible2_json" | strip_non_applicable | \
-    grep -v "\"applicable\" :" > "$EXPECTED".applicable2
+    grep -v "\"applicable\" :" | ensure json_pp > "$EXPECTED".applicable2
 
 echo "$no_all_no_visible2_json" | strip_non_applicable | \
-    grep -v "\"applicable\" :" > "$EXPECTED".applicable-visibility2
+    grep -v "\"applicable\" :" | \
+    ensure json_pp > "$EXPECTED".applicable-visibility2
 
-echo "$all_pjson" | remove_suites "!all" "!visible" | ensure json_pp > "$EXPECTED".all
+echo "$all_pjson" | remove_suites "!all" "!visible" | \
+    ensure json_pp > "$EXPECTED".all
 
-echo "$no_all_visible_json" | strip_non_invalid > "$EXPECTED".invalid
+echo "$no_all_visible_json" | strip_non_invalid | \
+    ensure json_pp > "$EXPECTED".invalid
 
-strip_non_invalid < "$EXPECTED".applicable > "$EXPECTED".invalid-applicable
+echo "$no_all_visible_json" | strip_non_applicable | \
+    grep -v "\"applicable\" :" | strip_non_invalid | \
+    ensure json_pp > "$EXPECTED".invalid-applicable
 
 
 preview_pjson=$(example "$DOC_PREVIEW" 1 | testdoc_2_pjson)
@@ -228,7 +239,7 @@
 echo "$preview_pjson" | remove_suites "invalid" "!secret" | \
     ensure json_pp > "$EXPECTED".preview-admin
 echo "$preview_pjson" | remove_suites "secret" "!invalid" | \
-    strip_non_invalid > "$EXPECTED".preview-invalid
+    strip_non_invalid | ensure json_pp > "$EXPECTED".preview-invalid
 
 example "$DOC_PREVIEW" 1 | testdoc_2_cfg | replace_user > "$ROOT_CFG"
 cnum=$(create_repo_change "$ALL" "$REMOTE_ALL" "$REF_ALL")
diff --git a/test/check_task_visibility.sh b/test/check_task_visibility.sh
index 1dabec4..15566f7 100755
--- a/test/check_task_visibility.sh
+++ b/test/check_task_visibility.sh
@@ -93,7 +93,7 @@
 }
 
 get_remote() { # project > remote_url
-    echo "ssh://$SERVER:$PORT/$(basename "$1")"
+    echo "ssh://$SERVER:$PORT/$(basename -- "$1")"
 }
 
 # Gets json from the preview doc and creates
@@ -129,7 +129,7 @@
         local tip_content
 
         q_setup setup_repo "$project" "$(get_remote "$project")" "$ref"
-        mkdir -p "$(dirname "$project/$file")"
+        mkdir -p "$(dirname -- "$project/$file")"
 
         if diff_indicators_present "$content" ; then
             CHANGE_FILE_MARKER=$marker
@@ -173,6 +173,8 @@
 
     --help|-h                     help text
     --server|-s                   gerrit host
+    --root-config-project         project containing the root task config
+    --root-config-branch          branch containing the root task config
     --non-secret-user             user who doesn't have permission
                                   to view other user refs.
     --non-secret-group            non-secret group name
@@ -187,6 +189,8 @@
     case "$1" in
         --help|-h)                usage ;;
         --server|-s)              shift ; SERVER=$1 ;;
+        --root-config-project)    shift ; ROOT_CONFIG_PRJ=$1 ;;
+        --root-config-branch)     shift ; ROOT_CONFIG_BRANCH=$1 ;;
         --non-secret-user)        shift ; NON_SECRET_USER=$1 ;;
         --non-secret-group)       shift ; NON_SECRET_GROUP_NAME=$1 ;;
         --secret-group)           shift ; SECRET_GROUP_NAME=$1 ;;
@@ -200,6 +204,8 @@
 [ -z "$NON_SECRET_GROUP_NAME" ] && usage "You must specify --non-secret-group"
 [ -z "$SECRET_GROUP_NAME" ] && usage "You must specify --secret-group"
 [ -z "$GERRIT_GIT_DIR" ] && usage "GERRIT_GIT_DIR environment variable not set"
+[ -z "$ROOT_CONFIG_PRJ" ] && ROOT_CONFIG_PRJ=All-Projects
+[ -z "$ROOT_CONFIG_BRANCH" ] && ROOT_CONFIG_BRANCH=refs/meta/config
 
 RESULT=0
 PORT=29418
@@ -240,7 +246,8 @@
 )
 
 for test in "${TESTS[@]}" ; do
-    TEST_DOC="$(replace_user_refs < "$TEST_DOC_DIR/$test" | replace_users | replace_groups)"
+    TEST_DOC="$(replace_user_refs < "$TEST_DOC_DIR/$test" | replace_users | replace_groups | \
+        replace_root_configs)"
     init_configs
     test_change
 done
diff --git a/test/docker/docker-compose.yaml b/test/docker/docker-compose.yaml
index a228122..57cb5f2 100755
--- a/test/docker/docker-compose.yaml
+++ b/test/docker/docker-compose.yaml
@@ -7,6 +7,7 @@
       args:
         - GERRIT_WAR
         - TASK_PLUGIN_JAR
+        - NAMES_FACTORY_PROVIDER_PLUGIN_JAR
     networks:
       - gerrit-net
     volumes:
diff --git a/test/docker/gerrit/Dockerfile b/test/docker/gerrit/Dockerfile
index 65b717b..e41d8f5 100755
--- a/test/docker/gerrit/Dockerfile
+++ b/test/docker/gerrit/Dockerfile
@@ -9,5 +9,6 @@
 
 COPY artifacts /tmp/
 RUN cp /tmp/task.jar "$GERRIT_SITE/plugins/task.jar"
+RUN cp /tmp/names-factory-provider.jar "$GERRIT_SITE/plugins/names-factory-provider.jar"
 RUN { [ -e /tmp/gerrit.war ] && cp /tmp/gerrit.war "$GERRIT_SITE/bin/gerrit.war" ; } || true
 RUN chmod 777 "$GERRIT_SITE/git"
diff --git a/test/docker/run.sh b/test/docker/run.sh
index 5f9b412..0335cfa 100755
--- a/test/docker/run.sh
+++ b/test/docker/run.sh
@@ -24,21 +24,26 @@
 usage() { # [error_message]
     cat <<-EOF
 Usage:
-    $MYPROG [--task-plugin-jar|-t <FILE_PATH>] [--gerrit-war|-g <FILE_PATH>]
+    $MYPROG [--task-plugin-jar|-t <FILE_PATH>]
+            [--names-factory-provider-plugin-jar|-f <FILE_PATH>]
+            [--gerrit-war|-g <FILE_PATH>]
 
     This tool runs the plugin functional tests in a Docker environment built
     from the gerritcodereview/gerrit base Docker image.
 
-    The task plugin JAR and optionally a Gerrit WAR are expected to be in the
-    $ARTIFACTS dir;
-    however, the --task-plugin-jar and --gerrit-war switches may be used as
-    helpers to specify which files to copy there.
+    The task plugin JAR, names-factory-provider plugin JAR and optionally a
+    Gerrit WAR are expected to be in the $ARTIFACTS dir;
+    however, the --task-plugin-jar, --names-factory-provider-plugin-jar and
+    --gerrit-war switches may be used as helpers to specify which files to
+    copy there.
 
     Options:
     --help|-h
-    --gerrit-war|-g            path to Gerrit WAR file
-    --task-plugin-jar|-t       path to task plugin JAR file
-    --preserve                 To preserve the docker setup for debugging
+    --gerrit-war|-g                           path to Gerrit WAR file
+    --task-plugin-jar|-t                      path to task plugin JAR file
+    --names-factory-provider-plugin-jar|-f    path to names-factory-provider plugin JAR file.
+                                              It can be generated using 'bazel build names-factory-provider'
+    --preserve                                To preserve the docker setup for debugging
 
 EOF
 
@@ -56,14 +61,44 @@
 }
 
 run_task_plugin_tests() {
+    echo "Running test suite with default root_project and root_branch"
     docker-compose "${COMPOSE_ARGS[@]}" up --detach
     docker-compose "${COMPOSE_ARGS[@]}" exec -T --user=admin run_tests \
         '/task/test/docker/run_tests/start.sh'
+    # TODO: Once the 'retest' functionality is fixed, the re-run with custom root prj and
+    #       branch can be done without needing to bring down and start the containers again.
+    docker-compose "${COMPOSE_ARGS[@]}" down -v --rmi local 2>/dev/null
+
+    ROOT_CFG_PRJ=task-config
+    ROOT_CFG_BRANCH=refs/heads/master
+    echo "Re-running test suite with root_project=$ROOT_CFG_PRJ and root_branch=$ROOT_CFG_BRANCH"
+    docker-compose "${COMPOSE_ARGS[@]}" up --detach
+    configure_root_prj_and_branch
+    docker-compose "${COMPOSE_ARGS[@]}" exec -T --user=admin run_tests \
+        sh -c "/task/test/docker/run_tests/start.sh \
+            --root-config-project $ROOT_CFG_PRJ \
+            --root-config-branch $ROOT_CFG_BRANCH"
+}
+
+configure_root_prj_and_branch() {
+    docker-compose "${COMPOSE_ARGS[@]}" exec -T gerrit-01 \
+        sh -c "git config -f \$GERRIT_SITE/etc/task.config rootConfig.project $ROOT_CFG_PRJ"
+    docker-compose "${COMPOSE_ARGS[@]}" exec -T gerrit-01 \
+        sh -c "git config -f \$GERRIT_SITE/etc/task.config rootConfig.branch $ROOT_CFG_BRANCH"
+    docker-compose "${COMPOSE_ARGS[@]}" exec -T --user=admin run_tests \
+        sh -c "./task/test/docker/run_tests/wait-for-it.sh \$GERRIT_HOST:29418 -t -60" || \
+            die "Failed to start gerrit"
+    docker-compose "${COMPOSE_ARGS[@]}" exec -T --user=admin run_tests \
+        sh -c "cat \$USER_HOME/.ssh/id_rsa.pub | ssh -p 29418 -i /server-ssh-key/ssh_host_rsa_key \
+            \"Gerrit Code Review@\$GERRIT_HOST\" suexec --as admin@example.com -- gerrit set-account \
+            admin --add-ssh-key -" || die "Failed to add ssh key to admin account"
+    docker-compose "${COMPOSE_ARGS[@]}" exec -T --user=admin run_tests \
+        sh -c "ssh -p 29418 \$GERRIT_HOST gerrit plugin reload task" || die "Failed to reload plugin"
 }
 
 retest() {
-    docker-compose "${COMPOSE_ARGS[@]}" exec -T --user=admin \
-        run_tests task/test/docker/run_tests/start.sh retest
+    docker-compose "${COMPOSE_ARGS[@]}" exec -T --user=admin run_tests \
+        sh -c "/task/test/docker/run_tests/start.sh --retest"
     RESULT=$?
     cleanup
 }
@@ -96,13 +131,14 @@
 COMPOSE_ARGS=()
 while (( "$#" )) ; do
     case "$1" in
-        --help|-h)                usage ;;
-        --gerrit-war|-g)          shift ; GERRIT_WAR=$1 ;;
-        --task-plugin-jar|-t)     shift ; TASK_PLUGIN_JAR=$1 ;;
-        --preserve)               PRESERVE="true" ;;
-        --retest)                 RETEST="true" ;;
-        --compose-arg)            shift ; COMPOSE_ARGS+=("$1") ;;
-        *)                        usage "invalid argument $1" ;;
+        --help|-h)                                usage ;;
+        --gerrit-war|-g)                          shift ; GERRIT_WAR=$1 ;;
+        --task-plugin-jar|-t)                     shift ; TASK_PLUGIN_JAR=$1 ;;
+        --names-factory-provider-plugin-jar|-f)   shift ; NAMES_FACTORY_PROVIDER_PLUGIN_JAR=$1 ;;
+        --preserve)                               PRESERVE="true" ;;
+        --retest)                                 RETEST="true" ;;
+        --compose-arg)                            shift ; COMPOSE_ARGS+=("$1") ;;
+        *)                                        usage "invalid argument $1" ;;
     esac
     shift
 done
@@ -114,13 +150,20 @@
 COMPOSE_ARGS=(--project-name "$PROJECT_NAME" -f "$COMPOSE_YAML")
 check_prerequisite
 mkdir -p -- "$ARTIFACTS"
-[ -n "$TASK_PLUGIN_JAR" ] && cp -f "$TASK_PLUGIN_JAR" "$ARTIFACTS/task.jar"
+[ -n "$TASK_PLUGIN_JAR" ] && cp -f -- "$TASK_PLUGIN_JAR" "$ARTIFACTS/task.jar"
 if [ ! -e "$ARTIFACTS/task.jar" ] ; then
     MISSING="Missing $ARTIFACTS/task.jar"
     [ -n "$TASK_PLUGIN_JAR" ] && die "$MISSING, check for copy failure?"
     usage "$MISSING, did you forget --task-plugin-jar?"
 fi
-[ -n "$GERRIT_WAR" ] && cp -f "$GERRIT_WAR" "$ARTIFACTS/gerrit.war"
+[ -n "$NAMES_FACTORY_PROVIDER_PLUGIN_JAR" ] && cp -f -- "$NAMES_FACTORY_PROVIDER_PLUGIN_JAR" \
+    "$ARTIFACTS/names-factory-provider.jar"
+if [ ! -e "$ARTIFACTS/names-factory-provider.jar" ] ; then
+    MISSING="Missing $ARTIFACTS/names-factory-provider.jar"
+    [ -n "$NAMES_FACTORY_PROVIDER_PLUGIN_JAR" ] && die "$MISSING, check for copy failure?"
+    usage "$MISSING, did you forget --names-factory-provider-plugin-jar?"
+fi
+[ -n "$GERRIT_WAR" ] && cp -f -- "$GERRIT_WAR" "$ARTIFACTS/gerrit.war"
 ( trap cleanup EXIT SIGTERM
     progress "Building docker images" build_images
     run_task_plugin_tests
diff --git a/test/docker/run_tests/Dockerfile b/test/docker/run_tests/Dockerfile
index dd5ba8c..c4a68c6 100755
--- a/test/docker/run_tests/Dockerfile
+++ b/test/docker/run_tests/Dockerfile
@@ -1,4 +1,4 @@
-FROM alpine:3.11
+FROM alpine:3.16
 
 ARG UID=1000
 ARG GID=1000
@@ -7,7 +7,7 @@
 ENV RUN_TESTS_DIR task/test/docker/run_tests
 ENV WORKSPACE $USER_HOME/workspace
 
-RUN apk --update add --no-cache openssh bash git python2 shadow util-linux openssl xxd curl jq
+RUN apk --update add --no-cache openssh bash git python3 shadow util-linux openssl xxd curl jq
 RUN echo "StrictHostKeyChecking no" >> /etc/ssh/ssh_config
 
 RUN groupadd -f -g $GID users2
diff --git a/test/docker/run_tests/create-one-time-test-data.sh b/test/docker/run_tests/create-one-time-test-data.sh
index d949bb2..c0d9bd8 100755
--- a/test/docker/run_tests/create-one-time-test-data.sh
+++ b/test/docker/run_tests/create-one-time-test-data.sh
@@ -14,7 +14,7 @@
     gssh create-account "$UNTRUSTED_USER" --full-name "$UNTRUSTED_USER" \
         --email "$UNTRUSTED_USER"@example.com --ssh-key - < ~/.ssh/id_rsa.pub
 
-    gssh create-group "Visible-All-Projects-Config" --member "$NON_SECRET_USER"
+    gssh create-group "Visible-Root-Config-Project" --member "$NON_SECRET_USER"
 
     local secret_user=$USER
     gssh create-group "$NON_SECRET_GROUP_NAME_WITHOUT_SPACE" \
@@ -24,17 +24,40 @@
     gssh create-group "$SECRET_GROUP_NAME" --member "$secret_user"
 }
 
-setup_all_projects_repo() {
-    echo "Updating All-Projects repo ..."
+setup_root_cfg_repo() {
+    echo "Updating root-cfg repo ..."
 
-    local uuid=$(gssh ls-groups -v | awk '-F\t' '$1 == "Visible-All-Projects-Config" {print $2}')
+    local uuid=$(gssh ls-groups -v | awk '-F\t' '$1 == "Visible-Root-Config-Project" {print $2}')
     ( cd "$WORKSPACE"
-      q git clone ssh://"$GERRIT_HOST":"$SSH_PORT"/All-Projects allProjects
-      cd allProjects
+      if ! gssh ls-projects | grep -q "^$ROOT_CONFIG_PRJ$"; then
+          echo "Project $ROOT_CONFIG_PRJ does not exist. Creating ..."
+          gssh create-project "$ROOT_CONFIG_PRJ" --owner "Administrators" \
+              --submit-type "MERGE_IF_NECESSARY" --empty-commit
+      fi
+      if ! gssh ls-projects --show-branch "$ROOT_CONFIG_BRANCH" | grep -q "$ROOT_CONFIG_PRJ$"; then
+          echo "Branch $ROOT_CONFIG_BRANCH in $ROOT_CONFIG_PRJ does not exist. Creating ..."
+          gssh create-branch "$ROOT_CONFIG_PRJ" "$ROOT_CONFIG_BRANCH" master
+      fi
+      q git clone ssh://"$GERRIT_HOST":"$SSH_PORT"/"$ROOT_CONFIG_PRJ" root_cfg_prj
+      cd root_cfg_prj
       q git fetch origin refs/meta/config ; q git checkout FETCH_HEAD
-      echo -e "$uuid\tVisible-All-Projects-Config" >> groups
+
+      echo -e "$uuid\tVisible-Root-Config-Project" >> groups
       git config -f "project.config" \
-          --add access."refs/meta/config".read "group Visible-All-Projects-Config"
+          --add access."$ROOT_CONFIG_BRANCH".read "group Visible-Root-Config-Project"
+      git config -f "project.config" \
+          --add access."$ROOT_CONFIG_BRANCH".exclusiveGroupPermissions "read"
+      q git add . && q git commit -m "project config update"
+      q git push origin HEAD:refs/meta/config
+    )
+}
+
+setup_capabilities() {
+    echo "Updating All-Projects repo with capabilities ..."
+    ( cd "$WORKSPACE"
+      q git clone ssh://"$GERRIT_HOST":"$SSH_PORT"/All-Projects allProjects_capabilities
+      cd allProjects_capabilities
+      q git fetch origin refs/meta/config ; q git checkout FETCH_HEAD
       git config -f "project.config" \
           --add capability.viewTaskPaths "group Administrators"
 #     After migrating to version 3.5, it is no longer feasible to assign read permissions to
@@ -56,6 +79,8 @@
        --non-secret-group-without-space)  shift ; NON_SECRET_GROUP_NAME_WITHOUT_SPACE="$1" ;;
        --non-secret-group-with-space)     shift ; NON_SECRET_GROUP_NAME_WITH_SPACE="$1" ;;
        --secret-group)                    shift ; SECRET_GROUP_NAME="$1" ;;
+       --root-config-project)             shift ; ROOT_CONFIG_PRJ=$1 ;;
+       --root-config-branch)              shift ; ROOT_CONFIG_BRANCH=$1 ;;
        *)                                 die "invalid argument '$1'" ;;
    esac
    shift
@@ -66,8 +91,11 @@
 [ -z "$NON_SECRET_GROUP_NAME_WITHOUT_SPACE" ] && die "non-secret-group-without-space not set"
 [ -z "$NON_SECRET_GROUP_NAME_WITH_SPACE" ] && die "non-secret-group-with-space not set"
 [ -z "$SECRET_GROUP_NAME" ] && die "secret-group not set"
+[ -z "$ROOT_CONFIG_PRJ" ] && ROOT_CONFIG_PRJ=All-Projects
+[ -z "$ROOT_CONFIG_BRANCH" ] && ROOT_CONFIG_BRANCH=refs/meta/config
 
 "$USER_RUN_TESTS_DIR"/create-test-project-and-changes.sh
 "$USER_RUN_TESTS_DIR"/update-all-users-project.sh
 create_test_users_and_group
-setup_all_projects_repo
+setup_root_cfg_repo
+setup_capabilities
diff --git a/test/docker/run_tests/create-test-project-and-changes.sh b/test/docker/run_tests/create-test-project-and-changes.sh
index 6192461..6e5621b 100755
--- a/test/docker/run_tests/create-test-project-and-changes.sh
+++ b/test/docker/run_tests/create-test-project-and-changes.sh
@@ -1,21 +1,28 @@
 #!/usr/bin/env bash
 
-PORT=29418
+SSH_PORT=29418
+HTTP_PORT=8080
 
-gssh() { ssh -x -p "$PORT" "$GERRIT_HOST" gerrit "$@" ; } # cmd [args]...
+gssh() { ssh -x -p "$SSH_PORT" "$GERRIT_HOST" gerrit "$@" ; } # cmd [args]...
 
 create_project() { # project
     echo "Creating a test project ..."
     gssh create-project "$1" --owner "Administrators" --submit-type "MERGE_IF_NECESSARY"
-    cd "$WORKSPACE" && git clone ssh://"$GERRIT_HOST":"$PORT"/"$1" "$1" && cd "$1"
-    gitdir=$(git rev-parse --git-dir)
-    scp -p -P "$PORT" "$USER"@"$GERRIT_HOST":hooks/commit-msg "$gitdir"/hooks/
+    cd "$WORKSPACE" && git clone ssh://"$GERRIT_HOST":"$SSH_PORT"/"$1" "$1" && cd "$1"
+    install_changeid_hook
+}
+
+install_changeid_hook() {
+    local hook=$(git rev-parse --git-dir)/hooks/commit-msg
+    mkdir -p "$(dirname -- "$hook")"
+    curl -Lo "$hook" "http://$GERRIT_HOST:$HTTP_PORT/tools/hooks/commit-msg"
+    chmod +x "$hook"
 }
 
 create_change() { # subject project
     touch readme.txt && echo "$(date)" >> readme.txt
     git add . && git commit -m "$1"
-    git push ssh://"$GERRIT_HOST":"$PORT"/"$2" HEAD:refs/for/master
+    git push ssh://"$GERRIT_HOST":"$SSH_PORT"/"$2" HEAD:refs/for/master
 }
 
 create_project 'test'
diff --git a/test/docker/run_tests/start.sh b/test/docker/run_tests/start.sh
index 4a73f24..1abbd7e 100755
--- a/test/docker/run_tests/start.sh
+++ b/test/docker/run_tests/start.sh
@@ -9,8 +9,23 @@
 USER_RUN_TESTS_DIR="$USER_HOME"/"$RUN_TESTS_DIR"
 mkdir "$USER_HOME"/task && cp -r /task/{src,test} "$USER_HOME"/task
 
-if [ "$1" = "retest" ] ; then
-    cd "$USER_RUN_TESTS_DIR"/../../ && ./check_task_statuses.sh "$GERRIT_HOST"
+RETEST=false
+while (( "$#" )) ; do
+   case "$1" in
+       --retest)                          RETEST="true" ;;
+       --root-config-project)             shift ; ROOT_CONFIG_PRJ=$1 ;;
+       --root-config-branch)              shift ; ROOT_CONFIG_BRANCH=$1 ;;
+       *)                                 die "invalid argument '$1'" ;;
+   esac
+   shift
+done
+[ -z "$ROOT_CONFIG_PRJ" ] && ROOT_CONFIG_PRJ=All-Projects
+[ -z "$ROOT_CONFIG_BRANCH" ] && ROOT_CONFIG_BRANCH=refs/meta/config
+
+if [ "$RETEST" = "true" ] ; then
+    cd "$USER_RUN_TESTS_DIR"/../../ && \
+        ./check_task_statuses.sh "$GERRIT_HOST" \
+            --root-config-project "$ROOT_CONFIG_PRJ" --root-config-branch "$ROOT_CONFIG_BRANCH"
     exit $?
 fi
 
@@ -29,6 +44,7 @@
 ssh -p 29418 "$GERRIT_HOST" gerrit set-account --http-password "$PASSWORD" "$USER"
 
 is_plugin_loaded "task" || die "Task plugin is not installed"
+is_plugin_loaded "names-factory-provider" || die "names-factory-provider plugin is not installed"
 
 NON_SECRET_USER="non_secret_user"
 UNTRUSTED_USER="untrusted_user"
@@ -36,6 +52,7 @@
 GROUP_NAME_WITH_SPACE="test group"
 SECRET_GROUP="private_group"
 "$USER_RUN_TESTS_DIR"/create-one-time-test-data.sh --non-secret-user "$NON_SECRET_USER" \
+    --root-config-project "$ROOT_CONFIG_PRJ" --root-config-branch "$ROOT_CONFIG_BRANCH" \
     --untrusted-user "$UNTRUSTED_USER" --non-secret-group-without-space "$GROUP_NAME_WITHOUT_SPACE" \
     --non-secret-group-with-space "$GROUP_NAME_WITH_SPACE" --secret-group "$SECRET_GROUP"
 
@@ -45,10 +62,12 @@
 
 "$USER_RUN_TESTS_DIR"/../../check_task_statuses.sh \
     --server "$GERRIT_HOST" --non-secret-user "$NON_SECRET_USER" \
+    --root-config-project "$ROOT_CONFIG_PRJ" --root-config-branch "$ROOT_CONFIG_BRANCH" \
     --untrusted-user "$UNTRUSTED_USER" --non-secret-group-without-space "$GROUP_NAME_WITHOUT_SPACE" \
     --non-secret-group-with-space "$GROUP_NAME_WITH_SPACE" || RESULT=1
 
 "$USER_RUN_TESTS_DIR"/../../check_task_visibility.sh --server "$GERRIT_HOST" \
+    --root-config-project "$ROOT_CONFIG_PRJ" --root-config-branch "$ROOT_CONFIG_BRANCH" \
     --non-secret-user "$NON_SECRET_USER" --non-secret-group "$GROUP_NAME_WITHOUT_SPACE" \
     --secret-group "$SECRET_GROUP" || RESULT=1
 
diff --git a/test/lib/lib_helper.sh b/test/lib/lib_helper.sh
index f4006db..249834c 100644
--- a/test/lib/lib_helper.sh
+++ b/test/lib/lib_helper.sh
@@ -31,12 +31,13 @@
     local name=$1 expected=$2 actual=$3
 
     [ "$expected" = "$actual" ]
-    result "$name" "$(diff <(echo "$expected") <(echo "$actual"))"
+    result "$name" "$(diff -- <(echo "$expected") <(echo "$actual"))"
 }
 
 result_root() { # group root
-    local name="$1 - $(echo "$2" | sed -es'/Root //')"
-    result_out "$name" "${EXPECTED_ROOTS[$2]}" "${OUTPUT_ROOTS[$2]}"
+    local root=$(echo "$2" | sed -es'/Root //')
+    local name="$1 - $root"
+    result_out "$name" "${EXPECTED_ROOTS[$root]}" "${OUTPUT_ROOTS[$root]}"
 }
 
 # -------- Git Config
@@ -112,7 +113,8 @@
 #
 testdoc_2_cfg() { awk '/^\{/,/^$/ { next } ; 1' ; } # testdoc_format > task_config
 
-# Strip the git config from Test Doc formatted text. For the sample above, the output would be:
+# Strip the git config from Test Doc formatted text and wrap in a Gerrit-like
+# 'plugins' JSON section. For the sample above, the output would be:
 #
 # { "plugins" : [
 #     { "name" : "task",
@@ -140,16 +142,12 @@
 # ---- JSON PARSING ----
 
 json_pp() { # < json > json
-    python -c "import sys, json; \
-            print json.dumps(json.loads(sys.stdin.read()), indent=3, \
-            separators=(',', ' : '), sort_keys=True)"
+    jq --indent 3 --sort-keys
 }
 
-json_val_by() { # json index|'key' > value
-    echo "$1" | python -c "import json,sys;print json.load(sys.stdin)[$2]"
+json_val_by_key() {  # json key > value
+    echo "$1" | jq -r --arg key "$2" '.[$key] // empty'
 }
-json_val_by_key() { json_val_by "$1" "'$2'" ; }  # json key > value
-
 # --------
 
 gssh() {  # [-l user] cmd [args]...
@@ -223,6 +221,11 @@
     sed -e"s/testuser/$USER/"
 }
 
+replace_root_configs() { # < text_with_root_tokens > text_with_root_tokens_replaced
+ sed -e "s,{root-cfg-prj},$ROOT_CONFIG_PRJ," \
+     -e "s,{root-cfg-branch},$ROOT_CONFIG_BRANCH,"
+}
+
 get_user_ref() { # username > refs/users/<accountidshard>/<accountid>
     local user_account_id="$(curl --netrc --silent "http://$SERVER:$HTTP_PORT/a/accounts/$1" | \
     sed -e '1!b' -e "/^)]}'$/d" | jq ._account_id)"
@@ -238,7 +241,8 @@
 }
 
 replace_tokens() { # < text > text with replacing all tokens(changes, user)
-    replace_default_changes | replace_user_refs | replace_user | replace_groups
+    replace_default_changes | replace_user_refs | replace_user | replace_groups | \
+        replace_root_configs
 }
 
 strip_non_applicable() { ensure "$MYDIR"/strip_non_applicable.py ; } # < json > json
@@ -254,23 +258,15 @@
             jsonByRoot[$root]=$record
             root=''
         fi
-    done < <(python -c "if True: # NOP to start indent
-        import sys, json
-
-        roots=json.loads(sys.stdin.read())['plugins'][0]['roots']
-        for root in roots:
-            root_json = json.dumps(root, indent=3, separators=(',', ' : '), sort_keys=True)
-            sys.stdout.write(root['name'] + '\x00' + root_json + '\x00')"
-    )
+    done < <(jq -r --indent 3 --sort-keys \
+        '.plugins[0].roots[] | "\(.name)\u0000\(.)\u0000"')
 
     local def=$(declare -p jsonByRoot)
     echo "${def#*=}" # declare -A jsonByRoot='(...)' > '(...)'
 }
 
 get_plugins() { # < change_json > plugins_json
-    python -c "import sys, json; \
-        plugins={}; plugins['plugins']=json.loads(sys.stdin.read())['plugins']; \
-        print json.dumps(plugins, indent=3, separators=(',', ' : '), sort_keys=True)"
+    jq --indent 3 --sort-keys '{plugins: .plugins}'
 }
 
 example() { # doc example_num > text_for_example_num
@@ -282,18 +278,19 @@
     echo "${url##*\/}" | tr -d -c '[:digit:]'
 }
 
-install_changeid_hook() { # repo
+install_changeid_hook() {
     local hook=$(git rev-parse --git-dir)/hooks/commit-msg
-    scp -p -P "$PORT" "$SERVER":hooks/commit-msg "$hook"
+    mkdir -p -- "$(dirname -- "$hook")"
+    curl -Lo "$hook" "http://$SERVER:$HTTP_PORT/tools/hooks/commit-msg"
     chmod +x "$hook"
 }
 
 setup_repo() { # repo remote ref [--initial-commit]
     local repo=$1 remote=$2 ref=$3 init=$4
-    git init "$repo"
+    git init -- "$repo"
     (
-        cd "$repo"
-        install_changeid_hook "$repo"
+        cd -- "$repo"
+        install_changeid_hook
         git fetch "$remote" "$ref"
         if ! git checkout FETCH_HEAD ; then
             if [ "$init" = "--initial-commit" ] ; then
@@ -306,7 +303,7 @@
 update_repo() { # repo remote ref
     local repo=$1 remote=$2 ref=$3
     (
-        cd "$repo"
+        cd -- "$repo"
         git add .
         git commit -m 'Testing task plugin'
         git push "$remote" HEAD:"$ref"
@@ -316,7 +313,7 @@
 create_repo_change() { # repo remote ref [change_id] > change_num
     local repo=$1 remote=$2 ref=$3 change_id=$4 msg="Test change"
     (
-        q cd "$repo"
+        q cd -- "$repo"
         uuidgen > file
         q git add .
         [ -n "$change_id" ] && msg=$(commit_message "$msg" "$change_id")
@@ -335,16 +332,16 @@
 change_plugins() { awk "NR==$1" | get_plugins | json_pp ; }
 
 results_suite() { # name expected_file plugins_json
-    local name=$1 expected=$2 actual=$3
+    local name=$1 expected_file=$2 actual=$3
 
-    local -A EXPECTED_ROOTS=$(define_jsonByRoot < "$expected")
+    local -A EXPECTED_ROOTS=$(define_jsonByRoot < "$expected_file")
     local -A OUTPUT_ROOTS=$(echo "$actual" | define_jsonByRoot)
 
     local out root
     echo "$ROOTS" | while read root ; do
         result_root "$name" "$root"
     done
-    out=$(diff "$expected" <(echo "$actual") | head -15)
+    out=$(diff -- "$expected_file" <(echo "$actual") | head -15)
     [ -z "$out" ]
     result "$name - Full Test Suite" "$out"
 }
diff --git a/test/strip_non_applicable.py b/test/strip_non_applicable.py
index 41c21fa..a0ce4fa 100755
--- a/test/strip_non_applicable.py
+++ b/test/strip_non_applicable.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 #
 # Copyright (C) 2021 The Android Open Source Project
 #
@@ -28,20 +28,20 @@
         nexti = i + 1
 
         task=tasks[i]
-        if APPLICABLE in task.keys() and task[APPLICABLE] == False:
+        if APPLICABLE in list(task.keys()) and task[APPLICABLE] == False:
             del tasks[i]
             nexti = i
         else:
             subtasks=[]
-            if SUBTASKS in task.keys():
+            if SUBTASKS in list(task.keys()):
                 subtasks=task[SUBTASKS]
                 del_non_applicable(subtasks)
-            if SUBTASKS in task.keys() and len(subtasks) == 0:
+            if SUBTASKS in list(task.keys()) and len(subtasks) == 0:
                 del task[SUBTASKS]
-            if not SUBTASKS in task.keys():
-                if HASPASS in task.keys() and task[HASPASS] == False:
+            if not SUBTASKS in list(task.keys()):
+                if HASPASS in list(task.keys()) and task[HASPASS] == False:
                     status=''
-                    if STATUS in task.keys():
+                    if STATUS in list(task.keys()):
                         status = task[STATUS]
                     if status != 'INVALID' and status != 'DUPLICATE':
                         del tasks[i]
@@ -52,4 +52,4 @@
 plugins=json.loads(sys.stdin.read())
 roots=plugins['plugins'][0]['roots']
 del_non_applicable(roots)
-print json.dumps(plugins, indent=3, separators=(',', ' : '), sort_keys=True)
+print(json.dumps(plugins, indent=3, separators=(',', ' : '), sort_keys=True))
diff --git a/test/strip_non_invalid.py b/test/strip_non_invalid.py
index dafae15..0227c79 100755
--- a/test/strip_non_invalid.py
+++ b/test/strip_non_invalid.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 #
 # Copyright (C) 2021 The Android Open Source Project
 #
@@ -26,16 +26,16 @@
         nexti = i + 1
 
         task=tasks[i]
-        if SUBTASKS in task.keys():
+        if SUBTASKS in list(task.keys()):
             subtasks=task[SUBTASKS]
             keep_invalid(subtasks)
             if len(subtasks) == 0:
                 del task[SUBTASKS]
 
         status=''
-        if STATUS in task.keys():
+        if STATUS in list(task.keys()):
             status = task[STATUS]
-        if status != 'INVALID' and not SUBTASKS in task.keys():
+        if status != 'INVALID' and not SUBTASKS in list(task.keys()):
             del tasks[i]
             nexti = i
 
@@ -44,4 +44,4 @@
 plugins=json.loads(sys.stdin.read())
 roots=plugins['plugins'][0]['roots']
 keep_invalid(roots)
-print json.dumps(plugins, indent=3, separators=(',', ' : '), sort_keys=True)
+print(json.dumps(plugins, indent=3, separators=(',', ' : '), sort_keys=True))
diff --git a/tools/workspace_status.py b/tools/workspace_status.py
index fb5ec6d..4d0c6c3 100644
--- a/tools/workspace_status.py
+++ b/tools/workspace_status.py
@@ -29,3 +29,4 @@
 
 
 print("STABLE_BUILD_TASK_LABEL %s" % revision())
+print("STABLE_BUILD_NAMES-FACTORY-PROVIDER_LABEL %s" % revision())