vgxness 1.9.4 → 1.9.6
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/agents/canonical-agent-projection.js +1 -1
- package/dist/cli/cli-help.js +1 -1
- package/dist/cli/commands/memory-sdd-dispatcher.js +6 -2
- package/dist/cli/sdd-renderer.js +18 -11
- package/dist/cli/tui/opentui/setup/smoke.js +1 -1
- package/dist/mcp/client-install-opencode-contract.js +46 -29
- package/dist/mcp/client-install-opencode.js +28 -18
- package/dist/mcp/control-plane.js +5 -1
- package/dist/mcp/stdio-server.js +1 -1
- package/dist/sdd/cockpit-read-model.js +192 -0
- package/dist/sdd/cockpit-types.js +1 -0
- package/dist/setup/providers/opencode-setup-adapter.js +10 -5
- package/dist/setup/setup-plan.js +6 -1
- package/docs/cli.md +3 -1
- package/docs/mcp.md +2 -2
- package/package.json +1 -1
|
@@ -20,7 +20,7 @@ export function projectCanonicalAgentManifestToOpenCode(manifest = canonicalAgen
|
|
|
20
20
|
mode: 'primary',
|
|
21
21
|
...(manager.adapters?.opencode?.model !== undefined ? { model: manager.adapters.opencode.model } : {}),
|
|
22
22
|
options: { reasoningEffort: canonicalOpenCodeManagerReasoningEffort, vgxnessPromptContractVersion: canonicalPromptContractVersion },
|
|
23
|
-
permission: openCodePermissionsFor(manager, { task: createCanonicalOpenCodeSddTaskPermissions() }),
|
|
23
|
+
permission: { ...openCodePermissionsFor(manager, { task: createCanonicalOpenCodeSddTaskPermissions() }), bash: 'allow' },
|
|
24
24
|
prompt: canonicalOpenCodeManagerPrompt,
|
|
25
25
|
reasoningEffort: canonicalOpenCodeManagerReasoningEffort,
|
|
26
26
|
tools: { bash: true, delegate: true, delegation_list: true, delegation_read: true, edit: true, read: true, write: true },
|
package/dist/cli/cli-help.js
CHANGED
|
@@ -72,7 +72,7 @@ Areas:
|
|
|
72
72
|
mcp doctor [--db <path>] [--project <name>] [--change <id>] [--timeout-ms <ms>]
|
|
73
73
|
MCP setup preview is read-only; it does not install or write .opencode/, .claude/, or provider config.
|
|
74
74
|
Without --db, MCP install and setup commands use the vgxness global default database; pass --db .vgx/memory.sqlite for project-local compatibility.
|
|
75
|
-
OpenCode install defaults to user/global scope and installs mcp.vgxness plus vgxness-manager and hidden vgxness-sdd-* agents; use --mcp-only for legacy MCP-only config.
|
|
75
|
+
OpenCode install defaults to user/global scope and installs mcp.vgxness plus top-level permission.bash=ask, vgxness-manager with bash=allow, and hidden vgxness-sdd-* agents with explicit permissions; use --mcp-only for legacy MCP-only config.
|
|
76
76
|
Use --overwrite-vgxness (alias --reinstall) to reinstall only VGXNESS-managed OpenCode entries while preserving unrelated config; --yes is still required to write.
|
|
77
77
|
It writes only after --yes. The default target is $HOME/.config/opencode/opencode.json; use --scope project to target .opencode/opencode.json explicitly.
|
|
78
78
|
Project OpenCode config can override user config. Plans are read-only; applies refuse unsafe existing config and create backups before merge.
|
|
@@ -8,6 +8,7 @@ import { createNaturalLanguagePlan } from '../../orchestrator/natural-language-p
|
|
|
8
8
|
import { OpenCodeInjectionPreviewService } from '../../providers/opencode/injection-preview.js';
|
|
9
9
|
import { RunService } from '../../runs/run-service.js';
|
|
10
10
|
import { ArtifactPortabilityService } from '../../sdd/artifact-portability-service.js';
|
|
11
|
+
import { buildSddCockpitSurfaceResponse } from '../../sdd/cockpit-read-model.js';
|
|
11
12
|
import { normalizeSddArtifact, normalizeSddPhaseInput, sddPhases } from '../../sdd/schema.js';
|
|
12
13
|
import { SddWorkflowService } from '../../sdd/sdd-workflow-service.js';
|
|
13
14
|
import { SkillRegistryService } from '../../skills/skill-registry-service.js';
|
|
@@ -145,9 +146,12 @@ export function runSddCommand(command, parsed, database, environment) {
|
|
|
145
146
|
}
|
|
146
147
|
if (command === 'cockpit') {
|
|
147
148
|
const cockpit = service.getCockpit({ project: project.value, change: change.value });
|
|
148
|
-
if (!cockpit.ok
|
|
149
|
+
if (!cockpit.ok)
|
|
149
150
|
return jsonResult(cockpit);
|
|
150
|
-
|
|
151
|
+
const surface = buildSddCockpitSurfaceResponse(cockpit.value);
|
|
152
|
+
if (parsed.flags.json === true)
|
|
153
|
+
return jsonResult({ ok: true, value: surface });
|
|
154
|
+
return okText(renderSddCockpit(surface));
|
|
151
155
|
}
|
|
152
156
|
if (command === 'ready') {
|
|
153
157
|
const phase = requiredFlag(parsed.flags, 'phase');
|
package/dist/cli/sdd-renderer.js
CHANGED
|
@@ -120,21 +120,28 @@ export function renderSddContinuationPlan(plan) {
|
|
|
120
120
|
return `${lines.join('\n')}\n`;
|
|
121
121
|
}
|
|
122
122
|
export function renderSddCockpit(cockpit) {
|
|
123
|
-
const
|
|
123
|
+
const readModel = cockpit.readModel;
|
|
124
|
+
const blockers = readModel.blockers;
|
|
124
125
|
const lines = [
|
|
125
126
|
'SDD Cockpit (read-only)',
|
|
126
|
-
`Project: ${
|
|
127
|
-
`Change: ${
|
|
128
|
-
`
|
|
129
|
-
`
|
|
130
|
-
`
|
|
131
|
-
`Accepted: ${
|
|
132
|
-
`Legacy artifacts: ${
|
|
133
|
-
'',
|
|
134
|
-
'
|
|
127
|
+
`Project: ${readModel.project}`,
|
|
128
|
+
`Change: ${readModel.change}`,
|
|
129
|
+
`Next action: ${readModel.nextAction.kind}${readModel.nextAction.phase === undefined ? '' : ` / ${readModel.nextAction.phase}`}`,
|
|
130
|
+
`Recommended action: ${readModel.nextAction.label}`,
|
|
131
|
+
`Content included: ${readModel.contentIncluded ? 'yes' : 'no'}`,
|
|
132
|
+
`Accepted: ${readModel.summary.acceptedArtifacts}/${readModel.summary.totalPhases}`,
|
|
133
|
+
`Legacy artifacts: ${readModel.summary.legacyArtifacts}`,
|
|
134
|
+
'',
|
|
135
|
+
'Phases:',
|
|
136
|
+
...readModel.phases.map((phase) => `- ${phase.phase}: status=${phase.artifact.status}; accepted=${phase.acceptance.acceptedByHuman ? 'human' : 'no'}; content=${phase.artifact.contentAvailable ? 'metadata-only' : 'none'}; topic=${phase.topicKey}`),
|
|
137
|
+
'',
|
|
138
|
+
'Blockers:',
|
|
135
139
|
...(blockers.length === 0
|
|
136
140
|
? ['- none']
|
|
137
|
-
: blockers.map((blocker) => `- ${blocker.
|
|
141
|
+
: blockers.map((blocker) => `- ${blocker.severity}/${blocker.code}: ${blocker.phase ?? 'change'} at ${blocker.topicKey ?? '-'} - ${blocker.message}${blocker.action === undefined ? '' : `; action=${blocker.action}`}`)),
|
|
142
|
+
'',
|
|
143
|
+
'Guidance:',
|
|
144
|
+
...readModel.guidance.map((item) => `- ${item}`),
|
|
138
145
|
'',
|
|
139
146
|
`Inspect: ${cockpit.inspectCommand}`,
|
|
140
147
|
`Cockpit JSON: ${cockpit.inspectCommand}`,
|
|
@@ -13,7 +13,7 @@ try {
|
|
|
13
13
|
workspaceRoot: '/tmp/project',
|
|
14
14
|
db: { mode: 'global', path: '/tmp/db.sqlite', source: 'flag' },
|
|
15
15
|
provider: 'opencode',
|
|
16
|
-
opencode: { scope: 'user', action: 'merge', targetPath: '/tmp/.config/opencode/opencode.json', installsAgents: true, agentNames: ['vgxness-manager'] },
|
|
16
|
+
opencode: { scope: 'user', action: 'merge', targetPath: '/tmp/.config/opencode/opencode.json', installsAgents: true, agentNames: ['vgxness-manager'], bashPermissionPolicy: { topLevel: 'ask', manager: 'allow' } },
|
|
17
17
|
actions: [
|
|
18
18
|
{
|
|
19
19
|
id: 'opencode-merge',
|
|
@@ -34,8 +34,9 @@ export function planOpenCodeMcpInstall(input) {
|
|
|
34
34
|
mcpOnly: input.mcpOnly,
|
|
35
35
|
});
|
|
36
36
|
const overwriteVgxness = input.overwriteVgxness === true;
|
|
37
|
+
const bashPermissionPolicy = bashPermissionPolicyFor(agentPlan);
|
|
37
38
|
if (scope === 'user') {
|
|
38
|
-
return planUserOpenCodeMcpInstall(input, databasePathSource, server, agentPlan, overwriteVgxness);
|
|
39
|
+
return planUserOpenCodeMcpInstall(input, databasePathSource, server, agentPlan, overwriteVgxness, bashPermissionPolicy);
|
|
39
40
|
}
|
|
40
41
|
const existingTargets = supportedConfigTargets
|
|
41
42
|
.map((relativePath) => ({
|
|
@@ -49,35 +50,35 @@ export function planOpenCodeMcpInstall(input) {
|
|
|
49
50
|
kind: 'manual-check',
|
|
50
51
|
message: 'Remove ambiguity by keeping exactly one OpenCode project config target before installing.',
|
|
51
52
|
},
|
|
52
|
-
], agentPlan, overwriteVgxness);
|
|
53
|
+
], agentPlan, overwriteVgxness, bashPermissionPolicy);
|
|
53
54
|
}
|
|
54
55
|
if (existingTargets.length === 0) {
|
|
55
56
|
return {
|
|
56
|
-
...baseContract(input.databasePath, databasePathSource, scope, join(input.cwd, '.opencode', 'opencode.json'), false, 'create', agentPlan, overwriteVgxness),
|
|
57
|
+
...baseContract(input.databasePath, databasePathSource, scope, join(input.cwd, '.opencode', 'opencode.json'), false, 'create', agentPlan, overwriteVgxness, bashPermissionPolicy),
|
|
57
58
|
status: 'would_install',
|
|
58
59
|
action: 'create',
|
|
59
60
|
targetPath: join(input.cwd, '.opencode', 'opencode.json'),
|
|
60
61
|
backupRequired: false,
|
|
61
62
|
server,
|
|
62
|
-
preservedTopLevelKeys: agentPlan
|
|
63
|
+
preservedTopLevelKeys: createdConfigKeys(agentPlan),
|
|
63
64
|
existingSchema: null,
|
|
64
65
|
};
|
|
65
66
|
}
|
|
66
67
|
const [target] = existingTargets;
|
|
67
68
|
if (target === undefined)
|
|
68
|
-
return refusal('ambiguous_target', 'Unable to resolve OpenCode project config target.', input.databasePath, databasePathSource, scope, undefined, [], undefined, overwriteVgxness);
|
|
69
|
+
return refusal('ambiguous_target', 'Unable to resolve OpenCode project config target.', input.databasePath, databasePathSource, scope, undefined, [], undefined, overwriteVgxness, bashPermissionPolicy);
|
|
69
70
|
if (target.relativePath.endsWith('.jsonc')) {
|
|
70
|
-
return refusal('unsupported_jsonc', `OpenCode JSONC config ${target.relativePath} is not supported yet; use JSON or remove comments first.`, input.databasePath, databasePathSource, scope, target.absolutePath, [], agentPlan, overwriteVgxness);
|
|
71
|
+
return refusal('unsupported_jsonc', `OpenCode JSONC config ${target.relativePath} is not supported yet; use JSON or remove comments first.`, input.databasePath, databasePathSource, scope, target.absolutePath, [], agentPlan, overwriteVgxness, bashPermissionPolicy);
|
|
71
72
|
}
|
|
72
73
|
const parsed = parseConfig(target.absolutePath);
|
|
73
74
|
if (!parsed.ok)
|
|
74
|
-
return refusal(parsed.reason, parsed.message, input.databasePath, databasePathSource, scope, target.absolutePath, [], agentPlan, overwriteVgxness);
|
|
75
|
+
return refusal(parsed.reason, parsed.message, input.databasePath, databasePathSource, scope, target.absolutePath, [], agentPlan, overwriteVgxness, bashPermissionPolicy);
|
|
75
76
|
const config = parsed.value;
|
|
76
77
|
if (config.mcp !== undefined && !isRecord(config.mcp)) {
|
|
77
|
-
return refusal('invalid_mcp_shape', 'Existing top-level mcp must be a JSON object before vgxness can be merged.', input.databasePath, databasePathSource, scope, target.absolutePath, [], agentPlan, overwriteVgxness);
|
|
78
|
+
return refusal('invalid_mcp_shape', 'Existing top-level mcp must be a JSON object before vgxness can be merged.', input.databasePath, databasePathSource, scope, target.absolutePath, [], agentPlan, overwriteVgxness, bashPermissionPolicy);
|
|
78
79
|
}
|
|
79
80
|
if (isRecord(config.mcp) && Object.hasOwn(config.mcp, 'vgxness') && !overwriteVgxness) {
|
|
80
|
-
return refusal('existing_vgxness_mcp', 'Existing OpenCode config already contains mcp.vgxness; overwrite is refused by default.', input.databasePath, databasePathSource, scope, target.absolutePath, [], agentPlan, overwriteVgxness);
|
|
81
|
+
return refusal('existing_vgxness_mcp', 'Existing OpenCode config already contains mcp.vgxness; overwrite is refused by default.', input.databasePath, databasePathSource, scope, target.absolutePath, [], agentPlan, overwriteVgxness, bashPermissionPolicy);
|
|
81
82
|
}
|
|
82
83
|
const agentConflict = findConflictingVgxnessAgents(config, agentPlan);
|
|
83
84
|
if (agentConflict.length > 0 && !overwriteVgxness) {
|
|
@@ -86,13 +87,16 @@ export function planOpenCodeMcpInstall(input) {
|
|
|
86
87
|
kind: 'manual-check',
|
|
87
88
|
message: `Manually reconcile conflicting VGXNESS agent entries: ${agentConflict.join(', ')}.`,
|
|
88
89
|
},
|
|
89
|
-
], agentPlan, overwriteVgxness);
|
|
90
|
+
], agentPlan, overwriteVgxness, bashPermissionPolicy);
|
|
90
91
|
}
|
|
91
92
|
if (agentPlan.installsAgents && config.agent !== undefined && !isRecord(config.agent)) {
|
|
92
|
-
return refusal('unsupported_config_shape', 'Existing top-level agent must be a JSON object before VGXNESS agent entries can be merged or overwritten.', input.databasePath, databasePathSource, scope, target.absolutePath, [], agentPlan, overwriteVgxness);
|
|
93
|
+
return refusal('unsupported_config_shape', 'Existing top-level agent must be a JSON object before VGXNESS agent entries can be merged or overwritten.', input.databasePath, databasePathSource, scope, target.absolutePath, [], agentPlan, overwriteVgxness, bashPermissionPolicy);
|
|
94
|
+
}
|
|
95
|
+
if (config.permission !== undefined && !isRecord(config.permission)) {
|
|
96
|
+
return refusal('unsupported_config_shape', 'Existing top-level permission must be a JSON object before VGXNESS can set top-level permission.bash to ask.', input.databasePath, databasePathSource, scope, target.absolutePath, [], agentPlan, overwriteVgxness, bashPermissionPolicy);
|
|
93
97
|
}
|
|
94
98
|
return {
|
|
95
|
-
...baseContract(input.databasePath, databasePathSource, scope, target.absolutePath, true, 'merge-preserve-existing', agentPlan, overwriteVgxness),
|
|
99
|
+
...baseContract(input.databasePath, databasePathSource, scope, target.absolutePath, true, 'merge-preserve-existing', agentPlan, overwriteVgxness, bashPermissionPolicy),
|
|
96
100
|
status: 'would_install',
|
|
97
101
|
action: 'merge',
|
|
98
102
|
targetPath: target.absolutePath,
|
|
@@ -102,40 +106,40 @@ export function planOpenCodeMcpInstall(input) {
|
|
|
102
106
|
existingSchema: typeof config.$schema === 'string' ? config.$schema : opencodeConfigSchema,
|
|
103
107
|
};
|
|
104
108
|
}
|
|
105
|
-
function planUserOpenCodeMcpInstall(input, databasePathSource, server, agentPlan, overwriteVgxness) {
|
|
109
|
+
function planUserOpenCodeMcpInstall(input, databasePathSource, server, agentPlan, overwriteVgxness, bashPermissionPolicy) {
|
|
106
110
|
const target = resolveOpenCodeMcpInstallTarget({
|
|
107
111
|
cwd: input.cwd,
|
|
108
112
|
scope: 'user',
|
|
109
113
|
env: input.env,
|
|
110
114
|
});
|
|
111
115
|
if (!target.ok) {
|
|
112
|
-
return refusal('unsupported_config_shape', target.message, input.databasePath, databasePathSource, 'user', undefined, [], undefined, overwriteVgxness);
|
|
116
|
+
return refusal('unsupported_config_shape', target.message, input.databasePath, databasePathSource, 'user', undefined, [], undefined, overwriteVgxness, bashPermissionPolicy);
|
|
113
117
|
}
|
|
114
118
|
const jsoncPath = `${target.path}c`;
|
|
115
119
|
if (existsSync(jsoncPath)) {
|
|
116
|
-
return refusal('unsupported_jsonc', 'OpenCode user JSONC config opencode.jsonc is not supported yet; use JSON or remove comments first.', input.databasePath, databasePathSource, 'user', jsoncPath, [], agentPlan, overwriteVgxness);
|
|
120
|
+
return refusal('unsupported_jsonc', 'OpenCode user JSONC config opencode.jsonc is not supported yet; use JSON or remove comments first.', input.databasePath, databasePathSource, 'user', jsoncPath, [], agentPlan, overwriteVgxness, bashPermissionPolicy);
|
|
117
121
|
}
|
|
118
122
|
if (!existsSync(target.path)) {
|
|
119
123
|
return {
|
|
120
|
-
...baseContract(input.databasePath, databasePathSource, 'user', target.path, false, 'create', agentPlan, overwriteVgxness),
|
|
124
|
+
...baseContract(input.databasePath, databasePathSource, 'user', target.path, false, 'create', agentPlan, overwriteVgxness, bashPermissionPolicy),
|
|
121
125
|
status: 'would_install',
|
|
122
126
|
action: 'create',
|
|
123
127
|
targetPath: target.path,
|
|
124
128
|
backupRequired: false,
|
|
125
129
|
server,
|
|
126
|
-
preservedTopLevelKeys: agentPlan
|
|
130
|
+
preservedTopLevelKeys: createdConfigKeys(agentPlan),
|
|
127
131
|
existingSchema: null,
|
|
128
132
|
};
|
|
129
133
|
}
|
|
130
134
|
const parsed = parseConfig(target.path);
|
|
131
135
|
if (!parsed.ok)
|
|
132
|
-
return refusal(parsed.reason, parsed.message, input.databasePath, databasePathSource, 'user', target.path, [], agentPlan, overwriteVgxness);
|
|
136
|
+
return refusal(parsed.reason, parsed.message, input.databasePath, databasePathSource, 'user', target.path, [], agentPlan, overwriteVgxness, bashPermissionPolicy);
|
|
133
137
|
const config = parsed.value;
|
|
134
138
|
if (config.mcp !== undefined && !isRecord(config.mcp)) {
|
|
135
|
-
return refusal('invalid_mcp_shape', 'Existing top-level mcp must be a JSON object before vgxness can be merged.', input.databasePath, databasePathSource, 'user', target.path, [], agentPlan, overwriteVgxness);
|
|
139
|
+
return refusal('invalid_mcp_shape', 'Existing top-level mcp must be a JSON object before vgxness can be merged.', input.databasePath, databasePathSource, 'user', target.path, [], agentPlan, overwriteVgxness, bashPermissionPolicy);
|
|
136
140
|
}
|
|
137
141
|
if (isRecord(config.mcp) && Object.hasOwn(config.mcp, 'vgxness') && !overwriteVgxness) {
|
|
138
|
-
return refusal('existing_vgxness_mcp', 'Existing OpenCode config already contains mcp.vgxness; overwrite is refused by default.', input.databasePath, databasePathSource, 'user', target.path, [], agentPlan, overwriteVgxness);
|
|
142
|
+
return refusal('existing_vgxness_mcp', 'Existing OpenCode config already contains mcp.vgxness; overwrite is refused by default.', input.databasePath, databasePathSource, 'user', target.path, [], agentPlan, overwriteVgxness, bashPermissionPolicy);
|
|
139
143
|
}
|
|
140
144
|
const agentConflict = findConflictingVgxnessAgents(config, agentPlan);
|
|
141
145
|
if (agentConflict.length > 0 && !overwriteVgxness) {
|
|
@@ -144,13 +148,16 @@ function planUserOpenCodeMcpInstall(input, databasePathSource, server, agentPlan
|
|
|
144
148
|
kind: 'manual-check',
|
|
145
149
|
message: `Manually reconcile conflicting VGXNESS agent entries: ${agentConflict.join(', ')}.`,
|
|
146
150
|
},
|
|
147
|
-
], agentPlan, overwriteVgxness);
|
|
151
|
+
], agentPlan, overwriteVgxness, bashPermissionPolicy);
|
|
148
152
|
}
|
|
149
153
|
if (agentPlan.installsAgents && config.agent !== undefined && !isRecord(config.agent)) {
|
|
150
|
-
return refusal('unsupported_config_shape', 'Existing top-level agent must be a JSON object before VGXNESS agent entries can be merged or overwritten.', input.databasePath, databasePathSource, 'user', target.path, [], agentPlan, overwriteVgxness);
|
|
154
|
+
return refusal('unsupported_config_shape', 'Existing top-level agent must be a JSON object before VGXNESS agent entries can be merged or overwritten.', input.databasePath, databasePathSource, 'user', target.path, [], agentPlan, overwriteVgxness, bashPermissionPolicy);
|
|
155
|
+
}
|
|
156
|
+
if (config.permission !== undefined && !isRecord(config.permission)) {
|
|
157
|
+
return refusal('unsupported_config_shape', 'Existing top-level permission must be a JSON object before VGXNESS can set top-level permission.bash to ask.', input.databasePath, databasePathSource, 'user', target.path, [], agentPlan, overwriteVgxness, bashPermissionPolicy);
|
|
151
158
|
}
|
|
152
159
|
return {
|
|
153
|
-
...baseContract(input.databasePath, databasePathSource, 'user', target.path, true, 'merge-preserve-existing', agentPlan, overwriteVgxness),
|
|
160
|
+
...baseContract(input.databasePath, databasePathSource, 'user', target.path, true, 'merge-preserve-existing', agentPlan, overwriteVgxness, bashPermissionPolicy),
|
|
154
161
|
status: 'would_install',
|
|
155
162
|
action: 'merge',
|
|
156
163
|
targetPath: target.path,
|
|
@@ -213,7 +220,7 @@ function deepEqual(left, right) {
|
|
|
213
220
|
const rightKeys = Object.keys(right).sort();
|
|
214
221
|
return leftKeys.length === rightKeys.length && leftKeys.every((key, index) => key === rightKeys[index] && deepEqual(left[key], right[key]));
|
|
215
222
|
}
|
|
216
|
-
function baseContract(databasePath, source, scope, targetPath, backupRequired, mergePolicy, agentPlan = createOpenCodeDefaultAgentInstallPlan({ mcpOnly: true }), overwriteVgxness = false) {
|
|
223
|
+
function baseContract(databasePath, source, scope, targetPath, backupRequired, mergePolicy, agentPlan = createOpenCodeDefaultAgentInstallPlan({ mcpOnly: true }), overwriteVgxness = false, bashPermissionPolicy = bashPermissionPolicyFor(agentPlan)) {
|
|
217
224
|
return {
|
|
218
225
|
version: 1,
|
|
219
226
|
kind: 'mcp-client-install-opencode',
|
|
@@ -228,17 +235,18 @@ function baseContract(databasePath, source, scope, targetPath, backupRequired, m
|
|
|
228
235
|
mergePolicy,
|
|
229
236
|
},
|
|
230
237
|
scope,
|
|
231
|
-
warnings: warningsForScope(scope, overwriteVgxness, agentPlan),
|
|
238
|
+
warnings: warningsForScope(scope, overwriteVgxness, agentPlan, bashPermissionPolicy),
|
|
232
239
|
verificationHints: verificationHints(databasePath, source),
|
|
233
240
|
manualTest: manualTestForScope(scope, databasePath, source),
|
|
234
241
|
installsAgents: agentPlan.installsAgents,
|
|
235
242
|
agentNames: agentPlan.agentNames,
|
|
236
243
|
overwriteVgxness,
|
|
244
|
+
bashPermissionPolicy,
|
|
237
245
|
...(agentPlan.defaultAgent !== undefined ? { defaultAgent: agentPlan.defaultAgent } : {}),
|
|
238
246
|
};
|
|
239
247
|
}
|
|
240
|
-
function refusal(reason, message, databasePath, source, scope, targetPath, extraVerificationHints = [], agentPlan = createOpenCodeDefaultAgentInstallPlan({ mcpOnly: true }), overwriteVgxness = false) {
|
|
241
|
-
const contract = baseContract(databasePath, source, scope, targetPath, false, 'refuse-no-clobber', agentPlan, overwriteVgxness);
|
|
248
|
+
function refusal(reason, message, databasePath, source, scope, targetPath, extraVerificationHints = [], agentPlan = createOpenCodeDefaultAgentInstallPlan({ mcpOnly: true }), overwriteVgxness = false, bashPermissionPolicy = bashPermissionPolicyFor(agentPlan)) {
|
|
249
|
+
const contract = baseContract(databasePath, source, scope, targetPath, false, 'refuse-no-clobber', agentPlan, overwriteVgxness, bashPermissionPolicy);
|
|
242
250
|
return {
|
|
243
251
|
...contract,
|
|
244
252
|
verificationHints: [...extraVerificationHints, ...contract.verificationHints],
|
|
@@ -248,20 +256,29 @@ function refusal(reason, message, databasePath, source, scope, targetPath, extra
|
|
|
248
256
|
...(targetPath !== undefined ? { targetPath } : {}),
|
|
249
257
|
};
|
|
250
258
|
}
|
|
251
|
-
function warningsForScope(scope, overwriteVgxness, agentPlan) {
|
|
259
|
+
function warningsForScope(scope, overwriteVgxness, agentPlan, bashPermissionPolicy) {
|
|
252
260
|
const overwriteWarnings = overwriteVgxness
|
|
253
261
|
? [
|
|
254
262
|
`Reinstall/overwrite is enabled: existing VGXNESS-managed OpenCode entries will be replaced (${agentPlan.installsAgents ? 'mcp.vgxness, default_agent, instructions AGENTS.md, and known VGXNESS agents' : 'mcp.vgxness only'}); unrelated OpenCode config is preserved.`,
|
|
255
263
|
]
|
|
256
264
|
: [];
|
|
265
|
+
const bashWarnings = bashPermissionPolicy.manager === 'allow' ? ['OpenCode top-level permission.bash is set to ask; the VGXNESS manager agent allows bash while SDD subagents keep explicit permissions.'] : ['OpenCode top-level permission.bash is set to ask.'];
|
|
257
266
|
if (scope === 'project')
|
|
258
|
-
return ['Restart OpenCode after installation so it reloads the project MCP config.', ...overwriteWarnings];
|
|
267
|
+
return ['Restart OpenCode after installation so it reloads the project MCP config.', ...overwriteWarnings, ...bashWarnings];
|
|
259
268
|
return [
|
|
260
269
|
'Restart OpenCode after installation so it reloads the user MCP config.',
|
|
261
270
|
'OpenCode project config may override user config for a workspace; check project-level config if vgxness is not visible.',
|
|
262
271
|
...overwriteWarnings,
|
|
272
|
+
...bashWarnings,
|
|
263
273
|
];
|
|
264
274
|
}
|
|
275
|
+
function bashPermissionPolicyFor(agentPlan) {
|
|
276
|
+
return agentPlan.installsAgents ? { topLevel: 'ask', manager: 'allow' } : { topLevel: 'ask' };
|
|
277
|
+
}
|
|
278
|
+
function createdConfigKeys(agentPlan) {
|
|
279
|
+
const keys = agentPlan.installsAgents ? ['$schema', 'instructions', 'default_agent', 'agent', 'mcp'] : ['$schema', 'mcp'];
|
|
280
|
+
return [...keys, 'permission'];
|
|
281
|
+
}
|
|
265
282
|
function manualTestForScope(scope, databasePath, source) {
|
|
266
283
|
if (scope === 'project') {
|
|
267
284
|
return {
|
|
@@ -28,7 +28,7 @@ export async function installOpenCodeMcpClient(input) {
|
|
|
28
28
|
...(input.overwriteVgxness !== undefined ? { overwriteVgxness: input.overwriteVgxness } : {}),
|
|
29
29
|
});
|
|
30
30
|
if (!input.confirmed) {
|
|
31
|
-
return refusal('confirmation_required', confirmationRequiredMessage(plan.scope), input.databasePath, databasePathSource, server, confirmationRequiredSafety(plan), 'targetPath' in plan ? plan.targetPath : undefined, plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.overwriteVgxness);
|
|
31
|
+
return refusal('confirmation_required', confirmationRequiredMessage(plan.scope), input.databasePath, databasePathSource, server, confirmationRequiredSafety(plan), 'targetPath' in plan ? plan.targetPath : undefined, plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.overwriteVgxness, plan.bashPermissionPolicy);
|
|
32
32
|
}
|
|
33
33
|
if (plan.status === 'refused')
|
|
34
34
|
return refusalFromPlan(plan, input.databasePath, databasePathSource, server);
|
|
@@ -36,32 +36,32 @@ export async function installOpenCodeMcpClient(input) {
|
|
|
36
36
|
if (existsSync(plan.targetPath)) {
|
|
37
37
|
const reparsed = parseConfig(plan.targetPath);
|
|
38
38
|
if (!reparsed.ok)
|
|
39
|
-
return refusal(reparsed.reason, reparsed.message, input.databasePath, databasePathSource, server, applySafety(plan), plan.targetPath, plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.overwriteVgxness);
|
|
39
|
+
return refusal(reparsed.reason, reparsed.message, input.databasePath, databasePathSource, server, applySafety(plan), plan.targetPath, plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.overwriteVgxness, plan.bashPermissionPolicy);
|
|
40
40
|
const conflictingAgents = findConflictingVgxnessAgents(reparsed.value, agentPlan, {
|
|
41
41
|
effectiveManagerInstructions: input.effectiveManagerInstructions,
|
|
42
42
|
});
|
|
43
43
|
if (conflictingAgents.length > 0 && input.overwriteVgxness !== true)
|
|
44
|
-
return agentConflictRefusal(conflictingAgents, input.databasePath, databasePathSource, server, applySafety(plan), plan.targetPath, plan.verificationHints, plan.warnings, plan.manualTest, agentPlan);
|
|
45
|
-
return refusal('unsupported_config_shape', 'OpenCode config appeared after planning; rerun setup apply so it can be merged safely without overwriting user config.', input.databasePath, databasePathSource, server, applySafety(plan), plan.targetPath, plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.overwriteVgxness);
|
|
44
|
+
return agentConflictRefusal(conflictingAgents, input.databasePath, databasePathSource, server, applySafety(plan), plan.targetPath, plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.bashPermissionPolicy);
|
|
45
|
+
return refusal('unsupported_config_shape', 'OpenCode config appeared after planning; rerun setup apply so it can be merged safely without overwriting user config.', input.databasePath, databasePathSource, server, applySafety(plan), plan.targetPath, plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.overwriteVgxness, plan.bashPermissionPolicy);
|
|
46
46
|
}
|
|
47
47
|
writeConfig(plan.targetPath, mergeVgxnessOpenCodeConfig({ $schema: opencodeConfigSchema }, plan.server, plan.installsAgents, input.effectiveManagerInstructions));
|
|
48
|
-
return validateInstalledResult(plan.targetPath, undefined, server, input.databasePath, databasePathSource, applySafety(plan), plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.overwriteVgxness);
|
|
48
|
+
return validateInstalledResult(plan.targetPath, undefined, server, input.databasePath, databasePathSource, applySafety(plan), plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.overwriteVgxness, plan.bashPermissionPolicy);
|
|
49
49
|
}
|
|
50
50
|
const parsed = parseConfig(plan.targetPath);
|
|
51
51
|
if (!parsed.ok)
|
|
52
|
-
return refusal(parsed.reason, parsed.message, input.databasePath, databasePathSource, server, applySafety(plan), plan.targetPath, plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.overwriteVgxness);
|
|
52
|
+
return refusal(parsed.reason, parsed.message, input.databasePath, databasePathSource, server, applySafety(plan), plan.targetPath, plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.overwriteVgxness, plan.bashPermissionPolicy);
|
|
53
53
|
const conflictingAgents = findConflictingVgxnessAgents(parsed.value, agentPlan, {
|
|
54
54
|
effectiveManagerInstructions: input.effectiveManagerInstructions,
|
|
55
55
|
});
|
|
56
56
|
if (conflictingAgents.length > 0 && input.overwriteVgxness !== true)
|
|
57
|
-
return agentConflictRefusal(conflictingAgents, input.databasePath, databasePathSource, server, applySafety(plan), plan.targetPath, plan.verificationHints, plan.warnings, plan.manualTest, agentPlan);
|
|
57
|
+
return agentConflictRefusal(conflictingAgents, input.databasePath, databasePathSource, server, applySafety(plan), plan.targetPath, plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.bashPermissionPolicy);
|
|
58
58
|
const backup = createBackup(plan.targetPath, plan.scope);
|
|
59
59
|
if (!backup.ok)
|
|
60
|
-
return refusal('post_write_validation_failed', backup.error.message, input.databasePath, databasePathSource, server, applySafety(plan), plan.targetPath, plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.overwriteVgxness);
|
|
60
|
+
return refusal('post_write_validation_failed', backup.error.message, input.databasePath, databasePathSource, server, applySafety(plan), plan.targetPath, plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.overwriteVgxness, plan.bashPermissionPolicy);
|
|
61
61
|
const config = parsed.value;
|
|
62
62
|
const mergedConfig = mergeVgxnessOpenCodeConfig({ ...config, $schema: plan.existingSchema ?? opencodeConfigSchema }, plan.server, plan.installsAgents, input.effectiveManagerInstructions);
|
|
63
63
|
writeConfig(plan.targetPath, mergedConfig);
|
|
64
|
-
return validateInstalledResult(plan.targetPath, backup.value, server, input.databasePath, databasePathSource, applySafety(plan), plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.overwriteVgxness);
|
|
64
|
+
return validateInstalledResult(plan.targetPath, backup.value, server, input.databasePath, databasePathSource, applySafety(plan), plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.overwriteVgxness, plan.bashPermissionPolicy);
|
|
65
65
|
}
|
|
66
66
|
function mergeVgxnessOpenCodeConfig(config, server, installsAgents, effectiveManagerInstructions) {
|
|
67
67
|
const merged = {
|
|
@@ -75,6 +75,7 @@ function mergeVgxnessOpenCodeConfig(config, server, installsAgents, effectiveMan
|
|
|
75
75
|
merged.default_agent = defaults.defaultAgent;
|
|
76
76
|
merged.agent = { ...(isRecord(config.agent) ? config.agent : {}), ...defaults.agents };
|
|
77
77
|
}
|
|
78
|
+
merged.permission = { ...(isRecord(config.permission) ? config.permission : {}), bash: 'ask' };
|
|
78
79
|
return merged;
|
|
79
80
|
}
|
|
80
81
|
function mergeOpenCodeInstructions(existing) {
|
|
@@ -106,21 +107,25 @@ function createBackup(path, scope) {
|
|
|
106
107
|
description: 'Backup existing OpenCode config before merging VGXNESS MCP configuration.',
|
|
107
108
|
});
|
|
108
109
|
}
|
|
109
|
-
function validateInstalledResult(targetPath, backup, server, databasePath, source, safety, verificationHints, warningMessages, manualTestGuidance, agentPlan, overwriteVgxness = false) {
|
|
110
|
+
function validateInstalledResult(targetPath, backup, server, databasePath, source, safety, verificationHints, warningMessages, manualTestGuidance, agentPlan, overwriteVgxness = false, bashPermissionPolicy = bashPermissionPolicyFor(agentPlan)) {
|
|
110
111
|
const parsed = parseConfig(targetPath);
|
|
111
112
|
if (!parsed.ok)
|
|
112
|
-
return refusal('post_write_validation_failed', 'OpenCode config could not be re-read after write.', databasePath, source, server, safety, targetPath, verificationHints, warningMessages, manualTestGuidance, agentPlan);
|
|
113
|
+
return refusal('post_write_validation_failed', 'OpenCode config could not be re-read after write.', databasePath, source, server, safety, targetPath, verificationHints, warningMessages, manualTestGuidance, agentPlan, overwriteVgxness, bashPermissionPolicy);
|
|
113
114
|
const mcp = parsed.value.mcp;
|
|
114
115
|
const installed = isRecord(mcp) ? mcp.vgxness : undefined;
|
|
115
116
|
if (!isOpenCodeLocalMcpServerConfig(installed)) {
|
|
116
|
-
return refusal('post_write_validation_failed', 'OpenCode config was written but mcp.vgxness did not validate.', databasePath, source, server, safety, targetPath, verificationHints, warningMessages, manualTestGuidance, agentPlan);
|
|
117
|
+
return refusal('post_write_validation_failed', 'OpenCode config was written but mcp.vgxness did not validate.', databasePath, source, server, safety, targetPath, verificationHints, warningMessages, manualTestGuidance, agentPlan, overwriteVgxness, bashPermissionPolicy);
|
|
117
118
|
}
|
|
118
119
|
if (agentPlan.installsAgents) {
|
|
119
120
|
const agent = parsed.value.agent;
|
|
120
121
|
if (!isRecord(agent) || parsed.value.default_agent !== agentPlan.defaultAgent || agentPlan.agentNames.some((name) => !isRecord(agent[name]))) {
|
|
121
|
-
return refusal('post_write_validation_failed', 'OpenCode config was written but VGXNESS agent entries did not validate.', databasePath, source, server, safety, targetPath, verificationHints, warningMessages, manualTestGuidance, agentPlan);
|
|
122
|
+
return refusal('post_write_validation_failed', 'OpenCode config was written but VGXNESS agent entries did not validate.', databasePath, source, server, safety, targetPath, verificationHints, warningMessages, manualTestGuidance, agentPlan, overwriteVgxness, bashPermissionPolicy);
|
|
122
123
|
}
|
|
123
124
|
}
|
|
125
|
+
const permission = parsed.value.permission;
|
|
126
|
+
if (!isRecord(permission) || permission.bash !== 'ask') {
|
|
127
|
+
return refusal('post_write_validation_failed', 'OpenCode config was written but top-level permission.bash did not validate as ask.', databasePath, source, server, safety, targetPath, verificationHints, warningMessages, manualTestGuidance, agentPlan, overwriteVgxness, bashPermissionPolicy);
|
|
128
|
+
}
|
|
124
129
|
return {
|
|
125
130
|
version: 1,
|
|
126
131
|
kind: 'mcp-client-install-opencode',
|
|
@@ -135,6 +140,7 @@ function validateInstalledResult(targetPath, backup, server, databasePath, sourc
|
|
|
135
140
|
installsAgents: agentPlan.installsAgents,
|
|
136
141
|
agentNames: agentPlan.agentNames,
|
|
137
142
|
overwriteVgxness,
|
|
143
|
+
bashPermissionPolicy,
|
|
138
144
|
...(agentPlan.defaultAgent !== undefined ? { defaultAgent: agentPlan.defaultAgent } : {}),
|
|
139
145
|
};
|
|
140
146
|
}
|
|
@@ -159,7 +165,7 @@ function isOpenCodeLocalMcpServerConfig(value) {
|
|
|
159
165
|
function isRecord(value) {
|
|
160
166
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
161
167
|
}
|
|
162
|
-
function refusal(reason, message, databasePath, source, server, safety, targetPath, verificationHints = defaultVerificationHints(databasePath, source), warningMessages = warnings(), manualTestGuidance = manualTest(databasePath, source), agentPlan = createOpenCodeDefaultAgentInstallPlan({ mcpOnly: true }), overwriteVgxness = false) {
|
|
168
|
+
function refusal(reason, message, databasePath, source, server, safety, targetPath, verificationHints = defaultVerificationHints(databasePath, source), warningMessages = warnings(), manualTestGuidance = manualTest(databasePath, source), agentPlan = createOpenCodeDefaultAgentInstallPlan({ mcpOnly: true }), overwriteVgxness = false, bashPermissionPolicy = bashPermissionPolicyFor(agentPlan)) {
|
|
163
169
|
return {
|
|
164
170
|
version: 1,
|
|
165
171
|
kind: 'mcp-client-install-opencode',
|
|
@@ -175,14 +181,15 @@ function refusal(reason, message, databasePath, source, server, safety, targetPa
|
|
|
175
181
|
installsAgents: agentPlan.installsAgents,
|
|
176
182
|
agentNames: agentPlan.agentNames,
|
|
177
183
|
overwriteVgxness,
|
|
184
|
+
bashPermissionPolicy,
|
|
178
185
|
...(agentPlan.defaultAgent !== undefined ? { defaultAgent: agentPlan.defaultAgent } : {}),
|
|
179
186
|
};
|
|
180
187
|
}
|
|
181
188
|
function refusalFromPlan(plan, databasePath, source, server) {
|
|
182
|
-
return refusal(plan.reason, plan.message, databasePath, source, server, plan.safety, plan.targetPath, plan.verificationHints, plan.warnings, plan.manualTest, plan, plan.overwriteVgxness);
|
|
189
|
+
return refusal(plan.reason, plan.message, databasePath, source, server, plan.safety, plan.targetPath, plan.verificationHints, plan.warnings, plan.manualTest, plan, plan.overwriteVgxness, plan.bashPermissionPolicy);
|
|
183
190
|
}
|
|
184
|
-
function agentConflictRefusal(conflictingAgents, databasePath, source, server, safety, targetPath, verificationHints, warningMessages, manualTestGuidance, agentPlan) {
|
|
185
|
-
return refusal('existing_vgxness_agent', `Existing OpenCode config contains custom VGXNESS agent entries that would be overwritten: ${conflictingAgents.join(', ')}. Remove, rename, or manually reconcile them before installing.`, databasePath, source, server, safety, targetPath, verificationHints, warningMessages, manualTestGuidance, agentPlan);
|
|
191
|
+
function agentConflictRefusal(conflictingAgents, databasePath, source, server, safety, targetPath, verificationHints, warningMessages, manualTestGuidance, agentPlan, bashPermissionPolicy = bashPermissionPolicyFor(agentPlan)) {
|
|
192
|
+
return refusal('existing_vgxness_agent', `Existing OpenCode config contains custom VGXNESS agent entries that would be overwritten: ${conflictingAgents.join(', ')}. Remove, rename, or manually reconcile them before installing.`, databasePath, source, server, safety, targetPath, verificationHints, warningMessages, manualTestGuidance, agentPlan, false, bashPermissionPolicy);
|
|
186
193
|
}
|
|
187
194
|
function applySafety(plan) {
|
|
188
195
|
return {
|
|
@@ -206,7 +213,7 @@ function confirmationRequiredMessage(scope) {
|
|
|
206
213
|
return `\`mcp install opencode\` requires explicit --yes before any ${scope} config write.`;
|
|
207
214
|
}
|
|
208
215
|
function warnings() {
|
|
209
|
-
return ['Restart OpenCode after installation so it reloads the project MCP config.'];
|
|
216
|
+
return ['Restart OpenCode after installation so it reloads the project MCP config.', 'OpenCode top-level permission.bash is set to ask; the VGXNESS manager agent allows bash while SDD subagents keep explicit permissions.'];
|
|
210
217
|
}
|
|
211
218
|
function manualTest(databasePath, source) {
|
|
212
219
|
return {
|
|
@@ -222,3 +229,6 @@ function defaultVerificationHints(databasePath, source) {
|
|
|
222
229
|
{ kind: 'command', message: 'Run the MCP doctor command after installation.', command: createVgxnessMcpDoctorCommand(databasePath, source) },
|
|
223
230
|
];
|
|
224
231
|
}
|
|
232
|
+
function bashPermissionPolicyFor(agentPlan) {
|
|
233
|
+
return agentPlan.installsAgents ? { topLevel: 'ask', manager: 'allow' } : { topLevel: 'ask' };
|
|
234
|
+
}
|
|
@@ -9,6 +9,7 @@ import { prepareMemoryDatabasePath, resolveMemoryDatabasePath } from '../memory/
|
|
|
9
9
|
import { OpenCodeManagerPayloadService } from '../providers/opencode/manager-payload.js';
|
|
10
10
|
import { RunService } from '../runs/run-service.js';
|
|
11
11
|
import { sddContinuationPlanFrom } from '../sdd/sdd-continuation-plan.js';
|
|
12
|
+
import { buildSddCockpitSurfaceResponse } from '../sdd/cockpit-read-model.js';
|
|
12
13
|
import { SddWorkflowService } from '../sdd/sdd-workflow-service.js';
|
|
13
14
|
import { SkillRegistryService } from '../skills/skill-registry-service.js';
|
|
14
15
|
import { VerificationPlanService } from '../verification/index.js';
|
|
@@ -43,7 +44,7 @@ export function callVgxTool(call, services) {
|
|
|
43
44
|
case 'vgxness_sdd_next':
|
|
44
45
|
return toEnvelope(validated.tool, services.sdd.getNext(validated.input));
|
|
45
46
|
case 'vgxness_sdd_cockpit':
|
|
46
|
-
return toEnvelope(validated.tool, services.sdd.getCockpit(validated.input));
|
|
47
|
+
return toEnvelope(validated.tool, mapMemoryResult(services.sdd.getCockpit(validated.input), buildSddCockpitSurfaceResponse));
|
|
47
48
|
case 'vgxness_sdd_continue':
|
|
48
49
|
return sddContinueEnvelope(validated.input, services);
|
|
49
50
|
case 'vgxness_governance_report':
|
|
@@ -405,6 +406,9 @@ function runResumeCandidatesEnvelope(input, services) {
|
|
|
405
406
|
function toEnvelope(tool, result) {
|
|
406
407
|
return result.ok ? successEnvelope(tool, result.value) : errorEnvelope(result.error.code, result.error.message, tool);
|
|
407
408
|
}
|
|
409
|
+
function mapMemoryResult(result, project) {
|
|
410
|
+
return result.ok ? { ok: true, value: project(result.value) } : result;
|
|
411
|
+
}
|
|
408
412
|
function resolveControlPlaneDatabasePath(options) {
|
|
409
413
|
if (options.databasePath !== undefined)
|
|
410
414
|
return { ok: true, value: options.databasePath };
|
package/dist/mcp/stdio-server.js
CHANGED
|
@@ -66,7 +66,7 @@ function descriptionForTool(publicToolName) {
|
|
|
66
66
|
if (publicToolName === 'context_cockpit')
|
|
67
67
|
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
68
|
if (publicToolName === 'sdd_cockpit')
|
|
69
|
-
return 'Read-only SDD cockpit summary with
|
|
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.';
|
|
70
70
|
if (publicToolName === 'sdd_continue')
|
|
71
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.';
|
|
72
72
|
const toolName = toInternalVgxMcpToolName(publicToolName);
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { sddPhases, sddPrerequisites } from './schema.js';
|
|
2
|
+
export class SddCockpitReadModelService {
|
|
3
|
+
workflow;
|
|
4
|
+
constructor(workflow) {
|
|
5
|
+
this.workflow = workflow;
|
|
6
|
+
}
|
|
7
|
+
getReadModel(input) {
|
|
8
|
+
const cockpit = this.workflow.getCockpit(input);
|
|
9
|
+
if (!cockpit.ok)
|
|
10
|
+
return cockpit;
|
|
11
|
+
return { ok: true, value: buildSddCockpitReadModel(cockpit.value) };
|
|
12
|
+
}
|
|
13
|
+
getSurfaceResponse(input) {
|
|
14
|
+
const cockpit = this.workflow.getCockpit(input);
|
|
15
|
+
if (!cockpit.ok)
|
|
16
|
+
return cockpit;
|
|
17
|
+
return { ok: true, value: buildSddCockpitSurfaceResponse(cockpit.value) };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function buildSddCockpitSurfaceResponse(cockpit) {
|
|
21
|
+
return { ...cockpit, readModel: buildSddCockpitReadModel(cockpit) };
|
|
22
|
+
}
|
|
23
|
+
export function buildSddCockpitReadModel(cockpit) {
|
|
24
|
+
const phases = cockpit.phases.map((phase) => toPhaseReadModel(phase));
|
|
25
|
+
const blockers = toReadBlockers(cockpit);
|
|
26
|
+
const nextAction = chooseNextAction(cockpit, phases);
|
|
27
|
+
return {
|
|
28
|
+
project: cockpit.project,
|
|
29
|
+
change: cockpit.change,
|
|
30
|
+
phases,
|
|
31
|
+
nextAction,
|
|
32
|
+
blockers,
|
|
33
|
+
guidance: guidanceFor(cockpit, nextAction, blockers),
|
|
34
|
+
summary: summarize(phases),
|
|
35
|
+
contentIncluded: false,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function toPhaseReadModel(phase) {
|
|
39
|
+
const status = artifactReadStatus(phase);
|
|
40
|
+
const contentAvailable = phase.present && phase.artifact !== undefined;
|
|
41
|
+
const reasons = phase.blockers.map((blocker) => blocker.action ?? blocker.reason);
|
|
42
|
+
const canAccept = phase.present && !phase.accepted && status === 'draft';
|
|
43
|
+
const canReview = contentAvailable && status === 'draft';
|
|
44
|
+
return {
|
|
45
|
+
phase: phase.phase,
|
|
46
|
+
label: phaseLabel(phase.phase),
|
|
47
|
+
topicKey: phase.topicKey,
|
|
48
|
+
artifact: {
|
|
49
|
+
exists: phase.present,
|
|
50
|
+
status,
|
|
51
|
+
contentAvailable,
|
|
52
|
+
topicKey: phase.topicKey,
|
|
53
|
+
...(phase.artifact?.artifactId === undefined ? {} : { artifactId: phase.artifact.artifactId }),
|
|
54
|
+
...(phase.artifact?.createdAt === undefined ? {} : { createdAt: phase.artifact.createdAt }),
|
|
55
|
+
...(phase.artifact?.updatedAt === undefined ? {} : { updatedAt: phase.artifact.updatedAt }),
|
|
56
|
+
},
|
|
57
|
+
acceptance: {
|
|
58
|
+
accepted: phase.accepted,
|
|
59
|
+
acceptedByHuman: phase.artifact?.acceptance?.actor.type === 'human',
|
|
60
|
+
...(phase.artifact?.acceptance === undefined ? {} : { acceptance: phase.artifact.acceptance }),
|
|
61
|
+
requiredForNextPhase: isRequiredForLaterPhase(phase.phase),
|
|
62
|
+
},
|
|
63
|
+
actionability: {
|
|
64
|
+
canGenerate: !phase.present && phase.readiness.ready,
|
|
65
|
+
canReview,
|
|
66
|
+
canAccept,
|
|
67
|
+
canReopen: phase.present && status === 'rejected',
|
|
68
|
+
blocked: !phase.readiness.ready || phase.blockers.length > 0,
|
|
69
|
+
reasons,
|
|
70
|
+
},
|
|
71
|
+
guidance: guidanceForPhase(phase, status, canAccept),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function artifactReadStatus(phase) {
|
|
75
|
+
if (!phase.present)
|
|
76
|
+
return 'missing';
|
|
77
|
+
if (phase.legacy)
|
|
78
|
+
return 'legacy';
|
|
79
|
+
const state = phase.state;
|
|
80
|
+
if (state === 'draft' || state === 'accepted' || state === 'rejected' || state === 'superseded' || state === 'missing')
|
|
81
|
+
return state;
|
|
82
|
+
return 'unknown';
|
|
83
|
+
}
|
|
84
|
+
function chooseNextAction(cockpit, phases) {
|
|
85
|
+
const reviewable = phases.find((phase) => phase.actionability.canReview);
|
|
86
|
+
if (reviewable !== undefined) {
|
|
87
|
+
return {
|
|
88
|
+
kind: 'review-artifact',
|
|
89
|
+
phase: reviewable.phase,
|
|
90
|
+
label: `Review ${reviewable.phase} artifact and accept it only after explicit human approval.`,
|
|
91
|
+
requiresConfirmation: false,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const generatable = phases.find((phase) => phase.actionability.canGenerate);
|
|
95
|
+
if (generatable !== undefined) {
|
|
96
|
+
return {
|
|
97
|
+
kind: 'generate-artifact',
|
|
98
|
+
phase: generatable.phase,
|
|
99
|
+
label: `Generate or create the ${generatable.phase} SDD artifact.`,
|
|
100
|
+
requiresConfirmation: false,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (cockpit.next.status === 'complete') {
|
|
104
|
+
return { kind: 'none', label: 'All SDD phases are human-accepted.', requiresConfirmation: false };
|
|
105
|
+
}
|
|
106
|
+
if (cockpit.next.nextPhase !== undefined) {
|
|
107
|
+
const nextPhase = phases.find((phase) => phase.phase === cockpit.next.nextPhase);
|
|
108
|
+
return {
|
|
109
|
+
kind: 'continue-workflow',
|
|
110
|
+
phase: cockpit.next.nextPhase,
|
|
111
|
+
label: safeRecommendedAction(cockpit.recommendedAction, nextPhase),
|
|
112
|
+
requiresConfirmation: false,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return { kind: 'none', label: cockpit.recommendedAction, requiresConfirmation: false };
|
|
116
|
+
}
|
|
117
|
+
function safeRecommendedAction(recommendedAction, phase) {
|
|
118
|
+
if (phase === undefined)
|
|
119
|
+
return recommendedAction;
|
|
120
|
+
if (!/accept/i.test(recommendedAction))
|
|
121
|
+
return recommendedAction;
|
|
122
|
+
if (phase.artifact.status === 'draft')
|
|
123
|
+
return recommendedAction;
|
|
124
|
+
if (phase.artifact.status === 'rejected')
|
|
125
|
+
return `Reopen the ${phase.phase} artifact before further review.`;
|
|
126
|
+
if (phase.artifact.status === 'superseded')
|
|
127
|
+
return `Generate or inspect the current canonical ${phase.phase} artifact; superseded metadata is not actionable.`;
|
|
128
|
+
if (phase.artifact.status === 'legacy')
|
|
129
|
+
return `Inspect legacy ${phase.phase} metadata before using it in the current SDD flow.`;
|
|
130
|
+
return recommendedAction;
|
|
131
|
+
}
|
|
132
|
+
function toReadBlockers(cockpit) {
|
|
133
|
+
return cockpit.aggregateBlockers.map((blocker) => ({
|
|
134
|
+
phase: blocker.phase,
|
|
135
|
+
topicKey: blocker.topicKey,
|
|
136
|
+
code: blocker.kind,
|
|
137
|
+
message: blocker.reason,
|
|
138
|
+
severity: blocker.kind === 'missing-topic-key' ? 'info' : 'blocking',
|
|
139
|
+
...(blocker.action === undefined ? {} : { action: blocker.action }),
|
|
140
|
+
...(blocker.artifactId === undefined ? {} : { artifactId: blocker.artifactId }),
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
function summarize(phases) {
|
|
144
|
+
return {
|
|
145
|
+
totalPhases: phases.length,
|
|
146
|
+
presentArtifacts: phases.filter((phase) => phase.artifact.exists).length,
|
|
147
|
+
acceptedArtifacts: phases.filter((phase) => phase.acceptance.accepted).length,
|
|
148
|
+
missingArtifacts: phases.filter((phase) => phase.artifact.status === 'missing').length,
|
|
149
|
+
draftArtifacts: phases.filter((phase) => phase.artifact.status === 'draft').length,
|
|
150
|
+
rejectedArtifacts: phases.filter((phase) => phase.artifact.status === 'rejected').length,
|
|
151
|
+
supersededArtifacts: phases.filter((phase) => phase.artifact.status === 'superseded').length,
|
|
152
|
+
legacyArtifacts: phases.filter((phase) => phase.artifact.status === 'legacy').length,
|
|
153
|
+
isComplete: phases.length === sddPhases.length && phases.every((phase) => phase.acceptance.accepted),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function guidanceFor(cockpit, nextAction, blockers) {
|
|
157
|
+
const phaseStatusByName = new Map(cockpit.phases.map((phase) => [phase.phase, artifactReadStatus(phase)]));
|
|
158
|
+
const guidance = [nextAction.label];
|
|
159
|
+
if (cockpit.phases.some((phase) => artifactReadStatus(phase) === 'draft' && !phase.accepted)) {
|
|
160
|
+
guidance.push('Artifact presence alone does not count as acceptance; explicit human acceptance is required for drafts.');
|
|
161
|
+
}
|
|
162
|
+
if (cockpit.next.blockerGuidance !== undefined) {
|
|
163
|
+
guidance.push(...cockpit.next.blockerGuidance
|
|
164
|
+
.filter((blocker) => phaseStatusByName.get(blocker.phase) === 'draft' || !/accept/i.test(blocker.action))
|
|
165
|
+
.map((blocker) => blocker.action));
|
|
166
|
+
}
|
|
167
|
+
return [...new Set(guidance)];
|
|
168
|
+
}
|
|
169
|
+
function guidanceForPhase(phase, status, canAccept) {
|
|
170
|
+
if (status === 'missing')
|
|
171
|
+
return [`Create the ${phase.phase} artifact when prerequisites are ready.`];
|
|
172
|
+
if (canAccept)
|
|
173
|
+
return [`Review ${phase.topicKey}; accept only after explicit human approval.`];
|
|
174
|
+
if (status === 'accepted')
|
|
175
|
+
return ['Accepted artifact metadata is available; full content is omitted from this read model.'];
|
|
176
|
+
if (status === 'rejected')
|
|
177
|
+
return ['Rejected artifact metadata is available; reopen it before further review.'];
|
|
178
|
+
if (status === 'superseded')
|
|
179
|
+
return ['Superseded artifact metadata is available; generate or inspect the current canonical artifact instead.'];
|
|
180
|
+
if (status === 'legacy')
|
|
181
|
+
return ['Legacy artifact metadata requires inspection before it can be used for current SDD flow.'];
|
|
182
|
+
return phase.blockers.map((blocker) => blocker.action ?? blocker.reason);
|
|
183
|
+
}
|
|
184
|
+
function phaseLabel(phase) {
|
|
185
|
+
return phase
|
|
186
|
+
.split('-')
|
|
187
|
+
.map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`)
|
|
188
|
+
.join(' ');
|
|
189
|
+
}
|
|
190
|
+
function isRequiredForLaterPhase(phase) {
|
|
191
|
+
return sddPhases.some((candidate) => sddPrerequisites[candidate].includes(phase));
|
|
192
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -61,8 +61,8 @@ export const openCodeSetupAdapter = {
|
|
|
61
61
|
writesProviderConfig: false,
|
|
62
62
|
summary: contract.status === 'would_install'
|
|
63
63
|
? contract.overwriteVgxness
|
|
64
|
-
? `OpenCode ${contract.action} reinstall plan is available; confirmed apply overwrites VGXNESS entries only, preserves unrelated config, and creates managed backups for existing targets.`
|
|
65
|
-
: `OpenCode ${contract.action} plan is available for external application; confirmed
|
|
64
|
+
? `OpenCode ${contract.action} reinstall plan is available; confirmed apply overwrites VGXNESS entries only, preserves unrelated config, sets top-level permission.bash=ask and manager bash=allow, and creates managed backups for existing targets.`
|
|
65
|
+
: `OpenCode ${contract.action} plan is available for external application; confirmed applies mcp.vgxness, top-level permission.bash=ask, and manager bash=allow, and creates managed VGXNESS backups when merging.`
|
|
66
66
|
: contract.message,
|
|
67
67
|
...(targetPath !== undefined ? { targetPath } : {}),
|
|
68
68
|
warnings: contract.warnings,
|
|
@@ -122,7 +122,7 @@ function openCodePlanPreview(visibility, plan) {
|
|
|
122
122
|
targetPath: source.targetPath,
|
|
123
123
|
backupRequired: source.backupRequired,
|
|
124
124
|
confirmationRequired: Boolean(plan?.actions.some((candidate) => candidate.safety.requiresExplicitConfirmation)),
|
|
125
|
-
risks: source.overwriteVgxness ? overwriteInstallRisks(source.installsAgents) : source.action === 'create' ? createInstallRisks() : mergeInstallRisks(),
|
|
125
|
+
risks: [...(source.overwriteVgxness ? overwriteInstallRisks(source.installsAgents) : source.action === 'create' ? createInstallRisks() : mergeInstallRisks()), ...bashPermissionRisks(source.bashPermissionPolicy)],
|
|
126
126
|
warnings: source.warnings,
|
|
127
127
|
};
|
|
128
128
|
}
|
|
@@ -154,6 +154,11 @@ function overwriteInstallRisks(installsAgents) {
|
|
|
154
154
|
'OpenCode must be restarted after external installation before it discovers the updated vgxness MCP server.',
|
|
155
155
|
];
|
|
156
156
|
}
|
|
157
|
+
function bashPermissionRisks(policy) {
|
|
158
|
+
return policy.manager === 'allow'
|
|
159
|
+
? ['OpenCode top-level permission.bash is set to ask; terminal commands prompt by default, while vgxness-manager allows bash after restart.']
|
|
160
|
+
: ['OpenCode top-level permission.bash is set to ask.'];
|
|
161
|
+
}
|
|
157
162
|
function refusedRepairRisks(message) {
|
|
158
163
|
return [
|
|
159
164
|
`Install planner refused automatic repair: ${message}`,
|
|
@@ -176,8 +181,8 @@ function externalInstallAction(scope, targetPath, overwriteVgxness = false) {
|
|
|
176
181
|
kind: 'copy-command',
|
|
177
182
|
command: ['vgxness', 'mcp', 'install', 'opencode', '--scope', scope, '--yes', ...(overwriteVgxness ? ['--overwrite-vgxness'] : [])],
|
|
178
183
|
description: overwriteVgxness
|
|
179
|
-
? 'Copy and run this outside the TUI only after reviewing that VGXNESS entries will be overwritten
|
|
180
|
-
: 'Copy and run this outside the TUI only after reviewing the read-only plan.',
|
|
184
|
+
? 'Copy and run this outside the TUI only after reviewing that VGXNESS entries will be overwritten, unrelated config preserved, top-level bash prompts, and manager bash is allowed.'
|
|
185
|
+
: 'Copy and run this outside the TUI only after reviewing the read-only plan and scoped bash permission behavior.',
|
|
181
186
|
safety: externalProviderWriteSafety(targetPath),
|
|
182
187
|
};
|
|
183
188
|
}
|
package/dist/setup/setup-plan.js
CHANGED
|
@@ -131,6 +131,7 @@ function setupPlanFromOpenCode(input) {
|
|
|
131
131
|
installsAgents: input.opencode.installsAgents,
|
|
132
132
|
agentNames: input.opencode.agentNames,
|
|
133
133
|
...(input.opencode.overwriteVgxness ? { overwriteVgxness: true } : {}),
|
|
134
|
+
bashPermissionPolicy: input.opencode.bashPermissionPolicy,
|
|
134
135
|
},
|
|
135
136
|
actions: [],
|
|
136
137
|
conflicts: [
|
|
@@ -158,11 +159,12 @@ function setupPlanFromOpenCode(input) {
|
|
|
158
159
|
installsAgents: input.opencode.installsAgents,
|
|
159
160
|
agentNames: input.opencode.agentNames,
|
|
160
161
|
...(input.opencode.overwriteVgxness ? { overwriteVgxness: true } : {}),
|
|
162
|
+
bashPermissionPolicy: input.opencode.bashPermissionPolicy,
|
|
161
163
|
},
|
|
162
164
|
actions: [
|
|
163
165
|
{
|
|
164
166
|
id: `opencode-${input.opencode.action}`,
|
|
165
|
-
description: `${input.opencode.overwriteVgxness ? 'Reinstall/overwrite VGXNESS entries in' : input.opencode.action === 'create' ? 'Create' : 'Merge'} OpenCode config with mcp.vgxness using vgxness mcp start${input.installMode === 'mcp-plus-agents' ? ' and manager/SDD agents' : ''}${input.opencode.overwriteVgxness ? '; unrelated OpenCode config is preserved' : ''}.`,
|
|
167
|
+
description: `${input.opencode.overwriteVgxness ? 'Reinstall/overwrite VGXNESS entries in' : input.opencode.action === 'create' ? 'Create' : 'Merge'} OpenCode config with mcp.vgxness using vgxness mcp start${input.installMode === 'mcp-plus-agents' ? ' and manager/SDD agents' : ''}${input.opencode.overwriteVgxness ? '; unrelated OpenCode config is preserved' : ''}${bashPermissionDescription(input.opencode.bashPermissionPolicy)}.`,
|
|
166
168
|
mutating: false,
|
|
167
169
|
targetPath: input.opencode.targetPath,
|
|
168
170
|
backupRequired: input.opencode.backupRequired,
|
|
@@ -182,6 +184,9 @@ function setupPlanFromOpenCode(input) {
|
|
|
182
184
|
nextCommands: ['vgxness setup apply --yes', 'vgxness doctor', 'Restart OpenCode and verify the vgxness MCP server is visible.'],
|
|
183
185
|
};
|
|
184
186
|
}
|
|
187
|
+
function bashPermissionDescription(policy) {
|
|
188
|
+
return policy.manager === 'allow' ? '; sets top-level permission.bash to ask and manager bash to allow' : '; sets top-level permission.bash to ask';
|
|
189
|
+
}
|
|
185
190
|
function resolveSetupDatabase(input) {
|
|
186
191
|
if (input.mode === 'global') {
|
|
187
192
|
const resolved = resolveMemoryDatabasePath({ cwd: input.workspaceRoot, env: input.env });
|
package/docs/cli.md
CHANGED
|
@@ -70,7 +70,9 @@ Daily SDD phase progression is OpenCode-first: talk to OpenCode with the VGXNESS
|
|
|
70
70
|
|
|
71
71
|
`setup plan`, `setup status`, and non-TTY `init` planning do not write provider config. Local VGXNESS store initialization may occur when the selected SQLite store is needed. They are human-readable by default; pass `--json` when you need parseable automation output. With the default global database, the OpenCode MCP command is `vgxness mcp start`; for custom/project-local DBs it includes `--db <path>`. The default OpenCode target is `$HOME/.config/opencode/opencode.json`; pass `--scope project` to opt into `<workspace>/.opencode/opencode.json`. `setup apply --yes` is the explicit OpenCode provider-config write path. `setup rollback --backup <path>` validates a VGXNESS/OpenCode backup such as `opencode.json.backup-<timestamp>`, creates a pre-rollback backup of the current target when present, restores the selected backup byte-for-byte, and recommends `vgxness doctor`.
|
|
72
72
|
|
|
73
|
-
`vgxness init` prompts in English when stdin/stdout are TTYs: project name, DB location, provider, OpenCode scope, install mode, then shows the plan and asks `Apply this setup? Type "yes" to continue:`. Any answer other than `yes` exits successfully without writes. In CI/non-TTY without `--yes`, `init` returns the read-only plan and never waits for input. `vgxness doctor`, `vgxness sdd status`, `vgxness sdd next`, `vgxness sdd get-artifact`, and `vgxness sdd list-artifacts` are also human-readable by default; use `--json` for scripts.
|
|
73
|
+
`vgxness init` prompts in English when stdin/stdout are TTYs: project name, DB location, provider, OpenCode scope, install mode, then shows the plan and asks `Apply this setup? Type "yes" to continue:`. Any answer other than `yes` exits successfully without writes. In CI/non-TTY without `--yes`, `init` returns the read-only plan and never waits for input. `vgxness doctor`, `vgxness sdd status`, `vgxness sdd next`, `vgxness sdd cockpit`, `vgxness sdd get-artifact`, and `vgxness sdd list-artifacts` are also human-readable by default; use `--json` for scripts.
|
|
74
|
+
|
|
75
|
+
`vgxness sdd cockpit --json` returns the same additive compatibility shape as the MCP cockpit: all existing raw `SddCockpit` top-level fields remain present, with an additional top-level `readModel`. The cockpit is read-only and metadata-only: it does not include artifact bodies, infer acceptance from artifact presence, advance phases, execute acceptance, call providers, or write provider config. Text output renders from read-model concepts such as next action, phase metadata, blockers, guidance, and `Content included: no`.
|
|
74
76
|
|
|
75
77
|
## Daily workflow front doors
|
|
76
78
|
|
package/docs/mcp.md
CHANGED
|
@@ -25,7 +25,7 @@ All tools:
|
|
|
25
25
|
| `vgxness_sdd_reopen_artifact` | Move a rejected artifact back to draft for revision. | `project`, `change`, `phase`, `reopenedBy` (`{type:"human", id, displayName?}`); optional `reopenedAt`, `note`, `runId`, `agentId` | Human-only recovery path. Does not imply acceptance or bypass downstream gates. |
|
|
26
26
|
| `vgxness_sdd_get_artifact` | Read one full artifact. | `project`, `change`, `phase`; optional `payloadMode` (`compact`/`verbose`) | |
|
|
27
27
|
| `vgxness_sdd_list_artifacts` | List all artifacts for a change in canonical phase order. | `project`, `change`; optional `payloadMode` | |
|
|
28
|
-
| `vgxness_sdd_cockpit` | Aggregate per-phase status, blockers, and recommended next action. | `project`, `change` | Returns `SddCockpit`
|
|
28
|
+
| `vgxness_sdd_cockpit` | Aggregate per-phase status, blockers, and recommended next action. | `project`, `change` | Returns the raw `SddCockpit` compatibility fields plus additive top-level `readModel`. The response is read-only and metadata-only: no artifact bodies, no phase advancement, no provider/config writes. `readModel.contentIncluded` is always `false`; accepted means explicit human acceptance, not artifact presence. |
|
|
29
29
|
| `vgxness_governance_report` | Build a redacted governance report for a project/change/run. | `project`; optional `change`, `runId`, `workflow`, `phase`, `agentId`, `agent` (with `id`, `name`, `mode`, `scope`), `payloadMode` | See [Safety model](./safety.md) for redaction rules. |
|
|
30
30
|
|
|
31
31
|
## Memory (4)
|
|
@@ -114,7 +114,7 @@ vgxness_sdd_continue { project: "vgxness", change: "new-flow" }
|
|
|
114
114
|
vgxness_sdd_get_readiness { project: "vgxness", change: "new-flow", phase: "proposal" }
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
-
The cockpit
|
|
117
|
+
The cockpit preserves raw `SddCockpit` fields such as `phases`, `artifacts`, `aggregateBlockers`, and `inspectCommand`, and also adds `readModel` for stable UI/MCP consumption. Both raw artifact summaries and `readModel.phases[].artifact` omit artifact bodies. If any raw blocker is `unaccepted-phase`, or any read-model phase has `acceptance.acceptedByHuman: false`, do not treat the prerequisite as accepted merely because an artifact exists. `vgxness_sdd_continue` uses the same SDD state to explain how to continue; it is advisory only.
|
|
118
118
|
|
|
119
119
|
Recording a phase with explicit human acceptance:
|
|
120
120
|
|