Follow-up to the salvaged #111044 commits:
- self_removal_delivery_allowed() now also requires that no record currently
holds the job id. The marker alone said "this run removed its record"; it did
not say the id is still empty. A replacement record (another owner reclaiming
the id) must be treated as a stolen claim, not a self-removal.
- Drop the allow_self_removed kwarg on fire_claim_fence: the fence already has
the job_id and the ContextVar marker, so it can decide on its own; the caller
no longer threads a flag it computed from the same predicate.
- _FireOwnership.lost(): keep the explicit lost event and the no-owner short
circuit ahead of the self-removal check so an interrupted run is still
reported as lost even after it removed its record.
- _finish_completed_run: skip mark_job_run entirely for a self-removed job
(nothing to mark) instead of calling it and then excusing the False.
- Tests trimmed to two invariants, both A/B'd against origin/main: the
self-removing run delivers after a post-removal heartbeat tick (RED on main),
and a self-removal followed by a replacement record is still discarded
(GREEN on main, guards the new predicate).
- Docs: user-guide cron.md notes that a job may remove itself and still report.
A run that deletes its own job after the first heartbeat interval was still marked stale because the fire-claim loop treated a missing record as lost ownership before the self-removal marker could win.
Co-authored-by: Cursor <cursoragent@cursor.com>
Asserting the loud variant via the Tailwind `font-medium` class couples
the test to styling: any restyle of the accent breaks it without the
behaviour changing. Give the note role="status" (it announces which
profile edits land in) and a `data-scope-loud` flag for the non-default
variant, and assert those. Also fixes the padding-line lint warning in
settings-scope.test.ts.
config-settings.test.tsx mocks @/store/settings-scope with a hand-written
subset of exports; SettingsProfileScope now also reads
$settingsScopeProfile and $settingsScopeEditsNonDefault, so the partial
mock threw at render time in CI (JS & TS checks red).
Rewrite of the original component-local `editingNonDefault` computation
onto the existing scope architecture (owner's ask: "use the same
existing architecture" as the Capabilities/toolset scope handling).
- store/settings-scope.ts gains `$settingsScopeEditsNonDefault`, a
computed over `$settingsScopeProfile` × `$profiles`, so "the settings
pages are editing a non-default profile" is a store fact any surface
can subscribe to, not a per-component recomputation.
- profile-scope.tsx reads `$settingsScopeProfile` for the selected chip
(instead of re-deriving `override ?? active`) and the new selector for
the loud/quiet note. User-visible outcome is unchanged: accented note
for any non-default target, override or not; quiet note for an explicit
override onto the default; nothing when following the active default.
- Review: an unloaded roster (no `is_default` entry yet) used to suppress
the note exactly at the landing moment where a bot may already be the
active profile. The selector now assumes the root profile's canonical
key as the default, so an unknown default fails loud, not quiet. The
four-cell truth table is documented at the JSX conditional.
- settings-scope.test.ts pins the selector across active-profile,
override and unloaded-roster inputs; the component tests from the
original PR are kept as-is and still pass.
After any Bot Mode chat the active gateway profile is the bot's, so the
settings scope silently followed it — edits landed in
profiles/<bot>/config.yaml with only a faint chip tint as the tell
(#89190/#89162/#89597 report class, live-repro'd: Max Agent Steps written
to scout's config). The applies-to note now renders for ANY non-default
target, override or not, accented; default-profile editing stays quiet.
The salvaged parametrized set collapsed to one neutral case (transient) plus
the needs_input positive control. hermes kanban block now mirrors the
notifier: 'needs a human decision' only when the block was typed
needs_input, 'orchestration attention needed' otherwise.
The sibling surfaces of the gateway ping rendered the same false claim:
`hermes kanban block` said "needs a human decision", the Desktop toast title
said "needs a decision", the wake status line (locales/*.yaml
gateway.kanban.wake.block_loop_detected) said "needs a decision" and the
docs described the triage route as "for a human decision". A repeated-block
circuit breaker only establishes that orchestration attention is needed.
Surface sweep from PR #111131 (notifier/test hunks dropped in favour of the
typed-kind formatter from PR #111132).
A repeated-block circuit breaker routes a task to triage and establishes that
orchestration attention is needed — it does not establish that a human
decision exists. The notifier unconditionally rendered every
block_loop_detected event as "needs a human decision", overclaiming owner
intent for dependency waits, capability gaps, and transient failures.
Branch on the typed block kind in the event payload: only needs_input (the
one kind carrying a concrete question for the owner) keeps the decision
wording; dependency/capability/transient/None get neutral orchestration
wording. TRIAGE visibility, the reason, and recurrence count are preserved.
Closes#111125
The PR added four test functions for one fix. The host-side short-write and
multibyte cases are the same invariant as the sandbox size probe (an archive
that is not byte-exact is never referenced to the model), so they become two
parametrized rows of test_size_probe_decides_lossless, which now also uses
multibyte content so every row pins the byte-vs-char comparison. Net new
tests for the fix: 2 functions.
Also names in _write_to_sandbox why the +1 tolerance is keyed on heredoc
mode only: the payload backend delivers stdin verbatim and is expected to be
byte-exact. The three bare write_text() calls the footgun scanner flags in
this file gain encoding= while it is being touched.
Five near-identical MagicMock scripts pinned the same contract (byte-exact
or discard, heredoc gets exactly one extra byte); one parametrized test
plus the unprobeable-backend case cover it. Drop the host-side happy-path
test that duplicated test_multibyte_content_verified_by_byte_count.
Docstring now names the real heredoc wrapper
(BaseEnvironment._embed_stdin_heredoc) and notes the extra exec RTT per
oversized result (review point).
Oversized tool results are archived to disk and replaced in-context with a
'Full output saved to: <path>' reference. Until now the write was trusted
blind: a partially-flushed host file (ENOSPC/quota races) or a lossy sandbox
write (API-body truncation on payload backends) still produced the archive
reference, so the model was told the full result was recoverable when bytes
had silently vanished.
Both persistence paths now round-trip-verify size before building the
reference and fail closed to the bounded inline truncation otherwise:
- _write_to_spillover: byte-count check via os.stat after write; mismatched
archives are deleted and the caller falls through to inline truncation.
- _write_to_sandbox: wc -c probe after the cat; heredoc-mode backends get a
+1 byte tolerance (wrap_modal_stdin_heredoc appends one newline by
construction), unprobeable backends stay best-effort success.
Regression tests fail without the fix (verified by stashing the source
change: 4 failed). E2E-verified against a temp HERMES_HOME with real file
I/O including multibyte content and a simulated short write.
Parents (profile-config.tsx) build the `profile` scope as a fresh object
literal on every render, so `useMemo(..., [profile])` produced a new
cache writer each time and the autosave effect, which depends on both,
tore down and re-armed its 550ms timer on every unrelated re-render.
Key both on profileScopeKey(profile) — the same string the query cache
already uses as the scope's identity — so only a real scope change
re-arms them.
Review on the PR: the load-bearing direction (profile B's scope
forwarded into saveHermesConfigRecord) was only covered by the live
E2E; the unit test asserted just the unscoped default. Renders the panel
with profile={profile:'scout', connectionId:'gw-2'} and asserts the
autosave carries exactly that scope. The @/hermes full-replacement mock
gains profileScopeKey, which use-config-record reaches when a scope is
present. Sabotage (drop the profile prop from <VoiceProviderFields>)
fails this test with `expected undefined to deeply equal {profile:
'scout', …}`.
ToolsetConfigPanel threads its profile scope into every fetch but rendered
VoiceProviderFields without it, and the fields were hard-wired unscoped —
configuring profile B's TTS from the Capabilities selector read and
autosaved profile A's whole config record. New capability-scoped
saveHermesConfigRecord (symmetric with getHermesConfigRecord), profile
prop threaded through, per-scope cache write-through. Unscoped callers
(Settings → Voice) unchanged.
local_models._set_runtime_enabled (quickstart/activate/stop job threads) and
profiles._disable_unselected_skills ran load_config -> mutate -> save_config
without _CONFIG_MUTATION_LOCK, so the dashboard's debounced PUT /api/config
autosave could erase their writes exactly like the routers this PR already
fixed. Both spans now hold the lock.
POST /api/model/set held the global lock across switch_model's catalog
fetches / endpoint probes, stalling every other config writer for the
duration of a network round-trip. The main-slot validation is split into
_prepare_main_assignment (runs under the profile scope only) and the
load -> apply -> save half runs under the lock. The Nous entitlement refresh
(force_fresh) stays inside the write half: it must read the on-disk config
it mutates, and it is bounded by the portal timeout.
The PR's race test slowed save_config and hoped for an interleave, and
called the six handlers as web_server attributes that no longer exist
(web_server.py is a facade over web_routers/). Its second test read
inspect.getsource() for the lock name — a change-detector on source text.
Replace both with two invariant tests that drive the real endpoints via
TestClient and FORCE the interleaving: writer 1 is held inside save_config
until writer 2 has run load_config (or the lock kept it out). Unlocked the
second save erases the first mutation; locked the writes serialize.
Verified: reverting hermes_cli/web_routers/ to main fails both tests
(KeyError 'providers' / 'aggregator' = the lost write), the fix passes.
Only PUT /api/config took the span lock; model.set, moa, custom-endpoint
create/activate/delete, memory-provider saves, and the profile-dir model
write ran load→mutate→save unlocked in worker threads. The desktop fires
these concurrently with its debounced autosave — whichever save landed
second silently dropped the other's mutation (#88913/#89184 lost-write
flavor). Race test with slowed save proves both writes now survive.
`model.key_env` is not custom-only: the Desktop settings UI stores REGISTRY
provider keys there (e.g. HERMES_CUSTOM_LMSTUDIO_API_KEY with provider
lmstudio, #106336) and auth._model_level_key_env honours it. The previous
predicate (`not custom or route_changed`) therefore wiped that pointer on a
same-provider same-base_url model re-pick and silently broke the user's
credential. The pointer now clears ONLY when provider or base_url changed;
the inline api_key/api rule is unchanged.
Custom-endpoint activation (model_setup_flows_custom) popped base_url /
api_key but never key_env, so a stale pointer from a previous endpoint
outranked the credential it had just written — pop it alongside.
Tests: the same-route re-pick case now covers a registry provider (red on
the old predicate), and the two clear_model_endpoint_credentials tests are
folded into one invariant.
Main no longer clears credentials in web_server's _apply_main_model_assignment;
every /model surface (CLI, gateway, TUI, dashboard) persists through
model_selection_config_updates(), which only dropped api_key/api on a route
change. Custom-endpoint activation writes model.key_env with NO inline key, so
a pointer-only model block survived every provider switch and routed the new
provider's requests to the old endpoint's env var (the PR's Bug 5) — live
repro on main: activate custom_myep -> /api/model/set openrouter left
key_env: CUSTOM_MYEP_API_KEY on disk.
Port the PR's web_server hunk to the one shape function: key_env/api_key_env
clear under the same route-changed rule as api_key (same-route re-pick keeps
them). The dashboard's _resolve_assignment_credentials re-adds the TARGET
provider's own pointer after this, so custom->custom still ends with the new
endpoint's key_env. One invariant test in the one-shape suite.
Custom-endpoint activation writes model.key_env, but
clear_model_endpoint_credentials never popped it and the switch-clears
trigger only fired on inline keys — a pointer-only model block survived
every provider switch, routing the new provider's requests to the old
endpoint's env var. key_env/api_key_env now clear under clear_api_key;
the trigger fires on pointer-only blocks too. All key_env writers set it
after the clear, so custom-to-custom switches keep the new pointer.
- codex_runtime._CODEX_PROGRESS_DELTA_TYPES gains response.refusal.delta so the
stream watchdog sees progress on a refusal-only stream instead of timing it
out as idle.
- auxiliary_client._parse_codex_final_response reads type=refusal content
parts; without it an aux refusal-only turn parsed to content=None and hit the
empty-response path the main loop was just taught to avoid.
- tests: parametrize test_streamed_refusal_accumulated (refusal-only /
alongside-content) so there is one test per surface; drop upstream product
references from docstrings (credit stays in the PR body); pass encoding= to
the read_text calls flagged by the Windows footgun scanner.
- docs: fallback-providers notes that a streamed refusal is a terminal
content_filter result, not an empty response to retry.
A model that declines mid-stream delivers the explanation on the
structured refusal channel (chat_completions delta.refusal; Responses
response.refusal.delta / refusal content parts) and leaves content
empty. The streaming accumulators dropped that channel entirely, so a
streamed refusal assembled into an empty message and fell into the
empty/invalid-response retry loops - burning paid retries reproducing a
deterministic refusal - while the non-streaming path had already fixed
this class in #46013.
- chat_completions streaming: accumulate delta.refusal (incl.
model_extra), expose message.refusal on the assembled mock response so
ChatCompletionsTransport.normalize_response applies the existing
sole-payload -> content_filter promotion; count refusal deltas in the
zero-chunk guard; carry refusal in the Relay final-response dict.
- Codex Responses stream consumer: collect response.refusal.delta as
answer text so a refusal-only stream no longer raises 'did not emit a
terminal response' with zero usable content.
- Responses normalizer: read type=refusal content parts in
_extract_responses_message_text (attr and dict shapes).
Sabotage-verified: each new test fails with its wiring line disabled.
E2E: refusal-only stream -> terminal content_filter with explanation;
refusal-alongside-content stays a normal usable turn; plain-text
streams unchanged.
`_provider_pip_dependencies` still read ~/.hermes/hindsight/config.json with
strict utf-8 inside a bare `except Exception`, so a Windows-editor BOM made the
`mode` lookup silently fail and `hermes update` reinstalled only
`hindsight-client`, leaving the embedded daemon broken — the exact #70636
symptom this helper exists to prevent. Route it through the shared
`read_json_or_empty` (utf-8-sig, {} on missing/corrupt) like the other memory
readers in this PR, and add the reader to the parametrized BOM invariant.
mem0, hindsight and the honcho CLI all read through utils.read_json_or_empty,
so the seven per-loader tests collapse to one parametrized invariant plus the
Qwen creds case. The per-loader fix landed in the shared reader (one site, not
six), which is the salvage bar: <=2 invariant tests, no change-detectors.
Port from earendil-works/pi#8337 (UTF-8 BOM normalization in text inputs):
sibling sites the merged #81967 BOM sweep missed. json.loads hard-fails on
a leading U+FEFF and every one of these loaders swallows the exception and
silently falls back to defaults — a user who edited mem0.json, honcho.json,
hindsight/config.json, or supermemory.json in Notepad lost their whole
config with no error, and Qwen CLI OAuth creds saved with a BOM raised
qwen_auth_read_failed.
- plugins/memory/{honcho,mem0,hindsight,supermemory}: 13 read sites -> utf-8-sig
- hermes_cli/auth.py: _read_qwen_cli_tokens -> utf-8-sig
- tests: BOM regression tests per loader (sabotage-proven) + plain-UTF-8 guard
Why: the tui_gateway live formatter (`_format_live_context_output`, used when
the session runs on a compute host) renders its own summary and never got the
"Context files" block, and `session.context_breakdown` had no structured rows,
so Desktop's popover could not show them. The formatter now appends
render_context_file_lines() with the session cwd bound (the RPC thread has no
session context, so the discovery walk would key on the backend's cwd), and
the RPC payload gains a `context_files` list (contract + generated TS/OpenRPC
+ Desktop type). The docs sentence is scoped to the surfaces that render it.
A file whose content _scan_context_content replaces with a BLOCKED marker was
reported "loaded"; the manifest now runs the same scan and reports `blocked`.
The module docstring names the frontmatter-strip / chain-cap approximations
and drops the product-name attribution (credit stays in the PR body).
Review follow-up (Enough1122) on the salvaged #91272: the original
list_context_file_sources() hand-mirrored the priority ladder inside
build_context_files_prompt, so the two would drift the moment the builder
gained a context type or changed precedence — misreporting what the prompt
holds is worse than not showing it.
Now prompt_builder exposes one candidate finder per context type
(_CONTEXT_FILE_CANDIDATES → discover_context_files) and BOTH the loaders and
the manifest walk it. The manifest lives in the new sibling
agent/context_file_sources.py (not appended to the facade) and:
- reports empty / unreadable files truthfully instead of "✓ 0 tokens",
- mirrors the install-tree guard ("suppressed") so a Desktop session that
fell back into the Hermes tree sees why nothing loaded,
- lists every .cursor/rules/*.mdc as loaded, matching the builder which
concatenates all of them,
- measures truncation on the rendered "## label" section like the builder.
The block now renders on every surface that shows the /context category
table: CLI/TUI (hermes_cli/cli_info_mixin.py) and the messaging gateway
(gateway/slash_commands_status.py). The Desktop popover consumes the raw
session.context_breakdown payload (no text table) and is left as-is.
Tests trimmed to the two invariants: manifest/prompt parity across every
context type at once, and truncated/suppressed follow the builder.
Copilot CLI 1.0.81-6 shows each user instruction file separately in
/instructions. Hermes loaded AGENTS.md/.hermes.md/CLAUDE.md/.cursorrules/
SOUL.md through a priority ladder but gave the user no visibility into
WHICH files were discovered, which one won, which were shadowed, or how
much context each costs — the /context 'rules' category was one opaque
number.
- agent/prompt_builder.py: list_context_file_sources() — read-only
manifest mirroring build_context_files_prompt discovery (priority
ladder, AGENTS.md directory chain with AGENTS.override.md precedence,
cwd-only CLAUDE.md/.cursorrules, SOUL.md from profile home) with
per-file chars, est_tokens, and loaded/truncated/shadowed status
- cli.py /context: 'Context files' section rendering the manifest with
status glyphs and shadowing/truncation notes; zero prompt/cache impact
- docs: reference/slash-commands.md /context row
- tests/agent/test_context_file_sources.py: 11 tests incl. E2E parity
with build_context_files_prompt shadowing
sortByProfileOrder moves to a pure lib module with a key selector so
buildRestGroups sorts its named squares directly, replacing the collator
sort that the component then re-sorted with a different comparator. The
fleet rail test no longer needs importOriginal (and three store mocks) to
reach the helper.
Sharing the remainder of SWITCH_DIAL_TIMEOUT_MS meant a slow-but-successful
socket dial left the preflight 0 ms and the switch failed as "Timed out
connecting" although the socket had just opened.
The renderer's per-connection list no longer feeds buildRestGroups: with no
roster reconciler it could outlive a profile deleted elsewhere. The active
gateway is never rendered through FleetRestGroup — that path routed its own
squares through selectConnection (full dial + wipe) instead of selectProfile's
live swap. What survives from the original change is the order parity: at-rest
named squares follow $profileOrder like the active strip.
The per-connection list cache is kept only to repaint $profiles on re-home.
The $fleetRoster listener is dropped: a roster landing while the active
source's own /api/profiles read was in flight invalidated that read, so
$profiles stayed empty/stale after every switch or focus refresh that the
roster IPC won. Both are reads of the same backend; neither is "older".
A null descriptor is a reconnect blip (setConnection's contract) and keeps
the current owner instead of blanking the rail; the first published
descriptor adopts whatever list is already loaded. Legacy sources are keyed
by endpoint rather than a JSON tuple. Tests cover the roster race and the
null blip; the two use-session-actions tests now publish the descriptor
before seeding $profiles, matching the runtime order.
fal's LTX 2.5 fast endpoints accept 6-20s only up to 1080p — "At 1440p and
2160p, all frame rates support up to 10 seconds" — so a 4K request with the
family's 20s ceiling was rejected by the vendor. Families can now declare
`duration_cap_by_resolution`, applied after the enum snap / range clamp on the
resolved resolution enum.
An unset duration on a duration_enum family also snapped to enum[0] (6s),
silently overriding the endpoint's own "auto" default; None now omits the key
for enum families exactly as it already did for range families.
test_managed_media_gateways asserts the alibaba/happy-horse/ namespace by
prefix rather than the exact v1.1 literal so the next version bump doesn't
flip an unrelated gateway test.
`durations` carried two meanings told apart only by len==2 and gap>1: a
(min, max) range to clamp, or an enum to snap. A family with exactly two
legal values would have been misread as a range (review finding on #91311).
`durations` is now always the (min, max) window (what capabilities()/
list_models() read) and families with discrete values add `duration_enum`;
_clamp_duration takes the family and branches on the key, not the shape.
Also: restore the exact v1.1 endpoint assertion in the gateway namespace
test (a startswith/endswith check would not catch a silent version drift),
add the ltx-2.5 i2v snap case, and keep happy-horse on audio_native (the
schema test forbids audio+audio_native together, and v1.1 audio is always on).
The managed-gateway test asserted the literal v1.0 endpoint ids; its
stated purpose is verifying the alibaba/ (not fal-ai/) namespace. Assert
prefix+modality-suffix instead so version bumps don't break it.
Adds two new FAL video families and upgrades one:
- ltx-2.5 (cheap tier): lightricks/ltx-2.5/{text,image}-to-video/fast.
Lightricks' open-source audio-video model. Native audio, 6-20s integer
duration enum, 720p-2160p (i2v), $0.09/s at 720p. duration_int + 2k/4k
resolution aliases; no seed key in the schema.
- kling-o3 (premium tier): fal-ai/kling-video/o3/standard/{text,image}-to-video.
Kuaishou's frontier multi-shot model, 3-15s, optional native audio
($0.084/s off, $0.112/s on). String durations, i2v drops aspect_ratio,
no seed/resolution keys.
- happy-horse upgraded from the sparse-docs 1.0 endpoints to
alibaba/happy-horse/v1.1/{text,image}-to-video with the full published
schema: nine aspect ratios, 720p/1080p, 3-15s integer durations, seed
supported, audio native (no generate_audio key), i2v drops aspect_ratio.
All flags derived from each endpoint's llms.txt schema. Payload builder
asserted locally against the schemas; test for the old Happy Horse
"prompt-only" contract updated to pin the v1.1 schema, plus new payload
tests for ltx-2.5 and kling-o3.
Squashed integration of the user-facing message audit for this surface set.
Full per-finding receipts: /tmp/ux-audit/lanes/*-receipt.md (campaign artifacts).
test_silent_stall_still_times_out kept the 0.1s idle window and flaked
once locally under heavy load (load avg ~250): the child was killed
before its single stderr line was read, so the pre-stall-output
assertion saw an empty stderr. Same class as the progress test this PR
de-flakes; give it the same 0.25s budget. The 30s sleep still trips the
window, so the must-time-out direction is unchanged.
andrexibiza's review is right: the base test already printed tick 0
before its first sleep, so this change never removed a
sleep-before-first-tick race. The actual de-flake is the larger idle
budget (0.1s -> 0.25s) plus a longer heartbeat sequence whose ~400ms
runtime still exceeds the idle window. Say exactly that so the causal
record is accurate.
The stderr-progress idle-timeout test used a 0.1s idle window with 0.04s
ticks — shorter than Windows process spawn, so the first chunk could never
arrive in time (deterministic failure on Windows, flake under Linux CI
load). Verified failing identically on pristine main before the change.
Fix: emit the first tick immediately, tick every 50ms for ~400ms total,
250ms idle window (5x tick period). The pass still depends on the progress
extension while tolerating real spawn/scheduling latency.
Signed-off-by: andrexibiza <84248988+andrexibiza@users.noreply.github.com>
Authoring standard 4 requires the human first in `author`; the skill was
drafted with Hermes so the tool was credited instead. The intro cited a
third-party product, which is allowed only in LICENSE/credit lines. Also
adds metadata.hermes.category and lowercases tags to match sibling
skills; docs page regenerated for this skill only.
Tests: the two prose tests asserted sentence literals (change-detectors);
they now assert structure — three Procedure phases, a "Done when" per step,
standard headings, and the tool wiring (cronjob/desktop_preview/[SILENT]).
Why: the web dashboard is being deprecated in favour of the Electron Desktop
app, and a skill that ships a bespoke cron blueprint should not be bundled by
default. Reconfigure instead of just rebasing:
- Move skills/productivity/live-dashboard -> optional-skills/productivity/
live-dashboard (install with `hermes skills install
official/productivity/live-dashboard`); register it like every other
optional skill: per-skill docs page under user-guide/skills/optional/,
optional-skills-catalog row, sidebars entry.
- Desktop reality: add a "Show the dashboard" step — when `desktop_preview`
is in the toolset (Desktop/GUI sessions) render index.html in the in-app
preview pane after every build/tick and on request; otherwise report the
absolute file path. Prerequisites section added (Enough1122 review).
- Drop the hard-wired cron/blueprint_catalog.py entry: the curated catalog
is for bundled skills and would preload a skill that may not be
installed. Use the skills-pipeline blueprint instead —
`metadata.hermes.blueprint` on the SKILL.md registers a daily
all-dashboards sweep as a /suggestions entry at install time (opt-in,
never auto-scheduled), which is exactly the mechanism main provides for
optional skills.
- Never hardcode ~/.hermes in prose the agent executes: refer to the Hermes
home directory's dashboards/<slug>/ and write absolute paths into cron
prompts (also answers the review's "tick prompt must name the state-file
path" point).
- Tests follow the skill to optional-skills/, the catalog-blueprint tests
are replaced by one parse_blueprint/blueprint_to_job_spec invariant and
one desktop_preview-with-path-fallback invariant.
Energy (getenergy.com) ships natural-language persistent dashboards:
describe what you want to see in one sentence and the agent builds a
self-updating status page fed by email threads, signed-in websites, and
files. This ports the concept onto Hermes's existing cron + connector
architecture:
- skills/productivity/live-dashboard: setup/tick split skill — pin the
dashboard contract, verify one live read per source before scheduling,
keep dashboard.json as source of truth with a self-contained HTML
projection, stale-read discipline, deliver only on material change.
- cron/blueprint_catalog.py: live-dashboard automation blueprint
(purpose/sources/time/recurrence/deliver slots) rendering to the
dashboard form, /blueprint command, and hermes:// deep-link.
- tests/skills/test_live_dashboard_skill.py: skill standards + blueprint
registration + real fill_blueprint E2E.
- docs: per-skill page, skills catalog row, sidebar entry.
Competitor product names are allowed only in LICENSE attribution and
salvage credit lines; the test docstring had an inspiration tag left over
from the port.