vgxness 0.1.0 → 0.1.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
File without changes
@@ -11,6 +11,8 @@ import { OpenCodeManagerPayloadService } from '../providers/opencode/manager-pay
11
11
  import { RunService } from '../runs/run-service.js';
12
12
  import { errorEnvelope, successEnvelope } from './schema.js';
13
13
  import { validateVgxMcpToolCall } from './validation.js';
14
+ import { ProviderStatusService } from './provider-status.js';
15
+ import { ProviderDoctorService } from './provider-doctor.js';
14
16
  export function callVgxTool(call, services) {
15
17
  const validated = validateVgxMcpToolCall(call);
16
18
  if (!validated.ok)
@@ -68,6 +70,14 @@ export function callVgxTool(call, services) {
68
70
  return toEnvelope(validated.tool, services.runs.appendCheckpoint(validated.input));
69
71
  case 'vgxness_run_finalize':
70
72
  return toEnvelope(validated.tool, services.runs.updateFinalStatus(validated.input));
73
+ case 'vgxness_provider_status':
74
+ if (services.providerStatus === undefined)
75
+ return errorEnvelope('validation_failed', 'Provider status service is not available', validated.tool);
76
+ return toEnvelope(validated.tool, services.providerStatus.getStatus(validated.input));
77
+ case 'vgxness_provider_doctor':
78
+ if (services.providerDoctor === undefined)
79
+ return errorEnvelope('validation_failed', 'Provider doctor service is not available', validated.tool);
80
+ return toEnvelope(validated.tool, services.providerDoctor.getDoctor(validated.input));
71
81
  }
72
82
  return errorEnvelope('validation_failed', 'Tool dispatch is not implemented');
73
83
  }
@@ -121,6 +131,8 @@ function createServices(database) {
121
131
  opencodeManagerPayload,
122
132
  activation: new AgentActivationService({ agents, managerProfiles, runs, opencodeManagerPayload }),
123
133
  runs,
134
+ providerStatus: new ProviderStatusService({ sdd: new SddWorkflowService(memory, { actor: 'mcp-control-plane' }) }),
135
+ providerDoctor: new ProviderDoctorService(),
124
136
  };
125
137
  }
126
138
  function getManagerProfileEnvelope(input, services) {
package/dist/mcp/index.js CHANGED
@@ -7,4 +7,7 @@ export * from './client-install-opencode-contract.js';
7
7
  export * from './client-install-opencode.js';
8
8
  export * from './doctor.js';
9
9
  export * from './opencode-visibility.js';
10
+ export * from './provider-health-types.js';
11
+ export * from './provider-status.js';
12
+ export * from './provider-doctor.js';
10
13
  export * from '../providers/opencode/manager-payload.js';
@@ -1,4 +1,5 @@
1
1
  export const vgxnessOpenCodeDefaultAgent = 'vgxness-manager';
2
+ export const vgxnessOpenCodePromptContractVersion = 2;
2
3
  export const vgxnessOpenCodeSddSubagents = [
3
4
  'vgxness-sdd-explore',
4
5
  'vgxness-sdd-propose',
@@ -28,7 +29,7 @@ export function createOpenCodeDefaultAgentConfig() {
28
29
  description: 'VGXNESS SDD Manager - coordinates MCP state and SDD sub-agents, avoids inline execution when delegation is safer',
29
30
  mode: 'primary',
30
31
  model: 'openai/gpt-5.5',
31
- options: { reasoningEffort: 'high' },
32
+ options: { reasoningEffort: 'high', vgxnessPromptContractVersion: vgxnessOpenCodePromptContractVersion },
32
33
  permission: { task: createOpenCodeSddTaskPermissions() },
33
34
  prompt: vgxnessManagerPrompt,
34
35
  reasoningEffort: 'high',
@@ -54,6 +55,7 @@ export function createOpenCodeDefaultAgentConfig() {
54
55
  hidden: true,
55
56
  mode: 'subagent',
56
57
  model: 'openai/gpt-5.5',
58
+ options: { vgxnessPromptContractVersion: vgxnessOpenCodePromptContractVersion },
57
59
  prompt: createSddSubagentPrompt(name),
58
60
  tools: { bash: true, edit: true, read: true, write: true },
59
61
  };
@@ -75,7 +77,7 @@ function createSddSubagentPrompt(name) {
75
77
  'vgxness-sdd-init': 'Bootstrap SDD context and project setup safely. Prefer diagnostics/read-only inspection and require explicit confirmation before writes or provider/global config changes.',
76
78
  'vgxness-sdd-onboard': 'Guide the user through the SDD cycle with the smallest missing decision at each step. Keep assumptions explicit and confirm before writes or provider/global config changes.',
77
79
  };
78
- return `${common}\n\nPhase contract: ${contracts[name]}`;
80
+ return `${common}\n\nPhase contract: ${contracts[name]}\n\nProvider-native daily progression: daily SDD progression happens inside OpenCode through conversation, VGXNESS MCP, and hidden SDD subagents. Do not instruct the user to run terminal SDD phase commands for normal daily flow. Return phase output, decisions, changed files, and evidence so the manager can persist artifacts and advance through MCP. Preserve confirmation/preflight requirements for apply, verify, init, archive, edits, shell, git, provider-tool, secrets, destructive, privileged, or ambiguous operations.`;
79
81
  }
80
82
  const vgxnessManagerPrompt = `# VGXNESS Manager - SDD Orchestrator Instructions
81
83
 
@@ -91,15 +93,21 @@ Act with the personality configured for VGXNESS: direct, practical, grounded in
91
93
 
92
94
  Use the \`vgxness\` MCP server as the source of durable workflow state. MCP tools coordinate and persist. OpenCode sub-agent delegation performs phase work. Do not rely on external \`sdd-*\` skill files; default manager and subagent contracts are inline.
93
95
 
96
+ ## Provider-Native Daily Flow
97
+
98
+ Daily SDD happens inside OpenCode through conversation, VGXNESS MCP, and hidden SDD subagents. Use \`vgxness_provider_status\` for configured/phase/next questions and \`vgxness_provider_doctor\` for OpenCode MCP/manager health. CLI is an escape hatch for bootstrap, doctor, rollback, recovery, MCP unavailable/setup missing, provider-native repair being out of scope, or an explicit user request. For normal daily progression, do not tell the user to run terminal SDD phase commands; advance by checking MCP state, delegating hidden phase subagents, and persisting accepted outputs through MCP.
99
+
94
100
  ## MCP Tool Playbook
95
101
 
96
102
  - SDD state and artifacts: use \`vgxness_sdd_status\`, \`vgxness_sdd_next\`, and \`vgxness_sdd_ready\` before advancing phases; read prerequisites with \`vgxness_sdd_get_artifact\` or \`vgxness_sdd_list_artifacts\`; persist accepted phase output with \`vgxness_sdd_save_artifact\`. SDD artifacts are not generic memory.
103
+ - Provider status/doctor: use \`vgxness_provider_status\` for provider configuration, phase, and next-action questions; use \`vgxness_provider_doctor\` for read-only OpenCode MCP/manager health checks. These tools report only and do not install, repair, or mutate provider config.
97
104
  - Session lifecycle: when starting, resuming, or recovering context, call \`vgxness_session_restore\` with the project and workspace directory before inferring state from chat. Before ending, pausing, handing off, or compacting, call \`vgxness_session_close\` with the current session id, actor \`manager\`, and a concise actionable summary. If no current session id is available, do not invent one; state that clearly and preserve the actionable summary in the final response.
98
105
  - Memory: use \`vgxness_memory_search\` at the start when the user references previous work, remembered decisions, prior solutions, or unclear project context; use \`vgxness_memory_get\` for full relevant results; use \`vgxness_memory_save\` immediately after durable discoveries, decisions, bug fixes, configuration changes, patterns, or preferences; use \`vgxness_memory_update\` when correcting/evolving a known observation id. Do not duplicate full SDD artifacts as memory.
99
106
  - Agents: use \`vgxness_agent_resolve\` with project/scope/workflow/phase/provider/mode before substantial SDD phase execution. \`vgxness_agent_activate\` prepares provider-ready context/run payloads only; it does not execute a provider or write provider config.
100
107
  - Manager/profile and payloads: use \`vgxness_manager_profile_get\` before changing manager behavior. \`vgxness_manager_profile_set\` is a persistent behavior change and requires explicit human confirmation. \`vgxness_skill_payload\` builds context only and does not install skill files. \`vgxness_opencode_manager_payload\` is a read-only preview; it does not install, execute, load seeds, or write provider config.
101
108
  - Runs: simple questions do not require runs by default. For significant implementation, verification, risky, or multi-step delegated work, create or recover state with \`vgxness_run_start\`, \`vgxness_run_list\`, and \`vgxness_run_get\`; record progress with \`vgxness_run_checkpoint\`; call \`vgxness_run_preflight\` before edit, shell, git, network, external-directory, provider-tool, secrets, privileged, destructive, or ambiguous operations; close clear outcomes with \`vgxness_run_finalize\`.
102
109
  - Safety: do not call MCP with unsupported execution/config fields such as \`executeProvider\` or \`writesProviderConfig\`. Do not mutate provider/global OpenCode config unless the user explicitly asks for install/apply/configuration changes.
110
+ - Terminal leakage: do not give terminal SDD phase commands for normal daily flow. Mention CLI only as an escape hatch for bootstrap, doctor, rollback, recovery, MCP unavailable/setup missing, provider-native repair being out of scope, or an explicit user request.
103
111
 
104
112
  ## Request Type -> Minimum MCP Sequence
105
113
 
@@ -0,0 +1,121 @@
1
+ import { existsSync, readFileSync, statSync } from 'node:fs';
2
+ import { PROVIDER_HEALTH_SAFETY, REQUIRED_PROVIDER_NATIVE_MCP_TOOLS, normalizeProviderHealthInput, providerHealthFailure, rollupProviderDoctor } from './provider-health-types.js';
3
+ import { vgxnessOpenCodeDefaultAgent, vgxnessOpenCodeSddSubagents } from './opencode-default-agent-config.js';
4
+ import { inspectProjectConfigPaths } from './provider-status.js';
5
+ export class ProviderDoctorService {
6
+ runDoctor(input = {}) {
7
+ return this.getDoctor(input);
8
+ }
9
+ getDoctor(input = {}) {
10
+ const normalized = normalizeProviderHealthInput(input);
11
+ if (normalized.providerAdapter !== 'opencode')
12
+ return providerHealthFailure(`Unsupported provider adapter: ${normalized.providerAdapter}`);
13
+ const paths = inspectProjectConfigPaths(normalized.workspaceRoot);
14
+ const before = snapshotPaths(paths.map((path) => path.path), normalized.workspaceRoot);
15
+ const readableJson = paths.filter((path) => path.parsed);
16
+ const config = readFirstConfig(readableJson[0]?.path);
17
+ const checks = [
18
+ { id: 'workspace-root', status: existsSync(normalized.workspaceRoot) ? 'pass' : 'fail', detail: `Workspace root ${existsSync(normalized.workspaceRoot) ? 'exists' : 'does not exist'}: ${normalized.workspaceRoot}` },
19
+ { id: 'provider-supported', status: 'pass', detail: 'OpenCode provider adapter selected.' },
20
+ configReadableCheck(readableJson.length, paths.filter((path) => path.exists).length),
21
+ mcpEntryCheck(config),
22
+ defaultAgentCheck(config),
23
+ subagentsCheck(config),
24
+ delegationCheck(config),
25
+ { id: 'mcp-current-call', status: 'pass', detail: 'Current MCP call reached the VGXNESS control-plane.' },
26
+ { id: 'mcp-required-tools', status: 'pass', detail: `Required provider-native tools are registered: ${REQUIRED_PROVIDER_NATIVE_MCP_TOOLS.join(', ')}.` },
27
+ promptContractCheck(config, normalized.expectedPromptContractVersion),
28
+ readonlySafetyCheck(before, snapshotPaths(paths.map((path) => path.path), normalized.workspaceRoot)),
29
+ ];
30
+ return {
31
+ ok: true,
32
+ value: {
33
+ version: 1,
34
+ kind: 'provider-doctor',
35
+ project: normalized.project,
36
+ providerAdapter: normalized.providerAdapter,
37
+ scope: normalized.scope,
38
+ workspaceRoot: normalized.workspaceRoot,
39
+ status: rollupProviderDoctor(checks.map((check) => check.status)),
40
+ checks,
41
+ checkedPaths: paths.map((path) => path.path),
42
+ safety: PROVIDER_HEALTH_SAFETY,
43
+ },
44
+ };
45
+ }
46
+ }
47
+ function readFirstConfig(path) {
48
+ if (path === undefined)
49
+ return undefined;
50
+ try {
51
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
52
+ return isRecord(parsed) ? parsed : undefined;
53
+ }
54
+ catch {
55
+ return undefined;
56
+ }
57
+ }
58
+ function configReadableCheck(readableCount, existingCount) {
59
+ if (existingCount === 0)
60
+ return { id: 'opencode-config-readable', status: 'not-configured', detail: 'No OpenCode project config path exists.', remediation: 'Choose an explicit setup/install action if provider configuration should be created.' };
61
+ if (readableCount === 0)
62
+ return { id: 'opencode-config-readable', status: 'fail', detail: 'OpenCode config exists but no JSON config could be parsed.' };
63
+ return { id: 'opencode-config-readable', status: existingCount === 1 ? 'pass' : 'warn', detail: `Found ${existingCount} OpenCode config path(s), ${readableCount} readable JSON config(s).` };
64
+ }
65
+ function mcpEntryCheck(config) {
66
+ const mcp = isRecord(config?.mcp) ? config.mcp : undefined;
67
+ const entry = mcp?.vgxness;
68
+ if (entry === undefined)
69
+ return { id: 'opencode-mcp-entry', status: 'fail', detail: 'No vgxness MCP server entry was found in readable OpenCode JSON config.', remediation: 'Use explicit setup/install outside provider status/doctor if you want to mutate provider config.' };
70
+ if (isRecord(entry) && entry.enabled === false)
71
+ return { id: 'opencode-mcp-entry', status: 'fail', detail: 'The vgxness MCP server entry exists but is disabled.' };
72
+ return { id: 'opencode-mcp-entry', status: 'pass', detail: 'Found enabled vgxness MCP server entry in readable OpenCode JSON config.' };
73
+ }
74
+ function defaultAgentCheck(config) {
75
+ const agents = isRecord(config?.agent) ? config.agent : undefined;
76
+ if (config?.default_agent !== vgxnessOpenCodeDefaultAgent || !isRecord(agents?.[vgxnessOpenCodeDefaultAgent]))
77
+ return { id: 'opencode-default-agent', status: 'fail', detail: 'OpenCode default_agent is not vgxness-manager or the manager config is missing.' };
78
+ return { id: 'opencode-default-agent', status: 'pass', detail: 'OpenCode default agent points to vgxness-manager.' };
79
+ }
80
+ function subagentsCheck(config) {
81
+ const agents = isRecord(config?.agent) ? config.agent : undefined;
82
+ const missing = vgxnessOpenCodeSddSubagents.filter((name) => !isRecord(agents?.[name]) || agents[name]?.hidden !== true);
83
+ if (missing.length > 0)
84
+ return { id: 'opencode-sdd-subagents', status: 'fail', detail: `Missing or non-hidden SDD subagents: ${missing.join(', ')}.` };
85
+ return { id: 'opencode-sdd-subagents', status: 'pass', detail: 'All expected SDD subagents exist and are hidden.' };
86
+ }
87
+ function delegationCheck(config) {
88
+ const manager = isRecord(config?.agent) && isRecord(config.agent[vgxnessOpenCodeDefaultAgent]) ? config.agent[vgxnessOpenCodeDefaultAgent] : undefined;
89
+ const permission = isRecord(manager?.permission) && isRecord(manager.permission.task) ? manager.permission.task : undefined;
90
+ if (permission?.['*'] !== 'deny')
91
+ return { id: 'opencode-delegation-permissions', status: 'fail', detail: 'Manager task permission wildcard is not deny.' };
92
+ const missing = vgxnessOpenCodeSddSubagents.filter((name) => permission[name] !== 'allow');
93
+ if (missing.length > 0)
94
+ return { id: 'opencode-delegation-permissions', status: 'fail', detail: `Manager cannot delegate to expected subagents: ${missing.join(', ')}.` };
95
+ return { id: 'opencode-delegation-permissions', status: 'pass', detail: 'Manager task permission is deny-by-default and allows exact SDD subagents.' };
96
+ }
97
+ function promptContractCheck(config, expected) {
98
+ const agents = isRecord(config?.agent) ? config.agent : undefined;
99
+ const names = [vgxnessOpenCodeDefaultAgent, ...vgxnessOpenCodeSddSubagents];
100
+ const mismatched = names.filter((name) => promptContractVersion(agents?.[name]) !== expected);
101
+ if (mismatched.length > 0)
102
+ return { id: 'prompt-contract-version', status: 'warn', detail: `Prompt contract version mismatch for: ${mismatched.join(', ')}. Expected ${expected}.` };
103
+ return { id: 'prompt-contract-version', status: 'pass', detail: `Prompt contract version ${expected} is present on manager and subagents.` };
104
+ }
105
+ function readonlySafetyCheck(before, after) {
106
+ return JSON.stringify(before) === JSON.stringify(after)
107
+ ? { id: 'provider-config-readonly-safety', status: 'pass', detail: 'Provider config path existence and mtimes were unchanged; no repair/install/write occurred.' }
108
+ : { id: 'provider-config-readonly-safety', status: 'fail', detail: 'Provider config path existence or mtimes changed during read-only doctor.' };
109
+ }
110
+ function snapshotPaths(paths, workspaceRoot) {
111
+ const unique = [...paths, `${workspaceRoot}/.vgx`];
112
+ return Object.fromEntries(unique.map((path) => [path, existsSync(path) ? statSync(path).mtimeMs : false]));
113
+ }
114
+ function promptContractVersion(value) {
115
+ if (!isRecord(value) || !isRecord(value.options))
116
+ return undefined;
117
+ return typeof value.options.vgxnessPromptContractVersion === 'number' ? value.options.vgxnessPromptContractVersion : undefined;
118
+ }
119
+ function isRecord(value) {
120
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
121
+ }
@@ -0,0 +1,67 @@
1
+ export const PROVIDER_HEALTH_SAFETY = {
2
+ readOnly: true,
3
+ writesProviderConfig: false,
4
+ writesProjectFiles: false,
5
+ createsVgxDirectory: false,
6
+ savesArtifacts: false,
7
+ savesMemory: false,
8
+ recordsCheckpoints: false,
9
+ installsPackages: false,
10
+ runsRepair: false,
11
+ executesProvider: false,
12
+ };
13
+ export const REQUIRED_PROVIDER_MCP_TOOLS = ['vgxness_provider_status', 'vgxness_provider_doctor'];
14
+ export const REQUIRED_PROVIDER_NATIVE_MCP_TOOLS = [
15
+ 'vgxness_provider_status',
16
+ 'vgxness_provider_doctor',
17
+ 'vgxness_sdd_status',
18
+ 'vgxness_sdd_next',
19
+ 'vgxness_sdd_ready',
20
+ 'vgxness_sdd_get_artifact',
21
+ 'vgxness_sdd_list_artifacts',
22
+ 'vgxness_sdd_save_artifact',
23
+ 'vgxness_agent_resolve',
24
+ 'vgxness_run_start',
25
+ 'vgxness_run_checkpoint',
26
+ 'vgxness_run_finalize',
27
+ 'vgxness_run_preflight',
28
+ 'vgxness_memory_search',
29
+ 'vgxness_memory_get',
30
+ 'vgxness_memory_save',
31
+ 'vgxness_session_restore',
32
+ 'vgxness_session_close',
33
+ ];
34
+ export function normalizeProviderHealthInput(input) {
35
+ return {
36
+ project: input.project?.trim() || 'vgxness',
37
+ scope: input.scope ?? 'project',
38
+ providerAdapter: input.providerAdapter ?? 'opencode',
39
+ workspaceRoot: input.workspaceRoot?.trim() || process.cwd(),
40
+ change: input.change?.trim() ?? '',
41
+ expectedPromptContractVersion: input.expectedPromptContractVersion ?? 2,
42
+ };
43
+ }
44
+ export function rollupProviderHealth(statuses) {
45
+ if (statuses.some((status) => status === 'fail'))
46
+ return 'failed';
47
+ if (statuses.some((status) => status === 'not-configured'))
48
+ return 'not-configured';
49
+ if (statuses.some((status) => status === 'warn'))
50
+ return 'warning';
51
+ return 'ready';
52
+ }
53
+ export function providerHealthFailure(message) {
54
+ const error = { code: 'validation_failed', message };
55
+ return { ok: false, error };
56
+ }
57
+ export function rollupProviderDoctor(statuses) {
58
+ if (statuses.length === 0 || statuses.every((status) => status === 'skip'))
59
+ return 'skipped';
60
+ if (statuses.some((status) => status === 'fail'))
61
+ return 'failed';
62
+ if (statuses.some((status) => status === 'warn' || status === 'not-configured'))
63
+ return 'degraded';
64
+ if (statuses.some((status) => status === 'skip'))
65
+ return 'unknown';
66
+ return 'healthy';
67
+ }
@@ -0,0 +1,152 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { PROVIDER_HEALTH_SAFETY, REQUIRED_PROVIDER_MCP_TOOLS, normalizeProviderHealthInput, providerHealthFailure, rollupProviderHealth } from './provider-health-types.js';
4
+ import { vgxnessOpenCodeDefaultAgent, vgxnessOpenCodePromptContractVersion, vgxnessOpenCodeSddSubagents } from './opencode-default-agent-config.js';
5
+ const projectConfigTargets = ['.opencode/opencode.json', 'opencode.json', '.opencode/opencode.jsonc', 'opencode.jsonc'];
6
+ export class ProviderStatusService {
7
+ deps;
8
+ constructor(deps = {}) {
9
+ this.deps = deps;
10
+ }
11
+ getStatus(input = {}) {
12
+ const normalized = normalizeProviderHealthInput(input);
13
+ if (normalized.providerAdapter !== 'opencode')
14
+ return providerHealthFailure(`Unsupported provider adapter: ${normalized.providerAdapter}`);
15
+ const paths = inspectProjectConfigPaths(normalized.workspaceRoot);
16
+ const mcpEntry = inspectOpenCodeMcpEntry(paths);
17
+ const providerConfig = readFirstParsedConfig(paths);
18
+ const managerConfigured = hasConfiguredManager(providerConfig);
19
+ const subagentsConfigured = hasConfiguredSubagents(providerConfig);
20
+ const tools = requiredToolPresence();
21
+ const configStatus = resolveConfigStatus(paths, mcpEntry, managerConfigured, subagentsConfigured);
22
+ const status = rollupProviderHealth([configStatus, ...tools.map((tool) => (tool.present ? 'pass' : 'fail'))]);
23
+ const sdd = normalized.change.length > 0 ? this.readSdd(normalized.project, normalized.change) : undefined;
24
+ return {
25
+ ok: true,
26
+ value: {
27
+ version: 1,
28
+ kind: 'provider-status',
29
+ project: normalized.project,
30
+ providerAdapter: normalized.providerAdapter,
31
+ scope: normalized.scope,
32
+ workspaceRoot: normalized.workspaceRoot,
33
+ status,
34
+ summary: summarizeStatus(status, mcpEntry),
35
+ nextAction: nextActionFor(status, mcpEntry, sdd?.next),
36
+ checkedPaths: paths.map((path) => path.path),
37
+ config: { status: configStatus, paths, mcpEntry },
38
+ ...(sdd === undefined ? {} : { sdd }),
39
+ mcpRequiredTools: tools,
40
+ safety: PROVIDER_HEALTH_SAFETY,
41
+ },
42
+ };
43
+ }
44
+ readSdd(project, change) {
45
+ if (this.deps.sdd === undefined)
46
+ return { change };
47
+ const status = this.deps.sdd.getStatus({ project, change });
48
+ const next = this.deps.sdd.getNext({ project, change });
49
+ return { change, ...(status.ok ? { status: status.value } : {}), ...(next.ok ? { next: next.value } : {}) };
50
+ }
51
+ }
52
+ function readFirstParsedConfig(paths) {
53
+ const parsedPath = paths.find((path) => path.parsed);
54
+ if (parsedPath === undefined)
55
+ return undefined;
56
+ try {
57
+ const parsed = JSON.parse(readFileSync(parsedPath.path, 'utf8'));
58
+ return isRecord(parsed) ? parsed : undefined;
59
+ }
60
+ catch {
61
+ return undefined;
62
+ }
63
+ }
64
+ export function inspectProjectConfigPaths(workspaceRoot) {
65
+ return projectConfigTargets.map((relativePath) => inspectConfigPath(relativePath, join(workspaceRoot, relativePath)));
66
+ }
67
+ function inspectConfigPath(label, path) {
68
+ if (!existsSync(path))
69
+ return { label, path, exists: false, readable: false, parsed: false, status: 'not-configured', detail: 'Config path does not exist.' };
70
+ if (label.endsWith('.jsonc'))
71
+ return { label, path, exists: true, readable: true, parsed: false, status: 'warn', detail: 'JSONC config exists but is not parsed by this read-only status check.' };
72
+ try {
73
+ JSON.parse(readFileSync(path, 'utf8'));
74
+ return { label, path, exists: true, readable: true, parsed: true, status: 'pass', detail: 'Config exists and parses as JSON.' };
75
+ }
76
+ catch (cause) {
77
+ const message = cause instanceof Error ? cause.message : String(cause);
78
+ return { label, path, exists: true, readable: false, parsed: false, status: 'fail', detail: `Config could not be read or parsed: ${message}` };
79
+ }
80
+ }
81
+ function inspectOpenCodeMcpEntry(paths) {
82
+ const parsedPath = paths.find((path) => path.parsed);
83
+ if (parsedPath === undefined)
84
+ return { configured: false, status: 'not-configured', serverName: 'vgxness', detail: 'No readable OpenCode JSON config was found.' };
85
+ try {
86
+ const config = JSON.parse(readFileSync(parsedPath.path, 'utf8'));
87
+ const entry = typeof config.mcp === 'object' && config.mcp !== null ? config.mcp.vgxness : undefined;
88
+ const configured = entry !== undefined;
89
+ const enabled = !isRecord(entry) || entry.enabled !== false;
90
+ return configured
91
+ ? { configured: true, status: enabled ? 'pass' : 'warn', serverName: 'vgxness', enabled, detail: `MCP server entry found in ${parsedPath.label}${enabled ? '' : ' but it is disabled'}.` }
92
+ : { configured: false, status: 'not-configured', serverName: 'vgxness', detail: `No vgxness MCP server entry found in ${parsedPath.label}.` };
93
+ }
94
+ catch (cause) {
95
+ const message = cause instanceof Error ? cause.message : String(cause);
96
+ return { configured: false, status: 'fail', serverName: 'vgxness', detail: `Unable to inspect MCP entry: ${message}` };
97
+ }
98
+ }
99
+ function requiredToolPresence() {
100
+ return REQUIRED_PROVIDER_MCP_TOOLS.map((tool) => ({ tool, present: true }));
101
+ }
102
+ function resolveConfigStatus(paths, mcpEntry, managerConfigured, subagentsConfigured) {
103
+ if (paths.some((path) => path.status === 'fail') || mcpEntry.status === 'fail')
104
+ return 'fail';
105
+ if (paths.filter((path) => path.exists).length > 1)
106
+ return 'warn';
107
+ if (mcpEntry.status === 'not-configured')
108
+ return 'not-configured';
109
+ if (!managerConfigured || !subagentsConfigured)
110
+ return 'warn';
111
+ if (paths.some((path) => path.status === 'warn'))
112
+ return 'warn';
113
+ return 'pass';
114
+ }
115
+ function summarizeStatus(status, mcpEntry) {
116
+ if (status === 'ready')
117
+ return 'OpenCode provider config is readable and the vgxness MCP entry is present.';
118
+ if (status === 'not-configured')
119
+ return mcpEntry.detail;
120
+ if (status === 'warning')
121
+ return 'OpenCode provider config has warnings; no changes were made.';
122
+ return 'OpenCode provider status check found a blocking issue; no changes were made.';
123
+ }
124
+ function nextActionFor(status, mcpEntry, sddNext) {
125
+ if (sddNext !== undefined)
126
+ return { kind: 'advance-sdd', message: 'Continue the next SDD phase inside OpenCode using MCP status/ready/artifact tools and hidden SDD subagents.' };
127
+ if (status === 'ready')
128
+ return { kind: 'continue', message: 'Continue using the provider; no read-only follow-up is required.' };
129
+ if (status === 'not-configured' && !mcpEntry.configured)
130
+ return { kind: 'setup-required', message: 'Review the reported config paths and choose an explicit provider setup action if you want configuration changes.' };
131
+ return { kind: 'review', message: 'Review the failed or warning checks before attempting provider setup.' };
132
+ }
133
+ function hasConfiguredManager(config) {
134
+ if (config === undefined)
135
+ return false;
136
+ const agent = isRecord(config.agent) ? config.agent : undefined;
137
+ return config.default_agent === vgxnessOpenCodeDefaultAgent && isRecord(agent?.[vgxnessOpenCodeDefaultAgent]) && promptContractVersion(agent[vgxnessOpenCodeDefaultAgent]) === vgxnessOpenCodePromptContractVersion;
138
+ }
139
+ function hasConfiguredSubagents(config) {
140
+ if (config === undefined)
141
+ return false;
142
+ const agent = isRecord(config.agent) ? config.agent : undefined;
143
+ return vgxnessOpenCodeSddSubagents.every((name) => isRecord(agent?.[name]) && agent[name]?.hidden === true && promptContractVersion(agent[name]) === vgxnessOpenCodePromptContractVersion);
144
+ }
145
+ function promptContractVersion(value) {
146
+ if (!isRecord(value) || !isRecord(value.options))
147
+ return undefined;
148
+ return typeof value.options.vgxnessPromptContractVersion === 'number' ? value.options.vgxnessPromptContractVersion : undefined;
149
+ }
150
+ function isRecord(value) {
151
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
152
+ }
@@ -27,6 +27,8 @@ export const SUPPORTED_VGX_MCP_TOOL_NAMES = [
27
27
  'vgxness_run_start',
28
28
  'vgxness_run_checkpoint',
29
29
  'vgxness_run_finalize',
30
+ 'vgxness_provider_status',
31
+ 'vgxness_provider_doctor',
30
32
  ];
31
33
  const scopes = ['project', 'personal'];
32
34
  const agentModes = ['agent', 'subagent'];
@@ -219,6 +221,20 @@ export const VGX_MCP_TOOL_INPUT_SCHEMAS = {
219
221
  outcome: z.enum(runOutcomes),
220
222
  outcomeReason: z.string().min(1).optional(),
221
223
  }).passthrough(),
224
+ vgxness_provider_status: z.object({
225
+ project: z.string().min(1).optional(),
226
+ scope: z.enum(scopes).optional(),
227
+ providerAdapter: z.literal('opencode').optional(),
228
+ workspaceRoot: z.string().min(1).optional(),
229
+ change: z.string().min(1).optional(),
230
+ }).passthrough(),
231
+ vgxness_provider_doctor: z.object({
232
+ project: z.string().min(1).optional(),
233
+ scope: z.enum(scopes).optional(),
234
+ providerAdapter: z.literal('opencode').optional(),
235
+ workspaceRoot: z.string().min(1).optional(),
236
+ expectedPromptContractVersion: z.number().int().positive().optional(),
237
+ }).passthrough(),
222
238
  };
223
239
  export function successEnvelope(tool, value) {
224
240
  return { ok: true, tool, value };
@@ -38,11 +38,18 @@ function registerVgxTools(server, controlPlane) {
38
38
  for (const toolName of SUPPORTED_VGX_MCP_TOOL_NAMES) {
39
39
  server.registerTool(toolName, {
40
40
  title: toolName,
41
- description: `VGX control-plane tool ${toolName}`,
41
+ description: descriptionForTool(toolName),
42
42
  inputSchema: VGX_MCP_TOOL_INPUT_SCHEMAS[toolName],
43
43
  }, async (args) => toMcpTextResult(controlPlane.callVgxTool(toolName, argsForTool(args))));
44
44
  }
45
45
  }
46
+ function descriptionForTool(toolName) {
47
+ if (toolName === 'vgxness_provider_status')
48
+ return 'Read-only provider status report; inspects OpenCode config paths without installing, repairing, or writing files.';
49
+ if (toolName === 'vgxness_provider_doctor')
50
+ return 'Read-only provider doctor checks; reports OpenCode MCP configuration health without repair/install side effects.';
51
+ return `VGX control-plane tool ${toolName}`;
52
+ }
46
53
  function argsForTool(args) {
47
54
  return args;
48
55
  }
@@ -77,8 +77,50 @@ export function validateVgxMcpToolCall(call) {
77
77
  return validationSuccess(tool.value, validateRunCheckpointInput(input, tool.value));
78
78
  case 'vgxness_run_finalize':
79
79
  return validationSuccess(tool.value, validateRunFinalizeInput(input, tool.value));
80
+ case 'vgxness_provider_status':
81
+ return validationSuccess(tool.value, validateProviderHealthInput(input, tool.value));
82
+ case 'vgxness_provider_doctor':
83
+ return validationSuccess(tool.value, validateProviderHealthInput(input, tool.value));
80
84
  }
81
85
  }
86
+ function validateProviderHealthInput(input, tool) {
87
+ const record = inputRecord(input ?? {}, tool, ['project', 'scope', 'providerAdapter', 'workspaceRoot', 'change', 'expectedPromptContractVersion']);
88
+ if (!record.ok)
89
+ return record;
90
+ const result = {};
91
+ const project = readOptionalNonEmptyString(record.value, 'project', tool);
92
+ if (!project.ok)
93
+ return project;
94
+ if (project.value !== undefined)
95
+ result.project = project.value;
96
+ const scope = readOptionalOneOf(record.value, 'scope', scopes, tool);
97
+ if (!scope.ok)
98
+ return scope;
99
+ if (scope.value !== undefined)
100
+ result.scope = scope.value;
101
+ const providerAdapter = readOptionalOneOf(record.value, 'providerAdapter', ['opencode'], tool);
102
+ if (!providerAdapter.ok)
103
+ return providerAdapter;
104
+ if (providerAdapter.value !== undefined)
105
+ result.providerAdapter = providerAdapter.value;
106
+ const workspaceRoot = readOptionalNonEmptyString(record.value, 'workspaceRoot', tool);
107
+ if (!workspaceRoot.ok)
108
+ return workspaceRoot;
109
+ if (workspaceRoot.value !== undefined)
110
+ result.workspaceRoot = workspaceRoot.value;
111
+ const change = readOptionalNonEmptyString(record.value, 'change', tool);
112
+ if (!change.ok)
113
+ return change;
114
+ if (change.value !== undefined)
115
+ result.change = change.value;
116
+ const expectedPromptContractVersion = record.value.expectedPromptContractVersion;
117
+ if (expectedPromptContractVersion !== undefined) {
118
+ if (typeof expectedPromptContractVersion !== 'number' || !Number.isSafeInteger(expectedPromptContractVersion) || expectedPromptContractVersion <= 0)
119
+ return validationFailure('expectedPromptContractVersion must be a positive safe integer', tool);
120
+ result.expectedPromptContractVersion = expectedPromptContractVersion;
121
+ }
122
+ return { ok: true, value: result };
123
+ }
82
124
  export function validateVgxMcpToolInput(tool, input) {
83
125
  return validateVgxMcpToolCall({ tool, input });
84
126
  }
@@ -574,6 +616,11 @@ function readNonEmptyString(record, field, tool) {
574
616
  return validationFailure(`${field} must not be empty`, tool);
575
617
  return value;
576
618
  }
619
+ function readOptionalNonEmptyString(record, field, tool) {
620
+ if (record[field] === undefined)
621
+ return { ok: true, value: undefined };
622
+ return readNonEmptyString(record, field, tool);
623
+ }
577
624
  function readChange(record, tool) {
578
625
  const change = readNonEmptyString(record, 'change', tool);
579
626
  if (!change.ok)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vgxness",
3
- "version": "0.1.0",
3
+ "version": "0.1.1-alpha.0",
4
4
  "description": "Alpha 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": {