vgxness 1.20.0 → 1.20.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.
@@ -168,7 +168,7 @@ Coordinate SDD; keep chat thin, use VGXNESS MCP state, delegate to exact SDD sub
168
168
  - Before phase advancement, call \`vgxness_sdd_status\`/\`vgxness_sdd_next\` plus \`vgxness_sdd_ready\` or \`vgxness_sdd_get_readiness\`.
169
169
  - Before risky VGX-managed side effects (edit, shell/tests, git, network, provider-tool, secrets, external-directory, destructive, privileged, ambiguous), call \`vgxness_run_preflight\` with runId/workflow/phase/agent when available. Stop on approval/block; never invent approval.
170
170
  - Direct human acceptance via \`vgxness_sdd_accept_artifact\` is not a generic SDD write: do not preflight solely for it; require exact project/change/phase, \`acceptedBy.type\` \`"human"\`, non-empty \`acceptedBy.id\`, and eligible status/readiness. Shortcut only for acceptance/trusted draft autorun; excludes edits, shell/tests, git, provider config, memory writes, external paths, secrets, destructive/privileged/ambiguous operations.
171
- - manager native repo tools are disabled in config (read/glob/edit/write/bash); report config-level enforcement, not proof of host runtime behavior. Subagents keep phase tools.
171
+ - bounded read/search repo tools may be enabled for inspection; bash, edit, and write remain denied. Diagnostics are config-level evidence, not host runtime proof.
172
172
  - Do not mutate provider/global OpenCode config. Do not publish packages unless explicitly requested. Never revert/overwrite unrelated user work. Preserve \`permission.task\` deny-by-default with only exact known SDD subagents.
173
173
  - Do not change provider model/reasoning config unless explicitly requested.
174
174
 
@@ -137,7 +137,7 @@ function openCodeManagerPermissionsFor(agent) {
137
137
  ...openCodePermissionsFor(agent, { task: createCanonicalOpenCodeSddTaskPermissions() }),
138
138
  bash: 'deny',
139
139
  edit: 'deny',
140
- read: 'deny',
140
+ read: 'allow',
141
141
  write: 'deny',
142
142
  };
143
143
  }
@@ -148,9 +148,9 @@ function openCodeManagerTools() {
148
148
  delegation_list: true,
149
149
  delegation_read: true,
150
150
  edit: false,
151
- glob: false,
152
- grep: false,
153
- read: false,
151
+ glob: true,
152
+ grep: true,
153
+ read: true,
154
154
  write: false,
155
155
  };
156
156
  }
@@ -15,7 +15,7 @@ const openCodeCapabilities = {
15
15
  'instructions',
16
16
  'mode',
17
17
  'adapter model',
18
- 'adapter config keys: variant, temperature, top_p, hidden, options, color, steps, permission',
18
+ 'adapter config keys: variant, temperature, top_p, hidden, options, color, steps, permission, tools',
19
19
  ],
20
20
  skillInjectionModes: ['external preview services may inject skill payloads before rendering; renderer does not resolve skills'],
21
21
  permissionCategories: ['registry permission metadata mapped from adapter permission config'],
@@ -78,6 +78,7 @@ function openCodeAgentConfig(agent) {
78
78
  copyString(config, rendered, 'color');
79
79
  copyNumber(config, rendered, 'steps');
80
80
  copyJson(config, rendered, 'permission');
81
+ copyJson(config, rendered, 'tools');
81
82
  return rendered;
82
83
  }
83
84
  function copyString(source, target, key) {
@@ -197,7 +197,11 @@ function warningsForScope(scope, overwriteVgxness, agentPlan, bashPermissionPoli
197
197
  `Reinstall/overwrite is enabled: existing VGXNESS-managed OpenCode entries will be replaced (${agentPlan.installsAgents ? 'mcp.vgxness, default_agent, instructions AGENTS.md, and known VGXNESS agents' : 'mcp.vgxness only'}); unrelated OpenCode config is preserved.`,
198
198
  ]
199
199
  : [];
200
- const bashWarnings = bashPermissionPolicy.manager === 'deny' ? ['OpenCode top-level permission.bash is set to ask; the VGXNESS manager agent denies bash while SDD subagents keep explicit phase permissions.'] : ['OpenCode top-level permission.bash is set to ask.'];
200
+ const bashWarnings = bashPermissionPolicy.manager === 'deny'
201
+ ? [
202
+ 'OpenCode config sets top-level permission.bash = ask; manager read/search discovery tools (read/glob/grep) are allowed while manager bash/edit/write are denied; SDD subagents keep explicit phase permissions. This is config-level evidence only and does not verify provider runtime enforcement.',
203
+ ]
204
+ : ['OpenCode top-level permission.bash is set to ask.'];
201
205
  return [
202
206
  'Restart OpenCode after installation so it reloads the user MCP config.',
203
207
  'OpenCode project config may override user config for a workspace; check project-level config if vgxness is not visible.',
@@ -214,7 +214,10 @@ function confirmationRequiredMessage(scope) {
214
214
  return `\`mcp install opencode\` requires explicit --yes before any ${scope} config write.`;
215
215
  }
216
216
  function warnings() {
217
- return ['Restart OpenCode after installation so it reloads the project MCP config.', 'OpenCode top-level permission.bash is set to ask; the VGXNESS manager agent denies bash while SDD subagents keep explicit phase permissions.'];
217
+ return [
218
+ 'Restart OpenCode after installation so it reloads the project MCP config.',
219
+ 'OpenCode config sets top-level permission.bash = ask; manager read/search discovery tools (read/glob/grep) are allowed while manager bash/edit/write are denied; SDD subagents keep explicit phase permissions. This is config-level evidence only and does not verify provider runtime enforcement.',
220
+ ];
218
221
  }
219
222
  function manualTest(databasePath, source) {
220
223
  return {
@@ -308,19 +308,16 @@ function governanceSddServices(services) {
308
308
  };
309
309
  }
310
310
  function sddContinueEnvelope(input, services) {
311
- const next = services.sdd.getNext({ project: input.project, change: input.change });
312
- if (!next.ok)
313
- return errorEnvelope(next.error.code, next.error.message, 'vgxness_sdd_continue');
314
- const cockpit = services.sdd.getCockpit({ project: input.project, change: input.change });
315
- if (!cockpit.ok)
316
- return errorEnvelope(cockpit.error.code, cockpit.error.message, 'vgxness_sdd_continue');
311
+ const context = services.sdd.getContinuationContext({ project: input.project, change: input.change });
312
+ if (!context.ok)
313
+ return errorEnvelope(context.error.code, context.error.message, 'vgxness_sdd_continue');
317
314
  const relatedRun = services.runs.findRelatedInterruptedSddRun({ project: input.project, change: input.change });
318
315
  if (!relatedRun.ok)
319
316
  return errorEnvelope(relatedRun.error.code, relatedRun.error.message, 'vgxness_sdd_continue');
320
317
  return successEnvelope('vgxness_sdd_continue', sddContinuationPlanFrom({
321
318
  project: input.project,
322
- next: next.value,
323
- cockpit: cockpit.value,
319
+ next: context.value.next,
320
+ cockpit: context.value.cockpit,
324
321
  ...(relatedRun.value === undefined ? {} : { relatedRunContext: relatedRun.value }),
325
322
  }));
326
323
  }
@@ -70,23 +70,42 @@ export class OpenCodeHandoffPreviewService {
70
70
  return { summary: { ...summary, unavailableReason: reason }, inspection: notInspectedSdd(reason) };
71
71
  }
72
72
  const status = this.deps.sdd.getStatus({ project: input.project, change: input.change });
73
- const next = this.deps.sdd.getNext({ project: input.project, change: input.change });
74
- const inspection = { inspected: true, statusQueried: true, nextQueried: true, readinessQueried: false };
73
+ const inspection = { inspected: true, statusQueried: true, nextQueried: true, readinessQueried: input.phase !== undefined };
75
74
  if (status.ok)
76
75
  summary.status = status.value;
77
76
  else
78
77
  warnings.push(`SDD status unavailable: ${status.error.message}`);
79
- if (next.ok)
80
- summary.next = next.value;
81
- else
82
- warnings.push(`SDD next phase unavailable: ${next.error.message}`);
83
- if (input.phase !== undefined) {
84
- const ready = this.deps.sdd.getReady({ project: input.project, change: input.change, phase: input.phase });
85
- inspection.readinessQueried = true;
86
- if (ready.ok)
87
- summary.readiness = ready.value;
78
+ const continuation = this.deps.sdd.getContinuationContext?.({ project: input.project, change: input.change });
79
+ if (continuation !== undefined) {
80
+ if (continuation.ok) {
81
+ const continuationNext = continuationNextValue(continuation.value);
82
+ if (continuationNext !== undefined)
83
+ summary.next = continuationNext;
84
+ if (input.phase !== undefined) {
85
+ const continuationReadiness = continuationReadinessValue(continuation.value, input.phase);
86
+ if (continuationReadiness !== undefined)
87
+ summary.readiness = continuationReadiness;
88
+ }
89
+ }
90
+ else {
91
+ warnings.push(`SDD continuation context unavailable: ${continuation.error.message}`);
92
+ }
93
+ }
94
+ if (summary.next === undefined) {
95
+ const next = this.deps.sdd.getNext({ project: input.project, change: input.change });
96
+ if (next.ok)
97
+ summary.next = next.value;
88
98
  else
89
- warnings.push(`SDD readiness unavailable: ${ready.error.message}`);
99
+ warnings.push(`SDD next phase unavailable: ${next.error.message}`);
100
+ }
101
+ if (input.phase !== undefined) {
102
+ if (summary.readiness === undefined) {
103
+ const ready = this.deps.sdd.getReady({ project: input.project, change: input.change, phase: input.phase });
104
+ if (ready.ok)
105
+ summary.readiness = ready.value;
106
+ else
107
+ warnings.push(`SDD readiness unavailable: ${ready.error.message}`);
108
+ }
90
109
  }
91
110
  return { summary, inspection };
92
111
  }
@@ -108,6 +127,47 @@ export class OpenCodeHandoffPreviewService {
108
127
  function notInspectedSdd(reason) {
109
128
  return { inspected: false, statusQueried: false, nextQueried: false, readinessQueried: false, notInspectedReason: reason };
110
129
  }
130
+ function continuationNextValue(value) {
131
+ const record = asRecord(value);
132
+ if (record === undefined)
133
+ return undefined;
134
+ const next = record.next;
135
+ return next === null ? undefined : next;
136
+ }
137
+ function continuationReadinessValue(value, phase) {
138
+ const record = asRecord(value);
139
+ if (record === undefined)
140
+ return undefined;
141
+ return readinessFromPhaseEntries(asRecord(record.cockpit)?.phases, phase) ?? readinessFromPhaseEntries(record.phases, phase) ?? readinessFromPhaseEntries(asRecord(record.readModel)?.phases, phase) ?? readinessFromReadinessMap(record.readinessByPhase, phase) ?? readinessFromReadinessMap(record.phaseReadiness, phase) ?? matchingReadiness(record.readiness, phase);
142
+ }
143
+ function readinessFromPhaseEntries(value, phase) {
144
+ if (!Array.isArray(value))
145
+ return undefined;
146
+ for (const entry of value) {
147
+ const record = asRecord(entry);
148
+ if (record?.phase === phase)
149
+ return record.readiness === null ? undefined : record.readiness;
150
+ }
151
+ return undefined;
152
+ }
153
+ function readinessFromReadinessMap(value, phase) {
154
+ const record = asRecord(value);
155
+ if (record === undefined)
156
+ return undefined;
157
+ const readiness = record[phase];
158
+ return readiness === null ? undefined : readiness;
159
+ }
160
+ function matchingReadiness(value, phase) {
161
+ const record = asRecord(value);
162
+ if (record === undefined)
163
+ return undefined;
164
+ if (record.phase !== undefined && record.phase !== phase)
165
+ return undefined;
166
+ return value;
167
+ }
168
+ function asRecord(value) {
169
+ return typeof value === 'object' && value !== null ? value : undefined;
170
+ }
111
171
  function managerInput(input, scope) {
112
172
  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' };
113
173
  }
@@ -40,6 +40,7 @@ export class ProviderDoctorService {
40
40
  configReadableCheck(readableJson.length, paths.filter((path) => path.exists).length),
41
41
  mcpEntryCheck(config),
42
42
  defaultAgentCheck(config),
43
+ managerNativeCapabilityCheck(config),
43
44
  subagentsCheck(config),
44
45
  delegationCheck(config),
45
46
  { id: 'mcp-current-call', status: 'pass', detail: 'Current MCP call reached the VGXNESS control-plane.' },
@@ -255,6 +256,35 @@ function defaultAgentCheck(config) {
255
256
  return { id: 'opencode-default-agent', status: 'fail', detail: 'OpenCode default_agent is not vgxness-manager or the manager config is missing.' };
256
257
  return { id: 'opencode-default-agent', status: 'pass', detail: 'OpenCode default agent points to vgxness-manager.' };
257
258
  }
259
+ function managerNativeCapabilityCheck(config) {
260
+ const manager = isRecord(config?.agent) && isRecord(config.agent[vgxnessOpenCodeDefaultAgent]) ? config.agent[vgxnessOpenCodeDefaultAgent] : undefined;
261
+ const permission = isRecord(manager?.permission) ? manager.permission : undefined;
262
+ const tools = isRecord(manager?.tools) ? manager.tools : undefined;
263
+ const problems = [];
264
+ if (permission?.read !== 'allow')
265
+ problems.push('permission.read is not allow');
266
+ for (const tool of ['read', 'glob', 'grep']) {
267
+ if (tools?.[tool] !== true)
268
+ problems.push(`tools.${tool} is not enabled`);
269
+ }
270
+ for (const tool of ['bash', 'edit', 'write']) {
271
+ if (tools?.[tool] !== false)
272
+ problems.push(`tools.${tool} is not disabled`);
273
+ }
274
+ const evidenceSuffix = 'This is config-level evidence only; host runtime/tool presence is not verified.';
275
+ if (problems.length > 0)
276
+ return {
277
+ id: 'opencode-manager-native-capability',
278
+ status: 'fail',
279
+ detail: `Manager native capability drift detected: ${problems.join('; ')}. ${evidenceSuffix}`,
280
+ remediation: 'Re-run explicit OpenCode setup/install only after reviewing provider config drift; doctor does not auto-repair or write config.',
281
+ };
282
+ return {
283
+ id: 'opencode-manager-native-capability',
284
+ status: 'pass',
285
+ detail: `Manager config allows read/glob/grep inspection and denies bash/edit/write native tools. ${evidenceSuffix}`,
286
+ };
287
+ }
258
288
  function subagentsCheck(config) {
259
289
  const agents = isRecord(config?.agent) ? config.agent : undefined;
260
290
  const missing = vgxnessOpenCodeSddSubagents.filter((name) => !isRecord(agents?.[name]) || agents[name]?.hidden !== true);
@@ -27,10 +27,11 @@ export class ProviderStatusService {
27
27
  const mcpEntry = inspectOpenCodeMcpEntry(paths);
28
28
  const providerConfig = readFirstParsedConfig(paths);
29
29
  const managerConfigured = hasConfiguredManager(providerConfig);
30
+ const managerNativeCapabilityStatus = inspectManagerNativeCapability(providerConfig);
30
31
  const subagentsConfigured = hasConfiguredSubagents(providerConfig);
31
32
  const tools = requiredToolPresence();
32
33
  const evidence = providerEvidence({ staticManifestKnown: true, renderedConfigExpected: true, configPathInspected: paths.length > 0 });
33
- const configStatus = resolveConfigStatus(paths, mcpEntry, managerConfigured, subagentsConfigured);
34
+ const configStatus = resolveConfigStatus(paths, mcpEntry, managerConfigured, managerNativeCapabilityStatus, subagentsConfigured);
34
35
  const status = rollupProviderHealth([canonicalAgentManifest.status, configStatus, ...tools.map((tool) => (tool.present ? 'pass' : 'fail'))]);
35
36
  const sdd = normalized.change.length > 0 ? this.readSdd(normalized.project, normalized.change) : undefined;
36
37
  const generatedAt = 'read-only-snapshot';
@@ -38,14 +39,14 @@ export class ProviderStatusService {
38
39
  ? paths.map((path) => path.path)
39
40
  : paths.filter((path) => path.exists || path.status !== 'not-configured').map((path) => path.path);
40
41
  const verboseShape = {
41
- config: { status: configStatus, paths, mcpEntry },
42
+ config: { status: configStatus, paths, mcpEntry, managerNativeCapability: managerNativeCapabilityStatus },
42
43
  canonicalAgentManifest,
43
44
  providerEvidence: evidence,
44
45
  sdd,
45
46
  mcpRequiredTools: tools,
46
47
  };
47
48
  const compactShape = {
48
- config: { status: configStatus, paths: compactPaths(paths, 'compact'), mcpEntry: compactMcpEntry(mcpEntry, 'compact') },
49
+ config: { status: configStatus, paths: compactPaths(paths, 'compact'), mcpEntry: compactMcpEntry(mcpEntry, 'compact'), managerNativeCapability: managerNativeCapabilityStatus },
49
50
  canonicalAgentManifest,
50
51
  providerEvidence: evidence,
51
52
  sdd: sdd === undefined ? undefined : compactSdd(sdd, 'compact'),
@@ -86,13 +87,32 @@ export class ProviderStatusService {
86
87
  return {
87
88
  ok: true,
88
89
  value: normalized.payloadMode === 'verbose'
89
- ? { ...reportBase, payloadMode: 'verbose', config: { status: configStatus, paths: compactPaths(paths, 'verbose'), mcpEntry: compactMcpEntry(mcpEntry, 'verbose') } }
90
- : { ...reportBase, payloadMode: 'compact', config: { status: configStatus, paths: compactPaths(paths, 'compact'), mcpEntry: compactMcpEntry(mcpEntry, 'compact') } },
90
+ ? { ...reportBase, payloadMode: 'verbose', config: { status: configStatus, paths: compactPaths(paths, 'verbose'), mcpEntry: compactMcpEntry(mcpEntry, 'verbose'), managerNativeCapability: managerNativeCapabilityStatus } }
91
+ : { ...reportBase, payloadMode: 'compact', config: { status: configStatus, paths: compactPaths(paths, 'compact'), mcpEntry: compactMcpEntry(mcpEntry, 'compact'), managerNativeCapability: managerNativeCapabilityStatus } },
91
92
  };
92
93
  }
93
94
  readSdd(project, change) {
94
95
  if (this.deps.sdd === undefined)
95
96
  return { change };
97
+ const continuation = this.deps.sdd.getContinuationContext?.({ project, change });
98
+ if (continuation?.ok === true && isRecord(continuation.value)) {
99
+ const continuationNext = isRecord(continuation.value.next) ? continuation.value.next : undefined;
100
+ const continuationStatus = projectSddCockpitStatus(continuation.value.cockpit, change, continuationNext);
101
+ if (continuationNext !== undefined) {
102
+ if (continuationStatus !== undefined)
103
+ return { change, status: continuationStatus, next: continuationNext };
104
+ const status = this.deps.sdd.getStatus({ project, change });
105
+ return { change, ...(status.ok ? { status: status.value } : {}), next: continuationNext };
106
+ }
107
+ if (continuationStatus !== undefined) {
108
+ const next = this.deps.sdd.getNext({ project, change });
109
+ return {
110
+ change,
111
+ status: projectSddCockpitStatus(continuation.value.cockpit, change, next.ok && isRecord(next.value) ? next.value : undefined),
112
+ ...(next.ok && isRecord(next.value) ? { next: next.value } : {}),
113
+ };
114
+ }
115
+ }
96
116
  const status = this.deps.sdd.getStatus({ project, change });
97
117
  const next = this.deps.sdd.getNext({ project, change });
98
118
  return { change, ...(status.ok ? { status: status.value } : {}), ...(next.ok ? { next: next.value } : {}) };
@@ -257,6 +277,31 @@ function compactSdd(sdd, mode) {
257
277
  ...(sdd.next === undefined ? {} : { next: compactUnknown(sdd.next) }),
258
278
  };
259
279
  }
280
+ function projectSddCockpitStatus(cockpit, change, next) {
281
+ if (!isRecord(cockpit) || !Array.isArray(cockpit.phases))
282
+ return undefined;
283
+ const phases = cockpit.phases.map((phase) => projectSddPhaseStatus(phase));
284
+ if (phases.some((phase) => phase === undefined))
285
+ return undefined;
286
+ return {
287
+ change,
288
+ phases,
289
+ ...(next?.status === 'runnable' && typeof next.nextPhase === 'string' ? { nextReadyPhase: next.nextPhase } : {}),
290
+ };
291
+ }
292
+ function projectSddPhaseStatus(phase) {
293
+ if (!isRecord(phase) || typeof phase.phase !== 'string' || typeof phase.topicKey !== 'string' || typeof phase.present !== 'boolean')
294
+ return undefined;
295
+ return {
296
+ phase: phase.phase,
297
+ topicKey: phase.topicKey,
298
+ present: phase.present,
299
+ ...(typeof phase.state === 'string' ? { state: phase.state } : {}),
300
+ ...(typeof phase.accepted === 'boolean' ? { accepted: phase.accepted } : {}),
301
+ ...(typeof phase.legacy === 'boolean' ? { legacy: phase.legacy } : {}),
302
+ ...(typeof phase.artifactId === 'string' ? { artifactId: phase.artifactId } : {}),
303
+ };
304
+ }
260
305
  function compactUnknown(value) {
261
306
  if (!isRecord(value))
262
307
  return value;
@@ -349,12 +394,12 @@ function requiredToolPresence() {
349
394
  diagnostic: 'Static VGXNESS manifest expects this MCP tool; provider host tool presence was not verified.',
350
395
  }));
351
396
  }
352
- function resolveConfigStatus(paths, mcpEntry, managerConfigured, subagentsConfigured) {
397
+ function resolveConfigStatus(paths, mcpEntry, managerConfigured, managerNativeCapabilityStatus, subagentsConfigured) {
353
398
  if (paths.some((path) => path.status === 'fail') || mcpEntry.status === 'fail')
354
399
  return 'fail';
355
400
  if (mcpEntry.status === 'not-configured')
356
401
  return 'not-configured';
357
- if (!managerConfigured || !subagentsConfigured)
402
+ if (!managerConfigured || managerNativeCapabilityStatus === 'warn' || !subagentsConfigured)
358
403
  return 'warn';
359
404
  if (hasProjectConfigAlongsideUser(paths))
360
405
  return 'warn';
@@ -405,6 +450,17 @@ function hasConfiguredManager(config) {
405
450
  isRecord(agent?.[vgxnessOpenCodeDefaultAgent]) &&
406
451
  promptContractVersion(agent[vgxnessOpenCodeDefaultAgent]) === vgxnessOpenCodePromptContractVersion);
407
452
  }
453
+ function inspectManagerNativeCapability(config) {
454
+ if (config === undefined)
455
+ return 'not-configured';
456
+ const agent = isRecord(config.agent) ? config.agent : undefined;
457
+ const manager = isRecord(agent?.[vgxnessOpenCodeDefaultAgent]) ? agent[vgxnessOpenCodeDefaultAgent] : undefined;
458
+ const permission = isRecord(manager?.permission) ? manager.permission : undefined;
459
+ const tools = isRecord(manager?.tools) ? manager.tools : undefined;
460
+ const readSearchEnabled = permission?.read === 'allow' && tools?.read === true && tools.glob === true && tools.grep === true;
461
+ const writeShellDenied = tools?.bash === false && tools.edit === false && tools.write === false;
462
+ return readSearchEnabled && writeShellDenied ? 'pass' : 'warn';
463
+ }
408
464
  function hasConfiguredSubagents(config) {
409
465
  if (config === undefined)
410
466
  return false;
@@ -80,57 +80,17 @@ export class SddWorkflowService {
80
80
  const snapshot = this.loadPhaseSnapshot(validated.value.project, validated.value.change);
81
81
  if (!snapshot.ok)
82
82
  return snapshot;
83
- const phases = snapshot.value.phases;
84
- const next = nextDecisionFromStatuses(validated.value.change, phases, this.options);
85
- const cockpitPhases = phases.map((phaseStatus) => {
86
- const readiness = {
87
- change: validated.value.change,
88
- phase: phaseStatus.phase,
89
- ...getReadinessFromStatuses(validated.value.change, phaseStatus.phase, phases, this.options),
90
- };
91
- const artifact = phaseStatus.present ? cockpitArtifactSummaryFromSnapshotItem(phaseStatus) : undefined;
92
- const blockers = cockpitBlockersForPhase(phaseStatus, readiness);
93
- const gates = phaseGateFromStatus(phaseStatus, readiness);
94
- return {
95
- phase: phaseStatus.phase,
96
- topicKey: phaseStatus.topicKey,
97
- present: phaseStatus.present,
98
- accepted: phaseStatus.accepted === true,
99
- legacy: phaseStatus.legacy === true,
100
- state: phaseStatus.state ?? 'missing',
101
- readiness,
102
- ...(artifact === undefined ? {} : { artifact }),
103
- blockers,
104
- gates,
105
- };
106
- });
107
- const artifacts = cockpitPhases.map((phase) => phase.artifact).filter((artifact) => artifact !== undefined);
108
- const aggregateBlockers = dedupeCockpitBlockers([
109
- ...cockpitPhases.flatMap((phase) => phase.blockers),
110
- ...(next.missingPrerequisiteTopicKeys ?? []).map((topicKey) => blockerFromTopicKey(validated.value.change, topicKey)),
111
- ...(next.blockedPrerequisites ?? []).map((blocker) => ({
112
- kind: 'readiness',
113
- phase: blocker.phase,
114
- topicKey: blocker.topicKey,
115
- reason: blocker.reason,
116
- ...(blocker.artifactId === undefined ? {} : { artifactId: blocker.artifactId }),
117
- })),
118
- ]);
119
- const cockpit = {
120
- project: validated.value.project,
121
- change: validated.value.change,
122
- ...(next.nextPhase === undefined ? {} : { actionablePhase: next.nextPhase }),
123
- next,
124
- recommendedAction: cockpitRecommendedAction(next, aggregateBlockers),
125
- phases: cockpitPhases,
126
- artifacts,
127
- acceptedCount: cockpitPhases.filter((phase) => phase.accepted).length,
128
- legacyCount: cockpitPhases.filter((phase) => phase.legacy).length,
129
- aggregateBlockers,
130
- gates: cockpitGatesFromPhases(cockpitPhases, next),
131
- inspectCommand: `vgxness sdd cockpit --project ${validated.value.project} --change ${validated.value.change} --json`,
132
- };
133
- return ok(cockpit);
83
+ return ok(this.cockpitFromSnapshot(snapshot.value));
84
+ }
85
+ getContinuationContext(input) {
86
+ const validated = validateProjectAndChange(input.project, input.change);
87
+ if (!validated.ok)
88
+ return validated;
89
+ const snapshot = this.loadPhaseSnapshot(validated.value.project, validated.value.change);
90
+ if (!snapshot.ok)
91
+ return snapshot;
92
+ const cockpit = this.cockpitFromSnapshot(snapshot.value);
93
+ return ok({ next: cockpit.next, cockpit });
134
94
  }
135
95
  getGovernanceSnapshot(input) {
136
96
  const validated = validateProjectAndChange(input.project, input.change);
@@ -306,13 +266,10 @@ export class SddWorkflowService {
306
266
  },
307
267
  });
308
268
  }
309
- const listed = this.memory.listArtifactsByTopicPrefix(validated.value.project, `sdd/${validated.value.change}/`, this.context);
310
- if (!listed.ok)
311
- return listed;
312
- const artifactsByTopicKey = new Map(listed.value.map((artifact) => [artifact.topicKey, artifact]));
313
- const artifacts = sddPhases
314
- .map((phase) => artifactsByTopicKey.get(sddTopicKey(validated.value.change, phase)))
315
- .filter((artifact) => artifact !== undefined);
269
+ const snapshot = this.loadPhaseSnapshot(validated.value.project, validated.value.change);
270
+ if (!snapshot.ok)
271
+ return snapshot;
272
+ const artifacts = snapshot.value.phases.flatMap((phase) => (phase.artifact === undefined ? [] : [phase.artifact]));
316
273
  return ok({ project: validated.value.project, change: validated.value.change, artifacts });
317
274
  }
318
275
  validatePhaseInput(input) {
@@ -358,6 +315,58 @@ export class SddWorkflowService {
358
315
  });
359
316
  return ok({ project, change, phases });
360
317
  }
318
+ cockpitFromSnapshot(snapshot) {
319
+ const phases = snapshot.phases;
320
+ const next = nextDecisionFromStatuses(snapshot.change, phases, this.options);
321
+ const cockpitPhases = phases.map((phaseStatus) => {
322
+ const readiness = {
323
+ change: snapshot.change,
324
+ phase: phaseStatus.phase,
325
+ ...getReadinessFromStatuses(snapshot.change, phaseStatus.phase, phases, this.options),
326
+ };
327
+ const artifact = phaseStatus.present ? cockpitArtifactSummaryFromSnapshotItem(phaseStatus) : undefined;
328
+ const blockers = cockpitBlockersForPhase(phaseStatus, readiness);
329
+ const gates = phaseGateFromStatus(phaseStatus, readiness);
330
+ return {
331
+ phase: phaseStatus.phase,
332
+ topicKey: phaseStatus.topicKey,
333
+ present: phaseStatus.present,
334
+ accepted: phaseStatus.accepted === true,
335
+ legacy: phaseStatus.legacy === true,
336
+ state: phaseStatus.state ?? 'missing',
337
+ readiness,
338
+ ...(artifact === undefined ? {} : { artifact }),
339
+ blockers,
340
+ gates,
341
+ };
342
+ });
343
+ const artifacts = cockpitPhases.map((phase) => phase.artifact).filter((artifact) => artifact !== undefined);
344
+ const aggregateBlockers = dedupeCockpitBlockers([
345
+ ...cockpitPhases.flatMap((phase) => phase.blockers),
346
+ ...(next.missingPrerequisiteTopicKeys ?? []).map((topicKey) => blockerFromTopicKey(snapshot.change, topicKey)),
347
+ ...(next.blockedPrerequisites ?? []).map((blocker) => ({
348
+ kind: 'readiness',
349
+ phase: blocker.phase,
350
+ topicKey: blocker.topicKey,
351
+ reason: blocker.reason,
352
+ ...(blocker.artifactId === undefined ? {} : { artifactId: blocker.artifactId }),
353
+ })),
354
+ ]);
355
+ return {
356
+ project: snapshot.project,
357
+ change: snapshot.change,
358
+ ...(next.nextPhase === undefined ? {} : { actionablePhase: next.nextPhase }),
359
+ next,
360
+ recommendedAction: cockpitRecommendedAction(next, aggregateBlockers),
361
+ phases: cockpitPhases,
362
+ artifacts,
363
+ acceptedCount: cockpitPhases.filter((phase) => phase.accepted).length,
364
+ legacyCount: cockpitPhases.filter((phase) => phase.legacy).length,
365
+ aggregateBlockers,
366
+ gates: cockpitGatesFromPhases(cockpitPhases, next),
367
+ inspectCommand: `vgxness sdd cockpit --project ${snapshot.project} --change ${snapshot.change} --json`,
368
+ };
369
+ }
361
370
  }
362
371
  export function nextDecisionFromStatuses(change, phases, options = {}) {
363
372
  const blockedPresentPhase = phases.find((status) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vgxness",
3
- "version": "1.20.0",
3
+ "version": "1.20.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": {