Skip to content
Closed
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
2 changes: 1 addition & 1 deletion docs/architecture/doctor-health-modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ 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,
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`
Expand Down
4 changes: 2 additions & 2 deletions lib/jarvos-cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -1378,15 +1378,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
3 changes: 2 additions & 1 deletion lib/jarvos-doctor-modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ const CONTINUITY_FACTS_VERSION = 'jarvos-gbrain-continuity-facts/v1';
const SYSTEM_FACTS_VERSION = 'jarvos-system-doctor-facts/v1';
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
98 changes: 69 additions & 29 deletions lib/jarvos-system-doctor.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,29 @@

const REPORT_SCHEMA = 'jarvos-system-doctor-report/v1';

const STATUS_ICON = Object.freeze({
healthy: '✅',
warning: '⚠️',
'repair needed': '❌',
'not configured': '◻️',
});

const REASON_EXPLANATION = Object.freeze({
none: null,
failed: 'broken; repair the failing check',
'not-configured': 'not configured yet',
skipped: 'not verified yet',
'search-empty': 'reachable, but search returned no results',
'http-unreachable': 'HTTP endpoint unreachable',
'runtime-tool-missing': 'runtime search tool not available',
'module-invalid': 'receipt invalid; republish a trusted snapshot',
'module-stale': 'receipt stale; refresh the producer snapshot',
'module-untrusted': 'receipt untrusted; check producer ownership',
'profile-mismatch': 'snapshot profile does not match doctor profile',
'component-failed': 'one or more selected components need repair',
'component-degraded': 'one or more selected components need attention',
});

function componentState(item) {
if (item?.status === 'fail' || item?.ok === false) return 'repair needed';
if (item?.status === 'warn') return 'warning';
Expand Down Expand Up @@ -67,6 +90,7 @@ function buildSystemDoctorReceipt(report) {
profile,
workspace: report.workspace,
status,
ok: report.ok !== false && status === 'healthy',
components,
};
}
Expand All @@ -75,44 +99,60 @@ function attachSystemDoctorReceipt(report) {
return { ...report, systemDoctor: buildSystemDoctorReceipt(report) };
}

function explanationFor(component) {
if (typeof component.message === 'string' && component.message.trim()) {
return component.message.trim();
}

const reason = component.reasonClass || 'none';
if (Object.prototype.hasOwnProperty.call(REASON_EXPLANATION, reason) && REASON_EXPLANATION[reason]) {
return REASON_EXPLANATION[reason];
}

if (component.state === 'healthy') return null;
if (component.state === 'not configured') return 'not configured yet';
if (component.state === 'warning') {
return reason && reason !== 'none'
? `unverified or degraded (${reason})`
: 'unverified or degraded';
}
if (component.state === 'repair needed') {
return reason && reason !== 'none'
? `broken (${reason}); repair before trusting READY`
: 'broken; repair before trusting READY';
}
return null;
}

function renderComponentLine(component) {
const icon = STATUS_ICON[component.state] || '⚠️';
const explanation = explanationFor(component);
// Exactly one status icon and a concise label/explanation.
// No PASS/FAIL tokens and no repeated state words.
return explanation ? `${icon} ${component.label} — ${explanation}` : `${icon} ${component.label}`;
}

function renderSystemDoctor(report, { legacyText = null } = {}) {
const receipt = report.systemDoctor || buildSystemDoctorReceipt(report);
const lines = legacyText === null
? [`jarvOS System Doctor — ${receipt.profile.title}`, `Workspace: ${receipt.workspace}`, '', 'Core:']
: [legacyText.trimEnd()];
const marker = {
healthy: '✅',
warning: '⚠️',
'repair needed': '❌',
'not configured': '◻️',
};
const word = {
healthy: 'PASS',
warning: 'WARN',
'repair needed': 'FAIL',
'not configured': 'SKIP',
};
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}`);
}
? [`jarvOS System Doctor — ${receipt.profile.title}`, `Workspace: ${receipt.workspace}`, '']
: [legacyText.trimEnd(), ''];

for (const component of receipt.components) {
lines.push(renderComponentLine(component));
}
const prefix = legacyText === null ? '' : 'System Doctor: ';
lines.push('', receipt.status === 'healthy' ? `${prefix}READY` : `${prefix}NOT READY — ${receipt.status}`);
return lines.join('\n');

// No "Selected optional components" heading and no redundant READY aggregate line.
// Authoritative readiness remains on receipt.ok / receipt.status for JSON consumers.
return `${lines.join('\n').trimEnd()}\n`;
}

module.exports = {
REPORT_SCHEMA,
STATUS_ICON,
attachSystemDoctorReceipt,
buildSystemDoctorReceipt,
explanationFor,
renderComponentLine,
renderSystemDoctor,
};
4 changes: 3 additions & 1 deletion modules/jarvos-runtime-kit/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,9 @@ function collectCompoundEngineeringFixtureEntries(root, relative = '') {

function computeCompoundEngineeringFixtureDigest(root, entries = null) {
const fixtureEntries = entries || collectCompoundEngineeringFixtureEntries(root);
const canonical = fixtureEntries.map((entry) => `${entry.path}\0${entry.type}\0${entry.mode.toString(8)}\0${entry.digest || ''}\n`).join('');
// Digest content and path identity only. Permission bits vary across checkout
// umasks (macOS vs Linux CI) and must not invalidate the shipped pin.
const canonical = fixtureEntries.map((entry) => `${entry.path}\0${entry.type}\0${entry.digest || ''}\n`).join('');
return crypto.createHash('sha256').update(canonical).digest('hex');
}

Expand Down
29 changes: 24 additions & 5 deletions runtimes/codex/compound-engineering-capability.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@
},
"harness": "codex",
"admission": "supported",
"operations": ["plan", "work", "compound"],
"operations": [
"plan",
"work",
"compound"
],
"activation": {
"mechanism": "codex-plugin-marketplace",
"marketplaceArgv": [
Expand All @@ -38,19 +42,34 @@
"commands": [
{
"id": "version",
"argv": ["codex", "--version"],
"argv": [
"codex",
"--version"
],
"readOnly": true,
"activatesPluginCode": false
},
{
"id": "marketplace-list",
"argv": ["codex", "plugin", "marketplace", "list", "--json"],
"argv": [
"codex",
"plugin",
"marketplace",
"list",
"--json"
],
"readOnly": true,
"activatesPluginCode": false
},
{
"id": "plugin-list",
"argv": ["codex", "plugin", "list", "--available", "--json"],
"argv": [
"codex",
"plugin",
"list",
"--available",
"--json"
],
"readOnly": true,
"activatesPluginCode": false
}
Expand Down Expand Up @@ -89,5 +108,5 @@
"invocation.json",
"plugin-manifest.json"
],
"fixtureTreeDigest": "5ade826c32f4ca14b00bb71be6fce330ae9cd06b28aff8d6816311a6343617a2"
"fixtureTreeDigest": "dd12011a4122e4ac0541752323263d0e68f289957936b30529f13a168f6cd29b"
}
49 changes: 25 additions & 24 deletions tests/cli-smoke-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ try {
CODEX_HOME: path.join(tmp, 'codex-home'),
JARVOS_CONTROL_PLANE_SERVICE_MODULE: controlPlaneHost,
};
fs.mkdirSync(path.join(tmp, 'codex-home'), { recursive: true });
const fakeOpenClaw = path.join(tmp, 'openclaw');
const fakePluginRoot = path.join(ROOT, 'runtimes', 'openclaw');
const fakePluginManifest = path.join(fakePluginRoot, 'adapter.json');
Expand Down Expand Up @@ -301,9 +302,10 @@ try {
'doctor', '--profile', 'minimal', '--workspace', attachWorkspace,
], { env: attachEnv });
assert.equal(attachDoctor.status, 0, attachDoctor.stderr || attachDoctor.stdout);
assert.match(attachDoctor.stdout, /PASS workspace-files/);
assert.match(attachDoctor.stdout, /PASS vault-path/);
assert.match(attachDoctor.stdout, /READY/);
assert.match(attachDoctor.stdout, /✅ workspace-files/);
assert.match(attachDoctor.stdout, /✅ vault-path/);
assert.match(attachDoctor.stdout, /jarvOS System Doctor/);
assert.doesNotMatch(attachDoctor.stdout, /\bPASS\b|\bFAIL\b|READY/);

const attachSync = run([
'sync', '--workspace', attachWorkspace, '--dry-run', '--json',
Expand Down Expand Up @@ -475,21 +477,21 @@ try {
delete envWithoutHost.JARVOS_CONTROL_PLANE_SERVICE_MODULE;
const doctorNoHost = run(['doctor', '--profile', 'minimal', '--workspace', workspace], { env: envWithoutHost });
assert.equal(doctorNoHost.status, 0, doctorNoHost.stderr || doctorNoHost.stdout);
assert.match(doctorNoHost.stdout, /PASS control-plane-module/);
assert.match(doctorNoHost.stdout, / control-plane-module/);
assert.match(doctorNoHost.stdout, /host service not configured/);
assert.match(doctorNoHost.stdout, /READY/);
assert.doesNotMatch(doctorNoHost.stdout, /\bPASS\b|READY/);

const doctor = run(['doctor', '--profile', 'minimal', '--workspace', workspace], { env });
assert.equal(doctor.status, 0, doctor.stderr || doctor.stdout);
assert.match(doctor.stdout, /PASS node-version/);
assert.match(doctor.stdout, /PASS workspace-files/);
assert.match(doctor.stdout, /PASS config-schema/);
assert.match(doctor.stdout, /PASS vault-path/);
assert.match(doctor.stdout, /PASS vault-path-stale/);
assert.match(doctor.stdout, /PASS journal-conflict/);
assert.match(doctor.stdout, /PASS control-plane-module/);
assert.match(doctor.stdout, / node-version/);
assert.match(doctor.stdout, / workspace-files/);
assert.match(doctor.stdout, / config-schema/);
assert.match(doctor.stdout, / vault-path/);
assert.match(doctor.stdout, / vault-path-stale/);
assert.match(doctor.stdout, / journal-conflict/);
assert.match(doctor.stdout, / control-plane-module/);
assert.match(doctor.stdout, /authenticated host service/);
assert.match(doctor.stdout, /READY/);
assert.doesNotMatch(doctor.stdout, /\bPASS\b|\bFAIL\b|READY/);

for (const file of [
'AGENTS.md',
Expand All @@ -505,15 +507,15 @@ try {
}
const syncDoctor = run(['doctor', '--profile', 'minimal', '--workspace', syncWorkspace], { env });
assert.equal(syncDoctor.status, 0, syncDoctor.stderr || syncDoctor.stdout);
assert.match(syncDoctor.stdout, /PASS config-schema/);
assert.match(syncDoctor.stdout, /PASS vault-path/);
assert.match(syncDoctor.stdout, /READY/);
assert.match(syncDoctor.stdout, / config-schema/);
assert.match(syncDoctor.stdout, / vault-path/);
assert.doesNotMatch(syncDoctor.stdout, /\bPASS\b|READY/);

fs.rmSync(path.join(syncVault, 'Tags'), { recursive: true });
fs.writeFileSync(path.join(syncVault, 'Tags'), 'not a directory\n');
const fileTagsDoctor = run(['doctor', '--profile', 'minimal', '--workspace', syncWorkspace], { env });
assert.notEqual(fileTagsDoctor.status, 0);
assert.match(fileTagsDoctor.stdout, /FAIL vault-path/);
assert.match(fileTagsDoctor.stdout, / vault-path/);
fs.rmSync(path.join(syncVault, 'Tags'));
fs.mkdirSync(path.join(syncVault, 'Tags'));

Expand All @@ -531,7 +533,7 @@ try {
fs.writeFileSync(path.join(workspace, 'jarvos.config.json'), JSON.stringify(configured, null, 2));
const duplicateTelegramDoctor = run(['doctor', '--profile', 'minimal', '--workspace', workspace], { env });
assert.equal(duplicateTelegramDoctor.status, 1, duplicateTelegramDoctor.stderr || duplicateTelegramDoctor.stdout);
assert.match(duplicateTelegramDoctor.stdout, /FAIL config-schema/);
assert.match(duplicateTelegramDoctor.stdout, / config-schema/);
assert.match(duplicateTelegramDoctor.stdout, /only one Telegram update consumer/);
delete configured.runtimeMode;
fs.writeFileSync(path.join(workspace, 'jarvos.config.json'), JSON.stringify(configured, null, 2));
Expand Down Expand Up @@ -572,9 +574,8 @@ try {
}]);
const moduleTextDoctor = run(['doctor', '--profile', 'minimal', '--workspace', workspace], { env });
assert.equal(moduleTextDoctor.status, 0, moduleTextDoctor.stderr || moduleTextDoctor.stdout);
assert.match(moduleTextDoctor.stdout, /jarvOS doctor — Minimal/);
assert.match(moduleTextDoctor.stdout, /Optional modules:\nMemory — update available/);
assert.match(moduleTextDoctor.stdout, /\nREADY\n\nSystem Doctor: READY\n$/);
assert.match(moduleTextDoctor.stdout, /jarvOS System Doctor — Minimal/);
assert.doesNotMatch(moduleTextDoctor.stdout, /Optional modules:|READY|\bPASS\b/);

const systemSnapshotPath = path.join(healthModules, 'system.json');
fs.writeFileSync(systemSnapshotPath, `${JSON.stringify({
Expand Down Expand Up @@ -605,8 +606,8 @@ try {
assert.equal(searxng.reasonClass, 'search-empty');
const systemTextDoctor = run(['doctor', '--profile', 'minimal', '--workspace', workspace], { env });
assert.equal(systemTextDoctor.status, 1, systemTextDoctor.stderr || systemTextDoctor.stdout);
assert.match(systemTextDoctor.stdout, /⚠️ WARN SearXNG — warning/);
assert.match(systemTextDoctor.stdout, /NOT READY — needs your attention/);
assert.match(systemTextDoctor.stdout, /⚠️ SearXNG — reachable, but search returned no results/);
assert.doesNotMatch(systemTextDoctor.stdout, /WARN|NOT READY|Selected optional components/);
fs.unlinkSync(systemSnapshotPath);

const localDoctorEnv = {
Expand Down Expand Up @@ -713,7 +714,7 @@ try {
};
const doctorBadHost = run(['doctor', '--profile', 'minimal', '--workspace', workspace], { env: badHostEnv });
assert.notEqual(doctorBadHost.status, 0);
assert.match(doctorBadHost.stdout, /FAIL control-plane-module/);
assert.match(doctorBadHost.stdout, / control-plane-module/);
assert.match(doctorBadHost.stdout, /configure a usable JARVOS_CONTROL_PLANE_SERVICE_MODULE/);
assert.doesNotMatch(doctorBadHost.stdout, /missing-host\.js/);

Expand Down
Loading
Loading