From b8ad801c1deffb46dd64c3f7bffcba09537b4035 Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Sun, 9 Aug 2026 16:59:34 +0200 Subject: [PATCH] feat(hub): Version + Build-Commit im Header und in /health (v0.11.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zwei Maschinen liessen sich nicht auseinanderhalten. Der Watchdog-Fix von 6fa9411 ging ohne Versions-Bump raus, also meldeten beide Hubs 0.10.2, waehrend nur einer den Fix hatte. Zusaetzlich faehrt der Hub aus dist/ — ein Neustart ohne Build zieht still den alten Code wieder hoch. Beides zusammen hat heute eine Diagnose in die falsche Richtung geschickt. - Version auf 0.11.0 (Feature + Fix seit 0.10.2). - version.ts liefert zusaetzlich buildRef(): den kurzen Commit, aus dem der laufende Code stammt. Bewusst LAZY und gecacht — VERSION wird von jedem CLI-Aufruf importiert, ein git-Aufruf beim Import wuerde Befehle verteuern, die ihn nie anzeigen. Bei Nicht-git-Installs faellt versionLabel() sauber auf die blosse Version zurueck. - Board-Header (appHeader) und der aeltere pageHeader zeigen "v0.11.0 · ", mit Tooltip warum die Nummer allein nicht reicht. - /health liefert buildRef, damit sich zwei Maschinen ueber die Leitung vergleichen lassen, ohne das Board zu oeffnen. 318 Tests gruen, tsc --noEmit sauber. Co-Authored-By: Claude Opus 5 (1M context) --- package.json | 2 +- src/core/services/presenceService.ts | 5 ++- src/server/ui-shared.ts | 10 +++++ src/version.ts | 64 +++++++++++++++++++++++++++- tests/health.test.ts | 4 ++ tests/ui-shared.test.ts | 20 +++++++++ 6 files changed, 102 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 271aa79..dedc223 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agenthub", - "version": "0.10.2", + "version": "0.11.0", "description": "Local coordination layer for AI coding agents", "type": "module", "main": "./dist/index.js", diff --git a/src/core/services/presenceService.ts b/src/core/services/presenceService.ts index 5b260f8..e461e39 100644 --- a/src/core/services/presenceService.ts +++ b/src/core/services/presenceService.ts @@ -2,7 +2,7 @@ import { listTasks } from './taskService.js'; import { listMessages } from './messageService.js'; import { listAsks } from './askService.js'; import { getRoster } from './rosterService.js'; -import { VERSION } from '../../version.js'; +import { VERSION, buildRef } from '../../version.js'; /** * Per-agent presence (lastSeen) + the /health report (TSK-0226). @@ -73,6 +73,8 @@ export interface AgentHealth { export interface HealthReport { status: 'ok'; version: string; + /** Short commit this hub runs from — '' for non-git installs. Compares machines. */ + buildRef: string; startedAt: string; uptimeSec: number; counts: { tasks: number; open: number; inProgress: number; review: number; unreadMessages: number; pendingAsks: number; oldestAskAgeSec?: number }; @@ -199,6 +201,7 @@ export function computeHealth(cwd: string, startedAtMs: number, now: number = Da return { status: 'ok', version: VERSION, + buildRef: buildRef(), startedAt: new Date(startedAtMs).toISOString(), uptimeSec: Math.max(0, Math.round((now - startedAtMs) / 1000)), counts: { diff --git a/src/server/ui-shared.ts b/src/server/ui-shared.ts index 490c85b..783b147 100644 --- a/src/server/ui-shared.ts +++ b/src/server/ui-shared.ts @@ -8,6 +8,7 @@ */ import { TaskStatus as TaskStatusSchema } from '../core/schema.js'; +import { versionLabel } from '../version.js'; type TaskStatus = 'open' | 'in_progress' | 'review' | 'done' | 'cancelled'; /** CSS variables block matching the AgentHub dark design spec. */ @@ -272,6 +273,7 @@ export function pageHeader(projectName: string, current: 'board' | 'team' | 'act ${mark}

AgentHub

${escapeHtml(projectName)} + ${escapeHtml(versionLabel())} + ${escapeHtml(versionLabel())} diff --git a/src/version.ts b/src/version.ts index 0c63845..d2f5ce2 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,2 +1,64 @@ +import { execFileSync } from 'node:child_process'; +import { existsSync, readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join, parse } from 'node:path'; + /** Single source of truth for the agenthub version (package.json, CLI, MCP, /health). */ -export const VERSION = '0.10.2'; +export const VERSION = '0.11.0'; + +/** Locate the agenthub install dir by walking up from this file. */ +function findPackageRoot(): string | undefined { + let dir = dirname(fileURLToPath(import.meta.url)); + const { root } = parse(dir); + for (;;) { + const pkgPath = join(dir, 'package.json'); + if (existsSync(pkgPath)) { + try { + const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as { name?: string }; + if (pkg.name === 'agenthub') return dir; + } catch { + /* malformed package.json — keep walking */ + } + } + if (dir === root) return undefined; + dir = dirname(dir); + } +} + +/** Memoised so repeated header renders never re-shell out. `''` once resolved empty. */ +let cachedBuildRef: string | undefined; + +/** + * Short commit the running code was built from — `''` for non-git installs. + * + * The version alone cannot tell two machines apart: a fix can ship without a + * version bump, and then both hubs report the same number while running + * different code. That happened on 2026-08-09 and sent a diagnosis down the + * wrong path, so every surface that shows the version shows this next to it. + * + * Lazy on purpose: `VERSION` is imported by every CLI invocation, and shelling + * out to git at import time would tax commands that never display it. + */ +export function buildRef(): string { + if (cachedBuildRef !== undefined) return cachedBuildRef; + cachedBuildRef = ''; + try { + const root = findPackageRoot(); + if (root) { + cachedBuildRef = execFileSync('git', ['rev-parse', '--short', 'HEAD'], { + cwd: root, + encoding: 'utf-8', + stdio: ['ignore', 'pipe', 'ignore'], + }).trim(); + } + } catch { + /* not a git checkout, or git unavailable — the version has to stand alone */ + } + return cachedBuildRef; +} + +/** `v0.11.0 · 6fa9411`, or just `v0.11.0` when the commit is unknown. */ +export function versionLabel(): string { + const ref = buildRef(); + return ref ? `v${VERSION} · ${ref}` : `v${VERSION}`; +} diff --git a/tests/health.test.ts b/tests/health.test.ts index 26249cc..754da96 100644 --- a/tests/health.test.ts +++ b/tests/health.test.ts @@ -44,6 +44,10 @@ describe('GET /health + lastSeen stamping (TSK-0226)', () => { const h = await getHealth(); expect(h.status).toBe('ok'); expect(h.version).toBe(VERSION); + // The version alone cannot tell two hubs apart — a fix can ship without a + // bump. /health therefore carries the commit, so machines are comparable + // over the wire without opening the board. + expect(typeof h.buildRef).toBe('string'); expect(typeof h.uptimeSec).toBe('number'); expect(h.uptimeSec).toBeGreaterThanOrEqual(0); expect(typeof h.startedAt).toBe('string'); diff --git a/tests/ui-shared.test.ts b/tests/ui-shared.test.ts index ed0a410..530ab35 100644 --- a/tests/ui-shared.test.ts +++ b/tests/ui-shared.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect } from 'vitest'; import { agentAvatar, designTokensCss, escapeHtml, liveTimerJs, statusPill, appHeader, appHeaderCss, appHeaderOnlyCss } from '../src/server/ui-shared.js'; +import { VERSION, buildRef, versionLabel } from '../src/version.js'; describe('ui-shared helpers', () => { it('designTokensCss returns the dark palette variables', () => { @@ -76,6 +77,25 @@ describe('appHeader (v2 shared header)', () => { expect(appHeaderOnlyCss()).toContain('.app-header'); expect(appHeaderCss()).toContain('.modal-backdrop'); }); + it('shows version + build commit in the header so two machines can be compared', () => { + // On 2026-08-09 a fix shipped without a version bump: both hubs reported + // 0.10.2 while running different code, and the mismatch was invisible. + // The header therefore carries the commit next to the version. + const html = appHeader('proj', 'board'); + expect(html).toContain(versionLabel()); + expect(html).toContain(`v${VERSION}`); + expect(appHeaderOnlyCss()).toContain('.app-header .b2-build'); + }); + + it('versionLabel falls back to the bare version when the commit is unknown', () => { + // Non-git installs must still render a header — never an empty badge. + const label = versionLabel(); + expect(label.startsWith(`v${VERSION}`)).toBe(true); + const ref = buildRef(); + if (ref) expect(label).toBe(`v${VERSION} · ${ref}`); + else expect(label).toBe(`v${VERSION}`); + }); + it('makes the mobile nav horizontally scrollable with 44px tap targets', () => { const css = appHeaderOnlyCss(); expect(css).toMatch(/@media \(max-width: 680px\)[\s\S]*\.app-header \.b2-nav \{[^}]*overflow-x: auto/);