vgxness 1.6.0 → 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.
@@ -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 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, 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]
@@ -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';
@@ -2,9 +2,7 @@ import { resolveMemoryDatabasePath } from '../memory/storage-paths.js';
2
2
  import { planClaudeCodeMcpInstall } from './client-install-claude-code-contract.js';
3
3
  import { planOpenCodeMcpInstall } from './client-install-opencode-contract.js';
4
4
  import { ProviderDoctorService } from './provider-doctor.js';
5
- import { CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES, isUserGlobalScope } from './provider-health-types.js';
6
5
  import { ProviderStatusService } from './provider-status.js';
7
- import { createClaudeCodeCliRegistrationPreview } from './claude-code-cli.js';
8
6
  import { resolveClaudeCodeScope } from './claude-code-scope.js';
9
7
  export const providerChangePlanProviders = ['opencode', 'claude', 'antigravity', 'custom'];
10
8
  export const providerChangePlanTypes = ['opencode-mcp-install', 'setup', 'install', 'config-preparation'];
@@ -81,51 +79,13 @@ export class ProviderChangePlanService {
81
79
  const claudeScope = resolveClaudeCodeScope(normalized.scope);
82
80
  if (!claudeScope.ok)
83
81
  return claudeScope;
84
- if (isUserGlobalScope(normalized.scope) || claudeScope.value.canonical === 'local')
85
- return { ok: true, value: claudeUserGlobalEnvelope(normalized, status.value, doctor.value, claudeScope.value.canonical, claudeScope.value.warnings) };
86
82
  const databasePath = resolveMemoryDatabasePath({ cwd: normalized.workspaceRoot, env: this.deps.env ?? process.env });
87
83
  if (!databasePath.ok)
88
84
  return { ok: true, value: blockedPlanEnvelope(normalized, status.value, doctor.value, databasePath.error.message) };
89
- 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 });
90
86
  return { ok: true, value: claudeEnvelope(normalized, status.value, doctor.value, installPlan, databasePath.value.source) };
91
87
  }
92
88
  }
93
- function claudeUserGlobalEnvelope(input, status, doctor, canonicalScope = 'user', scopeWarnings = []) {
94
- const warnings = [
95
- ...scopeWarnings,
96
- ...statusWarnings(status),
97
- ...doctor.recommendations,
98
- 'Claude private config mutation is intentionally unsupported by VGXNESS; future apply uses Claude CLI argv only after confirmation/preflight.',
99
- ];
100
- const envelope = {
101
- ...baseEnvelope(input),
102
- supported: true,
103
- status: 'planned',
104
- summary: `Read-only Claude ${canonicalScope} change planning completed. VGXNESS will not read or write private Claude config files from this plan. Future apply requires confirmation/preflight and uses Claude CLI argv only: claude mcp add --scope ${canonicalScope} vgxness -- vgxness mcp start.`,
105
- providerIdentity: { provider: 'claude', adapter: 'claude', support: 'supported' },
106
- statusSummary: statusSummary(status),
107
- statusFindings: status.config,
108
- doctorSummary: doctorSummary(doctor),
109
- doctorFindings: doctor.checks,
110
- previewEffects: {
111
- action: 'none',
112
- backupRequired: false,
113
- preservedTopLevelKeys: [],
114
- cliCommandPreview: createClaudeCodeCliRegistrationPreview(canonicalScope),
115
- installsAgents: false,
116
- agentNames: [],
117
- refusalMessage: 'Read-only plan only; future apply requires confirmation/preflight and does not mutate ~/.claude.json manually.',
118
- },
119
- backupRollback: descriptiveBackupPolicy(false),
120
- confirmations: confirmationPolicy(),
121
- risks: [
122
- `VGXNESS cannot verify Claude ${canonicalScope} MCP registration from read-only planning because it intentionally avoids reading private Claude config and does not execute Claude Code.`,
123
- `Supported Claude read-only capabilities are: ${CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES.join(', ')}.`,
124
- ],
125
- warnings,
126
- };
127
- return input.payloadMode === 'verbose' ? { ...envelope, rawSources: { status, doctor } } : envelope;
128
- }
129
89
  function normalizeInput(input) {
130
90
  return {
131
91
  project: input.project?.trim() || 'vgxness',
@@ -224,7 +184,7 @@ function claudeEnvelope(input, status, doctor, installPlan, source) {
224
184
  supported: true,
225
185
  status: installPlan.status === 'refused' ? 'blocked' : 'planned',
226
186
  ...(installPlan.status === 'refused' ? { code: 'PLAN_UNAVAILABLE' } : {}),
227
- 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, ${installPlan.agentNames.length} agent file(s), and project-root CLAUDE.md as needed.`,
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.`,
228
188
  providerIdentity: { provider: 'claude', adapter: 'claude', support: 'supported' },
229
189
  statusSummary: statusSummary(status),
230
190
  statusFindings: status.config,
@@ -309,11 +269,11 @@ function previewEffectsClaude(plan) {
309
269
  const projectMemory = projectMemoryPreview(plan.targets);
310
270
  if (plan.status === 'refused')
311
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 }) };
312
- const mcpTarget = plan.targets.find((target) => target.kind === 'mcp-json');
272
+ const mcpTarget = plan.targets.find((target) => target.kind === 'mcp-json' || target.kind === 'user-mcp-json');
313
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 }) };
314
274
  }
315
275
  function projectMemoryPreview(targets) {
316
- const target = targets.find((item) => item.kind === 'project-memory');
276
+ const target = targets.find((item) => item.kind === 'project-memory' || item.kind === 'user-memory');
317
277
  if (target === undefined)
318
278
  return undefined;
319
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';
@@ -352,7 +312,7 @@ function risksForOpenCode(status, doctor, plan, source) {
352
312
  return risks;
353
313
  }
354
314
  function risksForClaude(status, doctor, plan, source) {
355
- const risks = ['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.'];
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.'];
356
316
  if (status.status !== 'ready')
357
317
  risks.push(`Provider status is ${status.status}; future writes should review status findings first.`);
358
318
  if (doctor.status !== 'healthy')
@@ -3,6 +3,8 @@ import { inspectClaudeCodeAgents } from './claude-code-agent-config.js';
3
3
  import { claudeAdvisoryPaths, inspectClaudeCodeMcpConfig } 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 } from './claude-code-user-config.js';
7
+ import { inspectClaudeUserMemory } from './claude-code-user-memory.js';
6
8
  import { vgxnessOpenCodeDefaultAgent, vgxnessOpenCodeSddSubagents } from './opencode-default-agent-config.js';
7
9
  import { buildCanonicalAgentManifestDiagnostic } from './provider-canonical-agent-manifest.js';
8
10
  import { normalizeProviderHealthInput, PROVIDER_HEALTH_SAFETY, CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES, providerHealthFailure, isUserGlobalScope, REQUIRED_PROVIDER_NATIVE_MCP_TOOLS, rollupProviderDoctor, } from './provider-health-types.js';
@@ -132,21 +134,35 @@ export class ProviderDoctorService {
132
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 } };
133
135
  }
134
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);
135
146
  const checks = [
136
147
  { id: 'workspace-root', status: existsSync(normalized.workspaceRoot) ? 'pass' : 'fail', detail: `Workspace root ${existsSync(normalized.workspaceRoot) ? 'exists' : 'does not exist'}: ${normalized.workspaceRoot}` },
137
- { id: 'provider-supported', status: 'warn', detail: `Claude ${canonicalScope} scope is supported as read-only advisory status/doctor/change-plan. Future install requires explicit confirmation/preflight and Claude CLI; private config mutation is unsupported.` },
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.` },
138
149
  { id: 'claude-scope', status: scopeWarnings.length === 0 ? 'pass' : 'warn', detail: scopeWarnings.join(' ') || `Claude scope resolved to canonical ${canonicalScope}.` },
139
150
  { id: 'claude-cli-presence', status: 'warn', detail: 'Read-only doctor does not execute `claude --version`; no provider process was launched.' },
140
- { id: 'provider-config-readonly-safety', status: 'pass', detail: `No Claude ${canonicalScope} paths were inspected or mutated; no repair/install/write occurred.` },
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)),
141
159
  ];
142
160
  const status = rollupProviderDoctor(checks.map((check) => check.status));
143
161
  const compactChecksValue = compactChecks(checks, normalized.payloadMode);
144
162
  const failedChecks = checks.filter((check) => check.status === 'fail').map((check) => check.id);
145
- const recommendations = [
146
- `If you choose to configure Claude ${canonicalScope} scope manually, run Claude Code yourself and use a command like: claude mcp add --scope ${canonicalScope} vgxness -- vgxness mcp start.`,
147
- 'VGXNESS will not read or write private Claude config files during status/doctor/change-plan, repair config, or execute Claude Code from read-only surfaces.',
148
- ];
149
- 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: `Claude ${canonicalScope} doctor is advisory and read-only; no private Claude files were inspected, no config was changed, and Claude Code was not executed.`, recommendations, checks: compactChecksValue, checkedPaths: [], bytes: { originalBytes: Buffer.byteLength(JSON.stringify({ checks, checkedPaths: [] }), '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 } } };
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 } } };
150
166
  }
151
167
  }
152
168
  function summarizeDoctor(status, failedCount, recommendationCount) {
@@ -1,10 +1,11 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { inspectClaudeCodeAgents } from './claude-code-agent-config.js';
4
- import { createClaudeCodeCliRegistrationPreview } from './claude-code-cli.js';
5
4
  import { claudeAdvisoryPaths, claudeMcpConfigPathStatus, claudeMcpEntryStatus, inspectClaudeCodeMcpConfig } from './claude-code-config.js';
6
5
  import { inspectClaudeProjectMemory } from './claude-code-project-memory.js';
7
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';
8
9
  import { resolveOpenCodeMcpInstallTarget } from './client-install-opencode-contract.js';
9
10
  import { vgxnessOpenCodeDefaultAgent, vgxnessOpenCodePromptContractVersion, vgxnessOpenCodeSddSubagents } from './opencode-default-agent-config.js';
10
11
  import { buildCanonicalAgentManifestDiagnostic } from './provider-canonical-agent-manifest.js';
@@ -147,28 +148,20 @@ export class ProviderStatusService {
147
148
  }
148
149
  getClaudeUserGlobalStatus(normalized, canonicalScope = 'user', scopeWarnings = []) {
149
150
  const canonicalAgentManifest = (this.deps.canonicalAgentManifestDiagnostic ?? buildCanonicalAgentManifestDiagnostic)();
150
- const paths = [
151
- {
152
- label: `Claude ${canonicalScope} scope`,
153
- path: `${canonicalScope} Claude configuration (not inspected)`,
154
- exists: false,
155
- readable: false,
156
- parsed: false,
157
- status: 'warn',
158
- detail: `Claude ${canonicalScope} scope is advisory only in read-only status: VGXNESS does not read private Claude files, write config, execute Claude Code, or install provider config.`,
159
- },
160
- ];
161
- const mcpEntry = {
162
- configured: false,
163
- status: 'warn',
164
- serverName: 'vgxness',
165
- detail: `${canonicalScope} Claude MCP status is not inspected by VGXNESS; use this as planning guidance only. Future apply uses Claude CLI argv only: ${createClaudeCodeCliRegistrationPreview(canonicalScope).join(' ')}`,
166
- };
167
- const tools = requiredToolPresence();
168
- const status = rollupProviderHealth([canonicalAgentManifest.status, 'warn']);
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]);
169
161
  const sdd = normalized.change.length > 0 ? this.readSdd(normalized.project, normalized.change) : undefined;
170
- const verboseShape = { config: { status: 'warn', paths, mcpEntry }, canonicalAgentManifest, scopeCapabilities: CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES, scopeWarnings, sdd, mcpRequiredTools: tools };
171
- const compactShape = { config: { status: 'warn', paths: compactPaths(paths, 'compact'), mcpEntry: compactMcpEntry(mcpEntry, 'compact') }, canonicalAgentManifest, scopeCapabilities: CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES, sdd: sdd === undefined ? undefined : compactSdd(sdd, 'compact'), mcpRequiredTools: tools };
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 };
172
165
  return {
173
166
  ok: true,
174
167
  value: {
@@ -181,14 +174,14 @@ export class ProviderStatusService {
181
174
  status,
182
175
  payloadMode: normalized.payloadMode,
183
176
  overallStatus: status,
184
- inspectedPaths: [],
185
- issueCount: canonicalAgentManifest.status === 'fail' ? 1 : 0,
186
- warningCount: 1 + scopeWarnings.length,
187
- summary: `Claude ${canonicalScope} scope status is read-only/advisory; no private Claude files are inspected or written, and Claude Code is not executed.`,
188
- nextAction: { kind: 'review', message: 'Review the planning-only guidance; future apply requires explicit confirmation/preflight and uses Claude CLI argv, not private config mutation.' },
189
- checkedPaths: [],
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,
190
183
  canonicalAgentManifest,
191
- config: { status: 'warn', paths: compactPaths(paths, normalized.payloadMode), mcpEntry: compactMcpEntry(mcpEntry, normalized.payloadMode) },
184
+ config: { status: configStatus, paths: compactPaths(paths, normalized.payloadMode), mcpEntry: compactMcpEntry(mcpEntry, normalized.payloadMode) },
192
185
  ...(sdd === undefined ? {} : { sdd: compactSdd(sdd, normalized.payloadMode) }),
193
186
  mcpRequiredTools: tools,
194
187
  originalBytes: Buffer.byteLength(JSON.stringify(verboseShape), 'utf8'),
@@ -1,5 +1,20 @@
1
1
  import { z } from 'zod';
2
2
  export const sddPhases = ['explore', 'proposal', 'spec', 'design', 'tasks', 'apply-progress', 'verify', 'archive'];
3
+ /**
4
+ * Governance status of an SDD artifact.
5
+ *
6
+ * - `draft` — present in the DB; satisfies downstream readiness when the
7
+ * `VGXNESS_SDD_AUTO_ADVANCE` feature flag is on (or
8
+ * `SddWorkflowService` is constructed with `autoAdvance: true`).
9
+ * Does NOT count toward the change being `complete`.
10
+ * - `accepted` — content has been frozen by an explicit human
11
+ * `vgxness_sdd_accept_artifact` call. Satisfies downstream
12
+ * readiness regardless of the auto-advance flag.
13
+ * - `rejected` — explicit human rejection. Hard-blocker for downstream
14
+ * readiness; can only be cleared by re-opening the artifact.
15
+ * - `superseded`— replaced by a newer artifact under a different topic key.
16
+ * Hard-blocker; cannot be re-saved or accepted.
17
+ */
3
18
  export const sddArtifactStatuses = ['draft', 'accepted', 'rejected', 'superseded'];
4
19
  export const sddArtifactNormalizationWarnings = ['legacy-artifact-defaulted-to-draft', 'invalid-artifact-metadata-defaulted-to-draft'];
5
20
  export const SddArtifactStatusSchema = z.enum(sddArtifactStatuses);
@@ -2,12 +2,32 @@ import { summarizePayloadContent } from '../payload/payload-summary.js';
2
2
  import { isSddPhase, normalizeSddArtifact, sddPhases, sddPrerequisites, sddTopicKey, } from './schema.js';
3
3
  const defaultContext = { actor: 'sdd-workflow-service' };
4
4
  const validChangePattern = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
5
+ function readAutoAdvanceFromEnv() {
6
+ const value = process.env.VGXNESS_SDD_AUTO_ADVANCE;
7
+ return value === 'true' || value === '1';
8
+ }
9
+ function isPrerequisiteSatisfied(status) {
10
+ if (status === undefined)
11
+ return false;
12
+ if (status.legacy === true)
13
+ return false;
14
+ if (status.accepted === true)
15
+ return true;
16
+ if (status.state === 'draft')
17
+ return true;
18
+ return false;
19
+ }
5
20
  export class SddWorkflowService {
6
21
  memory;
7
22
  context;
8
- constructor(memory, context = defaultContext) {
23
+ options;
24
+ constructor(memory, context = defaultContext, options = {}) {
9
25
  this.memory = memory;
10
26
  this.context = context;
27
+ this.options = {
28
+ ...options,
29
+ autoAdvance: options.autoAdvance ?? readAutoAdvanceFromEnv(),
30
+ };
11
31
  }
12
32
  getWorkflow(change) {
13
33
  return sddPhases.map((phase) => ({
@@ -23,7 +43,7 @@ export class SddWorkflowService {
23
43
  const phases = this.getPhaseStatuses(validated.value.project, validated.value.change);
24
44
  if (!phases.ok)
25
45
  return phases;
26
- const readiness = getReadinessFromStatuses(validated.value.change, validated.value.phase, phases.value);
46
+ const readiness = getReadinessFromStatuses(validated.value.change, validated.value.phase, phases.value, this.options);
27
47
  return {
28
48
  ok: true,
29
49
  value: {
@@ -40,7 +60,7 @@ export class SddWorkflowService {
40
60
  const phases = this.getPhaseStatuses(validated.value.project, validated.value.change);
41
61
  if (!phases.ok)
42
62
  return phases;
43
- return statusFromPhases(validated.value.change, phases.value);
63
+ return statusFromPhases(validated.value.change, phases.value, this.options);
44
64
  }
45
65
  getNext(input) {
46
66
  const validated = validateProjectAndChange(input.project, input.change);
@@ -49,7 +69,7 @@ export class SddWorkflowService {
49
69
  const phases = this.getPhaseStatuses(validated.value.project, validated.value.change);
50
70
  if (!phases.ok)
51
71
  return phases;
52
- return ok(nextDecisionFromStatuses(validated.value.change, phases.value));
72
+ return ok(nextDecisionFromStatuses(validated.value.change, phases.value, this.options));
53
73
  }
54
74
  getCockpit(input) {
55
75
  const validated = validateProjectAndChange(input.project, input.change);
@@ -59,12 +79,12 @@ export class SddWorkflowService {
59
79
  if (!snapshot.ok)
60
80
  return snapshot;
61
81
  const phases = snapshot.value.phases;
62
- const next = nextDecisionFromStatuses(validated.value.change, phases);
82
+ const next = nextDecisionFromStatuses(validated.value.change, phases, this.options);
63
83
  const cockpitPhases = phases.map((phaseStatus) => {
64
84
  const readiness = {
65
85
  change: validated.value.change,
66
86
  phase: phaseStatus.phase,
67
- ...getReadinessFromStatuses(validated.value.change, phaseStatus.phase, phases),
87
+ ...getReadinessFromStatuses(validated.value.change, phaseStatus.phase, phases, this.options),
68
88
  };
69
89
  const artifact = phaseStatus.present ? cockpitArtifactSummaryFromSnapshotItem(phaseStatus) : undefined;
70
90
  const blockers = cockpitBlockersForPhase(phaseStatus, readiness);
@@ -115,7 +135,7 @@ export class SddWorkflowService {
115
135
  if (!snapshot.ok)
116
136
  return snapshot;
117
137
  const phases = snapshot.value.phases;
118
- const status = statusFromPhases(validated.value.change, phases);
138
+ const status = statusFromPhases(validated.value.change, phases, this.options);
119
139
  if (!status.ok)
120
140
  return status;
121
141
  const warnings = [];
@@ -137,7 +157,7 @@ export class SddWorkflowService {
137
157
  }
138
158
  return [{ phase: phase.phase, topicKey: phase.topicKey, artifact: phase.artifact, envelope }];
139
159
  });
140
- const readiness = input.phase === undefined ? undefined : { change: validated.value.change, phase: input.phase, ...getReadinessFromStatuses(validated.value.change, input.phase, phases) };
160
+ const readiness = input.phase === undefined ? undefined : { change: validated.value.change, phase: input.phase, ...getReadinessFromStatuses(validated.value.change, input.phase, phases, this.options) };
141
161
  return ok({ status: status.value, artifacts, ...(readiness === undefined ? {} : { readiness }), warnings });
142
162
  }
143
163
  saveArtifact(input) {
@@ -146,13 +166,20 @@ export class SddWorkflowService {
146
166
  return validated;
147
167
  if (input.content.trim().length === 0)
148
168
  return validationFailure('SDD artifact content must not be empty');
149
- return this.memory.saveArtifact({
150
- project: validated.value.project,
151
- topicKey: sddTopicKey(validated.value.change, validated.value.phase),
152
- phase: validated.value.phase,
153
- content: input.content,
154
- metadata: { status: 'draft' },
155
- }, this.context);
169
+ const { project, change, phase } = validated.value;
170
+ const topicKey = sddTopicKey(change, phase);
171
+ const existing = this.memory.getArtifact(project, topicKey, this.context);
172
+ if (existing.ok) {
173
+ const envelope = normalizeSddArtifact(existing.value);
174
+ const status = envelope.metadata.status;
175
+ if (status === 'accepted')
176
+ return validationFailure('Cannot overwrite an accepted SDD artifact; supersede it under a new topic key.');
177
+ if (status === 'rejected')
178
+ return validationFailure('Rejected SDD artifact must be re-opened with an explicit decision before saving.');
179
+ if (status === 'superseded')
180
+ return validationFailure('Cannot overwrite a superseded SDD artifact.');
181
+ }
182
+ return this.memory.saveArtifact({ project, topicKey, phase, content: input.content, metadata: { status: 'draft' } }, this.context);
156
183
  }
157
184
  acceptArtifact(input) {
158
185
  const validated = this.validatePhaseInput(input);
@@ -169,6 +196,11 @@ export class SddWorkflowService {
169
196
  if (!existing.ok)
170
197
  return existing;
171
198
  const existingEnvelope = normalizeSddArtifact(existing.value);
199
+ const existingStatus = existingEnvelope.metadata.status;
200
+ if (existingStatus === 'rejected')
201
+ return validationFailure('Rejected SDD artifact must be re-opened with an explicit decision before accepting.');
202
+ if (existingStatus === 'superseded')
203
+ return validationFailure('Cannot accept a superseded SDD artifact; create a new artifact under a different topic key instead.');
172
204
  const acceptance = {
173
205
  actor: {
174
206
  type: 'human',
@@ -203,7 +235,7 @@ export class SddWorkflowService {
203
235
  const statuses = this.getPhaseStatuses(validated.value.project, validated.value.change);
204
236
  if (!statuses.ok)
205
237
  return statuses;
206
- return ok(compactArtifactProjection(artifact.value, validated.value.change, validated.value.phase, getReadinessFromStatuses(validated.value.change, validated.value.phase, statuses.value)));
238
+ return ok(compactArtifactProjection(artifact.value, validated.value.change, validated.value.phase, getReadinessFromStatuses(validated.value.change, validated.value.phase, statuses.value, this.options)));
207
239
  }
208
240
  listArtifacts(input) {
209
241
  const validated = validateProjectAndChange(input.project, input.change);
@@ -219,7 +251,7 @@ export class SddWorkflowService {
219
251
  artifacts: snapshot.value.phases.flatMap((phase) => {
220
252
  if (phase.artifact === undefined)
221
253
  return [];
222
- return [compactArtifactProjection(phase.artifact, validated.value.change, phase.phase, getReadinessFromStatuses(validated.value.change, phase.phase, snapshot.value.phases))];
254
+ return [compactArtifactProjection(phase.artifact, validated.value.change, phase.phase, getReadinessFromStatuses(validated.value.change, phase.phase, snapshot.value.phases, this.options))];
223
255
  }),
224
256
  fullRetrieval: {
225
257
  tool: 'vgxness_sdd_list_artifacts',
@@ -280,14 +312,14 @@ export class SddWorkflowService {
280
312
  return ok({ project, change, phases });
281
313
  }
282
314
  }
283
- export function nextDecisionFromStatuses(change, phases) {
315
+ export function nextDecisionFromStatuses(change, phases, options = {}) {
284
316
  const blockedPresentPhase = phases.find((status) => {
285
317
  if (!status.present)
286
318
  return false;
287
- return (getReadinessFromStatuses(change, status.phase, phases).blockedPrerequisites ?? []).length > 0;
319
+ return (getReadinessFromStatuses(change, status.phase, phases, options).blockedPrerequisites ?? []).length > 0;
288
320
  });
289
321
  if (blockedPresentPhase !== undefined) {
290
- const readiness = getReadinessFromStatuses(change, blockedPresentPhase.phase, phases);
322
+ const readiness = getReadinessFromStatuses(change, blockedPresentPhase.phase, phases, options);
291
323
  const blockers = readiness.blockedPrerequisites ?? [];
292
324
  return {
293
325
  change,
@@ -329,7 +361,7 @@ export function nextDecisionFromStatuses(change, phases) {
329
361
  recommendedAction: 'No next SDD phase remains for this change.',
330
362
  };
331
363
  }
332
- const readiness = getReadinessFromStatuses(change, nextMissingPhase, phases);
364
+ const readiness = getReadinessFromStatuses(change, nextMissingPhase, phases, options);
333
365
  if (!readiness.ready) {
334
366
  const blockers = readiness.blockedPrerequisites ?? [];
335
367
  return {
@@ -352,24 +384,26 @@ export function nextDecisionFromStatuses(change, phases) {
352
384
  recommendedAction: `Run the ${nextMissingPhase} SDD phase for ${change}.`,
353
385
  };
354
386
  }
355
- function statusFromPhases(change, phases) {
387
+ function statusFromPhases(change, phases, options = {}) {
356
388
  const nextReadyPhase = sddPhases.find((phase) => {
357
389
  if (phases.find((status) => status.phase === phase)?.present)
358
390
  return false;
359
- return getReadinessFromStatuses(change, phase, phases).ready;
391
+ return getReadinessFromStatuses(change, phase, phases, options).ready;
360
392
  });
361
393
  const status = { change, phases };
362
394
  if (nextReadyPhase !== undefined)
363
395
  status.nextReadyPhase = nextReadyPhase;
364
396
  return { ok: true, value: status };
365
397
  }
366
- function getReadinessFromStatuses(change, phase, phases) {
398
+ function getReadinessFromStatuses(change, phase, phases, options = {}) {
367
399
  const satisfiedPrerequisites = [];
368
400
  const missingArtifactTopicKeys = [];
369
401
  const blockedPrerequisites = [];
402
+ const useAutoAdvance = options.autoAdvance === true;
370
403
  for (const prerequisite of sddPrerequisites[phase]) {
371
404
  const status = phases.find((candidate) => candidate.phase === prerequisite);
372
- if (status?.accepted) {
405
+ const satisfied = useAutoAdvance ? isPrerequisiteSatisfied(status) : status?.accepted === true;
406
+ if (satisfied) {
373
407
  satisfiedPrerequisites.push(prerequisite);
374
408
  continue;
375
409
  }
@@ -508,10 +542,6 @@ function blockerReasonForStatus(status) {
508
542
  if (status.legacy === true)
509
543
  return 'legacy';
510
544
  const state = status.state ?? (status.present ? 'draft' : 'missing');
511
- if (state === 'missing')
512
- return 'missing';
513
- if (state === 'accepted')
514
- return 'draft';
515
545
  return state;
516
546
  }
517
547
  function formatBlockers(blockers) {
@@ -9,17 +9,19 @@ export const claudeSetupAdapter = {
9
9
  { kind: 'project-config', label: 'Project .mcp.json', path: '.mcp.json', writableBySetup: false },
10
10
  { kind: 'project-config', label: 'Project Claude agents', path: '.claude/agents/*.md', writableBySetup: false },
11
11
  { kind: 'project-config', label: 'Project Claude memory', path: 'CLAUDE.md', writableBySetup: false },
12
+ { kind: 'user-config', label: 'User Claude MCP config', path: '~/.claude.json', writableBySetup: false },
12
13
  { kind: 'user-config', label: 'User Claude agents', path: '~/.claude/agents/*.md', writableBySetup: false },
14
+ { kind: 'user-config', label: 'User Claude memory', path: '~/.claude/CLAUDE.md', writableBySetup: false },
13
15
  ],
14
16
  getStatus(context) {
15
17
  return {
16
18
  providerId: 'claude',
17
19
  status: 'supported-secondary',
18
- summary: 'Claude is supported as a secondary, non-default provider for CLI MCP registration planning/apply, project .mcp.json compatibility, and project/user agent planning; explicit guarded apply is required for writes or CLI execution.',
20
+ summary: 'Claude is supported as a secondary, non-default provider for CLI MCP registration planning/apply, project .mcp.json compatibility, guarded CLAUDE.md memory, user ~/.claude.json MCP merge, and project/user agents; explicit guarded apply is required for writes or CLI execution.',
19
21
  evidence: context.databasePath !== undefined
20
22
  ? ['Claude MCP preview can be generated from the selected database path.']
21
23
  : ['Claude MCP preview uses a placeholder until a database path is selected.'],
22
- guidance: ['Use `vgxness mcp install claude --scope local|project|user --yes --run-id <id>` for explicit guarded apply. VGXNESS may write project-root CLAUDE.md only through confirmed, preflighted Claude project install; it never manually writes ~/.claude.json or .claude/CLAUDE.md. Read-only setup/status surfaces do not execute Claude Code.'],
24
+ guidance: ['Use `vgxness mcp install claude --scope local|project|user --yes --run-id <id>` for explicit guarded apply. Project scope may write .mcp.json, .claude/agents/*.md, and the project-root CLAUDE.md managed block. User/global scope narrowly merges only mcpServers.vgxness in ~/.claude.json, writes ~/.claude/agents/*.md, and manages only the VGXNESS block in ~/.claude/CLAUDE.md. Read-only setup/status surfaces do not execute Claude Code.'],
23
25
  actions: [
24
26
  {
25
27
  id: 'claude-manual-guidance',
@@ -18,7 +18,7 @@ The user-facing shape is deliberately four-surface: **MCP for agents**, **CLI fo
18
18
  | Core workflow | SDD-first canonical state: explore → proposal → spec → design → tasks → apply-progress → verify → archive. |
19
19
  | Interfaces | MCP server for AI tools, CLI for automation/power users, TUI for guided install/status/profile/SDD workflows, and `vgxness code` runtime for bounded workspace work. |
20
20
  | Installation UX | Step-based guided setup with doctor checks, dry-run support, and no manual provider JSON editing on the happy path. |
21
- | Provider strategy | Provider-agnostic domain model with OpenCode as the primary/default supported provider; Claude Code is supported secondary for CLI MCP registration, project `.mcp.json` compatibility, guarded project-root `CLAUDE.md` managed memory, and project/user agent planning. Claude scopes are `local`, `project`, and `user` (`personal`/`global` map to `user`). Read-only status/doctor/change-plan never execute Claude Code or inspect private Claude config, and VGXNESS never manually mutates `~/.claude.json` or `.claude/CLAUDE.md`. Pi/`gentle-pi` compatibility is a future adapter/reference target. The code runtime speaks to any OpenAI-compatible endpoint through `src/code/providers/openai-compatible-provider-adapter.ts`. |
21
+ | Provider strategy | Provider-agnostic domain model with OpenCode as the primary/default supported provider; Claude Code is supported secondary for CLI MCP registration, project `.mcp.json` compatibility, guarded `CLAUDE.md` managed memory, direct user `~/.claude.json` MCP merge, and project/user agent files. Claude scopes are `local`, `project`, and `user` (`personal`/`global` map to `user`). Read-only status/doctor/change-plan never execute Claude Code; user/global apply mutates only `mcpServers.vgxness`, VGXNESS-owned `~/.claude/agents/*.md`, and the VGXNESS block in `~/.claude/CLAUDE.md` after confirmation/preflight. Pi/`gentle-pi` compatibility is a future adapter/reference target. The code runtime speaks to any OpenAI-compatible endpoint through `src/code/providers/openai-compatible-provider-adapter.ts`. |
22
22
  | Memory | Project memory plus personal/global memory, backed locally. |
23
23
  | Agents | Agents/subagents are registered in a neutral schema, then rendered into provider-specific config. |
24
24
  | Skills | Skills are first-class, versioned, attachable to agents/workflows/adapters, and improved through reviewable proposals. |
@@ -439,7 +439,7 @@ vgxness agents render --provider opencode --project vgxness --name apply-agent
439
439
  Rendering is intentionally read-only: it returns generated content in the CLI response. It does **not** write `.opencode/`, `.claude/`, or any user/global provider config.
440
440
  OpenCode agent keys are sanitized deterministically from registry names, and rendering rejects key collisions instead of overwriting generated config.
441
441
 
442
- Claude Code install planning/apply is supported through the guarded MCP install path. Rendering remains read-only; confirmed project compatibility writes are limited to `.mcp.json`, `.claude/agents/*.md`, and the project-root `CLAUDE.md` managed block with run preflight metadata. Local/user MCP registration is represented through Claude CLI argv after confirmation/preflight, not private config mutation.
442
+ Claude Code install planning/apply is supported through the guarded MCP install path. Rendering remains read-only; confirmed project compatibility writes are limited to `.mcp.json`, `.claude/agents/*.md`, and the project-root `CLAUDE.md` managed block with run preflight metadata. User/global install is a guarded direct path that backs up existing files, merges only `mcpServers.vgxness` in `~/.claude.json`, writes VGXNESS-owned `~/.claude/agents/*.md`, and manages only the VGXNESS block in `~/.claude/CLAUDE.md`.
443
443
 
444
444
  ### OpenCode injection preview
445
445
 
package/docs/cli.md CHANGED
@@ -232,7 +232,7 @@ bun run cli:bun -- sdd status --project vgxness --change my-change
232
232
  Provider support shown in the Installation surface is:
233
233
 
234
234
  - **OpenCode** — supported primary provider with preview/status/doctor and read-only install planning.
235
- - **Claude** — supported secondary provider for Claude CLI MCP registration, project `.mcp.json` compatibility, project-root `CLAUDE.md` managed memory, and project/user agent planning. Scopes are `local|project|user`; `personal`/`global` map to `user`. Explicit guarded apply is outside the guided OpenCode flow and requires `mcp install claude --scope <scope> --yes --run-id <id>`. Read-only status/doctor/change-plan do not read private Claude config or execute Claude Code.
235
+ - **Claude** — supported secondary provider for Claude CLI MCP registration, project `.mcp.json` compatibility, guarded `CLAUDE.md` managed memory, user `~/.claude.json` MCP merge, and project/user agents. Scopes are `local|project|user`; `personal`/`global` map to `user`. Explicit guarded apply is outside the guided OpenCode flow and requires `mcp install claude --scope <scope> --yes --run-id <id>`. Read-only status/doctor/change-plan do not execute Claude Code.
236
236
  - **Antigravity** — placeholder/coming-soon guidance only.
237
237
  - **Custom/future** — extension point with safe manual/default unsupported behavior.
238
238
 
@@ -508,7 +508,7 @@ Manual OpenCode verification checklist:
508
508
 
509
509
  ## Claude Code MCP install
510
510
 
511
- Claude Code is supported as a secondary, non-default provider for VGXNESS MCP/subagent configuration. Planning/status/doctor/change-plan are read-only and do not require Claude Code, network, or credentials. Scopes are `local`, `project`, and `user`; compatibility aliases `personal` and legacy `global` map to Claude `user` with warnings. MCP registration is represented as Claude CLI argv (`claude mcp add --scope <scope> vgxness -- vgxness mcp start`), never shell strings or manual `~/.claude.json` mutation.
511
+ Claude Code is supported as a secondary, non-default provider for VGXNESS MCP/subagent configuration. Planning/status/doctor/change-plan are read-only and do not require Claude Code, network, or credentials. Scopes are `local`, `project`, and `user`; compatibility aliases `personal` and legacy `global` map to Claude `user` with warnings. MCP registration is represented as structured config/argv (`claude mcp add --scope <scope> vgxness -- vgxness mcp start`), never shell strings. User/global apply narrowly merges only `mcpServers.vgxness` in `~/.claude.json` and preserves unknown keys/auth/session data.
512
512
 
513
513
  ```bash
514
514
  bun run cli:bun -- mcp install claude --plan --scope project --db <path>
@@ -526,8 +526,8 @@ User agent files target `~/.claude/agents/*.md` only when a future apply path ex
526
526
 
527
527
  Forbidden writes:
528
528
 
529
- - `~/.claude.json`
530
- - `.claude/CLAUDE.md`
529
+ - `~/.claude.json` except guarded user/global `mcpServers.vgxness` merge
530
+ - `.claude/CLAUDE.md`; user/global uses `~/.claude/CLAUDE.md` and manages only the VGXNESS block
531
531
 
532
532
  Malformed or conflicting VGXNESS markers in project-root `CLAUDE.md` refuse the full Claude project install before any Claude target is mutated.
533
533
 
package/docs/providers.md CHANGED
@@ -7,7 +7,7 @@ VGXNESS is provider-agnostic at the core: the registry stores provider-neutral d
7
7
  | Provider | Control plane | Code runtime | Notes |
8
8
  |---|---|---|---|
9
9
  | OpenCode | `managed` | n/a (target) | Primary supported provider. The configurator renders OpenCode MCP config and manager/SDD agent definitions into the chosen scope. |
10
- | Claude Code | `supported-secondary` | n/a | Supported for Claude CLI MCP registration planning/apply, project `.mcp.json` compatibility, project-root `CLAUDE.md` managed memory, and project/user agent planning. Scopes are `local`, `project`, and `user`; `personal`/`global` map to `user` for compatibility. Confirmed applies are explicit and guarded; read-only surfaces do not execute Claude Code and VGXNESS never manually mutates `~/.claude.json`. |
10
+ | Claude Code | `supported-secondary` | n/a | Supported for Claude CLI MCP registration planning/apply, project `.mcp.json` compatibility, guarded `CLAUDE.md` managed memory, user `~/.claude.json` MCP merge, and project/user agent files. Scopes are `local`, `project`, and `user`; `personal`/`global` map to `user` for compatibility. Confirmed applies are explicit and guarded; read-only surfaces inspect files without executing Claude Code. |
11
11
  | Antigravity | `placeholder` | n/a | Listed in the TUI Installation surface as coming-soon. |
12
12
  | Custom / future | `extension` | extension point | Per the [Architecture](./architecture.md) decision, anything not OpenCode or Claude is a custom extension. |
13
13
  | OpenAI-compatible | n/a | `openai-compatible-provider-adapter.ts` | Real adapter used by `vgxness code`. Speaks to any OpenAI-compatible endpoint. |
@@ -46,7 +46,7 @@ vgxness agents render --provider json --project vgxness --name apply-agent
46
46
 
47
47
  Claude Code is supported as a secondary, non-default control-plane target. Read-only status, doctor, setup plan, and change-plan surfaces can inspect or preview Claude targets without Claude Code installed and without writing files.
48
48
 
49
- Claude scopes are canonicalized to `local`, `project`, and `user`; VGXNESS compatibility aliases `personal` and legacy `global` map to Claude `user` with warnings. MCP registration is represented as argv only, for example `claude mcp add --scope user vgxness -- vgxness mcp start`; no shell strings or manual `~/.claude.json` edits are generated. Status/doctor/change-plan avoid private Claude config reads and do not execute Claude Code.
49
+ Claude scopes are canonicalized to `local`, `project`, and `user`; VGXNESS compatibility aliases `personal` and legacy `global` map to Claude `user` with warnings. MCP registration is represented as structured config/argv, for example `claude mcp add --scope user vgxness -- vgxness mcp start`; no shell strings are generated. For user/global apply, VGXNESS narrowly merges only `mcpServers.vgxness` in `~/.claude.json`, preserves unknown keys, backs up existing files, and does not manage auth/session data. Status/doctor/change-plan are read-only and do not execute Claude Code.
50
50
 
51
51
  Allowed project-scope writes, only after explicit guarded apply with run preflight metadata (`vgxness mcp install claude --scope project --yes --run-id <id>`):
52
52
 
@@ -58,8 +58,8 @@ User-scoped agent files target `~/.claude/agents/*.md` only as an explicit exter
58
58
 
59
59
  Forbidden writes:
60
60
 
61
- - `~/.claude.json`
62
- - `.claude/CLAUDE.md`
61
+ - `~/.claude.json` except the guarded user/global merge of `mcpServers.vgxness`
62
+ - `.claude/CLAUDE.md`; user/global memory is `~/.claude/CLAUDE.md` and only the VGXNESS managed block is touched
63
63
 
64
64
  Malformed, duplicate, partial, or conflicting VGXNESS markers in `CLAUDE.md` cause the full Claude project install to be refused before `.mcp.json`, `.claude/agents/*.md`, or `CLAUDE.md` are mutated.
65
65
 
@@ -140,7 +140,7 @@ Writes happen only through `apply` with explicit consent. Plans, status, doctor,
140
140
 
141
141
  Adapters and renderers must not:
142
142
 
143
- - Write provider config outside explicit guarded apply. OpenCode install writes only its selected target; Claude project compatibility apply writes only `.mcp.json`, `.claude/agents/*.md`, and the guarded project-root `CLAUDE.md` managed block; Claude local/user MCP registration uses Claude CLI argv after confirmation/preflight and never manual private-config mutation.
143
+ - Write provider config outside explicit guarded apply. OpenCode install writes only its selected target; Claude project compatibility apply writes only `.mcp.json`, `.claude/agents/*.md`, and the guarded project-root `CLAUDE.md` managed block. Claude user/global apply narrowly merges `mcpServers.vgxness` in `~/.claude.json`, writes VGXNESS-owned `~/.claude/agents/*.md`, and manages only the VGXNESS block in `~/.claude/CLAUDE.md` after confirmation/preflight.
144
144
  - Call providers (`opencode`, `claude`, etc.) during preview or status.
145
145
  - Install global memory.
146
146
  - Create `openspec/`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vgxness",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "CLI and MCP control plane for guided AI-agent workflows, SDD, memory, and OpenCode setup.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {