vgxness 1.9.4 → 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.
@@ -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 permission.bash=allow, vgxness-manager, and hidden vgxness-sdd-* agents; 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 || parsed.flags.json === true)
149
+ if (!cockpit.ok)
149
150
  return jsonResult(cockpit);
150
- return okText(renderSddCockpit(cockpit.value));
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');
@@ -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 blockers = cockpit.aggregateBlockers;
123
+ const readModel = cockpit.readModel;
124
+ const blockers = readModel.blockers;
124
125
  const lines = [
125
126
  'SDD Cockpit (read-only)',
126
- `Project: ${cockpit.project}`,
127
- `Change: ${cockpit.change}`,
128
- `Actionable phase: ${cockpit.actionablePhase ?? 'none'}`,
129
- `Next: ${cockpit.next.status}${cockpit.next.nextPhase === undefined ? '' : ` / ${cockpit.next.nextPhase}`}`,
130
- `Recommended action: ${cockpit.recommendedAction}`,
131
- `Accepted: ${cockpit.acceptedCount}/${cockpit.phases.length}`,
132
- `Legacy artifacts: ${cockpit.legacyCount}`,
133
- '',
134
- 'Aggregate blockers:',
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.kind}: ${blocker.phase} at ${blocker.topicKey} - ${blocker.reason}${blocker.action === undefined ? '' : `; action=${blocker.action}`}`)),
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}`,
@@ -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.installsAgents ? ['$schema', 'instructions', 'default_agent', 'agent', 'mcp'] : ['$schema', 'mcp'],
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.installsAgents ? ['$schema', 'instructions', 'default_agent', 'agent', 'mcp'] : ['$schema', 'mcp'],
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,
@@ -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,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.allowBash);
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.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);
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.allowBash);
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.allowBash);
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.allowBash);
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.allowBash);
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.allowBash);
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: 'allow' };
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, allowBash = true) {
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, allowBash);
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, allowBash);
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, allowBash);
122
123
  }
123
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
+ }
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
+ allowBash,
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, allowBash = true) {
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
+ allowBash,
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.allowBash);
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, 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);
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 permission.bash is set to allow by default; terminal commands may run without prompting.'];
210
217
  }
211
218
  function manualTest(databasePath, source) {
212
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 };
@@ -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 next decision, explicit acceptance state, metadata-only artifact summaries, and aggregate blockers.';
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 merges create managed VGXNESS backups.`
64
+ ? `OpenCode ${contract.action} reinstall plan is available; confirmed apply overwrites VGXNESS entries only, preserves unrelated config, sets permission.bash=allow, and creates managed backups for existing targets.`
65
+ : `OpenCode ${contract.action} plan is available for external application; confirmed applies mcp.vgxness and permission.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.allowBash)],
126
126
  warnings: source.warnings,
127
127
  };
128
128
  }
@@ -154,6 +154,9 @@ 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(allowBash) {
158
+ return allowBash === true ? ['OpenCode permission.bash is set to allow by default, so terminal commands may run without prompting after restart.'] : [];
159
+ }
157
160
  function refusedRepairRisks(message) {
158
161
  return [
159
162
  `Install planner refused automatic repair: ${message}`,
@@ -176,8 +179,8 @@ function externalInstallAction(scope, targetPath, overwriteVgxness = false) {
176
179
  kind: 'copy-command',
177
180
  command: ['vgxness', 'mcp', 'install', 'opencode', '--scope', scope, '--yes', ...(overwriteVgxness ? ['--overwrite-vgxness'] : [])],
178
181
  description: overwriteVgxness
179
- ? 'Copy and run this outside the TUI only after reviewing that VGXNESS entries will be overwritten and unrelated config preserved.'
180
- : 'Copy and run this outside the TUI only after reviewing the read-only plan.',
182
+ ? 'Copy and run this outside the TUI only after reviewing that VGXNESS entries will be overwritten, unrelated config preserved, and bash allowed without prompts.'
183
+ : 'Copy and run this outside the TUI only after reviewing the read-only plan and bash permission risk.',
181
184
  safety: externalProviderWriteSafety(targetPath),
182
185
  };
183
186
  }
@@ -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
+ ...(input.opencode.allowBash ? { allowBash: true } : {}),
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
+ ...(input.opencode.allowBash ? { allowBash: true } : {}),
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' : ''}${input.opencode.allowBash ? '; sets permission.bash to allow by default' : ''}.`,
166
168
  mutating: false,
167
169
  targetPath: input.opencode.targetPath,
168
170
  backupRequired: input.opencode.backupRequired,
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` with `aggregateBlockers` of kinds `missing-topic-key`, `unaccepted-phase`, `legacy-artifact`, `readiness`. |
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 returns `aggregateBlockers`; if any are `unaccepted-phase`, the agent should not run the next phase until a human accepts the prerequisite. `vgxness_sdd_continue` uses the same SDD state to explain how to continue; it is advisory only.
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vgxness",
3
- "version": "1.9.4",
3
+ "version": "1.9.5",
4
4
  "description": "CLI and MCP control plane for guided AI-agent workflows, SDD, memory, and OpenCode setup.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {