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.
- package/dist/cli/cli-help.js +2 -2
- package/dist/cli/setup-status-renderer.js +6 -1
- package/dist/cli/tui/main-menu/main-menu-read-model.js +1 -1
- package/dist/cli/tui/setup/setup-tui-read-model.js +7 -7
- package/dist/mcp/claude-code-user-config.js +55 -0
- package/dist/mcp/claude-code-user-memory.js +90 -0
- package/dist/mcp/client-install-claude-code-contract.js +57 -6
- package/dist/mcp/client-install-claude-code.js +78 -8
- package/dist/mcp/control-plane.js +18 -1
- package/dist/mcp/index.js +2 -0
- package/dist/mcp/provider-change-plan.js +24 -53
- package/dist/mcp/provider-doctor.js +23 -7
- package/dist/mcp/provider-status.js +22 -29
- package/dist/mcp/schema.js +1 -1
- package/dist/mcp/validation.js +1 -1
- package/dist/sdd/schema.js +15 -0
- package/dist/sdd/sdd-workflow-service.js +59 -29
- package/dist/setup/providers/claude-setup-adapter.js +51 -6
- package/dist/setup/providers/opencode-setup-adapter.js +2 -2
- package/dist/setup/providers/provider-setup-adapter.js +3 -0
- package/dist/setup/setup-lifecycle-service.js +2 -1
- package/dist/setup/setup-plan.js +2 -2
- package/docs/architecture.md +2 -2
- package/docs/cli.md +4 -4
- package/docs/providers.md +5 -5
- package/package.json +1 -1
|
@@ -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
|
|
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:
|
|
129
|
+
summary: `${providerName} change planning could not resolve the future MCP server command: ${message}`,
|
|
169
130
|
providerIdentity: {
|
|
170
|
-
provider:
|
|
171
|
-
adapter:
|
|
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,
|
|
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
|
-
|
|
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: '
|
|
148
|
+
{ id: 'provider-supported', status: 'pass', detail: `Claude ${canonicalScope} scope supports guarded ~/.claude.json MCP merge, user agents, and ~/.claude/CLAUDE.md managed block after confirmation/preflight.` },
|
|
138
149
|
{ id: 'claude-scope', status: scopeWarnings.length === 0 ? 'pass' : 'warn', detail: scopeWarnings.join(' ') || `Claude scope resolved to canonical ${canonicalScope}.` },
|
|
139
150
|
{ id: 'claude-cli-presence', status: 'warn', detail: 'Read-only doctor does not execute `claude --version`; no provider process was launched.' },
|
|
140
|
-
{ id: '
|
|
151
|
+
{ id: 'claude-user-mcp-readable', status: mcp.status === 'invalid' ? 'fail' : mcp.status === 'missing' ? 'not-configured' : 'pass', detail: mcp.message, ...(mcp.status === 'invalid' ? { remediation: 'Fix malformed ~/.claude.json before installing VGXNESS Claude user support.' } : {}) },
|
|
152
|
+
{ id: 'claude-user-vgxness-mcp-entry', status: mcp.status === 'configured' ? 'pass' : mcp.status === 'conflicting' ? 'fail' : 'not-configured', detail: mcp.message, ...(mcp.status === 'conflicting' ? { remediation: 'Manually reconcile mcpServers.vgxness in ~/.claude.json before applying VGXNESS Claude user support.' } : {}) },
|
|
153
|
+
{ id: 'claude-user-agents-directory', status: agents.directoryExists ? 'pass' : 'not-configured', detail: agents.directoryExists ? 'Claude user agents directory exists.' : 'Claude user agents directory is missing; confirmed apply may create it.' },
|
|
154
|
+
{ id: 'claude-user-vgxness-agents', status: blockingAgents.length > 0 ? 'fail' : missingAgents.length > 0 ? 'not-configured' : 'pass', detail: blockingAgents.length > 0 ? `Conflicting or invalid VGXNESS Claude user agents: ${blockingAgents.map((agent) => agent.agentName).join(', ')}.` : missingAgents.length > 0 ? `Missing VGXNESS Claude user agents: ${missingAgents.map((agent) => agent.agentName).join(', ')}.` : 'Expected VGXNESS Claude user agent targets were inspected.' },
|
|
155
|
+
{ id: 'claude-user-agent-frontmatter', status: badFrontmatter.length === 0 ? 'pass' : 'fail', detail: badFrontmatter.length === 0 ? 'Expected Claude user agent frontmatter is valid.' : `Invalid or missing frontmatter for: ${badFrontmatter.map((agent) => agent.agentName).join(', ')}.` },
|
|
156
|
+
{ id: 'claude-user-agent-generated-metadata', status: badMarkers.length === 0 ? 'pass' : 'fail', detail: badMarkers.length === 0 ? 'Existing VGXNESS Claude user agent files include generated metadata markers.' : `Missing VGXNESS generated marker for: ${badMarkers.map((agent) => agent.agentName).join(', ')}.` },
|
|
157
|
+
{ ...claudeProjectMemoryCheck(userMemory), id: 'claude-user-memory-managed-block' },
|
|
158
|
+
readonlySafetyCheck(before, snapshotPaths(checkedPathList, normalized.workspaceRoot)),
|
|
141
159
|
];
|
|
142
160
|
const status = rollupProviderDoctor(checks.map((check) => check.status));
|
|
143
161
|
const compactChecksValue = compactChecks(checks, normalized.payloadMode);
|
|
144
162
|
const failedChecks = checks.filter((check) => check.status === 'fail').map((check) => check.id);
|
|
145
|
-
const recommendations = [
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
];
|
|
149
|
-
return { ok: true, value: { version: 1, kind: 'provider-doctor', project: normalized.project, providerAdapter: 'claude', scope: normalized.scope, workspaceRoot: normalized.workspaceRoot, status, payloadMode: normalized.payloadMode, overallStatus: status, checkCount: checks.length, passedCount: checks.filter((check) => check.status === 'pass').length, warningCount: checks.filter((check) => check.status === 'warn' || check.status === 'not-configured').length, errorCount: failedChecks.length, skippedCount: checks.filter((check) => check.status === 'skip').length, failedChecks, summary: `Claude ${canonicalScope} doctor is advisory and read-only; no private Claude files were inspected, no config was changed, and Claude Code was not executed.`, recommendations, checks: compactChecksValue, checkedPaths: [], bytes: { originalBytes: Buffer.byteLength(JSON.stringify({ checks, checkedPaths: [] }), 'utf8'), compactBytes: Buffer.byteLength(JSON.stringify({ checks: compactChecksValue, checkedPaths: [] }), 'utf8') }, verboseAvailable: normalized.payloadMode === 'compact', fullContentRef: `provider-doctor:claude:${canonicalScope}:${normalized.workspaceRoot}`, generatedAt: 'read-only-snapshot', safety: { ...PROVIDER_HEALTH_SAFETY, scopeCapabilities: CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES } } };
|
|
163
|
+
const recommendations = checks.flatMap((check) => (check.remediation === undefined ? [] : [check.remediation]));
|
|
164
|
+
const checkedPaths = normalized.payloadMode === 'verbose' ? checkedPathList : checkedPathList.filter((path) => existsSync(path) || path === mcp.path || path === userMemory.path);
|
|
165
|
+
return { ok: true, value: { version: 1, kind: 'provider-doctor', project: normalized.project, providerAdapter: 'claude', scope: normalized.scope, workspaceRoot: normalized.workspaceRoot, status, payloadMode: normalized.payloadMode, overallStatus: status, checkCount: checks.length, passedCount: checks.filter((check) => check.status === 'pass').length, warningCount: checks.filter((check) => check.status === 'warn' || check.status === 'not-configured').length, errorCount: failedChecks.length, skippedCount: checks.filter((check) => check.status === 'skip').length, failedChecks, summary: summarizeDoctor(status, failedChecks.length, recommendations.length), recommendations, checks: compactChecksValue, checkedPaths, bytes: { originalBytes: Buffer.byteLength(JSON.stringify({ checks, checkedPaths: checkedPathList }), 'utf8'), compactBytes: Buffer.byteLength(JSON.stringify({ checks: compactChecksValue, checkedPaths }), 'utf8') }, verboseAvailable: normalized.payloadMode === 'compact', fullContentRef: `provider-doctor:claude:${canonicalScope}:${normalized.workspaceRoot}`, generatedAt: 'read-only-snapshot', safety: { ...PROVIDER_HEALTH_SAFETY, scopeCapabilities: CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES } } };
|
|
150
166
|
}
|
|
151
167
|
}
|
|
152
168
|
function summarizeDoctor(status, failedCount, recommendationCount) {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { inspectClaudeCodeAgents } from './claude-code-agent-config.js';
|
|
4
|
-
import { createClaudeCodeCliRegistrationPreview } from './claude-code-cli.js';
|
|
5
4
|
import { claudeAdvisoryPaths, claudeMcpConfigPathStatus, claudeMcpEntryStatus, inspectClaudeCodeMcpConfig } from './claude-code-config.js';
|
|
6
5
|
import { inspectClaudeProjectMemory } from './claude-code-project-memory.js';
|
|
7
6
|
import { resolveClaudeCodeScope } from './claude-code-scope.js';
|
|
7
|
+
import { claudeUserMcpConfigPathStatus, claudeUserMcpEntryStatus, inspectClaudeCodeUserMcpConfig } from './claude-code-user-config.js';
|
|
8
|
+
import { inspectClaudeUserMemory } from './claude-code-user-memory.js';
|
|
8
9
|
import { resolveOpenCodeMcpInstallTarget } from './client-install-opencode-contract.js';
|
|
9
10
|
import { vgxnessOpenCodeDefaultAgent, vgxnessOpenCodePromptContractVersion, vgxnessOpenCodeSddSubagents } from './opencode-default-agent-config.js';
|
|
10
11
|
import { buildCanonicalAgentManifestDiagnostic } from './provider-canonical-agent-manifest.js';
|
|
@@ -147,28 +148,20 @@ export class ProviderStatusService {
|
|
|
147
148
|
}
|
|
148
149
|
getClaudeUserGlobalStatus(normalized, canonicalScope = 'user', scopeWarnings = []) {
|
|
149
150
|
const canonicalAgentManifest = (this.deps.canonicalAgentManifestDiagnostic ?? buildCanonicalAgentManifestDiagnostic)();
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
];
|
|
161
|
-
const mcpEntry = {
|
|
162
|
-
configured: false,
|
|
163
|
-
status: 'warn',
|
|
164
|
-
serverName: 'vgxness',
|
|
165
|
-
detail: `${canonicalScope} Claude MCP status is not inspected by VGXNESS; use this as planning guidance only. Future apply uses Claude CLI argv only: ${createClaudeCodeCliRegistrationPreview(canonicalScope).join(' ')}`,
|
|
166
|
-
};
|
|
167
|
-
const tools = requiredToolPresence();
|
|
168
|
-
const status = rollupProviderHealth([canonicalAgentManifest.status, 'warn']);
|
|
151
|
+
const mcpState = inspectClaudeCodeUserMcpConfig(normalized.env);
|
|
152
|
+
const agents = inspectClaudeCodeAgents({ workspaceRoot: normalized.workspaceRoot, scope: 'user', env: normalized.env });
|
|
153
|
+
const userMemory = inspectClaudeUserMemory(normalized.env);
|
|
154
|
+
const userMemoryPathStatus = { ...claudeProjectMemoryPathStatus(userMemory), label: 'user ~/.claude/CLAUDE.md managed block' };
|
|
155
|
+
const paths = [claudeUserMcpConfigPathStatus(mcpState), userMemoryPathStatus];
|
|
156
|
+
const mcpEntry = claudeUserMcpEntryStatus(mcpState);
|
|
157
|
+
const agentStatuses = agents.agents.map((agent) => (agent.status === 'managed' ? 'pass' : agent.status === 'missing' ? 'not-configured' : 'fail'));
|
|
158
|
+
const configStatus = claudeConfigHealthStatus([...paths.map((path) => path.status), ...agentStatuses]);
|
|
159
|
+
const tools = [...requiredToolPresence(), { tool: 'claude-cli', present: false, diagnostic: 'Read-only status does not execute `claude --version`; no Claude Code process was launched.' }];
|
|
160
|
+
const status = rollupProviderHealth([canonicalAgentManifest.status, configStatus]);
|
|
169
161
|
const sdd = normalized.change.length > 0 ? this.readSdd(normalized.project, normalized.change) : undefined;
|
|
170
|
-
const
|
|
171
|
-
const
|
|
162
|
+
const checkedPaths = normalized.payloadMode === 'verbose' ? [mcpState.path, agents.directoryPath, ...agents.agents.map((agent) => agent.path), userMemory.path] : [mcpState.path, userMemory.path, ...agents.agents.filter((agent) => agent.exists || agent.status !== 'missing').map((agent) => agent.path)];
|
|
163
|
+
const verboseShape = { config: { status: configStatus, paths, mcpEntry }, canonicalAgentManifest, agents, userMemory: { status: userMemory.status, action: userMemory.action }, scopeCapabilities: CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES, scopeWarnings, sdd, mcpRequiredTools: tools };
|
|
164
|
+
const compactShape = { config: { status: configStatus, paths: compactPaths(paths, 'compact'), mcpEntry: compactMcpEntry(mcpEntry, 'compact') }, canonicalAgentManifest, agentSummary: summarizeClaudeAgents(agents), userMemory: { status: userMemory.status, action: userMemory.action }, scopeCapabilities: CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES, sdd: sdd === undefined ? undefined : compactSdd(sdd, 'compact'), mcpRequiredTools: tools };
|
|
172
165
|
return {
|
|
173
166
|
ok: true,
|
|
174
167
|
value: {
|
|
@@ -181,14 +174,14 @@ export class ProviderStatusService {
|
|
|
181
174
|
status,
|
|
182
175
|
payloadMode: normalized.payloadMode,
|
|
183
176
|
overallStatus: status,
|
|
184
|
-
inspectedPaths:
|
|
185
|
-
issueCount: canonicalAgentManifest.status === 'fail'
|
|
186
|
-
warningCount:
|
|
187
|
-
summary:
|
|
188
|
-
nextAction:
|
|
189
|
-
checkedPaths
|
|
177
|
+
inspectedPaths: checkedPaths,
|
|
178
|
+
issueCount: [canonicalAgentManifest.status, configStatus, ...agentStatuses].filter((item) => item === 'fail' || item === 'not-configured').length,
|
|
179
|
+
warningCount: scopeWarnings.length + paths.filter((path) => path.status === 'warn').length + 1,
|
|
180
|
+
summary: summarizeClaudeStatus(status, mcpEntry),
|
|
181
|
+
nextAction: nextActionFor(status, mcpEntry, sdd?.next),
|
|
182
|
+
checkedPaths,
|
|
190
183
|
canonicalAgentManifest,
|
|
191
|
-
config: { status:
|
|
184
|
+
config: { status: configStatus, paths: compactPaths(paths, normalized.payloadMode), mcpEntry: compactMcpEntry(mcpEntry, normalized.payloadMode) },
|
|
192
185
|
...(sdd === undefined ? {} : { sdd: compactSdd(sdd, normalized.payloadMode) }),
|
|
193
186
|
mcpRequiredTools: tools,
|
|
194
187
|
originalBytes: Buffer.byteLength(JSON.stringify(verboseShape), 'utf8'),
|
package/dist/mcp/schema.js
CHANGED
|
@@ -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
|
package/dist/mcp/validation.js
CHANGED
|
@@ -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;
|
package/dist/sdd/schema.js
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
export const sddPhases = ['explore', 'proposal', 'spec', 'design', 'tasks', 'apply-progress', 'verify', 'archive'];
|
|
3
|
+
/**
|
|
4
|
+
* Governance status of an SDD artifact.
|
|
5
|
+
*
|
|
6
|
+
* - `draft` — present in the DB; satisfies downstream readiness when the
|
|
7
|
+
* `VGXNESS_SDD_AUTO_ADVANCE` feature flag is on (or
|
|
8
|
+
* `SddWorkflowService` is constructed with `autoAdvance: true`).
|
|
9
|
+
* Does NOT count toward the change being `complete`.
|
|
10
|
+
* - `accepted` — content has been frozen by an explicit human
|
|
11
|
+
* `vgxness_sdd_accept_artifact` call. Satisfies downstream
|
|
12
|
+
* readiness regardless of the auto-advance flag.
|
|
13
|
+
* - `rejected` — explicit human rejection. Hard-blocker for downstream
|
|
14
|
+
* readiness; can only be cleared by re-opening the artifact.
|
|
15
|
+
* - `superseded`— replaced by a newer artifact under a different topic key.
|
|
16
|
+
* Hard-blocker; cannot be re-saved or accepted.
|
|
17
|
+
*/
|
|
3
18
|
export const sddArtifactStatuses = ['draft', 'accepted', 'rejected', 'superseded'];
|
|
4
19
|
export const sddArtifactNormalizationWarnings = ['legacy-artifact-defaulted-to-draft', 'invalid-artifact-metadata-defaulted-to-draft'];
|
|
5
20
|
export const SddArtifactStatusSchema = z.enum(sddArtifactStatuses);
|
|
@@ -2,12 +2,32 @@ import { summarizePayloadContent } from '../payload/payload-summary.js';
|
|
|
2
2
|
import { isSddPhase, normalizeSddArtifact, sddPhases, sddPrerequisites, sddTopicKey, } from './schema.js';
|
|
3
3
|
const defaultContext = { actor: 'sdd-workflow-service' };
|
|
4
4
|
const validChangePattern = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
5
|
+
function readAutoAdvanceFromEnv() {
|
|
6
|
+
const value = process.env.VGXNESS_SDD_AUTO_ADVANCE;
|
|
7
|
+
return value === 'true' || value === '1';
|
|
8
|
+
}
|
|
9
|
+
function isPrerequisiteSatisfied(status) {
|
|
10
|
+
if (status === undefined)
|
|
11
|
+
return false;
|
|
12
|
+
if (status.legacy === true)
|
|
13
|
+
return false;
|
|
14
|
+
if (status.accepted === true)
|
|
15
|
+
return true;
|
|
16
|
+
if (status.state === 'draft')
|
|
17
|
+
return true;
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
5
20
|
export class SddWorkflowService {
|
|
6
21
|
memory;
|
|
7
22
|
context;
|
|
8
|
-
|
|
23
|
+
options;
|
|
24
|
+
constructor(memory, context = defaultContext, options = {}) {
|
|
9
25
|
this.memory = memory;
|
|
10
26
|
this.context = context;
|
|
27
|
+
this.options = {
|
|
28
|
+
...options,
|
|
29
|
+
autoAdvance: options.autoAdvance ?? readAutoAdvanceFromEnv(),
|
|
30
|
+
};
|
|
11
31
|
}
|
|
12
32
|
getWorkflow(change) {
|
|
13
33
|
return sddPhases.map((phase) => ({
|
|
@@ -23,7 +43,7 @@ export class SddWorkflowService {
|
|
|
23
43
|
const phases = this.getPhaseStatuses(validated.value.project, validated.value.change);
|
|
24
44
|
if (!phases.ok)
|
|
25
45
|
return phases;
|
|
26
|
-
const readiness = getReadinessFromStatuses(validated.value.change, validated.value.phase, phases.value);
|
|
46
|
+
const readiness = getReadinessFromStatuses(validated.value.change, validated.value.phase, phases.value, this.options);
|
|
27
47
|
return {
|
|
28
48
|
ok: true,
|
|
29
49
|
value: {
|
|
@@ -40,7 +60,7 @@ export class SddWorkflowService {
|
|
|
40
60
|
const phases = this.getPhaseStatuses(validated.value.project, validated.value.change);
|
|
41
61
|
if (!phases.ok)
|
|
42
62
|
return phases;
|
|
43
|
-
return statusFromPhases(validated.value.change, phases.value);
|
|
63
|
+
return statusFromPhases(validated.value.change, phases.value, this.options);
|
|
44
64
|
}
|
|
45
65
|
getNext(input) {
|
|
46
66
|
const validated = validateProjectAndChange(input.project, input.change);
|
|
@@ -49,7 +69,7 @@ export class SddWorkflowService {
|
|
|
49
69
|
const phases = this.getPhaseStatuses(validated.value.project, validated.value.change);
|
|
50
70
|
if (!phases.ok)
|
|
51
71
|
return phases;
|
|
52
|
-
return ok(nextDecisionFromStatuses(validated.value.change, phases.value));
|
|
72
|
+
return ok(nextDecisionFromStatuses(validated.value.change, phases.value, this.options));
|
|
53
73
|
}
|
|
54
74
|
getCockpit(input) {
|
|
55
75
|
const validated = validateProjectAndChange(input.project, input.change);
|
|
@@ -59,12 +79,12 @@ export class SddWorkflowService {
|
|
|
59
79
|
if (!snapshot.ok)
|
|
60
80
|
return snapshot;
|
|
61
81
|
const phases = snapshot.value.phases;
|
|
62
|
-
const next = nextDecisionFromStatuses(validated.value.change, phases);
|
|
82
|
+
const next = nextDecisionFromStatuses(validated.value.change, phases, this.options);
|
|
63
83
|
const cockpitPhases = phases.map((phaseStatus) => {
|
|
64
84
|
const readiness = {
|
|
65
85
|
change: validated.value.change,
|
|
66
86
|
phase: phaseStatus.phase,
|
|
67
|
-
...getReadinessFromStatuses(validated.value.change, phaseStatus.phase, phases),
|
|
87
|
+
...getReadinessFromStatuses(validated.value.change, phaseStatus.phase, phases, this.options),
|
|
68
88
|
};
|
|
69
89
|
const artifact = phaseStatus.present ? cockpitArtifactSummaryFromSnapshotItem(phaseStatus) : undefined;
|
|
70
90
|
const blockers = cockpitBlockersForPhase(phaseStatus, readiness);
|
|
@@ -115,7 +135,7 @@ export class SddWorkflowService {
|
|
|
115
135
|
if (!snapshot.ok)
|
|
116
136
|
return snapshot;
|
|
117
137
|
const phases = snapshot.value.phases;
|
|
118
|
-
const status = statusFromPhases(validated.value.change, phases);
|
|
138
|
+
const status = statusFromPhases(validated.value.change, phases, this.options);
|
|
119
139
|
if (!status.ok)
|
|
120
140
|
return status;
|
|
121
141
|
const warnings = [];
|
|
@@ -137,7 +157,7 @@ export class SddWorkflowService {
|
|
|
137
157
|
}
|
|
138
158
|
return [{ phase: phase.phase, topicKey: phase.topicKey, artifact: phase.artifact, envelope }];
|
|
139
159
|
});
|
|
140
|
-
const readiness = input.phase === undefined ? undefined : { change: validated.value.change, phase: input.phase, ...getReadinessFromStatuses(validated.value.change, input.phase, phases) };
|
|
160
|
+
const readiness = input.phase === undefined ? undefined : { change: validated.value.change, phase: input.phase, ...getReadinessFromStatuses(validated.value.change, input.phase, phases, this.options) };
|
|
141
161
|
return ok({ status: status.value, artifacts, ...(readiness === undefined ? {} : { readiness }), warnings });
|
|
142
162
|
}
|
|
143
163
|
saveArtifact(input) {
|
|
@@ -146,13 +166,20 @@ export class SddWorkflowService {
|
|
|
146
166
|
return validated;
|
|
147
167
|
if (input.content.trim().length === 0)
|
|
148
168
|
return validationFailure('SDD artifact content must not be empty');
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
169
|
+
const { project, change, phase } = validated.value;
|
|
170
|
+
const topicKey = sddTopicKey(change, phase);
|
|
171
|
+
const existing = this.memory.getArtifact(project, topicKey, this.context);
|
|
172
|
+
if (existing.ok) {
|
|
173
|
+
const envelope = normalizeSddArtifact(existing.value);
|
|
174
|
+
const status = envelope.metadata.status;
|
|
175
|
+
if (status === 'accepted')
|
|
176
|
+
return validationFailure('Cannot overwrite an accepted SDD artifact; supersede it under a new topic key.');
|
|
177
|
+
if (status === 'rejected')
|
|
178
|
+
return validationFailure('Rejected SDD artifact must be re-opened with an explicit decision before saving.');
|
|
179
|
+
if (status === 'superseded')
|
|
180
|
+
return validationFailure('Cannot overwrite a superseded SDD artifact.');
|
|
181
|
+
}
|
|
182
|
+
return this.memory.saveArtifact({ project, topicKey, phase, content: input.content, metadata: { status: 'draft' } }, this.context);
|
|
156
183
|
}
|
|
157
184
|
acceptArtifact(input) {
|
|
158
185
|
const validated = this.validatePhaseInput(input);
|
|
@@ -169,6 +196,11 @@ export class SddWorkflowService {
|
|
|
169
196
|
if (!existing.ok)
|
|
170
197
|
return existing;
|
|
171
198
|
const existingEnvelope = normalizeSddArtifact(existing.value);
|
|
199
|
+
const existingStatus = existingEnvelope.metadata.status;
|
|
200
|
+
if (existingStatus === 'rejected')
|
|
201
|
+
return validationFailure('Rejected SDD artifact must be re-opened with an explicit decision before accepting.');
|
|
202
|
+
if (existingStatus === 'superseded')
|
|
203
|
+
return validationFailure('Cannot accept a superseded SDD artifact; create a new artifact under a different topic key instead.');
|
|
172
204
|
const acceptance = {
|
|
173
205
|
actor: {
|
|
174
206
|
type: 'human',
|
|
@@ -203,7 +235,7 @@ export class SddWorkflowService {
|
|
|
203
235
|
const statuses = this.getPhaseStatuses(validated.value.project, validated.value.change);
|
|
204
236
|
if (!statuses.ok)
|
|
205
237
|
return statuses;
|
|
206
|
-
return ok(compactArtifactProjection(artifact.value, validated.value.change, validated.value.phase, getReadinessFromStatuses(validated.value.change, validated.value.phase, statuses.value)));
|
|
238
|
+
return ok(compactArtifactProjection(artifact.value, validated.value.change, validated.value.phase, getReadinessFromStatuses(validated.value.change, validated.value.phase, statuses.value, this.options)));
|
|
207
239
|
}
|
|
208
240
|
listArtifacts(input) {
|
|
209
241
|
const validated = validateProjectAndChange(input.project, input.change);
|
|
@@ -219,7 +251,7 @@ export class SddWorkflowService {
|
|
|
219
251
|
artifacts: snapshot.value.phases.flatMap((phase) => {
|
|
220
252
|
if (phase.artifact === undefined)
|
|
221
253
|
return [];
|
|
222
|
-
return [compactArtifactProjection(phase.artifact, validated.value.change, phase.phase, getReadinessFromStatuses(validated.value.change, phase.phase, snapshot.value.phases))];
|
|
254
|
+
return [compactArtifactProjection(phase.artifact, validated.value.change, phase.phase, getReadinessFromStatuses(validated.value.change, phase.phase, snapshot.value.phases, this.options))];
|
|
223
255
|
}),
|
|
224
256
|
fullRetrieval: {
|
|
225
257
|
tool: 'vgxness_sdd_list_artifacts',
|
|
@@ -280,14 +312,14 @@ export class SddWorkflowService {
|
|
|
280
312
|
return ok({ project, change, phases });
|
|
281
313
|
}
|
|
282
314
|
}
|
|
283
|
-
export function nextDecisionFromStatuses(change, phases) {
|
|
315
|
+
export function nextDecisionFromStatuses(change, phases, options = {}) {
|
|
284
316
|
const blockedPresentPhase = phases.find((status) => {
|
|
285
317
|
if (!status.present)
|
|
286
318
|
return false;
|
|
287
|
-
return (getReadinessFromStatuses(change, status.phase, phases).blockedPrerequisites ?? []).length > 0;
|
|
319
|
+
return (getReadinessFromStatuses(change, status.phase, phases, options).blockedPrerequisites ?? []).length > 0;
|
|
288
320
|
});
|
|
289
321
|
if (blockedPresentPhase !== undefined) {
|
|
290
|
-
const readiness = getReadinessFromStatuses(change, blockedPresentPhase.phase, phases);
|
|
322
|
+
const readiness = getReadinessFromStatuses(change, blockedPresentPhase.phase, phases, options);
|
|
291
323
|
const blockers = readiness.blockedPrerequisites ?? [];
|
|
292
324
|
return {
|
|
293
325
|
change,
|
|
@@ -329,7 +361,7 @@ export function nextDecisionFromStatuses(change, phases) {
|
|
|
329
361
|
recommendedAction: 'No next SDD phase remains for this change.',
|
|
330
362
|
};
|
|
331
363
|
}
|
|
332
|
-
const readiness = getReadinessFromStatuses(change, nextMissingPhase, phases);
|
|
364
|
+
const readiness = getReadinessFromStatuses(change, nextMissingPhase, phases, options);
|
|
333
365
|
if (!readiness.ready) {
|
|
334
366
|
const blockers = readiness.blockedPrerequisites ?? [];
|
|
335
367
|
return {
|
|
@@ -352,24 +384,26 @@ export function nextDecisionFromStatuses(change, phases) {
|
|
|
352
384
|
recommendedAction: `Run the ${nextMissingPhase} SDD phase for ${change}.`,
|
|
353
385
|
};
|
|
354
386
|
}
|
|
355
|
-
function statusFromPhases(change, phases) {
|
|
387
|
+
function statusFromPhases(change, phases, options = {}) {
|
|
356
388
|
const nextReadyPhase = sddPhases.find((phase) => {
|
|
357
389
|
if (phases.find((status) => status.phase === phase)?.present)
|
|
358
390
|
return false;
|
|
359
|
-
return getReadinessFromStatuses(change, phase, phases).ready;
|
|
391
|
+
return getReadinessFromStatuses(change, phase, phases, options).ready;
|
|
360
392
|
});
|
|
361
393
|
const status = { change, phases };
|
|
362
394
|
if (nextReadyPhase !== undefined)
|
|
363
395
|
status.nextReadyPhase = nextReadyPhase;
|
|
364
396
|
return { ok: true, value: status };
|
|
365
397
|
}
|
|
366
|
-
function getReadinessFromStatuses(change, phase, phases) {
|
|
398
|
+
function getReadinessFromStatuses(change, phase, phases, options = {}) {
|
|
367
399
|
const satisfiedPrerequisites = [];
|
|
368
400
|
const missingArtifactTopicKeys = [];
|
|
369
401
|
const blockedPrerequisites = [];
|
|
402
|
+
const useAutoAdvance = options.autoAdvance === true;
|
|
370
403
|
for (const prerequisite of sddPrerequisites[phase]) {
|
|
371
404
|
const status = phases.find((candidate) => candidate.phase === prerequisite);
|
|
372
|
-
|
|
405
|
+
const satisfied = useAutoAdvance ? isPrerequisiteSatisfied(status) : status?.accepted === true;
|
|
406
|
+
if (satisfied) {
|
|
373
407
|
satisfiedPrerequisites.push(prerequisite);
|
|
374
408
|
continue;
|
|
375
409
|
}
|
|
@@ -508,10 +542,6 @@ function blockerReasonForStatus(status) {
|
|
|
508
542
|
if (status.legacy === true)
|
|
509
543
|
return 'legacy';
|
|
510
544
|
const state = status.state ?? (status.present ? 'draft' : 'missing');
|
|
511
|
-
if (state === 'missing')
|
|
512
|
-
return 'missing';
|
|
513
|
-
if (state === 'accepted')
|
|
514
|
-
return 'draft';
|
|
515
545
|
return state;
|
|
516
546
|
}
|
|
517
547
|
function formatBlockers(blockers) {
|