Developer-Dashboard

 view release on metacpan or  search on metacpan

README.md  view on Meta::CPAN

resolve `dashboard` correctly. It also
reads optional hook results from
`~/.developer-dashboard/cli/doctor.d` so users can layer in more
site-specific checks later.

Frequently used built-in commands such as `jq`, `yq`, `tomq`, `propq`,
`iniq`, `csvq`, `xmlq`, `of`, `open-file`, `file`, `files`, and
`workspace` are staged
privately under `~/.developer-dashboard/cli/dd/` and dispatched by
`dashboard` without polluting the global PATH. That keeps dashboard-owned
built-ins separate from user commands and hooks under
`~/.developer-dashboard/cli/`. Compatibility aliases `pjq`, `pyq`,
`ptomq`, `pjp`, and `ticket` still normalize to the current commands when
they are invoked through `dashboard`. The public switchboard now keeps the
prompt path lighter as well: once the managed helper files are already staged,
`dashboard ps1` refreshes only the requested helper, reuses one path registry
for the whole invocation, and avoids loading the suggestion and skill dispatch
stack on the ordinary prompt fast path.

Dashboard also normalizes `PERL5LIB` for its own processes before the staged
helper runtime loads. Dashboard-owned libraries stay visible, but the active
Perl core, site, and vendor directories are forced ahead of inherited
user-local shadow copies. That keeps stale dual-life XS modules such as
`Encode` from breaking helper startup, collector child commands, saved Ajax
subprocesses, or skill hooks on hosts with older local-lib artefacts.
Dashboard-managed child commands also keep the current interpreter's bin
directory plus the active shell directory at the front of `PATH`, and
collector shell commands now run through a non-login shell so macOS
shell-session restore banners and similar startup chatter do not get prefixed
onto JSON collector output. On Windows, long-lived web, collector-loop,
collector-worker, and watchdog launches now re-enter through staged private
`_dashboard-core` foreground commands instead of relying on Perl
pseudo-forking, and their runtime-state writes keep explicit replacement
fallbacks for hosts where Windows rename collisions would otherwise leave stale
or missing state files behind.

Explicit named collector stop and restart actions also pause the watchdog
supervisor for the targeted collector set while the lifecycle command is in
flight, then restore supervision for the remaining watched fleet afterwards.
That prevents the watchdog from racing a manual collector restart and spawning
another replacement loop underneath the CLI.

It provides a small ecosystem for:

- saved and transient dashboard pages built from the original bookmark-file shape
- bookmark-file syntax compatibility using the original
`:--------------------------------------------------------------------------------:` separator plus directives such as
`TITLE:`, `STASH:`, `HTML:`, and `CODE1:`
- Template Toolkit rendering for `HTML:`, with access to `stash`, `ENV`, and
`SYSTEM`
- bookmark `CODE*` execution with captured `STDOUT` rendered into the page and
captured `STDERR` rendered as visible errors
- per-page sandpit isolation so one bookmark run can share runtime
variables across `CODE*` blocks without leaking them into later page runs
- old-style root editor behavior with a free-form bookmark textarea when no path is provided
- file-backed collectors and indicators
- prompt rendering for `PS1` and the PowerShell `prompt` function
- project/path discovery helpers
- a lightweight local web interface
- action execution with trusted and safer page boundaries
- config-backed providers, path aliases, and compose overlays
- update scripts and installable runtime packaging

Managed runtime children are expected to clean up after themselves. Detached
web startup helpers, collector loops, the collector watchdog supervisor, the
SSL frontend connection workers, and background page actions now reap the
direct children they own instead of leaving zombie processes behind on hosts
such as macOS and WSL. Collector loops and the collector watchdog supervisor
also reap those children immediately on `SIGCHLD`, so long-interval
collectors and orphaned watchdogs do not leave visible `<defunct>`
dashboard processes behind until some later housekeeping pass. Managed
collectors are also watched after startup: an
unexpected exit triggers an automatic restart, while repeated crash loops are
raised as explicit `attention_required` collector state instead of silently
stopping or spinning forever.
Managed collector indicators also keep the collector array order declared in
`config/config.json` even after a live collector run rewrites its own status,
so the browser status board and `dashboard ps1` do not drift back to
alphabetical ordering after one collector refreshes.
Collector schedules now also support bounded overlap control. The default
collector `mode` is `singleton`, which means one long-running collector run
blocks the next scheduled start until the active run finishes. Set
`mode => "multiple"` to allow overlap, and use `multiple => N` to
bound how many concurrent runs of that same collector can exist at once. When
the field is omitted in `multiple` mode, the runtime defaults that bound to
`2`.

Developer Dashboard is meant to become the developer's working home:

- shared nav fragments from saved `nav/*.tt` bookmarks rendered between the top
chrome and the main page body on other saved pages
- a local dashboard page that can hold links, notes, forms, actions, and
rendered output
- a prompt layer that shows live status for the things you care about
- a command surface for opening files, jumping to known paths, querying data, and
running repeatable local tasks
- a configurable runtime that can adapt to each codebase without losing one
familiar entrypoint

## Shared Nav Fragments

If `nav/*.tt` files exist under the saved bookmark root, every non-nav page
render includes them between the top chrome and the main page body.

For the default runtime that means files such as:

- `~/.developer-dashboard/dashboards/nav/foo.tt`
- `~/.developer-dashboard/dashboards/nav/bar.tt`

And with route access such as:

- `/app/nav/foo.tt`
- `/app/nav/foo.tt/edit`
- `/app/nav/foo.tt/source`

The bookmark editor can save those nested ids directly, for example
`BOOKMARK: nav/foo.tt`. Raw TT/HTML fragment files under `nav/` also work
without bookmark wrappers, for example:

    [% index = '/app/index' %]
    <a href=[% index %]>[% index %]</a>

README.md  view on Meta::CPAN

    scalars, so CLI output and JSON-encoded query results stay stable instead of
    depending on backend-specific boolean objects.

- Private CLI Helper Assets

    Private `~/.developer-dashboard/cli/dd/` helper files provide the built-in
    command behaviour without installing generic command names into the global
    PATH. Query, open-file, workspace, path, file, and prompt commands keep
    dedicated helper bodies, while the remaining built-ins stage thin wrappers
    that hand off to a shared private `_dashboard-core` runtime.

    Only `dashboard` is intended to be the public CPAN-facing command-line
    entrypoint. The real built-in command bodies live outside `bin/dashboard`
    under `share/private-cli/`, then stage into `~/.developer-dashboard/cli/dd/`
    on demand. Generic helper names such as `workspace`, `of`, `open-file`,
    `jq`, `yq`, `tomq`, `propq`, `iniq`, `csvq`, `xmlq`, `path`,
    `paths`, `file`, and `files` are intentionally kept out of the installed
    global PATH to avoid
    polluting the wider Perl and shell ecosystem while still keeping
    dashboard-owned commands separate from user commands under
    `~/.developer-dashboard/cli/`. While those staged helpers run, their process
    title is normalized to the public `developer-dashboard ...` form so `ps`
    output shows the user-facing command instead of the staged helper path.

    `dashboard workspace` creates or reuses a tmux session for the requested
    workspace reference, seeds `WORKSPACE_REF`, keeps `TICKET_REF` for
    compatibility with older shells, refreshes plain-directory `.env` files from
    the highest ancestor down to the current directory when it creates or resumes a
    session, attaches through a dashboard-managed private helper instead of a
    public standalone binary, and completes already-open tmux session names when
    shell completion is enabled. The older `dashboard ticket` spelling remains as
    a compatibility alias.

    Passing `-c` before or after the workspace name changes directory first. When
    the workspace name is registered in the dashboard paths inventory, the same
    registered names the shell `cdr` helper resolves, the command changes into
    that registered directory before planning the session, so
    `dashboard workspace -c foobar` behaves like running `cdr foobar` followed
    by `dashboard workspace foobar`: the tmux session and its layered `.env`
    refresh both start from the registered project directory. When the name is not
    a registered dashboard path, `-c` fails with an explicit error instead of
    silently starting the workspace from the wrong directory.

- Runtime Manager

    `Developer::Dashboard::RuntimeManager` manages the background web service and
    collector lifecycle with process-title validation, numeric POSIX shutdown
    signals for Alpine/iSH compatibility, `pkill`-style fallback shutdown, and
    restart orchestration, tying the browser and prepared-state loops together as
    one runtime.

- Update Manager

    `Developer::Dashboard::UpdateManager` runs ordered update scripts and
    restarts validated collector loops when needed, giving the runtime a
    controlled bootstrap and upgrade path.

- Docker Compose Resolver

    `Developer::Dashboard::DockerCompose` resolves project-aware compose files,
    explicit overlay layers, services, addons, modes, env injection, and the
    final `docker compose` command so container workflows can live inside the
    same dashboard ecosystem instead of in separate wrapper scripts.

## Environment Variables

The distribution supports these compatibility-style customization variables:

- `DEVELOPER_DASHBOARD_BOOKMARKS`

    Override the saved page root.

- `DEVELOPER_DASHBOARD_CHECKERS`

    Filter enabled collector/checker names.

- `DEVELOPER_DASHBOARD_CONFIGS`

    Override the config root.

- `DEVELOPER_DASHBOARD_ALLOW_TRANSIENT_URLS`

    Allow browser execution of transient `/?token=...`, `/action?atoken=...`,
    and older `/ajax?token=...` payloads. The default is off, so the web UI only
    executes saved bookmark files unless this is set to a truthy value such as
    `1`, `true`, `yes`, or `on`.

## Transient Web Token Policy

Transient page tokens still exist for CLI workflows such as `dashboard page encode`
and `dashboard page decode`, but browser routes that execute a transient payload
from `token=` or `atoken=` are disabled by default.

That means links such as:

- `http://127.0.0.1:7890/?token=...`
- `http://127.0.0.1:7890/action?atoken=...`
- `http://127.0.0.1:7890/ajax?token=...`

return a `403` unless `DEVELOPER_DASHBOARD_ALLOW_TRANSIENT_URLS` is enabled.
Saved bookmark-file routes such as `/app/index` and
`/app/index/action/...` continue to work without that flag. Saved bookmark
editor pages also stay on their named `/app/<id>/edit` and
`/app/<id>` routes when you save from the browser, so editing an
existing bookmark file does not fall back to transient `token=` URLs under the
default deny policy.

`Ajax` helper calls inside saved bookmark `CODE*` blocks should use
an explicit `file => 'name.json'` argument. When a saved page supplies that
name, the helper stores the Ajax Perl code under the saved dashboard ajax tree and emits a
stable saved-bookmark endpoint such as
`/ajax/name.json?type=text`. Skill pages use the same helper contract. Without
extra skill route metadata the generated saved endpoint is namespaced under the
longest matching skill route, for example
`/ajax/example-skill/name.json?type=text` or
`/ajax/example-skill/sub-skill/name.json?type=text`. The runtime config tree
and installed skills can both ship `config/routes.json` to declare canonical
custom paths for normal saved app pages, skill-local app pages, Ajax handlers,
JavaScript assets, CSS assets, and other public assets. The schema is a JSON
object whose keys are the public custom paths and whose values are either one
smart local route string or an object with `to` plus an optional `type`, for

README.md  view on Meta::CPAN

installed-skill API fragments that contribute saved Ajax machine auth. Updates
never rewrite installed skill files; they only change the writable runtime
layer for the current working context. List, add, and remove actions print
human-readable tables by default; use `-o json` when you need the full raw
machine payload.

When you pass `--secret`, the raw secret is hashed to a SHA-256 hex digest
before it is stored. `--maybe-secret` is the route-friendly alias for the
same raw secret input: if the key is missing it creates the group, and if the
key already exists it overwrites the stored secret while the command updates
the requested routes. The saved JSON keeps the digest under `secret` plus the
exact allowed saved Ajax routes under `ajax`. `dashboard api add` accepts
one or more repeated `--route` flags, so one command can create the key,
hash the secret, and register multiple exact routes at once. Adding the same
route twice is a no-op. Removing an inherited API group from a deeper child
layer writes a child-layer tombstone so the parent definition is hidden
without editing the parent file.

## Working With Pages

Create a starter page document:

    dashboard page new sample "Sample Page"

Save a page:

    dashboard page new sample "Sample Page" | dashboard page save sample

List saved pages:

    dashboard page list

Render a saved page:

    dashboard page render sample

`dashboard page render` now uses the same page-runtime preparation path as
the browser route, so saved bookmark TT such as `[% title %]` and
`[% stash.foo %]` is rendered there too instead of only working under
`/app/<id>`.

Encode and decode transient pages:

    dashboard page show sample | dashboard page encode
    dashboard page show sample | dashboard page encode | dashboard page decode

Run a page action:

    dashboard action run system-status paths

Bookmark documents use the original separator-line format with directive
headers such as `TITLE:`, `STASH:`, `HTML:`, and `CODE1:`.

Posting a bookmark document with `BOOKMARK: some-id` back through the root
editor now saves it to the bookmark store so `/app/some-id` resolves it
immediately.

The browser editor now loads one visible block per bookmark section while it
keeps the canonical separator-based bookmark source in a hidden form field for
saves and play requests. Each visible block still uses the syntax-highlighted
overlay viewport that follows the real textarea scroll position by transform
instead of a second scrollbox, so long lines, full-text selection, and caret
placement stay aligned with the real editor. Pressing `Tab` inside one block
starts the next section block, and the browser recomposes those blocks back
into the original separator-line bookmark document before it posts or plays
the page. The directive assist still understands `:---` when you paste older
single-textarea workflows into one block, expanding it to the full separator
line and seeding the next sensible directive.

Edit and source views preserve raw Template Toolkit placeholders inside
`HTML:` sections, so values such as `[% title %]` are kept in the bookmark
source instead of being rewritten to rendered HTML after a browser save.
Template Toolkit rendering exposes the page title as `title`, so a bookmark
with `TITLE: Sample Dashboard` can reference it directly inside `HTML:`
with `[% title %]`. Transient play and view-source links are also encoded
from the raw bookmark instruction text when it is available, so
`[% stash.foo %]` stays in source views instead of being baked into the
rendered scalar value after a render pass.

Earlier `CODE*` blocks now run before Template Toolkit rendering during
`prepare_page`, so a block such as `CODE1: { a =` 1 }> can feed
`[% stash.a %]` in the page body. Returned hash and array values are also
dumped into the runtime output area, so `CODE1: { a =` 1 }> both populates
stash and shows the bookmark-style dumped value below the rendered page body.
The `hide` helper no longer discards already-printed STDOUT, so
`CODE2: hide print $a` keeps the printed value while suppressing the Perl
return value from affecting later merge logic.

Page `TITLE:` values only populate the HTML `<title>` element. If a
bookmark should show its title in the page body, add it explicitly inside
`HTML:`, for example with `[% title %]`.

`/apps` redirects to `/app/index`, and `/app/<name>` can load
either a saved bookmark document, a saved ajax/url bookmark file, or an
installed skill index page when the smart route resolves to a skill.

## Working With Collectors

Ensure the home config file exists without seeding collectors:

    dashboard config init

If `config/config.json` is missing, that command creates it as:

    {}

It does not inject an example collector, and if the file already exists it is
left untouched.

List collector status:

    dashboard collector list
    dashboard collector status shell.example

Inspect collector logs:

    dashboard collector log
    dashboard collector log shell.example

`dashboard collector log` prints the known collector log streams.
`dashboard collector log <name>` prints one collector transcript.

README.md  view on Meta::CPAN

`{"a":123}` on `stdout`; the runner decodes that JSON into Perl data and
renders `[% a %]` into the live icon `123`. Later config-sync passes keep
the configured `icon_template` metadata and the already-rendered live
`icon`, so commands such as `dashboard indicator list` and `dashboard ps1`
do not revert the persisted icon back to raw `[% ... %]` text between runs.
The blank-environment integration flow also keeps a regression for mixed
collector health: one intentionally broken Perl collector must stay red
without stopping a second healthy collector from staying green in
`dashboard indicator list`, `dashboard ps1`, and `/system/status`.

## Docker Compose

Inspect the resolved compose stack without running Docker:

    dashboard docker compose --dry-run config

Include addons or modes:

    dashboard docker compose --addon mailhog --mode dev up -d
    dashboard docker compose config green
    dashboard docker compose config
    dashboard docker list
    dashboard docker list --disabled
    dashboard docker list --enabled
    dashboard docker disable green
    dashboard docker enable green

The resolver also supports isolated service folders without adding entries to
dashboard JSON config. If
`./.developer-dashboard/config/docker/green/compose.yml` exists in the current
project it wins; otherwise the resolver falls back to
`~/.developer-dashboard/config/docker/green/compose.yml`.
`dashboard docker compose config green` or
`dashboard docker compose up green` will pick it up automatically by
inferring service names from the passthrough compose args before the real
`docker compose` command is assembled. If no service name is passed, the
resolver scans isolated service folders and preloads every non-disabled folder.
If a folder contains `disabled.yml` it is skipped. Each isolated folder
contributes `development.compose.yml` when present, otherwise `compose.yml`.
To toggle that marker without creating or deleting the file manually, use
`dashboard docker disable <service>` or
`dashboard docker enable <service>`. The toggle writes to the
deepest runtime `config/docker` root, so a child project layer can locally
disable an inherited home service by creating
`./.developer-dashboard/config/docker/<service>/disabled.yml` and can
re-enable it again by removing that same local marker.
To inspect the effective marker state without walking the folders manually,
use `dashboard docker list`. Add `--disabled` to show only disabled
services or `--enabled` to show only enabled services.

During compose execution the dashboard exports `DDDC` as the runtime
`config/docker` directory for the current runtime, so compose YAML can keep using
`${DDDC}` paths inside the YAML itself. Project-local isolated services are
discovered from that same
`./.developer-dashboard/config/docker/<service>/...` tree. Wrapper flags such as
`--service`, `--addon`, `--mode`, `--project`, and `--dry-run` are
consumed first, and all remaining docker compose flags such as `-d` and
`--build` pass straight through to the real `docker compose` command.
If one resolved service comes from an installed skill docker root, the
resolver also loads that skill's `<skill-root>/.env` file into the compose
environment before docker-config, addon, and mode env overlays are applied.
Only skills whose compose service files actually participate are included,
disabled skills are skipped, and `<skill-root>/.env.pl` is not executed from
this compose path. Nested skill services expand their env chain from the root
nested skill to the participating leaf service, preserving overwritten parent
keys under cumulative aliases such as `foo_VERSION` and
`foo_bar_VERSION` before the leaf value becomes the plain key. The resolver
also exports one skill-specific `<skill-name>_DDDC` variable for each
participating skill, using the leaf skill name with non-identifier characters
normalized to underscores and pointing that variable at the owning
`config/docker/` root. Nested skill services additionally export the full
cumulative skill path alias such as `foo_bar_zzz_DDDC` for the same compose
root, while the leaf alias stays available as `zzz_DDDC`.
When `--dry-run` is omitted, the dashboard hands off with `exec` so the
terminal sees the normal streaming output from `docker compose` itself
instead of a dashboard JSON wrapper.

## Prompt Integration

Render prompt text directly:

    dashboard ps1 --jobs 2

`dashboard ps1` now follows the original `~/bin/ps1` shape more closely: a
`(YYYY-MM-DD HH:MM:SS)` timestamp prefix, dashboard status and workspace info, a
bracketed working directory, an optional jobs suffix, and a trailing
`🌿branch` marker when git metadata is available. The prompt helper reads the
branch directly from on-disk git metadata instead of shelling out to
`git branch`, so repeated prompt renders stay lightweight on slower hosts such
as iSH. If the workspace workflow seeded `WORKSPACE_REF` or the older
`TICKET_REF` into the current tmux session, `dashboard ps1` also reads that
context from tmux when the shell environment does not already export it, but it
skips that tmux probe entirely when the shell is not inside tmux.

Generate shell bootstrap:

    dashboard shell bash
    dashboard shell zsh
    dashboard shell sh
    dashboard shell ps

The generated shell helper keeps the same bookmark-aware `cdr`, `dd_cdr`,
`d2`,
and `which_dir` functions across all supported shells. `cdr` first tries a
saved alias, then falls back to an AND-matched directory search beneath the
alias root or the current directory depending on whether that first argument
was a known alias. One match changes directory, multiple matches print the
list, and `which_dir` prints the same selected target or match list without
changing directory. Bash still uses `\j` for job counts, zsh refreshes
The shell-smoke regression coverage also compares those printed paths by
canonical identity, so macOS `/var/...` and `/private/var/...` aliases do
not fail equivalent `pwd` / `which_dir` checks. Bash still uses `\j` for
job counts, zsh refreshes
`PS1` through a `precmd` hook with `${#jobstates}`, POSIX `sh` falls back
to a prompt command that does not depend on bash-only prompt escapes, and
PowerShell installs a `prompt` function instead of using the POSIX `PS1`
variable.

`d2` is the short shell shortcut for `dashboard`, so after loading the
bootstrap you can run `d2 version`, `d2 doctor`, or
`d2 docker compose ps` without typing the full command name each time.

README.md  view on Meta::CPAN

pid and an accepting listener on the requested port. Restart now also reuses
the saved listener port to recover the real serving pid when the web process
has renamed itself into the underlying `starman master` form, so container
restarts still own and replace the active listener instead of losing control
after startup. On Linux hosts that are also running Developer Dashboard inside
Docker containers, managed stop and restart paths now reject sibling runtime
pids that live in a different Linux pid namespace, so a host-side restart does
not accidentally kill or adopt a container-owned web listener or collector
loop
- `dashboard restart web` only restarts the managed web service
- `dashboard restart collector` only restarts managed collector loops
- `dashboard restart collector <name>` only restarts the requested
collector loop, including an on-demand manual collector by converting it into
a managed interval loop, and collector-name shell completion suggests
registered collector names
- managed collector loops also run under a watchdog supervisor; if a loop dies
unexpectedly after startup, the watchdog restarts it automatically, records
the restart attempt in collector status/logs, and after too many crashes
inside the watchdog window marks the collector `attention_required` so the
operator sees an explicit problem instead of infinite silent restart churn
- `dashboard log` and `dashboard logs` print the combined dashboard web log
plus collector logs
- `dashboard log web` prints only the dashboard web log and still supports
`-n` and `-f`
- `dashboard log collector` prints only collector logs
- `dashboard log collector <name>` prints only the requested collector
log, and collector-name shell completion suggests registered collector names
- interactive restart and stop task boards mark the active step with a blue
`-`, stream active detail lines in blue, mark completed steps with a green
`[OK]`, mark failed steps with a red `[X]` plus red failure detail lines,
keep the final table or JSON summary on `stdout`, and use numeric POSIX
shutdown signals so minimal Alpine/iSH Perl builds that reject `TERM` by
name still terminate managed web and collector processes correctly
- web shutdown and duplicate detection do not trust pid files alone; they validate managed processes by environment marker or process title and use a `pkill`-style scan fallback when needed

## Environment Customization

After installing with `cpanm`, the runtime can be customized with these environment variables:

- `DEVELOPER_DASHBOARD_BOOKMARKS`

    Overrides the saved page or bookmark directory.

- `DEVELOPER_DASHBOARD_CHECKERS`

    Limits enabled collector or checker jobs to a colon-separated list of names.

- `DEVELOPER_DASHBOARD_CONFIGS`

    Overrides the config directory.

- `DEVELOPER_DASHBOARD_ALLOW_TRANSIENT_URLS`

    Allows browser execution of transient `/?token=...`, `/action?atoken=...`,
    and older `/ajax?token=...` payloads. The default is off, so the web UI only
    executes saved bookmark files unless this is set to a truthy value such as
    `1`, `true`, `yes`, or `on`.

Collector definitions come only from dashboard configuration JSON, so config
remains the single source of truth for path aliases, providers, collectors,
and Docker compose overlays.

## Testing And Coverage

Run the test suite:

    prove -lr t

Measure library coverage with Devel::Cover:

    cpanm --no-wget --notest --local-lib-contained ./.perl5 Devel::Cover
    export PERL5LIB="$PWD/.perl5/lib/perl5${PERL5LIB:+:$PERL5LIB}"
    export PATH="$PWD/.perl5/bin:$PATH"
    cover -delete
    HARNESS_PERL_SWITCHES=-MDevel::Cover prove -lr t
    PERL5OPT=-MDevel::Cover prove -lr t
    cover -report text -select_re '^lib/' -coverage statement -coverage subroutine

The repository target is 100% statement and subroutine coverage for `lib/`.
This is a standing QA gate for every change, not only releases. After the
normal `prove -lr t` test gate passes, run the numeric `Devel::Cover` gate
and do not treat the work as done until the `cover` summary still reports
100% statement and 100% subroutine coverage for `lib/`.
GitHub workflow coverage gates must match the `Devel::Cover` `Total` summary
line by regex rather than one fixed-width spacing layout, because runner or
module upgrades can change column padding without changing the real
`100.0 / 100.0 / 100.0` result.
The tag-driven GitHub release workflow must also install `Devel::Cover`
before it runs that numeric coverage gate, or the signed-release path can
fail before any release assets are published.
The GitHub release tag path is intentionally decoupled from the repository's
GitHub Actions CPAN upload workflow. Tag pushes in the form `vX.XX` are for
the signed GitHub release path, while any GitHub-hosted CPAN upload remains a
manual `workflow_dispatch` action so an ordinary release tag cannot perform an
unasked PAUSE upload behind the operator's back.

The coverage-closure suite includes managed collector loop start/stop paths
under `Devel::Cover`, including wrapped fork coverage in
`t/14-coverage-closure-extra.t`, so the covered run stays green without
breaking TAP from daemon-style child processes.
The `t/07-core-units.t` collector loop guard treats both
`HARNESS_PERL_SWITCHES` and `PERL5OPT` as valid `Devel::Cover` signals,
because this machine uses both launch styles during verification.
The runtime-manager coverage cases also use bounded child reaping for stubborn
process shutdown scenarios, so `Devel::Cover` runs do not stall indefinitely
after the escalation path has already been exercised.
The focused skill regression in `t/19-skill-system.t` now also exercises
`PathRegistry::installed_skill_docker_roots()` directly, including the
default enabled-only view and the explicit `include_disabled => 1` path,
so skill docker layering changes do not silently pull the `lib/` total below
the required `100.0 / 100.0 / 100.0`.
The focused web coverage suites must also call low-traffic
`Developer::Dashboard::Web::App` compatibility helpers directly when their
other execution path would only be indirect route fan-out. That is now part
of the reviewed contract for the `/apps` redirect helper, the singleton stop
helper, the shipped shim asset responses, and the installed-distribution
`File::ShareDir` asset-root fallback, so GitHub-hosted `Devel::Cover` runs
cannot drift below the local `100.0 / 100.0 / 100.0` result for
`lib/Developer/Dashboard/Web/App.pm`.
The packaged `t/09-runtime-manager.t` fallback assertions also stub ambient
managed-web discovery explicitly, so tarball and PAUSE installs do not get



( run in 0.590 second using v1.01-cache-2.11-cpan-df04353d9ac )