Skip to content

Weatherdeck 0.9.0

Choose a tag to compare

@Moshik21 Moshik21 released this 16 Oct 17:09
· 6 commits to main since this release

Watch the 63-second tour (demo data, sound on): https://github.com/Moshik21/weatherdeck/releases/download/v0.9.0/weatherdeck-launch-sound.mp4 (a silent version with captions burnt in: https://github.com/Moshik21/weatherdeck/releases/download/v0.9.0/weatherdeck-launch.mp4)

Status. Early software from one maintainer. On the first run of the test suite on GitHub's hosted runners (2026-10-16) macOS and Linux passed and the hosted Windows image failed 13 of 433 tests; they were fixed the same day (mostly tests: large Node inputs, a code-page decode, simulated-Linux assumptions; plus one real fix: the merge-check script left a scratch clone behind on Windows) and a second run passed all 18 jobs (Ubuntu, macOS, Windows; Python 3.9 to 3.14). The fixes are on main, not in this 0.9.0 zip. Details below are the changelog for this version.

[0.9.0] - 2026-10-16

Changed: a silent feed reads as silent, with the machine's liveness from its beat

  • A source that went quiet now says how long, against the interval it declares ("Late 1 min (missing after 4 x 20 s)", "Silent for 6 min (missing after 4 x 20 s)"), on the Sources list, in the evidence line, on the machine row and in the trust line. A source that declares no interval_s says "interval not declared" and is never called late or missing.
  • The machine behind a silent source says what its beat shows: "machine last seen 40 s ago via its beat", "machine not seen for 20 min, its beat is silent too" (the beat stopped as well), or "machine not seen" (no beat was ever seen). Silence alone never reads as down.
  • The view gains two derived fields (no new feed field): a source's interval_declared and a machine's beat_seen. The wording is in defaults.json (src_say_*, machine_seen_*, machine_unseen).

Added: Linux lingering preflight, a beat with every join, a doctor check

  • weatherdeck join --dry-run checks lingering on Linux (not WSL, nor a host without a systemd user manager unless --scheduler cron or system is chosen). A systemd user unit is stopped when the
    last login session of its user ends, unless lingering is on, and returns only at the next login, so the board shows the machine missing in between.
    With lingering off the plan is not ready (LINGER_OFF, with the one-line remedy loginctl enable-linger <user>); a lingering state that could not be read
    is LINGER_UNKNOWN, never a pass. --scheduler cron (a crontab line) or --scheduler system (a system-level unit) is the named alternative and does not
    need lingering. The join never changes lingering, and removing it leaves lingering as found. An adapter's pusher that is a user unit is named at risk too.
  • Every join plans the beat (beat-<machine>, docs/beat.md) beside any adapter pusher on the machine, registered the same way and under the same lingering rule.
    The dry run prints that plan, what it leaves alone and how to undo it.
  • weatherdeck doctor has one more check, systemd user timers survive logout: lingering and the next elapse of a Weatherdeck user timer; warn with the
    remedy when lingering is off, not applicable on macOS, Windows and hosts without systemd. Advice only; doctor never runs enable-linger.

Changed: open-source text

  • MIT licensed (see LICENSE); README, CONTRIBUTING, SECURITY, SUPPORT and CODE_OF_CONDUCT (Contributor Covenant 2.1) carry real text, with reports routed through GitHub's private vulnerability reporting and issues, and no placeholders left.

Changed: the demo never shows this machine's name, and the README gives the demo's silence its real timing

  • weatherdeck run --demo serves as "demo host" when hosts.label is not set, where it used to show the machine's own hostname on the page. A configured hosts.label still wins, and a real (non-demo) host still defaults to its hostname.
  • README: the demo's silent source stops reporting after about two and a half minutes, shows late from about 2:40 and silent about three minutes after start (the README had put silent at two and a half minutes).

Removed: the bundled adapter left this repository (the product and the feed are untouched)

  • adapters/ is gone. The tool-specific adapter that lived there (its pusher, replay, installers, host preset, runbooks, fixtures and its
    own tests) is kept and built in its own repository now. Nothing a running install uses changes: every machine runs an installed copy, and
    the next package comes from there under the same names and layout. To feed the board from your own tool, see
    docs/adapters.md; docs/runbook-windows.md and docs/runbook-macos.md
    are the host guides.
  • scripts/build-package builds the product zip only (weatherdeck-<version>.zip). It no longer reads adapters/*/packages.json; a
    package that bundles an adapter (its pusher, or a host package of the product plus the adapter's files) is built from the adapter's
    repository, which runs this script for the product part.
  • scripts/self-audit exempts nothing by default (it used to skip adapters/ in the vendor check; --exempt DIR still marks an adapter
    directory), and --denylist can be given more than once. Its built-in vendor list keeps only the generic AI-provider and VPN names; the names of
    tools the core meets only through an adapter belong in a private denylist kept outside the tree.
  • MIGRATION.md is gone. The schema-id table (why the wire ids still say lookout.*) is now the "Schema ids" section of
    docs/feed-spec.md, with the ids unchanged.
  • scripts/merged-check, the CI workflow, CONTRIBUTING.md, the README and the pull request template no longer mention the adapter's suite,
    replay or directory: the core suite (python3 -m unittest discover -s tests) is the one suite here.

Changed: a merged PR is never trouble or an ask, and old records fold under Housekeeping (readability slice 3; the feed is untouched)

  • No agent gone after a merge. A lane whose PR merged is never agent_gone, whatever its state says (src/weatherdeck/lanes.py
    agent_gone). A lane paused after its PR merged, whose worker's session then ended, read AGENT GONE and was counted off course;
    the exemption covered only a failed, done or unknown state, as the feed spec's "No agent at the work" already promised for any state.
  • An approval whose PR merged or was closed is an old record. It was already left out of the waiting-on-you number; its row no
    longer reads Waiting on you or Finished among the live rows, and its sailor is not at the rail.
  • Housekeeping holds every old record the board can tell from the feed alone, each with its reason as its sentence: an old approval
    ("Old approval: PR #1 merged, nothing left to approve."), a lane whose PR merged and whose worker's session ended without the lane
    being closed ("Finished but never closed: ..."), and the rows that read Can't tell only because their books are old ("Old record:
    ..."). The Lists page folds them under "Housekeeping: N old records", closed until tapped; the phone's list ends with the same fold; the
    sea's "+N more" line counts them as "N old records". None of them is counted as waiting on you, off course (including a project's head
    on the Gun deck), a trouble badge, the Gun deck door's outline, a Problems line, the waiting count or the waits outside the fleet, and the page never closes or
    changes any of them. Words are hk_* in labels (the rs_approval_merged, rs_approval_closed, rs_gone_merged and rs_unread
    sentences are now hk_approval_merged, hk_approval_closed, hk_gone_merged and hk_unread). The host's counts.off_course still
    counts an old record whose only sign is its books' words, which only the page reads. See docs/theme-spec.md 24.3.

Changed: approval lanes join the waiting-on-you number, one ask is one line, and a "sources disagree" badge (readability pass, second part)

  • A lane that waits for your OK is counted. A lane that says it waits for the viewer's OK (handoff.state: awaiting_you) with
    its PR open (link.state: open), and that no call names, is an ask of its own: it joins the one waiting-on-you number on the
    call band, the Lists header, the Bridge door and the ship plates, and is drawn as its own line ("Waiting for your OK: <title>")
    with its own reason ("Its lane says it is waiting for your OK; has not raised it as a call", naming the source that
    raised the calls when exactly one did). A raised call keeps its own reason line. A lane whose PR merged or closed is not counted.
  • One ask is one line. A call whose item names the lane, or, with no item, whose link.url is the lane's link.url,
    absorbs the lane. The host decides it (src/weatherdeck/view.py lane_asks) and marks the lane lane.asks_you in /api/view;
    counts.waiting_on_you counts the calls plus those lanes.
  • The number says how old it is: "as of 3 min ago" beside it on the call band and the Lists header, from the oldest report
    behind its asks (counts.calls_snapshot_at when the view carries one, else when the host received each feed); "as of: not
    reported" when no ask has a known time.
  • "Sources disagree". When the calls and the lanes that say they wait for the viewer's OK report different sets (a lane no
    call names, or a call whose lane does not say it waits), a small badge sits beside the number, and its detail lists each
    difference: under the Lists header, under the calls on the band, and on the Bridge under "Sources disagree". The view lists them
    as waiting_disagree. Nothing in it is counted, and the board closes nothing.
  • The Bridge heads the lane asks "Lanes waiting for your OK"; its rows no longer squash when the list is longer than the place,
    the list scrolls instead.
  • A call or lane ask with no start time says "Waiting since: not reported" on the Lists page (was "Waiting ?").

Added: an instruction left unread says so on its lane's row (readability slice 12, the page half; the feed spec is untouched)

  • An idle lane with an unread instruction shows one amber line on its row on the sea's log, the page board and the Lanes deck:
    "An instruction from atlas has sat unread for 25 min" (web/frontdoor.js steerNote). An instruction whose doorbell never
    reached the worker says so whatever the worker is doing: "An instruction from atlas was never delivered (12 min ago)". A line
    the source marks escalated sorts with Stuck. The opened row says how many wait unread, the oldest's age, how often it was
    rung and whether the worker is idle.
  • It reads only fields a source may send on its own lane word, proposed and not yet in the feed spec: lane.unread_steer
    {n, oldest_at, rings, escalated} (null: it looked and found none), lane.undelivered_steer {n, oldest_at} and lane.idle.
    No source sends them yet. A source without them says nothing about unread instructions on its rows, and its opened row says
    "Unread messages: not reported", never 0. Words are in labels (steer_*). See docs/theme-spec.md 9.5 and 24.2.

Changed: one sort on the sea and the Lists page (readability slice 6; the feed is untouched)

  • Both front doors list the lanes in the same order: Waiting on you, Stuck, Can't tell, Working (by project), Waiting on someone else,
    Finished, and the old records last. The sea led with the oldest failed record and the Lists page with what waits on you, so they disagreed
    about what comes first. One function (web/frontdoor.js sortRows) and one list (display.row_sort in defaults.json) now order both.
  • Longest-waiting first inside Waiting on you and Stuck, from the host's age in state; a row with no age follows the rows that have one
    and says nothing of how long.
  • An old record never sorts above live work. A row that reads Can't tell only because its books are old goes last, and on the Lists page
    into a folded "Housekeeping: N old records" line. It is drawn calm on the sea, and is not counted off course (the log's header, the phone's
    list, a ship's trouble badge); the "+N more" line counts it as "N old records".
  • The Lists page is now one card for each row word, with the goal and project cards where Working falls (each still counts all its lanes);
    the sea's log is one list, and its "+N more" line counts the rest in the six words (log_overflow, log_on_more and log_rest are
    now log_more).

Changed: one waiting-on-you number, and alarms out of it (readability pass, first part)

  • The Bridge door counts the calls only. It counted the critical alarms too, so it could say "6 wait on you" beside a call band
    of 5. The call band, the Lists header, the Bridge door and the ship plates now show one number, from one rule (web/frontdoor.js
    cardTier, waitingOnYou): every attention card that is neither critical nor info.
  • The host says the same number as counts.waiting_on_you in /api/view (one per ask; counts.calls is unchanged).
  • A critical is a Problem, on its own line, never counted. The sea's alarm bar and a place's bar say "Problem" (was "Critical"),
    the Bridge heads it "Problems", and the Lists page's second list is now Problems (was "Needs attention", which read like the
    waiting number beside it).
  • An info card is an FYI, never counted. The call band and the calm plate say "FYI:" before it (was "Also:", which an
    unverified share of the number keeps), and the Bridge lists it under its own "FYI" heading.

Changed: the supervision alarm says whose it is and how old, and is critical only on evidence (readability, page only; the feed is untouched)

  • A service:supervision:<source> card's title names the home by its source's name and says its heartbeat's age in words
    ("cinder's supervisor hasn't checked in for 6 min") instead of "supervision is not beating"; how to act names the home
    too, never "this home". A card whose detail carries no age says "for how long: not reported".
  • The page sets the card's severity. With no supervisor object in the feed (none sends one yet) the card is FYI with
    "busy or stopped? not reported" while its heartbeat is younger than thresholds.supervision_stale_critical_s (1800 s) or
    its age is not reported: the watcher-down marker such a card may be raised on is written on every ordinary wake. A
    heartbeat at least that old stays critical, "hasn't checked in for 40 min (no busy marker reported)". With one, it is
    critical only when no watcher runs and the oldest unacknowledged wake is older than
    thresholds.supervision_wake_limit_s (300 s); busy reads "busy in a long turn". See docs/theme-spec.md 9.3.

Changed: plain rows and titles that read on the front door (readability slices 4 and 5; the feed is untouched)

  • Every lane row says one of six words and one sentence, on the sea's log and on the page board (the phone's front door):
    Waiting on you, Stuck, Can't tell, Working, Waiting on someone else, Finished. The word comes from the host's lane.kind and the
    source's own lane word (web/frontdoor.js rowState); the sentence from the source's say[] parts, or, for a feed with no lane
    word, from its structured fields only. No row shows "run failed", "superseded", "harness", "unknown missing" or a task id, and the
    log's "+N more" line counts in the six words.
  • An old failed record no longer reads FAILED. A lane with no lane word whose only lines are its books' "status-log superseded"
    or "harness state unavailable" reads Can't tell with its last news and when; a lane whose run failed or whose worker's session
    ended while its own records say it was waiting reads Waiting on someone else; a merged PR reads Finished. The page board's Needs
    attention list follows the row words.
  • Titles cut only at a whole word, never leave a bracket open and never fall to one word: the project chip gives way first
    (fitTitle). A title the source already cut loses its partial last word; no title reads "title not reported", never the task id.
  • The words "run failed" in the page board's sentences read "its last checks failed". All words are in labels (row_*, rs_*,
    rcount_*, title_not_reported).

Changed: the bundled adapter's tests moved next to it (tests only)

The 93 tests that load or run the bundled adapter now live in adapters/*/tests/ and run with
python3 -m unittest discover -s adapters/*/tests. The rest of tests/ needs nothing under adapters/ and passes with it
deleted. Five files were split by class (test_boardlink, test_goals, test_lanestate) or by test (test_calloptions, test_stalecalls); three
moved whole (test_quotause, test_pusherhonesty, test_replayschema). The adapter's tests take the core from this repository's src/, or from
WEATHERDECK_SRC, and stop with a message when neither has it. The CI workflow and scripts/merged-check run both suites. No code and no
behaviour changed, and no test was added or removed.

Added: waiting.note, the card that explains an unverified waiting number (feed 1.11, adapter 1.23)

  • Feed 1.11 adds one optional field to waiting: note, the id of the source's own attention card that says why its waiting number is
    unverified (docs/feed-spec.md). The page already shows that source's "unverified" line with its reason, so it drops the
    card whose id equals note (the same source's only) and no other. Before this the page found that card by matching an id prefix that
    only the bundled adapter used; any source can now name its own. Additive: an older host ignores the field, and a source that sends none
    behaves as before. Without note the page lists that card as well as its own unverified line, so the same fact reads twice until the
    source sends note; nothing is hidden and no number changes.
  • Bundled adapter 1.23 sends waiting.note (the id of its "waiting on you is unverified" card) while its waiting number is unverified,
    and says in producer.meaning[] what an older copy cannot do. A machine still on 1.22 or older shows the card twice, as above, only while
    its number is unverified. The board's other cards and numbers are unchanged.

Changed: the host guide is generic, and an example host config ships (docs and an example only)

docs/runbook-windows.md now covers what a Windows host needs without any fleet detail: a config to start from, host names
(hosts.allowed_hosts), privacy mode with a two-line double-click recipe, health checks by hand (/healthz, weatherdeck-check,
weatherdeck received --summary), the received log, removal, and a troubleshooting table (the Startup-folder fallback, the firewall on
Private networks, the wrong authorized_keys file, a source id that does not match, 403 host names, privacy). New
examples/host/config.example.json is that config with neutral values (a VPN range for the full view, a home LAN
range for the tv view, quiet hours, may_sleep, one alias), and tests/test_hostexample.py loads it through the config loader and checks that
every key is one the host reads and that its bind entries are accepted. Words that named one fleet in docs/beat.md, docs/device-checklist.md,
docs/feed-spec.md, docs/hosting.md, docs/runbook-windows.md and docs/theme-spec.md now read "the host", "a Windows PC", "the owner" or "the
maintainer". No code and no behaviour changed.

Fixed: the layout replay fails when it cannot check the schema (adapter tooling only; the feed is untouched)

The bundled adapter's weatherdeck-*-replay checks each replayed feed against the core's schema, but when it could not import weatherdeck.schema
(a copy of the adapter directory without the core beside it) it skipped that check without a word and printed
26 snapshot(s), 0 failed. It now takes the core from WEATHERDECK_SRC (the core's src/), else ../../src, and when that core does
not import the schema it exits 2 before it records or replays anything: with cannot validate feeds: core not found, or, when
WEATHERDECK_SRC is set, with cannot validate feeds: WEATHERDECK_SRC=<path> has no weatherdeck/schema.py (a set but wrong
WEATHERDECK_SRC no longer falls back to ../../src). An installed or
PYTHONPATH copy of the core is not used in their place. With the core found, nothing changes. Covered by the adapter's tests/test_replayschema.py.

Added: docs/adapters.md, "Write a pusher for your tool"

  • What an adapter reads, maps and pushes; the wire schema in brief; the sinks file and the three transports; the honesty rules for
    adapters (unknown is not zero, never guess, say how old a reading is, silence is a state); and how to test one without the real
    tool by recording the files it reads and replaying them, including the broken ones. The README points to it.

Changed: neutral words in core prose and code comments (no behaviour change)

  • Comments and docs in the core say "the owner", "the viewer" or "the source's own count" where they named a person or a private
    tool. The demo's hold cause is now owner-hold (was a word naming a person), in examples/demo/atlas-main.json and the
    re-recorded examples/static-demo/recorded-feed.json. No feed field, schema id or screen text changed.

Added: weatherdeck invite and weatherdeck join --dry-run (adding a machine, first part)

  • weatherdeck invite --machine ID (on the host) mints a push token for the new source (default beat-<machine>) and prints
    one join string carrying the host's address, the source and machine ids, the expiry and the token, shown once. It refuses an id
    that already has a token or reports (ID_TAKEN), and replaces an invite still open. The invite is recorded in a small registry,
    <data>/invites.json (no token or hash in it; format in docs/onboarding.md).
  • The host enforces the expiry (--ttl, default 15 minutes): until it accepts a first push from the source, a push with its
    token is refused with HTTP 401 "this invite expired ..." once the TTL is over. After a first accepted push the token is an
    ordinary source token. No new route; weatherdeck token revoke <source> withdraws an invite and drops its entry.
  • weatherdeck join --dry-run (on the new machine) runs the preflight only and writes nothing: Python 3.9 or newer, on Windows a
    real interpreter and never the Microsoft Store alias (STORE_ALIAS), a service manager, write access to the config and state
    folders, earlier beat installs, the host's address (refused, timeout, name not resolved) and name (HOST_NAME_REFUSED), its
    clock (CLOCK_SKEW, a warning only), the token through a probe that stores nothing (411 good, 401 refused or expired), and
    whether the source id is taken or the machine id disagrees with what this computer already reports as (MACHINE_ID_DIFFERS, the
    WSL -wsl case). Each failure is named with one next step. The string is read from a hidden prompt or stdin, so the token stays
    out of the process list. Installing the service, the first push and the verify line come later.

Added: weatherdeck doctor and doctor --host say why a machine is not on the board

A machine that pushes and does not show on the board had no single place to ask why. weatherdeck doctor, run on that machine, checks its
join config (or --url, --source and --token-file for a beat set up by hand), the service it runs under (a systemd user unit, a
LaunchAgent or a logon task, read with query commands only) and that the service names a real interpreter (STORE_ALIAS for anything
under \WindowsApps\, BARE_INTERPRETER, INTERPRETER_MISSING), the token file (there, not empty, mode 0600; the token is never
printed), the host (HOST_UNREACHABLE (dns|refused|timeout), HOST_NAME_REFUSED), whether the host accepted a push from this source
within three intervals by the host's own clock
(NEVER_RECEIVED, REJECTED with the host's reason, SILENT), the beat file's push
state (beat 1.2) or an adapter pusher's sink state when one is named, clock skew and version skew. weatherdeck doctor --host reads the
host's data directory: invited sources never seen (invites.json, when there is one), sources and addresses whose last push was refused
(the rejected records and the refusal lines in server.log), push tokens never used, and loopback pushers that are refused now or will be
at the first token. Both are read-only, print one line per check and an overall line, and say not checked: <why> for a check they
cannot make, never ok. The machine side exits 0 only when the host's receipt proves the source live. Linux is checked for real; the
macOS and Windows service readers are tested from injected answers and still need a real Mac and Windows PC. See
docs/doctor.md. No feed, beat or pusher change, and no new route on the host.

Added: scripts/merged-check, and one statement of the supported Python versions

scripts/merged-check merges a branch onto the current origin/main in a scratch clone outside the repository (nothing in your checkout is touched), then runs the self-audit, the full test suite and the adapter layout replay on the merged result and prints one verdict line. It exists because two branches that each pass their own gate can be red together, and there is no hosted CI to look at the merged result. CONTRIBUTING and the pull request template now put it in the definition of done. Python 3.9 to 3.14 is now stated the same way everywhere (README, pyproject.toml with classifiers, CONTRIBUTING, the manual workflow matrix, hosting, the beat and the runbooks), and 3.8 is not supported; the bundled adapter's two install scripts are unchanged by this and still accept 3.8. Nothing in the board changed.

Changed: README first-run wording and neutral ledger names (docs only)

The README now says what the board is not (not a monitoring or alerting system, not hosted, pre-release), how to get the code (clone the repository, or unzip what scripts/build-package builds), where the config file lives (<data>/config.json, with a minimal hosts.bind example), why the example schema id reads lookout.feed, how to remove it, and that Python 3.9 to 3.14 was tested and 3.8 is not supported. "By the captain" in the device table reads "by the maintainer". docs/ledger-spec.md and docs/ADDINS.md name the tools infirmary, lodgings and seaworthy and state once that doctor, harbor and preflight are accepted as aliases; no behaviour changed.

Fixed: the pusher's verify commands tell the truth (adapter 1.22; the feed is untouched)

Three ways a new machine looked fine when its host was not taking its pushes:

  • push --once no longer goes quiet while a host is being retried. A host whose last try did not get through is skipped until its
    backoff ends; that pass printed nothing and exited 0, which is what a person sees when they run the documented verify command right after
    a refusal. It now prints one line, tower: waiting (backing off, next try in 52 s; last: Permission denied (publickey).), and the run exits
    1 unless every enabled host is ok (or has nothing due). A run the pace slows on purpose prints the same line under its slowed line
    for each host whose last state is not ok, and exits 1 too. --force is unchanged.
  • A host that turns the key away is refused, not down. ssh's exit 255 with Permission denied now reads as tower: refused ... and logs
    credential refused, like an http host's 401 or 403. Any other 255 (unreachable, host key not matching) stays down. Backoff and seq handling are
    the same as for down.
  • check asks the host. For an ssh sink it runs weatherdeck-check (the one other command a forced-command key may run; it sends and changes
    nothing) and prints ok sink tower: the host accepted this key (...), FAIL sink tower: the host refused this key (...), or FAIL ... could not reach the host ...; whether it accepts this key was NOT tested. For an http sink it POSTs with no Content-Length and no body: 411 means the
    credential passed, 401 that a token is required (none sent) or not taken, 403 that the host has no tokens yet; the host answers before it reads
    a body or writes its received log. check --no-host skips the question and says NOT tested; both installers (install.sh for Linux and for the Mac)
    pass it, because they run check before the host has the key and a refusal there must not stop the timer from being enabled.
    A timer or launchd job that runs push --once now exits 1 on every pass while a host is down, not only on the passes that try it. The pusher
    version is 1.22 (no MEANING entry: nothing the feed carries or means changed). See the bundled adapter's README (adapters/<adapter>/README.md).

Fixed: the zips carry the community files their documents link, and the README names no script they lack

scripts/build-package left SECURITY.md, SUPPORT.md and CODE_OF_CONDUCT.md out of the product zip (and so out of the tower zip, which carries the product), while CONTRIBUTING.md inside it links all three; they are now included. The README's "Try it without installing" told a reader of the zip to run python3 scripts/build-static-demo, which the zip does not carry (it never carries scripts/); it now says the demo is built in the source repository. The entry stamp is unchanged: two builds of one commit are still byte-identical.

Fixed: pip install no longer succeeds and then crashes; an old Python and a bad port say so plainly

pip install . used to report success, install no command, and python -m weatherdeck run --demo then failed with a FileNotFoundError for examples/demo/scenarios.json, because the page, the demo data and the launcher are found beside the source tree, not inside the package. Packaging them is not small, so the build now refuses at once (pyproject.toml names a refusing backend, pip_not_supported.py, which is in the release zip too) with "Weatherdeck cannot be installed with pip yet: it runs from a source checkout or the release zip", and the README says the same. Python older than 3.9 now stops at startup with one sentence (3.8 used to start and fail later in the pace code). --port outside 1 to 65535 prints weatherdeck: --port must be a whole number from 1 to 65535 (got 99999). and exits 2, instead of an OverflowError traceback.

Fixed: the Windows runbooks no longer lead into silent beats

Docs only, no code and no version change. The beat's loopback push needs no token only while the host holds no push tokens; the first token new for any machine makes it refuse the beat with HTTP 401, and the runbooks now say so and tell the reader to run token list and give the beat its own token. A scheduled task must name the real interpreter, never a bare pythonw.exe or the Microsoft Store alias (the process looks healthy and delivers nothing); the lines that called Store Python "fine" are corrected. Also written down: never schtasks /change (it hangs on a password prompt over ssh; re-create with /create /f), the 261-character /tr limit and the short unpack path, where Store Python really keeps the beat file and log, and the tower preset's display.may_sleep, which now agrees with the tower runbook (mac, laptop; the matching is by machine id, exactly).

Added: scripts/release-scan, and Python 3.9, 3.13 and 3.14 in the manual CI matrix

scripts/release-scan is one command for the pre-release checks: it runs scripts/self-audit (with --denylist FILE when given) and then scans the history of every ref for author and committer names and e-mail addresses that are not on an allow list given by argument (--allow, --allow-file), and for home paths and token shapes in added lines. It prints counts and kinds only, never the text of a hit, and exits non-zero on any finding. See CONTRIBUTING.md, "Release scan". .github/workflows/test.yml (still manual dispatch only) now runs the suite on Python 3.9, 3.12, 3.13 and 3.14 on each of the three systems. Nothing in the product changed.

Fixed: the host says when it refuses a push, and token new says what the first token breaks

A push refused with 401 (no token, or one the host does not hold) or with the "no tokens yet" 403 (a non-loopback caller) left nothing on the host: no server.log line and no line in the received log, so a pusher that had been accepted on loopback and was refused after the first token was made looked healthy from the host's side. Both are now logged through the same path as every other refusal (refused POST /api/ingest from <address>: <reason>), with the real reason (no Authorization header, not a bearer token, or a token this host does not hold; the token itself is never written), at most one line per client address per 10 minutes, the next line saying how many more came since. The last refusal per address is also kept in memory (256 addresses, gone at restart) in server.last_refusals(); nothing displays it yet, and a refused push has no source to key it by, so it is per address only.

weatherdeck token new, when it makes the first push token, now says that loopback pushers without a token are refused from now on, and names the loopback HTTP source ids the received log shows from the last day (or says none were seen). To make that possible the received log's lines for HTTP pushes now carry client, the pusher's address; older lines have none and are never counted as loopback. client is left out of /api/log on every view. No feed, pusher or beat change: the pusher stays 1.21 and the feed 1.10, and the version is not bumped here. docs/hosting.md states the rule in one sentence.

Changed: the machine beat records its push state where people look (beat 1.2)

A beat whose push was refused or could not reach the host said so once, in a log file nobody opens (under pythonw on Windows, in the
interpreter's private cache), and the beat file said nothing. With --push, the beat file now carries a top-level
push: {state, since, code, reason}: state is ok, refused (the host answered 4xx) or down (no answer, or a 5xx), since is when
the state last changed, code is the HTTP status or null, and reason is the host's own words or the connection error, such as
HTTP 401 a valid bearer token is required. It is written in the same sample as the push. A push that stays down is now said in the log
again every hour (with how long it has been down); the first line of an outage and the line on recovery are as before, and sampling is
never held longer than the 5 s request timeout. The key is the file's own, outside machine, so the pushed feed (apart from producer.version reading 1.2) and the bundled
adapter's reading of the file are unchanged, and lookout.beat stays at 1.0. docs/beat.md describes the key. Contract
tests (tests/test_beatpush.py) use a stub host answering 401 and an injected clock.

Fixed: the example pusher says what it is doing and names a missing token file

examples/adapter/push_example.py now prints one line when it starts (which hosts it pushes to, and that it repeats every 30 s until Ctrl-C, or pushes once with --once), where it used to run silent. A token-file: credential whose file does not exist is reported as token file not found: <path> instead of the host being "down". examples/adapter/sinks.example.txt now starts with a line that works on a clean machine: local-board at 127.0.0.1:8787 with no credential (-), which a host with no tokens accepts from loopback; the file explains what to change once the host has a token.

Fixed: the value-ledger reader knows the renamed tools

The reader had profiles only for harbor, doctor and preflight, so a ledger written under a new name (Lodgings already writes "tool": "lodgings" into lodgings.jsonl) read as a tool with no rule: no value events, no shares. lodgings, infirmary and seaworthy now read the profile of their old names (same events, verbs, shares and counter names; no counter changed), and the old names keep reading exactly as before. A tool name with no profile reads as it did. ledger.tools now defaults to weatherdeck, infirmary, lodgings, seaworthy (the old names still work when listed). docs/ledger-spec.md names the new files and says what each renamed tool writes today.

Changed: Lookout is now Weatherdeck (breaking for every running install, no shim)

The dashboard is renamed throughout: the weatherdeck command (bin/weatherdeck, bin\weatherdeck.cmd), the adapter and beat commands, the weatherdeck Python package (src/weatherdeck/), the WEATHERDECK_ environment variables, every state, config, cache and key path, the
logon and beat task names, the launchd label, the systemd unit and cron marker names, the ssh command words
(weatherdeck-check, weatherdeck-ingest), the package names (weatherdeck-<version>.zip, weatherdeck-tower-<version>.zip, the pusher zips), the value ledger file
(weatherdeck.jsonl, now also written by the host's own ledger writer, weatherdeck ledger write, with its lock file and key domain strings), the ledger cards' default tool name (ledger.tools), the static demo's page title and browser globals, the browser storage keys and cookie names, and the product name on the page, in the docs and in the scene. The ordinary
word "lookout" stays as scene vocabulary for the crow's-nest character (the ship-deck crow and its crow's-nest label).
Nothing answers to the old names. Migration: a MIGRATION.md listed old to new for each kind of install, each with the one command
that verifies it (it has since left this tree; the schema ids it explained are in docs/feed-spec.md, "Schema ids").

The feed, card and beat schema ids (lookout.feed, lookout.card, lookout.beat) and the concept fixtures' lookout.fixture
are unchanged on purpose: they are wire-protocol identifiers every deployed adapter and the live tower already speak, not
branding (docs/feed-spec.md, "Schema ids"). A future major version may add a new-name alias accepted alongside, never a replacement.

Fixed: a placeholder source no longer reads as a failing feed

A source that declares itself a placeholder (named by another source or owning relayed work, never seen by this host, such as a
project that only ever appears through the main source) was counted in the total of the trust line ("4/5 live") and named first
among the problems ("X is not reporting") ahead of real notes. It is now left out of the total, the live count and the named
problems, on the sea's trust line, the page board and phone header and strip, and the ship plan's Crow's nest door. It is still listed
as not reporting in the Crow's nest and the Sources list, and a placeholder that does report is counted like any source. A source that
was seen and goes quiet is counted and named as before. The count rule is trustCounts in web/frontdoor.js, with contract tests on
synthetic sources.

Changed: the open sea is the front door again; the page board is a door away

Since 0.7.0 the page board was #/sea on every size, so a TV, which cannot scroll, showed only the call lists and never the fleet.
Now, on any window 700 CSS px or wider, #/sea (and no route) is the full-screen stage again, as in 0.6.0: the fleet on the water,
one ship per machine with its name plate, sky and weather by time of day, the headline card for the calls on the left and the log
with the allowance along the bottom. The ship opens its plan, a place its list, and the plan's OPEN SEA comes back out, with the
camera moves of 0.6.0 (theme spec section 24.4).

  • The page board, unchanged, is now #/board. It is opened from a Lists door beside the ? on the sea (the theme's new zone
    lists) and from the same door in a ship's plan bar; its header has an Open sea door ("Back to the open sea"), and Esc and
    Back go up to the sea. An idle TV left on it sails home after display.idle_return_s, as from a place.
  • The television profile (the tv view, any household-level profile, or privacy mode) never shows the page board: the sea is the
    whole display there and #/board draws the sea.
  • The headline card opens the Bridge on every profile, as in 0.6.0.
  • The default theme's place_sea is now "Open sea" (was "Board"), so the ship plan's way out reads OPEN SEA in every theme.
  • On a phone #/sea is still the page board, as before.
  • Each ship plate on the sea carries a quiet badge: the calls waiting on you from that machine and its lanes off course plus its
    critical alerts, each a glyph and a number (a call naming no machine counts on its source's machine; one naming neither stays on
    the card only), so nothing that needs you hides when the board is no longer the front door.
  • The sea's ambient loop now parks off screen only where its picture can scroll away (the page board's picture slot, the phone's
    banner), so the TV stage's sea never stands still while it is shown.
  • The ship-deck and default themes' since plate (and ship-deck's open-sea banner) end before the new Lists door. New labels:
    place_board, open_board, back_to_sea, ship_calls, ship_trouble. The route and badge rules live in web/frontdoor.js, with contract
    tests on synthetic data (tests/test_frontdoor.py). Checked headless at 1920 x 1080 and 390 px; the device check on the TV is
    open (docs/device-checklist.md).

Added

  • The library (feed 1.10, mirrors[]; feed spec, "Library"): a source that mirrors folders of notes into a shared repository says, per
    folder, when its sync job last ran, how that run ended, origin's newest change to the folder and what is not in the repository yet,
    copied from the receipt the job writes on every run. The host gives each folder one word from a closed set, each with one rule
    (src/weatherdeck/gaps.py mirrors): UP TO DATE (a live source, a receipt within its limit, a run that pushed or found nothing new,
    nothing pending), WAITING n, FAILING, NOT CHECKED (the job's last run is older than its limit; "the board cannot tell why"), SILENT
    (the source is silent; the last word is kept as last known and the ages stop), UNKNOWN (a receipt the rules cannot read, never 0),
    NOT REPORTING (a folder the new host config mirrors.expected lists and no source reports) and RETIRED (only from mirrors.retired
    or the row's own retired, never from age). It raises no card. The board draws it as a LIBRARY chip row on the TV log and in the
    page board's quiet rows, worst first, and as a LIBRARY section on the phone and in the Crow's nest. The household view keeps the
    folder, the word and the ages and drops the job's words and the retirement note; the public view has no library. Contract tests on
    synthetic data for the derivation and the 1.10 field.
  • Bundled adapter 1.21 (adapter README, "Library"): mirrors: [{"receipt": path}] reads the knowledge sync's receipt
    (fm.knowledge-receipt version 1, written atomically to the home's state/knowledge-sync.json; the README is its contract for the
    sync script) and sends one mirrors[] row per folder, plus a mirrors collector that is an error for a receipt that is missing,
    unreadable, of another format or partial, and stale for one older than three of its job's intervals, so the trust line names it
    even on a host that does not draw the row. check prints each receipt. A MEANING entry says what an older adapter cannot.

Fixed

  • scripts/build-package stamped every zip entry with the newest file mtime of the working tree, so a fresh clone of the same commit gave
    different zip checksums with identical contents and checksums could not be compared between machines. Entries now carry one fixed time:
    SOURCE_DATE_EPOCH when set, else the commit time of HEAD, else 1980-01-01 outside a git checkout; the build prints it. Two builds of
    one commit with the same Python and zlib on the same OS, in different clones, are byte-identical (checked with sha256sum on two fresh
    clones); across machines or OSes, compare per-file content as the README shows (file modes still differ on a Windows checkout). Nothing packaged, no name and no version changed.
  • Every redacted view (household, guest, public) now drops a collector's error text from sources[].freshness[] too, as it already
    did from sources[].collectors[].