Skip to content

v1.0.0

Choose a tag to compare

@github-actions github-actions released this 09 Sep 05:00
· 12 commits to main since this release

What's new

Changed

  • "Fetch all" now says what it is doing and can be stopped.
  • Every daemon-owned setting the dashboard mirrors now lives in one table.

Added

  • The unattended loops now keep a record of what they did, and can be stopped mid-round.
  • Every conflict can now be resolved from the panel, including the ones AI cannot touch.
  • The database can be checked, snapshotted and repaired without leaving the app.
  • A browser gate now runs in CI and blocks a release.
  • Auto-commit skips and sync failures now leave a reviewable trail, with a panel to review it in.
  • Grouped operational-error history, now with a Settings panel.
  • AI providers can hold a rotation pool of keys, not just one.

Security

  • A page on another local port can no longer drive the daemon's API.
  • Revoking the API token now survives a restart, and minting one reports where it really went.
  • Binary updates now come only from the RepoYeti release location on GitHub, bounded in bytes and time.
  • AI provider responses now have a byte ceiling.
  • Dependency advisories cleared where the fix exists within the declared ranges.

Fixed

  • Switching an automation loop off now actually stops the round in flight.
  • One damaged cached status no longer empties the whole repository list.
  • The auto-commit and sync-check timers can no longer double up.
  • Settings synced from Connections are validated like settings set in the app.
  • You can read the whole MCP request before approving it.
  • A scan that fails to start or stop now says so, and a phone that lost the stream mid-scan recovers.
  • A repo removed while the dashboard was loading no longer comes back, and a status that arrives before its repo is listed is no longer lost.
  • Saving several settings at once writes the config file once.
  • Commit details and the incoming preview decode file names from git's NUL-delimited records.
  • Source-checkout updates are only advertised when they can actually be applied.
  • "Create and push tag" pushes through the repo's selected GitHub account, and a failed push can be retried.
  • A cold History view no longer launches several git processes per read slot.
  • The privacy copy now says what actually leaves the machine.
  • Release notes and contributor instructions no longer describe a build that does not ship.
  • A file save can no longer overwrite an edit that landed first.
  • Committing a selection of files no longer disturbs the staging of everything else.

Internal

  • Database ownership is split by domain, behind the same public surface.
  • Every schema change now leaves a record of whether it worked.
  • The browser gate's menu sweep was measuring one menu while claiming to sweep them all.
  • The Windows rebuild helper is misc/rebuild_repoyeti.bat.
Full notes

Changed

  • "Fetch all" now says what it is doing and can be stopped. It processed one repository at a
    time and returned only after the whole set finished, so the phone got a spinner, no idea how
    many repositories there were or which one it was on, and no way out. On a large installation, or
    behind a credential helper waiting on something that will never arrive, that is a long opaque
    action with the queue still marching. It is now a job like the project scan: the request only
    acknowledges the start, the counters and the current repository stream over SSE, and a Stop in
    the header ends it. Stopping means the sweep starts no further repositories; the one already
    fetching is allowed to finish, because aborting git mid-transfer is how a lock file gets left
    behind. It is still deliberately serial: progress reporting is the answer to "this is slow", not
    more parallel network work. A phone that backgrounded mid-sweep asks the daemon what happened
    when it comes back, and the daemon keeps the last run's counters so the answer is the summary it
    missed rather than just "nothing is running". The scan and the fetch now share one job
    lifecycle, which also gives both of them something they lacked: a run that ends by throwing
    still emits a terminal event, so the spinner cannot run forever on a job that died.

  • Every daemon-owned setting the dashboard mirrors now lives in one table. Forty-odd runtime
    fields were declared in the root store, defaulted again in one snapshot function, re-validated
    again in six separate settings_changed handlers, and threaded through a forty-entry dependency
    bundle so those handlers could reach them. Adding a setting meant four coordinated edits in four
    distant blocks, and forgetting any of them failed silently: the field would update on a settings
    change but not on a reconnect, or the reverse. store/runtime-status.ts now holds one row per
    field saying where it comes from in GET /api/status, what an absent value there means, which
    broadcasts patch it, and what a patched value has to look like to be believed. Leaving a field
    out of the table is a build error. The two semantics the old code kept restating differently are
    now stated once: a snapshot is authoritative, so an absent key resets the field to the daemon's
    own default (which is what a share-link guest's deliberately narrow projection produces), while
    settings_changed and daemon_status are patches, so an absent key changes nothing. The root
    store lost 260 lines and the dashboard has 14 new tests, including one that fails by name if a
    future table row points at a key the daemon does not send.

Added

  • The unattended loops now keep a record of what they did, and can be stopped mid-round. Auto
    commit and the background sync check reported themselves only over the live event stream, which
    reaches whoever happens to have the dashboard open at the moment the timer fires. For a feature
    whose entire purpose is running while nobody is watching, that is close to nobody. The incident
    ledger covered half of the gap and only half, deliberately: it is a list of open problems, one
    row per repository and reason, acknowledged and then closed, so it can neither record a
    successful commit nor say how long anything took without ruining the "needs attention" count it
    exists to feed. Every round now also writes a run row saying when it ran, whether the timer or
    the owner started it, how it ended and how long it took, with a child row per repository it
    actually touched, capped at the newest 300 rounds. Repositories where nothing happened produce
    no row, because a history that is mostly "nothing, 200 times" answers no question. A run whose
    daemon was killed is marked interrupted at the next boot rather than sitting in flight forever,
    and its end time stays empty rather than being invented. Settings, Automation now shows the live
    round with a Stop, then the recent rounds, each expanding to the per repository breakdown. New
    owner-only routes: GET /api/automation/runs, GET /api/automation/runs/:id and
    POST /api/automation/cancel.

  • Every conflict can now be resolved from the panel, including the ones AI cannot touch. The
    resolver offered exactly one action per file, and only for files it could parse: a text file,
    small enough to read, carrying usable markers. A binary asset, a generated file past the size
    cap and a delete or modify pair were listed with a reason and then completely inert, no button
    of any kind, so finishing that merge meant leaving the panel, finding the same path in a
    terminal and remembering the plumbing. Three actions now sit on every row. Open puts the file in
    the viewer this app already has, for hand editing the markers. Keep ours and Keep theirs take
    one side whole, and they work on everything, because the daemon copies out of the index rather
    than reading the file: that is also the only thing that can express "keep the deletion" for a
    path one side removed. Stage is the explicit finish, and it refuses while the file still
    contains conflict markers, which is exactly the mistake a hand resolution makes and which
    nothing anywhere caught before. Keeping a side deliberately does NOT stage, for the same reason
    applying an AI resolution does not: "I picked a side" and "the merge is finished" stay two
    different states, so git commit keeps refusing and the auto-commit safety gate keeps skipping
    the repository until the owner says otherwise.

  • The database can be checked, snapshotted and repaired without leaving the app. There was no
    way to ask whether the store was healthy, no way to take a copy of it that is actually
    consistent, and no way to recover from the one kind of damage that is genuinely recoverable.
    GET /api/db/verify runs SQLite's own integrity check, counts dangling references, reads the
    new migration ledger and counts cached rows this build can no longer parse, and changes nothing
    at all. POST /api/db/backup writes a real SQLite snapshot beside the database and refuses to
    overwrite an earlier one: a plain file copy of a live database in write-ahead mode captures a
    torn page set with the committed tail sitting in a sidecar file nobody copied, which is a backup
    that looks fine until the day it is needed. POST /api/db/repair-status-cache is the only one
    that changes stored data, and it is a separate call on purpose, made after reading the other
    two, because a recovery tool that quietly repairs while it diagnoses is one nobody can safely
    run twice.

  • A browser gate now runs in CI and blocks a release. Three things this project depends on were
    invisible to every test it had, because all three only exist once a real browser has laid out a
    page and stamped a request. The loopback guard's foreign-origin refusal was proven by fabricating
    headers and calling the app directly, which tests the decision table and not the premise it rests
    on. The SSE reconnect tests mock @vueuse/core, so no EventSource is ever opened. The popper
    guard reads Vue templates, so it proves the anchor wiring is structurally right and says nothing
    about whether the menu lands on screen, which is exactly how issue #15 shipped four times with
    correct aria and nothing logged. bun run --cwd web test:gate boots an isolated daemon on an
    ephemeral port with a throwaway state directory, its own scratch repositories and an in-memory
    keychain, serves an attack page from a second loopback port, and checks that a page on another
    local port cannot drive a write, that a dashboard which loses its stream converges on the
    daemon's truth when it returns, and that every menu and popover opens inside the window and is
    reachable. It needs nothing running and touches nothing of yours, which is why it can run
    anywhere. Measured against a daemon with the fix removed: the foreign page's write succeeds and
    the gate goes red. The older test:e2e suite is unchanged and still the developer one.

  • Auto-commit skips and sync failures now leave a reviewable trail, with a panel to review it
    in.
    The scheduled auto-commit timer already refused to touch a conflicted or mid-operation
    repo, and already told any dashboard connected at that exact moment via SSE, but nobody is
    always connected to an unattended timer, and once that broadcast passed, the only record of the
    skip was gone. A repo could sit un-synced for days with nothing to show for it beyond
    "auto-commit is on and yet this tree never moves." Every skip (CONFLICT, AI_UNAVAILABLE,
    ERROR, …) and every otherwise-successful round that still carries a sync note (e.g.
    NON_FAST_FORWARD) is now persisted as one incident row per open (repo, reason) pair (repeat
    ticks of the same unresolved problem bump that row instead of piling up new ones), capped at the
    newest 500 across all repos. New routes: GET /api/auto-commit/incidents (optionally
    ?unackedOnly=1) and POST /api/auto-commit/incidents/:id/ack, surfaced in Settings →
    Automation as an incident list under the auto-commit controls, with an unacked-count badge and
    an Acknowledge button per row. Data shape adapted from Hermes Agent's cron/incidents.py (MIT).

  • Grouped operational-error history, now with a Settings panel. The dashboard has always
    shown a repo's status right now (ahead/behind, dirty, conflicted) but nothing about what has
    gone wrong operationally over time: a fetch that has failed against a rotated SSH key five times
    in a row read identically to one that failed once, moments ago, on a flaky connection.
    runAction (src/service/core.ts), the single funnel every mutating git action
    (fetch/pull/push/commit/checkout/branch/stash/tag/remote) already goes through, now records
    every failure, grouped by a fingerprint of (repo, operation, error code), adapted from
    PostHog's issue-tracking grouping pattern (products/error_tracking/, MIT). Repeated failures
    of the same kind bump an occurrence count and refresh the message rather than piling up as
    separate rows. New owner-only routes: GET /api/errors (the grouped list),
    POST /api/errors/:fingerprint/mute, and DELETE /api/errors/:fingerprint - none reachable by
    a share-link guest, and the change events they emit are not in the guest SSE allowlist either.
    Settings → Advanced now has an Operational errors panel listing every group with mute/unmute and
    dismiss actions.

  • AI providers can hold a rotation pool of keys, not just one. Every provider (Groq, OpenAI,
    Gemini, ...) held exactly one bring-your-own key, so an owner on a free tier who hit that
    tier's rate limit blocked Smart Commit, commit-message drafting, conflict-resolve, and the
    unattended auto-commit timer until the provider's own window reset, even though an owner
    juggling several free accounts for the same provider had a second key sitting unused. A
    provider's config can now carry additional pool keys (PUT /api/ai/providers/:provider/keys);
    every generation/model-list call tries the pool in round-robin order and cools a key down only
    on the outcome that is actually its fault (rate-limited or auth-rejected), so a spent free-tier
    key no longer stalls the daemon while a working one sits idle in Settings.
    GET /api/ai/providers/:provider/keys shows non-secret per-key health (a fingerprint and
    cooldown state, never the key itself).

Security

  • A page on another local port can no longer drive the daemon's API. The loopback guard in
    front of /api/* ran in its default mode, which accepts any loopback Origin whatever its port.
    Per the Fetch spec a site ignores the port, so a preview server, a docs build or another daemon's
    dashboard on 127.0.0.1:<other> was same-site with RepoYeti: its browser stamped
    Sec-Fetch-Site: same-site, a simple text/plain POST needed no CORS preflight, and in local
    mode the route ran unauthenticated (in remote mode the host-scoped owner cookie rode along, since
    cookies do not see ports). The 1.0 audit reproduced the guard's decision for
    Origin: http://127.0.0.1:31337: allowed. The guard now runs in its exact-origin mode against the
    origin the daemon actually bound (127.0.0.1, localhost and ::1 on the bound port, read per
    request so a port hop is followed). The Vite dev origin is admitted only under REPOYETI_DEV=1
    (bun run dev) or an explicit REPOYETI_DEV_ORIGINS. Non-browser clients send no Origin and
    are unaffected. The existing tests covered a cross-site origin and the daemon's own; the
    foreign-local-port case is now a regression test.
  • Revoking the API token now survives a restart, and minting one reports where it really went.
    DELETE /api/auth/token and "sign out everywhere" cleared the in-memory token and answered ok,
    but the keychain delete underneath was best-effort and its failure was swallowed: on a locked or
    denied credential store the durable copy stayed put, and the next daemon boot hydrated the
    revoked token straight back. Mint had the mirror-image gap: it ignored whether the keychain
    accepted the new token, so it could print a token that died with the process while the old one
    lived on. Neither route ever saved the config, so on a keychain-less host a mint was not
    durable until some unrelated setting happened to be saved. Both routes now change live access
    first, keep the credential store's answer instead of assuming it, and save the config: a refused
    delete leaves a persisted apiTokenRevoked tombstone that stops hydration from loading that slot
    and makes each boot retry the delete; a refused write falls back to the owner-only config file.
    The responses say what happened (durable, keychainCleared, store), and repoyeti token
    prints the note. Covered by regression tests with an injected failing store, including the
    simulated restart.
  • Binary updates now come only from the RepoYeti release location on GitHub, bounded in bytes
    and time.
    The update check's metadata comes from the Connections Studio proxy (GitHub's API as
    the fallback), and that JSON used to supply both the archive URL and the checksum-manifest URL.
    Both were fetched from wherever they pointed with redirects followed blindly, and a manifest that
    agreed with the archive was taken as proof. A checksum from the same untrusted source proves the
    transfer was intact, not who published it: a wrong or compromised proxy response could name any
    host and any file, and the updater renames what it downloads over the running executable. Asset
    URLs are now accepted only when they are exactly the GitHub release-asset location for
    LunarWerxs/RepoYeti, the release's own tag and the expected asset name; redirects are followed
    by hand and must land on github.com or a *.githubusercontent.com host; the byte count is
    bound to the size the release lists and to an absolute ceiling; the SHA-256 is computed over the
    stream (the archive is no longer read whole into memory); the manifest is fetched first, so an
    unverifiable release is refused before the large download; and the release record the version
    check returned is the one staged from, instead of asking "latest" twice and verifying a newer
    release against the older tag. Every transfer and the extractor have a deadline, so a body that
    sends headers and never finishes no longer holds the daemon in "applying" forever, deferring
    later updates and refusing a dashboard restart. Malformed metadata is refused. This is not a
    signature over the release: the trust established is "GitHub served this for LunarWerxs/RepoYeti
    under this tag", which is the trust the release workflow already rests on.
  • AI provider responses now have a byte ceiling. The shared provider HTTP reader read every
    response body whole before parsing it or trimming an error message, so a misconfigured or
    hostile configured endpoint could make the daemon allocate whatever it chose to send; the
    request timeout bounded the time, not the bytes. Success and error bodies are now read through a
    bounded stream reader (4 MB), cancelled at the limit and reported as a short error. The update
    check's metadata documents get the same treatment (1 MB).
  • Dependency advisories cleared where the fix exists within the declared ranges. Hono moves
    from 4.13.2 to 4.13.5 (query parameters read past a URL fragment, unbounded parseBody()
    nesting, and the toSSG() path issue; only the first has a plausible path into this daemon, and
    it needs a literal fragment to reach the runtime). Dashboard tooling: Vitest 4.1.11 and the
    in-range transitive updates for nanoid, PostCSS, browserslist, baseline-browser-mapping and
    brace-expansion, plus the fast-uri override from 3.1.5 to 3.1.6. brace-expansion deliberately
    keeps its two majors (see the note in web/package.json): flattening them broke the PWA
    precache once before. Both bun audit runs are clean. Everything but Hono is build-time or
    test-time tooling.

Fixed

  • Switching an automation loop off now actually stops the round in flight. Turning auto commit
    or the background sync check off disarmed the next timer and did nothing else, so an unattended
    pass already under way carried on committing and pushing through every remaining repository
    after the owner had switched the feature off. That is the one moment they are most clearly
    saying stop. Both toggles now also ask the running round to stop, which is cooperative and
    narrow by design: the repository being committed, fetched or pulled right now is allowed to
    finish, and only the ones after it are dropped. Killing a git process mid transfer is how a lock
    file gets left behind in the repository the owner was least expecting to touch. The scheduling
    rule that a cadence change must never abandon work halfway is unchanged.

  • One damaged cached status no longer empties the whole repository list. Each repository's
    last known state is cached as a small blob, written by this daemon and read back by it, and it
    was parsed with no guard at all. A single truncated row, from a disk that filled mid write or a
    process that was killed, threw on the way out of the repository list and took every other
    repository with it: a dashboard showing nothing whatsoever, over one repository's stale cache.
    That row now reads as "not scanned yet", which is what it is, everything else lists normally,
    and the count of such rows is reported by the new database check for an owner who wants to clear
    them on purpose.

  • The auto-commit and sync-check timers can no longer double up. Both loops carried the same
    hand-rolled scheduling: a manual round checked the in-flight flag, but the timer's round set it
    without checking, so a timer that fired while a manual round was still waiting on the AI or the
    network started a second full pass over every repository (planning, status scans, fetches,
    baselines and incident rows all duplicated; the per-repo queue still serialised the actual git
    mutations, which is why it never lost data), and whichever round finished first cleared the
    shared flag under the other. One small controller now owns both loops: at most one round in
    flight, at most one timer armed, exactly one re-arm when a round ends, and disable or re-enable
    mid-round handled. Tested against a controlled clock, including the timer-fires-during-a-manual-
    round case that used to go wrong.

  • Settings synced from Connections are validated like settings set in the app. The sync
    applied each allowlisted remote value with a plain cast on the reasoning that the store only
    ever holds what this same allowlist wrote. That holds for a healthy peer and not for a stale,
    malformed, differently-versioned or tampered document: a one-second timer cadence, a mode outside
    its enum or an editor id that is not an editor would have been persisted to config.json, values
    PUT /api/settings refuses. Every synced key now goes through one codec that applies the same
    clamps, closed enums and editor catalogue the settings route uses; a value that fails is ignored
    and logged by key name only, never by value. The appearance blob is bounded to a flat object of
    small primitives. Adding a synced key without a codec is now a build error, like adding a config
    key without a sync decision already was.

  • You can read the whole MCP request before approving it. The approval card showed each
    argument clipped to 80 characters while the tool ran with the full original arguments, so a
    branch name or commit message that differed past the visible prefix could not be inspected
    before approval. The daemon now keeps a bounded, secret-redacted copy of the exact request the
    tool will run with (per-value and whole-request ceilings; token/secret/password-style fields
    shown as hidden, by name) and serves it at GET /api/approvals/:id; the list and the live
    events keep carrying only the short summary. The approval card gained a "Show full request"
    toggle that fetches it on first expand and renders every argument in full, hidden fields as a
    labelled placeholder, with a note when anything was shortened for display.

  • A scan that fails to start or stop now says so, and a phone that lost the stream mid-scan
    recovers.
    The Scan modal started a scan with an unhandled promise and asked for cancellation
    without handling a failed request: a failed start cleared the spinner with no explanation, and a
    failed stop left the modal waiting for a scan_cancelled event a disconnected phone never
    received. Start and stop are awaited and report a localized, retryable error; the stop control
    shows "Stopping…" until the daemon confirms; and on SSE reconnect the dashboard asks the new
    GET /api/scan whether a scan is still running, settling the spinner if not, and leaving it
    alone when the question itself fails (an unknown server-side job is never marked stopped).

  • A repo removed while the dashboard was loading no longer comes back, and a status that arrives
    before its repo is listed is no longer lost.
    The dashboard fetches the repository list and opens
    the event stream at the same time (and again on every reconnect), and the daemon has no event
    cursor. An event that arrived while the list request was in flight was applied to the OLD list:
    a removal filtered it and the late snapshot put the repo straight back; a status for a repo the
    old list did not have was dropped, so the phone stayed stale until another matching event
    happened by. Repo-scoped events are now held while a list snapshot is in flight and replayed, in
    order, the moment it lands (on the failure path too), and a status older than the one already
    installed is never applied, so a late frame cannot move a card backwards. Covered by store tests
    with a deferred list request and a fake stream, including the reconnect resync.

  • Saving several settings at once writes the config file once. PUT /api/settings applied
    each field as an independent block that serialised and atomically replaced config.json and
    emitted its own settings_changed event, so a multi-field save meant one file replacement and one
    SSE frame per field, overlapping. Every accepted field is still applied in the same order with the
    same runtime effects and the same clamping, and the documented partial-acceptance rule still
    holds (an unrecognised changesStatDisplay is refused after everything else has been applied),
    but the durable save and the notification now happen once, with one merged payload.

  • Commit details and the incoming preview decode file names from git's NUL-delimited records.
    Both readers split the human-oriented --name-status and --numstat output by lines and tabs,
    so a path containing a tab or a newline (legal on Unix) broke the split, and any path git quotes
    under the default core.quotePath (every non-ASCII name) came back as its escaped spelling,
    "h\303\251llo.txt" for héllo.txt, which names no file the viewer could open. Stats were also
    zipped onto files by row position. Both now ask git for -z records, decode them through one
    shared decoder, keep both sides of a rename, and join stats to files by identity. The incoming
    file list gains from on a rename. Regression tests cover a non-ASCII name and a rename on every
    platform, and tab and newline names where the filesystem allows them.

  • Source-checkout updates are only advertised when they can actually be applied. The shared
    update engine proved that the remote commit was not an ancestor of the local one and called a
    clean checkout applicable. That included diverged histories, which git pull --ff-only refuses,
    and a local branch the remote does not have, where the check fell back to the remote's HEAD but
    the apply still pulled the local branch name; both advertised an update that failed every cycle.
    The check now fetches just the resolved branch (nothing local moves) and proves the fast-forward
    before advertising; a diverged checkout is reported as such (diverged, with the reason), an
    unverifiable fetch says so, and the apply pulls the branch the check resolved (remoteBranch).
    Changed upstream in the shared kit and synced here.

  • "Create and push tag" pushes through the repo's selected GitHub account, and a failed push can
    be retried.
    The tag push carried the identity's SSH options but not the HTTPS credential an
    ordinary push injects, so a repo assigned to a non-active GitHub account pushed with whatever
    login was ambient, or failed; and because the local tag was (correctly) kept, running create
    again only answered "already exists". The push now uses the same per-child credential injection
    and error classification as push, the local tag write never resolves a token, and
    POST /api/repos/:id/tag/push pushes an existing local tag on its own.

  • A cold History view no longer launches several git processes per read slot. The commit-stat
    enrichment ran its diff-tree chunks in parallel inside one read-gate transaction: four children
    for a Daily view, up to fourteen for Hourly, all charged to a single slot and multiplied by
    concurrent requests, which is exactly the process burst the gate exists to prevent (measured at
    20 to 40 git children on Windows in the incident that introduced the gate). The chunks now run
    one at a time; a failing chunk still costs only its own statistics.

  • The privacy copy now says what actually leaves the machine. The README, the website and the
    Settings → AI description said code never leaves the machine and keys never leave the daemon.
    Neither is true as written: Smart Commit, AI commit messages and AI conflict resolution send the
    changed file list and diff (or the conflicted text) to the AI provider you configured, and the
    key is transmitted to that provider to authenticate; Git traffic goes to your own remotes; and
    optional settings sync sends dashboard preferences to Connections. What was always true, and is
    now what the copy claims, is that nothing is mirrored or uploaded to RepoYeti's own servers and
    nothing reaches an AI unless you use those features. The update-check ping description is
    unchanged.

  • Release notes and contributor instructions no longer describe a build that does not ship.
    The generated GitHub release notes told people to keep a web folder beside the executable,
    and a test locked that sentence in; the binaries have embedded the dashboard for many releases.
    The notes now describe the single self-contained binary, list the Windows tray bundle, and
    explain how compiled installs update (only from this repository's release assets, verified
    against the published checksums). The contributor guide and AGENTS.md described a
    --timeout 20000 flag that moved into the per-file useSuiteTimeout() helper, and a pre-commit
    hook that "runs i18n:check" when it runs lint, both typechecks and the kit guard too. The
    website's README claimed no JavaScript and no external fonts; the page loads Inter from Google
    Fonts and a small analytics pixel, and now says so.

  • A file save can no longer overwrite an edit that landed first. The viewer's Edit mode saved
    with no notion of which version it was editing, so two dashboards (or a phone and a desktop
    editor) silently overwrote each other. The conflict resolver did check a content hash, but it
    checked it BEFORE waiting for the per-repo operation queue; the 1.0 audit reproduced a desktop
    edit landing in that gap being replaced, with the call returning OK. Reads of a working-tree
    text file (GET /file, and the Diff tab's working side) now return a hash, the dashboard
    echoes it as expectedHash on save, and the writer re-checks it inside its queue slot at the
    point of no return, once more after writing the temp file, right before the atomic rename. A
    mismatch answers 409 FILE_STALE (the resolver reports CONFLICT_STALE) and the dashboard
    says the file changed on disk and asks for a reload instead of replacing your buffer. Saves
    without a hash remain unconditional for callers with no prior read. Residual: an editor outside
    the daemon can still race the microseconds between the final check and the rename; nothing on a
    plain filesystem closes that without a compare-and-swap primitive.

  • Committing a selection of files no longer disturbs the staging of everything else. Smart
    Commit and commit-selected began with a repository-wide git reset (mixed, "not --hard, so
    harmless") to make each commit contain exactly its group. It discarded every unrelated file's
    staging state, and where a file had been staged and then edited again, the staged content
    existed nowhere but the index and was gone for good; only the later working-tree edit survived
    (1.0 audit, item 2, reproduced). Each group is now built in a scratch index seeded from HEAD,
    the way git commit -- <paths> works internally, and only the committed paths' real-index
    entries are brought up to the new commit afterwards. Unrelated staged content is untouched on
    success and on failure. A partial commit is now explicitly refused while a merge, rebase,
    cherry-pick or revert is in progress (new code OPERATION_IN_PROGRESS), as git itself refuses it: the
    repository's unmerged entries used to make the commit fail by accident, and the scratch index
    has none.

Internal

  • Database ownership is split by domain, behind the same public surface. One module carried
    the connection, the migration system and six unrelated data domains, so every feature that
    touched storage edited the same 1,900 line hotspot. The connection, its migrations and the new
    recovery tools now live in their own module, alongside the shared row types, the three leaf
    repository reads, shares and collaboration, and the automation tables. Nothing outside changed:
    every one of the eighty files that import the database by name still does, and not one call site
    needed editing, because the original module stays as the facade and re-exports each domain. The
    repository writes, identities and the operational error log deliberately did NOT move. They are
    entangled by real transactions rather than by file position, and one of them deletes across five
    tables in a single transaction, so splitting them would trade a tidier file listing for a
    removal that can half apply.
  • Every schema change now leaves a record of whether it worked. A migration that failed for
    any reason other than "already applied" printed one line to the console of a background process
    and was otherwise invisible, so the daemon ran indefinitely with a column that does not exist
    and threw about it from whichever request happened to touch it first, a long way from the cause.
    It is still deliberately not fatal, and that posture is the point: a transient file lock at boot
    should not turn into an app that will not start. It is simply no longer the only thing that
    knows it is hurt. The daemon says so once, loudly, at boot, and the new database check lists
    exactly which changes did not apply.
  • The browser gate's menu sweep was measuring one menu while claiming to sweep them all. It
    located each opened menu through the trigger's aria-controls attribute and skipped, silently,
    any trigger that does not set one. Two of the dashboard's three header menus render their
    content through a portal and never set it, so the sweep was quietly checking a single control.
    Its own guard against exactly this caught it. It now falls back to asking the browser which
    menu, dialog or listbox is visible, which is what the test was always about, and it names the
    triggers it could not measure when it fails.
  • The Windows rebuild helper is misc/rebuild_repoyeti.bat. misc/Rebuild.bat was renamed
    with its callers (the root .gitignore entry for the convenience wrapper and the tray adapter's
    guidance) so no reference dangles, and it keeps the window open on a failed build, because the
    file is double-clicked and a console that closes on its own takes the only diagnostics with it.
    It is for source checkouts only: a compiled release embeds the dashboard and has nothing to
    rebuild, so it is deliberately not part of the tray bundle.

Install

Download the archive for your platform, extract it, and run the RepoYeti executable. It is a
single self-contained binary with the dashboard embedded; no install step, runtime, or sidecar
folder is required.

Platform Asset
Linux (x64) repoyeti-linux-x64.tar.gz
macOS (Apple silicon) repoyeti-macos-arm64.tar.gz
Windows (x64) repoyeti-windows-x64.zip
Windows (x64), with the system-tray toolkit repoyeti-windows-x64-with-tray.zip

Compiled installs check GitHub Releases for updates and can install one from the dashboard (or
unattended, if you turn auto-update on); an update is only ever downloaded from this repository's
release assets and is verified against the published SHA256SUMS.txt before it is installed.
Source checkouts update in-app with git.
Full history: CHANGELOG.md

Full Changelog: v0.21.5...v1.0.0