vgxness 1.9.3 → 1.9.5
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/README.md +4 -3
- package/dist/agents/canonical-agent-manifest.js +35 -9
- package/dist/agents/canonical-agent-projection.js +15 -0
- package/dist/cli/cli-help.js +1 -1
- package/dist/cli/commands/mcp-dispatcher.js +49 -18
- package/dist/cli/commands/memory-sdd-dispatcher.js +6 -2
- package/dist/cli/commands/setup-dispatcher.js +22 -10
- package/dist/cli/product-status-renderer.js +9 -2
- package/dist/cli/sdd-renderer.js +33 -20
- package/dist/cli/tui/main-menu/main-menu-read-model.js +8 -8
- package/dist/cli/tui/setup/setup-tui-services.js +27 -10
- package/dist/mcp/client-install-opencode-contract.js +45 -31
- package/dist/mcp/client-install-opencode.js +35 -24
- package/dist/mcp/control-plane.js +5 -1
- package/dist/mcp/opencode-default-agent-config.js +7 -4
- 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 +8 -5
- package/dist/setup/setup-plan.js +3 -1
- package/dist/status/product-status.js +5 -1
- package/docs/cli.md +19 -14
- package/docs/mcp.md +2 -2
- package/package.json +1 -1
|
@@ -34,8 +34,9 @@ export function planOpenCodeMcpInstall(input) {
|
|
|
34
34
|
mcpOnly: input.mcpOnly,
|
|
35
35
|
});
|
|
36
36
|
const overwriteVgxness = input.overwriteVgxness === true;
|
|
37
|
+
const allowBash = true;
|
|
37
38
|
if (scope === 'user') {
|
|
38
|
-
return planUserOpenCodeMcpInstall(input, databasePathSource, server, agentPlan, overwriteVgxness);
|
|
39
|
+
return planUserOpenCodeMcpInstall(input, databasePathSource, server, agentPlan, overwriteVgxness, allowBash);
|
|
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, allowBash);
|
|
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, allowBash),
|
|
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, allowBash);
|
|
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, allowBash);
|
|
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, allowBash);
|
|
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, allowBash);
|
|
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, allowBash);
|
|
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, allowBash);
|
|
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, allowBash);
|
|
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 permission.bash to allow by default.', input.databasePath, databasePathSource, scope, target.absolutePath, [], agentPlan, overwriteVgxness, allowBash);
|
|
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, allowBash),
|
|
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, allowBash) {
|
|
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, allowBash);
|
|
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, allowBash);
|
|
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, allowBash),
|
|
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, allowBash);
|
|
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, allowBash);
|
|
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, allowBash);
|
|
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, allowBash);
|
|
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, allowBash);
|
|
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 permission.bash to allow by default.', input.databasePath, databasePathSource, 'user', target.path, [], agentPlan, overwriteVgxness, allowBash);
|
|
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, allowBash),
|
|
154
161
|
status: 'would_install',
|
|
155
162
|
action: 'merge',
|
|
156
163
|
targetPath: target.path,
|
|
@@ -195,10 +202,10 @@ function parseConfig(path) {
|
|
|
195
202
|
function isRecord(value) {
|
|
196
203
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
197
204
|
}
|
|
198
|
-
export function findConflictingVgxnessAgents(config, agentPlan) {
|
|
205
|
+
export function findConflictingVgxnessAgents(config, agentPlan, input = {}) {
|
|
199
206
|
if (!agentPlan.installsAgents || !isRecord(config.agent))
|
|
200
207
|
return [];
|
|
201
|
-
const defaults = createOpenCodeDefaultAgentConfig().agents;
|
|
208
|
+
const defaults = createOpenCodeDefaultAgentConfig(input).agents;
|
|
202
209
|
return agentPlan.agentNames.filter((name) => Object.hasOwn(config.agent, name) && !deepEqual(config.agent[name], defaults[name]));
|
|
203
210
|
}
|
|
204
211
|
function deepEqual(left, right) {
|
|
@@ -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, allowBash = true) {
|
|
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, allowBash),
|
|
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
|
+
allowBash,
|
|
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, allowBash = true) {
|
|
249
|
+
const contract = baseContract(databasePath, source, scope, targetPath, false, 'refuse-no-clobber', agentPlan, overwriteVgxness, allowBash);
|
|
242
250
|
return {
|
|
243
251
|
...contract,
|
|
244
252
|
verificationHints: [...extraVerificationHints, ...contract.verificationHints],
|
|
@@ -248,20 +256,26 @@ 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, allowBash) {
|
|
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 = allowBash ? ['OpenCode permission.bash is set to allow by default; terminal commands may run without prompting.'] : [];
|
|
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 createdConfigKeys(agentPlan) {
|
|
276
|
+
const keys = agentPlan.installsAgents ? ['$schema', 'instructions', 'default_agent', 'agent', 'mcp'] : ['$schema', 'mcp'];
|
|
277
|
+
return [...keys, 'permission'];
|
|
278
|
+
}
|
|
265
279
|
function manualTestForScope(scope, databasePath, source) {
|
|
266
280
|
if (scope === 'project') {
|
|
267
281
|
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.allowBash);
|
|
32
32
|
}
|
|
33
33
|
if (plan.status === 'refused')
|
|
34
34
|
return refusalFromPlan(plan, input.databasePath, databasePathSource, server);
|
|
@@ -36,41 +36,46 @@ 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);
|
|
40
|
-
const conflictingAgents = findConflictingVgxnessAgents(reparsed.value, agentPlan
|
|
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.allowBash);
|
|
40
|
+
const conflictingAgents = findConflictingVgxnessAgents(reparsed.value, agentPlan, {
|
|
41
|
+
effectiveManagerInstructions: input.effectiveManagerInstructions,
|
|
42
|
+
});
|
|
41
43
|
if (conflictingAgents.length > 0 && input.overwriteVgxness !== true)
|
|
42
|
-
return agentConflictRefusal(conflictingAgents, input.databasePath, databasePathSource, server, applySafety(plan), plan.targetPath, plan.verificationHints, plan.warnings, plan.manualTest, agentPlan);
|
|
43
|
-
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.allowBash);
|
|
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.allowBash);
|
|
44
46
|
}
|
|
45
|
-
writeConfig(plan.targetPath, mergeVgxnessOpenCodeConfig({ $schema: opencodeConfigSchema }, plan.server, plan.installsAgents));
|
|
46
|
-
return validateInstalledResult(plan.targetPath, undefined, server, input.databasePath, databasePathSource, applySafety(plan), plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.overwriteVgxness);
|
|
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, plan.allowBash);
|
|
47
49
|
}
|
|
48
50
|
const parsed = parseConfig(plan.targetPath);
|
|
49
51
|
if (!parsed.ok)
|
|
50
|
-
return refusal(parsed.reason, parsed.message, input.databasePath, databasePathSource, server, applySafety(plan), plan.targetPath, plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.overwriteVgxness);
|
|
51
|
-
const conflictingAgents = findConflictingVgxnessAgents(parsed.value, agentPlan
|
|
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.allowBash);
|
|
53
|
+
const conflictingAgents = findConflictingVgxnessAgents(parsed.value, agentPlan, {
|
|
54
|
+
effectiveManagerInstructions: input.effectiveManagerInstructions,
|
|
55
|
+
});
|
|
52
56
|
if (conflictingAgents.length > 0 && input.overwriteVgxness !== true)
|
|
53
|
-
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.allowBash);
|
|
54
58
|
const backup = createBackup(plan.targetPath, plan.scope);
|
|
55
59
|
if (!backup.ok)
|
|
56
|
-
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.allowBash);
|
|
57
61
|
const config = parsed.value;
|
|
58
|
-
const mergedConfig = mergeVgxnessOpenCodeConfig({ ...config, $schema: plan.existingSchema ?? opencodeConfigSchema }, plan.server, plan.installsAgents);
|
|
62
|
+
const mergedConfig = mergeVgxnessOpenCodeConfig({ ...config, $schema: plan.existingSchema ?? opencodeConfigSchema }, plan.server, plan.installsAgents, input.effectiveManagerInstructions);
|
|
59
63
|
writeConfig(plan.targetPath, mergedConfig);
|
|
60
|
-
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.allowBash);
|
|
61
65
|
}
|
|
62
|
-
function mergeVgxnessOpenCodeConfig(config, server, installsAgents) {
|
|
66
|
+
function mergeVgxnessOpenCodeConfig(config, server, installsAgents, effectiveManagerInstructions) {
|
|
63
67
|
const merged = {
|
|
64
68
|
...config,
|
|
65
69
|
$schema: typeof config.$schema === 'string' ? config.$schema : opencodeConfigSchema,
|
|
66
70
|
mcp: { ...(isRecord(config.mcp) ? config.mcp : {}), vgxness: server },
|
|
67
71
|
};
|
|
68
72
|
if (installsAgents) {
|
|
69
|
-
const defaults = createOpenCodeDefaultAgentConfig();
|
|
73
|
+
const defaults = createOpenCodeDefaultAgentConfig({ effectiveManagerInstructions });
|
|
70
74
|
merged.instructions = mergeOpenCodeInstructions(config.instructions);
|
|
71
75
|
merged.default_agent = defaults.defaultAgent;
|
|
72
76
|
merged.agent = { ...(isRecord(config.agent) ? config.agent : {}), ...defaults.agents };
|
|
73
77
|
}
|
|
78
|
+
merged.permission = { ...(isRecord(config.permission) ? config.permission : {}), bash: 'allow' };
|
|
74
79
|
return merged;
|
|
75
80
|
}
|
|
76
81
|
function mergeOpenCodeInstructions(existing) {
|
|
@@ -102,21 +107,25 @@ function createBackup(path, scope) {
|
|
|
102
107
|
description: 'Backup existing OpenCode config before merging VGXNESS MCP configuration.',
|
|
103
108
|
});
|
|
104
109
|
}
|
|
105
|
-
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, allowBash = true) {
|
|
106
111
|
const parsed = parseConfig(targetPath);
|
|
107
112
|
if (!parsed.ok)
|
|
108
|
-
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, allowBash);
|
|
109
114
|
const mcp = parsed.value.mcp;
|
|
110
115
|
const installed = isRecord(mcp) ? mcp.vgxness : undefined;
|
|
111
116
|
if (!isOpenCodeLocalMcpServerConfig(installed)) {
|
|
112
|
-
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, allowBash);
|
|
113
118
|
}
|
|
114
119
|
if (agentPlan.installsAgents) {
|
|
115
120
|
const agent = parsed.value.agent;
|
|
116
121
|
if (!isRecord(agent) || parsed.value.default_agent !== agentPlan.defaultAgent || agentPlan.agentNames.some((name) => !isRecord(agent[name]))) {
|
|
117
|
-
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, allowBash);
|
|
118
123
|
}
|
|
119
124
|
}
|
|
125
|
+
const permission = parsed.value.permission;
|
|
126
|
+
if (!isRecord(permission) || permission.bash !== 'allow') {
|
|
127
|
+
return refusal('post_write_validation_failed', 'OpenCode config was written but permission.bash did not validate as allow.', databasePath, source, server, safety, targetPath, verificationHints, warningMessages, manualTestGuidance, agentPlan, overwriteVgxness, allowBash);
|
|
128
|
+
}
|
|
120
129
|
return {
|
|
121
130
|
version: 1,
|
|
122
131
|
kind: 'mcp-client-install-opencode',
|
|
@@ -131,6 +140,7 @@ function validateInstalledResult(targetPath, backup, server, databasePath, sourc
|
|
|
131
140
|
installsAgents: agentPlan.installsAgents,
|
|
132
141
|
agentNames: agentPlan.agentNames,
|
|
133
142
|
overwriteVgxness,
|
|
143
|
+
allowBash,
|
|
134
144
|
...(agentPlan.defaultAgent !== undefined ? { defaultAgent: agentPlan.defaultAgent } : {}),
|
|
135
145
|
};
|
|
136
146
|
}
|
|
@@ -155,7 +165,7 @@ function isOpenCodeLocalMcpServerConfig(value) {
|
|
|
155
165
|
function isRecord(value) {
|
|
156
166
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
157
167
|
}
|
|
158
|
-
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, allowBash = true) {
|
|
159
169
|
return {
|
|
160
170
|
version: 1,
|
|
161
171
|
kind: 'mcp-client-install-opencode',
|
|
@@ -171,14 +181,15 @@ function refusal(reason, message, databasePath, source, server, safety, targetPa
|
|
|
171
181
|
installsAgents: agentPlan.installsAgents,
|
|
172
182
|
agentNames: agentPlan.agentNames,
|
|
173
183
|
overwriteVgxness,
|
|
184
|
+
allowBash,
|
|
174
185
|
...(agentPlan.defaultAgent !== undefined ? { defaultAgent: agentPlan.defaultAgent } : {}),
|
|
175
186
|
};
|
|
176
187
|
}
|
|
177
188
|
function refusalFromPlan(plan, databasePath, source, server) {
|
|
178
|
-
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.allowBash);
|
|
179
190
|
}
|
|
180
|
-
function agentConflictRefusal(conflictingAgents, databasePath, source, server, safety, targetPath, verificationHints, warningMessages, manualTestGuidance, agentPlan) {
|
|
181
|
-
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, allowBash = true) {
|
|
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, allowBash);
|
|
182
193
|
}
|
|
183
194
|
function applySafety(plan) {
|
|
184
195
|
return {
|
|
@@ -202,7 +213,7 @@ function confirmationRequiredMessage(scope) {
|
|
|
202
213
|
return `\`mcp install opencode\` requires explicit --yes before any ${scope} config write.`;
|
|
203
214
|
}
|
|
204
215
|
function warnings() {
|
|
205
|
-
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 permission.bash is set to allow by default; terminal commands may run without prompting.'];
|
|
206
217
|
}
|
|
207
218
|
function manualTest(databasePath, source) {
|
|
208
219
|
return {
|
|
@@ -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 };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { projectCanonicalAgentManifestToOpenCode, } from '../agents/canonical-agent-projection.js';
|
|
2
|
-
import { canonicalDefaultAgentName, canonicalPromptContractVersion, canonicalSddSubagentNames, createCanonicalOpenCodeSddTaskPermissions, } from '../agents/canonical-agent-manifest.js';
|
|
1
|
+
import { projectCanonicalAgentManifestToOpenCode, withEffectiveOpenCodeManagerInstructions, } from '../agents/canonical-agent-projection.js';
|
|
2
|
+
import { canonicalDefaultAgentName, canonicalPromptContractVersion, canonicalSddSubagentNames, createCanonicalOpenCodeSddMcpToolPermissions, createCanonicalOpenCodeSddTaskPermissions, } from '../agents/canonical-agent-manifest.js';
|
|
3
3
|
export const vgxnessOpenCodeDefaultAgent = canonicalDefaultAgentName;
|
|
4
4
|
export const vgxnessOpenCodePromptContractVersion = canonicalPromptContractVersion;
|
|
5
5
|
export const vgxnessOpenCodeInstructionsPath = 'AGENTS.md';
|
|
@@ -7,11 +7,14 @@ export const vgxnessOpenCodeSddSubagents = canonicalSddSubagentNames;
|
|
|
7
7
|
export function createOpenCodeSddTaskPermissions() {
|
|
8
8
|
return createCanonicalOpenCodeSddTaskPermissions();
|
|
9
9
|
}
|
|
10
|
+
export function createOpenCodeSddMcpToolPermissions() {
|
|
11
|
+
return createCanonicalOpenCodeSddMcpToolPermissions();
|
|
12
|
+
}
|
|
10
13
|
export function createOpenCodeDefaultAgentInstallPlan(input = {}) {
|
|
11
14
|
if (input.mcpOnly === true)
|
|
12
15
|
return { installsAgents: false, agentNames: [] };
|
|
13
16
|
return { installsAgents: true, agentNames: [vgxnessOpenCodeDefaultAgent, ...vgxnessOpenCodeSddSubagents], defaultAgent: vgxnessOpenCodeDefaultAgent };
|
|
14
17
|
}
|
|
15
|
-
export function createOpenCodeDefaultAgentConfig() {
|
|
16
|
-
return projectCanonicalAgentManifestToOpenCode();
|
|
18
|
+
export function createOpenCodeDefaultAgentConfig(input = {}) {
|
|
19
|
+
return withEffectiveOpenCodeManagerInstructions(projectCanonicalAgentManifestToOpenCode(), input.effectiveManagerInstructions);
|
|
17
20
|
}
|
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 {};
|