原始 JSON 报告 机器可读
{
"data": {
"repo": {
"topics": [
"bubbletea",
"cli",
"daemon",
"golang",
"internet-radio",
"radio",
"somafm",
"tui"
],
"is_fork": false,
"size_kb": 5543,
"has_wiki": false,
"homepage": null,
"languages": {
"Go": 430818,
"Nix": 2312,
"Ruby": 903,
"Shell": 13729,
"Makefile": 6543
},
"pushed_at": "2026-07-21T03:43:25Z",
"created_at": "2026-02-03T23:36:36Z",
"owner_type": "User",
"updated_at": "2026-07-13T22:16:21Z",
"description": "Play and explore SomaFM radio from your terminal — daemon-backed playback that outlives the TUI, plus a headless CLI.",
"is_archived": false,
"is_disabled": false,
"license_spdx": "MIT",
"default_branch": "main",
"license_spdx_raw": "MIT",
"primary_language": "Go",
"significant_languages": [
"Go"
]
},
"owner": {
"blog": null,
"name": "Samuel Barabas",
"type": "User",
"login": "samuelb",
"company": null,
"location": "Switzerland",
"followers": 10,
"avatar_url": "https://avatars.githubusercontent.com/u/900568?v=4",
"created_at": "2011-07-07T13:24:52Z",
"is_verified": null,
"public_repos": 52,
"account_age_days": 5497
},
"license": {
"state": "standard",
"spdx_id": "MIT",
"raw_spdx": "MIT",
"file_present": true,
"scorecard_found": true,
"profile_has_license": true
},
"activity": {
"releases": [
{
"tag": "v0.14.1",
"kind": "patch",
"published_at": "2026-07-13T22:18:27Z"
},
{
"tag": "v0.14.0",
"kind": "minor",
"published_at": "2026-07-13T13:39:23Z"
},
{
"tag": "v0.13.0",
"kind": "minor",
"published_at": "2026-07-12T08:43:14Z"
},
{
"tag": "v0.12.0",
"kind": "minor",
"published_at": "2026-07-08T12:43:52Z"
},
{
"tag": "v0.11.0",
"kind": "minor",
"published_at": "2026-07-08T12:01:49Z"
},
{
"tag": "v0.10.0",
"kind": "minor",
"published_at": "2026-07-06T14:05:48Z"
},
{
"tag": "v0.9.0",
"kind": "minor",
"published_at": "2026-07-05T14:50:18Z"
},
{
"tag": "v0.8.0",
"kind": "minor",
"published_at": "2026-07-03T10:46:54Z"
},
{
"tag": "v0.7.0",
"kind": "minor",
"published_at": "2026-06-19T21:56:00Z"
},
{
"tag": "v0.6.1",
"kind": "patch",
"published_at": "2026-05-24T12:29:26Z"
},
{
"tag": "v0.6.0",
"kind": "minor",
"published_at": "2026-02-19T23:28:50Z"
},
{
"tag": "v0.5.0",
"kind": "minor",
"published_at": "2026-02-11T21:30:44Z"
},
{
"tag": "v0.4.0",
"kind": "minor",
"published_at": "2026-02-09T22:59:25Z"
},
{
"tag": "v0.3.0",
"kind": "minor",
"published_at": "2026-02-05T01:46:26Z"
},
{
"tag": "v0.2.0",
"kind": "minor",
"published_at": "2026-02-03T23:46:32Z"
}
],
"recent_commits": [
{
"oid": "ca4adeb472937af4011d0c6f62845ac33ba58bfc",
"body": null,
"is_bot": true,
"headline": "Release v0.14.1",
"author_name": "github-actions[bot]",
"author_login": "github-actions[bot]",
"committed_at": "2026-07-13T22:16:16Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "88312ec0ba970bbf5424280635cbf70662db122e",
"body": "handleAuth sent the success response before serveConn added the\nconnection to s.conns, so a client acting on that response could miss\nbroadcasts sent in the window before registration — the intermittent\nTestAuth_UnauthenticatedConnsReceiveNoBroadcasts timeout on the macOS\nrunners. Verification (verifyAuth) now answers only failures inline;\nthe success response is sent after addConn.",
"is_bot": false,
"headline": "fix(server): register connections for broadcasts before confirming auth",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-13T22:12:50Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "1b90bdf687cf5249793a7dfca383a1d2f3edb444",
"body": "Replace the Release workflow's manual version input with a bump choice\n(auto/patch/minor/major, default auto): git-cliff --bump computes the\nnext version from the conventional commits since the last tag\n(breaking -> major, feat -> minor, else patch). The prepare job now\nrenders the release notes once and shares them as an artifact, and a\nconcurrency group prevents two releases from racing the version\ncomputation. dry_run behaves as before.",
"is_bot": false,
"headline": "ci: derive the release version from conventional commits",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-13T22:06:04Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "626e40eda4bb8ac2e293f1f6b35f2d92be78947a",
"body": null,
"is_bot": true,
"headline": "Release v0.14.0",
"author_name": "github-actions[bot]",
"author_login": "github-actions[bot]",
"committed_at": "2026-07-13T13:37:57Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "bcc819e79a44ba33ea56ee5aee699f8443f93a34",
"body": "Restructure the release workflow into the shared shape both apps now\nuse: prepare (validate + test + version bump) -> per-OS builds ->\npublish -> Homebrew tap.\n\n- The prepare job bumps flake.nix via the new scripts/bump-version.sh\n and commits \"Release vX.Y.Z\" on real runs, so the Nix package versi\n[…]\npublish workflow\n instead of inlining the tap checkout/commit/push here.\n- Generate release notes with git-cliff (cliff.toml) and adopt\n Conventional Commits going forward (documented in CLAUDE.md).",
"is_bot": false,
"headline": "ci: unify the release pipeline with whirr",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-13T13:26:18Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "0c0e7ba2638ae20b9b997057409060b251d8157a",
"body": null,
"is_bot": false,
"headline": "Suspend audio device when playback stops",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-13T11:28:42Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "5c999371de0419669442a97237a8399c61977e7c",
"body": null,
"is_bot": false,
"headline": "Capitalize Somad in README prose",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-12T13:32:40Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "3a60bcd26fb670377ab007f7af50a4175ee21a01",
"body": "Rename the tap repo from homebrew-somad to homebrew-tap: the release workflow now checks out samuelb/homebrew-tap, and the README install instructions use `brew tap samuelb/tap`. The formula name (somad) is unchanged.",
"is_bot": false,
"headline": "Point Homebrew publishing at homebrew-tap",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-12T13:17:44Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "f6f2e6715e03cfa863af1738727e63c6a2fe251d",
"body": "The run* commands (play, list, stop, pause, next/prev, status,\nvolume, favorite, daemon stop) had no coverage at all; a scripted\nin-process daemon now exercises them over the real wire protocol,\nincluding channel resolution, JSON output modes, favorite toggling,\nand relative volume. cmd/soma statement coverage: 22.6% -> 44.6%;\nthe remainder is process-level plumbing (main, runServer, runTUI).",
"is_bot": false,
"headline": "Cover the CLI commands end-to-end against a fake daemon",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-12T07:53:26Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "591c434d6d2d0045598897093d5d6ef2d3dcfb21",
"body": "The ICY title delimiter is inherently ambiguous (the format has no\nescaping), and SelectMP3PlaylistURL's rank seed is load-bearing: a\n'simplification' to len(rank) would silently break channels whose\nplaylists all carry unknown quality labels.",
"is_bot": false,
"headline": "Document two subtle invariants",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-12T07:51:41Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "35e56fe429316fb8b7b43e4febfe8d501ffb4b25",
"body": "'daemon flags go after the subcommand' was wrong advice when the\nrejected flag was the TUI-only --shutdown-on-exit, which has no\nafter-the-subcommand form. Name what was actually passed.",
"is_bot": false,
"headline": "Name the offending flag when rejecting flags before daemon",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-12T07:51:12Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "c2d4fbc824b04f1d4d97b6517d1328b2976bf938",
"body": "A double Close returned the net package's 'use of closed connection'\nerror; every caller had to discard it. Guard with sync.Once and have\nthe read loop's disconnect teardown consume it too, so a Close after\na server-side disconnect also returns nil.",
"is_bot": false,
"headline": "Make Client.Close idempotent",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-12T07:50:45Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "12045deb925edd052e9cf4fd9878b0924639ff76",
"body": "A malformed decoder reporting sample rate 0 produced step 0: the\nresampler would repeat the first frame forever, an infinite stream\nthat never errors out to trigger reconnection. Fall back to\npass-through instead.",
"is_bot": false,
"headline": "Guard the resampler against a zero source rate",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-12T07:50:05Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "ec9e74d424fd0016e99b255295ab017fe1bde7a9",
"body": "The bar rendered one long line that the terminal renderer truncates,\nclipping exactly the stream/request errors it exists to show. Give it\nthe terminal width so lipgloss wraps; UpdateListSize already measures\nthe rendered height, so the list shrinks to make room.",
"is_bot": false,
"headline": "Wrap the status bar on narrow terminals",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-12T07:49:21Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "0dadcbdf056a67ed5e2e30d8fcf25c6a77315a0e",
"body": "An unreachable remote server made status --json exit 1 with a\nmessage on stderr, breaking status bars that poll it, while the local\npath degraded gracefully to a stopped snapshot. The JSON path now\nnever exits: a missing local server reports stopped with the\npersisted volume as before, and an unreachable remote one reports\nstopped with the connection error attached in streamError.",
"is_bot": false,
"headline": "Keep status --json parseable when the server is unreachable",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-12T07:48:19Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "3a143ad29dd1027f9e8ac3a75288e3475051e2c4",
"body": "The parser only matched a byte-exact 'File1=' at column zero, so a\nplaylist with different key casing, surrounding whitespace, or\nentries starting at File2 yielded 'no stream URL found'. Match any\nFileN key case-insensitively, tolerate whitespace, and cap the body\nread at 1 MiB — the playlist URL is the one attacker-influenceable\ninput (via redirect targets), so an unbounded read was a hazard.",
"is_bot": false,
"headline": "Parse .pls playlists leniently and cap the download",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-12T07:46:36Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "a6f0d6fdec0b8e89425e183ac6213a1e9a956631",
"body": "CanQuit was advertised as true while Quit was a no-op, leaving\ndesktop shells with a Quit action that silently did nothing. Route it\nthrough the command sender to Shutdown, exactly like the tray's Quit\nitem.",
"is_bot": false,
"headline": "Route MPRIS Quit to server shutdown",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-12T07:34:44Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "f0d3eb474db7e57f9dff7452fb4aa998d3631fbc",
"body": "With maxConcurrentRequests in flight, the read loop blocked\nunconditionally on the semaphore; during teardown nothing frees a\nslot promptly, so the goroutine lingered until a handler happened to\nfinish instead of exiting with the connection.",
"is_bot": false,
"headline": "Make the request-throttle send shutdown-aware",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-12T07:33:07Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "93ddc935a47c26c0229eb8c45692e1016f55697c",
"body": "The remove path deleted in place via append, silently shifting the\ncontents of any slice header handed out before the call. Correctness\ndepended entirely on every caller cloning under an external lock\nfirst. Rebuild on a clone instead, matching the TUI's favorites code,\nso handed-out snapshots can never mutate under their holders.",
"is_bot": false,
"headline": "Make state.ToggleFavorite copy-on-write",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-12T07:32:46Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "073aa73139fd5fb78acf5136ddad4455075cc483",
"body": "NewPlayer blocked forever on the oto ready channel, so a hung audio\nbackend hung the whole daemon before the socket was even useful (the\nspawning client would then quote an empty server log). Fail with a\nclear error after 15s instead. Also document that the oto context is\nprocess-global, so at most one AudioPlayer per process.",
"is_bot": false,
"headline": "Bound the wait for audio-device readiness at startup",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-12T07:31:31Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "de4cb38db69f0b4284bc1479881d654da1e2383d",
"body": "The client skipped PSK authentication on Unix-socket endpoints, so an\nendpoint configured to authenticate could silently connect without\ndoing so, working only because the server happens to exempt the Unix\nsocket. The server is the single source of truth on whether auth is\nrequired and accepts a voluntary handshake everywhere, so a PSK now\nalways authenticates. Adds the first client-side coverage of the HMAC\nchallenge-response round trip.",
"is_bot": false,
"headline": "Always authenticate when the endpoint has a PSK",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-12T07:31:02Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "a4d60169df70a3d3fcc660132d26e0d89704b6e5",
"body": "saveState advances savedSeq before writing (deliberately, so a slow\nolder save can never clobber newer state), which meant a failed write\nwas treated as durable: unless another mutation came along, the state\nwas silently lost. Keep the newest failed state as dirty and flush it\non shutdown; any newer successful save supersedes it.",
"is_bot": false,
"headline": "Retry a failed state save at shutdown",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-12T07:29:50Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "acdf2e50ba2130ac0c914d431f8b36f74e93b60c",
"body": "The catalog fetch decoded an allowlisted-but-remote body with no size\nlimit; a misbehaving or compromised upstream could stream an\narbitrarily large body into memory and from there into the cache\nfile. The real catalog is a few hundred KB.",
"is_bot": false,
"headline": "Cap the channel-catalog download at 4 MiB",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-12T07:28:40Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "de8dd79969006070f9ac8d4d4250b32cd5a411ed",
"body": "Header-phase failures (bad URL, failed connect, bad status, stall\nbefore first byte) were reported both through the pipe — surfacing\nsynchronously from Play — and on the async errors channel. The server\nignores the async duplicate while connecting, but a stale queued copy\ncould tear down a later, he\n[…]\nrrors channel.\n\nAlso cancel the fetch context before closing the pipe in\nfadeOutAndClose, so a clean stop no longer surfaces as a spurious\n'stream read error' that could trigger an unwanted reconnect.",
"is_bot": false,
"headline": "Give each stream failure exactly one reporting path",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-12T07:27:37Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "5e1cf2f04a49bab9a1854c8475a7451fbf11b021",
"body": "Three silent-failure paths made keypresses appear to do nothing:\nrequest errors with a live connection were discarded (a failed first\ncatalog fetch left 'Loading...' up forever), ToggleFavorite ignored\nthe server's result and error so favorites could diverge, and a\nfailed shutdown during a version-s\n[…]\nrface\nsolely via ServerLostMsg. A failed first catalog load shows the error\nscreen, favorite toggles apply the server's authoritative list, and a\nfailed restart clears the pending play and reports it.",
"is_bot": false,
"headline": "Surface TUI request failures instead of dropping them",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-12T07:25:41Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "59fccbd57788575094ed0579e0cd8033653fba3e",
"body": "A version-skew restart gave the old daemon 3s to exit, but shutdown\nincludes an async fade-out plus a 400ms sleep, so on a loaded machine\nthe user's stop/pause/play command hard-failed with the old server\npossibly still running. The user's command outranks the upgrade: on\ntimeout, reconnect to whatever answers (the handshake already\nguaranteed a matching protocol version) and let a later interrupting\ncommand retry the upgrade. Also raise the wait to 5s.",
"is_bot": false,
"headline": "Fall back to the running server when a skew restart times out",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-12T07:22:14Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "890fd5f797dc82af4c419d275858056360b7200d",
"body": "Previously --listen without a PSK only logged a warning, leaving full\nplayback control (including remote shutdown) open to anyone who can\nreach the port, and a PSK over plaintext TCP authenticated only the\nhandshake — an on-path attacker could hijack the established session.\n\nThe daemon now refuses \n[…]\nLS and a PSK are configured. --insecure (or server.insecure in\nthe config file) restores the old behavior explicitly; loopback binds\nstay warn-only since they are no more exposed than the Unix socket.",
"is_bot": false,
"headline": "Require TLS and a PSK for non-loopback TCP listeners",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-12T07:21:05Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "f4aa57036506f27b6c1d6eeb16bff5d71744efea",
"body": "D-Bus method handlers read MPRIS.sender from godbus goroutines while\nSetSender is called after the bus objects are already exported. Guard\nthe field with a mutex and route all handler sends through one\naccessor, mirroring the tray. Add routing and concurrency tests so\nthe Linux CI race detector covers this path.",
"is_bot": false,
"headline": "Fix data race on the MPRIS command sender",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-12T07:18:49Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "0fd2f1d1f8b39710e4ddef509623d9fa758fa23c",
"body": "Every client RPC selected on time.After, leaving a live 30s timer\nbehind on the (common) success path; a busy TUI accumulates them.\nUse a stopped NewTimer instead, and cover the timeout path with a\ntest now that callTimeout is shrinkable.",
"is_bot": false,
"headline": "Stop the per-call timeout timer once a response arrives",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-12T07:17:34Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "0c2224a769a89ae45d3c65f8f45f1a1b891f85d5",
"body": "soma completion <bash|zsh> prints a completion script, embedded from\nplain files in cmd/soma/completions/ so packages can also install them\ndirectly (deb, AUR, and Nix wired up accordingly). The scripts cover\nall subcommands, global connection flags, daemon flags, and --json,\nand complete channel ar\n[…]\nffect-free channels.PeekChannelsFromCache —\na Tab press never spawns the daemon, touches the network, or moves a\ncorrupt cache aside. A drift-guard test keeps the scripts in sync with\nthe CLI surface.",
"is_bot": false,
"headline": "Add bash and zsh shell completions",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-11T12:27:15Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "a396db0ac7d334c29925053d940a70d07eb631ed",
"body": "The daemon can now serve frontends on other machines: soma daemon\n--listen host:port adds a TCP listener next to the always-on Unix\nsocket, and every client command accepts --server host:port (or\n$SOMAD_SERVER / client.server in the config) to connect to it.\n\nOptional protections, both off by defaul\n[…]\nol version.\n\nAll CLI parsing now goes through flag.FlagSet (global connection\nflags before the command, each command parsing its own flags), and\nhelp output consistently shows options with two dashes.",
"is_bot": false,
"headline": "Add optional TCP transport with TLS and pre-shared-key auth",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-11T11:29:41Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "eb5799e156c6eba2eea06825e9dac1e04f4b9b35",
"body": "…aemon",
"is_bot": false,
"headline": "Document starting the daemon with soma daemon; call it the playback d…",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-08T12:35:44Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "935b5dd681cd30c721fda7510475f172ae29443a",
"body": null,
"is_bot": false,
"headline": "Refer to the project as somad in the README",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-08T12:31:50Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "c82d8fc4435bbd49e286c389e7603a31b1663f55",
"body": "The project has grown beyond a pure terminal client (background playback\ndaemon, MPRIS/tray integration, headless CLI), so \"somatui\" no longer fits.\nRename the Go module and package/distribution name to somad, and the compiled\nbinary/command to soma.\n\n- Module path somatui -> somad; command dir cmd/\n[…]\nile, flake.nix, AUR, deb, and release workflow (assets\n soma_*, formula somad.rb, tap homebrew-somad, AUR somad). deb/homebrew keep\n a somatui upgrade path; the never-published AUR package does not.",
"is_bot": false,
"headline": "Rename project to somad with a soma binary",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-08T12:23:42Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "7ba2316354c3845d1cc41cfba335f8ca25fa7b97",
"body": "govulncheck in CI installed exactly the go.mod floor (1.25.0), whose\nstdlib carries CVEs fixed in later patches, producing findings that\ndidn't reproduce locally on a newer toolchain. Bump the go directive to\n1.25.12 and set check-latest so CI and release builds use a patched\ntoolchain.",
"is_bot": false,
"headline": "Scan and build with a patched Go toolchain",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-08T11:47:34Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "8fbbe1d1cbade20419b789c8d03129c0a01fd4b5",
"body": "The macOS test job ran `go test -v ./...` without -race while the Linux\njob used -race, so a data race that only surfaces on the darwin scheduler\ncould pass CI. Add -race to match.\n\nAdd a vuln job running govulncheck ./... (with the ALSA headers needed to\ncompile the Linux audio cgo path) so vendored-dependency CVEs surface in\nCI, not just in local runs.",
"is_bot": false,
"headline": "Run macOS tests under -race and add a govulncheck CI job",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-08T11:18:24Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "f062394c4576a0c6e107a4d9c7dcd2a918441347",
"body": "Errors() returns a buffered channel that reportError writes with a\nnon-blocking send, so a burst of failures can be dropped or coalesced. Add\na doc comment on Errors() spelling out that a reader can't assume it sees\nevery error — it's \"stream currently unhealthy\" signalling, not a lossless\nlog — and a one-line note at the drop site.",
"is_bot": false,
"headline": "Document the lossy behavior of the audio error channel",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-08T11:14:57Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "848502fcba6a7f96f3fb7da29193802c50844e94",
"body": "The cmd/somatui package had the largest line count but the thinnest tests\n(7.9% coverage): the pure argument/formatting logic behind the command\nhandlers was untested.\n\nAdd table-driven tests for resolveChannel (exact ID beats substring,\nunique ID/title substring, case-insensitive, no match, ambiguo\n[…]\ned across four call sites into a\nvolumePercent helper and test its rounding boundaries. Coverage 7.9% ->\n13.1%; the remainder is server-talking I/O glue not unit-testable without\ndependency injection.",
"is_bot": false,
"headline": "Add CLI unit tests and extract volumePercent helper",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-08T11:14:41Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "748e409bf15ea28116d5b166a60487297b095618",
"body": "saveState was called after releasing s.mu at three sites (ToggleFavorite,\nplayChannel, SetVolume). Two concurrent mutations could clone under the\nlock in order A→B but reach the disk write in order B→A, leaving stale\nstate on disk.\n\nStamp each mutation with a monotonic saveSeq captured under s.mu at\n[…]\ne written is dropped, so a slow save can never clobber the\nstate produced by a newer mutation. Saves stay synchronous, so callers and\npersistence-reading tests see the write complete before returning.",
"is_bot": false,
"headline": "Serialize state persistence so stale writes can't win",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-08T11:06:11Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "3596858e8671556e1017706c2688a4e7ec89f187",
"body": "TestToggleFavorite_ConcurrentReadIsRaceFree ran ~4000 fsync-heavy atomic\ndisk writes (one per ToggleFavorite), taking ~29s and dominating the whole\ntest suite. The race it guards (returning slice copies vs. mutating the\nbacking slice) is exercised in the first handful of iterations; the disk\nwrites \n[…]\nw(),\nand turn the package-level saveState into a method that calls it. The\nconcurrent test now injects a no-op persister, dropping its runtime from\n29.3s to 0.13s and the server suite from 33s to ~4s.",
"is_bot": false,
"headline": "Make server state persister injectable to speed up race test",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-08T11:00:08Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "be4000e9a01fd59705337a5bc625fbe29ef78121",
"body": "The server's PlayPause already tears down the live stream when playing,\nso separate Pause and Stop menu items were redundant. Replace them with a\nsingle toggle that reads \"Play\" when stopped and \"Stop\" when playing.",
"is_bot": false,
"headline": "Collapse tray play/pause/stop into one Play/Stop item",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-07T13:12:25Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "91ec5ba86fd5a7a22463d2c6d26391bde60fd619",
"body": "- Close a quit race in the reconnect bridge: quitting while a\n replacement server was being spawned could orphan it despite\n --shutdown-on-exit; the bridge now shuts it down, and the TUI waits\n for the bridge before exiting (only in shutdown-on-exit mode).\n- Make NewHelpKeys take a plain bool instead of a variadic one.\n- Drop the redundant --shutdown-on-exit dispatch case and replace the\n goto with a labeled break.\n- Document the flag and the tui.shutdown_on_exit config key in the\n README.",
"is_bot": false,
"headline": "Polish the shutdown-on-exit option",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-07T12:52:01Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "9c5e5d003798deb093539c3dbee8efed9155361b",
"body": null,
"is_bot": false,
"headline": "Add TUI shutdown-on-exit option",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-07T12:39:38Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "f263967e153c613efb909253134474765512c19b",
"body": "A \"★ Favorite\" checkbox below the playback controls marks or unmarks\nthe currently playing channel; it is disabled while stopped. The click\nis routed as a new ToggleFavoriteMsg through the shared MPRIS/tray\ncommand router into the existing Server.ToggleFavorite, and the\ncheckbox mirrors the server's catalog pushes rather than flipping\nlocally, so toggles from the TUI stay in sync.",
"is_bot": false,
"headline": "Add a favorite toggle for the playing channel to the tray menu",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-07T12:37:11Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "40d4b13105fd182ec700586a94d3610cf398ec15",
"body": "CLAUDE.md documents the build/test commands and the client-server\narchitecture for future Claude Code sessions. The .claude/agents/\ndefinitions add four project sub-agents: code-reviewer, test-runner,\nand computer-use delegate their work to the Codex CLI (gpt-5.5) and\nverify its results, while docs-updater keeps README, CLI help, and\nconfig docs in sync.",
"is_bot": false,
"headline": "Add CLAUDE.md and Codex-backed Claude Code sub-agents",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-07T10:50:15Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "3cd4a18b36be261e3a49b7976246d1b8dd0bc6f9",
"body": "The 2-minute idle exit surprised users who expected the server to stay\naround like any other daemon: reopening the TUI after a break paid the\nspawn cost again. Default the idle timeout to 0 (never exit on idle);\nthe previous behavior remains available via --idle-timeout or the\nserver.idle_timeout config setting. The generated config template now\nspells out what the different idle_timeout values mean.",
"is_bot": false,
"headline": "Keep the server running by default instead of idle-exiting",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-07T10:39:29Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "4d177db6d1b3d2ca3dcca463ca6b0e220b467fd5",
"body": "The server's flags (--idle-timeout, --no-tray) only apply when the\nserver is launched by hand; the TUI and CLI auto-spawn it without any\nflags. A config file at $XDG_CONFIG_HOME/somatui/config.yaml (Linux) or\n~/Library/Application Support/somatui/config.yaml (macOS) now supplies\nthe flag defaults, s\n[…]\nngs\nare discoverable. A config file that fails to parse, contains unknown\nkeys, or holds an invalid value stops the server with an error naming\nthe line, rather than silently falling back to defaults.",
"is_bot": false,
"headline": "Add a configuration file for the server settings",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-07T10:29:58Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "d1d33dfec66b1a23da4d6ffa1a5ea86450788646",
"body": "Show a menu-bar / system-tray item while the server runs (macOS and\nLinux) with the current track, a channel picker, and play/pause, next,\nprevious, and stop controls, plus a Quit item. The tray lives in the\nserver process and reuses the MPRIS command router, so its clicks map\nonto the existing play\n[…]\nsession check (CGSession on macOS, display/bus env on\nLinux) skip the tray when disabled or headless, so the server still runs\nanywhere. macOS release builds now compile with cgo per arch before lipo.",
"is_bot": false,
"headline": "Add a system tray icon with playback controls",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-07T10:02:14Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "de2e0674af6397e14716faa68abf06a915c72e4b",
"body": "Replace the separate darwin/amd64 and darwin/arm64 release artifacts\nwith a single universal binary. A dedicated build-darwin job cross-\ncompiles both arches (CGO disabled) and merges them with lipo, and the\nHomebrew formula's on_macos block collapses to a single url/sha256.",
"is_bot": false,
"headline": "Build a universal macOS binary for releases",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-06T20:30:04Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "ac1dccedf9776b6e323b9dda938d8e3fc517e499",
"body": "Build and publish linux/arm64 binaries and Debian packages alongside\nthe existing targets, and extend the Homebrew formula and AUR package\nto cover ARM64 Linux.\n\nAdd a dry_run input (default true) to the Release workflow that builds\nand renders all artifacts — binaries, deb packages, checksums, Homebrew\nformula, PKGBUILD/.SRCINFO — and uploads them for inspection without\ncreating a tag, GitHub Release, tap commit, or AUR push.",
"is_bot": false,
"headline": "Add linux/arm64 packages and a release dry-run mode",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-06T12:55:09Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "7333b4fab16e753725a6798523e5f0514cdf2a66",
"body": null,
"is_bot": false,
"headline": "Add Linux package release support",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-06T12:54:39Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "97d6e2024c4cc089467a34fa3ce0ee6f9640fd2d",
"body": null,
"is_bot": false,
"headline": "Add macOS CI test coverage",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-05T17:46:01Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "7a627eca33b20217ec85e91c503fbccd11f29037",
"body": null,
"is_bot": false,
"headline": "Remove unused GoReleaser config",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-05T17:45:40Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "974c713d74ecfcf72af2ddf9fda91e81b3bff1bc",
"body": null,
"is_bot": false,
"headline": "Validate favorite channel IDs",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-05T17:45:17Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "1a5c1917099f93ea04076c1e0fd86fbb2a6dbf89",
"body": null,
"is_bot": false,
"headline": "Persist server state outside lock",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-05T17:44:17Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "afdd677cbe7a0c7ad93a5dbb807c901a8a42818a",
"body": null,
"is_bot": false,
"headline": "Sync parent directory after atomic writes",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-05T17:43:08Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "477035d91933ff0740872d451e09f0c10914b0f0",
"body": null,
"is_bot": false,
"headline": "Harden socket directory validation",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-05T17:42:43Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "9936858768ab7f62ba2863c291ef97e336bcee6a",
"body": "The previous change restarted an out-of-date server onto the new binary\non any command, cutting off music just to upgrade the daemon. Now the\nrestart happens only when the action interrupts the stream regardless —\nchanging channel, pausing, or stopping — or when the server is idle\n(nothing to interr\n[…]\ndoes not cut it off; selecting a channel or stopping\nagainst a skewed server shuts it down and lets the reconnect loop respawn\nthe new version, then replays the queued channel. Backend gains Shutdown.",
"is_bot": false,
"headline": "Only restart a version-skewed server when playback is interrupted anyway",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-05T17:19:20Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "e5620c4206d1a75a92095d5da72c35eb6bae71da",
"body": "When a client's binary version differs from the running server's, the\nclient now restarts the server onto its own version even while playback\nis in progress, instead of leaving the stale server running and asking\nthe user to restart it manually once stopped.\n\nThe outgoing server's playing channel is\n[…]\nw-unreachable \"restart manually\" UX is removed: the CLI mismatch\nwarning and the TUI VersionSkew banner (and its field/test) go away,\nsince EnsureServer now always yields a matching server on success.",
"is_bot": false,
"headline": "Auto-restart a version-skewed server, resuming playback",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-05T16:59:00Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "79f4ba175b3e896c40c799ceae9cbde12364dc44",
"body": "`status` already had --json for scripting; list and favorite were\ntext-only, forcing scripts to scrape human-readable output. Both now\naccept --json and print machine-readable JSON, matching status's\nconvention.",
"is_bot": false,
"headline": "Add --json output to `somatui list` and `somatui favorite`",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-05T16:00:44Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "658f3b69d3722763c52ab84bb7659014fce002f3",
"body": "Coverage was displayed but never enforced, so it could regress silently.\nFail the build if total coverage drops below 60%, comfortably below the\ncurrent ~65%.",
"is_bot": false,
"headline": "Gate CI on a minimum test coverage threshold",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-05T15:58:40Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "a5d6a61b5a7d862f7217ffdfa0973458299103de",
"body": "server.loadCatalog, refreshCatalog, refreshLoop, and state.GetLogFilePath\nhad no direct test coverage. Make channelRefreshInterval a variable (like\nreconnectBaseDelay) so refreshLoop's ticker can be exercised without a\n10-minute wait.",
"is_bot": false,
"headline": "Add tests for previously-uncovered catalog and log-path functions",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-05T15:56:02Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "bd150a41cebbfb2d61b9e6c4acf0edf5f17250f2",
"body": "Server.ChannelsPayload and Server.ToggleFavorite returned the server's own\nFavoriteChannelIDs slice, which the conn handler marshals to JSON after\nreleasing the server lock. state.ToggleFavorite removes an entry with an\nin-place append(s[:i], s[i+1:]...), rewriting the backing array under the\nlock. \n[…]\ntine) race: one marshals the slice while another shifts it.\n\nClone the slice at both hand-out points. The catalog beside it is only ever\nreplaced wholesale, never mutated in place, so it stays shared.",
"is_bot": false,
"headline": "Copy the favorites slice before handing it to clients",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-05T15:51:26Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "d5a526c0afc6859cfb6a386e45b6bf68d6a3a4a5",
"body": "errcheck exempts fmt.Fprint to os.Stderr but not to the io.Writer\nprintUsage now takes; there is nothing to do about a failed write to\nstdout/stderr here anyway.",
"is_bot": false,
"headline": "Ignore the write error when printing usage",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-05T15:31:49Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "cc3a8f2bff30f715ed00172ad21ecde251798e7b",
"body": "Emits the playback state as a single JSON line for status bars and\nscripts, instead of making them parse the human-readable output. With\nno server running it reports stopped with the persisted volume, without\nspawning one.",
"is_bot": false,
"headline": "Add --json output to `somatui status`",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-05T15:30:59Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "3f1e82568de8b073b1954763d0f66c603bdee85f",
"body": "`somatui volume` without an argument prints the current volume (from\nthe running server, or from the persisted state without spawning one),\nand explicitly signed arguments like +5/-10 adjust it relative to the\ncurrent value, for volume-key bindings.",
"is_bot": false,
"headline": "Show and adjust the volume from the CLI",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-05T15:30:59Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "e8f929a548994537305fd492414321998ce43ff8",
"body": "Toggles a channel's favorite flag by ID or name match, like `play`\nresolves channels, so favorites can be managed from scripts and\nkeybindings without opening the TUI. `fav` is accepted as an alias.",
"is_bot": false,
"headline": "Add a `somatui favorite` CLI command",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-05T15:30:59Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "b7f0f3d8989b3b43df5cfe84c5d877312aaa769b",
"body": "The workflows hardcoded go-version 1.24 in four places while go.mod\nsays 1.24.5; go-version-file keeps them from drifting apart.",
"is_bot": false,
"headline": "Pin the CI Go version to go.mod",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-05T15:30:59Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "a8f541bc7e4fd246763770b5c681db3bbd1a9adf",
"body": "Nothing calls it; the CLI distinguishes a missing server by whether\nDial succeeds.",
"is_bot": false,
"headline": "Remove unused client.IsNotRunning",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-05T15:30:31Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "7a0304985af8c9d9f4dbcd929f511989c54cd5a9",
"body": "`make install` copied the binary to `$(GOPATH)/bin`, but the GOPATH\nenvironment variable is usually unset with modern Go, which expanded to\n`cp somatui /bin/`. Install to GOBIN, defaulting to the toolchain's\nreported GOPATH, and create the directory if needed.",
"is_bot": false,
"headline": "Default the install directory from go env GOPATH",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-05T15:30:31Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "44d2c799a2f94eccaf631a708639af6634394883",
"body": "`somatui --help`, `-h`, and `help` used to print \"unknown command\" and\nexit with status 2; they now print the usage to stdout and exit 0.",
"is_bot": false,
"headline": "Handle help as a command instead of failing as unknown",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-05T15:30:31Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "6a85d95609be763156d7cdb381d4f782dce76a43",
"body": "Run go get -u ./... && go mod tidy. golang.org/x/sys (v0.41.0 => v0.46.0),\ngolang.org/x/text (v0.33.0 => v0.38.0), and charmbracelet/colorprofile\n(v0.4.1 => v0.4.3) now require Go 1.25, so bump the go directive and the\nCI/release workflows and README to match. Other bumps: go-isatty,\ngo-runewidth, sahilm/fuzzy, ebitengine/purego. Build, go vet, race tests,\nand golangci-lint all pass.",
"is_bot": false,
"headline": "Update dependencies and raise minimum Go to 1.25",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-05T15:27:53Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "bc1ed2e1206c0c94e1ff182990c4d1ab8936748c",
"body": "The security package sits on the trust boundary but ValidateURL lacked\ntests for a few boundary conditions. Add cases for a host that ends in\n\"somafm.com\" without a dot boundary (evilsomafm.com), an empty host, and\nthe extra-allowed-host path (AddAllowedHost/ClearAllowedHosts). Raises\npackage coverage from ~45% to ~71%; no production code changes.",
"is_bot": false,
"headline": "Cover more ValidateURL edge cases",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-05T15:24:06Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "04b1ad4f576b350e5438a04417ac07b8e12cec2a",
"body": "security.HTTPClient used net/http's default redirect policy, which\nfollows up to 10 hops to any host. ValidateURL only guards the initial\nURL, so a redirect (feasible over the allowed http scheme) could send a\nrequest to an internal or otherwise disallowed host — an SSRF\ndefense-in-depth gap.\n\nAdd a\n[…]\nrect that runs the existing ValidateURL on every hop and\npreserves net/http's 10-redirect limit. Reuses the existing validation,\nso the allowlist (case-insensitive, suffix-anchored) applies uniformly.",
"is_bot": false,
"headline": "Re-validate redirect targets against the host allowlist",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-05T15:23:34Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "523f241f25dd2da99afcbdd16c0bd9bff1b10961",
"body": "parseICYMetadata split the whole metadata string on \";\" and took the\nStreamTitle field, which truncated any title containing a semicolon (e.g.\n\"Artist - A; B\" became \"Artist - A\"). Parse the value from the\nStreamTitle=' opener up to the closing \"';\" delimiter instead, so\nembedded semicolons survive while the real field boundary is still\nrespected.",
"is_bot": false,
"headline": "Preserve semicolons inside ICY stream titles",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-05T15:20:48Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "f47d0fe7e1a98734722965ba345f4e6e67e93d6a",
"body": "ReadChannelsFromCache dropped an unparseable cache file with no trace,\nunlike state.LoadState's corrupt-file handling. It now moves the file to\na .corrupt sibling and logs a warning before falling back to a network\nfetch, giving the same diagnostic trail as state.json.",
"is_bot": false,
"headline": "Move a corrupt channel cache aside instead of discarding it silently",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-05T15:15:25Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "48dff39d21a472bbaf8d1a4b404576f8800f019d",
"body": "Every request line spawned its own goroutine with no limit, so a client\nsending requests faster than they can be handled could spawn unbounded\ngoroutines. A semaphore now caps concurrent dispatch per connection,\napplying backpressure on the read loop instead.",
"is_bot": false,
"headline": "Cap concurrent in-flight requests per connection",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-05T15:14:02Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "2efbb4fcd8d84962268bc6b0e1c2c4448b04c00d",
"body": "EnsureServer's version-mismatch restart shut down the old server, waited\nfor it to exit, then unconditionally reconnected. If the old server didn't\nexit within restartWait, the reconnect just found it still listening and\nsilently returned it as if the restart had succeeded, hiding the skew.",
"is_bot": false,
"headline": "Surface an error when a version-skewed server won't exit",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-05T15:12:59Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "54dd6fe3c260c09a124561c374c163e52dfb03b0",
"body": "Hostnames are case-insensitive, but the allowlist check compared them\nverbatim, so a mixed-case host that DNS treats as identical to a\nsubdomain of somafm.com would be wrongly rejected.",
"is_bot": false,
"headline": "Make the SomaFM host allowlist check case-insensitive",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-05T15:11:40Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "1095e4b9228cb8e6073507686bb2665d23714019",
"body": "Struct field alignment and comment spacing had drifted out of gofmt\ncompliance in a few files.",
"is_bot": false,
"headline": "Fix gofmt formatting drift",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-05T15:09:22Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "6ab78d88bda42c701ba7203327cb06993b1a8ca4",
"body": "Publish a pre-built binary via a personal tap (samuelb/homebrew-somatui).\nThe release workflow now regenerates the formula from the release checksums\nand pushes it to the tap on every release. Document the install steps in the\nREADME.",
"is_bot": false,
"headline": "Add Homebrew tap installation",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-05T14:45:52Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "224efb258b07f4346c2661a269ba1a5d5188006b",
"body": "Channels could only be browsed in the TUI, leaving scripts and shell\ncompletions no way to discover channel IDs. `somatui list` prints the\ncatalog one channel per line — favorite marker, ID, title, genre in\naligned columns — in server order (favorites first), spawning the\nserver and waiting for the catalog like `play` does.",
"is_bot": false,
"headline": "Add a `somatui list` CLI command",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-05T12:29:53Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "eead2e10081012600d486f92e63468d88fa6551f",
"body": "After 5 failed reconnect attempts (~1 minute of backoff) playback\nstopped for good, so any longer outage — an overnight suspend, a\nrouter reboot — meant the music never came back even though the\nserver is a long-running daemon.\n\nDrop the attempt budget: the backoff still doubles from 2s but now\ncaps at 1 minute and retries forever. Only an explicit stop or a new\nplay ends reconnecting. The MaxReconnects protocol field is gone; the\nTUI and CLI show the running attempt count instead.",
"is_bot": false,
"headline": "Reconnect indefinitely with capped backoff instead of giving up",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-05T12:16:23Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "51cb0007df3dbb1dcc753e0372a6cb2839f3c034",
"body": "When audio init or state loading failed in a freshly spawned server,\nthe client polled the socket for 15 seconds and then reported only\n\"server did not come up\" — the actual cause was buried in server.log.\n\nRecord the log's size before spawning and, on timeout, append what the\nnew server wrote (capped to the last 10 lines) to the error message,\nso startup failures are self-diagnosing.",
"is_bot": false,
"headline": "Quote the server log when a spawned server fails to start",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-05T12:13:44Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "75c43c699d62f9e9878510e2206a86c979a6b9a2",
"body": "Every spawn appended to server.log and nothing ever truncated or\nrotated it, so it grew without bound. A spawn that finds the log above\nthe cap now truncates it before handing it to the new server.",
"is_bot": false,
"headline": "Cap server.log growth at 1 MiB",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-05T12:12:09Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "df4f0f0f9e4120e1755fc51c40046a9095f447e7",
"body": "state.json and the channels cache were written in place, so a crash or\npower loss mid-write could leave truncated JSON behind — and a corrupt\nstate file then made the server refuse to start until the user deleted\nit by hand.\n\nRoute both saves through a new atomicfile helper (temp file + fsync +\nrename), and have LoadState treat an unparseable state file as a fresh\nstart, moving the bad file to state.json.corrupt for inspection instead\nof failing hard.",
"is_bot": false,
"headline": "Write state and cache atomically, survive a corrupt state file",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-05T12:11:25Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "d39497b3f6348b446dda698cb734e819ad088757",
"body": "A connection that died without a FIN (lost link, suspend/resume, NAT\ntimeout) left the stream read blocked forever: audio went silent while\nthe status still said playing, and reconnection never started. The same\nhappened when the server hung up cleanly, since io.Copy treats EOF as a\nnormal end and n\n[…]\n the stream with a watchdog that aborts the request when no data\narrives for 30 seconds, re-armed by every read, and report a clean EOF\nas an error too. Both now feed the existing reconnect machinery.",
"is_bot": false,
"headline": "Detect silently stalled streams and hung-up servers",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-05T12:09:57Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "95fb7237a6949deea2cb488feaea15560c8021fe",
"body": "Split the app into a background playback server and a thin TUI client\ntalking newline-delimited JSON over a Unix socket:\n\n- internal/server owns audio, stream resolution, reconnect logic, track\n titles, volume, the channel catalog, state.json, and MPRIS, so media\n keys keep working with the TUI cl\n[…]\n it transparently.\n- New headless commands: play [channel] (resumes last played when\n omitted), next/prev, pause (live radio: unpause rejoins the stream),\n stop, status, volume, server, server stop.",
"is_bot": false,
"headline": "Rewrite as client-server: playback survives closing the TUI",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-05T11:12:30Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "0fc73887dc521d006ce05f72977cf8bba78a756b",
"body": "…/checkout-7\n\nchore(deps): bump actions/checkout from 6 to 7",
"is_bot": false,
"headline": "Merge pull request #13 from samuelb/dependabot/github_actions/actions…",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-03T10:43:35Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "ef2ed4a00c4db61e131ce3fbed54b17d5db48ff3",
"body": null,
"is_bot": false,
"headline": "Document volume keys and updated dependencies in README",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-03T10:04:34Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "a0b1dc8b4e3a137f6a7fb652c68c72f1d79e912d",
"body": "The oto player always supported volume (it drove the fades) but there\nwas no way to set it. Now:\n\n- +/= and -/_ adjust volume in 5% steps, clamped to 0–100%, with the\n level shown in the status bar\n- volume changes route through the session goroutine (a volumeCh on\n the session), preserving the in\n[…]\nend MPRISVolumeMsg into the app, keyboard changes update the\n property\n- the level persists in state.json (pointer field so an explicit mute\n is distinct from \"never set\") and is restored at startup",
"is_bot": false,
"headline": "Add volume control with +/- keys, MPRIS integration, and persistence",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-03T10:03:49Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "4f3332dd9b29f5fcdce93fc47d4d419fbe1963de",
"body": "A mid-stream network error used to just stop playback with an error in\nthe status bar. Now the app retries the dropped channel with\nexponential backoff (2s doubling to 32s, five attempts), showing\n\"↻ Reconnecting n/5\" while it waits. A successful connect resets the\nbudget; stopping, switching channels, or a non-retryable error (no\nplayable playlist) cancels the pending retry.",
"is_bot": false,
"headline": "Reconnect automatically after a stream drop",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-03T10:00:16Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "1ee802537d302c02e808f083f2c835c18e346f3c",
"body": "The metadata reader opened a second HTTP connection to the stream\nevery 10 seconds, downloading and discarding a full icy-metaint audio\nblock each time just to read one title — roughly doubling the traffic\nto SomaFM and lagging track changes by up to 10s.\n\nInstead, request Icy-MetaData on the playba\n[…]\nlicated, gated\nagainst superseded sessions, and drained on stop/switch so titles\nnever leak across channels. One connection now serves both audio and\nnow-playing info; the MetadataReader type is gone.",
"is_bot": false,
"headline": "Demux now-playing titles from the playback stream itself",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-03T09:57:36Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "f9445e6ba7c1d4053d825733e1acc044d6f21d60",
"body": "playChannel ran the playlist fetch (up to 15s) and the stream\nconnect/decode inline in Update, freezing the whole TUI when starting\na channel. The network work now runs in a tea.Cmd and reports back via\nPlaybackStartedMsg/StreamErrorMsg, while the status bar shows\n\"◌ Connecting\" with the channel nam\n[…]\nuest backs out with ErrSuperseded instead of committing, so the\nlast user action always wins both the audio and the UI state. Stale\nresults and errors are additionally ignored in Update by channel ID.",
"is_bot": false,
"headline": "Connect to streams asynchronously with a visible connecting state",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-02T13:10:35Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "dbaa748207c4bbbda7450cf323918c3f871a5ad4",
"body": "ebiten/v2 (a full game engine) was a dependency solely for its\naudio/mp3 package, which is a thin resampling wrapper around\nhajimehoshi/go-mp3. Use go-mp3 directly and add a small linear\nresampler for the unlikely case of a stream that is not at the oto\ncontext's 44.1 kHz rate.",
"is_bot": false,
"headline": "Decode MP3 with go-mp3 directly instead of the ebiten wrapper",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-02T13:04:22Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "494330dffcf9b6713d47b069e90f768b92e15c33",
"body": "Player, playlist parser, channel fetch, and metadata reader each built\na fresh &http.Client{} per call, defeating connection reuse against the\nsame few SomaFM hosts. Expose a single shared client next to the\nrequest factory in the security package.",
"is_bot": false,
"headline": "Share one http.Client across all SomaFM requests",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-02T13:02:12Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "f1acbfa201d4a80f44edec0cc5b01eded7895c96",
"body": "The cache and state paths it guarded are built from os.UserCacheDir /\nos.UserHomeDir plus constant segments, never from user input, so the\nsubstring check added noise without protecting anything. Update the\ngosec suppressions to state the real justification.",
"is_bot": false,
"headline": "Drop ValidatePathNoTraversal",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-02T13:00:59Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "f2955b4f17b18e8b8ab338d825c16207f4ac2d0a",
"body": "internal/security imported the testing package for AllowTestHosts,\nlinking testing into the shipped binary. Move the helper into a new\ninternal/security/securitytest package that only test code imports.",
"is_bot": false,
"headline": "Move test-only host allowlist helper out of the production package",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-02T13:00:08Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "495c559307d6e556086bb55bf95c2a241796299e",
"body": "The stop-playback state reset was duplicated across the \"s\" key,\nMPRISStopMsg, MPRISPlayPauseMsg, and StreamErrorMsg handlers (one of\nwhich missed the Player nil-guard), the quit path was duplicated in\nand out of search mode, and the restore-selection-by-ID loop appeared\nthree times. Centralize each in a single helper.",
"is_bot": false,
"headline": "Extract stopPlayback, quitApp, and selectChannelByID helpers",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-02T12:59:04Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "3c875df965552a56f835bdad92871d363f80f2b5",
"body": "Search input only accepted single printable ASCII bytes, so queries\nwith umlauts or CJK characters were silently dropped, and backspace\ntruncated one byte, corrupting multi-byte runes. Append printable\nrunes from KeyRunes/KeySpace messages and delete whole runes on\nbackspace.",
"is_bot": false,
"headline": "Accept non-ASCII characters in search input",
"author_name": "Samuel Barabas",
"author_login": "samuelb",
"committed_at": "2026-07-02T12:57:44Z",
"body_truncated": false,
"is_coding_agent": false
}
],
"releases_count": 15,
"commits_last_year": 238,
"latest_release_at": "2026-07-13T22:18:27Z",
"latest_release_tag": "v0.14.1",
"releases_from_tags": false,
"days_since_last_push": 5,
"active_weeks_last_year": 18,
"days_since_latest_release": 12,
"mean_days_between_releases": 5.6
},
"community": {
"has_readme": true,
"has_license": true,
"has_description": true,
"has_contributing": false,
"health_percentage": 42,
"has_issue_template": false,
"has_code_of_conduct": false,
"has_pull_request_template": false
},
"ecosystem": {
"packages": []
},
"popularity": {
"forks": 0,
"stars": 0,
"watchers": 0,
"fork_history": {
"days": [],
"complete": true,
"collected": 0,
"total_forks": 0
},
"star_history": {
"days": [],
"complete": true,
"collected": 0,
"total_stars": 0,
"collected_at": null
},
"open_issues_and_prs": 1
},
"ai_readiness": {
"has_nix": true,
"example_dirs": [],
"has_llms_txt": false,
"has_dockerfile": false,
"has_mcp_signal": false,
"bootstrap_files": [
"Makefile",
"vendor/fyne.io/systray/Makefile",
"vendor/github.com/charmbracelet/bubbletea/Taskfile.yaml",
"vendor/github.com/charmbracelet/lipgloss/Taskfile.yaml",
"vendor/github.com/sahilm/fuzzy/Makefile"
],
"api_schema_files": [],
"has_devcontainer": false,
"typecheck_configs": [],
"toolchain_manifests": [
"go.mod"
],
"largest_source_bytes": 21852,
"source_files_sampled": 82,
"oversized_source_files": 0,
"agent_instruction_files": [
"CLAUDE.md",
"vendor/github.com/clipperhouse/displaywidth/AGENTS.md"
],
"agent_instruction_max_bytes": 5270
},
"dependencies": {
"manifests": [
"go.mod"
],
"advisories": {
"error": null,
"scope": null,
"source": null,
"findings": [],
"collected": false,
"malicious": [],
"truncated": false,
"by_severity": {},
"advisory_count": 0,
"affected_count": 0,
"assessed_count": 0,
"malicious_count": 0,
"assessed_package": null,
"unassessed_count": 0,
"direct_affected_count": 0
},
"ecosystems": [
"go"
],
"dependencies": [
{
"name": "fyne.io/systray",
"manifest": "go.mod",
"ecosystem": "go",
"version_constraint": "v1.12.2"
},
{
"name": "github.com/charmbracelet/bubbles",
"manifest": "go.mod",
"ecosystem": "go",
"version_constraint": "v1.0.0"
},
{
"name": "github.com/charmbracelet/bubbletea",
"manifest": "go.mod",
"ecosystem": "go",
"version_constraint": "v1.3.10"
},
{
"name": "github.com/charmbracelet/lipgloss",
"manifest": "go.mod",
"ecosystem": "go",
"version_constraint": "v1.1.0"
},
{
"name": "github.com/charmbracelet/x/ansi",
"manifest": "go.mod",
"ecosystem": "go",
"version_constraint": "v0.11.7"
},
{
"name": "github.com/ebitengine/oto/v3",
"manifest": "go.mod",
"ecosystem": "go",
"version_constraint": "v3.4.0"
},
{
"name": "github.com/godbus/dbus/v5",
"manifest": "go.mod",
"ecosystem": "go",
"version_constraint": "v5.2.2"
},
{
"name": "github.com/hajimehoshi/go-mp3",
"manifest": "go.mod",
"ecosystem": "go",
"version_constraint": "v0.3.4"
},
{
"name": "github.com/stretchr/testify",
"manifest": "go.mod",
"ecosystem": "go",
"version_constraint": "v1.11.1"
},
{
"name": "gopkg.in/yaml.v3",
"manifest": "go.mod",
"ecosystem": "go",
"version_constraint": "v3.0.1"
}
],
"all_dependencies": {
"error": "GitHub dependency-graph SBOM unavailable (404); the dependency graph may be disabled for this repository",
"source": null,
"packages": [],
"collected": false,
"truncated": false,
"total_count": null,
"direct_count": null,
"indirect_count": null
}
},
"maintainership": {
"issues": {
"open_prs": 1,
"merged_prs": 12,
"open_issues": 0,
"closed_ratio": null,
"closed_issues": 0,
"closed_unmerged_prs": 1
},
"bus_factor": 1,
"bot_contributors": 2,
"top_contributors": [
{
"type": "User",
"login": "samuelb",
"commits": 239,
"avatar_url": "https://avatars.githubusercontent.com/u/900568?v=4"
}
],
"contributors_sampled": 1,
"top_contributor_share": 1
},
"quality_signals": {
"has_ci": true,
"has_tests": true,
"ci_workflows": [
"ci.yml",
"release.yml"
],
"has_docs_dir": false,
"linter_configs": [
".golangci.yml"
],
"has_editorconfig": true,
"has_linter_config": true,
"has_precommit_config": false
},
"security_signals": {
"lockfiles": [
"go.sum"
],
"scorecard": {
"checks": [
{
"name": "Binary-Artifacts",
"score": 10,
"reason": "no binaries found in the repo",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#binary-artifacts"
},
{
"name": "Branch-Protection",
"score": 0,
"reason": "branch protection not enabled on development/release branches",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#branch-protection"
},
{
"name": "CI-Tests",
"score": null,
"reason": "no pull request found",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#ci-tests"
},
{
"name": "CII-Best-Practices",
"score": 0,
"reason": "no effort to earn an OpenSSF best practices badge detected",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#cii-best-practices"
},
{
"name": "Code-Review",
"score": 0,
"reason": "Found 0/30 approved changesets -- score normalized to 0",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#code-review"
},
{
"name": "Contributors",
"score": 0,
"reason": "project has 0 contributing companies or organizations -- score normalized to 0",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#contributors"
},
{
"name": "Dangerous-Workflow",
"score": 10,
"reason": "no dangerous workflow patterns detected",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#dangerous-workflow"
},
{
"name": "Dependency-Update-Tool",
"score": 10,
"reason": "update tool detected",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#dependency-update-tool"
},
{
"name": "Fuzzing",
"score": 0,
"reason": "project is not fuzzed",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#fuzzing"
},
{
"name": "License",
"score": 10,
"reason": "license file detected",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#license"
},
{
"name": "Maintained",
"score": 10,
"reason": "30 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#maintained"
},
{
"name": "Packaging",
"score": null,
"reason": "packaging workflow not detected",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#packaging"
},
{
"name": "Pinned-Dependencies",
"score": 0,
"reason": "dependency not pinned by hash detected -- score normalized to 0",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#pinned-dependencies"
},
{
"name": "SAST",
"score": 0,
"reason": "no SAST tool detected",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#sast"
},
{
"name": "Security-Policy",
"score": 0,
"reason": "security policy file not detected",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#security-policy"
},
{
"name": "Signed-Releases",
"score": 0,
"reason": "Project has not signed or included provenance with any releases.",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#signed-releases"
},
{
"name": "Token-Permissions",
"score": 0,
"reason": "detected GitHub workflow tokens with excessive permissions",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#token-permissions"
},
{
"name": "Vulnerabilities",
"score": 9,
"reason": "1 existing vulnerabilities detected",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#vulnerabilities"
}
],
"commit": "ca4adeb472937af4011d0c6f62845ac33ba58bfc",
"ran_at": "2026-07-26T05:13:40Z",
"aggregate_score": 4.3,
"scorecard_version": "v5.5.0"
},
"has_codeql_workflow": false,
"has_security_policy": false,
"has_dependabot_config": true
},
"contribution_flow": {
"collected": true,
"ci_last_run_at": "2026-07-21T03:43:29Z",
"oldest_open_prs": [
{
"number": 14,
"created_at": "2026-07-21T03:43:26Z",
"last_comment_at": null,
"last_comment_author": null
}
],
"last_merged_pr_at": "2026-07-03T10:43:35Z",
"ci_last_conclusion": "SUCCESS",
"oldest_open_issues": []
}
},
"config": {
"disabled_metrics": [],
"disabled_categories": [],
"disabled_components": {}
},
"source": {
"url": "https://github.com/samuelb/somad",
"host": "github.com",
"name": "somad",
"owner": "samuelb"
},
"metrics": {
"overall": {
"key": "overall",
"band": "moderate",
"name": "Overall health",
"note": null,
"notes": [],
"value": 54,
"inputs": {
"security": 43,
"vitality": 82,
"community": 24,
"governance": 41,
"engineering": 73
},
"components": []
},
"categories": [
{
"key": "vitality",
"band": "good",
"name": "Vitality",
"value": 82,
"weight": 0.22,
"metrics": [
{
"key": "development_activity",
"band": "good",
"name": "Development activity",
"note": null,
"notes": [],
"value": 76,
"inputs": {
"commits_last_year": 238,
"human_commit_share": 0.98,
"days_since_last_push": 5,
"active_weeks_last_year": 18
},
"components": [
{
"key": "push_recency",
"name": "Push recency",
"detail": "last push 5 days ago",
"points": 36,
"status": "met",
"details": [
{
"code": "push_recency",
"params": {
"days": 5
}
}
],
"max_points": 36
},
{
"key": "commit_cadence",
"name": "Commit cadence",
"detail": "18/52 weeks with commits",
"points": 12.5,
"status": "partial",
"details": [
{
"code": "commit_cadence_weeks",
"params": {
"weeks": 18
}
}
],
"max_points": 36
},
{
"key": "commit_volume",
"name": "Commit volume",
"detail": "238 commits in the last year",
"points": 18,
"status": "met",
"details": [
{
"code": "commits_last_year",
"params": {
"count": 238
}
}
],
"max_points": 18
},
{
"key": "openssf_scorecard_maintained",
"name": "OpenSSF Scorecard: Maintained",
"detail": "30 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10",
"points": 10,
"status": "met",
"details": [],
"max_points": 10
}
]
},
{
"key": "release_discipline",
"band": "excellent",
"name": "Release discipline",
"note": null,
"notes": [],
"value": 90,
"inputs": {
"releases_count": 15,
"latest_release_tag": "v0.14.1",
"releases_from_tags": false,
"days_since_latest_release": 12,
"mean_days_between_releases": 5.6
},
"components": [
{
"key": "ships_releases",
"name": "Ships releases",
"detail": "15 releases published",
"points": 27,
"status": "met",
"details": [
{
"code": "releases_published",
"params": {
"count": 15
}
}
],
"max_points": 27
},
{
"key": "release_recency",
"name": "Release recency",
"detail": "latest release 12 days ago",
"points": 36,
"status": "met",
"details": [
{
"code": "release_recency",
"params": {
"days": 12
}
}
],
"max_points": 36
},
{
"key": "release_cadence",
"name": "Release cadence",
"detail": "a release every ~5.6 days",
"points": 27,
"status": "met",
"details": [
{
"code": "release_cadence",
"params": {
"gap": 5.6
}
}
],
"max_points": 27
},
{
"key": "openssf_scorecard_signed_releases",
"name": "OpenSSF Scorecard: Signed-Releases",
"detail": "Project has not signed or included provenance with any releases.",
"points": 0,
"status": "missed",
"details": [],
"max_points": 10
}
]
},
{
"key": "abandonment",
"band": "excellent",
"name": "Abandonment",
"note": null,
"notes": [],
"value": 100,
"inputs": {
"cap": null,
"state": "unverified",
"guards": [],
"signals": [],
"red_flag": false,
"multiplier_pct": 100,
"declared_reason": null,
"unverified_reason": "repository_too_young",
"unanswered_open_prs": null,
"unanswered_open_issues": null,
"days_since_last_merged_pr": null,
"days_since_last_human_commit": null,
"days_since_last_human_commit_is_floor": false
},
"components": [
{
"key": "project_is_still_maintained",
"name": "Project is still maintained",
"detail": "maintenance record not established from the collected data",
"points": 100,
"status": "met",
"details": [
{
"code": "abandonment_unverified",
"params": {}
}
],
"max_points": 100
}
]
}
],
"description": "Is the project alive — is code being written and are releases shipping?"
},
{
"key": "community",
"band": "critical",
"name": "Community & Adoption",
"value": 24,
"weight": 0.18,
"metrics": [
{
"key": "popularity",
"band": "critical",
"name": "Popularity & adoption",
"note": null,
"notes": [],
"value": 1,
"inputs": {
"forks": 0,
"stars": 0,
"watchers": 0,
"growth_state": "unverified",
"growth_factor_pct": 100,
"growth_unverified_reason": "no_history"
},
"components": [
{
"key": "stars",
"name": "Stars",
"detail": "0 stars",
"points": 0,
"status": "missed",
"details": [
{
"code": "stars",
"params": {
"count": 0
}
}
],
"max_points": 60
},
{
"key": "forks",
"name": "Forks",
"detail": "0 forks",
"points": 0,
"status": "missed",
"details": [
{
"code": "forks",
"params": {
"count": 0
}
}
],
"max_points": 25
},
{
"key": "watchers",
"name": "Watchers",
"detail": "0 watchers",
"points": 0,
"status": "missed",
"details": [
{
"code": "watchers",
"params": {
"count": 0
}
}
],
"max_points": 15
}
]
},
{
"key": "community_health",
"band": "moderate",
"name": "Community health",
"note": null,
"notes": [],
"value": 50,
"inputs": {
"has_readme": true,
"has_license": true,
"has_contributing": false,
"has_issue_template": false,
"has_code_of_conduct": false,
"has_pull_request_template": false
},
"components": [
{
"key": "readme",
"name": "README",
"detail": null,
"points": 22.5,
"status": "met",
"details": [],
"max_points": 22.5
},
{
"key": "license",
"name": "License",
"detail": "recognized license (MIT)",
"points": 22.5,
"status": "met",
"details": [
{
"code": "license_standard",
"params": {}
},
{
"code": "license_spdx",
"params": {
"spdx": "MIT"
}
}
],
"max_points": 22.5
},
{
"key": "contributing_guide",
"name": "CONTRIBUTING guide",
"detail": null,
"points": 0,
"status": "missed",
"details": [],
"max_points": 18
},
{
"key": "code_of_conduct",
"name": "Code of conduct",
"detail": null,
"points": 0,
"status": "missed",
"details": [],
"max_points": 13.5
},
{
"key": "issue_template",
"name": "Issue template",
"detail": null,
"points": 0,
"status": "missed",
"details": [],
"max_points": 7.2
},
{
"key": "pr_template",
"name": "PR template",
"detail": null,
"points": 0,
"status": "missed",
"details": [],
"max_points": 6.3
}
]
}
],
"description": "Does the project have users, downloads, attention, and a welcoming setup for contributors?"
},
{
"key": "governance",
"band": "at_risk",
"name": "Sustainability & Governance",
"value": 41,
"weight": 0.24,
"metrics": [
{
"key": "maintainer_resilience",
"band": "critical",
"name": "Maintainer resilience (bus factor)",
"note": null,
"notes": [],
"value": 10,
"inputs": {
"bus_factor": 1,
"contributors_sampled": 1,
"top_contributor_share": 1
},
"components": [
{
"key": "bus_factor",
"name": "Bus factor",
"detail": "1 contributor(s) cover half of all commits",
"points": 9,
"status": "partial",
"details": [
{
"code": "bus_factor",
"params": {
"count": 1
}
}
],
"max_points": 54
},
{
"key": "commit_distribution",
"name": "Commit distribution",
"detail": "top contributor authored 100% of commits",
"points": 0,
"status": "missed",
"details": [
{
"code": "top_contributor_share",
"params": {
"share": 100
}
}
],
"max_points": 22.5
},
{
"key": "contributor_breadth",
"name": "Contributor breadth",
"detail": "1 contributors",
"points": 1.4,
"status": "partial",
"details": [
{
"code": "contributors_sampled",
"params": {
"count": 1
}
}
],
"max_points": 13.5
},
{
"key": "openssf_scorecard_contributors",
"name": "OpenSSF Scorecard: Contributors",
"detail": "project has 0 contributing companies or organizations -- score normalized to 0",
"points": 0,
"status": "missed",
"details": [],
"max_points": 10
}
]
},
{
"key": "responsiveness",
"band": "moderate",
"name": "Issue & PR responsiveness",
"note": "Excluded from scoring (no data or not applicable): Issue resolution. Remaining weights renormalized.",
"notes": [
{
"code": "excluded_no_data",
"params": {
"components": [
"issue_resolution"
]
}
},
{
"code": "weights_renormalized",
"params": {}
}
],
"value": 66,
"inputs": {
"merged_prs": 12,
"open_issues": 0,
"closed_issues": 0,
"issue_closed_ratio": null,
"closed_unmerged_prs": 1
},
"components": [
{
"key": "issue_resolution",
"name": "Issue resolution",
"detail": "no issues or no data",
"points": 0,
"status": "excluded",
"details": [
{
"code": "no_issues_or_data",
"params": {}
}
],
"max_points": 46.75
},
{
"key": "pr_acceptance",
"name": "PR acceptance",
"detail": "12/13 decided PRs merged",
"points": 35.3,
"status": "partial",
"details": [
{
"code": "decided_prs_merged",
"params": {
"merged": 12,
"decided": 13
}
}
],
"max_points": 38.25
},
{
"key": "openssf_scorecard_code_review",
"name": "OpenSSF Scorecard: Code-Review",
"detail": "Found 0/30 approved changesets -- score normalized to 0",
"points": 0,
"status": "missed",
"details": [],
"max_points": 15
}
]
},
{
"key": "stewardship",
"band": "moderate",
"name": "Ownership & stewardship",
"note": "Excluded from scoring (no data or not applicable): Verified domain. Remaining weights renormalized.",
"notes": [
{
"code": "excluded_no_data",
"params": {
"components": [
"verified_domain"
]
}
},
{
"code": "weights_renormalized",
"params": {}
}
],
"value": 53,
"inputs": {
"followers": 10,
"owner_type": "User",
"is_verified": null,
"owner_login": "samuelb",
"public_repos": 52,
"account_age_days": 5497
},
"components": [
{
"key": "ownership_backing",
"name": "Ownership backing",
"detail": "personal (user) account",
"points": 10,
"status": "partial",
"details": [
{
"code": "owner_personal",
"params": {}
}
],
"max_points": 30
},
{
"key": "verified_domain",
"name": "Verified domain",
"detail": "not applicable to user accounts",
"points": 0,
"status": "excluded",
"details": [
{
"code": "not_applicable_to_user_accounts",
"params": {}
}
],
"max_points": 20
},
{
"key": "owner_reach",
"name": "Owner reach",
"detail": "10 followers of samuelb",
"points": 7.5,
"status": "partial",
"details": [
{
"code": "owner_followers",
"params": {
"count": 10,
"login": "samuelb"
}
}
],
"max_points": 25
},
{
"key": "track_record",
"name": "Track record",
"detail": "52 public repos, account ~15 yr old",
"points": 24.6,
"status": "partial",
"details": [
{
"code": "public_repos",
"params": {
"count": 52
}
},
{
"code": "account_age_years",
"params": {
"years": 15
}
}
],
"max_points": 25
}
]
}
],
"description": "Will the project survive its people — bus factor, responsiveness, who backs it, and package upkeep?"
},
{
"key": "engineering",
"band": "good",
"name": "Engineering Quality",
"value": 73,
"weight": 0.2,
"metrics": [
{
"key": "engineering_practices",
"band": "excellent",
"name": "Engineering practices",
"note": "Excluded from scoring (no data or not applicable): OpenSSF Scorecard: CI-Tests. Remaining weights renormalized.",
"notes": [
{
"code": "excluded_no_data",
"params": {
"components": [
"openssf_scorecard_ci_tests"
]
}
},
{
"code": "weights_renormalized",
"params": {}
}
],
"value": 88,
"inputs": {
"has_ci": true,
"has_tests": true,
"has_editorconfig": true,
"has_linter_config": true,
"has_precommit_config": false
},
"components": [
{
"key": "ci_workflows",
"name": "CI workflows",
"detail": "2 workflow(s)",
"points": 24,
"status": "met",
"details": [
{
"code": "ci_workflows",
"params": {
"count": 2
}
}
],
"max_points": 24
},
{
"key": "tests_present",
"name": "Tests present",
"detail": null,
"points": 24,
"status": "met",
"details": [],
"max_points": 24
},
{
"key": "linter_config",
"name": "Linter config",
"detail": ".golangci.yml",
"points": 16,
"status": "met",
"details": [
{
"code": "file_list",
"params": {
"files": ".golangci.yml"
}
}
],
"max_points": 16
},
{
"key": "pre_commit_hooks",
"name": "Pre-commit hooks",
"detail": null,
"points": 0,
"status": "missed",
"details": [],
"max_points": 9.6
},
{
"key": "editorconfig",
"name": ".editorconfig",
"detail": null,
"points": 6.4,
"status": "met",
"details": [],
"max_points": 6.4
},
{
"key": "openssf_scorecard_ci_tests",
"name": "OpenSSF Scorecard: CI-Tests",
"detail": "no pull request found",
"points": 0,
"status": "excluded",
"details": [
{
"code": "no_data",
"params": {}
}
],
"max_points": 20
}
]
},
{
"key": "documentation",
"band": "moderate",
"name": "Documentation",
"note": null,
"notes": [],
"value": 50,
"inputs": {
"topics": [
"bubbletea",
"cli",
"daemon",
"golang",
"internet-radio",
"radio",
"somafm",
"tui"
],
"has_wiki": false,
"homepage": null,
"has_readme": true,
"has_docs_dir": false,
"has_description": true
},
"components": [
{
"key": "readme",
"name": "README",
"detail": null,
"points": 30,
"status": "met",
"details": [],
"max_points": 30
},
{
"key": "documentation_directory",
"name": "Documentation directory",
"detail": null,
"points": 0,
"status": "missed",
"details": [],
"max_points": 25
},
{
"key": "documentation_homepage_site",
"name": "Documentation / homepage site",
"detail": null,
"points": 0,
"status": "missed",
"details": [],
"max_points": 15
},
{
"key": "repository_description",
"name": "Repository description",
"detail": null,
"points": 10,
"status": "met",
"details": [],
"max_points": 10
},
{
"key": "topics",
"name": "Topics",
"detail": "8 topics",
"points": 10,
"status": "met",
"details": [
{
"code": "topics_count",
"params": {
"count": 8
}
}
],
"max_points": 10
},
{
"key": "wiki",
"name": "Wiki",
"detail": null,
"points": 0,
"status": "missed",
"details": [],
"max_points": 10
}
]
}
],
"description": "Are baseline engineering and documentation practices in place?"
},
{
"key": "security",
"band": "at_risk",
"name": "Security",
"value": 43,
"weight": 0.16,
"metrics": [
{
"key": "security_posture",
"band": "at_risk",
"name": "Security posture",
"note": "Excluded from scoring (no data or not applicable): CI-Tests, Packaging. Remaining weights renormalized.",
"notes": [
{
"code": "excluded_no_data",
"params": {
"components": [
"ci_tests",
"packaging"
]
}
},
{
"code": "weights_renormalized",
"params": {}
}
],
"value": 43,
"inputs": {
"source": "openssf_scorecard",
"checks_evaluated": 16,
"scorecard_version": "v5.5.0",
"checks_inconclusive": 2,
"scorecard_aggregate": 4.3
},
"components": [
{
"key": "binary_artifacts",
"name": "Binary-Artifacts",
"detail": "no binaries found in the repo",
"points": 7.5,
"status": "met",
"details": [],
"max_points": 7.5
},
{
"key": "branch_protection",
"name": "Branch-Protection",
"detail": "branch protection not enabled on development/release branches",
"points": 0,
"status": "missed",
"details": [],
"max_points": 7.5
},
{
"key": "ci_tests",
"name": "CI-Tests",
"detail": "no pull request found",
"points": 0,
"status": "excluded",
"details": [
{
"code": "no_data",
"params": {}
}
],
"max_points": 2.5
},
{
"key": "cii_best_practices",
"name": "CII-Best-Practices",
"detail": "no effort to earn an OpenSSF best practices badge detected",
"points": 0,
"status": "missed",
"details": [],
"max_points": 2.5
},
{
"key": "code_review",
"name": "Code-Review",
"detail": "Found 0/30 approved changesets -- score normalized to 0",
"points": 0,
"status": "missed",
"details": [],
"max_points": 7.5
},
{
"key": "contributors",
"name": "Contributors",
"detail": "project has 0 contributing companies or organizations -- score normalized to 0",
"points": 0,
"status": "missed",
"details": [],
"max_points": 2.5
},
{
"key": "dangerous_workflow",
"name": "Dangerous-Workflow",
"detail": "no dangerous workflow patterns detected",
"points": 10,
"status": "met",
"details": [],
"max_points": 10
},
{
"key": "dependency_update_tool",
"name": "Dependency-Update-Tool",
"detail": "update tool detected",
"points": 7.5,
"status": "met",
"details": [],
"max_points": 7.5
},
{
"key": "fuzzing",
"name": "Fuzzing",
"detail": "project is not fuzzed",
"points": 0,
"status": "missed",
"details": [],
"max_points": 5
},
{
"key": "license",
"name": "License",
"detail": "license file detected",
"points": 2.5,
"status": "met",
"details": [],
"max_points": 2.5
},
{
"key": "maintained",
"name": "Maintained",
"detail": "30 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10",
"points": 7.5,
"status": "met",
"details": [],
"max_points": 7.5
},
{
"key": "packaging",
"name": "Packaging",
"detail": "packaging workflow not detected",
"points": 0,
"status": "excluded",
"details": [
{
"code": "no_data",
"params": {}
}
],
"max_points": 5
},
{
"key": "pinned_dependencies",
"name": "Pinned-Dependencies",
"detail": "dependency not pinned by hash detected -- score normalized to 0",
"points": 0,
"status": "missed",
"details": [],
"max_points": 5
},
{
"key": "sast",
"name": "SAST",
"detail": "no SAST tool detected",
"points": 0,
"status": "missed",
"details": [],
"max_points": 5
},
{
"key": "security_policy",
"name": "Security-Policy",
"detail": "security policy file not detected",
"points": 0,
"status": "missed",
"details": [],
"max_points": 5
},
{
"key": "signed_releases",
"name": "Signed-Releases",
"detail": "Project has not signed or included provenance with any releases.",
"points": 0,
"status": "missed",
"details": [],
"max_points": 7.5
},
{
"key": "token_permissions",
"name": "Token-Permissions",
"detail": "detected GitHub workflow tokens with excessive permissions",
"points": 0,
"status": "missed",
"details": [],
"max_points": 7.5
},
{
"key": "vulnerabilities",
"name": "Vulnerabilities",
"detail": "1 existing vulnerabilities detected",
"points": 6.8,
"status": "partial",
"details": [],
"max_points": 7.5
}
]
},
{
"key": "high_risk_jurisdiction_exposure",
"band": "excellent",
"name": "High-Risk Jurisdiction Exposure",
"note": "Only high-confidence self-published location evidence affects this multiplier. Ambiguous matches are review-only; country evidence is not proof of nationality, citizenship, legal registration, malicious intent, or sanctions status.",
"notes": [
{
"code": "jurisdiction_evidence_limits",
"params": {}
}
],
"value": 100,
"inputs": {
"meaning": "self-published location evidence; not nationality or citizenship",
"red_flag": false,
"exposures": [],
"policy_countries": [
"Russia",
"Iran",
"North Korea"
],
"review_only_matches": 0,
"assessed_self_published_locations": 2
},
"components": [
{
"key": "policy_exposure_multiplier",
"name": "Policy exposure multiplier",
"detail": "no confirmed policy-scope location match",
"points": 100,
"status": "met",
"details": [
{
"code": "jurisdiction_no_match",
"params": {}
}
],
"max_points": 100
}
]
}
],
"description": "Are visible security and supply-chain practices strong, with no malicious dependency and no unresolved high-risk jurisdiction exposure?"
},
{
"key": "ai_readiness",
"band": "good",
"name": "AI Readiness",
"value": 84,
"weight": 0,
"metrics": [
{
"key": "ai_agent_context",
"band": "excellent",
"name": "Agent context & guidance",
"note": null,
"notes": [],
"value": 85,
"inputs": {
"has_llms_txt": false,
"legible_history_share": 0.867,
"agent_instruction_files": [
"CLAUDE.md",
"vendor/github.com/clipperhouse/displaywidth/AGENTS.md"
],
"agent_instruction_max_bytes": 5270
},
"components": [
{
"key": "agent_instructions",
"name": "Agent instructions",
"detail": "CLAUDE.md, vendor/github.com/clipperhouse/displaywidth/AGENTS.md",
"points": 45,
"status": "met",
"details": [
{
"code": "file_list",
"params": {
"files": "CLAUDE.md, vendor/github.com/clipperhouse/displaywidth/AGENTS.md"
}
}
],
"max_points": 45
},
{
"key": "machine_readable_docs_llms_txt",
"name": "Machine-readable docs (llms.txt)",
"detail": null,
"points": 0,
"status": "missed",
"details": [],
"max_points": 15
},
{
"key": "legible_commit_history",
"name": "Legible commit history",
"detail": "85 of 98 human commits state their intent (structured subject or explanatory body)",
"points": 40,
"status": "met",
"details": [
{
"code": "legible_history",
"params": {
"legible": 85,
"sampled": 98
}
}
],
"max_points": 40
}
]
},
{
"key": "ai_verify_loop",
"band": "good",
"name": "Verify loop (build / test / typecheck)",
"note": null,
"notes": [],
"value": 77,
"inputs": {
"has_nix": true,
"has_tests": true,
"lockfiles": [
"go.sum"
],
"has_dockerfile": false,
"typed_language": true,
"bootstrap_files": [
"Makefile",
"vendor/fyne.io/systray/Makefile",
"vendor/github.com/charmbracelet/bubbletea/Taskfile.yaml",
"vendor/github.com/charmbracelet/lipgloss/Taskfile.yaml",
"vendor/github.com/sahilm/fuzzy/Makefile"
],
"has_devcontainer": false,
"has_linter_config": true,
"typecheck_configs": [],
"agent_commit_share": 0,
"toolchain_manifests": [
"go.mod"
],
"dependency_bot_commit_share": 0
},
"components": [
{
"key": "one_command_bootstrap",
"name": "One-command bootstrap",
"detail": "Makefile, vendor/fyne.io/systray/Makefile, vendor/github.com/charmbracelet/bubbletea/Taskfile.yaml, vendor/github.com/charmbracelet/lipgloss/Taskfile.yaml, vendor/github.com/sahilm/fuzzy/Makefile",
"points": 18,
"status": "met",
"details": [
{
"code": "file_list",
"params": {
"files": "Makefile, vendor/fyne.io/systray/Makefile, vendor/github.com/charmbracelet/bubbletea/Taskfile.yaml, vendor/github.com/charmbracelet/lipgloss/Taskfile.yaml, vendor/github.com/sahilm/fuzzy/Makefile"
}
}
],
"max_points": 18
},
{
"key": "automated_tests",
"name": "Automated tests",
"detail": null,
"points": 22,
"status": "met",
"details": [],
"max_points": 22
},
{
"key": "lint_format_config",
"name": "Lint / format config",
"detail": ".golangci.yml",
"points": 11,
"status": "met",
"details": [
{
"code": "file_list",
"params": {
"files": ".golangci.yml"
}
}
],
"max_points": 11
},
{
"key": "static_type_checking",
"name": "Static type checking",
"detail": "Go (statically typed)",
"points": 11,
"status": "met",
"details": [
{
"code": "statically_typed_language",
"params": {
"language": "Go"
}
}
],
"max_points": 11
},
{
"key": "reproducible_environment",
"name": "Reproducible environment",
"detail": "Nix, lockfile",
"points": 10,
"status": "met",
"details": [
{
"code": "file_list",
"params": {
"files": "Nix, lockfile"
}
}
],
"max_points": 10
},
{
"key": "demonstrated_agent_practice",
"name": "Demonstrated agent practice",
"detail": "no agent-authored commits among the last 100",
"points": 0,
"status": "missed",
"details": [
{
"code": "no_agent_authored_commits",
"params": {
"sampled": 100
}
}
],
"max_points": 10
},
{
"key": "automated_maintenance",
"name": "Automated maintenance",
"detail": "dependency automation configured, none observed in the sampled commits",
"points": 5,
"status": "partial",
"details": [
{
"code": "dependency_bot_config_only",
"params": {}
}
],
"max_points": 8
},
{
"key": "openssf_scorecard_pinned_dependencies",
"name": "OpenSSF Scorecard: Pinned-Dependencies",
"detail": "dependency not pinned by hash detected -- score normalized to 0",
"points": 0,
"status": "missed",
"details": [],
"max_points": 10
}
]
},
{
"key": "ai_code_legibility",
"band": "excellent",
"name": "Code legibility for models",
"note": null,
"notes": [],
"value": 100,
"inputs": {
"primary_language": "Go",
"largest_source_bytes": 21852,
"source_files_sampled": 82,
"oversized_source_files": 0
},
"components": [
{
"key": "type_checkable_code",
"name": "Type-checkable code",
"detail": "Go (statically typed)",
"points": 45,
"status": "met",
"details": [
{
"code": "statically_typed_language",
"params": {
"language": "Go"
}
}
],
"max_points": 45
},
{
"key": "manageable_file_sizes",
"name": "Manageable file sizes",
"detail": "0/82 source files over 60KB",
"points": 55,
"status": "met",
"details": [
{
"code": "oversized_source_files",
"params": {
"kb": 60,
"sampled": 82,
"oversized": 0
}
}
],
"max_points": 55
}
]
}
],
"description": "How well is the repo equipped to be developed and maintained with AI coding agents? An independent, experimental badge — weight 0.0, so it is surfaced on its own and does not affect the overall health score."
}
],
"metrics_version": "1.13.0"
},
"warnings": [
"GitHub dependency-graph SBOM unavailable (404); the dependency graph may be disabled for this repository"
],
"report_type": "repository",
"generated_at": "2026-07-26T05:13:46.995778Z",
"schema_version": "0.27.0",
"badge_url": "https://raw.githubusercontent.com/inspect-software/badges/main/v1/s/samuelb/somad.svg",
"full_name": "samuelb/somad",
"license_state": "standard",
"license_spdx": "MIT"
}