v1.0.0
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 separatesettings_changedhandlers, 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.tsnow holds one row per
field saying where it comes from inGET /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_changedanddaemon_statusare 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/:idand
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, sogit commitkeeps 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/verifyruns 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/backupwrites 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-cacheis 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 noEventSourceis 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:gateboots 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 oldertest:e2esuite 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) andPOST /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'scron/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, andDELETE /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/keysshows 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 loopbackOriginwhatever its port.
Per the Fetch spec a site ignores the port, so a preview server, a docs build or another daemon's
dashboard on127.0.0.1:<other>was same-site with RepoYeti: its browser stamped
Sec-Fetch-Site: same-site, a simpletext/plainPOST 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,localhostand::1on the bound port, read per
request so a port hop is followed). The Vite dev origin is admitted only underREPOYETI_DEV=1
(bun run dev) or an explicitREPOYETI_DEV_ORIGINS. Non-browser clients send noOriginand
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/tokenand "sign out everywhere" cleared the in-memory token and answeredok,
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 persistedapiTokenRevokedtombstone 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), andrepoyeti 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 ongithub.comor a*.githubusercontent.comhost; 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, unboundedparseBody()
nesting, and thetoSSG()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 thefast-urioverride from 3.1.5 to 3.1.6. brace-expansion deliberately
keeps its two majors (see the note inweb/package.json): flattening them broke the PWA
precache once before. Bothbun auditruns 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 toconfig.json, values
PUT /api/settingsrefuses. 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 atGET /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 ascan_cancelledevent 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/scanwhether 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/settingsapplied
each field as an independent block that serialised and atomically replacedconfig.jsonand
emitted its ownsettings_changedevent, 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 unrecognisedchangesStatDisplayis 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-statusand--numstatoutput 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 defaultcore.quotePath(every non-ASCII name) came back as its escaped spelling,
"h\303\251llo.txt"forhéllo.txt, which names no file the viewer could open. Stats were also
zipped onto files by row position. Both now ask git for-zrecords, decode them through one
shared decoder, keep both sides of a rename, and join stats to files by identity. The incoming
file list gainsfromon 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, whichgit pull --ff-onlyrefuses,
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 aspush, the local tag write never resolves a token, and
POST /api/repos/:id/tag/pushpushes 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 awebfolder 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 andAGENTS.mddescribed a
--timeout 20000flag that moved into the per-fileuseSuiteTimeout()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 ahash, the dashboard
echoes it asexpectedHashon 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 answers409 FILE_STALE(the resolver reportsCONFLICT_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-widegit 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 waygit 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 codeOPERATION_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'saria-controlsattribute 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.batwas renamed
with its callers (the root.gitignoreentry 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