vgxness 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/cli-help.js +1 -1
- package/dist/cli/dashboard-screen-renderers.js +24 -2
- package/dist/cli/dashboard-tui-state.js +61 -10
- package/dist/code/runtime/code-runtime.js +28 -5
- package/dist/code/runtime/gateways.js +75 -14
- package/dist/code/runtime/sdd-context.js +41 -8
- package/dist/code/runtime/sdd-workflow-gateway.js +20 -1
- package/dist/mcp/control-plane.js +13 -2
- package/dist/mcp/index.js +1 -0
- package/dist/mcp/opencode-handoff-preview.js +95 -0
- package/dist/mcp/schema.js +35 -0
- package/dist/mcp/stdio-server.js +6 -0
- package/dist/mcp/validation.js +71 -0
- package/dist/memory/repositories/sessions.js +3 -0
- package/dist/memory/storage-paths.js +5 -3
- package/dist/sdd/sdd-workflow-service.js +2 -0
- package/docs/cli.md +1 -1
- package/docs/funcionamiento-del-sistema.md +2 -2
- package/package.json +1 -1
package/dist/cli/cli-help.js
CHANGED
|
@@ -138,7 +138,7 @@ Areas:
|
|
|
138
138
|
permissions check --category <category> --operation <name> [--path <path>] [--agent-id <id>] [--external] [--privileged]
|
|
139
139
|
|
|
140
140
|
Storage:
|
|
141
|
-
Default: global user data vgxness/memory.sqlite (macOS ~/Library/Application Support/vgxness/memory.sqlite; Linux \${XDG_DATA_HOME:-~/.local/share}/vgxness/memory.sqlite; Windows %APPDATA%\\vgxness\\memory.sqlite).
|
|
141
|
+
Default: global user data vgxness/memory.sqlite (macOS ~/Library/Application Support/vgxness/memory.sqlite; Linux \${XDG_DATA_HOME:-~/.local/share}/vgxness/memory.sqlite; Windows %LOCALAPPDATA%\\vgxness\\memory.sqlite, falling back to %APPDATA%\\vgxness\\memory.sqlite).
|
|
142
142
|
Precedence: --db <path> (source: flag) > VGXNESS_DB_PATH (source: environment) > global default (source: global-default).
|
|
143
143
|
Compatibility: pass --db .vgx/memory.sqlite to use the old project-local database explicitly.
|
|
144
144
|
`;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { classifySetupForProject, copyOnly, isSetupReady } from './dashboard-tui-read-model.js';
|
|
1
|
+
import { classifySetupForProject, copyOnly, isSetupReady, } from './dashboard-tui-read-model.js';
|
|
2
2
|
import { dashboardScreens } from './dashboard-tui-state.js';
|
|
3
3
|
import { renderSetupStatus } from './setup-status-renderer.js';
|
|
4
4
|
import { joinSections, renderFooterGroups, renderPanel, renderTabRow, styleFocusedRow } from './tui-render-helpers.js';
|
|
@@ -26,6 +26,7 @@ export function renderDashboardFrame(state, options = {}) {
|
|
|
26
26
|
renderErrorsPanel(state, options),
|
|
27
27
|
renderBackControl(options),
|
|
28
28
|
renderPanel(`Content · ${activeLabel(state.activeScreen)}`, renderActiveScreen(state, options).split('\n'), options),
|
|
29
|
+
renderActionPreviewPanel(state.actionPreview, options),
|
|
29
30
|
renderDashboardFooter(state, options),
|
|
30
31
|
]);
|
|
31
32
|
}
|
|
@@ -83,12 +84,33 @@ function renderErrorsPanel(state, options) {
|
|
|
83
84
|
function renderDashboardFooter(state, options) {
|
|
84
85
|
const modeGroups = state.viewMode === 'menu'
|
|
85
86
|
? [{ label: 'Menu', items: ['↑/↓ or j/k move', 'Enter open', '1-9/0 open'] }]
|
|
86
|
-
: [{ label: 'Section', items: ['j/k or ↑/↓ move', 'Enter detail only', 'b/Esc/Backspace back'] }];
|
|
87
|
+
: [{ label: 'Section', items: ['j/k or ↑/↓ move', 'Enter detail only', '[a] Action preview', 'b/Esc/Backspace back'] }];
|
|
87
88
|
return renderPanel('Footer/help', [
|
|
88
89
|
...renderFooterGroups([...modeGroups, { label: 'Global', items: ['r refresh', '? help', 'q quit'] }], options),
|
|
89
90
|
'Safety: read-only dashboard; actions are copy-only and never apply/install/approve.',
|
|
90
91
|
], options);
|
|
91
92
|
}
|
|
93
|
+
function renderActionPreviewPanel(preview, options) {
|
|
94
|
+
if (preview === undefined)
|
|
95
|
+
return undefined;
|
|
96
|
+
if (!preview.available)
|
|
97
|
+
return renderPanel('Action preview', [
|
|
98
|
+
`Source: ${preview.screen}`,
|
|
99
|
+
`State: unavailable`,
|
|
100
|
+
preview.message,
|
|
101
|
+
'Safety: display-only; does not run in TUI.',
|
|
102
|
+
], options);
|
|
103
|
+
const descriptor = preview.descriptor;
|
|
104
|
+
return renderPanel('Action preview', [
|
|
105
|
+
`Label: ${descriptor.label}`,
|
|
106
|
+
`Source: ${descriptor.source.screen} · ${descriptor.source.rowTitle} (${descriptor.source.rowId})`,
|
|
107
|
+
`Mode: ${descriptor.mode}`,
|
|
108
|
+
`Safety: ${descriptor.safety}; copyOnly=${String(descriptor.copyOnly)}; executesInTui=${String(descriptor.executesInTui)}`,
|
|
109
|
+
`Command: ${descriptor.command}`,
|
|
110
|
+
`Guidance: ${descriptor.guidance}`,
|
|
111
|
+
'This preview does not run in TUI.',
|
|
112
|
+
], options);
|
|
113
|
+
}
|
|
92
114
|
function renderActiveScreen(state, options) {
|
|
93
115
|
if (state.activeScreen === 'installation')
|
|
94
116
|
return renderInstallationScreen(state, options);
|
|
@@ -71,6 +71,8 @@ export function createDashboardState(input) {
|
|
|
71
71
|
state.management = input.management;
|
|
72
72
|
if (input.runInsights !== undefined)
|
|
73
73
|
state.runInsights = input.runInsights;
|
|
74
|
+
if (input.actionPreview !== undefined)
|
|
75
|
+
state.actionPreview = input.actionPreview;
|
|
74
76
|
if (input.navigationNotice !== undefined)
|
|
75
77
|
state.navigationNotice = input.navigationNotice;
|
|
76
78
|
if (input.error !== undefined)
|
|
@@ -83,19 +85,19 @@ export function initialDashboardScreen(setup, setupError) {
|
|
|
83
85
|
export function reduceDashboardKey(state, key) {
|
|
84
86
|
const screen = screenForKey(key);
|
|
85
87
|
if (screen !== undefined) {
|
|
86
|
-
const { navigationNotice: _navigationNotice, ...rest } = state;
|
|
88
|
+
const { actionPreview: _actionPreview, navigationNotice: _navigationNotice, ...rest } = state;
|
|
87
89
|
return { ...rest, viewMode: 'screen', activeScreen: screen, focusedScreen: screen };
|
|
88
90
|
}
|
|
89
91
|
if (key === 'menu-back')
|
|
90
92
|
return state.viewMode === 'screen' ? backFromScreen(state) : state;
|
|
91
93
|
if (key === 'menu-left')
|
|
92
|
-
return moveMenuFocus(state, -1);
|
|
94
|
+
return moveMenuFocus(clearActionPreview(state), -1);
|
|
93
95
|
if (key === 'menu-right')
|
|
94
|
-
return moveMenuFocus(state, 1);
|
|
96
|
+
return moveMenuFocus(clearActionPreview(state), 1);
|
|
95
97
|
if (key === 'menu-open') {
|
|
96
98
|
if (state.viewMode === 'screen')
|
|
97
99
|
return showDetail(state);
|
|
98
|
-
const { navigationNotice: _navigationNotice, ...rest } = state;
|
|
100
|
+
const { actionPreview: _actionPreview, navigationNotice: _navigationNotice, ...rest } = state;
|
|
99
101
|
return { ...rest, viewMode: 'screen', activeScreen: state.focusedScreen };
|
|
100
102
|
}
|
|
101
103
|
if (isSetupKey(key))
|
|
@@ -104,8 +106,12 @@ export function reduceDashboardKey(state, key) {
|
|
|
104
106
|
return state.viewMode === 'menu' ? moveMenuFocus(state, -1) : navigateScreenRows(state, -1);
|
|
105
107
|
if (key === 'down')
|
|
106
108
|
return state.viewMode === 'menu' ? moveMenuFocus(state, 1) : navigateScreenRows(state, 1);
|
|
109
|
+
if (key === 'action-preview')
|
|
110
|
+
return state.viewMode === 'screen' ? showActionPreview(state) : state;
|
|
107
111
|
if (key === 'help')
|
|
108
112
|
return { ...state, helpVisible: !state.helpVisible };
|
|
113
|
+
if (key === 'refresh')
|
|
114
|
+
return clearActionPreview(state);
|
|
109
115
|
if (key === 'quit')
|
|
110
116
|
return { ...state, shouldExit: true };
|
|
111
117
|
return state;
|
|
@@ -137,6 +143,8 @@ export function dashboardKeyFromInput(input) {
|
|
|
137
143
|
return 'menu-right';
|
|
138
144
|
if (input === '\r' || input === '\n')
|
|
139
145
|
return 'menu-open';
|
|
146
|
+
if (input === 'a')
|
|
147
|
+
return 'action-preview';
|
|
140
148
|
if (input === 'b' || input === '\u001B' || input === '\u007F')
|
|
141
149
|
return 'menu-back';
|
|
142
150
|
if (input === 'd')
|
|
@@ -216,22 +224,65 @@ function screenForKey(key) {
|
|
|
216
224
|
return undefined;
|
|
217
225
|
}
|
|
218
226
|
function backFromScreen(state) {
|
|
227
|
+
if (state.actionPreview !== undefined)
|
|
228
|
+
return clearActionPreview(state);
|
|
219
229
|
if (state.detailVisibleByScreen[state.activeScreen] === true)
|
|
220
|
-
return { ...state, detailVisibleByScreen: { ...state.detailVisibleByScreen, [state.activeScreen]: false } };
|
|
221
|
-
return { ...state, viewMode: 'menu' };
|
|
230
|
+
return { ...clearActionPreview(state), detailVisibleByScreen: { ...state.detailVisibleByScreen, [state.activeScreen]: false } };
|
|
231
|
+
return { ...clearActionPreview(state), viewMode: 'menu' };
|
|
222
232
|
}
|
|
223
233
|
function showDetail(state) {
|
|
224
|
-
return { ...state, detailVisibleByScreen: { ...state.detailVisibleByScreen, [state.activeScreen]: true } };
|
|
234
|
+
return { ...clearActionPreview(state), detailVisibleByScreen: { ...state.detailVisibleByScreen, [state.activeScreen]: true } };
|
|
225
235
|
}
|
|
226
236
|
function navigateScreenRows(state, delta) {
|
|
227
|
-
const rows = state.management?.[state.activeScreen]?.rows.length ?? 0;
|
|
237
|
+
const rows = isManagementScreen(state.activeScreen) ? (state.management?.[state.activeScreen]?.rows.length ?? 0) : 0;
|
|
228
238
|
if (rows === 0 && state.activeScreen === 'runs')
|
|
229
|
-
return navigateRuns(state, delta);
|
|
239
|
+
return navigateRuns(clearActionPreview(state), delta);
|
|
230
240
|
if (rows === 0)
|
|
231
241
|
return state;
|
|
232
242
|
const current = state.selectedByScreen[state.activeScreen] ?? 0;
|
|
233
243
|
const next = (current + delta + rows) % rows;
|
|
234
|
-
return { ...state, selectedByScreen: { ...state.selectedByScreen, [state.activeScreen]: next } };
|
|
244
|
+
return { ...clearActionPreview(state), selectedByScreen: { ...state.selectedByScreen, [state.activeScreen]: next } };
|
|
245
|
+
}
|
|
246
|
+
function showActionPreview(state) {
|
|
247
|
+
if (!isManagementScreen(state.activeScreen))
|
|
248
|
+
return { ...state, actionPreview: { available: false, screen: state.activeScreen, message: 'No action preview is available for this screen.' } };
|
|
249
|
+
const model = state.management?.[state.activeScreen];
|
|
250
|
+
const row = model?.rows[state.selectedByScreen[state.activeScreen] ?? 0];
|
|
251
|
+
if (row === undefined)
|
|
252
|
+
return { ...state, actionPreview: { available: false, screen: state.activeScreen, message: 'No selected row has an action to preview.' } };
|
|
253
|
+
const action = row.copyActions[0];
|
|
254
|
+
if (action === undefined)
|
|
255
|
+
return { ...state, actionPreview: { available: false, screen: state.activeScreen, message: 'The selected row has no copy-only action.' } };
|
|
256
|
+
return { ...state, actionPreview: { available: true, descriptor: actionDescriptor(state.activeScreen, row.id, row.title, action) } };
|
|
257
|
+
}
|
|
258
|
+
function actionDescriptor(screen, rowId, rowTitle, action) {
|
|
259
|
+
return {
|
|
260
|
+
id: `${screen}:${rowId}:copy-action:0`,
|
|
261
|
+
label: action.label,
|
|
262
|
+
source: { screen, rowId, rowTitle },
|
|
263
|
+
mode: 'copy-command',
|
|
264
|
+
safety: action.safety,
|
|
265
|
+
command: action.command,
|
|
266
|
+
guidance: 'Copy and run outside the TUI only after manual review. This dashboard does not execute the action.',
|
|
267
|
+
copyOnly: true,
|
|
268
|
+
executesInTui: false,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function clearActionPreview(state) {
|
|
272
|
+
if (state.actionPreview === undefined)
|
|
273
|
+
return state;
|
|
274
|
+
const { actionPreview: _actionPreview, ...rest } = state;
|
|
275
|
+
return rest;
|
|
276
|
+
}
|
|
277
|
+
function isManagementScreen(screen) {
|
|
278
|
+
return (screen === 'agents' ||
|
|
279
|
+
screen === 'skills' ||
|
|
280
|
+
screen === 'memory' ||
|
|
281
|
+
screen === 'sdd' ||
|
|
282
|
+
screen === 'runs' ||
|
|
283
|
+
screen === 'approvals' ||
|
|
284
|
+
screen === 'permissions' ||
|
|
285
|
+
screen === 'settings');
|
|
235
286
|
}
|
|
236
287
|
function isSetupKey(key) {
|
|
237
288
|
return key.startsWith('setup-');
|
|
@@ -12,7 +12,7 @@ import { prepareWorkspaceApprovalRequest, WorkspaceToolExecutor } from '../tools
|
|
|
12
12
|
import { createApprovalId, ConservativePermissionGateway, PolicyApprovalBroker, PolicyApprovalPrompt, StdioApprovalBroker, } from './approval-coordinator.js';
|
|
13
13
|
import { InMemoryRunGateway, InMemorySddGateway } from './gateways.js';
|
|
14
14
|
import { detectProject } from './project-detection.js';
|
|
15
|
-
import { implementationEnabledForSddPhase, isSddCodePhase, loadSddContext, shellEnabledForSddPhase, validateSddReadiness, } from './sdd-context.js';
|
|
15
|
+
import { activeBlockedPrerequisites, implementationEnabledForSddPhase, isSddCodePhase, loadSddContext, shellEnabledForSddPhase, validateSddReadiness, } from './sdd-context.js';
|
|
16
16
|
import { createVerificationToolCall, VerificationCoordinator } from './verification-coordinator.js';
|
|
17
17
|
export class CodeRuntime {
|
|
18
18
|
provider;
|
|
@@ -122,8 +122,22 @@ export class CodeRuntime {
|
|
|
122
122
|
await this.checkpoint(run.id, session, 'sdd-context-loaded', {
|
|
123
123
|
changeId: sddContext.changeId,
|
|
124
124
|
phase: sddContext.phase,
|
|
125
|
-
artifacts: sddContext.artifacts.map((artifact) => ({
|
|
126
|
-
|
|
125
|
+
artifacts: sddContext.artifacts.map((artifact) => ({
|
|
126
|
+
phase: artifact.phase,
|
|
127
|
+
role: artifact.role,
|
|
128
|
+
bytes: artifact.content.length,
|
|
129
|
+
state: artifact.state ?? null,
|
|
130
|
+
accepted: artifact.accepted === true,
|
|
131
|
+
legacy: artifact.legacy === true,
|
|
132
|
+
artifactId: artifact.artifactId ?? null,
|
|
133
|
+
topicKey: artifact.topicKey ?? null,
|
|
134
|
+
})),
|
|
135
|
+
activePhase: sddContext.status.phases.find((phase) => phase.phase === sddContext.phase) ?? null,
|
|
136
|
+
next: {
|
|
137
|
+
...sddContext.next,
|
|
138
|
+
missingArtifactTopicKeys: [...sddContext.next.missingArtifactTopicKeys],
|
|
139
|
+
blockedPrerequisites: [...(sddContext.next.blockedPrerequisites ?? [])],
|
|
140
|
+
},
|
|
127
141
|
});
|
|
128
142
|
const readiness = validateSddReadiness(sddContext);
|
|
129
143
|
if (!readiness.ok) {
|
|
@@ -555,7 +569,11 @@ export class CodeRuntime {
|
|
|
555
569
|
events: [],
|
|
556
570
|
changedFiles: [],
|
|
557
571
|
};
|
|
558
|
-
if (call.name === 'sdd_get_readiness')
|
|
572
|
+
if (call.name === 'sdd_get_readiness') {
|
|
573
|
+
const blockedPrerequisites = activeBlockedPrerequisites(context);
|
|
574
|
+
const missingArtifactTopicKeys = blockedPrerequisites.length > 0
|
|
575
|
+
? blockedPrerequisites.filter((blocker) => blocker.reason === 'missing').map((blocker) => blocker.topicKey)
|
|
576
|
+
: [...context.next.missingArtifactTopicKeys];
|
|
559
577
|
return {
|
|
560
578
|
result: {
|
|
561
579
|
toolCallId: call.id,
|
|
@@ -566,6 +584,8 @@ export class CodeRuntime {
|
|
|
566
584
|
phase: context.phase,
|
|
567
585
|
next: context.next,
|
|
568
586
|
activePhase: context.status.phases.find((phase) => phase.phase === context.phase) ?? null,
|
|
587
|
+
blockedPrerequisites: blockedPrerequisites.length > 0 ? blockedPrerequisites : [...(context.next.blockedPrerequisites ?? [])],
|
|
588
|
+
missingArtifactTopicKeys,
|
|
569
589
|
},
|
|
570
590
|
metadata: { capability, truncated: false },
|
|
571
591
|
},
|
|
@@ -577,12 +597,15 @@ export class CodeRuntime {
|
|
|
577
597
|
phase: context.phase,
|
|
578
598
|
nextPhase: context.next.nextPhase ?? null,
|
|
579
599
|
status: context.next.status,
|
|
580
|
-
|
|
600
|
+
activePhase: context.status.phases.find((phase) => phase.phase === context.phase) ?? null,
|
|
601
|
+
blockedPrerequisites: blockedPrerequisites.length > 0 ? blockedPrerequisites : [...(context.next.blockedPrerequisites ?? [])],
|
|
602
|
+
missingArtifactTopicKeys,
|
|
581
603
|
},
|
|
582
604
|
},
|
|
583
605
|
],
|
|
584
606
|
changedFiles: [],
|
|
585
607
|
};
|
|
608
|
+
}
|
|
586
609
|
if (call.name === 'sdd_next_phase')
|
|
587
610
|
return {
|
|
588
611
|
result: {
|
|
@@ -26,29 +26,26 @@ export class InMemorySddGateway {
|
|
|
26
26
|
artifacts = new Map();
|
|
27
27
|
saved = [];
|
|
28
28
|
constructor(initialArtifacts = {}) {
|
|
29
|
-
for (const [key,
|
|
30
|
-
this.artifacts.set(key,
|
|
29
|
+
for (const [key, artifact] of Object.entries(initialArtifacts))
|
|
30
|
+
this.artifacts.set(key, normalizeInMemoryArtifact(artifact));
|
|
31
31
|
}
|
|
32
32
|
async getStatus(input) {
|
|
33
|
-
const phases = sddPhaseOrder.map((phase) => (
|
|
34
|
-
|
|
35
|
-
topicKey: artifactKey(input.project, input.change, phase),
|
|
36
|
-
present: this.artifacts.has(artifactKey(input.project, input.change, phase)),
|
|
37
|
-
}));
|
|
38
|
-
const nextReadyPhase = phases.find((status) => !status.present && sddPrerequisites[status.phase].every((phase) => this.artifacts.has(artifactKey(input.project, input.change, phase))))?.phase;
|
|
33
|
+
const phases = sddPhaseOrder.map((phase) => inMemoryPhaseStatus(this.artifacts.get(artifactKey(input.project, input.change, phase)), input.change, phase));
|
|
34
|
+
const nextReadyPhase = phases.find((status) => !status.accepted && blockersForPhase(this.artifacts, input.project, input.change, status.phase).length === 0)?.phase;
|
|
39
35
|
return { change: input.change, phases, ...(nextReadyPhase === undefined ? {} : { nextReadyPhase }) };
|
|
40
36
|
}
|
|
41
37
|
async getArtifact(input) {
|
|
42
|
-
const
|
|
43
|
-
return
|
|
38
|
+
const artifact = this.artifacts.get(artifactKey(input.project, input.change, input.phase));
|
|
39
|
+
return artifact === undefined ? null : { content: artifact.content };
|
|
44
40
|
}
|
|
45
41
|
async saveArtifact(input) {
|
|
46
|
-
this.artifacts.set(artifactKey(input.project, input.change, input.phase), input.content);
|
|
42
|
+
this.artifacts.set(artifactKey(input.project, input.change, input.phase), normalizeInMemoryArtifact({ content: input.content, state: 'draft', accepted: false }));
|
|
47
43
|
this.saved.push(input);
|
|
48
44
|
}
|
|
49
45
|
async markReady() { }
|
|
50
46
|
async getNextPhase(input) {
|
|
51
47
|
const status = await this.getStatus(input);
|
|
48
|
+
const blockedPrerequisites = status.nextReadyPhase === undefined ? blockersForFirstBlockedPhase(this.artifacts, input.project, input.change) : [];
|
|
52
49
|
if (status.nextReadyPhase !== undefined)
|
|
53
50
|
return {
|
|
54
51
|
status: 'runnable',
|
|
@@ -57,7 +54,7 @@ export class InMemorySddGateway {
|
|
|
57
54
|
recommendedAction: `Run the ${status.nextReadyPhase} SDD phase for ${input.change}.`,
|
|
58
55
|
missingArtifactTopicKeys: [],
|
|
59
56
|
};
|
|
60
|
-
const missing = sddPhaseOrder.find((phase) => !status.phases.find((candidate) => candidate.phase === phase)?.
|
|
57
|
+
const missing = sddPhaseOrder.find((phase) => !status.phases.find((candidate) => candidate.phase === phase)?.accepted);
|
|
61
58
|
if (missing === undefined)
|
|
62
59
|
return {
|
|
63
60
|
status: 'complete',
|
|
@@ -71,12 +68,76 @@ export class InMemorySddGateway {
|
|
|
71
68
|
return {
|
|
72
69
|
status: 'blocked',
|
|
73
70
|
nextPhase: missing,
|
|
74
|
-
reason: `${missing} is blocked by
|
|
75
|
-
recommendedAction: `
|
|
71
|
+
reason: `${missing} is blocked by prerequisite artifact governance.`,
|
|
72
|
+
recommendedAction: `Accept or restore prerequisites before running ${missing}.`,
|
|
76
73
|
missingArtifactTopicKeys,
|
|
74
|
+
blockedPrerequisites,
|
|
77
75
|
};
|
|
78
76
|
}
|
|
79
77
|
}
|
|
80
78
|
function artifactKey(project, change, phase) {
|
|
81
79
|
return `${project}:${change}:${phase}`;
|
|
82
80
|
}
|
|
81
|
+
function normalizeInMemoryArtifact(input) {
|
|
82
|
+
if (typeof input === 'string')
|
|
83
|
+
return { content: input, state: 'accepted', accepted: true, legacy: false };
|
|
84
|
+
const state = input.state ?? (input.accepted === true ? 'accepted' : 'draft');
|
|
85
|
+
return {
|
|
86
|
+
content: input.content,
|
|
87
|
+
state,
|
|
88
|
+
accepted: input.accepted ?? state === 'accepted',
|
|
89
|
+
legacy: input.legacy ?? false,
|
|
90
|
+
...(input.artifactId === undefined ? {} : { artifactId: input.artifactId }),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function inMemoryPhaseStatus(artifact, change, phase) {
|
|
94
|
+
const topicKey = `sdd/${change}/${phase}`;
|
|
95
|
+
if (artifact === undefined)
|
|
96
|
+
return { phase, topicKey, present: false, state: 'missing', accepted: false, legacy: false };
|
|
97
|
+
return {
|
|
98
|
+
phase,
|
|
99
|
+
topicKey,
|
|
100
|
+
present: true,
|
|
101
|
+
state: artifact.state,
|
|
102
|
+
accepted: artifact.accepted,
|
|
103
|
+
legacy: artifact.legacy,
|
|
104
|
+
...(artifact.artifactId === undefined ? {} : { artifactId: artifact.artifactId }),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function blockersForFirstBlockedPhase(artifacts, project, change) {
|
|
108
|
+
for (const phase of sddPhaseOrder) {
|
|
109
|
+
const status = inMemoryPhaseStatus(artifacts.get(artifactKey(project, change, phase)), change, phase);
|
|
110
|
+
if (status.accepted === true)
|
|
111
|
+
continue;
|
|
112
|
+
const blockers = blockersForPhase(artifacts, project, change, phase);
|
|
113
|
+
if (blockers.length > 0)
|
|
114
|
+
return blockers;
|
|
115
|
+
}
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
function blockersForPhase(artifacts, project, change, phase) {
|
|
119
|
+
return sddPrerequisites[phase].flatMap((prerequisite) => {
|
|
120
|
+
const key = artifactKey(project, change, prerequisite);
|
|
121
|
+
const artifact = artifacts.get(key);
|
|
122
|
+
const topicKey = `sdd/${change}/${prerequisite}`;
|
|
123
|
+
if (artifact === undefined)
|
|
124
|
+
return [{ phase: prerequisite, topicKey, reason: 'missing' }];
|
|
125
|
+
if (artifact.accepted === true && artifact.legacy !== true)
|
|
126
|
+
return [];
|
|
127
|
+
return [
|
|
128
|
+
{
|
|
129
|
+
phase: prerequisite,
|
|
130
|
+
topicKey,
|
|
131
|
+
reason: blockerReasonForInMemoryArtifact(artifact),
|
|
132
|
+
...(artifact.artifactId === undefined ? {} : { artifactId: artifact.artifactId }),
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
function blockerReasonForInMemoryArtifact(artifact) {
|
|
138
|
+
if (artifact.legacy === true)
|
|
139
|
+
return 'legacy';
|
|
140
|
+
if (artifact.state === 'rejected' || artifact.state === 'superseded')
|
|
141
|
+
return artifact.state;
|
|
142
|
+
return 'draft';
|
|
143
|
+
}
|
|
@@ -47,8 +47,8 @@ export function compactSddArtifactForPrompt(input) {
|
|
|
47
47
|
project: input.project,
|
|
48
48
|
change: input.change,
|
|
49
49
|
phase: input.artifact.phase,
|
|
50
|
-
state: input.artifact.state ??
|
|
51
|
-
accepted: input.artifact.accepted
|
|
50
|
+
state: input.artifact.state ?? 'draft',
|
|
51
|
+
accepted: input.artifact.accepted === true,
|
|
52
52
|
legacy: input.artifact.legacy ?? false,
|
|
53
53
|
...(input.artifact.artifactId === undefined ? {} : { artifactId: input.artifact.artifactId }),
|
|
54
54
|
...(input.artifact.topicKey === undefined ? {} : { topicKey: input.artifact.topicKey }),
|
|
@@ -65,10 +65,10 @@ export function compactSddArtifactForPrompt(input) {
|
|
|
65
65
|
}
|
|
66
66
|
function contextPhaseStatus(status, phase) {
|
|
67
67
|
const value = status.phases.find((candidate) => candidate.phase === phase);
|
|
68
|
-
const state = typeof value?.state === 'string' ? value.state : value?.present === true ? '
|
|
68
|
+
const state = typeof value?.state === 'string' ? value.state : value?.present === true ? 'draft' : 'missing';
|
|
69
69
|
return {
|
|
70
70
|
state,
|
|
71
|
-
accepted:
|
|
71
|
+
accepted: value?.accepted === true,
|
|
72
72
|
legacy: typeof value?.legacy === 'boolean' ? value.legacy : false,
|
|
73
73
|
...(typeof value?.artifactId === 'string' ? { artifactId: value.artifactId } : {}),
|
|
74
74
|
...(typeof value?.topicKey === 'string' ? { topicKey: value.topicKey } : {}),
|
|
@@ -86,6 +86,20 @@ export function validateSddReadiness(context) {
|
|
|
86
86
|
const present = context.status.phases.find((candidate) => candidate.phase === context.phase)?.present === true;
|
|
87
87
|
if (present)
|
|
88
88
|
return { ok: true };
|
|
89
|
+
const blockedPrerequisites = activeBlockedPrerequisites(context);
|
|
90
|
+
if (blockedPrerequisites.length > 0)
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
reason: `${context.phase} is blocked by prerequisite artifact governance.`,
|
|
94
|
+
state: {
|
|
95
|
+
phase: context.phase,
|
|
96
|
+
nextPhase: context.next.nextPhase ?? null,
|
|
97
|
+
activePhase: context.status.phases.find((candidate) => candidate.phase === context.phase) ?? null,
|
|
98
|
+
blockedPrerequisites,
|
|
99
|
+
missingArtifactTopicKeys: blockedPrerequisites.filter((blocker) => blocker.reason === 'missing').map((blocker) => blocker.topicKey),
|
|
100
|
+
recommendedAction: `Accept or restore prerequisites before running ${context.phase}.`,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
89
103
|
if (context.status.nextReadyPhase === context.phase)
|
|
90
104
|
return { ok: true };
|
|
91
105
|
return {
|
|
@@ -94,20 +108,39 @@ export function validateSddReadiness(context) {
|
|
|
94
108
|
state: {
|
|
95
109
|
phase: context.phase,
|
|
96
110
|
nextPhase: context.next.nextPhase ?? null,
|
|
111
|
+
activePhase: context.status.phases.find((candidate) => candidate.phase === context.phase) ?? null,
|
|
112
|
+
blockedPrerequisites: [...(context.next.blockedPrerequisites ?? [])],
|
|
97
113
|
missingArtifactTopicKeys: [...context.next.missingArtifactTopicKeys],
|
|
98
114
|
recommendedAction: context.next.recommendedAction,
|
|
99
115
|
},
|
|
100
116
|
};
|
|
101
117
|
}
|
|
118
|
+
export function activeBlockedPrerequisites(context) {
|
|
119
|
+
return prerequisitePhases(context.phase).flatMap((phase) => {
|
|
120
|
+
const status = context.status.phases.find((candidate) => candidate.phase === phase);
|
|
121
|
+
const topicKey = status?.topicKey ?? `sdd/${context.changeId}/${phase}`;
|
|
122
|
+
if (status === undefined || status.present !== true)
|
|
123
|
+
return [{ phase, topicKey, reason: 'missing' }];
|
|
124
|
+
if (status.accepted === true && status.legacy !== true)
|
|
125
|
+
return [];
|
|
126
|
+
const reason = status.legacy === true ? 'legacy' : status.state === 'rejected' || status.state === 'superseded' ? status.state : 'draft';
|
|
127
|
+
return [{ phase, topicKey, reason, ...(status.artifactId === undefined ? {} : { artifactId: status.artifactId }) }];
|
|
128
|
+
});
|
|
129
|
+
}
|
|
102
130
|
function acceptedArtifactPhases(active) {
|
|
131
|
+
return prerequisitePhases(active);
|
|
132
|
+
}
|
|
133
|
+
function prerequisitePhases(active) {
|
|
134
|
+
if (active === 'proposal')
|
|
135
|
+
return ['explore'];
|
|
103
136
|
if (active === 'spec')
|
|
104
|
-
return [];
|
|
137
|
+
return ['proposal'];
|
|
105
138
|
if (active === 'design')
|
|
106
|
-
return ['spec'];
|
|
139
|
+
return ['proposal', 'spec'];
|
|
107
140
|
if (active === 'tasks')
|
|
108
|
-
return ['spec', 'design'];
|
|
141
|
+
return ['proposal', 'spec', 'design'];
|
|
109
142
|
if (active === 'apply-progress' || active === 'verify' || active === 'archive')
|
|
110
|
-
return ['spec', 'design', 'tasks'];
|
|
143
|
+
return ['proposal', 'spec', 'design', 'tasks'];
|
|
111
144
|
return [];
|
|
112
145
|
}
|
|
113
146
|
function uniquePhases(phases) {
|
|
@@ -9,7 +9,15 @@ export class SddWorkflowGateway {
|
|
|
9
9
|
throw new Error(result.error.message);
|
|
10
10
|
return {
|
|
11
11
|
change: result.value.change,
|
|
12
|
-
phases: result.value.phases.map((phase) => ({
|
|
12
|
+
phases: result.value.phases.map((phase) => ({
|
|
13
|
+
phase: phase.phase,
|
|
14
|
+
present: phase.present,
|
|
15
|
+
topicKey: phase.topicKey,
|
|
16
|
+
...(phase.state === undefined ? {} : { state: phase.state }),
|
|
17
|
+
...(phase.accepted === undefined ? {} : { accepted: phase.accepted }),
|
|
18
|
+
...(phase.legacy === undefined ? {} : { legacy: phase.legacy }),
|
|
19
|
+
...(phase.artifactId === undefined ? {} : { artifactId: phase.artifactId }),
|
|
20
|
+
})),
|
|
13
21
|
...(result.value.nextReadyPhase === undefined ? {} : { nextReadyPhase: result.value.nextReadyPhase }),
|
|
14
22
|
};
|
|
15
23
|
}
|
|
@@ -33,12 +41,23 @@ export class SddWorkflowGateway {
|
|
|
33
41
|
const result = this.service.getNext(input);
|
|
34
42
|
if (!result.ok)
|
|
35
43
|
throw new Error(result.error.message);
|
|
44
|
+
const status = this.service.getStatus(input);
|
|
45
|
+
if (!status.ok)
|
|
46
|
+
throw new Error(status.error.message);
|
|
47
|
+
const legacyPrerequisites = new Set(status.value.phases.filter((phase) => phase.legacy === true).map((phase) => `${phase.phase}:${phase.topicKey}`));
|
|
48
|
+
const blockedPrerequisites = (result.value.blockedPrerequisites ?? []).map((blocker) => ({
|
|
49
|
+
phase: blocker.phase,
|
|
50
|
+
topicKey: blocker.topicKey,
|
|
51
|
+
reason: legacyPrerequisites.has(`${blocker.phase}:${blocker.topicKey}`) ? 'legacy' : blocker.reason,
|
|
52
|
+
...(blocker.artifactId === undefined ? {} : { artifactId: blocker.artifactId }),
|
|
53
|
+
}));
|
|
36
54
|
return {
|
|
37
55
|
status: result.value.status,
|
|
38
56
|
...(result.value.nextPhase === undefined ? {} : { nextPhase: result.value.nextPhase }),
|
|
39
57
|
reason: result.value.reason,
|
|
40
58
|
recommendedAction: result.value.recommendedAction,
|
|
41
59
|
missingArtifactTopicKeys: result.value.missingArtifactTopicKeys,
|
|
60
|
+
...(blockedPrerequisites.length === 0 ? {} : { blockedPrerequisites }),
|
|
42
61
|
};
|
|
43
62
|
}
|
|
44
63
|
}
|
|
@@ -16,6 +16,7 @@ import { ProviderDoctorService } from './provider-doctor.js';
|
|
|
16
16
|
import { ProviderStatusService } from './provider-status.js';
|
|
17
17
|
import { errorEnvelope, successEnvelope, } from './schema.js';
|
|
18
18
|
import { validateVgxMcpToolCall } from './validation.js';
|
|
19
|
+
import { OpenCodeHandoffPreviewService } from './opencode-handoff-preview.js';
|
|
19
20
|
export function callVgxTool(call, services) {
|
|
20
21
|
const validated = validateVgxMcpToolCall(call);
|
|
21
22
|
if (!validated.ok)
|
|
@@ -74,6 +75,10 @@ export function callVgxTool(call, services) {
|
|
|
74
75
|
return buildSkillPayloadEnvelope(validated.input, services);
|
|
75
76
|
case 'vgxness_opencode_manager_payload':
|
|
76
77
|
return toEnvelope(validated.tool, services.opencodeManagerPayload.build(validated.input));
|
|
78
|
+
case 'vgxness_opencode_handoff_preview':
|
|
79
|
+
if (services.opencodeHandoffPreview === undefined)
|
|
80
|
+
return errorEnvelope('validation_failed', 'OpenCode handoff preview service is not available', validated.tool);
|
|
81
|
+
return toEnvelope(validated.tool, services.opencodeHandoffPreview.build(validated.input));
|
|
77
82
|
case 'vgxness_run_list':
|
|
78
83
|
return listRunsEnvelope(validated.input, services);
|
|
79
84
|
case 'vgxness_run_get':
|
|
@@ -86,6 +91,10 @@ export function callVgxTool(call, services) {
|
|
|
86
91
|
return toEnvelope(validated.tool, services.runs.appendCheckpoint(validated.input));
|
|
87
92
|
case 'vgxness_run_finalize':
|
|
88
93
|
return toEnvelope(validated.tool, services.runs.updateFinalStatus(validated.input));
|
|
94
|
+
case 'vgxness_run_resume_inspect':
|
|
95
|
+
return toEnvelope(validated.tool, services.runs.getRunOperatorResumePlan(validated.input.runId));
|
|
96
|
+
case 'vgxness_run_resume_gate':
|
|
97
|
+
return toEnvelope(validated.tool, services.runs.getRunResumeOrchestrationPlan(validated.input));
|
|
89
98
|
case 'vgxness_provider_status':
|
|
90
99
|
if (services.providerStatus === undefined)
|
|
91
100
|
return errorEnvelope('validation_failed', 'Provider status service is not available', validated.tool);
|
|
@@ -234,15 +243,17 @@ function createServices(database) {
|
|
|
234
243
|
const runs = new RunService(database);
|
|
235
244
|
const managerProfiles = new ManagerProfileOverlayService({ agents, overlays: new ManagerProfileOverlayRepository(database) });
|
|
236
245
|
const opencodeManagerPayload = new OpenCodeManagerPayloadService({ agents, managerProfiles, skills });
|
|
237
|
-
const
|
|
246
|
+
const sdd = new SddWorkflowService(memory, { actor: 'mcp-control-plane' });
|
|
247
|
+
const providerStatus = new ProviderStatusService({ sdd });
|
|
238
248
|
const providerDoctor = new ProviderDoctorService();
|
|
239
249
|
return {
|
|
240
|
-
sdd
|
|
250
|
+
sdd,
|
|
241
251
|
memory,
|
|
242
252
|
agents,
|
|
243
253
|
managerProfiles,
|
|
244
254
|
skills,
|
|
245
255
|
opencodeManagerPayload,
|
|
256
|
+
opencodeHandoffPreview: new OpenCodeHandoffPreviewService({ managerPayload: opencodeManagerPayload, sdd, providerStatus }),
|
|
246
257
|
activation: new AgentActivationService({ agents, managerProfiles, runs, opencodeManagerPayload }),
|
|
247
258
|
runs,
|
|
248
259
|
providerStatus,
|
package/dist/mcp/index.js
CHANGED
|
@@ -6,6 +6,7 @@ export * from './client-setup-preview.js';
|
|
|
6
6
|
export * from './control-plane.js';
|
|
7
7
|
export * from './doctor.js';
|
|
8
8
|
export * from './opencode-visibility.js';
|
|
9
|
+
export * from './opencode-handoff-preview.js';
|
|
9
10
|
export * from './provider-change-plan.js';
|
|
10
11
|
export * from './provider-doctor.js';
|
|
11
12
|
export * from './provider-health-types.js';
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export class OpenCodeHandoffPreviewService {
|
|
2
|
+
deps;
|
|
3
|
+
constructor(deps) {
|
|
4
|
+
this.deps = deps;
|
|
5
|
+
}
|
|
6
|
+
build(input) {
|
|
7
|
+
const scope = input.scope ?? 'project';
|
|
8
|
+
const warnings = [
|
|
9
|
+
'This handoff preview does not launch, control, or execute OpenCode.',
|
|
10
|
+
'This handoff preview does not write .opencode, provider config, backups, .vgx, worktrees, or sandboxes.',
|
|
11
|
+
'This handoff preview does not create runs, checkpoints, timeline events, sessions, or skill-usage records.',
|
|
12
|
+
];
|
|
13
|
+
const manager = this.deps.managerPayload.build(managerInput(input, scope));
|
|
14
|
+
if (!manager.ok)
|
|
15
|
+
return manager;
|
|
16
|
+
const provider = this.providerSummary(input, scope, warnings);
|
|
17
|
+
if (!provider.ok)
|
|
18
|
+
return provider;
|
|
19
|
+
const sdd = this.sddSummary(input, warnings);
|
|
20
|
+
return {
|
|
21
|
+
ok: true,
|
|
22
|
+
value: {
|
|
23
|
+
version: 1,
|
|
24
|
+
kind: 'opencode-handoff-preview',
|
|
25
|
+
provider: 'opencode',
|
|
26
|
+
context: context(input, scope, manager.value),
|
|
27
|
+
managerPayload: compactManager(manager.value),
|
|
28
|
+
...(sdd !== undefined ? { sdd } : {}),
|
|
29
|
+
...(provider.value !== undefined ? { providerStatus: compactProvider(provider.value) } : {}),
|
|
30
|
+
nextSafeAction: 'Manually continue in OpenCode using this preview context; call opencode_manager_payload separately if a verbose payload is needed.',
|
|
31
|
+
safety: {
|
|
32
|
+
readOnly: true,
|
|
33
|
+
executesProvider: false,
|
|
34
|
+
writesProviderConfig: false,
|
|
35
|
+
mutatesProviderConfig: false,
|
|
36
|
+
createsRun: false,
|
|
37
|
+
createsCheckpoint: false,
|
|
38
|
+
recordsTimelineEvent: false,
|
|
39
|
+
recordsSkillUsage: false,
|
|
40
|
+
},
|
|
41
|
+
warnings: [...warnings, ...manager.value.warnings],
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
sddSummary(input, warnings) {
|
|
46
|
+
if (input.change === undefined && input.phase === undefined)
|
|
47
|
+
return undefined;
|
|
48
|
+
if (input.change === undefined) {
|
|
49
|
+
warnings.push('phase was provided without change; SDD status/readiness was not queried.');
|
|
50
|
+
return { ...(input.phase === undefined ? {} : { phase: input.phase }), unavailableReason: 'change is required to read SDD status/readiness' };
|
|
51
|
+
}
|
|
52
|
+
const summary = { change: input.change, ...(input.phase === undefined ? {} : { phase: input.phase }) };
|
|
53
|
+
if (this.deps.sdd === undefined)
|
|
54
|
+
return { ...summary, unavailableReason: 'SDD service is unavailable' };
|
|
55
|
+
const status = this.deps.sdd.getStatus({ project: input.project, change: input.change });
|
|
56
|
+
const next = this.deps.sdd.getNext({ project: input.project, change: input.change });
|
|
57
|
+
if (status.ok)
|
|
58
|
+
summary.status = status.value;
|
|
59
|
+
else
|
|
60
|
+
warnings.push(`SDD status unavailable: ${status.error.message}`);
|
|
61
|
+
if (next.ok)
|
|
62
|
+
summary.next = next.value;
|
|
63
|
+
else
|
|
64
|
+
warnings.push(`SDD next phase unavailable: ${next.error.message}`);
|
|
65
|
+
if (input.phase !== undefined) {
|
|
66
|
+
const ready = this.deps.sdd.getReady({ project: input.project, change: input.change, phase: input.phase });
|
|
67
|
+
if (ready.ok)
|
|
68
|
+
summary.readiness = ready.value;
|
|
69
|
+
else
|
|
70
|
+
warnings.push(`SDD readiness unavailable: ${ready.error.message}`);
|
|
71
|
+
}
|
|
72
|
+
return summary;
|
|
73
|
+
}
|
|
74
|
+
providerSummary(input, scope, warnings) {
|
|
75
|
+
if (input.workspaceRoot === undefined)
|
|
76
|
+
return { ok: true, value: undefined };
|
|
77
|
+
if (this.deps.providerStatus === undefined) {
|
|
78
|
+
warnings.push('Provider status service is unavailable; provider summary was not inspected.');
|
|
79
|
+
return { ok: true, value: undefined };
|
|
80
|
+
}
|
|
81
|
+
return this.deps.providerStatus.getStatus({ project: input.project, scope, providerAdapter: 'opencode', workspaceRoot: input.workspaceRoot, ...(input.change === undefined ? {} : { change: input.change }), payloadMode: 'compact' });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function managerInput(input, scope) {
|
|
85
|
+
return { project: input.project, scope, agentName: input.agentName ?? 'vgxness-manager', ...(input.agentId === undefined ? {} : { agentId: input.agentId }), ...(input.workspaceRoot === undefined ? {} : { workspaceRoot: input.workspaceRoot }), ...(input.maxSourceBytes === undefined ? {} : { maxSourceBytes: input.maxSourceBytes }), payloadMode: 'compact' };
|
|
86
|
+
}
|
|
87
|
+
function context(input, scope, manager) {
|
|
88
|
+
return { project: input.project, scope, ...(input.change === undefined ? {} : { change: input.change }), ...(input.phase === undefined ? {} : { phase: input.phase }), ...(input.workspaceRoot === undefined ? {} : { workspaceRoot: input.workspaceRoot }), selectedAgent: manager.selection.agent };
|
|
89
|
+
}
|
|
90
|
+
function compactManager(payload) {
|
|
91
|
+
return { version: payload.version, provider: payload.provider, selection: payload.selection, payloadMode: payload.payloadMode, providerArtifacts: payload.providerArtifacts, ...(payload.skillDiagnostics === undefined ? {} : { skillDiagnostics: payload.skillDiagnostics }), safety: payload.safety, warnings: payload.warnings };
|
|
92
|
+
}
|
|
93
|
+
function compactProvider(status) {
|
|
94
|
+
return { version: status.version, kind: status.kind, project: status.project, providerAdapter: status.providerAdapter, scope: status.scope, workspaceRoot: status.workspaceRoot, status: status.status, overallStatus: status.overallStatus, summary: status.summary, nextAction: status.nextAction, issueCount: status.issueCount, warningCount: status.warningCount, safety: status.safety };
|
|
95
|
+
}
|
package/dist/mcp/schema.js
CHANGED
|
@@ -27,12 +27,15 @@ export const SUPPORTED_VGX_MCP_TOOL_NAMES = [
|
|
|
27
27
|
'vgxness_manager_profile_set',
|
|
28
28
|
'vgxness_skill_payload',
|
|
29
29
|
'vgxness_opencode_manager_payload',
|
|
30
|
+
'vgxness_opencode_handoff_preview',
|
|
30
31
|
'vgxness_run_list',
|
|
31
32
|
'vgxness_run_get',
|
|
32
33
|
'vgxness_run_preflight',
|
|
33
34
|
'vgxness_run_start',
|
|
34
35
|
'vgxness_run_checkpoint',
|
|
35
36
|
'vgxness_run_finalize',
|
|
37
|
+
'vgxness_run_resume_inspect',
|
|
38
|
+
'vgxness_run_resume_gate',
|
|
36
39
|
'vgxness_provider_status',
|
|
37
40
|
'vgxness_provider_doctor',
|
|
38
41
|
'vgxness_provider_change_plan',
|
|
@@ -63,12 +66,15 @@ export const EXPOSED_VGX_MCP_TOOL_NAMES = [
|
|
|
63
66
|
'manager_profile_set',
|
|
64
67
|
'skill_payload',
|
|
65
68
|
'opencode_manager_payload',
|
|
69
|
+
'opencode_handoff_preview',
|
|
66
70
|
'run_list',
|
|
67
71
|
'run_get',
|
|
68
72
|
'run_preflight',
|
|
69
73
|
'run_start',
|
|
70
74
|
'run_checkpoint',
|
|
71
75
|
'run_finalize',
|
|
76
|
+
'run_resume_inspect',
|
|
77
|
+
'run_resume_gate',
|
|
72
78
|
'provider_status',
|
|
73
79
|
'provider_doctor',
|
|
74
80
|
'provider_change_plan',
|
|
@@ -319,6 +325,18 @@ export const VGX_MCP_TOOL_INPUT_SCHEMAS = {
|
|
|
319
325
|
payloadMode: z.enum(payloadModes).optional(),
|
|
320
326
|
})
|
|
321
327
|
.passthrough(),
|
|
328
|
+
vgxness_opencode_handoff_preview: z
|
|
329
|
+
.object({
|
|
330
|
+
project: z.string().min(1),
|
|
331
|
+
scope: z.enum(scopes).optional(),
|
|
332
|
+
agentId: z.string().min(1).optional(),
|
|
333
|
+
agentName: z.string().min(1).optional(),
|
|
334
|
+
workspaceRoot: z.string().min(1).optional(),
|
|
335
|
+
maxSourceBytes: z.number().int().positive().optional(),
|
|
336
|
+
change: z.string().min(1).optional(),
|
|
337
|
+
phase: z.enum(sddPhases).optional(),
|
|
338
|
+
})
|
|
339
|
+
.passthrough(),
|
|
322
340
|
vgxness_run_list: z
|
|
323
341
|
.object({
|
|
324
342
|
project: z.string().min(1).optional(),
|
|
@@ -382,6 +400,23 @@ export const VGX_MCP_TOOL_INPUT_SCHEMAS = {
|
|
|
382
400
|
outcomeReason: z.string().min(1).optional(),
|
|
383
401
|
})
|
|
384
402
|
.passthrough(),
|
|
403
|
+
vgxness_run_resume_inspect: z
|
|
404
|
+
.object({
|
|
405
|
+
runId: z.string().min(1),
|
|
406
|
+
})
|
|
407
|
+
.passthrough(),
|
|
408
|
+
vgxness_run_resume_gate: z
|
|
409
|
+
.object({
|
|
410
|
+
approvalId: z.string().min(1),
|
|
411
|
+
policy: z
|
|
412
|
+
.object({
|
|
413
|
+
mode: z.enum(['never', 'after-abandoned', 'after-failure', 'after-failure-or-abandoned']),
|
|
414
|
+
retryableStatuses: z.array(z.enum(['failed', 'abandoned'])).optional(),
|
|
415
|
+
})
|
|
416
|
+
.passthrough()
|
|
417
|
+
.optional(),
|
|
418
|
+
})
|
|
419
|
+
.passthrough(),
|
|
385
420
|
vgxness_provider_status: z
|
|
386
421
|
.object({
|
|
387
422
|
project: z.string().min(1).optional(),
|
package/dist/mcp/stdio-server.js
CHANGED
|
@@ -51,8 +51,14 @@ function descriptionForTool(publicToolName) {
|
|
|
51
51
|
return 'Read-only provider doctor checks; reports OpenCode MCP configuration health without repair/install side effects.';
|
|
52
52
|
if (publicToolName === 'provider_change_plan')
|
|
53
53
|
return 'Read-only provider change plan preview; composes status, doctor, and OpenCode install planning without writing provider config.';
|
|
54
|
+
if (publicToolName === 'opencode_handoff_preview')
|
|
55
|
+
return 'Read-only OpenCode handoff preview; returns context for manual continuation only, does not execute/control OpenCode, write .opencode/provider config, or create runs, checkpoints, events, sessions, or skill-usage records.';
|
|
54
56
|
if (publicToolName === 'verification_plan')
|
|
55
57
|
return 'Read-only verification plan recommendations only; does not execute commands, write provider config, persist results, create checkpoints, or infer SDD acceptance.';
|
|
58
|
+
if (publicToolName === 'run_resume_inspect')
|
|
59
|
+
return 'Read-only run resume advisory inspect; plan-only and does not execute resume logic, invoke providers, write provider config, mutate retry/abandon/attempt state, or reconstruct sandboxes, worktrees, sessions, or transcripts.';
|
|
60
|
+
if (publicToolName === 'run_resume_gate')
|
|
61
|
+
return 'Read-only run resume gate advisory; plan-only and does not execute resume logic, invoke providers, write provider config, admit retries, abandon or mutate attempts, or reconstruct sandboxes, worktrees, sessions, or transcripts.';
|
|
56
62
|
if (publicToolName === 'sdd_cockpit')
|
|
57
63
|
return 'Read-only SDD cockpit summary with next decision, explicit acceptance state, metadata-only artifact summaries, and aggregate blockers.';
|
|
58
64
|
const toolName = toInternalVgxMcpToolName(publicToolName);
|
package/dist/mcp/validation.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isRiskyPermissionCategory, permissionCategories } from '../permissions/schema.js';
|
|
2
|
+
import { parseOperationRetryPolicy } from '../runs/operation-retry.js';
|
|
2
3
|
import { isSddPhase, sddPhases } from '../sdd/schema.js';
|
|
3
4
|
import { supportedTypeMessage, verificationChangeTypes } from '../verification/index.js';
|
|
4
5
|
import { errorEnvelope, isVgxMcpToolName, } from './schema.js';
|
|
@@ -77,6 +78,8 @@ export function validateVgxMcpToolCall(call) {
|
|
|
77
78
|
return validationSuccess(tool.value, validateSkillPayloadInput(input, tool.value));
|
|
78
79
|
case 'vgxness_opencode_manager_payload':
|
|
79
80
|
return validationSuccess(tool.value, validateOpenCodeManagerPayloadInput(input, tool.value));
|
|
81
|
+
case 'vgxness_opencode_handoff_preview':
|
|
82
|
+
return validationSuccess(tool.value, validateOpenCodeHandoffPreviewInput(input, tool.value));
|
|
80
83
|
case 'vgxness_run_list':
|
|
81
84
|
return validationSuccess(tool.value, validateRunListInput(input, tool.value));
|
|
82
85
|
case 'vgxness_run_get':
|
|
@@ -89,6 +92,10 @@ export function validateVgxMcpToolCall(call) {
|
|
|
89
92
|
return validationSuccess(tool.value, validateRunCheckpointInput(input, tool.value));
|
|
90
93
|
case 'vgxness_run_finalize':
|
|
91
94
|
return validationSuccess(tool.value, validateRunFinalizeInput(input, tool.value));
|
|
95
|
+
case 'vgxness_run_resume_inspect':
|
|
96
|
+
return validationSuccess(tool.value, validateRunResumeInspectInput(input, tool.value));
|
|
97
|
+
case 'vgxness_run_resume_gate':
|
|
98
|
+
return validationSuccess(tool.value, validateRunResumeGateInput(input, tool.value));
|
|
92
99
|
case 'vgxness_provider_status':
|
|
93
100
|
return validationSuccess(tool.value, validateProviderHealthInput(input, tool.value));
|
|
94
101
|
case 'vgxness_provider_doctor':
|
|
@@ -722,6 +729,40 @@ function validateOpenCodeManagerPayloadInput(input, tool) {
|
|
|
722
729
|
}
|
|
723
730
|
return { ok: true, value: result };
|
|
724
731
|
}
|
|
732
|
+
function validateOpenCodeHandoffPreviewInput(input, tool) {
|
|
733
|
+
const record = inputRecord(input, tool, ['project', 'scope', 'agentId', 'agentName', 'workspaceRoot', 'maxSourceBytes', 'change', 'phase']);
|
|
734
|
+
if (!record.ok)
|
|
735
|
+
return record;
|
|
736
|
+
const project = readNonEmptyString(record.value, 'project', tool);
|
|
737
|
+
if (!project.ok)
|
|
738
|
+
return project;
|
|
739
|
+
const result = { project: project.value, scope: 'project', agentName: 'vgxness-manager' };
|
|
740
|
+
const copied = copyOptionalStrings(result, record.value, tool, ['agentId', 'agentName', 'workspaceRoot']);
|
|
741
|
+
if (!copied.ok)
|
|
742
|
+
return copied;
|
|
743
|
+
const scope = readOptionalOneOf(record.value, 'scope', scopes, tool);
|
|
744
|
+
if (!scope.ok)
|
|
745
|
+
return scope;
|
|
746
|
+
result.scope = scope.value ?? 'project';
|
|
747
|
+
const change = readOptionalChange(record.value, tool);
|
|
748
|
+
if (!change.ok)
|
|
749
|
+
return change;
|
|
750
|
+
if (change.value !== undefined)
|
|
751
|
+
result.change = change.value;
|
|
752
|
+
if (record.value.phase !== undefined) {
|
|
753
|
+
const phase = readPhase(record.value, tool);
|
|
754
|
+
if (!phase.ok)
|
|
755
|
+
return phase;
|
|
756
|
+
result.phase = phase.value;
|
|
757
|
+
}
|
|
758
|
+
if (record.value.maxSourceBytes !== undefined) {
|
|
759
|
+
const maxSourceBytes = record.value.maxSourceBytes;
|
|
760
|
+
if (typeof maxSourceBytes !== 'number' || !Number.isSafeInteger(maxSourceBytes) || maxSourceBytes <= 0)
|
|
761
|
+
return validationFailure('maxSourceBytes must be a positive safe integer', tool);
|
|
762
|
+
result.maxSourceBytes = maxSourceBytes;
|
|
763
|
+
}
|
|
764
|
+
return { ok: true, value: result };
|
|
765
|
+
}
|
|
725
766
|
function validateRunListInput(input, tool) {
|
|
726
767
|
const record = inputRecord(input, tool, ['project', 'status', 'limit']);
|
|
727
768
|
if (!record.ok)
|
|
@@ -850,6 +891,31 @@ function validateRunFinalizeInput(input, tool) {
|
|
|
850
891
|
return copied;
|
|
851
892
|
return { ok: true, value: result };
|
|
852
893
|
}
|
|
894
|
+
function validateRunResumeInspectInput(input, tool) {
|
|
895
|
+
const record = inputRecord(input, tool, ['runId']);
|
|
896
|
+
if (!record.ok)
|
|
897
|
+
return record;
|
|
898
|
+
const runId = readNonEmptyString(record.value, 'runId', tool);
|
|
899
|
+
if (!runId.ok)
|
|
900
|
+
return runId;
|
|
901
|
+
return { ok: true, value: { runId: runId.value } };
|
|
902
|
+
}
|
|
903
|
+
function validateRunResumeGateInput(input, tool) {
|
|
904
|
+
const record = inputRecord(input, tool, ['approvalId', 'policy']);
|
|
905
|
+
if (!record.ok)
|
|
906
|
+
return record;
|
|
907
|
+
const approvalId = readNonEmptyString(record.value, 'approvalId', tool);
|
|
908
|
+
if (!approvalId.ok)
|
|
909
|
+
return approvalId;
|
|
910
|
+
const result = { approvalId: approvalId.value };
|
|
911
|
+
if (record.value.policy !== undefined) {
|
|
912
|
+
const policy = parseOperationRetryPolicy(record.value.policy);
|
|
913
|
+
if (!policy.ok)
|
|
914
|
+
return validationFailure(policy.errors.join('; '), tool);
|
|
915
|
+
result.policy = policy.policy;
|
|
916
|
+
}
|
|
917
|
+
return { ok: true, value: result };
|
|
918
|
+
}
|
|
853
919
|
function validationSuccess(tool, validation) {
|
|
854
920
|
if (!validation.ok)
|
|
855
921
|
return validation;
|
|
@@ -898,6 +964,11 @@ function readChange(record, tool) {
|
|
|
898
964
|
return validationFailure(`Invalid SDD change id: ${change.value}`, tool);
|
|
899
965
|
return change;
|
|
900
966
|
}
|
|
967
|
+
function readOptionalChange(record, tool) {
|
|
968
|
+
if (record.change === undefined)
|
|
969
|
+
return { ok: true, value: undefined };
|
|
970
|
+
return readChange(record, tool);
|
|
971
|
+
}
|
|
901
972
|
function readPhase(record, tool) {
|
|
902
973
|
const phase = readNonEmptyString(record, 'phase', tool);
|
|
903
974
|
if (!phase.ok)
|
|
@@ -52,6 +52,9 @@ export class SessionRepository {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
appendActivity(input) {
|
|
55
|
+
const session = this.getById(input.sessionId);
|
|
56
|
+
if (!session.ok)
|
|
57
|
+
return session;
|
|
55
58
|
const result = this.db.transaction(() => {
|
|
56
59
|
const next = this.db.connection
|
|
57
60
|
.prepare('SELECT COALESCE(MAX(sequence), 0) + 1 AS sequence FROM session_activity WHERE session_id=?')
|
|
@@ -40,9 +40,11 @@ function linuxDataPath(env) {
|
|
|
40
40
|
return homeRelativePath(env, ['.local', 'share', 'vgxness', 'memory.sqlite']);
|
|
41
41
|
}
|
|
42
42
|
function windowsAppDataPath(env) {
|
|
43
|
-
if (
|
|
44
|
-
return
|
|
45
|
-
|
|
43
|
+
if (hasPathValue(env.LOCALAPPDATA))
|
|
44
|
+
return { ok: true, value: path.win32.join(env.LOCALAPPDATA, 'vgxness', 'memory.sqlite') };
|
|
45
|
+
if (hasPathValue(env.APPDATA))
|
|
46
|
+
return { ok: true, value: path.win32.join(env.APPDATA, 'vgxness', 'memory.sqlite') };
|
|
47
|
+
return missingBase('LOCALAPPDATA or APPDATA');
|
|
46
48
|
}
|
|
47
49
|
function homeRelativePath(env, segments) {
|
|
48
50
|
const home = userHome(env);
|
|
@@ -459,6 +459,8 @@ function phaseFromTopicKey(change, topicKey) {
|
|
|
459
459
|
return 'explore';
|
|
460
460
|
}
|
|
461
461
|
function blockerReasonForStatus(status) {
|
|
462
|
+
if (status.legacy === true)
|
|
463
|
+
return 'legacy';
|
|
462
464
|
const state = status.state ?? (status.present ? 'draft' : 'missing');
|
|
463
465
|
if (state === 'missing')
|
|
464
466
|
return 'missing';
|
package/docs/cli.md
CHANGED
|
@@ -742,7 +742,7 @@ By default, vgxness uses a global user data database and creates the parent dire
|
|
|
742
742
|
|---|---|
|
|
743
743
|
| macOS | `~/Library/Application Support/vgxness/memory.sqlite` |
|
|
744
744
|
| Linux | `${XDG_DATA_HOME:-~/.local/share}/vgxness/memory.sqlite` |
|
|
745
|
-
| Windows | `%APPDATA%\\vgxness\\memory.sqlite` |
|
|
745
|
+
| Windows | `%LOCALAPPDATA%\\vgxness\\memory.sqlite` when available; otherwise `%APPDATA%\\vgxness\\memory.sqlite` |
|
|
746
746
|
|
|
747
747
|
Selection precedence is `--db <path>` (selected source: `flag`) > `VGXNESS_DB_PATH` (selected source: `environment`) > global default (selected source: `global-default`). The resolver fails clearly if no user data base directory is available instead of falling back to the current working directory.
|
|
748
748
|
|
|
@@ -149,7 +149,7 @@ Ruta por defecto de usuario:
|
|
|
149
149
|
```text
|
|
150
150
|
macOS: ~/Library/Application Support/vgxness/memory.sqlite
|
|
151
151
|
Linux: ${XDG_DATA_HOME:-~/.local/share}/vgxness/memory.sqlite
|
|
152
|
-
Windows: %APPDATA%\vgxness\memory.sqlite
|
|
152
|
+
Windows: %LOCALAPPDATA%\vgxness\memory.sqlite; fallback a %APPDATA%\vgxness\memory.sqlite
|
|
153
153
|
```
|
|
154
154
|
|
|
155
155
|
La ruta `.vgx/memory.sqlite` se sigue usando para dogfood local o pruebas cuando se pasa explicitamente con `--db`.
|
|
@@ -407,7 +407,7 @@ Por defecto, la CLI instalada usa una base global de usuario. Esto permite insta
|
|
|
407
407
|
|---|---|
|
|
408
408
|
| macOS | `~/Library/Application Support/vgxness/memory.sqlite` |
|
|
409
409
|
| Linux | `${XDG_DATA_HOME:-~/.local/share}/vgxness/memory.sqlite` |
|
|
410
|
-
| Windows | `%APPDATA%\vgxness\memory.sqlite` |
|
|
410
|
+
| Windows | `%LOCALAPPDATA%\vgxness\memory.sqlite` si existe; si no, `%APPDATA%\vgxness\memory.sqlite` |
|
|
411
411
|
|
|
412
412
|
La precedencia de seleccion de base es:
|
|
413
413
|
|