4.30  2026-09-05

  - DD-764: a stored session record whose expires_at was absent, empty or "0"
    was accepted as valid forever by SessionStore::from_cookie and skipped by
    sweep_expired, so the record was both immortal and uncollectable - while a
    malformed timestamp already failed closed, because _iso8601_to_epoch
    returns 0 for anything not matching its strict ISO-8601 pattern. Both
    truthiness guards are gone, so an absent expiry now compares the same way
    malformed input already did: the session is rejected and deleted on use,
    and the file is collected by the sweep. create() always writes expires_at,
    so reaching this needed a record written outside the product's own path -
    a hand-edit, a restore, or an older schema - which makes it a latent
    fail-open on a session bearer credential rather than an exploitable bug.
  - DD-737: a collectors config entry carrying only a name and an interval (no
    'command' and no 'code') was forked into a loop worker anyway. That worker
    died on its very first tick with "Collector <name> missing command or
    code" and on every tick after that, forever, without ever disabling
    itself, leaving an unreaped process behind each time it died on a host
    whose PID 1 does not reap orphans. Reproduced in the project's container
    image, where two `dashboard restart` cycles left three or more defunct
    "dashboard collector" entries that later restarts never cleared.
    start_loop now runs the same command/code validation _collector_source
    already performed per tick, before writing a pidfile or forking, so a
    misconfigured collector fails immediately with that error and spawns no
    process.
  - DD-753: RuntimeManager launched PowerShell by hardcoding the bare string
    'powershell' at four call sites, while CollectorRunner did the same job
    through a resolver that tries PATH under both spellings, then the
    SystemRoot install path, and names the missing executable when none
    resolves. On a Windows host where PowerShell is not on PATH, the collector
    said what was wrong and the dashboard runtime failed through system() into
    an empty capture, indistinguishable from a legitimately empty result. The
    resolver now lives once in ProcessSupervision and is used by both modules,
    so all four runtime call sites resolve PowerShell the same way, and each
    reports the missing executable in the shape its own contract requires - a
    status pair, a die, or an empty list. Verified on Linux only: no Windows
    guest was available, and the card records the platform verification as
    still owed.
  - DD-738: d2()'s AUTOLOAD handled only a single word and shelled out
    immediately, returning a plain string, so a second method call on it -
    d2()->collector->list - died as a bareword class lookup. A bareword method
    now starts a lazy proxy that accumulates dotted segments at any depth,
    mirroring the CLI's own dotted dispatch, and nothing executes until the
    chain is terminated with a call: d2()->foo->bar->() runs `dashboard
    foo.bar`. An un-terminated chain is inert in boolean, numeric and string
    context and stringifies to something obviously non-executing ("d2 proxy:
    foo.bar"), so a stray debug print cannot run a command; underscores in a
    segment are rewritten to hyphens, since a Perl method name cannot carry a
    hyphen and the hyphenated half of the CLI was otherwise unreachable
    through the chained form. INCOMPATIBLE CHANGE, and it reaches the
    single-word form too:
    d2->doctor now needs the trailing ->() to execute. d2->run(...), which
    takes its words as separate arguments and passes them through verbatim, is
    unaffected.
  - DD-616: Developer::Dashboard::File and Developer::Dashboard::Folder each
    carried their own copy of the computation that builds the configured-alias
    cache key, so the key and the runtime roots it is derived from could drift
    apart. Both now delegate to a single
    Developer::Dashboard::PathRegistry::alias_cache_key, on the registry that
    owns those roots. Of the five sub names the two modules share across 45
    lines, none was byte-identical, and the AUTOLOAD and
    configured-alias-loading pairs were deliberately left alone because they
    encode a real difference - Folder creates the directory it resolves and
    File must not create the file it resolves. Behaviour of the installed
    product is unchanged.
  - DD-669: nine subroutines shared a name between RuntimeManager and
    CollectorRunner while having different bodies, so the name asserted a
    sameness that did not hold and no copy-paste detector could see it. Four
    are now single implementations in ProcessSupervision -
    _helper_file_supports_internal_command, _same_pid_namespace
    (RuntimeManager's _current_pid_namespace_id became dead and was deleted),
    _close_inherited_fds (a union, since RuntimeManager's preserve_harness
    guard is a strict superset) and _dashboard_core_helper_path (its command
    argument is now required, the two copies having differed only in a default
    no caller in lib/ reached) - for a net -33 lines in lib/: 155 duplicated
    lines removed, 114 shared added. The remaining five keep their per-class
    names with the reason recorded beside each, including two that are
    interface contracts the shared helpers dispatch through and one that
    diverges because RuntimeManager's side is defective (tracked separately as
    DD-753). t/160 now derives the divergent set from source and compares it
    against the declared set, so both a new divergence and a
    resolved-but-still-declared one fail.
  - DD-656: tests that chmod a file unreadable and then assert the operation
    is denied cannot observe a denial when the running process is root, so a
    container run of the suite reported seven of them as product failures when
    the mismatch was in the environment, not the code. A new gate,
    t/168-permission-assertion-guards.t, scans every test file for that shape
    and was RED at 62 sites across 26 files; each site now probes whether a
    denial is actually observable - by attempting the operation after the
    chmod - and skips with a named reason when it is not, replacing earlier
    guards that tested process identity instead. A follow-up run on
    ubuntu:24.04, debian:stable-slim and the project image measured that -r
    still answers true for uid 0 where the open is denied, and that the
    `filetest` pragma named as the alternative is absent from stock perl on
    Ubuntu and Debian, so attempting the operation is the form kept.
    Test-suite work only; no change to shipped code.
  - DD-725: t/97-pathregistry-coverage.t deliberately writes to /dev/full to
    exercise a failed flush, but unlike t/103 and t/89 it carried no tolerance
    for the resulting "unable to close filehandle ... No space left on device"
    warning, so every full-suite run on a host with /dev/full leaked it to
    stderr. That block now carries a scoped local $SIG{__WARN__} matching
    exactly that text - both the close-filehandle phrase and the device clause
    - and rethrowing anything else. Measured before and after on the same
    tree: 164 tests and 0 failures both times, stderr lines 1 to 0, with the
    ENOSPC assertion present and not skipped in both runs. No library code
    changed: the warning is perl's own notice about an implicit close during
    die-unwind, not a defect in PathRegistry.
  - DD-732: t/35-js-fast-check.t asserted npm ci's exit code inside the
    `if ( !-d node_modules )` branch that performed the install, so the file
    planned 1..4 on the first run in a tree and 1..3 on every run afterwards -
    the same file at the same commit reporting a different count depending on
    a directory it had created itself. That made the whole suite's total a
    property of the working tree rather than of the commit, presenting as
    flakiness (17540 once and 17539 four times on one unchanged tree) and
    hiding any genuine loss of a single test in the noise. The install stays
    conditional, but the assertion now runs every time and checks the
    postcondition - that the dependency tree is present - which also catches
    an install that exits 0 and produces nothing. Verified from both states,
    with node_modules present and with it removed.
  - DD-748: in a container without Devel::Cover,
    t/148-coverage-gate-entrypoint.t failed 29 of 50 assertions and
    t/151-coverage-gate-launch-boundary.t died after 3 of a planned 9, so a
    missing optional tool was reported as a test failure rather than an absent
    prerequisite. Both files now detect Devel::Cover's absence before any plan
    is declared and skip_all with a reason naming the module, following
    t/138-coverage-exec-truncation.t which already did this. A new
    t/170-optional-tooling-skips.t masks the module with an @INC hook in a
    child process and asserts all three specs skip, name the module, exit 0
    and abandon no plan - while confirming t/148 and t/151 still run all 59
    assertions when Devel::Cover is present, so a guard that always skipped
    would be caught.
  - DD-766: the behavioural tarball-exclusion block in
    t/15-release-metadata.t compiled all 17 exclude_match patterns out of
    dist.ini but applied them to a hardcoded list of 8 sample paths, so 9
    patterns were compiled, exercised by nothing, and passed silently. Nothing
    leaked, but a deleted dist.ini line would not have been noticed. Sample
    paths were added for those nine, and a loop now asserts that every
    compiled exclusion is matched by at least one sample, so an exclude_match
    added to dist.ini without a sample fails instead of quietly reducing what
    the block covers. Added alone, the loop failed for exactly nine patterns;
    with the samples it reports 17 of 17 exercised, and the pre-existing
    must_be_shipped assertions are unchanged at 7. Falsified by adding a
    throwaway exclusion, confirming it went red, and removing it again.
  - DD-741: CodeQL refuses to run unless its init, autobuild and analyze steps
    use the same version, but Dependabot opens one pull request per action
    path - so each bump moved a single step, stranded the other two, and every
    pull request failed the required "analyze (javascript-typescript)" check
    with "Loaded a configuration file for version X, but running version Y".
    It happened three times (4.37.6 to 4.37.7, 4.37.7 to 4.37.8 and 4.37.8 to
    4.37.9, as PRs 48/49/50) before being fixed at the cause. A Dependabot
    group scoped to "github/codeql-action*" now makes the family arrive as a
    single pull request that can pass; the pattern is deliberately
    family-scoped rather than a blanket wildcard, which would batch every
    action update so one failure blocked the rest.
    t/171-dependabot-grouping.t guards the block and was falsified in three
    directions - removing the block, narrowing it to one path, and widening it
    to "*" - before being trusted.
  - DD-655: a recorded gate verdict stored only the HEAD tree hash, so
    gate-status could tell that a verdict described an older commit but not
    that it described different uncommitted work - measured on DD-652, where
    the sandbox's HEAD tree had not moved all night while all six files the
    card changed hashed to something that tree does not describe. coverage-run
    now records a working-state fingerprint beside the tree hash - git status
    --porcelain --untracked-files=no plus git diff HEAD for uncommitted
    tracked work, and every file under the operator tools directory by
    relative path and content for the git-ignored tooling - and gate-status
    compares it with the same expression, so "a different commit" and "the
    same commit with different uncommitted work" are told apart, while a
    fingerprint that cannot be computed reports unknown rather than clean. The
    rejected alternatives are recorded with their reasons: git stash create
    captures tracked modifications only, and hashing the whole working tree
    invalidates on every unrelated edit. A follow-up closed a blind spot in
    the walk itself - a filename containing a newline split into two unreadable
    non-paths, letting its content change without moving the digest.
  - DD-696: host exclusivity between gate runs was enforced on only one side:
    coverage-run serialises by delegating to script/coverage-gate's flock,
    while run-suite exec'd prove directly and took no lock at all - measured
    by starting two full suites twelve seconds apart, both of which ran and
    the second printed no refusal. run-suite now takes an exclusive lock
    immediately before launching prove, refuses naming the holder rather than
    waiting, and scopes the lock to the prove run only; script/coverage-gate
    was deliberately left unchanged here, with that half tracked separately as
    DD-734.
  - DD-734: the suite runner has held a host-wide lock since DD-696, while
    script/coverage-gate locked only a file named after its coverage database
    (DD-526) - two different files, so neither tool could ever refuse the
    other, and a suite starting mid-coverage silently invalidated the coverage
    verdict. The gate now also takes the same host lock the suite runner uses
    (DD_SUITE_LOCK, default /tmp/dd-gate-host.lock), appended to the database
    lock rather than replacing it, so two gates given different --database
    paths still do not refuse each other. Because the suite itself invokes the
    gate in nine test files, the host lock is skipped when HARNESS_ACTIVE is
    set, so the gate cannot be refused by the suite that is running it; a
    follow-up fixed the lock test, whose real-run cases had inherited
    HARNESS_ACTIVE from prove and were exercising the guard instead of the
    lock. t/146-coverage-gate-lock.t grew from 9 tests to 15.
  - DD-744: script/coverage-gate derived its repository from its own file
    location and chdir'd there, so every relative path after that - the
    cover_db database and the t/ test directory - resolved in the script's own
    tree; invoking one checkout's copy from a different working directory ran
    that first tree's suite and deleted and rebuilt its coverage database,
    while the run succeeded and named neither tree. The gate now refuses when
    both the working directory and its own repository are git checkouts and
    they differ, naming both absolute paths, and it reports the resolved
    absolute database instead of the configured relative name that was
    byte-identical whichever tree it landed in. The refusal condition is
    deliberately narrow so the existing specs' temporary non-checkout
    repositories still run, and the two roots are compared case-insensitively
    where the filesystem is case-tolerant so the guard cannot falsely refuse
    in its own checkout on Windows or a case-tolerant macOS volume. New spec
    t/169-gate-target-resolution.t covers the refusal, the proceed case, and
    the absolute database reporting.
  - DD-729: host-readiness waiting was rewritten from scratch in each session
    that needed it, and the copies disagreed about what "foreign" means: the
    in-run sampler counted only a Devel::Cover process while the ad-hoc
    pre-run waits also matched a foreign workspace path, so a run could be
    launched as clear by one definition and then classified against the other.
    There is now a single readiness waiter with a spec, and both mechanisms
    obtain one definition rather than each carrying a copy. Two recorded
    claims were corrected by measurement: the path-based definition returns 19
    processes on this host against 2 for the harness-based one - 18 of them a
    policy bridge, a `tail -F` and another project's watcher - so a wait using
    it would never launch, and every contention figure recorded before the
    definition change now carries a caveat that it cannot be pooled with
    anything measured after it.
  - DD-746: the suite runner's in-run sampler counted only Devel::Cover
    processes, so a competing plain prove was never counted - measured side by
    side, a /proc scan saw one foreign prove at the same moment the runner
    wrote FOREIGN_PEAK=0, a marker that reads as "the host was empty".
    Widening the pattern under DD-729 reversed the defect rather than fixing
    it: the verdict still called what it saw "a foreign coverage run" that
    "held the host", while the grep now matched any foreign test or coverage
    process and being seen in a sample is not evidence of holding anything.
    The CONTENDED and SLOWED lines now read "a foreign test or coverage
    process was seen in N of M sampled windows (peak P)", and the marker line
    carries FOREIGN_PATTERN so every recorded figure states the definition
    that produced it - without which the eleven figures gathered under the old
    narrow pattern cannot be pooled with new ones.
  - DD-727: the gate-status reporter decided whether a suite verdict stood
    from SUITE_EXIT and GATE_EXIT alone, and read none of the contention
    markers run-suite writes into the same log - a grep for CONTENDED, SLOWED,
    FOREIGN_PEAK, FOREIGN_SAMPLES and WINDOW_SAMPLES across the whole tool
    returned zero matches - so a log whose own line read "this verdict
    competed for the host and does not stand" was reported as a finished gate,
    in the reassuring direction. Corroborated four times on the card (DD-682
    at 49 of 55 contended windows, DD-733 at 50 of 56, and both of DD-744's
    runs at 34 of 44 and 35 of 45), all of which passed and all of which were
    discarded by hand. The reporter now carries run-suite's own classification
    rather than re-deriving the threshold in a second place: a CONTENDED line
    overrides SUITE_EXIT and the verdict is reported as not standing, a SLOWED
    line is recorded without invalidating, and the three states - passed, did
    not stand, could not look - stay distinct, so broken code remains
    distinguishable from inadmissible evidence.
  - DD-750: the coverage tool's host-contention sampler excluded only its own
    process group, while the gate it launches runs under setsid in a different
    one - and a coverage run's children are Devel::Cover processes by
    construction, so the count was inflated in every window of every real run
    and no coverage verdict on this host could come out uncontended. Measured
    with a control pair at identical ambient load, a stand-in gate spawning no
    Devel::Cover-shaped child gave FOREIGN_PEAK=6 and one spawning a single
    child gave 7; after the fix both read 2. Rather than patch it in place,
    the private copy of the predicate was replaced by a call to the shared
    definition, which also supplies four protections that copy had never
    received: the process-group exclusion list, the quoted-pattern grep, the
    pattern-compile check and the empty-process-table check.
  - DD-761: the Docker image the platform gate runs against was built FROM
    itself and tagged back to the same name, so rebuilds layered on the
    previous build instead of starting clean, and a checked-in tarball that
    nothing regenerated left the image three weeks behind master.
    String::Compare::ConstantTime - declared as a runtime dependency in
    cpanfile, dist.ini and Makefile.PL, and loaded by Auth.pm at compile time
    - was absent from the image, so every path through Auth.pm died there and
    `dashboard encode`/`decode` exited 2 for every input; the failure first
    surfaced as a plausible-looking empty-string property-test
    counterexample, and only a control run (encode("hello") failing
    identically) showed the module was never loading at all. The image build
    now pins a real base, regenerates its tarball from master, and fails at
    build time when any dependency cpanfile declares cannot load. No shipped
    code changed: the dependency was always declared correctly, and the fix
    lives in the operator's local image-build directory, which the
    distribution excludes.
  - DD-682: the scheduled repository hunters fingerprinted a finding's whole
    item set, so any change of membership refiled every member: 28 filed cards
    carried only 11 distinct findings, 17 of them redundant refs, with
    "modules over 1500 lines" filed seven times in three days. Card titles
    also carried the item count, so one finding appeared as "(6)" and "(7)"
    and no title match could tell they were the same. Findings are now
    fingerprinted per item and only unreported items are filed, titles carry
    no count, and a new --report-all mode empties the ledger for the run so a
    card's recorded reproduce command prints the finding instead of "0 card(s)
    raised". Operator-local board automation only; no shipped code was
    touched.
  - DD-733: the repository's internal "what next" helper encoded a picture of
    the board that had drifted in four measured ways: it offered a card the
    owner had explicitly deferred through two answered-and-marked questions,
    never proposed work from the buglist, new-enhancements or backlog columns,
    was blind to the researching, analysing and documentation columns so its
    own "never start something new while something is claimed" rule could not
    fire, and asserted every backlog card was parked on a condition when 24 of
    27 carried none. It now consults per-card policy declines when judging
    whether a candidate is workable, covers those columns in both its
    in-flight and candidate loops, and drops the false parked claim; its
    accompanying spec grew from 15 assertions to 35.
  - DD-742: the operator next-action guard listed four columns as in-flight
    (in-progress, researching, analysing, documentation) while the board
    declares eleven working columns, so a card at a later gate did not
    suppress a new recommendation: observed 2026-09-02 with a card in
    unit-test, the guard answered "DD-659 is waiting to be started. Claim it
    and work it". It now asks the board, where required_actions > 0 selects
    exactly the eleven working columns, so a twelfth column needs no code
    change, and an empty result fails into the existing could-not-look path
    rather than reporting that nothing is in flight.
  - DD-747: the next-action helper walked the pick-next column order but
    applied no ordering within a column: its candidate listing returns refs in
    ascending order, so the loop always took the oldest workable card.
    Measured on 2026-09-02, it named DD-637 (priority 2) as the next thing to
    do while DD-744 (priority 4) sat unworked in the same column. The tool now
    fetches ref and priority once per run and sorts each column's candidates
    by descending priority before picking.
  - DD-756: the hourly policy sweep compares the board's live policy set
    against the project's policy manifest, and the two records had drifted
    across the board upgrades: 61 policies were declared on the board while
    the manifest held 32 declared and 2 declined, with ten rules
    (card-changed-by-owner, agent-still, card-agentless, task-unlinked,
    task-changed, job-due, task-card-mismatch, monitor-dead, monitor-output,
    monitor-silent) present on the board and absent from the manifest, so the
    sweep exited 1 every hour asking for judgements that had already been
    made. Each of the ten is now judged and recorded in the manifest with its
    reason, and the sweep returns to exit 0. The manifest and the sweep are
    operator-local tooling; nothing in the installed distribution changes.
  - DD-679: nothing walked the working copies on the development host, so work
    sitting on disk that no card knew about went unnoticed: three checkouts
    were found holding content not on master, two of them belonging to cards
    already in terminal columns. A new developer-documentation page records
    the measured layout - 169 registered worktrees split 32/100/36 across
    three roots, one of them inside the main checkout - and the two identity
    tests a checkout must satisfy before anything reports on it, since a clone
    of this repository fails a git-common-dir check while a
    correctly-registered worktree of an unrelated project can sit in our own
    superseded sandbox root. It also records why "this directory differs from
    master" is the wrong question: scored by hand against three cases it gave
    one true finding and two false alarms, because a stale tree routinely
    holds a superseded draft, so the sweep asks whether the commit carrying
    the card's ref landed instead.
  - DD-659: the development host-exclusivity rule was written down but not
    scoped, and a hand-written per-session wrapper had held a host lock across
    an entire gate chain - 2529 seconds, with its prove child long since
    exited, while another session's twelve-minute suite waited over forty
    minutes. A new development-process page records the convention that such a
    lock is acquired immediately before the work and released when that
    process exits, notes that run-suite and coverage-gate already scope this
    way (per run and per coverage database respectively), and states the two
    limits run-scope does not remove: a waiter cannot tell a deliberate hold
    from a hang, and it does not prevent starvation between waiters using
    different strategies. No code changed - the offending wrapper was
    per-session and no longer exists.
  - DD-757: two places in the repository explained that docs/ never reaches
    the release tarball because dist.ini's [GatherDir] gathers under doc/
    (singular) and never looks at docs/ (plural). That reason is false:
    [GatherDir] declares no root, so it gathers the whole distribution root,
    and docs/ is kept out by one explicit line, dist.ini's
    `exclude_match = ^docs/`, which neither artifact mentioned. The conclusion
    itself was never wrong and had been checked by building the distribution
    before and after and finding zero docs/ entries both times; only the
    attribution was wrong, and a reader trusting it could delete that exclude
    line as redundant, after which docs/ would ship. Both now name the line
    and state plainly that removing it ships docs/, cross-referencing the
    earlier case (DD-401) where a git-ignored file still reached a local 4.22
    tarball because dzil gathers from disk rather than git.
  - DD-776: since Tira 5.77 the board tool raises its own card whenever it is
    upgraded, but no written procedure existed for working one, so each review
    invented its own checklist. An internal documentation page now records
    what that ticket actually asks - that nothing is left undeclared, and that
    no already-declared policy would be refused by a validation the upgrade
    added - and what it does not ask, since there is no per-version manifest
    to diff against. The 5.56 to 5.77 review it was written from found
    `tira.policy.undeclared` empty and none of this project's 61 declared
    policies setting --age on the eighteen rules 5.61 gave forbids => ['age'],
    so nothing needed correcting.

4.29  2026-08-18

  - DD-585/589/590/591/592/593: six query functions across CollectorRunner.pm,
    IndicatorStore.pm, RuntimeManager.pm, ActionRunner.pm, SkillManager.pm
    (three functions) and PageRuntime.pm each shelled out (via system(),
    Capture::Tiny, or a waitpid reap) and left Perl's global $? holding their
    own last subprocess's exit status after returning, without ever
    localizing $? at sub entry. A caller reading bare $? afterward for an
    unrelated reason - the shape that first surfaced this in DD-585, an END
    block in a test file - silently inherits the wrong value. Found by a
    systematic sweep of lib/ once the first instance was fixed, and
    independently by the autonomous hourly bug-hunt round, which found and
    fixed DD-590 and DD-593 on its own (both resumed here after the round's
    own session ended mid-verification, with nothing lost either time - its
    park mechanism correctly left the tree untouched while a suite was live).
    Fixed uniformly with `local $?;` at each sub's entry. One subtlety
    verified rather than assumed: SkillManager's three functions never read
    $? explicitly, but Capture::Tiny returns system()'s own return value
    automatically while system() ALSO mutates the global $? as an
    unavoidable side effect - confirmed with a standalone repro before
    writing any fix. A fourth possible instance (ActionRunner's own sibling
    query pattern) was checked and is genuinely absent.
  - DD-594: dd-round-park.sh's automatic end-of-round park comment defaults
    to the shared, retired 'DD Bot' identity (DD-527 retired it project-wide)
    because nothing in the automation chain ever sets DD_TIRA_PARK_AUTHOR.
    Found by the autonomous round tracing the root cause of a finding that
    had been recurring on every card it touched.
  - DD-586: t-dd-round-park.sh's specs-guard timeout (600s) was too tight for
    a spec that deliberately spawns real long-lived background processes to
    model a genuinely-alive concurrent round; raised to 1200s.
  - DD-587: the hourly bug-hunt and 10-minute blocked-resolver systemd
    timers had been silently disabled for 9 days. Re-enabled and verified
    both correctly defer to an active interactive session before doing real
    work.
  - DD-562: master's coverage gate read 100.0 locally but 99.9 on CI.
  - DD-567: a perl-interpreter CVE advisory blocked the CI step immediately
    before tests and coverage, on 5 consecutive pushes.
  - DD-568: an early CI step failing silently skipped the test and coverage
    steps that followed it; those steps now run with if: always().
  - DD-572: the pre-push security grep matched 'brewfile' against the
    retired-system marker pattern 'ewf', making a required gate return
    permanent noise.
  - DD-573: 13 operator-tool specs existed under dd-tg with nothing ever
    running them, so a broken spec could never fail loudly.
  - DD-574: the audit gate could fail without ever saying which check failed
    it, a defect DD-567's own fix left behind.
  - DD-581: nothing recorded a coverage verdict anywhere the board's own gate
    indicator could read, so it could never show green even on a clean pass.
  - DD-582: gate-status reported RUNNING for a log file it had no way to know
    was actually still being written.
  - DD-583: the Telegram ack watcher answered a direct question about its own
    silence with more silence.
  - DD-584: two dd-tg operator specs had been failing with nobody told.

4.28  2026-08-15

  - The coverage gate's own regression test no longer asserts that this machine's
    library tree exists everywhere. It runs the gate in an environment stripped of
    PERL5LIB and expected it to resolve its dependencies anyway, which is true only
    where those dependencies sit in the one directory the gate repairs. Where they
    do not, the gate correctly reports a missing module and the test called that a
    failure. It now skips those two cases with the reason printed, keeps every
    structural assertion, and still goes red if the repair is removed.
  - 4.27 was tagged but never published: this test failed in the release workflow,
    and a job that fails early makes every later step skip, so the tag exists with
    no release behind it. Nothing in 4.27 is lost - it is all here.

4.27  2026-08-15

  - Browser smoke tests no longer hang a machine whose browser cannot reach its
    own loopback address. A coverage run sat on one such fetch for one day and
    twenty hours, holding a web server, a starman master and worker and a whole
    Chrome tree, while producing no failure, no exit status and no last line - a
    stalled run looks exactly like a slow one. Every external command those tests
    run is now bounded, and the bound kills the command's whole process group
    rather than the single process it can see, because the wedge left seven
    processes alive and killing the visible one would have reaped one of them.
  - Those tests now ask, before doing anything else, whether the browser can
    fetch a page that is definitely being served. If it cannot, they skip and
    print why - the probe URL, what the browser did, and that the product is not
    implicated. The probe serves that page over plain HTTP from a nine-line
    socket server rather than from the product, which is what makes it a
    diagnosis instead of a second copy of the test: if plain HTTP works and the
    real fetch then fails, that is a genuine failure and is not skipped.
  - The workspace command hands the terminal to tmux with exec instead of
    capturing the attach. It used to funnel a full-screen interactive program
    through a capture buffer nobody reads until it exits, and leave a perl
    process parked as its parent for the life of the session - possibly days.
  - Collectors: a lost pidfile no longer multiplies supervisors. Start, stop and
    status all treated that file as the authority on what was running, so once it
    was gone every start forked another loop that nothing could see, stop or
    count; twenty-seven were alive at once for a collector declared singleton,
    each firing every fifteen minutes. They now ask the process table, adopt the
    loop already running, and repair the record.
  - A collector stopped by a signal now records which signal stopped it and which
    process received it. A 'stopped' state with no pidfile is otherwise identical
    whether the loop shut down in an orderly way, was stopped by a watchdog, or
    was killed by something else on the machine - and that ambiguity sent four
    separate investigations down the wrong path.
  - The coverage gate carries its own library path. Launched from cron or a
    transient systemd unit - environments that read no shell profile - it died
    naming a dependency that is declared in three manifests and installed. It
    resolves that tree itself now, and still fails loudly when a dependency is
    genuinely absent, because it adds a directory only when the directory exists
    and runs no external command.
  - The collector start path no longer re-checks a pid it has already guaranteed.
    When the recorded pid is missing or empty it is replaced by a search of the
    process table, which returns a real pid or nothing, so the later test for a
    usable pid could never fail. The guarantee is now stated once where it holds
    instead of being re-checked where it cannot, which is one fewer piece of code
    that looks like it protects something.

4.26  2026-08-10

  - Packaging: stopped the release tarball carrying two things it never should
    have. logs/ shipped the board sweep's own runtime output - findings about
    this project's cards - to anyone who installed the product, because dzil
    gathers from disk and cron recreates that directory hourly. SECURITY_CHECKS.md
    shipped the internal security-audit protocol, which belongs with the operator
    files that are deliberately never released. Both are excluded now and t/15
    fails if either exclusion is removed.
  - Board tooling: the FT99 sweep decides from the cards themselves instead of a
    retired markdown page, so a card parked on the owner with nothing left to
    answer is reported rather than silently accepted, and a card holding an
    unanswered question anywhere else is reported too. It also reports a card
    claimed in a working column that has stopped moving, and one left untouched in
    the backlog.
  - Coverage gate: refuses to grade a database written by a different
    Devel::Cover install, and says which two disagree, instead of exiting with an
    unexplained status.

4.25  2026-08-09

  - CI: restored a pipeline that had certified nothing for ten days. Three
    GitHub Action pins sat on the node20 line while their version comments
    claimed node24, so every run died at Setup Perl and the suite, the coverage
    gate and both CVE audits never executed (DD-449). The pin comments are now
    resolved against the upstream tag rather than trusted (DD-449), the
    environment export no longer swallows its own failure inside a command
    substitution (DD-483), and two suite files no longer assume tools and
    libraries CI installs elsewhere (DD-485).
  - Security: helper session ids are drawn from the operating system CSPRNG
    instead of a SHA over the pid, the wall-clock second and built-in rand -
    the construction CVE-2026-13577 describes in Dancer2, reimplemented here in
    the code guarding the web interface. Crypt::URandom is now a declared
    dependency floored at 0.55, because CVE-2026-2474 affects earlier releases
    (DD-452).
  - Security: dispositioned CVE-2026-13577 for Dancer2, which has no fixed
    release, behind a guard that fails the audit if any production code ever
    activates a Dancer2 session (DD-451).
  - Security: closed a Zip Slip in dashboard open-file, where a sources jar
    could write attacker-controlled paths anywhere the user can write (DD-498).
  - Security: corrected the CVE gate's subject so it judges the product's
    declared chain rather than the host interpreter (DD-499).
  - Fix: a collector's loop state is written before its pidfile, so a
    concurrent running_loops can no longer delete the pidfile of a healthy loop
    and leave it unstoppable by name while the supervisor forks a duplicate
    (DD-488).
  - Fix: Housekeeper's temp-file cleanup lists its directory instead of
    globbing a pattern built from the path, so a directory whose path contains
    a space stops hiding every real temp file and unlinking unrelated ones
    (DD-502).
  - Test reliability: the coverage gate, the empty-environ probe and several
    collector lifecycle fixtures no longer fail purely because the host is
    CPU-starved, which is the shape of every CI runner (DD-482, DD-486,
    DD-489). Every timeout changed here is backed by a measurement rather than
    by the fact that a larger number passes.

4.24  2026-08-08

  - Security: closed the open-redirect class in the login redirect sanitizer
    and on both HTTPS-enforcement Host-header redirect paths (DD-415, DD-419),
    rather than only the single reported byte.
  - Security: closed three path-traversal classes - saved-page writes outside
    the dashboards root (DD-395), parent-directory traversal in every
    skill-namespaced web route with static-file containment asserted (DD-416),
    and skill installs whose repo name escapes the skills root (DD-426).
  - Security: every page-derived value the web layer places inside an HTML
    attribute is now escaped for attribute context (DD-421), and saved
    bookmark link segments are percent-encoded for URL context (DD-423).
  - Security: state-changing requests are rejected when Origin or Referer is
    foreign, on every web tier (DD-422).
  - Security: the transient-token denial now depends on the token alone, so a
    file value riding along can no longer bypass the 403 (DD-425).
  - Security: release tarballs no longer carry dogfood QA evidence, ticket
    worktrees (DD-414), or the Hermes runtime state root and its credentials
    (DD-432).
  - Feature: first-class self-upgrade - dashboard upgrade and d2 upgrade run
    the canonical platform installer (DD-386).
  - Feature: a real /favicon.ico is served instead of a 404 on every page
    load (DD-407).
  - Collectors: a timed-out command's full process subtree is terminated
    (DD-388); blocked Windows command collectors are interrupted at their
    timeout (DD-389); the atomic writer uses a per-process pending name so
    concurrent workers stop clobbering each other (DD-418); log append and
    rotation share a per-log lock (DD-399) and rotation cuts on entry
    boundaries so a combined lines+days rule cannot orphan an entry body
    (DD-427); collectors keep running when the updates directory is absent
    (DD-398).
  - Runtime: saved-Ajax disconnect cleanup terminates descendant processes
    (DD-396) and gives them a real elapsed SIGTERM grace window (DD-428);
    runtime state readers survive a transient empty state file (DD-411) and
    the web lifecycle no longer acts on incomplete runtime state (DD-412).
  - Windows: the SSL certificate home resolves through PathRegistry so the
    HTTPS server starts (DD-417); every passwd lookup is guarded so
    non-interactive sessions resolve a user and a home (DD-413); PERL5LIB
    generation is cross-platform (DD-397); PowerShell startup caches are
    anchored to the home layer and no longer clobber live PATH state
    (DD-390).
  - Web UI: every generated HTML document carries a lang attribute (DD-405);
    logout and an authorized /login route by trust tier (DD-408); page-editor
    textareas carry programmatic accessible names (DD-409); rendered page
    chrome no longer overflows 320px phone viewports (DD-410).
  - Performance: PathRegistry working-directory lookups no longer fork
    /bin/pwd repeatedly (DD-393).
  - Gates: the POD syntax gate reads the source tree instead of a stale copy
    and the three reds it was hiding are fixed (DD-429); the coverage gate
    grades lib/ instead of the stale build tree (DD-431) and a failed exec in
    the harness process can no longer blind it (DD-428); the Windows
    collector-timeout E2E is proven on a real QEMU Windows guest and made
    repeatable (DD-404).
  - Metadata: MakeMaker license metadata aligned with MIT (DD-400).

4.23  2026-07-29

  - Coverage: every module under lib/ now reaches 100.0 on all four
    Devel::Cover metrics - statement, subroutine, branch, AND condition -
    driven by the new per-module hermetic coverage suites (t/59 through
    t/106) with genuinely-unreachable outcomes annotated inline and dead
    guard operands removed where redundancy caused unreachable branches.
  - CI: all three workflows (test, release-github, release-cpan) enforce the
    all-metric standard through script/check-all-metric-coverage, which
    fails closed on any metric below 100.0 or any malformed report
    (t/107-all-metric-coverage-gate.t).
  - Security: helper host-trust no longer accepts arbitrary Host headers
    that merely resolve to loopback, closing a DNS-rebinding vector; exact
    loopback literals and configured alias hosts remain trusted.
  - Test isolation: the fresh-runtime API assertions in
    t/15-cli-module-coverage.t now chdir into their isolated HOME so a
    dev-runtime layer in the working checkout cannot capture their writes.
  - Installability: the dependency hardening initially raised the Perl floor
    to 5.44, which cpanm rejects on every mainstream perl (perl:latest is
    5.42); the floor is restored to 5.38 (the code's real requirement) while
    CI continues to run on Perl 5.44 and the dependency version pins keep
    the security gate.

4.22  2026-07-15
    - the housekeeper now sweeps expired helper-session files each run (wiring the
      SessionStore expired-session sweep added in 4.21 into the periodic cleanup),
      so stale session records no longer accumulate unbounded between logins

4.21  2026-07-15
    - batch of fixes from the automated bug-hunt (round 1), each test-covered:
    - config: the merged config now always dedupes the `collectors` array by
      name and `providers` by id, even when only one layer defines them (a
      single layer repeating a collector name previously leaked duplicates);
      config.json / api.json are now written atomically (temp file + checked
      close + rename) instead of truncated in place
    - security: the encoded-action transient path no longer trusts a
      payload-supplied `source`/trusted flag, closing a forged-token arbitrary
      command execution; the session id from the cookie is charset-sanitized
      before path use, closing a session-file path traversal; the session cookie
      gains `Secure` over HTTPS; helper passwords are now stretched with
      PBKDF2-HMAC-SHA256 while still verifying existing hashes
    - collectors: `timeout_ms` now round-trips correctly (no more x1000 blow-up
      on Windows worker restart); a `* * * * *` cron collector no longer bypasses
      per-minute dedup; signal-killed collector/action commands are recorded as
      failures instead of exit 0; background actions guard setsid on Windows
    - state writes: Collector and IndicatorStore atomic-write helpers now check
      close() and no longer unlink before rename; a git error is no longer
      reported as a dirty tree
    - added SessionStore::sweep_expired for expired-session reclaim (wiring it
      into the housekeeper, plus the DNS-rebinding host-trust tightening and the
      timed-out-command subtree kill, are deferred as follow-ups)

4.20  2026-07-15
    - security: in `dashboard serve --ssl` the public front-proxy relays raw TLS
      to the internal loopback backend, so every backend connection arrived from
      127.0.0.1; combined with a `Host: 127.0.0.1` header a remote HTTPS client
      was classified as trusted loopback admin and got unauthenticated admin.
      The internal SSL backend now marks itself proxied and never grants the
      loopback-admin shortcut, so remote HTTPS clients must authenticate.

4.19  2026-07-15
    - ship `d2` as a real, installed command instead of only a shell-bootstrap
      alias: `bin/d2` is a first-class entrypoint that re-execs its sibling
      `dashboard` with the same arguments, so `d2 <command>` behaves exactly like
      `dashboard <command>` and now works in scripts, cron jobs, and fresh shells
      that never sourced the dashboard shell integration
    - the shell bootstrap keeps completion wiring for `d2`, and the generated
      shell shortcut remains as a convenience where the bootstrap is loaded

4.18  2026-07-15
    - add `dashboard ask <question>`: a uniform AI-assistant command that answers
      from the Anthropic API when an `ANTHROPIC_API_KEY` (or the `claude` config
      domain) is set, and otherwise falls back to the local `claude` CLI
    - support pluggable backends `--claude` (default), `--codex`, `--copilot`,
      and `--gemini`; the chosen backend becomes sticky per workspace, and each
      shells out non-interactively in a forced read-only mode
    - remember the conversation per workspace (keyed by WORKSPACE_REF or the
      active project root) so follow-up questions carry prior turns; `--new`
      starts fresh and `--no-memory` skips the transcript for one turn
    - inline `--file` text attachments into the prompt and pass image attachments
      natively (Anthropic image blocks, codex `-i`, copilot `--attachment`); the
      transcript is stored owner-only under the runtime state root

4.17  2026-07-15
    - make `dashboard collector stop <name>` actually stop the collector: it now
      routes through the supervisor-aware stop path so the collector watchdog is
      deregistered and cannot immediately respawn the loop (previously it called
      the low-level loop stop directly, so the supervisor restarted the collector
      and its status snapped back to running)
    - stop the collector supervisor from creating a DUPLICATE loop when it races
      a freshly started loop before that loop has set its process title, by
      recognizing an already-running loop from its recorded state as well as by
      process identity
    - reset the consumer-facing collector `status.json` (running / active_runs)
      when a loop is stopped, so prompt, web, and CLI status readers reflect the
      stop immediately
    - reliably terminate long-running collector command subtrees on stop: send
      the worker process-group SIGKILL unconditionally so a SIGTERM-ignoring
      child is still reaped, kill the loop first and scale the TERM grace to the
      worker count so the loop is not KILLed mid-cleanup, persist active worker
      pids on spawn, and sweep orphaned workers left behind by a crashed loop
    - recognize managed collector loops from recorded state on hosts without
      `/proc` or `ps` (Windows), so stop can still terminate them
4.16  2026-06-11
    - teach the built-in `dashboard workspace` command a `-c` flag, accepted
      before or after the workspace name, that changes into the registered
      dashboard path for that name before planning the session, so
      `dashboard workspace -c foobar` behaves like `cdr foobar` followed by
      `dashboard workspace foobar` and the tmux session plus its layered
      `.env` refresh start from the registered project directory
    - fail `-c` explicitly when the workspace name is not a registered
      dashboard path, when the registered target is not a directory, or when
      the directory cannot be entered, instead of silently starting the
      workspace from the wrong directory
4.15  2026-06-11
    - keep the `install.sh` post-install activation runner on the same shell
      dialect as the bootstrap target, so blank hosts without `SHELL` exported
      (fresh Docker containers, `curl ... | sh` runs) no longer source the
      bash-flavoured activation files through plain `sh` and die on the first
      bash-only construct
4.14  2026-06-11
    - align every module under `lib/` back onto the single repository release
      version after the `4.13` bump only advanced `Developer::Dashboard`,
      leaving the other 53 packaged modules still declaring `4.12`
    - add a release-metadata gate that walks every `lib/**/*.pm` and asserts
      its `$VERSION` matches the `dist.ini` release version, so a partial
      version bump can never ship again
    - validate IPv4 loopback octets strictly in `Auth::_ip_is_loopback`, so
      malformed literals such as `127.0.0.999` are no longer classified as
      loopback addresses
    - restore the `4.12` entries in `Changes` and `FIXED_BUGS.md` that the
      `4.13` update overwrote instead of prepending
4.13  2026-06-11
    - harden the GitHub-hosted `Devel::Cover` gate again by exercising the
      installed-dist `File::ShareDir` asset lookup branch in
      `Web::App::_bundled_public_asset_path`, so clean Ubuntu runners close
      the final `99.9` statement gap and keep the reviewed `lib/` totals at
      `100.0 / 100.0 / 100.0`
4.12  2026-06-10
    - harden the GitHub-hosted `Devel::Cover` gate by exercising the
      low-traffic `Web::App` compatibility helpers directly in the focused web
      coverage tests, so route fan-out timing on CI runners cannot leave the
      reviewed `lib/` totals below `100.0 / 100.0 / 100.0`
4.11  2026-06-10
    - decouple the GitHub-hosted CPAN upload workflow from ordinary `vX.XX`
      tag pushes so signed GitHub release tags can be published without
      triggering an unasked PAUSE upload
    - broaden the CodeQL push trigger to every branch so repository-side SAST
      coverage is visible on more commit paths instead of only `master`
4.10  2026-06-10
    - move project-local isolated Docker Compose service discovery and
      disable markers under `./.developer-dashboard/config/docker/...` so
      the implementation matches the documented config-root contract across
      resolver lookup, toggle writes, and exported `DDDC` paths
4.09  2026-06-06
    - keep smart-routed skill page browser edits on their canonical
      `/app/<skill>` and nested `/app/<skill>/<sub-skill>` aliases by
      teaching the edit/source routes and Play handoff to load skill index
      pages through the same smart resolver instead of collapsing to the
      underlying `BOOKMARK: index` id
4.08  2026-06-06
    - split the Web UI bookmark editor into Jupyter-like section blocks while
      keeping the saved bookmark file format canonical, including block-aware
      syntax overlays, Tab-created sections, hidden source recomposition, and
      Play-mode save-and-render handoff
    - standardize operator-facing built-in CLI output so path, file, api, and
      skill inventory or mutation commands default to readable table summaries
      while `-o json` returns the full raw machine payload
4.04  2026-06-05
    - restore the public switchboard helper path contract for dotted
      `dashboard <skill>.<command>` dispatch, keep Windows core-backed
      built-ins routed through the shared staged `_dashboard-core` helper,
      and fix the staged private core import drift that broke `dashboard init`
      and later helper refreshes
    - harden Windows runtime helper and state-file flows so detached web,
      collector-loop, collector-worker, and watchdog launches use the staged
      foreground helper entrypoints and tolerate Windows rename collisions
      through explicit replace fallbacks
    - teach the Windows checkout bootstrap to seed `HOME` from PowerShell's
      own `HOME`, create a stable user-space `make.cmd` shim for Strawberry
      GNU make, and keep the fuzz gate lightweight under `Devel::Cover`
    - force non-interactive CPAN environment defaults for skill `cpanm`
      installs so Windows fresh-session bootstrap checks do not stall behind
      dependency configure prompts
4.03  2026-06-05
    - ship an explicit OWASP compliance SOW and evidence matrix, wire it into
      the OWASP and release-metadata tests, and tighten the manuals so the
      safe public wording stays `OWASP-aligned` / `OWASP-gated` until the
      remaining governance blockers are truly closed
4.02  2026-06-04
    - fix `_helper_file_supports_internal_command` substring detection so
      runtime helper refresh probes return true for matching staged helpers
      and stay stable under the plain and covered harnesses
    - harden `t/47-zombie-coverage-closure.t` with a persistent helper
      fixture and direct runtime-object assertions so the final gate loop
      stays deterministic
4.01  2026-06-02
    - add the built-in `dashboard api` management command for layered
      `config/api.json` machine auth, including merged registry listing,
      per-key inspection, SHA-256 hashing of raw secrets before persistence,
      optional `--maybe-secret` route-oriented secret input, exact saved
      `/ajax/...` route add/remove controls, duplicate-route no-op handling,
      and child-layer tombstones that hide inherited API groups without
      rewriting parent config
4.00  2026-06-02
    - add layered `config/api.json` machine auth for selected saved
      `/ajax/...` routes, including installed skill `config/api.json`
      fragments, exact route allowlists, and `X-DD-API-Key` plus
      `X-DD-API-Secret` header verification against stored SHA-256 digests
    - keep helper-session auth working on those same API-registered ajax
      routes while failing unauthenticated or wrong-secret machine callers
      closed with `403 {"status":"forbidden"}`
    - pass saved ajax API auth headers through the PSGI/Dancer adapter so
      browser-facing and direct HTTP entrypoints enforce the same machine-auth
      contract
3.99  2026-05-26
    - keep the active shell directory in dashboard-managed child `PATH`
      repairs alongside the current Perl interpreter bin so collector shell
      commands still execute cleanly during blank-environment tarball installs
      and any runtime that inherits a stripped `PATH`
3.98  2026-05-20
    - treat collector `disable` as a hard stop-and-skip flag so disabled
      collectors are not started, explicit named starts reject them, and any
      already-running managed loop is stopped during the next lifecycle action
    - remove managed collector indicators for disabled collectors instead of
      leaving stale active indicator state behind
    - normalize dashboard-owned `PERL5LIB` ordering so core/site/vendor Perl
      dirs stay ahead of inherited user-local shadow copies, preventing stale
      dual-life XS modules such as `Encode` from breaking helper startup,
      collector child commands, saved Ajax subprocesses, and skill hooks on
      hosts with older local-lib artefacts
    - keep the current Perl interpreter directory at the front of `PATH` for
      dashboard-managed child commands so collector loops, saved Ajax
      subprocesses, and skill hooks do not fall back to a mismatched system
      `perl` when they execute `dashboard` through `/usr/bin/env perl`
    - run collector shell commands through a non-login shell so macOS shell
      session restore output and other shell-init chatter do not get prefixed
      onto JSON collector output
    - slow tmux ticket/workspace status refresh from a 2-second hot loop to a
      15-second cadence so dashboard-managed tmux sessions do not burn CPU just
      to repaint status fragments
    - apply a 30-second default safety floor to shell collectors that recurse
      back into `dashboard` or `d2`, with explicit opt-outs and an environment
      override for intentionally fast dashboard polling
    - pause the collector watchdog supervisor during explicit named collector
      stop and restart actions, then restore supervision for the remaining
      watched fleet afterwards so watchdog restarts do not race manual
      lifecycle commands
    - preserve the saved managed web pid during stop flows even when shutdown
      has to fall back to persisted-state listener pids, and still escalate the
      saved pid to KILL when process discovery lags after TERM
    - scope web stop ajax cleanup to the current dashboard runtime root instead
      of sweeping every `dashboard ajax:` process owned by the user
    - make collector loop shutdown kill active worker process groups so
      long-running singleton commands stop with the collector loop instead of
      surviving as orphaned subprocesses
    - reap exited collector workers and watchdog-adopted dashboard children
      immediately on `SIGCHLD` instead of waiting for the next scheduler or
      watchdog tick, so long-interval collectors and orphaned supervisors do
      not leave visible `<defunct>` dashboard processes behind for minutes or
      days
    - persist active worker pids in collector loop state and lock in a live
      singleton regression where a long-running due run must be skipped instead
      of overlapped

3.92  2026-05-20
    - keep `dashboard ps1` on a lighter switchboard path by lazy-loading the
      unknown-command suggestion runtime and helper-staging-only modules
      instead of paying for them on every prompt render
    - stage only the requested built-in helper on the hot path and reuse one
      `PathRegistry` object across the switchboard invocation, instead of
      rescanning and restaging the entire helper tree for every prompt
    - load installed skill config fragments directly from layered
      `config/config.json` files in `Config` so prompt-time config reads do not
      pull in the full skill dispatch runtime

3.91  2026-05-20
    - make `dashboard ps1` skip `tmux show-environment` when `TMUX` is unset,
      stop constructing an unused collector object, and refresh only
      prompt-visible core indicators on the prompt path
    - read the prompt branch directly from `.git/HEAD` and worktree `gitdir:`
      metadata instead of spawning `git branch`, keeping branch rendering fast
      on slower hosts such as iSH
    - add a collector-sync fast path so repeated prompt renders avoid the full
      collector indicator rewrite path when the stored config-backed indicator
      state already matches

3.90  2026-05-20
    - make `PathRegistry` actually reuse a precomputed invocation cwd and
      memoize repeated DD-OOP-LAYERS path derivation so thin helper commands
      stop recalculating the same runtime chain during one process
    - make `EnvLoader` plain-directory traversal reuse the registry cwd cache
      instead of re-running `cwd()` while it walks root -> project -> leaf
      `.env` layers
    - tighten the nested skill env/compose fix set with the final blank-host
      and coverage gate closure work

3.89  2026-05-19
    - expand nested skill env loading from root to leaf for dotted skill
      commands and participating docker compose skill services, preserving
      overwritten parent values under cumulative aliases such as
      `foo_VERSION` and `foo_bar_VERSION`
    - discover nested installed-skill docker compose roots so participating
      leaf services such as `skills/foo/skills/bar/skills/zzz/config/docker`
      contribute both leaf `zzz_DDDC` and cumulative `foo_bar_zzz_DDDC`
      aliases during compose resolution
    - cover the remaining action-runner defensive and background-child startup
      branches so the final tree can close the 100% coverage gate again

3.88  2026-05-19
    - load participating skill-root `.env` files into `dashboard docker compose`
      resolution and export normalized `<skill>_DDDC` variables for each
      participating skill docker root
    - run detached background page command actions through a reaped supervisor
      path so blank-container tarball installs and root-owned hosts do not
      leave stale background wrappers behind or fail the tarball gate

3.86  2026-05-19
    - fixed packaged shell bootstrap generation so staged helpers always
      re-enter the active dashboard entrypoint instead of leaking a stale
      inherited source-tree path into extracted tarball installs and tests

3.85  2026-05-19
    - export one skill-specific <skill-name>_DDDC compose env variable for each participating skill docker root
    - keep participating skill .env loading isolated to docker compose resolution without executing skill .env.pl files

3.84  2026-05-19
    - load participating installed skill .env files into docker compose resolution without executing skill .env.pl files

3.83  2026-05-18
    - fixed interactive `dashboard skills install` progress so dependency rows
      are only added when the matching manifest file actually exists in the
      fetched skill, instead of printing `[OK] ... skipped: ... not present`
      noise for absent files
    - filtered operating-system-specific dependency rows so `aptfile`,
      `apkfile`, `dnfile`, `wingetfile`, and `brewfile` only appear on their
      matching host families and stay hidden on unrelated platforms even when
      those files exist in the skill
    - kept cross-platform manifest rows such as `package.json`,
      `requirements.txt`, `cpanfile`, `cpanfile.local`, `Makefile`, `ddfile`,
      and `ddfile.local` visible only when those files are present, which
      leaves the progress board focused on real work instead of skipped probes

3.79  2026-05-16
    - replaced the `tomq` backend dependency with `TOML::Parser`, inflating
      TOML booleans to plain Perl `1` and `0` scalars so query output stays
      stable while the blank-container `cpanm` install gate no longer trips
      over the failing upstream `TOML::Tiny 0.21` test suite on Perl 5.38

3.78  2026-05-16
    - fixed the tag-driven GitHub release workflow so it installs
      `Devel::Cover` before running the numeric coverage gate; this avoids
      the exit-127 `cover` failure that previously blocked signed GitHub
      release publication and kept Scorecard from seeing any releases

3.77  2026-05-15
    - aligned every shipped Perl module under `lib/` with the distribution
      version after the blank-host installer fix so the source tree, release
      metadata, and CI coverage gates all read one consistent `3.77` version
      instead of a mixed `3.75`/`3.76` module set

3.76  2026-05-15
    - fixed blank Ubuntu `curl ... | sh` bootstrap installs so `install.sh`
      seeds `File::ShareDir::Install` into `~/perl5` before `cpanm --notest .`
      runs against the cloned checkout, avoiding the configure-time
      `Can't locate File/ShareDir/Install.pm` failure on fresh hosts
    - aligned `install.ps1` with the same configure-prerequisite bootstrap so
      the checkout install path does not drift between Unix-like and Windows
      operators

3.75  2026-05-15
    - fixed Windows background web helper lookup so source-tree and CI test
      runs no longer die when `RuntimeManager` asks `File::ShareDir` for an
      installed dist share that does not exist yet; helper fallback now reuses
      the same guarded helper-asset resolver used elsewhere in the runtime
    - replaced the built-in `/js/jquery.js` shim with a bundled local copy of
      jQuery 4.0.0, while keeping `/js/jquery-4.0.0.min.js` as a compatibility
      alias for the same shipped asset
    - taught the browser bookmark editor to expand `:---` into the full
      separator line and insert the next sensible unique directive so common
      `TITLE -> HTML -> CODE<N>` authoring flows are smoother
    - added release and route coverage for the bundled jQuery asset and the
      missing-dist-share helper fallback path

3.74  2026-05-15
    - added direct `python` dispatch for executable `cli/*.py` commands and
      hook files so Python-backed skills now run through the same logical
      command and hook resolution path as Perl, Node, Go, Java, and shell
      helpers
    - added `requirements.txt` to the skill dependency chain after
      `package.json`, installing Python dependencies through
      `python -m pip install --user --requirement requirements.txt`
    - regenerated README.md from the canonical POD after the runtime and
      dependency-chain changes so release metadata stays in sync

3.73  2026-05-15
    - added direct `node` dispatch for executable `cli/*.js` commands and
      hook files, so JavaScript-backed skill commands now run through Node in
      the same extension-resolution path already used for Perl, Go, Java, and
      shell helpers
    - fixed the platform runnable-file resolver so logical command names can
      discover matching `.js` files on Unix and Windows without breaking the
      existing `package.json` dependency-install chain used by
      `dashboard skills install`
    - fixed the prompt tmux-status unit coverage to clear `WORKSPACE_REF`
      explicitly so ordinary tmux sessions still test the non-dashboard path

3.72  2026-05-14
    - fixed collector watchdog supervision so a managed loop that stays alive
      but stops making progress is treated as stalled, restarted
      automatically, and reported explicitly instead of sitting silent until
      an operator notices
    - renamed the primary tmux workflow from `dashboard ticket` to
      `dashboard workspace`, while keeping the older `dashboard ticket`
      spelling plus `TICKET_REF` session state as compatibility fallbacks
    - added layered plain-directory `.env` refresh for workspace sessions, so
      the highest ancestor `.env` acts as the base, parent `.env` files merge
      next, the current directory `.env` overrides last, and resumed tmux
      workspace sessions are refreshed in place with dropped keys unset

3.71  2026-05-14
    - fixed live collector status writes so managed indicator ordering now
      keeps the collector array order from `config/config.json` even after one
      collector refreshes its own state, instead of dropping back to
      alphabetical order in `/system/status`, page-header indicators, and
      `dashboard ps1`
    - fixed runtime `config/routes.json` aliases so normal saved bookmarks
      such as `/app/learn.ai` can be reached through shorter custom paths like
      `/java`, and widened that runtime alias loading across `/app`, `/ajax`,
      `/js`, `/css`, and `/others` instead of limiting custom paths to
      installed skill route metadata only

3.70  2026-05-13
    - moved skill route metadata to `config/routes.json` and changed the
      schema to the flat custom-path form, where each public path maps to one
      local smart route string or an object with `to` and optional `type`
    - expanded custom skill route support beyond Ajax so installed skills can
      publish fallback custom paths for `/app`, `/ajax`, `/js`, `/css`, and
      `/others` without breaking the smart longest-prefix parent routing
    - kept saved skill-page Ajax URL generation on the canonical custom path
      from `config/routes.json`, with Ajax custom routes defaulting to `json`
      unless an explicit route-level `type` is supplied
    - fixed browser `/system/status`, page-header status, and `dashboard ps1`
      indicator ordering so managed collector indicators now follow the
      collector array order from config instead of drifting alphabetically
    - fixed `dashboard serve logs -f` so a line appended immediately after
      the initial tail output is no longer skipped by a seek-to-end race
      before follow mode starts streaming new content

3.69  2026-05-13
    - added skill-local `dashboards/routes.json` metadata so installed skill
      Ajax handlers can publish canonical custom paths, optional fallback
      aliases, and default response types such as `json`, `html`, or raw
      mime strings without breaking the smart `/ajax/<repo>/...` resolver
    - fixed saved skill-page Ajax URL generation so `Ajax(file => ...)`
      emits the declared canonical custom route when route metadata exists,
      while the longest-prefix smart `/ajax/...` route remains the primary
      resolver and custom aliases are fallback-only before a normal `404`
    - fixed browser `/system/status`, page-header status, and `dashboard ps1`
      indicator ordering so managed collector indicators now follow the
      collector array order from config instead of drifting alphabetically

3.68  2026-05-13
    - added collector overlap control with explicit `mode` and `multiple`
      config fields, defaulting collectors to singleton scheduling and
      allowing bounded parallel runs only when a collector opts into
      `mode = multiple`
    - fixed collector loop scheduling so long-running jobs no longer block
      the next interval tick for opt-in multiple-mode collectors, while
      singleton collectors still suppress overlap by design
    - made collector status updates lock-safe so active-run counts and the
      `running` flag stay correct when overlapping collector workers finish
      concurrently

3.67  2026-05-13
    - fixed managed runtime child cleanup so detached web startup helpers,
      collector stop paths, the collector watchdog supervisor, SSL frontend
      connection workers, and background page actions no longer leave zombie
      processes behind on hosts such as macOS and WSL
    - fixed the SSL frontend accept loop so child-reaping `SIGCHLD` traffic
      no longer makes the public listener shut its backend down early, while
      TERM/INT/HUP now request a clean frontend shutdown explicitly instead
      of depending on interrupted `accept()` side effects
    - tightened regression coverage so collector stop and watchdog shutdown
      tests fail if the runtime leaves direct child processes unreaped, and
      action/background SSL runtime tests now enforce the no-zombie contract

3.66  2026-05-08
    - added a collector watchdog supervisor that restarts unexpectedly-dead
      managed collector loops after startup instead of leaving them silently
      stopped until an operator notices
    - persisted watchdog restart counters, timestamps, error text, and
      `attention_required` collector status metadata so repeated crash loops
      are explicit in `dashboard collector status <name>` and collector logs
    - stopped the watchdog from thrashing forever by raising an explicit
      attention-required state after too many crashes inside the watchdog
      window, forcing a visible operator follow-up instead of silent churn

3.64  2026-05-08
    - fixed macOS Terminal.app shell bootstrap to source /etc/bashrc_Apple_Terminal
      when update_terminal_cwd is missing, preventing "command not found"
      errors on fresh macOS bash sessions

3.63  2026-05-08
    - added dockerfile manifest support to skill installation, allowing skills
      to declare Docker images that get built automatically during install
    - fixed Progress.pm to support unlimited streaming output for long-running
      operations like Docker builds, with optional max_detail_lines cap
    - added has_dockerfile metadata to skill list and usage output
    - fixed macOS Terminal.app shell bootstrap to source /etc/bashrc_Apple_Terminal
      when update_terminal_cwd is missing, preventing "command not found"
      errors on fresh macOS bash sessions

3.62  2026-05-07
    - remove matching root ddfile skill source entries when uninstalling a skill

3.61  2026-05-06
    - switched the project license from Perl_5 to MIT across the repository
      metadata, shipped LICENSE text, README, canonical POD, and Scorecard
      guardrails so the published license state is explicit and consistent
    - removed the old Artistic sidecar license file and Perl dual-license
      wording so the repo no longer advertises conflicting licensing terms

3.60  2026-05-06
    - documented the open-source license disclaimer more explicitly in the
      canonical POD and synced README, making it clear that the software is
      provided "as is", no warranty is given, and normal free-software use is
      covered by the project's baseline liability disclaimer

3.59  2026-05-06
    - fixed shared skill-nav discovery so nested installed skill trees such
      as `skills/ho/skills/coverage/dashboards/nav/index.tt` now render on
      the nested skill route itself and also join the shared nav strip above
      normal saved pages such as `/app/index`
    - taught the skill dispatcher to recurse through nested installed
      `skills/<repo>` trees when collecting skill nav pages for the web UI,
      while preserving deterministic order and disabled-skill filtering
    - fixed layered skill nav route-id discovery so `dashboards/nav/*`
      recursively includes nested fragment paths instead of only direct files
      in the first `nav/` directory

3.58  2026-05-06
    - stamped dashboard-managed helper bodies with an explicit
      `developer-dashboard-managed-helper-version` marker so installed hosts
      can prove which helper build they actually have when staged helper
      behavior drifts from the active tarball
    - fixed Debian-family bash bootstrap installs and repairs so
      dashboard-managed shell lines are kept above the standard
      non-interactive `return` guards in `~/.bashrc`, including the Ubuntu
      single-line `[ -z "$PS1" ] && return` form, which keeps
      `dashboard ticket` tmux status commands working on installed hosts such
      as `hp.local`
    - taught `dashboard doctor` to audit and repair that misplaced
      `~/.bashrc` bootstrap shape with `dashboard doctor --fix`
    - fixed Windows `.cmd` and `.bat` command dispatch on Linux, WSL, and
      packaged-install hosts so an extensionless local `cmd` helper found in
      `PATH` is normalized back to `cmd.exe` instead of being treated as a
      custom command processor
    - taught `dashboard doctor` to audit staged helper drift under
      `~/.developer-dashboard/cli/dd/`, report stale or missing
      dashboard-managed helpers such as `_dashboard-core`, and restage them
      with `dashboard doctor --fix` when the currently shipped helper assets
      can repair the runtime
    - added direct regression coverage for extensionless `cmd` shims, stale
      helper staging, and shell bootstrap drift through both source-tree and
      packaged-install paths
    - fixed packaged helper asset discovery so tarball, PAUSE, and `cpanm`
      install-test trees keep resolving `share/private-cli` from a stable
      absolute module source path even after later `chdir` calls in
      long-running test processes
    - hardened the post-build smart-router two-stage Docker guard so one
      transient upstream `cpanm` fetch or unpack failure is retried once
      before the repository packaging gate is treated as broken
    - fixed the installer docs so they match the real checkout/master
      bootstrap behavior and the current shell-bootstrap repair workflow

3.45  2026-05-05
    - fixed Unix-like `install.sh` so blank streamed installs no longer fall
      back to the stale `Developer::Dashboard` CPAN target; when no explicit
      `DD_INSTALL_CPAN_TARGET` override is set they now install the current
      checkout directly or clone GitHub `master` into a temporary checkout
      for `curl ... | sh` style bootstraps
    - added `tmux` to the shipped `aptfile`, `apkfile`, `dnfile`, and
      `brewfile` bootstrap package sets because `dashboard ticket` is a
      first-party tmux workflow
    - fixed blank Ubuntu bootstrap coverage so Debian-family installer docs
      and tests now prove the extra XS build header requirement for the live
      `cpanm` dependency chain instead of silently assuming the base image
      already ships it

3.43  2026-05-05
    - fixed managed helper staging so upgraded home runtimes remove
      dashboard-owned older flat helpers from `~/.developer-dashboard/cli/`
      and keep the active built-in helper surface converged on
      `~/.developer-dashboard/cli/dd/`
    - added direct staged-helper regression coverage so the managed
      `dashboard shell bash` helper itself must emit the ticket tmux status
      bootstrap after staging, not just the repo checkout command path
    - fixed the blank-environment integration image so host-built tarball
      installs now include the native CPAN build packages required by
      XML::Parser, Net::SSLeay, and related transitive prerequisites
    - switched the blank-environment integration image to a real Debian
      Chromium runtime so browser smoke checks no longer die on the Ubuntu
      snap-wrapper launcher stub
    - fixed the host-side blank-environment launcher so it rebuilds the
      integration image from the current Dockerfile before running the
      installed-runtime smoke, preventing stale cached images from hiding
      browser-runtime fixes

3.42  2026-05-05
    - fixed blank-mac `curl ... | sh` bootstrap so `install.sh` now
      bootstraps Homebrew automatically before reading the repo `brewfile`
      and continuing the normal macOS package flow
    - fixed Linux runtime lifecycle isolation so `dashboard restart` and
      `dashboard stop` no longer adopt or kill Developer Dashboard web and
      collector pids that belong to a different pid namespace such as a
      sibling Docker container
    - added Docker-style rolling detail lines to `dashboard skills install`
      so long-running dependency manifests keep the epic task board visible
      while streaming the newest manifest output under the active step
    - fixed tmux ticket-session prompt/status behavior so existing and new
      `dashboard ticket` tmux sessions suppress inline prompt indicators,
      move the full indicator strip into the first row of a two-line bottom
      tmux status block, keep tmux's normal indexed session/window row
      beneath it, preserve TT-backed percentage indicators and live
      collector values, refresh automatically through tmux status updates,
      and leave ordinary tmux sessions on the normal inline prompt path

3.40  2026-05-05
    - moved prompt indicators out of inline shell prompts and into tmux
      `status-right` whenever the generated shell bootstrap runs inside tmux,
      while keeping the existing prompt indicators outside tmux
    - added a dedicated `dashboard ps1 --mode tmux-status` formatter and a
      `--no-indicators` prompt path so bash, zsh, POSIX sh, and PowerShell
      all share the same tmux-aware indicator behavior

3.37  2026-05-02
    - added an explicit InternalCLI coverage guard for installed layouts where
      File::ShareDir already returns the private-cli root, keeping the numeric
      Devel::Cover gate at 100 percent after the Windows bootstrap fixes
    - added a Windows smoke guard that re-enters a fresh profile-loaded
      PowerShell session after install.ps1 so the streamed bootstrap must
      resolve `dashboard`, print `dashboard version`, and run
      `dashboard logs`

3.36  2026-05-02
    - fixed the generated PowerShell profile bootstrap so fresh Windows
      sessions no longer try to pass the multi-line `dashboard shell ps`
      output array straight into `Invoke-Expression`
    - fixed the streamed Windows installer so `irm .../install.ps1 | iex`
      now writes a self-contained profile block that can load `dashboard`,
      `local::lib`, and the PowerShell prompt bootstrap in future sessions

3.35  2026-05-02
    - fixed the installed private helper asset lookup on Windows so
      `dashboard init` no longer stops at an empty
      `MSWin32-x64-multi-thread/auto/Developer/Dashboard/private-cli`
      directory when the real shipped helper assets live under
      `auto/share/dist/Developer-Dashboard/private-cli`
    - fixed the streamed Windows checkout bootstrap so a blank host can now
      finish `dashboard init` after `irm .../install.ps1 | iex`, stage the
      home helper runtime, and continue into the generated PowerShell shell
      bootstrap cleanly

3.34  2026-05-02
    - fixed the installed private helper asset lookup so Windows local::lib
      installs no longer look for `_dashboard-core` under the wrong arch-auto
      path when the shared dist asset root actually lives under
      `auto/share/dist/Developer-Dashboard/private-cli`
    - fixed the streamed PowerShell bootstrap so `install.ps1` now runs
      `dashboard init` before activating `dashboard shell ps` in the current
      shell, ensuring the staged home helper runtime exists before the
      bootstrap asks for PowerShell shell wiring
    - fixed the generated PowerShell profile guard so future sessions only ask
      `dashboard shell ps` for bootstrap output after the staged home helper
      runtime exists under `~/.developer-dashboard/cli/dd/_dashboard-core`

3.33  2026-05-01
    - fixed the packaged install metadata so blank Windows hosts no longer
      pull test-only dependencies such as `Plack::Test` and `Test::Pod`
      during `cpanm --notest Developer::Dashboard`, which previously dragged
      the `Test::SharedFork` chain into the streamed PowerShell bootstrap and
      broke fresh installs
    - fixed the streamed Windows checkout bootstrap so it now installs from
      the current GitHub checkout shape cleanly on Windows, including the
      Windows-safe `Makefile.PL` version path and the self-healing private
      helper staging path used by lightweight commands such as `dashboard
      encode`
    - fixed the PowerShell bootstrap so `install.ps1` now sets the CurrentUser
      execution policy to `RemoteSigned` before writing the generated profile,
      allowing new PowerShell sessions to load the `dashboard shell ps`
      bootstrap instead of failing with `running scripts is disabled`
    - fixed the generated PowerShell profile block so `PERL_MB_OPT` is now
      written with format-string quoting instead of nested literal quotes that
      broke new sessions with `Unexpected token '$ddInstallRoot""'`
    - replaced the repository PSGI test dependency on `Plack::Test` with a
      local harness under `t/lib` so the release metadata can stay focused on
      real runtime requirements while the web route coverage still stays
      intact
    - corrected the install bootstrap docs so the Windows examples use the
      `install.ps1` entrypoint and describe the lighter packaged install path

3.30  2026-05-01
    - fixed the streamed Windows bootstrap so native `winget` command output
      is written to the host terminal instead of leaking into typed helper
      return values, which previously broke the Strawberry Perl path hand-off
      during `irm .../install.ps1 | iex`
    - fixed the Windows Perl bootstrap so `install.ps1` no longer tries to
      self-install `App::cpanminus` while the downloaded `cpanm` bootstrap
      script is still running, avoiding the Windows file-replacement failure
      that broke the `local::lib` setup step on blank hosts
    - documented the exact streamed PowerShell bootstrap command as
      `irm https://raw.githubusercontent.com/manif3station/developer-dashboard/refs/heads/master/install.ps1 | iex`
      and updated the Windows install docs to describe the standalone
      `cpanmin.us` plus `local::lib` bootstrap flow accurately

3.28  2026-05-01
    - renamed the Windows checkout bootstrap entrypoint from `install.ps` to
      `install.ps1` so the shipped filename matches normal PowerShell script
      expectations while keeping the streamed `irm ... | iex` flow explicit
    - hardened the Windows bootstrap winget path so package installs are
      pinned to the community `winget` source and the installer performs one
      `winget source reset --force` plus source refresh retry before failing
      with a clear HRESULT-style exit code

3.27  2026-05-01
    - added a repo-root `install.ps1` bootstrap entrypoint for Windows
      PowerShell hosts so blank-machine installs can bootstrap Git,
      Strawberry Perl, Node.js, user-space Perl tooling, and the dashboard
      runtime from a checkout or a streamed `irm ... | iex` flow
    - updated the install bootstrap docs, release metadata guards, and
      shipped tarball asset checks so the Windows bootstrap path is tracked
      alongside the existing Unix-like `install.sh` flow

3.26  2026-04-30
    - changed the blank-container tarball installation gate to use
      `cpanm --notest` after the normal source-tree test and coverage gates
      have already passed, so packaged dependency resolution and installed
      runtime behavior stay verified without rerunning the full tarball test
      suite
    - updated the blank-environment integration runner, release docs, and
      integration guardrails so the post-build `t/44-smart-router-two-stage.t`
      check and the broader tarball integration flow both follow the same
      `cpanm --notest` policy

3.25  2026-04-29
    - fixed the installed Dancer web stack so the public `/app`, `/ajax`,
      `/js`, `/css`, and `/others` routes now reuse the backend smart route
      dispatcher instead of bypassing skill-local namespace resolution
    - added PSGI coverage that proves top-level and nested skill-local app,
      ajax, js, css, and others routes all work through the real installed
      web route layer
    - added the post-build `t/44-smart-router-two-stage.t` guard and wired the
      blank-environment host integration launcher to run it immediately after
      `dzil build` so extracted-dashboard routing regressions are caught at the
      tarball stage instead of only in source-tree tests
    - fixed helper-login blank-editor rendering so a helper user who lands on
      the root editor path still sees helper chrome, including the logout link
      and helper username, during installed integration runs

3.24  2026-04-29
    - added skill-local web namespaces for `dashboards/ajax/*` and
      `dashboards/public/{js,css,others}/*` so installed skills can serve
      saved Ajax handlers and public assets without copying them into the
      shared dashboard roots
    - added longest-prefix route resolution so `/app`, `/ajax`, `/js`,
      `/css`, and `/others` can distinguish skill prefixes from normal nested
      global paths and can also resolve nested child skills under
      `skills/<repo>/...`
    - kept nested global saved-bookmark assets and saved Ajax files working
      even when their leading path segments match an installed skill name

3.23  2026-04-29
    - fixed the README source-of-truth process so the checked-in README.md is
      regenerated from the canonical Developer::Dashboard POD instead of being
      hand-edited independently
    - added a checkout sync helper and release-metadata guard so README.md
      must exactly match the generated Markdown output from the canonical POD
    - fixed the JS fast-check wrapper so npm update-notifier chatter does not
      break the stderr-clean fuzz gate on newer npm releases

3.22  2026-04-29
    - regenerated README.md as real Markdown instead of accidental POD content
    - added a release-metadata guard so README.md cannot silently regress back
      into POD format

3.21  2026-04-29
    - removed the final stale API Dashboard POD example from the public
      dashboard entrypoint so the extracted API and SQL dashboards are now
      fully pruned from core code, docs, POD, tests, and shipped assets

3.20  2026-04-29
    - fixed bare dashboard skills install so a first successful install from
      the home root ddfile is reported as installed even when the skill ships
      no .env VERSION metadata, instead of showing unknown with blank version
      markers
    - fixed named collector stop summaries so dashboard stop collector NAME
      still reports the stopped collector row when the managed loop is alive
      but its process title has not become observable yet

3.19  2026-04-29
    - extracted optional browser workspaces out of the core distribution,
      leaving the core runtime seeded-page set and manuals focused on the
      features that still ship in Developer Dashboard itself
    - removed core seeded-page handling, runtime bootstrap wiring, release
      metadata assertions, and shipped browser tests for the extracted
      optional browser workspaces
    - moved the extracted page assets, dedicated tests, and supporting
      notes out of the core repository tree so the default core tarball no
      longer carries that optional feature set

3.19  2026-04-29
    - fixed dashboard stop and restart so they follow the saved managed
      listener port back to the real serving pid when the live web process
      renames itself into the underlying starman master listener shape
    - fixed minimal Docker lifecycle control so container stop/restart runs no
      longer lose ownership of the active listener after startup
