Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"dependencies": {
"@cloudflare/workers-oauth-provider": "^0.2.4",
"@modelcontextprotocol/sdk": "^1.27.1",
"@stackbilt/scaffold-core": "^1.1.0",
"agents": "^0.7.2",
"hono": "^4.12.8",
"zod": "^3.25.0"
Expand Down
80 changes: 28 additions & 52 deletions src/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { OAUTH_PROVIDER_CONFIG } from './oauth-config.js';
import { resolveRoute, getToolRiskLevel, ROUTE_TABLE, type BackendRoute } from './route-table.js';
import { toBackendToolName, buildAggregatedCatalog, validateToolArguments, isInternalTool } from './tool-registry.js';
import { type AuditArtifact, generateTraceId, summarizeInput, emitAudit, queueAuditEvent } from './audit.js';
import { materializeScaffold } from './scaffold-materializer.js';
import { buildScaffold, classify } from '@stackbilt/scaffold-core';
import { publishToGitHub } from './scaffold-publish.js';
import { classifyIntention, type IntentClassification } from './intent-classifier.js';
import { logDivergence } from './divergence-logger.js';
Expand Down Expand Up @@ -232,39 +232,15 @@ async function proxyRestToolCall(
}

if (toolName === 'scaffold_classify') {
const response = await binding.fetch(new Request('https://internal/run', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Gateway-Tenant-Id': session.tenantId ?? '',
},
body: JSON.stringify({
spreadType: 'classify-cast',
querent: {
id: session.tenantId ?? session.userId ?? 'gateway',
intention: a.message as string,
state: {
message: a.message as string,
source: (a.source as string) ?? 'user',
},
},
}),
}));

if (!response.ok) {
return { content: [{ type: 'text', text: `classify-cast failed: HTTP ${response.status}` }], isError: true };
}

const result = await response.json() as { facts?: Record<string, string>; output?: string[] };
const intention = (a.message ?? a.intention) as string;
const result = classify(intention);
return {
content: [{ type: 'text', text: JSON.stringify({
classification: result.facts?.classification,
confidence: result.facts?.classification_confidence,
executor: result.facts?.classification_executor,
complexity: result.facts?.classification_complexity,
secondary: result.facts?.secondary_classification,
compound_intent: result.facts?.compound_intent,
tiebreaker_override: result.facts?.tiebreaker_override,
pattern: result.pattern,
confidence: result.confidence,
traits: result.traits,
qualityProfile: result.qualityProfile,
enrichedIntention: result.enrichedIntention,
}, null, 2) }],
};
}
Expand Down Expand Up @@ -487,28 +463,28 @@ async function proxyRestToolCall(
}
}

// Merge governance files from materializer with engine code files,
// or fall back to materializer entirely if engine didn't produce files
if (result.facts) {
try {
const materialized = materializeScaffold(result.facts, intention);
if (!files) {
// Engine didn't produce files — use materializer output entirely
files = materialized.files;
nextSteps = materialized.nextSteps;
fileSource = 'basic';
} else {
// Engine produced code files — merge in governance (.ai/) files from materializer
const enginePaths = new Set(files.map(f => f.path));
const govFiles = materialized.files.filter(f => f.path.startsWith('.ai/') && !enginePaths.has(f.path));
if (govFiles.length > 0) {
files = [...files, ...govFiles];
}
nextSteps = materialized.nextSteps;
// Merge governance files from scaffold-core with engine code files,
// or fall back to scaffold-core entirely if engine didn't produce files.
// buildScaffold returns LocalScaffoldResult: { classification, knowledge, governance, files, facts }
// files is ScaffoldFile[] with { path, content, role } — strip role for downstream consumers.
try {
const coreResult = await buildScaffold(intention, { projectName: (result.facts as Record<string, string> | undefined)?.project_name });
// Normalise: drop role field so the shape matches what callers expect ({ path, content })
const coreFiles = coreResult.files.map(({ path, content }: { path: string; content: string; role?: string }) => ({ path, content }));
if (!files) {
// Engine didn't produce files — use scaffold-core output entirely
files = coreFiles;
fileSource = 'basic';
} else {
// Engine produced code files — merge in governance (.ai/) files from scaffold-core
const enginePaths = new Set(files.map(f => f.path));
const govFiles = coreFiles.filter((f: { path: string }) => f.path.startsWith('.ai/') && !enginePaths.has(f.path));
if (govFiles.length > 0) {
files = [...files, ...govFiles];
}
} catch {
// Materializer failure is non-fatal — return facts without files
}
} catch {
// scaffold-core failure is non-fatal — return facts without files
}

// Default next steps if engine produced files but no steps
Expand Down
13 changes: 7 additions & 6 deletions src/rest-scaffold.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import type { GatewayEnv } from './types.js';
import { extractBearerToken, validateBearerToken } from './auth.js';
import { materializeScaffold } from './scaffold-materializer.js';
import { buildScaffold } from '@stackbilt/scaffold-core';
import { checkRateLimit, rateLimitHeaders } from './rate-limiter.js';
import { reserveQuota, settleQuota } from './cost-attribution.js';
import { generateTraceId, summarizeInput, emitAudit, queueAuditEvent, type AuditArtifact } from './audit.js';
Expand Down Expand Up @@ -245,12 +245,13 @@ export async function handleRestScaffold(
}
}

// Fall back to basic materializer if engine didn't produce files
if (!files && result.facts) {
// Fall back to scaffold-core if engine didn't produce files.
// buildScaffold returns LocalScaffoldResult: { classification, knowledge, governance, files, facts }
// files is ScaffoldFile[] with { path, content, role } — strip role for downstream consumers.
if (!files) {
try {
const materialized = materializeScaffold(result.facts, intention);
files = materialized.files;
nextSteps = materialized.nextSteps;
const coreResult = await buildScaffold(intention, { projectName: (result.facts as Record<string, string> | undefined)?.project_name });
files = coreResult.files.map(({ path, content }: { path: string; content: string; role?: string }) => ({ path, content }));
fileSource = 'basic';
} catch {
// Non-fatal
Expand Down
Loading
Loading