vgxness 1.5.2 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/agents/canonical-agent-projection.js +18 -0
  2. package/dist/agents/renderers/claude-renderer.js +3 -3
  3. package/dist/cli/cli-flags.js +1 -1
  4. package/dist/cli/cli-help.js +7 -7
  5. package/dist/cli/commands/interactive-entrypoint-dispatcher.js +2 -2
  6. package/dist/cli/commands/mcp-dispatcher.js +11 -1
  7. package/dist/cli/commands/setup-dispatcher.js +9 -0
  8. package/dist/cli/tui/main-menu/main-menu-read-model.js +41 -44
  9. package/dist/cli/tui/main-menu/main-menu-render-shape.js +15 -15
  10. package/dist/cli/tui/opentui/main-menu/screen.js +39 -41
  11. package/dist/cli/tui/opentui/main-menu/smoke.js +1 -1
  12. package/dist/cli/tui/opentui/main-menu/view.js +1 -1
  13. package/dist/cli/tui/setup/setup-tui-read-model.js +15 -12
  14. package/dist/mcp/claude-code-agent-config.js +23 -7
  15. package/dist/mcp/claude-code-cli.js +71 -0
  16. package/dist/mcp/claude-code-config.js +1 -1
  17. package/dist/mcp/claude-code-project-memory.js +127 -0
  18. package/dist/mcp/claude-code-scope.js +18 -0
  19. package/dist/mcp/claude-code-user-config.js +55 -0
  20. package/dist/mcp/claude-code-user-memory.js +90 -0
  21. package/dist/mcp/client-install-claude-code-contract.js +91 -12
  22. package/dist/mcp/client-install-claude-code.js +133 -12
  23. package/dist/mcp/control-plane.js +18 -1
  24. package/dist/mcp/index.js +5 -0
  25. package/dist/mcp/provider-change-plan.js +18 -6
  26. package/dist/mcp/provider-doctor.js +71 -5
  27. package/dist/mcp/provider-health-types.js +4 -0
  28. package/dist/mcp/provider-status.js +77 -8
  29. package/dist/mcp/schema.js +4 -3
  30. package/dist/sdd/schema.js +15 -0
  31. package/dist/sdd/sdd-workflow-service.js +59 -29
  32. package/dist/setup/providers/claude-setup-adapter.js +11 -7
  33. package/dist/setup/setup-plan.js +60 -1
  34. package/docs/architecture.md +2 -2
  35. package/docs/cli.md +37 -2
  36. package/docs/glossary.md +2 -2
  37. package/docs/prd.md +2 -2
  38. package/docs/providers.md +33 -6
  39. package/docs/roadmap.md +1 -1
  40. package/package.json +1 -1
@@ -2,24 +2,30 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { dirname } from 'node:path';
3
3
  import { createManagedProviderConfigBackup } from '../setup/backup-rollback-service.js';
4
4
  import { parseClaudeAgentFrontmatter } from './claude-code-agent-config.js';
5
+ import { runClaudeCodeCliCommand } from './claude-code-cli.js';
5
6
  import { createClaudeCodeMcpServerConfig, inspectClaudeCodeMcpConfig, mergeClaudeCodeMcpConfig } from './claude-code-config.js';
6
- import { expectedClaudeCodeRenderedAgents, planClaudeCodeMcpInstall } from './client-install-claude-code-contract.js';
7
+ import { inspectClaudeProjectMemory, mergeClaudeProjectMemory } from './claude-code-project-memory.js';
8
+ import { expectedClaudeCodeRenderedAgents, expectedClaudeCodeRenderedUserAgents, planClaudeCodeMcpInstall } from './client-install-claude-code-contract.js';
9
+ import { inspectClaudeCodeUserMcpConfig, mergeClaudeCodeUserMcpConfig } from './claude-code-user-config.js';
10
+ import { inspectClaudeUserMemory, mergeClaudeUserMemory } from './claude-code-user-memory.js';
7
11
  export async function installClaudeCodeMcpClient(input) {
8
12
  const source = input.databasePathSource ?? 'flag';
9
13
  const server = createClaudeCodeMcpServerConfig(input.databasePath, source);
10
- const plan = planClaudeCodeMcpInstall({ cwd: input.cwd, databasePath: input.databasePath, databasePathSource: source, ...(input.overwriteVgxness !== undefined ? { overwriteVgxness: input.overwriteVgxness } : {}) });
11
- if (!input.confirmed)
12
- return refusal('confirmation_required', '`mcp install claude` requires explicit --yes before any project config write.', plan, server, [], []);
14
+ const plan = planClaudeCodeMcpInstall({ cwd: input.cwd, databasePath: input.databasePath, databasePathSource: source, ...(input.overwriteVgxness !== undefined ? { overwriteVgxness: input.overwriteVgxness } : {}), ...(input.scope !== undefined ? { scope: input.scope } : {}), ...(input.env !== undefined ? { env: input.env } : {}) });
13
15
  if (plan.status === 'refused')
14
16
  return refusal(plan.reason, plan.message, plan, server, [], []);
17
+ if (!input.confirmed)
18
+ return refusal('confirmation_required', '`mcp install claude` requires explicit --yes before any project config write.', plan, server, [], []);
15
19
  if (input.preflight === undefined) {
16
20
  return refusal('preflight_failed', 'Claude Code provider config writes require VGXNESS preflight before any project config write.', plan, server, [], []);
17
21
  }
18
- const preflightPaths = unique(plan.targets.filter((target) => target.action !== 'blocked').map((target) => target.path));
22
+ const preflightPaths = unique(plan.targets.flatMap((target) => (isMutatingTarget(target) ? [target.path] : [])));
23
+ if (plan.canonicalClaudeScope !== 'project')
24
+ preflightPaths.unshift(plan.targetPath);
19
25
  for (const targetPath of preflightPaths) {
20
26
  const preflight = await input.preflight({
21
27
  category: 'provider-tool',
22
- operation: 'write claude project provider config',
28
+ operation: plan.canonicalClaudeScope === 'user' ? 'write claude user provider config' : 'write claude project provider config',
23
29
  targetPath,
24
30
  workspaceRoot: input.cwd,
25
31
  providerToolName: 'claude-code',
@@ -32,9 +38,22 @@ export async function installClaudeCodeMcpClient(input) {
32
38
  }
33
39
  const backups = [];
34
40
  const writtenPaths = [];
35
- const existingTargets = plan.targets.filter((target) => target.action === 'merge' || target.action === 'update-vgxness');
41
+ if (plan.canonicalClaudeScope === 'local') {
42
+ if (input.cliRunner === undefined)
43
+ return refusal('preflight_failed', 'Claude CLI MCP registration apply requires an injected runner boundary; read-only plans never execute Claude Code.', plan, server, [], []);
44
+ if (plan.cliCommand === undefined)
45
+ return refusal('preflight_failed', 'Claude CLI MCP registration command is unavailable.', plan, server, [], []);
46
+ const cli = await runClaudeCodeCliCommand(plan.cliCommand, input.cliRunner);
47
+ const cliResult = { exitCode: cli.exitCode, stdout: cli.stdout, stderr: cli.stderr, preview: cli.command.preview };
48
+ if (cli.exitCode !== 0)
49
+ return refusal('preflight_failed', `Claude CLI MCP registration failed with exit code ${cli.exitCode ?? 'unknown'}.`, plan, server, [], [], cliResult);
50
+ return { version: 1, kind: 'mcp-client-install-claude-code', status: 'installed', targetPath: plan.targetPath, writtenPaths: [], backups: [], safety: applySafety(plan), server, warnings: plan.warnings, verificationHints: plan.verificationHints, agentNames: plan.agentNames, overwriteVgxness: plan.overwriteVgxness, cliResult };
51
+ }
52
+ if (plan.canonicalClaudeScope === 'user')
53
+ return applyUserInstall(input, plan, server, writtenPaths, backups);
54
+ const existingTargets = plan.targets.filter((target) => target.kind !== 'cli-mcp-registration' && (target.action === 'merge' || target.action === 'update-vgxness' || target.action === 'append-managed-block'));
36
55
  for (const target of existingTargets) {
37
- const backup = createBackup(target.path);
56
+ const backup = createBackup(target.path, target.kind === 'project-memory' ? 'project-memory' : 'config');
38
57
  if (!backup.ok)
39
58
  return refusal('backup_failed', backup.error.message, plan, server, writtenPaths, backups);
40
59
  backups.push(toBackupSummary(backup.value));
@@ -54,6 +73,11 @@ export async function installClaudeCodeMcpClient(input) {
54
73
  return refusal('post_write_validation_failed', `Claude agent ${agent.agentName} failed post-write validation.`, plan, server, writtenPaths, backups);
55
74
  writtenPaths.push(agent.path);
56
75
  }
76
+ const projectMemoryWrite = writeProjectMemory(plan);
77
+ if (!projectMemoryWrite.ok)
78
+ return refusal('post_write_validation_failed', projectMemoryWrite.error.message, plan, server, writtenPaths, backups);
79
+ if (projectMemoryWrite.value !== undefined)
80
+ writtenPaths.push(projectMemoryWrite.value);
57
81
  return { version: 1, kind: 'mcp-client-install-claude-code', status: 'installed', targetPath: plan.targetPath, writtenPaths, backups, safety: applySafety(plan), server, warnings: plan.warnings, verificationHints: plan.verificationHints, agentNames: plan.agentNames, overwriteVgxness: plan.overwriteVgxness };
58
82
  }
59
83
  function writeMcpJson(cwd, plan, server) {
@@ -68,11 +92,101 @@ function writeMcpJson(cwd, plan, server) {
68
92
  const after = inspectClaudeCodeMcpConfig(cwd);
69
93
  return after.status === 'configured' ? { ok: true, value: plan.targetPath } : { ok: false, error: { code: 'validation_failed', message: 'Claude .mcp.json did not validate after write.' } };
70
94
  }
71
- function createBackup(path) {
72
- return createManagedProviderConfigBackup({ targetPath: path, provider: 'claude', scope: 'project', createdByOperation: 'mcp-client-install-claude-code', reason: 'pre-merge-safety', description: 'Backup existing Claude Code project config before merging VGXNESS MCP or agent configuration.' });
95
+ function writeUserMcpJson(env, plan, server) {
96
+ const target = plan.targets.find((item) => item.kind === 'user-mcp-json');
97
+ if (target === undefined)
98
+ return { ok: true, value: plan.targetPath };
99
+ if (target.action === 'blocked')
100
+ return { ok: false, error: { code: 'validation_failed', message: target.reason ?? 'Claude user config is blocked.' } };
101
+ const state = inspectClaudeCodeUserMcpConfig(env);
102
+ if (state.status === 'invalid' || state.status === 'conflicting')
103
+ return { ok: false, error: { code: 'validation_failed', message: state.message } };
104
+ if (target.action === 'create' && existsSync(target.path))
105
+ return { ok: false, error: { code: 'validation_failed', message: 'Claude user ~/.claude.json appeared after planning; rerun apply to merge safely.' } };
106
+ if (state.path !== target.path)
107
+ return { ok: false, error: { code: 'validation_failed', message: 'Claude user config path changed after planning; rerun apply.' } };
108
+ const merged = mergeClaudeCodeUserMcpConfig(state.parsed ? state.config : {}, server);
109
+ mkdirSync(dirname(target.path), { recursive: true });
110
+ writeFileSync(target.path, `${JSON.stringify(merged, null, 2)}\n`);
111
+ const after = inspectClaudeCodeUserMcpConfig(env);
112
+ return after.status === 'configured' ? { ok: true, value: target.path } : { ok: false, error: { code: 'validation_failed', message: 'Claude user ~/.claude.json did not validate after write.' } };
113
+ }
114
+ function writeProjectMemory(plan) {
115
+ const target = plan.targets.find((item) => item.kind === 'project-memory');
116
+ if (target === undefined || target.action === 'none')
117
+ return { ok: true, value: undefined };
118
+ if (target.action === 'blocked')
119
+ return { ok: false, error: { code: 'validation_failed', message: target.reason ?? 'Claude project memory is blocked.' } };
120
+ const state = inspectClaudeProjectMemory(dirname(target.path));
121
+ if (state.action !== target.action)
122
+ return { ok: false, error: { code: 'validation_failed', message: 'Claude project memory changed after planning; rerun apply.' } };
123
+ const merged = mergeClaudeProjectMemory(state);
124
+ if (!merged.ok)
125
+ return merged;
126
+ mkdirSync(dirname(target.path), { recursive: true });
127
+ writeFileSync(target.path, merged.value.contents);
128
+ const after = inspectClaudeProjectMemory(dirname(target.path));
129
+ return after.status === 'managed-current' ? { ok: true, value: target.path } : { ok: false, error: { code: 'validation_failed', message: 'Claude project memory did not validate as managed-current after write.' } };
130
+ }
131
+ async function applyUserInstall(input, plan, server, writtenPaths, backups) {
132
+ const existingTargets = plan.targets.filter((target) => target.kind !== 'cli-mcp-registration' && (target.action === 'merge' || target.action === 'update-vgxness' || target.action === 'append-managed-block' || target.action === 'update-managed-block'));
133
+ for (const target of existingTargets) {
134
+ const backup = createBackup(target.path, target.kind === 'user-memory' ? 'user-memory' : 'config', 'user');
135
+ if (!backup.ok)
136
+ return refusal('backup_failed', backup.error.message, plan, server, writtenPaths, backups);
137
+ backups.push(toBackupSummary(backup.value));
138
+ }
139
+ const mcpWrite = writeUserMcpJson(input.env, plan, server);
140
+ if (!mcpWrite.ok)
141
+ return refusal('post_write_validation_failed', mcpWrite.error.message, plan, server, writtenPaths, backups);
142
+ writtenPaths.push(mcpWrite.value);
143
+ for (const agent of expectedClaudeCodeRenderedUserAgents(input.cwd, input.env)) {
144
+ const target = plan.targets.find((item) => item.kind === 'agent-file' && item.path === agent.path);
145
+ if (target?.action !== 'create' && target?.action !== 'update-vgxness')
146
+ continue;
147
+ mkdirSync(dirname(agent.path), { recursive: true });
148
+ writeFileSync(agent.path, agent.contents);
149
+ const validation = parseClaudeAgentFrontmatter(readFileSync(agent.path, 'utf8'));
150
+ if (!validation.ok || validation.data.name !== agent.agentName)
151
+ return refusal('post_write_validation_failed', `Claude user agent ${agent.agentName} failed post-write validation.`, plan, server, writtenPaths, backups);
152
+ writtenPaths.push(agent.path);
153
+ }
154
+ const memoryWrite = writeUserMemory(input.env, plan);
155
+ if (!memoryWrite.ok)
156
+ return refusal('post_write_validation_failed', memoryWrite.error.message, plan, server, writtenPaths, backups);
157
+ if (memoryWrite.value !== undefined)
158
+ writtenPaths.push(memoryWrite.value);
159
+ return { version: 1, kind: 'mcp-client-install-claude-code', status: 'installed', targetPath: plan.targetPath, writtenPaths, backups, safety: applySafety(plan), server, warnings: plan.warnings, verificationHints: plan.verificationHints, agentNames: plan.agentNames, overwriteVgxness: plan.overwriteVgxness };
160
+ }
161
+ function writeUserMemory(env, plan) {
162
+ const target = plan.targets.find((item) => item.kind === 'user-memory');
163
+ if (target === undefined || target.action === 'none')
164
+ return { ok: true, value: undefined };
165
+ if (target.action === 'blocked')
166
+ return { ok: false, error: { code: 'validation_failed', message: target.reason ?? 'Claude user memory is blocked.' } };
167
+ const state = inspectClaudeUserMemory(env);
168
+ if (state.path !== target.path || state.action !== target.action)
169
+ return { ok: false, error: { code: 'validation_failed', message: 'Claude user memory changed after planning; rerun apply.' } };
170
+ const merged = mergeClaudeUserMemory(state);
171
+ if (!merged.ok)
172
+ return merged;
173
+ mkdirSync(dirname(target.path), { recursive: true });
174
+ writeFileSync(target.path, merged.value.contents);
175
+ const after = inspectClaudeUserMemory(env);
176
+ return after.status === 'managed-current' ? { ok: true, value: target.path } : { ok: false, error: { code: 'validation_failed', message: 'Claude user memory did not validate as managed-current after write.' } };
177
+ }
178
+ function createBackup(path, kind, scope = 'project') {
179
+ return createManagedProviderConfigBackup({
180
+ targetPath: path,
181
+ provider: 'claude',
182
+ scope,
183
+ createdByOperation: 'mcp-client-install-claude-code',
184
+ reason: kind === 'project-memory' ? 'pre-project-memory-update-safety' : kind === 'user-memory' ? 'pre-user-memory-update-safety' : 'pre-merge-safety',
185
+ description: kind === 'project-memory' ? 'Backup existing Claude Code project memory before appending or updating the VGXNESS managed block.' : kind === 'user-memory' ? 'Backup existing Claude Code user memory before appending or updating the VGXNESS managed block.' : 'Backup existing Claude Code config before merging VGXNESS MCP or agent configuration.',
186
+ });
73
187
  }
74
- function refusal(reason, message, plan, server, writtenPaths, backups) {
75
- return { version: 1, kind: 'mcp-client-install-claude-code', status: 'refused', reason, message, targetPath: plan.targetPath, writtenPaths, backups, safety: { ...plan.safety, operation: 'apply', mutating: false, writesProviderConfig: false }, server, warnings: plan.warnings, verificationHints: plan.verificationHints, agentNames: plan.agentNames, overwriteVgxness: plan.overwriteVgxness };
188
+ function refusal(reason, message, plan, server, writtenPaths, backups, cliResult) {
189
+ return { version: 1, kind: 'mcp-client-install-claude-code', status: 'refused', reason, message, targetPath: plan.targetPath, writtenPaths, backups, safety: { ...plan.safety, operation: 'apply', mutating: false, writesProviderConfig: false }, server, warnings: plan.warnings, verificationHints: plan.verificationHints, agentNames: plan.agentNames, overwriteVgxness: plan.overwriteVgxness, ...(cliResult === undefined ? {} : { cliResult }) };
76
190
  }
77
191
  function applySafety(plan) {
78
192
  return { ...plan.safety, operation: 'apply', mutating: true, writesProviderConfig: true };
@@ -83,3 +197,10 @@ function toBackupSummary(backup) {
83
197
  function unique(values) {
84
198
  return [...new Set(values)];
85
199
  }
200
+ function isMutatingTarget(target) {
201
+ if (target.kind === 'cli-mcp-registration')
202
+ return false;
203
+ if (target.action === 'blocked' || target.action === 'none')
204
+ return false;
205
+ return true;
206
+ }
@@ -29,7 +29,7 @@ export function callVgxTool(call, services) {
29
29
  case 'vgxness_sdd_get_readiness':
30
30
  return auditedEnvelope(validated.tool, services.sdd.getReady(validated.input), services, sddReadinessAuditPayload(validated.input));
31
31
  case 'vgxness_sdd_save_artifact':
32
- return toEnvelope(validated.tool, services.sdd.saveArtifact(validated.input));
32
+ return auditedEnvelope(validated.tool, services.sdd.saveArtifact(validated.input), services, sddSaveAuditPayload(validated.input));
33
33
  case 'vgxness_sdd_accept_artifact':
34
34
  return auditedEnvelope(validated.tool, services.sdd.acceptArtifact(toAcceptArtifactServiceInput(validated.input)), services, sddAcceptanceAuditPayload(validated.input));
35
35
  case 'vgxness_sdd_get_artifact':
@@ -170,6 +170,23 @@ function sddAcceptanceAuditPayload(input) {
170
170
  }),
171
171
  };
172
172
  }
173
+ function sddSaveAuditPayload(input) {
174
+ return {
175
+ runId: input.runId,
176
+ title: 'Audit: sdd-artifact-saved',
177
+ relatedType: 'sdd-artifact',
178
+ relatedId: `${input.change}:${input.phase}`,
179
+ payload: (value) => ({
180
+ eventType: 'sdd-artifact-saved',
181
+ project: input.project,
182
+ change: input.change,
183
+ phase: input.phase,
184
+ topicKey: value.topicKey,
185
+ artifactId: value.id,
186
+ ...(input.agentId === undefined ? {} : { agentId: input.agentId }),
187
+ }),
188
+ };
189
+ }
173
190
  function governanceReportAuditPayload(input) {
174
191
  return {
175
192
  runId: input.runId,
package/dist/mcp/index.js CHANGED
@@ -6,7 +6,12 @@ export * from './client-install-claude-code.js';
6
6
  export * from './client-install-claude-code-contract.js';
7
7
  export * from './client-setup-preview.js';
8
8
  export * from './claude-code-agent-config.js';
9
+ export * from './claude-code-cli.js';
9
10
  export * from './claude-code-config.js';
11
+ export * from './claude-code-project-memory.js';
12
+ export * from './claude-code-scope.js';
13
+ export * from './claude-code-user-config.js';
14
+ export * from './claude-code-user-memory.js';
10
15
  export * from './control-plane.js';
11
16
  export * from './doctor.js';
12
17
  export * from './opencode-visibility.js';
@@ -3,6 +3,7 @@ import { planClaudeCodeMcpInstall } from './client-install-claude-code-contract.
3
3
  import { planOpenCodeMcpInstall } from './client-install-opencode-contract.js';
4
4
  import { ProviderDoctorService } from './provider-doctor.js';
5
5
  import { ProviderStatusService } from './provider-status.js';
6
+ import { resolveClaudeCodeScope } from './claude-code-scope.js';
6
7
  export const providerChangePlanProviders = ['opencode', 'claude', 'antigravity', 'custom'];
7
8
  export const providerChangePlanTypes = ['opencode-mcp-install', 'setup', 'install', 'config-preparation'];
8
9
  const safety = {
@@ -75,10 +76,13 @@ export class ProviderChangePlanService {
75
76
  const doctor = (this.deps.providerDoctor ?? new ProviderDoctorService()).getDoctor({ project: normalized.project, scope: normalized.scope, providerAdapter: 'claude', workspaceRoot: normalized.workspaceRoot, env: this.deps.env ?? process.env, payloadMode: normalized.payloadMode });
76
77
  if (!doctor.ok)
77
78
  return doctor;
79
+ const claudeScope = resolveClaudeCodeScope(normalized.scope);
80
+ if (!claudeScope.ok)
81
+ return claudeScope;
78
82
  const databasePath = resolveMemoryDatabasePath({ cwd: normalized.workspaceRoot, env: this.deps.env ?? process.env });
79
83
  if (!databasePath.ok)
80
84
  return { ok: true, value: blockedPlanEnvelope(normalized, status.value, doctor.value, databasePath.error.message) };
81
- const installPlan = planClaudeCodeMcpInstall({ cwd: normalized.workspaceRoot, databasePath: databasePath.value.path, databasePathSource: databasePath.value.source });
85
+ const installPlan = planClaudeCodeMcpInstall({ cwd: normalized.workspaceRoot, databasePath: databasePath.value.path, databasePathSource: databasePath.value.source, scope: normalized.scope, env: this.deps.env ?? process.env });
82
86
  return { ok: true, value: claudeEnvelope(normalized, status.value, doctor.value, installPlan, databasePath.value.source) };
83
87
  }
84
88
  }
@@ -180,7 +184,7 @@ function claudeEnvelope(input, status, doctor, installPlan, source) {
180
184
  supported: true,
181
185
  status: installPlan.status === 'refused' ? 'blocked' : 'planned',
182
186
  ...(installPlan.status === 'refused' ? { code: 'PLAN_UNAVAILABLE' } : {}),
183
- summary: installPlan.status === 'refused' ? `Read-only Claude ${input.changeType} planning completed; future install is currently refused: ${installPlan.message}` : `Read-only Claude ${input.changeType} planning completed; future confirmed write would update project .mcp.json and ${installPlan.agentNames.length} agent file(s).`,
187
+ summary: installPlan.status === 'refused' ? `Read-only Claude ${input.changeType} planning completed; future install is currently refused: ${installPlan.message}` : `Read-only Claude ${input.changeType} planning completed; future confirmed write would update ${installPlan.canonicalClaudeScope === 'user' ? '~/.claude.json, user agents, and ~/.claude/CLAUDE.md' : 'project .mcp.json, agent files, and project-root CLAUDE.md'} as needed.`,
184
188
  providerIdentity: { provider: 'claude', adapter: 'claude', support: 'supported' },
185
189
  statusSummary: statusSummary(status),
186
190
  statusFindings: status.config,
@@ -262,10 +266,18 @@ function previewEffects(plan) {
262
266
  };
263
267
  }
264
268
  function previewEffectsClaude(plan) {
269
+ const projectMemory = projectMemoryPreview(plan.targets);
265
270
  if (plan.status === 'refused')
266
- return { action: 'refused', targetPath: plan.targetPath, backupRequired: false, preservedTopLevelKeys: plan.preservedTopLevelKeys, serverCommand: ['vgxness', ...plan.server.args], installsAgents: true, agentNames: plan.agentNames, installPlanStatus: 'refused', refusalReason: plan.reason, refusalMessage: plan.message };
267
- const mcpTarget = plan.targets.find((target) => target.kind === 'mcp-json');
268
- return { action: mcpTarget?.action === 'create' ? 'would-create' : 'would-merge', targetPath: plan.targetPath, backupRequired: plan.backupRequired, preservedTopLevelKeys: plan.preservedTopLevelKeys, serverCommand: ['vgxness', ...plan.server.args], installsAgents: true, agentNames: plan.agentNames, installPlanStatus: 'would_install' };
271
+ return { action: 'refused', targetPath: plan.targetPath, backupRequired: false, preservedTopLevelKeys: plan.preservedTopLevelKeys, serverCommand: ['vgxness', ...plan.server.args], installsAgents: true, agentNames: plan.agentNames, installPlanStatus: 'refused', refusalReason: plan.reason, refusalMessage: plan.message, ...(projectMemory === undefined ? {} : { projectMemory }) };
272
+ const mcpTarget = plan.targets.find((target) => target.kind === 'mcp-json' || target.kind === 'user-mcp-json');
273
+ return { action: mcpTarget?.action === 'create' ? 'would-create' : 'would-merge', targetPath: plan.targetPath, backupRequired: plan.backupRequired, preservedTopLevelKeys: plan.preservedTopLevelKeys, serverCommand: ['vgxness', ...plan.server.args], installsAgents: true, agentNames: plan.agentNames, installPlanStatus: 'would_install', ...(projectMemory === undefined ? {} : { projectMemory }) };
274
+ }
275
+ function projectMemoryPreview(targets) {
276
+ const target = targets.find((item) => item.kind === 'project-memory' || item.kind === 'user-memory');
277
+ if (target === undefined)
278
+ return undefined;
279
+ const action = target.action === 'create' ? 'would-create' : target.action === 'append-managed-block' ? 'would-append' : target.action === 'update-managed-block' ? 'would-update-managed-block' : target.action === 'none' ? 'up-to-date' : 'refused';
280
+ return { path: target.path, status: target.status, action, backupRequired: target.backupRequired, ...(target.reason === undefined ? {} : { message: target.reason }) };
269
281
  }
270
282
  function descriptiveBackupPolicy(backupRequired) {
271
283
  return {
@@ -300,7 +312,7 @@ function risksForOpenCode(status, doctor, plan, source) {
300
312
  return risks;
301
313
  }
302
314
  function risksForClaude(status, doctor, plan, source) {
303
- const risks = ['Claude project .mcp.json may affect collaborators if committed; review before committing.', 'Claude Code runtime MCP approval happens in Claude Code; this plan does not prove runtime connection.'];
315
+ const risks = [plan.canonicalClaudeScope === 'user' ? 'Claude user/global files affect this OS user; backups are required before merging existing files.' : 'Claude project .mcp.json and project-root CLAUDE.md may affect collaborators if committed; review before committing.', 'Claude Code runtime MCP approval happens in Claude Code; this plan does not prove runtime connection.'];
304
316
  if (status.status !== 'ready')
305
317
  risks.push(`Provider status is ${status.status}; future writes should review status findings first.`);
306
318
  if (doctor.status !== 'healthy')
@@ -1,9 +1,13 @@
1
1
  import { existsSync, readFileSync, statSync } from 'node:fs';
2
2
  import { inspectClaudeCodeAgents } from './claude-code-agent-config.js';
3
3
  import { claudeAdvisoryPaths, inspectClaudeCodeMcpConfig } from './claude-code-config.js';
4
+ import { inspectClaudeProjectMemory } from './claude-code-project-memory.js';
5
+ import { resolveClaudeCodeScope } from './claude-code-scope.js';
6
+ import { inspectClaudeCodeUserMcpConfig } from './claude-code-user-config.js';
7
+ import { inspectClaudeUserMemory } from './claude-code-user-memory.js';
4
8
  import { vgxnessOpenCodeDefaultAgent, vgxnessOpenCodeSddSubagents } from './opencode-default-agent-config.js';
5
9
  import { buildCanonicalAgentManifestDiagnostic } from './provider-canonical-agent-manifest.js';
6
- import { normalizeProviderHealthInput, PROVIDER_HEALTH_SAFETY, providerHealthFailure, REQUIRED_PROVIDER_NATIVE_MCP_TOOLS, rollupProviderDoctor, } from './provider-health-types.js';
10
+ import { normalizeProviderHealthInput, PROVIDER_HEALTH_SAFETY, CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES, providerHealthFailure, isUserGlobalScope, REQUIRED_PROVIDER_NATIVE_MCP_TOOLS, rollupProviderDoctor, } from './provider-health-types.js';
7
11
  import { inspectOpenCodeConfigPaths } from './provider-status.js';
8
12
  export class ProviderDoctorService {
9
13
  deps;
@@ -83,24 +87,42 @@ export class ProviderDoctorService {
83
87
  };
84
88
  }
85
89
  getClaudeDoctor(normalized) {
90
+ const resolvedScope = resolveClaudeCodeScope(normalized.scope);
91
+ if (!resolvedScope.ok)
92
+ return resolvedScope;
93
+ if (isUserGlobalScope(normalized.scope) || resolvedScope.value.canonical === 'local')
94
+ return this.getClaudeUserGlobalDoctor(normalized, resolvedScope.value.canonical, resolvedScope.value.warnings);
86
95
  const mcp = inspectClaudeCodeMcpConfig(normalized.workspaceRoot);
87
96
  const agents = inspectClaudeCodeAgents(normalized.workspaceRoot);
97
+ const projectMemory = inspectClaudeProjectMemory(normalized.workspaceRoot);
88
98
  const advisoryPaths = claudeAdvisoryPaths(normalized.workspaceRoot);
89
- const checkedPathList = [mcp.path, agents.directoryPath, ...agents.agents.map((agent) => agent.path), ...advisoryPaths];
99
+ const checkedPathList = [mcp.path, agents.directoryPath, ...agents.agents.map((agent) => agent.path), projectMemory.path, ...advisoryPaths];
90
100
  const before = snapshotPaths(checkedPathList, normalized.workspaceRoot);
91
101
  const missingAgents = agents.agents.filter((agent) => agent.status === 'missing');
92
- const badFrontmatter = agents.agents.filter((agent) => agent.frontmatter === 'invalid' || agent.frontmatter === 'missing');
102
+ const blockingAgents = agents.agents.filter((agent) => agent.status === 'conflicting' || agent.status === 'invalid');
103
+ const badFrontmatter = agents.agents.filter((agent) => agent.exists && agent.frontmatter === 'invalid');
93
104
  const badMarkers = agents.agents.filter((agent) => agent.exists && !agent.generatedMarker);
94
105
  const memoryFiles = advisoryPaths.filter((path) => existsSync(path));
95
106
  const checks = [
96
107
  { id: 'workspace-root', status: existsSync(normalized.workspaceRoot) ? 'pass' : 'fail', detail: `Workspace root ${existsSync(normalized.workspaceRoot) ? 'exists' : 'does not exist'}: ${normalized.workspaceRoot}` },
97
- { id: 'provider-supported', status: 'pass', detail: 'Claude provider adapter selected for project-local .mcp.json and .claude/agents support.' },
108
+ { id: 'provider-supported', status: 'pass', detail: 'Claude provider adapter selected for CLI MCP registration diagnostics, project .mcp.json compatibility, and Claude agents support.' },
109
+ { id: 'claude-scope', status: 'pass', detail: 'Claude scope project resolved to canonical project.' },
110
+ { id: 'claude-cli-presence', status: 'warn', detail: 'Read-only doctor does not execute `claude --version`; CLI availability is diagnosed during explicit apply/preflight only.' },
98
111
  { id: 'claude-project-mcp-readable', status: mcp.status === 'invalid' ? 'fail' : mcp.status === 'missing' ? 'not-configured' : 'pass', detail: mcp.message, ...(mcp.status === 'invalid' ? { remediation: 'Fix malformed .mcp.json before installing VGXNESS Claude support.' } : {}) },
99
112
  { id: 'claude-vgxness-mcp-entry', status: mcp.status === 'configured' ? 'pass' : mcp.status === 'conflicting' ? 'fail' : 'not-configured', detail: mcp.message, ...(mcp.status === 'conflicting' ? { remediation: 'Manually reconcile mcpServers.vgxness before applying VGXNESS Claude support.' } : {}) },
100
113
  { id: 'claude-agents-directory', status: agents.directoryExists ? 'pass' : 'not-configured', detail: agents.directoryExists ? 'Claude project agents directory exists.' : 'Claude project agents directory is missing; confirmed apply may create it.' },
101
- { id: 'claude-vgxness-agents', status: missingAgents.length === 0 && agents.agents.every((agent) => agent.status !== 'conflicting' && agent.status !== 'invalid') ? 'pass' : missingAgents.length > 0 ? 'not-configured' : 'fail', detail: missingAgents.length > 0 ? `Missing VGXNESS Claude agents: ${missingAgents.map((agent) => agent.agentName).join(', ')}.` : 'Expected VGXNESS Claude agent targets were inspected.' },
114
+ {
115
+ id: 'claude-vgxness-agents',
116
+ status: blockingAgents.length > 0 ? 'fail' : missingAgents.length > 0 ? 'not-configured' : 'pass',
117
+ detail: blockingAgents.length > 0
118
+ ? `Conflicting or invalid VGXNESS Claude agents: ${blockingAgents.map((agent) => agent.agentName).join(', ')}.`
119
+ : missingAgents.length > 0
120
+ ? `Missing VGXNESS Claude agents: ${missingAgents.map((agent) => agent.agentName).join(', ')}.`
121
+ : 'Expected VGXNESS Claude agent targets were inspected.',
122
+ },
102
123
  { id: 'claude-agent-frontmatter', status: badFrontmatter.length === 0 ? 'pass' : 'fail', detail: badFrontmatter.length === 0 ? 'Expected Claude agent frontmatter is valid.' : `Invalid or missing frontmatter for: ${badFrontmatter.map((agent) => agent.agentName).join(', ')}.` },
103
124
  { id: 'claude-agent-generated-metadata', status: badMarkers.length === 0 ? 'pass' : 'fail', detail: badMarkers.length === 0 ? 'Existing VGXNESS Claude agent files include generated metadata markers.' : `Missing VGXNESS generated marker for: ${badMarkers.map((agent) => agent.agentName).join(', ')}.` },
125
+ claudeProjectMemoryCheck(projectMemory),
104
126
  { id: 'claude-project-memory-advisory', status: memoryFiles.length === 0 ? 'pass' : 'warn', detail: memoryFiles.length === 0 ? 'No Claude project memory/settings advisory files were found.' : `Advisory only; VGXNESS will not modify: ${memoryFiles.join(', ')}.` },
105
127
  readonlySafetyCheck(before, snapshotPaths(checkedPathList, normalized.workspaceRoot)),
106
128
  ];
@@ -111,6 +133,37 @@ export class ProviderDoctorService {
111
133
  const recommendations = checks.flatMap((check) => (check.remediation === undefined ? [] : [check.remediation]));
112
134
  return { ok: true, value: { version: 1, kind: 'provider-doctor', project: normalized.project, providerAdapter: 'claude', scope: normalized.scope, workspaceRoot: normalized.workspaceRoot, status, payloadMode: normalized.payloadMode, overallStatus: status, checkCount: checks.length, passedCount: checks.filter((check) => check.status === 'pass').length, warningCount: checks.filter((check) => check.status === 'warn' || check.status === 'not-configured').length, errorCount: failedChecks.length, skippedCount: checks.filter((check) => check.status === 'skip').length, failedChecks, summary: summarizeDoctor(status, failedChecks.length, recommendations.length), recommendations, checks: compactChecksValue, checkedPaths, bytes: { originalBytes: Buffer.byteLength(JSON.stringify({ checks, checkedPaths: checkedPathList }), 'utf8'), compactBytes: Buffer.byteLength(JSON.stringify({ checks: compactChecksValue, checkedPaths }), 'utf8') }, verboseAvailable: normalized.payloadMode === 'compact', fullContentRef: `provider-doctor:claude:${normalized.workspaceRoot}`, generatedAt: 'read-only-snapshot', safety: PROVIDER_HEALTH_SAFETY } };
113
135
  }
136
+ getClaudeUserGlobalDoctor(normalized, canonicalScope = 'user', scopeWarnings = []) {
137
+ const mcp = inspectClaudeCodeUserMcpConfig(normalized.env);
138
+ const agents = inspectClaudeCodeAgents({ workspaceRoot: normalized.workspaceRoot, scope: 'user', env: normalized.env });
139
+ const userMemory = inspectClaudeUserMemory(normalized.env);
140
+ const checkedPathList = [mcp.path, agents.directoryPath, ...agents.agents.map((agent) => agent.path), userMemory.path];
141
+ const before = snapshotPaths(checkedPathList, normalized.workspaceRoot);
142
+ const missingAgents = agents.agents.filter((agent) => agent.status === 'missing');
143
+ const blockingAgents = agents.agents.filter((agent) => agent.status === 'conflicting' || agent.status === 'invalid');
144
+ const badFrontmatter = agents.agents.filter((agent) => agent.exists && agent.frontmatter === 'invalid');
145
+ const badMarkers = agents.agents.filter((agent) => agent.exists && !agent.generatedMarker);
146
+ const checks = [
147
+ { id: 'workspace-root', status: existsSync(normalized.workspaceRoot) ? 'pass' : 'fail', detail: `Workspace root ${existsSync(normalized.workspaceRoot) ? 'exists' : 'does not exist'}: ${normalized.workspaceRoot}` },
148
+ { id: 'provider-supported', status: 'pass', detail: `Claude ${canonicalScope} scope supports guarded ~/.claude.json MCP merge, user agents, and ~/.claude/CLAUDE.md managed block after confirmation/preflight.` },
149
+ { id: 'claude-scope', status: scopeWarnings.length === 0 ? 'pass' : 'warn', detail: scopeWarnings.join(' ') || `Claude scope resolved to canonical ${canonicalScope}.` },
150
+ { id: 'claude-cli-presence', status: 'warn', detail: 'Read-only doctor does not execute `claude --version`; no provider process was launched.' },
151
+ { id: 'claude-user-mcp-readable', status: mcp.status === 'invalid' ? 'fail' : mcp.status === 'missing' ? 'not-configured' : 'pass', detail: mcp.message, ...(mcp.status === 'invalid' ? { remediation: 'Fix malformed ~/.claude.json before installing VGXNESS Claude user support.' } : {}) },
152
+ { id: 'claude-user-vgxness-mcp-entry', status: mcp.status === 'configured' ? 'pass' : mcp.status === 'conflicting' ? 'fail' : 'not-configured', detail: mcp.message, ...(mcp.status === 'conflicting' ? { remediation: 'Manually reconcile mcpServers.vgxness in ~/.claude.json before applying VGXNESS Claude user support.' } : {}) },
153
+ { id: 'claude-user-agents-directory', status: agents.directoryExists ? 'pass' : 'not-configured', detail: agents.directoryExists ? 'Claude user agents directory exists.' : 'Claude user agents directory is missing; confirmed apply may create it.' },
154
+ { id: 'claude-user-vgxness-agents', status: blockingAgents.length > 0 ? 'fail' : missingAgents.length > 0 ? 'not-configured' : 'pass', detail: blockingAgents.length > 0 ? `Conflicting or invalid VGXNESS Claude user agents: ${blockingAgents.map((agent) => agent.agentName).join(', ')}.` : missingAgents.length > 0 ? `Missing VGXNESS Claude user agents: ${missingAgents.map((agent) => agent.agentName).join(', ')}.` : 'Expected VGXNESS Claude user agent targets were inspected.' },
155
+ { id: 'claude-user-agent-frontmatter', status: badFrontmatter.length === 0 ? 'pass' : 'fail', detail: badFrontmatter.length === 0 ? 'Expected Claude user agent frontmatter is valid.' : `Invalid or missing frontmatter for: ${badFrontmatter.map((agent) => agent.agentName).join(', ')}.` },
156
+ { id: 'claude-user-agent-generated-metadata', status: badMarkers.length === 0 ? 'pass' : 'fail', detail: badMarkers.length === 0 ? 'Existing VGXNESS Claude user agent files include generated metadata markers.' : `Missing VGXNESS generated marker for: ${badMarkers.map((agent) => agent.agentName).join(', ')}.` },
157
+ { ...claudeProjectMemoryCheck(userMemory), id: 'claude-user-memory-managed-block' },
158
+ readonlySafetyCheck(before, snapshotPaths(checkedPathList, normalized.workspaceRoot)),
159
+ ];
160
+ const status = rollupProviderDoctor(checks.map((check) => check.status));
161
+ const compactChecksValue = compactChecks(checks, normalized.payloadMode);
162
+ const failedChecks = checks.filter((check) => check.status === 'fail').map((check) => check.id);
163
+ const recommendations = checks.flatMap((check) => (check.remediation === undefined ? [] : [check.remediation]));
164
+ const checkedPaths = normalized.payloadMode === 'verbose' ? checkedPathList : checkedPathList.filter((path) => existsSync(path) || path === mcp.path || path === userMemory.path);
165
+ return { ok: true, value: { version: 1, kind: 'provider-doctor', project: normalized.project, providerAdapter: 'claude', scope: normalized.scope, workspaceRoot: normalized.workspaceRoot, status, payloadMode: normalized.payloadMode, overallStatus: status, checkCount: checks.length, passedCount: checks.filter((check) => check.status === 'pass').length, warningCount: checks.filter((check) => check.status === 'warn' || check.status === 'not-configured').length, errorCount: failedChecks.length, skippedCount: checks.filter((check) => check.status === 'skip').length, failedChecks, summary: summarizeDoctor(status, failedChecks.length, recommendations.length), recommendations, checks: compactChecksValue, checkedPaths, bytes: { originalBytes: Buffer.byteLength(JSON.stringify({ checks, checkedPaths: checkedPathList }), 'utf8'), compactBytes: Buffer.byteLength(JSON.stringify({ checks: compactChecksValue, checkedPaths }), 'utf8') }, verboseAvailable: normalized.payloadMode === 'compact', fullContentRef: `provider-doctor:claude:${canonicalScope}:${normalized.workspaceRoot}`, generatedAt: 'read-only-snapshot', safety: { ...PROVIDER_HEALTH_SAFETY, scopeCapabilities: CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES } } };
166
+ }
114
167
  }
115
168
  function summarizeDoctor(status, failedCount, recommendationCount) {
116
169
  if (status === 'healthy')
@@ -223,6 +276,19 @@ function readonlySafetyCheck(before, after) {
223
276
  }
224
277
  : { id: 'provider-config-readonly-safety', status: 'fail', detail: 'Provider config path existence or mtimes changed during read-only doctor.' };
225
278
  }
279
+ function claudeProjectMemoryCheck(state) {
280
+ const status = state.status === 'managed-current' ? 'pass' : state.status === 'managed-stale' ? 'warn' : state.status === 'blocked' ? 'fail' : 'not-configured';
281
+ return {
282
+ id: 'claude-project-memory-managed-block',
283
+ status,
284
+ detail: state.message,
285
+ ...(state.status === 'blocked'
286
+ ? { remediation: 'Manually reconcile VGXNESS Claude project-memory markers before installing.' }
287
+ : state.status === 'managed-current'
288
+ ? {}
289
+ : { remediation: 'Run confirmed Claude project install when ready.' }),
290
+ };
291
+ }
226
292
  function snapshotPaths(paths, workspaceRoot) {
227
293
  const unique = [...paths, `${workspaceRoot}/.vgx`];
228
294
  return Object.fromEntries(unique.map((path) => [path, existsSync(path) ? statSync(path).mtimeMs : false]));
@@ -11,6 +11,10 @@ export const PROVIDER_HEALTH_SAFETY = {
11
11
  runsRepair: false,
12
12
  executesProvider: false,
13
13
  };
14
+ export const CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES = ['status', 'doctor', 'change-plan'];
15
+ export function isUserGlobalScope(scope) {
16
+ return scope === 'personal' || scope === 'user';
17
+ }
14
18
  export const REQUIRED_PROVIDER_MCP_TOOLS = ['vgxness_provider_status', 'vgxness_provider_doctor', 'vgxness_provider_change_plan'];
15
19
  export const REQUIRED_PROVIDER_NATIVE_MCP_TOOLS = [
16
20
  'vgxness_provider_status',
@@ -2,10 +2,14 @@ import { existsSync, readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { inspectClaudeCodeAgents } from './claude-code-agent-config.js';
4
4
  import { claudeAdvisoryPaths, claudeMcpConfigPathStatus, claudeMcpEntryStatus, inspectClaudeCodeMcpConfig } from './claude-code-config.js';
5
+ import { inspectClaudeProjectMemory } from './claude-code-project-memory.js';
6
+ import { resolveClaudeCodeScope } from './claude-code-scope.js';
7
+ import { claudeUserMcpConfigPathStatus, claudeUserMcpEntryStatus, inspectClaudeCodeUserMcpConfig } from './claude-code-user-config.js';
8
+ import { inspectClaudeUserMemory } from './claude-code-user-memory.js';
5
9
  import { resolveOpenCodeMcpInstallTarget } from './client-install-opencode-contract.js';
6
10
  import { vgxnessOpenCodeDefaultAgent, vgxnessOpenCodePromptContractVersion, vgxnessOpenCodeSddSubagents } from './opencode-default-agent-config.js';
7
11
  import { buildCanonicalAgentManifestDiagnostic } from './provider-canonical-agent-manifest.js';
8
- import { normalizeProviderHealthInput, PROVIDER_HEALTH_SAFETY, providerHealthFailure, REQUIRED_PROVIDER_MCP_TOOLS, rollupProviderHealth, } from './provider-health-types.js';
12
+ import { normalizeProviderHealthInput, PROVIDER_HEALTH_SAFETY, CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES, providerHealthFailure, isUserGlobalScope, REQUIRED_PROVIDER_MCP_TOOLS, rollupProviderHealth, } from './provider-health-types.js';
9
13
  const projectConfigTargets = ['.opencode/opencode.json', 'opencode.json', '.opencode/opencode.jsonc', 'opencode.jsonc'];
10
14
  export class ProviderStatusService {
11
15
  deps;
@@ -87,24 +91,30 @@ export class ProviderStatusService {
87
91
  return { change, ...(status.ok ? { status: status.value } : {}), ...(next.ok ? { next: next.value } : {}) };
88
92
  }
89
93
  getClaudeStatus(normalized) {
94
+ const resolvedScope = resolveClaudeCodeScope(normalized.scope);
95
+ if (!resolvedScope.ok)
96
+ return resolvedScope;
97
+ if (isUserGlobalScope(normalized.scope) || resolvedScope.value.canonical === 'local')
98
+ return this.getClaudeUserGlobalStatus(normalized, resolvedScope.value.canonical, resolvedScope.value.warnings);
90
99
  const canonicalAgentManifest = (this.deps.canonicalAgentManifestDiagnostic ?? buildCanonicalAgentManifestDiagnostic)();
91
100
  const mcpState = inspectClaudeCodeMcpConfig(normalized.workspaceRoot);
92
101
  const agents = inspectClaudeCodeAgents(normalized.workspaceRoot);
93
- const paths = [claudeMcpConfigPathStatus(mcpState)];
102
+ const projectMemory = inspectClaudeProjectMemory(normalized.workspaceRoot);
103
+ const paths = [claudeMcpConfigPathStatus(mcpState), claudeProjectMemoryPathStatus(projectMemory)];
94
104
  const mcpEntry = claudeMcpEntryStatus(mcpState);
95
105
  const agentStatuses = agents.agents.map((agent) => (agent.status === 'managed' ? 'pass' : agent.status === 'missing' ? 'not-configured' : 'fail'));
96
106
  const advisory = claudeAdvisoryPaths(normalized.workspaceRoot).filter((path) => existsSync(path));
97
- const configStatus = claudeConfigHealthStatus([paths[0]?.status ?? 'not-configured', ...agentStatuses]);
107
+ const configStatus = claudeConfigHealthStatus([...paths.map((path) => path.status), ...agentStatuses]);
98
108
  const status = rollupProviderHealth([canonicalAgentManifest.status, configStatus]);
99
109
  const sdd = normalized.change.length > 0 ? this.readSdd(normalized.project, normalized.change) : undefined;
100
- const checkedPaths = normalized.payloadMode === 'verbose' ? [mcpState.path, agents.directoryPath, ...agents.agents.map((agent) => agent.path), ...claudeAdvisoryPaths(normalized.workspaceRoot)] : [mcpState.path, ...agents.agents.filter((agent) => agent.exists || agent.status !== 'missing').map((agent) => agent.path), ...advisory];
101
- const tools = requiredToolPresence();
102
- const verboseShape = { config: { status: configStatus, paths, mcpEntry }, canonicalAgentManifest, agents, advisory, sdd, mcpRequiredTools: tools };
103
- const compactShape = { config: { status: configStatus, paths: compactPaths(paths, 'compact'), mcpEntry: compactMcpEntry(mcpEntry, 'compact') }, canonicalAgentManifest, agentSummary: summarizeClaudeAgents(agents), advisory, sdd: sdd === undefined ? undefined : compactSdd(sdd, 'compact'), mcpRequiredTools: tools };
110
+ const checkedPaths = normalized.payloadMode === 'verbose' ? [mcpState.path, agents.directoryPath, ...agents.agents.map((agent) => agent.path), projectMemory.path, ...claudeAdvisoryPaths(normalized.workspaceRoot)] : [mcpState.path, projectMemory.path, ...agents.agents.filter((agent) => agent.exists || agent.status !== 'missing').map((agent) => agent.path), ...advisory];
111
+ const tools = [...requiredToolPresence(), { tool: 'claude-cli', present: false, diagnostic: 'Read-only status does not execute `claude --version`; CLI presence is checked during explicit apply/preflight only.' }];
112
+ const verboseShape = { config: { status: configStatus, paths, mcpEntry }, canonicalAgentManifest, agents, projectMemory, advisory, sdd, mcpRequiredTools: tools };
113
+ const compactShape = { config: { status: configStatus, paths: compactPaths(paths, 'compact'), mcpEntry: compactMcpEntry(mcpEntry, 'compact') }, canonicalAgentManifest, agentSummary: summarizeClaudeAgents(agents), projectMemory: { status: projectMemory.status, action: projectMemory.action }, advisory, sdd: sdd === undefined ? undefined : compactSdd(sdd, 'compact'), mcpRequiredTools: tools };
104
114
  const originalBytes = Buffer.byteLength(JSON.stringify(verboseShape), 'utf8');
105
115
  const compactBytes = Buffer.byteLength(JSON.stringify(compactShape), 'utf8');
106
116
  const issueCount = [canonicalAgentManifest.status, configStatus, ...agentStatuses].filter((item) => item === 'fail' || item === 'not-configured').length;
107
- const warningCount = advisory.length;
117
+ const warningCount = advisory.length + resolvedScope.value.warnings.length + paths.filter((path) => path.status === 'warn').length;
108
118
  return {
109
119
  ok: true,
110
120
  value: {
@@ -136,6 +146,53 @@ export class ProviderStatusService {
136
146
  },
137
147
  };
138
148
  }
149
+ getClaudeUserGlobalStatus(normalized, canonicalScope = 'user', scopeWarnings = []) {
150
+ const canonicalAgentManifest = (this.deps.canonicalAgentManifestDiagnostic ?? buildCanonicalAgentManifestDiagnostic)();
151
+ const mcpState = inspectClaudeCodeUserMcpConfig(normalized.env);
152
+ const agents = inspectClaudeCodeAgents({ workspaceRoot: normalized.workspaceRoot, scope: 'user', env: normalized.env });
153
+ const userMemory = inspectClaudeUserMemory(normalized.env);
154
+ const userMemoryPathStatus = { ...claudeProjectMemoryPathStatus(userMemory), label: 'user ~/.claude/CLAUDE.md managed block' };
155
+ const paths = [claudeUserMcpConfigPathStatus(mcpState), userMemoryPathStatus];
156
+ const mcpEntry = claudeUserMcpEntryStatus(mcpState);
157
+ const agentStatuses = agents.agents.map((agent) => (agent.status === 'managed' ? 'pass' : agent.status === 'missing' ? 'not-configured' : 'fail'));
158
+ const configStatus = claudeConfigHealthStatus([...paths.map((path) => path.status), ...agentStatuses]);
159
+ const tools = [...requiredToolPresence(), { tool: 'claude-cli', present: false, diagnostic: 'Read-only status does not execute `claude --version`; no Claude Code process was launched.' }];
160
+ const status = rollupProviderHealth([canonicalAgentManifest.status, configStatus]);
161
+ const sdd = normalized.change.length > 0 ? this.readSdd(normalized.project, normalized.change) : undefined;
162
+ const checkedPaths = normalized.payloadMode === 'verbose' ? [mcpState.path, agents.directoryPath, ...agents.agents.map((agent) => agent.path), userMemory.path] : [mcpState.path, userMemory.path, ...agents.agents.filter((agent) => agent.exists || agent.status !== 'missing').map((agent) => agent.path)];
163
+ const verboseShape = { config: { status: configStatus, paths, mcpEntry }, canonicalAgentManifest, agents, userMemory: { status: userMemory.status, action: userMemory.action }, scopeCapabilities: CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES, scopeWarnings, sdd, mcpRequiredTools: tools };
164
+ const compactShape = { config: { status: configStatus, paths: compactPaths(paths, 'compact'), mcpEntry: compactMcpEntry(mcpEntry, 'compact') }, canonicalAgentManifest, agentSummary: summarizeClaudeAgents(agents), userMemory: { status: userMemory.status, action: userMemory.action }, scopeCapabilities: CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES, sdd: sdd === undefined ? undefined : compactSdd(sdd, 'compact'), mcpRequiredTools: tools };
165
+ return {
166
+ ok: true,
167
+ value: {
168
+ version: 1,
169
+ kind: 'provider-status',
170
+ project: normalized.project,
171
+ providerAdapter: 'claude',
172
+ scope: normalized.scope,
173
+ workspaceRoot: normalized.workspaceRoot,
174
+ status,
175
+ payloadMode: normalized.payloadMode,
176
+ overallStatus: status,
177
+ inspectedPaths: checkedPaths,
178
+ issueCount: [canonicalAgentManifest.status, configStatus, ...agentStatuses].filter((item) => item === 'fail' || item === 'not-configured').length,
179
+ warningCount: scopeWarnings.length + paths.filter((path) => path.status === 'warn').length + 1,
180
+ summary: summarizeClaudeStatus(status, mcpEntry),
181
+ nextAction: nextActionFor(status, mcpEntry, sdd?.next),
182
+ checkedPaths,
183
+ canonicalAgentManifest,
184
+ config: { status: configStatus, paths: compactPaths(paths, normalized.payloadMode), mcpEntry: compactMcpEntry(mcpEntry, normalized.payloadMode) },
185
+ ...(sdd === undefined ? {} : { sdd: compactSdd(sdd, normalized.payloadMode) }),
186
+ mcpRequiredTools: tools,
187
+ originalBytes: Buffer.byteLength(JSON.stringify(verboseShape), 'utf8'),
188
+ compactBytes: Buffer.byteLength(JSON.stringify(compactShape), 'utf8'),
189
+ verboseAvailable: normalized.payloadMode === 'compact',
190
+ fullContentRef: `provider-status:claude:user-global:${normalized.workspaceRoot}`,
191
+ generatedAt: 'read-only-snapshot',
192
+ safety: { ...PROVIDER_HEALTH_SAFETY, scopeCapabilities: CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES },
193
+ },
194
+ };
195
+ }
139
196
  }
140
197
  function summarizeClaudeAgents(agents) {
141
198
  return { expected: agents.agents.length, managed: agents.agents.filter((agent) => agent.status === 'managed').length, missing: agents.agents.filter((agent) => agent.status === 'missing').length, conflicting: agents.agents.filter((agent) => agent.status === 'conflicting' || agent.status === 'invalid').length };
@@ -149,6 +206,18 @@ function claudeConfigHealthStatus(statuses) {
149
206
  return 'warn';
150
207
  return 'pass';
151
208
  }
209
+ function claudeProjectMemoryPathStatus(state) {
210
+ const status = state.status === 'managed-current' ? 'pass' : state.status === 'managed-stale' ? 'warn' : state.status === 'blocked' ? 'fail' : 'not-configured';
211
+ return {
212
+ label: 'project CLAUDE.md managed block',
213
+ path: state.path,
214
+ exists: state.exists,
215
+ readable: state.status !== 'blocked' || state.reason !== 'unreadable',
216
+ parsed: state.status === 'managed-current' || state.status === 'managed-stale',
217
+ status,
218
+ detail: state.message,
219
+ };
220
+ }
152
221
  function compactPaths(paths, mode) {
153
222
  if (mode === 'verbose')
154
223
  return paths;
@@ -90,6 +90,7 @@ export function toInternalVgxMcpToolName(toolName) {
90
90
  return EXPOSED_TO_INTERNAL_TOOL_NAMES[toolName];
91
91
  }
92
92
  const scopes = ['project', 'personal'];
93
+ const providerScopes = ['project', 'personal', 'local', 'user'];
93
94
  const agentModes = ['agent', 'subagent'];
94
95
  const memoryTypes = ['architecture', 'decision', 'bugfix', 'pattern', 'config', 'discovery', 'learning', 'preference', 'manual'];
95
96
  const activityKinds = ['prompt', 'tool_call', 'artifact', 'summary', 'error'];
@@ -429,7 +430,7 @@ export const VGX_MCP_TOOL_INPUT_SCHEMAS = {
429
430
  vgxness_provider_status: z
430
431
  .object({
431
432
  project: z.string().min(1).optional(),
432
- scope: z.enum(scopes).optional(),
433
+ scope: z.enum(providerScopes).optional(),
433
434
  providerAdapter: z.enum(['opencode', 'claude']).optional(),
434
435
  workspaceRoot: z.string().min(1).optional(),
435
436
  change: z.string().min(1).optional(),
@@ -439,7 +440,7 @@ export const VGX_MCP_TOOL_INPUT_SCHEMAS = {
439
440
  vgxness_provider_doctor: z
440
441
  .object({
441
442
  project: z.string().min(1).optional(),
442
- scope: z.enum(scopes).optional(),
443
+ scope: z.enum(providerScopes).optional(),
443
444
  providerAdapter: z.enum(['opencode', 'claude']).optional(),
444
445
  workspaceRoot: z.string().min(1).optional(),
445
446
  expectedPromptContractVersion: z.number().int().positive().optional(),
@@ -449,7 +450,7 @@ export const VGX_MCP_TOOL_INPUT_SCHEMAS = {
449
450
  vgxness_provider_change_plan: z
450
451
  .object({
451
452
  project: z.string().min(1).optional(),
452
- scope: z.enum(scopes).optional(),
453
+ scope: z.enum(providerScopes).optional(),
453
454
  provider: z.enum(providerChangePlanProviders),
454
455
  changeType: z.enum(providerChangePlanTypes),
455
456
  workspaceRoot: z.string().min(1),