Skip to content

Commit 7b731ec

Browse files
omercangumusCopilotbrady gaster
authored
Add machine-readable health metadata (#2384)
* Expose structured health metadata (#2357) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: report resolved health metadata Part of #2357 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix: use project backend in health metadata Part of #2357 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix: match runtime backend resolution Part of #2357 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: brady gaster <bradygaster@github.com>
1 parent c047a05 commit 7b731ec

8 files changed

Lines changed: 236 additions & 14 deletions

File tree

‎.changeset/blue-health-metadata.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@bradygaster/squad-cli': patch
3+
'@bradygaster/squad-sdk': patch
4+
---
5+
6+
Expose versioned runtime and state backend metadata in health reports.

‎docs/src/content/docs/reference/cli.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ prerequisites, all supported methods, and update commands.
4040
| `squad start [--tunnel] [--port N] [--command cmd]` | Start Copilot with remote phone access via PTY and WebSocket | No |
4141
| `squad status` | Show which squad is active and why | Yes |
4242
| `squad doctor` | Validate squad setup integrity and diagnose issues (alias: `heartbeat`) | Yes |
43+
| `squad health [--json]` | Check team readiness; JSON includes the versioned runtime, state backend, and resolved team metadata | Yes |
4344
| `squad upgrade` | Upgrade Squad-owned files to latest version; locally customized files are saved as `<file>.local-backup` | Yes |
4445
| `squad upgrade --client <list>` | Add hosts to the configured set and refresh every configured host projection; user settings in `.mcp.json`, `.kiro/settings/mcp.json`, `opencode.json`, `.cursor/mcp.json` and `.codex/config.toml` are preserved | Yes |
4546
| `squad upgrade --state-backend <type>` | Migrate state backend (`orphan`, `two-layer`); installs git hooks automatically | Yes |
@@ -82,6 +83,10 @@ prerequisites, all supported methods, and update commands.
8283
| `squad state-mcp [--experimental-backend]` | Run the `squad_state` stdio MCP server; pass `--experimental-backend` (or `SQUAD_EXPERIMENTAL_BACKEND=1`) to also expose the 11 experimental `squad_backend_*` conversation tools (transcript/event-log only until a harness is registered) | Yes |
8384
| `squad --version` | Print installed version | No |
8485

86+
`squad health --json` preserves the `squad-health/v1` schema and includes `metadata` fields for `cliVersion`, `sdkVersion`, `stateBackend`, `resolvedTeamDir`, `bridgeVersion`, and `launchMode`. Human-readable output is unchanged.
87+
88+
The `squad_state_health` MCP tool includes the same fields in a JSON `Health metadata` line while retaining its existing state and storage lines.
89+
8590
### Remote Init Mode
8691

8792
Use `--mode remote` to link your project to a shared team root:

‎packages/squad-cli/src/cli/commands/health.ts‎

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { readdirSync } from 'node:fs';
99
import { parse as parseJsonc } from 'jsonc-parser';
1010
import {
1111
FSStorageProvider,
12+
VERSION as SDK_VERSION,
1213
resolveStateBackend,
1314
verifyStateBackend,
1415
type StateBackend,
@@ -21,6 +22,7 @@ import {
2122
parseTeamMarkdown,
2223
} from '@bradygaster/squad-sdk/parsers';
2324
import { effectiveSquadDir } from '../core/effective-squad-dir.js';
25+
import { getPackageVersion } from '../core/version.js';
2426

2527
const storage = new FSStorageProvider();
2628

@@ -43,6 +45,14 @@ export interface HealthCheckItem {
4345

4446
export interface HealthReport {
4547
schema: typeof HEALTH_SCHEMA;
48+
metadata: {
49+
cliVersion: string;
50+
sdkVersion: string;
51+
stateBackend: string;
52+
resolvedTeamDir: string | null;
53+
bridgeVersion: string;
54+
launchMode: 'cli';
55+
};
4656
/** `fail` when any check failed; informational items and warnings never fail the report. */
4757
status: 'pass' | 'fail';
4858
checks: HealthCheckItem[];
@@ -599,6 +609,44 @@ function checkStateBackend(
599609
}
600610
}
601611

612+
function resolvedTeamDirectory(stateDir: string): string {
613+
const resolved = path.resolve(stateDir);
614+
const basename = path.basename(resolved);
615+
return basename === '.squad' || basename === '.ai-team'
616+
? path.dirname(resolved)
617+
: resolved;
618+
}
619+
620+
function hasRuntimeBackendSetting(runtimeConfigDir: string): boolean {
621+
const configPath = path.join(runtimeConfigDir, 'config.json');
622+
if (!storage.existsSync(configPath)) return false;
623+
624+
try {
625+
const config = parseJsonObject(
626+
readRequiredFile(configPath, '.squad/config.json'),
627+
'.squad/config.json',
628+
);
629+
return config.stateBackend !== undefined && config.stateBackend !== null;
630+
} catch {
631+
return true;
632+
}
633+
}
634+
635+
function healthMetadata(
636+
stateDir: string | null,
637+
stateBackend: string,
638+
): HealthReport['metadata'] {
639+
const cliVersion = getPackageVersion();
640+
return {
641+
cliVersion,
642+
sdkVersion: SDK_VERSION,
643+
stateBackend,
644+
resolvedTeamDir: stateDir ? resolvedTeamDirectory(stateDir) : null,
645+
bridgeVersion: cliVersion,
646+
launchMode: 'cli',
647+
};
648+
}
649+
602650
const ENV_JSON_CONFIGS = [
603651
{ relativePath: '.mcp.json', serverKeys: ['mcpServers'] },
604652
{
@@ -816,18 +864,36 @@ function checkEnvVars(repoRoot: string): HealthCheckItem {
816864

817865
function buildHealthReport(
818866
stateDir: string,
819-
localSquadDir: string,
867+
readinessConfigDir: string,
868+
runtimeConfigDir: string,
820869
repoRoot: string,
821870
): HealthReport {
871+
const hasRuntimeBackend = hasRuntimeBackendSetting(runtimeConfigDir);
872+
let resolvedBackend: StateBackend | undefined;
873+
if (hasRuntimeBackend) {
874+
try {
875+
resolvedBackend = captureResolverOutput(() =>
876+
resolveStateBackend(runtimeConfigDir, repoRoot),
877+
);
878+
} catch {
879+
resolvedBackend = undefined;
880+
}
881+
}
822882
const checks: HealthCheckItem[] = [
823883
checkTeam(stateDir),
824884
checkRegistryAndCharters(stateDir),
825885
checkRouting(stateDir),
826-
checkStateBackend(localSquadDir, repoRoot),
886+
checkStateBackend(readinessConfigDir, repoRoot),
827887
checkEnvVars(repoRoot),
828888
];
829889
return {
830890
schema: HEALTH_SCHEMA,
891+
metadata: healthMetadata(
892+
stateDir,
893+
!hasRuntimeBackend
894+
? 'local'
895+
: resolvedBackend?.name ?? 'unknown',
896+
),
831897
status: checks.some((check) => check.status === 'fail') ? 'fail' : 'pass',
832898
checks,
833899
};
@@ -837,7 +903,7 @@ export function runSquadHealth(
837903
squadDir: string,
838904
repoRoot: string,
839905
): HealthReport {
840-
return buildHealthReport(squadDir, squadDir, repoRoot);
906+
return buildHealthReport(squadDir, squadDir, squadDir, repoRoot);
841907
}
842908

843909
function buildResolutionFailureReport(
@@ -862,7 +928,15 @@ function buildResolutionFailureReport(
862928
),
863929
checkEnvVars(repoRoot),
864930
];
865-
return { schema: HEALTH_SCHEMA, status: 'fail', checks };
931+
return {
932+
schema: HEALTH_SCHEMA,
933+
metadata: {
934+
...healthMetadata(null, 'unknown'),
935+
stateBackend: 'unknown',
936+
},
937+
status: 'fail',
938+
checks,
939+
};
866940
}
867941

868942
function printHuman(report: HealthReport): void {
@@ -888,6 +962,7 @@ export async function runHealthCommand(
888962
report = buildHealthReport(
889963
dirs.stateDir,
890964
dirs.backendConfigDir,
965+
dirs.local.path,
891966
repoRoot,
892967
);
893968
} catch (error) {

‎packages/squad-cli/src/cli/commands/state-mcp.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ export function createStateMcpToolRegistry(startDir: string): ToolRegistry {
8484
backendName: context.backend.name,
8585
upstreamDir: context.paths.projectDir,
8686
runtimeVersion: SERVER_INFO.version,
87+
cliVersion: SERVER_INFO.version,
88+
launchMode: 'stdio',
8789
});
8890
}
8991

‎packages/squad-sdk/src/tools/index.ts‎

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@ import { resolveUpstreams, buildEffectiveUpstreamContext, formatUpstreamReport }
2727
const tracer = trace.getTracer('squad-sdk');
2828
const require = createRequire(import.meta.url);
2929
const SQUAD_STATE_VERSION = (require('../../package.json') as { version?: string }).version ?? 'unknown';
30+
export const STATE_HEALTH_SCHEMA = 'squad-state-health/v1' as const;
31+
32+
function resolvedTeamDirectory(squadRoot: string): string {
33+
const resolved = path.resolve(squadRoot);
34+
const basename = path.basename(resolved);
35+
return basename === '.squad' || basename === '.ai-team'
36+
? path.dirname(resolved)
37+
: resolved;
38+
}
3039

3140
// --- Argument Sanitization ---
3241

@@ -385,6 +394,8 @@ export class ToolRegistry {
385394
private memoryStore: LocalMemoryStore;
386395
private backendName: string;
387396
private runtimeVersion: string;
397+
private cliVersion: string;
398+
private launchMode: string;
388399
/**
389400
* Where `upstream.json` and `_upstream_repos/` live. Upstream declarations are project-local:
390401
* `squad upstream add|sync` writes them beside the project's `.squad/config.json` and
@@ -400,7 +411,13 @@ export class ToolRegistry {
400411
state?: SquadState,
401412
fanOutDepsGetter?: () => FanOutDependencies | undefined,
402413
hostInjectedCopilotAdapterClient?: CopilotMemoryProviderClient,
403-
options: { backendName?: string; upstreamDir?: string; runtimeVersion?: string } = {},
414+
options: {
415+
backendName?: string;
416+
upstreamDir?: string;
417+
runtimeVersion?: string;
418+
cliVersion?: string;
419+
launchMode?: string;
420+
} = {},
404421
) {
405422
this.squadRoot = squadRoot;
406423
this.sessionPoolGetter = sessionPoolGetter;
@@ -410,6 +427,8 @@ export class ToolRegistry {
410427
// Plain FS storage is the `local` backend; adapters wrap a named git backend.
411428
this.backendName = options.backendName ?? (storage instanceof FSStorageProvider ? 'local' : 'unknown');
412429
this.runtimeVersion = options.runtimeVersion ?? SQUAD_STATE_VERSION;
430+
this.cliVersion = options.cliVersion ?? 'unknown';
431+
this.launchMode = options.launchMode ?? 'embedded';
413432
this.upstreamDir = options.upstreamDir ?? squadRoot;
414433
this.memoryStore = new LocalMemoryStore(storage, squadRoot, {
415434
rootKind: 'squad',
@@ -931,13 +950,35 @@ export class ToolRegistry {
931950

932951
const stateHealth = defineTool<Record<string, never>>({
933952
name: 'squad_state_health',
934-
description: 'Report the active Squad state backend (local, orphan, two-layer, ...) and storage layer so agents can verify they are using runtime-owned state instead of manual git/file choreography.',
953+
description: 'Report active Squad state backend and storage metadata, including a versioned JSON health payload for external coordinators.',
935954
parameters: { type: 'object', properties: {} },
936-
handler: async () => ({
937-
textResultForLlm: `squad-state ${this.runtimeVersion}\nState backend: ${this.backendName}\nState backend storage: ${this.storage.constructor.name}\nTeam squad dir: ${this.squadRoot}`,
938-
resultType: 'success',
939-
toolTelemetry: { version: this.runtimeVersion, backend: this.backendName, storage: this.storage.constructor.name },
940-
}),
955+
handler: async () => {
956+
const metadata = {
957+
schema: STATE_HEALTH_SCHEMA,
958+
cliVersion: this.cliVersion,
959+
sdkVersion: SQUAD_STATE_VERSION,
960+
stateBackend: this.backendName,
961+
resolvedTeamDir: resolvedTeamDirectory(this.squadRoot),
962+
bridgeVersion: this.runtimeVersion,
963+
launchMode: this.launchMode,
964+
};
965+
return {
966+
textResultForLlm: [
967+
`squad-state ${this.runtimeVersion}`,
968+
`Health metadata: ${JSON.stringify(metadata)}`,
969+
`State backend: ${this.backendName}`,
970+
`State backend storage: ${this.storage.constructor.name}`,
971+
`Team squad dir: ${this.squadRoot}`,
972+
].join('\n'),
973+
resultType: 'success',
974+
toolTelemetry: {
975+
version: this.runtimeVersion,
976+
backend: this.backendName,
977+
...metadata,
978+
storage: this.storage.constructor.name,
979+
},
980+
};
981+
},
941982
});
942983

943984
const stateUpstreams = defineTool<StateUpstreamsRequest>({

0 commit comments

Comments
 (0)