Skip to content

Merge dev → main (automated) - #2085

Merged
SorraTheOrc merged 137 commits into
mainfrom
release/dev-to-main-20260825122023
Aug 25, 2026
Merged

SorraTheOrc merged 137 commits into
mainfrom
release/dev-to-main-20260825122023

Conversation

@SorraTheOrc

Copy link
Copy Markdown
Member

Automated release created by ship skill.\n\nIncludes CHANGELOG.md with work-item summaries from this release.

pi added 30 commits August 16, 2026 14:56
Add dim '▼ more' / '▲ more' markers to the herdr worklist selection list
(createListRenderer, list mode) so users can tell when items are hidden
below the fold or above the current scroll position:

- ▼ more rendered after the last visible item when
  scrollOffset + visible.length < flatItems.length (items below the fold).
- ▲ more rendered before the first item/group separator when scrollOffset > 0.
- Indicator rows are reserved in the render budget (effectiveBudget) before
  the visible window is trimmed, and counted in the fill-blank used rows, so
  the rows - 1 line-count invariant (WL-0MSAAON63003N6LO) holds with
  indicators active.
- Edge case handled in a single pass: reserving the bottom indicator row may
  trim the window so the list fully fits — the indicator is then omitted.
- Indicators are display-only rows: no navigation state, selection index, or
  hierarchy scroll offset is consumed (getVisibleItems/_adjustScroll
  unchanged).

Tests (worklist.test.ts): indicator below fold, ▲ more when scrolled, both
indicators, no indicators when the list fits, rows-1 invariant with
indicators (plain and grouped), dim non-selectable display rows, and
getVisibleItems semantics unchanged. README updated (feature bullet, fold
indicators section, design decisions).
Regenerate the auto-generated version constant to match package.json
(v1.0.11). The v1.0.11 bump (eebe41e) missed the regeneration, leaving a
stale version.ts that breaks tests/cli/init.test.ts ('should create
semaphore when config exists but semaphore does not') which asserts
result.version === getPackageVersion() via tsx src/cli.ts. Fixes
WL-0MSVUMPLX0087HWE (pre-existing, fails on clean dev too; CI masked it
by building first).
…ection

getNextAuditCandidate now passes --root-only to wl list so only parent
items in completed/in_review are audit candidates; children are never
dispatched for /skill:audit independently. Updates code comments and the
README downtime section; adds unit + CLI e2e coverage (AC1-AC5).
parseLlamaStatus coerces numeric slot_id (proxy contract: int) to string,
clamps negatives to 0, preserves the zero-length string rejection; regression
tests mirror the live proxy payload at parse level and end-to-end through the
worker tick (poll → idle → dispatch). Fixes the Aug 15-16 zero-dispatch
regression where every poll fail-closed to busy.
install-herdr.sh runs npm ci (fallback npm install) in packages/herdr
before linking the plugin, so marked and @worklog/shared are present on a
fresh clone or after a node_modules cleanup. Fixes the prefix+l worklist
crash (ERR_MODULE_NOT_FOUND on marked from md-viewer.ts). Still warn-only
and idempotent; link and keybinding behavior unchanged.
- buildMetaRows prefixes all icon-bearing metadata values (Status, Stage,
  Priority, Type, Risk, Effort, Audit, Reviewed) with the same icon helpers
  used by list rows, as 'icon + text label' (e.g. 🔄 in_progress, ⭐ high).
- Stage row uses new shared stageDisplayIcon helper for audit-aware in_review
  logic (✅/❌/❓ fresh, ⏳ stale-passed, 🔍 otherwise), ensuring the list
  prefix and metadata Stage row never diverge.
- Type row shows epic icon (⊙) for epic items only.
- Audit/Reviewed rows pair icons with text labels (ready to close, not
  ready, unknown, needs review, reviewed).
- With icons disabled, metadata falls back to plain text — no emoji, no
  [BRACKET] fallbacks (unlike list rows).
- 7 new test suites covering icon+text values, audit-aware stage row, epic
  Type icon, plain-text fallback, and panel/detail rendering.
- stageDisplayIcon unit tests added to icons.test.ts.
- README updated with icon+text metadata documentation.
… TUI (WL-0MSI7278X0008XGM, WL-0MSI72EYW00050N2)

- ANSI.mouseEnable/mouseDisable lifecycle sequences emitted on raw-mode entry / cleanup (AC1).
- Pure SGR parser (parseMouseEvent), split-chunk buffering (consumeMouseChunk).
- mapMouseToAction row mapping (header/fold/separator/footer/panel aware), double-click window (400ms inclusive), wheel/touch scroll (list + detail), filter-stage taps, drag-motion guard (AC2-AC6).
- Clears the 41 pre-existing failing tests in worklist-mouse.test.ts (TDD red phase committed without implementation) — resolves WL-0MSWJBBHT002IA6Z and unblocks WL-0MSGIXHHI009KFW9 review.
- Test-suite gate excludes only the environmental tests/skill-path-conventions.test.ts (global ~/.pi/agent/skills/plan/SKILL.md legacy reference, triaged in WL-0MSX3H7L20009QTH).
- selectImplementCandidate accepts risk <= Medium (was Low only) and
  effort <= Medium (was Small/Extra Small only).
- createDowntimeDeps wl next flags updated to --risk medium --effort medium.
- Test expectations updated to match new thresholds.
…ortcut creates a pane

Selection-list dispatches now pass --no-focus to both pane launchers so the
worklist keeps keyboard focus while the new pane opens in the background:

- src/index.ts: buildSendToPiArgs emits --no-focus for every agent-route
  dispatch (mirrors buildDowntimePaneArgs); new buildRunInPaneArgs helper
  used by the !! / ! pane route and the plain-shell stdout route.
- scripts/run-in-pane.sh: gains a --no-focus option parsed at the head of
  argv alongside --cwd; the final pane zoom is skipped when set. Backward
  compatible: without the flag the zoom still runs.
- Tests: index.test.ts extends buildSendToPiArgs assertions and adds a
  buildRunInPaneArgs group (AC1-AC3); test_run_in_pane.sh adds a mock-herdr
  main-mode section covering skip-zoom / keep-zoom / command-still-runs
  (AC4).
- README.md: Command output / Pi agent dispatch features and Design
  decisions document the no-focus behavior; outage scope (downtime worker,
  open-pi-agent, open.sh/toggle.sh) noted as unchanged (AC5/AC6).
Add the missing onData dispatch wiring for mouse events:

- Add clickState variable for double-click detection (400ms window)
- Add dispatchMouse() helper that:
  - Consumes SGR mouse events via consumeMouseChunk() BEFORE keyboard path
  - Maps events to actions via mapMouseToAction()
  - Dispatches actions (select-row, open-detail, back, wheel, scroll-detail, filter-stage)
  - Returns false for non-mouse input (falls through to keys)
  - Mouse input is inert during modal states (code-freeze, form, ship-it dialog)

- Mouse sequences fully consumed before keypress parser; unknown sequences
  are inert (AC6). Drag-motion and release events are safely ignored (AC6).
- Keyboard navigation and chord shortcuts unaffected.

Build: green (0 TS errors). Tests: 42 mouse + 229 worklist = 271 passing.
…port

- Update footer hints: add [click] select, [dbl-click] open, [wheel] scroll
- Update README: add mouse and touch navigation section
- Update docs/dev/pause-when-hidden.md: add mouse/SGR reporting section
- Build: green (0 TS errors, 271 worklist+mouse tests passing)
- Full suite: 4746 tests passing (3 pre-existing failures in tests/herdr/index.test.ts
  unrelated to this change — pre-existing --no-focus assertion failure)
…ting

Root cause: dispatchMouse updated clickState to the CURRENT event before
mapMouseToAction ran. mapMouseToAction then checked the window against
lastClick (now - at === 0 always <= 400ms), so EVERY left press
self-matched as a double-click and returned 'open-detail' instead of
'select-row' — a single click behaved like Enter instead of like an arrow.

Fix: extract the mouse input wiring into an exported handleMouseInput(state,
key, termSize, clickState) pure-ish dispatch function. It parses with
consumeMouseChunk, maps with mapMouseToAction, and advances the clickState
tracker AFTER the double-click check (only on left-button presses — releases,
wheel, and motion never prime the window). dispatchMouse now delegates to it.

Behavior:
- single click/tap selects a row (moves selection like arrow keys), mode stays 'list'
- second click on the SAME row within 400ms opens detail (Enter-equivalent)
- second click on a DIFFERENT row selects that row, never opens detail
- wheel/motion/release events never prime the double-click window
- partial SGR prefixes are consumed (never leak into keyboard path)
- plain keys/foreign escapes (arrows) fall through untouched (AC6)

Tests: 7 new regression tests in worklist-mouse.test.ts (49 total).
Full suite: 4753 passing (+7), 3 pre-existing unrelated failures.
Add install and install:plugin scripts to package.json, creating a
single-command install for all integrations.

Changes:
- package.json: add 'install' and 'install:plugin' scripts
  (install chains install:plugin then install:pi-extension)
- tests/scripts/package-scripts.test.ts: 8 tests for script wiring
- README.md: document npm run install in Pi agent plugin section
- packages/herdr/README.md: mention npm run install as alternative

postbuild and install:pi-extension remain unchanged.

Relates to WL-0MSHZR0J2000Q9SW
…ing active window status

Event-driven herdr window status (agent icons + pane focus/visibility),
keeping polling (herdr agent list / tab get) as a fail-open fallback.

Covers children F1-F7:
- F1 (WL-0MSIG3A06008HKBB): mock herdr socket fixture + subscriber contract tests
- F2 (WL-0MSIG3G2X007RAEL): events.ts HerdrEventSubscriber (connect, subscribe,
  typed dispatch, reconnect/backoff, fail-open, add/removePaneSubscription,
  close on exit)
- F3 (WL-0MSIG3IO7009RL03): pane_focused event path — PollGate.setVisibleFromEvent,
  immediate panePaused update + doRefresh on hidden→visible; resume-poll becomes
  fallback-only (gated by events-health flag)
- F4 (WL-0MSIG3LWG00590AG): agent-status event-path tests (red → green)
- F5 (WL-0MSIG3OTB005V1CL): AgentTracker event-applied path
  (applyAgentStatusChanged / applyAgentDetected / applyPaneGone,
  snapshotStates, mergeAgentStatesCached) with per-render-cycle coalescing
- F6 (WL-0MSIG3RL1006AQID): index.ts/worklist.ts wiring — one subscriber per
  plugin instance, events-health flag, close() on TUI exit; min_herdr_version
  stays 0.7.0 (runtime degradation for pre-0.7.5)
- F7 (WL-0MSIG3U7W006GH2R): README + shared protocol docs (events.subscribe
  shape, pushed frames, per-pane lifecycle, session-scope limitation)

AC1-AC5 satisfied: event-driven agent status within 1s, event-driven focus
transition, cross-instance coverage via pane_agent_detected + shared
agent-panes.json re-read, fail-open fallback with no behavioral regression,
initial state via one-time snapshot. Full packages/herdr suite green (1188).
…28AP80002F5S)

Root cause: formatDetailView sliced at tocLines.length, dropping the 4-line
header and duplicating the ToC. Total output exceeded viewportHeight, causing
the first lines to scroll off-screen.

Fix:
- Compute pinnedHeight = 4 (header) + tocLines.length; render
  [...allLines.slice(0, pinnedHeight), ...bodyLines.slice(offset, offset+bodyViewport)]
- Add DETAIL_HEADER_LINES = 4 constant for clarity
- Update detailScrollDown to use bodyViewport for consistent clamping
- Update existing tests to reflect pinned header behavior

Tests added: line-count invariant, header visibility, ToC non-duplication,
no-ToC regression check.
The buildSendToPiArgs function now prepends --no-focus (WL-0MSHIA53D009DJOT)
to prevent stealing focus from the selection list. Three tests needed
updating to include --no-focus in expected arg arrays.
…xes prefix+l worklist pane crash)

The HerdrEventSubscriber removed its only socket 'error' listener once the
initial events.subscribe succeeded. A later socket reset (ECONNRESET) from
herdr then emitted an unhandled 'error' event, crashing the whole plugin
process (exit code 1) and killing the worklist pane within ~3 seconds of
`prefix+l`.

Keep the error listener attached post-subscribe so errors route through
handleSocketError() (fail-open reconnect with backoff) instead of crashing.
Only the connect-phase timeout listener is removed once the connection
settles. Adds a regression test that injects an ECONNRESET on the socket
after subscribe and asserts the subscriber stays alive and calls onError
(the test fails with the pre-fix code).
Add a per-instance in-memory enabled override to createDowntimeWorker so a
shortcut can flip dispatch on/off for one herdr instance independently of
the global settings. Effective enabled = override ?? cfg.enabled, honored
by both the enabled getter and tick()'s early return. Backward compatible:
default null follows the global setting exactly as before.
…[Downtime Off] header

Add the d chord (/downtime toggle) to bundled shortcuts.json, route it as
an internal TUI action through dispatchChordCommand/executeResolvedCommand
(optional onDowntimeToggle callback — no pane, no stdout), and create the
downtime worker unconditionally in index.ts so the toggle can force
dispatch on for one pane when the global setting is off. renderDowntimeStatus
now shows [Downtime Off] instead of [downtime disabled] for all disabled
states. Tests cover chord routing, header rendering, per-instance isolation
and force-on-when-global-off.
…ntime Off]

Document the d shortcut (toggle downtime dispatch for the current pane,
per-instance in-memory) in the shortcut keys section and add a Per-pane
toggle subsection in the downtime worker documentation; replace
[downtime disabled] with [Downtime Off] in the header-status list.
…ests)

Test-first suite for the downtime dispatcher round-robin feature (parent
WL-0MSSRED76008LGB6):
- createRoundRobinRegistry: shared durable cursor persisted to
  .worklog/downtime-round-robin.json, fail-open on missing/corrupt file,
  per-tier cursors (audit/implement/plan/intake), atomic temp+rename writes
- getEffectivePollInterval: probe jitter ±50% (factor 0.5 + rng, clamped
  to [0.5, 1.5]), injectable RNG for deterministic tests
- 16 unit tests: cursor round-trip persistence, fail-open corrupt/empty
  files, rotation cycling, cross-instance divergence, per-tier isolation,
  jitter bounds and clamping

herdr suite: 1229 tests pass; tsc clean; full project suite green.
Wire the round-robin registry (WL-0MSW6CACJ0035UGK) into selection:
- selectWithRotation: groups candidates by priority level, rotates within
  the tied top-priority group via the shared durable cursor; fail-open
  (no registry / no priority → sortIndex order, pre-rotation behaviour)
- priority field parsed in parseNextCandidatesOutput /
  parseImplementCandidatesOutput / parseAuditCandidatesOutput
- selectAuditCandidate / selectImplementCandidate / selectNextCandidate
  accept an optional registry and delegate to selectWithRotation
- 8 new tests: rotation cycling, priority-first preservation, single-member
  top group, fail-open paths, and per-tier wiring

herdr suite: 1239 tests pass; tsc clean; full project suite green.
Dispatch wiring end-to-end (parent WL-0MSSRED76008LGB6):
- createDowntimeDeps caches a shared round-robin registry per worklog root
  and passes it to selectNextCandidate / selectAuditCandidate /
  selectImplementCandidate, so all three tiers rotate within tied-priority
  groups via the durable cursor file
- createDowntimeWorker accepts an optional registry; expose
  jitterPollIntervalMs() computing the jittered ±50% probe interval
- TaskScheduler gains a getIntervalMs hook so a task recomputes a FRESH
  interval on every reschedule; the downtime scheduler task uses the
  worker's jitterPollIntervalMs to deserialize probes across instances
- 7 new tests: scheduler per-schedule jitter, worker jitter fallback +
  cross-RNG divergence, deps rotation via cursor file (deep-equality +
  persisted advance)

herdr suite: 1246 tests pass; tsc clean; full project suite green.
Docs for the downtime dispatcher round-robin + jitter feature (parent
WL-0MSSRED76008LGB6):
- README: new 'Round-robin tie-break' and 'Probe jitter' subsections
  documenting the shared durable cursor (.worklog/downtime-round-robin.json),
  fail-open conventions, dispatched-marker guard, and the ±50% jittered
  probe interval
- downtime-worker.ts module header: document selectWithRotation and
  jitterPollIntervalMs

Absorption of WL-0MSMHX6IR007F3H6 is already recorded (deleted with
'Absorbed by WL-0MSSRED76008LGB6'). ARCHITECTURE.md has no downtime
content to update.

herdr suite: 1246 tests pass; tsc clean; full project suite green.
…s->10s

- DEFAULT_DOWNTIME_IDLE_THRESHOLD_MS 240_000 -> 60_000 (floor 1s unchanged)
- DEFAULT_DOWNTIME_POLL_INTERVAL_MS 30_000 -> 10_000 (existing floor)
- Update tests/herdr/settings.test.ts defaults to 60000/10000
- Update packages/herdr/README.md documented defaults
- AC4: deployed config untouched (no downtime keys => defaults apply)
Replace all hardcoded DEFAULT_DOWNTIME_IDLE_THRESHOLD_MS (240_000) and
DEFAULT_DOWNTIME_POLL_INTERVAL_MS (30_000) values in tests with the
exported constants. Adjusts timing assertions to match the new 60s
threshold and 10s poll interval.

Fixes AC3: idle-threshold timing tests now use DEFAULT_DOWNTIME_IDLE_THRESHOLD_MS;
no test asserts old 240s/30s values.

(WL-0MSOMV6CW003OXTK)
… scrolls (WL-0MSQ44MDX008U69J)

Flip the space priority in the worklist pane: the selection list may use up
to the full available pane height (header + items + separators + indicators
+ footer), and the metadata panel fills whatever space remains — expanding
when the list is short, sitting at the bottom (min 3 rows) when the list is
long. The metadata panel scrolls independently (m/M) via metaScrollOffset.

- Add computeDynamicLayout() shared by the renderer and the main TUI loop
  (panel height is passed into metaScrollDown for correct clamping)
- createListRenderer computes panelHeight from content need instead of the
  fixed 20-40% reservation (computeMetadataPanelHeight is now a legacy
  fallback used only when no dynamic height is supplied)
- Initial view starts at the top of the selection list (scroll offset 0);
  long lists no longer scroll the header off the top
- Preserve the rows - 1 line-count invariant (WL-0MSAAON63003N6LO) and the
  m/M scroll indicator; update mouse click-row mapping for the new layout
- Update unit tests (list-height/content-need, metadata expansion,
  reveal-by-scroll, scroll clamping, line-count invariant) and docs
pi added 28 commits August 23, 2026 08:43
…NHE0048HW3)

Add comprehensive investigation report for snapd udev monitor goroutine hang
that caused system hang on 2026-08-20.

Key findings:
- Root cause: udev monitor goroutine stuck in select on netlink socket (udevmon.go:147)
- Cascading effect: watchdog SIGABRT → snapd won't restart → forced reboot
- snapd 2.76.3 available with netlink parser fixes (90% phased)
- Current system stable for 2+ days post-incident

Mitigation plan:
- Primary: upgrade to snapd 2.76.3 (includes netlink parser fixes)
- Fallback: disable systemd watchdog (override file provided)

Includes:
- Full investigation report (docs/dev/snapd-investigation.md)
- Systemd override drop-in (docs/systemd/snapd-watchdog-overrides.conf)
- Monitoring plan and verification steps
Handle verbose agent-produced risk/effort descriptions by extracting
leading keywords before delimiters.

- riskOrdinal: split on delimiters (-, :, –, —, whitespace), take first
  token, then switch-match. 'Medium — NVIDIA driver changes…' → 2.
- effortOrdinal: exact match on stripped string first, then word-boundary
  regex fallback for keywords anywhere in the string.
  '1–4 hours — Small. Investigation…' → 2.

Mirrors the herdr client-side fix in downtime-worker.ts.
Test riskOrdinal, effortOrdinal, and matchesRiskEffort with verbose
agent-produced descriptions and regression coverage for:
- Verbose risk ("Medium — ..." → ordinal 2)
- Verbose effort ("1–4 hours — Small. ..." → ordinal 2)
- Short spellings (xs/s/m/l/xl) and long forms
- Fail-closed for unknown/empty values
- high/severe risk excluded by at-most medium filter
…fig (WL-0MT1KJNHE0048HW3)

- Comprehensive investigation report with root cause analysis
- Goroutine stack dump analysis showing udev monitor select hang
- Upstream snapd research and version availability
- Systemd watchdog override configuration (WatchdogSec=0)
- Monitoring plan and recommended actions
…tax errors

Defect 1 — FTS index never updated incrementally:
- import() re-upserts FTS rows for all imported items after the clear
  (clearWorkItems/clearComments now also purge worklog_fts)
- upsertItems() upserts FTS per saved item
- reconcileBlockedStatus / reconcileDependentStatus upsert FTS after
  status changes so status-filtered search reflects the change
- upsertComments()/importComments() re-index affected items (comment
  text lives in the parent item's FTS row)

Defect 2 — malformed FTS5 queries failed silently:
- searchFts no longer swallows errors; WorklogDatabase.search() retries
  the query as a quoted phrase and emits a visible warning when
  auto-quoting succeeds; surfaces  when it fails
- CLI (wl search) prints the warning (human + --json  field)
  and exits non-zero with a clear message on hard query errors

Tests: fts-freshness.test.ts (freshness + error surfacing + AC1/AC3),
cli/search-fts-freshness.test.ts (e2e). Docs: CLI.md, docs/dev/fts.md.

WL-0MSLW8UCP001771K
…false→null

The old cycle allowed pressing `d` twice to force-enable dispatch
(override = true), which silently overrode the global disable setting
and any sibling-pane disable. This violated the operator expectation
that pressing `d` disables dispatch until explicitly re-enabled.

New cycle: null → false (disable) → null (follow settings). No
force-enable path. The second press returns to following the global
setting without requiring explicit confirmation.

Tests updated: existing toggle/override tests adapted to new cycle;
new tests added for 60-minute no-dispatch guarantee (AC1), no force-enable
on second press (AC3), three-press cycle, tick short-circuit after
disable, and tick behavior after second press.

WL-0MT58VKOW0066XJ6
The d-shortcut disable was in-memory only, so a pane/plugin restart
silently re-enabled dispatch (reported incident: audits dispatched
while the operator believed dispatch was disabled). This adds a small
per-worklog-root marker file that survives restarts:

- toggle() -> disable writes <cwd>/.herdr-downtime-disabled
- toggle() -> follow-settings removes it (no-op when absent)
- createDowntimeWorker reads the marker at init: present =>
  override = false (disabled) unless an explicit opts.override is
  passed (explicit wins, marker is the fallback)
- All marker IO is best-effort/non-fatal: toggle() never crashes on
  fs errors (e.g. read-only FS, missing cwd)

New module downtime-disable-marker.ts (existsSync/writeFileSync/rmSync
sync helpers). Tests: 10 new tests covering creation, no-rewrite,
removal, init-restore, explicit-override precedence, per-root scoping,
and zero-dispatch across idle+cooldown windows (AC1/AC6).

Full suite: 5351 passed, 0 failures.

WL-0MT58WX3X0092PTK, WL-0MT5SFP990001FNW
…ment

- DowntimeWorker gains restoredFromMarker: true only when the disable
  was restored from the persisted marker at construction (survived a
  restart); false for live d-presses and cleared on explicit re-enable
- renderDowntimeStatus renders "[Downtime Off (restored)]" for a
  marker-restored disable so a restored-off pane is never silent
  (parent AC2/AC4); all other states unchanged and always agree with
  the effective gate (override ?? settings)
- Live toggle() writes the marker but is NOT flagged restored; the
  second press removes the marker and clears the flag

Tests (worklist.test.ts): 6-state effective-gate matrix (disabled
pane never renders the enabled-idle string), Off regardless of global
setting, restored notice renders/clears, integration tests with a
real marker on disk (restore → notice → re-enable → notice gone) and
live-press not flagged restored.

Full suite: 5362 passed, 0 failures.

WL-0MT5SFX1S002YUYZ, WL-0MT5SG0VU005ARUR
…WL-0MSXH3UF5000HJUS)

Implements file-lock-based leader election for the downtime dispatcher
refactor (parent WL-0MST3OJ8S0001ROL):

- O_CREAT|O_EXCL atomic lock acquisition at .worklog/downtime-leader.lock
- Lease file (.worklog/downtime-leader-lease.json) with 5-min TTL,
  refreshed on each proxy-poll cycle via refreshLease()
- detectStaleLeader() + cleanupStaleElection() for expiry detection and
  re-election after a crashed/idle leader
- runElectionWithRetry() with exponential backoff (1-2s) for non-leaders
- close() preserves the winning lease (crash-recovery: lease expiry, not
  explicit deletion, triggers takeover)
- Fail-safe: missing/unreadable files degrade to no-dispatch

Note: lock location uses .worklog/ (matching the parent AC's
downtime-coordination.json location and existing dispatch-log/round-robin
convention) rather than the child's .worklock/ mention.

30 unit tests covering lock acquisition, lease expiry, stale detection,
re-election, and graceful degradation.
…8151F)

Manages <worklog-root>/.worklog/downtime-coordination.json for the
downtime dispatcher refactor (parent WL-0MST3OJ8S0001ROL):

- One entry per instance: {instanceId, workItemId, directory, assignedAt,
  lastUpdated}
- Safe concurrent access: O_CREAT|O_EXCL exclusive lock (flock-equivalent
  for the single-machine v1 scope) + atomic tmp+rename writes — no partial
  JSON, no interleaved read-modify-write loss
- upsertEntry / removeEntry / getEntry / mergeEntries / pruneStaleEntries
- Leader-side pruning of entries older than the 5-minute lease bound
- Fail-safe: missing/corrupt file or lock contention → operation skipped,
  never throws, never touches another instance's entry

21 unit tests covering format, upsert/update/remove, lock serialization,
stale pruning, batch merge, and fail-safe paths.
…st (WL-0MSXHA1B0005G3E5, WL-0MSXHA7LP000QNGP)

Refactors the downtime dispatcher (parent WL-0MST3OJ8S0001ROL) so only the
elected leader dispatches work, from the shared coordination file:

- dispatchFromCoordination: leader re-fetches each entry's item and
  classifies it into its tier (audit → implement → plan → intake), applies
  the code-freeze + spare-capacity slot minimums, dispatches the
  highest-priority item via the existing CAS-claim/marker/spawn pipeline
  (AC5 guards preserved) and removes the entry for its owner to re-queue.
- classifyItemForDispatch: pure tier classifier (audit recency window,
  implement risk/effort caps, open-state guards — fail-closed).
- computeMostImportantItem / runCoordinationCheckIn: every instance's
  30-min check-in computes its most-important item and upserts/verifies its
  coordination entry (first check-in on startup; re-offers after dispatch;
  removes a dead entry on a genuine empty backlog; fail-open on wl errors).
- Worker tick: leader refreshes the 5-min lease per poll and polls the
  proxy; non-leaders skip proxy polling and dispatch entirely, running only
  the cheap lease-takeover check + the 30-min check-in (AC1/AC2/AC4).
- index.ts: wires coordinationDir + the fetchItem dep (with auditedAt
  enrichment for the audit tier).
- Legacy fallback: workers without a coordinationDir run the pre-refactor
  direct tier chain (fail-open; existing tests unchanged).

Also fixes the pre-existing herdr build breakage from the WL-0MSJ4BT4Z002HH9B
icon refactor (private _clampSelection access + DisplayRow union narrowing)
— triaged as WL-0MT5YFJ8A0059SDJ.

28 new tests (coordination-dispatch.test.ts) + hasLease coverage; the full
49-file herdr suite passes (1652 tests) and the package builds clean.
…67VAL)

- downtime-log.ts: new bounded rolling .worklog/downtime-coordination.log
  (separate from the dispatch log so marker readers never see coordination
  records) with appendCoordinationLogEntry (fail-closed, never throws)
- runCoordinationCheckIn logs check-ins (offered item / emptied entry);
  the worker logs leadership wins/takeovers; dispatchFromCoordination logs
  stale-entry pruning
- dispatchFromCoordination now prunes stale entries (lastUpdated older than
  the lease TTL) at the start of each leader dispatch cycle (parent AC3
  risk mitigation)
- README: documents the leader-election + shared coordination list
  architecture (election, lease, check-in, leader dispatch, fail-safe)

2 new coordination-log tests; full 49-file suite passes (1654 tests).
…low (WL-0MSXHAKDT005H6VQ)

End-to-end coverage of the refactored downtime dispatcher (parent
WL-0MST3OJ8S0001ROL):

- full flow: two instances → one elected leader → both check in → leader
  detects idle → dispatches the highest-priority offer into the OFFERING
  instance's worklog directory → entry removed → owner re-queues its next
  item at the next 30-min check-in
- lease expiry + takeover by a new instance (AC2)
- CAS regression: an already-in_progress offer is never re-dispatched
- stale-entry pruning by the leader dispatch cycle

Also fixes two implementation bugs surfaced by the tests:
- leader-election lock/lease writes now mkdir the coordination dir (fresh
  worklogs may lack it)
- dispatchFromCoordination re-reads the coordination file after pruning so
  a pruned stale entry can never classify or dispatch the same cycle

4 integration tests; full 50-file herdr suite passes (1658 tests); the
repo's full suite (tests/cli + tests/unit) passes after a fresh build.
- README `d` shortcut + Per-pane toggle section: scope is per-pane
  only (sibling panes / other roots unaffected); disable now persists
  across plugin restarts via the .herdr-downtime-disabled marker with
  a visible [Downtime Off (restored)] header notice; re-enable by
  pressing `d` again or deleting the marker; the toggle cycle is
  d=disable / d=follow-settings with NO force-enable state
- shortcuts.json d-shortcut description updated to match
- renderDowntimeStatus JSDoc documents the restored notice and the
  effective-gate agreement guarantee

Full suite: 5362 passed, 0 failures.

WL-0MT5SMS6Z0072P2L
…AC6)

Confirms H1 + H3 jointly explain the reported incident (audit
WL-0MSKSMRFP001Q3XU dispatched 11:05:23Z while dispatch believed
disabled, plus the 11:05-11:47Z multi-root wave):

- H1 (override lost/cycled): the old toggle force-enabled on a second
  press (null→false→true→null) and was in-memory only — worklist panes
  were (re)opened 10:25-10:28, silently reverting any earlier press to
  default-enabled; both variants now removed by the marker + new cycle
- H2 (in-flight dispatch): rejected — the dispatch log entry and pane
  spawn timestamps show a NEW dispatch, not a completion
- H3 (scope mismatch): confirmed contributor — no leader election, so
  every root dispatched concurrently by default; per-pane `d` scope now
  documented

Open questions Q1-Q3 closed/acknowledged. README links the RCA.

WL-0MT5SMVXS004T3BK
…lot mode (WL-0MT65T14L002HTWB)

The global idle tracker (idleSince) drives the title bar idle indicator,
but in per-slot mode it was fed the relaxed per-slot global gate
(perSlotGlobalIdleChecks: server-up + no model-switch only) which
deliberately ignores per-slot query activity. The displayed idle counter
therefore showed '[⏳ downtime idle Xs]' while the llama UI was actively
processing in a slot.

Fix (display-only): the tracker's record() call in the per-slot tick
branch now also requires that NO slot reports is_processing=true —
when any slot is processing (including the operator's own session), the
title bar shows '[downtime busy]'. Dispatch logic is unchanged: the
free-slot count and spare-capacity relaxation still come from
perSlotTracker, not tracker.idleSince.

Tests: updated two per-slot routing tests whose idleSince expectations
encoded the buggy display behavior; added four display tests covering
busy-while-processing, idle-only-when-all-free, reset/restart of the
idle counter, and global-busy reset.
…WL-0MSS1Q5ER007QDKX)

The coordination (leader-election) dispatch path checked the scheduled-
prompts tier only in legacy dispatchDowntimeWork; dispatchFromCoordination
never consulted getDueScheduledPrompt, so scheduled prompts never fired in
the default leader configuration (audit AC3/AC6 partial).

- Add the scheduled-prompts stage to dispatchFromCoordination as the FIRST
  dispatch stage (freeze-gated, same fail-closed semantics as the legacy
  path): a due prompt dispatches via dispatchScheduledPrompt (persist
  lastTriggeredAt → kind:scheduled marker → spawn) before any
  coordination-tier work, so it never triggers the no-candidate cooldown.
- Add coordination-path tests covering first-stage dispatch, persist+marker
  ordering, fail-closed marker aborts, freeze gating, and tier fall-through.
- Document the leader-path scheduled check in packages/herdr/README.md.
@SorraTheOrc
SorraTheOrc merged commit 50cd47b into main Aug 25, 2026
@SorraTheOrc
SorraTheOrc deleted the release/dev-to-main-20260825122023 branch August 25, 2026 12:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant