Merge branch 'master' into stable-3.5

* master:
  TaskTree: remove 'ERROR:' prefix from log statement
  TaskTree: fix errorprone warning about Flogger

Most development has happened in the stable-3.5 branch, but master has a
couple commits missing from there. Merge them in before creating new
stable branches based on stable-3.5.

Change-Id: I0698db5f5cdadb5b208fdac09e0a4ffece20b2d0
diff --git a/.bazelignore b/.bazelignore
new file mode 100644
index 0000000..3c3629e
--- /dev/null
+++ b/.bazelignore
@@ -0,0 +1 @@
+node_modules
diff --git a/.bazelrc b/.bazelrc
index 3ae03ff..fb8e4d1 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -1,2 +1,18 @@
-build --workspace_status_command="python ./tools/workspace_status.py"
+build --java_language_version=11
+build --java_runtime_version=remotejdk_11
+build --tool_java_language_version=11
+build --tool_java_runtime_version=remotejdk_11
+
+build --workspace_status_command="python3 ./tools/workspace_status.py"
+build --repository_cache=~/.gerritcodereview/bazel-cache/repository
+build --action_env=PATH
+build --disk_cache=~/.gerritcodereview/bazel-cache/cas
+
+# Enable strict_action_env flag to. For more information on this feature see
+# https://groups.google.com/forum/#!topic/bazel-discuss/_VmRfMyyHBk.
+# This will be the new default behavior at some point (and the flag was flipped
+# shortly in 0.21.0 - https://github.com/bazelbuild/bazel/issues/7026). Remove
+# this flag here once flipped in Bazel again.
+build --incompatible_strict_action_env
+
 test --build_tests_only
diff --git a/.bazelversion b/.bazelversion
index fcdb2e1..c7cb131 100644
--- a/.bazelversion
+++ b/.bazelversion
@@ -1 +1 @@
-4.0.0
+5.3.1
diff --git a/.eslintrc.json b/.eslintrc.json
index 0c290fa..358c95d 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -4,7 +4,7 @@
       "google"
   ],
   "parserOptions": {
-      "ecmaVersion": 8,
+      "ecmaVersion": 2020,
       "sourceType": "module"
   },
   "env": {
diff --git a/.zuul.yaml b/.zuul.yaml
index 27c8491..6e83fbf 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -2,7 +2,9 @@
     name: plugins-task-build
     parent: gerrit-plugin-build
     pre-run:
-        tools/playbooks/install_docker.yaml
+        - tools/playbooks/install_maven.yaml
+        - tools/playbooks/install_docker.yaml
+        - tools/playbooks/install_python3-distutils.yaml
     vars:
         bazelisk_test_targets: "plugins/task/lint_test plugins/task/..."
 
diff --git a/BUILD b/BUILD
index 0b04753..537b627 100644
--- a/BUILD
+++ b/BUILD
@@ -1,8 +1,48 @@
-load("//tools/bzl:plugin.bzl", "gerrit_plugin")
+load(
+    "//tools/bzl:plugin.bzl",
+    "PLUGIN_DEPS",
+    "PLUGIN_TEST_DEPS",
+    "gerrit_plugin",
+)
 load("//tools/bzl:js.bzl", "gerrit_js_bundle")
 load("//tools/js:eslint.bzl", "eslint")
+load("//tools/bzl:junit.bzl", "junit_tests")
+load("@rules_java//java:defs.bzl", "java_library", "java_plugin")
+load("@rules_antlr//antlr:antlr4.bzl", "antlr")
+
 plugin_name = "task"
 
+java_plugin(
+    name = "auto-value-plugin",
+    processor_class = "com.google.auto.value.processor.AutoValueProcessor",
+    deps = [
+        "@auto-value-annotations//jar",
+        "@auto-value//jar",
+    ],
+)
+
+java_library(
+    name = "auto-value",
+    exported_plugins = [
+        ":auto-value-plugin",
+    ],
+    visibility = ["//visibility:public"],
+    exports = ["@auto-value//jar"],
+)
+
+antlr(
+    name = "task_reference",
+    srcs = ["src/main/antlr4/com/googlesource/gerrit/plugins/task/TaskReference.g4"],
+    package = "com.googlesource.gerrit.plugins.task",
+    visibility = ["//visibility:public"],
+)
+
+java_library(
+    name = "task_reference_parser",
+    srcs = [":task_reference"],
+    deps = ["@antlr4_runtime//jar"],
+)
+
 gerrit_plugin(
     name = plugin_name,
     srcs = glob(["src/main/java/**/*.java"]),
@@ -14,6 +54,11 @@
     ],
     resource_jars = [":gr-task-plugin"],
     resources = glob(["src/main/resources/**/*"]),
+    deps = [
+        ":auto-value",
+        ":task_reference_parser",
+        "@antlr4_runtime//jar",
+    ],
     javacopts = [ "-Werror", "-Xlint:all", "-Xlint:-classfile", "-Xlint:-processing"],
 )
 
@@ -23,6 +68,13 @@
     entry_point = "gr-task-plugin/plugin.js",
 )
 
+junit_tests(
+    name = "junit-tests",
+    size = "small",
+    srcs = glob(["src/test/java/**/*Test.java"]),
+    deps = PLUGIN_TEST_DEPS + PLUGIN_DEPS + [plugin_name],
+)
+
 sh_test(
     name = "docker-tests",
     size = "medium",
@@ -30,6 +82,7 @@
     args = ["--task-plugin-jar", "$(location :task)"],
     data = [plugin_name] + glob(["test/**"]) + glob(["src/main/resources/Documentation/*"]),
     local = True,
+    tags = ["docker"],
 )
 
 eslint(
@@ -43,10 +96,5 @@
         ".js",
     ],
     ignore = ".eslintignore",
-    plugins = [
-        "@npm//eslint-config-google",
-        "@npm//eslint-plugin-html",
-        "@npm//eslint-plugin-import",
-        "@npm//eslint-plugin-jsdoc",
-    ],
+    plugins = [],
 )
diff --git a/WORKSPACE b/WORKSPACE
index c217ecf..0cab5f6 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -1,14 +1,12 @@
 workspace(
     name = "task",
-    managed_directories = {
-        "@npm": ["node_modules"],
-    },
 )
 
 load("//:bazlets.bzl", "load_bazlets")
 
 load_bazlets(
-    commit = "6ebb3cfa1332a0dc0d2b7ea904a4703656f2ba54",
+    commit = "b6120a9fa50945d38f0a4d55d5879e3ec465c5e5",
+    shallow_since = "1701477032 -0700",
     #local_path = "/home/<user>/projects/bazlets",
 )
 
@@ -20,12 +18,23 @@
 
 gerrit_polymer()
 
-load("@build_bazel_rules_nodejs//:index.bzl", "yarn_install")
+load("@build_bazel_rules_nodejs//:repositories.bzl", "build_bazel_rules_nodejs_dependencies")
+
+build_bazel_rules_nodejs_dependencies()
+
+load("@build_bazel_rules_nodejs//:index.bzl", "node_repositories", "yarn_install")
+
+node_repositories(
+    node_version = "16.13.2",
+    yarn_version = "1.22.17",
+)
 
 yarn_install(
     name = "npm",
+    exports_directories_only = False,
     frozen_lockfile = False,
     package_json = "//:package.json",
+    symlink_node_modules = True,
     yarn_lock = "//:yarn.lock",
 )
 
@@ -35,4 +44,9 @@
     "gerrit_api",
 )
 
+# Release Plugin API
 gerrit_api()
+
+load("//:external_plugin_deps.bzl", "external_plugin_deps")
+
+external_plugin_deps()
diff --git a/bazlets.bzl b/bazlets.bzl
index f089af4..457bfec 100644
--- a/bazlets.bzl
+++ b/bazlets.bzl
@@ -4,12 +4,14 @@
 
 def load_bazlets(
         commit,
+        shallow_since = None,
         local_path = None):
     if not local_path:
         git_repository(
             name = NAME,
             remote = "https://gerrit.googlesource.com/bazlets",
             commit = commit,
+            shallow_since = shallow_since,
         )
     else:
         native.local_repository(
diff --git a/external_plugin_deps.bzl b/external_plugin_deps.bzl
new file mode 100644
index 0000000..901cd0f
--- /dev/null
+++ b/external_plugin_deps.bzl
@@ -0,0 +1,56 @@
+load("@bazel_tools//tools/build_defs/repo:maven_rules.bzl", "maven_jar")
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+
+def external_plugin_deps():
+    AUTO_VALUE_VERSION = "1.7.4"
+
+    maven_jar(
+        name = "auto-value",
+        artifact = "com.google.auto.value:auto-value:" + AUTO_VALUE_VERSION,
+        sha1 = "6b126cb218af768339e4d6e95a9b0ae41f74e73d",
+    )
+
+    maven_jar(
+        name = "auto-value-annotations",
+        artifact = "com.google.auto.value:auto-value-annotations:" + AUTO_VALUE_VERSION,
+        sha1 = "eff48ed53995db2dadf0456426cc1f8700136f86",
+    )
+
+    http_archive(
+        name = "rules_antlr",
+        sha256 = "26e6a83c665cf6c1093b628b3a749071322f0f70305d12ede30909695ed85591",
+        strip_prefix = "rules_antlr-0.5.0",
+        urls = ["https://github.com/marcohu/rules_antlr/archive/0.5.0.tar.gz"],
+    )
+
+    maven_jar(
+        name = "antlr3_runtime",
+        artifact = "org.antlr:antlr-runtime:3.5.2",
+        sha1 = "cd9cd41361c155f3af0f653009dcecb08d8b4afd",
+    )
+
+    ANTLR_VERSION = "4.9.3"
+
+    maven_jar(
+        name = "antlr4_runtime",
+        artifact = "org.antlr:antlr4-runtime:" + ANTLR_VERSION,
+        sha1 = "81befc16ebedb8b8aea3e4c0835dd5ca7e8523a8",
+    )
+
+    maven_jar(
+        name = "antlr4_tool",
+        artifact = "org.antlr:antlr4:" + ANTLR_VERSION,
+        sha1 = "9d47afaa75d70903b5b77413b034d6b201d7d5d6",
+    )
+
+    maven_jar(
+        name = "stringtemplate4",
+        artifact = "org.antlr:ST4:4.3.1",
+        sha1 = "9c61ac6d17b7f450b4048742c2cc73787972518e",
+    )
+
+    maven_jar(
+        name = "javax_json",
+        artifact = "org.glassfish:javax.json:1.0.4",
+        sha1 = "3178f73569fd7a1e5ffc464e680f7a8cc784b85a",
+    )
diff --git a/gr-task-plugin/gr-task-chip.js b/gr-task-plugin/gr-task-chip.js
new file mode 100644
index 0000000..8e46bd0
--- /dev/null
+++ b/gr-task-plugin/gr-task-chip.js
@@ -0,0 +1,66 @@
+/**
+ * @license
+ * Copyright (C) 2023 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 './gr-task-plugin.js';
+import {htmlTemplate} from './gr-task-chip_html.js';
+
+class GrTaskChip extends Polymer.Element {
+  static get is() {
+    return 'gr-task-chip';
+  }
+
+  static get template() {
+    return htmlTemplate;
+  }
+
+  static get properties() {
+    return {
+      chip_style: {
+        type: String,
+        notify: true,
+        value: 'ready',
+      },
+    };
+  }
+
+  _setTasksTabActive() {
+    // TODO: Identify a better way as current implementation is fragile
+    const endPointDecorators = 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;
+        }
+      }
+    }
+  }
+
+  _onChipClick() {
+    this._setTasksTabActive();
+  }
+}
+
+customElements.define(GrTaskChip.is, GrTaskChip);
\ No newline at end of file
diff --git a/gr-task-plugin/gr-task-chip_html.js b/gr-task-plugin/gr-task-chip_html.js
new file mode 100644
index 0000000..27cee0f
--- /dev/null
+++ b/gr-task-plugin/gr-task-chip_html.js
@@ -0,0 +1,129 @@
+/**
+ * @license
+ * Copyright (C) 2023 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`
+  <style include="gr-a11y-styles">
+    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+  </style>
+  <style include="shared-styles">
+    .taskSummaryChip {
+      color: var(--chip-color);
+      cursor: pointer;
+      display: inline-block;
+      padding: var(--spacing-xxs) var(--spacing-m) var(--spacing-xxs)
+        var(--spacing-s);
+      margin-right: var(--spacing-s);
+      border-radius: 12px;
+      border: 1px solid gray;
+      vertical-align: top;
+      /* centered position of 20px chips in 24px line-height inline flow */
+      vertical-align: top;
+      position: relative;
+      top: 2px;
+    }
+    .taskSummaryChip.loading {
+      border-color: var(--gray-foreground);
+      background: var(--gray-background);
+    }
+    .taskSummaryChip.loading:hover {
+      background: var(--gray-background-hover);
+      box-shadow: var(--elevation-level-1);
+    }
+    .taskSummaryChip.loading:focus-within {
+      background: var(--gray-background-focus);
+    }
+    .taskSummaryChip.success {
+      border-color: var(--success-foreground);
+      background: var(--success-background);
+    }
+    .taskSummaryChip.success:hover {
+      background: var(--success-background-hover);
+      box-shadow: var(--elevation-level-1);
+    }
+    .taskSummaryChip.success:focus-within {
+      background: var(--success-background-focus);
+    }
+    .taskSummaryChip.waiting {
+      border-color: var(--warning-foreground);
+      background: var(--warning-background);
+    }
+    .taskSummaryChip.waiting:hover {
+      background: var(--warning-background-hover);
+      box-shadow: var(--elevation-level-1);
+    }
+    .taskSummaryChip.waiting:focus-within {
+      background: var(--warning-background-focus);
+    }
+    .taskSummaryChip.ready {
+      border-color: var(--success-foreground);
+      background: var(--success-background);
+    }
+    .taskSummaryChip.ready:hover {
+      background: var(--success-background-hover);
+      box-shadow: var(--elevation-level-1);
+    }
+    .taskSummaryChip.ready:focus-within {
+      background: var(--success-background-focus);
+    }
+    .taskSummaryChip.invalid {
+      color: var(--error-foreground);
+      border-color: var(--error-foreground);
+      background: var(--error-background);
+    }
+    .taskSummaryChip.invalid:hover {
+      background: var(--error-background-hover);
+      box-shadow: var(--elevation-level-1);
+    }
+    .taskSummaryChip.invalid:focus-within {
+      background: var(--error-background-focus);
+    }
+    .taskSummaryChip.duplicate {
+      color: var(--success-foreground);
+      border-color: var(--success-foreground);
+      background: var(--success-background);
+    }
+    .taskSummaryChip.duplicate:hover {
+      background: var(--success-background-hover);
+      box-shadow: var(--elevation-level-1);
+    }
+    .taskSummaryChip.duplicate:focus-within {
+      background: var(--success-background-focus);
+    }
+    .taskSummaryChip.fail {
+      color: var(--error-foreground);
+      border-color: var(--error-foreground);
+      background: var(--error-background);
+    }
+    .taskSummaryChip.fail:hover {
+      background: var(--error-background-hover);
+      box-shadow: var(--elevation-level-1);
+    }
+    .taskSummaryChip.fail:focus-within {
+      background: var(--error-background-focus);
+    }
+    .font-small {
+      font-size: var(--font-size-small);
+      font-weight: var(--font-weight-normal);
+      line-height: var(--line-height-small);
+    }
+  </style>
+  <button
+    class$="taskSummaryChip font-small [[chip_style]]"
+    on-click="_onChipClick">
+    <slot></slot>
+  </button>
+`;
diff --git a/gr-task-plugin/gr-task-plugin-tasks.js b/gr-task-plugin/gr-task-plugin-tasks.js
index 54585d1..7653ed4 100644
--- a/gr-task-plugin/gr-task-plugin-tasks.js
+++ b/gr-task-plugin/gr-task-plugin-tasks.js
@@ -38,12 +38,21 @@
         type: String,
         notify: true,
       },
+
+      config: {
+        type: Object,
+        value() { return {}; },
+      },
     };
   }
 
   _can_show(show, task) {
     return show === 'true' || task.showOnFilter;
   }
+
+  _getChangeUrl(change) {
+    return '/c/' + change.toString();
+  }
 }
 
 customElements.define(GrTaskPluginTasks.is, GrTaskPluginTasks);
diff --git a/gr-task-plugin/gr-task-plugin-tasks_html.js b/gr-task-plugin/gr-task-plugin-tasks_html.js
index df1644f..60fbd32 100644
--- a/gr-task-plugin/gr-task-plugin-tasks_html.js
+++ b/gr-task-plugin/gr-task-plugin-tasks_html.js
@@ -20,12 +20,20 @@
   <template is="dom-if" if="[[_can_show(show_all, task)]]">
     <li>
       <style>
-        /* Matching colors with core code. */
+        /* Matching colors with core theme. */
         .green {
-          color: #9fcc6b;
+          color: var(--success-foreground);
         }
         .red {
-          color: #FFA62F;
+          color: var(--error-foreground);
+        }
+        .orange {
+          color: var(--warning-foreground);
+        }
+        .links {
+          color: var(--link-color);
+          cursor: pointer;
+          text-decoration: underline;
         }
       </style>
       <template is="dom-if" if="[[task.icon.id]]">
@@ -47,7 +55,21 @@
             </iron-icon>
         </gr-tooltip-content>
       </template>
-      [[task.message]]
+      <template is="dom-if" if="[[task.change]]">
+        <a class="links" href$="[[_getChangeUrl(task.change)]]">[[task.change]]</a>
+      </template>
+      <template is="dom-if" if="[[!task.change]]">
+        <template is="dom-if" if="[[!task.hint]]">
+          [[task.name]]
+        </template>
+      </template>
+      <template is="dom-if" if="[[task.hint]]">
+        <gr-linked-text style="display: -webkit-inline-box;"
+          pre=""
+          content="[[task.hint]]"
+          config="[[config]]">
+        </gr-linked-text>
+      </template>
     </li>
   </template>
   <ul style="list-style-type:none; margin: 0 0 0 0; padding: 0 0 0 2em;">
diff --git a/gr-task-plugin/gr-task-plugin.js b/gr-task-plugin/gr-task-plugin.js
index c665d61..2ac355d 100644
--- a/gr-task-plugin/gr-task-plugin.js
+++ b/gr-task-plugin/gr-task-plugin.js
@@ -22,10 +22,10 @@
 const Defs = {};
 /**
  * @typedef {{
- *  message: string,
  *  sub_tasks: Array<Defs.Task>,
  *  hint: ?string,
  *  name: string,
+ *  change: ?number,
  *  status: string
  * }} Defs.Task
  */
@@ -82,6 +82,30 @@
         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,
+      },
     };
   }
 
@@ -95,15 +119,21 @@
     this._getTasks();
   }
 
+  _is_hidden(_isPending, _tasks) {
+    return (!_isPending && !_tasks.length);
+  }
+
   _getTasks() {
     if (!this.change) {
       return;
     }
 
+    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) {
@@ -114,7 +144,20 @@
             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;
     });
   }
 
@@ -127,7 +170,7 @@
         icon.tooltip = 'Failed';
         break;
       case 'READY':
-        icon.id = 'gr-icons:rebase';
+        icon.id = 'gr-icons:playArrow';
         icon.color = 'green';
         icon.tooltip = 'Ready';
         break;
@@ -137,13 +180,18 @@
         icon.tooltip = 'Invalid';
         break;
       case 'WAITING':
-        icon.id = 'gr-icons:side-by-side';
-        icon.color = 'red';
+        icon.id = 'gr-icons:pause';
+        icon.color = 'orange';
         icon.tooltip = 'Waiting';
         break;
-      case 'PASS':
+      case 'DUPLICATE':
         icon.id = 'gr-icons:check';
         icon.color = 'green';
+        icon.tooltip = 'Duplicate';
+        break;
+      case 'PASS':
+        icon.id = 'gr-icons:check-circle';
+        icon.color = 'green';
         icon.tooltip = 'Passed';
         break;
     }
@@ -160,10 +208,10 @@
     return false;
   }
 
-  _computeShowOnNeedsAndBlockedFilter(task) {
+  _computeShowOnNeededAndBlockedFilter(task) {
     return this._isFailOrReadyOrInvalid(task) ||
       (task.sub_tasks && task.sub_tasks.some(t =>
-        this._computeShowOnNeedsAndBlockedFilter(t)));
+        this._computeShowOnNeededAndBlockedFilter(t)));
   }
 
   _compute_counts(task) {
@@ -175,15 +223,26 @@
       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;
     }
   }
 
   _addTasks(tasks) { // rename to process, remove DOM bits
     if (!tasks) return [];
     tasks.forEach(task => {
-      task.message = task.hint || task.name;
       task.icon = this._computeIcon(task);
-      task.showOnFilter = this._computeShowOnNeedsAndBlockedFilter(task);
+      task.showOnFilter = this._computeShowOnNeededAndBlockedFilter(task);
       this._compute_counts(task);
       this._addTasks(task.sub_tasks);
     });
@@ -195,7 +254,7 @@
     this._expand_all = 'true';
   }
 
-  _needs_and_blocked_tap() {
+  _needed_and_blocked_tap() {
     this._show_all = 'false';
     this._expand_all = 'true';
   }
@@ -203,6 +262,14 @@
   _switch_expand() {
     this._expand_all = !this._expand_all;
   }
+
+  _computeShowAllLabelText(showAllSections) {
+    if (showAllSections) {
+      return 'Hide all';
+    } else {
+      return 'Show all';
+    }
+  }
 }
 
 customElements.define(GrTaskPlugin.is, GrTaskPlugin);
diff --git a/gr-task-plugin/gr-task-plugin_html.js b/gr-task-plugin/gr-task-plugin_html.js
index 71c995c..6ab5fb9 100644
--- a/gr-task-plugin/gr-task-plugin_html.js
+++ b/gr-task-plugin/gr-task-plugin_html.js
@@ -16,65 +16,112 @@
  */
 
 export const htmlTemplate = Polymer.html`
-<style>
-  ul {
-    padding-left: 0.5em;
-    margin-top: 0;
-  }
-  h3 {
-    padding-left: 0.1em;
-    margin: 0 0 0 0;
-  }
-  .cursor { cursor: pointer; }
-  .links {
-    color: blue;
-    cursor: pointer;
-    text-decoration: underline;
-  }
-  #tasks_header {
-    align-items: center;
-    background-color: #fafafa;
-    border-top: 1px solid #ddd;
-    display: flex;
-    padding: 6px 1rem;
-  }
-  .no-margins { margin: 0 0 0 0; }
+  <style include="gr-a11y-styles">
+    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+  </style>
+  <style include="shared-styles">
+    .header {
+      align-items: center;
+      background-color: var(--background-color-primary);
+      border-bottom: 1px solid var(--border-color);
+      display: flex;
+      padding: var(--spacing-s) var(--spacing-l);
+      z-index: 99; /* Less than gr-overlay's backdrop */
+    }
+    .headerTitle {
+      align-items: center;
+      display: flex;
+      flex: 1;
+    }
+    .headerSubject {
+      font-family: var(--header-font-family);
+      font-size: var(--font-size-h3);
+      font-weight: var(--font-weight-h3);
+      line-height: var(--line-height-h3);
+      margin-left: var(--spacing-l);
+    }
+    paper-tabs {
+      background-color: var(--background-color-tertiary);
+      margin-top: var(--spacing-m);
+      height: calc(var(--line-height-h3) + var(--spacing-m));
+      --paper-tabs-selection-bar-color: var(--link-color);
+    }
+    paper-tab {
+      box-sizing: border-box;
+      max-width: 12em;
+      --paper-tab-ink: var(--link-color);
+    }
+    section {
+      background-color: var(--view-background-color);
+      box-shadow: var(--elevation-level-1);
+    }
+    ul {
+      padding-left: 0.5em;
+      margin-top: 0;
+    }
+    .links {
+      color: var(--link-color);
+      cursor: pointer;
+      text-decoration: underline;
+    }
+    .show-all-button iron-icon {
+      color: inherit;
+      --iron-icon-height: 18px;
+      --iron-icon-width: 18px;
+    }
+    .no-margins { margin: 0 0 0 0; }
+    .task-list-item {
+      display: flex;
+      align-items: center;
+      column-gap: 1em;
+      padding-top: 12px;
+      padding-left: 12px;
+    }
 </style>
 
-<div id="tasks" hidden$="[[!_tasks.length]]">
-  <div id="tasks_header" style="display: flex;">
-    <iron-icon
-        icon="gr-icons:expand-less"
-        hidden$="[[!_expand_all]]"
-        on-tap="_switch_expand"
-        class="cursor"> </iron-icon>
-    <iron-icon
-        icon="gr-icons:expand-more"
-        hidden$="[[_expand_all]]"
-        on-tap="_switch_expand"
-        class="cursor"> </iron-icon>
-    <div style="display: flex; align-items: center; column-gap: 1em;">
-    <h3 class="no-margins" on-tap="_switch_expand" class="cursor"> Tasks </h3>
+<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">
     <template is="dom-if" if="[[_is_show_all(_show_all)]]">
-      <p class="no-margins" >All ([[_all_count]]) |&nbsp;
+      <p> All ([[_all_count]]) |
         <span
-            on-click="_needs_and_blocked_tap"
-            class="links">Needs + Blocked ([[_ready_count]], [[_fail_count]])</span>
+            on-click="_needed_and_blocked_tap"
+            class="links">Needed + Blocked ([[_ready_count]], [[_fail_count]])</span>
       <p>
     </template>
     <template is="dom-if" if="[[!_is_show_all(_show_all)]]">
-      <p class="no-margins" > <span
+      <p> <span
             class="links"
             on-click="_show_all_tap">All ([[_all_count]])</span>
-        &nbsp;| Needs + Blocked ([[_ready_count]], [[_fail_count]])</p>
+        &nbsp;| Needed + Blocked ([[_ready_count]], [[_fail_count]])</p>
     </template>
+    <gr-button link="" class="show-all-button" on-click="_switch_expand"
+    >[[_computeShowAllLabelText(_expand_all)]]
+    <iron-icon
+      icon="gr-icons:expand-more"
+      hidden$="[[_expand_all]]"
+    ></iron-icon
+    ><iron-icon
+      icon="gr-icons:expand-less"
+      hidden$="[[!_expand_all]]"
+    ></iron-icon>
+    </gr-button>
   </div>
-  </div>
-  <div hidden$="[[!_expand_all]]">
+  <div hidden$="[[!_expand_all]]" style="padding-bottom: 12px">
     <ul style="list-style-type:none;">
       <gr-task-plugin-tasks
           tasks="[[_tasks]]"
           show_all$="[[_show_all]]"> </gr-task-plugin-tasks>
     </ul>
   </div>
+  </section>
 </div>`;
diff --git a/gr-task-plugin/gr-task-summary.js b/gr-task-plugin/gr-task-summary.js
new file mode 100644
index 0000000..49c858d
--- /dev/null
+++ b/gr-task-plugin/gr-task-summary.js
@@ -0,0 +1,101 @@
+/**
+ * @license
+ * Copyright (C) 2023 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-summary_html.js';
+import './gr-task-chip.js';
+
+class GrTaskSummary extends Polymer.Element {
+  static get is() {
+    return 'gr-task-summary';
+  }
+
+  static get template() {
+    return htmlTemplate;
+  }
+
+  static get properties() {
+    return {
+      ready_count: {
+        type: Number,
+        notify: true,
+        value: 0,
+      },
+
+      fail_count: {
+        type: Number,
+        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,
+      },
+
+      is_loading: {
+        type: Boolean,
+        value: true,
+      },
+    };
+  }
+
+  /** @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;
+    });
+  }
+
+  _can_show_summary(is_loading, ready_count,
+      fail_count, invalid_count,
+      waiting_count, duplicate_count,
+      pass_count) {
+    if (is_loading || ready_count || fail_count || invalid_count ||
+      waiting_count || duplicate_count || pass_count) {
+      return true;
+    }
+    return false;
+  }
+}
+
+customElements.define(GrTaskSummary.is, GrTaskSummary);
\ No newline at end of file
diff --git a/gr-task-plugin/gr-task-summary_html.js b/gr-task-plugin/gr-task-summary_html.js
new file mode 100644
index 0000000..4667060
--- /dev/null
+++ b/gr-task-plugin/gr-task-summary_html.js
@@ -0,0 +1,87 @@
+/**
+ * @license
+ * Copyright (C) 2023 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`
+  <style include="gr-a11y-styles">
+    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+  </style>
+  <style include="shared-styles">
+    :host {
+      display: block;
+      color: var(--deemphasized-text-color);
+      max-width: 625px;
+      margin-bottom: var(--spacing-m);
+    }
+    .zeroState {
+      color: var(--deemphasized-text-color);
+    }
+    .loading.zeroState {
+      margin-right: var(--spacing-m);
+    }
+    div.error,
+    .login {
+      display: flex;
+      color: var(--primary-text-color);
+      padding: 0 var(--spacing-s);
+      margin: var(--spacing-xs) 0;
+      width: 490px;
+    }
+    div.error {
+      background-color: var(--error-background);
+    }
+    div.error .right {
+      overflow: hidden;
+    }
+    div.error .right .message {
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+    td.key {
+      padding-right: var(--spacing-l);
+      padding-bottom: var(--spacing-s);
+      line-height: calc(var(--line-height-normal) + var(--spacing-s));
+    }
+    td.value {
+      padding-right: var(--spacing-l);
+      padding-bottom: var(--spacing-s);
+      line-height: calc(var(--line-height-normal) + var(--spacing-s));
+      display: flex;
+    }
+    div {
+      margin-left: var(--spacing-m);
+    }
+  </style>
+  <div class="task_summary">
+    <table>
+      <tr>
+        <template is="dom-if" if="[[_can_show_summary(is_loading, ready_count, fail_count, invalid_count, waiting_count, duplicate_count, pass_count)]]">
+          <td class="key">Tasks</td>
+          <td class="value">
+            <gr-task-chip chip_style="loading" hidden$="[[!is_loading]]">loading...</gr-task-chip>
+            <gr-task-chip chip_style="fail" hidden$="[[!fail_count]]">[[fail_count]] blocked</gr-task-chip>
+            <gr-task-chip chip_style="invalid" hidden$="[[!invalid_count]]">[[invalid_count]] invalid</gr-task-chip>
+            <gr-task-chip chip_style="waiting" hidden$="[[!waiting_count]]">[[waiting_count]] waiting</gr-task-chip>
+            <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>
+        </template>
+        </td>
+      </tr>
+    </table>
+  </div>
+`;
diff --git a/gr-task-plugin/plugin.js b/gr-task-plugin/plugin.js
index 59d05b0..2219f12 100644
--- a/gr-task-plugin/plugin.js
+++ b/gr-task-plugin/plugin.js
@@ -16,8 +16,11 @@
  */
 
 import './gr-task-plugin.js';
+import './gr-task-summary.js';
 
 Gerrit.install(plugin => {
   plugin.registerCustomComponent(
       'change-view-integration', 'gr-task-plugin');
+  plugin.registerCustomComponent(
+      'commit-container', 'gr-task-summary');
 });
diff --git a/package.json b/package.json
index fac70e5..82c0e23 100644
--- a/package.json
+++ b/package.json
@@ -1,10 +1,10 @@
 {
   "name": "task",
-  "version": "3.2.6-SNAPSHOT",
+  "version": "3.5.0-SNAPSHOT",
   "description": "Task Plugin",
   "dependencies": {
-    "@bazel/rollup": "^3.4.0",
-    "@bazel/terser": "^3.4.0"
+    "@bazel/rollup": "~5.1.0",
+    "@bazel/terser": "~5.1.0"
   },
   "devDependencies": {
     "eslint": "^7.24.0",
diff --git a/src/main/antlr4/com/googlesource/gerrit/plugins/task/TaskReference.g4 b/src/main/antlr4/com/googlesource/gerrit/plugins/task/TaskReference.g4
new file mode 100644
index 0000000..3869643
--- /dev/null
+++ b/src/main/antlr4/com/googlesource/gerrit/plugins/task/TaskReference.g4
@@ -0,0 +1,184 @@
+// Copyright (C) 2022 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.
+
+/**
+ *
+ * This file defines the grammar used for Task Reference
+ *
+ * TASK_REFERENCE = [
+ *                    [ // TASK_FILE_PATH ] |
+ *                    [ @USERNAME [ TASK_FILE_PATH ] ] |
+ *                    [ %GROUP_NAME [ TASK_FILE_PATH ] ] |
+ *                    [ %%GROUP_UUID [ TASK_FILE_PATH ] ] |
+ *                    [ TASK_FILE_PATH ]
+ *                  ] '^' TASK_NAME
+ *
+ * Examples:
+ *
+ * file: All-Projects:refs/meta/config:task.config
+ * reference: foo.config^sample
+ * Implied task:
+ *     file: All-Projects:refs/meta/config:task/foo.config task: sample
+ *
+ * file: All-Projects:refs/meta/config:task/dir/bar.config
+ * reference: /foo.config^sample
+ * Implied task:
+ *     file: All-Projects:refs/meta/config:task/foo.config task: sample
+ *
+ * file: All-Projects:refs/meta/config:task/dir/bar.config
+ * reference: sub-dir/foo.config^sample
+ * Implied task:
+ *     file: All-Projects:refs/meta/config:task/dir/sub-dir/foo.config task: sample
+ *
+ * file: All-Projects:refs/meta/config:task/dir/bar.config
+ * reference: ^sample
+ * Implied task:
+ *     file: All-Projects:refs/meta/config:task.config task: sample
+ *
+ * file: Any projects, ref, file
+ * reference: @jim^sample
+ * Implied task:
+ *     file: All-Users:refs/users/<jim>:task.config task: sample
+ *
+ * file: Any projects, ref, file
+ * reference: @jim/foo^simple
+ * Implied task:
+ *     file: All-Users:refs/users/<jim>:task/foo^simple task: sample
+ *
+ * file: Any projects, ref, file
+ * reference: //foo.config^sample
+ * Implied task:
+ *     file: All-Projects:refs/meta/config:task/foo task: sample
+ *
+ * file: Any projects, ref, file
+ * reference: //^simple
+ * Implied task:
+ *     file: All-Projects:refs/meta/config:task.config task: sample
+ *
+ * Suppose a8341ade45d83e867c24a2d37f47b410cfdbea6d is the UUID of 'CI System Owners' group.
+ * file: Any projects, ref, file
+ * reference: %CI System Owners^sample
+ * Implied task:
+ *     file: All-Users:refs/groups/a8/a8341ade45d83e867c24a2d37f47b410cfdbea6d:task.config
+ *     task: sample
+ *
+ * file: Any projects, ref, file
+ * reference: %CI System Owners/foo^simple
+ * Implied task:
+ *     file: All-Users:refs/groups/a8/a8341ade45d83e867c24a2d37f47b410cfdbea6d:task/foo^simple
+ *     task: sample
+ *
+ * file: Any projects, ref, file
+ * reference: %%a8341ade45d83e867c24a2d37f47b410cfdbea6d^sample
+ * Implied task:
+ *     file: All-Users:refs/groups/a8/a8341ade45d83e867c24a2d37f47b410cfdbea6d:task.config
+ *     task: sample
+ *
+ */
+
+grammar TaskReference;
+
+options {
+  language = Java;
+}
+
+reference
+  : file_path? TASK
+  ;
+
+file_path
+ : ALL_PROJECTS_ROOT
+ | FWD_SLASH absolute TASK_DELIMETER
+ | user absolute? TASK_DELIMETER
+ | group_name absolute? TASK_DELIMETER
+ | group_uuid absolute? TASK_DELIMETER
+ | (absolute| relative)? TASK_DELIMETER
+ ;
+
+user
+ : '@' NAME
+ ;
+
+group_name
+ : '%' (NAME | NAME_WITH_SPACES)
+ ;
+
+group_uuid
+ : '%%' INTERNAL_GROUP_UUID
+ ;
+
+absolute
+ : FWD_SLASH relative
+ ;
+
+relative
+ : dir* NAME
+ ;
+
+dir
+ : (NAME FWD_SLASH)
+ ;
+
+TASK
+ : (~'^')+ EOF
+ ;
+
+INTERNAL_GROUP_UUID
+ : HEX_10 HEX_10 HEX_10 HEX_10
+ ;
+
+fragment HEX_10
+ : HEX HEX HEX HEX HEX HEX HEX HEX HEX HEX
+ ;
+
+fragment HEX
+ : [0-9a-f]
+ ;
+
+NAME
+ : URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH_AND_AT_AND_PERCENTILE URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH*
+ ;
+
+NAME_WITH_SPACES
+ : URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH_AND_AT_AND_PERCENTILE (URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH | SPACE)*
+ ;
+
+fragment URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH
+ : URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH_AND_AT_AND_PERCENTILE
+ | '@' | '%'
+ ;
+
+fragment URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH_AND_AT_AND_PERCENTILE
+ : ':' | '?' | '#' | '[' | ']'
+ |'!' | '$' | '&' | '\'' | '(' | ')'
+ | '*' | '+' | ',' | ';' | '='
+ | 'A'..'Z' | 'a'..'z' | '0'..'9'
+ | '_' | '.' | '\\' | '-' | '~'
+ ;
+
+fragment SPACE
+ : ' '
+ ;
+
+TASK_DELIMETER
+ : '^'
+ ;
+
+ALL_PROJECTS_ROOT
+ : FWD_SLASH FWD_SLASH TASK_DELIMETER
+ ;
+
+FWD_SLASH
+ : '/'
+ ;
diff --git a/src/main/java/com/google/gerrit/common/BooleanTable.java b/src/main/java/com/google/gerrit/common/BooleanTable.java
new file mode 100644
index 0000000..9e89eb9
--- /dev/null
+++ b/src/main/java/com/google/gerrit/common/BooleanTable.java
@@ -0,0 +1,69 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.common;
+
+import java.util.BitSet;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A space efficient Table for Booleans. This Table takes advantage of the fact that the values
+ * stored in it are all Booleans and uses BitSets to make this very space efficient.
+ */
+public class BooleanTable<R, C> {
+  protected class Row {
+    public final BitSet hasValues = new BitSet();
+    public final BitSet values = new BitSet();
+
+    public void setPosition(int position, Boolean value) {
+      if (value != null) {
+        values.set(position, value);
+      }
+      hasValues.set(position, value != null);
+    }
+
+    public Boolean getPosition(int position) {
+      if (hasValues.get(position)) {
+        return values.get(position);
+      }
+      return null;
+    }
+  }
+
+  protected Map<R, Row> rowByRow = new HashMap<>();
+  protected Map<C, Integer> positionByColumn = new HashMap<>();
+  protected int highestPosition = -1;
+
+  public void put(R r, C c, Boolean v) {
+    Row row = rowByRow.computeIfAbsent(r, k -> new Row());
+    Integer columnPosition = positionByColumn.computeIfAbsent(c, k -> nextPosition());
+    row.setPosition(columnPosition, v);
+  }
+
+  protected int nextPosition() {
+    return ++highestPosition;
+  }
+
+  public Boolean get(R r, C c) {
+    Row row = rowByRow.get(r);
+    if (row != null) {
+      Integer columnPosition = positionByColumn.get(c);
+      if (columnPosition != null) {
+        return row.getPosition(columnPosition);
+      }
+    }
+    return null;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/ConfigSourcedValue.java b/src/main/java/com/googlesource/gerrit/plugins/task/ConfigSourcedValue.java
new file mode 100644
index 0000000..d5a8ad1
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/ConfigSourcedValue.java
@@ -0,0 +1,32 @@
+// 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 com.google.auto.value.AutoValue;
+
+@AutoValue
+public abstract class ConfigSourcedValue {
+  public static ConfigSourcedValue create(FileKey sourceFile, String value) {
+    return new AutoValue_ConfigSourcedValue(sourceFile, value);
+  }
+
+  public static Class<? extends ConfigSourcedValue> getClassType() {
+    return AutoValue_ConfigSourcedValue.class;
+  }
+
+  public abstract FileKey sourceFile();
+
+  public abstract String value();
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/FileKey.java b/src/main/java/com/googlesource/gerrit/plugins/task/FileKey.java
new file mode 100644
index 0000000..9856075
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/FileKey.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2021 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 com.google.auto.value.AutoValue;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Project;
+
+/** An immutable reference to a fully qualified file in gerrit repo. */
+@AutoValue
+public abstract class FileKey {
+  public static FileKey create(Project.NameKey project, String branch, String file) {
+    return new AutoValue_FileKey(BranchNameKey.create(project, branch), file);
+  }
+
+  public static FileKey create(BranchNameKey branch, String file) {
+    return new AutoValue_FileKey(branch, file);
+  }
+
+  public abstract BranchNameKey branch();
+
+  public abstract String file();
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/IsTrueOperator.java b/src/main/java/com/googlesource/gerrit/plugins/task/IsTrueOperator.java
new file mode 100644
index 0000000..472b1e7
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/IsTrueOperator.java
@@ -0,0 +1,61 @@
+// Copyright (C) 2023 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 com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.SubmitRequirementPredicate;
+import com.google.inject.AbstractModule;
+
+// TODO: Remove this class when up-merging to Gerrit v3.6+ as it supports
+//       the 'is:true' submit requirement
+public class IsTrueOperator implements ChangeQueryBuilder.ChangeIsOperandFactory {
+
+  public static final String TRUE = "true";
+
+  public static class Module extends AbstractModule {
+    @Override
+    protected void configure() {
+      bind(ChangeQueryBuilder.ChangeIsOperandFactory.class)
+          .annotatedWith(Exports.named(TRUE))
+          .to(IsTrueOperator.class);
+    }
+  }
+
+  public class TruePredicate extends SubmitRequirementPredicate {
+
+    public TruePredicate() {
+      super("is", TRUE);
+    }
+
+    @Override
+    public boolean match(ChangeData data) {
+      return true;
+    }
+
+    @Override
+    public int getCost() {
+      return 1;
+    }
+  }
+
+  @Override
+  public Predicate<ChangeData> create(ChangeQueryBuilder builder) throws QueryParseException {
+    return new IsTrueOperator.TruePredicate();
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/MatchCache.java b/src/main/java/com/googlesource/gerrit/plugins/task/MatchCache.java
index 45fe46d..0be04ba 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/MatchCache.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/MatchCache.java
@@ -14,44 +14,54 @@
 
 package com.googlesource.gerrit.plugins.task;
 
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.index.query.Matchable;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.server.query.change.ChangeData;
-import java.util.HashMap;
-import java.util.Map;
+import com.googlesource.gerrit.plugins.task.statistics.HitBooleanTable;
+import com.googlesource.gerrit.plugins.task.statistics.StopWatch;
 
 public class MatchCache {
+  protected final HitBooleanTable<String, Change.Id> resultByChangeByQuery =
+      new HitBooleanTable<>();
   protected final PredicateCache predicateCache;
-  protected final ChangeData changeData;
 
-  protected final Map<String, Boolean> matchResultByQuery = new HashMap<>();
-
-  public MatchCache(PredicateCache predicateCache, ChangeData changeData) {
+  public MatchCache(PredicateCache predicateCache) {
     this.predicateCache = predicateCache;
-    this.changeData = changeData;
   }
 
-  protected boolean match(String query) throws StorageException, QueryParseException {
-    if (query == null) {
+  public Boolean matchOrNull(ChangeData changeData, String query, boolean isVisible) {
+    try {
+      return match(changeData, query, isVisible);
+    } catch (StorageException | QueryParseException e) {
+    }
+    return null;
+  }
+
+  @SuppressWarnings("try")
+  public boolean match(ChangeData changeData, String query, boolean isVisible)
+      throws StorageException, QueryParseException {
+    if (query == null || "true".equalsIgnoreCase(query)) {
       return true;
     }
-    Boolean isMatched = matchResultByQuery.get(query);
+    Boolean isMatched = resultByChangeByQuery.get(query, changeData.getId());
     if (isMatched == null) {
-      isMatched = predicateCache.match(changeData, query);
-      matchResultByQuery.put(query, isMatched);
+      Matchable<ChangeData> matchable = predicateCache.getPredicate(query, isVisible).asMatchable();
+      try (StopWatch stopWatch =
+          resultByChangeByQuery.createLoadingStopWatch(query, changeData.getId(), isVisible)) {
+        isMatched = matchable.match(changeData);
+        resultByChangeByQuery.put(query, changeData.getId(), isMatched);
+      }
     }
     return isMatched;
   }
 
-  protected Boolean matchOrNull(String query) {
-    if (query == null) {
-      return null;
-    }
-    Boolean isMatched = matchResultByQuery.get(query);
-    if (isMatched == null) {
-      isMatched = predicateCache.matchOrNull(changeData, query);
-      matchResultByQuery.put(query, isMatched);
-    }
-    return isMatched;
+  public void initStatistics(int summaryCount) {
+    resultByChangeByQuery.initStatistics(summaryCount);
+  }
+
+  public Object getStatistics() {
+    return resultByChangeByQuery.getStatistics();
   }
 }
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 3a8d903..3451836 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java
@@ -15,6 +15,8 @@
 package com.googlesource.gerrit.plugins.task;
 
 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.DynamicSet;
 import com.google.gerrit.extensions.webui.JavaScriptPlugin;
 import com.google.gerrit.extensions.webui.WebUiPlugin;
@@ -23,7 +25,6 @@
 import com.google.gerrit.server.restapi.change.GetChange;
 import com.google.gerrit.server.restapi.change.QueryChanges;
 import com.google.gerrit.sshd.commands.Query;
-import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.task.cli.PatchSetArgument;
 import java.util.ArrayList;
@@ -31,13 +32,24 @@
 import org.kohsuke.args4j.Option;
 
 public class Modules {
-  public static class Module extends AbstractModule {
+  public static class Module extends FactoryModule {
     @Override
     protected void configure() {
+      bind(CapabilityDefinition.class)
+          .annotatedWith(Exports.named(ViewPathsCapability.VIEW_PATHS))
+          .to(ViewPathsCapability.class);
+      factory(TaskPath.Factory.class);
+      factory(TaskReference.Factory.class);
+      factory(TaskExpression.Factory.class);
+      factory(TaskTree.Factory.class);
+      factory(Preloader.Factory.class);
+
       bind(ChangePluginDefinedInfoFactory.class)
           .annotatedWith(Exports.named("task"))
           .to(TaskAttributeFactory.class);
 
+      install(new IsTrueOperator.Module());
+
       bind(DynamicBean.class).annotatedWith(Exports.named(GetChange.class)).to(MyOptions.class);
       bind(DynamicBean.class).annotatedWith(Exports.named(Query.class)).to(MyOptions.class);
       bind(DynamicBean.class).annotatedWith(Exports.named(QueryChanges.class)).to(MyOptions.class);
@@ -47,6 +59,13 @@
   }
 
   public static class MyOptions implements DynamicBean {
+    @Option(
+        name = "--only",
+        usage =
+            "Evaluate tasks under this task only. Only root task names are supported."
+                + " This option can be provided multiple times.")
+    public List<String> includedRoots = new ArrayList<>();
+
     @Option(name = "--all", usage = "Include all visible tasks in the output")
     public boolean all = false;
 
@@ -58,9 +77,18 @@
         usage = "Include only invalid tasks and the tasks referencing them in the output")
     public boolean onlyInvalid = false;
 
+    @Option(name = "--include-paths", usage = "Include absolute path to each task")
+    public boolean includePaths = false;
+
     @Option(name = "--evaluation-time", usage = "Include elapsed evaluation time on each task")
     public boolean evaluationTime = false;
 
+    @Option(name = "--include-statistics", usage = "Include statistcs about the task evaluations")
+    public boolean includeStatistics = false;
+
+    @Option(name = "--summary-count", usage = "number of items to output in statistics summaries")
+    public int summaryCount = 5;
+
     @Option(
         name = "--preview",
         metaVar = "{CHANGE,PATCHSET}",
@@ -78,5 +106,9 @@
     public MyOptions(PatchSetArgument.Factory patchSetArgumentFactory) {
       this.patchSetArgumentFactory = patchSetArgumentFactory;
     }
+
+    public boolean shouldFilterRoot(String rootName) {
+      return !includedRoots.isEmpty() && !includedRoots.contains(rootName);
+    }
   }
 }
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 7896417..268b1a3 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/PredicateCache.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/PredicateCache.java
@@ -14,63 +14,81 @@
 
 package com.googlesource.gerrit.plugins.task;
 
-import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.index.FieldDef;
+import com.google.gerrit.index.query.AndPredicate;
+import com.google.gerrit.index.query.NotPredicate;
+import com.google.gerrit.index.query.OrPredicate;
 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.ChangeData;
-import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.ChangeIndexPredicate;
+import com.google.gerrit.server.query.change.DestinationPredicate;
+import com.google.gerrit.server.query.change.RegexProjectPredicate;
+import com.google.gerrit.server.query.change.RegexRefPredicate;
+import com.google.gerrit.server.query.change.SubmitRequirementChangeQueryBuilder;
 import com.google.inject.Inject;
-import java.util.HashMap;
-import java.util.Map;
+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 {
-  protected final ChangeQueryBuilder cqb;
-  protected final CurrentUser user;
+  public static class Statistics {
+    protected Object predicatesByQueryCache;
+  }
 
-  protected final Map<String, ThrowingProvider<Predicate<ChangeData>, QueryParseException>>
-      predicatesByQuery = new HashMap<>();
+  protected final SubmitRequirementChangeQueryBuilder srcqb;
+  protected final Set<String> cacheableByBranchPredicateClassNames;
+  protected final CurrentUser user;
+  protected final HitHashMap<String, ThrowingProvider<Predicate<ChangeData>, QueryParseException>>
+      predicatesByQuery = new HitHashMap<>();
+
+  protected Statistics statistics;
 
   @Inject
-  public PredicateCache(CurrentUser user, ChangeQueryBuilder cqb) {
+  public PredicateCache(
+      @GerritServerConfig Config config,
+      @PluginName String pluginName,
+      CurrentUser user,
+      SubmitRequirementChangeQueryBuilder srcqb) {
     this.user = user;
-    this.cqb = cqb;
+    this.srcqb = srcqb;
+    cacheableByBranchPredicateClassNames =
+        new HashSet<>(
+            Arrays.asList(
+                config.getStringList(pluginName, "cacheable-predicates", "byBranch-className")));
   }
 
-  public boolean match(ChangeData c, String query) throws StorageException, QueryParseException {
-    if (query == null) {
-      return true;
+  public void initStatistics(int summaryCount) {
+    statistics = new Statistics();
+    predicatesByQuery.initStatistics(summaryCount);
+  }
+
+  public Object getStatistics() {
+    if (statistics != null) {
+      statistics.predicatesByQueryCache = predicatesByQuery.getStatistics();
     }
-    return matchWithExceptions(c, query);
+    return statistics;
   }
 
-  public Boolean matchOrNull(ChangeData c, String query) {
-    if (query != null) {
-      try {
-        return matchWithExceptions(c, query);
-      } catch (QueryParseException | RuntimeException e) {
-      }
-    }
-    return null;
-  }
-
-  protected boolean matchWithExceptions(ChangeData c, String query)
-      throws QueryParseException, StorageException {
-    if ("true".equalsIgnoreCase(query)) {
-      return true;
-    }
-    return getPredicate(query).asMatchable().match(c);
-  }
-
-  protected Predicate<ChangeData> getPredicate(String query) throws QueryParseException {
+  @SuppressWarnings("try")
+  public Predicate<ChangeData> getPredicate(String query, boolean isVisible)
+      throws QueryParseException {
     ThrowingProvider<Predicate<ChangeData>, QueryParseException> predProvider =
         predicatesByQuery.get(query);
     if (predProvider != null) {
       return predProvider.get();
     }
     // never seen 'query' before
-    try {
-      Predicate<ChangeData> pred = cqb.parse(query);
+    try (StopWatch stopWatch = predicatesByQuery.createLoadingStopWatch(query, isVisible)) {
+      Predicate<ChangeData> pred = srcqb.parse(query);
       predicatesByQuery.put(query, new ThrowingProvider.Entry<>(pred));
       return pred;
     } catch (QueryParseException e) {
@@ -78,4 +96,43 @@
       throw e;
     }
   }
+
+  /**
+   * Can this query's output be assumed to be constant given any Change destined for the same
+   * Branch.NameKey?
+   */
+  public boolean isCacheableByBranch(String query, boolean isVisible) throws QueryParseException {
+    if (query == null
+        || "".equals(query)
+        || "false".equalsIgnoreCase(query)
+        || "true".equalsIgnoreCase(query)) {
+      return true;
+    }
+    return isCacheableByBranch(getPredicate(query, isVisible));
+  }
+
+  protected boolean isCacheableByBranch(Predicate<ChangeData> predicate) {
+    if (predicate instanceof AndPredicate
+        || predicate instanceof NotPredicate
+        || predicate instanceof OrPredicate) {
+      for (Predicate<ChangeData> subPred : predicate.getChildren()) {
+        if (!isCacheableByBranch(subPred)) {
+          return false;
+        }
+      }
+      return true;
+    }
+    if (predicate instanceof DestinationPredicate
+        || predicate instanceof RegexProjectPredicate
+        || predicate instanceof RegexRefPredicate) {
+      return true;
+    }
+    if (predicate instanceof ChangeIndexPredicate) {
+      FieldDef<ChangeData, ?> field = ((ChangeIndexPredicate) predicate).getField();
+      if (field.equals(ChangeField.PROJECT) || field.equals(ChangeField.REF)) {
+        return true;
+      }
+    }
+    return cacheableByBranchPredicateClassNames.contains(predicate.getClass().getName());
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java b/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java
index 8babe1c..7b325cf 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java
@@ -14,68 +14,181 @@
 
 package com.googlesource.gerrit.plugins.task;
 
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
 import com.googlesource.gerrit.plugins.task.TaskConfig.Task;
+import com.googlesource.gerrit.plugins.task.statistics.HitHashMap;
+import com.googlesource.gerrit.plugins.task.statistics.StatisticsMap;
+import java.io.IOException;
 import java.lang.reflect.Field;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Optional;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
 /** Use to pre-load a task definition with values from its preload-task definition. */
 public class Preloader {
-  public static void preload(Task definition) throws ConfigInvalidException {
-    String name = definition.preloadTask;
-    if (name != null) {
-      Task task = definition.config.getTaskOptional(name);
-      if (task != null) {
-        preload(task);
-        preloadFrom(definition, task);
-      }
-    }
+  public interface Factory {
+    Preloader create(@Assisted TaskConfigCache taskConfigCache);
   }
 
-  protected static void preloadFrom(Task definition, Task preloadFrom) {
+  public static class Statistics {
+    protected Object optionalTaskByExpressionCache;
+    protected long loaded;
+    protected long preloaded;
+    protected long preloadedFromDefinition;
+  }
+
+  protected final TaskConfigCache taskConfigCache;
+  protected final TaskExpression.Factory taskExpressionFactory;
+  protected final StatisticsMap<TaskExpressionKey, Optional<Task>> optionalTaskByExpression =
+      new HitHashMap<>();
+
+  protected Statistics statistics;
+
+  @Inject
+  public Preloader(
+      TaskExpression.Factory taskExpressionFactory, @Assisted TaskConfigCache taskConfigCache) {
+    this.taskConfigCache = taskConfigCache;
+    this.taskExpressionFactory = taskExpressionFactory;
+  }
+
+  public List<Task> getRootTasks() throws IOException, ConfigInvalidException {
+    return getTasks(taskConfigCache.getRootConfig(), TaskConfig.SECTION_ROOT);
+  }
+
+  public List<Task> getTasks(FileKey file) throws IOException, ConfigInvalidException {
+    return getTasks(taskConfigCache.getTaskConfig(file), TaskConfig.SECTION_TASK);
+  }
+
+  protected List<Task> getTasks(TaskConfig cfg, String type) throws IOException {
+    List<Task> preloaded = new ArrayList<>();
+    for (Task task : cfg.getTasks(type)) {
+      try {
+        preloaded.add(preload(task));
+      } catch (ConfigInvalidException e) {
+        preloaded.add(null);
+      }
+    }
+    return preloaded;
+  }
+
+  boolean inGetOptionalTask;
+
+  /**
+   * Get a preloaded Task for this TaskExpression.
+   *
+   * @param expression
+   * @return Optional<Task> which is empty if the expression is optional and no tasks are resolved
+   * @throws ConfigInvalidException if the expression requires a task and no tasks are resolved
+   */
+  public Optional<Task> getOptionalTask(TaskExpression expression)
+      throws ConfigInvalidException, IOException {
+    Optional<Task> task = optionalTaskByExpression.get(expression.key);
+    if (task == null) {
+      boolean firstInGetOptionalTask = !inGetOptionalTask;
+      inGetOptionalTask = true;
+      task = preloadOptionalTask(expression);
+      optionalTaskByExpression.put(expression.key, task);
+      if (firstInGetOptionalTask) {
+        inGetOptionalTask = false;
+      }
+    }
+    return task;
+  }
+
+  protected Optional<Task> preloadOptionalTask(TaskExpression expression)
+      throws ConfigInvalidException, IOException {
+    Optional<Task> definition = loadOptionalTask(expression);
+    return definition.isPresent() ? Optional.of(preload(definition.get())) : definition;
+  }
+
+  public Task preload(Task definition) throws ConfigInvalidException, IOException {
+    if (statistics != null && !inGetOptionalTask) {
+      statistics.preloadedFromDefinition++;
+    }
+    String expression = definition.preloadTask;
+    if (expression != null) {
+      if (statistics != null) {
+        statistics.preloaded++;
+      }
+      Optional<Task> preloadFrom =
+          getOptionalTask(taskExpressionFactory.create(definition.file(), expression));
+      if (preloadFrom.isPresent()) {
+        return preloadFrom(definition, preloadFrom.get());
+      }
+    }
+    return definition;
+  }
+
+  protected Optional<Task> loadOptionalTask(TaskExpression expression)
+      throws ConfigInvalidException, IOException {
+    if (statistics != null) {
+      statistics.loaded++;
+    }
+    try {
+      for (TaskKey key : expression) {
+        Optional<Task> task = getOptionalTask(key);
+        if (task.isPresent()) {
+          return task;
+        }
+      }
+    } catch (RuntimeConfigInvalidException e) {
+      throw e.checkedException;
+    } catch (NoSuchElementException e) {
+      // expression was not optional but we ran out of names to try
+      throw new ConfigInvalidException("task not defined");
+    }
+    return Optional.empty();
+  }
+
+  protected static Task preloadFrom(Task definition, Task preloadFrom) {
+    Task preloadTo = definition.config.new Task(definition.subSection);
     for (Field field : definition.getClass().getFields()) {
       String name = field.getName();
-      if ("isVisible".equals(name) || "isTrusted".equals(name) || "config".equals(name)) {
+      if ("config".equals(name)) {
         continue;
       }
 
       try {
         field.setAccessible(true);
-        preloadField(field.getType(), field, definition, preloadFrom);
+        preloadField(field, definition, preloadFrom, preloadTo);
       } catch (IllegalAccessException | IllegalArgumentException e) {
         throw new RuntimeException();
       }
     }
+    return preloadTo;
   }
 
-  protected static <T, S, K, V> void preloadField(
-      Class<T> clz, Field field, Task definition, Task preloadFrom)
+  protected Optional<Task> getOptionalTask(TaskKey key) throws IOException, ConfigInvalidException {
+    return taskConfigCache.getTaskConfig(key.subSection().file()).getOptionalTask(key.task());
+  }
+
+  protected static <S, K, V> void preloadField(
+      Field field, Task definition, Task preloadFrom, Task preloadTo)
       throws IllegalArgumentException, IllegalAccessException {
-    T pre = getField(clz, field, preloadFrom);
-    if (pre != null) {
-      T val = getField(clz, field, definition);
-      if (val == null) {
-        field.set(definition, pre);
-      } else if (val instanceof List) {
-        List<?> valList = List.class.cast(val);
-        List<?> preList = List.class.cast(pre);
-        field.set(definition, preloadListFrom(castUnchecked(valList), castUnchecked(preList)));
-      } else if (val instanceof Map) {
-        Map<?, ?> valMap = Map.class.cast(val);
-        Map<?, ?> preMap = Map.class.cast(pre);
-        field.set(definition, preloadMapFrom(castUnchecked(valMap), castUnchecked(preMap)));
-      } // nothing to do for overridden preloaded scalars
+    Object pre = field.get(preloadFrom);
+    Object val = field.get(definition);
+    if (val == null) {
+      field.set(preloadTo, pre);
+    } else if (pre == null) {
+      field.set(preloadTo, val);
+    } else if (val instanceof List) {
+      List<?> valList = List.class.cast(val);
+      List<?> preList = List.class.cast(pre);
+      field.set(preloadTo, preloadListFrom(castUnchecked(valList), castUnchecked(preList)));
+    } else if (val instanceof Map) {
+      Map<?, ?> valMap = Map.class.cast(val);
+      Map<?, ?> preMap = Map.class.cast(pre);
+      field.set(preloadTo, preloadMapFrom(castUnchecked(valMap), castUnchecked(preMap)));
+    } else {
+      field.set(preloadTo, val);
     }
   }
 
-  protected static <T> T getField(Class<T> clz, Field field, Object obj)
-      throws IllegalArgumentException, IllegalAccessException {
-    return clz.cast(field.get(obj));
-  }
-
   @SuppressWarnings("unchecked")
   protected static <S> List<S> castUnchecked(List<?> list) {
     List<S> forceCheck = (List<S>) list;
@@ -89,28 +202,40 @@
   }
 
   protected static <T> List<T> preloadListFrom(List<T> list, List<T> preList) {
-    List<T> extended = list;
-    if (!preList.isEmpty()) {
-      extended = preList;
-      if (!list.isEmpty()) {
-        extended = new ArrayList<>(list.size() + preList.size());
-        extended.addAll(preList);
-        extended.addAll(list);
-      }
+    if (preList.isEmpty()) {
+      return list;
     }
+    if (list.isEmpty()) {
+      return preList;
+    }
+
+    List<T> extended = new ArrayList<>(list.size() + preList.size());
+    extended.addAll(preList);
+    extended.addAll(list);
     return extended;
   }
 
   protected static <K, V> Map<K, V> preloadMapFrom(Map<K, V> map, Map<K, V> preMap) {
-    Map<K, V> extended = map;
-    if (!preMap.isEmpty()) {
-      extended = preMap;
-      if (!map.isEmpty()) {
-        extended = new HashMap<>(map.size() + preMap.size());
-        extended.putAll(preMap);
-        extended.putAll(map);
-      }
+    if (preMap.isEmpty()) {
+      return map;
     }
+    if (map.isEmpty()) {
+      return preMap;
+    }
+
+    Map<K, V> extended = new HashMap<>(map.size() + preMap.size());
+    extended.putAll(preMap);
+    extended.putAll(map);
     return extended;
   }
+
+  public void initStatistics(int summaryCount) {
+    statistics = new Statistics();
+    optionalTaskByExpression.initStatistics(summaryCount);
+  }
+
+  public Statistics getStatistics() {
+    statistics.optionalTaskByExpressionCache = optionalTaskByExpression.getStatistics();
+    return statistics;
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/Properties.java b/src/main/java/com/googlesource/gerrit/plugins/task/Properties.java
deleted file mode 100644
index 028762c..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/task/Properties.java
+++ /dev/null
@@ -1,215 +0,0 @@
-// Copyright (C) 2019 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 com.google.common.collect.Sets;
-import com.google.gerrit.entities.Change;
-import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.server.query.change.ChangeData;
-import com.googlesource.gerrit.plugins.task.TaskConfig.NamesFactory;
-import com.googlesource.gerrit.plugins.task.TaskConfig.Task;
-import java.lang.reflect.Field;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.ListIterator;
-import java.util.Map;
-import java.util.Set;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/** Use to expand properties like ${_name} in the text of various definitions. */
-public class Properties {
-  /** Use to expand properties specifically for Tasks. */
-  public static class Task extends Expander {
-    public static final Task EMPTY_PARENT = new Task();
-
-    public Task() {
-      super(Collections.emptyMap());
-    }
-
-    public Task(ChangeData changeData, TaskConfig.Task definition, Task parentProperties)
-        throws StorageException {
-      super(parentProperties.forDescendants());
-      valueByName.putAll(getInternalProperties(definition, changeData));
-      new RecursiveExpander(valueByName).expand(definition.getAllProperties());
-
-      definition.setExpandedProperties(valueByName);
-
-      expandFieldValues(definition, Collections.emptySet());
-    }
-
-    protected Map<String, String> forDescendants() {
-      return new HashMap<>(valueByName);
-    }
-  }
-
-  /** Use to expand properties specifically for NamesFactories. */
-  public static class NamesFactory extends Expander {
-    public NamesFactory(TaskConfig.NamesFactory namesFactory, Task properties) {
-      super(properties.valueByName);
-      expandFieldValues(namesFactory, Sets.newHashSet(TaskConfig.KEY_TYPE));
-    }
-  }
-
-  protected static Map<String, String> getInternalProperties(
-      TaskConfig.Task definition, ChangeData changeData) throws StorageException {
-    Map<String, String> properties = new HashMap<>();
-
-    properties.put("_name", definition.name);
-
-    Change c = changeData.change();
-    properties.put("_change_number", String.valueOf(c.getId().get()));
-    properties.put("_change_id", c.getKey().get());
-    properties.put("_change_project", c.getProject().get());
-    properties.put("_change_branch", c.getDest().branch());
-    properties.put("_change_status", c.getStatus().toString());
-    properties.put("_change_topic", c.getTopic());
-
-    return properties;
-  }
-
-  /**
-   * Use to expand properties whose values may contain other references to properties.
-   *
-   * <p>Using a recursive expansion approach makes order of evaluation unimportant as long as there
-   * are no looping definitions.
-   *
-   * <p>Given some property name/value asssociations defined like this:
-   *
-   * <p><code>
-   * valueByName.put("obstacle", "fence");
-   * valueByName.put("action", "jumped over the ${obstacle}");
-   * </code>
-   *
-   * <p>a String like: <code>"The brown fox ${action}."</code>
-   *
-   * <p>will expand to: <code>"The brown fox jumped over the fence."</code>
-   */
-  protected static class RecursiveExpander {
-    protected final Expander expander;
-    protected Map<String, String> unexpandedByName;
-    protected Set<String> expanding;
-
-    public RecursiveExpander(Map<String, String> valueByName) {
-      expander =
-          new Expander(valueByName) {
-            @Override
-            protected String getValueForName(String name) {
-              expandUnexpanded(name); // recursive call
-              return super.getValueForName(name);
-            }
-          };
-    }
-
-    public void expand(Map<String, String> unexpandedByName) {
-      this.unexpandedByName = unexpandedByName;
-
-      // Copy keys to allow out of order removals during iteration
-      for (String unexpanedName : new ArrayList<>(unexpandedByName.keySet())) {
-        expanding = new HashSet<>();
-        expandUnexpanded(unexpanedName);
-      }
-    }
-
-    protected void expandUnexpanded(String name) {
-      if (!expanding.add(name)) {
-        throw new RuntimeException("Looping property definitions.");
-      }
-      String value = unexpandedByName.remove(name);
-      if (value != null) {
-        expander.valueByName.put(name, expander.expandText(value));
-      }
-    }
-  }
-
-  /**
-   * Use to expand properties like ${property} in Strings into their values.
-   *
-   * <p>Given some property name/value asssociations defined like this:
-   *
-   * <p><code>
-   * valueByName.put("animal", "fox");
-   * valueByName.put("bar", "foo");
-   * valueByName.put("obstacle", "fence");
-   * </code>
-   *
-   * <p>a String like: <code>"The brown ${animal} jumped over the ${obstacle}."</code>
-   *
-   * <p>will expand to: <code>"The brown fox jumped over the fence."</code>
-   */
-  protected static class Expander {
-    // "${_name}" -> group(1) = "_name"
-    protected static final Pattern PATTERN = Pattern.compile("\\$\\{([^}]+)\\}");
-
-    public final Map<String, String> valueByName;
-
-    public Expander(Map<String, String> valueByName) {
-      this.valueByName = valueByName;
-    }
-
-    /** Expand all properties in the Strings in the object's Fields (except the exclude ones) */
-    protected void expandFieldValues(Object object, Set<String> excludedFieldNames) {
-      for (Field field : object.getClass().getFields()) {
-        try {
-          if (!excludedFieldNames.contains(field.getName())) {
-            field.setAccessible(true);
-            Object o = field.get(object);
-            if (o instanceof String) {
-              field.set(object, expandText((String) o));
-            } else if (o instanceof List) {
-              @SuppressWarnings("unchecked")
-              List<String> forceCheck = List.class.cast(o);
-              expandElements(forceCheck);
-            }
-          }
-        } catch (IllegalAccessException e) {
-          throw new RuntimeException(e);
-        }
-      }
-    }
-
-    /** Expand all properties in the Strings in the List */
-    public void expandElements(List<String> list) {
-      if (list != null) {
-        for (ListIterator<String> it = list.listIterator(); it.hasNext(); ) {
-          it.set(expandText(it.next()));
-        }
-      }
-    }
-
-    /** Expand all properties (${property_name} -> property_value) in the given text */
-    public String expandText(String text) {
-      if (text == null) {
-        return null;
-      }
-      StringBuffer out = new StringBuffer();
-      Matcher m = PATTERN.matcher(text);
-      while (m.find()) {
-        m.appendReplacement(out, Matcher.quoteReplacement(getValueForName(m.group(1))));
-      }
-      m.appendTail(out);
-      return out.toString();
-    }
-
-    /** Get the replacement value for the property identified by name */
-    protected String getValueForName(String name) {
-      String value = valueByName.get(name);
-      return value == null ? "" : value;
-    }
-  }
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/RuntimeConfigInvalidException.java b/src/main/java/com/googlesource/gerrit/plugins/task/RuntimeConfigInvalidException.java
new file mode 100644
index 0000000..cc0ed94
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/RuntimeConfigInvalidException.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2022 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 org.eclipse.jgit.errors.ConfigInvalidException;
+
+public class RuntimeConfigInvalidException extends RuntimeException {
+  protected static final long serialVersionUID = 1L;
+  protected ConfigInvalidException checkedException;
+
+  public RuntimeConfigInvalidException(ConfigInvalidException e) {
+    super(e);
+    this.checkedException = e;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/SubSectionKey.java b/src/main/java/com/googlesource/gerrit/plugins/task/SubSectionKey.java
new file mode 100644
index 0000000..54db1e4
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/SubSectionKey.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2021 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 com.google.auto.value.AutoValue;
+
+/** An immutable reference to a SubSection in fully qualified task config file. */
+@AutoValue
+public abstract class SubSectionKey {
+  public static SubSectionKey create(FileKey file, String section, String subSection) {
+    return new AutoValue_SubSectionKey(file, section, subSection == null ? "" : subSection);
+  }
+
+  public abstract FileKey file();
+
+  public abstract String section();
+
+  public abstract String subSection();
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
index 1e19f7b..ca1ae2c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
@@ -14,13 +14,14 @@
 
 package com.googlesource.gerrit.plugins.task;
 
-import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.api.access.PluginPermission;
 import com.google.gerrit.extensions.common.PluginDefinedInfo;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.server.DynamicOptions.BeanProvider;
 import com.google.gerrit.server.change.ChangePluginDefinedInfoFactory;
+import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.task.TaskConfig.Task;
@@ -29,33 +30,64 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
 public class TaskAttributeFactory implements ChangePluginDefinedInfoFactory {
-  private static final FluentLogger log = FluentLogger.forEnclosingClass();
+  public static final TaskPath MISSING_VIEW_PATH_CAPABILITY =
+      new TaskPath(
+          String.format(
+              "Can't perform operation, need %s capability", ViewPathsCapability.VIEW_PATHS));
+
   public enum Status {
     INVALID,
     UNKNOWN,
+    DUPLICATE,
     WAITING,
     READY,
     PASS,
     FAIL;
   }
 
+  public static class Statistics {
+    public long numberOfChanges;
+    public long numberOfChangeNodes;
+    public long numberOfDuplicates;
+    public long numberOfNodes;
+    public long numberOfTaskPluginAttributes;
+    public Object predicateCache;
+    public Object matchCache;
+    public Preloader.Statistics preloader;
+    public TaskTree.Statistics treeCaches;
+  }
+
   public static class TaskAttribute {
+    public static class Statistics {
+      public boolean isApplicableRefreshRequired;
+      public boolean isSubNodeReloadRequired;
+      public boolean isTaskRefreshNeeded;
+      public Boolean hasUnfilterableSubNodes;
+      public Object nodesByBranchCache;
+      public Object properties;
+    }
+
     public Boolean applicable;
     public Map<String, String> exported;
     public Boolean hasPass;
     public String hint;
     public Boolean inProgress;
+    public TaskPath path;
     public String name;
+    public Integer change;
     public Status status;
     public List<TaskAttribute> subTasks;
     public Long evaluationMilliSeconds;
+    public Statistics statistics;
 
     public TaskAttribute(String name) {
       this.name = name;
@@ -64,29 +96,53 @@
 
   public static class TaskPluginAttribute extends PluginDefinedInfo {
     public List<TaskAttribute> roots = new ArrayList<>();
+    public Statistics queryStatistics;
   }
 
+  protected final String pluginName;
   protected final TaskTree definitions;
   protected final PredicateCache predicateCache;
+  protected final boolean hasViewPathsCapability;
+  protected final TaskPath.Factory taskPathFactory;
+  protected final TaskConfigCache taskConfigCache;
 
   protected Modules.MyOptions options;
+  protected TaskPluginAttribute lastTaskPluginAttribute;
+  protected Statistics statistics;
 
   @Inject
-  public TaskAttributeFactory(TaskTree definitions, PredicateCache predicateCache) {
-    this.definitions = definitions;
+  public TaskAttributeFactory(
+      String pluginName,
+      TaskTree.Factory taskTreeFactory,
+      PredicateCache predicateCache,
+      PermissionBackend permissionBackend,
+      TaskPath.Factory taskPathFactory,
+      TaskConfigCache taskConfigCache) {
+    this.pluginName = pluginName;
+    this.definitions = taskTreeFactory.create(taskConfigCache);
     this.predicateCache = predicateCache;
+    this.hasViewPathsCapability =
+        permissionBackend
+            .currentUser()
+            .testOrFalse(new PluginPermission(this.pluginName, ViewPathsCapability.VIEW_PATHS));
+    this.taskPathFactory = taskPathFactory;
+    this.taskConfigCache = taskConfigCache;
   }
 
   @Override
   public Map<Change.Id, PluginDefinedInfo> createPluginDefinedInfos(
-          Collection<ChangeData> cds, BeanProvider beanProvider, String plugin) {
+      Collection<ChangeData> cds, BeanProvider beanProvider, String plugin) {
     Map<Change.Id, PluginDefinedInfo> pluginInfosByChange = new HashMap<>();
     options = (Modules.MyOptions) beanProvider.getDynamicBean(plugin);
     if (options.all || options.onlyApplicable || options.onlyInvalid) {
+      initStatistics();
       for (PatchSetArgument psa : options.patchSetArguments) {
-        definitions.masquerade(psa);
+        taskConfigCache.masquerade(psa);
       }
       cds.forEach(cd -> pluginInfosByChange.put(cd.getId(), createWithExceptions(cd)));
+      if (lastTaskPluginAttribute != null) {
+        lastTaskPluginAttribute.queryStatistics = getStatistics(pluginInfosByChange);
+      }
     }
     return pluginInfosByChange;
   }
@@ -94,38 +150,47 @@
   protected PluginDefinedInfo createWithExceptions(ChangeData c) {
     TaskPluginAttribute a = new TaskPluginAttribute();
     try {
-      for (Node node : definitions.getRootNodes(c)) {
-        if (node == null) {
+      for (Node root : definitions.getRootNodes(c)) {
+        if (root instanceof Node.Invalid) {
           a.roots.add(invalid());
         } else {
-          new AttributeFactory(node).create().ifPresent(t -> a.roots.add(t));
+          if (options.shouldFilterRoot(root.task.name())) {
+            continue;
+          }
+          new AttributeFactory(root).create().ifPresent(t -> a.roots.add(t));
         }
       }
-    } catch (ConfigInvalidException | IOException e) {
+    } catch (ConfigInvalidException | IOException | StorageException e) {
       a.roots.add(invalid());
     }
 
     if (a.roots.isEmpty()) {
       return null;
     }
+    lastTaskPluginAttribute = a;
     return a;
   }
 
   protected class AttributeFactory {
     public Node node;
-    public MatchCache matchCache;
     protected Task task;
     protected TaskAttribute attribute;
 
     protected AttributeFactory(Node node) {
-      this(node, new MatchCache(predicateCache, node.getChangeData()));
-    }
-
-    protected AttributeFactory(Node node, MatchCache matchCache) {
       this.node = node;
-      this.matchCache = matchCache;
       this.task = node.task;
-      this.attribute = new TaskAttribute(task.name);
+      attribute = new TaskAttribute(task.name());
+      if (options.includeStatistics) {
+        statistics.numberOfNodes++;
+        if (node.isChange()) {
+          statistics.numberOfChangeNodes++;
+        }
+        if (node.isDuplicate) {
+          statistics.numberOfDuplicates++;
+        }
+        attribute.statistics = new TaskAttribute.Statistics();
+        attribute.statistics.properties = node.propertiesStatistics;
+      }
     }
 
     public Optional<TaskAttribute> create() {
@@ -134,20 +199,37 @@
           attribute.evaluationMilliSeconds = millis();
         }
 
-        boolean applicable = matchCache.match(task.applicable);
+        boolean applicable;
+        try {
+          applicable = node.match(task.applicable);
+        } catch (QueryParseException e) {
+          return Optional.of(invalid());
+        }
         if (!task.isVisible) {
-          if (!task.isTrusted || (!applicable && !options.onlyApplicable)) {
+          if (!node.isTrusted() || (!applicable && !options.onlyApplicable)) {
             return Optional.of(unknown());
           }
         }
 
         if (applicable || !options.onlyApplicable) {
-          attribute.hasPass = task.pass != null || task.fail != null;
-          attribute.subTasks = getSubTasks();
+          if (node.isChange()) {
+            attribute.change = node.getChangeData().getId().get();
+          }
+          attribute.hasPass = !node.isDuplicate && (task.pass != null || task.fail != null);
+          if (!node.isDuplicate) {
+            attribute.subTasks = getSubTasks();
+          }
           attribute.status = getStatus();
           if (options.onlyInvalid && !isValidQueries()) {
             attribute.status = Status.INVALID;
           }
+          if (options.includePaths) {
+            if (hasViewPathsCapability) {
+              attribute.path = taskPathFactory.create(node.taskKey);
+            } else {
+              attribute.path = MISSING_VIEW_PATH_CAPABILITY;
+            }
+          }
           boolean groupApplicable = attribute.status != null;
 
           if (groupApplicable || !options.onlyApplicable) {
@@ -157,26 +239,54 @@
               if (!options.onlyApplicable) {
                 attribute.applicable = applicable;
               }
-              if (task.inProgress != null) {
-                attribute.inProgress = matchCache.matchOrNull(task.inProgress);
+              if (!node.isDuplicate) {
+                if (task.inProgress != null) {
+                  attribute.inProgress = node.matchOrNull(task.inProgress);
+                }
+                attribute.exported = task.exported.isEmpty() ? null : task.exported;
               }
               attribute.hint = getHint(attribute.status, task);
-              attribute.exported = task.exported.isEmpty() ? null : task.exported;
 
               if (options.evaluationTime) {
                 attribute.evaluationMilliSeconds = millis() - attribute.evaluationMilliSeconds;
               }
+              addStatistics(attribute.statistics);
               return Optional.of(attribute);
             }
           }
         }
-      } catch (QueryParseException | RuntimeException e) {
+      } catch (IOException | RuntimeException | ConfigInvalidException e) {
         return Optional.of(invalid()); // bad applicability query
       }
       return Optional.empty();
     }
 
+    protected TaskAttribute invalid() {
+      TaskAttribute invalid = TaskAttributeFactory.invalid();
+      if (task.isVisible) {
+        invalid.name = task.name();
+      }
+      return invalid;
+    }
+
+    public void addStatistics(TaskAttribute.Statistics statistics) {
+      if (statistics != null) {
+        statistics.isApplicableRefreshRequired = node.properties.isApplicableRefreshRequired();
+        statistics.isSubNodeReloadRequired = node.properties.isSubNodeReloadRequired();
+        statistics.isTaskRefreshNeeded = node.properties.isTaskRefreshRequired();
+        if (!statistics.isSubNodeReloadRequired) {
+          statistics.hasUnfilterableSubNodes = node.hasUnfilterableSubNodes;
+        }
+        if (node.nodesByBranch != null) {
+          statistics.nodesByBranchCache = node.nodesByBranch.getStatistics();
+        }
+      }
+    }
+
     protected Status getStatusWithExceptions() throws StorageException, QueryParseException {
+      if (node.isDuplicate) {
+        return Status.DUPLICATE;
+      }
       if (isAllNull(task.pass, task.fail, attribute.subTasks)) {
         // A leaf def has no defined subdefs.
         boolean hasDefinedSubtasks =
@@ -198,7 +308,7 @@
       }
 
       if (task.fail != null) {
-        if (matchCache.match(task.fail)) {
+        if (node.match(task.fail)) {
           // A FAIL definition is meant to be a hard blocking criteria
           // (like a CodeReview -2).  Thus, if hard blocked, it is
           // irrelevant what the subtask states, or the PASS criteria are.
@@ -212,7 +322,8 @@
         }
       }
 
-      if (attribute.subTasks != null && !isAll(attribute.subTasks, Status.PASS)) {
+      if (attribute.subTasks != null
+          && !isAll(attribute.subTasks, EnumSet.of(Status.PASS, Status.DUPLICATE))) {
         // It is possible for a subtask's PASS criteria to change while
         // a parent task is executing, or even after the parent task
         // completes.  This can result in the parent PASS criteria being
@@ -224,7 +335,7 @@
         return Status.WAITING;
       }
 
-      if (task.pass != null && !matchCache.match(task.pass)) {
+      if (task.pass != null && !node.match(task.pass)) {
         // Non-leaf tasks with no PASS criteria are supported in order
         // to support "grouping tasks" (tasks with no function aside from
         // organizing tasks).  A task without a PASS criteria, cannot ever
@@ -245,13 +356,15 @@
       }
     }
 
-    protected List<TaskAttribute> getSubTasks() throws StorageException {
+    protected List<TaskAttribute> getSubTasks()
+        throws IOException, StorageException, ConfigInvalidException {
       List<TaskAttribute> subTasks = new ArrayList<>();
-      for (Node subNode : node.getSubNodes()) {
-        if (subNode == null) {
-          subTasks.add(invalid());
+      for (Node subNode :
+          options.onlyApplicable ? node.getApplicableSubNodes() : node.getSubNodes()) {
+        if (subNode instanceof Node.Invalid) {
+          subTasks.add(TaskAttributeFactory.invalid());
         } else {
-          new AttributeFactory(subNode, matchCache).create().ifPresent(t -> subTasks.add(t));
+          new AttributeFactory(subNode).create().ifPresent(t -> subTasks.add(t));
         }
       }
       if (subTasks.isEmpty()) {
@@ -262,9 +375,9 @@
 
     protected boolean isValidQueries() {
       try {
-        matchCache.match(task.inProgress);
-        matchCache.match(task.fail);
-        matchCache.match(task.pass);
+        node.match(task.inProgress);
+        node.match(task.fail);
+        node.match(task.pass);
         return true;
       } catch (QueryParseException | RuntimeException e) {
         return false;
@@ -276,7 +389,30 @@
     return System.nanoTime() / 1000000;
   }
 
-  protected TaskAttribute invalid() {
+  public void initStatistics() {
+    if (options.includeStatistics) {
+      statistics = new Statistics();
+      definitions.predicateCache.initStatistics(options.summaryCount);
+      definitions.matchCache.initStatistics(options.summaryCount);
+      definitions.preloader.initStatistics(options.summaryCount);
+      definitions.initStatistics(options.summaryCount);
+    }
+  }
+
+  public Statistics getStatistics(Map<Change.Id, PluginDefinedInfo> pluginInfosByChange) {
+    if (statistics != null) {
+      statistics.numberOfChanges = pluginInfosByChange.size();
+      statistics.numberOfTaskPluginAttributes =
+          pluginInfosByChange.values().stream().filter(tpa -> tpa != null).count();
+      statistics.predicateCache = definitions.predicateCache.getStatistics();
+      statistics.matchCache = definitions.matchCache.getStatistics();
+      statistics.preloader = definitions.preloader.getStatistics();
+      statistics.treeCaches = definitions.getStatistics();
+    }
+    return statistics;
+  }
+
+  protected static TaskAttribute invalid() {
     // For security reasons, do not expose the task name without knowing
     // the visibility which is derived from its applicability.
     TaskAttribute a = unknown();
@@ -284,17 +420,23 @@
     return a;
   }
 
-  protected TaskAttribute unknown() {
+  protected static TaskAttribute unknown() {
     TaskAttribute a = new TaskAttribute("UNKNOWN");
     a.status = Status.UNKNOWN;
     return a;
   }
 
-  protected String getHint(Status status, Task def) {
-    if (status == Status.READY) {
-      return def.readyHint;
-    } else if (status == Status.FAIL) {
-      return def.failHint;
+  protected static String getHint(Status status, Task def) {
+    if (status != null) {
+      switch (status) {
+        case READY:
+          return def.readyHint;
+        case FAIL:
+          return def.failHint;
+        case DUPLICATE:
+          return "Duplicate task is non blocking and empty to break the loop";
+        default:
+      }
     }
     return null;
   }
@@ -308,9 +450,9 @@
     return true;
   }
 
-  protected static boolean isAll(Iterable<TaskAttribute> atts, Status state) {
+  protected static boolean isAll(Iterable<TaskAttribute> atts, Set<Status> states) {
     for (TaskAttribute att : atts) {
-      if (att.status != state) {
+      if (!states.contains(att.status)) {
         return false;
       }
     }
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 5e987ca..53897b5 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
@@ -14,21 +14,20 @@
 
 package com.googlesource.gerrit.plugins.task;
 
-import com.google.common.primitives.Primitives;
 import com.google.gerrit.common.Container;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.server.git.meta.AbstractVersionedMetaData;
-import java.lang.reflect.Field;
+import com.googlesource.gerrit.plugins.task.util.Copier;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import org.eclipse.jgit.errors.ConfigInvalidException;
+import java.util.stream.Collectors;
 
 /** Task Configuration file living in git */
 public class TaskConfig extends AbstractVersionedMetaData {
@@ -44,16 +43,19 @@
     }
   }
 
-  protected class Section extends Container {
+  protected class SubSection extends Container {
     public TaskConfig config;
+    public final SubSectionKey subSection;
 
-    public Section() {
+    public SubSection(SubSectionKey s) {
       this.config = TaskConfig.this;
+      this.subSection = s;
     }
   }
 
-  public class TaskBase extends Section {
+  public class TaskBase extends SubSection {
     public String applicable;
+    public String duplicateKey;
     public Map<String, String> exported;
     public String fail;
     public String failHint;
@@ -62,18 +64,20 @@
     public String preloadTask;
     public Map<String, String> properties;
     public String readyHint;
-    public List<String> subTasks;
-    public List<String> subTasksExternals;
-    public List<String> subTasksFactories;
-    public List<String> subTasksFiles;
+    public List<ConfigSourcedValue> subTasks;
+    public List<ConfigSourcedValue> subTasksExternals;
+    public List<ConfigSourcedValue> subTasksFactories;
+    public List<ConfigSourcedValue> subTasksFiles;
 
     public boolean isVisible;
-    public boolean isTrusted;
+    public boolean isMasqueraded;
 
-    public TaskBase(SubSection s, boolean isVisible, boolean isTrusted) {
+    public TaskBase(SubSectionKey s, boolean isVisible, boolean isMasqueraded) {
+      super(s);
       this.isVisible = isVisible;
-      this.isTrusted = isTrusted;
+      this.isMasqueraded = isMasqueraded;
       applicable = getString(s, KEY_APPLICABLE, null);
+      duplicateKey = getString(s, KEY_DUPLICATE_KEY, null);
       exported = getProperties(s, KEY_EXPORT_PREFIX);
       fail = getString(s, KEY_FAIL, null);
       failHint = getString(s, KEY_FAIL_HINT, null);
@@ -82,60 +86,50 @@
       preloadTask = getString(s, KEY_PRELOAD_TASK, null);
       properties = getProperties(s, KEY_PROPERTIES_PREFIX);
       readyHint = getString(s, KEY_READY_HINT, null);
-      subTasks = getStringList(s, KEY_SUBTASK);
-      subTasksExternals = getStringList(s, KEY_SUBTASKS_EXTERNAL);
-      subTasksFactories = getStringList(s, KEY_SUBTASKS_FACTORY);
-      subTasksFiles = getStringList(s, KEY_SUBTASKS_FILE);
+      subTasks =
+          getStringList(s, KEY_SUBTASK).stream()
+              .map(subTask -> ConfigSourcedValue.create(s.file(), subTask))
+              .collect(Collectors.toList());
+      subTasksExternals =
+          getStringList(s, KEY_SUBTASKS_EXTERNAL).stream()
+              .map(subTask -> ConfigSourcedValue.create(s.file(), subTask))
+              .collect(Collectors.toList());
+      subTasksFactories =
+          getStringList(s, KEY_SUBTASKS_FACTORY).stream()
+              .map(subTask -> ConfigSourcedValue.create(s.file(), subTask))
+              .collect(Collectors.toList());
+      subTasksFiles =
+          getStringList(s, KEY_SUBTASKS_FILE).stream()
+              .map(fileName -> ConfigSourcedValue.create(s.file(), fileName))
+              .collect(Collectors.toList());
     }
 
     protected TaskBase(TaskBase base) {
-      copyDeclaredFields(TaskBase.class, base);
+      this(base.subSection);
+      Copier.shallowCopyDeclaredFields(TaskBase.class, base, this, false);
     }
 
-    protected <T> void copyDeclaredFields(Class<T> cls, T from) {
-      for (Field field : cls.getDeclaredFields()) {
-        try {
-          field.setAccessible(true);
-          Class<?> fieldCls = field.getType();
-          Object val = field.get(from);
-          if (field.getType().isPrimitive()
-              || Primitives.isWrapperType(fieldCls)
-              || (val instanceof String)
-              || val == null) {
-            field.set(this, val);
-          } else if (val instanceof List) {
-            List<?> list = List.class.cast(val);
-            field.set(this, new ArrayList<>(list));
-          } else if (val instanceof Map) {
-            Map<?, ?> map = Map.class.cast(val);
-            field.set(this, new HashMap<>(map));
-          } else if (field.getName().equals("this$0")) { // Don't copy internal final field
-          } else {
-            throw new RuntimeException(
-                "Don't know how to deep copy " + fieldValueToString(field, val));
-          }
-        } catch (IllegalAccessException e) {
-          throw new RuntimeException(
-              "Cannot access field to copy it " + fieldValueToString(field, "unknown"));
-        }
-      }
-    }
-
-    protected String fieldValueToString(Field field, Object val) {
-      return "field:" + field.getName() + " value:" + val + " type:" + field.getType();
+    protected TaskBase(SubSectionKey s) {
+      super(s);
     }
   }
 
-  public class Task extends TaskBase {
-    public String name;
+  public class Task extends TaskBase implements Cloneable {
+    public final TaskKey key;
 
-    public Task(SubSection s, boolean isVisible, boolean isTrusted) {
-      super(s, isVisible, isTrusted);
-      name = s.subSection;
+    public Task(SubSectionKey s, boolean isVisible, boolean isMasqueraded) {
+      super(s, isVisible, isMasqueraded);
+      key = TaskKey.create(s);
     }
 
-    protected Task(TaskBase base) {
-      super(base);
+    public Task(TasksFactory tasks, String name) {
+      super(tasks);
+      key = TaskKey.create(tasks.subSection, name);
+    }
+
+    public Task(SubSectionKey s) {
+      super(s);
+      key = TaskKey.create(s);
     }
 
     protected Map<String, String> getAllProperties() {
@@ -144,104 +138,103 @@
       return all;
     }
 
-    protected void setExpandedProperties(Map<String, String> expanded) {
-      properties.clear();
-      properties.putAll(expanded);
-      for (String property : exported.keySet()) {
-        exported.put(property, properties.get(property));
-      }
+    public String name() {
+      return key.task();
+    }
+
+    public FileKey file() {
+      return key.subSection().file();
+    }
+
+    public TaskKey key() {
+      return key;
     }
   }
 
   public class TasksFactory extends TaskBase {
     public String namesFactory;
 
-    public TasksFactory(SubSection s, boolean isVisible, boolean isTrusted) {
-      super(s, isVisible, isTrusted);
+    public TasksFactory(SubSectionKey s, boolean isVisible, boolean isMasqueraded) {
+      super(s, isVisible, isMasqueraded);
       namesFactory = getString(s, KEY_NAMES_FACTORY, null);
     }
   }
 
-  public class NamesFactory extends Section {
+  public class NamesFactory extends SubSection implements Cloneable {
     public String changes;
     public List<String> names;
     public String type;
 
-    public NamesFactory(SubSection s) {
+    public NamesFactory(SubSectionKey s) {
+      super(s);
       changes = getString(s, KEY_CHANGES, null);
       names = getStringList(s, KEY_NAME);
       type = getString(s, KEY_TYPE, null);
     }
   }
 
-  public class External extends Section {
+  public class External extends SubSection {
     public String name;
     public String file;
     public String user;
 
-    public External(SubSection s) {
-      name = s.subSection;
+    public External(SubSectionKey s) {
+      super(s);
+      name = s.subSection();
       file = getString(s, KEY_FILE, null);
       user = getString(s, KEY_USER, null);
     }
   }
 
-  protected static final Pattern OPTIONAL_TASK_PATTERN =
-      Pattern.compile("([^ |]*( *[^ |])*) *\\| *");
+  public static final String SEP = "\0";
 
-  protected static final String SECTION_EXTERNAL = "external";
-  protected static final String SECTION_NAMES_FACTORY = "names-factory";
-  protected static final String SECTION_ROOT = "root";
-  protected static final String SECTION_TASK = "task";
-  protected static final String SECTION_TASKS_FACTORY = "tasks-factory";
-  protected static final String KEY_APPLICABLE = "applicable";
-  protected static final String KEY_CHANGES = "changes";
-  protected static final String KEY_EXPORT_PREFIX = "export-";
-  protected static final String KEY_FAIL = "fail";
-  protected static final String KEY_FAIL_HINT = "fail-hint";
-  protected static final String KEY_FILE = "file";
-  protected static final String KEY_IN_PROGRESS = "in-progress";
-  protected static final String KEY_NAME = "name";
-  protected static final String KEY_NAMES_FACTORY = "names-factory";
-  protected static final String KEY_PASS = "pass";
-  protected static final String KEY_PRELOAD_TASK = "preload-task";
-  protected static final String KEY_PROPERTIES_PREFIX = "set-";
-  protected static final String KEY_READY_HINT = "ready-hint";
-  protected static final String KEY_SUBTASK = "subtask";
-  protected static final String KEY_SUBTASKS_EXTERNAL = "subtasks-external";
-  protected static final String KEY_SUBTASKS_FACTORY = "subtasks-factory";
-  protected static final String KEY_SUBTASKS_FILE = "subtasks-file";
-  protected static final String KEY_TYPE = "type";
-  protected static final String KEY_USER = "user";
+  public static final String SECTION_EXTERNAL = "external";
+  public static final String SECTION_NAMES_FACTORY = "names-factory";
+  public static final String SECTION_ROOT = "root";
+  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_CHANGES = "changes";
+  public static final String KEY_DUPLICATE_KEY = "duplicate-key";
+  public static final String KEY_EXPORT_PREFIX = "export-";
+  public static final String KEY_FAIL = "fail";
+  public static final String KEY_FAIL_HINT = "fail-hint";
+  public static final String KEY_FILE = "file";
+  public static final String KEY_IN_PROGRESS = "in-progress";
+  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_PRELOAD_TASK = "preload-task";
+  public static final String KEY_PROPERTIES_PREFIX = "set-";
+  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";
+  public static final String KEY_SUBTASKS_FACTORY = "subtasks-factory";
+  public static final String KEY_SUBTASKS_FILE = "subtasks-file";
+  public static final String KEY_TYPE = "type";
+  public static final String KEY_USER = "user";
 
+  protected final FileKey file;
   public boolean isVisible;
-  public boolean isTrusted;
+  public boolean isMasqueraded;
 
-  public Task createTask(TasksFactory tasks, String name) {
-    Task task = new Task(tasks);
-    task.name = name;
-    return task;
+  public TaskConfig(FileKey file, boolean isVisible, boolean isMasqueraded) {
+    this(file.branch(), file, isVisible, isMasqueraded);
   }
 
-  public TaskConfig(BranchNameKey branch, String fileName, boolean isVisible, boolean isTrusted) {
-    super(branch, fileName);
+  public TaskConfig(
+      BranchNameKey masqueraded, FileKey file, boolean isVisible, boolean isMasqueraded) {
+    super(masqueraded, file.file());
+    this.file = file;
     this.isVisible = isVisible;
-    this.isTrusted = isTrusted;
-  }
-
-  public List<Task> getRootTasks() {
-    return getTasks(SECTION_ROOT);
-  }
-
-  public List<Task> getTasks() {
-    return getTasks(SECTION_TASK);
+    this.isMasqueraded = isMasqueraded;
   }
 
   protected List<Task> getTasks(String type) {
     List<Task> tasks = new ArrayList<>();
     // No need to get a task with no name (what would we call it?)
     for (String task : cfg.getSubsections(type)) {
-      tasks.add(new Task(new SubSection(type, task), isVisible, isTrusted));
+      tasks.add(new Task(subSectionKey(type, task), isVisible, isMasqueraded));
     }
     return tasks;
   }
@@ -255,103 +248,77 @@
     return externals;
   }
 
-  /* returs null only if optional and not found */
-  public Task getTaskOptional(String name) throws ConfigInvalidException {
-    int end = 0;
-    Matcher m = OPTIONAL_TASK_PATTERN.matcher(name);
-    while (m.find()) {
-      end = m.end();
-      Task task = getTaskOrNull(m.group(1));
-      if (task != null) {
-        return task;
-      }
-    }
-
-    String last = name.substring(end);
-    if (!"".equals(last)) { // Last entry was not optional
-      Task task = getTaskOrNull(last);
-      if (task != null) {
-        return task;
-      }
-      throw new ConfigInvalidException("task not defined");
-    }
-    return null;
-  }
-
-  /* returns null if not found */
-  protected Task getTaskOrNull(String name) {
-    SubSection subSection = new SubSection(SECTION_TASK, name);
-    return getNames(subSection).isEmpty() ? null : new Task(subSection, isVisible, isTrusted);
+  protected Optional<Task> getOptionalTask(String name) {
+    SubSectionKey subSection = subSectionKey(SECTION_TASK, name);
+    return getNames(subSection).isEmpty()
+        ? Optional.empty()
+        : Optional.of(new Task(subSection, isVisible, isMasqueraded));
   }
 
   public TasksFactory getTasksFactory(String name) {
-    return new TasksFactory(new SubSection(SECTION_TASKS_FACTORY, name), isVisible, isTrusted);
+    return new TasksFactory(subSectionKey(SECTION_TASKS_FACTORY, name), isVisible, isMasqueraded);
   }
 
   public NamesFactory getNamesFactory(String name) {
-    return new NamesFactory(new SubSection(SECTION_NAMES_FACTORY, name));
+    return new NamesFactory(subSectionKey(SECTION_NAMES_FACTORY, name));
   }
 
   public External getExternal(String name) {
-    return getExternal(new SubSection(SECTION_EXTERNAL, name));
+    return getExternal(subSectionKey(SECTION_EXTERNAL, name));
   }
 
-  protected External getExternal(SubSection s) {
+  protected External getExternal(SubSectionKey s) {
     return new External(s);
   }
 
-  protected Map<String, String> getProperties(SubSection s, String prefix) {
+  protected Map<String, String> getProperties(SubSectionKey s, String prefix) {
     Map<String, String> valueByName = new HashMap<>();
     for (Map.Entry<String, String> e :
         getStringByName(s, getMatchingNames(s, prefix + ".+")).entrySet()) {
       String name = e.getKey();
       valueByName.put(name.substring(prefix.length()), e.getValue());
     }
-    return valueByName;
+    return Collections.unmodifiableMap(valueByName);
   }
 
-  protected Map<String, String> getStringByName(SubSection s, Iterable<String> names) {
+  protected Map<String, String> getStringByName(SubSectionKey s, Iterable<String> names) {
     Map<String, String> valueByName = new HashMap<>();
     for (String name : names) {
       valueByName.put(name, getString(s, name));
     }
-    return valueByName;
+    return Collections.unmodifiableMap(valueByName);
   }
 
-  protected Set<String> getMatchingNames(SubSection s, String match) {
+  protected Set<String> getMatchingNames(SubSectionKey s, String match) {
     Set<String> matched = new HashSet<>();
     for (String name : getNames(s)) {
       if (name.matches(match)) {
         matched.add(name);
       }
     }
-    return matched;
+    return Collections.unmodifiableSet(matched);
   }
 
-  protected Set<String> getNames(SubSection s) {
-    return cfg.getNames(s.section, s.subSection);
+  protected Set<String> getNames(SubSectionKey s) {
+    return cfg.getNames(s.section(), s.subSection());
   }
 
-  protected String getString(SubSection s, String key, String def) {
+  protected String getString(SubSectionKey s, String key, String def) {
     String v = getString(s, key);
     return v != null ? v : def;
   }
 
-  protected String getString(SubSection s, String key) {
-    return cfg.getString(s.section, s.subSection, key);
+  protected String getString(SubSectionKey s, String key) {
+    return cfg.getString(s.section(), s.subSection(), key);
   }
 
-  protected List<String> getStringList(SubSection s, String key) {
-    return Arrays.asList(cfg.getStringList(s.section, s.subSection, key));
+  protected List<String> getStringList(SubSectionKey s, String key) {
+    List<String> stringList = Arrays.asList(cfg.getStringList(s.section(), s.subSection(), key));
+    stringList.replaceAll(str -> str == null ? "" : str);
+    return Collections.unmodifiableList(stringList);
   }
 
-  protected static class SubSection {
-    public final String section;
-    public final String subSection;
-
-    protected SubSection(String section, String subSection) {
-      this.section = section;
-      this.subSection = subSection;
-    }
+  protected SubSectionKey subSectionKey(String section, String subSection) {
+    return SubSectionKey.create(file, section, subSection);
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfigFactory.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfigCache.java
similarity index 70%
rename from src/main/java/com/googlesource/gerrit/plugins/task/TaskConfigFactory.java
rename to src/main/java/com/googlesource/gerrit/plugins/task/TaskConfigCache.java
index f790ccc..648c729 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfigFactory.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfigCache.java
@@ -17,9 +17,11 @@
 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;
+import com.google.gerrit.server.config.AllProjectsNameProvider;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -32,12 +34,9 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Repository;
 
-public class TaskConfigFactory {
+public class TaskConfigCache {
   private static final FluentLogger log = FluentLogger.forEnclosingClass();
 
-  protected static final String EXTENSION = ".config";
-  protected static final String DEFAULT = "task" + EXTENSION;
-
   protected final GitRepositoryManager gitMgr;
   protected final PermissionBackend permissionBackend;
 
@@ -45,21 +44,22 @@
   protected final AllProjectsName allProjects;
 
   protected final Map<BranchNameKey, PatchSetArgument> psaMasquerades = new HashMap<>();
+  protected final Map<FileKey, TaskConfig> taskCfgByFile = new HashMap<>();
 
   @Inject
-  protected TaskConfigFactory(
-      AllProjectsName allProjects,
+  protected TaskConfigCache(
+      AllProjectsNameProvider allProjectsNameProvider,
       GitRepositoryManager gitMgr,
       PermissionBackend permissionBackend,
       CurrentUser user) {
-    this.allProjects = allProjects;
+    this.allProjects = allProjectsNameProvider.get();
     this.gitMgr = gitMgr;
     this.permissionBackend = permissionBackend;
     this.user = user;
   }
 
   public TaskConfig getRootConfig() throws ConfigInvalidException, IOException {
-    return getTaskConfig(getRootBranch(), DEFAULT, true);
+    return getTaskConfig(FileKey.create(getRootBranch(), TaskFileConstants.TASK_CFG));
   }
 
   public void masquerade(PatchSetArgument psa) {
@@ -67,26 +67,39 @@
   }
 
   protected BranchNameKey getRootBranch() {
-    return BranchNameKey.create(allProjects, "refs/meta/config");
+    return BranchNameKey.create(allProjects, RefNames.REFS_CONFIG);
   }
 
-  public TaskConfig getTaskConfig(BranchNameKey branch, String fileName, boolean isTrusted)
-      throws ConfigInvalidException, IOException {
+  public TaskConfig getTaskConfig(FileKey key) throws ConfigInvalidException, IOException {
+    TaskConfig cfg = taskCfgByFile.get(key);
+    if (cfg == null) {
+      cfg = loadTaskConfig(key);
+      taskCfgByFile.put(key, cfg);
+    }
+    return cfg;
+  }
+
+  private TaskConfig loadTaskConfig(FileKey file) throws ConfigInvalidException, IOException {
+    BranchNameKey branch = file.branch();
     PatchSetArgument psa = psaMasquerades.get(branch);
     boolean visible = true; // invisible psas are filtered out by commandline
+    boolean isMasqueraded = false;
     if (psa == null) {
-      visible = canRead(branch);
+      visible = isVisible(branch);
     } else {
-      isTrusted = false;
+      isMasqueraded = true;
       branch = BranchNameKey.create(psa.change.getProject(), psa.patchSet.refName());
     }
 
-    Project.NameKey project = branch.project();
-    TaskConfig cfg = new TaskConfig(branch, fileName, visible, isTrusted);
+    Project.NameKey project = file.branch().project();
+    TaskConfig cfg =
+        isMasqueraded
+            ? new TaskConfig(branch, file, visible, isMasqueraded)
+            : new TaskConfig(file, visible, isMasqueraded);
     try (Repository git = gitMgr.openRepository(project)) {
       cfg.load(project, git);
     } catch (IOException e) {
-      log.atWarning().withCause(e).log("Failed to load %s for %s", fileName, project);
+      log.atWarning().withCause(e).log("Failed to load %s for %s", file.file(), project);
       throw e;
     } catch (ConfigInvalidException e) {
       throw e;
@@ -94,7 +107,7 @@
     return cfg;
   }
 
-  public boolean canRead(BranchNameKey branch) {
+  public boolean isVisible(BranchNameKey branch) {
     try {
       PermissionBackend.ForProject permissions =
           permissionBackend.user(user).project(branch.project());
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskExpression.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskExpression.java
new file mode 100644
index 0000000..5b9bef0
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskExpression.java
@@ -0,0 +1,106 @@
+// Copyright (C) 2021 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 com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+/**
+ * A TaskExpression represents a config string pointing to an expression which includes zero or more
+ * task references separated by a '|', and potentially termintated by a '|'. If the expression is
+ * not terminated by a '|' it indicates that task resolution of at least one task is required. Task
+ * selection priority is from left to right. This can be expressed as:
+ *
+ * <pre>
+ * TASK_EXPR = TASK_REFERENCE [ WHITE_SPACE * '|' [ WHITE_SPACE * TASK_EXPR ] ]
+ * </pre>
+ *
+ * <a href="file:../../../../../../antlr4/com/googlesource/gerrit/plugins/task/TaskReference.g4">See
+ * this for Task Reference</a>
+ *
+ * <p>Example expressions to prioritized names and requirements:
+ *
+ * <ul>
+ *   <li>
+ *       <pre> "simple"            -> ("simple")                       required</pre>
+ *   <li>
+ *       <pre> "world | peace"     -> ("world", "peace")               required</pre>
+ *   <li>
+ *       <pre> "shadenfreud |"     -> ("shadenfreud")                  optional</pre>
+ *   <li>
+ *       <pre> "foo | bar |"       -> ("foo", "bar")                   optional</pre>
+ * </ul>
+ */
+public class TaskExpression implements Iterable<TaskKey> {
+  public interface Factory {
+    TaskExpression create(FileKey key, String expression);
+  }
+
+  protected static final Pattern EXPRESSION_PATTERN = Pattern.compile("([^ |]+[^|]*)(\\|)?");
+  protected final TaskExpressionKey key;
+  protected final TaskReference.Factory taskReferenceFactory;
+
+  @Inject
+  public TaskExpression(
+      TaskReference.Factory taskReferenceFactory,
+      @Assisted FileKey key,
+      @Assisted String expression) {
+    this.key = TaskExpressionKey.create(key, expression);
+    this.taskReferenceFactory = taskReferenceFactory;
+  }
+
+  @Override
+  public Iterator<TaskKey> iterator() {
+    return new Iterator<TaskKey>() {
+      Matcher m = EXPRESSION_PATTERN.matcher(key.expression());
+      Boolean hasNext;
+      boolean optional;
+
+      @Override
+      public boolean hasNext() {
+        if (hasNext == null) {
+          hasNext = m.find();
+          if (hasNext) {
+            optional = m.group(2) != null;
+          }
+        }
+        if (!hasNext && !optional) {
+          return true; // fake it so next() throws an Exception
+        }
+        return hasNext;
+      }
+
+      @Override
+      public TaskKey next() {
+        // Can't use @SuppressWarnings("ReturnValueIgnored") on method call
+        boolean ignored = hasNext(); // in case next() was (re)called w/o calling hasNext()
+        if (!hasNext) {
+          throw new NoSuchElementException("No more names, yet expression was not optional");
+        }
+        hasNext = null;
+        try {
+          return taskReferenceFactory.create(key.file(), m.group(1)).getTaskKey();
+        } catch (ConfigInvalidException e) {
+          throw new RuntimeConfigInvalidException(e);
+        }
+      }
+    };
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskExpressionKey.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskExpressionKey.java
new file mode 100644
index 0000000..6fcd30d
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskExpressionKey.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2022 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 com.google.auto.value.AutoValue;
+import com.google.gerrit.entities.BranchNameKey;
+
+/** A key for TaskExpression. */
+@AutoValue
+public abstract class TaskExpressionKey {
+  public static TaskExpressionKey create(FileKey file, String expression) {
+    return new AutoValue_TaskExpressionKey(file, expression);
+  }
+
+  public BranchNameKey branch() {
+    return file().branch();
+  }
+
+  public abstract FileKey file();
+
+  public abstract String expression();
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskFileConstants.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskFileConstants.java
new file mode 100644
index 0000000..904c933
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskFileConstants.java
@@ -0,0 +1,20 @@
+// Copyright (C) 2022 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;
+
+public final class TaskFileConstants {
+  public static final String TASK_DIR = "task";
+  public static final String TASK_CFG = "task.config";
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskKey.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskKey.java
new file mode 100644
index 0000000..bd0b683
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskKey.java
@@ -0,0 +1,204 @@
+// Copyright (C) 2021 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 com.google.auto.value.AutoValue;
+import com.google.common.base.Preconditions;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AllUsersName;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+/** An immutable reference to a task in task config file. */
+@AutoValue
+public abstract class TaskKey {
+  protected static final String CONFIG_SECTION = "task";
+  protected static final String CONFIG_TASKS_FACTORY = "tasks-factory";
+
+  /** Creates a TaskKey with task name as the name of sub section. */
+  public static TaskKey create(SubSectionKey section) {
+    return create(section, section.subSection());
+  }
+
+  /** Creates a TaskKey with given FileKey and task name and sub section's name as 'task'. */
+  public static TaskKey create(FileKey file, String task) {
+    return create(SubSectionKey.create(file, CONFIG_SECTION, task));
+  }
+
+  /** Creates a TaskKey from a sub section and task name, generally used by TasksFactory. */
+  public static TaskKey create(SubSectionKey section, String task) {
+    return new AutoValue_TaskKey(section, task);
+  }
+
+  public BranchNameKey branch() {
+    return subSection().file().branch();
+  }
+
+  public abstract SubSectionKey subSection();
+
+  public abstract String task();
+
+  public boolean isTasksFactoryGenerated() {
+    return subSection().section().equals(CONFIG_TASKS_FACTORY);
+  }
+
+  public static class Builder {
+    protected final AccountCache accountCache;
+    protected final AllProjectsName allProjectsName;
+    protected final AllUsersName allUsersName;
+    protected final FileKey relativeTo;
+    protected BranchNameKey branch;
+    protected String file;
+    protected String task;
+    protected GroupCache groupCache;
+
+    Builder(
+        FileKey relativeTo,
+        AllProjectsName allProjectsName,
+        AllUsersName allUsersName,
+        AccountCache accountCache,
+        GroupCache groupCache) {
+      this.relativeTo = relativeTo;
+      this.allProjectsName = allProjectsName;
+      this.allUsersName = allUsersName;
+      this.accountCache = accountCache;
+      this.groupCache = groupCache;
+    }
+
+    public TaskKey buildTaskKey() {
+      return isReferencingAnotherRef() ? getAnotherRefTask() : getSameRefTask();
+    }
+
+    protected TaskKey getAnotherRefTask() {
+      return TaskKey.create(
+          isReferencingRootFile()
+              ? FileKey.create(branch, TaskFileConstants.TASK_CFG)
+              : FileKey.create(branch, file),
+          task);
+    }
+
+    protected TaskKey getSameRefTask() {
+      return TaskKey.create(
+          isRelativePath() ? relativeTo : FileKey.create(relativeTo.branch(), file), task);
+    }
+
+    public void setAbsolute() {
+      file = TaskFileConstants.TASK_DIR;
+    }
+
+    public void setPath(Path path) throws ConfigInvalidException {
+      Path parentDir = Paths.get(relativeTo.file()).getParent();
+      if (parentDir == null) {
+        parentDir = Paths.get(TaskFileConstants.TASK_DIR);
+      }
+
+      file =
+          isRelativePath()
+              ? parentDir.resolve(path).toString()
+              : Paths.get(file).resolve(path).toString();
+      throwIfInvalidPath();
+    }
+
+    public void setRefRootFile() throws ConfigInvalidException {
+      Preconditions.checkState(!isFileAlreadySet());
+      file = TaskFileConstants.TASK_CFG;
+    }
+
+    public void setTaskName(String task) {
+      this.task = task;
+    }
+
+    public void setUsername(String username) throws ConfigInvalidException {
+      branch =
+          BranchNameKey.create(
+              allUsersName,
+              RefNames.refsUsers(
+                  accountCache
+                      .getByUsername(username)
+                      .orElseThrow(
+                          () -> new ConfigInvalidException("Cannot resolve username: " + username))
+                      .account()
+                      .id()));
+    }
+
+    public void setGroupName(String groupName) throws ConfigInvalidException {
+      branch =
+          BranchNameKey.create(
+              allUsersName,
+              RefNames.refsGroups(
+                  groupCache
+                      .get(AccountGroup.nameKey(groupName))
+                      .orElseThrow(
+                          () ->
+                              new ConfigInvalidException(
+                                  String.format("Cannot resolve group name: %s", groupName)))
+                      .getGroupUUID()));
+    }
+
+    public void setGroupUUID(String uuid) throws ConfigInvalidException {
+      branch =
+          BranchNameKey.create(
+              allUsersName,
+              RefNames.refsGroups(
+                  groupCache
+                      .get(AccountGroup.uuid(uuid))
+                      .orElseThrow(
+                          () ->
+                              new ConfigInvalidException(
+                                  String.format("Cannot resolve group uuid: %s", uuid)))
+                      .getGroupUUID()));
+    }
+
+    public void setReferringAllProjectsTask() {
+      branch = BranchNameKey.create(allProjectsName, RefNames.REFS_CONFIG);
+    }
+
+    protected void throwIfInvalidPath() throws ConfigInvalidException {
+      Path path = Paths.get(file);
+      if (!path.startsWith(TaskFileConstants.TASK_DIR)
+          && !path.equals(Paths.get(TaskFileConstants.TASK_CFG))) {
+        throw new ConfigInvalidException(
+            "Invalid config location, path should be "
+                + TaskFileConstants.TASK_CFG
+                + " or under "
+                + TaskFileConstants.TASK_DIR
+                + " directory");
+      }
+    }
+
+    /** Returns true when the path implies relative or same file. */
+    protected boolean isRelativePath() {
+      return file == null;
+    }
+
+    protected boolean isFileAlreadySet() {
+      return file != null;
+    }
+
+    protected boolean isReferencingRootFile() {
+      return file == null;
+    }
+
+    protected boolean isReferencingAnotherRef() {
+      return branch != null;
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskPath.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskPath.java
new file mode 100644
index 0000000..c0c5d8c
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskPath.java
@@ -0,0 +1,76 @@
+// Copyright (C) 2022 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 com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.account.Accounts;
+import com.google.gerrit.server.config.AllUsersNameProvider;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
+import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+public class TaskPath {
+  public interface Factory {
+    TaskPath create(TaskKey key);
+  }
+
+  protected String name;
+  protected String type;
+  protected String tasksFactory;
+  protected String user;
+  protected String project;
+  protected String ref;
+  protected String file;
+  protected String error;
+
+  @Inject
+  public TaskPath(AllUsersNameProvider allUsers, Accounts accounts, @Assisted TaskKey key) {
+    name = key.task();
+    type = key.subSection().section();
+    tasksFactory = key.isTasksFactoryGenerated() ? key.subSection().subSection() : null;
+    user = getUserOrNull(accounts, allUsers.get(), key);
+    project = key.branch().project().get();
+    ref = key.branch().branch();
+    file = key.subSection().file().file();
+  }
+
+  public TaskPath(String error) {
+    this.error = error;
+  }
+
+  private String getUserOrNull(Accounts accounts, Project.NameKey allUsers, TaskKey key) {
+    try {
+      if (allUsers.get().equals(key.branch().project().get())) {
+        String ref = key.branch().branch();
+        Account.Id id = Account.Id.fromRef(ref);
+        if (id != null) {
+          Optional<AccountState> state = accounts.get(id);
+          if (state.isPresent()) {
+            Optional<String> userName = state.get().userName();
+            if (userName.isPresent()) {
+              return userName.get();
+            }
+          }
+        }
+      }
+    } catch (ConfigInvalidException | IOException e) {
+    }
+    return null;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskReference.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskReference.java
new file mode 100644
index 0000000..a7824ac
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskReference.java
@@ -0,0 +1,180 @@
+// Copyright (C) 2020 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 com.google.common.annotations.VisibleForTesting;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.config.AllProjectsNameProvider;
+import com.google.gerrit.server.config.AllUsersNameProvider;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.nio.file.Paths;
+import java.util.NoSuchElementException;
+import org.antlr.v4.runtime.BaseErrorListener;
+import org.antlr.v4.runtime.CharStreams;
+import org.antlr.v4.runtime.CommonTokenStream;
+import org.antlr.v4.runtime.Lexer;
+import org.antlr.v4.runtime.RecognitionException;
+import org.antlr.v4.runtime.Recognizer;
+import org.antlr.v4.runtime.tree.ParseTree;
+import org.antlr.v4.runtime.tree.ParseTreeWalker;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+/** This class is used by TaskExpression to decode the task from task reference. */
+public class TaskReference {
+  protected final String reference;
+  protected final TaskKey.Builder taskKeyBuilder;
+
+  interface Factory {
+    TaskReference create(FileKey relativeTo, String reference);
+  }
+
+  @Inject
+  public TaskReference(
+      AllProjectsNameProvider allProjectsNameProvider,
+      AllUsersNameProvider allUsersNameProvider,
+      AccountCache accountCache,
+      GroupCache groupCache,
+      @Assisted FileKey relativeTo,
+      @Assisted String reference) {
+    this(
+        new TaskKey.Builder(
+            relativeTo,
+            allProjectsNameProvider.get(),
+            allUsersNameProvider.get(),
+            accountCache,
+            groupCache),
+        reference);
+  }
+
+  @VisibleForTesting
+  public TaskReference(TaskKey.Builder taskKeyBuilder, String reference) {
+    this.taskKeyBuilder = taskKeyBuilder;
+    this.reference = reference.trim();
+    if (reference.isEmpty()) {
+      throw new NoSuchElementException();
+    }
+  }
+
+  public TaskKey getTaskKey() throws ConfigInvalidException {
+    ParseTreeWalker walker = new ParseTreeWalker();
+    try {
+      walker.walk(new TaskReferenceListener(taskKeyBuilder), parse());
+    } catch (RuntimeConfigInvalidException e) {
+      throw e.checkedException;
+    }
+    return taskKeyBuilder.buildTaskKey();
+  }
+
+  protected ParseTree parse() {
+    Lexer lexer = new TaskReferenceLexer(CharStreams.fromString(reference));
+    lexer.removeErrorListeners();
+    lexer.addErrorListener(TaskReferenceErrorListener.INSTANCE);
+    return new TaskReferenceParser(new CommonTokenStream(lexer)).reference();
+  }
+
+  protected static class TaskReferenceErrorListener extends BaseErrorListener {
+    protected static final TaskReferenceErrorListener INSTANCE = new TaskReferenceErrorListener();
+
+    @Override
+    public void syntaxError(
+        Recognizer<?, ?> recognizer,
+        Object offendingSymbol,
+        int line,
+        int charPositionInLine,
+        String msg,
+        RecognitionException e) {
+      throw new NoSuchElementException();
+    }
+  }
+
+  protected class TaskReferenceListener extends TaskReferenceBaseListener {
+    TaskKey.Builder builder;
+
+    TaskReferenceListener(TaskKey.Builder builder) {
+      this.builder = builder;
+    }
+
+    @Override
+    public void enterAbsolute(TaskReferenceParser.AbsoluteContext ctx) {
+      builder.setAbsolute();
+    }
+
+    @Override
+    public void enterRelative(TaskReferenceParser.RelativeContext ctx) {
+      try {
+        builder.setPath(
+            ctx.dir().stream()
+                .map(dir -> Paths.get(dir.NAME().getText()))
+                .reduce(Paths.get(""), (a, b) -> a.resolve(b))
+                .resolve(ctx.NAME().getText()));
+      } catch (ConfigInvalidException e) {
+        throw new RuntimeConfigInvalidException(e);
+      }
+    }
+
+    @Override
+    public void enterReference(TaskReferenceParser.ReferenceContext ctx) {
+      builder.setTaskName(ctx.TASK().getText());
+    }
+
+    @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.absolute() == null && ctx.relative() == null) {
+        try {
+          builder.setRefRootFile();
+        } catch (ConfigInvalidException e) {
+          throw new RuntimeConfigInvalidException(e);
+        }
+      }
+    }
+
+    @Override
+    public void enterUser(TaskReferenceParser.UserContext ctx) {
+      try {
+        builder.setUsername(ctx.NAME().getText());
+      } catch (ConfigInvalidException e) {
+        throw new RuntimeConfigInvalidException(e);
+      }
+    }
+
+    @Override
+    public void enterGroup_name(TaskReferenceParser.Group_nameContext ctx) {
+      try {
+        String groupName =
+            ctx.NAME() == null
+                ? (ctx.NAME_WITH_SPACES() == null ? "" : ctx.NAME_WITH_SPACES().getText())
+                : ctx.NAME().getText();
+        builder.setGroupName(groupName);
+      } catch (ConfigInvalidException e) {
+        throw new RuntimeConfigInvalidException(e);
+      }
+    }
+
+    @Override
+    public void enterGroup_uuid(TaskReferenceParser.Group_uuidContext ctx) {
+      try {
+        builder.setGroupUUID(ctx.INTERNAL_GROUP_UUID().getText());
+      } catch (ConfigInvalidException e) {
+        throw new RuntimeConfigInvalidException(e);
+      }
+    }
+  }
+}
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 7079918..5586774 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
@@ -14,6 +14,8 @@
 
 package com.googlesource.gerrit.plugins.task;
 
+import static java.util.stream.Collectors.toList;
+
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.BranchNameKey;
@@ -30,22 +32,33 @@
 import com.google.gerrit.server.query.change.ChangeQueryProcessor;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.assistedinject.Assisted;
 import com.googlesource.gerrit.plugins.task.TaskConfig.External;
 import com.googlesource.gerrit.plugins.task.TaskConfig.NamesFactory;
 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.cli.PatchSetArgument;
+import com.googlesource.gerrit.plugins.task.properties.Properties;
+import com.googlesource.gerrit.plugins.task.statistics.HitHashMap;
+import com.googlesource.gerrit.plugins.task.statistics.HitHashMapOfCollection;
+import com.googlesource.gerrit.plugins.task.statistics.StatisticsMap;
+import com.googlesource.gerrit.plugins.task.statistics.StopWatch;
+import com.googlesource.gerrit.plugins.task.statistics.TracksStatistics;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
-import java.util.function.BiFunction;
 import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.util.StringUtils;
 
 /**
  * Add structure to access the task definitions from the config as a tree.
@@ -55,17 +68,46 @@
  */
 public class TaskTree {
   private static final FluentLogger log = FluentLogger.forEnclosingClass();
+
+  public interface Factory {
+    TaskTree create(@Assisted TaskConfigCache taskConfigCache);
+  }
+
+  @FunctionalInterface
+  public interface NodeFactory {
+    Node create(NodeList parent, Task definition) throws Exception;
+  }
+
+  public static class Statistics {
+    public Object definitionsPerSubSectionCache;
+    public Object definitionsByBranchBySubSectionCache;
+    public Object changesByNamesFactoryQueryCache;
+    public Properties.Statistics properties;
+    public transient int summaryCount;
+  }
+
   protected static final String TASK_DIR = "task";
 
   protected final AccountResolver accountResolver;
   protected final AllUsersNameProvider allUsers;
   protected final CurrentUser user;
-  protected final TaskConfigFactory taskFactory;
-  protected final Root root = new Root();
+  protected final PredicateCache predicateCache;
+  protected final MatchCache matchCache;
+  protected final Preloader preloader;
+  protected final TaskConfigCache taskConfigCache;
+  protected final TaskExpression.Factory taskExpressionFactory;
+  protected final NodeList root = new NodeList();
   protected final Provider<ChangeQueryBuilder> changeQueryBuilderProvider;
   protected final Provider<ChangeQueryProcessor> changeQueryProcessorProvider;
+  protected final StatisticsMap<String, List<ChangeData>> changesByNamesFactoryQuery =
+      new HitHashMap<>();
+  protected final StatisticsMap<SubSectionKey, List<Task>> definitionsBySubSection =
+      new HitHashMapOfCollection<>();
+  protected final StatisticsMap<SubSectionKey, Map<BranchNameKey, List<Task>>>
+      definitionsByBranchBySubSection = new HitHashMap<>();
 
   protected ChangeData changeData;
+  protected Statistics statistics;
 
   @Inject
   public TaskTree(
@@ -73,266 +115,562 @@
       AllUsersNameProvider allUsers,
       AnonymousUser anonymousUser,
       CurrentUser user,
-      TaskConfigFactory taskFactory,
       Provider<ChangeQueryBuilder> changeQueryBuilderProvider,
-      Provider<ChangeQueryProcessor> changeQueryProcessorProvider) {
+      Provider<ChangeQueryProcessor> changeQueryProcessorProvider,
+      PredicateCache predicateCache,
+      TaskExpression.Factory taskExpressionFactory,
+      Preloader.Factory preloaderFactory,
+      @Assisted TaskConfigCache taskConfigCache) {
     this.accountResolver = accountResolver;
     this.allUsers = allUsers;
     this.user = user != null ? user : anonymousUser;
-    this.taskFactory = taskFactory;
     this.changeQueryProcessorProvider = changeQueryProcessorProvider;
     this.changeQueryBuilderProvider = changeQueryBuilderProvider;
+    this.predicateCache = predicateCache;
+    this.matchCache = new MatchCache(predicateCache);
+    this.taskConfigCache = taskConfigCache;
+    this.taskExpressionFactory = taskExpressionFactory;
+    this.preloader = preloaderFactory.create(taskConfigCache);
   }
 
-  public void masquerade(PatchSetArgument psa) {
-    taskFactory.masquerade(psa);
-  }
-
-  public List<Node> getRootNodes(ChangeData changeData) throws ConfigInvalidException, IOException {
+  public List<Node> getRootNodes(ChangeData changeData)
+      throws ConfigInvalidException, IOException, StorageException {
     this.changeData = changeData;
-    return root.getRootNodes();
-  }
-
-  public Node createNodeOrNull(NodeList parent, Task definition) {
-    try {
-      return new Node(parent, definition);
-    } catch (Exception e) {
-      return null;
-    }
+    root.path = Collections.emptyList();
+    root.duplicateKeys = Collections.emptyList();
+    return root.getSubNodes();
   }
 
   protected class NodeList {
     protected NodeList parent = null;
-    protected LinkedList<String> path = new LinkedList<>();
-    protected List<Node> nodes;
-    protected Set<String> names = new HashSet<>();
+    protected Collection<String> path;
+    protected Collection<String> duplicateKeys;
+    protected Map<TaskKey, Node> cachedNodeByTask = new HashMap<>();
+    protected List<Node> cachedNodes;
 
-    protected void addSubDefinitions(List<Task> defs) {
-      for (Task def : defs) {
-        addSubDefinition(def);
+    protected List<Node> getSubNodes()
+        throws ConfigInvalidException, IOException, StorageException {
+      if (cachedNodes != null) {
+        return refresh(cachedNodes);
       }
+      return cachedNodes = loadSubNodes();
     }
 
-    protected void addSubDefinition(Task def) {
-      addSubDefinition(def, (d, c) -> createNodeOrNull(d, c));
-    }
-
-    protected void addSubDefinition(Task def, BiFunction<NodeList, Task, Node> nodeConstructor) {
-      Node node = null;
-      if (def != null && !path.contains(def.name) && names.add(def.name)) {
-        // path check above detects looping definitions
-        // names check above detects duplicate subtasks
-        node = nodeConstructor.apply(this, def);
-      }
-      nodes.add(node);
+    protected List<Node> loadSubNodes()
+        throws ConfigInvalidException, IOException, StorageException {
+      return new SubNodeFactory().createFromPreloaded(preloader.getRootTasks());
     }
 
     public ChangeData getChangeData() {
-      return parent == null ? TaskTree.this.changeData : parent.getChangeData();
+      return TaskTree.this.changeData;
     }
 
-    protected Properties.Task getProperties() {
-      return Properties.Task.EMPTY_PARENT;
+    protected boolean isTrusted() {
+      return true;
     }
-  }
 
-  protected class Root extends NodeList {
-    public List<Node> getRootNodes() throws ConfigInvalidException, IOException {
-      if (nodes == null) {
-        nodes = new ArrayList<>();
-        addSubDefinitions(getRootDefinitions());
+    protected class SubNodeFactory {
+      protected Set<String> names = new HashSet<>();
+
+      public List<Node> createFromPreloaded(List<Task> defs) {
+        List<Node> nodes = new ArrayList<>();
+        for (Task def : defs) {
+          nodes.add(createFromPreloaded(def));
+        }
+        return nodes;
       }
-      return nodes;
-    }
 
-    protected List<Task> getRootDefinitions() throws ConfigInvalidException, IOException {
-      return taskFactory.getRootConfig().getRootTasks();
+      public Node createFromPreloaded(Task def) {
+        return createFromPreloaded(def, (parent, definition) -> new Node(parent, definition));
+      }
+
+      public Node createFromPreloaded(Task def, ChangeData changeData) {
+        return createFromPreloaded(
+            def,
+            (parent, definition) ->
+                new Node(parent, definition) {
+                  @Override
+                  public ChangeData getChangeData() {
+                    return changeData;
+                  }
+
+                  @Override
+                  public boolean isChange() {
+                    return true;
+                  }
+                });
+      }
+
+      protected Node createFromPreloaded(Task def, NodeFactory nodeFactory) {
+        if (def != null) {
+          try {
+            Node node = cachedNodeByTask.get(def.key());
+            boolean isRefreshNeeded = node != null;
+            if (node == null) {
+              node = nodeFactory.create(NodeList.this, def);
+            }
+
+            if (names.add(def.name())) {
+              // names check above detects duplicate subtasks
+              if (isRefreshNeeded) {
+                node.refreshTask();
+              }
+              return node;
+            }
+          } catch (Exception e) {
+          }
+        }
+        return createInvalid();
+      }
+
+      protected Node createInvalid() {
+        return new Node().new Invalid();
+      }
     }
   }
 
   public class Node extends NodeList {
-    public final Task task;
-    protected final Properties.Task properties;
+    public class Invalid extends Node {
+      @Override
+      public void refreshTask() {}
 
-    public Node(NodeList parent, Task definition) throws ConfigInvalidException, StorageException {
-      this.parent = parent;
-      this.task = definition;
-      this.path.addAll(parent.path);
-      this.path.add(definition.name);
-      Preloader.preload(definition);
-      properties = new Properties.Task(getChangeData(), definition, parent.getProperties());
+      @Override
+      public Task getDefinition() {
+        return null;
+      }
     }
 
-    public List<Node> getSubNodes() {
-      if (nodes == null) {
-        nodes = new ArrayList<>();
-        addSubDefinitions();
+    public Task task;
+    public boolean isDuplicate;
+
+    protected Properties.Statistics propertiesStatistics;
+    protected final Properties properties;
+    protected final TaskKey taskKey;
+    protected StatisticsMap<BranchNameKey, List<Node>> nodesByBranch;
+    protected boolean hasUnfilterableSubNodes = false;
+
+    protected Node() { // Only for Invalid
+      taskKey = null;
+      properties = null;
+    }
+
+    public Node(NodeList parent, Task task) {
+      this.parent = parent;
+      taskKey = task.key();
+      properties = new Properties(this, task);
+      refreshTask();
+    }
+
+    public String key() {
+      return String.valueOf(getChangeData().getId().get()) + TaskConfig.SEP + taskKey;
+    }
+
+    public List<Node> getSubNodes() throws IOException, StorageException, ConfigInvalidException {
+      if (cachedNodes != null) {
+        return refresh(cachedNodes);
+      }
+      List<Node> nodes = loadSubNodes();
+      if (!properties.isSubNodeReloadRequired()) {
+        if (!isChange()) {
+          return cachedNodes = nodes;
+        }
+        definitionsBySubSection.computeIfAbsentTimed(
+            task.key().subSection(),
+            k -> nodes.stream().map(n -> n.getDefinition()).collect(toList()),
+            task.isVisible);
+      } else {
+        hasUnfilterableSubNodes = true;
+        cachedNodeByTask.clear();
+        nodes.stream()
+            .filter(n -> !(n instanceof Invalid) && !n.isChange())
+            .forEach(n -> cachedNodeByTask.put(n.task.key(), n));
       }
       return nodes;
     }
 
-    protected void addSubDefinitions() throws StorageException {
-      addSubTaskDefinitions();
-      addSubTasksFactoryDefinitions();
-      addSubFileDefinitions();
-      addExternalDefinitions();
-    }
-
-    protected void addSubTaskDefinitions() {
-      for (String name : task.subTasks) {
-        try {
-          Task def = task.config.getTaskOptional(name);
-          if (def != null) {
-            addSubDefinition(def);
-          }
-        } catch (ConfigInvalidException e) {
-          addSubDefinition(null);
-        }
-      }
-    }
-
-    protected void addSubFileDefinitions() {
-      for (String file : task.subTasksFiles) {
-        try {
-          addSubDefinitions(getTaskDefinitions(task.config.getBranch(), file));
-        } catch (ConfigInvalidException | IOException e) {
-          addSubDefinition(null);
-        }
-      }
-    }
-
-    protected void addExternalDefinitions() throws StorageException {
-      for (String external : task.subTasksExternals) {
-        try {
-          External ext = task.config.getExternal(external);
-          if (ext == null) {
-            addSubDefinition(null);
-          } else {
-            addSubDefinitions(getTaskDefinitions(ext));
-          }
-        } catch (ConfigInvalidException | IOException e) {
-          addSubDefinition(null);
-        }
-      }
-    }
-
-    protected void addSubTasksFactoryDefinitions() throws StorageException {
-      for (String taskFactoryName : task.subTasksFactories) {
-        TasksFactory tasksFactory = task.config.getTasksFactory(taskFactoryName);
-        if (tasksFactory != null) {
-          NamesFactory namesFactory = task.config.getNamesFactory(tasksFactory.namesFactory);
-          if (namesFactory != null && namesFactory.type != null) {
-            new Properties.NamesFactory(namesFactory, getProperties());
-            switch (NamesFactoryType.getNamesFactoryType(namesFactory.type)) {
-              case STATIC:
-                addStaticTypeTasksDefinitions(tasksFactory, namesFactory);
-                continue;
-              case CHANGE:
-                addChangesTypeTaskDefinitions(tasksFactory, namesFactory);
-                continue;
-            }
-          }
-        }
-        addSubDefinition(null);
-      }
-    }
-
-    protected void addStaticTypeTasksDefinitions(
-        TasksFactory tasksFactory, NamesFactory namesFactory) {
-      for (String name : namesFactory.names) {
-        addSubDefinition(task.config.createTask(tasksFactory, name));
-      }
-    }
-
-    protected void addChangesTypeTaskDefinitions(
-        TasksFactory tasksFactory, NamesFactory namesFactory) {
-      try {
-        if (namesFactory.changes != null) {
-          List<ChangeData> changeDataList =
-              changeQueryProcessorProvider
-                  .get()
-                  .query(changeQueryBuilderProvider.get().parse(namesFactory.changes))
-                  .entities();
-          for (ChangeData changeData : changeDataList) {
-            addSubDefinition(
-                task.config.createTask(tasksFactory, changeData.getId().toString()),
-                new ChangeNodeFactory(changeData)::createChangeNodeOrNull);
-          }
-          return;
-        }
-      } catch (StorageException e) {
-        log.atSevere().withCause(e).log("Running changes query '%s' failed", namesFactory.changes);
-      } catch (QueryParseException e) {
-      }
-      addSubDefinition(null);
-    }
-
-    protected List<Task> getTaskDefinitions(External external)
-        throws ConfigInvalidException, IOException, StorageException {
-      return getTaskDefinitions(resolveUserBranch(external.user), external.file);
-    }
-
-    protected List<Task> getTaskDefinitions(BranchNameKey branch, String file)
-        throws ConfigInvalidException, IOException {
-      return taskFactory
-          .getTaskConfig(branch, resolveTaskFileName(file), task.isTrusted)
-          .getTasks();
+    public List<Node> getApplicableSubNodes()
+        throws IOException, StorageException, ConfigInvalidException {
+      return hasUnfilterableSubNodes ? getSubNodes() : new ApplicableNodeFilter().getSubNodes();
     }
 
     @Override
-    protected Properties.Task getProperties() {
-      return properties;
+    protected List<Node> loadSubNodes()
+        throws IOException, StorageException, ConfigInvalidException {
+      List<Task> cachedDefinitions = definitionsBySubSection.get(task.key().subSection());
+      if (cachedDefinitions != null) {
+        return new SubNodeFactory().createFromPreloaded(cachedDefinitions);
+      }
+      List<Node> nodes = new SubNodeAdder().getSubNodes();
+      properties.expansionComplete();
+      return nodes;
     }
 
-    protected String resolveTaskFileName(String file) throws ConfigInvalidException {
-      if (file == null) {
-        throw new ConfigInvalidException("External file not defined");
+    /* The task needs to be refreshed before a node is used, however
+    subNode refreshing can wait until they are fetched since they may
+    not be needed. */
+    public void refreshTask() {
+      this.path = new LinkedList<>(parent.path);
+      String key = key();
+      isDuplicate = path.contains(key);
+      path.add(key);
+
+      if (statistics != null) {
+        properties.setStatisticsConsumer(
+            s -> statistics.properties = (propertiesStatistics = s).sum(statistics.properties));
       }
-      Path p = Paths.get(TASK_DIR, file);
-      if (!p.startsWith(TASK_DIR)) {
-        throw new ConfigInvalidException("task file not under " + TASK_DIR + " directory: " + file);
+      this.task = properties.getTask(getChangeData());
+
+      this.duplicateKeys = new LinkedList<>(parent.duplicateKeys);
+      if (task.duplicateKey != null) {
+        isDuplicate |= duplicateKeys.contains(task.duplicateKey);
+        duplicateKeys.add(task.duplicateKey);
       }
-      return p.toString();
     }
 
-    protected BranchNameKey resolveUserBranch(String user)
-        throws ConfigInvalidException, IOException, StorageException {
-      if (user == null) {
-        throw new ConfigInvalidException("External user not defined");
+    public Properties getParentProperties() {
+      return (parent instanceof Node) ? ((Node) parent).properties : Properties.EMPTY;
+    }
+
+    @Override
+    protected boolean isTrusted() {
+      return parent.isTrusted() && !task.isMasqueraded;
+    }
+
+    @Override
+    public ChangeData getChangeData() {
+      return parent.getChangeData();
+    }
+
+    public Task getDefinition() {
+      return properties.isTaskRefreshRequired() ? properties.origTask : task;
+    }
+
+    public boolean isChange() {
+      return false;
+    }
+
+    public boolean match(String query) throws StorageException, QueryParseException {
+      return matchCache.match(getChangeData(), query, task.isVisible);
+    }
+
+    public Boolean matchOrNull(String query) {
+      return matchCache.matchOrNull(getChangeData(), query, task.isVisible);
+    }
+
+    protected class SubNodeAdder {
+      protected List<Node> nodes = new ArrayList<>();
+      protected SubNodeFactory factory = new SubNodeFactory();
+
+      public List<Node> getSubNodes() throws IOException, StorageException, ConfigInvalidException {
+        addSubTasks();
+        addSubTasksFactoryTasks();
+        addSubTasksFiles();
+        addSubTasksExternals();
+        return nodes;
       }
-      Account.Id acct;
-      try {
-        acct = accountResolver.resolve(user).asUnique().account().id();
-      } catch (UnprocessableEntityException e) {
-        throw new ConfigInvalidException("Cannot resolve user: " + user);
+
+      protected void addSubTasks() throws IOException, StorageException {
+        for (ConfigSourcedValue configSourcedValue : task.subTasks) {
+          try {
+            Optional<Task> def =
+                preloader.getOptionalTask(
+                    taskExpressionFactory.create(
+                        configSourcedValue.sourceFile(), configSourcedValue.value()));
+            if (def.isPresent()) {
+              addPreloaded(def.get());
+            }
+          } catch (ConfigInvalidException e) {
+            addInvalidNode();
+          }
+        }
       }
-      return BranchNameKey.create(allUsers.get(), RefNames.refsUsers(acct));
+
+      protected void addSubTasksFiles() {
+        for (ConfigSourcedValue configSourcedValue : task.subTasksFiles) {
+          try {
+            addPreloaded(
+                preloader.getTasks(
+                    FileKey.create(
+                        configSourcedValue.sourceFile().branch(),
+                        resolveTaskFileName(configSourcedValue.value()))));
+          } catch (ConfigInvalidException | IOException e) {
+            addInvalidNode();
+          }
+        }
+      }
+
+      protected void addSubTasksExternals() throws StorageException {
+        for (ConfigSourcedValue configSourcedValue : task.subTasksExternals) {
+          try {
+            External ext =
+                taskConfigCache
+                    .getTaskConfig(configSourcedValue.sourceFile())
+                    .getExternal(configSourcedValue.value());
+            if (ext == null) {
+              addInvalidNode();
+            } else {
+              addPreloaded(getPreloadedTasks(ext));
+            }
+          } catch (ConfigInvalidException | IOException e) {
+            addInvalidNode();
+          }
+        }
+      }
+
+      protected void addSubTasksFactoryTasks()
+          throws IOException, StorageException, ConfigInvalidException {
+        for (ConfigSourcedValue configSourcedValue : task.subTasksFactories) {
+          TasksFactory tasksFactory =
+              taskConfigCache
+                  .getTaskConfig(configSourcedValue.sourceFile())
+                  .getTasksFactory(configSourcedValue.value());
+          if (tasksFactory != null) {
+            NamesFactory namesFactory =
+                taskConfigCache
+                    .getTaskConfig(configSourcedValue.sourceFile())
+                    .getNamesFactory(tasksFactory.namesFactory);
+            if (namesFactory != null && namesFactory.type != null) {
+              namesFactory = properties.getNamesFactory(namesFactory);
+              switch (NamesFactoryType.getNamesFactoryType(namesFactory.type)) {
+                case STATIC:
+                  addStaticTypeTasks(tasksFactory, namesFactory);
+                  continue;
+                case CHANGE:
+                  addChangeTypeTasks(tasksFactory, namesFactory);
+                  continue;
+              }
+            }
+          }
+          addInvalidNode();
+        }
+      }
+
+      protected void addStaticTypeTasks(TasksFactory tasksFactory, NamesFactory namesFactory)
+          throws IOException, StorageException {
+        for (String name : namesFactory.names) {
+          if (StringUtils.isEmptyOrNull(name)) {
+            addInvalidNode();
+          } else {
+            try {
+              addPreloaded(preloader.preload(task.config.new Task(tasksFactory, name)));
+            } catch (ConfigInvalidException e) {
+              addInvalidNode();
+            }
+          }
+        }
+      }
+
+      protected void addChangeTypeTasks(TasksFactory tasksFactory, NamesFactory namesFactory)
+          throws IOException {
+        try {
+          if (namesFactory.changes != null) {
+            for (ChangeData changeData : query(namesFactory.changes, task.isVisible)) {
+              addPreloaded(
+                  preloader.preload(
+                      task.config.new Task(tasksFactory, changeData.getId().toString())),
+                  changeData);
+            }
+            return;
+          }
+        } catch (StorageException e) {
+          log.atSevere().withCause(e).log("Running changes query '%s' failed", namesFactory.changes);
+        } catch (QueryParseException | ConfigInvalidException e) {
+        }
+        addInvalidNode();
+      }
+
+      public void addPreloaded(List<Task> defs) {
+        nodes.addAll(factory.createFromPreloaded(defs));
+      }
+
+      public void addPreloaded(Task def, ChangeData changeData) {
+        nodes.add(factory.createFromPreloaded(def, changeData));
+      }
+
+      public void addPreloaded(Task def) {
+        nodes.add(factory.createFromPreloaded(def));
+      }
+
+      public void addInvalidNode() {
+        nodes.add(factory.createInvalid());
+      }
+
+      protected List<Task> getPreloadedTasks(External external)
+          throws ConfigInvalidException, IOException, StorageException {
+        return preloader.getTasks(
+            FileKey.create(resolveUserBranch(external.user), resolveTaskFileName(external.file)));
+      }
+    }
+
+    public class ApplicableNodeFilter {
+      protected BranchNameKey branch = getChangeData().change().getDest();
+      protected SubSectionKey subSection = task.key.subSection();
+      protected Map<BranchNameKey, List<Task>> definitionsByBranch =
+          definitionsByBranchBySubSection.get(subSection);
+
+      public ApplicableNodeFilter() throws StorageException {}
+
+      public List<Node> getSubNodes() throws IOException, StorageException, ConfigInvalidException {
+        if (nodesByBranch != null) {
+          List<Node> nodes = nodesByBranch.get(branch);
+          if (nodes != null) {
+            return refresh(nodes);
+          }
+        }
+        if (definitionsByBranch != null) {
+          List<Task> branchDefinitions = definitionsByBranch.get(branch);
+          if (branchDefinitions != null) {
+            return new SubNodeFactory().createFromPreloaded(branchDefinitions);
+          }
+        }
+        List<Node> nodes = Node.this.getSubNodes();
+        if (isChange()
+            && definitionsByBranch == null
+            && definitionsByBranchBySubSection.containsKey(subSection)) {
+          hasUnfilterableSubNodes = true;
+        }
+
+        if (!hasUnfilterableSubNodes && !nodes.isEmpty()) {
+          Optional<List<Node>> filterable = getOptionalApplicableForBranch(nodes);
+          if (filterable.isPresent()) {
+            if (!isChange()) {
+              if (nodesByBranch == null) {
+                nodesByBranch = initStatistics(new HitHashMapOfCollection<>());
+              }
+              nodesByBranch.put(branch, filterable.get());
+            } else {
+              if (definitionsByBranch == null) {
+                definitionsByBranch = initStatistics(new HitHashMap<>());
+                definitionsByBranchBySubSection.put(subSection, definitionsByBranch);
+              }
+              definitionsByBranch.put(
+                  branch,
+                  filterable.get().stream().map(node -> node.getDefinition()).collect(toList()));
+            }
+            return filterable.get();
+          }
+          hasUnfilterableSubNodes = true;
+          if (isChange()) {
+            definitionsByBranchBySubSection.put(subSection, null);
+          }
+        }
+        return nodes;
+      }
+
+      protected Optional<List<Node>> getOptionalApplicableForBranch(List<Node> nodes)
+          throws StorageException {
+        int filterable = 0;
+        List<Node> applicableNodes = new ArrayList<>();
+        for (Node node : nodes) {
+          if (node instanceof Invalid) {
+            filterable++;
+          } else if (isApplicableCacheableByBranch(node)) {
+            filterable++;
+            try {
+              if (!node.match(node.task.applicable)) {
+                // Correctness will not be affected if more nodes are added than necessary
+                // (i.e. if isApplicableCacheableByBranch() does not realize a Node is cacheable
+                // based on its Branch), but it is incorrect to filter out a Node now that could
+                // later be applicable when a property, other than its Change's destination, is
+                // altered.
+                continue;
+              }
+            } catch (QueryParseException e) {
+            }
+          }
+          applicableNodes.add(node);
+        }
+        // Simple heuristic to determine whether storing the filtered nodes is worth it. There
+        // is minor evidence to suggest that storing a large list actually hurts performance.
+        return (filterable > nodes.size() / 2) ? Optional.of(applicableNodes) : Optional.empty();
+      }
+
+      protected boolean isApplicableCacheableByBranch(Node node) {
+        String applicable = node.task.applicable;
+        if (node.properties.isApplicableRefreshRequired()) {
+          return false;
+        }
+        try {
+          return predicateCache.isCacheableByBranch(applicable, task.isVisible);
+        } catch (QueryParseException e) {
+          return false;
+        }
+      }
     }
   }
 
-  public class ChangeNodeFactory {
-    public class ChangeNode extends Node {
-      public ChangeNode(NodeList parent, Task definition) throws ConfigInvalidException {
-        super(parent, definition);
-      }
-
-      public ChangeData getChangeData() {
-        return ChangeNodeFactory.this.changeData;
-      }
+  protected String resolveTaskFileName(String file) throws ConfigInvalidException {
+    if (file == null) {
+      throw new ConfigInvalidException("External file not defined");
     }
-
-    protected ChangeData changeData;
-
-    public ChangeNodeFactory(ChangeData changeData) {
-      this.changeData = changeData;
+    Path p = Paths.get(TASK_DIR, file);
+    if (!p.startsWith(TASK_DIR)) {
+      throw new ConfigInvalidException("task file not under " + TASK_DIR + " directory: " + file);
     }
+    return p.toString();
+  }
 
-    public ChangeNode createChangeNodeOrNull(NodeList parent, Task definition) {
-      try {
-        return new ChangeNode(parent, definition);
-      } catch (Exception e) {
-        return null;
+  protected BranchNameKey resolveUserBranch(String user)
+      throws ConfigInvalidException, IOException, StorageException {
+    if (user == null) {
+      throw new ConfigInvalidException("External user not defined");
+    }
+    Account.Id acct;
+    try {
+      acct = accountResolver.resolve(user).asUnique().account().id();
+    } catch (UnprocessableEntityException e) {
+      throw new ConfigInvalidException("Cannot resolve user: " + user);
+    }
+    return BranchNameKey.create(allUsers.get(), RefNames.refsUsers(acct));
+  }
+
+  @SuppressWarnings("try")
+  public List<ChangeData> query(String query, boolean isVisible)
+      throws StorageException, QueryParseException {
+    List<ChangeData> changeDataList = changesByNamesFactoryQuery.get(query);
+    if (changeDataList == null) {
+      try (StopWatch stopWatch =
+          changesByNamesFactoryQuery.createLoadingStopWatch(query, isVisible)) {
+        changeDataList =
+            changeQueryProcessorProvider
+                .get()
+                .query(changeQueryBuilderProvider.get().parse(query))
+                .entities();
       }
+      changesByNamesFactoryQuery.put(query, changeDataList);
     }
+    return changeDataList;
+  }
+
+  public void initStatistics(int summaryCount) {
+    statistics = new Statistics();
+    statistics.summaryCount = summaryCount;
+    definitionsBySubSection.initStatistics(summaryCount);
+    definitionsByBranchBySubSection.initStatistics(summaryCount);
+    changesByNamesFactoryQuery.initStatistics(summaryCount);
+  }
+
+  protected <T extends TracksStatistics> T initStatistics(T tracker) {
+    if (statistics != null) {
+      tracker.initStatistics(statistics.summaryCount);
+    }
+    return tracker;
+  }
+
+  public Statistics getStatistics() {
+    if (statistics != null) {
+      statistics.definitionsPerSubSectionCache = definitionsBySubSection.getStatistics();
+      statistics.definitionsByBranchBySubSectionCache =
+          definitionsByBranchBySubSection.getStatistics();
+      statistics.changesByNamesFactoryQueryCache = changesByNamesFactoryQuery.getStatistics();
+    }
+    return statistics;
+  }
+
+  protected static List<Node> refresh(List<Node> nodes) {
+    for (Node node : nodes) {
+      node.refreshTask();
+    }
+    return nodes;
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/ViewPathsCapability.java b/src/main/java/com/googlesource/gerrit/plugins/task/ViewPathsCapability.java
new file mode 100644
index 0000000..1fd1e3a
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/ViewPathsCapability.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2022 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 com.google.gerrit.extensions.config.CapabilityDefinition;
+
+public class ViewPathsCapability extends CapabilityDefinition {
+  public static final String VIEW_PATHS = "viewTaskPaths";
+
+  @Override
+  public String getDescription() {
+    return "View Task Paths";
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/properties/AbstractExpander.java b/src/main/java/com/googlesource/gerrit/plugins/task/properties/AbstractExpander.java
new file mode 100644
index 0000000..ce781d9
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/properties/AbstractExpander.java
@@ -0,0 +1,189 @@
+// Copyright (C) 2022 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.properties;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/**
+ * Use to expand properties like ${property} in Strings into their values.
+ *
+ * <p>Given some property name/value associations like this:
+ *
+ * <p><code>
+ * "animal" -> "fox"
+ * "bar" -> "foo"
+ * "obstacle" -> "fence"
+ * </code>
+ *
+ * <p>a String like: <code>"The brown ${animal} jumped over the ${obstacle}."</code>
+ *
+ * <p>will expand to: <code>"The brown fox jumped over the fence."</code> This class is meant to be
+ * used as a building block for other full featured expanders and thus must be overriden to provide
+ * the name/value associations via the getValueForName() method.
+ */
+public abstract class AbstractExpander {
+  protected Consumer<Matcher.Statistics> statisticsConsumer;
+
+  protected final Map<Class<?>, Function<?, ?>> expanderByClass = new HashMap<>();
+
+  protected AbstractExpander() {
+    registerClassExpander(String.class, this::expandText);
+  }
+
+  public <T> void registerClassExpander(Class<? extends T> classType, Function<T, T> expander) {
+    expanderByClass.put(classType, expander);
+  }
+
+  public void setStatisticsConsumer(Consumer<Matcher.Statistics> statisticsConsumer) {
+    this.statisticsConsumer = statisticsConsumer;
+  }
+
+  /**
+   * Returns expanded object if property found in the Strings in the object's Fields (except the
+   * excluded ones). Returns same object if no expansions occurred.
+   */
+  public <C extends Cloneable> C expand(C object, Set<String> excludedFieldNames) {
+    return expand(new CopyOnWrite.CloneOnWrite<>(object), excludedFieldNames);
+  }
+
+  /**
+   * Returns expanded object if property found in the Strings in the object's Fields (except the
+   * excluded ones). Returns same object if no expansions occurred.
+   */
+  public <T> T expand(T object, Function<T, T> copier, Set<String> excludedFieldNames) {
+    return expand(new CopyOnWrite<>(object, copier), excludedFieldNames);
+  }
+
+  /**
+   * Returns expanded object if property found in the Strings in the object's Fields (except the
+   * excluded ones). Returns same object if no expansions occurred.
+   */
+  public <T> T expand(CopyOnWrite<T> cow, Set<String> excludedFieldNames) {
+    for (Field field : cow.getOriginal().getClass().getFields()) {
+      if (!excludedFieldNames.contains(field.getName())) {
+        expand(cow, field);
+      }
+    }
+    return cow.getForRead();
+  }
+
+  /**
+   * Returns expanded object if property found in the fieldName Field if it is a String, or in the
+   * List's Strings if it is a List. Returns same object if no expansions occurred.
+   */
+  public <T> T expand(CopyOnWrite<T> cow, String fieldName) {
+    try {
+      return expand(cow, cow.getOriginal().getClass().getField(fieldName));
+    } catch (NoSuchFieldException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  /**
+   * Returns expanded object if property found in the Field if it is a String, or in the List's
+   * Strings if it is a List. Returns same object if no expansions occurred.
+   */
+  public <T> T expand(CopyOnWrite<T> cow, Field field) {
+    try {
+      field.setAccessible(true);
+      Object o = field.get(cow.getOriginal());
+      if (o instanceof String) {
+        String expanded = expandText((String) o);
+        if (expanded != o) {
+          field.set(cow.getForWrite(), expanded);
+        }
+      } else if (o instanceof List) {
+        List<?> expanded = expand((List<?>) o);
+        if (expanded != o) {
+          field.set(cow.getForWrite(), expanded);
+        }
+      }
+    } catch (IllegalAccessException e) {
+      throw new RuntimeException(e);
+    }
+    return cow.getForRead();
+  }
+
+  /**
+   * Returns expanded unmodifiable List if property found. Returns same object if no expansions
+   * occurred.
+   */
+  public <T> List<T> expand(List<T> list) {
+    if (list != null) {
+      boolean hasProperty = false;
+      List<T> expandedList = new ArrayList<>(list.size());
+      for (T value : list) {
+        T expanded = expand(value);
+        hasProperty = hasProperty || value != expanded;
+        expandedList.add(expanded);
+      }
+      return hasProperty ? Collections.unmodifiableList(expandedList) : list;
+    }
+    return null;
+  }
+
+  /**
+   * Expand all properties (${property_name} -> property_value) in the given generic value. Returns
+   * same object if no expansions occurred.
+   */
+  public <T> T expand(T value) {
+    if (value == null) {
+      return null;
+    }
+    @SuppressWarnings("unchecked")
+    Function<T, T> expander =
+        (Function<T, T>) expanderByClass.getOrDefault(value.getClass(), Function.identity());
+    return expander.apply(value);
+  }
+
+  /**
+   * Expand all properties (${property_name} -> property_value) in the given text. Returns same
+   * object if no expansions occurred.
+   */
+  public String expandText(String text) {
+    if (text == null) {
+      return null;
+    }
+    Matcher m = new Matcher(text);
+    m.setStatisticsConsumer(statisticsConsumer);
+    if (!m.find()) {
+      return text;
+    }
+    StringBuffer out = new StringBuffer();
+    do {
+      m.appendValue(out, getValueForName(m.getName()));
+    } while (m.find());
+    m.appendTail(out);
+    return out.toString();
+  }
+
+  /**
+   * Get the replacement value for the property identified by name
+   *
+   * @param name of the property to get the replacement value for
+   * @return the replacement value. Since the expandText() method alwyas needs a String to replace
+   *     '${property-name}' reference with, even when the property does not exist, this will never
+   *     return null, instead it will returns the empty string if the property is not found.
+   */
+  protected abstract String getValueForName(String name);
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/properties/CopyOnWrite.java b/src/main/java/com/googlesource/gerrit/plugins/task/properties/CopyOnWrite.java
new file mode 100644
index 0000000..d1510fa
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/properties/CopyOnWrite.java
@@ -0,0 +1,103 @@
+// Copyright (C) 2021 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.properties;
+
+import com.googlesource.gerrit.plugins.task.statistics.StopWatch;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.LongConsumer;
+
+public class CopyOnWrite<T> {
+  public static class CloneOnWrite<C extends Cloneable> extends CopyOnWrite<C> {
+    public CloneOnWrite(C cloneable) {
+      super(cloneable, copier(cloneable));
+    }
+  }
+
+  public static <C extends Cloneable> Function<C, C> copier(C cloneable) {
+    return c -> clone(c);
+  }
+
+  @SuppressWarnings("unchecked")
+  public static <C extends Cloneable> C clone(C cloneable) {
+    try {
+      for (Class<?> cls = cloneable.getClass(); cls != null; cls = cls.getSuperclass()) {
+        Optional<Method> optional = getOptionalDeclaredMethod(cls, "clone");
+        if (optional.isPresent()) {
+          Method clone = optional.get();
+          clone.setAccessible(true);
+          return (C) cloneable.getClass().cast(clone.invoke(cloneable));
+        }
+      }
+      throw new RuntimeException("Cannot find clone() method");
+    } catch (SecurityException
+        | IllegalAccessException
+        | IllegalArgumentException
+        | InvocationTargetException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  /**
+   * A faster getDeclaredMethod() without exceptions. The original apparently does a linear search
+   * anyway, and it is significantly slower when it throws NoSuchMethodExceptions.
+   */
+  public static Optional<Method> getOptionalDeclaredMethod(
+      Class<?> cls, String name, Class<?>... parameterTypes) {
+    for (Method method : cls.getDeclaredMethods()) {
+      if (method.getName().equals(name)
+          && Arrays.equals(method.getParameterTypes(), parameterTypes)) {
+        return Optional.of(method);
+      }
+    }
+    return Optional.empty();
+  }
+
+  protected Function<T, T> copier;
+  protected StopWatch.Runner stopWatch = StopWatch.Runner.DISABLED;
+  protected T original;
+  protected T copy;
+
+  public CopyOnWrite(T original, Function<T, T> copier) {
+    this.original = original;
+    this.copier = copier;
+  }
+
+  protected void setNanosecondsConsumer(LongConsumer nanosConsumer) {
+    stopWatch = new StopWatch.Runner.Enabled().setNanosConsumer(nanosConsumer);
+  }
+
+  public T getOriginal() {
+    return original;
+  }
+
+  public T getForRead() {
+    return isCopy() ? copy : original;
+  }
+
+  public T getForWrite() {
+    if (!isCopy()) {
+      stopWatch.run(() -> copy = copier.apply(original));
+    }
+    return copy;
+  }
+
+  public boolean isCopy() {
+    return copy != null;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/properties/Expander.java b/src/main/java/com/googlesource/gerrit/plugins/task/properties/Expander.java
new file mode 100644
index 0000000..df817a1
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/properties/Expander.java
@@ -0,0 +1,89 @@
+// Copyright (C) 2022 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.properties;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+
+/**
+ * Use to expand properties whose values may contain other references to properties.
+ *
+ * <p>Using a recursive expansion approach makes order of evaluation unimportant as long as there
+ * are no looping definitions.
+ *
+ * <p>Given some property name/value asssociations defined like this:
+ *
+ * <p><code>
+ * valueByName.put("obstacle", "fence");
+ * valueByName.put("action", "jumped over the ${obstacle}");
+ * </code>
+ *
+ * <p>a String like: <code>"The brown fox ${action}."</code>
+ *
+ * <p>will expand to: <code>"The brown fox jumped over the fence."</code>
+ */
+public class Expander extends AbstractExpander {
+  protected final Function<String, String> loadingFunction;
+  protected final Map<String, String> valueByName = new HashMap<>();
+  protected final Set<String> expanding = new HashSet<>();
+
+  public Expander(Function<String, String> loadingFunction) {
+    this.loadingFunction = loadingFunction;
+  }
+
+  /**
+   * Expand all properties (${property_name} -> property_value) in the given text. Returns same
+   * object if no expansions occurred.
+   */
+  public Map<String, String> expand(Map<String, String> map) {
+    if (map != null) {
+      boolean hasProperty = false;
+      Map<String, String> expandedMap = new HashMap<>(map.size());
+      for (Map.Entry<String, String> e : map.entrySet()) {
+        String name = e.getKey();
+        String value = e.getValue();
+        String expanded = getValueForName(name);
+        hasProperty = hasProperty || value != expanded;
+        expandedMap.put(name, expanded);
+      }
+      return hasProperty ? Collections.unmodifiableMap(expandedMap) : map;
+    }
+    return null;
+  }
+
+  @Override
+  public String getValueForName(String name) {
+    String value = valueByName.get(name);
+    if (value != null) {
+      return value;
+    }
+    value = loadingFunction.apply(name);
+    if (value == null) {
+      value = "";
+    } else if (!value.isEmpty()) {
+      if (!expanding.add(name)) {
+        throw new RuntimeException("Looping property definitions.");
+      }
+      value = expandText(value);
+      expanding.remove(name);
+    }
+    valueByName.put(name, value);
+    return value;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/properties/Loader.java b/src/main/java/com/googlesource/gerrit/plugins/task/properties/Loader.java
new file mode 100644
index 0000000..05120b3
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/properties/Loader.java
@@ -0,0 +1,93 @@
+// Copyright (C) 2022 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.properties;
+
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.googlesource.gerrit.plugins.task.TaskConfig.Task;
+import java.util.function.Function;
+
+public class Loader {
+  protected final Task task;
+  protected final ChangeData changeData;
+  protected final Function<String, String> inherritedMapper;
+  protected Change change;
+  protected boolean isInheritedPropertyLoaded;
+
+  public Loader(Task task, ChangeData changeData, Function<String, String> inherritedMapper) {
+    this.task = task;
+    this.changeData = changeData;
+    this.inherritedMapper = inherritedMapper;
+  }
+
+  public boolean isNonTaskDefinedPropertyLoaded() {
+    return change != null || isInheritedPropertyLoaded;
+  }
+
+  public String load(String name) throws StorageException {
+    if (name.startsWith("_")) {
+      return internal(name);
+    }
+    String value = task.exported.get(name);
+    if (value == null) {
+      value = task.properties.get(name);
+      if (value == null) {
+        value = inherritedMapper.apply(name);
+        if (!value.isEmpty()) {
+          isInheritedPropertyLoaded = true;
+        }
+      }
+    }
+    return value;
+  }
+
+  protected String internal(String name) throws StorageException {
+    if ("_name".equals(name)) {
+      return task.name();
+    }
+    String changeProp = name.replace("_change_", "");
+    if (changeProp != name) {
+      return change(changeProp);
+    }
+    return "";
+  }
+
+  protected String change(String changeProp) throws StorageException {
+    switch (changeProp) {
+      case "number":
+        return String.valueOf(change().getId().get());
+      case "id":
+        return change().getKey().get();
+      case "project":
+        return change().getProject().get();
+      case "branch":
+        return change().getDest().branch();
+      case "status":
+        return change().getStatus().toString();
+      case "topic":
+        return change().getTopic();
+      default:
+        return "";
+    }
+  }
+
+  protected Change change() {
+    if (change == null) {
+      change = changeData.change();
+    }
+    return change;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/properties/Matcher.java b/src/main/java/com/googlesource/gerrit/plugins/task/properties/Matcher.java
new file mode 100644
index 0000000..68151fb
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/properties/Matcher.java
@@ -0,0 +1,103 @@
+// Copyright (C) 2022 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.properties;
+
+import com.googlesource.gerrit.plugins.task.statistics.StopWatch;
+import java.util.function.Consumer;
+
+/** A handcrafted properties Matcher which has an API similar to an RE Matcher, but is faster. */
+public class Matcher {
+  public static class Statistics {
+    public long appendNanoseconds;
+    public long findNanoseconds;
+
+    public Statistics sum(Statistics other) {
+      if (other == null) {
+        return this;
+      }
+      Statistics statistics = new Statistics();
+      statistics.appendNanoseconds = appendNanoseconds + other.appendNanoseconds;
+      statistics.findNanoseconds = findNanoseconds + other.findNanoseconds;
+      return statistics;
+    }
+  }
+
+  protected String text;
+  protected int start;
+  protected int nameStart;
+  protected int end;
+  protected int cursor;
+
+  protected Statistics statistics;
+  protected StopWatch.Runner appendNanoseconds = StopWatch.Runner.DISABLED;
+  protected StopWatch.Runner findNanoseconds = StopWatch.Runner.DISABLED;
+
+  public Matcher(String text) {
+    this.text = text;
+  }
+
+  protected void setStatisticsConsumer(Consumer<Statistics> statisticsConsumer) {
+    if (statisticsConsumer != null) {
+      statistics = new Statistics();
+      statisticsConsumer.accept(statistics);
+      appendNanoseconds =
+          new StopWatch.Runner.Enabled().setNanosConsumer(ns -> statistics.appendNanoseconds = ns);
+      findNanoseconds =
+          new StopWatch.Runner.Enabled().setNanosConsumer(ns -> statistics.findNanoseconds = ns);
+    }
+  }
+
+  public boolean find() {
+    return findNanoseconds.get(() -> findUntimed());
+  }
+
+  protected boolean findUntimed() {
+    start = text.indexOf("${", cursor);
+    nameStart = start + 2;
+    if (start < 0 || text.length() < nameStart + 1) {
+      return false;
+    }
+    end = text.indexOf('}', nameStart);
+    boolean found = end >= 0;
+    return found;
+  }
+
+  public String getName() {
+    return text.substring(nameStart, end);
+  }
+
+  public void appendValue(StringBuffer buffer, String value) {
+    appendNanoseconds.accept((b, v) -> appendValueUntimed(b, v), buffer, value);
+  }
+
+  protected void appendValueUntimed(StringBuffer buffer, String value) {
+    if (start > cursor) {
+      buffer.append(text.substring(cursor, start));
+    }
+    buffer.append(value);
+    cursor = end + 1;
+  }
+
+  public void appendTail(StringBuffer buffer) {
+    appendNanoseconds.accept(b -> appendTailUntimed(b), buffer);
+  }
+
+  protected void appendTailUntimed(StringBuffer buffer) {
+    if (cursor < text.length()) {
+      buffer.append(text.substring(cursor));
+      cursor = text.length();
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/properties/Properties.java b/src/main/java/com/googlesource/gerrit/plugins/task/properties/Properties.java
new file mode 100644
index 0000000..6177f2b
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/properties/Properties.java
@@ -0,0 +1,170 @@
+// Copyright (C) 2022 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.properties;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.googlesource.gerrit.plugins.task.ConfigSourcedValue;
+import com.googlesource.gerrit.plugins.task.TaskConfig;
+import com.googlesource.gerrit.plugins.task.TaskConfig.NamesFactory;
+import com.googlesource.gerrit.plugins.task.TaskConfig.Task;
+import com.googlesource.gerrit.plugins.task.TaskTree;
+import com.googlesource.gerrit.plugins.task.statistics.StopWatch;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/** Use to expand properties like ${_name} in the text of various definitions. */
+public class Properties {
+  public static class Statistics {
+    public long getTaskNanoseconds;
+    public long copierNanoseconds;
+    public Matcher.Statistics matcher;
+
+    public static void setNanoseconds(Statistics stats, long nanos) {
+      if (stats != null) {
+        stats.getTaskNanoseconds = nanos;
+      }
+    }
+
+    public Statistics sum(Statistics other) {
+      if (other == null) {
+        return this;
+      }
+      Statistics statistics = new Statistics();
+      statistics.getTaskNanoseconds = getTaskNanoseconds + other.getTaskNanoseconds;
+      statistics.copierNanoseconds = copierNanoseconds + other.copierNanoseconds;
+      statistics.matcher = matcher == null ? other.matcher : matcher.sum(other.matcher);
+      return statistics;
+    }
+  }
+
+  public static final Properties EMPTY =
+      new Properties() {
+        @Override
+        protected Function<String, String> getParentMapper() {
+          return n -> "";
+        }
+      };
+
+  public final Task origTask;
+  protected final TaskTree.Node node;
+  protected final CopyOnWrite<Task> task;
+  protected Statistics statistics;
+  protected Consumer<Statistics> statisticsConsumer;
+  protected Consumer<Matcher.Statistics> matcherStatisticsConsumer;
+  protected Expander expander;
+  protected Loader loader;
+  protected boolean init = true;
+  protected boolean isTaskRefreshRequired;
+  protected boolean isApplicableRefreshRequired;
+  protected boolean isSubNodeReloadRequired;
+
+  public Properties() {
+    this(null, null);
+    expander = new Expander(n -> "");
+  }
+
+  public Properties(TaskTree.Node node, Task origTask) {
+    this.node = node;
+    this.origTask = origTask;
+    task = new CopyOnWrite.CloneOnWrite<>(origTask);
+  }
+
+  /** Use to expand properties specifically for Tasks. */
+  @SuppressWarnings("try")
+  public Task getTask(ChangeData changeData) {
+    try (StopWatch stopWatch =
+        StopWatch.builder()
+            .enabled(statistics != null)
+            .build()
+            .setNanosConsumer(l -> Statistics.setNanoseconds(statistics, l))) {
+      loader = new Loader(origTask, changeData, getParentMapper());
+      expander = new Expander(n -> loader.load(n));
+      expander.registerClassExpander(
+          ConfigSourcedValue.getClassType(), getConfigSourcedValueExpander(expander));
+      expander.setStatisticsConsumer(matcherStatisticsConsumer);
+      if (isTaskRefreshRequired || init) {
+        expander.expand(task, TaskConfig.KEY_APPLICABLE);
+        isApplicableRefreshRequired = loader.isNonTaskDefinedPropertyLoaded();
+
+        expander.expand(task, ImmutableSet.of(TaskConfig.KEY_APPLICABLE, TaskConfig.KEY_NAME));
+
+        Map<String, String> exported = expander.expand(origTask.exported);
+        if (exported != origTask.exported) {
+          task.getForWrite().exported = exported;
+        }
+
+        if (init) {
+          init = false;
+          isTaskRefreshRequired = loader.isNonTaskDefinedPropertyLoaded();
+        }
+      }
+    }
+    if (statisticsConsumer != null) {
+      statisticsConsumer.accept(statistics);
+    }
+    return task.getForRead();
+  }
+
+  protected Function<ConfigSourcedValue, ConfigSourcedValue> getConfigSourcedValueExpander(
+      Expander expander) {
+    return t -> {
+      String toExpand = t.value();
+      String expanded = expander.expandText(toExpand);
+      if (toExpand != expanded) {
+        return ConfigSourcedValue.create(t.sourceFile(), expanded);
+      }
+      return t;
+    };
+  }
+
+  public void setStatisticsConsumer(Consumer<Statistics> statisticsConsumer) {
+    if (statisticsConsumer != null) {
+      this.statisticsConsumer = statisticsConsumer;
+      statistics = new Statistics();
+      matcherStatisticsConsumer = s -> statistics.matcher = s;
+      task.setNanosecondsConsumer(ns -> statistics.copierNanoseconds = ns);
+    }
+  }
+
+  // To detect NamesFactories dependent on non task defined properties, the checking must be
+  // done after subnodes are fully loaded, which unfortunately happens after getTask() is
+  // called, therefore this must be called after all subnodes have been loaded.
+  public void expansionComplete() {
+    isSubNodeReloadRequired = loader.isNonTaskDefinedPropertyLoaded();
+  }
+
+  public boolean isApplicableRefreshRequired() {
+    return isApplicableRefreshRequired;
+  }
+
+  public boolean isTaskRefreshRequired() {
+    return isTaskRefreshRequired;
+  }
+
+  public boolean isSubNodeReloadRequired() {
+    return isSubNodeReloadRequired;
+  }
+
+  /** Use to expand properties specifically for NamesFactories. */
+  public NamesFactory getNamesFactory(NamesFactory namesFactory) {
+    return expander.expand(namesFactory, ImmutableSet.of(TaskConfig.KEY_TYPE));
+  }
+
+  protected Function<String, String> getParentMapper() {
+    return n -> node.getParentProperties().expander.getValueForName(n);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/statistics/HitBooleanTable.java b/src/main/java/com/googlesource/gerrit/plugins/task/statistics/HitBooleanTable.java
new file mode 100644
index 0000000..2f35084
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/statistics/HitBooleanTable.java
@@ -0,0 +1,94 @@
+// Copyright (C) 2022 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.statistics;
+
+import com.google.gerrit.common.BooleanTable;
+import com.googlesource.gerrit.plugins.task.util.TopKeyMap;
+
+/**
+ * A space efficient Table for Booleans. This Table takes advantage of the fact that the values
+ * stored in it are all Booleans and uses BitSets to make this very space efficient.
+ */
+public class HitBooleanTable<R, C> extends BooleanTable<R, C> implements TracksStatistics {
+  public static class Statistics<V> {
+    public long hits;
+    public long misses;
+    public long size;
+    public int numberOfRows;
+    public int numberOfColumns;
+    public Long sumNanosecondsLoading;
+    public TopKeyMap<V> topNanosecondsLoadingKeys;
+  }
+
+  protected Statistics<TopKeyMap.TableKeyValue<R, C>> statistics;
+
+  @Override
+  public Boolean get(R r, C c) {
+    Boolean value = super.get(r, c);
+    if (statistics != null) {
+      if (value != null) {
+        statistics.hits++;
+      } else {
+        statistics.misses++;
+      }
+    }
+    return value;
+  }
+
+  public StopWatch createLoadingStopWatch(R row, C column, boolean isVisible) {
+    if (statistics == null) {
+      return StopWatch.DISABLED;
+    }
+    if (statistics.sumNanosecondsLoading == null) {
+      statistics.sumNanosecondsLoading = 0L;
+    }
+    return new StopWatch.Enabled()
+        .setNanosConsumer(
+            ns ->
+                statistics.sumNanosecondsLoading +=
+                    updateTopLoadingTimes(ns, row, column, isVisible));
+  }
+
+  public long updateTopLoadingTimes(long nanos, R row, C column, boolean isVisible) {
+    statistics.topNanosecondsLoadingKeys.addIfTop(
+        nanos, isVisible ? new TopKeyMap.TableKeyValue<R, C>(row, column) : null);
+    return nanos;
+  }
+
+  @Override
+  public void initStatistics(int summaryCount) {
+    statistics = new Statistics<>();
+    statistics.topNanosecondsLoadingKeys = new TopKeyMap<>(summaryCount);
+  }
+
+  @Override
+  public void ensureStatistics(int summaryCount) {
+    if (statistics == null) {
+      initStatistics(summaryCount);
+    }
+  }
+
+  @Override
+  public Object getStatistics() {
+    statistics.numberOfRows = rowByRow.size();
+    statistics.numberOfColumns = positionByColumn.size();
+    statistics.size =
+        rowByRow.values().stream()
+            .map(r -> (long) r.hasValues.size() + (long) r.values.size())
+            .mapToLong(Long::longValue)
+            .sum();
+    return statistics;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/statistics/HitHashMap.java b/src/main/java/com/googlesource/gerrit/plugins/task/statistics/HitHashMap.java
new file mode 100644
index 0000000..ccb12a9
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/statistics/HitHashMap.java
@@ -0,0 +1,184 @@
+// Copyright (C) 2022 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.statistics;
+
+import static java.util.stream.Collectors.toList;
+
+import com.googlesource.gerrit.plugins.task.util.TopKeyMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+public class HitHashMap<K, V> extends HashMap<K, V> implements StatisticsMap<K, V> {
+  public static class Statistics<K> {
+    public long hits;
+    public int size;
+    public Long sumNanosecondsLoading;
+    public TopKeyMap<K> topNanosecondsLoadingKeys;
+    public List<Object> elements;
+  }
+
+  public static final long serialVersionUID = 1;
+
+  protected Statistics<K> statistics;
+
+  public HitHashMap() {}
+
+  @Override
+  public V get(Object key) {
+    V v = super.get(key);
+    if (statistics != null && v != null) {
+      statistics.hits++;
+    }
+    return v;
+  }
+
+  @Override
+  public V getOrDefault(Object key, V dv) {
+    V v = get(key);
+    if (v == null) {
+      return dv;
+    }
+    return v;
+  }
+
+  @Override
+  @SuppressWarnings("try")
+  public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
+    V v = get(key);
+    if (v == null) {
+      v = mappingFunction.apply(key);
+      if (v != null) {
+        put(key, v);
+      }
+    }
+    return v;
+  }
+
+  @Override
+  @SuppressWarnings("try")
+  public V computeIfAbsentTimed(
+      K key, Function<? super K, ? extends V> mappingFunction, boolean isVisible) {
+    V v = get(key);
+    if (v == null) {
+      try (StopWatch stopWatch = createLoadingStopWatch(key, isVisible)) {
+        v = mappingFunction.apply(key);
+      }
+      if (v != null) {
+        put(key, v);
+      }
+    }
+    return v;
+  }
+
+  @Override
+  public V put(K key, V value) {
+    if (statistics != null && value instanceof TracksStatistics) {
+      ((TracksStatistics) value).ensureStatistics(statistics.topNanosecondsLoadingKeys.size());
+    }
+    return super.put(key, value);
+  }
+
+  @Override
+  public void putAll(Map<? extends K, ? extends V> m) {
+    m.entrySet().stream().forEach(e -> put(e.getKey(), e.getValue()));
+  }
+
+  @Override
+  public V putIfAbsent(K key, V value) {
+    if (!containsKey(key)) {
+      put(key, value);
+      return null;
+    }
+    return get(key);
+  }
+
+  @Override
+  public V computeIfPresent(
+      K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
+    throw new UnsupportedOperationException(); // Todo if needed
+  }
+
+  @Override
+  public V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
+    throw new UnsupportedOperationException(); // Todo if needed
+  }
+
+  @Override
+  public V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
+    throw new UnsupportedOperationException(); // Todo if needed
+  }
+
+  @Override
+  public V replace(K key, V value) {
+    throw new UnsupportedOperationException(); // Todo if needed
+  }
+
+  @Override
+  public boolean replace(K key, V oldValue, V newValue) {
+    throw new UnsupportedOperationException(); // Todo if needed
+  }
+
+  @Override
+  public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
+    throw new UnsupportedOperationException(); // Todo if needed
+  }
+
+  @Override
+  public void initStatistics(int summaryCount) {
+    statistics = new Statistics<>();
+    statistics.topNanosecondsLoadingKeys = new TopKeyMap<>(summaryCount);
+  }
+
+  @Override
+  public void ensureStatistics(int summaryCount) {
+    if (statistics == null) {
+      initStatistics(summaryCount);
+    }
+  }
+
+  public StopWatch createLoadingStopWatch(K key, boolean isVisible) {
+    if (statistics == null) {
+      return StopWatch.DISABLED;
+    }
+    if (statistics.sumNanosecondsLoading == null) {
+      statistics.sumNanosecondsLoading = 0L;
+    }
+    return new StopWatch.Enabled()
+        .setNanosConsumer(
+            ns -> statistics.sumNanosecondsLoading += updateTopLoadingTimes(ns, key, isVisible));
+  }
+
+  public long updateTopLoadingTimes(long nanos, K key, boolean isVisible) {
+    statistics.topNanosecondsLoadingKeys.addIfTop(nanos, isVisible ? key : null);
+    return nanos;
+  }
+
+  @Override
+  public Object getStatistics() {
+    statistics.size = size();
+    List<Object> elementStatistics =
+        values().stream()
+            .filter(e -> e instanceof TracksStatistics)
+            .map(e -> ((TracksStatistics) e).getStatistics())
+            .collect(toList());
+    if (!elementStatistics.isEmpty()) {
+      statistics.elements = elementStatistics;
+    }
+    return statistics;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/statistics/HitHashMapOfCollection.java b/src/main/java/com/googlesource/gerrit/plugins/task/statistics/HitHashMapOfCollection.java
new file mode 100644
index 0000000..31a9d61
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/statistics/HitHashMapOfCollection.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2022 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.statistics;
+
+import static java.util.stream.Collectors.toList;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+
+public class HitHashMapOfCollection<K, V extends Collection<?>> extends HitHashMap<K, V> {
+  public static class Statistics<K> extends HitHashMap.Statistics<K> {
+    public List<Integer> top5CollectionSizes;
+    public List<Integer> bottom5CollectionSizes;
+  }
+
+  public static final long serialVersionUID = 1;
+
+  protected Statistics<K> statistics;
+
+  public HitHashMapOfCollection() {}
+
+  @Override
+  public void initStatistics(int summaryCount) {
+    super.initStatistics(summaryCount);
+    statistics = new Statistics<>();
+  }
+
+  @Override
+  public Object getStatistics() {
+    super.getStatistics();
+    statistics.hits = super.statistics.hits;
+    statistics.size = super.statistics.size;
+
+    List<Integer> collectionSizes =
+        values().stream().map(l -> l.size()).sorted(Comparator.reverseOrder()).collect(toList());
+    statistics.top5CollectionSizes = new ArrayList<>(5);
+    statistics.bottom5CollectionSizes = new ArrayList<>(5);
+    for (int i = 0; i < 5 && i < collectionSizes.size(); i++) {
+      statistics.top5CollectionSizes.add(collectionSizes.get(i));
+      int bottom = collectionSizes.size() - 6 + i;
+      if (bottom > 4 && bottom < collectionSizes.size()) {
+        // The > 4 ensures that there are no entries also in the top list
+        statistics.bottom5CollectionSizes.add(collectionSizes.get(bottom));
+      }
+    }
+    if (statistics.bottom5CollectionSizes.isEmpty()) {
+      statistics.bottom5CollectionSizes = null;
+    }
+    return statistics;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/statistics/StatisticsMap.java b/src/main/java/com/googlesource/gerrit/plugins/task/statistics/StatisticsMap.java
new file mode 100644
index 0000000..5bca24d
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/statistics/StatisticsMap.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2022 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.statistics;
+
+import java.util.Map;
+import java.util.function.Function;
+
+public interface StatisticsMap<K, V> extends Map<K, V>, TracksStatistics {
+  V computeIfAbsentTimed(
+      K key, Function<? super K, ? extends V> mappingFunction, boolean isVisible);
+
+  StopWatch createLoadingStopWatch(K key, boolean isVisible);
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/statistics/StopWatch.java b/src/main/java/com/googlesource/gerrit/plugins/task/statistics/StopWatch.java
new file mode 100644
index 0000000..05f942b
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/statistics/StopWatch.java
@@ -0,0 +1,140 @@
+// Copyright (C) 2022 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.statistics;
+
+import com.google.common.base.Stopwatch;
+import com.googlesource.gerrit.plugins.task.util.SamTryWrapper;
+import java.util.concurrent.TimeUnit;
+import java.util.function.LongConsumer;
+
+/**
+ * StopWatches with APIs designed to make it easy to disable, and to make robust in the face of
+ * exceptions.
+ *
+ * <p>The Stopwatch class from google commons is used by placing start() and stop() calls around
+ * code sections which need to be timed. This approach can be problematic since the code being timed
+ * could throw an exception and if the stop() is not in a finally clause, then it will likely never
+ * get called, potentially causing bad timings, or worse programatic issues elsewhere due to double
+ * calls to start(). The need for a finally clause to make things safe is an obvious hint that using
+ * an AutoCloseable approach is likely going to be safer. With that in mind, the two API approaches
+ * provided by these StopWatch classes are:
+ *
+ * <ol>
+ *   <li>The timed try-with-resource API. Use the StopWatch.Enabled class for this API.
+ *   <li>The timed SAM evaluation API. Use the StopWatch.Runner.Enabled class for these APIs.
+ * </ol>
+ *
+ * <p>Finally, the commons stopwatch API also does not provide an easy way to disable timings at
+ * runtime when they are not desired, so the DISABLED classes can be used for this with both
+ * approaches above, and thus provide low cost runtime substitutes for either the StopWatch.Enabled
+ * or StopWatch.Runner.Enabled classes.
+ */
+public interface StopWatch extends AutoCloseable {
+  /** Designed for the greatest simplicity to time SAM executions. */
+  public abstract static class Runner extends SamTryWrapper<AutoCloseable> {
+    public static class Enabled extends Runner {
+      protected LongConsumer nanosConsumer = EMPTY_LONG_CONSUMER;
+
+      @Override
+      protected AutoCloseable getAutoCloseable() {
+        return new StopWatch.Enabled().setNanosConsumer(nanosConsumer);
+      }
+
+      @Override
+      public Runner setNanosConsumer(LongConsumer nanosConsumer) {
+        this.nanosConsumer = nanosConsumer;
+        return this;
+      }
+    }
+
+    /** May be used anywhere that Enabled can be used */
+    public static final Runner DISABLED =
+        new Runner() {
+          @Override
+          protected AutoCloseable getAutoCloseable() {
+            return () -> {};
+          }
+        };
+
+    public Runner setNanosConsumer(LongConsumer nanosConsumer) {
+      return this;
+    }
+  }
+
+  /** Should be created and used only within a try-with-resource */
+  public static class Enabled implements StopWatch {
+    protected LongConsumer nanosConsumer = EMPTY_LONG_CONSUMER;
+    protected Stopwatch stopwatch = Stopwatch.createStarted();
+
+    @Override
+    public StopWatch setNanosConsumer(LongConsumer nanosConsumer) {
+      this.nanosConsumer = nanosConsumer;
+      return this;
+    }
+
+    @Override
+    public void close() {
+      stopwatch.stop();
+      nanosConsumer.accept(stopwatch.elapsed(TimeUnit.NANOSECONDS));
+    }
+  }
+
+  /**
+   * A easy way to build a timer which needes to be enabled/disabled based on a runtime boolean.
+   *
+   * <p>Example Usage:
+   *
+   * <p><code>
+   * try (StopWatch stopWatch =
+   *      StopWatch.builder().enabled(myBoolean).build().setNanosConsumer(myConsumer)) {
+   *   // Code to be timed here...
+   * }
+   * </code>
+   */
+  public static class Builder {
+    protected static class Enabled extends Builder {
+      @Override
+      public StopWatch build() {
+        return new StopWatch.Enabled();
+      }
+    }
+
+    protected static final Builder ENABLED = new Enabled();
+    protected static final Builder DISABLED = new Builder();
+
+    public Builder enabled(boolean enabled) {
+      return enabled ? ENABLED : this;
+    }
+
+    public StopWatch build() {
+      return StopWatch.DISABLED;
+    }
+  }
+
+  /** May be used anywhere that Enabled can be used */
+  public static final StopWatch DISABLED = new StopWatch() {};
+
+  public static final LongConsumer EMPTY_LONG_CONSUMER = l -> {};
+
+  public static Builder builder() {
+    return Builder.DISABLED;
+  }
+
+  default StopWatch setNanosConsumer(LongConsumer nanosConsumer) {
+    return this;
+  }
+
+  default void close() {}
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/statistics/TracksStatistics.java b/src/main/java/com/googlesource/gerrit/plugins/task/statistics/TracksStatistics.java
new file mode 100644
index 0000000..a150600
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/statistics/TracksStatistics.java
@@ -0,0 +1,23 @@
+// Copyright (C) 2022 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.statistics;
+
+public interface TracksStatistics {
+  void initStatistics(int summaryCount);
+
+  void ensureStatistics(int summaryCount);
+
+  Object getStatistics();
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/util/Copier.java b/src/main/java/com/googlesource/gerrit/plugins/task/util/Copier.java
new file mode 100644
index 0000000..434a4de
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/util/Copier.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2021 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.util;
+
+import java.lang.reflect.Field;
+
+public class Copier {
+  public static <T> void shallowCopyDeclaredFields(
+      Class<T> cls, T from, T to, boolean includeInaccessible) {
+    for (Field field : cls.getDeclaredFields()) {
+      try {
+        if (includeInaccessible) {
+          field.setAccessible(true);
+        }
+        Object val = field.get(from);
+        if (!field.getName().equals("this$0")) { // Can't copy internal final field
+          field.set(to, val);
+        }
+      } catch (IllegalAccessException e) {
+        if (includeInaccessible) {
+          throw new RuntimeException(
+              "Cannot access field to copy it " + fieldValueToString(field, "unknown"));
+        }
+      }
+    }
+  }
+
+  protected static String fieldValueToString(Field field, Object val) {
+    return "field:" + field.getName() + " value:" + val + " type:" + field.getType();
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/util/SamTryWrapper.java b/src/main/java/com/googlesource/gerrit/plugins/task/util/SamTryWrapper.java
new file mode 100644
index 0000000..ef42e5a
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/util/SamTryWrapper.java
@@ -0,0 +1,84 @@
+// Copyright (C) 2023 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.util;
+
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * Class designed to make SAM calls wrapped by an AutoCloseable. This is usefull with AutoCloseables
+ * which do not provide resources which are directly needed during the SAM call, but rather with
+ * AutoCloseables which likely manage external resources or state such as a locks or timers.
+ */
+public abstract class SamTryWrapper<A extends AutoCloseable> {
+  protected abstract A getAutoCloseable();
+
+  @SuppressWarnings("try")
+  public void run(Runnable runnable) {
+    try (A autoCloseable = getAutoCloseable()) {
+      runnable.run();
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @SuppressWarnings("try")
+  public <T> T get(Supplier<T> supplier) {
+    try (A autoCloseable = getAutoCloseable()) {
+      return supplier.get();
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @SuppressWarnings("try")
+  public <T> void accept(Consumer<T> consumer, T t) {
+    try (A autoCloseable = getAutoCloseable()) {
+      consumer.accept(t);
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @SuppressWarnings("try")
+  public <T, U> void accept(BiConsumer<T, U> consumer, T t, U u) {
+    try (A autoCloseable = getAutoCloseable()) {
+      consumer.accept(t, u);
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @SuppressWarnings("try")
+  public <T, R> R apply(Function<T, R> func, T t) {
+    try (A autoCloseable = getAutoCloseable()) {
+      return func.apply(t);
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @SuppressWarnings("try")
+  public <T, U, R> R apply(BiFunction<T, U, R> func, T t, U u) {
+    try (A autoCloseable = getAutoCloseable()) {
+      return func.apply(t, u);
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/ThrowingProvider.java b/src/main/java/com/googlesource/gerrit/plugins/task/util/ThrowingProvider.java
similarity index 95%
rename from src/main/java/com/googlesource/gerrit/plugins/task/ThrowingProvider.java
rename to src/main/java/com/googlesource/gerrit/plugins/task/util/ThrowingProvider.java
index 7644143..3d5197e 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/ThrowingProvider.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/util/ThrowingProvider.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.googlesource.gerrit.plugins.task;
+package com.googlesource.gerrit.plugins.task.util;
 
 public interface ThrowingProvider<V, E extends Exception> {
   public V get() throws E;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/util/TopKeyMap.java b/src/main/java/com/googlesource/gerrit/plugins/task/util/TopKeyMap.java
new file mode 100644
index 0000000..a6627fb
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/util/TopKeyMap.java
@@ -0,0 +1,94 @@
+// Copyright (C) 2023 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.util;
+
+/**
+ * A TopKeyMap is a lightweight limited size (default 5) map with 'long' keys designed to store only
+ * the elements with the top five largest keys.
+ *
+ * <p>A TopKeyMap is array based and has O(n) insertion time. Despite not having O(1) insertion
+ * times, it should likely be much faster than a hash based map for small n sizes. It also is more
+ * memory efficient than a hash based map, although both are likely O(n) in space usage. The
+ * TopKeyMap allocates all of its entries up front so it does not change its memory utilization at
+ * all, and it does not have to create or free any Objects during its post constructor lifespan.
+ *
+ * <p>While a TopKeyMap currently only uses 'long's as keys, it is possible to easiy upgrade this
+ * collection to use any type of Comparable key.
+ *
+ * <p>Although not currently thread safe, due to the simplicity of the data structures used, and the
+ * insertion approach, it is easy to make a TopKeyMap efficiently thread safe.
+ */
+public class TopKeyMap<V> {
+  /**
+   * A TableKeyValue is a helper class for TopKeyMap use cases, such as a table with with row and
+   * column keys, which involve two values.
+   */
+  public static class TableKeyValue<R, C> {
+    public final R row;
+    public final C column;
+
+    public TableKeyValue(R row, C column) {
+      this.row = row;
+      this.column = column;
+    }
+  }
+
+  protected class Entry {
+    public long key;
+    public V value;
+
+    protected void set(long key, V value) {
+      this.key = key;
+      this.value = value;
+    }
+  }
+
+  protected Entry[] entries;
+
+  public TopKeyMap() {
+    this(5);
+  }
+
+  @SuppressWarnings("unchecked")
+  public TopKeyMap(int length) {
+    entries = (Entry[]) new Object[length];
+    for (int i = 0; i < entries.length; i++) {
+      entries[i] = new Entry();
+    }
+  }
+
+  public void addIfTop(long key, V value) {
+    addIfTop(0, key, value);
+  }
+
+  protected void addIfTop(int i, long key, V value) {
+    if (entries[entries.length - 1].key < key) {
+      for (; i < entries.length; i++) {
+        Entry e = entries[i];
+        if (e.key < key) {
+          long eKValue = e.key;
+          V eValue = e.value;
+          e.set(key, value);
+          addIfTop(i + 1, eKValue, eValue);
+          return;
+        }
+      }
+    }
+  }
+
+  public int size() {
+    return entries.length;
+  }
+}
diff --git a/src/main/resources/Documentation/config-gerrit.md b/src/main/resources/Documentation/config-gerrit.md
new file mode 100644
index 0000000..6f0b0a5
--- /dev/null
+++ b/src/main/resources/Documentation/config-gerrit.md
@@ -0,0 +1,25 @@
+# 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/task.md b/src/main/resources/Documentation/task.md
index fa5e834..75ee189 100644
--- a/src/main/resources/Documentation/task.md
+++ b/src/main/resources/Documentation/task.md
@@ -51,11 +51,17 @@
 completes.
 
 A task with a `WAITING` status is not yet ready to execute. A task in this
-state is blocked by its subtasks which are not yet in the `PASS` state.
+state is blocked by its subtasks which are not yet in the `PASS` or `DUPLICATE`
+state.
 
 A task with a `READY` status is ready to be executed. All of its subtasks are
 in the `PASS` state.
 
+A task with a `DUPLICATE` status has the same task key as one of its ancestors.
+Task keys are generally made up of the canonical task name and the change to
+which it applies. To avoid infinite loops, subtasks are ignored on duplicate
+tasks.
+
 A task with a `PASS` status meets all the criteria for `READY`, and has
 executed and was successful.
 
@@ -160,8 +166,8 @@
 from the current task if they redefined in the current task. Attributes
 which are lists (such as subtasks) or maps (such as properties), will be
 preloaded by the preload-task and then extended with the attributes from the
-current task. See [Optional Tasks](#optional_tasks) for how to define optional
-preload-tasks.
+current task. See [Task Expression](task_expression.html) for how to define
+optional preload-tasks.
 
 Example:
 ```
@@ -172,8 +178,8 @@
 
 : This key lists the name of a subtask of the current task. This key may be
 used several times in a task section to define more than one subtask for a
-particular task. See [Optional Tasks](#optional_tasks) for how to define
-optional subtasks.
+particular task. See [Task Expression](task_expression.html) for how to define
+subtasks.
 
 Example:
 
@@ -231,6 +237,48 @@
     subtasks-file = common.config  # references the file named task/common.config
 ```
 
+`duplicate-key`
+
+: This key defines an identifier to help identify tasks which should be
+considered duplicates even if they are not exact duplicates. When the task
+plugin encounters a task with the same duplicate-key as one of its
+ancestors, it will be considered a duplicate of that ancestor. Tasks such as
+a starting task and a looping tasks-factory that preload the same base task
+are not exact duplicates, yet they may logically represent duplicates. In
+this case, defining a `duplicate-key` on the base task which is preloaded
+from two different places (usually a root and a change tasks-factory), will
+ensure that any loops are halted once the original change is reached. Without
+a duplicate-key, the walking would generally walk one task further than
+desired.
+
+Outlined below is a simple way to walk a change's git dependencies in the
+task plugin. While Git does not allow loops in commit histories, sometimes
+in Gerrit when changes get rebased, it can cause loops (because Gerrit
+sometimes tracks outdated dependencies). The use of the duplicate-key
+below results in the loop being detected when you would expect it to be.
+
+Example:
+
+```
+[root "git dependencies"]
+    applicable = status:new
+    preload-task = git dependencies
+
+[task "git dependencies"]
+    fail = -status:new
+    fail-hint = [${_change_status}] dependency needs to be OPEN
+    subtasks-factory = git dependencies
+    duplicate-key = git dependencies ${_change_number}
+
+[tasks-factory "git dependencies"]
+    names-factory = git dependencies
+    preload-task = git dependencies
+
+[names-factory "git dependencies"]
+    type = change
+    changes = -status:merged parentof:${_change_number} project:${_change_project} branch:${_change_branch}
+```
+
 Root Tasks
 ----------
 Root tasks typically define the "final verification" tasks for changes. Each
@@ -275,26 +323,7 @@
     fail = label:code-review-2
 ```
 
-<a id="optional_tasks"/>
-Optional Tasks
---------------
-To define a task that may not exist and that will not cause the task referencing
-it to be INVALID, follow the task name with pipe (`|`) character. This feature
-is particularly useful when a property is used in the task name.
-
-```
-    preload-task = Optional Subtask {$_name} |
-```
-
-To define an alternate task to load when an optional task does not exist,
-list the alterante task name after the pipe (`|`) character. This feature
-may be chained together as many times as needed.
-
-```
-    subtask = Optional Subtask {$_name} |
-              Backup Optional Subtask {$_name} Backup |
-              Default Subtask # Must exist if the above two don't!
-```
+<a id="tasks_factory"/>
 Tasks-Factory
 -------------
 A tasks-factory section supports all the keys supported by task sections.  In
@@ -419,9 +448,9 @@
 
 Examples:
 ```
-    fail-hint = {$_name} needs to be fixed
-    fail-hint = {$_change_number} with {$_change_status} needs to be fixed
-    fail-hint = {$_change_id} on {$_change_project} and {$_change_branch} needs to be fixed
+    fail-hint = ${_name} needs to be fixed
+    fail-hint = ${_change_number} with ${_change_status} needs to be fixed
+    fail-hint = ${_change_id} on ${_change_project} and ${_change_branch} needs to be fixed
     changes = parentof:${_change_number} project:${_change_project} branch:${_change_branch}
 ```
 
@@ -509,7 +538,33 @@
 not output anything. This switch is particularly useful in combination
 with the **\-\-@PLUGIN@\-\-preview** switch.
 
-**\-\-@PLUGIN@\-\-task\-\-evaluation-time**
+**\-\-@PLUGIN@\-\-include-paths**
+
+This switch will show the absolute path of each task. This is meant for
+debugging when tasks are spread out in different files. A task path includes
+task name, type (indicating one of root, task, tasks-factory),
+[task factory](#tasks_factory) name (if it is generated by one), file name,
+project, and branch the file belongs to. Additionally, if a task is on a user
+ref, it also shows the identity of that user. Only users with `viewTaskPaths`
+capability on the server can view absolute task paths with this switch.
+
+```
+  $ ssh -x -p 29418 example.com gerrit query change:123 \-\-@PLUGIN@\-\-include-paths
+  ...
+  plugins:
+    name: task
+    roots:
+      name: Jenkins Build and Test
+      inProgress: false
+      status: READY
+      path:
+        name: Jenkins Build and Test
+        project: All-Projects.git
+        branch: refs/meta/config
+        file: task.config
+```
+
+**\-\-@PLUGIN@\-\-evaluation-time**
 
 This switch is meant as a debug switch to evaluate task performance. This
 switch outputs an elapsed time value on every task indicating how much time
@@ -533,6 +588,13 @@
         status: PASS
 ```
 
+**\-\-@PLUGIN@\-\-only**
+
+This switch can be used to only evaluate tasks under a certain root when tasks
+from other roots are unwanted. For example, a CI system may not be interested
+in evaluating tasks for another CI system. The switch can be provided multiple
+times.
+
 Examples
 --------
 See [task_states](test/task_states.html) for a comprehensive list of examples
diff --git a/src/main/resources/Documentation/task_expression.md b/src/main/resources/Documentation/task_expression.md
new file mode 100644
index 0000000..c084a39
--- /dev/null
+++ b/src/main/resources/Documentation/task_expression.md
@@ -0,0 +1,220 @@
+<a id="task_expression"/>
+Task Expression
+--------------
+
+The tasks in subtask and preload-task can be defined using a Task Expression.
+Each task expression can contain multiple tasks (all can be optional). Tasks
+from other files and refs can be referenced using [Task Reference](#task_reference).
+
+```
+TASK_EXPR = TASK_REFERENCE [ WHITE_SPACE * '|' [ WHITE_SPACE * TASK_EXPR ] ]
+```
+
+To define a task that may not exist and that will not cause the task referencing
+it to be INVALID, follow the task name with pipe (`|`) character. This feature
+is particularly useful when a property is used in the task name.
+
+```
+    preload-task = Optional task ${_name} |
+```
+
+To define an alternate task to load when an optional task does not exist,
+list the alternate task name after the pipe (`|`) character. This feature
+may be chained together as many times as needed.
+
+```
+    subtask = Optional Subtask ${_name} |
+              Backup Optional Subtask ${_name} Backup |
+              Default Subtask # Must exist if the above two don't!
+```
+
+<a id="task_reference"/>
+Task Reference
+---------
+
+Tasks reference can be a simple task name when the defined task is intended to be in
+the same file, tasks from other files and refs can also be referenced by syntax explained
+below.
+
+```
+ TASK_REFERENCE = [
+                    [ // TASK_FILE_PATH ]
+                    [ @USERNAME [ TASK_FILE_PATH ] ] |
+                    [ %GROUP_NAME [ TASK_FILE_PATH ] ] |
+                    [ %%GROUP_UUID [ TASK_FILE_PATH ] ] |
+                    [ TASK_FILE_PATH ]
+                  ] '^' TASK_NAME
+```
+
+To reference a task from root task.config (top level task.config file of a repository)
+on the current ref, prefix the task name with `^`.
+
+Example:
+
+task/.../<any>.config
+```
+    ...
+    preload-task = ^Task in root task config
+    ...
+```
+
+task.config
+```
+    ...
+    [task "Task in root task config"]
+    ...
+```
+
+To provide an absolute reference to a task under the `task` folder, provide the subpath starting
+from `task` directory with a leading `/` followed by a `^` and then task name.
+
+Example:
+
+task.config
+```
+    ...
+    subtask =  /foo/bar/baz.config^Absolute Example Task
+    ...
+```
+
+task/foo/bar/baz.config
+```
+    ...
+    [task "Absolute Example Task"]
+    ...
+```
+
+Similarly, to provide reference to tasks which are in a subdirectory of the file containing the
+current task avoid the leading `/`.
+
+Example:
+
+task/foo/file.config
+```
+    ...
+    subtask = bar/baz.config^Relative Example Task
+    ...
+```
+
+task/foo/bar/baz.config
+```
+    ...
+    [task "Relative Example Task"]
+    ...
+```
+
+Relative tasks specified in a root task.config would look for a file path under the task directory.
+
+Example:
+
+task.config
+```
+    ...
+    subtask = foo/bar.config^Relative from Root Example Task
+    ...
+```
+
+task/foo/bar.config
+```
+    ...
+    [task "Relative from Root Example Task"]
+    ...
+```
+
+To reference a task from a specific user ref (All-Users.git:refs/users/<user>), specify the
+username with `@`.
+
+when referencing from user refs, to get task from top level task.config on a user ref use
+`@<username>^<task_name>` and to get any task under the task directory use the relative
+path, like: `@<username>/<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:
+Assumption: Account id of user_a is 1000000
+
+All-Users:refs/users/00/1000000:task.config
+```
+    ...
+    [task "top level task"]
+    ...
+```
+
+All-Users:refs/users/00/1000000:/task/dir/common.config
+```
+    ...
+    [task "common task"]
+    ...
+```
+
+All-Projects:refs/meta/config:/task.config
+```
+    ...
+    preload-task = @user_a_username^top level task
+    preload-task = @user_a_username/dir/common.config^common task
+    ...
+```
+
+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.
+
+Example:
+
+All-Projects:refs/meta/config:task.config
+```
+    ...
+    [task "root task"]
+    ...
+```
+
+All-Projects:refs/meta/config:/task/dir/sample.config
+
+```
+    ...
+    [task "sample task"]
+    ...
+```
+
+All-Users:refs/users/00/1000000:task.config
+```
+    ...
+    preload-task = //dir/sample.config^sample task
+    preload-task = //^root task
+    ...
+```
+
+To reference a task from a specific group ref (All-Users.git:refs/groups/<sharded-group-uuid>),
+specify the group name with `%` or group uuid with `%%`.
+
+When referencing from group refs, to get task from top level task.config on a group ref use
+`%<group_name>^<task_name>` or `%%<group_uuid>^<task_name>` and to get any task under the
+task directory use the relative path,
+like: `%<group_name>/<relative path from task dir>^<task_name>` or
+`%%<group_uuid>/<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:
+Assumption: Group uuid of group_a is 720269095421a08a24889e29d092df1839a7a706
+
+All-Users:refs/groups/72/720269095421a08a24889e29d092df1839a7a706:task.config
+```
+    ...
+    [task "top level task"]
+    ...
+```
+
+All-Users:refs/groups/72/720269095421a08a24889e29d092df1839a7a706:/task/dir/common.config
+```
+    ...
+    [task "common task"]
+    ...
+```
+
+All-Projects:refs/meta/config:/task.config
+```
+    ...
+    preload-task = %group_a^top level task
+    preload-task = %%720269095421a08a24889e29d092df1839a7a706/dir/common.config^common task
+    ...
+```
diff --git a/src/main/resources/Documentation/test/paths.md b/src/main/resources/Documentation/test/paths.md
new file mode 100644
index 0000000..8f43cb0
--- /dev/null
+++ b/src/main/resources/Documentation/test/paths.md
@@ -0,0 +1,208 @@
+`task.config` file in project `All-Projects` on ref `refs/meta/config`.
+
+```
+[root "Root Task PATHS"]
+  subtask = subtask pass
+
+[task "subtask pass"]
+  applicable = is:open
+  pass = is:open
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root Task PATHS",
+   "path" : {
+      "ref" : "refs/meta/config",
+      "file" : "task.config",
+      "name" : "Root Task PATHS",
+      "project" : "All-Projects",
+      "type" : "root"
+   },
+   "status" : "PASS",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "subtask pass",
+         "path" : {
+            "ref" : "refs/meta/config",
+            "file" : "task.config",
+            "name" : "subtask pass",
+            "project" : "All-Projects",
+            "type" : "task"
+         },
+         "status" : "PASS"
+      }
+   ]
+}
+
+[root "Root other FILE"]
+  applicable = is:open
+  subtasks-file = common.config
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root other FILE",
+   "path" : {
+      "ref" : "refs/meta/config",
+      "file" : "task.config",
+      "name" : "Root other FILE",
+      "project" : "All-Projects",
+      "type" : "root"
+   },
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "file task/common.config PASS",
+         "path" : {
+            "ref" : "refs/meta/config",
+            "file" : "task/common.config",
+            "name" : "file task/common.config PASS",
+            "project" : "All-Projects",
+            "type" : "task"
+         },
+         "status" : "PASS"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "file task/common.config FAIL",
+         "path" : {
+            "ref" : "refs/meta/config",
+            "file" : "task/common.config",
+            "name" : "file task/common.config FAIL",
+            "project" : "All-Projects",
+            "type" : "task"
+         },
+         "status" : "FAIL"
+      }
+   ]
+}
+
+[root "Root tasks-factory"]
+  subtasks-factory = tasks-factory example
+
+[tasks-factory "tasks-factory example"]
+  names-factory = names-factory example list
+
+[names-factory "names-factory example list"]
+  type = static
+  name = my a task
+  name = my b task
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root tasks-factory",
+   "path" : {
+      "ref" : "refs/meta/config",
+      "file" : "task.config",
+      "name" : "Root tasks-factory",
+      "project" : "All-Projects",
+      "type" : "root"
+   },
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : false,
+         "name" : "my a task",
+         "path" : {
+            "ref" : "refs/meta/config",
+            "file" : "task.config",
+            "name" : "my a task",
+            "project" : "All-Projects",
+            "tasksFactory" : "tasks-factory example",
+            "type" : "tasks-factory"
+         },
+         "status" : "INVALID"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : false,
+         "name" : "my b task",
+         "path" : {
+            "ref" : "refs/meta/config",
+            "file" : "task.config",
+            "name" : "my b task",
+            "project" : "All-Projects",
+            "tasksFactory" : "tasks-factory example",
+            "type" : "tasks-factory"
+         },
+         "status" : "INVALID"
+      }
+   ]
+}
+
+[root "Root other PROJECT"]
+  subtasks-external = user ref
+
+[external "user ref"]
+  user = testuser
+  file = common.config
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root other PROJECT",
+   "path" : {
+      "ref" : "refs/meta/config",
+      "file" : "task.config",
+      "name" : "Root other PROJECT",
+      "project" : "All-Projects",
+      "type" : "root"
+   },
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "file task/common.config PASS",
+         "path" : {
+            "ref" : "{testuser_user_ref}",
+            "file" : "task/common.config",
+            "name" : "file task/common.config PASS",
+            "project" : "All-Users",
+            "user" : "testuser",
+            "type" : "task"
+         },
+         "status" : "PASS"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "file task/common.config FAIL",
+         "path" : {
+            "ref" : "{testuser_user_ref}",
+            "file" : "task/common.config",
+            "name" : "file task/common.config FAIL",
+            "project" : "All-Users",
+            "user" : "testuser",
+            "type" : "task"
+         },
+         "status" : "FAIL"
+      }
+   ]
+}
+```
+`task.config` file in project `All-Projects` on ref `refs/meta/config`.
+
+```
+[root "Root Capability Error"]
+    applicable = is:open
+    pass = true
+
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "name" : "Root Capability Error",
+   "path" : {
+      "error" : "Can't perform operation, need viewTaskPaths capability"
+   },
+   "status" : "PASS"
+}
+```
\ No newline at end of file
diff --git a/src/main/resources/Documentation/test/preview.md b/src/main/resources/Documentation/test/preview.md
index e53b7d8..df25a6e 100644
--- a/src/main/resources/Documentation/test/preview.md
+++ b/src/main/resources/Documentation/test/preview.md
@@ -1,3 +1,4 @@
+```
 [root "INVALIDS Preview"]
   subtasks-file = invalids.config
 
@@ -102,6 +103,22 @@
          ]
       },
       {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "Subtask Blank",
+         "status" : "WAITING",
+         "subTasks" : [
+            {
+               "name" : "UNKNOWN",
+               "status" : "INVALID"
+            }
+         ]
+      },
+      {
+         "name" : "Bad APPLICABLE query",
+         "status" : "INVALID"
+      },
+      {
          "applicable" : false,
          "hasPass" : true,
          "name" : "NA Bad PASS query",
@@ -122,9 +139,13 @@
          "status" : "INVALID"   # Only Test Suite: invalid
       },
       {
+         "name" : "UNKNOWN",
+         "status" : "INVALID"
+      },
+      {
          "applicable" : true,
          "hasPass" : false,
-         "name" : "Looping",
+         "name" : "task (tasks-factory missing)",
          "status" : "WAITING",
          "subTasks" : [
             {
@@ -134,13 +155,21 @@
          ]
       },
       {
-         "name" : "UNKNOWN",
-         "status" : "INVALID"
+         "applicable" : true,
+         "hasPass" : false,
+         "name" : "task (tasks-factory static INVALID)",
+         "status" : "WAITING",
+         "subTasks" : [
+            {
+               "name" : "UNKNOWN",
+               "status" : "INVALID"
+            }
+         ]
       },
       {
          "applicable" : true,
          "hasPass" : false,
-         "name" : "task (tasks-factory missing)",
+         "name" : "task (tasks-factory change INVALID)",
          "status" : "WAITING",
          "subTasks" : [
             {
@@ -176,6 +205,18 @@
       {
          "applicable" : true,
          "hasPass" : false,
+         "name" : "task (names-factory name Blank)",
+         "status" : "WAITING",
+         "subTasks" : [
+            {
+               "name" : "UNKNOWN",
+               "status" : "INVALID"
+            }
+         ]
+      },
+      {
+         "applicable" : true,
+         "hasPass" : false,
          "name" : "task (names-factory duplicate)",
          "status" : "WAITING",
          "subTasks" : [
@@ -226,38 +267,6 @@
                "status" : "INVALID"
             }
          ]
-      },
-      {
-         "applicable" : true,
-         "hasPass" : false,
-         "name" : "task (tasks-factory changes loop)",
-         "status" : "WAITING",
-         "subTasks" : [
-            {
-               "applicable" : true,
-               "hasPass" : true,
-               "name" : "_change1_number",
-               "status" : "FAIL",
-               "subTasks" : [
-                  {
-                     "name" : "UNKNOWN",
-                     "status" : "INVALID"
-                  }
-               ]
-            },
-            {
-               "applicable" : true,
-               "hasPass" : true,
-               "name" : "_change2_number",
-               "status" : "FAIL",
-               "subTasks" : [
-                  {
-                     "name" : "UNKNOWN",
-                     "status" : "INVALID"
-                  }
-               ]
-            }
-         ]
       }
    ]
 }
@@ -312,16 +321,28 @@
    "status" : "WAITING",
    "subTasks" : [
       {
-         "applicable" : true,
-         "hasPass" : true,
-         "name" : "userfile task/special.config PASS",
-         "status" : "PASS"
+         "applicable" : true,                              # Only Test Suite: secret
+         "hasPass" : true,                                 # Only Test Suite: secret
+         "name" : "userfile task/special.config PASS",     # Only Test Suite: secret
+         "status" : "PASS"                                 # Only Test Suite: secret
+         "name" : "UNKNOWN",                               # Only Test Suite: !secret
+         "status" : "UNKNOWN"                              # Only Test Suite: !secret
       },
       {
-         "applicable" : true,
-         "hasPass" : true,
-         "name" : "userfile task/special.config FAIL",
-         "status" : "FAIL"
+         "applicable" : true,                              # Only Test Suite: secret
+         "hasPass" : true,                                 # Only Test Suite: secret
+         "name" : "userfile task/special.config FAIL",     # Only Test Suite: secret
+         "status" : "FAIL"                                 # Only Test Suite: secret
+         "name" : "UNKNOWN",                               # Only Test Suite: !secret
+         "status" : "UNKNOWN"                              # Only Test Suite: !secret
+      },
+      {
+         "applicable" : true,                              # Only Test Suite: secret
+         "hasPass" : true,                                 # Only Test Suite: secret
+         "name" : "file task/common.config Preload PASS",  # Only Test Suite: secret
+         "status" : "PASS"                                 # Only Test Suite: secret
+         "name" : "UNKNOWN",                               # Only Test Suite: !secret
+         "status" : "UNKNOWN"                              # Only Test Suite: !secret
       }
    ]
 }
@@ -442,6 +463,22 @@
          ]
       },
       {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "Subtask Blank",
+         "status" : "WAITING",
+         "subTasks" : [
+            {
+               "name" : "UNKNOWN",
+               "status" : "INVALID"
+            }
+         ]
+      },
+      {
+         "name" : "Bad APPLICABLE query",
+         "status" : "INVALID"
+      },
+      {
          "applicable" : false,
          "hasPass" : true,
          "name" : "NA Bad PASS query",
@@ -462,9 +499,13 @@
          "status" : "INVALID"   # Only Test Suite: invalid
       },
       {
+         "name" : "UNKNOWN",
+         "status" : "INVALID"
+      },
+      {
          "applicable" : true,
          "hasPass" : false,
-         "name" : "Looping",
+         "name" : "task (tasks-factory missing)",
          "status" : "WAITING",
          "subTasks" : [
             {
@@ -474,13 +515,21 @@
          ]
       },
       {
-         "name" : "UNKNOWN",
-         "status" : "INVALID"
+         "applicable" : true,
+         "hasPass" : false,
+         "name" : "task (tasks-factory static INVALID)",
+         "status" : "WAITING",
+         "subTasks" : [
+            {
+               "name" : "UNKNOWN",
+               "status" : "INVALID"
+            }
+         ]
       },
       {
          "applicable" : true,
          "hasPass" : false,
-         "name" : "task (tasks-factory missing)",
+         "name" : "task (tasks-factory change INVALID)",
          "status" : "WAITING",
          "subTasks" : [
             {
@@ -516,6 +565,18 @@
       {
          "applicable" : true,
          "hasPass" : false,
+         "name" : "task (names-factory name Blank)",
+         "status" : "WAITING",
+         "subTasks" : [
+            {
+               "name" : "UNKNOWN",
+               "status" : "INVALID"
+            }
+         ]
+      },
+      {
+         "applicable" : true,
+         "hasPass" : false,
          "name" : "task (names-factory duplicate)",
          "status" : "WAITING",
          "subTasks" : [
@@ -566,38 +627,7 @@
                "status" : "INVALID"
             }
          ]
-      },
-      {
-         "applicable" : true,
-         "hasPass" : false,
-         "name" : "task (tasks-factory changes loop)",
-         "status" : "WAITING",
-         "subTasks" : [
-            {
-               "applicable" : true,
-               "hasPass" : true,
-               "name" : "_change1_number",
-               "status" : "FAIL",
-               "subTasks" : [
-                  {
-                     "name" : "UNKNOWN",
-                     "status" : "INVALID"
-                  }
-               ]
-            },
-            {
-               "applicable" : true,
-               "hasPass" : true,
-               "name" : "_change2_number",
-               "status" : "FAIL",
-               "subTasks" : [
-                  {
-                     "name" : "UNKNOWN",
-                     "status" : "INVALID"
-                  }
-               ]
-            }
-         ]
       }
    ]
 }
+```
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
new file mode 100644
index 0000000..3d1d7cc
--- /dev/null
+++ b/src/main/resources/Documentation/test/task-preview/new_root_with_original_with_external_secret_ref.md
@@ -0,0 +1,60 @@
+# --task-preview a new root, original root with subtasks-external pointing to secret user ref.
+
+file: `All-Projects.git:refs/meta/config:task.config`
+```
+ [root "Root with SECRET external"]
+     applicable = is:open
+     subtasks-external = SECRET
+
+ [external "SECRET"]
+     user = {secret_user}
+     file = secret.config
++
++[root "Root Preview Simple"]
++    subtask = simple task
+
++[task "simple task"]
++    applicable = is:open
++    pass = True
+```
+
+file: `All-Users.git:{secret_user_ref}:task/secret.config`
+```
+[task "SECRET task"]
+    applicable = is:open
+    pass = Fail
+```
+
+json:
+```
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root with SECRET external",
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "name" : "UNKNOWN",            # Only Test Suite: non-secret
+         "status" : "UNKNOWN"           # Only Test Suite: non-secret
+         "applicable" : true,           # Only Test Suite: secret
+         "hasPass" : true,              # Only Test Suite: secret
+         "name" : "SECRET task",        # Only Test Suite: secret
+         "status" : "READY"             # Only Test Suite: secret
+      }
+   ]
+}
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root Preview Simple",
+   "status" : "PASS",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "simple task",
+         "status" : "PASS"
+      }
+   ]
+}
+```
\ No newline at end of file
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
new file mode 100644
index 0000000..d192050
--- /dev/null
+++ b/src/main/resources/Documentation/test/task-preview/non-secret_ref_with_external_secret_ref.md
@@ -0,0 +1,60 @@
+# --task-preview a non-secret user ref with subtasks-external pointing to secret user ref.
+
+file: `All-Projects.git:refs/meta/config:task.config`
+```
+[root "Root for NON-SECRET external Preview with SECRET external"]
+    applicable = "is:open"
+    pass = True
+    subtasks-external = NON-SECRET
+
+[external "NON-SECRET"]
+    user = {non_secret_user}
+    file = sample.config
+```
+
+file: `All-Users:{non_secret_user_ref}:task/sample.config`
+```
+ [task "NON-SECRET task"]
+     applicable = is:open
+     pass = Fail
++    subtasks-external = SECRET
+
++[external "SECRET"]
++    user = {secret_user}
++    file = secret.config
+```
+
+file: `All-Users.git:{secret_user_ref}:task/secret.config`
+```
+[task "SECRET task"]
+    applicable = is:open
+    pass = Fail
+```
+
+json:
+```
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "name" : "Root for NON-SECRET external Preview with SECRET external",
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "NON-SECRET task",
+         "status" : "WAITING",
+         "subTasks" : [
+            {
+               "name" : "UNKNOWN",            # Only Test Suite: non-secret
+               "status" : "UNKNOWN"           # Only Test Suite: non-secret
+               "applicable" : true,           # Only Test Suite: secret
+               "hasPass" : true,              # Only Test Suite: secret
+               "name" : "SECRET task",        # Only Test Suite: secret
+               "status" : "READY"             # Only Test Suite: secret
+            }
+         ]
+      }
+   ]
+}
+```
\ No newline at end of file
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
new file mode 100644
index 0000000..a0f11eb
--- /dev/null
+++ b/src/main/resources/Documentation/test/task-preview/non_root_with_subtask_from_root_task.md
@@ -0,0 +1,47 @@
+# --task-preview non-root file with subtask pointing root task
+
+file: `All-Projects.git:refs/meta/config:task.config`
+```
+[root "Points to subFile task with rootFile task preview"]
+    applicable = is:open
+    pass = True
+    subtask = foo/bar/baz.config^Preview pointing to rootFile task
+
+[task "Task in rootFile"]
+    applicable = is:open
+    pass = True
+```
+
+file: `All-Projects.git:refs/meta/config:task/foo/bar/baz.config`
+```
+ [task "Preview pointing to rootFile task"]
+     applicable = is:open
+     pass = Fail
++    subtask = ^Task in rootFile
+```
+
+json:
+```
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "name" : "Points to subFile task with rootFile task preview",
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "Preview pointing to rootFile task",
+         "status" : "READY",
+         "subTasks" : [
+            {
+               "applicable" : true,
+               "hasPass" : true,
+               "name" : "Task in rootFile",
+               "status" : "PASS"
+            }
+         ]
+      }
+   ]
+}
+```
\ No newline at end of file
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
new file mode 100644
index 0000000..c0add08
--- /dev/null
+++ b/src/main/resources/Documentation/test/task-preview/root_with_external_non-secret_ref_with_external_secret_ref.md
@@ -0,0 +1,60 @@
+# --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`
+```
+ [root "Root Preview NON-SECRET external with SECRET external"]
+     applicable = "is:open"
+     pass = True
++    subtasks-external = NON-SECRET with SECRET External
+
++[external "NON-SECRET with SECRET External"]
++    user = {non_secret_user}
++    file = secret_external.config
+```
+
+file: `All-Users.git:{non_secret_user_ref}:task/secret_external.config`
+```
+[task "NON-SECRET with SECRET external"]
+    applicable = is:open
+    pass = True
+    subtasks-external = SECRET external
+
+[external "SECRET external"]
+    user = {secret_user}
+    file = secret.config
+```
+
+file: `All-Users:{secret_user_ref}:task/secret.config`
+```
+[task "SECRET task"]
+    applicable = is:open
+    pass = Fail
+```
+
+json:
+```
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "name" : "Root Preview NON-SECRET external with SECRET external",
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "NON-SECRET with SECRET external",
+         "status" : "WAITING",
+         "subTasks" : [
+            {
+               "name" : "UNKNOWN",            # Only Test Suite: non-secret
+               "status" : "UNKNOWN"           # Only Test Suite: non-secret
+               "applicable" : true,           # Only Test Suite: secret
+               "hasPass" : true,              # Only Test Suite: secret
+               "name" : "SECRET task",        # Only Test Suite: secret
+               "status" : "READY"             # Only Test Suite: secret
+            }
+         ]
+      }
+   ]
+}
+```
\ No newline at end of file
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
new file mode 100644
index 0000000..4d81b12
--- /dev/null
+++ b/src/main/resources/Documentation/test/task-preview/root_with_external_secret_ref.md
@@ -0,0 +1,40 @@
+# --task-preview root file with subtasks-external pointing to secret user ref
+
+file: `All-Projects.git:refs/meta/config:task.config`
+```
+ [root "Root Preview SECRET external"]
+     applicable = is:open
+     pass = True
++    subtasks-external = SECRET external
+
++[external "SECRET external"]
++    user = {secret_user}
++    file = secret.config
+```
+
+file: `All-Users.git:{secret_user_ref}:task/secret.config`
+```
+[task "SECRET Task"]
+    applicable = is:open
+    pass = Fail
+```
+
+json:
+```
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "name" : "Root Preview SECRET external",
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "name" : "UNKNOWN",                  # Only Test Suite: non-secret
+         "status" : "UNKNOWN"                 # Only Test Suite: non-secret
+         "applicable" : true,                 # Only Test Suite: secret
+         "hasPass" : true,                    # Only Test Suite: secret
+         "name" : "SECRET Task",              # Only Test Suite: secret
+         "status" : "READY"                   # Only Test Suite: secret
+      }
+   ]
+}
+```
\ No newline at end of file
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
new file mode 100644
index 0000000..504d5a8
--- /dev/null
+++ b/src/main/resources/Documentation/test/task-preview/subtask_using_group_syntax/root_with_subtask_non-secret_ref_with_subtask_secret_ref.md
@@ -0,0 +1,52 @@
+# --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`
+```
+ [root "Root Preview NON-SECRET group subtask with SECRET group subtask"]
+     applicable = "is:open"
+     pass = True
++    subtask = %{non_secret_group_name}/secret_external.config^NON-SECRET with SECRET subtask
+```
+
+file: `All-Users.git:refs/groups/{sharded_non_secret_group_uuid}:task/secret_external.config`
+```
+[task "NON-SECRET with SECRET subtask"]
+    applicable = is:open
+    pass = True
+    subtask = %{secret_group_name}/secret.config^SECRET task
+```
+
+file: `All-Users:refs/groups/{sharded_secret_group_uuid}:task/secret.config`
+```
+[task "SECRET task"]
+    applicable = is:open
+    pass = Fail
+```
+
+json:
+```
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "name" : "Root Preview NON-SECRET group subtask with SECRET group subtask",
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "NON-SECRET with SECRET subtask",
+         "status" : "WAITING",
+         "subTasks" : [
+            {
+               "name" : "UNKNOWN",            # Only Test Suite: non-secret
+               "status" : "UNKNOWN"           # Only Test Suite: non-secret
+               "applicable" : true,           # Only Test Suite: secret
+               "hasPass" : true,              # Only Test Suite: secret
+               "name" : "SECRET task",        # Only Test Suite: secret
+               "status" : "READY"             # Only Test Suite: secret
+            }
+         ]
+      }
+   ]
+}
+```
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
new file mode 100644
index 0000000..9a1932c
--- /dev/null
+++ b/src/main/resources/Documentation/test/task-preview/subtask_using_group_syntax/root_with_subtask_secret_ref.md
@@ -0,0 +1,36 @@
+# --task-preview root file with subtask pointing to secret group ref
+
+file: `All-Projects.git:refs/meta/config:task.config`
+```
+ [root "Root Preview SECRET external group"]
+     applicable = is:open
+     pass = True
++    subtask = %{secret_group_name}/secret.config^SECRET Task
+```
+
+file: `All-Users.git:refs/groups/{sharded_secret_group_uuid}:task/secret.config`
+```
+[task "SECRET Task"]
+    applicable = is:open
+    pass = Fail
+```
+
+json:
+```
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "name" : "Root Preview SECRET external group",
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "name" : "UNKNOWN",                  # Only Test Suite: non-secret
+         "status" : "UNKNOWN"                 # Only Test Suite: non-secret
+         "applicable" : true,                 # Only Test Suite: secret
+         "hasPass" : true,                    # Only Test Suite: secret
+         "name" : "SECRET Task",              # Only Test Suite: secret
+         "status" : "READY"                   # Only Test Suite: secret
+      }
+   ]
+}
+```
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
new file mode 100644
index 0000000..fad2f1d
--- /dev/null
+++ b/src/main/resources/Documentation/test/task-preview/subtask_using_user_syntax/root_with_subtask_non-secret_ref_with_subtask_secret_ref.md
@@ -0,0 +1,52 @@
+# --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`
+```
+ [root "Root Preview NON-SECRET subtask with SECRET subtask"]
+     applicable = "is:open"
+     pass = True
++    subtask = @{non_secret_user}/secret_external.config^NON-SECRET with SECRET subtask
+```
+
+file: `All-Users.git:{non_secret_user_ref}:task/secret_external.config`
+```
+[task "NON-SECRET with SECRET subtask"]
+    applicable = is:open
+    pass = True
+    subtask = @{secret_user}/secret.config^SECRET task
+```
+
+file: `All-Users:{secret_user_ref}:task/secret.config`
+```
+[task "SECRET task"]
+    applicable = is:open
+    pass = Fail
+```
+
+json:
+```
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "name" : "Root Preview NON-SECRET subtask with SECRET subtask",
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "NON-SECRET with SECRET subtask",
+         "status" : "WAITING",
+         "subTasks" : [
+            {
+               "name" : "UNKNOWN",            # Only Test Suite: non-secret
+               "status" : "UNKNOWN"           # Only Test Suite: non-secret
+               "applicable" : true,           # Only Test Suite: secret
+               "hasPass" : true,              # Only Test Suite: secret
+               "name" : "SECRET task",        # Only Test Suite: secret
+               "status" : "READY"             # Only Test Suite: secret
+            }
+         ]
+      }
+   ]
+}
+```
\ No newline at end of file
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
new file mode 100644
index 0000000..d2b3349
--- /dev/null
+++ b/src/main/resources/Documentation/test/task-preview/subtask_using_user_syntax/root_with_subtask_secret_ref.md
@@ -0,0 +1,36 @@
+# --task-preview root file with subtask pointing to secret user ref
+
+file: `All-Projects.git:refs/meta/config:task.config`
+```
+ [root "Root Preview SECRET external"]
+     applicable = is:open
+     pass = True
++    subtask = @{secret_user}/secret.config^SECRET Task
+```
+
+file: `All-Users.git:{secret_user_ref}:task/secret.config`
+```
+[task "SECRET Task"]
+    applicable = is:open
+    pass = Fail
+```
+
+json:
+```
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "name" : "Root Preview SECRET external",
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "name" : "UNKNOWN",                  # Only Test Suite: non-secret
+         "status" : "UNKNOWN"                 # Only Test Suite: non-secret
+         "applicable" : true,                 # Only Test Suite: secret
+         "hasPass" : true,                    # Only Test Suite: secret
+         "name" : "SECRET Task",              # Only Test Suite: secret
+         "status" : "READY"                   # Only Test Suite: secret
+      }
+   ]
+}
+```
\ No newline at end of file
diff --git a/src/main/resources/Documentation/test/task_states.md b/src/main/resources/Documentation/test/task_states.md
index ea09541..7a6a8d0 100644
--- a/src/main/resources/Documentation/test/task_states.md
+++ b/src/main/resources/Documentation/test/task_states.md
@@ -17,6 +17,7 @@
 The config below is expected to be in the `task.config` file in project
 `All-Projects` on ref `refs/meta/config`.
 
+file: `All-Projects:refs/meta/config:task.config`
 ```
 [root "Root N/A"]
   applicable = is:closed # Assumes test query is "is:open"
@@ -55,12 +56,12 @@
 [root "Root PASS"]
   pass = True
 
-{
-   "applicable" : true,
-   "hasPass" : true,
-   "name" : "Root PASS",
-   "status" : "PASS"
-}
+{                              # Test Suite: task_only
+   "applicable" : true,        # Test Suite: task_only
+   "hasPass" : true,           # Test Suite: task_only
+   "name" : "Root PASS",       # Test Suite: task_only
+   "status" : "PASS"           # Test Suite: task_only
+}                              # Test Suite: task_only
 
 [root "Root FAIL"]
   fail = True
@@ -72,6 +73,26 @@
    "status" : "FAIL"
 }
 
+[root "Root PASS SR"]
+  pass = is:true_task
+
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "name" : "Root PASS SR",
+   "status" : "PASS"
+}
+
+[root "Root FAIL SR"]
+  fail = is:true_task
+
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "name" : "Root FAIL SR",
+   "status" : "FAIL"
+}
+
 [root "Root straight PASS"]
   applicable = is:open
   pass = is:open
@@ -570,6 +591,12 @@
          "hasPass" : true,
          "name" : "userfile task/special.config FAIL",
          "status" : "FAIL"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "file task/common.config Preload PASS",
+         "status" : "PASS"
       }
    ]
 }
@@ -597,6 +624,12 @@
          "status" : "FAIL"
       },
       {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "file task/common.config Preload PASS",
+         "status" : "PASS"
+      },
+      {
          "name" : "UNKNOWN",
          "status" : "INVALID"
       }
@@ -630,6 +663,12 @@
          "status" : "FAIL"
       },
       {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "file task/common.config Preload PASS",
+         "status" : "PASS"
+      },
+      {
          "name" : "UNKNOWN",
          "status" : "INVALID"
       }
@@ -661,6 +700,12 @@
          "hasPass" : true,
          "name" : "userfile task/special.config FAIL",
          "status" : "FAIL"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "file task/common.config Preload PASS",
+         "status" : "PASS"
       }
    ]
 }
@@ -724,12 +769,14 @@
    "subTasks" : [
       {
          "applicable" : true,
+         "change" : _change1_number,
          "hasPass" : true,
          "name" : "_change1_number",
          "status" : "FAIL"
       },
       {
          "applicable" : true,
+         "change" : _change2_number,
          "hasPass" : true,
          "name" : "_change2_number",
          "status" : "FAIL"
@@ -765,37 +812,192 @@
    "status" : "PASS"
 }
 
-[root "Root Properties"]
-  set-root-property = root-value
-  subtask = Subtask Properties
+[root "Root Same Name - Different Tasks-Factory"]
+  subtasks-factory = parent tasks-factory Same Name - Different Tasks-Factory
 
-[task "Subtask Properties"]
-  subtask = Subtask Properties Hints
-
-[task "Subtask Properties Hints"]
-  set-first-property = first-value
-  set-second-property = ${first-property} second-extra ${third-property}
-  set-third-property = third-value
+[tasks-factory "parent tasks-factory Same Name - Different Tasks-Factory"]
+  names-factory = parent names-factory Same Name - Different Tasks-Factory
   fail = True
-  fail-hint = root-property(${root-property}) first-property(${first-property}) second-property(${second-property})
+  subtasks-factory = child tasks-factory Same Name - Different Tasks-Factory
+
+[names-factory "parent names-factory Same Name - Different Tasks-Factory"]
+  type = static
+  name = Same Name
+
+[tasks-factory "child tasks-factory Same Name - Different Tasks-Factory"]
+  names-factory = child names-factory Same Name - Different Tasks-Factory
+  fail = False
+
+[names-factory "child names-factory Same Name - Different Tasks-Factory"]
+  type = static
+  name = Same Name
 
 {
    "applicable" : true,
    "hasPass" : false,
-   "name" : "Root Properties",
+   "name" : "Root Same Name - Different Tasks-Factory",
    "status" : "WAITING",
    "subTasks" : [
       {
          "applicable" : true,
+         "hasPass" : true,
+         "name" : "Same Name",
+         "status" : "FAIL",
+         "subTasks" : [
+            {
+               "applicable" : true,
+               "hasPass" : true,
+               "name" : "Same Name",
+               "status" : "PASS"
+            }
+         ]
+      }
+   ]
+}
+
+[root "Root Same Name - Different Change"]
+  subtasks-factory = init tasks-factory Same Name - Different Change
+
+[tasks-factory "init tasks-factory Same Name - Different Change"]
+  names-factory = init names-factory Same Name - Different Change
+  subtask = Same Name - Different Change
+
+[names-factory "init names-factory Same Name - Different Change"]
+  type = change
+  changes = change:_change2_number
+
+[task "Same Name - Different Change"]
+  subtasks-factory = tasks-factory Same Name - Different Change
+  pass = False
+  ready-hint = continues on to change _change1_number
+  fail-hint = stops here since we are change _change1_number
+  fail = change:_change1_number
+
+[tasks-factory "tasks-factory Same Name - Different Change"]
+  names-factory = names-factory Same Name - Different Change
+  subtask = Same Name - Different Change
+
+[names-factory "names-factory Same Name - Different Change"]
+  type = change
+  changes = change:_change1_number NOT change:${_change_number}
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root Same Name - Different Change",
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "change" : _change2_number,
          "hasPass" : false,
-         "name" : "Subtask Properties",
+         "name" : "_change2_number",
          "status" : "WAITING",
          "subTasks" : [
             {
                "applicable" : true,
                "hasPass" : true,
-               "hint" : "root-property(root-value) first-property(first-value) second-property(first-value second-extra third-value)",
-               "name" : "Subtask Properties Hints",
+               "name" : "Same Name - Different Change",
+               "status" : "WAITING",
+               "subTasks" : [
+                  {
+                     "applicable" : true,
+                     "change" : _change1_number,
+                     "hasPass" : false,
+                     "name" : "_change1_number",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "hint" : "stops here since we are change _change1_number",
+                           "name" : "Same Name - Different Change",
+                           "status" : "FAIL"
+                        }
+                     ]
+                  }
+               ]
+            }
+         ]
+      }
+   ]
+}
+
+[root "Root Property References"]
+  set-first-property = first-value
+  set-backward-reference = first-[${first-property}]
+  set-forward-reference = last-[${last-property}]
+  set-last-property = last-value
+  fail = True
+  fail-hint = backward-reference(${backward-reference}) forward-reference(${forward-reference})
+
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "hint" : "backward-reference(first-[first-value]) forward-reference(last-[last-value])",
+   "name" : "Root Property References",
+   "status" : "FAIL"
+}
+
+[root "Root Deep Property References"]
+  set-first-property = first-value
+  set-direct-reference = first-[${first-property}]
+  set-deep-reference = deep-{${direct-reference}}
+  fail = True
+  fail-hint = deep-reference(${deep-reference})
+
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "hint" : "deep-reference(deep-{first-[first-value]})",
+   "name" : "Root Deep Property References",
+   "status" : "FAIL"
+}
+
+[root "Root Properties Referenced Twice"]
+  set-first-property = first-value
+  set-referenced-twice = first-[${first-property}] first-[${first-property}]
+  fail = True
+  fail-hint = first-[${first-property}] referenced-twice(${referenced-twice}) referenced-twice(${referenced-twice})
+
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "hint" : "first-[first-value] referenced-twice(first-[first-value] first-[first-value]) referenced-twice(first-[first-value] first-[first-value])",
+   "name" : "Root Properties Referenced Twice",
+   "status" : "FAIL"
+}
+
+[root "Root Inherited Properties"]
+  set-root-property = root-value
+  subtask = Subtask Parent Inherited Properties
+
+[task "Subtask Parent Inherited Properties"]
+  set-parent-property = parent-value
+  subtask = Subtask Inherited Properties
+
+[task "Subtask Inherited Properties"]
+  set-my-property = my-value
+  fail = True
+  fail-hint = root-property(${root-property}) parent-property(${parent-property}) my-property(${my-property})
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root Inherited Properties",
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : false,
+         "name" : "Subtask Parent Inherited Properties",
+         "status" : "WAITING",
+         "subTasks" : [
+            {
+               "applicable" : true,
+               "hasPass" : true,
+               "hint" : "root-property(root-value) parent-property(parent-value) my-property(my-value)",
+               "name" : "Subtask Inherited Properties",
                "status" : "FAIL"
             }
          ]
@@ -803,6 +1005,41 @@
    ]
 }
 
+[root "Root Inherited Distant Properties"]
+  set-root-property = root-value
+  set-root-change-property = ${_change_number}
+  subtask = Subtask Parent Inherited Distant Properties
+
+[task "Subtask Parent Inherited Distant Properties"]
+  subtask = Subtask Inherited Distant Properties
+
+[task "Subtask Inherited Distant Properties"]
+  fail = True
+  fail-hint = root-property(${root-property}) root-change-property(${root-change-property})
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root Inherited Distant Properties",
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : false,
+         "name" : "Subtask Parent Inherited Distant Properties",
+         "status" : "WAITING",
+         "subTasks" : [
+            {
+               "applicable" : true,
+               "hasPass" : true,
+               "hint" : "root-property(root-value) root-change-property(_change_number)",
+               "name" : "Subtask Inherited Distant Properties",
+               "status" : "FAIL"
+            }
+         ]
+      }
+   ]
+}
 
 [root "Root Properties Reset By Subtask"]
   set-root-to-reset-by-subtask = reset-my-root-value
@@ -829,6 +1066,46 @@
    ]
 }
 
+[root "Root Inherited Property References"]
+  set-root-property = root-value
+  subtask = Subtask Parent Inherited Property References
+
+[task "Subtask Parent Inherited Property References"]
+  set-parent-property = parent-value
+  set-parent-inherited-root-reference = root-property(${root-property})
+  subtask = Subtask Inherited Property References
+
+[task "Subtask Inherited Property References"]
+  set-inherited-root-reference = root-[${root-property}]
+  set-inherited-parent-reference = parent-[${parent-property}]
+  set-inherited-root-deep-reference = parent-inherited-root-reference-[${parent-inherited-root-reference}]
+  fail = True
+  fail-hint = inherited-root-reference(${inherited-root-reference}) inherited-parent-reference(${inherited-parent-reference}) inherited-root-deep-reference(${inherited-root-deep-reference})
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root Inherited Property References",
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : false,
+         "name" : "Subtask Parent Inherited Property References",
+         "status" : "WAITING",
+         "subTasks" : [
+            {
+               "applicable" : true,
+               "hasPass" : true,
+               "hint" : "inherited-root-reference(root-[root-value]) inherited-parent-reference(parent-[parent-value]) inherited-root-deep-reference(parent-inherited-root-reference-[root-property(root-value)])",
+               "name" : "Subtask Inherited Property References",
+               "status" : "FAIL"
+            }
+         ]
+      }
+   ]
+}
+
 [root "Root Properties Exports"]
   export-root-exported = ${_name}
   subtask = Subtask Properties Exports
@@ -927,6 +1204,99 @@
    ]
 }
 
+[root "Root applicable Property"]
+  subtask = Subtask applicable Property
+  subtasks-factory = tasks-factory branch NOT applicable Property
+
+[tasks-factory "tasks-factory branch NOT applicable Property"]
+  names-factory = names-factory branch NOT applicable Property
+  applicable = branch:dev
+  fail = True
+
+[names-factory "names-factory branch NOT applicable Property"]
+  type = static
+  name = NOT Applicable 1
+  name = NOT Applicable 2
+  name = NOT Applicable 3
+
+[task "Subtask applicable Property"]
+  applicable = change:${_change_number}
+  fail = True
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root applicable Property",
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "Subtask applicable Property",
+         "status" : "FAIL"
+      },                              # Only Test Suite: all
+      {                               # Only Test Suite: all
+         "applicable" : false,        # Only Test Suite: all
+         "hasPass" : true,            # Only Test Suite: all
+         "name" : "NOT Applicable 1", # Only Test Suite: all
+         "status" : "FAIL"            # Only Test Suite: all
+      },                              # Only Test Suite: all
+      {                               # Only Test Suite: all
+         "applicable" : false,        # Only Test Suite: all
+         "hasPass" : true,            # Only Test Suite: all
+         "name" : "NOT Applicable 2", # Only Test Suite: all
+         "status" : "FAIL"            # Only Test Suite: all
+      },                              # Only Test Suite: all
+      {                               # Only Test Suite: all
+         "applicable" : false,        # Only Test Suite: all
+         "hasPass" : true,            # Only Test Suite: all
+         "name" : "NOT Applicable 3", # Only Test Suite: all
+         "status" : "FAIL"            # Only Test Suite: all
+      }
+   ]
+}
+
+[root "Root branch applicable Property"]
+  subtasks-factory = tasks-factory branch applicable Property
+
+[tasks-factory "tasks-factory branch applicable Property"]
+  names-factory = names-factory branch applicable Property
+  applicable = branch:master
+  fail = True
+
+[names-factory "names-factory branch applicable Property"]
+  type = static
+  name = Applicable 1
+  name = Applicable 2
+  name = Applicable 3
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root branch applicable Property",
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "Applicable 1",
+         "status" : "FAIL"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "Applicable 2",
+         "status" : "FAIL"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "Applicable 3",
+         "status" : "FAIL"
+      }
+   ]
+}
+
 [root "Root Properties tasks-factory STATIC"]
   subtasks-factory = tasks-factory STATIC Properties
 
@@ -973,7 +1343,7 @@
   set-welcome-message = Welcome to the pleasuredome
   names-factory = names-factory a change
   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
+  fail = change:_change1_number
 
 [names-factory "names-factory a change"]
   type = change
@@ -987,6 +1357,7 @@
    "subTasks" : [
       {
          "applicable" : true,
+         "change" : _change1_number,
          "hasPass" : true,
          "hint" : "Welcome to the pleasuredome Name(_change1_number) Change Number(_change1_number) Change Id(_change1_id) Change Project(_change1_project) Change Branch(_change1_branch) Change Status(_change1_status) Change Topic(_change1_topic)",
          "name" : "_change1_number",
@@ -994,9 +1365,48 @@
       },
       {
          "applicable" : true,
+         "change" : _change2_number,
          "hasPass" : true,
-         "hint" : "Welcome to the pleasuredome Name(_change2_number) Change Number(_change2_number) Change Id(_change2_id) Change Project(_change2_project) Change Branch(_change2_branch) Change Status(_change2_status) Change Topic(_change2_topic)",
          "name" : "_change2_number",
+         "status" : "PASS"
+      }
+   ]
+}
+
+[root "Root tasks-factory _name Property Reference"]
+  subtasks-factory = Properties tasks-factory _name Property Reference
+
+[tasks-factory "Properties tasks-factory _name Property Reference"]
+  set-name-reference = first-property ${_name}
+  fail-hint = ${name-reference}
+  fail = true
+  names-factory = names-factory static list
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root tasks-factory _name Property Reference",
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "hint" : "first-property my a task",
+         "name" : "my a task",
+         "status" : "FAIL"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "hint" : "first-property my b task",
+         "name" : "my b task",
+         "status" : "FAIL"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "hint" : "first-property my c task",
+         "name" : "my c task",
          "status" : "FAIL"
       }
    ]
@@ -1082,12 +1492,14 @@
    "subTasks" : [
       {
          "applicable" : true,
+         "change" : _change_number,
          "hasPass" : true,
          "name" : "_change_number",
          "status" : "FAIL"
       },
       {
          "applicable" : true,
+         "change" : _change1_number,
          "hasPass" : true,
          "name" : "_change1_number",
          "status" : "FAIL"
@@ -1095,50 +1507,39 @@
    ]
 }
 
-[root "Root Properties Expansion"]
-  applicable = status:open
-  subtask = Subtask Property Expansion fail-hint
+[root "Root CHANGE constant subtask list CHANGE Properties"]
+  subtasks-factory = tasks-factory Properties CHANGE names-factory CHANGE
 
-[task "Subtask Property Expansion fail-hint"]
-  subtasks-factory = tasks-factory Property Expansion fail-hint
+[tasks-factory "tasks-factory Properties CHANGE names-factory CHANGE"]
+  names-factory = Properties names-factory current CHANGE
+  subtask = Current CHANGE Property
 
-[tasks-factory "tasks-factory Property Expansion fail-hint"]
-  set-first-property = first-property ${_name}
-  fail-hint = ${first-property}
-  fail = true
-  names-factory = names-factory static list
+[task "Current CHANGE Property"]
+  fail = True
+  fail-hint = Current Change: ${_change_number}
+
+[names-factory "Properties names-factory current CHANGE"]
+  type = change
+  changes = change:${_change_number}
 
 {
    "applicable" : true,
    "hasPass" : false,
-   "name" : "Root Properties Expansion",
+   "name" : "Root CHANGE constant subtask list CHANGE Properties",
    "status" : "WAITING",
    "subTasks" : [
       {
          "applicable" : true,
+         "change" : _change_number,
          "hasPass" : false,
-         "name" : "Subtask Property Expansion fail-hint",
+         "name" : "_change_number",
          "status" : "WAITING",
          "subTasks" : [
             {
                "applicable" : true,
                "hasPass" : true,
-               "hint" : "first-property my a task",
-               "name" : "my a task",
-               "status" : "FAIL"
-            },
-            {
-               "applicable" : true,
-               "hasPass" : true,
-               "hint" : "first-property my b task",
-               "name" : "my b task",
-               "status" : "FAIL"
-            },
-            {
-               "applicable" : true,
-               "hasPass" : true,
-               "hint" : "first-property my c task",
-               "name" : "my c task",
+               "hint" : "Current Change: _change_number",
+               "name" : "Current CHANGE Property",
                "status" : "FAIL"
             }
          ]
@@ -1176,6 +1577,158 @@
    ]
 }
 
+[root "Root Properties names-factory Reference"]
+  subtasks-factory = tasks-factory Properties names-factory Reference
+  set-predicate = change:_change1_number
+
+[tasks-factory "tasks-factory Properties names-factory Reference"]
+  names-factory = Properties names-factory Reference
+  fail = True
+
+[names-factory "Properties names-factory Reference"]
+  type = change
+  changes = ${predicate} OR change:${_change_number} project:${_change_project} branch:${_change_branch}
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root Properties names-factory Reference",
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "change" : _change_number,
+         "hasPass" : true,
+         "name" : "_change_number",
+         "status" : "FAIL"
+      },
+      {
+         "applicable" : true,
+         "change" : _change1_number,
+         "hasPass" : true,
+         "name" : "_change1_number",
+         "status" : "FAIL"
+      }
+   ]
+}
+
+[root "Root Properties names-factory Deep Reference"]
+  subtasks-factory = tasks-factory Properties names-factory Deep Reference
+  set-predicate-reference = ${predicate}
+  set-predicate = change:_change1_number
+
+[tasks-factory "tasks-factory Properties names-factory Deep Reference"]
+  names-factory = Properties names-factory Deep Reference
+  fail = True
+
+[names-factory "Properties names-factory Deep Reference"]
+  type = change
+  changes = ${predicate-reference} OR change:${_change_number} project:${_change_project} branch:${_change_branch}
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root Properties names-factory Deep Reference",
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "change" : _change_number,
+         "hasPass" : true,
+         "name" : "_change_number",
+         "status" : "FAIL"
+      },
+      {
+         "applicable" : true,
+         "change" : _change1_number,
+         "hasPass" : true,
+         "name" : "_change1_number",
+         "status" : "FAIL"
+      }
+   ]
+}
+
+[root "Root Properties names-factory Reference Internal"]
+  subtasks-factory = tasks-factory Properties names-factory Reference Internal
+  set-predicate = change:${_change_number} project:${_change_project} branch:${_change_branch}
+
+[tasks-factory "tasks-factory Properties names-factory Reference Internal"]
+  names-factory = Properties names-factory Reference Internal
+  fail = True
+
+[names-factory "Properties names-factory Reference Internal"]
+  type = change
+  changes = change:_change1_number OR ${predicate}
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root Properties names-factory Reference Internal",
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "change" : _change_number,
+         "hasPass" : true,
+         "name" : "_change_number",
+         "status" : "FAIL"
+      },
+      {
+         "applicable" : true,
+         "change" : _change1_number,
+         "hasPass" : true,
+         "name" : "_change1_number",
+         "status" : "FAIL"
+      }
+   ]
+}
+
+[root "Root Properties names-factory Reference Inherited"]
+  subtask = task Properties names-factory Reference Inherited
+  set-predicate = change:${_change_number} project:${_change_project} branch:${_change_branch}
+
+[task "task Properties names-factory Reference Inherited"]
+  subtasks-factory = tasks-factory Properties names-factory Reference Inherited
+
+[tasks-factory "tasks-factory Properties names-factory Reference Inherited"]
+  names-factory = Properties names-factory Reference Inherited
+  fail = True
+
+[names-factory "Properties names-factory Reference Inherited"]
+  type = change
+  changes = change:_change1_number OR ${predicate}
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root Properties names-factory Reference Inherited",
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : false,
+         "name" : "task Properties names-factory Reference Inherited",
+         "status" : "WAITING",
+         "subTasks" : [
+            {
+               "applicable" : true,
+               "change" : _change_number,
+               "hasPass" : true,
+               "name" : "_change_number",
+               "status" : "FAIL"
+            },
+            {
+               "applicable" : true,
+               "change" : _change1_number,
+               "hasPass" : true,
+               "name" : "_change1_number",
+               "status" : "FAIL"
+            }
+         ]
+      }
+   ]
+}
+
 [root "Root Preload Preload"]
   subtask = Subtask Preload Preload
 
@@ -1354,10 +1907,17 @@
   subtask = Subtask Preload Properties
 
 [task "Subtask Preload Properties"]
-  preload-task = Subtask Properties Hints
+  preload-task = Subtask Preload Properties Hints
   set-fourth-property = fourth-value
   fail-hint = second-property(${second-property}) fourth-property(${fourth-property})
 
+[task "Subtask Preload Properties Hints"]
+  set-first-property = first-value
+  set-second-property = ${first-property} second-extra ${third-property}
+  set-third-property = third-value
+  fail = True
+  fail-hint = root-property(${root-property}) first-property(${first-property}) second-property(${second-property})
+
 {
    "applicable" : true,
    "hasPass" : false,
@@ -1374,13 +1934,834 @@
    ]
 }
 
+[root "Root Preload tasks-factory"]
+  subtasks-factory = tasks-factory Preload tasks-factory
+
+[tasks-factory "tasks-factory Preload tasks-factory"]
+  names-factory = names-factory static list
+  preload-task = Subtask PASS
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root Preload tasks-factory",
+   "status" : "PASS",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "my a task",
+         "status" : "PASS"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "my b task",
+         "status" : "PASS"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "my c task",
+         "status" : "PASS"
+      }
+   ]
+}
+
+[root "Root Looping"]
+  subtask = Looping
+
+[task "Looping"]
+  subtask = Looping
+  pass = True
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root Looping",
+   "status" : "PASS",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "Looping",
+         "status" : "PASS",
+         "subTasks" : [
+            {
+               "applicable" : true,
+               "hasPass" : false,
+               "hint" : "Duplicate task is non blocking and empty to break the loop",
+               "name" : "Looping",
+               "status" : "DUPLICATE"
+            }
+         ]
+      }
+   ]
+}
+
+[root "Root Looping DuplicateKey"]
+  preload-task = DuplicateKey
+
+[task "Looping DuplicateKey"]
+  preload-task = DuplicateKey
+  pass = True
+
+[task "DuplicateKey"]
+  duplicate-key = 1234
+  subtask = Looping DuplicateKey
+
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root Looping DuplicateKey",
+   "status" : "PASS",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : false,
+         "hint" : "Duplicate task is non blocking and empty to break the loop",
+         "name" : "Looping DuplicateKey",
+         "status" : "DUPLICATE"
+      }
+   ]
+}
+
+[root "Root changes loop"]
+  subtask = task (tasks-factory changes loop)
+
+[task "task (tasks-factory changes loop)"]
+  subtasks-factory = tasks-factory change loop
+
+[tasks-factory "tasks-factory change loop"]
+  names-factory = names-factory change constant
+  subtask = task (tasks-factory changes loop)
+  fail = True
+
+[names-factory "names-factory change constant"]
+  changes = change:_change1_number OR change:_change2_number
+  type = change
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root changes loop",
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : false,
+         "name" : "task (tasks-factory changes loop)",
+         "status" : "WAITING",
+         "subTasks" : [
+            {
+               "applicable" : true,
+               "change" : _change1_number,
+               "hasPass" : true,
+               "name" : "_change1_number",
+               "status" : "FAIL",
+               "subTasks" : [
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (tasks-factory changes loop)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "applicable" : true,
+                           "change" : _change1_number,
+                           "hasPass" : false,
+                           "hint" : "Duplicate task is non blocking and empty to break the loop",
+                           "name" : "_change1_number",
+                           "status" : "DUPLICATE"
+                        },
+                        {
+                           "applicable" : true,
+                           "change" : _change2_number,
+                           "hasPass" : true,
+                           "name" : "_change2_number",
+                           "status" : "FAIL",
+                           "subTasks" : [
+                              {
+                                 "applicable" : true,
+                                 "hasPass" : false,
+                                 "name" : "task (tasks-factory changes loop)",
+                                 "status" : "PASS",
+                                 "subTasks" : [
+                                    {
+                                       "applicable" : true,
+                                       "change" : _change1_number,
+                                       "hasPass" : false,
+                                       "hint" : "Duplicate task is non blocking and empty to break the loop",
+                                       "name" : "_change1_number",
+                                       "status" : "DUPLICATE"
+                                    },
+                                    {
+                                       "applicable" : true,
+                                       "change" : _change2_number,
+                                       "hasPass" : false,
+                                       "hint" : "Duplicate task is non blocking and empty to break the loop",
+                                       "name" : "_change2_number",
+                                       "status" : "DUPLICATE"
+                                    }
+                                 ]
+                              }
+                           ]
+                        }
+                     ]
+                  }
+               ]
+            },
+            {
+               "applicable" : true,
+               "change" : _change2_number,
+               "hasPass" : true,
+               "name" : "_change2_number",
+               "status" : "FAIL",
+               "subTasks" : [
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (tasks-factory changes loop)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "applicable" : true,
+                           "change" : _change1_number,
+                           "hasPass" : true,
+                           "name" : "_change1_number",
+                           "status" : "FAIL",
+                           "subTasks" : [
+                              {
+                                 "applicable" : true,
+                                 "hasPass" : false,
+                                 "name" : "task (tasks-factory changes loop)",
+                                 "status" : "PASS",
+                                 "subTasks" : [
+                                    {
+                                       "applicable" : true,
+                                       "change" : _change1_number,
+                                       "hasPass" : false,
+                                       "hint" : "Duplicate task is non blocking and empty to break the loop",
+                                       "name" : "_change1_number",
+                                       "status" : "DUPLICATE"
+                                    },
+                                    {
+                                       "applicable" : true,
+                                       "change" : _change2_number,
+                                       "hasPass" : false,
+                                       "hint" : "Duplicate task is non blocking and empty to break the loop",
+                                       "name" : "_change2_number",
+                                       "status" : "DUPLICATE"
+                                    }
+                                 ]
+                              }
+                           ]
+                        },
+                        {
+                           "applicable" : true,
+                           "change" : _change2_number,
+                           "hasPass" : false,
+                           "hint" : "Duplicate task is non blocking and empty to break the loop",
+                           "name" : "_change2_number",
+                           "status" : "DUPLICATE"
+                        }
+                     ]
+                  }
+               ]
+            }
+         ]
+      }
+   ]
+}
+
+[root "Root Import tasks using absolute syntax"]
+  applicable = is:open
+  subtask = /relative.config^Root Import task from subdir using relative syntax
+  subtask = /dir/common.config^Root Import task from root task.config
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root Import tasks using absolute syntax",
+   "status" : "PASS",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : false,
+         "name" : "Root Import task from subdir using relative syntax",
+         "status" : "PASS",
+         "subTasks" : [
+            {
+                "applicable" : true,
+                "hasPass" : true,
+                "name" : "Sample relative task in sub dir",
+                "status" : "PASS"
+            }
+         ]
+      },
+      {
+         "applicable" : true,
+         "hasPass" : false,
+         "name" : "Root Import task from root task.config",
+         "status" : "PASS",
+         "subTasks" : [
+             {
+                 "applicable" : true,
+                 "hasPass" : true,
+                 "name" : "Subtask PASS",
+                 "status" : "PASS"
+             }
+         ]
+      }
+   ]
+}
+
+[root "Root Import relative tasks from root config"]
+  applicable = is:open
+  subtask = dir/common.config^Root Import task from root task.config
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root Import relative tasks from root config",
+   "status" : "PASS",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : false,
+         "name" : "Root Import task from root task.config",
+         "status" : "PASS",
+         "subTasks" : [
+             {
+                 "applicable" : true,
+                 "hasPass" : true,
+                 "name" : "Subtask PASS",
+                 "status" : "PASS"
+             }
+         ]
+      }
+   ]
+}
+
+[root "Root subtasks-external user ref with Absolute and Relative syntaxes"]
+  subtasks-external = user absolute and relative syntaxes
+
+[external "user absolute and relative syntaxes"]
+  user = testuser
+  file = dir/sample.config
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root subtasks-external user ref with Absolute and Relative syntaxes",
+   "status" : "PASS",
+   "subTasks" : [
+       {
+           "applicable" : true,
+           "hasPass" : true,
+           "name" : "Referencing single task from same user ref",
+           "status" : "PASS",
+           "subTasks" : [
+               {
+                   "applicable" : true,
+                   "hasPass" : true,
+                   "name" : "Relative Task",
+                   "status" : "PASS"
+               },
+               {
+                   "applicable" : true,
+                   "hasPass" : true,
+                   "name" : "Relative Task in sub dir",
+                   "status" : "PASS"
+               },
+               {
+                   "applicable" : true,
+                   "hasPass" : true,
+                   "name" : "task in user root config file",
+                   "status" : "PASS"
+               },
+               {
+                   "applicable" : true,
+                   "hasPass" : true,
+                   "name" : "Absolute Task",
+                   "status" : "PASS"
+               }
+           ]
+       }
+   ]
+}
+
+[root "Root Import user tasks"]
+  applicable = is:open
+  subtask = @testuser/foo/bar.config^Absolute Task
+  subtask = @testuser^task in user root config file
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root Import user tasks",
+   "status" : "PASS",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "Absolute Task",
+         "status" : "PASS"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "task in user root config file",
+         "status" : "PASS"
+      }
+   ]
+}
+
+[root "Root Import group tasks"]
+  applicable = is:open
+  subtask = %{non_secret_group_name_without_space}/foo/bar.config^Absolute Task 1
+  subtask = %{non_secret_group_name_without_space}^task in group root config file 1
+  subtask = %{non_secret_group_name_with_space}/foo/bar.config^Absolute Task 3
+  subtask = %{non_secret_group_name_with_space}^task in group root config file 3
+  subtask = %%{non_secret_group_uuid}/foo/bar.config^Absolute Task 2
+  subtask = %%{non_secret_group_uuid}^task in group root config file 2
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root Import group tasks",
+   "status" : "PASS",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "Absolute Task 1",
+         "status" : "PASS"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "task in group root config file 1",
+         "status" : "PASS"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "Absolute Task 3",
+         "status" : "PASS"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "task in group root config file 3",
+         "status" : "PASS"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "Absolute Task 2",
+         "status" : "PASS"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "task in group root config file 2",
+         "status" : "PASS"
+      }
+   ]
+}
+
+[root "Root Reference tasks from All-Projects"]
+  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
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root Reference tasks from All-Projects",
+   "status" : "PASS",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "Subtask PASS",
+         "status" : "PASS"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : false,
+         "name" : "Import All-Projects root task",
+         "status" : "PASS",
+         "subTasks" : [
+            {
+               "applicable" : true,
+               "hasPass" : true,
+               "name" : "Subtask PASS",
+               "status" : "PASS"
+            }
+         ]
+      },
+      {
+         "applicable" : true,
+         "hasPass" : false,
+         "name" : "Import All-Projects non-root task",
+         "status" : "PASS",
+         "subTasks" : [
+            {
+               "applicable" : true,
+               "hasPass" : true,
+               "name" : "Sample relative task in sub dir",
+               "status" : "PASS"
+            }
+         ]
+      },
+      {
+         "applicable" : true,
+         "hasPass" : false,
+         "name" : "Import All-Projects root task - groups",
+         "status" : "PASS",
+         "subTasks" : [
+            {
+               "applicable" : true,
+               "hasPass" : true,
+               "name" : "Subtask PASS",
+               "status" : "PASS"
+            }
+         ]
+      },
+      {
+         "applicable" : true,
+         "hasPass" : false,
+         "name" : "Import All-Projects non-root task - groups",
+         "status" : "PASS",
+         "subTasks" : [
+            {
+               "applicable" : true,
+               "hasPass" : true,
+               "name" : "Sample relative task in sub dir",
+               "status" : "PASS"
+            }
+         ]
+      }
+   ]
+}
+
+[root "Root Preload from all-projects sub-dir which has preload-task in same file"]
+  preload-task = //dir/common.config^Sample task in sub dir with preload-task from same file
+
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "name" : "Root Preload from all-projects sub-dir which has preload-task in same file",
+   "status" : "PASS"
+}
+
+[root "Root Preload from all-projects sub-dir which has preload-task in different file"]
+  preload-task = //dir/common.config^Sample task in sub dir with preload-task from different file
+
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "name" : "Root Preload from all-projects sub-dir which has preload-task in different file",
+   "status" : "PASS"
+}
+
+[root "Root Preload from all-projects sub-dir"]
+  preload-task = //dir/common.config^Sample relative task in sub dir
+
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "name" : "Root Preload from all-projects sub-dir",
+   "status" : "PASS"
+}
+
+[root "Root Preload from all-projects sub-dir which has subtask in same file"]
+  preload-task = //dir/common.config^Sample relative task in sub dir with subtask from same file
+
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "name" : "Root Preload from all-projects sub-dir which has subtask in same file",
+   "status" : "PASS",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "Sample relative task in sub dir",
+         "status" : "PASS"
+      }
+   ]
+}
+
+[root "Root Preload from all-projects sub-dir which has subtask in different file"]
+  preload-task = //dir/common.config^Sample relative task in sub dir with subtask from different file
+
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "name" : "Root Preload from all-projects sub-dir which has subtask in different file",
+   "status" : "PASS",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "Passing task",
+         "status" : "PASS"
+      }
+   ]
+}
+
+[root "Root Preload from all-projects sub-dir which has subtasks-factory in same file"]
+  preload-task = //dir/common.config^Sample relative task in sub dir with subtasks-factory from same file
+
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "name" : "Root Preload from all-projects sub-dir which has subtasks-factory in same file",
+   "status" : "PASS",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "my a task",
+         "status" : "PASS"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "my b task",
+         "status" : "PASS"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "my c task",
+         "status" : "PASS"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "my d task",
+         "status" : "PASS"
+      }
+   ]
+}
+
+[root "Root Preload from all-projects sub-dir which has subtasks-external in same file"]
+  preload-task = //dir/common.config^Sample relative task in sub dir with subtasks-external from same file
+
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "name" : "Root Preload from all-projects sub-dir which has subtasks-external in same file",
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "userfile task/special.config PASS",
+         "status" : "PASS"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "userfile task/special.config FAIL",
+         "status" : "FAIL"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "file task/common.config Preload PASS",
+         "status" : "PASS"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "Referencing single task from same user ref",
+         "status" : "PASS",
+         "subTasks" : [
+             {
+                 "applicable" : true,
+                 "hasPass" : true,
+                 "name" : "Relative Task",
+                 "status" : "PASS"
+             },
+             {
+                 "applicable" : true,
+                 "hasPass" : true,
+                 "name" : "Relative Task in sub dir",
+                 "status" : "PASS"
+             },
+             {
+                 "applicable" : true,
+                 "hasPass" : true,
+                 "name" : "task in user root config file",
+                 "status" : "PASS"
+             },
+             {
+                 "applicable" : true,
+                 "hasPass" : true,
+                 "name" : "Absolute Task",
+                 "status" : "PASS"
+             }
+         ]
+      }
+   ]
+}
+
+[root "Root Preload from group ref which has subtasks-file"]
+  preload-task = %{non_secret_group_name_without_space}^Sample task with subtasks-file
+
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "name" : "Root Preload from group ref which has subtasks-file",
+   "status" : "PASS",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "Absolute Task 1",
+         "status" : "PASS"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "Absolute Task 2",
+         "status" : "PASS"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "Absolute Task",
+         "status" : "PASS"
+      }
+   ]
+}
+
+[root "Root Preload from user ref"]
+  preload-task = @testuser/dir/relative.config^Relative Task
+
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "name" : "Root Preload from user ref",
+   "status" : "PASS"
+}
+
+[root "Root Preload from user ref which has subtask in same file"]
+  preload-task = @testuser/dir/relative.config^Relative Task with subtask
+
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "name" : "Root Preload from user ref which has subtask in same file",
+   "status" : "PASS",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "Passing task",
+         "status" : "PASS"
+      }
+   ]
+}
+
+[root "Root Preload from user ref which has subtask in different file"]
+  preload-task = @testuser/dir/relative.config^Import All-Projects root task
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root Preload from user ref which has subtask in different file",
+   "status" : "PASS",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "Subtask PASS",
+         "status" : "PASS"
+      }
+   ]
+}
+
+[root "Root Preload from group ref"]
+  preload-task = %{non_secret_group_name_without_space}^task in group root config file 1
+
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "name" : "Root Preload from group ref",
+   "status" : "PASS"
+}
+
+[root "Root Preload from group ref which has subtask in same file"]
+  preload-task = %{non_secret_group_name_without_space}^task in group root with subtask
+
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "name" : "Root Preload from group ref which has subtask in same file",
+   "status" : "PASS",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "Passing task",
+         "status" : "PASS"
+      }
+   ]
+}
+
+[root "Root Preload from group ref which has subtask in different file"]
+  preload-task = %{non_secret_group_name_without_space}^task in group root with subtask from all-projects
+
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "name" : "Root Preload from group ref which has subtask in different file",
+   "status" : "PASS",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "Subtask PASS",
+         "status" : "PASS"
+      }
+   ]
+}
+
+[root "Root Preload from group ref which has subtask in different group ref"]
+  preload-task = %{non_secret_group_name_without_space}^task in group root with subtask from another group
+
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "name" : "Root Preload from group ref which has subtask in different group ref",
+   "status" : "PASS",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "task in group root config file 3",
+         "status" : "PASS"
+      }
+   ]
+}
+
 [root "Root INVALID Preload"]
   preload-task = missing
 
-{
-   "name" : "UNKNOWN",
-   "status" : "INVALID"
-}
+{                         # Test Suite: task_only
+   "name" : "UNKNOWN",    # Test Suite: task_only
+   "status" : "INVALID"   # Test Suite: task_only
+}                         # Test Suite: task_only
 
 [root "INVALIDS"]
   subtasks-file = invalids.config
@@ -1486,6 +2867,23 @@
          ]
       },
       {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "Subtask Blank",
+         "status" : "WAITING",
+         "subTasks" : [
+            {
+               "name" : "UNKNOWN",
+               "status" : "INVALID"
+            }
+         ]
+      },
+      {
+         "name" : "Bad APPLICABLE query",    # Only Test Suite: visible
+         "name" : "UNKNOWN",                 # Only Test Suite: !visible
+         "status" : "INVALID"
+      },
+      {
          "applicable" : false,
          "hasPass" : true,
          "name" : "NA Bad PASS query",
@@ -1506,9 +2904,13 @@
          "status" : "INVALID"   # Only Test Suite: !all
       },
       {
+         "name" : "UNKNOWN",
+         "status" : "INVALID"
+      },
+      {
          "applicable" : true,
          "hasPass" : false,
-         "name" : "Looping",
+         "name" : "task (tasks-factory missing)",
          "status" : "WAITING",
          "subTasks" : [
             {
@@ -1518,13 +2920,21 @@
          ]
       },
       {
-         "name" : "UNKNOWN",
-         "status" : "INVALID"
+         "applicable" : true,
+         "hasPass" : false,
+         "name" : "task (tasks-factory static INVALID)",
+         "status" : "WAITING",
+         "subTasks" : [
+            {
+               "name" : "UNKNOWN",
+               "status" : "INVALID"
+            }
+         ]
       },
       {
          "applicable" : true,
          "hasPass" : false,
-         "name" : "task (tasks-factory missing)",
+         "name" : "task (tasks-factory change INVALID)",
          "status" : "WAITING",
          "subTasks" : [
             {
@@ -1560,6 +2970,18 @@
       {
          "applicable" : true,
          "hasPass" : false,
+         "name" : "task (names-factory name Blank)",
+         "status" : "WAITING",
+         "subTasks" : [
+            {
+               "name" : "UNKNOWN",
+               "status" : "INVALID"
+            }
+         ]
+      },
+      {
+         "applicable" : true,
+         "hasPass" : false,
          "name" : "task (names-factory duplicate)",
          "status" : "WAITING",
          "subTasks" : [
@@ -1610,38 +3032,6 @@
                "status" : "INVALID"
             }
          ]
-      },
-      {
-         "applicable" : true,
-         "hasPass" : false,
-         "name" : "task (tasks-factory changes loop)",
-         "status" : "WAITING",
-         "subTasks" : [
-            {
-               "applicable" : true,
-               "hasPass" : true,
-               "name" : "_change1_number",
-               "status" : "FAIL",
-               "subTasks" : [
-                  {
-                     "name" : "UNKNOWN",
-                     "status" : "INVALID"
-                  }
-               ]
-            },
-            {
-               "applicable" : true,
-               "hasPass" : true,
-               "name" : "_change2_number",
-               "status" : "FAIL",
-               "subTasks" : [
-                  {
-                     "name" : "UNKNOWN",
-                     "status" : "INVALID"
-                  }
-               ]
-            }
-         ]
       }
    ]
 }
@@ -1773,6 +3163,23 @@
          ]
       },
       {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "Subtask Blank",
+         "status" : "WAITING",
+         "subTasks" : [
+            {
+               "name" : "UNKNOWN",
+               "status" : "INVALID"
+            }
+         ]
+      },
+      {
+         "name" : "Bad APPLICABLE query",    # Only Test Suite: visible
+         "name" : "UNKNOWN",                 # Only Test Suite: !visible
+         "status" : "INVALID"
+      },
+      {
          "applicable" : false,
          "hasPass" : true,
          "name" : "NA Bad PASS query",
@@ -1793,9 +3200,13 @@
          "status" : "INVALID"   # Only Test Suite: !all
       },
       {
+         "name" : "UNKNOWN",
+         "status" : "INVALID"
+      },
+      {
          "applicable" : true,
          "hasPass" : false,
-         "name" : "Looping",
+         "name" : "task (tasks-factory missing)",
          "status" : "WAITING",
          "subTasks" : [
             {
@@ -1805,13 +3216,21 @@
          ]
       },
       {
-         "name" : "UNKNOWN",
-         "status" : "INVALID"
+         "applicable" : true,
+         "hasPass" : false,
+         "name" : "task (tasks-factory static INVALID)",
+         "status" : "WAITING",
+         "subTasks" : [
+            {
+               "name" : "UNKNOWN",
+               "status" : "INVALID"
+            }
+         ]
       },
       {
          "applicable" : true,
          "hasPass" : false,
-         "name" : "task (tasks-factory missing)",
+         "name" : "task (tasks-factory change INVALID)",
          "status" : "WAITING",
          "subTasks" : [
             {
@@ -1847,6 +3266,18 @@
       {
          "applicable" : true,
          "hasPass" : false,
+         "name" : "task (names-factory name Blank)",
+         "status" : "WAITING",
+         "subTasks" : [
+            {
+               "name" : "UNKNOWN",
+               "status" : "INVALID"
+            }
+         ]
+      },
+      {
+         "applicable" : true,
+         "hasPass" : false,
          "name" : "task (names-factory duplicate)",
          "status" : "WAITING",
          "subTasks" : [
@@ -1897,46 +3328,13 @@
                "status" : "INVALID"
             }
          ]
-      },
-      {
-         "applicable" : true,
-         "hasPass" : false,
-         "name" : "task (tasks-factory changes loop)",
-         "status" : "WAITING",
-         "subTasks" : [
-            {
-               "applicable" : true,
-               "hasPass" : true,
-               "name" : "_change1_number",
-               "status" : "FAIL",
-               "subTasks" : [
-                  {
-                     "name" : "UNKNOWN",
-                     "status" : "INVALID"
-                  }
-               ]
-            },
-            {
-               "applicable" : true,
-               "hasPass" : true,
-               "name" : "_change2_number",
-               "status" : "FAIL",
-               "subTasks" : [
-                  {
-                     "name" : "UNKNOWN",
-                     "status" : "INVALID"
-                  }
-               ]
-            }
-         ]
       }
    ]
 }
 
 ```
 
-`task/common.config` file in project `All-Projects` on ref `refs/meta/config`.
-
+file: `All-Projects:refs/meta/config:task/common.config`
 ```
 [task "file task/common.config PASS"]
   applicable = is:open
@@ -1947,8 +3345,84 @@
   fail = is:open
 ```
 
-`task/invalids.config` file in project `All-Projects` on ref `refs/meta/config`.
+file: `All-Projects:refs/meta/config:task/relative.config`
+```
+[task "Root Import task from subdir using relative syntax"]
+    subtask = dir/common.config^Sample relative task in sub dir
 
+[task "Passing task"]
+    applicable = is:open
+    pass = True
+```
+
+file: `All-Projects:refs/meta/config:task/dir/common.config`
+```
+[task "Sample relative task in sub dir"]
+    applicable = is:open
+    pass = is:open
+
+[task "Sample relative task in sub dir with subtask from same file"]
+    applicable = is:open
+    pass = is:open
+    subtask = Sample relative task in sub dir
+
+[task "Sample relative task in sub dir with subtasks-factory from same file"]
+    applicable = is:open
+    pass = is:open
+    set-my-factory-prop = simple static tasks-factory 2
+    subtasks-factory = simple static tasks-factory 1
+    subtasks-factory = ${my-factory-prop}
+
+[tasks-factory "simple static tasks-factory 1"]
+    names-factory = names-factory static list 1
+    pass = True
+
+[names-factory "names-factory static list 1"]
+    type = static
+    name = my a task
+    name = my b task
+    name = my c task
+
+[tasks-factory "simple static tasks-factory 2"]
+    names-factory = names-factory static list 2
+    pass = True
+
+[names-factory "names-factory static list 2"]
+    type = static
+    name = my d task
+
+[task "Sample relative task in sub dir with subtasks-external from same file"]
+    applicable = is:open
+    pass = is:open
+    set-my-external-prop = user sample config
+    subtasks-external = user special tasks
+    subtasks-external = ${my-external-prop}
+
+[external "user special tasks"]
+  user = testuser
+  file = special.config
+
+[external "user sample config"]
+  user = testuser
+  file = dir/sample.config
+
+[task "Sample relative task in sub dir with subtask from different file"]
+    applicable = is:open
+    pass = is:open
+    subtask = //relative.config^Passing task
+
+[task "Root Import task from root task.config"]
+    applicable = is:open
+    subtask = ^Subtask PASS
+
+[task "Sample task in sub dir with preload-task from same file"]
+    preload-task = Sample relative task in sub dir
+
+[task "Sample task in sub dir with preload-task from different file"]
+    preload-task = %{non_secret_group_name_without_space}/foo/bar.config^Absolute Task 1
+```
+
+file: `All-Projects:refs/meta/config:task/invalids.config`
 ```
 [task "No PASS criteria"]
   fail-hint = Invalid without Pass criteria and without subtasks
@@ -1977,6 +3451,14 @@
 [task "Subtask Optional"]
    subtask = MISSING | MISSING
 
+[task "Subtask Blank"]
+  pass = True
+  subtask =
+
+[task "Bad APPLICABLE query"]
+  applicable = bad:query
+  fail = True
+
 [task "NA Bad PASS query"]
   applicable = NOT is:open # Assumes test query is "is:open"
   fail = True
@@ -1992,23 +3474,30 @@
   fail = True
   in-progress = has:bad
 
-[task "Looping"]
-  subtask = Looping
-
 [task "Looping Properties"]
   set-A = ${B}
   set-B = ${A}
+  fail-hint = ${A}
   fail = True
 
 [task "task (tasks-factory missing)"]
   subtasks-factory = missing
 
+[task "task (tasks-factory static INVALID)"]
+  subtasks-factory = tasks-factory (preload-task missing)
+
+[task "task (tasks-factory change INVALID)"]
+  subtasks-factory = tasks-factory change (preload-task missing)
+
 [task "task (names-factory type missing)"]
   subtasks-factory = tasks-factory (names-factory type missing)
 
 [task "task (names-factory type INVALID)"]
   subtasks-factory = tasks-factory (names-factory type INVALID)
 
+[task "task (names-factory name Blank)"]
+  subtasks-factory = tasks-factory (names-factory name Blank)
+
 [task "task (names-factory duplicate)"]
   subtasks-factory = tasks-factory (names-factory duplicate)
 
@@ -2021,16 +3510,27 @@
 [task "task (names-factory changes invalid)"]
   subtasks-factory = tasks-factory change (names-factory changes invalid)
 
-[task "task (tasks-factory changes loop)"]
-  subtasks-factory = tasks-factory change loop
-
 [tasks-factory "tasks-factory (names-factory type missing)"]
   names-factory = names-factory (type missing)
   fail = True
 
+[tasks-factory "tasks-factory (preload-task missing)"]
+  names-factory = names-factory static
+  fail = True
+  preload-task = missing
+
+[tasks-factory "tasks-factory change (preload-task missing)"]
+  names-factory = names-factory change list
+  fail = True
+  preload-task = missing
+
 [tasks-factory "tasks-factory (names-factory type INVALID)"]
   names-factory = name-factory (type INVALID)
 
+[tasks-factory "tasks-factory (names-factory name Blank)"]
+  names-factory = names-factory (name Blank)
+  fail = True
+
 [tasks-factory "tasks-factory (names-factory duplicate)"]
   names-factory = names-factory duplicate
   fail = True
@@ -2047,10 +3547,13 @@
   names-factory = names-factory change list (changes invalid)
   fail = True
 
-[tasks-factory "tasks-factory change loop"]
-  names-factory = names-factory change constant
-  subtask = task (tasks-factory changes loop)
-  fail = True
+[names-factory "names-factory static"]
+  name = task A
+  type = static
+
+[names-factory "names-factory change list"]
+  changes = change:_change1_number OR change:_change2_number
+  type = change
 
 [names-factory "names-factory (type missing)"]
   name = no type test
@@ -2062,6 +3565,10 @@
   name = invalid type test
   type = invalid
 
+[names-factory "names-factory (name Blank)"]
+  name =
+  type = static
+
 [names-factory "names-factory duplicate"]
   name = duplicate
   name = duplicate
@@ -2071,17 +3578,12 @@
   type = change
 
 [names-factory "names-factory change list (changes invalid)"]
-  change = change:invalidChange
-  type = change
-
-[names-factory "names-factory change constant"]
-  changes = change:_change1_number OR change:_change2_number
+  changes = change:invalidChange
   type = change
 
 ```
 
-`task/special.config` file in project `All-Users` on ref `refs/users/self`.
-
+file: `All-Users:refs/meta/config:task/special.config`
 ```
 [task "userfile task/special.config PASS"]
   applicable = is:open
@@ -2090,4 +3592,154 @@
 [task "userfile task/special.config FAIL"]
   applicable = is:open
   fail = is:open
+
+[task "file task/common.config Preload PASS"]
+  preload-task = userfile task/special.config PASS
+```
+
+file: `All-Users:refs/users/self:task/common.config`
+```
+[task "file task/common.config PASS"]
+  applicable = is:open
+  pass = is:open
+
+[task "file task/common.config FAIL"]
+  applicable = is:open
+  fail = is:open
+```
+
+file: `All-Users:refs/users/self:task.config`
+```
+[task "task in user root config file"]
+  applicable = is:open
+  pass = is:open
+```
+
+file: `All-Users:refs/users/self:task/dir/sample.config`
+```
+[task "Referencing single task from same user ref"]
+  applicable = is:open
+  pass = is:open
+  subtask = relative.config^Relative Task
+  subtask = sub_dir/relative.config^Relative Task in sub dir
+  subtask = ^task in user root config file
+  subtask = /foo/bar.config^Absolute Task
+```
+
+file: `All-Users:refs/users/self:task/dir/relative.config`
+```
+[task "Relative Task"]
+  applicable = is:open
+  pass = is:open
+
+[task "Relative Task with subtask"]
+  applicable = is:open
+  pass = is:open
+  subtask = Passing task
+
+[task "Passing task"]
+  applicable = is:open
+  pass = True
+
+[task "Import All-Projects root task"]
+  applicable = is:open
+  subtask = //^Subtask PASS
+
+[task "Import All-Projects non-root task"]
+  applicable = is:open
+  subtask = //dir/common.config^Sample relative task in sub dir
+```
+
+file: `All-Users:refs/users/self:task/dir/sub_dir/relative.config`
+```
+[task "Relative Task in sub dir"]
+  applicable = is:open
+  pass = is:open
+```
+
+file: `All-Users:refs/users/self:task/foo/bar.config`
+```
+[task "Absolute Task"]
+  applicable = is:open
+  pass = is:open
+```
+
+file: `All-Users:refs/groups/{sharded_non_secret_group_uuid_without_space}:task/dir/relative.config`
+```
+[task "Import All-Projects root task - groups"]
+  applicable = is:open
+  subtask = //^Subtask PASS
+
+[task "Import All-Projects non-root task - groups"]
+  applicable = is:open
+  subtask = //dir/common.config^Sample relative task in sub dir
+```
+
+file: `All-Users:refs/groups/{sharded_non_secret_group_uuid_without_space}:task/foo/bar.config`
+```
+[task "Absolute Task 1"]
+  applicable = is:open
+  pass = is:open
+
+[task "Absolute Task 2"]
+  applicable = is:open
+  pass = is:open
+```
+
+file: `All-Users:refs/groups/{sharded_non_secret_group_uuid_without_space}:task/foo.config`
+```
+[task "Absolute Task"]
+  applicable = is:open
+  pass = is:open
+```
+
+file: `All-Users:refs/groups/{sharded_non_secret_group_uuid_without_space}:task.config`
+```
+[task "task in group root config file 1"]
+  applicable = is:open
+  pass = is:open
+
+[task "task in group root with subtask"]
+  applicable = is:open
+  pass = is:open
+  subtask = Passing task
+
+[task "Passing task"]
+  applicable = is:open
+  pass = True
+
+[task "task in group root with subtask from all-projects"]
+  applicable = is:open
+  pass = is:open
+  subtask = //^Subtask PASS
+
+[task "task in group root with subtask from another group"]
+  applicable = is:open
+  pass = is:open
+  subtask = %{non_secret_group_name_with_space}^task in group root config file 3
+
+[task "task in group root config file 2"]
+  applicable = is:open
+  pass = is:open
+
+[task "Sample task with subtasks-file"]
+  applicable = is:open
+  pass = is:open
+  set-my-prop = foo.config
+  subtasks-file = foo/bar.config
+  subtasks-file = ${my-prop}
+```
+
+file: `All-Users:refs/groups/{sharded_non_secret_group_uuid_with_space}:task/foo/bar.config`
+```
+[task "Absolute Task 3"]
+  applicable = is:open
+  pass = is:open
+```
+
+file: `All-Users:refs/groups/{sharded_non_secret_group_uuid_with_space}:task.config`
+```
+[task "task in group root config file 3"]
+  applicable = is:open
+  pass = is:open
 ```
diff --git a/src/test/java/com/google/gerrit/common/BooleanTableTest.java b/src/test/java/com/google/gerrit/common/BooleanTableTest.java
new file mode 100644
index 0000000..746a0e9
--- /dev/null
+++ b/src/test/java/com/google/gerrit/common/BooleanTableTest.java
@@ -0,0 +1,81 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.common;
+
+import junit.framework.TestCase;
+
+public class BooleanTableTest extends TestCase {
+
+  public void testNulls() {
+    BooleanTable<String, String> cbt = new BooleanTable<>();
+    assertNull(cbt.get("r1", "c1"));
+    assertNull(cbt.get("r0", "c0"));
+
+    cbt.put("r1", "c0", true);
+    assertNull(cbt.get("r1", "c1"));
+    assertNull(cbt.get("r0", "c0"));
+
+    cbt.put("r0", "c1", true);
+    assertNull(cbt.get("r1", "c1"));
+    assertNull(cbt.get("r0", "c0"));
+  }
+
+  public void testRowColumn() {
+    BooleanTable<String, String> cbt = new BooleanTable<>();
+    cbt.put("r1", "c1", true);
+    cbt.put("r2", "c2", false);
+    assertTrue(cbt.get("r1", "c1"));
+    assertNull(cbt.get("r1", "c2"));
+    assertNull(cbt.get("r2", "c1"));
+    assertFalse(cbt.get("r2", "c2"));
+  }
+
+  public void testRowColumnOverride() {
+    BooleanTable<String, String> cbt = new BooleanTable<>();
+    cbt.put("r1", "c1", true);
+    assertTrue(cbt.get("r1", "c1"));
+
+    cbt.put("r1", "c1", false);
+    assertFalse(cbt.get("r1", "c1"));
+  }
+
+  public void testRepeatedColumns() {
+    BooleanTable<String, String> cbt = new BooleanTable<>();
+    cbt.put("r1", "c1", true);
+    cbt.put("r2", "c1", false);
+    assertTrue(cbt.get("r1", "c1"));
+    assertFalse(cbt.get("r2", "c1"));
+  }
+
+  public void testRepeatedRows() {
+    BooleanTable<String, String> cbt = new BooleanTable<>();
+    cbt.put("r1", "c1", true);
+    cbt.put("r1", "c2", false);
+    assertTrue(cbt.get("r1", "c1"));
+    assertFalse(cbt.get("r1", "c2"));
+  }
+
+  public void testRepeatedRowsAndColumns() {
+    BooleanTable<String, String> cbt = new BooleanTable<>();
+    cbt.put("r1", "c1", true);
+    cbt.put("r2", "c1", false);
+    cbt.put("r1", "c2", true);
+    cbt.put("r2", "c2", false);
+    assertTrue(cbt.get("r1", "c1"));
+    assertFalse(cbt.get("r2", "c1"));
+    assertTrue(cbt.get("r1", "c2"));
+    assertFalse(cbt.get("r2", "c2"));
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/task/TaskExpressionTest.java b/src/test/java/com/googlesource/gerrit/plugins/task/TaskExpressionTest.java
new file mode 100644
index 0000000..8ea0009
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/task/TaskExpressionTest.java
@@ -0,0 +1,260 @@
+// Copyright (C) 2021 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 com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AllUsersName;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import junit.framework.TestCase;
+import org.mockito.Mockito;
+
+/*
+ * <ul>
+ *   <li><code> "simple"            -> ("simple")                   required</code>
+ *   <li><code> "world | peace"     -> ("world", "peace")           required</code>
+ *   <li><code> "shadenfreud |"     -> ("shadenfreud")              optional</code>
+ *   <li><code> "foo | bar |"       -> ("foo", "bar")               optional</code>
+ *   <li><code> "/foo^bar | baz |"  -> ("task/foo^bar", "baz")      optional</code>
+ *   <li><code> "foo^bar | baz |"   -> ("cur_dir/foo^bar", "baz")   optional</code>
+ *   <li><code> "^bar | baz |"      -> ("task.config^bar", "baz")   optional</code>
+ * </ul>
+ */
+public class TaskExpressionTest extends TestCase {
+  public static String SIMPLE = "simple";
+  public static String WORLD = "world";
+  public static String PEACE = "peace";
+  public static FileKey file = createFileKey("foo", "bar", "baz");
+
+  public static TaskKey SIMPLE_TASK = TaskKey.create(file, SIMPLE);
+  public static TaskKey WORLD_TASK = TaskKey.create(file, WORLD);
+  public static TaskKey PEACE_TASK = TaskKey.create(file, PEACE);
+
+  public static String SAMPLE = "sample";
+  public static String TASK_CFG = "task.config";
+  public static String SIMPLE_CFG = "task/simple.config";
+  public static String PEACE_CFG = "task/peace.config";
+  public static String WORLD_PEACE_CFG = "task/world/peace.config";
+  public static String REL_WORLD_PEACE_CFG = "world/peace.config";
+  public static String ABS_PEACE_CFG = "/peace.config";
+
+  public void testBlank() {
+    TaskExpression exp = getTaskExpression("");
+    Iterator<TaskKey> it = exp.iterator();
+    assertTrue(it.hasNext());
+    assertNoSuchElementException(it);
+  }
+
+  public void testRequiredSingleName() {
+    TaskExpression exp = getTaskExpression(SIMPLE);
+    Iterator<TaskKey> it = exp.iterator();
+    assertTrue(it.hasNext());
+    assertEquals(it.next(), SIMPLE_TASK);
+    assertTrue(it.hasNext());
+    assertNoSuchElementException(it);
+  }
+
+  public void testOptionalSingleName() {
+    TaskExpression exp = getTaskExpression(SIMPLE + "|");
+    Iterator<TaskKey> it = exp.iterator();
+    assertTrue(it.hasNext());
+    assertEquals(it.next(), SIMPLE_TASK);
+    assertFalse(it.hasNext());
+  }
+
+  public void testRequiredTwoNames() {
+    TaskExpression exp = getTaskExpression(WORLD + "|" + PEACE);
+    Iterator<TaskKey> it = exp.iterator();
+    assertTrue(it.hasNext());
+    assertEquals(it.next(), WORLD_TASK);
+    assertTrue(it.hasNext());
+    assertEquals(it.next(), PEACE_TASK);
+    assertTrue(it.hasNext());
+    assertNoSuchElementException(it);
+  }
+
+  public void testOptionalTwoNames() {
+    TaskExpression exp = getTaskExpression(WORLD + "|" + PEACE + "|");
+    Iterator<TaskKey> it = exp.iterator();
+    assertTrue(it.hasNext());
+    assertEquals(it.next(), WORLD_TASK);
+    assertTrue(it.hasNext());
+    assertEquals(it.next(), PEACE_TASK);
+    assertFalse(it.hasNext());
+  }
+
+  public void testBlankSpaces() {
+    TaskExpression exp = getTaskExpression("  ");
+    Iterator<TaskKey> it = exp.iterator();
+    assertTrue(it.hasNext());
+    assertNoSuchElementException(it);
+  }
+
+  public void testRequiredSingleNameLeadingSpaces() {
+    TaskExpression exp = getTaskExpression("  " + SIMPLE);
+    Iterator<TaskKey> it = exp.iterator();
+    assertTrue(it.hasNext());
+    assertEquals(it.next(), SIMPLE_TASK);
+    assertTrue(it.hasNext());
+    assertNoSuchElementException(it);
+  }
+
+  public void testRequiredSingleNameTrailingSpaces() {
+    TaskExpression exp = getTaskExpression(SIMPLE + "  ");
+    Iterator<TaskKey> it = exp.iterator();
+    assertTrue(it.hasNext());
+    assertEquals(it.next(), SIMPLE_TASK);
+    assertTrue(it.hasNext());
+    assertNoSuchElementException(it);
+  }
+
+  public void testOptionalSingleNameLeadingSpaces() {
+    TaskExpression exp = getTaskExpression("  " + SIMPLE + "|");
+    Iterator<TaskKey> it = exp.iterator();
+    assertTrue(it.hasNext());
+    assertEquals(it.next(), SIMPLE_TASK);
+    assertFalse(it.hasNext());
+  }
+
+  public void testOptionalSingleNameTrailingSpaces() {
+    TaskExpression exp = getTaskExpression(SIMPLE + "|  ");
+    Iterator<TaskKey> it = exp.iterator();
+    assertTrue(it.hasNext());
+    assertEquals(it.next(), SIMPLE_TASK);
+    assertFalse(it.hasNext());
+  }
+
+  public void testOptionalSingleNameMiddleSpaces() {
+    TaskExpression exp = getTaskExpression(SIMPLE + "  |");
+    Iterator<TaskKey> it = exp.iterator();
+    assertTrue(it.hasNext());
+    assertEquals(it.next(), SIMPLE_TASK);
+    assertFalse(it.hasNext());
+  }
+
+  public void testRequiredTwoNamesMiddleSpaces() {
+    TaskExpression exp = getTaskExpression(WORLD + "  |  " + PEACE);
+    Iterator<TaskKey> it = exp.iterator();
+    assertTrue(it.hasNext());
+    assertEquals(it.next(), WORLD_TASK);
+    assertTrue(it.hasNext());
+    assertEquals(it.next(), PEACE_TASK);
+    assertTrue(it.hasNext());
+    assertNoSuchElementException(it);
+  }
+
+  public void testAbsoluteAndRelativeReference() {
+    TaskExpression exp =
+        getTaskExpression(
+            createFileKey(SIMPLE_CFG),
+            REL_WORLD_PEACE_CFG + "^" + SAMPLE + " | " + ABS_PEACE_CFG + "^" + SAMPLE);
+    Iterator<TaskKey> it = exp.iterator();
+    assertTrue(it.hasNext());
+    assertEquals(it.next(), TaskKey.create(createFileKey(WORLD_PEACE_CFG), SAMPLE));
+    assertTrue(it.hasNext());
+    assertEquals(it.next(), TaskKey.create(createFileKey(PEACE_CFG), SAMPLE));
+    assertTrue(it.hasNext());
+    assertNoSuchElementException(it);
+  }
+
+  public void testAbsoluteAndRelativeReferenceFromRoot() {
+    TaskExpression exp =
+        getTaskExpression(
+            createFileKey(TASK_CFG),
+            REL_WORLD_PEACE_CFG + "^" + SAMPLE + " | " + ABS_PEACE_CFG + "^" + SAMPLE);
+    Iterator<TaskKey> it = exp.iterator();
+    assertTrue(it.hasNext());
+    assertEquals(it.next(), TaskKey.create(createFileKey(WORLD_PEACE_CFG), SAMPLE));
+    assertTrue(it.hasNext());
+    assertEquals(it.next(), TaskKey.create(createFileKey(PEACE_CFG), SAMPLE));
+    assertTrue(it.hasNext());
+    assertNoSuchElementException(it);
+  }
+
+  public void testReferenceFromRoot() {
+    TaskExpression exp = getTaskExpression(createFileKey(SIMPLE_CFG), " ^" + SAMPLE + " | ");
+    Iterator<TaskKey> it = exp.iterator();
+    assertTrue(it.hasNext());
+    assertEquals(it.next(), TaskKey.create(createFileKey(TASK_CFG), SAMPLE));
+    assertNoSuchElementException(it);
+  }
+
+  public void testDifferentKeyOnDifferentFile() {
+    TaskExpression exp = getTaskExpression(createFileKey("foo", "bar", "baz"), SIMPLE);
+    TaskExpression otherExp = getTaskExpression(createFileKey("foo", "bar", "other"), SIMPLE);
+    assertFalse(exp.key.equals(otherExp.key));
+  }
+
+  public void testDifferentKeyOnDifferentBranch() {
+    TaskExpression exp = getTaskExpression(createFileKey("foo", "bar", "baz"), SIMPLE);
+    TaskExpression otherExp = getTaskExpression(createFileKey("foo", "other", "baz"), SIMPLE);
+    assertFalse(exp.key.equals(otherExp.key));
+  }
+
+  public void testDifferentKeyOnDifferentProject() {
+    TaskExpression exp = getTaskExpression(createFileKey("foo", "bar", "baz"), SIMPLE);
+    TaskExpression otherExp = getTaskExpression(createFileKey("other", "bar", "baz"), SIMPLE);
+    assertFalse(exp.key.equals(otherExp.key));
+  }
+
+  public void testDifferentKeyOnDifferentExpression() {
+    TaskExpression exp = getTaskExpression(SIMPLE);
+    TaskExpression otherExp = getTaskExpression(PEACE);
+    assertFalse(exp.key.equals(otherExp.key));
+  }
+
+  protected static void assertNoSuchElementException(Iterator<TaskKey> it) {
+    try {
+      it.next();
+      assertTrue(false);
+    } catch (NoSuchElementException e) {
+      assertTrue(true);
+    }
+  }
+
+  protected TaskExpression getTaskExpression(String expression) {
+    return getTaskExpression(file, expression);
+  }
+
+  protected TaskExpression getTaskExpression(FileKey file, String expression) {
+    AccountCache accountCache = Mockito.mock(AccountCache.class);
+    GroupCache groupCache = Mockito.mock(GroupCache.class);
+    TaskReference.Factory factory = Mockito.mock(TaskReference.Factory.class);
+    Mockito.when(factory.create(Mockito.any(), Mockito.any()))
+        .thenAnswer(
+            invocation ->
+                new TaskReference(
+                    new TaskKey.Builder(
+                        (FileKey) invocation.getArguments()[0],
+                        new AllProjectsName("All-Projects"),
+                        new AllUsersName("All-Users"),
+                        accountCache,
+                        groupCache),
+                    (String) invocation.getArguments()[1]));
+    return new TaskExpression(factory, file, expression);
+  }
+
+  protected static FileKey createFileKey(String file) {
+    return createFileKey("foo", "bar", file);
+  }
+
+  protected static FileKey createFileKey(String project, String branch, String file) {
+    return FileKey.create(BranchNameKey.create(Project.NameKey.parse(project), branch), file);
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/task/TaskReferenceTest.java b/src/test/java/com/googlesource/gerrit/plugins/task/TaskReferenceTest.java
new file mode 100644
index 0000000..e08240d
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/task/TaskReferenceTest.java
@@ -0,0 +1,299 @@
+// Copyright (C) 2022 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 com.google.common.collect.ImmutableSet;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.InternalGroup;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.server.account.AccountCache;
+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.AllUsersName;
+import java.sql.Timestamp;
+import java.util.NoSuchElementException;
+import java.util.Optional;
+import junit.framework.TestCase;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class TaskReferenceTest extends TestCase {
+  private static final String ALL_USERS = "All-Users";
+  private static final String ALL_PROJECTS = "All-Projects";
+  public static String SIMPLE = "simple";
+  public static String ROOT = "task.config";
+  public static String COMMON = "task/common.config";
+  public static String SUB_COMMON = "task/dir/common.config";
+  public static FileKey ROOT_CFG = createFileKey(ALL_PROJECTS, RefNames.REFS_CONFIG, ROOT);
+  public static FileKey COMMON_CFG = createFileKey(ALL_PROJECTS, RefNames.REFS_CONFIG, COMMON);
+  public static FileKey SUB_COMMON_CFG =
+      createFileKey(ALL_PROJECTS, RefNames.REFS_CONFIG, SUB_COMMON);
+
+  public static FileKey SAMPLE_PROJ_CFG = createFileKey("foo", RefNames.REFS_CONFIG, ROOT);
+
+  public static final String TEST_USER = "testuser";
+  public static final int TEST_USER_ID = 100000;
+  public static final Account TEST_USER_ACCOUNT =
+      Account.builder(Account.id(TEST_USER_ID), new Timestamp(0L)).build();
+  public static final String TEST_USER_REF =
+      "refs/users/" + String.format("%02d", TEST_USER_ID % 100) + "/" + TEST_USER_ID;
+  public static final FileKey TEST_USER_ROOT_CFG = createFileKey(ALL_USERS, TEST_USER_REF, ROOT);
+  public static final FileKey TEST_USER_COMMON_CFG =
+      createFileKey(ALL_USERS, TEST_USER_REF, COMMON);
+
+  public static final AccountGroup.NameKey TEST_GROUP1_NAME = AccountGroup.nameKey("testgroup");
+  public static final AccountGroup.NameKey TEST_GROUP2_NAME = AccountGroup.nameKey("test group");
+  public static final String TEST_GROUP1_UUID = "526d2bf882635380fbd3b72320464e342fc14533";
+  public static final String TEST_GROUP2_UUID = "62aa5663241f31b9483bad66132bd5d416b2bef9";
+  public static final InternalGroup TEST_GROUP1 =
+      buildTestGroup(AccountGroup.id(1), TEST_GROUP1_NAME, AccountGroup.uuid(TEST_GROUP1_UUID));
+  public static final InternalGroup TEST_GROUP2 =
+      buildTestGroup(AccountGroup.id(2), TEST_GROUP2_NAME, AccountGroup.uuid(TEST_GROUP2_UUID));
+  public static final String TEST_GROUP1_REF =
+      "refs/groups/" + TEST_GROUP1_UUID.substring(0, 2) + "/" + TEST_GROUP1_UUID;
+  public static final String TEST_GROUP2_REF =
+      "refs/groups/" + TEST_GROUP2_UUID.substring(0, 2) + "/" + TEST_GROUP2_UUID;
+  public static final FileKey TEST_GROUP1_ROOT_CFG =
+      createFileKey(ALL_USERS, TEST_GROUP1_REF, ROOT);
+  public static final FileKey TEST_GROUP1_COMMON_CFG =
+      createFileKey(ALL_USERS, TEST_GROUP1_REF, COMMON);
+  public static final FileKey TEST_GROUP2_ROOT_CFG =
+      createFileKey(ALL_USERS, TEST_GROUP2_REF, ROOT);
+  public static final FileKey TEST_GROUP2_COMMON_CFG =
+      createFileKey(ALL_USERS, TEST_GROUP2_REF, COMMON);
+
+  static InternalGroup buildTestGroup(
+      AccountGroup.Id id, AccountGroup.NameKey nameKey, AccountGroup.UUID uuid) {
+    return InternalGroup.builder()
+        .setGroupUUID(uuid)
+        .setNameKey(nameKey)
+        .setOwnerGroupUUID(uuid)
+        .setId(id)
+        .setVisibleToAll(true)
+        .setCreatedOn(new Timestamp(0L))
+        .setMembers(ImmutableSet.of())
+        .setSubgroups(ImmutableSet.of())
+        .build();
+  }
+
+  @Test
+  public void testReferencingTaskFromSameFile() throws Exception {
+    assertEquals(createTaskKey(ROOT_CFG, SIMPLE), getTaskFromReference(ROOT_CFG, SIMPLE));
+  }
+
+  @Test
+  public void testReferencingTaskFromRootConfig() throws Exception {
+    String reference = "^" + SIMPLE;
+    assertEquals(createTaskKey(ROOT_CFG, SIMPLE), getTaskFromReference(SUB_COMMON_CFG, reference));
+  }
+
+  @Test
+  public void testReferencingRelativeTaskFromRootConfig() throws Exception {
+    String reference = " dir/common.config^" + SIMPLE;
+    assertEquals(createTaskKey(SUB_COMMON_CFG, SIMPLE), getTaskFromReference(ROOT_CFG, reference));
+  }
+
+  @Test
+  public void testReferencingAbsoluteTaskFromRootConfig() throws Exception {
+    String reference = " /common.config^" + SIMPLE;
+    assertEquals(createTaskKey(COMMON_CFG, SIMPLE), getTaskFromReference(ROOT_CFG, reference));
+  }
+
+  @Test
+  public void testReferencingRelativeDirTask() throws Exception {
+    String reference = " dir/common.config^" + SIMPLE;
+    assertEquals(
+        createTaskKey(SUB_COMMON_CFG, SIMPLE), getTaskFromReference(COMMON_CFG, reference));
+  }
+
+  @Test
+  public void testReferencingRelativeFileTask() throws Exception {
+    String reference = "common.config^" + SIMPLE;
+    assertEquals(createTaskKey(COMMON_CFG, SIMPLE), getTaskFromReference(COMMON_CFG, reference));
+  }
+
+  @Test
+  public void testReferencingAbsoluteTask() throws Exception {
+    String reference = " /common.config^" + SIMPLE;
+    assertEquals(
+        createTaskKey(COMMON_CFG, SIMPLE), getTaskFromReference(SUB_COMMON_CFG, reference));
+  }
+
+  @Test
+  public void testReferencingRootAllProjectsTask() throws Exception {
+    String reference = "//^" + SIMPLE;
+    assertEquals(createTaskKey(ROOT_CFG, SIMPLE), getTaskFromReference(SAMPLE_PROJ_CFG, reference));
+  }
+
+  @Test
+  public void testReferencingAllProjectsTask() throws Exception {
+    String reference = "//common.config^" + SIMPLE;
+    assertEquals(
+        createTaskKey(COMMON_CFG, SIMPLE), getTaskFromReference(SAMPLE_PROJ_CFG, reference));
+  }
+
+  @Test
+  public void testReferencingRootUserTask() throws Exception {
+    String reference = "@" + TEST_USER + "^" + SIMPLE;
+    assertEquals(
+        createTaskKey(TEST_USER_ROOT_CFG, SIMPLE), getTaskFromReference(SUB_COMMON_CFG, reference));
+  }
+
+  @Test
+  public void testReferencingUserTaskDir() throws Exception {
+    String reference = "@" + TEST_USER + "/common.config^" + SIMPLE;
+    assertEquals(
+        createTaskKey(TEST_USER_COMMON_CFG, SIMPLE),
+        getTaskFromReference(SUB_COMMON_CFG, reference));
+  }
+
+  @Test
+  public void testMultipleUpchars() throws Exception {
+    String reference = " ^ /common.config^" + SIMPLE;
+    assertNoSuchElementException(() -> getTaskFromReference(SUB_COMMON_CFG, reference));
+  }
+
+  @Test
+  public void testEmptyReference() throws Exception {
+    String empty = "";
+    assertNoSuchElementException(() -> getTaskFromReference(SUB_COMMON_CFG, empty));
+  }
+
+  @Test
+  public void testReferencingRootGroupNameWithoutSpaceTask() throws Exception {
+    String reference = "%" + TEST_GROUP1_NAME.get() + "^" + SIMPLE;
+    assertEquals(
+        createTaskKey(TEST_GROUP1_ROOT_CFG, SIMPLE),
+        getTaskFromReference(SUB_COMMON_CFG, reference));
+  }
+
+  @Test
+  public void testReferencingRootGroupNameWithSpaceTask() throws Exception {
+    String reference = "%" + TEST_GROUP2_NAME.get() + "^" + SIMPLE;
+    assertEquals(
+        createTaskKey(TEST_GROUP2_ROOT_CFG, SIMPLE),
+        getTaskFromReference(SUB_COMMON_CFG, reference));
+  }
+
+  @Test
+  public void testReferencingGroupNameWithoutSpaceTaskDir() throws Exception {
+    String reference = "%" + TEST_GROUP1_NAME.get() + "/common.config^" + SIMPLE;
+    assertEquals(
+        createTaskKey(TEST_GROUP1_COMMON_CFG, SIMPLE),
+        getTaskFromReference(SUB_COMMON_CFG, reference));
+  }
+
+  @Test
+  public void testReferencingUnknownGroupName() throws Exception {
+    String reference = "%unknown^" + SIMPLE;
+    assertNoSuchElementException(() -> getTaskFromReference(SUB_COMMON_CFG, reference));
+  }
+
+  @Test
+  public void testReferencingEmptyGroupName() throws Exception {
+    String reference = "%^" + SIMPLE;
+    assertNoSuchElementException(() -> getTaskFromReference(SUB_COMMON_CFG, reference));
+  }
+
+  @Test
+  public void testReferencingGroupNameWithSpaceTaskDir() throws Exception {
+    String reference = "%" + TEST_GROUP2_NAME.get() + "/common.config^" + SIMPLE;
+    assertEquals(
+        createTaskKey(TEST_GROUP2_COMMON_CFG, SIMPLE),
+        getTaskFromReference(SUB_COMMON_CFG, reference));
+  }
+
+  @Test
+  public void testReferencingRootGroupUUIDTask() throws Exception {
+    String reference = "%%" + TEST_GROUP1_UUID + "^" + SIMPLE;
+    assertEquals(
+        createTaskKey(TEST_GROUP1_ROOT_CFG, SIMPLE),
+        getTaskFromReference(SUB_COMMON_CFG, reference));
+  }
+
+  @Test
+  public void testReferencingGroupUUIDTaskDir() throws Exception {
+    String reference = "%%" + TEST_GROUP1_UUID + "/common.config^" + SIMPLE;
+    assertEquals(
+        createTaskKey(TEST_GROUP1_COMMON_CFG, SIMPLE),
+        getTaskFromReference(SUB_COMMON_CFG, reference));
+  }
+
+  @Test
+  public void testReferencingUnknownGroupUUID() throws Exception {
+    String reference = "%%a8341ade45d83e867c24a2d37f47b410cfdbea6d^" + SIMPLE;
+    assertNoSuchElementException(() -> getTaskFromReference(SUB_COMMON_CFG, reference));
+  }
+
+  @Test
+  public void testReferencingEmptyGroupUUID() throws Exception {
+    String reference = "%%^" + SIMPLE;
+    assertNoSuchElementException(() -> getTaskFromReference(SUB_COMMON_CFG, reference));
+  }
+
+  protected static TaskKey getTaskFromReference(FileKey file, String expression) {
+    AccountCache accountCache = Mockito.mock(AccountCache.class);
+    GroupCache groupCache = Mockito.mock(GroupCache.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));
+    Mockito.when(groupCache.get(TEST_GROUP2_NAME)).thenReturn(Optional.of(TEST_GROUP2));
+    Mockito.when(groupCache.get(AccountGroup.uuid(TEST_GROUP1_UUID)))
+        .thenReturn(Optional.of(TEST_GROUP1));
+    Mockito.when(groupCache.get(AccountGroup.uuid(TEST_GROUP2_UUID)))
+        .thenReturn(Optional.of(TEST_GROUP2));
+
+    try {
+      return new TaskReference(
+              new TaskKey.Builder(
+                  file,
+                  new AllProjectsName(ALL_PROJECTS),
+                  new AllUsersName(ALL_USERS),
+                  accountCache,
+                  groupCache),
+              expression)
+          .getTaskKey();
+    } catch (ConfigInvalidException e) {
+      throw new NoSuchElementException();
+    }
+  }
+
+  protected static TaskKey createTaskKey(FileKey file, String task) {
+    return TaskKey.create(file, task);
+  }
+
+  protected static FileKey createFileKey(String project, String branch, String file) {
+    return FileKey.create(BranchNameKey.create(Project.NameKey.parse(project), branch), file);
+  }
+
+  protected static void assertNoSuchElementException(Executable f) throws Exception {
+    try {
+      f.run();
+      assertTrue(false);
+    } catch (NoSuchElementException e) {
+      assertTrue(true);
+    }
+  }
+
+  @FunctionalInterface
+  interface Executable {
+    void run() throws Exception;
+  }
+}
diff --git a/test/check_task_statuses.sh b/test/check_task_statuses.sh
index 5b7e161..30e2ece 100755
--- a/test/check_task_statuses.sh
+++ b/test/check_task_statuses.sh
@@ -15,277 +15,82 @@
 # limitations under the License.
 
 # Usage:
-# All-Projects.git - must have 'Push' rights on refs/meta/config
+# 1. All-Projects.git - must have 'Push' rights on refs/meta/config for test user
+# 2. All-Projects.git - must have 'viewTaskPaths' capability for test user
+# 3. All-Projects.git - must have 'accessDatabase' capability for test user
+# 4. All-Users.git - must have 'push' rights on refs/users/* for test user
+# 5. All-Users.git - must have 'push' rights on refs/users/${shardeduserid} for Registered Users
+# 6. All-Users.git - must have 'read' rights on refs/users/${shardeduserid} for Registered Users
+# 7. All-Users.git - must have 'create' rights on refs/users/${shardeduserid} for Registered Users
+# 8. All-Users.git - must deny 'read' rights on refs/* for Anonymous Users
+# 9. GERRIT_GIT_DIR environment variable must have the path to gerrit
+#    site's git directory (as group ref updates are done directly to git).
 
-# ---- TEST RESULTS ----
-result() { # test [error_message]
-    local result=$?
-    if [ $result -eq 0 ] ; then
-        echo "PASSED - $1 test"
-    else
-        echo "*** FAILED *** - $1 test"
-        RESULT=$result
-        [ $# -gt 1 ] && echo "$2"
-    fi
-}
+create_configs_from_task_states() {
+    for marker in $(md_file_markers "$DOC_STATES") ; do
+        local project_name="$(md_file_marker_project "$marker")"
+        local project_dir="$OUT/$project_name"
+        local file="$(md_file_marker_file "$marker")"
+        local ref="$(md_file_marker_ref "$marker")"
 
-# output must match expected to pass
-result_out() { # test expected actual
-    local name=$1 expected=$2 actual=$3
-
-    [ "$expected" = "$actual" ]
-    result "$name" "$(diff <(echo "$expected") <(echo "$actual"))"
-}
-
-result_root() { # group root expected_file actual_file
-    local name="$1 - $(echo "$2" | sed -es'/Root //')"
-    result_out "$name" "$(get_root "$2" < "$3")" "$(get_root "$2" < "$4")"
-}
-
-# -------- Git Config
-
-config() { git config -f "$CONFIG" "$@" ; } # [args]...
-config_section_keys() { # section > keys ...
-    # handlers.handler-filter filter.sh -> handler-filter
-    config -l --name-only |\
-        grep "^$1\." | \
-        sed -es"/^$1\.//;s/\..*$//" |\
-        awk '$0 != prev ; {prev = $0}'
-}
-
-# -------- Pre JSON --------
-#
-# pre_json is a "templated json" used in the test docs to express test results. It looks
-# like json but has some extra comments to express when a certain output should be used.
-# These comments look like: "# Only Test Suite: <suite>"
-#
-
-remove_suite() { # suite < pre_json > json
-    grep -v "# Only Test Suite: $1" | \
-    sed -e's/# Only Test Suite:.*$//; s/ *$//'
-}
-
-remove_not_suite() { remove_suite !"$1" ; } # suite < pre_json > json
-
-# -------- Test Doc Format --------
-#
-# Test Doc Format has intermixed git config task definitions with json roots. This
-# makes it easy to define tests close to their outputs. Be aware that all of the
-# config will get consolidated into a single file, so non root config will be shared
-# amongst all the roots.
-#
-
-# Sample Test Doc for 2 roots:
-#
-# [root "Root PASS"]
-#   pass = True
-#
-# {
-#    "applicable" : true,
-#    "hasPass" : true,
-#    "name" : "Root PASS",
-#    "status" : "PASS"
-# }
-#
-# [root "Root FAIL"]
-#   fail = True
-#
-# {
-#    <other root>
-# }
-
-# Strip the json from Test Doc formatted text. For the sample above, the output would be:
-#
-# [root "Root PASS"]
-#   pass = True
-#
-# [root "Root FAIL"]
-#   fail = True
-# ...
-#
-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:
-#
-# { "plugins" : [
-#     { "name" : "task",
-#       "roots" : [
-#         {
-#           "applicable" : true,
-#           "hasPass" : true,
-#           "name" : "Root PASS",
-#           "status" : "PASS"
-#        },
-#        {
-#           <other root>
-#        },
-#    ...
-# }
-testdoc_2_pjson() { # < testdoc_format > pjson_task_roots
-    awk 'BEGIN { print "{ \"plugins\" : [ { \"name\" : \"task\", \"roots\" : [" }; \
-         /^\{/  { open=1 }; \
-         open && end { print "}," ; end=0 }; \
-         /^\}/  { open=0 ; end=1 }; \
-         open; \
-         END   { print "}]}]}" }'
-}
-
-# ---- 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)"
-}
-
-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_val_by "$1" "'$2'" ; }  # json key > value
-
-# --------
-gssh() { ssh -x -p "$PORT" "$SERVER" gerrit "$@" ; } # cmd [args]...
-
-q() { "$@" > /dev/null 2>&1 ; } # cmd [args...]  # quiet a command
-
-gen_change_id() { echo "I$(uuidgen | openssl dgst -sha1 -binary | xxd -p)"; } # > change_id
-
-commit_message() { printf "$1 \n\nChange-Id: $2" ; } # message change-id > commit_msg
-
-err() { echo "ERROR: $1" >&2 ; exit 1 ; }
-
-# Run a test setup command quietly, exit on failure
-q_setup() { local out ; out=$("$@" 2>&1) || err "$out" ; } # cmd [args...]
-
-ensure() { "$@" || err "$1 results are not valid" ; } # cmd [args]... < data > data
-
-set_change() { # change_json
-    { CHANGE=("$(json_val_by_key "$1" number)" \
-        "$(json_val_by_key "$1" id)" \
-        "$(json_val_by_key "$1" project)" \
-        "refs/heads/$(json_val_by_key "$1" branch)" \
-        "$(json_val_by_key "$1" status)" \
-        "$(json_val_by_key "$1" topic)") ; } 2> /dev/null
-}
-
-# change_token change_number change_id project branch status topic < templated_txt > change_txt
-replace_change_properties() {
-    sed -e "s|_change$1_number|$2|g" \
-        -e "s|_change$1_id|$3|g" \
-        -e "s|_change$1_project|$4|g" \
-        -e "s|_change$1_branch|$5|g" \
-        -e "s|_change$1_status|$6|g" \
-        -e "s|_change$1_topic|$7|g"
-}
-
-replace_default_changes() {
-    replace_change_properties "1" "${CHANGE1[@]}" | replace_change_properties "2" "${CHANGE2[@]}"
-}
-
-replace_user() { # < text_with_testuser > text_with_$USER
-    sed -e"s/testuser/$USER/"
-}
-
-strip_non_applicable() { ensure "$MYDIR"/strip_non_applicable.py ; } # < json > json
-strip_non_invalid() { ensure "$MYDIR"/strip_non_invalid.py ; } # < json > json
-
-get_root() { # root < task_plugin_ouptut > root_json
-    python -c "if True: # NOP to start indent
-        import sys, json
-
-        roots=json.loads(sys.stdin.read())['plugins'][0]['roots']
-        for root in roots:
-            if 'name' in root.keys() and root['name']=='$1':
-                print json.dumps(root, indent=3, separators=(',', ' : '), sort_keys=True)"
-}
-
-example() { # example_num
-    echo "$DOC_STATES" | awk '/```/{Q++;E=(Q+1)/2};E=='"$1" | grep -v '```' | replace_user
-}
-
-get_change_num() { # < gerrit_push_response > changenum
-    local url=$(awk '$NF ~ /\[NEW\]/ { print $2 }')
-    echo "${url##*\/}" | tr -d -c '[:digit:]'
-}
-
-install_changeid_hook() { # repo
-    local hook=$(git rev-parse --git-dir)/hooks/commit-msg
-    scp -p -P "$PORT" "$SERVER":hooks/commit-msg "$hook"
-    chmod +x "$hook"
-}
-
-setup_repo() { # repo remote ref [--initial-commit]
-    local repo=$1 remote=$2 ref=$3 init=$4
-    git init "$repo"
-    (
-        cd "$repo"
-        install_changeid_hook "$repo"
-        git fetch "$remote" "$ref"
-        if ! git checkout FETCH_HEAD ; then
-            if [ "$init" = "--initial-commit" ] ; then
-                git commit --allow-empty -a -m "Initial Commit"
-            fi
+        if [[ "$ref" == refs/groups/* ]] ; then
+            project_dir="$project_dir-${ref:(-7)}}"
+            q_setup setup_repo "$project_dir" "$REMOTE_USERS" "$ref"
         fi
-    )
-}
 
-update_repo() { # repo remote ref
-    local repo=$1 remote=$2 ref=$3
-    (
-        cd "$repo"
-        git add .
-        git commit -m 'Testing task plugin'
-        git push "$remote" HEAD:"$ref"
-    )
-}
+        mkdir -p "$(dirname "$project_dir/$file")"
+        md_marker_content "$DOC_STATES" "$marker" | replace_user \
+            | testdoc_2_cfg > "$project_dir/$file"
 
-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"
-        date > file
-        q git add .
-        [ -n "$change_id" ] && msg=$(commit_message "$msg" "$change_id")
-        q git commit -m "$msg"
-        git push "$remote" HEAD:"refs/for/$ref" 2>&1 | get_change_num
-    )
-}
-
-query_plugins() { # query
-    gssh query "$@" --format json | head -1 | python -c "import sys, json; \
-        plugins={}; plugins['plugins']=json.loads(sys.stdin.read())['plugins']; \
-        print json.dumps(plugins, indent=3, separators=(',', ' : '), sort_keys=True)"
-}
-
-test_tasks() { # name expected_file task_args...
-    local name=$1 expected=$2 ; shift 2
-    local output=$STATUSES.$name out root
-
-    query_plugins "$@" > "$output"
-    echo "$ROOTS" | while read root ; do
-        result_root "$name" "$root" "$expected" "$output"
+        if [[ "$ref" == refs/groups/* ]] ; then
+            # As support for pushing a change to group refs [1] is not yet in any release,
+            # push the update behind gerrit's back, directly into git.
+            # [1] https://gerrit-review.googlesource.com/c/gerrit/+/390614
+            q_setup update_repo "$project_dir" "$GERRIT_GIT_DIR/All-Users.git" "$ref"
+        fi
     done
-    out=$(diff "$expected" "$output" | head -15)
-    [ -z "$out" ]
-    result "$name - Full Test Suite" "$out"
 }
 
-test_generated() { # name task_args...
+test_2generated() { # name task_args...
     local name=$1 ; shift
-    test_tasks "$name" "$EXPECTED.$name" "$@"
+    local out=$(query "$@")
+    results_suite "$name" "$EXPECTED.$name" "$(echo "$out" | change_plugins 1)"
+    results_suite "$name 2nd change" "$EXPECTED.$name"2 "$(echo "$out" | change_plugins 2)"
 }
 
-test_file() { # name task_args...
+test_generated() { # name [-l query_user] task_args...
     local name=$1 ; shift
-    local expected=$MYDIR/$name output=$STATUSES.$name
+    query "$@" | change_plugins 1 > "$ACTUAL.$name"
+    results_suite "$name" "$EXPECTED.$name" "$( < "$ACTUAL.$name")"
+}
 
-    query "$@" | awk '$0=="   \"plugins\" : [",$0=="   ],"' > "$output"
-    out=$(diff "$expected" "$output")
-    result "$name" "$out"
+usage() { # [error_message]
+    cat <<-EOF
+Usage:
+    "$MYPROG" --server <gerrit_host> --non-secret-user <non-secret user>
+    --untrusted-user <untrusted user>
+
+    --help|-h                         help text
+    --server|-s                       gerrit host
+    --non-secret-user                 user who don't have permission
+                                      to view other user refs.
+    --untrusted-user                  user who doesn't have permission
+                                      to view refs/meta/config ref on All-Projects repo
+    --non-secret-group-without-space  non-secret group name without spaces
+    --non-secret-group-with-space     non-secret group name with spaces
+EOF
+
+    [ -n "$1" ] && { echo "Error: $1" ; exit 1 ; }
+    exit 0
 }
 
 readlink -f / &> /dev/null || readlink() { greadlink "$@" ; } # for MacOS
 MYDIR=$(dirname -- "$(readlink -f -- "$0")")
+MYPROG=$(basename -- "$0")
+
+source "$MYDIR/lib/lib_helper.sh"
+source "$MYDIR/lib/lib_md.sh"
+
 DOCS=$MYDIR/.././src/main/resources/Documentation/test
 OUT=$MYDIR/../target/tests
 
@@ -295,20 +100,36 @@
 USERS=$OUT/All-Users
 USER_TASKS=$USERS/task
 
-DOC_PREVIEW=$DOCS/preview.md
 EXPECTED=$OUT/expected
-STATUSES=$OUT/statuses
+ACTUAL=$OUT/actual
 
 ROOT_CFG=$ALL/task.config
-COMMON_CFG=$ALL_TASKS/common.config
-INVALIDS_CFG=$ALL_TASKS/invalids.config
-USER_SPECIAL_CFG=$USER_TASKS/special.config
 
 # --- Args ----
-SERVER=$1
-[ -z "$SERVER" ] && { echo "You must specify a server" ; exit ; }
+
+while (( "$#" )) ; do
+    case "$1" in
+        --help|-h)                        usage ;;
+        --server|-s)                      shift ; SERVER=$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 ;;
+        --non-secret-group-with-space)    shift ; GROUP_NAME_WITH_SPACE=$1 ;;
+        *)                                usage "invalid argument $1" ;;
+    esac
+    shift
+done
+
+[ -z "$SERVER" ] && usage "You must specify --server"
+[ -z "$NON_SECRET_USER" ] && usage "You must specify --non-secret-user"
+[ -z "$UNTRUSTED_USER" ] && usage "You must specify --untrusted-user"
+[ -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"
+
 
 PORT=29418
+HTTP_PORT=8080
 PROJECT=test
 BRANCH=master
 REMOTE_ALL=ssh://$SERVER:$PORT/All-Projects
@@ -320,6 +141,16 @@
 
 CONFIG=$ROOT_CFG
 
+declare -A USER_REFS
+USER_REFS["{testuser_user_ref}"]="$(get_user_ref "$USER")"
+
+declare -A GROUP_EXPANDED_BY_PLACEHOLDER
+GROUP_EXPANDED_BY_PLACEHOLDER["{non_secret_group_name_without_space}"]="$GROUP_NAME_WITHOUT_SPACE"
+GROUP_EXPANDED_BY_PLACEHOLDER["{non_secret_group_name_with_space}"]="$GROUP_NAME_WITH_SPACE"
+GROUP_EXPANDED_BY_PLACEHOLDER["{non_secret_group_uuid}"]="$(get_group_uuid "$GROUP_NAME_WITHOUT_SPACE")"
+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"
 
 q_setup setup_repo "$ALL" "$REMOTE_ALL" "$REF_ALL"
@@ -329,12 +160,12 @@
 changes=$(gssh query "status:open limit:2" --format json)
 set_change "$(echo "$changes" | awk 'NR==1')" ; CHANGE1=("${CHANGE[@]}")
 set_change "$(echo "$changes" | awk 'NR==2')" ; CHANGE2=("${CHANGE[@]}")
-DOC_STATES=$(replace_default_changes < "$DOCS/task_states.md")
 
-example 2 | replace_user | testdoc_2_cfg > "$ROOT_CFG"
-example 3 > "$COMMON_CFG"
-example 4 > "$INVALIDS_CFG"
-example 5 > "$USER_SPECIAL_CFG"
+DOC_STATES=$(replace_tokens < "$DOCS/task_states.md")
+DOC_PREVIEW=$(replace_tokens < "$DOCS/preview.md")
+DOC_PATHS=$(replace_tokens < "$DOCS/paths.md")
+
+create_configs_from_task_states
 
 ROOTS=$(config_section_keys "root") || err "Invalid ROOTS"
 
@@ -342,9 +173,12 @@
 q_setup update_repo "$USERS" "$REMOTE_USERS" "$REF_USERS"
 
 change3_id=$(gen_change_id)
+change4_id=$(gen_change_id)
+change4_number=$(create_repo_change "$OUT/$PROJECT" "$REMOTE_TEST" "$BRANCH" "$change4_id")
 change3_number=$(create_repo_change "$OUT/$PROJECT" "$REMOTE_TEST" "$BRANCH" "$change3_id")
 
-all_pjson=$(example 2 | testdoc_2_pjson | \
+ex2_pjson=$(example "$DOC_STATES" 2 | testdoc_2_pjson)
+all_pjson=$(echo "$ex2_pjson" | \
     replace_change_properties \
         "" \
         "$change3_number" \
@@ -354,36 +188,82 @@
         "NEW" \
         "")
 
-no_all_json=$(echo "$all_pjson" | remove_suite all)
+all2_pjson=$(echo "$ex2_pjson" | \
+    replace_change_properties \
+        "" \
+        "$change4_number" \
+        "$change4_id" \
+        "$PROJECT" \
+        "refs\/heads\/$BRANCH" \
+        "NEW" \
+        "")
 
-echo "$no_all_json" | strip_non_applicable | \
+no_all_visible_json=$(echo "$all_pjson" | remove_suites "all" "!visible")
+no_all_no_visible_json=$(echo "$all_pjson" | remove_suites "all" "visible")
+no_all_visible2_json=$(echo "$all2_pjson" | remove_suites "all" "!visible")
+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
 
-echo "$all_pjson" | remove_not_suite all | ensure json_pp > "$EXPECTED".all
+echo "$no_all_no_visible_json" | strip_non_applicable | \
+    grep -v "\"applicable\" :" > "$EXPECTED".applicable-visibility
 
-echo "$no_all_json" | strip_non_invalid > "$EXPECTED".invalid
+echo "$no_all_visible2_json" | strip_non_applicable | \
+    grep -v "\"applicable\" :" > "$EXPECTED".applicable2
+
+echo "$no_all_no_visible2_json" | strip_non_applicable | \
+    grep -v "\"applicable\" :" > "$EXPECTED".applicable-visibility2
+
+echo "$all_pjson" | remove_suites "!all" "!visible" | ensure json_pp > "$EXPECTED".all
+
+echo "$no_all_visible_json" | strip_non_invalid > "$EXPECTED".invalid
 
 strip_non_invalid < "$EXPECTED".applicable > "$EXPECTED".invalid-applicable
 
 
-preview_pjson=$(testdoc_2_pjson < "$DOC_PREVIEW" | replace_default_changes)
-echo "$preview_pjson" | remove_suite invalid | ensure json_pp > "$EXPECTED".preview
-echo "$preview_pjson" | remove_not_suite invalid | strip_non_invalid > "$EXPECTED".preview-invalid
+preview_pjson=$(example "$DOC_PREVIEW" 1 | testdoc_2_pjson)
+echo "$preview_pjson" | remove_suites "invalid" "secret" | \
+    ensure json_pp > "$EXPECTED".preview-non-secret
+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
 
-testdoc_2_cfg < "$DOC_PREVIEW" | replace_user > "$ROOT_CFG"
+example "$DOC_PREVIEW" 1 | testdoc_2_cfg | replace_user > "$ROOT_CFG"
 cnum=$(create_repo_change "$ALL" "$REMOTE_ALL" "$REF_ALL")
 PREVIEW_ROOTS=$(config_section_keys "root")
 
 
 RESULT=0
-query="change:$change3_number status:open"
-test_generated applicable --task--applicable "$query"
+query="(change:$change3_number OR change:$change4_number) status:open"
+test_2generated applicable --task--applicable "$query"
+test_2generated applicable-visibility -l "$UNTRUSTED_USER" --task--applicable "$query"
 test_generated all --task--all "$query"
 
 test_generated invalid --task--invalid "$query"
 test_generated invalid-applicable --task--applicable --task--invalid "$query"
 
 ROOTS=$PREVIEW_ROOTS
-test_generated preview --task--preview "$cnum,1" --task--all "$query"
+test_generated preview-admin --task--preview "$cnum,1" --task--all "$query"
+test_generated preview-non-secret -l "$NON_SECRET_USER" --task--preview "$cnum,1" --task--all "$query"
 test_generated preview-invalid --task--preview "$cnum,1" --task--invalid "$query"
+
+example "$DOC_STATES" 2 | keep_suites "task_only" | testdoc_2_pjson | \
+    ensure json_pp > "$EXPECTED".task-roots-filter
+test_generated task-roots-filter --task--all --task--only "Root\ PASS" "$query"
+
+example "$DOC_PATHS" 1 | testdoc_2_cfg | replace_user > "$ROOT_CFG"
+q_setup update_repo "$ALL" "$REMOTE_ALL" "$REF_ALL"
+ROOTS=$(config_section_keys "root")
+example "$DOC_PATHS" 1 | testdoc_2_pjson | ensure json_pp > "$EXPECTED".task-paths
+
+test_generated task-paths --task--all --task--include-paths "$query"
+
+example "$DOC_PATHS" 2 | testdoc_2_cfg > "$ROOT_CFG"
+q_setup update_repo "$ALL" "$REMOTE_ALL" "$REF_ALL"
+ROOTS=$(config_section_keys "root")
+example "$DOC_PATHS" 2 | testdoc_2_pjson | ensure json_pp > "$EXPECTED".task-paths.non-secret
+test_generated task-paths.non-secret -l "$NON_SECRET_USER" --task--all --task--include-paths "$query"
+
 exit $RESULT
diff --git a/test/check_task_visibility.sh b/test/check_task_visibility.sh
new file mode 100755
index 0000000..1dabec4
--- /dev/null
+++ b/test/check_task_visibility.sh
@@ -0,0 +1,248 @@
+#!/usr/bin/env bash
+#
+# Copyright (C) 2022 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.
+
+# Usage:
+# 1. All-Projects.git - must have 'Push' rights on refs/meta/config for test user
+# 2. All-Projects.git - must have 'viewTaskPaths' capability for test user
+# 3. All-Projects.git - must have 'accessDatabase' capability for test user
+# 4. All-Users.git - must have 'push' rights on refs/users/* for test user
+# 5. All-Users.git - must have 'push' rights on refs/users/${shardeduserid} for Registered Users
+# 6. All-Users.git - must have 'read' rights on refs/users/${shardeduserid} for Registered Users
+# 7. All-Users.git - must have 'create' rights on refs/users/${shardeduserid} for Registered Users
+# 8. All-Users.git - must deny 'read' rights on refs/* for Anonymous Users
+# 9. GERRIT_GIT_DIR environment variable must have the path to gerrit
+#    site's git directory (as group ref updates are done directly to git).
+
+readlink -f / &> /dev/null || readlink() { greadlink "$@" ; } # for MacOS
+MYDIR=$(dirname -- "$(readlink -f -- "$0")")
+MYPROG=$(basename -- "$0")
+
+source "$MYDIR/lib/lib_helper.sh"
+source "$MYDIR/lib/lib_md.sh"
+
+# Visibility tests cases are described using a markdown file.
+# Each file has a list of config files specified by file
+# markers. The initial state of task configs is created using
+# them. Only one of the config file has an inline diff. Gerrit
+# change is created by applying that diff to the specified file
+# marker and the expected json is asserted by using that change
+# as an input to the '--task-preview' switch.
+
+# The syntax for inline diff is similar to  diff --unified=MAX_INT.
+# All lines start with a leading space and if a specific line is
+# part of diff, we use diff indicators (+/-) instead of a leading
+# space.
+
+# Example input for all diff functions:
+#
+#  [root "Root Preview SECRET external"]
+#      applicable = is:open
+#      pass = True
+# -    subtask = Subtask APPLICABLE
+# +    subtasks-external = SECRET external
+#
+# +[external "SECRET external"]
+# +    user = {secret_user}
+# +    file = secret.config
+
+
+# Returns if a config has inline diff or not.
+diff_indicators_present() { # file_content
+    echo "$1" | grep -q "^-\|^+"
+}
+
+# file_content_with_diff_indicators > file_content_with_diff_applied
+# out:
+#[root "Root Preview SECRET external"]
+#    applicable = is:open
+#    pass = True
+#    subtask = Subtask APPLICABLE
+diff_apply() {
+    sed -e '/^-/d' -e 's/^.//'
+}
+
+# file_content_with_diff_indicators > file_content_with_diff_reverted
+# out:
+#[root "Root Preview SECRET external"]
+#    applicable = is:open
+#    pass = True
+#    subtasks-external = SECRET external
+#
+#[external "SECRET external"]
+#    user = {secret_user}
+#    file = secret.config
+diff_revert() {
+    sed -e '/^+/d' -e 's/^.//'
+}
+
+config_ensure() { # config_file_path
+    q git config --list -f "$1" || err "Invalid config file: $1"
+}
+
+get_remote() { # project > remote_url
+    echo "ssh://$SERVER:$PORT/$(basename "$1")"
+}
+
+# Gets json from the preview doc and creates
+# expected json in workspace to assert later.
+create_expected_json() {
+    local json=$(md_marker_content "$TEST_DOC" "json:")
+
+    echo "$json" | remove_suites "non-secret" | \
+      testdoc_2_pjson | ensure json_pp > "$EXPECTED_SECRET"
+    echo "$json" | remove_suites "secret" | \
+      testdoc_2_pjson | ensure json_pp > "$EXPECTED_NON_SECRET"
+}
+
+test_preview() { # preview_change_number
+    query --task--all --task--preview "$1,1" "change:1" \
+      | change_plugins 1 > "$ACTUAL_SECRET"
+    query -l "$NON_SECRET_USER" --task--all --task--preview "$1,1" "change:1" \
+      | change_plugins 1 > "$ACTUAL_NON_SECRET"
+
+    ROOTS=$(jq -r '.plugins[].roots | .[].name' < "$EXPECTED_SECRET")
+    results_suite "Visibility Secret Test" "$EXPECTED_SECRET" "$( < "$ACTUAL_SECRET" )"
+
+    ROOTS=$(jq -r '.plugins[].roots | .[].name' < "$EXPECTED_NON_SECRET")
+    results_suite "Visibility Non-Secret Test" "$EXPECTED_NON_SECRET" "$( < "$ACTUAL_NON_SECRET" )"
+}
+
+init_configs() {
+    for marker in $(md_file_markers "$TEST_DOC") ; do
+        local project="$OUT/$(md_file_marker_project "$marker")"
+        local ref="$(md_file_marker_ref "$marker")"
+        local file="$(md_file_marker_file "$marker")"
+        local content="$(md_marker_content "$TEST_DOC" "$marker")"
+        local tip_content
+
+        q_setup setup_repo "$project" "$(get_remote "$project")" "$ref"
+        mkdir -p "$(dirname "$project/$file")"
+
+        if diff_indicators_present "$content" ; then
+            CHANGE_FILE_MARKER=$marker
+            CHANGE_CONTENT=$(echo "$content" | diff_apply)
+            tip_content=$(echo "$content" | diff_revert)
+        else
+            tip_content=$content
+        fi
+
+        echo "$tip_content" > "$project/$file"
+        config_ensure "$project/$file"
+        if [[ "$ref" == refs/groups/* ]] ; then
+            # As support for pushing a change to group refs [1] is not yet in any release,
+            # push the update behind gerrit's back, directly into git.
+            # [1] https://gerrit-review.googlesource.com/c/gerrit/+/390614
+            q_setup update_repo "$project" "$GERRIT_GIT_DIR/All-Users.git" "$ref"
+        else
+            q_setup update_repo "$project" "$(get_remote "$project")" "$ref"
+        fi
+    done
+}
+
+test_change() {
+    local project="$OUT/$(md_file_marker_project "$CHANGE_FILE_MARKER")"
+    local ref="$(md_file_marker_ref "$CHANGE_FILE_MARKER")"
+    local file="$(md_file_marker_file "$CHANGE_FILE_MARKER")"
+    q_setup setup_repo "$project" "$(get_remote "$project")" "$ref"
+
+    echo "$CHANGE_CONTENT"  > "$project/$file"
+    config_ensure "$project/$file"
+    local cnum=$(create_repo_change "$project" "$(get_remote "$project")" "$ref")
+
+    create_expected_json
+    test_preview "$cnum"
+}
+
+usage() { # [error_message]
+    cat <<-EOF
+Usage:
+    "$MYPROG" --server <gerrit_host> --non-secret-user <non-secret user>
+
+    --help|-h                     help text
+    --server|-s                   gerrit host
+    --non-secret-user             user who doesn't have permission
+                                  to view other user refs.
+    --non-secret-group            non-secret group name
+    --secret-group                secret group name
+EOF
+
+    [ -n "$1" ] && { echo "Error: $1" ; exit 1 ; }
+    exit 0
+}
+
+while (( "$#" )) ; do
+    case "$1" in
+        --help|-h)                usage ;;
+        --server|-s)              shift ; SERVER=$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 ;;
+        *)                        usage "invalid argument $1" ;;
+    esac
+    shift
+done
+
+[ -z "$SERVER" ] && usage "You must specify --server"
+[ -z "$NON_SECRET_USER" ] && usage "You must specify --non-secret-user"
+[ -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"
+
+RESULT=0
+PORT=29418
+HTTP_PORT=8080
+OUT=$MYDIR/../target/preview
+EXPECTED_SECRET="$OUT/expected-secret"
+EXPECTED_NON_SECRET="$OUT/expected-non-secret"
+ACTUAL_SECRET="$OUT/actual-secret"
+ACTUAL_NON_SECRET="$OUT/actual-non-secret"
+TEST_DOC_DIR="$MYDIR/../src/main/resources/Documentation/test/task-preview/"
+
+declare -A USERS
+declare -A USER_REFS
+USERS["{secret_user}"]="$USER"
+USER_REFS["{secret_user_ref}"]="$(get_user_ref "$USER")"
+USERS["{non_secret_user}"]="$NON_SECRET_USER"
+USER_REFS["{non_secret_user_ref}"]="$(get_user_ref "$NON_SECRET_USER")"
+
+declare -A GROUP_EXPANDED_BY_PLACEHOLDER
+GROUP_EXPANDED_BY_PLACEHOLDER["{secret_group_name}"]="$SECRET_GROUP_NAME"
+GROUP_EXPANDED_BY_PLACEHOLDER["{sharded_secret_group_uuid}"]="$(get_sharded_group_uuid "$SECRET_GROUP_NAME")"
+GROUP_EXPANDED_BY_PLACEHOLDER["{non_secret_group_name}"]="$NON_SECRET_GROUP_NAME"
+GROUP_EXPANDED_BY_PLACEHOLDER["{sharded_non_secret_group_uuid}"]="$(get_sharded_group_uuid "$NON_SECRET_GROUP_NAME")"
+
+mkdir -p "$OUT"
+trap 'rm -rf "$OUT"' EXIT
+
+TESTS=(
+"new_root_with_original_with_external_secret_ref.md"
+"non-secret_ref_with_external_secret_ref.md"
+"root_with_external_non-secret_ref_with_external_secret_ref.md"
+"root_with_external_secret_ref.md"
+"non_root_with_subtask_from_root_task.md"
+"subtask_using_user_syntax/root_with_subtask_secret_ref.md"
+"subtask_using_user_syntax/root_with_subtask_non-secret_ref_with_subtask_secret_ref.md"
+"subtask_using_group_syntax/root_with_subtask_secret_ref.md"
+"subtask_using_group_syntax/root_with_subtask_non-secret_ref_with_subtask_secret_ref.md"
+)
+
+for test in "${TESTS[@]}" ; do
+    TEST_DOC="$(replace_user_refs < "$TEST_DOC_DIR/$test" | replace_users | replace_groups)"
+    init_configs
+    test_change
+done
+
+exit $RESULT
diff --git a/test/docker/docker-compose.yaml b/test/docker/docker-compose.yaml
index 634cde4..a228122 100755
--- a/test/docker/docker-compose.yaml
+++ b/test/docker/docker-compose.yaml
@@ -11,6 +11,7 @@
       - gerrit-net
     volumes:
       - "gerrit-site-etc:/var/gerrit/etc"
+      - "gerrit-site-git:/var/gerrit/git"
 
   run_tests:
     build: run_tests
@@ -19,10 +20,12 @@
     volumes:
       - "../../:/task:ro"
       - "gerrit-site-etc:/server-ssh-key:ro"
+      - "gerrit-site-git:/gerrit-site-git"
     depends_on:
       - gerrit-01
     environment:
       - GERRIT_HOST=gerrit-01
+      - GERRIT_GIT_DIR=/gerrit-site-git
 
 networks:
   gerrit-net:
@@ -30,3 +33,4 @@
 
 volumes:
   gerrit-site-etc:
+  gerrit-site-git:
diff --git a/test/docker/gerrit/Dockerfile b/test/docker/gerrit/Dockerfile
index 12aa74a..a407854 100755
--- a/test/docker/gerrit/Dockerfile
+++ b/test/docker/gerrit/Dockerfile
@@ -1,9 +1,11 @@
-FROM gerritcodereview/gerrit:3.4.0-ubuntu20
+FROM gerritcodereview/gerrit:3.5.6-ubuntu20
 
 ENV GERRIT_SITE /var/gerrit
 RUN git config -f "$GERRIT_SITE/etc/gerrit.config" auth.type \
     DEVELOPMENT_BECOME_ANY_ACCOUNT
+RUN touch "$GERRIT_SITE"/.firstTimeRedirect
 
 COPY artifacts /tmp/
 RUN cp /tmp/task.jar "$GERRIT_SITE/plugins/task.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 75b9b3a..5f9b412 100755
--- a/test/docker/run.sh
+++ b/test/docker/run.sh
@@ -57,12 +57,12 @@
 
 run_task_plugin_tests() {
     docker-compose "${COMPOSE_ARGS[@]}" up --detach
-    docker-compose "${COMPOSE_ARGS[@]}" exec -T --user=gerrit_admin run_tests \
+    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=gerrit_admin \
+    docker-compose "${COMPOSE_ARGS[@]}" exec -T --user=admin \
         run_tests task/test/docker/run_tests/start.sh retest
     RESULT=$?
     cleanup
diff --git a/test/docker/run_tests/Dockerfile b/test/docker/run_tests/Dockerfile
index 06691e1..dd5ba8c 100755
--- a/test/docker/run_tests/Dockerfile
+++ b/test/docker/run_tests/Dockerfile
@@ -2,12 +2,12 @@
 
 ARG UID=1000
 ARG GID=1000
-ENV USER gerrit_admin
+ENV USER admin
 ENV USER_HOME /home/$USER
 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
+RUN apk --update add --no-cache openssh bash git python2 shadow util-linux openssl xxd curl jq
 RUN echo "StrictHostKeyChecking no" >> /etc/ssh/ssh_config
 
 RUN groupadd -f -g $GID users2
@@ -21,6 +21,6 @@
 RUN chmod 400 "$USER_HOME"/.ssh/id_rsa
 RUN chmod 400 "$USER_HOME"/.ssh/id_rsa.pub
 RUN git config --global user.name "Gerrit Admin"
-RUN git config --global user.email "gerrit_admin@localdomain"
+RUN git config --global user.email "admin@example.com"
 
 ENTRYPOINT ["tail", "-f", "/dev/null"]
diff --git a/test/docker/run_tests/create-one-time-test-data.sh b/test/docker/run_tests/create-one-time-test-data.sh
new file mode 100755
index 0000000..d949bb2
--- /dev/null
+++ b/test/docker/run_tests/create-one-time-test-data.sh
@@ -0,0 +1,73 @@
+#!/usr/bin/env bash
+
+die() { echo -e "\nERROR:" "$@" ; kill $$ ; exit 1 ; } # error_message
+
+q() { "$@" > /dev/null 2>&1 ; } # cmd [args...]  # quiet a command
+
+gssh() { ssh -x -p "$SSH_PORT" "$GERRIT_HOST" gerrit "$@" ; } # run a gerrit ssh command
+
+create_test_users_and_group() {
+    echo "Creating test users and group ..."
+    gssh create-account "$NON_SECRET_USER" --full-name "$NON_SECRET_USER" \
+           --email "$NON_SECRET_USER"@example.com --ssh-key - < ~/.ssh/id_rsa.pub
+
+    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"
+
+    local secret_user=$USER
+    gssh create-group "$NON_SECRET_GROUP_NAME_WITHOUT_SPACE" \
+        --member "$NON_SECRET_USER" --member "$secret_user"
+    gssh create-group "\"$NON_SECRET_GROUP_NAME_WITH_SPACE\"" \
+        --member "$NON_SECRET_USER" --member "$secret_user"
+    gssh create-group "$SECRET_GROUP_NAME" --member "$secret_user"
+}
+
+setup_all_projects_repo() {
+    echo "Updating All-Projects repo ..."
+
+    local uuid=$(gssh ls-groups -v | awk '-F\t' '$1 == "Visible-All-Projects-Config" {print $2}')
+    ( cd "$WORKSPACE"
+      q git clone ssh://"$GERRIT_HOST":"$SSH_PORT"/All-Projects allProjects
+      cd allProjects
+      q git fetch origin refs/meta/config ; q git checkout FETCH_HEAD
+      echo -e "$uuid\tVisible-All-Projects-Config" >> groups
+      git config -f "project.config" \
+          --add access."refs/meta/config".read "group Visible-All-Projects-Config"
+      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
+#     Administrators for another user's ref. To address this, add the 'accessDatabase' capability,
+#     allowing admins to read the user ref of other users
+      git config -f "project.config" \
+                --add capability.accessDatabase "group Administrators"
+      q git add . && q git commit -m "project config update"
+      q git push origin HEAD:refs/meta/config
+    )
+}
+
+SSH_PORT=29418
+USER_RUN_TESTS_DIR="$USER_HOME"/"$RUN_TESTS_DIR"
+while (( "$#" )) ; do
+   case "$1" in
+       --non-secret-user)                 shift ; NON_SECRET_USER="$1" ;;
+       --untrusted-user)                  shift ; UNTRUSTED_USER="$1" ;;
+       --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" ;;
+       *)                                 die "invalid argument '$1'" ;;
+   esac
+   shift
+done
+
+[ -z "$NON_SECRET_USER" ] && die "non-secret-user not set"
+[ -z "$UNTRUSTED_USER" ] && die "untrusted-user not set"
+[ -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"
+
+"$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
diff --git a/test/docker/run_tests/start.sh b/test/docker/run_tests/start.sh
index ac185d8..4a73f24 100755
--- a/test/docker/run_tests/start.sh
+++ b/test/docker/run_tests/start.sh
@@ -1,23 +1,55 @@
 #!/usr/bin/env bash
 
+die() { echo "ERROR: $1" >&2 ; exit 1 ; } # errormsg
+
+is_plugin_loaded() { # plugin_name
+    ssh -p 29418 "$GERRIT_HOST" gerrit plugin ls | awk '{print $1}' | grep -q "^$1\$"
+}
+
 USER_RUN_TESTS_DIR="$USER_HOME"/"$RUN_TESTS_DIR"
-cp -r /task "$USER_HOME"/
+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"
     exit $?
 fi
 
-./"$USER_RUN_TESTS_DIR"/wait-for-it.sh "$GERRIT_HOST":29418 -t 60 -- echo "gerrit is up"
+./"$USER_RUN_TESTS_DIR"/wait-for-it.sh "$GERRIT_HOST":29418 \
+    -t -60 || die "Failed to start gerrit"
+echo "gerrit is up"
 
-echo "Creating a default user account ..."
+echo "Update admin account ..."
 
 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 create-account \
-     --ssh-key - --email "gerrit_admin@localdomain"  --group "Administrators" "gerrit_admin"
+    "Gerrit Code Review@$GERRIT_HOST" suexec --as "admin@example.com" -- gerrit set-account \
+    admin --add-ssh-key -
 
-./"$USER_RUN_TESTS_DIR"/create-test-project-and-changes.sh
-./"$USER_RUN_TESTS_DIR"/update-all-users-project.sh
+PASSWORD=$(uuidgen)
+echo "machine $GERRIT_HOST login $USER password $PASSWORD" > "$USER_HOME"/.netrc
+ssh -p 29418 "$GERRIT_HOST" gerrit set-account --http-password "$PASSWORD" "$USER"
+
+is_plugin_loaded "task" || die "Task plugin is not installed"
+
+NON_SECRET_USER="non_secret_user"
+UNTRUSTED_USER="untrusted_user"
+GROUP_NAME_WITHOUT_SPACE="test.group"
+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" \
+    --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"
 
 echo "Running Task plugin tests ..."
-cd "$USER_RUN_TESTS_DIR"/../../ && ./check_task_statuses.sh "$GERRIT_HOST"
+
+RESULT=0
+
+"$USER_RUN_TESTS_DIR"/../../check_task_statuses.sh \
+    --server "$GERRIT_HOST" --non-secret-user "$NON_SECRET_USER" \
+    --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" \
+    --non-secret-user "$NON_SECRET_USER" --non-secret-group "$GROUP_NAME_WITHOUT_SPACE" \
+    --secret-group "$SECRET_GROUP" || RESULT=1
+
+exit $RESULT
diff --git a/test/docker/run_tests/update-all-users-project.sh b/test/docker/run_tests/update-all-users-project.sh
index d0e1527..cfe2def 100755
--- a/test/docker/run_tests/update-all-users-project.sh
+++ b/test/docker/run_tests/update-all-users-project.sh
@@ -4,7 +4,13 @@
 
 cd "$WORKSPACE" && git clone ssh://"$GERRIT_HOST":29418/All-Users allusers && cd allusers
 git fetch origin refs/meta/config && git checkout FETCH_HEAD
-git config -f project.config access."refs/users/*".read "group Administrators"
 git config -f project.config access."refs/users/*".push "group Administrators"
-git config -f project.config access."refs/users/*".create "group Administrators"
+
+git config -f project.config access.'refs/users/${shardeduserid}'.read "group Registered Users"
+git config -f project.config access.'refs/users/${shardeduserid}'.push "group Registered Users"
+git config -f project.config access.'refs/users/${shardeduserid}'.create "group Registered Users"
+git config -f "project.config" \
+   access."refs/*".read "deny group Anonymous Users"
+echo -e "global:Registered-Users\tRegistered Users" >> groups
+echo -e "global:Anonymous-Users\tAnonymous Users" >> groups
 git add . && git commit -m "project config update" && git push origin HEAD:refs/meta/config
diff --git a/test/lib/lib_helper.sh b/test/lib/lib_helper.sh
new file mode 100644
index 0000000..f4006db
--- /dev/null
+++ b/test/lib/lib_helper.sh
@@ -0,0 +1,350 @@
+#!/usr/bin/env bash
+#
+# Copyright (C) 2021 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.
+
+# ---- TEST RESULTS ----
+result() { # test [error_message]
+    local result=$?
+    if [ $result -eq 0 ] ; then
+        echo "PASSED - $1 test"
+    else
+        echo "*** FAILED *** - $1 test"
+        RESULT=$result
+        [ $# -gt 1 ] && echo "$2"
+    fi
+}
+
+# output must match expected to pass
+result_out() { # test expected actual
+    local name=$1 expected=$2 actual=$3
+
+    [ "$expected" = "$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]}"
+}
+
+# -------- Git Config
+
+config() { git config -f "$CONFIG" "$@" ; } # [args]...
+config_section_keys() { # section > keys ...
+    # handlers.handler-filter filter.sh -> handler-filter
+    config -l --name-only |\
+        grep "^$1\." | \
+        sed -es"/^$1\.//;s/\..*$//" |\
+        awk '$0 != prev ; {prev = $0}'
+}
+
+# -------- Pre JSON --------
+#
+# pre_json is a "templated json" used in the test docs to express test results. It looks
+# like json but has some extra comments to express when a certain output should be used.
+# These comments look like: "# Only Test Suite: <suite>"
+#
+
+remove_suites() { # suites... < pre_json > json
+    grep -vE "# Only Test Suite: ($(echo "$@" | sed "s/ /|/g"))" | \
+         sed -e's/# Only Test Suite:.*$//; s/# Test Suite:.*$//; s/ *$//'
+}
+
+# pre_json is a "templated json" used in the test docs to express test results. It looks
+# like json but has some extra comments to express when a certain output should be used.
+# These comments look like: "# Test Suite: <suite>[, <suite>][, <suite>]..."
+#
+
+keep_suites() { # suites... < pre_json > json
+    grep -E "# Test Suite: (.*, )?($(echo "$@" | sed "s/ /|/g"))(, .*)?$" | \
+         sed -e's/# Only Test Suite:.*$//; s/# Test Suite:.*$//; s/ *$//'
+}
+
+remove_not_suite() { remove_suites !"$1" ; } # suite < pre_json > json
+
+# -------- Test Doc Format --------
+#
+# Test Doc Format has intermixed git config task definitions with json roots. This
+# makes it easy to define tests close to their outputs. Be aware that all of the
+# config will get consolidated into a single file, so non root config will be shared
+# amongst all the roots.
+#
+
+# Sample Test Doc for 2 roots:
+#
+# [root "Root PASS"]
+#   pass = True
+#
+# {
+#    "applicable" : true,
+#    "hasPass" : true,
+#    "name" : "Root PASS",
+#    "status" : "PASS"
+# }
+#
+# [root "Root FAIL"]
+#   fail = True
+#
+# {
+#    <other root>
+# }
+
+# Strip the json from Test Doc formatted text. For the sample above, the output would be:
+#
+# [root "Root PASS"]
+#   pass = True
+#
+# [root "Root FAIL"]
+#   fail = True
+# ...
+#
+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:
+#
+# { "plugins" : [
+#     { "name" : "task",
+#       "roots" : [
+#         {
+#           "applicable" : true,
+#           "hasPass" : true,
+#           "name" : "Root PASS",
+#           "status" : "PASS"
+#        },
+#        {
+#           <other root>
+#        },
+#    ...
+# }
+testdoc_2_pjson() { # < testdoc_format > pjson_task_roots
+    awk 'BEGIN { print "{ \"plugins\" : [ { \"name\" : \"task\", \"roots\" : [" }; \
+         /^\{/  { open=1 }; \
+         open && end { print "}," ; end=0 }; \
+         /^\}/  { open=0 ; end=1 }; \
+         open; \
+         END   { print "}]}]}" }'
+}
+
+# ---- 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)"
+}
+
+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_val_by "$1" "'$2'" ; }  # json key > value
+
+# --------
+
+gssh() {  # [-l user] cmd [args]...
+    local user_args=()
+    [ "-l" = "$1" ] && { user_args=("-l" "$2") ; shift 2 ; }
+    ssh -x -p "$PORT" "${user_args[@]}" "$SERVER" gerrit "$@"
+}
+
+q() { "$@" > /dev/null 2>&1 ; } # cmd [args...]  # quiet a command
+
+gen_change_id() { echo "I$(uuidgen | sha1sum | awk '{print $1}')"; } # > change_id
+
+commit_message() { printf "$1 \n\nChange-Id: $2" ; } # message change-id > commit_msg
+
+err() { echo "ERROR: $1" >&2 ; exit 1 ; }
+
+# Run a test setup command quietly, exit on failure
+q_setup() { local out ; out=$("$@" 2>&1) || err "$out" ; } # cmd [args...]
+
+ensure() { "$@" || err "$1 results are not valid" ; } # cmd [args]... < data > data
+
+set_change() { # change_json
+    { CHANGE=("$(json_val_by_key "$1" number)" \
+        "$(json_val_by_key "$1" id)" \
+        "$(json_val_by_key "$1" project)" \
+        "refs/heads/$(json_val_by_key "$1" branch)" \
+        "$(json_val_by_key "$1" status)" \
+        "$(json_val_by_key "$1" topic)") ; } 2> /dev/null
+}
+
+# change_token change_number change_id project branch status topic < templated_txt > change_txt
+replace_change_properties() {
+    sed -e "s|_change$1_number|$2|g" \
+        -e "s|_change$1_id|$3|g" \
+        -e "s|_change$1_project|$4|g" \
+        -e "s|_change$1_branch|$5|g" \
+        -e "s|_change$1_status|$6|g" \
+        -e "s|_change$1_topic|$7|g"
+}
+
+replace_default_changes() {
+    replace_change_properties "1" "${CHANGE1[@]}" | replace_change_properties "2" "${CHANGE2[@]}"
+}
+
+replace_groups() { # < text_with_groups > test_with_expanded_groups
+    local text="$(< /dev/stdin)"
+    for placeholder in "${!GROUP_EXPANDED_BY_PLACEHOLDER[@]}" ; do
+        text="${text//"$placeholder"/${GROUP_EXPANDED_BY_PLACEHOLDER["$placeholder"]}}"
+    done
+    echo "$text"
+}
+
+get_group_uuid() { # group_name > group_uuid
+    gssh ls-groups -v | awk '-F\t' '$1 == "'"$1"'" {print $2}'
+}
+
+get_sharded_group_uuid() { # group_name > sharded_group_uuid
+    local group_id=$(get_group_uuid "$1")
+    echo "${group_id:0:2}/$group_id"
+}
+
+replace_users() { # < text_with_users > test_with_expanded_users
+  local text="$(< /dev/stdin)"
+  for user in "${!USERS[@]}" ; do
+    text="${text//"$user"/${USERS["$user"]}}"
+  done
+  echo "$text"
+}
+
+replace_user() { # < text_with_testuser > text_with_$USER
+    sed -e"s/testuser/$USER/"
+}
+
+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)"
+    echo "refs/users/${user_account_id:(-2)}/$user_account_id"
+}
+
+replace_user_refs() { # < text_with_user_refs > test_with_expanded_user_refs
+    local text="$(< /dev/stdin)"
+    for user in "${!USER_REFS[@]}" ; do
+        text="${text//"$user"/${USER_REFS["$user"]}}"
+    done
+    echo "$text"
+}
+
+replace_tokens() { # < text > text with replacing all tokens(changes, user)
+    replace_default_changes | replace_user_refs | replace_user | replace_groups
+}
+
+strip_non_applicable() { ensure "$MYDIR"/strip_non_applicable.py ; } # < json > json
+strip_non_invalid() { ensure "$MYDIR"/strip_non_invalid.py ; } # < json > json
+
+define_jsonByRoot() { # task_plugin_ouptut > jsonByRoot_array_definition
+    local record root=''
+    local -A jsonByRoot
+    while IFS= read -r -d '' record ; do
+        if [ -z "$root" ] ; then
+            root=$record
+        else
+            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')"
+    )
+
+    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)"
+}
+
+example() { # doc example_num > text_for_example_num
+    echo "$1" | awk '/```/{Q++;E=(Q+1)/2};E=='"$2" | grep -v '```'
+}
+
+get_change_num() { # < gerrit_push_response > changenum
+    local url=$(awk '$NF ~ /\[NEW\]/ { print $2 }')
+    echo "${url##*\/}" | tr -d -c '[:digit:]'
+}
+
+install_changeid_hook() { # repo
+    local hook=$(git rev-parse --git-dir)/hooks/commit-msg
+    scp -p -P "$PORT" "$SERVER":hooks/commit-msg "$hook"
+    chmod +x "$hook"
+}
+
+setup_repo() { # repo remote ref [--initial-commit]
+    local repo=$1 remote=$2 ref=$3 init=$4
+    git init "$repo"
+    (
+        cd "$repo"
+        install_changeid_hook "$repo"
+        git fetch "$remote" "$ref"
+        if ! git checkout FETCH_HEAD ; then
+            if [ "$init" = "--initial-commit" ] ; then
+                git commit --allow-empty -a -m "Initial Commit"
+            fi
+        fi
+    )
+}
+
+update_repo() { # repo remote ref
+    local repo=$1 remote=$2 ref=$3
+    (
+        cd "$repo"
+        git add .
+        git commit -m 'Testing task plugin'
+        git push "$remote" HEAD:"$ref"
+    )
+}
+
+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"
+        uuidgen > file
+        q git add .
+        [ -n "$change_id" ] && msg=$(commit_message "$msg" "$change_id")
+        q git commit -m "$msg"
+        git push "$remote" HEAD:"refs/for/$ref" 2>&1 | get_change_num
+    )
+}
+
+query() {  # [-l user] query > json lines
+  local user_args=()
+  [ "-l" = "$1" ] && { user_args=("-l" "$2") ; shift 2 ; }
+  gssh "${user_args[@]}" query "$@" --format json
+}
+
+# N < json lines > changeN_json
+change_plugins() { awk "NR==$1" | get_plugins | json_pp ; }
+
+results_suite() { # name expected_file plugins_json
+    local name=$1 expected=$2 actual=$3
+
+    local -A EXPECTED_ROOTS=$(define_jsonByRoot < "$expected")
+    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)
+    [ -z "$out" ]
+    result "$name - Full Test Suite" "$out"
+}
diff --git a/test/lib/lib_md.sh b/test/lib/lib_md.sh
new file mode 100644
index 0000000..2838163
--- /dev/null
+++ b/test/lib/lib_md.sh
@@ -0,0 +1,100 @@
+#!/usr/bin/env bash
+#
+# Copyright (C) 2021 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.
+
+# ---- Markdown Format Helpers ----
+
+# Example markdown file:
+# (Using block comment to better understand the file syntax.)
+
+: <<'END'
+# Test case description header
+
+file: `All-Projects.git:refs/meta/config:task.config`
+```
+[root "Test root"]
+    applicable = "is:open"
+    pass = True
+```
+
+file: `All-Users:refs/users/some_ref:task/sample.config`
+```
+ [task "NON-SECRET task"]
+     applicable = is:open
+     pass = Fail
++    subtasks-external = SECRET
+
++[external "SECRET"]
++    user = {secret_user}
++    file = secret.config
+```
+
+json:
+```
+{
+   {
+     "some": "example"
+   }
+}
+END
+
+# (For example above)
+# out:
+# `All-Projects.git:refs/meta/config:task.config`
+# `All-Users:refs/users/some_ref:task/sample.config`
+md_file_markers() { # DOC_CONTENT
+    echo "$1" | grep -o "^file: .*" | cut -f2 -d'`'
+}
+
+# (For example above)
+# in: `All-Projects.git:refs/meta/config:task.config`
+# out:
+#[root "Test root"]
+#    applicable = "is:open"
+#    pass = True
+#
+# in: json:
+# out :
+# {
+#    {
+#      "some": "example"
+#    }
+# }
+md_marker_content() { # DOC marker
+    local start_line=$(echo "$1" | grep -n "$2" | cut -f1 -d':')
+    echo "$1" | tail -n+"$start_line" | \
+        sed '1,/```/d;/```/,$d' | grep -v '```'
+}
+
+# file_marker > project
+# in: `All-Projects.git:refs/meta/config:task/task.config`
+# out: All-Projects.git
+md_file_marker_project() {
+    echo "$1" | cut -f1 -d':'
+}
+
+# file_marker > ref
+# in: `All-Projects.git:refs/meta/config:task/task.config`
+# out: refs/meta/config
+md_file_marker_ref() {
+    echo "$1" | cut -f2 -d':'
+}
+
+# file_marker > file
+# in: `All-Projects.git:refs/meta/config:task/task.config`
+#out: task/task.config
+md_file_marker_file() {
+    echo "$1" | cut -f3 -d':'
+}
\ No newline at end of file
diff --git a/test/strip_non_applicable.py b/test/strip_non_applicable.py
index 1ff097a..41c21fa 100755
--- a/test/strip_non_applicable.py
+++ b/test/strip_non_applicable.py
@@ -43,7 +43,7 @@
                     status=''
                     if STATUS in task.keys():
                         status = task[STATUS]
-                    if status != 'INVALID':
+                    if status != 'INVALID' and status != 'DUPLICATE':
                         del tasks[i]
                         nexti = i
 
diff --git a/tools/bzl/junit.bzl b/tools/bzl/junit.bzl
new file mode 100644
index 0000000..5df79bb
--- /dev/null
+++ b/tools/bzl/junit.bzl
@@ -0,0 +1,6 @@
+load(
+    "@com_googlesource_gerrit_bazlets//tools:junit.bzl",
+    _junit_tests = "junit_tests",
+)
+
+junit_tests = _junit_tests
\ No newline at end of file
diff --git a/tools/bzl/maven_jar.bzl b/tools/bzl/maven_jar.bzl
new file mode 100644
index 0000000..4871c7b
--- /dev/null
+++ b/tools/bzl/maven_jar.bzl
@@ -0,0 +1,4 @@
+load("@com_googlesource_gerrit_bazlets//tools:maven_jar.bzl", _gerrit = "GERRIT", _maven_jar = "maven_jar")
+
+maven_jar = _maven_jar
+GERRIT = _gerrit
\ No newline at end of file
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl
index 89a1643..67536ef 100644
--- a/tools/bzl/plugin.bzl
+++ b/tools/bzl/plugin.bzl
@@ -2,7 +2,9 @@
     "@com_googlesource_gerrit_bazlets//:gerrit_plugin.bzl",
     _gerrit_plugin = "gerrit_plugin",
     _plugin_deps = "PLUGIN_DEPS",
+    _plugin_test_deps = "PLUGIN_TEST_DEPS",
 )
 
 gerrit_plugin = _gerrit_plugin
 PLUGIN_DEPS = _plugin_deps
+PLUGIN_TEST_DEPS = _plugin_test_deps
\ No newline at end of file
diff --git a/tools/playbooks/install_maven.yaml b/tools/playbooks/install_maven.yaml
new file mode 100644
index 0000000..ae1690d
--- /dev/null
+++ b/tools/playbooks/install_maven.yaml
@@ -0,0 +1,8 @@
+- hosts: all
+  tasks:
+    - name: Install maven
+      become: true
+      package:
+        name:
+          - maven
+        state: present
diff --git a/tools/playbooks/install_python3-distutils.yaml b/tools/playbooks/install_python3-distutils.yaml
new file mode 100644
index 0000000..75f5a2a
--- /dev/null
+++ b/tools/playbooks/install_python3-distutils.yaml
@@ -0,0 +1,10 @@
+- hosts: all
+  roles:
+    - name: ensure-python
+  tasks:
+    - name: Install python3-distutils
+      become: true
+      package:
+        name:
+          - python3-distutils
+        state: present
diff --git a/tools/workspace_status.py b/tools/workspace_status.py
index d948424..fb5ec6d 100644
--- a/tools/workspace_status.py
+++ b/tools/workspace_status.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
 # This script will be run by bazel when the build process starts to
 # generate key-value information that represents the status of the
diff --git a/yarn.lock b/yarn.lock
index 0bdc3f3..da0237a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2,53 +2,91 @@
 # yarn lockfile v1
 
 
-"@babel/code-frame@^7.0.0":
-  version "7.12.13"
-  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658"
-  integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==
+"@babel/code-frame@7.12.11":
+  version "7.12.11"
+  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f"
+  integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==
   dependencies:
-    "@babel/highlight" "^7.12.13"
+    "@babel/highlight" "^7.10.4"
 
-"@babel/helper-validator-identifier@^7.14.0":
-  version "7.14.0"
-  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz#d26cad8a47c65286b15df1547319a5d0bcf27288"
-  integrity sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==
+"@babel/helper-validator-identifier@^7.18.6":
+  version "7.19.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2"
+  integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==
 
-"@babel/highlight@^7.12.13":
-  version "7.14.0"
-  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.0.tgz#3197e375711ef6bf834e67d0daec88e4f46113cf"
-  integrity sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==
+"@babel/highlight@^7.10.4":
+  version "7.18.6"
+  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf"
+  integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==
   dependencies:
-    "@babel/helper-validator-identifier" "^7.14.0"
+    "@babel/helper-validator-identifier" "^7.18.6"
     chalk "^2.0.0"
     js-tokens "^4.0.0"
 
-"@bazel/rollup@^3.4.0":
-  version "3.5.0"
-  resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-3.5.0.tgz#3de2db08cbc62c3cffbbabaa4517ec250cf6419a"
-  integrity sha512-sFPqbzSbIn6h66uuZdXgK5oitSmEGtnDPfL3TwTS4ZWy75SpYvk9X1TFGlvkralEkVnFfdH15sq80/1t+YgQow==
+"@bazel/rollup@~5.1.0":
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-5.1.0.tgz#dc858ddc93c9fdb9cc2e7982e632c939c646ebdc"
+  integrity sha512-wEiWdSyVbsycSirSYjR6FGfPGbRNI7sGNAYmrV0hIzYIi+KqXeTNcwKIRSE9PESP3mb0VWbZmHvXvmrWk6daPQ==
+  dependencies:
+    "@bazel/worker" "5.1.0"
 
-"@bazel/terser@^3.4.0":
-  version "3.5.0"
-  resolved "https://registry.yarnpkg.com/@bazel/terser/-/terser-3.5.0.tgz#4b1c3a3b781e65547694aa05bc600c251e4d8c0b"
-  integrity sha512-dpWHn1Iu+w0uA/kvPb0pP+4Io0PrVuzCCbVg2Ow4uRt/gTFKQJJWp4EiTitEZlPA2dHlW7PHThAb93lGo2c8qA==
+"@bazel/terser@~5.1.0":
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/@bazel/terser/-/terser-5.1.0.tgz#5c82b93f4d9def8103c16be2dd33900d156fa066"
+  integrity sha512-uE3hTqfkZr4nvlk3jwi0xx6URqqI7r6GGPtDAU02/PVei+O4PfThaov7cwHO+D1FnoLncDqChb9Iolr7Crw/8A==
+
+"@bazel/worker@5.1.0":
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/@bazel/worker/-/worker-5.1.0.tgz#6f1e0f3ef628e3449d424cacd341c9abd09a3735"
+  integrity sha512-u3aU93UtHz3vL6ozezq0jnw83s1cNT4dAnW+vvB7M++YKFlB3CWzZFb0JRJbCp1b6DDe30ML0WOdd3nVYuylpw==
+  dependencies:
+    google-protobuf "^3.6.1"
+
+"@eslint/eslintrc@^0.4.3":
+  version "0.4.3"
+  resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c"
+  integrity sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==
+  dependencies:
+    ajv "^6.12.4"
+    debug "^4.1.1"
+    espree "^7.3.0"
+    globals "^13.9.0"
+    ignore "^4.0.6"
+    import-fresh "^3.2.1"
+    js-yaml "^3.13.1"
+    minimatch "^3.0.4"
+    strip-json-comments "^3.1.1"
+
+"@humanwhocodes/config-array@^0.5.0":
+  version "0.5.0"
+  resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9"
+  integrity sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==
+  dependencies:
+    "@humanwhocodes/object-schema" "^1.2.0"
+    debug "^4.1.1"
+    minimatch "^3.0.4"
+
+"@humanwhocodes/object-schema@^1.2.0":
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
+  integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
 
 "@types/json5@^0.0.29":
   version "0.0.29"
   resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
   integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
 
-acorn-jsx@^5.2.0:
-  version "5.3.1"
-  resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b"
-  integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==
+acorn-jsx@^5.3.1:
+  version "5.3.2"
+  resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
+  integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
 
-acorn@^7.1.1:
+acorn@^7.4.0:
   version "7.4.1"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
   integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
 
-ajv@^6.10.0, ajv@^6.10.2:
+ajv@^6.10.0, ajv@^6.12.4:
   version "6.12.6"
   resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
   integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
@@ -58,31 +96,39 @@
     json-schema-traverse "^0.4.1"
     uri-js "^4.2.2"
 
-ansi-escapes@^4.2.1:
-  version "4.3.2"
-  resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e"
-  integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==
+ajv@^8.0.1:
+  version "8.11.0"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f"
+  integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==
   dependencies:
-    type-fest "^0.21.3"
+    fast-deep-equal "^3.1.1"
+    json-schema-traverse "^1.0.0"
+    require-from-string "^2.0.2"
+    uri-js "^4.2.2"
 
-ansi-regex@^4.1.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
-  integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
+ansi-colors@^4.1.1:
+  version "4.1.3"
+  resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b"
+  integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==
 
 ansi-regex@^5.0.0:
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75"
   integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==
 
-ansi-styles@^3.2.0, ansi-styles@^3.2.1:
+ansi-regex@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
+  integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
+
+ansi-styles@^3.2.1:
   version "3.2.1"
   resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
   integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
   dependencies:
     color-convert "^1.9.0"
 
-ansi-styles@^4.1.0:
+ansi-styles@^4.0.0, ansi-styles@^4.1.0:
   version "4.3.0"
   resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
   integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
@@ -96,30 +142,31 @@
   dependencies:
     sprintf-js "~1.0.2"
 
-array-includes@^3.1.3:
-  version "3.1.3"
-  resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.3.tgz#c7f619b382ad2afaf5326cddfdc0afc61af7690a"
-  integrity sha512-gcem1KlBU7c9rB+Rq8/3PPKsK2kjqeEBa3bD5kkQo4nYlOHQCJqIJFqBXDEfwaRuYTT4E+FxA9xez7Gf/e3Q7A==
+array-includes@^3.1.4:
+  version "3.1.5"
+  resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.5.tgz#2c320010db8d31031fd2a5f6b3bbd4b1aad31bdb"
+  integrity sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==
+  dependencies:
+    call-bind "^1.0.2"
+    define-properties "^1.1.4"
+    es-abstract "^1.19.5"
+    get-intrinsic "^1.1.1"
+    is-string "^1.0.7"
+
+array.prototype.flat@^1.2.5:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz#0b0c1567bf57b38b56b4c97b8aa72ab45e4adc7b"
+  integrity sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==
   dependencies:
     call-bind "^1.0.2"
     define-properties "^1.1.3"
-    es-abstract "^1.18.0-next.2"
-    get-intrinsic "^1.1.1"
-    is-string "^1.0.5"
+    es-abstract "^1.19.2"
+    es-shim-unscopables "^1.0.0"
 
-array.prototype.flat@^1.2.4:
-  version "1.2.4"
-  resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.4.tgz#6ef638b43312bd401b4c6199fdec7e2dc9e9a123"
-  integrity sha512-4470Xi3GAPAjZqFcljX2xzckv1qeKPizoNkiS0+O4IoPR2ZNpcjE0pkhdihlDouK+x6QOast26B4Q/O9DJnwSg==
-  dependencies:
-    call-bind "^1.0.0"
-    define-properties "^1.1.3"
-    es-abstract "^1.18.0-next.1"
-
-astral-regex@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9"
-  integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==
+astral-regex@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
+  integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
 
 balanced-match@^1.0.0:
   version "1.0.2"
@@ -152,7 +199,7 @@
   resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
   integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
 
-chalk@^2.0.0, chalk@^2.1.0:
+chalk@^2.0.0:
   version "2.4.2"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
   integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
@@ -161,31 +208,14 @@
     escape-string-regexp "^1.0.5"
     supports-color "^5.3.0"
 
-chalk@^4.1.0:
-  version "4.1.1"
-  resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad"
-  integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==
+chalk@^4.0.0:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
+  integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
   dependencies:
     ansi-styles "^4.1.0"
     supports-color "^7.1.0"
 
-chardet@^0.7.0:
-  version "0.7.0"
-  resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
-  integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
-
-cli-cursor@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307"
-  integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==
-  dependencies:
-    restore-cursor "^3.1.0"
-
-cli-width@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6"
-  integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==
-
 color-convert@^1.9.0:
   version "1.9.3"
   resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
@@ -215,26 +245,24 @@
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
   integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
 
-comment-parser@^0.7.2:
-  version "0.7.6"
-  resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-0.7.6.tgz#0e743a53c8e646c899a1323db31f6cd337b10f12"
-  integrity sha512-GKNxVA7/iuTnAqGADlTWX4tkhzxZKXp5fLJqKTlQLHkE65XDUKutZ3BHaJC5IGcper2tT3QRD1xr4o3jNpgXXg==
+comment-parser@1.1.5:
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-1.1.5.tgz#453627ef8f67dbcec44e79a9bd5baa37f0bce9b2"
+  integrity sha512-RePCE4leIhBlmrqiYTvaqEeGYg7qpSl4etaIabKtdOQVi+mSTIBBklGUwIr79GXYnl3LpMwmDw4KeR2stNc6FA==
 
 concat-map@0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
   integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
 
-cross-spawn@^6.0.5:
-  version "6.0.5"
-  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
-  integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
+cross-spawn@^7.0.2:
+  version "7.0.3"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
+  integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
   dependencies:
-    nice-try "^1.0.4"
-    path-key "^2.0.1"
-    semver "^5.5.0"
-    shebang-command "^1.2.0"
-    which "^1.2.9"
+    path-key "^3.1.0"
+    shebang-command "^2.0.0"
+    which "^2.0.1"
 
 debug@^2.6.9:
   version "2.6.9"
@@ -264,10 +292,17 @@
   dependencies:
     ms "2.1.2"
 
-deep-is@~0.1.3:
-  version "0.1.3"
-  resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
-  integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
+debug@^4.3.1:
+  version "4.3.4"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
+  integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
+  dependencies:
+    ms "2.1.2"
+
+deep-is@^0.1.3:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
+  integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
 
 define-properties@^1.1.3:
   version "1.1.3"
@@ -276,6 +311,14 @@
   dependencies:
     object-keys "^1.0.12"
 
+define-properties@^1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1"
+  integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==
+  dependencies:
+    has-property-descriptors "^1.0.0"
+    object-keys "^1.1.1"
+
 doctrine@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
@@ -311,60 +354,79 @@
   dependencies:
     domelementtype "^2.2.0"
 
-domutils@^2.5.2:
-  version "2.6.0"
-  resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.6.0.tgz#2e15c04185d43fb16ae7057cb76433c6edb938b7"
-  integrity sha512-y0BezHuy4MDYxh6OvolXYsH+1EMGmFbwv5FKW7ovwMG6zTPWqNPq3WF9ayZssFq+UlKdffGLbOEaghNdaOm1WA==
+domhandler@^4.2.2:
+  version "4.3.1"
+  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c"
+  integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==
+  dependencies:
+    domelementtype "^2.2.0"
+
+domutils@^2.8.0:
+  version "2.8.0"
+  resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
+  integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==
   dependencies:
     dom-serializer "^1.0.1"
     domelementtype "^2.2.0"
     domhandler "^4.2.0"
 
-emoji-regex@^7.0.1:
-  version "7.0.3"
-  resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
-  integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==
-
 emoji-regex@^8.0.0:
   version "8.0.0"
   resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
   integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
 
+enquirer@^2.3.5:
+  version "2.3.6"
+  resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d"
+  integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==
+  dependencies:
+    ansi-colors "^4.1.1"
+
 entities@^2.0.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
   integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==
 
-error-ex@^1.3.1:
-  version "1.3.2"
-  resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
-  integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
-  dependencies:
-    is-arrayish "^0.2.1"
+entities@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4"
+  integrity sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==
 
-es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2, es-abstract@^1.18.2:
-  version "1.18.6"
-  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.6.tgz#2c44e3ea7a6255039164d26559777a6d978cb456"
-  integrity sha512-kAeIT4cku5eNLNuUKhlmtuk1/TRZvQoYccn6TO0cSVdf1kzB0T7+dYuVK9MWM7l+/53W2Q8M7N2c6MQvhXFcUQ==
+es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19.5:
+  version "1.20.2"
+  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.2.tgz#8495a07bc56d342a3b8ea3ab01bd986700c2ccb3"
+  integrity sha512-XxXQuVNrySBNlEkTYJoDNFe5+s2yIOpzq80sUHEdPdQr0S5nTLz4ZPPPswNIpKseDDUS5yghX1gfLIHQZ1iNuQ==
   dependencies:
     call-bind "^1.0.2"
     es-to-primitive "^1.2.1"
     function-bind "^1.1.1"
-    get-intrinsic "^1.1.1"
+    function.prototype.name "^1.1.5"
+    get-intrinsic "^1.1.2"
     get-symbol-description "^1.0.0"
     has "^1.0.3"
-    has-symbols "^1.0.2"
+    has-property-descriptors "^1.0.0"
+    has-symbols "^1.0.3"
     internal-slot "^1.0.3"
     is-callable "^1.2.4"
-    is-negative-zero "^2.0.1"
+    is-negative-zero "^2.0.2"
     is-regex "^1.1.4"
+    is-shared-array-buffer "^1.0.2"
     is-string "^1.0.7"
-    object-inspect "^1.11.0"
+    is-weakref "^1.0.2"
+    object-inspect "^1.12.2"
     object-keys "^1.1.1"
-    object.assign "^4.1.2"
-    string.prototype.trimend "^1.0.4"
-    string.prototype.trimstart "^1.0.4"
-    unbox-primitive "^1.0.1"
+    object.assign "^4.1.4"
+    regexp.prototype.flags "^1.4.3"
+    string.prototype.trimend "^1.0.5"
+    string.prototype.trimstart "^1.0.5"
+    unbox-primitive "^1.0.2"
+
+es-shim-unscopables@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241"
+  integrity sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==
+  dependencies:
+    has "^1.0.3"
 
 es-to-primitive@^1.2.1:
   version "1.2.1"
@@ -380,10 +442,15 @@
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
   integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
 
-eslint-config-google@^0.13.0:
-  version "0.13.0"
-  resolved "https://registry.yarnpkg.com/eslint-config-google/-/eslint-config-google-0.13.0.tgz#e277d16d2cb25c1ffd3fd13fb0035ad7421382fe"
-  integrity sha512-ELgMdOIpn0CFdsQS+FuxO+Ttu4p+aLaXHv9wA9yVnzqlUGV7oN/eRRnJekk7TCur6Cu2FXX0fqfIXRBaM14lpQ==
+escape-string-regexp@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
+  integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
+
+eslint-config-google@^0.14.0:
+  version "0.14.0"
+  resolved "https://registry.yarnpkg.com/eslint-config-google/-/eslint-config-google-0.14.0.tgz#4f5f8759ba6e11b424294a219dbfa18c508bcc1a"
+  integrity sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==
 
 eslint-import-resolver-node@^0.3.6:
   version "0.3.6"
@@ -393,57 +460,53 @@
     debug "^3.2.7"
     resolve "^1.20.0"
 
-eslint-module-utils@^2.6.2:
-  version "2.6.2"
-  resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.6.2.tgz#94e5540dd15fe1522e8ffa3ec8db3b7fa7e7a534"
-  integrity sha512-QG8pcgThYOuqxupd06oYTZoNOGaUdTY1PqK+oS6ElF6vs4pBdk/aYxFVQQXzcrAqp9m7cl7lb2ubazX+g16k2Q==
+eslint-module-utils@^2.7.3:
+  version "2.7.4"
+  resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz#4f3e41116aaf13a20792261e61d3a2e7e0583974"
+  integrity sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==
   dependencies:
     debug "^3.2.7"
-    pkg-dir "^2.0.0"
 
-eslint-plugin-html@^6.0.0:
-  version "6.1.2"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-html/-/eslint-plugin-html-6.1.2.tgz#fa26e4804428956c80e963b6499c192061c2daf3"
-  integrity sha512-bhBIRyZFqI4EoF12lGDHAmgfff8eLXx6R52/K3ESQhsxzCzIE6hdebS7Py651f7U3RBotqroUnC3L29bR7qJWQ==
+eslint-plugin-html@^6.1.2:
+  version "6.2.0"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-html/-/eslint-plugin-html-6.2.0.tgz#715bc00b50bbd0d996e28f953c289a5ebec69d43"
+  integrity sha512-vi3NW0E8AJombTvt8beMwkL1R/fdRWl4QSNRNMhVQKWm36/X0KF0unGNAY4mqUF06mnwVWZcIcerrCnfn9025g==
   dependencies:
-    htmlparser2 "^6.0.1"
+    htmlparser2 "^7.1.2"
 
-eslint-plugin-import@^2.20.1:
-  version "2.24.2"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.24.2.tgz#2c8cd2e341f3885918ee27d18479910ade7bb4da"
-  integrity sha512-hNVtyhiEtZmpsabL4neEj+6M5DCLgpYyG9nzJY8lZQeQXEn5UPW1DpUdsMHMXsq98dbNm7nt1w9ZMSVpfJdi8Q==
+eslint-plugin-import@^2.22.1:
+  version "2.26.0"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz#f812dc47be4f2b72b478a021605a59fc6fe8b88b"
+  integrity sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==
   dependencies:
-    array-includes "^3.1.3"
-    array.prototype.flat "^1.2.4"
+    array-includes "^3.1.4"
+    array.prototype.flat "^1.2.5"
     debug "^2.6.9"
     doctrine "^2.1.0"
     eslint-import-resolver-node "^0.3.6"
-    eslint-module-utils "^2.6.2"
-    find-up "^2.0.0"
+    eslint-module-utils "^2.7.3"
     has "^1.0.3"
-    is-core-module "^2.6.0"
-    minimatch "^3.0.4"
-    object.values "^1.1.4"
-    pkg-up "^2.0.0"
-    read-pkg-up "^3.0.0"
-    resolve "^1.20.0"
-    tsconfig-paths "^3.11.0"
+    is-core-module "^2.8.1"
+    is-glob "^4.0.3"
+    minimatch "^3.1.2"
+    object.values "^1.1.5"
+    resolve "^1.22.0"
+    tsconfig-paths "^3.14.1"
 
-eslint-plugin-jsdoc@^19.2.0:
-  version "19.2.0"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-19.2.0.tgz#f522b970878ae402b28ce62187305b33dfe2c834"
-  integrity sha512-QdNifBFLXCDGdy+26RXxcrqzEZarFWNybCZQVqJQYEYPlxd6lm+LPkrs6mCOhaGc2wqC6zqpedBQFX8nQJuKSw==
+eslint-plugin-jsdoc@^32.3.0:
+  version "32.3.4"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-32.3.4.tgz#6888f3b2dbb9f73fb551458c639a4e8c84fe9ddc"
+  integrity sha512-xSWfsYvffXnN0OkwLnB7MoDDDDjqcp46W7YlY1j7JyfAQBQ+WnGCfLov3gVNZjUGtK9Otj8mEhTZTqJu4QtIGA==
   dependencies:
-    comment-parser "^0.7.2"
-    debug "^4.1.1"
-    jsdoctypeparser "^6.1.0"
-    lodash "^4.17.15"
-    object.entries-ponyfill "^1.0.1"
-    regextras "^0.7.0"
-    semver "^6.3.0"
-    spdx-expression-parse "^3.0.0"
+    comment-parser "1.1.5"
+    debug "^4.3.1"
+    jsdoctypeparser "^9.0.0"
+    lodash "^4.17.21"
+    regextras "^0.7.1"
+    semver "^7.3.5"
+    spdx-expression-parse "^3.0.1"
 
-eslint-scope@^5.0.0:
+eslint-scope@^5.1.1:
   version "5.1.1"
   resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
   integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
@@ -451,76 +514,84 @@
     esrecurse "^4.3.0"
     estraverse "^4.1.1"
 
-eslint-utils@^1.4.3:
-  version "1.4.3"
-  resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f"
-  integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==
+eslint-utils@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27"
+  integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==
   dependencies:
     eslint-visitor-keys "^1.1.0"
 
-eslint-visitor-keys@^1.1.0:
+eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e"
   integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==
 
-eslint@^6.6.0:
-  version "6.8.0"
-  resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.8.0.tgz#62262d6729739f9275723824302fb227c8c93ffb"
-  integrity sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==
+eslint-visitor-keys@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303"
+  integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==
+
+eslint@^7.24.0:
+  version "7.32.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d"
+  integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==
   dependencies:
-    "@babel/code-frame" "^7.0.0"
+    "@babel/code-frame" "7.12.11"
+    "@eslint/eslintrc" "^0.4.3"
+    "@humanwhocodes/config-array" "^0.5.0"
     ajv "^6.10.0"
-    chalk "^2.1.0"
-    cross-spawn "^6.0.5"
+    chalk "^4.0.0"
+    cross-spawn "^7.0.2"
     debug "^4.0.1"
     doctrine "^3.0.0"
-    eslint-scope "^5.0.0"
-    eslint-utils "^1.4.3"
-    eslint-visitor-keys "^1.1.0"
-    espree "^6.1.2"
-    esquery "^1.0.1"
+    enquirer "^2.3.5"
+    escape-string-regexp "^4.0.0"
+    eslint-scope "^5.1.1"
+    eslint-utils "^2.1.0"
+    eslint-visitor-keys "^2.0.0"
+    espree "^7.3.1"
+    esquery "^1.4.0"
     esutils "^2.0.2"
-    file-entry-cache "^5.0.1"
+    fast-deep-equal "^3.1.3"
+    file-entry-cache "^6.0.1"
     functional-red-black-tree "^1.0.1"
-    glob-parent "^5.0.0"
-    globals "^12.1.0"
+    glob-parent "^5.1.2"
+    globals "^13.6.0"
     ignore "^4.0.6"
     import-fresh "^3.0.0"
     imurmurhash "^0.1.4"
-    inquirer "^7.0.0"
     is-glob "^4.0.0"
     js-yaml "^3.13.1"
     json-stable-stringify-without-jsonify "^1.0.1"
-    levn "^0.3.0"
-    lodash "^4.17.14"
+    levn "^0.4.1"
+    lodash.merge "^4.6.2"
     minimatch "^3.0.4"
-    mkdirp "^0.5.1"
     natural-compare "^1.4.0"
-    optionator "^0.8.3"
+    optionator "^0.9.1"
     progress "^2.0.0"
-    regexpp "^2.0.1"
-    semver "^6.1.2"
-    strip-ansi "^5.2.0"
-    strip-json-comments "^3.0.1"
-    table "^5.2.3"
+    regexpp "^3.1.0"
+    semver "^7.2.1"
+    strip-ansi "^6.0.0"
+    strip-json-comments "^3.1.0"
+    table "^6.0.9"
     text-table "^0.2.0"
     v8-compile-cache "^2.0.3"
 
-espree@^6.1.2:
-  version "6.2.1"
-  resolved "https://registry.yarnpkg.com/espree/-/espree-6.2.1.tgz#77fc72e1fd744a2052c20f38a5b575832e82734a"
-  integrity sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==
+espree@^7.3.0, espree@^7.3.1:
+  version "7.3.1"
+  resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6"
+  integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==
   dependencies:
-    acorn "^7.1.1"
-    acorn-jsx "^5.2.0"
-    eslint-visitor-keys "^1.1.0"
+    acorn "^7.4.0"
+    acorn-jsx "^5.3.1"
+    eslint-visitor-keys "^1.3.0"
 
 esprima@^4.0.0:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
   integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
 
-esquery@^1.0.1:
+esquery@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5"
   integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==
@@ -549,16 +620,7 @@
   resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
   integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
 
-external-editor@^3.0.3:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495"
-  integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==
-  dependencies:
-    chardet "^0.7.0"
-    iconv-lite "^0.4.24"
-    tmp "^0.0.33"
-
-fast-deep-equal@^3.1.1:
+fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
   version "3.1.3"
   resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
   integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
@@ -568,45 +630,30 @@
   resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
   integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
 
-fast-levenshtein@~2.0.6:
+fast-levenshtein@^2.0.6:
   version "2.0.6"
   resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
-  integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
+  integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
 
-figures@^3.0.0:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af"
-  integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==
+file-entry-cache@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
+  integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==
   dependencies:
-    escape-string-regexp "^1.0.5"
+    flat-cache "^3.0.4"
 
-file-entry-cache@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c"
-  integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==
+flat-cache@^3.0.4:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11"
+  integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==
   dependencies:
-    flat-cache "^2.0.1"
+    flatted "^3.1.0"
+    rimraf "^3.0.2"
 
-find-up@^2.0.0, find-up@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
-  integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c=
-  dependencies:
-    locate-path "^2.0.0"
-
-flat-cache@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0"
-  integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==
-  dependencies:
-    flatted "^2.0.0"
-    rimraf "2.6.3"
-    write "1.0.3"
-
-flatted@^2.0.0:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138"
-  integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==
+flatted@^3.1.0:
+  version "3.2.7"
+  resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787"
+  integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==
 
 fs.realpath@^1.0.0:
   version "1.0.0"
@@ -623,11 +670,26 @@
   resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
   integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
 
+function.prototype.name@^1.1.5:
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621"
+  integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==
+  dependencies:
+    call-bind "^1.0.2"
+    define-properties "^1.1.3"
+    es-abstract "^1.19.0"
+    functions-have-names "^1.2.2"
+
 functional-red-black-tree@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
   integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
 
+functions-have-names@^1.2.2:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
+  integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
+
 get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6"
@@ -637,6 +699,15 @@
     has "^1.0.3"
     has-symbols "^1.0.1"
 
+get-intrinsic@^1.1.2:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.3.tgz#063c84329ad93e83893c7f4f243ef63ffa351385"
+  integrity sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==
+  dependencies:
+    function-bind "^1.1.1"
+    has "^1.0.3"
+    has-symbols "^1.0.3"
+
 get-symbol-description@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6"
@@ -645,7 +716,7 @@
     call-bind "^1.0.2"
     get-intrinsic "^1.1.1"
 
-glob-parent@^5.0.0:
+glob-parent@^5.1.2:
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
   integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
@@ -664,23 +735,28 @@
     once "^1.3.0"
     path-is-absolute "^1.0.0"
 
-globals@^12.1.0:
-  version "12.4.0"
-  resolved "https://registry.yarnpkg.com/globals/-/globals-12.4.0.tgz#a18813576a41b00a24a97e7f815918c2e19925f8"
-  integrity sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==
+globals@^13.6.0, globals@^13.9.0:
+  version "13.17.0"
+  resolved "https://registry.yarnpkg.com/globals/-/globals-13.17.0.tgz#902eb1e680a41da93945adbdcb5a9f361ba69bd4"
+  integrity sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==
   dependencies:
-    type-fest "^0.8.1"
+    type-fest "^0.20.2"
 
-graceful-fs@^4.1.2:
-  version "4.2.8"
-  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
-  integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
+google-protobuf@^3.6.1:
+  version "3.21.2"
+  resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.21.2.tgz#4580a2bea8bbb291ee579d1fefb14d6fa3070ea4"
+  integrity sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA==
 
 has-bigints@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113"
   integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==
 
+has-bigints@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa"
+  integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==
+
 has-flag@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
@@ -691,11 +767,23 @@
   resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
   integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
 
+has-property-descriptors@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861"
+  integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==
+  dependencies:
+    get-intrinsic "^1.1.1"
+
 has-symbols@^1.0.1, has-symbols@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423"
   integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==
 
+has-symbols@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
+  integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
+
 has-tostringtag@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25"
@@ -710,34 +798,22 @@
   dependencies:
     function-bind "^1.1.1"
 
-hosted-git-info@^2.1.4:
-  version "2.8.9"
-  resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
-  integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
-
-htmlparser2@^6.0.1:
-  version "6.1.0"
-  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7"
-  integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==
+htmlparser2@^7.1.2:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-7.2.0.tgz#8817cdea38bbc324392a90b1990908e81a65f5a5"
+  integrity sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==
   dependencies:
     domelementtype "^2.0.1"
-    domhandler "^4.0.0"
-    domutils "^2.5.2"
-    entities "^2.0.0"
-
-iconv-lite@^0.4.24:
-  version "0.4.24"
-  resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
-  integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
-  dependencies:
-    safer-buffer ">= 2.1.2 < 3"
+    domhandler "^4.2.2"
+    domutils "^2.8.0"
+    entities "^3.0.1"
 
 ignore@^4.0.6:
   version "4.0.6"
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
   integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
 
-import-fresh@^3.0.0:
+import-fresh@^3.0.0, import-fresh@^3.2.1:
   version "3.3.0"
   resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
   integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
@@ -763,25 +839,6 @@
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
   integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
 
-inquirer@^7.0.0:
-  version "7.3.3"
-  resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003"
-  integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==
-  dependencies:
-    ansi-escapes "^4.2.1"
-    chalk "^4.1.0"
-    cli-cursor "^3.1.0"
-    cli-width "^3.0.0"
-    external-editor "^3.0.3"
-    figures "^3.0.0"
-    lodash "^4.17.19"
-    mute-stream "0.0.8"
-    run-async "^2.4.0"
-    rxjs "^6.6.0"
-    string-width "^4.1.0"
-    strip-ansi "^6.0.0"
-    through "^2.3.6"
-
 internal-slot@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c"
@@ -791,11 +848,6 @@
     has "^1.0.3"
     side-channel "^1.0.4"
 
-is-arrayish@^0.2.1:
-  version "0.2.1"
-  resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
-  integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=
-
 is-bigint@^1.0.1:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3"
@@ -816,13 +868,20 @@
   resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945"
   integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==
 
-is-core-module@^2.2.0, is-core-module@^2.6.0:
+is-core-module@^2.2.0:
   version "2.6.0"
   resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.6.0.tgz#d7553b2526fe59b92ba3e40c8df757ec8a709e19"
   integrity sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ==
   dependencies:
     has "^1.0.3"
 
+is-core-module@^2.8.1, is-core-module@^2.9.0:
+  version "2.10.0"
+  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.10.0.tgz#9012ede0a91c69587e647514e1d5277019e728ed"
+  integrity sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==
+  dependencies:
+    has "^1.0.3"
+
 is-date-object@^1.0.1:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f"
@@ -835,11 +894,6 @@
   resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
   integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
 
-is-fullwidth-code-point@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
-  integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
-
 is-fullwidth-code-point@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
@@ -852,10 +906,17 @@
   dependencies:
     is-extglob "^2.1.1"
 
-is-negative-zero@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24"
-  integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==
+is-glob@^4.0.3:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
+  integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+  dependencies:
+    is-extglob "^2.1.1"
+
+is-negative-zero@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150"
+  integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==
 
 is-number-object@^1.0.4:
   version "1.0.6"
@@ -872,6 +933,13 @@
     call-bind "^1.0.2"
     has-tostringtag "^1.0.0"
 
+is-shared-array-buffer@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79"
+  integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==
+  dependencies:
+    call-bind "^1.0.2"
+
 is-string@^1.0.5, is-string@^1.0.7:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd"
@@ -886,6 +954,13 @@
   dependencies:
     has-symbols "^1.0.2"
 
+is-weakref@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2"
+  integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==
+  dependencies:
+    call-bind "^1.0.2"
+
 isexe@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
@@ -904,21 +979,21 @@
     argparse "^1.0.7"
     esprima "^4.0.0"
 
-jsdoctypeparser@^6.1.0:
-  version "6.1.0"
-  resolved "https://registry.yarnpkg.com/jsdoctypeparser/-/jsdoctypeparser-6.1.0.tgz#acfb936c26300d98f1405cb03e20b06748e512a8"
-  integrity sha512-UCQBZ3xCUBv/PLfwKAJhp6jmGOSLFNKzrotXGNgbKhWvz27wPsCsVeP7gIcHPElQw2agBmynAitXqhxR58XAmA==
-
-json-parse-better-errors@^1.0.1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
-  integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
+jsdoctypeparser@^9.0.0:
+  version "9.0.0"
+  resolved "https://registry.yarnpkg.com/jsdoctypeparser/-/jsdoctypeparser-9.0.0.tgz#8c97e2fb69315eb274b0f01377eaa5c940bd7b26"
+  integrity sha512-jrTA2jJIL6/DAEILBEh2/w9QxCuwmvNXIry39Ay/HVfhE3o2yVV0U44blYkqdHA/OKloJEqvJy0xU+GSdE2SIw==
 
 json-schema-traverse@^0.4.1:
   version "0.4.1"
   resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
   integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
 
+json-schema-traverse@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
+  integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
+
 json-stable-stringify-without-jsonify@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
@@ -931,41 +1006,35 @@
   dependencies:
     minimist "^1.2.0"
 
-levn@^0.3.0, levn@~0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
-  integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=
+levn@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
+  integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==
   dependencies:
-    prelude-ls "~1.1.2"
-    type-check "~0.3.2"
+    prelude-ls "^1.2.1"
+    type-check "~0.4.0"
 
-load-json-file@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
-  integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs=
-  dependencies:
-    graceful-fs "^4.1.2"
-    parse-json "^4.0.0"
-    pify "^3.0.0"
-    strip-bom "^3.0.0"
+lodash.merge@^4.6.2:
+  version "4.6.2"
+  resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
+  integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
 
-locate-path@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
-  integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=
-  dependencies:
-    p-locate "^2.0.0"
-    path-exists "^3.0.0"
+lodash.truncate@^4.4.2:
+  version "4.4.2"
+  resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
+  integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==
 
-lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19:
+lodash@^4.17.21:
   version "4.17.21"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
   integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
 
-mimic-fn@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
-  integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
+lru-cache@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
+  integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
+  dependencies:
+    yallist "^4.0.0"
 
 minimatch@^3.0.4:
   version "3.0.4"
@@ -974,17 +1043,22 @@
   dependencies:
     brace-expansion "^1.1.7"
 
-minimist@^1.2.0, minimist@^1.2.5:
+minimatch@^3.1.2:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
+  integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
+  dependencies:
+    brace-expansion "^1.1.7"
+
+minimist@^1.2.0:
   version "1.2.5"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
   integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
 
-mkdirp@^0.5.1:
-  version "0.5.5"
-  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
-  integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
-  dependencies:
-    minimist "^1.2.5"
+minimist@^1.2.6:
+  version "1.2.6"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
+  integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
 
 ms@2.0.0:
   version "2.0.0"
@@ -1001,32 +1075,17 @@
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
   integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
 
-mute-stream@0.0.8:
-  version "0.0.8"
-  resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
-  integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
-
 natural-compare@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
   integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
 
-nice-try@^1.0.4:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
-  integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
+object-inspect@^1.12.2:
+  version "1.12.2"
+  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea"
+  integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==
 
-normalize-package-data@^2.3.2:
-  version "2.5.0"
-  resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
-  integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==
-  dependencies:
-    hosted-git-info "^2.1.4"
-    resolve "^1.10.0"
-    semver "2 || 3 || 4 || 5"
-    validate-npm-package-license "^3.0.1"
-
-object-inspect@^1.11.0, object-inspect@^1.9.0:
+object-inspect@^1.9.0:
   version "1.11.0"
   resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1"
   integrity sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==
@@ -1036,29 +1095,24 @@
   resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
   integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
 
-object.assign@^4.1.2:
-  version "4.1.2"
-  resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940"
-  integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==
+object.assign@^4.1.4:
+  version "4.1.4"
+  resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f"
+  integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==
   dependencies:
-    call-bind "^1.0.0"
-    define-properties "^1.1.3"
-    has-symbols "^1.0.1"
+    call-bind "^1.0.2"
+    define-properties "^1.1.4"
+    has-symbols "^1.0.3"
     object-keys "^1.1.1"
 
-object.entries-ponyfill@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/object.entries-ponyfill/-/object.entries-ponyfill-1.0.1.tgz#29abdf77cbfbd26566dd1aa24e9d88f65433d256"
-  integrity sha1-Kavfd8v70mVm3RqiTp2I9lQz0lY=
-
-object.values@^1.1.4:
-  version "1.1.4"
-  resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.4.tgz#0d273762833e816b693a637d30073e7051535b30"
-  integrity sha512-TnGo7j4XSnKQoK3MfvkzqKCi0nVe/D9I9IjwTNYdb/fxYHpjrluHVOgw0AF6jrRFGMPHdfuidR09tIDiIvnaSg==
+object.values@^1.1.5:
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.5.tgz#959f63e3ce9ef108720333082131e4a459b716ac"
+  integrity sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==
   dependencies:
     call-bind "^1.0.2"
     define-properties "^1.1.3"
-    es-abstract "^1.18.2"
+    es-abstract "^1.19.1"
 
 once@^1.3.0:
   version "1.4.0"
@@ -1067,48 +1121,17 @@
   dependencies:
     wrappy "1"
 
-onetime@^5.1.0:
-  version "5.1.2"
-  resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
-  integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==
+optionator@^0.9.1:
+  version "0.9.1"
+  resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499"
+  integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==
   dependencies:
-    mimic-fn "^2.1.0"
-
-optionator@^0.8.3:
-  version "0.8.3"
-  resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
-  integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==
-  dependencies:
-    deep-is "~0.1.3"
-    fast-levenshtein "~2.0.6"
-    levn "~0.3.0"
-    prelude-ls "~1.1.2"
-    type-check "~0.3.2"
-    word-wrap "~1.2.3"
-
-os-tmpdir@~1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
-  integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
-
-p-limit@^1.1.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8"
-  integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==
-  dependencies:
-    p-try "^1.0.0"
-
-p-locate@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
-  integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=
-  dependencies:
-    p-limit "^1.1.0"
-
-p-try@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
-  integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=
+    deep-is "^0.1.3"
+    fast-levenshtein "^2.0.6"
+    levn "^0.4.1"
+    prelude-ls "^1.2.1"
+    type-check "^0.4.0"
+    word-wrap "^1.2.3"
 
 parent-module@^1.0.0:
   version "1.0.1"
@@ -1117,64 +1140,25 @@
   dependencies:
     callsites "^3.0.0"
 
-parse-json@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
-  integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=
-  dependencies:
-    error-ex "^1.3.1"
-    json-parse-better-errors "^1.0.1"
-
-path-exists@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
-  integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
-
 path-is-absolute@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
   integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
 
-path-key@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
-  integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
+path-key@^3.1.0:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
+  integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
 
-path-parse@^1.0.6:
+path-parse@^1.0.6, path-parse@^1.0.7:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
   integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
 
-path-type@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
-  integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==
-  dependencies:
-    pify "^3.0.0"
-
-pify@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
-  integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=
-
-pkg-dir@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b"
-  integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=
-  dependencies:
-    find-up "^2.1.0"
-
-pkg-up@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-2.0.0.tgz#c819ac728059a461cab1c3889a2be3c49a004d7f"
-  integrity sha1-yBmscoBZpGHKscOImivjxJoATX8=
-  dependencies:
-    find-up "^2.1.0"
-
-prelude-ls@~1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
-  integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
+prelude-ls@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
+  integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
 
 progress@^2.0.0:
   version "2.0.3"
@@ -1186,39 +1170,36 @@
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
   integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
 
-read-pkg-up@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07"
-  integrity sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=
+regexp.prototype.flags@^1.4.3:
+  version "1.4.3"
+  resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac"
+  integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==
   dependencies:
-    find-up "^2.0.0"
-    read-pkg "^3.0.0"
+    call-bind "^1.0.2"
+    define-properties "^1.1.3"
+    functions-have-names "^1.2.2"
 
-read-pkg@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389"
-  integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=
-  dependencies:
-    load-json-file "^4.0.0"
-    normalize-package-data "^2.3.2"
-    path-type "^3.0.0"
+regexpp@^3.1.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2"
+  integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==
 
-regexpp@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f"
-  integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==
-
-regextras@^0.7.0:
+regextras@^0.7.1:
   version "0.7.1"
   resolved "https://registry.yarnpkg.com/regextras/-/regextras-0.7.1.tgz#be95719d5f43f9ef0b9fa07ad89b7c606995a3b2"
   integrity sha512-9YXf6xtW+qzQ+hcMQXx95MOvfqXFgsKDZodX3qZB0x2n5Z94ioetIITsBtvJbiOyxa/6s9AtyweBLCdPmPko/w==
 
+require-from-string@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
+  integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
+
 resolve-from@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
   integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
 
-resolve@^1.10.0, resolve@^1.20.0:
+resolve@^1.20.0:
   version "1.20.0"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
   integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
@@ -1226,18 +1207,19 @@
     is-core-module "^2.2.0"
     path-parse "^1.0.6"
 
-restore-cursor@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e"
-  integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==
+resolve@^1.22.0:
+  version "1.22.1"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
+  integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==
   dependencies:
-    onetime "^5.1.0"
-    signal-exit "^3.0.2"
+    is-core-module "^2.9.0"
+    path-parse "^1.0.7"
+    supports-preserve-symlinks-flag "^1.0.0"
 
-rimraf@2.6.3:
-  version "2.6.3"
-  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
-  integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
+rimraf@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
+  integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
   dependencies:
     glob "^7.1.3"
 
@@ -1248,44 +1230,24 @@
   optionalDependencies:
     fsevents "~2.3.1"
 
-run-async@^2.4.0:
-  version "2.4.1"
-  resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"
-  integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==
-
-rxjs@^6.6.0:
-  version "6.6.7"
-  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"
-  integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==
+semver@^7.2.1, semver@^7.3.5:
+  version "7.3.7"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f"
+  integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==
   dependencies:
-    tslib "^1.9.0"
+    lru-cache "^6.0.0"
 
-"safer-buffer@>= 2.1.2 < 3":
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
-  integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
-
-"semver@2 || 3 || 4 || 5", semver@^5.5.0:
-  version "5.7.1"
-  resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
-  integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
-
-semver@^6.1.2, semver@^6.3.0:
-  version "6.3.0"
-  resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
-  integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
-
-shebang-command@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
-  integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=
+shebang-command@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
+  integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
   dependencies:
-    shebang-regex "^1.0.0"
+    shebang-regex "^3.0.0"
 
-shebang-regex@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
-  integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
+shebang-regex@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
+  integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
 
 side-channel@^1.0.4:
   version "1.0.4"
@@ -1296,19 +1258,14 @@
     get-intrinsic "^1.0.2"
     object-inspect "^1.9.0"
 
-signal-exit@^3.0.2:
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
-  integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
-
-slice-ansi@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636"
-  integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==
+slice-ansi@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b"
+  integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==
   dependencies:
-    ansi-styles "^3.2.0"
-    astral-regex "^1.0.0"
-    is-fullwidth-code-point "^2.0.0"
+    ansi-styles "^4.0.0"
+    astral-regex "^2.0.0"
+    is-fullwidth-code-point "^3.0.0"
 
 source-map-support@~0.5.19:
   version "0.5.19"
@@ -1328,20 +1285,12 @@
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
   integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
 
-spdx-correct@^3.0.0:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
-  integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==
-  dependencies:
-    spdx-expression-parse "^3.0.0"
-    spdx-license-ids "^3.0.0"
-
 spdx-exceptions@^2.1.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d"
   integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==
 
-spdx-expression-parse@^3.0.0:
+spdx-expression-parse@^3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679"
   integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==
@@ -1359,46 +1308,32 @@
   resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
   integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
 
-string-width@^3.0.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
-  integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==
-  dependencies:
-    emoji-regex "^7.0.1"
-    is-fullwidth-code-point "^2.0.0"
-    strip-ansi "^5.1.0"
-
-string-width@^4.1.0:
-  version "4.2.2"
-  resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5"
-  integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==
+string-width@^4.2.3:
+  version "4.2.3"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
+  integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
   dependencies:
     emoji-regex "^8.0.0"
     is-fullwidth-code-point "^3.0.0"
-    strip-ansi "^6.0.0"
+    strip-ansi "^6.0.1"
 
-string.prototype.trimend@^1.0.4:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80"
-  integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==
+string.prototype.trimend@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz#914a65baaab25fbdd4ee291ca7dde57e869cb8d0"
+  integrity sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==
   dependencies:
     call-bind "^1.0.2"
-    define-properties "^1.1.3"
+    define-properties "^1.1.4"
+    es-abstract "^1.19.5"
 
-string.prototype.trimstart@^1.0.4:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed"
-  integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==
+string.prototype.trimstart@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz#5466d93ba58cfa2134839f81d7f42437e8c01fef"
+  integrity sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==
   dependencies:
     call-bind "^1.0.2"
-    define-properties "^1.1.3"
-
-strip-ansi@^5.1.0, strip-ansi@^5.2.0:
-  version "5.2.0"
-  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
-  integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
-  dependencies:
-    ansi-regex "^4.1.0"
+    define-properties "^1.1.4"
+    es-abstract "^1.19.5"
 
 strip-ansi@^6.0.0:
   version "6.0.0"
@@ -1407,12 +1342,19 @@
   dependencies:
     ansi-regex "^5.0.0"
 
+strip-ansi@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+  integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+  dependencies:
+    ansi-regex "^5.0.1"
+
 strip-bom@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
   integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=
 
-strip-json-comments@^3.0.1:
+strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
   integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
@@ -1431,15 +1373,21 @@
   dependencies:
     has-flag "^4.0.0"
 
-table@^5.2.3:
-  version "5.4.6"
-  resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"
-  integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==
+supports-preserve-symlinks-flag@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
+  integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
+
+table@^6.0.9:
+  version "6.8.0"
+  resolved "https://registry.yarnpkg.com/table/-/table-6.8.0.tgz#87e28f14fa4321c3377ba286f07b79b281a3b3ca"
+  integrity sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA==
   dependencies:
-    ajv "^6.10.2"
-    lodash "^4.17.14"
-    slice-ansi "^2.1.0"
-    string-width "^3.0.0"
+    ajv "^8.0.1"
+    lodash.truncate "^4.4.2"
+    slice-ansi "^4.0.0"
+    string-width "^4.2.3"
+    strip-ansi "^6.0.1"
 
 terser@^5.6.1:
   version "5.7.0"
@@ -1455,58 +1403,36 @@
   resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
   integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
 
-through@^2.3.6:
-  version "2.3.8"
-  resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
-  integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
-
-tmp@^0.0.33:
-  version "0.0.33"
-  resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
-  integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==
-  dependencies:
-    os-tmpdir "~1.0.2"
-
-tsconfig-paths@^3.11.0:
-  version "3.11.0"
-  resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.11.0.tgz#954c1fe973da6339c78e06b03ce2e48810b65f36"
-  integrity sha512-7ecdYDnIdmv639mmDwslG6KQg1Z9STTz1j7Gcz0xa+nshh/gKDAHcPxRbWOsA3SPp0tXP2leTcY9Kw+NAkfZzA==
+tsconfig-paths@^3.14.1:
+  version "3.14.1"
+  resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a"
+  integrity sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==
   dependencies:
     "@types/json5" "^0.0.29"
     json5 "^1.0.1"
-    minimist "^1.2.0"
+    minimist "^1.2.6"
     strip-bom "^3.0.0"
 
-tslib@^1.9.0:
-  version "1.14.1"
-  resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
-  integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
-
-type-check@~0.3.2:
-  version "0.3.2"
-  resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
-  integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=
+type-check@^0.4.0, type-check@~0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
+  integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==
   dependencies:
-    prelude-ls "~1.1.2"
+    prelude-ls "^1.2.1"
 
-type-fest@^0.21.3:
-  version "0.21.3"
-  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
-  integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
+type-fest@^0.20.2:
+  version "0.20.2"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
+  integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
 
-type-fest@^0.8.1:
-  version "0.8.1"
-  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
-  integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
-
-unbox-primitive@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471"
-  integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==
+unbox-primitive@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"
+  integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==
   dependencies:
-    function-bind "^1.1.1"
-    has-bigints "^1.0.1"
-    has-symbols "^1.0.2"
+    call-bind "^1.0.2"
+    has-bigints "^1.0.2"
+    has-symbols "^1.0.3"
     which-boxed-primitive "^1.0.2"
 
 uri-js@^4.2.2:
@@ -1521,14 +1447,6 @@
   resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
   integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
 
-validate-npm-package-license@^3.0.1:
-  version "3.0.4"
-  resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
-  integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==
-  dependencies:
-    spdx-correct "^3.0.0"
-    spdx-expression-parse "^3.0.0"
-
 which-boxed-primitive@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"
@@ -1540,14 +1458,14 @@
     is-string "^1.0.5"
     is-symbol "^1.0.3"
 
-which@^1.2.9:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
-  integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
+which@^2.0.1:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
+  integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
   dependencies:
     isexe "^2.0.0"
 
-word-wrap@~1.2.3:
+word-wrap@^1.2.3:
   version "1.2.3"
   resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
   integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
@@ -1557,9 +1475,7 @@
   resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
   integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
 
-write@1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3"
-  integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==
-  dependencies:
-    mkdirp "^0.5.1"
+yallist@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
+  integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==