vgxness 1.12.0 → 1.13.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.
@@ -7,7 +7,7 @@ import { inspectClaudeCodeUserMcpConfig } from './claude-code-user-config.js';
7
7
  import { inspectClaudeUserMemory } from './claude-code-user-memory.js';
8
8
  import { vgxnessOpenCodeDefaultAgent, vgxnessOpenCodeSddSubagents } from './opencode-default-agent-config.js';
9
9
  import { buildCanonicalAgentManifestDiagnostic } from './provider-canonical-agent-manifest.js';
10
- import { normalizeProviderHealthInput, PROVIDER_HEALTH_SAFETY, CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES, classifyProviderConfigPaths, providerHealthFailure, providerRuntimeContext, isUserGlobalScope, REQUIRED_PROVIDER_NATIVE_MCP_TOOLS, rollupProviderDoctor, } from './provider-health-types.js';
10
+ import { normalizeProviderHealthInput, PROVIDER_HEALTH_SAFETY, CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES, classifyProviderConfigPaths, providerEvidence, providerHealthFailure, providerRuntimeContext, isUserGlobalScope, REQUIRED_PROVIDER_NATIVE_MCP_TOOLS, rollupProviderDoctor, } from './provider-health-types.js';
11
11
  import { inspectOpenCodeConfigPaths } from './provider-status.js';
12
12
  export class ProviderDoctorService {
13
13
  deps;
@@ -28,6 +28,7 @@ export class ProviderDoctorService {
28
28
  const before = snapshotPaths(paths.map((path) => path.path), normalized.workspaceRoot);
29
29
  const readableJson = paths.filter((path) => path.parsed);
30
30
  const config = readFirstConfig(readableJson[0]?.path);
31
+ const evidence = providerEvidence({ staticManifestKnown: true, renderedConfigExpected: true, configPathInspected: paths.length > 0 });
31
32
  const checks = [
32
33
  {
33
34
  id: 'workspace-root',
@@ -42,7 +43,7 @@ export class ProviderDoctorService {
42
43
  subagentsCheck(config),
43
44
  delegationCheck(config),
44
45
  { id: 'mcp-current-call', status: 'pass', detail: 'Current MCP call reached the VGXNESS control-plane.' },
45
- { id: 'mcp-required-tools', status: 'pass', detail: `Required provider-native tools are registered: ${REQUIRED_PROVIDER_NATIVE_MCP_TOOLS.join(', ')}.` },
46
+ { id: 'mcp-required-tools', status: 'pass', detail: `Static VGXNESS manifest expects provider-native tools: ${REQUIRED_PROVIDER_NATIVE_MCP_TOOLS.join(', ')}. MCP host tool presence was not verified.` },
46
47
  promptContractCheck(config, normalized.expectedPromptContractVersion),
47
48
  readonlySafetyCheck(before, snapshotPaths(paths.map((path) => path.path), normalized.workspaceRoot)),
48
49
  ];
@@ -54,8 +55,8 @@ export class ProviderDoctorService {
54
55
  : paths.filter((path) => path.exists || path.status !== 'not-configured').map((path) => path.path);
55
56
  const failedChecks = checks.filter((check) => check.status === 'fail').map((check) => check.id);
56
57
  const recommendations = checks.flatMap((check) => (check.remediation === undefined ? [] : [check.remediation]));
57
- const originalBytes = Buffer.byteLength(JSON.stringify({ checks, checkedPaths: paths.map((path) => path.path) }), 'utf8');
58
- const compactBytes = Buffer.byteLength(JSON.stringify({ checks: compactChecksValue, checkedPaths }), 'utf8');
58
+ const originalBytes = Buffer.byteLength(JSON.stringify({ checks, checkedPaths: paths.map((path) => path.path), providerEvidence: evidence }), 'utf8');
59
+ const compactBytes = Buffer.byteLength(JSON.stringify({ checks: compactChecksValue, checkedPaths, providerEvidence: evidence }), 'utf8');
59
60
  return {
60
61
  ok: true,
61
62
  value: {
@@ -80,6 +81,7 @@ export class ProviderDoctorService {
80
81
  recommendations,
81
82
  checks: compactChecksValue,
82
83
  checkedPaths,
84
+ providerEvidence: evidence,
83
85
  bytes: { originalBytes, compactBytes },
84
86
  verboseAvailable: normalized.payloadMode === 'compact',
85
87
  fullContentRef: `provider-doctor:${normalized.providerAdapter}:${normalized.workspaceRoot}`,
@@ -99,6 +101,7 @@ export class ProviderDoctorService {
99
101
  const projectMemory = inspectClaudeProjectMemory(normalized.workspaceRoot);
100
102
  const advisoryPaths = claudeAdvisoryPaths(normalized.workspaceRoot);
101
103
  const checkedPathList = [mcp.path, agents.directoryPath, ...agents.agents.map((agent) => agent.path), projectMemory.path, ...advisoryPaths];
104
+ const evidence = providerEvidence({ staticManifestKnown: true, renderedConfigExpected: true, configPathInspected: checkedPathList.length > 0 });
102
105
  const before = snapshotPaths(checkedPathList, normalized.workspaceRoot);
103
106
  const missingAgents = agents.agents.filter((agent) => agent.status === 'missing');
104
107
  const blockingAgents = agents.agents.filter((agent) => agent.status === 'conflicting' || agent.status === 'invalid');
@@ -133,13 +136,14 @@ export class ProviderDoctorService {
133
136
  const checkedPaths = normalized.payloadMode === 'verbose' ? checkedPathList : checkedPathList.filter((path) => existsSync(path) || path === mcp.path);
134
137
  const failedChecks = checks.filter((check) => check.status === 'fail').map((check) => check.id);
135
138
  const recommendations = checks.flatMap((check) => (check.remediation === undefined ? [] : [check.remediation]));
136
- return { ok: true, value: { version: 1, kind: 'provider-doctor', project: normalized.project, providerAdapter: 'claude', scope: normalized.scope, workspaceRoot: normalized.workspaceRoot, runtimeContext: providerRuntimeContext(normalized), configClassification: externalProjectClassification(checkedPathList), 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 } };
139
+ return { ok: true, value: { version: 1, kind: 'provider-doctor', project: normalized.project, providerAdapter: 'claude', scope: normalized.scope, workspaceRoot: normalized.workspaceRoot, runtimeContext: providerRuntimeContext(normalized), configClassification: externalProjectClassification(checkedPathList), 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, providerEvidence: evidence, bytes: { originalBytes: Buffer.byteLength(JSON.stringify({ checks, checkedPaths: checkedPathList, providerEvidence: evidence }), 'utf8'), compactBytes: Buffer.byteLength(JSON.stringify({ checks: compactChecksValue, checkedPaths, providerEvidence: evidence }), 'utf8') }, verboseAvailable: normalized.payloadMode === 'compact', fullContentRef: `provider-doctor:claude:${normalized.workspaceRoot}`, generatedAt: 'read-only-snapshot', safety: PROVIDER_HEALTH_SAFETY } };
137
140
  }
138
141
  getClaudeUserGlobalDoctor(normalized, canonicalScope = 'user', scopeWarnings = []) {
139
142
  const mcp = inspectClaudeCodeUserMcpConfig(normalized.env);
140
143
  const agents = inspectClaudeCodeAgents({ workspaceRoot: normalized.workspaceRoot, scope: 'user', env: normalized.env });
141
144
  const userMemory = inspectClaudeUserMemory(normalized.env);
142
145
  const checkedPathList = [mcp.path, agents.directoryPath, ...agents.agents.map((agent) => agent.path), userMemory.path];
146
+ const evidence = providerEvidence({ staticManifestKnown: true, renderedConfigExpected: true, configPathInspected: checkedPathList.length > 0 });
143
147
  const before = snapshotPaths(checkedPathList, normalized.workspaceRoot);
144
148
  const missingAgents = agents.agents.filter((agent) => agent.status === 'missing');
145
149
  const blockingAgents = agents.agents.filter((agent) => agent.status === 'conflicting' || agent.status === 'invalid');
@@ -164,7 +168,7 @@ export class ProviderDoctorService {
164
168
  const failedChecks = checks.filter((check) => check.status === 'fail').map((check) => check.id);
165
169
  const recommendations = checks.flatMap((check) => (check.remediation === undefined ? [] : [check.remediation]));
166
170
  const checkedPaths = normalized.payloadMode === 'verbose' ? checkedPathList : checkedPathList.filter((path) => existsSync(path) || path === mcp.path || path === userMemory.path);
167
- return { ok: true, value: { version: 1, kind: 'provider-doctor', project: normalized.project, providerAdapter: 'claude', scope: normalized.scope, workspaceRoot: normalized.workspaceRoot, runtimeContext: providerRuntimeContext(normalized), configClassification: managedUserGlobalClassification(checkedPathList), 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 } } };
171
+ return { ok: true, value: { version: 1, kind: 'provider-doctor', project: normalized.project, providerAdapter: 'claude', scope: normalized.scope, workspaceRoot: normalized.workspaceRoot, runtimeContext: providerRuntimeContext(normalized), configClassification: managedUserGlobalClassification(checkedPathList), 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, providerEvidence: evidence, bytes: { originalBytes: Buffer.byteLength(JSON.stringify({ checks, checkedPaths: checkedPathList, providerEvidence: evidence }), 'utf8'), compactBytes: Buffer.byteLength(JSON.stringify({ checks: compactChecksValue, checkedPaths, providerEvidence: evidence }), '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 } } };
168
172
  }
169
173
  }
170
174
  function externalProjectClassification(paths) {
@@ -34,6 +34,26 @@ export function classifyProviderConfigPaths(paths) {
34
34
  detectedExternalUserConfig: paths.filter((path) => path.diagnostics.ownership === 'external-user' && path.exists).map((path) => path.path),
35
35
  };
36
36
  }
37
+ export function providerEvidence(input) {
38
+ const hostIntrospectionAvailable = input.hostIntrospectionAvailable ?? false;
39
+ const hostToolPresenceVerified = input.hostToolPresenceVerified ?? false;
40
+ return {
41
+ staticManifestKnown: input.staticManifestKnown,
42
+ renderedConfigExpected: input.renderedConfigExpected,
43
+ configPathInspected: input.configPathInspected,
44
+ hostIntrospectionAvailable,
45
+ hostToolPresenceVerified,
46
+ evidenceLevel: hostIntrospectionAvailable ? 'host-introspection' : input.configPathInspected ? 'config-inspection' : 'static-manifest',
47
+ notes: [
48
+ 'VGXNESS inspected static provider manifests and expected provider configuration metadata only.',
49
+ input.configPathInspected
50
+ ? 'VGXNESS inspected provider config paths as read-only diagnostics.'
51
+ : 'VGXNESS did not inspect provider config paths; evidence is limited to static manifests.',
52
+ 'No provider host process was launched during this diagnostic.',
53
+ 'MCP host tool presence is not verified by provider status/doctor.',
54
+ ],
55
+ };
56
+ }
37
57
  export const REQUIRED_PROVIDER_MCP_TOOLS = ['vgxness_provider_status', 'vgxness_provider_doctor', 'vgxness_provider_change_plan'];
38
58
  export const REQUIRED_PROVIDER_NATIVE_MCP_TOOLS = [
39
59
  'vgxness_provider_status',
@@ -9,7 +9,7 @@ import { inspectClaudeUserMemory } from './claude-code-user-memory.js';
9
9
  import { resolveOpenCodeMcpInstallTarget } from './client-install-opencode-contract.js';
10
10
  import { vgxnessOpenCodeDefaultAgent, vgxnessOpenCodePromptContractVersion, vgxnessOpenCodeSddSubagents } from './opencode-default-agent-config.js';
11
11
  import { buildCanonicalAgentManifestDiagnostic } from './provider-canonical-agent-manifest.js';
12
- import { normalizeProviderHealthInput, PROVIDER_HEALTH_SAFETY, CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES, classifyProviderConfigPaths, providerConfigPathDiagnostics, providerRuntimeContext, providerHealthFailure, isUserGlobalScope, REQUIRED_PROVIDER_MCP_TOOLS, rollupProviderHealth, } from './provider-health-types.js';
12
+ import { normalizeProviderHealthInput, PROVIDER_HEALTH_SAFETY, CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES, classifyProviderConfigPaths, providerConfigPathDiagnostics, providerRuntimeContext, providerEvidence, providerHealthFailure, isUserGlobalScope, REQUIRED_PROVIDER_MCP_TOOLS, rollupProviderHealth, } from './provider-health-types.js';
13
13
  const projectConfigTargets = ['.opencode/opencode.json', 'opencode.json', '.opencode/opencode.jsonc', 'opencode.jsonc'];
14
14
  export class ProviderStatusService {
15
15
  deps;
@@ -29,6 +29,7 @@ export class ProviderStatusService {
29
29
  const managerConfigured = hasConfiguredManager(providerConfig);
30
30
  const subagentsConfigured = hasConfiguredSubagents(providerConfig);
31
31
  const tools = requiredToolPresence();
32
+ const evidence = providerEvidence({ staticManifestKnown: true, renderedConfigExpected: true, configPathInspected: paths.length > 0 });
32
33
  const configStatus = resolveConfigStatus(paths, mcpEntry, managerConfigured, subagentsConfigured);
33
34
  const status = rollupProviderHealth([canonicalAgentManifest.status, configStatus, ...tools.map((tool) => (tool.present ? 'pass' : 'fail'))]);
34
35
  const sdd = normalized.change.length > 0 ? this.readSdd(normalized.project, normalized.change) : undefined;
@@ -39,12 +40,14 @@ export class ProviderStatusService {
39
40
  const verboseShape = {
40
41
  config: { status: configStatus, paths, mcpEntry },
41
42
  canonicalAgentManifest,
43
+ providerEvidence: evidence,
42
44
  sdd,
43
45
  mcpRequiredTools: tools,
44
46
  };
45
47
  const compactShape = {
46
48
  config: { status: configStatus, paths: compactPaths(paths, 'compact'), mcpEntry: compactMcpEntry(mcpEntry, 'compact') },
47
49
  canonicalAgentManifest,
50
+ providerEvidence: evidence,
48
51
  sdd: sdd === undefined ? undefined : compactSdd(sdd, 'compact'),
49
52
  mcpRequiredTools: tools,
50
53
  };
@@ -70,6 +73,7 @@ export class ProviderStatusService {
70
73
  nextAction: nextActionFor(status, mcpEntry, sdd?.next),
71
74
  checkedPaths,
72
75
  canonicalAgentManifest,
76
+ providerEvidence: evidence,
73
77
  ...(sdd === undefined ? {} : { sdd: compactSdd(sdd, normalized.payloadMode) }),
74
78
  mcpRequiredTools: tools,
75
79
  originalBytes,
@@ -112,8 +116,9 @@ export class ProviderStatusService {
112
116
  const sdd = normalized.change.length > 0 ? this.readSdd(normalized.project, normalized.change) : undefined;
113
117
  const checkedPaths = normalized.payloadMode === 'verbose' ? [mcpState.path, agents.directoryPath, ...agents.agents.map((agent) => agent.path), projectMemory.path, ...claudeAdvisoryPaths(normalized.workspaceRoot)] : [mcpState.path, projectMemory.path, ...agents.agents.filter((agent) => agent.exists || agent.status !== 'missing').map((agent) => agent.path), ...advisory];
114
118
  const tools = [...requiredToolPresence(), { tool: 'claude-cli', present: false, diagnostic: 'Read-only status does not execute `claude --version`; CLI presence is checked during explicit apply/preflight only.' }];
115
- const verboseShape = { config: { status: configStatus, paths, mcpEntry }, canonicalAgentManifest, agents, projectMemory, advisory, sdd, mcpRequiredTools: tools };
116
- const compactShape = { config: { status: configStatus, paths: compactPaths(paths, 'compact'), mcpEntry: compactMcpEntry(mcpEntry, 'compact') }, canonicalAgentManifest, agentSummary: summarizeClaudeAgents(agents), projectMemory: { status: projectMemory.status, action: projectMemory.action }, advisory, sdd: sdd === undefined ? undefined : compactSdd(sdd, 'compact'), mcpRequiredTools: tools };
119
+ const evidence = providerEvidence({ staticManifestKnown: true, renderedConfigExpected: true, configPathInspected: paths.length > 0 });
120
+ const verboseShape = { config: { status: configStatus, paths, mcpEntry }, canonicalAgentManifest, providerEvidence: evidence, agents, projectMemory, advisory, sdd, mcpRequiredTools: tools };
121
+ const compactShape = { config: { status: configStatus, paths: compactPaths(paths, 'compact'), mcpEntry: compactMcpEntry(mcpEntry, 'compact') }, canonicalAgentManifest, providerEvidence: evidence, agentSummary: summarizeClaudeAgents(agents), projectMemory: { status: projectMemory.status, action: projectMemory.action }, advisory, sdd: sdd === undefined ? undefined : compactSdd(sdd, 'compact'), mcpRequiredTools: tools };
117
122
  const originalBytes = Buffer.byteLength(JSON.stringify(verboseShape), 'utf8');
118
123
  const compactBytes = Buffer.byteLength(JSON.stringify(compactShape), 'utf8');
119
124
  const issueCount = [canonicalAgentManifest.status, configStatus, ...agentStatuses].filter((item) => item === 'fail' || item === 'not-configured').length;
@@ -136,6 +141,7 @@ export class ProviderStatusService {
136
141
  nextAction: nextActionFor(status, mcpEntry, sdd?.next),
137
142
  checkedPaths,
138
143
  canonicalAgentManifest,
144
+ providerEvidence: evidence,
139
145
  ...(sdd === undefined ? {} : { sdd: compactSdd(sdd, normalized.payloadMode) }),
140
146
  mcpRequiredTools: tools,
141
147
  originalBytes,
@@ -163,11 +169,12 @@ export class ProviderStatusService {
163
169
  const agentStatuses = agents.agents.map((agent) => (agent.status === 'managed' ? 'pass' : agent.status === 'missing' ? 'not-configured' : 'fail'));
164
170
  const configStatus = claudeConfigHealthStatus([...paths.map((path) => path.status), ...agentStatuses]);
165
171
  const tools = [...requiredToolPresence(), { tool: 'claude-cli', present: false, diagnostic: 'Read-only status does not execute `claude --version`; no Claude Code process was launched.' }];
172
+ const evidence = providerEvidence({ staticManifestKnown: true, renderedConfigExpected: true, configPathInspected: paths.length > 0 });
166
173
  const status = rollupProviderHealth([canonicalAgentManifest.status, configStatus]);
167
174
  const sdd = normalized.change.length > 0 ? this.readSdd(normalized.project, normalized.change) : undefined;
168
175
  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)];
169
- 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 };
170
- 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 };
176
+ const verboseShape = { config: { status: configStatus, paths, mcpEntry }, canonicalAgentManifest, providerEvidence: evidence, agents, userMemory: { status: userMemory.status, action: userMemory.action }, scopeCapabilities: CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES, scopeWarnings, sdd, mcpRequiredTools: tools };
177
+ const compactShape = { config: { status: configStatus, paths: compactPaths(paths, 'compact'), mcpEntry: compactMcpEntry(mcpEntry, 'compact') }, canonicalAgentManifest, providerEvidence: evidence, 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 };
171
178
  const originalBytes = Buffer.byteLength(JSON.stringify(verboseShape), 'utf8');
172
179
  const compactBytes = Buffer.byteLength(JSON.stringify(compactShape), 'utf8');
173
180
  const reportBase = {
@@ -188,6 +195,7 @@ export class ProviderStatusService {
188
195
  nextAction: nextActionFor(status, mcpEntry, sdd?.next),
189
196
  checkedPaths,
190
197
  canonicalAgentManifest,
198
+ providerEvidence: evidence,
191
199
  ...(sdd === undefined ? {} : { sdd: compactSdd(sdd, normalized.payloadMode) }),
192
200
  mcpRequiredTools: tools,
193
201
  originalBytes,
@@ -335,7 +343,11 @@ function inspectOpenCodeMcpEntry(paths) {
335
343
  }
336
344
  }
337
345
  function requiredToolPresence() {
338
- return REQUIRED_PROVIDER_MCP_TOOLS.map((tool) => ({ tool, present: true }));
346
+ return REQUIRED_PROVIDER_MCP_TOOLS.map((tool) => ({
347
+ tool,
348
+ present: true,
349
+ diagnostic: 'Static VGXNESS manifest expects this MCP tool; provider host tool presence was not verified.',
350
+ }));
339
351
  }
340
352
  function resolveConfigStatus(paths, mcpEntry, managerConfigured, subagentsConfigured) {
341
353
  if (paths.some((path) => path.status === 'fail') || mcpEntry.status === 'fail')
@@ -109,7 +109,125 @@ const contextCockpitLevels = ['compact', 'expanded', 'verbose'];
109
109
  const providerChangePlanProviders = ['opencode', 'claude', 'antigravity', 'custom'];
110
110
  const providerChangePlanTypes = ['opencode-mcp-install', 'claude-mcp-install', 'setup', 'install', 'config-preparation'];
111
111
  const sddPhaseInputSchema = z.union([z.enum(sddPhases), z.literal('apply')]);
112
+ const sddPhaseSchema = z.enum(sddPhases);
112
113
  const jsonValueSchema = z.lazy(() => z.union([z.string(), z.number().finite(), z.boolean(), z.null(), z.array(jsonValueSchema), z.record(z.string(), jsonValueSchema)]));
114
+ const providerEvidenceOutputSchema = z
115
+ .object({
116
+ staticManifestKnown: z.boolean(),
117
+ renderedConfigExpected: z.boolean(),
118
+ configPathInspected: z.boolean(),
119
+ hostIntrospectionAvailable: z.boolean(),
120
+ hostToolPresenceVerified: z.boolean(),
121
+ evidenceLevel: z.enum(['static-manifest', 'config-inspection', 'host-introspection']),
122
+ notes: z.array(z.string()),
123
+ })
124
+ .passthrough();
125
+ const sddPhaseGateOutputSchema = z
126
+ .object({
127
+ phase: sddPhaseSchema,
128
+ topicKey: z.string(),
129
+ artifactPresent: z.boolean(),
130
+ artifactStatus: z.enum(['missing', 'draft', 'accepted', 'rejected', 'superseded']),
131
+ artifactState: z.enum(['missing', 'draft', 'accepted', 'rejected', 'superseded', 'legacy']),
132
+ accepted: z.boolean(),
133
+ acceptedByHuman: z.boolean(),
134
+ agentCallable: z.boolean(),
135
+ requiresHumanAcceptance: z.boolean(),
136
+ draftPresent: z.boolean(),
137
+ contentFrozen: z.boolean(),
138
+ runnable: z.boolean(),
139
+ blocked: z.boolean(),
140
+ blockedReasons: z.array(z.string()),
141
+ humanOnly: z.boolean(),
142
+ preflightRequired: z.boolean(),
143
+ requiresPreflight: z.boolean(),
144
+ requiresProviderWriteConsent: z.boolean(),
145
+ canDraft: z.boolean(),
146
+ canMarkReady: z.boolean(),
147
+ canAccept: z.boolean(),
148
+ canReopen: z.boolean(),
149
+ nextAllowedActions: z.array(z.string()),
150
+ requiredPriorPhase: sddPhaseSchema.optional(),
151
+ blockerReason: z.enum(['missing', 'draft', 'accepted', 'legacy', 'rejected', 'superseded']).optional(),
152
+ })
153
+ .passthrough();
154
+ const sddReadinessGatesOutputSchema = z
155
+ .object({
156
+ phase: sddPhaseGateOutputSchema,
157
+ prerequisites: z.array(sddPhaseGateOutputSchema),
158
+ runnable: z.boolean(),
159
+ blocked: z.boolean(),
160
+ blockedReasons: z.array(z.string()),
161
+ humanOnly: z.boolean(),
162
+ preflightRequired: z.boolean(),
163
+ agentCallable: z.boolean(),
164
+ canDraft: z.boolean(),
165
+ canMarkReady: z.boolean(),
166
+ canAccept: z.boolean(),
167
+ canReopen: z.boolean(),
168
+ nextAllowedActions: z.array(z.string()),
169
+ requiresProviderWriteConsent: z.boolean(),
170
+ })
171
+ .passthrough();
172
+ const sddCockpitGatesOutputSchema = z
173
+ .object({
174
+ phases: z.array(sddPhaseGateOutputSchema),
175
+ changeComplete: z.boolean(),
176
+ nextPhase: sddPhaseSchema.optional(),
177
+ nextPhaseRunnable: z.boolean(),
178
+ runnableNextPhases: z.array(sddPhaseSchema),
179
+ blockedTransitions: z.array(sddPhaseGateOutputSchema),
180
+ blockedReasons: z.array(z.string()),
181
+ requiresHumanAcceptance: z.boolean(),
182
+ humanOnly: z.boolean(),
183
+ preflightRequired: z.boolean(),
184
+ agentCallable: z.boolean(),
185
+ canDraft: z.boolean(),
186
+ canMarkReady: z.boolean(),
187
+ canAccept: z.boolean(),
188
+ canReopen: z.boolean(),
189
+ nextAllowedActions: z.array(z.string()),
190
+ requiresProviderWriteConsent: z.boolean(),
191
+ })
192
+ .passthrough();
193
+ const sddRecommendedActionOutputSchema = z
194
+ .object({
195
+ id: z.string(),
196
+ label: z.string(),
197
+ title: z.string(),
198
+ description: z.string(),
199
+ kind: z.enum(['inspect', 'draft-phase', 'mark-ready', 'accept-human', 'reopen-human']),
200
+ category: z.enum(['inspection', 'sdd-phase', 'human-governance']),
201
+ phase: sddPhaseSchema.optional(),
202
+ targetTool: z.string(),
203
+ suggestedArgs: z.record(z.string(), jsonValueSchema),
204
+ readOnly: z.boolean(),
205
+ mutating: z.boolean(),
206
+ agentCallable: z.boolean(),
207
+ humanOnly: z.boolean(),
208
+ requiresHumanApproval: z.boolean(),
209
+ requiresHumanConfirmation: z.boolean(),
210
+ requiresPreflight: z.boolean(),
211
+ requiresProviderWriteConsent: z.boolean(),
212
+ reason: z.string(),
213
+ rationale: z.string(),
214
+ blockingPrerequisites: z.array(z
215
+ .object({
216
+ phase: sddPhaseSchema,
217
+ topicKey: z.string(),
218
+ reason: z.string(),
219
+ artifactId: z.string().optional(),
220
+ })
221
+ .passthrough()),
222
+ })
223
+ .passthrough();
224
+ const mcpSuccessOutputSchema = (value) => z
225
+ .object({
226
+ ok: z.literal(true),
227
+ tool: z.enum(SUPPORTED_VGX_MCP_TOOL_NAMES),
228
+ value,
229
+ })
230
+ .passthrough();
113
231
  export const VGX_MCP_TOOL_INPUT_SCHEMAS = {
114
232
  vgxness_sdd_status: z
115
233
  .object({
@@ -503,6 +621,64 @@ export const VGX_MCP_TOOL_INPUT_SCHEMAS = {
503
621
  })
504
622
  .passthrough(),
505
623
  };
624
+ export const VGX_MCP_TOOL_OUTPUT_SCHEMAS = {
625
+ vgxness_sdd_ready: mcpSuccessOutputSchema(z
626
+ .object({
627
+ change: z.string(),
628
+ phase: sddPhaseSchema,
629
+ ready: z.boolean(),
630
+ gates: sddReadinessGatesOutputSchema.optional(),
631
+ })
632
+ .passthrough()),
633
+ vgxness_sdd_get_readiness: mcpSuccessOutputSchema(z
634
+ .object({
635
+ change: z.string(),
636
+ phase: sddPhaseSchema,
637
+ ready: z.boolean(),
638
+ gates: sddReadinessGatesOutputSchema.optional(),
639
+ })
640
+ .passthrough()),
641
+ vgxness_sdd_cockpit: mcpSuccessOutputSchema(z
642
+ .object({
643
+ project: z.string(),
644
+ change: z.string(),
645
+ gates: sddCockpitGatesOutputSchema.optional(),
646
+ readModel: z
647
+ .object({
648
+ project: z.string(),
649
+ change: z.string(),
650
+ gates: sddCockpitGatesOutputSchema.optional(),
651
+ phases: z.array(z
652
+ .object({
653
+ phase: sddPhaseSchema,
654
+ gates: sddPhaseGateOutputSchema.optional(),
655
+ })
656
+ .passthrough()),
657
+ })
658
+ .passthrough(),
659
+ })
660
+ .passthrough()),
661
+ vgxness_sdd_continue: mcpSuccessOutputSchema(z
662
+ .object({
663
+ kind: z.literal('sdd-continuation-plan'),
664
+ project: z.string(),
665
+ change: z.string(),
666
+ recommendedActions: z.array(sddRecommendedActionOutputSchema),
667
+ })
668
+ .passthrough()),
669
+ vgxness_provider_status: mcpSuccessOutputSchema(z
670
+ .object({
671
+ kind: z.literal('provider-status'),
672
+ providerEvidence: providerEvidenceOutputSchema,
673
+ })
674
+ .passthrough()),
675
+ vgxness_provider_doctor: mcpSuccessOutputSchema(z
676
+ .object({
677
+ kind: z.literal('provider-doctor'),
678
+ providerEvidence: providerEvidenceOutputSchema,
679
+ })
680
+ .passthrough()),
681
+ };
506
682
  export function successEnvelope(tool, value) {
507
683
  return { ok: true, tool, value };
508
684
  }
@@ -2,7 +2,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
3
  import { runBootAgentSeedUpgrade } from '../agents/boot-upgrade.js';
4
4
  import { createVgxMcpControlPlane } from './control-plane.js';
5
- import { EXPOSED_VGX_MCP_TOOL_NAMES, toInternalVgxMcpToolName, VGX_MCP_TOOL_INPUT_SCHEMAS, } from './schema.js';
5
+ import { EXPOSED_VGX_MCP_TOOL_NAMES, toInternalVgxMcpToolName, VGX_MCP_TOOL_INPUT_SCHEMAS, VGX_MCP_TOOL_OUTPUT_SCHEMAS, } from './schema.js';
6
6
  export async function startVgxMcpStdioServer(options = {}) {
7
7
  const controlPlane = createVgxMcpControlPlane(options.databasePath === undefined ? {} : { databasePath: options.databasePath });
8
8
  runBootAgentSeedUpgrade(controlPlane.database);
@@ -39,18 +39,20 @@ export async function startVgxMcpStdioServer(options = {}) {
39
39
  function registerVgxTools(server, controlPlane) {
40
40
  for (const publicToolName of EXPOSED_VGX_MCP_TOOL_NAMES) {
41
41
  const toolName = toInternalVgxMcpToolName(publicToolName);
42
+ const outputSchema = VGX_MCP_TOOL_OUTPUT_SCHEMAS[toolName];
42
43
  server.registerTool(publicToolName, {
43
44
  title: publicToolName,
44
45
  description: descriptionForTool(publicToolName),
45
46
  inputSchema: VGX_MCP_TOOL_INPUT_SCHEMAS[toolName],
47
+ ...(outputSchema === undefined ? {} : { outputSchema }),
46
48
  }, async (args) => toMcpTextResult(controlPlane.callVgxTool(toolName, argsForTool(args))));
47
49
  }
48
50
  }
49
51
  function descriptionForTool(publicToolName) {
50
52
  if (publicToolName === 'provider_status')
51
- return 'Read-only provider status report; inspects OpenCode config paths without installing, repairing, or writing files.';
53
+ return 'Agent-callable read-only provider status report; inspects expected config paths without installing, repairing, or writing provider config, and does not verify true host tool presence. Use it to decide whether to inspect doctor output or request explicit setup consent.';
52
54
  if (publicToolName === 'provider_doctor')
53
- return 'Read-only provider doctor checks; reports OpenCode MCP configuration health without repair/install side effects.';
55
+ return 'Agent-callable read-only provider doctor advisory; checks VGXNESS-known provider config health without install/repair/config writes and without proving true host tool presence. Use it to decide the next setup or troubleshooting recommendation.';
54
56
  if (publicToolName === 'provider_change_plan')
55
57
  return 'Read-only provider change plan preview; composes status, doctor, and OpenCode install planning without writing provider config.';
56
58
  if (publicToolName === 'opencode_handoff_preview')
@@ -66,9 +68,21 @@ function descriptionForTool(publicToolName) {
66
68
  if (publicToolName === 'context_cockpit')
67
69
  return 'Read-only context cockpit for start/resume/recovery; returns latest restorable session plus bounded memory previews without traces, provider config writes, repository writes, runs, artifacts, or session mutations.';
68
70
  if (publicToolName === 'sdd_cockpit')
69
- return 'Read-only SDD cockpit summary with raw compatibility fields plus additive readModel; metadata-only artifact summaries, no artifact bodies, and explicit human acceptance semantics.';
71
+ return 'Agent-callable read-only SDD cockpit summary; returns phase metadata/readModel without artifact bodies or state changes and preserves explicit human acceptance semantics. Use it to decide the next safe SDD action or blocker.';
70
72
  if (publicToolName === 'sdd_continue')
71
- return 'Read-only SDD continuation planner; returns blocker actions, safe suggested commands, related interrupted run context, and safety notes without provider execution, run creation, artifact mutation, provider config writes, or openspec writes.';
73
+ return 'Agent-callable read-only SDD continuation advisory; returns blocker actions, suggested next steps, interrupted-run context, and safety notes without provider execution, run creation, artifact mutation, provider config writes, or openspec writes. Use it to choose the next manual or agent-safe action.';
74
+ if (publicToolName === 'sdd_status')
75
+ return 'Agent-callable read-only SDD status summary; reports phase presence/state and next ready phase without creating, accepting, or mutating artifacts. Use it to decide whether to inspect, draft, mark ready, or wait for human acceptance.';
76
+ if (publicToolName === 'sdd_get_readiness')
77
+ return 'Agent-callable read-only SDD readiness check; reports whether a phase can proceed and why, without marking ready or changing artifacts. Use it to decide whether to call sdd_ready or resolve prerequisites first.';
78
+ if (publicToolName === 'sdd_ready')
79
+ return 'Agent-callable state-changing SDD readiness marker; marks an existing phase artifact ready when prerequisites allow, but does not human-accept it. Use only after readiness is clear, then ask a human to accept when governance requires acceptance.';
80
+ if (publicToolName === 'sdd_accept_artifact')
81
+ return 'Human-only mutating SDD acceptance gate; records explicit human acceptance for an artifact and must not be called by agents on their own behalf. Use only after a human approval decision, then continue to the next phase.';
82
+ if (publicToolName === 'sdd_reopen_artifact')
83
+ return 'Human-only mutating SDD reopen gate; records an explicit human decision to reopen an artifact and must not be called by agents on their own behalf. Use only after human instruction, then revise or re-review the phase.';
84
+ if (publicToolName === 'run_preflight')
85
+ return 'Agent-callable advisory/planning-only run preflight; evaluates permissions and records a plan but does not execute the checked shell/git/install/network/provider/secrets operation. Use it to decide whether human approval or a separate executor step is required.';
72
86
  const toolName = toInternalVgxMcpToolName(publicToolName);
73
87
  return `VGX control-plane tool ${toolName}`;
74
88
  }
@@ -78,6 +92,7 @@ function argsForTool(args) {
78
92
  function toMcpTextResult(envelope) {
79
93
  const result = {
80
94
  content: [{ type: 'text', text: JSON.stringify(envelope, null, 2) }],
95
+ structuredContent: envelope,
81
96
  };
82
97
  if (!envelope.ok)
83
98
  result.isError = true;
@@ -30,6 +30,7 @@ export function buildSddCockpitReadModel(cockpit) {
30
30
  phases,
31
31
  nextAction,
32
32
  blockers,
33
+ ...(cockpit.gates === undefined ? {} : { gates: cockpit.gates }),
33
34
  guidance: guidanceFor(cockpit, nextAction, blockers),
34
35
  summary: summarize(phases),
35
36
  contentIncluded: false,
@@ -68,6 +69,7 @@ function toPhaseReadModel(phase) {
68
69
  blocked: !phase.readiness.ready || phase.blockers.length > 0,
69
70
  reasons,
70
71
  },
72
+ ...(phase.gates === undefined ? {} : { gates: phase.gates }),
71
73
  guidance: guidanceForPhase(phase, status, canAccept),
72
74
  };
73
75
  }
@@ -5,6 +5,7 @@ export function sddContinuationPlanFrom(input) {
5
5
  const suggestedCommand = suggestedContinuationCommand(input.project, input.next, inspectCommand, dbFlag);
6
6
  const blockerGuidance = input.next.blockerGuidance ?? [];
7
7
  const blockerActions = blockerGuidance.map((blocker) => continuationBlockerAction(input.project, input.next.change, blocker, dbFlag));
8
+ const recommendedActions = continuationRecommendedActions(input.project, input.next, blockerGuidance);
8
9
  const relatedRunContext = relatedRunContextView(input.project, input.relatedRunContext, dbFlag);
9
10
  return {
10
11
  kind: 'sdd-continuation-plan',
@@ -18,6 +19,7 @@ export function sddContinuationPlanFrom(input) {
18
19
  suggestedCommand,
19
20
  inspectCommand,
20
21
  blockerActions,
22
+ recommendedActions,
21
23
  ...(relatedRunContext === undefined ? {} : { relatedRunContext }),
22
24
  ...(input.explicitDatabasePath === undefined ? {} : { explicitDatabasePath: input.explicitDatabasePath }),
23
25
  safety: [
@@ -28,6 +30,153 @@ export function sddContinuationPlanFrom(input) {
28
30
  ],
29
31
  };
30
32
  }
33
+ function continuationRecommendedActions(project, next, blockerGuidance) {
34
+ const actions = [inspectCockpitAction(project, next.change)];
35
+ if (next.status === 'runnable' && next.nextPhase !== undefined)
36
+ actions.push(draftPhaseAction(project, next.change, next.nextPhase, next.reason));
37
+ for (const blocker of blockerGuidance) {
38
+ const action = recommendedActionForBlocker(project, next.change, blocker);
39
+ if (action !== undefined)
40
+ actions.push(action);
41
+ }
42
+ return actions;
43
+ }
44
+ function inspectCockpitAction(project, change) {
45
+ return {
46
+ id: `sdd.${change}.inspect-cockpit`,
47
+ label: 'Inspect SDD cockpit',
48
+ title: 'Inspect SDD cockpit',
49
+ description: 'Read the current SDD cockpit/read model before choosing the next action.',
50
+ kind: 'inspect',
51
+ category: 'inspection',
52
+ targetTool: 'vgxness_sdd_cockpit',
53
+ suggestedArgs: { project, change },
54
+ readOnly: true,
55
+ mutating: false,
56
+ agentCallable: true,
57
+ humanOnly: false,
58
+ requiresHumanApproval: false,
59
+ requiresHumanConfirmation: false,
60
+ requiresPreflight: false,
61
+ requiresProviderWriteConsent: false,
62
+ reason: 'Continuation plans are advisory; cockpit inspection is a safe MCP-native way to refresh state.',
63
+ rationale: 'Keeps agents grounded in current artifact states without mutating SDD state or provider configuration.',
64
+ blockingPrerequisites: [],
65
+ };
66
+ }
67
+ function draftPhaseAction(project, change, phase, reason) {
68
+ return {
69
+ id: `sdd.${change}.${phase}.draft`,
70
+ label: `Prepare ${phase} draft through normal SDD phase flow`,
71
+ title: `Prepare ${phase} draft`,
72
+ description: 'The next SDD phase appears runnable; draft generation must still use the normal phase workflow and any required preflight/confirmation gates.',
73
+ kind: 'draft-phase',
74
+ category: 'sdd-phase',
75
+ phase,
76
+ targetTool: 'vgxness_sdd_get_readiness',
77
+ suggestedArgs: { project, change, phase },
78
+ readOnly: false,
79
+ mutating: true,
80
+ agentCallable: true,
81
+ humanOnly: false,
82
+ requiresHumanApproval: false,
83
+ requiresHumanConfirmation: false,
84
+ requiresPreflight: true,
85
+ requiresProviderWriteConsent: false,
86
+ reason,
87
+ rationale: 'This continuation tool is read-only, so it can recommend the next phase but cannot execute providers or save artifacts itself.',
88
+ blockingPrerequisites: [],
89
+ };
90
+ }
91
+ function recommendedActionForBlocker(project, change, blocker) {
92
+ if (blocker.reason === 'draft' || blocker.reason === 'legacy')
93
+ return humanAcceptAction(project, change, blocker);
94
+ if (blocker.reason === 'rejected')
95
+ return humanReopenAction(project, change, blocker);
96
+ if (blocker.reason === 'missing')
97
+ return draftPhaseAction(project, change, blocker.phase, blocker.action);
98
+ return inspectArtifactAction(project, change, blocker);
99
+ }
100
+ function humanAcceptAction(project, change, blocker) {
101
+ return {
102
+ id: `sdd.${change}.${blocker.phase}.accept-human`,
103
+ label: `Human review and acceptance required for ${blocker.phase}`,
104
+ title: `Accept ${blocker.phase} artifact as a human`,
105
+ description: 'A human must explicitly accept this artifact before it counts as accepted; agents must not accept on the human’s behalf.',
106
+ kind: 'accept-human',
107
+ category: 'human-governance',
108
+ phase: blocker.phase,
109
+ targetTool: 'vgxness_sdd_accept_artifact',
110
+ suggestedArgs: { project, change, phase: blocker.phase },
111
+ readOnly: false,
112
+ mutating: true,
113
+ agentCallable: false,
114
+ humanOnly: true,
115
+ requiresHumanApproval: true,
116
+ requiresHumanConfirmation: true,
117
+ requiresPreflight: true,
118
+ requiresProviderWriteConsent: false,
119
+ reason: blocker.action,
120
+ rationale: 'Human-only acceptance is an explicit governance event and cannot be inferred from draft content, readiness, or artifact presence.',
121
+ blockingPrerequisites: [blockingPrerequisite(blocker)],
122
+ };
123
+ }
124
+ function humanReopenAction(project, change, blocker) {
125
+ return {
126
+ id: `sdd.${change}.${blocker.phase}.reopen-human`,
127
+ label: `Human reopen required for rejected ${blocker.phase}`,
128
+ title: `Reopen ${blocker.phase} artifact as a human`,
129
+ description: 'A human may reopen the rejected artifact to draft; agents must not perform this governance action on the human’s behalf.',
130
+ kind: 'reopen-human',
131
+ category: 'human-governance',
132
+ phase: blocker.phase,
133
+ targetTool: 'vgxness_sdd_reopen_artifact',
134
+ suggestedArgs: { project, change, phase: blocker.phase },
135
+ readOnly: false,
136
+ mutating: true,
137
+ agentCallable: false,
138
+ humanOnly: true,
139
+ requiresHumanApproval: true,
140
+ requiresHumanConfirmation: true,
141
+ requiresPreflight: true,
142
+ requiresProviderWriteConsent: false,
143
+ reason: blocker.action,
144
+ rationale: 'Reopen is a human governance transition from rejected back to draft and remains outside this read-only planner.',
145
+ blockingPrerequisites: [blockingPrerequisite(blocker)],
146
+ };
147
+ }
148
+ function inspectArtifactAction(project, change, blocker) {
149
+ return {
150
+ id: `sdd.${change}.${blocker.phase}.inspect`,
151
+ label: `Inspect ${blocker.phase} artifact`,
152
+ title: `Inspect ${blocker.phase} artifact`,
153
+ description: 'Inspect the blocking artifact before deciding whether human governance or phase work is needed.',
154
+ kind: 'inspect',
155
+ category: 'inspection',
156
+ phase: blocker.phase,
157
+ targetTool: 'vgxness_sdd_get_artifact',
158
+ suggestedArgs: { project, change, phase: blocker.phase, payloadMode: 'compact' },
159
+ readOnly: true,
160
+ mutating: false,
161
+ agentCallable: true,
162
+ humanOnly: false,
163
+ requiresHumanApproval: false,
164
+ requiresHumanConfirmation: false,
165
+ requiresPreflight: false,
166
+ requiresProviderWriteConsent: false,
167
+ reason: blocker.action,
168
+ rationale: 'Artifact inspection is read-only and helps agents understand the blocker without changing SDD state.',
169
+ blockingPrerequisites: [blockingPrerequisite(blocker)],
170
+ };
171
+ }
172
+ function blockingPrerequisite(blocker) {
173
+ return {
174
+ phase: blocker.phase,
175
+ topicKey: blocker.topicKey,
176
+ reason: blocker.reason,
177
+ ...(blocker.artifactId === undefined ? {} : { artifactId: blocker.artifactId }),
178
+ };
179
+ }
31
180
  function relatedRunContextView(project, relatedRunContext, dbFlag) {
32
181
  if (relatedRunContext === undefined)
33
182
  return undefined;
@@ -40,16 +40,18 @@ export class SddWorkflowService {
40
40
  const validated = this.validatePhaseInput(input);
41
41
  if (!validated.ok)
42
42
  return validated;
43
- const phases = this.getPhaseStatuses(validated.value.project, validated.value.change);
44
- if (!phases.ok)
45
- return phases;
46
- const readiness = getReadinessFromStatuses(validated.value.change, validated.value.phase, phases.value, this.options);
43
+ const snapshot = this.loadPhaseSnapshot(validated.value.project, validated.value.change);
44
+ if (!snapshot.ok)
45
+ return snapshot;
46
+ const phases = snapshot.value.phases;
47
+ const readiness = getReadinessFromStatuses(validated.value.change, validated.value.phase, phases, this.options);
47
48
  return {
48
49
  ok: true,
49
50
  value: {
50
51
  change: validated.value.change,
51
52
  phase: validated.value.phase,
52
53
  ...readiness,
54
+ gates: readinessGatesForPhase(validated.value.change, validated.value.phase, phases, readiness),
53
55
  },
54
56
  };
55
57
  }
@@ -88,6 +90,7 @@ export class SddWorkflowService {
88
90
  };
89
91
  const artifact = phaseStatus.present ? cockpitArtifactSummaryFromSnapshotItem(phaseStatus) : undefined;
90
92
  const blockers = cockpitBlockersForPhase(phaseStatus, readiness);
93
+ const gates = phaseGateFromStatus(phaseStatus, readiness);
91
94
  return {
92
95
  phase: phaseStatus.phase,
93
96
  topicKey: phaseStatus.topicKey,
@@ -98,6 +101,7 @@ export class SddWorkflowService {
98
101
  readiness,
99
102
  ...(artifact === undefined ? {} : { artifact }),
100
103
  blockers,
104
+ gates,
101
105
  };
102
106
  });
103
107
  const artifacts = cockpitPhases.map((phase) => phase.artifact).filter((artifact) => artifact !== undefined);
@@ -123,6 +127,7 @@ export class SddWorkflowService {
123
127
  acceptedCount: cockpitPhases.filter((phase) => phase.accepted).length,
124
128
  legacyCount: cockpitPhases.filter((phase) => phase.legacy).length,
125
129
  aggregateBlockers,
130
+ gates: cockpitGatesFromPhases(cockpitPhases, next),
126
131
  inspectCommand: `vgxness sdd cockpit --project ${validated.value.project} --change ${validated.value.change} --json`,
127
132
  };
128
133
  return ok(cockpit);
@@ -518,6 +523,124 @@ function cockpitArtifactSummaryFromSnapshotItem(item) {
518
523
  updatedAt: item.updatedAt,
519
524
  };
520
525
  }
526
+ function readinessGatesForPhase(change, phase, phases, readiness) {
527
+ const targetStatus = phases.find((candidate) => candidate.phase === phase) ?? { phase, topicKey: sddTopicKey(change, phase), present: false, state: 'missing', accepted: false, legacy: false, warnings: [] };
528
+ const targetGate = phaseGateFromStatus(targetStatus, readiness);
529
+ const prerequisites = sddPrerequisites[phase].map((prerequisite) => {
530
+ const prerequisiteStatus = phases.find((candidate) => candidate.phase === prerequisite) ?? {
531
+ phase: prerequisite,
532
+ topicKey: sddTopicKey(change, prerequisite),
533
+ present: false,
534
+ state: 'missing',
535
+ accepted: false,
536
+ legacy: false,
537
+ warnings: [],
538
+ };
539
+ const blocker = readiness.blockedPrerequisites?.find((candidate) => candidate.phase === prerequisite);
540
+ return phaseGateFromStatus(prerequisiteStatus, {
541
+ ready: blocker === undefined,
542
+ blockedPrerequisites: blocker === undefined ? [] : [blocker],
543
+ });
544
+ });
545
+ const blockedReasons = [...targetGate.blockedReasons, ...prerequisites.flatMap((gate) => gate.blockedReasons)];
546
+ return {
547
+ phase: targetGate,
548
+ prerequisites,
549
+ runnable: readiness.ready,
550
+ blocked: !readiness.ready,
551
+ blockedReasons: [...new Set(blockedReasons)],
552
+ humanOnly: targetGate.humanOnly || prerequisites.some((gate) => gate.humanOnly),
553
+ preflightRequired: targetGate.preflightRequired,
554
+ agentCallable: true,
555
+ canDraft: targetGate.canDraft,
556
+ canMarkReady: targetGate.canMarkReady,
557
+ canAccept: targetGate.canAccept || prerequisites.some((gate) => gate.canAccept),
558
+ canReopen: targetGate.canReopen || prerequisites.some((gate) => gate.canReopen),
559
+ nextAllowedActions: [...new Set([targetGate, ...prerequisites].flatMap((gate) => gate.nextAllowedActions))],
560
+ requiresProviderWriteConsent: false,
561
+ };
562
+ }
563
+ function cockpitGatesFromPhases(phases, next) {
564
+ const phaseGates = phases.map((phase) => phase.gates ?? phaseGateFromStatus(phase, phase.readiness));
565
+ const blockedTransitions = phaseGates.filter((gate) => gate.blocked);
566
+ const blockedReasons = [...new Set(blockedTransitions.flatMap((gate) => gate.blockedReasons))];
567
+ const runnableNextPhases = phaseGates.filter((gate) => gate.runnable && !gate.artifactPresent).map((gate) => gate.phase);
568
+ return {
569
+ phases: phaseGates,
570
+ changeComplete: phases.length === sddPhases.length && phases.every((phase) => phase.accepted),
571
+ ...(next.nextPhase === undefined ? {} : { nextPhase: next.nextPhase }),
572
+ nextPhaseRunnable: next.status === 'runnable',
573
+ runnableNextPhases,
574
+ blockedTransitions,
575
+ blockedReasons,
576
+ requiresHumanAcceptance: phaseGates.some((gate) => gate.requiresHumanAcceptance),
577
+ humanOnly: phaseGates.some((gate) => gate.humanOnly),
578
+ preflightRequired: next.status === 'runnable',
579
+ agentCallable: true,
580
+ canDraft: phaseGates.some((gate) => gate.canDraft),
581
+ canMarkReady: phaseGates.some((gate) => gate.canMarkReady),
582
+ canAccept: phaseGates.some((gate) => gate.canAccept),
583
+ canReopen: phaseGates.some((gate) => gate.canReopen),
584
+ nextAllowedActions: [...new Set(phaseGates.flatMap((gate) => gate.nextAllowedActions))],
585
+ requiresProviderWriteConsent: false,
586
+ };
587
+ }
588
+ function phaseGateFromStatus(status, readiness) {
589
+ const artifactStatus = status.state ?? (status.present ? 'draft' : 'missing');
590
+ const acceptedByHuman = status.accepted === true && status.acceptance?.actor.type === 'human';
591
+ const blockedPrerequisite = readiness.blockedPrerequisites?.[0];
592
+ const blockedReasons = [
593
+ ...((readiness.blockedPrerequisites ?? []).map((blocker) => `${blocker.phase}:${blocker.reason}`)),
594
+ ...(status.present && status.accepted !== true ? [`${status.phase}:${blockerReasonForStatus(status)}`] : []),
595
+ ...(status.legacy === true ? [`${status.phase}:legacy`] : []),
596
+ ];
597
+ const requiresHumanAcceptance = status.present && !acceptedByHuman && artifactStatus !== 'missing';
598
+ const canDraft = readiness.ready && !status.present;
599
+ const canMarkReady = readiness.ready;
600
+ const canAccept = status.present && !acceptedByHuman && status.legacy !== true && (artifactStatus === 'draft' || artifactStatus === 'accepted');
601
+ const canReopen = status.present && artifactStatus === 'rejected';
602
+ const nextAllowedActions = phaseNextAllowedActions({ canDraft, canMarkReady, canAccept, canReopen, present: status.present });
603
+ return {
604
+ phase: status.phase,
605
+ topicKey: status.topicKey,
606
+ artifactPresent: status.present,
607
+ artifactStatus,
608
+ artifactState: status.legacy === true ? 'legacy' : artifactStatus,
609
+ accepted: status.accepted === true,
610
+ acceptedByHuman,
611
+ agentCallable: true,
612
+ requiresHumanAcceptance,
613
+ draftPresent: artifactStatus === 'draft',
614
+ contentFrozen: acceptedByHuman,
615
+ runnable: readiness.ready && !status.present,
616
+ blocked: !readiness.ready || status.legacy === true || (status.present && status.accepted !== true),
617
+ blockedReasons: [...new Set(blockedReasons)],
618
+ humanOnly: requiresHumanAcceptance,
619
+ preflightRequired: readiness.ready && !status.present,
620
+ requiresPreflight: readiness.ready && !status.present,
621
+ requiresProviderWriteConsent: false,
622
+ canDraft,
623
+ canMarkReady,
624
+ canAccept,
625
+ canReopen,
626
+ nextAllowedActions,
627
+ ...(blockedPrerequisite === undefined ? {} : { requiredPriorPhase: blockedPrerequisite.phase, blockerReason: blockedPrerequisite.reason }),
628
+ };
629
+ }
630
+ function phaseNextAllowedActions(input) {
631
+ const actions = ['vgxness_sdd_get_readiness', 'vgxness_sdd_cockpit'];
632
+ if (input.canDraft)
633
+ actions.push('vgxness_sdd_save_artifact');
634
+ if (input.canMarkReady)
635
+ actions.push('vgxness_sdd_ready');
636
+ if (input.present)
637
+ actions.push('vgxness_sdd_get_artifact');
638
+ if (input.canAccept)
639
+ actions.push('vgxness_sdd_accept_artifact');
640
+ if (input.canReopen)
641
+ actions.push('vgxness_sdd_reopen_artifact');
642
+ return [...new Set(actions)];
643
+ }
521
644
  function compactGovernanceArtifact(artifact) {
522
645
  return {
523
646
  id: artifact.id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vgxness",
3
- "version": "1.12.0",
3
+ "version": "1.13.0",
4
4
  "description": "CLI and MCP control plane for guided AI-agent workflows, SDD, memory, and OpenCode setup.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {