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.
@@ -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) => ({ phase: artifact.phase, role: artifact.role, bytes: artifact.content.length })),
126
- next: { ...sddContext.next, missingArtifactTopicKeys: [...sddContext.next.missingArtifactTopicKeys] },
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
- missingArtifactTopicKeys: [...context.next.missingArtifactTopicKeys],
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, content] of Object.entries(initialArtifacts))
30
- this.artifacts.set(key, content);
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
- phase,
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 content = this.artifacts.get(artifactKey(input.project, input.change, input.phase));
43
- return content === undefined ? null : { content };
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)?.present);
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 missing prerequisite artifacts.`,
75
- recommendedAction: `Create or restore ${missingArtifactTopicKeys.join(', ')} before running ${missing}.`,
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 ?? (input.artifact.role === 'accepted' ? 'accepted' : 'draft'),
51
- accepted: input.artifact.accepted ?? input.artifact.role === '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 ? 'accepted' : 'missing';
68
+ const state = typeof value?.state === 'string' ? value.state : value?.present === true ? 'draft' : 'missing';
69
69
  return {
70
70
  state,
71
- accepted: typeof value?.accepted === 'boolean' ? value.accepted : state === '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) => ({ phase: phase.phase, present: phase.present, topicKey: phase.topicKey })),
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 providerStatus = new ProviderStatusService({ sdd: new SddWorkflowService(memory, { actor: 'mcp-control-plane' }) });
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: new SddWorkflowService(memory, { actor: 'mcp-control-plane' }),
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
+ }
@@ -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(),
@@ -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);
@@ -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 (!hasPathValue(env.APPDATA))
44
- return missingBase('APPDATA');
45
- return { ok: true, value: path.win32.join(env.APPDATA, 'vgxness', 'memory.sqlite') };
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vgxness",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
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": {