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.
- package/dist/agents/canonical-agent-manifest.js +8 -7
- package/dist/cli/cli-flags.js +3 -3
- package/dist/cli/cli-help.js +4 -4
- package/dist/cli/commands/agent-skill-dispatcher.js +10 -1
- package/dist/mcp/control-plane.js +5 -0
- package/dist/mcp/provider-doctor.js +10 -6
- package/dist/mcp/provider-health-types.js +20 -0
- package/dist/mcp/provider-status.js +18 -6
- package/dist/mcp/schema.js +177 -0
- package/dist/mcp/stdio-server.js +20 -5
- package/dist/mcp/validation.js +6 -0
- package/dist/memory/sqlite/migrations/017_intent_signal_skill_targets.sql +42 -0
- package/dist/orchestrator/natural-language-planner.js +53 -8
- package/dist/sdd/cockpit-read-model.js +2 -0
- package/dist/sdd/sdd-continuation-plan.js +149 -0
- package/dist/sdd/sdd-workflow-service.js +127 -4
- package/dist/skills/boot-seed.js +42 -0
- package/dist/skills/skill-resolver.js +6 -0
- package/dist/skills/skill-seed-service.js +39 -16
- package/docs/sdd-flow.es.md +403 -0
- package/docs/sdd-flow.md +403 -0
- package/package.json +1 -1
- package/seeds/skills/skill-seed-v1.json +73 -1
package/dist/mcp/stdio-server.js
CHANGED
|
@@ -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 '
|
|
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 '
|
|
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 '
|
|
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 '
|
|
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;
|
package/dist/mcp/validation.js
CHANGED
|
@@ -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
|
|
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
|
|
44
|
-
if (!
|
|
45
|
-
return
|
|
46
|
-
const
|
|
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
|
+
}
|