vgxness 1.6.0 → 1.8.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.
@@ -69,7 +69,7 @@ Areas:
69
69
  Use --overwrite-vgxness (alias --reinstall) to reinstall only VGXNESS-managed OpenCode entries while preserving unrelated config; --yes is still required to write.
70
70
  It writes only after --yes. The default target is $HOME/.config/opencode/opencode.json; use --scope project to target .opencode/opencode.json explicitly.
71
71
  Project OpenCode config can override user config. Plans are read-only; applies refuse unsafe existing config and create backups before merge.
72
- Claude support is secondary. Claude scopes are local|project|user; compatibility aliases personal/global map to user with warnings. Plans show Claude CLI argv, project .mcp.json compatibility, agents, and guarded project-root CLAUDE.md managed memory separately. Project scope confirmed applies write .mcp.json, .claude/agents/*.md, and the CLAUDE.md managed block as needed. Confirmed Claude writes/CLI execution require VGXNESS run preflight metadata (--run-id, with optional --phase/--agent-id). VGXNESS does not manually read/write ~/.claude.json or private Claude config; status/doctor/change-plan are read-only and do not execute Claude Code.
72
+ Claude support is first-class for guarded MCP setup. Claude scopes are local|project|user; compatibility aliases personal/global map to user with warnings. Plans show Claude CLI argv, project .mcp.json compatibility, user ~/.claude.json MCP merge, agents, and guarded CLAUDE.md managed memory separately. Project scope confirmed applies write .mcp.json, .claude/agents/*.md, and the project-root CLAUDE.md managed block as needed. User/global confirmed applies narrowly merge only mcpServers.vgxness in ~/.claude.json, write ~/.claude/agents/*.md, and manage only the VGXNESS block in ~/.claude/CLAUDE.md. Confirmed Claude writes/CLI execution require VGXNESS run preflight metadata (--run-id, with optional --phase/--agent-id). Status/doctor/change-plan are read-only and do not execute Claude Code.
73
73
 
74
74
  skills register --project <name> --name <name> --description <text>
75
75
  skills list [--project <name>] [--scope project|personal]
@@ -98,7 +98,7 @@ Areas:
98
98
  No args in an interactive TTY opens the OpenTUI main menu.
99
99
  No args without a TTY prints static safe setup guidance and exits 0 without opening project state.
100
100
  Setup TUI may launch without --project; Installation remains available and project-scoped checks are deferred while project screens render project-required recovery states.
101
- Provider setup support: OpenCode supported primary/default; Claude supported secondary for CLI MCP registration, project compatibility, and project/user agent planning; Antigravity placeholder; Custom/future extension point.
101
+ Provider setup support: OpenCode first-class supported default guided install; Claude first-class supported guarded install for CLI MCP registration, project compatibility, and project/user agent planning; Antigravity placeholder; Custom/future extension point.
102
102
  Provider config writes/install/apply are external-only and require explicit confirmation.
103
103
 
104
104
  sdd status --project <name> --change <id> [--json]
@@ -41,5 +41,10 @@ function renderDefaults(status) {
41
41
  }
42
42
  function renderProvider(provider) {
43
43
  const safety = provider.actions.some((action) => action.safety.writesProviderConfig) ? 'external write action flagged' : 'no provider writes';
44
- return `${provider.displayName} ${provider.status}/${provider.supportLevel} (${safety})`;
44
+ return `${provider.displayName} ${provider.status}/${renderSupportLevel(provider.supportLevel)} (${safety})`;
45
+ }
46
+ function renderSupportLevel(supportLevel) {
47
+ if (supportLevel === 'supported-primary' || supportLevel === 'supported-secondary')
48
+ return 'supported';
49
+ return supportLevel;
45
50
  }
@@ -14,7 +14,7 @@ const statCards = [
14
14
  { label: 'SDD', value: 'guided', badge: '', description: 'MCP flow' },
15
15
  ];
16
16
  const statusSnapshotLines = [
17
- 'OpenCode primary; dashboard does not call providers.',
17
+ 'OpenCode supported; dashboard does not call providers.',
18
18
  'Advanced checks stay explicit: setup status, mcp doctor opencode.',
19
19
  ];
20
20
  const optionCopy = {
@@ -31,7 +31,7 @@ export function setupTuiViewModelFromPlan(plan, status, state) {
31
31
  workspaceRootLabel: compactPath(workspaceRoot, 72),
32
32
  readinessLabel: label(readiness),
33
33
  readinessBadge: readinessBadge(readiness),
34
- providerLabel: isOpenCode ? 'OpenCode' : isClaude ? 'Claude (supported secondary)' : 'Manual / none',
34
+ providerLabel: isOpenCode ? 'OpenCode' : isClaude ? 'Claude (first-class supported)' : 'Manual / none',
35
35
  databaseLabel: plan === undefined ? 'pending' : plan.db.mode,
36
36
  databasePathLabel: compactPath(databasePath, 72),
37
37
  databaseSourceLabel: String(databaseSource),
@@ -47,8 +47,8 @@ export function setupTuiViewModelFromPlan(plan, status, state) {
47
47
  opencodeActionLabel: isOpenCode ? opencodeActionLabel(opencode?.action) : isClaude ? 'No guided setup write; use guarded Claude install with run preflight.' : 'No automatic provider write; use manual setup guidance.',
48
48
  memoryPathExplanation: memoryExplanation,
49
49
  providerInstallabilityLabel: isOpenCode
50
- ? 'OpenCode is the default guided install provider. Claude is supported secondary via guarded mcp install claude --scope project --yes --run-id <id>.'
51
- : 'Manual / none is read-only guidance. Claude remains supported secondary through explicit guarded apply outside this guided OpenCode flow.',
50
+ ? 'OpenCode is the default guided install provider. Claude is first-class supported via guarded mcp install claude --scope project --yes --run-id <id>.'
51
+ : 'Manual / none is read-only guidance. Claude remains supported through explicit guarded apply outside this guided OpenCode flow.',
52
52
  agentReadinessLabel: agentReadiness.label,
53
53
  agentReadinessDetail: agentReadiness.detail,
54
54
  plannedActions: plan?.actions.map((action) => ({
@@ -79,7 +79,7 @@ export function setupTuiViewModelFromPlan(plan, status, state) {
79
79
  ],
80
80
  providerChoices: [
81
81
  choice('provider:opencode', 'OpenCode', 'Default guided install provider; writes still require final confirmation.', (selections?.provider ?? plan?.provider ?? 'opencode') === 'opencode', state?.focusedChoiceId, [badgeLabels.recommended]),
82
- choice('provider:claude-supported-secondary', 'Claude (supported secondary)', 'Claude CLI MCP registration and project compatibility apply only via guarded mcp install claude --scope project --yes --run-id <id>; not default.', (selections?.provider ?? plan?.provider) === 'claude', state?.focusedChoiceId, ['[supported secondary]', badgeLabels.readOnly]),
82
+ choice('provider:claude-supported', 'Claude (first-class supported)', 'Claude CLI MCP registration and project compatibility apply only via guarded mcp install claude --scope project --yes --run-id <id>; explicit install path.', (selections?.provider ?? plan?.provider) === 'claude', state?.focusedChoiceId, ['[supported]', badgeLabels.readOnly]),
83
83
  choice('provider:none', 'Manual / none', 'No automatic provider config write; follow manual setup guidance.', (selections?.provider ?? plan?.provider) === 'none', state?.focusedChoiceId, [badgeLabels.manual, badgeLabels.readOnly]),
84
84
  ],
85
85
  scopeChoices: isOpenCode
@@ -106,13 +106,13 @@ function previewDetailLines(input) {
106
106
  const opencode = plan?.opencode;
107
107
  const actions = plan?.actions.map((action) => `Planned action: ${action.description}${action.targetPath === undefined ? '' : ` Target: ${compactPath(action.targetPath, 72)}`}${action.backupRequired ? ' Backup required.' : ''}`) ?? ['Planned action: create read-only preview plan.'];
108
108
  return [
109
- `Provider: ${input.provider === 'opencode' ? 'OpenCode [recommended default]' : input.provider === 'claude' ? 'Claude [supported secondary] [guarded explicit apply]' : 'Manual / none [manual] [read-only]'}`,
109
+ `Provider: ${input.provider === 'opencode' ? 'OpenCode [recommended default]' : input.provider === 'claude' ? 'Claude [first-class supported] [guarded explicit apply]' : 'Manual / none [manual] [read-only]'}`,
110
110
  `Memory path: ${plan?.db.mode ?? 'pending'} at ${compactPath(input.databasePath, 72)} (source: ${String(input.databaseSource)})`,
111
111
  `Memory guidance: ${memoryPathExplanation(plan, input.databasePath, input.databaseSource)}`,
112
112
  `Scope: ${input.isOpenCode ? (opencode?.scope ?? 'user') : 'disabled for manual/none provider'}`,
113
113
  `Install mode: ${input.isOpenCode ? (opencode?.installsAgents === false ? 'mcp-only' : 'mcp-plus-agents') : 'disabled for manual/none provider'}`,
114
114
  `Reinstall VGXNESS entries: ${input.isOpenCode ? String(opencode?.overwriteVgxness ?? false) : 'disabled for manual/none provider'}`,
115
- `Provider installability: ${input.isOpenCode ? 'OpenCode installable after final confirmation; Claude supported secondary requires guarded mcp install claude --scope project --yes --run-id <id>.' : 'No guided provider install from this selection; Claude supported secondary uses guarded explicit apply.'}`,
115
+ `Provider installability: ${input.isOpenCode ? 'OpenCode installable after final confirmation; Claude first-class support requires guarded mcp install claude --scope project --yes --run-id <id>.' : 'No guided provider install from this selection; Claude uses guarded explicit apply.'}`,
116
116
  `Agent readiness: ${agentReadinessFromPlan(plan)}`,
117
117
  `Target config: ${input.isOpenCode && opencode?.targetPath !== undefined ? compactPath(opencode.targetPath, 72) : 'none; no provider config will be written'}`,
118
118
  `Safety: ${input.isOpenCode ? '[will write after confirm] only on final confirmation' : '[read-only] manual/no-provider-write mode'}`,
@@ -132,7 +132,7 @@ function helpLines(screen) {
132
132
  'Next/back: Tab continues; Shift+Tab goes back. Enter continues on review and confirms only on final confirmation.',
133
133
  'Cancel/close: q or Esc cancels setup; when help is open, ?/h toggles it closed.',
134
134
  reviewLine,
135
- 'Provider support: OpenCode is the default guided install provider; Claude is supported secondary for guarded explicit apply via mcp install claude --scope project --yes --run-id <id>; Manual / none writes no provider config.',
135
+ 'Provider support: OpenCode is the default guided install provider; Claude is first-class supported for guarded explicit apply via mcp install claude --scope project --yes --run-id <id>; Manual / none writes no provider config.',
136
136
  'Agent readiness: the preview checks vgxness-manager/SDD readiness guidance; preview screens never seed agents.',
137
137
  'No-write guarantee: no provider config is written before explicit final confirmation.',
138
138
  ];
@@ -0,0 +1,55 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { isManagedClaudeCodeMcpServer } from './claude-code-config.js';
4
+ import { safeHomeDirectory } from './claude-code-agent-config.js';
5
+ export function resolveClaudeCodeUserMcpJsonPath(env = process.env) {
6
+ return join(safeHomeDirectory(env), '.claude.json');
7
+ }
8
+ export function inspectClaudeCodeUserMcpConfig(env = process.env) {
9
+ const path = resolveClaudeCodeUserMcpJsonPath(env);
10
+ if (!existsSync(path))
11
+ return { status: 'missing', path, exists: false, parsed: false, message: 'Claude user ~/.claude.json does not exist.' };
12
+ try {
13
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
14
+ if (!isRecord(parsed))
15
+ return { status: 'invalid', path, exists: true, parsed: false, message: 'Claude user ~/.claude.json must be a JSON object.' };
16
+ if (parsed.mcpServers !== undefined && !isRecord(parsed.mcpServers))
17
+ return { status: 'invalid', path, exists: true, parsed: false, message: 'Claude user ~/.claude.json mcpServers must be a JSON object.' };
18
+ const entry = isRecord(parsed.mcpServers) ? parsed.mcpServers.vgxness : undefined;
19
+ if (entry === undefined)
20
+ return { status: 'stale', path, exists: true, parsed: true, config: parsed, message: 'Claude user ~/.claude.json is readable but mcpServers.vgxness is missing.' };
21
+ if (isManagedClaudeCodeMcpServer(entry))
22
+ return { status: 'configured', path, exists: true, parsed: true, config: parsed, message: 'Claude user ~/.claude.json has a managed mcpServers.vgxness entry.' };
23
+ return { status: 'conflicting', path, exists: true, parsed: true, config: parsed, message: 'Claude user ~/.claude.json has a conflicting mcpServers.vgxness entry.' };
24
+ }
25
+ catch (cause) {
26
+ const message = cause instanceof Error ? cause.message : String(cause);
27
+ return { status: 'invalid', path, exists: true, parsed: false, message: `Claude user ~/.claude.json could not be read or parsed: ${message}` };
28
+ }
29
+ }
30
+ export function mergeClaudeCodeUserMcpConfig(existing, server) {
31
+ return { ...existing, mcpServers: { ...(isRecord(existing.mcpServers) ? existing.mcpServers : {}), vgxness: server } };
32
+ }
33
+ export function claudeUserMcpConfigPathStatus(state) {
34
+ return {
35
+ label: 'user ~/.claude.json',
36
+ path: state.path,
37
+ exists: state.exists,
38
+ readable: state.status !== 'invalid',
39
+ parsed: state.parsed,
40
+ status: state.status === 'configured' ? 'pass' : state.status === 'invalid' || state.status === 'conflicting' ? 'fail' : 'not-configured',
41
+ detail: state.message,
42
+ };
43
+ }
44
+ export function claudeUserMcpEntryStatus(state) {
45
+ if (state.status === 'configured')
46
+ return { configured: true, status: 'pass', serverName: 'vgxness', enabled: true, detail: state.message };
47
+ if (state.status === 'conflicting')
48
+ return { configured: true, status: 'fail', serverName: 'vgxness', detail: state.message };
49
+ if (state.status === 'invalid')
50
+ return { configured: false, status: 'fail', serverName: 'vgxness', detail: state.message };
51
+ return { configured: false, status: 'not-configured', serverName: 'vgxness', detail: state.message };
52
+ }
53
+ function isRecord(value) {
54
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
55
+ }
@@ -0,0 +1,90 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { projectCanonicalAgentManifestToClaudeProjectMemory } from '../agents/canonical-agent-projection.js';
4
+ import { safeHomeDirectory } from './claude-code-agent-config.js';
5
+ export const claudeUserMemoryBeginMarker = '<!-- BEGIN VGXNESS-MANAGED CLAUDE USER MEMORY -->';
6
+ export const claudeUserMemoryEndMarker = '<!-- END VGXNESS-MANAGED CLAUDE USER MEMORY -->';
7
+ export function resolveClaudeUserMemoryPath(env = process.env) {
8
+ return join(safeHomeDirectory(env), '.claude', 'CLAUDE.md');
9
+ }
10
+ export function renderClaudeUserMemoryBlock() {
11
+ const projection = projectCanonicalAgentManifestToClaudeProjectMemory();
12
+ return [
13
+ claudeUserMemoryBeginMarker,
14
+ `<!-- VGXNESS-GENERATED owner=vgxness provider=claude artifact=claude-user-memory safe-update=true defaultAgent=${projection.defaultAgent} promptContractVersion=${projection.promptContractVersion} canonicalSource=canonical-agent-manifest repositoryInstructions=${projection.repositoryInstructions} -->`,
15
+ '',
16
+ '# VGXNESS Claude User Memory',
17
+ '',
18
+ 'Global/user Claude Code guidance installed by VGXNESS. Use the VGXNESS MCP server for SDD, runs, memory, provider status, and governance-aware workflow progress.',
19
+ '',
20
+ ...projection.guidance.slice(1).map((line) => `- ${line}`),
21
+ '',
22
+ claudeUserMemoryEndMarker,
23
+ ].join('\n');
24
+ }
25
+ export function inspectClaudeUserMemory(env = process.env) {
26
+ const path = resolveClaudeUserMemoryPath(env);
27
+ if (!existsSync(path))
28
+ return { status: 'missing', path, exists: false, action: 'create', backupRequired: false, message: 'Claude user memory ~/.claude/CLAUDE.md does not exist.' };
29
+ let contents;
30
+ try {
31
+ const bytes = readFileSync(path);
32
+ contents = bytes.toString('utf8');
33
+ if (!Buffer.from(contents, 'utf8').equals(bytes))
34
+ return blocked(path, true, 'non_utf8', 'Claude user memory is not valid UTF-8; refusing safe managed-block updates.');
35
+ }
36
+ catch (cause) {
37
+ return blocked(path, true, 'unreadable', `Claude user memory could not be read: ${cause instanceof Error ? cause.message : String(cause)}`);
38
+ }
39
+ const beginCount = countOccurrences(contents, claudeUserMemoryBeginMarker);
40
+ const endCount = countOccurrences(contents, claudeUserMemoryEndMarker);
41
+ if (beginCount === 0 && endCount === 0)
42
+ return { status: 'unmanaged', path, exists: true, action: 'append-managed-block', backupRequired: true, message: 'Claude user memory exists without a VGXNESS managed block; apply would append one after backup.', contents };
43
+ if (beginCount === 0)
44
+ return blocked(path, true, 'missing_begin_marker', 'Claude user memory has an end marker without a begin marker.');
45
+ if (endCount === 0)
46
+ return blocked(path, true, 'missing_end_marker', 'Claude user memory has a begin marker without an end marker.');
47
+ if (beginCount > 1 || endCount > 1)
48
+ return blocked(path, true, 'duplicate_markers', 'Claude user memory has duplicate VGXNESS managed-block markers.');
49
+ const start = contents.indexOf(claudeUserMemoryBeginMarker);
50
+ const endMarkerStart = contents.indexOf(claudeUserMemoryEndMarker);
51
+ if (endMarkerStart < start)
52
+ return blocked(path, true, 'reordered_markers', 'Claude user memory end marker appears before begin marker.');
53
+ const end = endMarkerStart + claudeUserMemoryEndMarker.length;
54
+ const inner = contents.slice(start + claudeUserMemoryBeginMarker.length, endMarkerStart);
55
+ if (inner.includes(claudeUserMemoryBeginMarker) || inner.includes(claudeUserMemoryEndMarker))
56
+ return blocked(path, true, 'nested_markers', 'Claude user memory has nested VGXNESS managed-block markers.');
57
+ if (!inner.includes('owner=vgxness') || !inner.includes('provider=claude') || !inner.includes('artifact=claude-user-memory') || !inner.includes('safe-update=true'))
58
+ return blocked(path, true, 'conflicting_ownership', 'Claude user memory managed block is not a VGXNESS-owned user-memory block.');
59
+ const expected = renderClaudeUserMemoryBlock();
60
+ const actual = contents.slice(start, end);
61
+ if (actual === expected)
62
+ return { status: 'managed-current', path, exists: true, action: 'none', backupRequired: false, message: 'Claude user memory has the current VGXNESS managed block.', blockRange: { start, end }, contents };
63
+ return { status: 'managed-stale', path, exists: true, action: 'update-managed-block', backupRequired: true, message: 'Claude user memory has a stale VGXNESS managed block; apply would update only that block after backup.', blockRange: { start, end }, staleReasons: ['managed_block_differs_from_canonical_render'], contents };
64
+ }
65
+ export function mergeClaudeUserMemory(state) {
66
+ const block = renderClaudeUserMemoryBlock();
67
+ if (state.status === 'missing')
68
+ return { ok: true, value: { path: state.path, contents: `${block}\n` } };
69
+ if (state.status === 'unmanaged') {
70
+ const separator = state.contents.length === 0 ? '' : state.contents.endsWith('\n') ? '\n' : '\n\n';
71
+ return { ok: true, value: { path: state.path, contents: `${state.contents}${separator}${block}\n` } };
72
+ }
73
+ if (state.status === 'managed-stale')
74
+ return { ok: true, value: { path: state.path, contents: state.contents.slice(0, state.blockRange.start) + block + state.contents.slice(state.blockRange.end) } };
75
+ if (state.status === 'managed-current')
76
+ return { ok: true, value: { path: state.path, contents: state.contents } };
77
+ return { ok: false, error: { code: 'validation_failed', message: state.message } };
78
+ }
79
+ function blocked(path, exists, reason, message) {
80
+ return { status: 'blocked', path, exists, action: 'blocked', backupRequired: false, reason, message };
81
+ }
82
+ function countOccurrences(contents, needle) {
83
+ let count = 0;
84
+ let index = contents.indexOf(needle);
85
+ while (index !== -1) {
86
+ count += 1;
87
+ index = contents.indexOf(needle, index + needle.length);
88
+ }
89
+ return count;
90
+ }
@@ -3,6 +3,8 @@ import { buildClaudeCodeMcpAddCommand } from './claude-code-cli.js';
3
3
  import { createClaudeCodeMcpDoctorCommand, createClaudeCodeMcpServerConfig, inspectClaudeCodeMcpConfig, resolveClaudeCodeMcpJsonPath } from './claude-code-config.js';
4
4
  import { inspectClaudeProjectMemory } from './claude-code-project-memory.js';
5
5
  import { resolveClaudeCodeScope } from './claude-code-scope.js';
6
+ import { inspectClaudeCodeUserMcpConfig, resolveClaudeCodeUserMcpJsonPath } from './claude-code-user-config.js';
7
+ import { inspectClaudeUserMemory } from './claude-code-user-memory.js';
6
8
  export function planClaudeCodeMcpInstall(input) {
7
9
  const source = input.databasePathSource ?? 'flag';
8
10
  const server = createClaudeCodeMcpServerConfig(input.databasePath, source);
@@ -14,8 +16,11 @@ export function planClaudeCodeMcpInstall(input) {
14
16
  if (!cliCommand.ok)
15
17
  return refused(input, server, 'unsupported_scope', cliCommand.error.message, [], [], overwriteVgxness);
16
18
  if (resolvedScope.value.canonical !== 'project') {
17
- const targets = [{ kind: 'cli-mcp-registration', scope: resolvedScope.value.canonical, command: cliCommand.value, action: 'register' }];
18
- return { ...base(input, server, targets, [], false, overwriteVgxness, resolvedScope.value.canonical, cliCommand.value, resolvedScope.value.warnings), status: 'would_install' };
19
+ if (resolvedScope.value.canonical === 'local') {
20
+ const targets = [{ kind: 'cli-mcp-registration', scope: resolvedScope.value.canonical, command: cliCommand.value, action: 'register' }];
21
+ return { ...base(input, server, targets, [], false, overwriteVgxness, resolvedScope.value.canonical, cliCommand.value, resolvedScope.value.warnings), status: 'would_install' };
22
+ }
23
+ return planUserInstall(input, server, overwriteVgxness, cliCommand.value, resolvedScope.value.warnings);
19
24
  }
20
25
  let mcpPath;
21
26
  try {
@@ -61,6 +66,40 @@ export function planClaudeCodeMcpInstall(input) {
61
66
  export function expectedClaudeCodeRenderedAgents(workspaceRoot) {
62
67
  return expectedClaudeCodeAgentFiles(workspaceRoot).map((agent) => ({ path: agent.path, agentName: agent.name, contents: renderClaudeCodeAgentMarkdown(agent) }));
63
68
  }
69
+ export function expectedClaudeCodeRenderedUserAgents(workspaceRoot, env) {
70
+ return expectedClaudeCodeAgentFiles({ workspaceRoot, scope: 'user', ...(env === undefined ? {} : { env }) }).map((agent) => ({ path: agent.path, agentName: agent.name, contents: renderClaudeCodeAgentMarkdown(agent) }));
71
+ }
72
+ function planUserInstall(input, server, overwriteVgxness, cliCommand, scopeWarnings) {
73
+ const mcpState = inspectClaudeCodeUserMcpConfig(input.env);
74
+ const targets = [{ kind: 'cli-mcp-registration', scope: 'user', command: cliCommand, action: 'register' }];
75
+ const preservedTopLevelKeys = mcpState.parsed ? Object.keys(mcpState.config) : [];
76
+ if (mcpState.status === 'missing')
77
+ targets.push({ kind: 'user-mcp-json', path: mcpState.path, external: true, action: 'create' });
78
+ else if (mcpState.status === 'stale')
79
+ targets.push({ kind: 'user-mcp-json', path: mcpState.path, external: true, action: 'merge' });
80
+ else if (mcpState.status === 'configured')
81
+ targets.push({ kind: 'user-mcp-json', path: mcpState.path, external: true, action: 'update-vgxness' });
82
+ else
83
+ targets.push({ kind: 'user-mcp-json', path: mcpState.path, external: true, action: 'blocked', reason: mcpState.message });
84
+ const agentInspection = inspectClaudeCodeAgents({ workspaceRoot: input.cwd, scope: 'user', ...(input.env === undefined ? {} : { env: input.env }) });
85
+ for (const agent of agentInspection.agents) {
86
+ if (agent.status === 'missing')
87
+ targets.push({ kind: 'agent-file', path: agent.path, agentName: agent.agentName, scope: 'user', external: true, action: 'create' });
88
+ else if (agent.status === 'managed')
89
+ targets.push({ kind: 'agent-file', path: agent.path, agentName: agent.agentName, scope: 'user', external: true, action: 'update-vgxness' });
90
+ else
91
+ targets.push({ kind: 'agent-file', path: agent.path, agentName: agent.agentName, scope: 'user', external: true, action: 'blocked', reason: agent.detail });
92
+ }
93
+ const userMemory = inspectClaudeUserMemory(input.env);
94
+ targets.push({ kind: 'user-memory', path: userMemory.path, external: true, action: userMemory.action, status: userMemory.status, backupRequired: userMemory.backupRequired, ...(userMemory.status === 'blocked' ? { reason: userMemory.message } : {}) });
95
+ const blocked = targets.find((target) => target.action === 'blocked');
96
+ if (blocked !== undefined) {
97
+ const reason = blocked.kind === 'user-mcp-json' ? userMcpRefusalReason(mcpState.status) : blocked.kind === 'user-memory' ? userMemoryRefusalReason(userMemory) : 'existing_vgxness_agent';
98
+ return refused(input, server, reason, blocked.reason ?? 'Claude user install plan is blocked by an existing conflicting target.', targets, preservedTopLevelKeys, overwriteVgxness);
99
+ }
100
+ const backupRequired = targets.some((target) => (target.kind === 'user-mcp-json' && target.action !== 'create') || (target.kind === 'agent-file' && target.action === 'update-vgxness') || (target.kind === 'user-memory' && target.backupRequired));
101
+ return { ...base(input, server, targets, preservedTopLevelKeys, backupRequired, overwriteVgxness, 'user', cliCommand, scopeWarnings), status: 'would_install' };
102
+ }
64
103
  function refused(input, server, reason, message, targets, preservedTopLevelKeys, overwriteVgxness) {
65
104
  const resolved = resolveClaudeCodeScope(input.scope);
66
105
  const canonical = resolved.ok ? resolved.value.canonical : 'project';
@@ -69,7 +108,7 @@ function refused(input, server, reason, message, targets, preservedTopLevelKeys,
69
108
  }
70
109
  function base(input, server, targets, preservedTopLevelKeys, backupRequired, overwriteVgxness, canonicalClaudeScope, cliCommand, scopeWarnings) {
71
110
  const source = input.databasePathSource ?? 'flag';
72
- const targetPath = canonicalClaudeScope === 'project' ? resolveClaudeCodeMcpJsonPath(input.cwd) : `claude-cli:${canonicalClaudeScope}:vgxness`;
111
+ const targetPath = canonicalClaudeScope === 'project' ? resolveClaudeCodeMcpJsonPath(input.cwd) : canonicalClaudeScope === 'user' ? resolveClaudeCodeUserMcpJsonPath(input.env) : `claude-cli:${canonicalClaudeScope}:vgxness`;
73
112
  return {
74
113
  version: 1,
75
114
  kind: 'mcp-client-install-claude-code',
@@ -83,9 +122,9 @@ function base(input, server, targets, preservedTopLevelKeys, backupRequired, ove
83
122
  safety: { operation: 'plan', mutating: false, writesProviderConfig: false, targetPath, backupRequired, mergePolicy: backupRequired ? 'merge-preserve-existing' : 'create' },
84
123
  warnings: [
85
124
  ...scopeWarnings,
86
- 'Claude Code MCP registration is modeled as Claude CLI argv, never shell strings or manual ~/.claude.json mutation.',
87
- 'Project compatibility may write .mcp.json, .claude/agents/*.md, and a guarded project-root CLAUDE.md managed block after explicit confirmation/preflight; user/local scopes do not inspect or write private Claude config.',
88
- 'VGXNESS never manually writes ~/.claude.json or .claude/CLAUDE.md; project-root CLAUDE.md is only touched through the guarded project install managed block.',
125
+ 'Claude Code MCP registration is modeled as structured config/argv, never shell strings.',
126
+ 'Project compatibility may write .mcp.json, .claude/agents/*.md, and a guarded project-root CLAUDE.md managed block after explicit confirmation/preflight.',
127
+ 'Claude user/global support narrowly merges only mcpServers.vgxness in ~/.claude.json and writes VGXNESS-owned ~/.claude/agents/*.md plus a managed block in ~/.claude/CLAUDE.md after confirmation/preflight; unknown config keys and non-managed memory content are preserved.',
89
128
  ],
90
129
  verificationHints: [
91
130
  { kind: 'restart-client', message: 'Restart or reload Claude Code after confirmed project config installation.' },
@@ -107,8 +146,20 @@ function mcpRefusalReason(status) {
107
146
  return 'existing_vgxness_mcp';
108
147
  return 'invalid_mcp_shape';
109
148
  }
149
+ function userMcpRefusalReason(status) {
150
+ if (status === 'invalid')
151
+ return 'malformed_json';
152
+ if (status === 'conflicting')
153
+ return 'existing_vgxness_mcp';
154
+ return 'invalid_mcp_shape';
155
+ }
110
156
  function projectMemoryRefusalReason(state) {
111
157
  if (state.status === 'blocked' && state.reason === 'conflicting_ownership')
112
158
  return 'conflicting_claude_project_memory';
113
159
  return 'malformed_claude_project_memory';
114
160
  }
161
+ function userMemoryRefusalReason(state) {
162
+ if (state.status === 'blocked' && state.reason === 'conflicting_ownership')
163
+ return 'conflicting_claude_project_memory';
164
+ return 'malformed_claude_project_memory';
165
+ }
@@ -5,11 +5,13 @@ import { parseClaudeAgentFrontmatter } from './claude-code-agent-config.js';
5
5
  import { runClaudeCodeCliCommand } from './claude-code-cli.js';
6
6
  import { createClaudeCodeMcpServerConfig, inspectClaudeCodeMcpConfig, mergeClaudeCodeMcpConfig } from './claude-code-config.js';
7
7
  import { inspectClaudeProjectMemory, mergeClaudeProjectMemory } from './claude-code-project-memory.js';
8
- import { expectedClaudeCodeRenderedAgents, planClaudeCodeMcpInstall } from './client-install-claude-code-contract.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';
9
11
  export async function installClaudeCodeMcpClient(input) {
10
12
  const source = input.databasePathSource ?? 'flag';
11
13
  const server = createClaudeCodeMcpServerConfig(input.databasePath, source);
12
- const plan = planClaudeCodeMcpInstall({ cwd: input.cwd, databasePath: input.databasePath, databasePathSource: source, ...(input.overwriteVgxness !== undefined ? { overwriteVgxness: input.overwriteVgxness } : {}), ...(input.scope !== undefined ? { scope: input.scope } : {}) });
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, [], []);
15
17
  if (!input.confirmed)
@@ -23,7 +25,7 @@ export async function installClaudeCodeMcpClient(input) {
23
25
  for (const targetPath of preflightPaths) {
24
26
  const preflight = await input.preflight({
25
27
  category: 'provider-tool',
26
- operation: 'write claude project provider config',
28
+ operation: plan.canonicalClaudeScope === 'user' ? 'write claude user provider config' : 'write claude project provider config',
27
29
  targetPath,
28
30
  workspaceRoot: input.cwd,
29
31
  providerToolName: 'claude-code',
@@ -36,7 +38,7 @@ export async function installClaudeCodeMcpClient(input) {
36
38
  }
37
39
  const backups = [];
38
40
  const writtenPaths = [];
39
- if (plan.canonicalClaudeScope !== 'project') {
41
+ if (plan.canonicalClaudeScope === 'local') {
40
42
  if (input.cliRunner === undefined)
41
43
  return refusal('preflight_failed', 'Claude CLI MCP registration apply requires an injected runner boundary; read-only plans never execute Claude Code.', plan, server, [], []);
42
44
  if (plan.cliCommand === undefined)
@@ -47,6 +49,8 @@ export async function installClaudeCodeMcpClient(input) {
47
49
  return refusal('preflight_failed', `Claude CLI MCP registration failed with exit code ${cli.exitCode ?? 'unknown'}.`, plan, server, [], [], cliResult);
48
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 };
49
51
  }
52
+ if (plan.canonicalClaudeScope === 'user')
53
+ return applyUserInstall(input, plan, server, writtenPaths, backups);
50
54
  const existingTargets = plan.targets.filter((target) => target.kind !== 'cli-mcp-registration' && (target.action === 'merge' || target.action === 'update-vgxness' || target.action === 'append-managed-block'));
51
55
  for (const target of existingTargets) {
52
56
  const backup = createBackup(target.path, target.kind === 'project-memory' ? 'project-memory' : 'config');
@@ -88,6 +92,25 @@ function writeMcpJson(cwd, plan, server) {
88
92
  const after = inspectClaudeCodeMcpConfig(cwd);
89
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.' } };
90
94
  }
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
+ }
91
114
  function writeProjectMemory(plan) {
92
115
  const target = plan.targets.find((item) => item.kind === 'project-memory');
93
116
  if (target === undefined || target.action === 'none')
@@ -105,14 +128,61 @@ function writeProjectMemory(plan) {
105
128
  const after = inspectClaudeProjectMemory(dirname(target.path));
106
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.' } };
107
130
  }
108
- function createBackup(path, kind) {
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') {
109
179
  return createManagedProviderConfigBackup({
110
180
  targetPath: path,
111
181
  provider: 'claude',
112
- scope: 'project',
182
+ scope,
113
183
  createdByOperation: 'mcp-client-install-claude-code',
114
- reason: kind === 'project-memory' ? 'pre-project-memory-update-safety' : 'pre-merge-safety',
115
- description: kind === 'project-memory' ? 'Backup existing Claude Code project memory before appending or updating the VGXNESS managed block.' : 'Backup existing Claude Code project config before merging VGXNESS MCP or agent configuration.',
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.',
116
186
  });
117
187
  }
118
188
  function refusal(reason, message, plan, server, writtenPaths, backups, cliResult) {
@@ -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
@@ -10,6 +10,8 @@ export * from './claude-code-cli.js';
10
10
  export * from './claude-code-config.js';
11
11
  export * from './claude-code-project-memory.js';
12
12
  export * from './claude-code-scope.js';
13
+ export * from './claude-code-user-config.js';
14
+ export * from './claude-code-user-memory.js';
13
15
  export * from './control-plane.js';
14
16
  export * from './doctor.js';
15
17
  export * from './opencode-visibility.js';