vgxness 1.12.0 → 1.14.0

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.
@@ -2,7 +2,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
3
  import { runBootAgentSeedUpgrade } from '../agents/boot-upgrade.js';
4
4
  import { createVgxMcpControlPlane } from './control-plane.js';
5
- import { EXPOSED_VGX_MCP_TOOL_NAMES, toInternalVgxMcpToolName, VGX_MCP_TOOL_INPUT_SCHEMAS, } from './schema.js';
5
+ import { EXPOSED_VGX_MCP_TOOL_NAMES, toInternalVgxMcpToolName, VGX_MCP_TOOL_INPUT_SCHEMAS, VGX_MCP_TOOL_OUTPUT_SCHEMAS, } from './schema.js';
6
6
  export async function startVgxMcpStdioServer(options = {}) {
7
7
  const controlPlane = createVgxMcpControlPlane(options.databasePath === undefined ? {} : { databasePath: options.databasePath });
8
8
  runBootAgentSeedUpgrade(controlPlane.database);
@@ -39,18 +39,20 @@ export async function startVgxMcpStdioServer(options = {}) {
39
39
  function registerVgxTools(server, controlPlane) {
40
40
  for (const publicToolName of EXPOSED_VGX_MCP_TOOL_NAMES) {
41
41
  const toolName = toInternalVgxMcpToolName(publicToolName);
42
+ const outputSchema = VGX_MCP_TOOL_OUTPUT_SCHEMAS[toolName];
42
43
  server.registerTool(publicToolName, {
43
44
  title: publicToolName,
44
45
  description: descriptionForTool(publicToolName),
45
46
  inputSchema: VGX_MCP_TOOL_INPUT_SCHEMAS[toolName],
47
+ ...(outputSchema === undefined ? {} : { outputSchema }),
46
48
  }, async (args) => toMcpTextResult(controlPlane.callVgxTool(toolName, argsForTool(args))));
47
49
  }
48
50
  }
49
51
  function descriptionForTool(publicToolName) {
50
52
  if (publicToolName === 'provider_status')
51
- return 'Read-only provider status report; inspects OpenCode config paths without installing, repairing, or writing files.';
53
+ return 'Agent-callable read-only provider status report; inspects expected config paths without installing, repairing, or writing provider config, and does not verify true host tool presence. Use it to decide whether to inspect doctor output or request explicit setup consent.';
52
54
  if (publicToolName === 'provider_doctor')
53
- return 'Read-only provider doctor checks; reports OpenCode MCP configuration health without repair/install side effects.';
55
+ return 'Agent-callable read-only provider doctor advisory; checks VGXNESS-known provider config health without install/repair/config writes and without proving true host tool presence. Use it to decide the next setup or troubleshooting recommendation.';
54
56
  if (publicToolName === 'provider_change_plan')
55
57
  return 'Read-only provider change plan preview; composes status, doctor, and OpenCode install planning without writing provider config.';
56
58
  if (publicToolName === 'opencode_handoff_preview')
@@ -66,9 +68,21 @@ function descriptionForTool(publicToolName) {
66
68
  if (publicToolName === 'context_cockpit')
67
69
  return 'Read-only context cockpit for start/resume/recovery; returns latest restorable session plus bounded memory previews without traces, provider config writes, repository writes, runs, artifacts, or session mutations.';
68
70
  if (publicToolName === 'sdd_cockpit')
69
- return 'Read-only SDD cockpit summary with raw compatibility fields plus additive readModel; metadata-only artifact summaries, no artifact bodies, and explicit human acceptance semantics.';
71
+ return 'Agent-callable read-only SDD cockpit summary; returns phase metadata/readModel without artifact bodies or state changes and preserves explicit human acceptance semantics. Use it to decide the next safe SDD action or blocker.';
70
72
  if (publicToolName === 'sdd_continue')
71
- return 'Read-only SDD continuation planner; returns blocker actions, safe suggested commands, related interrupted run context, and safety notes without provider execution, run creation, artifact mutation, provider config writes, or openspec writes.';
73
+ return 'Agent-callable read-only SDD continuation advisory; returns blocker actions, suggested next steps, interrupted-run context, and safety notes without provider execution, run creation, artifact mutation, provider config writes, or openspec writes. Use it to choose the next manual or agent-safe action.';
74
+ if (publicToolName === 'sdd_status')
75
+ return 'Agent-callable read-only SDD status summary; reports phase presence/state and next ready phase without creating, accepting, or mutating artifacts. Use it to decide whether to inspect, draft, mark ready, or wait for human acceptance.';
76
+ if (publicToolName === 'sdd_get_readiness')
77
+ return 'Agent-callable read-only SDD readiness check; reports whether a phase can proceed and why, without marking ready or changing artifacts. Use it to decide whether to call sdd_ready or resolve prerequisites first.';
78
+ if (publicToolName === 'sdd_ready')
79
+ return 'Agent-callable state-changing SDD readiness marker; marks an existing phase artifact ready when prerequisites allow, but does not human-accept it. Use only after readiness is clear, then ask a human to accept when governance requires acceptance.';
80
+ if (publicToolName === 'sdd_accept_artifact')
81
+ return 'Human-only mutating SDD acceptance gate; records explicit human acceptance for an artifact and must not be called by agents on their own behalf. Use only after a human approval decision, then continue to the next phase.';
82
+ if (publicToolName === 'sdd_reopen_artifact')
83
+ return 'Human-only mutating SDD reopen gate; records an explicit human decision to reopen an artifact and must not be called by agents on their own behalf. Use only after human instruction, then revise or re-review the phase.';
84
+ if (publicToolName === 'run_preflight')
85
+ return 'Agent-callable advisory/planning-only run preflight; evaluates permissions and records a plan but does not execute the checked shell/git/install/network/provider/secrets operation. Use it to decide whether human approval or a separate executor step is required.';
72
86
  const toolName = toInternalVgxMcpToolName(publicToolName);
73
87
  return `VGX control-plane tool ${toolName}`;
74
88
  }
@@ -78,6 +92,7 @@ function argsForTool(args) {
78
92
  function toMcpTextResult(envelope) {
79
93
  const result = {
80
94
  content: [{ type: 'text', text: JSON.stringify(envelope, null, 2) }],
95
+ structuredContent: envelope,
81
96
  };
82
97
  if (!envelope.ok)
83
98
  result.isError = true;
@@ -736,6 +736,7 @@ function validateSkillPayloadInput(input, tool) {
736
736
  'agentName',
737
737
  'workflow',
738
738
  'phase',
739
+ 'intentSignals',
739
740
  'providerAdapter',
740
741
  'runId',
741
742
  'mode',
@@ -749,6 +750,11 @@ function validateSkillPayloadInput(input, tool) {
749
750
  const copied = copyOptionalStrings(result, record.value, tool, ['project', 'agentId', 'agentName', 'workflow', 'phase', 'providerAdapter', 'runId']);
750
751
  if (!copied.ok)
751
752
  return copied;
753
+ const intentSignals = readOptionalStringArray(record.value, 'intentSignals', tool);
754
+ if (!intentSignals.ok)
755
+ return intentSignals;
756
+ if (intentSignals.value !== undefined)
757
+ result.intentSignals = intentSignals.value;
752
758
  const scope = readOptionalOneOf(record.value, 'scope', scopes, tool);
753
759
  if (!scope.ok)
754
760
  return scope;
@@ -0,0 +1,42 @@
1
+ CREATE TABLE IF NOT EXISTS skill_attachments_next (
2
+ id TEXT PRIMARY KEY,
3
+ skill_id TEXT NOT NULL REFERENCES skills(id) ON DELETE CASCADE,
4
+ version_id TEXT REFERENCES skill_versions(id) ON DELETE SET NULL,
5
+ target_type TEXT NOT NULL CHECK (target_type IN ('agent', 'subagent', 'workflow-phase', 'provider-adapter', 'intent-signal')),
6
+ target_key TEXT NOT NULL,
7
+ metadata_json TEXT NOT NULL,
8
+ created_at TEXT NOT NULL,
9
+ UNIQUE(skill_id, target_type, target_key)
10
+ );
11
+
12
+ INSERT INTO skill_attachments_next(id, skill_id, version_id, target_type, target_key, metadata_json, created_at)
13
+ SELECT id, skill_id, version_id, target_type, target_key, metadata_json, created_at
14
+ FROM skill_attachments;
15
+
16
+ DROP TABLE skill_attachments;
17
+ ALTER TABLE skill_attachments_next RENAME TO skill_attachments;
18
+
19
+ CREATE INDEX IF NOT EXISTS skill_attachments_target_idx
20
+ ON skill_attachments(target_type, target_key);
21
+
22
+ CREATE TABLE IF NOT EXISTS skill_usage_records_next (
23
+ id TEXT PRIMARY KEY,
24
+ skill_id TEXT NOT NULL REFERENCES skills(id) ON DELETE CASCADE,
25
+ version_id TEXT REFERENCES skill_versions(id) ON DELETE SET NULL,
26
+ run_id TEXT,
27
+ target_type TEXT CHECK (target_type IN ('agent', 'subagent', 'workflow-phase', 'provider-adapter', 'intent-signal')),
28
+ target_key TEXT,
29
+ outcome TEXT NOT NULL CHECK (outcome IN ('selected', 'injected', 'helped', 'failed', 'neutral')),
30
+ notes TEXT,
31
+ created_at TEXT NOT NULL
32
+ );
33
+
34
+ INSERT INTO skill_usage_records_next(id, skill_id, version_id, run_id, target_type, target_key, outcome, notes, created_at)
35
+ SELECT id, skill_id, version_id, run_id, target_type, target_key, outcome, notes, created_at
36
+ FROM skill_usage_records;
37
+
38
+ DROP TABLE skill_usage_records;
39
+ ALTER TABLE skill_usage_records_next RENAME TO skill_usage_records;
40
+
41
+ CREATE INDEX IF NOT EXISTS skill_usage_records_run_idx
42
+ ON skill_usage_records(run_id, created_at DESC);
@@ -27,7 +27,7 @@ const signalRules = [
27
27
  { signal: 'workflow-change', terms: [/\bworkflow\b/, /\borchestrat(e|ion|or)\b/, /\bsdd\b/, /\bphase\b/] },
28
28
  { signal: 'persistence-change', terms: [/\bpersist(ent|ence)?\b/, /\bstorage\b/, /\bsqlite\b/, /\bdatabase\b/, /\bmemory\b/, /\bmigration\b/] },
29
29
  { signal: 'security-sensitive', terms: [/\bsecurity\b/, /\bauth\b/, /\btoken\b/, /\bsecret\b/, /\bpermission\b/, /\bpermiso\b/, /\baprobaci[oó]n\b/] },
30
- { signal: 'broad-change', terms: [/\bmulti[- ]file\b/, /\bacross\b/, /\bend[- ]to[- ]end\b/, /\bsystem\b/] },
30
+ { signal: 'broad-change', terms: [/\bmulti[- ]file\b/, /\bacross\b/, /\bend[- ]to[- ]end\b/, /\bsystem\b/, /\blarge[- ]diff\b/, /\bbig[- ]diff\b/, /\bcross[- ]cutting\b/] },
31
31
  { signal: 'execution-request', terms: [/\brun\b/, /\bexecute\b/, /\bapply\b/, /\bstart\b/, /\binstall\b/, /\bpush\b/] },
32
32
  { signal: 'provider-execution', terms: [/\bprovider\b/, /\bopencode\b/, /\bclaude\b/, /\bmodel\b/, /\bllm\b/] },
33
33
  { signal: 'file-edit-request', terms: [/\bedit\b/, /\bwrite\b/, /\bmodify\b/, /\bpatch\b/, /\barreglar\b/] },
@@ -43,6 +43,7 @@ export function createNaturalLanguagePlan(input) {
43
43
  const ambiguous = isAmbiguous(normalizedIntent);
44
44
  if (ambiguous)
45
45
  addSignal(signals, 'ambiguous');
46
+ const intentSignals = buildSkillIntentSignals(normalizedIntent, signals);
46
47
  const risk = classifyIntentRisk({ project: input.project, intent: input.intent });
47
48
  const flow = chooseFlow(signals, risk);
48
49
  const workflow = workflowFor(flow);
@@ -55,7 +56,7 @@ export function createNaturalLanguagePlan(input) {
55
56
  ...(suggestedChangeId !== undefined ? { change: suggestedChangeId } : {}),
56
57
  ...(input.sdd !== undefined ? { sdd: input.sdd } : {}),
57
58
  };
58
- const previewActions = buildPreviewActions(actionInput, flow, needsClarification);
59
+ const previewActions = buildPreviewActions(actionInput, flow, workflow, intentSignals, needsClarification);
59
60
  return {
60
61
  version: 1,
61
62
  project: input.project,
@@ -65,6 +66,7 @@ export function createNaturalLanguagePlan(input) {
65
66
  confidence: confidenceFor(flow, signals, needsClarification),
66
67
  reason: reasonFor(flow, signals, needsClarification),
67
68
  signals,
69
+ intentSignals,
68
70
  needsClarification,
69
71
  ...(needsClarification ? { clarifyingQuestion: 'What target should this change inspect or modify, and what outcome do you want?' } : {}),
70
72
  ...(suggestedChangeId !== undefined ? { suggestedChangeId } : {}),
@@ -231,20 +233,42 @@ function buildSafety(signals) {
231
233
  notes.push('Privileged impact was detected; this preview does not request elevated access.');
232
234
  return { executed: false, callsProvider: false, editsFiles: false, writesProviderConfig: false, recordsRuns: false, notes };
233
235
  }
234
- function buildPreviewActions(input, flow, needsClarification) {
236
+ function buildSkillIntentSignals(intent, signals) {
237
+ const skillSignals = ['workflow-selection'];
238
+ if (signals.includes('broad-change'))
239
+ addSkillSignal(skillSignals, 'broad-change');
240
+ if (/\b(git|commit|branch|merge|rebase|push|stage|staging|diff|checkout)\b/.test(intent))
241
+ addSkillSignal(skillSignals, 'git');
242
+ if (/\b(pr|prs|pull[- ]request|pull request|reviewer|reviewers)\b/.test(intent))
243
+ addSkillSignal(skillSignals, 'pull-request');
244
+ if (/\b(stacked[- ]pr|stacked[- ]prs|stacked pull requests?|stacked|apilad[oa]s?)\b/.test(intent))
245
+ addSkillSignal(skillSignals, 'stacked-prs');
246
+ if (/\b(tdd|test[- ]driven|red[- ]green|strict[- ]tdd)\b/.test(intent))
247
+ addSkillSignal(skillSignals, intent.includes('strict') ? 'strict-tdd' : 'tdd');
248
+ if (/\b(review[- ]size|reviewable|split|slice|slices|large[- ]diff|big[- ]diff)\b/.test(intent))
249
+ addSkillSignal(skillSignals, 'review-size');
250
+ return skillSignals;
251
+ }
252
+ function addSkillSignal(signals, signal) {
253
+ if (!signals.includes(signal))
254
+ signals.push(signal);
255
+ }
256
+ function buildPreviewActions(input, flow, workflow, intentSignals, needsClarification) {
235
257
  if (needsClarification)
236
258
  return [{ kind: 'clarification', description: 'Ask for the missing target and desired outcome before previewing write actions.' }];
259
+ const skillAction = skillPayloadPreviewAction(input, workflow, intentSignals);
237
260
  if (flow === 'debug' || flow === 'diagnose')
238
- return [{ kind: 'diagnostic-preview', description: 'Preview read-only diagnostic commands such as status, doctor, logs, or SDD next checks.' }];
261
+ return [skillAction, { kind: 'diagnostic-preview', description: 'Preview read-only diagnostic commands such as status, doctor, logs, or SDD next checks.' }];
239
262
  if (flow === 'plan')
240
- return [{ kind: 'manual-plan', description: 'Draft a manual implementation plan without executing providers or editing files.' }];
263
+ return [skillAction, { kind: 'manual-plan', description: 'Draft a manual implementation plan without executing providers or editing files.' }];
241
264
  if (flow === 'explore' || flow === 'direct')
242
- return [{ kind: 'answer', description: 'Explore or answer directly without entering SDD.' }];
265
+ return [skillAction, { kind: 'answer', description: 'Explore or answer directly without entering SDD.' }];
243
266
  if (flow === 'quickfix')
244
- return [{ kind: 'workflow-preview', description: 'Preview a small localized quickfix; no execution occurs in this planner response.' }];
267
+ return [skillAction, { kind: 'workflow-preview', description: 'Preview a small localized quickfix; no execution occurs in this planner response.' }];
245
268
  if (flow === 'build')
246
- return [{ kind: 'workflow-preview', description: 'Preview a scoped build workflow; no execution occurs in this planner response.' }];
269
+ return [skillAction, { kind: 'workflow-preview', description: 'Preview a scoped build workflow; no execution occurs in this planner response.' }];
247
270
  return [
271
+ skillAction,
248
272
  {
249
273
  kind: 'sdd-preview',
250
274
  description: input.sdd?.next?.recommendedAction ??
@@ -252,3 +276,24 @@ function buildPreviewActions(input, flow, needsClarification) {
252
276
  },
253
277
  ];
254
278
  }
279
+ function skillPayloadPreviewAction(input, workflow, intentSignals) {
280
+ const phase = input.sdd?.next?.nextPhase;
281
+ const command = [
282
+ 'vgxness skills payload',
283
+ '--project',
284
+ shellQuote(input.project),
285
+ '--workflow',
286
+ shellQuote(workflow),
287
+ ...(phase === undefined ? [] : ['--phase', shellQuote(phase)]),
288
+ '--intent-signals',
289
+ shellQuote(intentSignals.join(',')),
290
+ ].join(' ');
291
+ return {
292
+ kind: 'skill-payload-preview',
293
+ description: 'Preview the registry/context skills that match this intent; this is read-only and does not install provider-native skills.',
294
+ command,
295
+ };
296
+ }
297
+ function shellQuote(value) {
298
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
299
+ }
@@ -30,6 +30,7 @@ export function buildSddCockpitReadModel(cockpit) {
30
30
  phases,
31
31
  nextAction,
32
32
  blockers,
33
+ ...(cockpit.gates === undefined ? {} : { gates: cockpit.gates }),
33
34
  guidance: guidanceFor(cockpit, nextAction, blockers),
34
35
  summary: summarize(phases),
35
36
  contentIncluded: false,
@@ -68,6 +69,7 @@ function toPhaseReadModel(phase) {
68
69
  blocked: !phase.readiness.ready || phase.blockers.length > 0,
69
70
  reasons,
70
71
  },
72
+ ...(phase.gates === undefined ? {} : { gates: phase.gates }),
71
73
  guidance: guidanceForPhase(phase, status, canAccept),
72
74
  };
73
75
  }
@@ -5,6 +5,7 @@ export function sddContinuationPlanFrom(input) {
5
5
  const suggestedCommand = suggestedContinuationCommand(input.project, input.next, inspectCommand, dbFlag);
6
6
  const blockerGuidance = input.next.blockerGuidance ?? [];
7
7
  const blockerActions = blockerGuidance.map((blocker) => continuationBlockerAction(input.project, input.next.change, blocker, dbFlag));
8
+ const recommendedActions = continuationRecommendedActions(input.project, input.next, blockerGuidance);
8
9
  const relatedRunContext = relatedRunContextView(input.project, input.relatedRunContext, dbFlag);
9
10
  return {
10
11
  kind: 'sdd-continuation-plan',
@@ -18,6 +19,7 @@ export function sddContinuationPlanFrom(input) {
18
19
  suggestedCommand,
19
20
  inspectCommand,
20
21
  blockerActions,
22
+ recommendedActions,
21
23
  ...(relatedRunContext === undefined ? {} : { relatedRunContext }),
22
24
  ...(input.explicitDatabasePath === undefined ? {} : { explicitDatabasePath: input.explicitDatabasePath }),
23
25
  safety: [
@@ -28,6 +30,153 @@ export function sddContinuationPlanFrom(input) {
28
30
  ],
29
31
  };
30
32
  }
33
+ function continuationRecommendedActions(project, next, blockerGuidance) {
34
+ const actions = [inspectCockpitAction(project, next.change)];
35
+ if (next.status === 'runnable' && next.nextPhase !== undefined)
36
+ actions.push(draftPhaseAction(project, next.change, next.nextPhase, next.reason));
37
+ for (const blocker of blockerGuidance) {
38
+ const action = recommendedActionForBlocker(project, next.change, blocker);
39
+ if (action !== undefined)
40
+ actions.push(action);
41
+ }
42
+ return actions;
43
+ }
44
+ function inspectCockpitAction(project, change) {
45
+ return {
46
+ id: `sdd.${change}.inspect-cockpit`,
47
+ label: 'Inspect SDD cockpit',
48
+ title: 'Inspect SDD cockpit',
49
+ description: 'Read the current SDD cockpit/read model before choosing the next action.',
50
+ kind: 'inspect',
51
+ category: 'inspection',
52
+ targetTool: 'vgxness_sdd_cockpit',
53
+ suggestedArgs: { project, change },
54
+ readOnly: true,
55
+ mutating: false,
56
+ agentCallable: true,
57
+ humanOnly: false,
58
+ requiresHumanApproval: false,
59
+ requiresHumanConfirmation: false,
60
+ requiresPreflight: false,
61
+ requiresProviderWriteConsent: false,
62
+ reason: 'Continuation plans are advisory; cockpit inspection is a safe MCP-native way to refresh state.',
63
+ rationale: 'Keeps agents grounded in current artifact states without mutating SDD state or provider configuration.',
64
+ blockingPrerequisites: [],
65
+ };
66
+ }
67
+ function draftPhaseAction(project, change, phase, reason) {
68
+ return {
69
+ id: `sdd.${change}.${phase}.draft`,
70
+ label: `Prepare ${phase} draft through normal SDD phase flow`,
71
+ title: `Prepare ${phase} draft`,
72
+ description: 'The next SDD phase appears runnable; draft generation must still use the normal phase workflow and any required preflight/confirmation gates.',
73
+ kind: 'draft-phase',
74
+ category: 'sdd-phase',
75
+ phase,
76
+ targetTool: 'vgxness_sdd_get_readiness',
77
+ suggestedArgs: { project, change, phase },
78
+ readOnly: false,
79
+ mutating: true,
80
+ agentCallable: true,
81
+ humanOnly: false,
82
+ requiresHumanApproval: false,
83
+ requiresHumanConfirmation: false,
84
+ requiresPreflight: true,
85
+ requiresProviderWriteConsent: false,
86
+ reason,
87
+ rationale: 'This continuation tool is read-only, so it can recommend the next phase but cannot execute providers or save artifacts itself.',
88
+ blockingPrerequisites: [],
89
+ };
90
+ }
91
+ function recommendedActionForBlocker(project, change, blocker) {
92
+ if (blocker.reason === 'draft' || blocker.reason === 'legacy')
93
+ return humanAcceptAction(project, change, blocker);
94
+ if (blocker.reason === 'rejected')
95
+ return humanReopenAction(project, change, blocker);
96
+ if (blocker.reason === 'missing')
97
+ return draftPhaseAction(project, change, blocker.phase, blocker.action);
98
+ return inspectArtifactAction(project, change, blocker);
99
+ }
100
+ function humanAcceptAction(project, change, blocker) {
101
+ return {
102
+ id: `sdd.${change}.${blocker.phase}.accept-human`,
103
+ label: `Human review and acceptance required for ${blocker.phase}`,
104
+ title: `Accept ${blocker.phase} artifact as a human`,
105
+ description: 'A human must explicitly accept this artifact before it counts as accepted; agents must not accept on the human’s behalf.',
106
+ kind: 'accept-human',
107
+ category: 'human-governance',
108
+ phase: blocker.phase,
109
+ targetTool: 'vgxness_sdd_accept_artifact',
110
+ suggestedArgs: { project, change, phase: blocker.phase },
111
+ readOnly: false,
112
+ mutating: true,
113
+ agentCallable: false,
114
+ humanOnly: true,
115
+ requiresHumanApproval: true,
116
+ requiresHumanConfirmation: true,
117
+ requiresPreflight: true,
118
+ requiresProviderWriteConsent: false,
119
+ reason: blocker.action,
120
+ rationale: 'Human-only acceptance is an explicit governance event and cannot be inferred from draft content, readiness, or artifact presence.',
121
+ blockingPrerequisites: [blockingPrerequisite(blocker)],
122
+ };
123
+ }
124
+ function humanReopenAction(project, change, blocker) {
125
+ return {
126
+ id: `sdd.${change}.${blocker.phase}.reopen-human`,
127
+ label: `Human reopen required for rejected ${blocker.phase}`,
128
+ title: `Reopen ${blocker.phase} artifact as a human`,
129
+ description: 'A human may reopen the rejected artifact to draft; agents must not perform this governance action on the human’s behalf.',
130
+ kind: 'reopen-human',
131
+ category: 'human-governance',
132
+ phase: blocker.phase,
133
+ targetTool: 'vgxness_sdd_reopen_artifact',
134
+ suggestedArgs: { project, change, phase: blocker.phase },
135
+ readOnly: false,
136
+ mutating: true,
137
+ agentCallable: false,
138
+ humanOnly: true,
139
+ requiresHumanApproval: true,
140
+ requiresHumanConfirmation: true,
141
+ requiresPreflight: true,
142
+ requiresProviderWriteConsent: false,
143
+ reason: blocker.action,
144
+ rationale: 'Reopen is a human governance transition from rejected back to draft and remains outside this read-only planner.',
145
+ blockingPrerequisites: [blockingPrerequisite(blocker)],
146
+ };
147
+ }
148
+ function inspectArtifactAction(project, change, blocker) {
149
+ return {
150
+ id: `sdd.${change}.${blocker.phase}.inspect`,
151
+ label: `Inspect ${blocker.phase} artifact`,
152
+ title: `Inspect ${blocker.phase} artifact`,
153
+ description: 'Inspect the blocking artifact before deciding whether human governance or phase work is needed.',
154
+ kind: 'inspect',
155
+ category: 'inspection',
156
+ phase: blocker.phase,
157
+ targetTool: 'vgxness_sdd_get_artifact',
158
+ suggestedArgs: { project, change, phase: blocker.phase, payloadMode: 'compact' },
159
+ readOnly: true,
160
+ mutating: false,
161
+ agentCallable: true,
162
+ humanOnly: false,
163
+ requiresHumanApproval: false,
164
+ requiresHumanConfirmation: false,
165
+ requiresPreflight: false,
166
+ requiresProviderWriteConsent: false,
167
+ reason: blocker.action,
168
+ rationale: 'Artifact inspection is read-only and helps agents understand the blocker without changing SDD state.',
169
+ blockingPrerequisites: [blockingPrerequisite(blocker)],
170
+ };
171
+ }
172
+ function blockingPrerequisite(blocker) {
173
+ return {
174
+ phase: blocker.phase,
175
+ topicKey: blocker.topicKey,
176
+ reason: blocker.reason,
177
+ ...(blocker.artifactId === undefined ? {} : { artifactId: blocker.artifactId }),
178
+ };
179
+ }
31
180
  function relatedRunContextView(project, relatedRunContext, dbFlag) {
32
181
  if (relatedRunContext === undefined)
33
182
  return undefined;
@@ -40,16 +40,18 @@ export class SddWorkflowService {
40
40
  const validated = this.validatePhaseInput(input);
41
41
  if (!validated.ok)
42
42
  return validated;
43
- const phases = this.getPhaseStatuses(validated.value.project, validated.value.change);
44
- if (!phases.ok)
45
- return phases;
46
- const readiness = getReadinessFromStatuses(validated.value.change, validated.value.phase, phases.value, this.options);
43
+ const snapshot = this.loadPhaseSnapshot(validated.value.project, validated.value.change);
44
+ if (!snapshot.ok)
45
+ return snapshot;
46
+ const phases = snapshot.value.phases;
47
+ const readiness = getReadinessFromStatuses(validated.value.change, validated.value.phase, phases, this.options);
47
48
  return {
48
49
  ok: true,
49
50
  value: {
50
51
  change: validated.value.change,
51
52
  phase: validated.value.phase,
52
53
  ...readiness,
54
+ gates: readinessGatesForPhase(validated.value.change, validated.value.phase, phases, readiness),
53
55
  },
54
56
  };
55
57
  }
@@ -88,6 +90,7 @@ export class SddWorkflowService {
88
90
  };
89
91
  const artifact = phaseStatus.present ? cockpitArtifactSummaryFromSnapshotItem(phaseStatus) : undefined;
90
92
  const blockers = cockpitBlockersForPhase(phaseStatus, readiness);
93
+ const gates = phaseGateFromStatus(phaseStatus, readiness);
91
94
  return {
92
95
  phase: phaseStatus.phase,
93
96
  topicKey: phaseStatus.topicKey,
@@ -98,6 +101,7 @@ export class SddWorkflowService {
98
101
  readiness,
99
102
  ...(artifact === undefined ? {} : { artifact }),
100
103
  blockers,
104
+ gates,
101
105
  };
102
106
  });
103
107
  const artifacts = cockpitPhases.map((phase) => phase.artifact).filter((artifact) => artifact !== undefined);
@@ -123,6 +127,7 @@ export class SddWorkflowService {
123
127
  acceptedCount: cockpitPhases.filter((phase) => phase.accepted).length,
124
128
  legacyCount: cockpitPhases.filter((phase) => phase.legacy).length,
125
129
  aggregateBlockers,
130
+ gates: cockpitGatesFromPhases(cockpitPhases, next),
126
131
  inspectCommand: `vgxness sdd cockpit --project ${validated.value.project} --change ${validated.value.change} --json`,
127
132
  };
128
133
  return ok(cockpit);
@@ -518,6 +523,124 @@ function cockpitArtifactSummaryFromSnapshotItem(item) {
518
523
  updatedAt: item.updatedAt,
519
524
  };
520
525
  }
526
+ function readinessGatesForPhase(change, phase, phases, readiness) {
527
+ const targetStatus = phases.find((candidate) => candidate.phase === phase) ?? { phase, topicKey: sddTopicKey(change, phase), present: false, state: 'missing', accepted: false, legacy: false, warnings: [] };
528
+ const targetGate = phaseGateFromStatus(targetStatus, readiness);
529
+ const prerequisites = sddPrerequisites[phase].map((prerequisite) => {
530
+ const prerequisiteStatus = phases.find((candidate) => candidate.phase === prerequisite) ?? {
531
+ phase: prerequisite,
532
+ topicKey: sddTopicKey(change, prerequisite),
533
+ present: false,
534
+ state: 'missing',
535
+ accepted: false,
536
+ legacy: false,
537
+ warnings: [],
538
+ };
539
+ const blocker = readiness.blockedPrerequisites?.find((candidate) => candidate.phase === prerequisite);
540
+ return phaseGateFromStatus(prerequisiteStatus, {
541
+ ready: blocker === undefined,
542
+ blockedPrerequisites: blocker === undefined ? [] : [blocker],
543
+ });
544
+ });
545
+ const blockedReasons = [...targetGate.blockedReasons, ...prerequisites.flatMap((gate) => gate.blockedReasons)];
546
+ return {
547
+ phase: targetGate,
548
+ prerequisites,
549
+ runnable: readiness.ready,
550
+ blocked: !readiness.ready,
551
+ blockedReasons: [...new Set(blockedReasons)],
552
+ humanOnly: targetGate.humanOnly || prerequisites.some((gate) => gate.humanOnly),
553
+ preflightRequired: targetGate.preflightRequired,
554
+ agentCallable: true,
555
+ canDraft: targetGate.canDraft,
556
+ canMarkReady: targetGate.canMarkReady,
557
+ canAccept: targetGate.canAccept || prerequisites.some((gate) => gate.canAccept),
558
+ canReopen: targetGate.canReopen || prerequisites.some((gate) => gate.canReopen),
559
+ nextAllowedActions: [...new Set([targetGate, ...prerequisites].flatMap((gate) => gate.nextAllowedActions))],
560
+ requiresProviderWriteConsent: false,
561
+ };
562
+ }
563
+ function cockpitGatesFromPhases(phases, next) {
564
+ const phaseGates = phases.map((phase) => phase.gates ?? phaseGateFromStatus(phase, phase.readiness));
565
+ const blockedTransitions = phaseGates.filter((gate) => gate.blocked);
566
+ const blockedReasons = [...new Set(blockedTransitions.flatMap((gate) => gate.blockedReasons))];
567
+ const runnableNextPhases = phaseGates.filter((gate) => gate.runnable && !gate.artifactPresent).map((gate) => gate.phase);
568
+ return {
569
+ phases: phaseGates,
570
+ changeComplete: phases.length === sddPhases.length && phases.every((phase) => phase.accepted),
571
+ ...(next.nextPhase === undefined ? {} : { nextPhase: next.nextPhase }),
572
+ nextPhaseRunnable: next.status === 'runnable',
573
+ runnableNextPhases,
574
+ blockedTransitions,
575
+ blockedReasons,
576
+ requiresHumanAcceptance: phaseGates.some((gate) => gate.requiresHumanAcceptance),
577
+ humanOnly: phaseGates.some((gate) => gate.humanOnly),
578
+ preflightRequired: next.status === 'runnable',
579
+ agentCallable: true,
580
+ canDraft: phaseGates.some((gate) => gate.canDraft),
581
+ canMarkReady: phaseGates.some((gate) => gate.canMarkReady),
582
+ canAccept: phaseGates.some((gate) => gate.canAccept),
583
+ canReopen: phaseGates.some((gate) => gate.canReopen),
584
+ nextAllowedActions: [...new Set(phaseGates.flatMap((gate) => gate.nextAllowedActions))],
585
+ requiresProviderWriteConsent: false,
586
+ };
587
+ }
588
+ function phaseGateFromStatus(status, readiness) {
589
+ const artifactStatus = status.state ?? (status.present ? 'draft' : 'missing');
590
+ const acceptedByHuman = status.accepted === true && status.acceptance?.actor.type === 'human';
591
+ const blockedPrerequisite = readiness.blockedPrerequisites?.[0];
592
+ const blockedReasons = [
593
+ ...((readiness.blockedPrerequisites ?? []).map((blocker) => `${blocker.phase}:${blocker.reason}`)),
594
+ ...(status.present && status.accepted !== true ? [`${status.phase}:${blockerReasonForStatus(status)}`] : []),
595
+ ...(status.legacy === true ? [`${status.phase}:legacy`] : []),
596
+ ];
597
+ const requiresHumanAcceptance = status.present && !acceptedByHuman && artifactStatus !== 'missing';
598
+ const canDraft = readiness.ready && !status.present;
599
+ const canMarkReady = readiness.ready;
600
+ const canAccept = status.present && !acceptedByHuman && status.legacy !== true && (artifactStatus === 'draft' || artifactStatus === 'accepted');
601
+ const canReopen = status.present && artifactStatus === 'rejected';
602
+ const nextAllowedActions = phaseNextAllowedActions({ canDraft, canMarkReady, canAccept, canReopen, present: status.present });
603
+ return {
604
+ phase: status.phase,
605
+ topicKey: status.topicKey,
606
+ artifactPresent: status.present,
607
+ artifactStatus,
608
+ artifactState: status.legacy === true ? 'legacy' : artifactStatus,
609
+ accepted: status.accepted === true,
610
+ acceptedByHuman,
611
+ agentCallable: true,
612
+ requiresHumanAcceptance,
613
+ draftPresent: artifactStatus === 'draft',
614
+ contentFrozen: acceptedByHuman,
615
+ runnable: readiness.ready && !status.present,
616
+ blocked: !readiness.ready || status.legacy === true || (status.present && status.accepted !== true),
617
+ blockedReasons: [...new Set(blockedReasons)],
618
+ humanOnly: requiresHumanAcceptance,
619
+ preflightRequired: readiness.ready && !status.present,
620
+ requiresPreflight: readiness.ready && !status.present,
621
+ requiresProviderWriteConsent: false,
622
+ canDraft,
623
+ canMarkReady,
624
+ canAccept,
625
+ canReopen,
626
+ nextAllowedActions,
627
+ ...(blockedPrerequisite === undefined ? {} : { requiredPriorPhase: blockedPrerequisite.phase, blockerReason: blockedPrerequisite.reason }),
628
+ };
629
+ }
630
+ function phaseNextAllowedActions(input) {
631
+ const actions = ['vgxness_sdd_get_readiness', 'vgxness_sdd_cockpit'];
632
+ if (input.canDraft)
633
+ actions.push('vgxness_sdd_save_artifact');
634
+ if (input.canMarkReady)
635
+ actions.push('vgxness_sdd_ready');
636
+ if (input.present)
637
+ actions.push('vgxness_sdd_get_artifact');
638
+ if (input.canAccept)
639
+ actions.push('vgxness_sdd_accept_artifact');
640
+ if (input.canReopen)
641
+ actions.push('vgxness_sdd_reopen_artifact');
642
+ return [...new Set(actions)];
643
+ }
521
644
  function compactGovernanceArtifact(artifact) {
522
645
  return {
523
646
  id: artifact.id,
@@ -0,0 +1,42 @@
1
+ import { SkillSeedService } from './skill-seed-service.js';
2
+ const skipEnvVar = 'VGXNESS_SKIP_SKILL_SEED_AUTO_UPGRADE';
3
+ export function runBootSkillSeed(database, env = process.env) {
4
+ if (isOptOut(env))
5
+ return okFromSeedResult({ skipped: true });
6
+ const seeded = new SkillSeedService(database).seedFromDefaultManifest();
7
+ if (!seeded.ok)
8
+ return seeded;
9
+ return okFromSeedResult({ skipped: false, seed: seeded.value });
10
+ }
11
+ function isOptOut(env) {
12
+ const value = env[skipEnvVar];
13
+ return value === '1' || value === 'true';
14
+ }
15
+ function okFromSeedResult(input) {
16
+ if (input.skipped) {
17
+ return {
18
+ ok: true,
19
+ value: {
20
+ skipped: true,
21
+ skillsCreated: 0,
22
+ skillsUpdated: 0,
23
+ versionsCreated: 0,
24
+ versionsSkipped: 0,
25
+ attachmentsCreated: 0,
26
+ attachmentsSkipped: 0,
27
+ },
28
+ };
29
+ }
30
+ return {
31
+ ok: true,
32
+ value: {
33
+ skipped: false,
34
+ skillsCreated: input.seed.skillsCreated,
35
+ skillsUpdated: input.seed.skillsUpdated,
36
+ versionsCreated: input.seed.versionsCreated,
37
+ versionsSkipped: input.seed.versionsSkipped,
38
+ attachmentsCreated: input.seed.attachmentsCreated,
39
+ attachmentsSkipped: input.seed.attachmentsSkipped,
40
+ },
41
+ };
42
+ }