Repository navigation
Merge dev → main (automated) - #2086
Merged
Merged
Conversation
added 30 commits
August 25, 2026 20:58
Pair Status+Stage, Priority+Type, Created+Updated and Audit+AuditedAt onto single rows in the list-mode metadata panel and the detail-view table via pairMetaRows, a display-only post-processing step over buildMetaRows output. Incomplete pairs keep their present half as a single row so no field information is lost; noIcons mode is handled. Saves 4 vertical rows for fully-populated items.
Operator-sanctioned test-first commit (F1 mirror, plan comment 2026-08-26T01:45): C1 locks the per-slot cheap-switch gate semantics with unit tests BEFORE the production gate change, so the implementation child (WL-0MT9FL9JO002JEAZ) lands against a green, behavior-pinning suite. EXPECTED-RED at this commit by design — 4 new-behavior pins fail against the current all-slots-free gate: - per-slot 1-free-2-busy fires cheap (AC1) - spare-capacity >=1 free fires (AC3) - malformed per-slot payloads -> fetchProxyStatus resolves null -> no POST (AC6 x2) All 35 pre-existing mode-switch tests and the rest of the full suite pass (5505 passed at C1's verification run; tsc + build clean). Lifecycle: C1 stage -> in_review / status completed; C2 turns the pins green.
… list Parent audit (WL-0MT9F67Y3008S0PR belt-and-suspenders) flagged child AC6 as partial: the malformed-per-slot fixture list did not explicitly cover a non-finite slot_id, even though parseLlamaStatus rejects NaN/Infinity via its Number.isFinite/Number.isInteger guard (covered in downtime-worker tests). Add NaN and Infinity fixtures so the worker-level AC6 pin exercises the exact contract the parent AC text names. 39/39 mode-switch tests pass; tsc clean.
…L-0MTA217DZ003H5K8) The previous fix added isWlModifyingCommand and extended the refresh condition, but !!wl close/delete/update/search commands bypass dispatchChordCommand entirely (only !!wl reviewed was routed), so executeResolvedCommand returned 'callback' and the result === 'dispatched' refresh check never fired — the selection list stayed stale. Route these commands through resolveAndRouteCommand in dispatchChordCommand (mirroring !!wl reviewed) so they return 'dispatched' and the caller's isWlModifyingCommand check triggers an immediate list refresh after close/delete/update/search shortcuts (x-c, x-d, u-p-*, u-s, u-t, s). Update tests that locked in the old 'callback'/fall-through behavior.
…cStalenessMs setting - NEW: packages/herdr/src/db-change.ts — DB-change signal module with resolveCacheDir(), readStateCounter(), and DbChangeTracker - NEW: packages/herdr/src/db-change.test.ts — 14 tests covering cache-dir resolution, state-counter reads, and tracker fail-open behavior - EXTEND: packages/herdr/src/settings.test.ts — 7 tests for maxSyncStalenessMs (default 60s, clamp [1000, 300000], parse/clamp behavior) - EXTEND: packages/herdr/src/settings.ts — added maxSyncStalenessMs setting with MIN_MAX_SYNC_STALENESS_MS and MAX_MAX_SYNC_STALENESS_MS constants - EXTEND: packages/herdr/src/worklist.test.ts — 10 DB-change gate decision tests (refresh skip/run, sync skip/run, first-cycle, fail-open, manual) All 1,790 herdr tests pass.
…maxSyncStalenessMs setting
Centralize pane-title construction in src/pane-title.ts with three builders: buildManuallyTriggeredPaneTitle, buildShellPaneTitle, and buildDowntimePaneTitle. Every spawn path now produces a descriptive, bounded title instead of generic defaults. Changes: - src/pane-title.ts: shared module with builders, truncation, helpers - index.ts: onCommand handler gains paneTitle arg; agent and shell spawn paths compute pane names from title/id - worklist.ts: onCommand signature extended with optional paneTitle; callers compute paneTitle for agent/! commands from selected item - downtime-worker.ts: spawnAgentPane forwards itemTitle/itemId; buildDowntimePaneArgs uses shared builder - run-in-pane.sh: --pane-name option parsing (overrides env/default) - 18 existing tests updated for new 5-arg onCommand calls - 21 new pane-title tests covering all builders and length bounds
Operator directive (context: WL-0MTEZ4XZJ006Y9U7 downtime dispatch): the marker is an environment-specific toggle, not repo state; stop tracking it. Authorized by operator.
added 28 commits
August 30, 2026 06:37
…o-retry Add patterns for 'all providers exhausted' and 'no provider available' to DEFAULT_SERVER_ERROR_PATTERNS so the Worklog recovery module classifies these as ErrorCategory.SERVER_ERROR instead of UNKNOWN. This enables the existing retry loop (with exponential backoff) to automatically recover when proxy providers are temporarily exhausted, instead of halting the session and requiring user intervention. WL-0MT656GDS0052FTK
- close.ts: Standardize JSON output to {results, closed, failed} shape
instead of {success, results} — AC4 compliance
- Add batch close integration tests (AC5): multi-id close with
--force/--reason, partial failure isolation, JSON output structure,
and human-readable output
- Fix 3 test files that expected old {success, results} JSON format
from close command to match the new {results, closed, failed} shape
…irst) Operator-sanctioned test-first commit (per 1f05039 precedent, plan comment): locks the zombie-leader lease lifecycle with unit tests BEFORE the production fixes (F4 refreshLease self-heal, F5 per-tick re-derivation + cooldown-exit renewal) land, so the implementation children land against a behavior-pinning suite. EXPECTED-RED at this commit by design — 2 new-behavior pins fail against the current cached-leaderState code: - AC2: lease must be VALID after the cooldown-exit tick (Hole A — refreshLease no-ops on owned-but-expired lease; isLeader() requires remaining > 10s). FAILS: isLeaseValid(leaseAfterExit) === false. - AC3: no zombie dispatch after resume when another instance holds a valid lease (Hole B — leaderState cached at :2505, never re-derived while true; A dispatches inst-b's item and spawnAgentPane IS called). FAILS: spawn 1 call + isLeader true. Regression pins GREEN on current code: - AC1: lease content advanced during the no-candidate cooldown pause (sibling fix e842208 already runs the leader block before the cooldown gate). - AC4: worker created while a valid foreign lease is held stays silent (leaderState=false at startup; non-leader short-circuit). All 371 pre-existing downtime-worker tests and the rest of the herdr suite pass (1831 passed at verification; tsc clean; build excludes test files). Lifecycle: F2 stage -> in_review; F4/F5 turn AC2 + AC3 green.
…irst) Operator-sanctioned test-first commit (per 1f05039 precedent): locks the refreshLease() self-heal semantics (F4) with unit tests BEFORE the production change, so F4 lands against a behavior-pinning suite. EXPECTED-RED at this commit by design — 1 new-behavior pin fails against the current `if (!this.isLeader()) return` guard (Hole A): - self-heal AC1: an OWNED-but-EXPIRED lease must be renewed (fresh acquiredAt) instead of no-oped. FAILS: acquiredAt unchanged after refreshLease() (isLeader() requires > MIN_VALID_LEASE_TTL_MS remaining). Regression guards GREEN on current code: - AC2: never renews a valid foreign lease (content unchanged). - AC2b: never steals from an EXPIRED foreign lease (election path owns that). - AC3: unreadable/corrupt lease file → no-op, file untouched (fail-safe). All 36 pre-existing leader-election tests and the rest of the herdr suite pass (1832 passed at verification; tsc clean). F4 turns AC1 green. Lifecycle: F3 stage -> in_review; F4 then F5 turn the F2+F3 RED pins green.
…first) Operator-sanctioned test-first commit (per 1f05039 precedent): adds the dual-instance fake-timer regression the parent AC wanted BEFORE the production fixes (F4 self-heal refreshLease, F5 per-tick re-derivation + cooldown-exit renewal), so the implementations land against a reproducing suite. EXPECTED-RED at this commit by design — 1 new-behavior pin fails against the current cached-leaderState code: - F1: A elects itself leader, enters the no-candidate cooldown, its tick loop freezes mid-pause and its lease expires (TTL 300s < cooldown 600s); B detects the stale lease, wins a fresh election and keeps re-offering WL-B1 with fresh lastUpdated. On A's resume, the CACHED leaderState=true makes A zombie-dispatch B's entry (spawnAgentPane called 1x) and A's isLeader getter stays true — both assertions FAIL. (B is busy/0 free slots, so the only possible dispatch of the entry is A's zombie.) The 4 pre-existing integration tests and the rest of the herdr suite pass (1829 passed at verification; tsc clean). F5 + F4 turn the pins green. Scenario mechanics: A cooldown 600s > lease TTL (expires mid-pause); B's check-in interval 300s re-offers within the leader's 300s prune window, so B's entry survives pruneStaleEntries until A's dispatch tick (age 61s). Lifecycle: F1 stage -> in_review; F4/F5 turn F2/F3/F1 RED pins green.
…irst) Operator-sanctioned test-first commit (per 1f05039 precedent): locks the refreshLease() self-heal semantics (F4) with unit tests BEFORE the production change, so F4 lands against a behavior-pinning suite. EXPECTED-RED at this commit by design — 1 new-behavior pin fails against the current `if (!this.isLeader()) return` guard (Hole A): - self-heal AC1: an OWNED-but-EXPIRED lease must be renewed (fresh acquiredAt) instead of no-oped. FAILS: acquiredAt unchanged after refreshLease() (isLeader() requires > MIN_VALID_LEASE_TTL_MS remaining). Regression guards GREEN on current code: - AC2: never renews a valid foreign lease (content unchanged). - AC2b: never steals from an EXPIRED foreign lease (election path owns that). - AC3: unreadable/corrupt lease file → no-op, file untouched (fail-safe). All 36 pre-existing leader-election tests and the rest of the herdr suite pass (1832 passed at verification; tsc clean). F4 turns AC1 green. Lifecycle: F3 stage -> in_review; F4 then F5 turn the F2+F3 RED pins green.
F4 turns the F3 RED pin green: refreshLease() now renews by OWNERSHIP instead of validity. The old `if (!this.isLeader()) return` guard required > MIN_VALID_LEASE_TTL_MS remaining, so an owned-but-EXPIRED lease made refreshLease() no-op forever — the zombie could never renew (Hole A). New behavior: - lease.leaderId === instanceId (even if expired) → rewrite with fresh acquiredAt, same TTL (self-heal). - missing/unreadable lease (readLeaseFile → null) → no-op (fail-safe). - FOREIGN lease → no-op (takeover stays on the election path; never renews or stops another instance's lease — takeover-race safe). isLeader() / isLeaseValid unchanged for claim/election paths. Verification (F3 suite + full herdr): 37/37 leader-election tests pass incl. F3 AC1 self-heal (previously RED), 1833/1833 full herdr suite, tsc + build clean. F5 (per-tick leader re-derivation + cooldown-exit renewal) turns the F2/F1 worker-level RED pins green.
…irst) Operator-sanctioned test-first commit (per 1f05039 precedent, plan comment): locks the zombie-leader lease lifecycle with unit tests BEFORE the production fixes (F4 refreshLease self-heal, F5 per-tick re-derivation + cooldown-exit renewal) land, so the implementation children land against a behavior-pinning suite. EXPECTED-RED at this commit by design — 2 new-behavior pins fail against the current cached-leaderState code: - AC2: lease must be VALID after the cooldown-exit tick (Hole A — refreshLease no-ops on owned-but-expired lease; isLeader() requires remaining > 10s). FAILS: isLeaseValid(leaseAfterExit) === false. - AC3: no zombie dispatch after resume when another instance holds a valid lease (Hole B — leaderState cached at :2505, never re-derived while true; A dispatches inst-b's item and spawnAgentPane IS called). FAILS: spawn 1 call + isLeader true. Regression pins GREEN on current code: - AC1: lease content advanced during the no-candidate cooldown pause (sibling fix e842208 already runs the leader block before the cooldown gate). - AC4: worker created while a valid foreign lease is held stays silent (leaderState=false at startup; non-leader short-circuit). All 371 pre-existing downtime-worker tests and the rest of the herdr suite pass (1831 passed at verification; tsc clean; build excludes test files). Lifecycle: F2 stage -> in_review; F4/F5 turn AC2 + AC3 green.
…irst) Operator-sanctioned test-first commit (per 1f05039 precedent): locks the refreshLease() self-heal semantics (F4) with unit tests BEFORE the production change, so F4 lands against a behavior-pinning suite. EXPECTED-RED at this commit by design — 1 new-behavior pin fails against the current `if (!this.isLeader()) return` guard (Hole A): - self-heal AC1: an OWNED-but-EXPIRED lease must be renewed (fresh acquiredAt) instead of no-oped. FAILS: acquiredAt unchanged after refreshLease() (isLeader() requires > MIN_VALID_LEASE_TTL_MS remaining). Regression guards GREEN on current code: - AC2: never renews a valid foreign lease (content unchanged). - AC2b: never steals from an EXPIRED foreign lease (election path owns that). - AC3: unreadable/corrupt lease file → no-op, file untouched (fail-safe). All 36 pre-existing leader-election tests and the rest of the herdr suite pass (1832 passed at verification; tsc clean). F4 turns AC1 green. Lifecycle: F3 stage -> in_review; F4 then F5 turn the F2+F3 RED pins green.
…first) Operator-sanctioned test-first commit (per 1f05039 precedent): adds the dual-instance fake-timer regression the parent AC wanted BEFORE the production fixes (F4 self-heal refreshLease, F5 per-tick re-derivation + cooldown-exit renewal), so the implementations land against a reproducing suite. EXPECTED-RED at this commit by design — 1 new-behavior pin fails against the current cached-leaderState code: - F1: A elects itself leader, enters the no-candidate cooldown, its tick loop freezes mid-pause and its lease expires (TTL 300s < cooldown 600s); B detects the stale lease, wins a fresh election and keeps re-offering WL-B1 with fresh lastUpdated. On A's resume, the CACHED leaderState=true makes A zombie-dispatch B's entry (spawnAgentPane called 1x) and A's isLeader getter stays true — both assertions FAIL. (B is busy/0 free slots, so the only possible dispatch of the entry is A's zombie.) The 4 pre-existing integration tests and the rest of the herdr suite pass (1829 passed at verification; tsc clean). F5 + F4 turn the pins green. Scenario mechanics: A cooldown 600s > lease TTL (expires mid-pause); B's check-in interval 300s re-offers within the leader's 300s prune window, so B's entry survives pruneStaleEntries until A's dispatch tick (age 61s). Lifecycle: F1 stage -> in_review; F4/F5 turn F2/F3/F1 RED pins green.
F4 turns the F3 RED pin green: refreshLease() now renews by OWNERSHIP instead of validity. The old `if (!this.isLeader()) return` guard required > MIN_VALID_LEASE_TTL_MS remaining, so an owned-but-EXPIRED lease made refreshLease() no-op forever — the zombie could never renew (Hole A). New behavior: - lease.leaderId === instanceId (even if expired) → rewrite with fresh acquiredAt, same TTL (self-heal). - missing/unreadable lease (readLeaseFile → null) → no-op (fail-safe). - FOREIGN lease → no-op (takeover stays on the election path; never renews or stops another instance's lease — takeover-race safe). isLeader() / isLeaseValid unchanged for claim/election paths. Verification (F3 suite + full herdr): 37/37 leader-election tests pass incl. F3 AC1 self-heal (previously RED), 1833/1833 full herdr suite, tsc + build clean. F5 (per-tick leader re-derivation + cooldown-exit renewal) turns the F2/F1 worker-level RED pins green.
F5 turns the F1/F2 RED pins green (Hole B: leaderState was derived once at worker creation and only updated inside the takeover branch — a lease that expired mid-pause left the worker dispatching with cached leadership as a zombie). Changes in the leader block of tick(): - AC2 self-heal FIRST: leaderManager.refreshLease() now runs UNCONDITIONALLY each tick. With F4's ownership-based refresh this renews an owned-but-EXPIRED lease; foreign/missing leases stay untouched (no-op). Must precede re-derivation: detectStaleLeader() returns false for our own lease, so an expired owned lease would otherwise fall through BOTH the leader branch (isLeader false) AND the takeover branch (hasLease true + detectStaleLeader false), starving the self-heal entirely. - AC1 per-tick re-derivation: leaderState = leaderManager.isLeader() every tick (one cheap lease-file read). If another instance won the lease during a pause, the re-derived state yields at the non-leader short-circuit — never fights the new leader, never double-dispatches (AC4). Non-leader with valid foreign lease stays silent (AC5); missing/unreadable lease ⇒ not leader (AC6, preserved). - AC3 cooldown-exit: the resume tick's leader block runs BEFORE the cooldown gate, so renewal + re-derivation always precede any poll/dispatch decision on the resume path — even if the tick loop stalled mid-pause (documented at the gate). Verification: full herdr suite 1838/1838 (F2 cooldown AC1-AC4 + F3 self-heal + F1 zombie integration all GREEN; 378/378 in the two worker suites), tsc + build clean. Parent AC suite now fully satisfied by F2+F3+F1 (tests) with F4+F5 (impl). Lifecycle: F5 stage -> in_review; sibling branch recomposition for parent.
Update docs/dev/downtime-dispatcher.md to the corrected lease contract implemented by F4+F5: - Lease section: refresh happens during the no-candidate cooldown pause too; refreshLease() is ownership-based (leaderId === instanceId), so an owned-but-EXPIRED lease is still renewed (self-heal — never silently lost). - New bullet: leadership re-derived every tick (cheap lease-file read) — a lease expired mid-pause routes the worker out of zombie dispatch; a foreign winner during the pause yields silently (no fight/double dispatch). - Cooldown section: self-heal keeps the renewal path alive through the pause; cooldown-exit renewal + re-derivation run BEFORE the cooldown gate on the resume tick, so no dispatch decision can use stale leadership even if the tick loop stalled mid-pause. docs/ARCHITECTURE.md verified — no cooldown/lease content (only Git file lock) → untouched per AC2. Inline code comments were synced to the new contract in F4 (leader-election.ts refreshLease) and F5 (downtime-worker.ts leader block + cooldown gate) already. Verification: full herdr suite 1838/1838 on the complete F2→F3→F1→F4→F5 chain + this branch; tsc + build clean. Full-repo non-herdr failures (plugin/sync/completion/validator/grouping/smoke/doctor) reproduce identically in the F2 test-only worktree — pre-existing base-commit environments, unrelated to F1-F6. Lifecycle: F6 stage -> in_review; all six children terminal.
… (WL-0MTGGGP2000642JS) When the local store holds more records than the remote tip (e.g. after a foreign-author full snapshot replaced the remote with fewer records), the watermark heuristic alone cannot detect this — records older than the watermark are assumed 'already synced' even though the remote never received them. The §5.4 zero-change fast path silently skips export+push, causing the remote to diverge (missing ~102 of 124 local items). Fix: compare local item/comment counts against the remote tip during sync. When local > remote, emit a visible warning, force a full snapshot export, and prevent the zero-change fast path from short-circuiting. The remote converges to the local state on the next sync (self-healing, AC2). Changes: - Added count-based divergence detection in performSync() - Moved syncMode computation after divergence detection (syncMode was previously computed before, causing delta mode even when full was forced) - Added console.warn() for operator visibility (AC1) - Added zero-change fast path exclusion when divergence is detected (AC3) - Added regression test: sync-divergence-detection.test.ts
…al deletes F1 (regression tests): 14 new tests covering: - same-timestamp sortIndex-only never bumps updatedAt (AC1) - metadata conflict bumps once then stabilises (AC2) - unattributed delete never overrides live open (AC3 same-ts & newer) - attributed delete propagates — newer & equal-timestamp (AC4) - both-sides-deleted converges; close-preserved stays quiescent Adapted existing close-conflict test: bumped-at assertion replaced with quiescent semantics — per F2 design, no-content-change merge must NOT bump updatedAt (the unconditional bump was the sync-conflict-loop cause). F2 (same-timestamp merge bump fix): only bump updatedAt when mergedFields.length > 0 (non-sortIndex user-visible change) AND merged result differs from local. Conflicts still reported; bump only on real change. F3 (delete-side protection): in both mergeSameTimestampItems and mergeDifferentTimestampItems, unattributed soft-deletes (empty deletedBy/ deleteReason) never override a live item's status. Only attributed deletes or both-sides-deleted converge to deleted. F4 (attribute real deletes): WorklogDatabase.delete() now accepts optional deletedBy/deleteReason. CLI delete and REST API attribute via configured git identity (getConfiguredUserEmail()). ConflictFieldDetail.chosenSource extended with 'both' variant for both-sides-deleted. Pre-existing test failures (5) in sync-delta-fallback/push/convergence: verified as dev-HEAD baseline failures, not introduced by this change.
…so delta pushes aren't forced to full The WL-0MTGGGP2000642JS count-based divergence detection fired on any local > remote count mismatch, but that is the normal transient state during a delta push (a freshly created local item not yet on remote). Genuine divergence is only when the mismatch cannot be repaired by a delta export — i.e. no dirty records exist to carry the missing data (the zero-change fast path would silently skip). Gate item/comment divergence checks on dirty.items === 0 / dirty.comments === 0. Fixes 5 failing tests: sync-delta-push (AC1-AC4, cadence), sync-delta-fallback (AC1/AC2/AC5, delta tip), sync-integration-convergence (AC7).
…-exit-re' into dev
…test-int' into dev
…der-elec' into dev
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Automated release created by ship skill.\n\nIncludes CHANGELOG.md with work-item summaries from this release.