Skip to content
Merged
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
23 changes: 15 additions & 8 deletions docs/architecture/doctor-health-modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,10 @@ they must not appear in routine Doctor output or public JSON.

Every `jarvos doctor` profile also projects its core checks and selected
optional components into `systemDoctor`, a backward-compatible
`jarvos-system-doctor-report/v1` receipt. The text view preserves its existing
transcript and appends the receipt's selected components and final result.
`jarvos-system-doctor-report/v1` receipt. The text view is a compact scoreboard:
one icon and one line per component, with short section labels only when the
receipt contains more than one section. Degraded rows include a concise reason
and next action; the structured reason code remains available in JSON.
Existing top-level JSON fields remain available to older consumers.

A clean profile has no selected optional components unless an owner-side
Expand All @@ -101,7 +103,7 @@ of `healthy`, `warning`, `repair needed`, or `not configured`.
"observedAt": "2026-09-03T18:00:00.000Z",
"validUntil": "2026-09-04T18:00:00.000Z",
"trust": "trusted",
"factsVersion": "jarvos-system-doctor-facts/v1",
"factsVersion": "jarvos-system-doctor-facts/v2",
"facts": {
"profile": "minimal",
"components": [
Expand All @@ -126,12 +128,17 @@ the selected runtime's local-search tool. If the service responds while a real
search returns zero results or the runtime tool is absent, jarvOS downgrades
the component to `warning`.

When the system snapshot selects Memory, it must contain the existing ten
components in their fixed order: GBrain, Lossless Claw, QMD search,
When the system snapshot selects Memory, it must contain the existing eleven
components in their fixed order: GBrain core, GBrain semantic coverage,
Lossless Claw, QMD search,
memory-wiki, Notes & provenance, Recall evaluation, Scheduled maintenance,
Reviewed runtime, Automatic repair, and Telegram follow-up & proof. Partial or
reordered Memory rosters fail closed. The legacy aggregate `memory.json`
contract remains supported.
reordered Memory rosters fail closed. The eleven-row roster is versioned as
`jarvos-system-doctor-facts/v2`; v1 snapshots fail closed rather than being
mistaken for complete coverage. The legacy aggregate `memory.json` contract
remains supported when no v2 Memory roster is present. Once that roster is
present, it is authoritative; the aggregate is neither rendered nor part of
the eleven-component result.

## Consumer and ownership boundary

Expand All @@ -147,7 +154,7 @@ The private owner remains responsible for machine-specific probes, credentials,
Telegram identities, launchd bindings, repair procedures, scheduling, and
publishing the normalized snapshot. The existing Memory Doctor Telegram
message remains unchanged until the new projection is behaviorally proven to
retain all ten rows and at least the same operator information.
retain all eleven rows and at least the same operator information.

Documentation impact: module-docs. This producer contract is a public jarvOS
release candidate; private command declarations, scheduling, runtime
Expand Down
10 changes: 7 additions & 3 deletions lib/jarvos-cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -956,7 +956,11 @@ function runDoctor(options = {}) {
profile: profile.id,
}).modules;
const continuityRequired = config?.gbrainContinuity?.required === true;
const moduleBlocking = modules.some((module) => healthModuleBlocksDoctor(module, { continuityRequired }));
const memoryProjected = modules.some((module) => module.id === 'system'
&& Array.isArray(module.components)
&& module.components.some((component) => component.id.startsWith('memory.')));
const moduleBlocking = modules.some((module) => !(module.id === 'memory' && memoryProjected)
&& healthModuleBlocksDoctor(module, { continuityRequired }));

return {
ok: results.every((item) => item.ok) && !moduleBlocking,
Expand Down Expand Up @@ -1378,15 +1382,15 @@ async function runCli(argv = process.argv.slice(2), env = process.env, invokedAs
if (parsed.options.json) {
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
} else {
process.stdout.write(`${renderSystemDoctor(report, { legacyText: profileDoctor.formatDoctorResult(report) })}\n`);
process.stdout.write(`${renderSystemDoctor(report)}\n`);
}
return report.ok ? 0 : 1;
}
const report = attachSystemDoctorReceipt(runDoctor({ ...parsed.options, env, homeDir: env.HOME || undefined }));
if (parsed.options.json) {
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
} else {
process.stdout.write(`${renderSystemDoctor(report, { legacyText: renderDoctor(report) })}\n`);
process.stdout.write(`${renderSystemDoctor(report)}\n`);
}
return report.ok ? 0 : 1;
} catch (error) {
Expand Down
5 changes: 3 additions & 2 deletions lib/jarvos-doctor-modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ const PUBLIC_STATES = Object.freeze(['healthy', 'update available', 'repair need
const SNAPSHOT_FIELDS = Object.freeze(['schema', 'moduleId', 'generation', 'observedAt', 'validUntil', 'trust', 'repairable', 'updateAvailable']);
const CONTINUITY_SNAPSHOT_FIELDS = Object.freeze(['schema', 'moduleId', 'generation', 'observedAt', 'validUntil', 'trust', 'factsVersion', 'facts']);
const CONTINUITY_FACTS_VERSION = 'jarvos-gbrain-continuity-facts/v1';
const SYSTEM_FACTS_VERSION = 'jarvos-system-doctor-facts/v1';
const SYSTEM_FACTS_VERSION = 'jarvos-system-doctor-facts/v2';
const SYSTEM_COMPONENT_STATES = Object.freeze(['healthy', 'warning', 'repair needed', 'not configured']);
const MEMORY_COMPONENTS = Object.freeze([
['memory.gbrain', 'GBrain'],
['memory.gbrain', 'GBrain core'],
['memory.gbrain-semantic-coverage', 'GBrain semantic coverage'],
['memory.lossless-claw', 'Lossless Claw'],
['memory.qmd-search', 'QMD search'],
['memory.memory-wiki', 'memory-wiki'],
Expand Down
115 changes: 77 additions & 38 deletions lib/jarvos-system-doctor.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,33 +22,48 @@ function coreComponents(report) {
}));
}

function moduleState(state) {
if (state === 'repair needed') return 'repair needed';
function moduleState(state, reasonClass) {
if (state === 'repair needed' || reasonClass === 'module-invalid') return 'repair needed';
if (state === 'healthy') return 'healthy';
return 'warning';
}

function optionalComponents(modules = []) {
const system = modules.find((module) => module.id === 'system');
if (!system) return [];
if (!Array.isArray(system.components)) {
return [{
const components = [];
if (system && !Array.isArray(system.components)) {
components.push({
id: 'module.system',
label: 'System health receipt',
section: 'optional',
state: moduleState(system.state),
state: moduleState(system.state, system.reasonClass),
reasonClass: system.reasonClass,
message: null,
}];
});
} else if (system) {
components.push(...system.components.map((component) => ({
id: component.id,
label: component.label,
section: component.id.startsWith('memory.') ? 'memory' : 'optional',
state: component.state,
reasonClass: component.reasonClass,
message: null,
})));
}
return system.components.map((component) => ({
id: component.id,
label: component.label,
section: component.id.startsWith('memory.') ? 'memory' : 'optional',
state: component.state,
reasonClass: component.reasonClass,
message: null,
}));
const memoryProjected = components.some((component) => component.section === 'memory');
for (const module of modules) {
if (module.id === 'system' || (module.id === 'memory' && memoryProjected)) continue;
if (!['repair needed', 'needs your attention'].includes(module.state)) continue;
components.push({
id: `module.${module.id}`,
label: module.id === 'memory' ? 'Memory receipt' : 'GBrain continuity',
section: module.id === 'memory' ? 'memory' : 'optional',
state: moduleState(module.state, module.reasonClass),
reasonClass: module.reasonClass,
message: null,
});
}
return components;
}

function buildSystemDoctorReceipt(report) {
Expand All @@ -75,38 +90,62 @@ function attachSystemDoctorReceipt(report) {
return { ...report, systemDoctor: buildSystemDoctorReceipt(report) };
}

function renderSystemDoctor(report, { legacyText = null } = {}) {
function sentence(value) {
if (!value) return '';
return /[.!?]$/.test(value) ? value : `${value}.`;
}

function humanizeReason(reasonClass) {
return String(reasonClass || 'unverified').replace(/[.-]+/g, ' ');
}

function componentExplanation(component) {
const known = {
'http-unreachable': 'HTTP check failed. Restore access, then rerun Doctor.',
'search-empty': 'No search results. Run a real search, then rerun Doctor.',
'runtime-tool-missing': 'Runtime search tool unavailable. Enable it, then rerun Doctor.',
'profile-mismatch': 'Receipt is for another profile. Publish a matching receipt.',
'module-invalid': 'Receipt is invalid. Republish it.',
'module-stale': 'Receipt is stale. Refresh it.',
'module-untrusted': 'Receipt is untrusted. Publish a trusted receipt.',
};
if (known[component.reasonClass]) return known[component.reasonClass];
if (component.state === 'not configured') {
const detail = component.message ? sentence(component.message) : 'Not configured.';
return `${detail} Configure it when needed.`;
}
const detail = sentence(component.message || humanizeReason(component.reasonClass));
const action = component.state === 'repair needed'
? 'Fix it, then rerun Doctor.'
: 'Verify it, then rerun Doctor.';
return `${detail} ${action}`;
}

function renderSystemDoctor(report) {
const receipt = report.systemDoctor || buildSystemDoctorReceipt(report);
const lines = legacyText === null
? [`jarvOS System Doctor — ${receipt.profile.title}`, `Workspace: ${receipt.workspace}`, '', 'Core:']
: [legacyText.trimEnd()];
const lines = [`jarvOS System Doctor — ${receipt.profile.title}`, `Workspace: ${receipt.workspace}`];
const marker = {
healthy: '✅',
warning: '⚠️',
'repair needed': '❌',
'not configured': '️',
'not configured': '️',
};
const word = {
healthy: 'PASS',
warning: 'WARN',
'repair needed': 'FAIL',
'not configured': 'SKIP',
const sectionLabel = {
core: 'Core',
optional: 'Services',
memory: 'Memory',
};
if (legacyText === null) {
for (const component of receipt.components) {
if (component.section !== 'core') continue;
lines.push(`${marker[component.state]} ${word[component.state]} ${component.label} — ${component.state}${component.message ? ` (${component.message})` : ''}`);
}
}
const selected = receipt.components.filter((component) => component.section !== 'core');
if (selected.length) {
lines.push('', 'Selected optional components:');
for (const component of selected) {
lines.push(`${marker[component.state]} ${word[component.state]} ${component.label} — ${component.state}`);
const sections = ['core', 'optional', 'memory']
.filter((section) => receipt.components.some((component) => component.section === section));
const showSections = sections.length > 1;
for (const section of sections) {
lines.push('');
if (showSections) lines.push(sectionLabel[section]);
for (const component of receipt.components.filter((item) => item.section === section)) {
const explanation = component.state === 'healthy' ? '' : ` — ${componentExplanation(component)}`;
lines.push(`${marker[component.state]} ${component.label}${explanation}`);
}
}
const prefix = legacyText === null ? '' : 'System Doctor: ';
lines.push('', receipt.status === 'healthy' ? `${prefix}READY` : `${prefix}NOT READY — ${receipt.status}`);
return lines.join('\n');
}

Expand Down
5 changes: 3 additions & 2 deletions modules/jarvos/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,9 @@ The minimal profile checks the reusable JarvOS contract:

Failure output names the exact component, such as `agent.context` or
`path.vault`, so an installer or assistant can repair the missing piece directly.
JSON includes the `jarvos-system-doctor-report/v1` projection, while text keeps
its existing transcript and appends the same selected-component result.
JSON includes the `jarvos-system-doctor-report/v1` projection, while text shows
one icon and one line per component, adding a concise reason and next action only
when a component is degraded.
Optional providers are absent unless selected by a profile-bound, data-only
health snapshot; selected components remain individually visible as healthy,
warning, repair needed, or not configured.
Expand Down
Loading
Loading