Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ All notable changes to this project will be documented in this file.
- Project Hub Overview now opens with a compact Context card above the atlas instead of a header button. It links to Context when the Wiki index is fresh and to Health otherwise, so a stale or unavailable index no longer leads to a page that cannot load.

### Fixed
- `mex graph status` no longer prints placeholder zeros when immutable inspection is skipped (for example a stranded `graph.db-wal`). Text marks parse health, last index, and sources as not inspected, and `--json` adds an additive `inspected: false` flag while keeping the existing zero `parseHealth`/`changes` objects (#204).
- Agent population failures retain the real copyable manual prompt for retry or manual continuation. Integration pointer notes are visible as non-blocking guidance.
- Setup and Overview share the computer's contact preference so completing or skipping the invitation does not immediately trigger another request.

Expand Down
1 change: 1 addition & 0 deletions src/drift/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,7 @@ function unavailableGraphStatus(message: string): GraphStatus {
severity: "warning",
message,
}],
inspected: false,
};
}

Expand Down
87 changes: 85 additions & 2 deletions src/graph/__tests__/cli-graph.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,17 @@ import { mkdirSync, mkdtempSync, writeFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it, vi } from "vitest";
import type { GraphSourceChanges } from "../../team/contracts/graph.js";
import type { GraphParseHealth, GraphSourceChanges, GraphStatus } from "../../team/contracts/graph.js";
import { runGraphScope } from "../cli-agent.js";
import { formatGraphSourceChanges, runGraph, runGraphRefresh, runGraphRebuild } from "../cli-graph.js";
import {
formatGraphLastSuccessfulIndex,
formatGraphParseHealth,
formatGraphSourceChanges,
formatGraphStatusSources,
runGraph,
runGraphRefresh,
runGraphRebuild,
} from "../cli-graph.js";

function changes(overrides: Partial<GraphSourceChanges> = {}): GraphSourceChanges {
return {
Expand All @@ -21,7 +29,82 @@ function changes(overrides: Partial<GraphSourceChanges> = {}): GraphSourceChange
};
}

function parseHealth(overrides: Partial<GraphParseHealth> = {}): GraphParseHealth {
return {
total: 0,
ok: 0,
partial: 0,
failed: 0,
failedPaths: [],
failedPathsTruncated: false,
...overrides,
};
}

function status(overrides: Partial<GraphStatus> = {}): GraphStatus {
return {
status: "degraded",
observedAt: "2026-09-13T04:33:14.973Z",
currentRepo: {
branch: "main",
head: "df89810df4f5aaaaaaaaaaaaaaaaaaaaaaaaaaaa",
dirty: false,
observedAt: "2026-09-13T04:33:14.973Z",
},
lastSuccessfulIndexAt: null,
indexedAt: null,
indexedBranch: null,
indexedHead: null,
schemaVersion: null,
extractorVersion: null,
grammarVersion: null,
parseHealth: parseHealth(),
changes: changes(),
diagnostics: [],
...overrides,
};
}

describe("graph CLI status formatting", () => {
it("does not print placeholder zeros when immutable inspection was skipped", () => {
const skipped = status({
inspected: false,
diagnostics: [{
code: "GRAPH_INDEX_SIDECAR_ACTIVE",
severity: "warning",
message: "Graph maintenance or recovery is active (graph.db-wal); immutable inspection was skipped.",
}],
});

expect(formatGraphParseHealth(skipped)).toBe("Parse health: not inspected (graph.db-wal present)");
expect(formatGraphParseHealth(skipped)).not.toMatch(/\b0 ok\b/);
expect(formatGraphLastSuccessfulIndex(skipped)).toBe("Last successful index: not inspected");
expect(formatGraphLastSuccessfulIndex(skipped)).not.toContain("never");
expect(formatGraphStatusSources(skipped)).toBe("Sources: not inspected");
expect(formatGraphStatusSources(skipped)).not.toMatch(/0 changed/);
});

it("prints measured parse health for an inspected graph", () => {
const inspected = status({
status: "stale",
inspected: true,
lastSuccessfulIndexAt: "2026-09-13T04:33:14.973Z",
parseHealth: parseHealth({ total: 111, ok: 111 }),
changes: changes({ total: 1, deleted: ["src/gone.ts"] }),
});

expect(formatGraphParseHealth(inspected)).toBe("Parse health: 111 ok, 0 partial, 0 failed");
expect(formatGraphLastSuccessfulIndex(inspected)).toBe("Last successful index: 2026-09-13T04:33:14.973Z");
expect(formatGraphStatusSources(inspected)).toBe("Sources: 1 changed (0 added, 0 modified, 1 deleted)");
});

it("treats omitted inspected as measured so existing consumers stay valid", () => {
const measuredEmpty = status();
expect(formatGraphParseHealth(measuredEmpty)).toBe("Parse health: 0 ok, 0 partial, 0 failed");
expect(formatGraphLastSuccessfulIndex(measuredEmpty)).toBe("Last successful index: never");
expect(formatGraphStatusSources(measuredEmpty)).toBe("Sources: 0 changed (0 added, 0 modified, 0 deleted)");
});

it("labels bounded path arrays as shown instead of an exact breakdown", () => {
const rendered = formatGraphSourceChanges(changes({
total: 125,
Expand Down
60 changes: 57 additions & 3 deletions src/graph/__tests__/status.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ import {
serializeGraphSnapshot,
type GraphSnapshot,
} from "../snapshot.js";
import {
formatGraphLastSuccessfulIndex,
formatGraphParseHealth,
formatGraphStatusSources,
} from "../cli-graph.js";
import {
inspectGraphSidecars,
inspectGraphStatus,
Expand Down Expand Up @@ -156,6 +161,7 @@ describe("inspectGraphStatus", () => {

expect(status).toMatchObject({
status: "missing",
inspected: true,
observedAt: NOW.toISOString(),
currentRepo: { branch: null, head: null, dirty: false, observedAt: NOW.toISOString() },
schemaVersion: null,
Expand Down Expand Up @@ -277,8 +283,12 @@ describe("inspectGraphStatus", () => {
const status = await inspect(root);

expect(status.status).toBe("fresh");
expect(status.inspected).toBe(true);
expect(status.schemaVersion).toBe(DB_SCHEMA_VERSION);
expect(status.parseHealth).toMatchObject({ total: 1, ok: 1, partial: 0, failed: 0 });
expect(formatGraphParseHealth(status)).toBe("Parse health: 1 ok, 0 partial, 0 failed");
expect(formatGraphLastSuccessfulIndex(status)).toBe(`Last successful index: ${status.lastSuccessfulIndexAt}`);
expect(formatGraphStatusSources(status)).toBe("Sources: 0 changed (0 added, 0 modified, 0 deleted)");
expect(status.changes).toMatchObject({
total: 0,
added: [],
Expand Down Expand Up @@ -524,6 +534,15 @@ describe("inspectGraphStatus", () => {
expect(statSync(`${dbPath}-wal`).size).toBeGreaterThan(0);
const transient = await inspect(transientRoot);
expect(transient.status).toBe("degraded");
expect(transient.inspected).toBe(false);
expect(transient.parseHealth).toEqual({
total: 0,
ok: 0,
partial: 0,
failed: 0,
failedPaths: [],
failedPathsTruncated: false,
});
expect(transient.changes.total).toBe(0);
expect(transient.diagnostics).toContainEqual(expect.objectContaining({ code: "GRAPH_INDEX_SIDECAR_ACTIVE" }));
expect(transient.diagnostics).not.toContainEqual(expect.objectContaining({ code: "GRAPH_INDEX_CORRUPT" }));
Expand All @@ -533,6 +552,40 @@ describe("inspectGraphStatus", () => {
}
});

it("does not treat a stranded WAL sidecar as a measured empty graph", async () => {
const root = temporaryRoot("mex-graph-stranded-wal-");
const dbPath = join(root, ".mex", "graph.db");
mkdirSync(dirname(dbPath), { recursive: true });
writeFileSync(dbPath, "untouched store bytes");
writeFileSync(`${dbPath}-wal`, "stranded wal");

const status = await inspect(root);

expect(status).toMatchObject({
status: "degraded",
inspected: false,
lastSuccessfulIndexAt: null,
schemaVersion: null,
});
expect(status.parseHealth).toEqual({
total: 0,
ok: 0,
partial: 0,
failed: 0,
failedPaths: [],
failedPathsTruncated: false,
});
expect(status.changes.total).toBe(0);
expect(status.diagnostics).toContainEqual(expect.objectContaining({
code: "GRAPH_INDEX_SIDECAR_ACTIVE",
message: expect.stringContaining("graph.db-wal"),
}));
expect(formatGraphParseHealth(status)).toBe("Parse health: not inspected (graph.db-wal present)");
expect(formatGraphParseHealth(status)).not.toMatch(/\b0 ok\b/);
expect(formatGraphLastSuccessfulIndex(status)).toBe("Last successful index: not inspected");
expect(formatGraphStatusSources(status)).toBe("Sources: not inspected");
});

it("reports sidecars deterministically and refuses immutable interpretation while one is active or unavailable", async () => {
const root = temporaryRoot("mex-graph-sidecar-probe-");
const dbPath = join(root, ".mex", "graph.db");
Expand All @@ -546,7 +599,7 @@ describe("inspectGraphStatus", () => {
paths: ["graph.db-journal"],
});
const active = await inspect(root);
expect(active).toMatchObject({ status: "degraded", schemaVersion: null });
expect(active).toMatchObject({ status: "degraded", schemaVersion: null, inspected: false });
expect(active.diagnostics).toContainEqual(expect.objectContaining({
code: "GRAPH_INDEX_SIDECAR_ACTIVE",
}));
Expand All @@ -567,7 +620,7 @@ describe("inspectGraphStatus", () => {
paths: ["graph.db-wal"],
});
const unavailable = await inspect(root);
expect(unavailable).toMatchObject({ status: "degraded", schemaVersion: null });
expect(unavailable).toMatchObject({ status: "degraded", schemaVersion: null, inspected: false });
expect(unavailable.diagnostics).toContainEqual(expect.objectContaining({
code: "GRAPH_INDEX_SIDECAR_UNAVAILABLE",
}));
Expand All @@ -588,7 +641,7 @@ describe("inspectGraphStatus", () => {
expect(statSync(`${dbPath}-journal`).size).toBeGreaterThan(0);

const status = await inspect(root);
expect(status).toMatchObject({ status: "degraded", schemaVersion: null });
expect(status).toMatchObject({ status: "degraded", schemaVersion: null, inspected: false });
expect(status.diagnostics).toContainEqual(expect.objectContaining({
code: "GRAPH_INDEX_SIDECAR_ACTIVE",
}));
Expand Down Expand Up @@ -1202,6 +1255,7 @@ describe("inspectGraphStatus", () => {
now: NOW,
});
expect(lexical.status).toBe("degraded");
expect(lexical.inspected).toBe(false);
expect(lexical.diagnostics).toContainEqual(expect.objectContaining({
code: "GRAPH_INDEX_PATH_OUTSIDE_PROJECT",
}));
Expand Down
53 changes: 46 additions & 7 deletions src/graph/cli-graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,15 +181,11 @@ const MAX_DIAGNOSTICS_SHOWN = 20;
function printStatus(status: GraphStatus): void {
const branch = status.currentRepo.branch ?? "detached/no branch";
const head = status.currentRepo.head?.slice(0, 12) ?? "no HEAD";
const changes = status.changes;
console.log(`Graph status: ${status.status}`);
console.log(`Repository: ${branch} @ ${head}${status.currentRepo.dirty ? " (dirty)" : ""}`);
console.log(`Last successful index: ${status.lastSuccessfulIndexAt ?? "never"}`);
console.log(formatGraphSourceChanges(changes));
console.log(
`Parse health: ${status.parseHealth.ok} ok, ${status.parseHealth.partial} partial, `
+ `${status.parseHealth.failed} failed`,
);
console.log(formatGraphLastSuccessfulIndex(status));
console.log(formatGraphStatusSources(status));
console.log(formatGraphParseHealth(status));
for (const diagnostic of status.diagnostics) {
console.log(`${diagnostic.severity.toUpperCase()} ${diagnostic.code}: ${diagnostic.message}`);
}
Expand All @@ -199,6 +195,49 @@ function printStatus(status: GraphStatus): void {
if (command) console.log(`Next: ${command}`);
}

function graphWasInspected(status: GraphStatus): boolean {
return status.inspected !== false;
}

/** Internal human-output formatter; JSON output retains the complete contract. */
export function formatGraphLastSuccessfulIndex(status: GraphStatus): string {
if (!graphWasInspected(status)) return "Last successful index: not inspected";
return `Last successful index: ${status.lastSuccessfulIndexAt ?? "never"}`;
}

/** Internal human-output formatter; JSON output retains the complete contract. */
export function formatGraphStatusSources(status: GraphStatus): string {
if (!graphWasInspected(status)) return "Sources: not inspected";
return formatGraphSourceChanges(status.changes);
}

/** Internal human-output formatter; JSON output retains the complete contract. */
export function formatGraphParseHealth(status: GraphStatus): string {
if (!graphWasInspected(status)) {
const detail = uninspectedParseHealthDetail(status);
return detail ? `Parse health: not inspected (${detail})` : "Parse health: not inspected";
}
return `Parse health: ${status.parseHealth.ok} ok, ${status.parseHealth.partial} partial, `
+ `${status.parseHealth.failed} failed`;
}

function uninspectedParseHealthDetail(status: GraphStatus): string | undefined {
for (const diagnostic of status.diagnostics) {
if (
diagnostic.code !== "GRAPH_INDEX_SIDECAR_ACTIVE"
&& diagnostic.code !== "GRAPH_INDEX_SIDECAR_UNAVAILABLE"
) {
continue;
}
const listed = /\(([^)]+)\)/.exec(diagnostic.message)?.[1];
if (!listed) continue;
return diagnostic.code === "GRAPH_INDEX_SIDECAR_ACTIVE"
? `${listed} present`
: `${listed} unavailable`;
}
return undefined;
}

/** Internal human-output formatter; JSON output retains the complete contract. */
export function formatGraphSourceChanges(changes: GraphSourceChanges): string {
if (changes.truncated) {
Expand Down
Loading