vgxness 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,12 +2,10 @@ 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
- export const providerChangePlanTypes = ['opencode-mcp-install', 'setup', 'install', 'config-preparation'];
8
+ export const providerChangePlanTypes = ['opencode-mcp-install', 'claude-mcp-install', 'setup', 'install', 'config-preparation'];
11
9
  const safety = {
12
10
  readOnly: true,
13
11
  willWrite: false,
@@ -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',
@@ -142,7 +102,7 @@ function unsupportedProviderEnvelope(input) {
142
102
  supported: false,
143
103
  status: 'unsupported',
144
104
  code: 'UNSUPPORTED_PROVIDER',
145
- summary: `${input.provider} is a recognized provider value, but provider change planning is currently implemented only for OpenCode.`,
105
+ summary: `${input.provider} is a recognized provider value, but read-only provider change planning currently supports OpenCode and Claude.`,
146
106
  providerIdentity: {
147
107
  provider: input.provider,
148
108
  adapter: input.provider,
@@ -160,15 +120,16 @@ function unsupportedProviderEnvelope(input) {
160
120
  };
161
121
  }
162
122
  function blockedPlanEnvelope(input, status, doctor, message) {
123
+ const providerName = input.provider === 'claude' ? 'Claude' : 'OpenCode';
163
124
  return {
164
125
  ...baseEnvelope(input),
165
126
  supported: true,
166
127
  status: 'blocked',
167
128
  code: 'PLAN_UNAVAILABLE',
168
- summary: `OpenCode change planning could not resolve the future MCP server command: ${message}`,
129
+ summary: `${providerName} change planning could not resolve the future MCP server command: ${message}`,
169
130
  providerIdentity: {
170
- provider: 'opencode',
171
- adapter: 'opencode',
131
+ provider: input.provider,
132
+ adapter: input.provider,
172
133
  support: 'supported',
173
134
  },
174
135
  statusSummary: statusSummary(status),
@@ -224,7 +185,7 @@ function claudeEnvelope(input, status, doctor, installPlan, source) {
224
185
  supported: true,
225
186
  status: installPlan.status === 'refused' ? 'blocked' : 'planned',
226
187
  ...(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.`,
188
+ 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
189
  providerIdentity: { provider: 'claude', adapter: 'claude', support: 'supported' },
229
190
  statusSummary: statusSummary(status),
230
191
  statusFindings: status.config,
@@ -246,14 +207,24 @@ function baseEnvelope(input) {
246
207
  scope: input.scope,
247
208
  provider: input.provider,
248
209
  requestedChangeType: input.changeType,
249
- normalizedChangeType: normalizeChangeType(input.changeType),
210
+ normalizedChangeType: normalizeChangeType(input.provider, input.changeType),
250
211
  payloadMode: input.payloadMode,
251
212
  workspaceRoot: input.workspaceRoot,
252
213
  safety,
253
214
  };
254
215
  }
255
- function normalizeChangeType(changeType) {
256
- return changeType === 'setup' || changeType === 'install' || changeType === 'config-preparation' ? 'opencode-mcp-install' : changeType;
216
+ function normalizeChangeType(provider, changeType) {
217
+ if (provider === 'opencode') {
218
+ return changeType === 'setup' || changeType === 'install' || changeType === 'config-preparation' || changeType === 'claude-mcp-install'
219
+ ? 'opencode-mcp-install'
220
+ : changeType;
221
+ }
222
+ if (provider === 'claude') {
223
+ return changeType === 'setup' || changeType === 'install' || changeType === 'config-preparation' || changeType === 'opencode-mcp-install'
224
+ ? 'claude-mcp-install'
225
+ : changeType;
226
+ }
227
+ return changeType;
257
228
  }
258
229
  function statusSummary(status) {
259
230
  return {
@@ -309,11 +280,11 @@ function previewEffectsClaude(plan) {
309
280
  const projectMemory = projectMemoryPreview(plan.targets);
310
281
  if (plan.status === 'refused')
311
282
  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');
283
+ const mcpTarget = plan.targets.find((target) => target.kind === 'mcp-json' || target.kind === 'user-mcp-json');
313
284
  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
285
  }
315
286
  function projectMemoryPreview(targets) {
316
- const target = targets.find((item) => item.kind === 'project-memory');
287
+ const target = targets.find((item) => item.kind === 'project-memory' || item.kind === 'user-memory');
317
288
  if (target === undefined)
318
289
  return undefined;
319
290
  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 +323,7 @@ function risksForOpenCode(status, doctor, plan, source) {
352
323
  return risks;
353
324
  }
354
325
  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.'];
326
+ 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
327
  if (status.status !== 'ready')
357
328
  risks.push(`Provider status is ${status.status}; future writes should review status findings first.`);
358
329
  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'),
@@ -99,7 +99,7 @@ const finalRunStatuses = ['completed', 'failed', 'blocked', 'cancelled'];
99
99
  const runOutcomes = ['success', 'partial', 'failure', 'blocked', 'cancelled'];
100
100
  const payloadModes = ['compact', 'verbose'];
101
101
  const providerChangePlanProviders = ['opencode', 'claude', 'antigravity', 'custom'];
102
- const providerChangePlanTypes = ['opencode-mcp-install', 'setup', 'install', 'config-preparation'];
102
+ const providerChangePlanTypes = ['opencode-mcp-install', 'claude-mcp-install', 'setup', 'install', 'config-preparation'];
103
103
  const jsonValueSchema = z.lazy(() => z.union([z.string(), z.number().finite(), z.boolean(), z.null(), z.array(jsonValueSchema), z.record(z.string(), jsonValueSchema)]));
104
104
  export const VGX_MCP_TOOL_INPUT_SCHEMAS = {
105
105
  vgxness_sdd_status: z
@@ -13,7 +13,7 @@ const runOutcomes = ['success', 'partial', 'failure', 'blocked', 'cancelled'];
13
13
  const permissionDecisions = ['allow', 'ask', 'deny'];
14
14
  const payloadModes = ['compact', 'verbose'];
15
15
  const providerChangePlanProviders = ['opencode', 'claude', 'antigravity', 'custom'];
16
- const providerChangePlanTypes = ['opencode-mcp-install', 'setup', 'install', 'config-preparation'];
16
+ const providerChangePlanTypes = ['opencode-mcp-install', 'claude-mcp-install', 'setup', 'install', 'config-preparation'];
17
17
  const validChangePattern = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
18
18
  const validMemoryTopicKeyPattern = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
19
19
  const maxMemoryTitleLength = 200;
@@ -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) {