version: '3'

vars:
  PROJECT_NAME: deft
  # VERSION is resolved dynamically (#723) so `task build` produces
  # `dist/deft-{version}.{zip,tar.gz}` matching the in-flight release.
  # Resolution priority:
  #   1. $DEFT_RELEASE_VERSION env var (set by `task release -- 0.21.0`
  #      so the release task builds dist/deft-0.21.0.zip).
  #   2. `git describe --tags --abbrev=0` stripped of leading `v`.
  #   3. `0.0.0-dev` fallback for fresh checkouts with no tags.
  # The previous hard-coded literal (`VERSION: 0.20.0`) was the root cause
  # of #723: every release after v0.20.0 produced a stale artifact name
  # because nothing in the release pipeline updated the Taskfile literal.
  # Inline POSIX sh (run by go-task's embedded mvdan/sh interpreter so the
  # block is cross-platform on Windows / macOS / Linux without requiring
  # uv/python at parse time).
  VERSION:
    sh: |
      if [ -n "$DEFT_RELEASE_VERSION" ]; then
        printf '%s' "$DEFT_RELEASE_VERSION"
      elif tag=$(git describe --tags --abbrev=0 2>/dev/null); then
        printf '%s' "${tag#v}"
      else
        printf '0.0.0-dev'
      fi
  # Each included sub-taskfile (tasks/*.yml) defines its own DEFT_ROOT as
  # `{{joinPath .TASKFILE_DIR ".."}}` so scripts can be dispatched via
  # `{{.DEFT_ROOT}}/scripts/...` on every platform (#566). `joinPath` is
  # evaluated eagerly by go-task's templating and uses Go's `filepath.Clean`,
  # which yields a native-separator, `..`-free absolute path that
  # `uv run python` can open on Windows. The previous
  # `{{.TASKFILE_DIR}}/../scripts/...` shape mixed native separators with
  # forward-slash traversal and normalized incorrectly under Windows Python,
  # dropping the deft/ prefix.
  #
  # DEFT_ROOT is intentionally NOT defined here at the root Taskfile level
  # because go-task re-evaluates var templates at use site in included
  # subfiles -- a root-level `DEFT_ROOT: '{{.TASKFILE_DIR}}'` would expand
  # to the subfile's own directory (tasks/) when referenced from a subfile,
  # not to the deft/ root.

# Top-level env propagates into included taskfiles (Task v3 includes inherit
# the parent env:). PYTHONUTF8=1 ensures Python scripts invoked from any
# deft task have UTF-8 stdout/stderr on Windows (cp1252 default would crash
# on the ✓/→/✗/⚠ symbols emitted by spec_validate / roadmap_render / etc.).
# Every task that runs Python ALSO sets this in its own env: block as a
# belt-and-suspenders guard -- see #540 for the full audit.
#
# UV_PROJECT is the Layer-1 safety net for #1011: every framework-side
# `uv run` invocation must resolve against the framework's own pyproject.toml,
# not an ancestor pyproject.toml that happens to live above the install
# location on the consumer machine. Without this, `uv run` walks upward
# from cwd looking for the nearest pyproject.toml and binds to whatever
# it finds first -- on a typical dev box with a parent workspace
# pyproject.toml, that workspace's build backend (e.g. unresolvable
# `setuptools.backends._legacy:_Backend`) crashes during environment
# resolution before any framework task body runs. UV_PROJECT short-circuits
# the upward walk by pinning the project root explicitly. {{.TASKFILE_DIR}}
# at this top-level env: resolves to the directory containing this
# Taskfile.yml (the framework root), which is exactly the right pin for
# the framework's own pyproject.toml. CLI `--project` on each call site
# (Layer 2, see tasks/*.yml) overrides env, which overrides walk -- the
# CLI flag is the contract; UV_PROJECT is defense-in-depth for any task
# that drops the flag in a future edit. See #1011 for the full root-cause
# analysis and the two-layer rationale.
env:
  PYTHONUTF8: "1"
  UV_PROJECT: '{{.TASKFILE_DIR}}'

includes:
  docs:
    taskfile: ./tasks/docs.yml
    optional: true
  ts:
    taskfile: ./tasks/ts.yml
    optional: true
  spec:
    taskfile: ./tasks/spec.yml
    optional: true
  install:
    taskfile: ./tasks/install.yml
    optional: true
  deployments:
    taskfile: ./tasks/deployments.yml
    optional: true
  # check-graph namespaces (#3070): must NOT be optional. CONSUMER_CHECK_GATES /
  # check:consumer depend on these includes; optional silent omit yields opaque
  # go-task "Task does not exist" (exit 200/201). Fail loud when the file is
  # missing so operators get a clear deposit-repair signal (`deft update`).
  toolchain:
    taskfile: ./tasks/toolchain.yml
    optional: false
  verify:
    taskfile: ./tasks/verify.yml
    optional: false
  coverage:
    taskfile: ./tasks/coverage.yml
    optional: true
  review-monitor:
    taskfile: ./tasks/review-monitor.yml
    optional: true
  agent:
    taskfile: ./tasks/agent.yml
    optional: true
  architecture:
    taskfile: ./tasks/architecture.yml
    optional: true
  codebase:
    taskfile: ./tasks/codebase.yml
    optional: true
  change:
    taskfile: ./tasks/change.yml
    optional: true
  commit:
    taskfile: ./tasks/commit.yml
    optional: true
  scope:
    taskfile: ./tasks/scope.yml
    optional: true
  # D15 (#1134): scope:undo audit-log reversibility verb. Standalone
  # fragment per the post-D12 sub-task convention so the cross-child
  # coordination contract on tasks/scope.yml does not collide. Inner
  # task `undo` is exposed as the user-facing alias `task scope:undo`
  # below.
  scope-undo:
    taskfile: ./tasks/scope-undo.yml
    optional: true
  swarm:
    taskfile: ./tasks/swarm.yml
    optional: false
  roadmap:
    taskfile: ./tasks/roadmap.yml
    optional: true
  project:
    taskfile: ./tasks/project.yml
    optional: true
  migrate:
    taskfile: ./tasks/migrate.yml
    optional: true
  # check-graph namespace (#3070): vbrief:validate is on CONSUMER_CHECK_GATES.
  vbrief:
    taskfile: ./tasks/vbrief.yml
    optional: false
  # Non-optional (#3483): the swarm skill names xbrief:validate / xbrief:activate
  # / xbrief:preflight at MUST/⊗ level, so a silently-omitted include would
  # reproduce the opaque exit-200 "task does not exist" failure at cohort close.
  xbrief:
    taskfile: ./tasks/xbrief.yml
    optional: false
  prd:
    taskfile: ./tasks/prd.yml
    optional: true
  reconcile:
    taskfile: ./tasks/reconcile.yml
    optional: true
  issue:
    taskfile: ./tasks/issue.yml
    optional: true
  pr:
    taskfile: ./tasks/pr.yml
    optional: true
  directive:
    taskfile: ./tasks/directive.yml
    optional: true
  policy:
    taskfile: ./tasks/policy.yml
    optional: true
  framework:
    taskfile: ./tasks/framework.yml
    optional: true
  umbrella:
    taskfile: ./tasks/umbrella.yml
    optional: true
  session:
    taskfile: ./tasks/session.yml
    optional: true
  occupancy:
    taskfile: ./tasks/occupancy.yml
    optional: true
  lifecycle:
    taskfile: ./tasks/lifecycle.yml
    optional: true
  plan-sequence:
    taskfile: ./tasks/plan-sequence.yml
    optional: true
  # #883 Story 1 stub include. The fragment exposes its inner tasks
  # (`issue:list` / `issue:view` / `issue:close` / `issue:edit`) under the
  # `scm` namespace key, producing the canonical `scm:issue:*` surface in
  # `task -l` with no doubled prefix. Forward-compat marker block lives at
  # the top of `tasks/scm.yml`; the full scm:* namespace lives at #881 and
  # replaces this stub wholesale when it lands.
  scm:
    taskfile: ./tasks/scm.yml
    optional: true
  # #883 Story 2 cache + quarantine layer. Five-command surface
  # (cache:put / cache:get / cache:invalidate / cache:fetch-all /
  # cache:prune) under the canonical `cache:` namespace via the include
  # key. Forward-compat: future v2 cache surfaces (cache:check /
  # cache:refresh / cache:doctor / cache:stats / quarantine:scan) are
  # deferred per the epic deferred_to_v2 list and land here when v2
  # ships.
  cache:
    taskfile: ./tasks/cache.yml
    optional: true
  # Triage v1 fragment includes (#845 epic, #883 Story 3 rebind onto cache:*).
  #
  # Three standalone fragments authored by stories A3, A4, and A6
  # respectively (the legacy A1 `triage-cache` fragment was deleted in
  # #883 Story 3 -- the unified `cache:*` surface above is the
  # single content-mirroring layer). Each fragment is `optional: true`
  # so the parent Taskfile parses cleanly even when a sibling fragment
  # has not yet landed.
  #
  # Namespacing: go-task v3 prefixes inner task names with the include
  # key. Two includes cannot share a single namespace key, so the
  # fragments use unique keys derived from the file basename. The
  # user-facing `task triage:bootstrap` / `task triage:accept` /
  # `task triage:bulk-*` etc. forms are exposed via top-level alias
  # tasks below.
  triage-actions:
    taskfile: ./tasks/triage-actions.yml
    optional: true
  triage-bulk:
    taskfile: ./tasks/triage-bulk.yml
    optional: true
  triage-bootstrap:
    taskfile: ./tasks/triage-bootstrap.yml
    optional: true
  # #1468: `task triage:reconcile` audit-log self-heal. Inner task
  # `reconcile` is exposed as the user-facing alias `task triage:reconcile`
  # in the alias block below.
  triage-reconcile:
    taskfile: ./tasks/triage-reconcile.yml
    optional: true
  # D12 (#1131): typed plan.policy.triageScope[] subscription surface.
  # Inner task `scope` is exposed as the user-facing alias `task triage:scope`
  # in the alias block below.
  triage-scope:
    taskfile: ./tasks/triage-scope.yml
    optional: true
  # D14 (#1133): subscription drift detector. Inner task `scope-drift` is
  # exposed as the user-facing alias `task triage:scope-drift` in the
  # alias block below.
  triage-scope-drift:
    taskfile: ./tasks/triage-scope-drift.yml
    optional: true
  # D14 (#1133): subscribe / unsubscribe mutation verbs. Inner tasks
  # `subscribe` / `unsubscribe` are exposed as the user-facing aliases
  # `task triage:subscribe` / `task triage:unsubscribe` below.
  triage-subscribe:
    taskfile: ./tasks/triage-subscribe.yml
    optional: true
  # D10 (#1129): auto-classification surface. Inner task `classify` is
  # exposed as the user-facing alias `task triage:classify` in the alias
  # block below.
  triage-classify:
    taskfile: ./tasks/triage-classify.yml
    optional: true
  # #3648: `task triage:evaluate` Stage A isolated validity + parent WIP census.
  triage-evaluate:
    taskfile: ./tasks/triage-evaluate.yml
    optional: true
  # D2 (#1122): `task triage:summary` one-liner -- status surface invoked by
  # the session-start ritual (N9 / #1149). Inner task `summary` is exposed
  # as the user-facing alias `task triage:summary` in the alias block below.
  triage-summary:
    taskfile: ./tasks/triage-summary.yml
    optional: true
  # D11 (#1128): ranked queue + per-item show + audit-log triage surfaces.
  # Inner tasks queue/show/audit are exposed as the user-facing aliases
  # `task triage:queue` / `task triage:show` / `task triage:audit` below.
  triage-queue:
    taskfile: ./tasks/triage-queue.yml
    optional: true
  # N3 (#1143): `task triage:welcome` 6-phase onboarding ritual. Inner
  # task `welcome` is exposed as the user-facing alias `task triage:welcome`
  # in the alias block below.
  triage-welcome:
    taskfile: ./tasks/triage-welcome.yml
    optional: true
  # N6 (#1146): `task triage:smoketest` end-to-end synthetic test of the
  # cache-as-operator-working-set surface. Inner task `smoketest` is
  # exposed as the user-facing alias `task triage:smoketest` in the
  # alias block below.
  triage-smoketest:
    taskfile: ./tasks/triage-smoketest.yml
    optional: true
  # D17 (#1709): `task triage:metrics` trend readout from summary-history.jsonl.
  # Inner task `metrics` is exposed as the user-facing alias `task triage:metrics`
  # in the alias block below.
  triage-metrics:
    taskfile: ./tasks/triage-metrics.yml
    optional: true
  # Windows maintainer onboarding fragment (#902). Exposes the inner task
  # `toolchain` as `task setup:toolchain`. Note: the root-level `setup` task
  # below (git-hooks bootstrap) coexists with `setup:toolchain` because
  # go-task treats them as distinct task names.
  setup:
    taskfile: ./tasks/setup.yml
    optional: true
  # CHANGELOG.md union-merge helper (#911). Exposes
  # `task changelog:resolve-unreleased` -- the canonical swarm-cascade Phase 6
  # Step 1 surface that replaces the older HEAD-take-and-discard pattern that
  # silently dropped rebasing-branch CHANGELOG entries on every cascade rebase.
  # See skills/deft-directive-swarm/SKILL.md Phase 6 Step 1 for the manual-
  # fallback contract; the include is `optional: true` so the parent Taskfile
  # parses cleanly when the fragment is absent (rolling-merge tolerance).
  changelog:
    taskfile: ./tasks/changelog.yml
    optional: true
  # NOTE (#2022 Python-purge): the `relocate:` include was DROPPED from the
  # consumer task surface. The relocate task shelled into scripts/relocate.py
  # via `uv run python` -- the sole remaining consumer-exposed Python coupling
  # on the deft task surface. It is intentionally NOT wired here anymore. The
  # canonical consumer (re)install / relocate path is the npm installer
  # (`npm i -g @deftai/directive@latest`; see UPGRADING.md / #1912, where
  # relocate is a back-compat / legacy bridge only). tasks/relocate.yml is
  # retained (un-wired) and the helper scripts/relocate.py stays for #1860
  # (big-bang Python delete) to remove.
  # N7 (#1147): slice:* fragment exposing `task slice:record-existing`
  # (retrofit slices.jsonl for hand-filed cohorts) + `task slice:list`
  # (read surface). Include key `slice-record` (not `slice`) so the
  # fragment-namespace forms (`slice-record:record-existing`,
  # `slice-record:list`) do NOT collide with the user-facing
  # `slice:record-existing` / `slice:list` aliases defined below.
  # Inner tasks are `internal: true`; aliases at the root surface the
  # documented user-facing names per the established triage:* / scope:*
  # convention.
  slice-record:
    taskfile: ./tasks/slice.yml
    optional: true
  # Capacity allocation accounting surface (#1419 Delivery Slice 4). Exposes
  # `task capacity:show` (advisory, offline target-vs-actual bucket mix). The
  # companion `task verify:capacity` lives in tasks/verify.yml and is
  # DELIBERATELY absent from the `task check` aggregate -- capacity is
  # advise-by-default and must never fail-closed on the framework tree.
  # `optional: true` for rolling-merge tolerance.
  capacity:
    taskfile: ./tasks/capacity.yml
    optional: true
  # Tier 0 framework-eval surface (#1703). Exposes `task eval:health` --
  # aggregates static self-consistency gates into a versioned health score.
  eval:
    taskfile: ./tasks/eval.yml
    optional: true
  # Gap escalation upstream filing (#1709 child 5). Exposes `task feedback:file` --
  # confirmation-gated, deduped framework-gap issues for consumer projects.
  feedback:
    taskfile: ./tasks/feedback.yml
    optional: true
  # Pull-based value-awareness readbacks (#1709). Inner task `show` is exposed as
  # `task value:show` via the include namespace key.
  value:
    taskfile: ./tasks/value.yml
    optional: true
  # Structured agent decision log (#1396). Inner tasks write/list → decision:write / decision:list.
  decision:
    taskfile: ./tasks/decision.yml
    optional: true
  product-signal:
    taskfile: ./tasks/product-signal.yml
    optional: true
  # Pack-slicing surface (#1283 design, #1294 pilot, ADR-001 Layer B). Exposes
  # `task packs:slice` (named-slice API), `task packs:render` (regenerate the
  # meta/lessons.md projection), and `task packs:verify-drift` (the drift gate,
  # also surfaced as the user-facing alias `task verify:pack-drift` below and
  # wired into `check:framework-source`). `optional: true` for rolling-merge
  # tolerance.
  packs:
    taskfile: ./tasks/packs.yml
    optional: true
  # Maintainer-only packaging lane (#1813 contributor path / #2022
  # Phase 2). `internal: true` hides `core:*` / `ci:*` from `task -l` and
  # blocks direct CLI invocation on consumer installs; only
  # `check:framework-source` below wires them as deps. Consumer `task check`
  # dispatches to `check:consumer` (TS / deft verbs).
  core:
    taskfile: ./tasks/core.yml
    optional: true
    internal: true
  # npm consumer deposits resolve verbs through global `deft` when the vendored
  # packages/cli/dist/bin.js is absent (#2022 Phase 3).
  engine:
    taskfile: ./tasks/engine.yml
    optional: true

tasks:
  default:
    desc: List all available tasks
    cmds:
      - task --list
    silent: true

  # Backward-compatible aliases -- project convention is `task check`, `task test`, etc.
  check:
    desc: "Run the context-appropriate pre-commit gate: full framework self-check in this repo, consumer-safe gate from vendored installs (#1519)."
    dir: '{{.USER_WORKING_DIR}}'
    deps:
      - task: engine:_ts-build
    cmds:
      - task: engine:invoke
        vars:
          ENGINE_CMD: 'check --framework-root "{{.TASKFILE_DIR}}" --project-root "{{.USER_WORKING_DIR}}"'

  test:coverage:
    desc: "Run tests with coverage (alias for ts:test; canonical coverage path used by task check, #2528)"
    cmds:
      - task: ts:test

  check:merge:
    desc: "Merge chokepoint gate — explicit alias for check:framework-source (#1704). CI and PR gates SHOULD invoke this (or task check) as the single SoT so discrete workflow steps cannot drift."
    deps:
      - task: engine:_ts-build
    cmds:
      - task: check:framework-source

  check:framework-source:
    desc: "Run all framework source-repo pre-commit checks (TS-only after #1860). Sole wired consumer of maintainer-only core:build / core:clean."
    deps:
      - ts:check-lane
      - toolchain:check
      - verify:stubs
      - verify:links
      - verify:rule-ownership
      - verify:biome-config
      - verify:content-manifest
      - verify:deposit-closure
      - verify:license-sync
      - verify:skill-external-fetch-gate
      - verify:semantic-single-source
      - verify:contract-drift
      - verify:cursor-tier1
      - verify:openclaw-tier1
      - verify:go-freeze
      - verify:bridge-drift
      - verify:branch
      - verify:encoding
      - verify:closing-keywords
      - verify:forward-coverage
      - verify:test-boundary
      - verify:scope-provenance
      # #3893: this repo owns its own check composition, so the contract runs
      # fail-closed here on the merge-chokepoint scoping rule.
      - task: verify:consumer-check-contract
        vars:
          CLI_ARGS: "--framework-source"
      - task: verify:evaluator-surface
        vars:
          CLI_ARGS: "--base-ref origin/master"
      - verify:observable-scope
      - verify:intent-constraint
      - verify:telemetry-coverage
      - verify:vbrief-conformance
      - verify:destructive-gh-verbs
      - verify:scm-boundary
      - verify:xbrief-drift
      - verify:no-task-runtime
      - verify:cache-fresh
      - verify:pack-drift
      - verify-wip-cap-framework-self-check
      # #3893: candidate-scoped on the merge chokepoint; bare verify:orphan-active
      # stays repo-wide for doctor, manual runs, and the after-merge --issue N run.
      - task: verify:orphan-active
        vars:
          CLI_ARGS: "--changed-only"
      - verify:completed-write-guard
      - verify:agents-md-budget
      - verify-eval-health-relocation-framework-check
      - verify-eval-triggers-relocation-framework-check
      - vbrief:validate
      - task: codebase:validate-structure
        vars:
          CLI_ARGS: "--enforce"
      - verify:codebase-map-fresh
      - verify:spec-prd-fresh
      # #4095: committed RULE-MAP freshness (byte-identical renderer output).
      - docs:rule-map:check
      # #4099: committed capability index freshness (overlay vs registries).
      - docs:capability-map:check
      - verify-strategy-output
      # #2980 residual: fail-closed product raw-write inventory (allowlist primitives + temporary residual).
      - verify-contained-writes-enforce
    cmds:
      - echo "All checks passed"

  # Framework self-check shim so check:framework-source can pass --enforce without
  # changing the default fail-open CLI for `task verify:contained-writes` alone.
  verify-contained-writes-enforce:
    internal: true
    desc: "Fail-closed contained-writes inventory for task check (#2980 residual)."
    cmds:
      - task: verify:contained-writes
        vars:
          CLI_ARGS: "--enforce"

  check:consumer:
    desc: "Run the consumer-safe Deft quality gate for vendored installs (#1519)."
    deps:
      - doctor
      - toolchain:check-consumer
      - verify:branch
      - verify:cache-fresh
      - verify:wip-cap
      # #3893: candidate-scoped on the merge chokepoint; bare verify:orphan-active
      # stays repo-wide for doctor, manual runs, and the after-merge --issue N run.
      - task: verify:orphan-active
        vars:
          CLI_ARGS: "--changed-only"
      - verify:completed-write-guard
      - verify:test-boundary
      - verify:scope-provenance
      - verify:consumer-check-contract
      - verify:evaluator-surface
      - verify:observable-scope
      - verify:intent-constraint
      - vbrief:validate
      - verify-strategy-output
      - verify:consumer-test-lane
    cmds:
      - echo "Consumer checks passed"

  # D4 (#1124): framework self-check wrapper that passes
  # --allow-over-cap so deft's own landing-day overage (pending/+active/
  # currently >> 10, resolved via D1 scope:demote --batch per umbrella
  # v3) does not break framework `task check`. Consumer projects depend
  # on `verify:wip-cap` directly (without --allow-over-cap) so a
  # stale-branch / --force-merge over-cap state fails their `task check`
  # loudly. Mirrors the verify:cache-fresh / verify:branch
  # --allow-missing-* tolerance pattern.
  verify-wip-cap-framework-self-check:
    internal: true
    desc: "Framework self-check shim for verify:wip-cap (#1124)."
    dir: '{{.USER_WORKING_DIR}}'
    env:
      PYTHONUTF8: "1"
    cmds:
      - task: verify:wip-cap
        vars:
          CLI_ARGS: "--allow-over-cap"

  verify-eval-health-relocation-framework-check:
    internal: true
    desc: "Framework self-check shim for verify:eval-health-relocation (#2373)."
    dir: '{{.USER_WORKING_DIR}}'
    cmds:
      - task: verify:eval-health-relocation
        vars:
          CLI_ARGS: "--base-ref origin/master"

  verify-eval-triggers-relocation-framework-check:
    internal: true
    desc: "Framework self-check shim for verify:eval-triggers-relocation (#1586)."
    dir: '{{.USER_WORKING_DIR}}'
    cmds:
      - task: verify:eval-triggers-relocation
        vars:
          CLI_ARGS: "--base-ref origin/master"

  # s2-deterministic-gate (#1166): deterministic validation gate for strategy
  # output shape. Runs on every `task check` (including CI). Implemented in
  # scripts/validate_strategy_output.py. Respects Grok Build Windows rules
  # (plain uv python invocation, no pipes in this Taskfile context).
  verify-strategy-output:
    desc: "Deterministic v0.20 strategy output shape gate (#1166 s2). Fails on non-date-prefixed vBRIEFs in lifecycle dirs, missing PROJECT-DEFINITION.vbrief.json, or legacy specification.vbrief.json in user projects."
    dir: '{{.USER_WORKING_DIR}}'
    deps:
      - task: engine:_ts-build
    cmds:
      - task: engine:invoke
        vars:
          ENGINE_CMD: 'validate-strategy-output --project-root "{{.USER_WORKING_DIR}}"'

  # Pack-projection drift gate (#1294 / #1283, ADR-001). User-facing alias for
  # `packs:verify-drift`, defined at the root Taskfile so it carries the
  # documented `verify:*` prefix that the gate stack + `task check` reference
  # while the implementation lives in the `packs:` include namespace (mirrors
  # the established triage:* / scope:* alias precedent above). Wired into
  # `check:framework-source` deps. Forwards {{.CLI_ARGS}} so --source / --output
  # overrides reach the script.
  verify:pack-drift:
    desc: "Fail when meta/lessons.md drifts from the canonical lessons-pack source (#1294 / ADR-001). Alias for packs:verify-drift; wired into `task check`."
    cmds:
      - task: packs:verify-drift
        vars:
          CLI_ARGS: "{{.CLI_ARGS}}"

  verify:no-task-runtime:
    desc: "Fail when runtime Python code hard-depends on go-task (#1659)."
    deps:
      - task: engine:_ts-build
    cmds:
      # Oracle/fallback (parity): scripts/verify_no_task_runtime.py (#1854 s3).
      - task: engine:invoke
        vars:
          ENGINE_CMD: 'verify-no-task-runtime'

  # Maintainer-only packaging aliases (#1860). `core:*` is internal;
  # these root aliases remain callable for release Step 8 (`task build`) and
  # local maintainer workflows. Not wired into `check:consumer`.
  build:
    desc: Package framework for distribution (maintainer-only; alias for core:build)
    cmds:
      - task: core:build
  clean:
    desc: Clean generated artifacts (maintainer-only; alias for core:clean)
    cmds:
      - task: core:clean

  install:
    desc: Install deft (alias for install:install)
    cmds:
      - task: install:install
  uninstall:
    desc: Remove deft entry from AGENTS.md (alias for install:uninstall)
    cmds:
      - task: install:uninstall
  # User-facing upgrade entrypoint (#1061). Aliases the install:upgrade
  # wrapper. As of #2064 the native deft-ts install-upgrade handler is a thin
  # redirect onto `directive update` -- the single canonical upgrade verb that
  # file-swaps the vendored .deft/core payload, rewrites the install manifest,
  # and regenerates .deft-version. Prefer `deft update` directly; this alias is
  # retained for compatibility. Cited by the doctor's failure prose and
  # docs/install-manifest.md as the post-drift repair entrypoint.
  upgrade:
    desc: "Upgrade deft framework -- redirects to `deft update` (the canonical verb: refresh payload + AGENTS.md + install manifest + .deft-version; alias for install:upgrade, #1061/#2064)"
    cmds:
      - task: install:upgrade

  # #1272: canonical doctor surface -- thin shim that forwards to
  # ``.deft/core/run doctor``. The ``run`` CLI owns the diagnostic
  # logic (system deps, framework layout, root Taskfile.yml include
  # health) and the optional interactive repair path. The task
  # accepts user-facing flags (``--session`` / ``--fix`` / ``--json``
  # / ``--quiet``) via ``{{.CLI_ARGS}}`` so per
  # ``conventions/task-caching.md`` (#574) MUST NOT declare
  # ``sources:`` / ``generates:`` -- a cached cmds skip would silently
  # swallow the flags. The deprecated ``task framework:doctor`` task
  # in ``tasks/framework.yml`` now prints a redaction notice pointing
  # the operator at this surface.
  doctor:
    desc: "Canonical doctor surface (#1272) -- task doctor [-- --session | --fix | --json | --quiet | --network]. Uses vendored bin.js in source checkouts or global deft on npm consumer deposits (#2022 Phase 3). --network is required to run the payload-staleness check (git verifies the pin; npm compares stable release availability); it is offline (skipped) by default and discloses the tool + registry class before contacting either (#2182)."
    dir: '{{.USER_WORKING_DIR}}'
    cmds:
      - task: engine:invoke
        vars:
          ENGINE_CMD: 'doctor --project-root "{{.USER_WORKING_DIR}}" {{.CLI_ARGS}}'

  setup:
    desc: "Idempotent local-dev setup: configure git hooks (#747); detect-and-prompt ghx (#884). Sets core.hooksPath=.githooks so .githooks/pre-commit + .githooks/pre-push run."
    dir: '{{.USER_WORKING_DIR}}'
    deps:
      - task: engine:_ts-build
    env:
      PYTHONUTF8: "1"
    cmds:
      # Inline POSIX-sh under go-task's mvdan/sh interpreter -- cross-platform.
      # `git config core.hooksPath .githooks` is itself idempotent, but we
      # detect the no-op so the task prints a friendly message rather than
      # silently re-asserting on every invocation.
      - |
        missing=0
        for f in pre-commit pre-push _deft-run.sh; do
          if [ ! -f ".githooks/$f" ]; then
            missing=1
            break
          fi
        done
        if [ "$missing" = 1 ]; then
          echo "❌ deft setup refused: project-root .githooks/ is missing hook files (#2530)."
          echo "  Recovery: run \`deft update\` (or \`deft init\` on a greenfield tree) to deposit hooks first."
          exit 1
        fi
        configured=$(git config --get core.hooksPath || true)
        if [ "$configured" = ".githooks" ]; then
          echo "✓ core.hooksPath already set to .githooks (no change)."
        else
          git config core.hooksPath .githooks
          echo "✓ core.hooksPath set to .githooks (#747 branch gate active)."
        fi
      # #814: Detect a Python interpreter whose stdout defaults to a
      # non-UTF-8 Windows code page (cp1252 / cp437). Informational only --
      # the deft hook scripts themselves self-reconfigure to UTF-8 at
      # main() entry, so this warning surfaces a fact about the user's
      # environment, not a defect that blocks setup. We do NOT auto-set
      # PYTHONIOENCODING because mutating the user's environment would
      # surprise other tooling (out-of-scope per the issue body).
      - |
        python_bin="${DEFT_PYTHON:-}"
        if [ -z "$python_bin" ]; then
          if command -v python3 >/dev/null 2>&1; then
            python_bin=python3
          elif command -v python >/dev/null 2>&1; then
            python_bin=python
          fi
        fi
        if [ -n "$python_bin" ]; then
          # NOTE: we explicitly unset PYTHONUTF8 / PYTHONIOENCODING in the
          # subshell so the probe measures what Python would do under git's
          # hook environment (which inherits the user's shell, NOT the
          # Taskfile root env: block). Without the unset, the root-level
          # `env: PYTHONUTF8: "1"` (#540) masks cp1252 here and the warning
          # would never surface even when the user actually has the bug.
          enc=$(unset PYTHONUTF8 PYTHONIOENCODING; "$python_bin" -c "import sys; print((sys.stdout.encoding or '').lower())" 2>/dev/null || true)
          case "$enc" in
            cp1252|cp437|charmap)
              echo ""
              echo "WARN: Detected Python stdout encoding '$enc' (Windows default). Deft hooks"
              echo "print non-ASCII glyphs and may crash without UTF-8 stdout. Either:"
              echo "  (a) set the user-environment variable PYTHONIOENCODING=utf-8 (one-time,"
              echo "      via System Properties > Environment Variables), OR"
              echo "  (b) add \$env:PYTHONIOENCODING='utf-8' to your PowerShell profile."
              echo "The deft hook scripts also self-reconfigure as of #814, so this"
              echo "warning is informational on current versions."
              ;;
          esac
        fi
      # #884: ghx adoption -- consent-gated install of the brunoborges/ghx
      # GitHub CLI cache proxy. Default invocation here passes --check so
      # `task setup` never prompts on a clean re-run; operators wanting to
      # opt in run `task setup:ghx` (defined below) which is the
      # interactive entry point. Native TypeScript handler (#2022 Phase 1).
      - task: engine:invoke
        vars:
          ENGINE_CMD: 'setup:ghx --check'

  setup:ghx:
    desc: "Consent-gated ghx (brunoborges/ghx) installer (#884) -- task setup:ghx [-- --yes]"
    dir: '{{.USER_WORKING_DIR}}'
    deps:
      - task: engine:_ts-build
    # Per `conventions/task-caching.md` (#574): no `sources:` / `generates:`
    # because the script forwards user-facing flags via {{.CLI_ARGS}}
    # (notably --yes for non-interactive CI / scripted approval).
    cmds:
      - task: engine:invoke
        vars:
          ENGINE_CMD: 'setup:ghx {{.CLI_ARGS}}'

  # Release pipeline tasks (#74 + #716 safety hardening, namespace flatten #718).
  #
  # Defined inline at the root Taskfile rather than via `includes: release: ./tasks/release.yml`
  # because go-task concatenates the namespace key with each inner task name -- so an inner
  # task named `release:` under namespace `release:` would install as `task release:release`,
  # not `task release` (the documented invocation in skills/deft-directive-release/SKILL.md
  # and CHANGELOG entries from #74/#716). See #718 for the full root-cause analysis.
  #
  # Sibling pattern: tasks/pr.yml uses inner task name `check-protected-issues` (no `pr:`
  # repeat) so the include-namespace mechanism produces `pr:check-protected-issues`. The
  # release pipeline cannot use that pattern because it needs BOTH a top-level `release`
  # name AND `release:e2e` / `release:publish` / `release:rollback` sub-names.
  #
  # Per `conventions/task-caching.md` (#574): tasks that accept user-facing recovery flags
  # via {{.CLI_ARGS}} (here: --dry-run / --skip-tag / --skip-release / --allow-dirty /
  # --repo / --no-draft / --allow-low-downloads / --allow-data-loss / --force-strict-0 /
  # --owner / --destroy-repo) MUST NOT declare `sources:` / `generates:` -- the cached `cmds:`
  # skip would silently discard the recovery flag.
  #
  # Path resolution uses `{{.TASKFILE_DIR}}/scripts/<script>.py` directly because these
  # tasks live in the root Taskfile.yml where `{{.TASKFILE_DIR}}` already resolves to the
  # deft/ root (no per-subfile DEFT_ROOT joinPath dance is needed -- see Taskfile.yml
  # header comment for why `DEFT_ROOT` is intentionally absent at the root level).
  #
  # Companion scripts: scripts/release.py, scripts/release_publish.py,
  #                    scripts/release_rollback.py, scripts/release_e2e.py
  # Companion tests:   tests/cli/test_release.py,
  #                    tests/cli/test_release_publish.py,
  #                    tests/cli/test_release_rollback.py,
  #                    tests/cli/test_release_e2e.py
  # Refs #74, #233, #642, #635, #709, #710, #716, #718.
  release:
    desc: "Automate the v0.X.Y release flow (#74) -- task release -- <version> [--dry-run] [--skip-tag] [--skip-release] [--no-draft] [--allow-vbrief-drift]"
    deps: [ts:build]
    dir: '{{.USER_WORKING_DIR}}'
    cmds:
      - task: engine:invoke
        vars:
          ENGINE_CMD: 'release {{.CLI_ARGS}}'

  release:publish:
    desc: "Flip a draft GitHub release to public (#716) -- task release:publish -- <version> [--dry-run] [--repo OWNER/REPO]"
    deps: [ts:build]
    dir: '{{.USER_WORKING_DIR}}'
    cmds:
      - task: engine:invoke
        vars:
          ENGINE_CMD: 'release-publish {{.CLI_ARGS}}'

  release:rollback:
    desc: "State-aware release unwind (#716) -- task release:rollback -- <version> [--dry-run] [--allow-low-downloads N] [--allow-data-loss] [--force-strict-0]"
    deps: [ts:build]
    dir: '{{.USER_WORKING_DIR}}'
    cmds:
      - task: engine:invoke
        vars:
          ENGINE_CMD: 'release-rollback {{.CLI_ARGS}}'

  release:e2e:
    desc: "End-to-end release rehearsal against an auto-created temp repo (#716, #2572 keep+report default) -- task release:e2e [-- --dry-run] [--owner OWNER] [--destroy-repo]"
    deps: [ts:build]
    dir: '{{.USER_WORKING_DIR}}'
    cmds:
      - task: engine:invoke
        vars:
          ENGINE_CMD: 'release-e2e {{.CLI_ARGS}}'

  release:wait-npm:
    desc: "Phase 7 wait until all four npm siblings list the cut version (#4267) -- task release:wait-npm -- <version>"
    deps: [ts:build]
    dir: '{{.USER_WORKING_DIR}}'
    cmds:
      - task: engine:invoke
        vars:
          ENGINE_CMD: 'release-wait-npm {{.CLI_ARGS}}'


  # ------------------------------------------------------------------
  # Triage v1 user-facing alias tasks (#845, #913).
  #
  # The four triage fragments are namespaced under their unique include
  # keys (`triage-cache`, `triage-actions`, `triage-bulk`,
  # `triage-bootstrap`) -- a single shared `triage:` include namespace
  # is not supported by go-task v3 (two includes cannot share a key).
  # The aliases below provide the documented `task triage:<verb>`
  # user-facing surface that xBRIEFs / UPGRADING.md describe. Each alias
  # delegates to the underlying namespaced task and forwards
  # `{{.CLI_ARGS}}` so flags (`--repo`, `--reason`, etc.) reach
  # engine:invoke / packages/cli/dist. The inner tasks in each fragment are
  # `internal: true` so the fragment-namespace forms
  # (`triage-cache:cache`, `triage-actions:accept`, `triage-bulk:bulk-defer`,
  # `triage-bootstrap:bootstrap`) drop out of `task -l`; only the
  # documented `triage:*` aliases below appear in the listing. The
  # internal tasks remain CALLABLE for legacy invocations via the fragment
  # include.
  #
  # Aliases are inline at the root Taskfile rather than in a separate
  # fragment because they cross include namespaces and must exist
  # regardless of which fragments are present.
  #
  # Mirrors the established #718 release-pipeline flatten precedent
  # (release: tasks defined inline at the root Taskfile so go-task
  # does not double-prefix them).
  # ------------------------------------------------------------------
  # task triage:cache and task triage:show were removed in #883 Story 3.
  # The unified cache:* surface (cache:fetch-all, cache:get) is the
  # canonical replacement; see UPGRADING.md for migration text.

  # ------------------------------------------------------------------
  # N10 (#1150): bare-invocation `task triage` / `task scope` print a
  # categorized verb list grouped by role (Session-start, State verbs,
  # Read verbs, Lifecycle, Subscription mutation, Archive-rotation /
  # Promote-demote, Activate-complete, Reversibility, Decomposition).
  # The registry + renderer live in
  # packages/core/src/triage/help/registry-data.ts (edit in place; no
  # generator). Each existing `triage:X` / `scope:X` alias forwards
  # `--help` to the same registry via interceptHelp (handler + dispatch).
  #
  # Defined at the root Taskfile (not in a fragment) because the bare
  # target shares the `triage:` / `scope:` prefix with the documented
  # alias surface and must coexist with `triage:accept`,
  # `scope:promote`, etc. go-task v3 treats `triage` and `triage:accept`
  # as distinct task names; the same precedent established by `release`
  # + `release:publish` in this file (see #718 root-cause rationale).
  # ------------------------------------------------------------------
  triage:
    desc: "Print the categorized triage verb list (#1150 / N10). Run `task triage:<verb> --help` for usage examples."
    dir: '{{.USER_WORKING_DIR}}'
    env:
      PYTHONUTF8: "1"
    cmds:
      - task: engine:invoke
        vars:
          ENGINE_CMD: 'triage-help triage'

  scope:
    desc: "Print the categorized scope verb list (#1150 / N10). Run `task scope:<verb> --help` for usage examples."
    dir: '{{.USER_WORKING_DIR}}'
    env:
      PYTHONUTF8: "1"
    cmds:
      - task: engine:invoke
        vars:
          ENGINE_CMD: 'triage-help scope'

  triage:accept:
    desc: "Accept an issue for triage. Records an audit entry. (#845 Story 3)"
    cmds:
      - task: triage-actions:accept
        vars:
          CLI_ARGS: "{{.CLI_ARGS}}"

  triage:reject:
    desc: "Reject an issue. Closes upstream via gh + applies triage-rejected label; rolls audit back on gh failure. (#845 Story 3)"
    cmds:
      - task: triage-actions:reject
        vars:
          CLI_ARGS: "{{.CLI_ARGS}}"

  triage:defer:
    desc: "Defer an issue. Records an audit entry. (#845 Story 3)"
    cmds:
      - task: triage-actions:defer
        vars:
          CLI_ARGS: "{{.CLI_ARGS}}"

  triage:needs-ac:
    desc: "Mark an issue as needing acceptance criteria + post AC-request comment upstream. (#845 Story 3)"
    cmds:
      - task: triage-actions:needs-ac
        vars:
          CLI_ARGS: "{{.CLI_ARGS}}"

  triage:mark-duplicate:
    desc: "Link an issue as a duplicate of another (validated against Story 1 cache). (#845 Story 3)"
    cmds:
      - task: triage-actions:mark-duplicate
        vars:
          CLI_ARGS: "{{.CLI_ARGS}}"

  triage:status:
    desc: "Print the latest triage decision for an issue. Read-only. (#845 Story 3)"
    cmds:
      - task: triage-actions:status
        vars:
          CLI_ARGS: "{{.CLI_ARGS}}"

  triage:reset:
    desc: "Reset an issue's triage state. Appends a reset audit entry referencing prior; does NOT delete history. (#845 Story 3)"
    cmds:
      - task: triage-actions:reset
        vars:
          CLI_ARGS: "{{.CLI_ARGS}}"

  triage:history:
    desc: "Print the full triage timeline for an issue, ordered by timestamp. Read-only. (#845 Story 3)"
    cmds:
      - task: triage-actions:history
        vars:
          CLI_ARGS: "{{.CLI_ARGS}}"

  triage:bulk-accept:
    desc: "Bulk accept cached candidates -- task triage:bulk-accept -- --repo OWNER/NAME [--label L] [--author A] [--age-days N] [--cluster C] [--re-action] (#845 Story 4 / #915)"
    cmds:
      - task: triage-bulk:bulk-accept
        vars:
          CLI_ARGS: "{{.CLI_ARGS}}"

  triage:bulk-reject:
    desc: "Bulk reject cached candidates -- task triage:bulk-reject -- --repo OWNER/NAME --reason 'why' [--label L] [--author A] [--age-days N] [--cluster C] [--re-action] (#845 Story 4 / #915)"
    cmds:
      - task: triage-bulk:bulk-reject
        vars:
          CLI_ARGS: "{{.CLI_ARGS}}"

  triage:bulk-defer:
    desc: "Bulk defer cached candidates -- task triage:bulk-defer -- --repo OWNER/NAME [--label L] [--author A] [--age-days N] [--cluster C] [--re-action] (#845 Story 4 / #915)"
    cmds:
      - task: triage-bulk:bulk-defer
        vars:
          CLI_ARGS: "{{.CLI_ARGS}}"

  triage:bulk-needs-ac:
    desc: "Bulk needs-ac cached candidates -- task triage:bulk-needs-ac -- --repo OWNER/NAME [--label L] [--author A] [--age-days N] [--cluster C] [--re-action] (#845 Story 4 / #915)"
    cmds:
      - task: triage-bulk:bulk-needs-ac
        vars:
          CLI_ARGS: "{{.CLI_ARGS}}"

  triage:refresh-active:
    desc: "Pre-swarm freshness gate (#845 Story 4) -- detects drift between cached and live `gh issue view` for every issue referenced in vbrief/active/*.vbrief.json"
    cmds:
      - task: triage-bulk:refresh-active
        vars:
          CLI_ARGS: "{{.CLI_ARGS}}"

  triage:bootstrap:
    desc: "Run the triage v1 idempotent installer (#845 Story 6)."
    cmds:
      - task: triage-bootstrap:bootstrap
        vars:
          CLI_ARGS: "{{.CLI_ARGS}}"

  triage:reconcile:
    desc: "Self-heal the triage audit log (#1468): backfill missing `accept` decisions for proposed/pending/active vBRIEFs carrying an x-vbrief/github-issue reference, without a cache re-fetch. Idempotent. -- task triage:reconcile [-- --repo OWNER/NAME] [--dry-run] [--json]"
    cmds:
      - task: triage-reconcile:reconcile
        vars:
          CLI_ARGS: "{{.CLI_ARGS}}"

  triage:scope:
    desc: "Inspect / mutate / diff the typed plan.policy.triageScope[] subscription + triageScopeIgnores[] (#1131 / D12, #1133 / D14, #1182 / D14c). -- task triage:scope -- [--list] [--add-label=L | --add-milestone=M | --ignore-label=L] [--diff-from-upstream --repo OWNER/NAME] [--refresh-denominator --repo OWNER/NAME --count N]"
    cmds:
      - task: triage-scope:scope
        vars:
          CLI_ARGS: "{{.CLI_ARGS}}"

  triage:scope-drift:
    desc: "Detect subscription drift (#1133 / D14): unsubscribed labels/milestones on cached open issues. -- task triage:scope-drift [-- --ignore-label=L | --ignore-milestone=M]"
    cmds:
      - task: triage-scope-drift:scope-drift
        vars:
          CLI_ARGS: "{{.CLI_ARGS}}"

  triage:subscribe:
    desc: "Subscribe to a label / milestone / issue (#1133 / D14). -- task triage:subscribe -- (--label=L | --milestone=M | --issue=N)"
    cmds:
      - task: triage-subscribe:subscribe
        vars:
          CLI_ARGS: "{{.CLI_ARGS}}"

  triage:unsubscribe:
    desc: "Unsubscribe a label / milestone / issue (#1133 / D14). -- task triage:unsubscribe -- (--label=L | --milestone=M | --issue=N)"
    cmds:
      - task: triage-subscribe:unsubscribe
        vars:
          CLI_ARGS: "{{.CLI_ARGS}}"

  triage:classify:
    desc: "Inspect / validate auto-classification. --mirror is withdrawn (#4070). -- task triage:classify -- [--list | --validate]"
    cmds:
      - task: triage-classify:classify
        vars:
          CLI_ARGS: "{{.CLI_ARGS}}"

  triage:strip-withdrawn-chips:
    desc: "Remaining-set strip of withdrawn triaged / triage:* chips (#4070). -- task triage:strip-withdrawn-chips -- [--apply] [--emit-digest] [--json] [--repo OWNER/NAME]"
    cmds:
      - task: triage-classify:strip-withdrawn-chips
        vars:
          CLI_ARGS: "{{.CLI_ARGS}}"

  triage:summary:
    desc: "Emit the one-line triage state for the session-start ritual (D2 / #1122). Always exits 0; appends a JSONL record to <lifecycle-root>/.triage-cache/summary-history.jsonl. -- task triage:summary -- [--json] [--no-history]"
    cmds:
      - task: triage-summary:summary
        vars:
          CLI_ARGS: "{{.CLI_ARGS}}"

  triage:metrics:
    desc: "Trend lines from summary-history.jsonl (#1709 / D17). -- task triage:metrics -- [--window=7d|30d] [--format=text|json]"
    cmds:
      - task: triage-metrics:metrics
        vars:
          CLI_ARGS: "{{.CLI_ARGS}}"

  triage:evaluate:
    desc: "Evaluate issues off origin/master (validity + parent WIP + value). -- task triage:evaluate -- <N...> [--concurrency N] [--repo OWNER/NAME] [--json] (#3648)"
    cmds:
      - task: triage-evaluate:evaluate
        vars:
          CLI_ARGS: "{{.CLI_ARGS}}"

  triage:queue:
    desc: "Print the ranked triage queue (#1128 / D11). -- task triage:queue [-- --repo OWNER/NAME] [--limit N]"
    cmds:
      - task: triage-queue:queue
        vars:
          CLI_ARGS: "{{.CLI_ARGS}}"

  triage:show:
    desc: "Per-issue triage detail (#1128 / D11, #2890). -- task triage:show -- <N> [--format=default|operator] [--repo OWNER/NAME]"
    cmds:
      - task: triage-queue:show
        vars:
          CLI_ARGS: "{{.CLI_ARGS}}"

  triage:audit:
    desc: "Audit-log surface (#1128 / D11, #1180). -- task triage:audit [-- --format=text|json] [--vbrief-staleness] [--since=<window>] [--action=<verb>] [--repo OWNER/NAME]"
    cmds:
      - task: triage-queue:audit
        vars:
          CLI_ARGS: "{{.CLI_ARGS}}"

  # #1137 — reversible closed github-issue cache archive (distinct from cache:prune TTL hard-delete)
  triage:cache-archive:
    desc: "Reversible archive of closed github-issue cache entries (#1137). Not TTL cache:prune. -- task triage:cache-archive -- [--dry-run] [--older-than-days 30] [--repo OWNER/NAME] [--json]"
    cmds:
      - task: cache:archive-closed
        vars:
          CLI_ARGS: "{{.CLI_ARGS}}"

  triage:archive-list:
    desc: "List archived github-issue cache entries (#1137). -- task triage:archive-list -- [--repo OWNER/NAME] [--format=json] [--since ISO] [--limit N]"
    cmds:
      - task: cache:archive-list
        vars:
          CLI_ARGS: "{{.CLI_ARGS}}"

  triage:restore-from-archive:
    desc: "Restore archived github-issue entry to live cache (#1137). -- task triage:restore-from-archive -- --issue N [--repo OWNER/NAME] [--force]"
    cmds:
      - task: cache:restore-from-archive
        vars:
          CLI_ARGS: "{{.CLI_ARGS}}"

  triage:welcome:
    desc: "Run the 6-phase onboarding ritual (N3 / #1143): detect prior state, prompt subscription scope, run triage:bootstrap, prompt wipCap, offer WIP relief, print triage:summary. Idempotent. -- task triage:welcome [-- --no-subprocess]"
    cmds:
      - task: triage-welcome:welcome
        vars:
          CLI_ARGS: "{{.CLI_ARGS}}"
          DEFT_TASK_PREFIX:
            sh: |
              task_name='{{.TASK}}'
              suffix='triage:welcome'
              prefix="${task_name%$suffix}"
              if [ "$prefix" = "$task_name" ]; then
                prefix=''
              fi
              printf '%s' "$prefix"

  triage:smoketest:
    desc: "Run the N6 (#1146) end-to-end smoketest against the hermetic 20-issue fixture; exits 0 on PASS / 1 on first failure. -- task triage:smoketest [-- --verbose] [--keep-tempdir] [--cache-only]"
    cmds:
      - task: triage-smoketest:smoketest
        vars:
          CLI_ARGS: "{{.CLI_ARGS}}"

  # D15 (#1134): scope:undo audit-log reversibility verb. Delegates to
  # the standalone fragment include `scope-undo`. Reverses a single
  # audit entry by decision_id or every entry tagged with batch_id;
  # terminal actions (`complete` / `fail`) are REFUSED.
  scope:undo:
    desc: "Reverse a scope-lifecycle audit entry (#1134 / D15). -- task scope:undo -- <decision_id> | --decision-id=<uuid> | --batch-id=<uuid> [--dry-run]"
    cmds:
      - task: scope-undo:undo
        vars:
          CLI_ARGS: "{{.CLI_ARGS}}"

  # N7 (#1147): slice:record-existing backfill verb. Retrofits a
  # <lifecycle-root>/.triage-cache/slices.jsonl entry for hand-filed
  # umbrella cohorts that D13's writer (#1132) never saw because they
  # were filed via `gh issue create` / `issue_write` MCP rather than via
  # a slicing skill. Companion `slice:list` exposes the read surface.
  slice:record-existing:
    desc: "Retrofit a slices.jsonl entry for a hand-filed cohort (#1147 / N7). -- task slice:record-existing -- --umbrella=N --children=A,B,C [--wave-1=A,B] [--wave-2=C] [--actor=manual:operator] [--expected-close-signal=all-children-merged] [--sliced-at=ISO] [--notes=TEXT] [--dry-run] [--force] [--skip-validation] [--repo OWNER/NAME]"
    cmds:
      - task: slice-record:record-existing
        vars:
          CLI_ARGS: "{{.CLI_ARGS}}"

  slice:list:
    desc: "List recorded slices in <lifecycle-root>/.triage-cache/slices.jsonl (#1147 / N7). -- task slice:list [-- --json]"
    cmds:
      - task: slice-record:list
        vars:
          CLI_ARGS: "{{.CLI_ARGS}}"
