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/);