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.
- package/dist/cli/cli-help.js +1 -1
- package/dist/mcp/claude-code-user-config.js +55 -0
- package/dist/mcp/claude-code-user-memory.js +90 -0
- package/dist/mcp/client-install-claude-code-contract.js +57 -6
- package/dist/mcp/client-install-claude-code.js +78 -8
- package/dist/mcp/control-plane.js +18 -1
- package/dist/mcp/index.js +2 -0
- package/dist/mcp/provider-change-plan.js +5 -45
- package/dist/mcp/provider-doctor.js +23 -7
- package/dist/mcp/provider-status.js +22 -29
- package/dist/sdd/schema.js +15 -0
- package/dist/sdd/sdd-workflow-service.js +59 -29
- package/dist/setup/providers/claude-setup-adapter.js +4 -2
- package/docs/architecture.md +2 -2
- package/docs/cli.md +4 -4
- package/docs/providers.md +5 -5
- package/package.json +1 -1
package/dist/cli/cli-help.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
18
|
-
|
|
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
|
|
87
|
-
'Project compatibility may write .mcp.json, .claude/agents/*.md, and a guarded project-root CLAUDE.md managed block after explicit confirmation/preflight
|
|
88
|
-
'
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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: '
|
|
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: '
|
|
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
|
-
|
|
147
|
-
|
|
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
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
171
|
-
const
|
|
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'
|
|
186
|
-
warningCount:
|
|
187
|
-
summary:
|
|
188
|
-
nextAction:
|
|
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:
|
|
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'),
|
package/dist/sdd/schema.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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',
|
package/docs/architecture.md
CHANGED
|
@@ -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
|
|
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.
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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/`.
|