Raw JSON report machine-readable
{
"data": {
"repo": {
"topics": [
"go",
"gitlab"
],
"is_fork": false,
"size_kb": 1127,
"has_wiki": true,
"homepage": null,
"languages": {
"Go": 770973,
"Dockerfile": 1594,
"Go Template": 5296
},
"pushed_at": "2026-07-27T03:27:12Z",
"created_at": "2020-09-28T07:46:52Z",
"owner_type": "User",
"updated_at": "2026-07-27T03:27:22Z",
"description": "Manage GitLab for Labs from the Command Line",
"is_archived": false,
"is_disabled": false,
"license_spdx": "BSD-3-Clause",
"default_branch": "main",
"license_spdx_raw": "BSD-3-Clause",
"primary_language": "Go",
"significant_languages": [
"Go"
]
},
"owner": {
"blog": null,
"name": "Oliver Braun",
"type": "User",
"login": "obcode",
"company": "Munich University of Applied Sciences",
"location": "Germany",
"followers": 54,
"avatar_url": "https://avatars.githubusercontent.com/u/281558?v=4",
"created_at": "2010-05-19T17:02:02Z",
"is_verified": null,
"public_repos": 47,
"account_age_days": 5913
},
"license": {
"state": "standard",
"spdx_id": "BSD-3-Clause",
"raw_spdx": "BSD-3-Clause",
"file_present": true,
"scorecard_found": true,
"profile_has_license": true
},
"activity": {
"releases": [
{
"tag": "v3.41.0",
"kind": "minor",
"published_at": "2026-07-22T15:54:14Z"
},
{
"tag": "v3.40.0",
"kind": "minor",
"published_at": "2026-07-21T19:11:16Z"
},
{
"tag": "v3.39.0",
"kind": "minor",
"published_at": "2026-07-20T12:54:54Z"
},
{
"tag": "v3.38.0",
"kind": "minor",
"published_at": "2026-07-20T11:24:46Z"
},
{
"tag": "v3.37.0",
"kind": "minor",
"published_at": "2026-07-20T11:00:31Z"
},
{
"tag": "v3.36.0",
"kind": "minor",
"published_at": "2026-07-19T16:37:22Z"
},
{
"tag": "v3.35.0",
"kind": "minor",
"published_at": "2026-07-19T15:49:29Z"
},
{
"tag": "v3.34.0",
"kind": "minor",
"published_at": "2026-07-19T14:49:29Z"
},
{
"tag": "v3.33.0",
"kind": "minor",
"published_at": "2026-07-19T14:45:08Z"
},
{
"tag": "v3.32.2",
"kind": "patch",
"published_at": "2026-07-19T14:33:37Z"
},
{
"tag": "v3.32.1",
"kind": "patch",
"published_at": "2026-07-19T14:14:50Z"
},
{
"tag": "v3.32.0",
"kind": "minor",
"published_at": "2026-07-19T13:09:11Z"
},
{
"tag": "v3.31.0",
"kind": "minor",
"published_at": "2026-07-19T13:00:47Z"
},
{
"tag": "v3.30.0",
"kind": "minor",
"published_at": "2026-07-19T12:37:47Z"
},
{
"tag": "v3.29.0",
"kind": "minor",
"published_at": "2026-07-19T10:23:57Z"
},
{
"tag": "v3.28.0",
"kind": "minor",
"published_at": "2026-07-19T10:17:35Z"
},
{
"tag": "v3.27.0",
"kind": "minor",
"published_at": "2026-07-19T10:09:14Z"
},
{
"tag": "v3.26.0",
"kind": "minor",
"published_at": "2026-07-19T10:02:43Z"
},
{
"tag": "v3.25.0",
"kind": "minor",
"published_at": "2026-07-19T08:51:16Z"
},
{
"tag": "v3.24.0",
"kind": "minor",
"published_at": "2026-07-19T08:36:59Z"
},
{
"tag": "v3.23.0",
"kind": "minor",
"published_at": "2026-07-19T08:27:23Z"
},
{
"tag": "v3.22.0",
"kind": "minor",
"published_at": "2026-07-19T08:17:22Z"
},
{
"tag": "v3.21.1",
"kind": "patch",
"published_at": "2026-07-18T19:25:18Z"
},
{
"tag": "v3.21.0",
"kind": "minor",
"published_at": "2026-07-18T18:26:13Z"
},
{
"tag": "v3.20.0",
"kind": "minor",
"published_at": "2026-07-18T17:54:12Z"
},
{
"tag": "v3.19.1",
"kind": "patch",
"published_at": "2026-07-18T17:31:50Z"
},
{
"tag": "v3.19.0",
"kind": "minor",
"published_at": "2026-07-18T16:43:11Z"
},
{
"tag": "v3.18.0",
"kind": "minor",
"published_at": "2026-07-18T16:11:50Z"
},
{
"tag": "v3.17.0",
"kind": "minor",
"published_at": "2026-07-18T15:44:08Z"
},
{
"tag": "v3.16.0",
"kind": "minor",
"published_at": "2026-07-18T15:19:21Z"
},
{
"tag": "v3.15.0",
"kind": "minor",
"published_at": "2026-07-18T14:27:24Z"
},
{
"tag": "v3.14.0",
"kind": "minor",
"published_at": "2026-07-18T11:56:22Z"
},
{
"tag": "v3.13.0",
"kind": "minor",
"published_at": "2026-07-18T11:39:37Z"
},
{
"tag": "v3.12.0",
"kind": "minor",
"published_at": "2026-07-18T11:09:51Z"
},
{
"tag": "v3.11.0",
"kind": "minor",
"published_at": "2026-07-18T10:49:41Z"
},
{
"tag": "v3.10.0",
"kind": "minor",
"published_at": "2026-07-18T10:21:33Z"
},
{
"tag": "v3.9.0",
"kind": "minor",
"published_at": "2026-07-18T10:04:48Z"
},
{
"tag": "v3.8.0",
"kind": "minor",
"published_at": "2026-07-18T09:53:03Z"
},
{
"tag": "v3.7.0",
"kind": "minor",
"published_at": "2026-07-18T09:25:53Z"
},
{
"tag": "v3.6.0",
"kind": "minor",
"published_at": "2026-07-18T09:08:40Z"
},
{
"tag": "v3.5.0",
"kind": "minor",
"published_at": "2026-07-18T08:50:56Z"
},
{
"tag": "v3.4.0",
"kind": "minor",
"published_at": "2026-07-18T08:43:05Z"
},
{
"tag": "v3.3.2",
"kind": "patch",
"published_at": "2026-07-17T19:27:16Z"
},
{
"tag": "v3.3.1",
"kind": "patch",
"published_at": "2026-07-17T19:16:08Z"
},
{
"tag": "v3.3.0",
"kind": "minor",
"published_at": "2026-07-17T19:00:38Z"
},
{
"tag": "v3.2.0",
"kind": "minor",
"published_at": "2026-07-17T18:38:01Z"
},
{
"tag": "v3.1.0",
"kind": "minor",
"published_at": "2026-07-17T18:08:48Z"
},
{
"tag": "v3.0.1",
"kind": "patch",
"published_at": "2026-07-17T18:07:10Z"
},
{
"tag": "v3.0.0",
"kind": "major",
"published_at": "2026-07-17T15:36:01Z"
},
{
"tag": "v2.12.1",
"kind": "patch",
"published_at": "2026-07-17T13:53:42Z"
},
{
"tag": "v2.12.0",
"kind": "minor",
"published_at": "2026-07-17T13:31:30Z"
},
{
"tag": "v2.11.2",
"kind": "patch",
"published_at": "2026-06-25T15:09:31Z"
},
{
"tag": "v2.11.1",
"kind": "patch",
"published_at": "2026-06-16T08:37:54Z"
},
{
"tag": "v2.11.0",
"kind": "minor",
"published_at": "2026-06-11T12:22:58Z"
},
{
"tag": "v2.10.0",
"kind": "minor",
"published_at": "2026-06-11T06:46:29Z"
},
{
"tag": "v2.9.0",
"kind": "minor",
"published_at": "2026-06-02T10:26:40Z"
},
{
"tag": "v2.8.3",
"kind": "patch",
"published_at": "2026-05-31T07:21:50Z"
},
{
"tag": "v2.8.2",
"kind": "patch",
"published_at": "2026-05-31T06:56:04Z"
},
{
"tag": "v2.8.1",
"kind": "patch",
"published_at": "2026-05-31T06:48:29Z"
},
{
"tag": "v2.8.0",
"kind": "minor",
"published_at": "2026-05-20T20:38:20Z"
},
{
"tag": "v2.7.0",
"kind": "minor",
"published_at": "2026-05-05T09:50:53Z"
},
{
"tag": "v2.6.1",
"kind": "patch",
"published_at": "2026-05-01T09:44:27Z"
},
{
"tag": "v2.6.0",
"kind": "minor",
"published_at": "2026-05-01T09:21:20Z"
},
{
"tag": "v2.5.0",
"kind": "minor",
"published_at": "2026-04-30T22:54:54Z"
},
{
"tag": "v2.4.1",
"kind": "patch",
"published_at": "2026-04-30T21:50:43Z"
},
{
"tag": "v2.4.0",
"kind": "minor",
"published_at": "2026-04-30T21:31:54Z"
},
{
"tag": "v2.3.0",
"kind": "minor",
"published_at": "2026-04-28T17:39:39Z"
},
{
"tag": "v2.2.0",
"kind": "minor",
"published_at": "2026-04-26T16:24:41Z"
},
{
"tag": "v2.1.3",
"kind": "patch",
"published_at": "2026-04-26T09:59:29Z"
},
{
"tag": "v2.1.2",
"kind": "patch",
"published_at": "2026-04-26T09:47:10Z"
},
{
"tag": "v2.1.1",
"kind": "patch",
"published_at": "2026-04-25T16:48:27Z"
},
{
"tag": "v2.1.0",
"kind": "minor",
"published_at": "2026-04-23T22:03:43Z"
},
{
"tag": "v2.0.0",
"kind": "major",
"published_at": "2026-04-23T21:08:36Z"
},
{
"tag": "v1.3.0",
"kind": "minor",
"published_at": "2026-04-23T19:12:20Z"
},
{
"tag": "v1.2.0",
"kind": "minor",
"published_at": "2026-04-23T18:36:34Z"
},
{
"tag": "v1.1.1",
"kind": "patch",
"published_at": "2026-04-23T17:49:11Z"
},
{
"tag": "v1.1.0",
"kind": "minor",
"published_at": "2026-04-23T17:35:19Z"
},
{
"tag": "v1.0.0",
"kind": "major",
"published_at": "2026-04-23T05:30:20Z"
},
{
"tag": "v0.29.0",
"kind": "minor",
"published_at": "2026-04-22T19:21:08Z"
},
{
"tag": "v0.28.0",
"kind": "minor",
"published_at": "2026-03-23T19:03:36Z"
},
{
"tag": "v0.27.0",
"kind": "minor",
"published_at": "2026-03-12T11:10:04Z"
},
{
"tag": "v0.26.0",
"kind": "minor",
"published_at": "2026-03-10T15:36:42Z"
},
{
"tag": "v0.25.0",
"kind": "minor",
"published_at": "2026-01-27T08:58:50Z"
},
{
"tag": "v0.24.1",
"kind": "patch",
"published_at": "2024-12-10T07:04:56Z"
},
{
"tag": "v0.24.0",
"kind": "minor",
"published_at": "2024-11-05T11:21:34Z"
},
{
"tag": "v0.23.0",
"kind": "minor",
"published_at": "2024-11-04T20:42:09Z"
},
{
"tag": "v0.22.0",
"kind": "minor",
"published_at": "2024-11-04T16:00:36Z"
},
{
"tag": "v0.21.0",
"kind": "minor",
"published_at": "2024-11-04T12:19:44Z"
},
{
"tag": "v0.20.2",
"kind": "patch",
"published_at": "2024-11-04T10:10:19Z"
},
{
"tag": "v0.20.1",
"kind": "patch",
"published_at": "2023-09-11T13:11:19Z"
},
{
"tag": "v0.19.0",
"kind": "minor",
"published_at": "2023-03-08T16:04:03Z"
},
{
"tag": "v0.18.2",
"kind": "patch",
"published_at": "2022-11-29T14:12:47Z"
},
{
"tag": "v0.18.1",
"kind": "patch",
"published_at": "2022-10-29T21:08:52Z"
},
{
"tag": "v0.18.0",
"kind": "minor",
"published_at": "2022-10-28T21:53:47Z"
},
{
"tag": "v0.17.2",
"kind": "patch",
"published_at": "2022-10-22T13:50:28Z"
},
{
"tag": "v0.16.1",
"kind": "patch",
"published_at": "2022-08-10T08:54:12Z"
},
{
"tag": "v0.15.0",
"kind": "minor",
"published_at": "2022-02-21T09:10:38Z"
},
{
"tag": "v0.14.0",
"kind": "minor",
"published_at": "2021-07-05T15:49:16Z"
},
{
"tag": "v0.13.0",
"kind": "minor",
"published_at": "2021-07-05T15:38:56Z"
},
{
"tag": "v0.12.0",
"kind": "minor",
"published_at": "2021-05-09T15:15:56Z"
}
],
"recent_commits": [
{
"oid": "de16f8abc0f9145977c73c25446504cce01edffa",
"body": "…README.md; add migration plans for Podman and k3s",
"is_bot": false,
"headline": "chore(docs): update Go version requirement to 1.26+ in CLAUDE.md and …",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-27T03:27:07Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "33310e50bc815bbab3b9ab385a7b56676fcc2e64",
"body": "Bumps the minor-and-patch group with 6 updates:\n\n| Package | From | To |\n| --- | --- | --- |\n| [github.com/go-git/go-billy/v5](https://github.com/go-git/go-billy) | `5.8.0` | `5.9.1` |\n| [github.com/go-git/go-git/v5](https://github.com/go-git/go-git) | `5.18.0` | `5.19.1` |\n| [github.com/gookit/colo\n[…]\nion-update:semver-minor\n dependency-group: minor-and-patch\n...\n\nSigned-off-by: dependabot[bot] <support@github.com>\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>",
"is_bot": true,
"headline": "chore(deps): bump the minor-and-patch group with 6 updates (#135)",
"author_name": "dependabot[bot]",
"author_login": "dependabot[bot]",
"committed_at": "2026-07-26T08:46:59Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "655d319a7e87f574b830a881ff962b45a4fd0790",
"body": "Bumps the actions group with 5 updates:\n\n| Package | From | To |\n| --- | --- | --- |\n| [actions/checkout](https://github.com/actions/checkout) | `4` | `7` |\n| [actions/setup-go](https://github.com/actions/setup-go) | `5` | `7` |\n| [docker/login-action](https://github.com/docker/login-action) | `3` |\n[…]\npe: version-update:semver-major\n dependency-group: actions\n...\n\nSigned-off-by: dependabot[bot] <support@github.com>\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>",
"is_bot": true,
"headline": "chore(deps): bump the actions group with 5 updates (#134)",
"author_name": "dependabot[bot]",
"author_login": "dependabot[bot]",
"committed_at": "2026-07-26T08:15:38Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "5f2c311a1742a2b7d50f5fe5e1264a7b1e7459aa",
"body": "Weekly grouped updates for gomod and github-actions, mirroring the\nglabs.gui setup. Minor/patch bumps are grouped into one PR; majors come\nseparately. A 3-day cooldown avoids brand-new, possibly-broken releases.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "ci(deps): add dependabot for go modules and github actions (#133)",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-26T08:02:32Z",
"body_truncated": false,
"is_coding_agent": true
},
{
"oid": "02c9890e05cc633a3ffbd6cdec14eca65b779f80",
"body": "There is no login allowlist anymore. The auth proxy (oauth2-proxy, restricted\nto the hm.edu domain) is the sole access boundary: anyone it authenticates is\nlet in and acts strictly as their own user (per-user isolation is unchanged).\nThis drops the users collection, the auth.seedusers seeding, and t\n[…]\nng: leftover auth.seedusers / zpa.* keys are simply\nignored. The now-unused users collection can be dropped from Mongo at leisure.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): open access to any authenticated user, remove ZPA (#132)",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-22T15:52:57Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "906c084cb8e25701f825b6007f271d90ea9a1569",
"body": "Give operators visibility into the platform. A new cross-user `events`\ncollection (180-day TTL) records logins (throttled to one per user per 8h),\nrejected logins, scheduled jobs, job outcomes, interactive operations, and\ncourse/token changes — separate from the owner-scoped `activity` log.\n\nAdmins \n[…]\nploy forwards it via oauth2-proxy groups-claim + Caddy,\nthe proven plexams recipe. It is authorization-irrelevant and absent-safe.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): admin monitoring log, nightly summary mail, admin page API",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-21T19:09:32Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "a876e05f5505a99997275c3364789ba690d65044",
"body": "The Dockerfile injects main.version/commit/date via ldflags, but the\nworkflow only passed VERSION. With .git excluded by .dockerignore the\nVCS fallback can't fill them either, so date stayed \"unknown\" and the\nGUI footer hid the build time. Compute the commit + an RFC3339 UTC\ntimestamp and pass both as build-args.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "ci(docker): pass COMMIT and DATE build-args to glabs-web image",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-21T18:24:59Z",
"body_truncated": false,
"is_coding_agent": true
},
{
"oid": "30486adbcb192bfa3055024daf69c400365760d3",
"body": "The deploy job's cd failed when the repo variable DEPLOY_DIR was unset\nbecause the fallback pointed at a nonexistent /home/glabs/glabs/deploy.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "ci(deploy): point DEPLOY_DIR fallback at /home/obraun/glabs/deploy",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-21T18:13:44Z",
"body_truncated": false,
"is_coding_agent": true
},
{
"oid": "5302faa53e9cb57bc90b65c0ded400cf8cf47c13",
"body": "Add `activityLog: [ActivityEntry!]!` — the caller's complete activity log\nacross ALL their courses, newest first, UNCAPPED (the per-assignment and\nper-course reads stay capped at 200; a dump must be complete, and one\nuser's audit trail is bounded in practice). ActivityEntry now also exposes\n`course`\n[…]\nbacks it.\n- app.ActivityLog + resolver; toGraphActivity maps course.\n\nBacks the /activity page and its JSON download in glabs.gui.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): owner-wide activity log query for the audit-log dump (#131)",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-20T12:53:02Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "5c710b702122d6d2ac317a61005d58a400d95260",
"body": "The images build on GitHub-hosted runners, but the deploy host is\nVPN-internal — GitHub cannot push in. Mirror the plexams pattern: a\nself-hosted runner ON the host polls GitHub outbound and rolls out each\nrelease locally.\n\n- docker.yml gains a gated `deploy` job (needs the build job, `if:\n vars.AU\n[…]\ns.gui deploy job follows in that repo. Until AUTO_DEPLOY=true the\nimage is still built & pushed; the deploy job is simply skipped.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "ci(deploy): auto-deploy releases via self-hosted runners (#130)",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-20T12:05:05Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "95009cdaf5e0912664db49caaf387a5ad8544a84",
"body": "…o) (#129)\n\ndeploy/ brings up glabs.cs.hm.edu: Caddy terminates TLS (HM CA via EAB) and\nauthenticates every request against oauth2-proxy (OIDC to sso.hm.edu),\ninjecting X-Remote-User authoritatively — including on `/`, so the GUI's\ncookieless SSR hop can relay it to the backend. glabs-web, gui, mong\n[…]\nRL trap,\n operating, Mongo SSH tunnel, backups).\n\nSecond and final core Milestone-F PR; references the glabs-web image from #128.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(deploy): add the self-hosting stack (Caddy + oauth2-proxy + mong…",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-20T11:23:15Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "d54460f251f50c950a16afdb224562f3f3747332",
"body": "Add a multi-stage Dockerfile that builds only ./cmd/glabs-web (the repo\nships two binaries from one module) into a static CGO-off binary on a\nminimal alpine runtime with ca-certificates + tzdata (main() sets\ntime.Local to Europe/Berlin) and a non-root user. A .dockerignore keeps\nthe build context le\n[…]\npipeline.\n\nThis is the first Milestone-F PR; the deploy/ stack (Caddyfile +\ndocker-compose.yml) follows and references this image.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): publish a glabs-web container image (#128)",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-20T10:59:15Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "95eadb304b0c68e8274ba58b98fd9c7cb77cfb76",
"body": "…#127)\n\nThe repo overview blocked the page for seconds while every assignment's GitLab\ngroup was listed, with no feedback. Add a subscription that emits each assignment\nas soon as it is checked, so the GUI can show live progress (like report/check).\n\n- app.StreamCourseRepoOverview mirrors CourseRepo\n[…]\nseRepoOverviewProgress(course): RepoOverviewEvent\n {assignment, total, done, error}. The one-shot courseRepoOverview query stays.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): stream the course repo overview assignment by assignment (…",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-19T16:36:04Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "ce5e79d1867e2524f237bafb8d19faadc3830854",
"body": "…(Milestone F5e, Z4) (#126)\n\nA whole-course view of what has been generated: per assignment, how many of its\ntarget repos exist in GitLab and who is missing.\n\n- gitlab.ExistingRepoNames lists an assignment group's projects in one paginated\n call (not a GetProject per student), so the overview costs\n[…]\nmatcher is unit\n tested; the GitLab listing is exercised live/by the report path.\n\nGUI page follows in the glabs.gui counterpart.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): course repo overview — which repos are actually generated …",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-19T15:48:13Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "f781b887b7652b9092c743911ae447e854885a13",
"body": "… F5d, Z2) (#125)\n\napp.CourseStudents returns a course's roster, each email enriched with ZPA details\n(first/last name, gender, group, Matrikelnummer) looked up concurrently (bounded to\n8 at a time). A ZPA failure for one student is logged and leaves that entry\nun-enriched rather than failing the pa\n[…]\nth, and auth.\n\nThe students page (Z3) renders this; a per-student generated-repos overview (needs\nGitLab) is a separate follow-up.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): courseStudents query — roster enriched with ZPA (Milestone…",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-19T14:48:02Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "e16e456a4c25e128ee526abc62f64123484a010e",
"body": "…124)\n\nA minimal read-only client for HM's ZPA REST API (mirrors plexams.go/zpa), so the\nstudents page can show a student's name/group/gender. Confined to web/zpa; the CLI\ndoes not use it.\n\n- Client.StudentByEmail looks up defensively: glabs knows emails but ZPA's\n get_student_info search is used w\n[…]\nart fallback, no-match nil, ambiguous nil, non-2xx error.\n\nThe courseStudents query (Z2) and the students page (Z3) build on this.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): ZPA client for enriching the roster (Milestone F5d, Z1) (#…",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-19T14:43:44Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "ee8eac65596cb14aa1ae3fa21d08cdb71268f1e9",
"body": "…n the web (#123)\n\ndelete logged per-project progress via zerolog (log.Info/Error), which the web's\nrunOp reporter never sees — so a delete op produced an empty live log in the\nbrowser (unlike setaccess/protect/archive). Route it through c.rep instead: a Task\nper deleted project (streams \"deleting …\n[…]\nporter lines for\n\"no groups\", a failed search, and a missing project. On the CLI this keeps the\nConsoleReporter spinner behaviour.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "fix(gitlab): route delete output through the reporter so it streams i…",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-19T14:32:18Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "5ceccbeec9fd33d3616b1eaf94a11b44ba9ba770",
"body": "updatePerStudent/updatePerGroup did `return` from the whole loop on a per-repo\nGetProject error, so one missing repo (a student whose repo was never generated\n→ 404) aborted the update for every remaining repo — and because rosters are\nalphabetical, an early one silently skipped almost everyone whil\n[…]\nking\nlike it ran. This is the same bug #109 fixed for setaccess/protect/archive;\nupdate was missed. Changed `return` → `continue`.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "fix(gitlab): skip missing repos instead of aborting update (#122)",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-19T14:13:21Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "07c8b8bb4467496261ba069e1185dd7bce75c182",
"body": "…ng (Milestone E3/F6) (#121)\n\nA debugging aid the user asked for: generate a batch of repos (with starter code)\nwithout adding or inviting the students/groups, so the pipeline can be exercised\nwithout notifying anyone.\n\n- gitlab: Generate takes skipInvite, threaded through generatePerStudent/\n gene\n[…]\nOp (and\n a scheduled run). Test: the flag round-trips through the plan token.\n\nGUI checkbox follows in the glabs.gui counterpart.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat: generate --no-invite / skipInvite — create repos without inviti…",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-19T13:07:41Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "8d92c42839d9f56d6ebfa96c0fcc221dcdcae7cc",
"body": "… E2) (#120)\n\nupdate pushes the starter code over EXISTING student repositories (force), so it\ncan wipe student work — the CLI warns \"USE WITH CARE — only untouched repos\". In\nthe web it is therefore gated like delete/archive: the plan is destructive and\nrunOp/scheduleOp require the confirm phrase.\n\n[…]\nst: update plans as destructive with the course/assignment confirm phrase.\n\nGUI picker entry follows in the glabs.gui counterpart.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): update as a plan/run op, treated as destructive (Milestone…",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-19T12:59:16Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "898ba558d4546f492ff5aaf969656595e50e8203",
"body": "Wires `generate` into the existing planOp/runOp machinery — the transport was\nunblocked by E1a, so this is small.\n\n- Op enum gains GENERATE; validOps accepts \"generate\" (the first op that touches\n git — an in-memory starter-code clone, so no server disk is involved).\n- executeOp dispatches \"generat\n[…]\nists only the four API ops; adding GENERATE there is a\none-line glabs.gui follow-up (E4). update (E2) and --no-invite (E3) follow.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): generate as a plan/run op (Milestone E1b) (#119)",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-19T12:36:23Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "080c2142792da2d9b7da2797ad5b61b558f27553",
"body": "… (Milestone E1a) (#118)\n\nThe git transport still read the token, host and committer from the global viper\nconfig, so it could authenticate as only one identity — fine for the CLI, but a\nblocker for the web server, which must clone/push as a specific user's per-user\nPAT. This threads them through in\n[…]\n is E1b. Verified with\ngo build/vet (incl. -tags=integration)/test/lint; the GitLab CE integration tests\nprove the real transport.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "refactor(git): inject git auth and committer instead of reading viper…",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-19T12:22:24Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "bef4559726e0eabf7e07fa970a7fde344bd2c806",
"body": "… mails (Milestone D4) (#117)\n\nThe D3 mail package is now driven by the runner and scheduler.\n\n- App gains an optional Mailer (an interface; *mail.Sender implements it) and a\n mailDryRun flag, built in bootstrap from smtp.* config. Without smtp.host the\n mailer is nil and every notification is sil\n[…]\nke mailer: send-once-and-mark, retry-on-failure,\nno-mailer no-op, and the scheduling confirmation. SMTP e2e via Mailpit is manual.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): wire email into scheduling — confirmation + terminal-state…",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-19T10:22:33Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "665b941cc2d3beb142b054dad25b36ccdea63219",
"body": "A standalone web/mail package, not yet wired to the runner. It keeps the go-mail\ndependency and the rendering confined here; callers see only Config, Sender,\nRender and JobMail.\n\n- renderer.go: one Markdown template renders into BOTH the plain-text part (the\n Markdown itself plus a footer) and the \n[…]\nany network call.\n\nWiring into the runner (email on every terminal state + a scheduling confirmation,\nnotified flag, retry) is D4.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): email subsystem for job notifications (Milestone D3) (#116)",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-19T10:15:45Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "1e9697a3dd29a09ac708efc8df3503e1ac519edd",
"body": "… (Milestone D2) (#115)\n\nThe runner that actually fires the jobs D1 persists — no email yet.\n\n- app.StartJobRunner: a ~30s poll loop that drains due jobs each tick, claiming\n them one at a time via the atomic ClaimDueJob (exactly-once across instances);\n started as a background goroutine in bootst\n[…]\nunit-tested; the atomic claim and\na real run are verified against Mongo/GitLab CE. Email on every terminal state\nfollows in D3/D4.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): scheduled-job poll runner with grace/configHash/PAT guards…",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-19T10:07:56Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "7e52f831c9dbe5fa68c3dbe429317fc14669f281",
"body": "…PI (Milestone D1) (#114)\n\nThe first slice of scheduling: persist a mutating op to run later, no runner yet.\n\n- web/db/jobs.go: a ScheduledJob document and an owner-scoped API — SaveJob, an\n atomic ClaimDueJob (FindOneAndUpdate flips the oldest due pending job to running,\n the single step that rep\n[…]\nlaims and executes these jobs (with the grace/configHash/PAT guards)\nand the email on every terminal state follow in D2 and D3/D4.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): scheduled jobs — persistence and scheduleOp/cancel/query A…",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-19T10:01:20Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "d9839f35d657972013cb94ead74e31743d8eef19",
"body": "…nt (#113)\n\nThe web has no shell history, so a course owner cannot see what has already been\ndone to an assignment. This adds an activity log (the deferred C2b audit log):\n\n- web/db/activity.go: an owner-scoped `activity` collection with a Record and two\n owner-filtered reads (per assignment, per c\n[…]\n E. CLI actions are deliberately not recorded (no shared\nstate) — an authoritative \"was it generated?\" still needs a GitLab query.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): activity log — record and expose mutating ops per assignme…",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-19T08:49:50Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "b4e1cda589c2f9d33bf36c5322f44b32b27cba53",
"body": "Two new mutations for the guided editor, matching what plain-YAML editing allows:\n\n- renameCourse(oldName, newName): the course name is the YAML file's top-level\n key, so the raw bytes are re-encoded under the new name; the Mongo document is\n re-keyed (save new, delete old). Rejects a taken or non\n[…]\nitance chain silently breaks\n (something a by-hand YAML rename would leave to the user). Rejects a taken or\n non-path-safe name.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): rename a course or an assignment (#112)",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-19T08:35:47Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "b8b2a968a264aec51e5d577d325648b287647499",
"body": "…(#111)\n\nOnly the assignment name has to be unique, so copying is a thin operation:\nduplicate the source AssignmentSource under a new key and re-marshal the course\nYAML. The copy carries everything over (including `extends`); the persisted\nround-trip (encode → store → decode) makes it fully independ\n[…]\neady exists or is not path-safe, and a missing\nsource. New GraphQL mutation copyAssignment(course, from, newName): AssignmentView.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): copyAssignment — duplicate an assignment under a new name …",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-19T08:25:59Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "e4011d777638d3b40c214b89b6de117d5db04bcc",
"body": "…own (#110)\n\nThe assignment editor's `extends` field was a free-text input. Since\n`extends` is course-internal (resolve.go requires the target to be another\nassignment in the same course), the valid choices are exactly the sibling\nassignments. AssignmentView now carries `extendsOptions` — all assignment\nnames in the course except the one being edited, sorted — so the GUI can\nrender a dropdown instead of a plain string.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): expose extendsOptions for the assignment inheritance dropd…",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-19T08:15:56Z",
"body_truncated": false,
"is_coding_agent": true
},
{
"oid": "ff5d633b274771b6f0db0bf52551ec88756a2974",
"body": "…/archive (#109)\n\nsetaccessPerStudent/PerGroup (and the protect/archive equivalents) returned from\nthe whole loop when GetProject failed for one repository — so a single repo that\ndoes not exist (e.g. a student whose repo was never generated → 404) aborted the\noperation for every remaining repo. Bec\n[…]\ns the web's\nlive log show every repo instead of stopping at the first gap.\n\ndelete is unaffected (each repo is independent there).\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "fix(gitlab): skip missing repos instead of aborting setaccess/protect…",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-18T19:24:02Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "afaf916cb86a69a6d61d88a2b83e934dd14ceac3",
"body": "… (Milestone C) (#108)\n\nfeat(web): runOp — run a planned op with exclusive lock and streaming\n\nSecond step of Milestone C: the apply half. runOp(token, confirmPhrase) is a\nsubscription that validates a plan token and runs the mutating GitLab operation\n(setaccess/protect/archive/delete) it describes,\n[…]\nnfig changed, seeder, destructive confirm phrase, missing token).\n\nFollow-ups: the audit log (web/db) and the GUI plan/apply flow.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): runOp — run a planned op with exclusive lock and streaming…",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-18T18:24:46Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "393ce291cc61d2c56c7b06143ab7024341eb1b0e",
"body": "…tone C) (#107)\n\nfeat(web): planOp — preview a mutating op with a confirm token\n\nFirst step of Milestone C (mutating GitLab ops: setaccess/protect/archive/delete).\nThis is the plan half of a Terraform-style plan/apply: a token-free preview, no\nGitLab call yet.\n\nplanOp(op, course, assignment, params,\n[…]\nete,\n unknown op, no sealer, missing assignment).\n\nrunOp (exclusive lock + WithoutCancel + streaming + audit) and the GUI follow.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): planOp — preview a mutating op with a confirm token (Miles…",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-18T17:52:53Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "44dd0314e15b83dc19d9c8d0e375acf98a44040d",
"body": "…ration files",
"is_bot": false,
"headline": "chore: update .gitignore to include additional file types and configu…",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-18T17:43:24Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "398dcb7317f5e732cd66642a651900b29ad5aafa",
"body": "…106)\n\nThe report/check streams restarted from the beginning several times on long runs:\ngraphql-ws speaks the graphql-transport-ws subprotocol, for which gqlgen's\nKeepAlivePingInterval is a no-op (it only covers the legacy apollo subprotocol).\nWith no keepalive, a long stream with gaps between prog\n[…]\n, the correct keepalive for graphql-transport-ws, so the\nserver emits an unsolicited pong every 10s and the connection stays warm.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "fix(web): keep report/check subscriptions alive (PongOnlyInterval) (#…",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-18T17:30:26Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "26419d8eac846efcf67f523586c4347a8b24a58b",
"body": "…#105)\n\nThe last read-only Milestone-B op: verify a course roster against GitLab (which\nstudents/groups resolve, which need inviting, who is in more than one group).\nReuses the token-decrypt path and the streaming reporter from the report.\n\n- gitlab: CheckResult/StudentCheck/GroupCheck/DuplicateChec\n[…]\napper.\n- tests: CheckCourseData classification (contract, httptest GitLab) and the app\n error paths (no token, course not found).\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): add courseCheck query + courseCheckProgress subscription (…",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-18T16:41:39Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "ef3b410f09a097ba029865271fc8852e7137523c",
"body": "Long reports made the GUI look frozen: the report is a slow GitLab fetch and\nnothing was shown while it ran. Add live progress over a WebSocket subscription.\n\n- server: add the WebSocket transport (gqlgen + coder/websocket). The auth\n middleware runs on the HTTP upgrade, so the injected identity is\n[…]\nress type + resolver.\n- tests: the offline stream paths (missing/abstract → done with no report; no\n token → done with an error).\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): stream report progress via a GraphQL subscription (#104)",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-18T16:10:30Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "853258d6fcc071ebc9a44e5575174bef733483e6",
"body": "Add importAssignmentYAML(course, yaml): upsert one assignment into an existing\ncourse from a YAML snippet in the same keyed form it has in a course file (the\nsingle top-level key is the assignment name), so a block can be copy-pasted\nstraight out of a course YAML instead of re-importing the whole co\n[…]\nsignmentYAML mutation + resolver.\n- tests: upsert keeps existing assignments; rejects multiple top-level keys and\n invalid names.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): import a single assignment from YAML (#103)",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-18T15:42:45Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "f06fc9c1b3bf5294eded970daff76223dd58a4d6",
"body": "Second Milestone-B operation: a live report over an assignment's repositories,\nfetched from GitLab with the caller's stored PAT. This introduces the first\nplace in glabs-web where the sealed token is decrypted and used.\n\n- app.gitlabClientFor: the single decrypt site — GetUserSecret → Sealer.Open →\n\n[…]\n (no token, no sealer, missing/abstract) and the\n report mapper. The live GitLab path stays covered by the gitlab contract tests.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): add assignmentReport query (first token-decrypt path) (#102)",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-18T15:17:56Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "93cef4d848c9eef0cf314ef1cfbf896e298537ea",
"body": "…#101)\n\nFirst read-only GitLab-adjacent operation for glabs-web (Milestone B). It is\nderived purely from the resolved course config — no GitLab token or API call —\nso it is fully testable and safe to expose before the token-backed operations.\n\n- config: extract RepoURLs() (For + URL per student/grou\n[…]\nl types + assignmentUrls query, resolver, mapping.\n- tests for RepoURLs (config) and AssignmentURLs (app, incl. abstract/missing).\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): add assignmentUrls query for per-student/group repo URLs (…",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-18T14:26:02Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "8810d0814f24bc4bc0b5e1c3bbf40a2ba5ded51f",
"body": "…hema (#100)\n\nThe most nested block: approval settings (tri-state) plus an approval-rules list,\nall in the same flat draft under approvals.*.\n\n- approvalSettingsSchema: the four prevent/require toggles as tri-state ENUMs\n (empty option = unset → *bool nil) plus whenCommitAdded as an ENUM with the\n \n[…]\n(both tags), golangci-lint, and an isolated live check — the persisted YAML nests\nmergeRequest.approvals.settings/rules correctly.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): mergeRequest approvals (settings + rules) in the editor sc…",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-18T11:55:09Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "43d9c8a6471c026f2d644f384ead1275c4d48eda",
"body": "The branches list (a list of branch rules) rides in the same flat draft as the\nscalar fields, via indexed keys, so live validation / preview / save all work.\n\n- branchRuleSchema query: field metadata for one branch rule (name + protect,\n mergeOnly, default, allowForcePush, codeOwnerApprovalRequired\n[…]\n, golangci-lint, and an isolated live check (branchRuleSchema\nlists the 6 fields; setting two branches persists via indexed keys).\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): branches repeat-group in the assignment editor schema (#99)",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-18T11:38:00Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "f3876d669d388510851f5c42f14289d343f39267",
"body": "Backend for the roster editor. The Course type now exposes students and groups,\nand two replace-mutations set them (the GUI merges additively before calling).\n\n- Course.students: [String!]! and Course.groups: [Group!]! (sorted).\n- setCourseStudents(name, students): replaces the course students; trim\n[…]\n vet (both tags), golangci-lint, and an isolated live check (students/groups\nround-trip; the downloaded YAML reflects the roster).\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): course-level students and groups (roster) (#98)",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-18T11:08:30Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "4864ad6e88dac3ee13c25e342e12ce72e57e5206",
"body": "Third nested block, backend-only (the GUI renders it generically).\n\n- assignmentSchema adds the Issues group: replicateFromStartercode (bool),\n issueNumbers (a comma-separated integer list), includeChildTasks (bool) — each\n with a short German description.\n- assignmentOwn emits the issues.* values\n[…]\n,\ngo vet (both tags), golangci-lint, and an isolated live check (schema exposes the\nIssues group; own reflects the source values).\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): issues fields in the assignment editor schema (#97)",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-18T10:48:17Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "dfab413930748a92d11f98f86a93ded595d79f56",
"body": "Closes the gap that course settings could only be set at creation time.\n\n- Course type exposes useCoursenameAsPrefix and useEmailDomainAsSuffix (the\n latter defaults to true when absent), so the GUI can show and pre-fill them.\n- Mutation setCourse(name, coursePath, semesterPath, useCoursenameAsPref\n[…]\nound), go vet (both tags), golangci-lint, and live against\nglabs-web (setCourse changes paths + toggles; the assignment survives).\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): edit course-level settings after creation (#96)",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-18T10:20:17Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "a9c7edab9d22e234f71948c6d70406dc5dfa572e",
"body": "Completes \"Config geführt anlegen\": a course can now be built entirely in the\nweb, without importing a YAML.\n\n- Mutation createCourse(name, coursePath, semesterPath, useCoursenameAsPrefix,\n useEmailDomainAsSuffix): creates a new empty course; fails if the caller\n already has one by that name. Name\n[…]\nand live against glabs-web: createCourse → upsert-create an\nassignment → the course YAML is built from scratch → deleteAssignment.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): create courses and assignments from scratch (#95)",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-18T10:03:24Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "47df5c2601b53fbe9640e646c4dadf93b2f562c4",
"body": "Second nested block, same pattern as startercode — and backend-only: the GUI\nrenders any group of enum/bool fields generically, so the Merge-Request section\nappears with no frontend change.\n\n- assignmentSchema adds the Merge-Request group: mergeMethod (enum\n merge/semi_linear/ff), squashOption (enu\n[…]\n, and live: the GUI from main auto-renders the Merge-Request\nsection with the mergeMethod/squashOption dropdowns; saving persists.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): mergeRequest fields in the assignment editor schema (#94)",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-18T09:51:49Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "151e17466efaa8333486f67b883eff2ecbbf9ff1",
"body": "First nested block for the guided editor. FieldMeta gains `group` so the GUI can\nrender sections; startercode fields use dotted keys (startercode.url, …).\n\n- assignmentSchema adds the Startercode group: url, fromBranch, tag, toBranch,\n template, templateMessage, additionalBranches (a STRINGLIST, co\n[…]\nags), golangci-lint, and live against glabs-web (schema lists the grouped\nfields; blatt01 own reflects the fixture's startercode).\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): startercode fields in the assignment editor schema (#93)",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-18T09:24:27Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "e166dd0b3ba4f9b13ab2cfb77def26564d41f1b0",
"body": "The editor's write path, validated by the real resolver so \"valid\" is truthful.\n\n- Query validateAssignmentDraft(course, name, draft): applies the draft to a\n copy of the course, runs it through the same ResolveAssignment the CLI uses,\n and returns ok/errors plus the resolved preview — without sav\n[…]\nainst\nglabs-web: a bad draft returns ok:false; a good draft previews live; setAssignment\npersists so courseYAML reflects the edit.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): validate and persist assignment drafts (#92)",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-18T09:07:21Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "9e04ccede8e0baa1f8eedcb688a824f62addf2c5",
"body": "…rm (#91)\n\nAssignmentView gains `own: [FieldValue!]!` — the assignment's own (source)\nfield values keyed by FieldMeta.key, stringified (booleans as true/false).\nThe GUI pre-fills the schema-driven editor form from these, while the resolved\npreview keeps showing the inherited/effective values.\n\nRefac\n[…]\nlint, and live against glabs-web (blatt01 own values:\nextends=defaults, description set, per/accesslevel empty because inherited).\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): expose an assignment's own source values for the editor fo…",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-18T08:49:38Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "eb8fa95339b916ecc09b0f4766cf7f4808edcc8a",
"body": "…(#90)\n\nBackend-Grundlage für den geführten Assignment-Editor im GUI. Der Server ist\ndie einzige Wahrheit für Beschreibungen, Dropdown-Werte und die aufgelöste Form.\n\n- config.ResolveAssignmentFromBytes: löst ein Assignment direkt aus den\n gespeicherten Kurs-Bytes auf (gleiche Vererbung/Legacy-/Low\n[…]\nide Tags), golangci-lint, und live gegen\nglabs-web (mpd.yaml: blatt01 löst mit geerbten Werten auf, defaults meldet\nresolveError).\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): assignment field schema and resolved-config preview query …",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-18T08:41:48Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "24f85627c5b3f1bc6cdde160cf235f9027bb4cea",
"body": "…he config (#89)\n\nThe archive split shipped in the previous fix used `archives.ids`, which\nonly exists in recent goreleaser. go-semantic-release bundles an older\nGoReleaser@dev that rejects it (\"field ids not found in type\nconfig.Archive\"), so the release hook failed before building and v3.3.1\nwas a\n[…]\nwants; recent goreleaser still accepts it (as a\ndeprecated alias), so it works on both. Verified with a full\n`release --snapshot`.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "fix(release): use archives.builds so the bundled goreleaser accepts t…",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-17T19:25:59Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "423051b924c6c4b1b30088da412a4defe502d806",
"body": "…per build (#88)\n\nSince glabs-web was added as a second build (linux-only) alongside glabs\n(all platforms), goreleaser bundled every binary of a build into one\narchive per platform, so linux archives held two binaries and the rest\nheld one. goreleaser v2 rejects that mismatch, so the archive step fa\n[…]\nGELOG.md\" release step left over after\nCHANGELOG.md was removed. Verified with `goreleaser check` and a full\n`release --snapshot`.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "fix(release): attach binaries again by splitting goreleaser archives …",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-17T19:14:53Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "d7abf17dcf173b8b024b63b6f1624415f5e9daa4",
"body": "Add a per-user GitLab personal access token, AES-256-GCM sealed at rest\nwith a KEK from secrets.key (never in the database or git). The token is\nwrite-only: setGitlabToken / removeGitlabToken mutate it and gitlabToken\nreports only whether one is stored and when it was last set — no query\nreturns the\n[…]\nn scopes to the authenticated principal,\nand without a configured KEK token storage fails closed rather than\npersisting plaintext.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): store per-user GitLab PAT encrypted at rest (#87)",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-17T18:59:19Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "0c9670a194ab7cfc0fe6c7a4febd84edf1de4571",
"body": "…per-user isolation (#85)\n\nThe config-editor core of Meilenstein A: a user uploads a course YAML, sees it\nparsed, downloads it back, and lints it — all scoped strictly to that user.\nGitLab operations and per-field editing build on this; a token is not needed.\n\nOwner isolation is the one property tha\n[…]\nn (name, paths, assignment names,\ncounts) — the full CourseSource stays in the store, with the YAML and lint on\ntheir own queries.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): course import, listing, YAML export and lint, with strict …",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-17T18:36:40Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "225bcedcc2dd5f3e68b4d000e07ca12543a06767",
"body": "Two bits of release/CI cruft.\n\nCHANGELOG.md was committed back to main by go-semantic-release on every release.\nThat push-back raced and failed when #83 and #84 merged seconds apart, leaving\nmain's changelog two releases behind while the tags and GitHub Releases were\nfine. The GitHub Releases page a\n[…]\nThe coverage job produced a number nobody gated on and a PR comment nobody read.\nDrop it and its entry in the release job's needs.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "ci: drop CHANGELOG.md and the coverage job (#86)",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-17T18:36:05Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "9ae4dc3e58bf8d7429e887ffd549bc9877c82446",
"body": "… (#83)\n\nFirst slice of Meilenstein A: a GraphQL server that authenticates and answers\n`me` and `serverInfo`. Course management, GitLab ops and scheduling build on this.\nIt is a second binary (./cmd/glabs-web) in the same module, sharing the CLI's\ncore packages and adding a web layer under web/.\n\nTh\n[…]\nMeilenstein C), not the skeleton.\n\ngoreleaser now builds both binaries; the CLI is untouched and its full test suite\nstill passes.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(web): add the glabs-web server skeleton — auth and me/serverInfo…",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-17T18:07:13Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "630f948ad9f7a252d7accc2d197aba152723c8d5",
"body": "The reporter refactor (#82) changed git.PrepareSourceRepo/Push to take a reporter\nand added a field to gitlab.Client, but the integration test — behind the\n`integration` build tag — was not updated, because `go test ./...` and\n`go vet ./...` do not compile tagged files. So it compiled fine locally a\n[…]\nead of surfacing on main. vet compiles\nthe files without running them, so it needs neither Docker nor the\nGLABS_RUN_GITLAB_TC env.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "fix(test): repair the integration test after the reporter refactor (#84)",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-17T18:05:50Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "8c4d2269ad5aad1956fb693a7351072cbe4445d4",
"body": "…rn errors (#82)\n\n* refactor(reporter): add the reporter package and route git/ and Show through it\n\nFirst half of introducing a reporter, the abstraction that lets the same\noperation code drive a terminal spinner in the CLI and a streamed log in the web\nserver. This commit lays the package down and\n[…]\nate was bigger than the abstraction replacing it.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\n\n---------\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "refactor(reporter): route CLI output through a reporter, methods retu…",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-17T17:30:12Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "cedebf5cbfe24554f4955d1e5aac748ff7f347ca",
"body": "…viper (#81)\n\nNewClient() read the host and token straight from the global viper config and\npanicked on failure. That is fine for a single-token CLI and impossible for the\nweb server, where each request acts as a different user with a different token —\na package-global token cannot serve them.\n\nNewC\n[…]\n are left for\nthe Reporter step, where a method's output and its termination get restructured\ntogether rather than piecemeal here.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "refactor(gitlab): construct the client via options instead of global …",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-17T16:12:02Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "507bed3ea31ab78f6d208cf00f9865bbfca18511",
"body": null,
"is_bot": true,
"headline": "update changelog for v3.0.0 [skip ci]",
"author_name": "github-actions[bot]",
"author_login": "github-actions[bot]",
"committed_at": "2026-07-17T15:39:03Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "b27a54fceeb5dff6c15ade0edebe31c2eb64c4d9",
"body": "glabs authenticated the API with a token and git with a separate SSH key\n(sshprivatekey). Now everything is the token, over HTTPS: one credential, one\ntransport, and — the reason it matters — the same in the CLI and the coming web\nserver, where a per-user SSH key would be a shared identity with acce\n[…]\n instance), and the module path is now\ngithub.com/obcode/glabs/v3 — reinstall with\n`go install github.com/obcode/glabs/v3@latest`.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(git)!: use the PAT over HTTPS for all git, drop SSH (#80)",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-17T15:34:44Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "af9d1e142e5e51d4899bdf372bebc4663bf83213",
"body": "…rse loading (#79)\n\nSwitches resolution to the pure resolver added in the previous commit and\ndeletes the viper-based loader. config/ no longer reads course files through\nviper at all: the ~63 viper.GetString(\"course.assignment.field\") calls are gone,\nand the only two viper reads left are gitlab.hos\n[…]\n of assignments. Their\ncoverage is unchanged; the differential test goes, having no second\nimplementation left to compare against.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "refactor(config): resolve from the source schema, drop viper from cou…",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-17T15:01:48Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "9913954aa58cfdbce8cd503cb3d2f0c3f198a932",
"body": "Resolution currently runs on global viper state and writes back to it while\nresolving `extends`, so a read path mutates a global registry and two courses\ncannot be resolved concurrently. Add ResolveAssignment as a pure function of its\ninputs, to be swapped in once it is proven equivalent.\n\nNothing c\n[…]\nt stands between it and a\nrequest handler — worth having under its own name and comment rather than buried\nin a seeder field read.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "refactor(config): add a pure resolver alongside the viper loader (#78)",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-17T14:05:17Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "4448dfcde76efc0588953febb15158ea9eaacb1e",
"body": null,
"is_bot": true,
"headline": "update changelog for v2.12.1 [skip ci]",
"author_name": "github-actions[bot]",
"author_login": "github-actions[bot]",
"committed_at": "2026-07-17T13:56:48Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "38af851f08dfe67be2e552367b3e7591fdf81206",
"body": "…e (#77)\n\n* fix(config): honour the approvalsRequired alias regardless of key case\n\n`approvalsRequired` was documented as an alias of `requiredApprovals` and did\nnothing. viper lowercases every key it loads, so by the time the alias table in\nnormalizeMergeRequestApprovalConfigKeys compared them, the\n[…]\n should not sit\nin a course file unnoticed again.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\n\n---------\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "fix(config): honour the approvalsRequired alias regardless of key cas…",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-17T13:52:24Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "431aceb30925905f431296c2a604857f71b3572c",
"body": null,
"is_bot": true,
"headline": "update changelog for v2.12.0 [skip ci]",
"author_name": "github-actions[bot]",
"author_login": "github-actions[bot]",
"committed_at": "2026-07-17T13:34:31Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "632c1dfc94e585286d7c6ef3de39621edf257a16",
"body": "glabs ignores unknown configuration keys silently, so a typo looks exactly like\na setting that works. Add `glabs config lint` to say so out loud, plus `fmt` and\n`migrate` — all three built on a typed model of the course file.\n\nRunning lint over the real course files finds two classes of dead config \n[…]\nt the camelCase\nspelling. Fix that in the viper path first, in its own commit, or the resolver\nswap stops being a zero-diff proof.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat(config): add typed source schema with config lint/fmt/migrate (#76)",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-17T13:30:17Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "2e74799a0160bbbe80f04f4afd37372a8ee252eb",
"body": "config/ called log.Fatal (= os.Exit(1)) on every failure, so it could only\never be used from a CLI: any library or server consumer would be killed\nmid-request by a config typo. Turn the 17 exit sites into errors and thread\nthem out through GetAssignmentConfig/GetCourseConfig; cmd/ is now the only\nla\n[…]\nhat\nintermediate again. It disappears for free there, when Resolve() becomes pure\nby construction. Documented in place until then.\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "refactor(config): return errors instead of exiting the process (#75)",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-17T12:59:56Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "6c62e3010d0dd45ce4f1c6f75a1be40708e5075b",
"body": "Add a golden-test net over GetAssignmentConfig so the upcoming rewrite of\nconfig loading (viper -> typed source schema) can be proven behaviour-\npreserving rather than merely compiling.\n\nFixtures in config/testdata/courses/:\n - mpd, vss, algdati, fundc, fun: structural copies of the real course\n \n[…]\nsilently yielding RequiredApprovals 0 instead\n of the configured value\n\nRefresh with: go test ./config/ -run TestGolden -update\n\nCo-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "test(config): pin resolved config with golden tests (#73)",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-07-17T12:57:46Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "656c95965108bc3dbeed30b2a91d9bc2436e9311",
"body": null,
"is_bot": true,
"headline": "update changelog for v2.11.2 [skip ci]",
"author_name": "github-actions[bot]",
"author_login": "github-actions[bot]",
"committed_at": "2026-06-25T15:12:34Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "5560ea55bf02275bef73c93e75186bc33660e3b0",
"body": null,
"is_bot": false,
"headline": "fix(config): lowercase and slugify group path segments to match GitLab",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-06-25T15:07:53Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "3df949503fbde96139752d528bc0a5d5f3270bea",
"body": null,
"is_bot": true,
"headline": "update changelog for v2.11.1 [skip ci]",
"author_name": "github-actions[bot]",
"author_login": "github-actions[bot]",
"committed_at": "2026-06-16T08:40:58Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "4e96cad1ea47822945df1223453539f6887382f3",
"body": "…d lookup",
"is_bot": false,
"headline": "fix(config): match GitLab project path slugification for repo URLs an…",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-06-16T08:35:55Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "244a59a87682339cfe44f07e3dc0a28184dd5953",
"body": null,
"is_bot": true,
"headline": "update changelog for v2.11.0 [skip ci]",
"author_name": "github-actions[bot]",
"author_login": "github-actions[bot]",
"committed_at": "2026-06-11T12:26:00Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "1725d0dd6f6cf8bae2952c76276fd2062028cb22",
"body": "…bases (#72)\n\n* feat(config): add assignment inheritance via 'extends' with abstract bases",
"is_bot": false,
"headline": "feat(config): add assignment inheritance via 'extends' with abstract …",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-06-11T12:21:25Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "8f60710969a0960c217eca1ebbc2f84c45ce1997",
"body": null,
"is_bot": true,
"headline": "update changelog for v2.10.0 [skip ci]",
"author_name": "github-actions[bot]",
"author_login": "github-actions[bot]",
"committed_at": "2026-06-11T06:49:34Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "8d756fabf311ae34dd947ef2b8a38413d1afdc9c",
"body": null,
"is_bot": false,
"headline": "feat(config): inherit assignment configuration via extends (#71)",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-06-11T06:45:04Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "a801be0f85298e5b621048276869ea24dcf1d18d",
"body": null,
"is_bot": false,
"headline": "chore: add CLAUDE.md",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-06-09T18:54:53Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "6f26b3c93ba932bfc7f49250976072e578bc17b4",
"body": "…-pull image",
"is_bot": false,
"headline": "ci(integration): add retry logic for GitLab container startup and pre…",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-06-02T10:55:44Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "f9a8e803774258b01af460cf7ee3194c9886573c",
"body": null,
"is_bot": true,
"headline": "update changelog for v2.9.0 [skip ci]",
"author_name": "github-actions[bot]",
"author_login": "github-actions[bot]",
"committed_at": "2026-06-02T10:29:38Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "0bb2177dff707ebb0daec831b928e6d1c3bbf62c",
"body": "…sitories (#70)\n\n* feat(startercode): add optional tag for startercode in generated repositories",
"is_bot": false,
"headline": "feat(startercode): add optional tag for startercode in generated repo…",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-06-02T10:25:15Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "b7faa7effc015bde7bd87d0e755805fda9939de8",
"body": null,
"is_bot": false,
"headline": "remove redundant documentation section and update format",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-05-31T07:31:17Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "fcab59913c485da6f5ee31e247e9aa66d66d71ea",
"body": null,
"is_bot": true,
"headline": "update changelog for v2.8.3 [skip ci]",
"author_name": "github-actions[bot]",
"author_login": "github-actions[bot]",
"committed_at": "2026-05-31T07:24:51Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "919946ef65899cb205e59844f3cd6a817959b2d4",
"body": null,
"is_bot": false,
"headline": "fix(changelog): simplify changelog update message format",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-05-31T07:20:33Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "85830e4d0564a8f9f2000c4a1a3b1dd28363e12c",
"body": null,
"is_bot": true,
"headline": "docs(changelog): update for v2.8.2 [skip ci]",
"author_name": "github-actions[bot]",
"author_login": "github-actions[bot]",
"committed_at": "2026-05-31T06:59:00Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "6c67ac226b1951cad22907505509a0bab2d55ea8",
"body": null,
"is_bot": false,
"headline": "fix(release): prepend changelog entries and restore history",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-05-31T06:54:41Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "feb507a37fb165b660b1756e1638731c205506b3",
"body": null,
"is_bot": true,
"headline": "docs(changelog): update for v2.8.1 [skip ci]",
"author_name": "github-actions[bot]",
"author_login": "github-actions[bot]",
"committed_at": "2026-05-31T06:51:31Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "1c0af52e565de9dbb71c8952e5e6993c45f3014c",
"body": null,
"is_bot": false,
"headline": "fix(release): persist changelog updates and enable changelog-file",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-05-31T06:45:36Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "155c55e2f902a930330e9e61bb76591d4b0aa76c",
"body": "…dency-Proxy",
"is_bot": false,
"headline": "feat: add 'addgroupguests' command to manage student access for Depen…",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-05-20T20:36:31Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "2e3dea67705bbc62f46f5b00ad269abedc01176e",
"body": "Co-authored-by: Copilot <copilot@github.com>",
"is_bot": false,
"headline": "feat: add support for including child tasks in issue replication (#69)",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-05-05T09:49:22Z",
"body_truncated": false,
"is_coding_agent": true
},
{
"oid": "0ff75c993f06c5835367912bd20f0a753bbdcd4c",
"body": null,
"is_bot": false,
"headline": "fix: encode also from branch in startercode url",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-05-01T09:42:58Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "642c1c21105399ca4d41d1eff9626170a689c9fb",
"body": "* feat: print semester url\n\n* feat: add startercode URL retrieval and refactor URL handling logic",
"is_bot": false,
"headline": "feat: show urls of course and startercode (#68)",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-05-01T09:19:55Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "bc6ee1ee8410b6dc2969faa27f683d23b69dacc7",
"body": "…nction signatures",
"is_bot": false,
"headline": "refactor: refactor source repo preparation and push logic with new fu…",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-05-01T08:00:46Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "38b1e4c2c88da001b00394931b1ce4ae0acf2e98",
"body": "* feat: add template and templateMessage fields to Startercode struct and update show function\n\n* feat: use startercode as template with one single commit",
"is_bot": false,
"headline": "feat: use startercode as template with one single commit (#67)",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-04-30T22:53:16Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "d6879d7988ed43c4aad359e08f9ef7f616eff678",
"body": "* fix: use committer name and email if set in glabs.yaml\n\n* docs: add instructions for setting committer name and email in glabs.yaml",
"is_bot": false,
"headline": "fix: use committer name and email if set in glabs.yaml (#66)",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-04-30T21:49:32Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "9e4ba64bb9626c0ba40ff2fcc40d7539e6bfdfc3",
"body": "- Implemented `push` command to push a specified deferred branch to student/group repositories.\n- Enhanced `GetAssignmentConfig` to include deferred branches configuration.\n- Updated `Show` method to display deferred branches in assignment configuration.\n- Created `Push` method in the GitLab client to manage pushing of deferred branches.\n- Refactored starter repository handling to a new `SourceRepo` type.\n- Updated related tests and documentation to reflect new functionality.",
"is_bot": false,
"headline": "feat: add push command to handle deferred branches (#65)",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-04-30T21:30:08Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "70f5aa3f4b84f90b70f2ede5151cd8e241218e21",
"body": null,
"is_bot": false,
"headline": "docs: add useEmailDomainAsSuffix to example",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-04-28T17:43:14Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "1cdb5789ee2f8b103e492a71012acd619bbe2404",
"body": null,
"is_bot": false,
"headline": "feat: add support for removing email domain suffix in repo naming (#64)",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-04-28T17:38:36Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "7486648b09cadf154511955a67cd80f3a63ea482",
"body": "…binaries\n\nCo-authored-by: Copilot <copilot@github.com>",
"is_bot": false,
"headline": "docs: enhance installation instructions with PATH setup and prebuilt …",
"author_name": "Oliver Braun",
"author_login": "obcode",
"committed_at": "2026-04-28T05:03:09Z",
"body_truncated": false,
"is_coding_agent": true
}
],
"releases_count": 100,
"commits_last_year": 125,
"latest_release_at": "2026-07-22T15:54:14Z",
"latest_release_tag": "v3.41.0",
"releases_from_tags": false,
"days_since_last_push": 1,
"active_weeks_last_year": 16,
"days_since_latest_release": 5,
"mean_days_between_releases": 0.3
},
"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": [
{
"name": "github.com/obcode/glabs/v3",
"exists": true,
"license": null,
"keywords": [],
"ecosystem": "go",
"matches_repo": true,
"registry_url": "https://pkg.go.dev/github.com/obcode/glabs/v3",
"is_deprecated": false,
"latest_version": "v3.41.0",
"repository_url": "https://github.com/obcode/glabs",
"versions_count": 49,
"total_downloads": null,
"dependents_count": null,
"deprecation_note": null,
"maintainers_count": null,
"monthly_downloads": null,
"first_published_at": null,
"latest_published_at": "2026-07-22T15:52:57Z",
"latest_version_yanked": null,
"days_since_latest_publish": 5
}
]
},
"popularity": {
"forks": 6,
"stars": 11,
"watchers": 2,
"fork_history": {
"days": [
{
"date": "2020-10-30",
"count": 1
},
{
"date": "2021-03-04",
"count": 1
},
{
"date": "2021-03-13",
"count": 1
},
{
"date": "2021-10-19",
"count": 1
},
{
"date": "2022-10-12",
"count": 1
},
{
"date": "2022-10-17",
"count": 1
}
],
"complete": true,
"collected": 6,
"total_forks": 6
},
"star_history": null,
"open_issues_and_prs": 0
},
"ai_readiness": {
"has_nix": false,
"example_dirs": [],
"has_llms_txt": false,
"has_dockerfile": true,
"has_mcp_signal": false,
"bootstrap_files": [],
"api_schema_files": [
"web/graph/activity.graphqls",
"web/graph/admin.graphqls",
"web/graph/assignments.graphqls",
"web/graph/check.graphqls",
"web/graph/courses.graphqls",
"web/graph/gitlab_token.graphqls",
"web/graph/op.graphqls",
"web/graph/report.graphqls",
"web/graph/repos.graphqls",
"web/graph/scheduling.graphqls",
"web/graph/schema.graphqls"
],
"has_devcontainer": false,
"typecheck_configs": [],
"toolchain_manifests": [
"go.mod"
],
"largest_source_bytes": 35175,
"source_files_sampled": 181,
"oversized_source_files": 0,
"agent_instruction_files": [
"CLAUDE.md"
],
"agent_instruction_max_bytes": 5968
},
"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": "github.com/99designs/gqlgen",
"manifest": "go.mod",
"ecosystem": "go",
"version_constraint": "v0.17.94"
},
{
"name": "github.com/ProtonMail/go-crypto",
"manifest": "go.mod",
"ecosystem": "go",
"version_constraint": "v1.4.1"
},
{
"name": "github.com/coder/websocket",
"manifest": "go.mod",
"ecosystem": "go",
"version_constraint": "v1.8.15"
},
{
"name": "github.com/go-chi/chi/v5",
"manifest": "go.mod",
"ecosystem": "go",
"version_constraint": "v5.3.1"
},
{
"name": "github.com/go-git/go-billy/v5",
"manifest": "go.mod",
"ecosystem": "go",
"version_constraint": "v5.9.1"
},
{
"name": "github.com/go-git/go-git/v5",
"manifest": "go.mod",
"ecosystem": "go",
"version_constraint": "v5.19.1"
},
{
"name": "github.com/go-viper/mapstructure/v2",
"manifest": "go.mod",
"ecosystem": "go",
"version_constraint": "v2.5.0"
},
{
"name": "github.com/google/go-cmp",
"manifest": "go.mod",
"ecosystem": "go",
"version_constraint": "v0.7.0"
},
{
"name": "github.com/gookit/color",
"manifest": "go.mod",
"ecosystem": "go",
"version_constraint": "v1.6.1"
},
{
"name": "github.com/logrusorgru/aurora",
"manifest": "go.mod",
"ecosystem": "go",
"version_constraint": "v2.0.3+incompatible"
},
{
"name": "github.com/logrusorgru/aurora/v4",
"manifest": "go.mod",
"ecosystem": "go",
"version_constraint": "v4.0.0"
},
{
"name": "github.com/mitchellh/go-homedir",
"manifest": "go.mod",
"ecosystem": "go",
"version_constraint": "v1.1.0"
},
{
"name": "github.com/rs/cors",
"manifest": "go.mod",
"ecosystem": "go",
"version_constraint": "v1.11.1"
},
{
"name": "github.com/rs/zerolog",
"manifest": "go.mod",
"ecosystem": "go",
"version_constraint": "v1.35.1"
},
{
"name": "github.com/russross/blackfriday/v2",
"manifest": "go.mod",
"ecosystem": "go",
"version_constraint": "v2.1.0"
},
{
"name": "github.com/spf13/cobra",
"manifest": "go.mod",
"ecosystem": "go",
"version_constraint": "v1.10.2"
},
{
"name": "github.com/spf13/viper",
"manifest": "go.mod",
"ecosystem": "go",
"version_constraint": "v1.21.0"
},
{
"name": "github.com/testcontainers/testcontainers-go",
"manifest": "go.mod",
"ecosystem": "go",
"version_constraint": "v0.43.0"
},
{
"name": "github.com/theckman/yacspin",
"manifest": "go.mod",
"ecosystem": "go",
"version_constraint": "v0.13.12"
},
{
"name": "github.com/vektah/gqlparser/v2",
"manifest": "go.mod",
"ecosystem": "go",
"version_constraint": "v2.5.36"
},
{
"name": "github.com/wneessen/go-mail",
"manifest": "go.mod",
"ecosystem": "go",
"version_constraint": "v0.8.1"
},
{
"name": "gitlab.com/gitlab-org/api/client-go/v2",
"manifest": "go.mod",
"ecosystem": "go",
"version_constraint": "v2.51.0"
},
{
"name": "go.mongodb.org/mongo-driver/v2",
"manifest": "go.mod",
"ecosystem": "go",
"version_constraint": "v2.8.0"
},
{
"name": "golang.org/x/term",
"manifest": "go.mod",
"ecosystem": "go",
"version_constraint": "v0.45.0"
},
{
"name": "golang.org/x/text",
"manifest": "go.mod",
"ecosystem": "go",
"version_constraint": "v0.40.0"
},
{
"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": 0,
"merged_prs": 105,
"open_issues": 0,
"closed_ratio": 1,
"closed_issues": 26,
"closed_unmerged_prs": 4
},
"bus_factor": 1,
"bot_contributors": 2,
"top_contributors": [
{
"type": "User",
"login": "obcode",
"commits": 271,
"avatar_url": "https://avatars.githubusercontent.com/u/281558?v=4"
},
{
"type": "User",
"login": "fritterhoff",
"commits": 12,
"avatar_url": "https://avatars.githubusercontent.com/u/32478819?v=4"
},
{
"type": "User",
"login": "JohannesEbke",
"commits": 2,
"avatar_url": "https://avatars.githubusercontent.com/u/355473?v=4"
},
{
"type": "User",
"login": "wallento",
"commits": 2,
"avatar_url": "https://avatars.githubusercontent.com/u/299017?v=4"
},
{
"type": "User",
"login": "mgudemann",
"commits": 2,
"avatar_url": "https://avatars.githubusercontent.com/u/2590540?v=4"
},
{
"type": "User",
"login": "gitbutler-client",
"commits": 1,
"avatar_url": "https://avatars.githubusercontent.com/u/132921372?v=4"
}
],
"contributors_sampled": 6,
"top_contributor_share": 0.934
},
"quality_signals": {
"has_ci": true,
"has_tests": true,
"ci_workflows": [
"ci.yml",
"docker.yml"
],
"has_docs_dir": true,
"linter_configs": [],
"has_editorconfig": true,
"has_linter_config": true,
"has_precommit_config": true
},
"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": 10,
"reason": "26 out of 26 merged PRs checked by a CI test -- score normalized to 10",
"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/28 approved changesets -- score normalized to 0",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#code-review"
},
{
"name": "Contributors",
"score": 10,
"reason": "project has 5 contributing companies or organizations",
"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": 10,
"reason": "packaging workflow 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": "SAST tool is not run on all commits -- score normalized to 0",
"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": 7,
"reason": "3 existing vulnerabilities detected",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#vulnerabilities"
}
],
"commit": "de16f8abc0f9145977c73c25446504cce01edffa",
"ran_at": "2026-07-28T04:26:58Z",
"aggregate_score": 4.8,
"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-27T03:36:22Z",
"oldest_open_prs": [],
"last_merged_pr_at": "2026-07-26T08:46:59Z",
"ci_last_conclusion": "SUCCESS",
"oldest_open_issues": []
}
},
"config": {
"disabled_metrics": [],
"disabled_categories": [],
"disabled_components": {}
},
"source": {
"url": "https://github.com/obcode/glabs",
"host": "github.com",
"name": "glabs",
"owner": "obcode"
},
"metrics": {
"overall": {
"key": "overall",
"band": "moderate",
"name": "Overall health",
"note": null,
"notes": [],
"value": 66,
"inputs": {
"security": 48,
"vitality": 81,
"community": 35,
"governance": 64,
"engineering": 94
},
"components": []
},
"categories": [
{
"key": "vitality",
"band": "good",
"name": "Vitality",
"value": 81,
"weight": 0.22,
"metrics": [
{
"key": "development_activity",
"band": "good",
"name": "Development activity",
"note": null,
"notes": [],
"value": 75,
"inputs": {
"commits_last_year": 125,
"human_commit_share": 0.87,
"days_since_last_push": 1,
"active_weeks_last_year": 16
},
"components": [
{
"key": "push_recency",
"name": "Push recency",
"detail": "last push 1 days ago",
"points": 36,
"status": "met",
"details": [
{
"code": "push_recency",
"params": {
"days": 1
}
}
],
"max_points": 36
},
{
"key": "commit_cadence",
"name": "Commit cadence",
"detail": "16/52 weeks with commits",
"points": 11.1,
"status": "partial",
"details": [
{
"code": "commit_cadence_weeks",
"params": {
"weeks": 16
}
}
],
"max_points": 36
},
{
"key": "commit_volume",
"name": "Commit volume",
"detail": "125 commits in the last year",
"points": 18,
"status": "met",
"details": [
{
"code": "commits_last_year",
"params": {
"count": 125
}
}
],
"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": 100,
"latest_release_tag": "v3.41.0",
"releases_from_tags": false,
"days_since_latest_release": 5,
"mean_days_between_releases": 0.3
},
"components": [
{
"key": "ships_releases",
"name": "Ships releases",
"detail": "100 releases published",
"points": 27,
"status": "met",
"details": [
{
"code": "releases_published",
"params": {
"count": 100
}
}
],
"max_points": 27
},
{
"key": "release_recency",
"name": "Release recency",
"detail": "latest release 5 days ago",
"points": 36,
"status": "met",
"details": [
{
"code": "release_recency",
"params": {
"days": 5
}
}
],
"max_points": 36
},
{
"key": "release_cadence",
"name": "Release cadence",
"detail": "a release every ~0.3 days",
"points": 27,
"status": "met",
"details": [
{
"code": "release_cadence",
"params": {
"gap": 0.3
}
}
],
"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": "maintained",
"guards": [],
"signals": [],
"red_flag": false,
"multiplier_pct": 100,
"declared_reason": null,
"unverified_reason": null,
"unanswered_open_prs": null,
"unanswered_open_issues": null,
"days_since_last_merged_pr": null,
"days_since_last_human_commit": 1,
"days_since_last_human_commit_is_floor": false
},
"components": [
{
"key": "project_is_still_maintained",
"name": "Project is still maintained",
"detail": "last human commit 1 days ago",
"points": 100,
"status": "met",
"details": [
{
"code": "abandonment_maintained",
"params": {
"days": 1
}
}
],
"max_points": 100
}
]
}
],
"description": "Is the project alive — is code being written and are releases shipping?"
},
{
"key": "community",
"band": "at_risk",
"name": "Community & Adoption",
"value": 35,
"weight": 0.18,
"metrics": [
{
"key": "popularity",
"band": "critical",
"name": "Popularity & adoption",
"note": null,
"notes": [],
"value": 22,
"inputs": {
"forks": 6,
"stars": 11,
"watchers": 2,
"growth_state": "unverified",
"growth_factor_pct": 100,
"growth_unverified_reason": "no_history"
},
"components": [
{
"key": "stars",
"name": "Stars",
"detail": "11 stars",
"points": 16.2,
"status": "partial",
"details": [
{
"code": "stars",
"params": {
"count": 11
}
}
],
"max_points": 60
},
{
"key": "forks",
"name": "Forks",
"detail": "6 forks",
"points": 5.8,
"status": "partial",
"details": [
{
"code": "forks",
"params": {
"count": 6
}
}
],
"max_points": 25
},
{
"key": "watchers",
"name": "Watchers",
"detail": "2 watchers",
"points": 0,
"status": "missed",
"details": [
{
"code": "watchers",
"params": {
"count": 2
}
}
],
"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 (BSD-3-Clause)",
"points": 22.5,
"status": "met",
"details": [
{
"code": "license_standard",
"params": {}
},
{
"code": "license_spdx",
"params": {
"spdx": "BSD-3-Clause"
}
}
],
"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": "moderate",
"name": "Sustainability & Governance",
"value": 64,
"weight": 0.24,
"metrics": [
{
"key": "maintainer_resilience",
"band": "critical",
"name": "Maintainer resilience (bus factor)",
"note": null,
"notes": [],
"value": 29,
"inputs": {
"bus_factor": 1,
"contributors_sampled": 6,
"top_contributor_share": 0.934
},
"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 93% of commits",
"points": 1.5,
"status": "partial",
"details": [
{
"code": "top_contributor_share",
"params": {
"share": 93
}
}
],
"max_points": 22.5
},
{
"key": "contributor_breadth",
"name": "Contributor breadth",
"detail": "6 contributors",
"points": 8.1,
"status": "partial",
"details": [
{
"code": "contributors_sampled",
"params": {
"count": 6
}
}
],
"max_points": 13.5
},
{
"key": "openssf_scorecard_contributors",
"name": "OpenSSF Scorecard: Contributors",
"detail": "project has 5 contributing companies or organizations",
"points": 10,
"status": "met",
"details": [],
"max_points": 10
}
]
},
{
"key": "responsiveness",
"band": "good",
"name": "Issue & PR responsiveness",
"note": null,
"notes": [],
"value": 84,
"inputs": {
"merged_prs": 105,
"open_issues": 0,
"closed_issues": 26,
"issue_closed_ratio": 1,
"closed_unmerged_prs": 4
},
"components": [
{
"key": "issue_resolution",
"name": "Issue resolution",
"detail": "100% of issues closed",
"points": 46.8,
"status": "met",
"details": [
{
"code": "issues_closed_share",
"params": {
"share": 100
}
}
],
"max_points": 46.75
},
{
"key": "pr_acceptance",
"name": "PR acceptance",
"detail": "105/109 decided PRs merged",
"points": 36.8,
"status": "partial",
"details": [
{
"code": "decided_prs_merged",
"params": {
"merged": 105,
"decided": 109
}
}
],
"max_points": 38.25
},
{
"key": "openssf_scorecard_code_review",
"name": "OpenSSF Scorecard: Code-Review",
"detail": "Found 0/28 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": 58,
"inputs": {
"followers": 54,
"owner_type": "User",
"is_verified": null,
"owner_login": "obcode",
"public_repos": 47,
"account_age_days": 5913
},
"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": "54 followers of obcode",
"points": 12.5,
"status": "partial",
"details": [
{
"code": "owner_followers",
"params": {
"count": 54,
"login": "obcode"
}
}
],
"max_points": 25
},
{
"key": "track_record",
"name": "Track record",
"detail": "47 public repos, account ~16 yr old",
"points": 24.2,
"status": "partial",
"details": [
{
"code": "public_repos",
"params": {
"count": 47
}
},
{
"code": "account_age_years",
"params": {
"years": 16
}
}
],
"max_points": 25
}
]
},
{
"key": "package_maintenance",
"band": "excellent",
"name": "Package maintenance",
"note": null,
"notes": [],
"value": 100,
"inputs": {
"packages": [
"github.com/obcode/glabs/v3"
],
"ecosystems": "go",
"any_deprecated": false,
"min_days_since_publish": 5
},
"components": [
{
"key": "published_resolvable",
"name": "Published & resolvable",
"detail": "1 package(s) on go",
"points": 25,
"status": "met",
"details": [
{
"code": "packages_published",
"params": {
"count": 1,
"ecosystems": "go"
}
}
],
"max_points": 25
},
{
"key": "publish_recency",
"name": "Publish recency",
"detail": "latest publish 5 days ago",
"points": 35,
"status": "met",
"details": [
{
"code": "publish_recency",
"params": {
"days": 5
}
}
],
"max_points": 35
},
{
"key": "version_history",
"name": "Version history",
"detail": "49 published versions",
"points": 20,
"status": "met",
"details": [
{
"code": "published_versions",
"params": {
"count": 49
}
}
],
"max_points": 20
},
{
"key": "not_deprecated",
"name": "Not deprecated",
"detail": "active, not deprecated or yanked",
"points": 20,
"status": "met",
"details": [
{
"code": "package_not_deprecated",
"params": {}
}
],
"max_points": 20
}
]
}
],
"description": "Will the project survive its people — bus factor, responsiveness, who backs it, and package upkeep?"
},
{
"key": "engineering",
"band": "excellent",
"name": "Engineering Quality",
"value": 94,
"weight": 0.2,
"metrics": [
{
"key": "engineering_practices",
"band": "excellent",
"name": "Engineering practices",
"note": null,
"notes": [],
"value": 100,
"inputs": {
"has_ci": true,
"has_tests": true,
"has_editorconfig": true,
"has_linter_config": true,
"has_precommit_config": true
},
"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": null,
"points": 16,
"status": "met",
"details": [],
"max_points": 16
},
{
"key": "pre_commit_hooks",
"name": "Pre-commit hooks",
"detail": null,
"points": 9.6,
"status": "met",
"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": "26 out of 26 merged PRs checked by a CI test -- score normalized to 10",
"points": 20,
"status": "met",
"details": [],
"max_points": 20
}
]
},
{
"key": "documentation",
"band": "excellent",
"name": "Documentation",
"note": null,
"notes": [],
"value": 85,
"inputs": {
"topics": [
"go",
"gitlab"
],
"has_wiki": true,
"homepage": null,
"has_readme": true,
"has_docs_dir": true,
"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": 25,
"status": "met",
"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": "2 topics",
"points": 10,
"status": "met",
"details": [
{
"code": "topics_count",
"params": {
"count": 2
}
}
],
"max_points": 10
},
{
"key": "wiki",
"name": "Wiki",
"detail": null,
"points": 10,
"status": "met",
"details": [],
"max_points": 10
}
]
}
],
"description": "Are baseline engineering and documentation practices in place?"
},
{
"key": "security",
"band": "at_risk",
"name": "Security",
"value": 48,
"weight": 0.16,
"metrics": [
{
"key": "security_posture",
"band": "at_risk",
"name": "Security posture",
"note": null,
"notes": [],
"value": 48,
"inputs": {
"source": "openssf_scorecard",
"checks_evaluated": 18,
"scorecard_version": "v5.5.0",
"checks_inconclusive": 0,
"scorecard_aggregate": 4.8
},
"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": "26 out of 26 merged PRs checked by a CI test -- score normalized to 10",
"points": 2.5,
"status": "met",
"details": [],
"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/28 approved changesets -- score normalized to 0",
"points": 0,
"status": "missed",
"details": [],
"max_points": 7.5
},
{
"key": "contributors",
"name": "Contributors",
"detail": "project has 5 contributing companies or organizations",
"points": 2.5,
"status": "met",
"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 detected",
"points": 5,
"status": "met",
"details": [],
"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": "SAST tool is not run on all commits -- score normalized to 0",
"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": "3 existing vulnerabilities detected",
"points": 5.2,
"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": 8
},
"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": 80,
"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.989,
"agent_instruction_files": [
"CLAUDE.md"
],
"agent_instruction_max_bytes": 5968
},
"components": [
{
"key": "agent_instructions",
"name": "Agent instructions",
"detail": "CLAUDE.md",
"points": 45,
"status": "met",
"details": [
{
"code": "file_list",
"params": {
"files": "CLAUDE.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": "86 of 87 human commits state their intent (structured subject or explanatory body)",
"points": 40,
"status": "met",
"details": [
{
"code": "legible_history",
"params": {
"legible": 86,
"sampled": 87
}
}
],
"max_points": 40
}
]
},
{
"key": "ai_verify_loop",
"band": "excellent",
"name": "Verify loop (build / test / typecheck)",
"note": null,
"notes": [],
"value": 85,
"inputs": {
"has_nix": false,
"has_tests": true,
"lockfiles": [
"go.sum"
],
"has_dockerfile": true,
"typed_language": true,
"bootstrap_files": [],
"has_devcontainer": false,
"has_linter_config": true,
"typecheck_configs": [],
"agent_commit_share": 0.65,
"toolchain_manifests": [
"go.mod"
],
"dependency_bot_commit_share": 0.02
},
"components": [
{
"key": "one_command_bootstrap",
"name": "One-command bootstrap",
"detail": "go.mod (toolchain convention, no task runner)",
"points": 12.6,
"status": "partial",
"details": [
{
"code": "toolchain_convention",
"params": {
"files": "go.mod"
}
}
],
"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": null,
"points": 11,
"status": "met",
"details": [],
"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": "Dockerfile, lockfile",
"points": 10,
"status": "met",
"details": [
{
"code": "file_list",
"params": {
"files": "Dockerfile, lockfile"
}
}
],
"max_points": 10
},
{
"key": "demonstrated_agent_practice",
"name": "Demonstrated agent practice",
"detail": "65 of the last 100 commits agent-authored or agent-credited",
"points": 10,
"status": "met",
"details": [
{
"code": "agent_authored_commits",
"params": {
"count": 65,
"sampled": 100
}
}
],
"max_points": 10
},
{
"key": "automated_maintenance",
"name": "Automated maintenance",
"detail": "2 of the last 100 commits are automated dependency updates",
"points": 8,
"status": "met",
"details": [
{
"code": "dependency_bot_commits",
"params": {
"count": 2,
"sampled": 100
}
}
],
"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": 35175,
"source_files_sampled": 181,
"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/181 source files over 60KB",
"points": 55,
"status": "met",
"details": [
{
"code": "oversized_source_files",
"params": {
"kb": 60,
"sampled": 181,
"oversized": 0
}
}
],
"max_points": 55
}
]
},
{
"key": "ai_interfaces",
"band": "at_risk",
"name": "Machine-readable interfaces",
"note": null,
"notes": [],
"value": 40,
"inputs": {
"example_dirs": [],
"has_mcp_signal": false,
"api_schema_files": [
"web/graph/activity.graphqls",
"web/graph/admin.graphqls",
"web/graph/assignments.graphqls",
"web/graph/check.graphqls",
"web/graph/courses.graphqls",
"web/graph/gitlab_token.graphqls",
"web/graph/op.graphqls",
"web/graph/report.graphqls",
"web/graph/repos.graphqls",
"web/graph/scheduling.graphqls",
"web/graph/schema.graphqls"
]
},
"components": [
{
"key": "api_schema_openapi_graphql_proto",
"name": "API schema (OpenAPI/GraphQL/proto)",
"detail": "web/graph/activity.graphqls, web/graph/admin.graphqls, web/graph/assignments.graphqls, web/graph/check.graphqls, web/graph/courses.graphqls, web/graph/gitlab_token.graphqls, web/graph/op.graphqls, web/graph/report.graphqls, web/graph/repos.graphqls, web/graph/scheduling.graphqls, web/graph/schema.graphqls",
"points": 40,
"status": "met",
"details": [
{
"code": "file_list",
"params": {
"files": "web/graph/activity.graphqls, web/graph/admin.graphqls, web/graph/assignments.graphqls, web/graph/check.graphqls, web/graph/courses.graphqls, web/graph/gitlab_token.graphqls, web/graph/op.graphqls, web/graph/report.graphqls, web/graph/repos.graphqls, web/graph/scheduling.graphqls, web/graph/schema.graphqls"
}
}
],
"max_points": 40
},
{
"key": "mcp_server",
"name": "MCP server",
"detail": null,
"points": 0,
"status": "missed",
"details": [],
"max_points": 20
},
{
"key": "runnable_examples",
"name": "Runnable examples",
"detail": null,
"points": 0,
"status": "missed",
"details": [],
"max_points": 40
}
]
}
],
"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": [
"Star history unavailable: GitHub GraphQL error: Resource not accessible by personal access token",
"GitHub dependency-graph SBOM unavailable (404); the dependency graph may be disabled for this repository"
],
"report_type": "repository",
"generated_at": "2026-07-28T04:27:15.789027Z",
"schema_version": "0.27.0",
"badge_url": "https://raw.githubusercontent.com/inspect-software/badges/main/v1/o/obcode/glabs.svg",
"full_name": "obcode/glabs",
"license_state": "standard",
"license_spdx": "BSD-3-Clause"
}