Skip to content
Closed
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
13 changes: 13 additions & 0 deletions .changeset/route-graph-substrate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"agent-bundle": patch
---

Add the #93 route-compiler substrate: deterministic discovery of route
modules under the conventional roots (`src/mcp/<server>/{tools,resources,prompts,apps}/`,
`src/events/`, `src/providers/`, `src/cli/`, `src/scripts/`) compiled into an
immutable, consumer-invisible route graph, a new `AB480x` diagnostic family
for mode conflicts (route directory versus entry-file conventions, duplicate
route ids, unsafe route names), and an `inspect --routes` focus that lists
the discovered graph. Modules explicit configuration already claims are never
routes, so existing layouts keep working unchanged; nothing generates entries
or registries from the graph yet.
1 change: 1 addition & 0 deletions docs/diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ gate a build, a validation, or a dev rebuild.
| `AB472x` | The `tools.rsbuild` / `tools.rspack` escape hatch. |
| `AB473x` | Migration nudges (informational; see below). |
| `AB474x`/`AB4750` | Prebuilt payloads and prebuilt entries (see below). |
| `AB480x` | Filesystem route conventions (#93 substrate): route-directory versus entry-file mode conflicts (`AB4800`: `src/mcp/<server>/` routes with a `src/mcp/<server>.ts` entry, `AB4801`: route-mode server also declared in `mcp.servers`, `AB4802`: `src/cli/` routes with `src/cli.ts`), `AB4803`: duplicate route ids, `AB4804`: unsafe route names. All errors: a server (and the package CLI) is in exactly one mode, and the compiler never silently chooses. |
| `AB5000` | General CLI and adapter failures. |
| `AB7xxx` | Project preparation and development rebuilds. |
| `AB8xxx` | Development server configuration. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
Date: 2026-08-25
Status: proposed

Status note (2026-08-31): the capability-aware Workbench described here has
been implemented in its current form; its evolution onto the compiled route
manifest continues under
[issue #105](https://github.com/ScriptedAlchemy/agent-bundle/issues/105).

## Context

The Workbench currently renders the same navigation and workflow for every
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,3 +212,7 @@ Real-world acceptance includes:
- Shipping media, cached Audible payloads, credentials, Whisper models, or Python environments.
- Adding hooks without a concrete curator lifecycle need.
- Building a filesystem router or general web framework when a declarative application tree and shared operation registry suffice.
- **Superseded (2026-08-31):** the meta-framework route compiler
([issue #93](https://github.com/ScriptedAlchemy/agent-bundle/issues/93))
deliberately adopts convention-driven filesystem routes; this non-goal no
longer binds future work.
6 changes: 5 additions & 1 deletion packages/agent-bundle/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export {
serializeArtifactManifest,
} from './build/manifest.ts';
import { composeBundlerInspection, type BundlerInspection } from './build/inspect-bundler.ts';
import { emptyAgentRouteGraph } from './routes/types.ts';
import type { AgentRouteGraph } from './routes/types.ts';
export type { BundlerInspection, BundlerInspectionEntry } from './build/inspect-bundler.ts';
import { validateArtifact } from './build/validate-artifact.ts';
import { freezeDiagnostics, hasErrors, DiagnosticError, type Diagnostic } from './core/diagnostics.ts';
Expand Down Expand Up @@ -199,7 +201,7 @@ export interface InspectionPlan {
}

export interface InspectOptions extends ProjectOptions {
readonly focus?: 'bundler' | 'hooks' | 'skills';
readonly focus?: 'bundler' | 'hooks' | 'routes' | 'skills';
readonly target?: string;
}

Expand All @@ -211,6 +213,7 @@ export interface ReadyInspectResult {
readonly selected?: {
readonly bundler?: BundlerInspection;
readonly hooks?: NormalizedPlugin['hooks'];
readonly routes?: AgentRouteGraph;
readonly skills?: NormalizedPlugin['skills'];
};
readonly state: 'ready';
Expand Down Expand Up @@ -477,6 +480,7 @@ export const inspect = async (options: InspectOptions): Promise<InspectResult> =
: Object.freeze({
...(bundler === undefined ? {} : { bundler }),
...(options.focus === 'hooks' ? { hooks: model.hooks } : {}),
...(options.focus === 'routes' ? { routes: prepared.routeGraph ?? emptyAgentRouteGraph } : {}),
...(options.focus === 'skills' ? { skills: model.skills } : {}),
});
return Object.freeze({
Expand Down
16 changes: 15 additions & 1 deletion packages/agent-bundle/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ interface InspectCommandOptions {
readonly json?: boolean;
readonly mode?: string;
readonly root: string;
readonly routes?: boolean;
readonly skills?: boolean;
readonly target?: string;
}
Expand Down Expand Up @@ -214,6 +215,17 @@ const writeHumanInspect = (output: Output, result: Awaited<ReturnType<typeof ins
output.write(`${JSON.stringify(result.selected.bundler, null, 2)}\n`);
return;
}
if (result.selected?.routes !== undefined) {
const graph = result.selected.routes;
output.write(`Discovered ${graph.routes.length} route(s) across ${graph.servers.length} generated server(s)\n`);
for (const route of graph.routes) {
output.write(`${route.kind} ${route.id}${route.serverId === undefined ? '' : ` (server ${route.serverId})`}\n`);
}
for (const diagnostic of graph.diagnostics) {
output.write(`${diagnostic.code}: ${diagnostic.message}\n`);
}
return;
}
output.write(`Inspected ${result.model.metadata.name}: ${result.plans.map((plan) => plan.target).join(', ')}\n`);
};

Expand Down Expand Up @@ -409,9 +421,10 @@ export const runCli = async (
)
.option('--bundler', 'Include the synthesized bundler configuration focus')
.option('--hooks', 'Include the hook focus')
.option('--routes', 'Include the route-graph focus')
.option('--skills', 'Include the skill focus');
inspectCommand.action(async (options: InspectCommandOptions) => {
const focuses = [options.bundler, options.hooks, options.skills].filter((focus) => focus === true);
const focuses = [options.bundler, options.hooks, options.routes, options.skills].filter((focus) => focus === true);
if (focuses.length > 1) {
throw new TypeError('Choose at most one inspect focus.');
}
Expand All @@ -420,6 +433,7 @@ export const runCli = async (
...inspectProjectOptions(options),
...(options.bundler === true ? { focus: 'bundler' as const } : {}),
...(options.hooks === true ? { focus: 'hooks' as const } : {}),
...(options.routes === true ? { focus: 'routes' as const } : {}),
...(options.skills === true ? { focus: 'skills' as const } : {}),
...(options.target === undefined ? {} : { target: options.target }),
});
Expand Down
12 changes: 12 additions & 0 deletions packages/agent-bundle/src/config/discover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import fastGlob from 'fast-glob';
import { isInside } from '../core/paths.ts';
import { isRecord } from '../core/strict-json.ts';
import type { AgentBundleConfig } from '../core/types.ts';
import { discoverRouteGraph } from '../routes/discover.ts';
import type { AgentRouteGraph } from '../routes/types.ts';
import { isProjectPathIgnored, readProjectIgnoreRules } from './ignore.ts';
import { isRenderedSkillSourceName } from './rendered-skill.ts';
import { parseSkill, type SkillDocument } from './skill.ts';
Expand Down Expand Up @@ -38,6 +40,14 @@ export interface DiscoveredPayload {
export interface DiscoveredProject {
assets?: DiscoveredAsset[];
payloads?: DiscoveredPayload[];
/**
* The immutable filesystem route graph (#93 substrate), compiled from the
* conventional route roots with the same sorted, ignore-aware discovery the
* rest of the project uses. Absent when no conventional route module
* matches and no route diagnostic fires — the state of every project that
* has not adopted route conventions.
*/
routeGraph?: AgentRouteGraph;
/**
* Conventional `skills/<name>/SKILL.md` documents that explicit `skills`
* configuration leaves uncovered — the confusable shadowed state surfaced
Expand Down Expand Up @@ -229,9 +239,11 @@ export const discoverProject = async (
const shadowedConventionalSkills = [...shadowedByDir.values()];

const payloads = await discoverPayloads(projectRoot, config.payload);
const routeGraph = await discoverRouteGraph(projectRoot, config, rules);
return {
assets: await discoverAssets(projectRoot, config.assets, rules),
...(payloads.length === 0 ? {} : { payloads }),
...(routeGraph.routes.length === 0 && routeGraph.diagnostics.length === 0 ? {} : { routeGraph }),
...(shadowedConventionalSkills.length === 0 ? {} : { shadowedConventionalSkills }),
skills: await Promise.all(
skillDirs.map((skillDir) => parseSkill(skillDir, projectRoot, rules)),
Expand Down
3 changes: 3 additions & 0 deletions packages/agent-bundle/src/config/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1397,6 +1397,9 @@ export const validateSource = (
diagnostics.push(...validateTools(loaded));
diagnostics.push(...packageConventionShadowNudges(loaded));
diagnostics.push(...skillConventionShadowNudges(loaded, discovered));
// Route-graph diagnostics (AB4800-AB4804): mode conflicts, duplicate route
// ids, and unsafe route names computed once at discovery (#93 substrate).
diagnostics.push(...(discovered.routeGraph?.diagnostics ?? []));

return diagnostics;
};
Expand Down
6 changes: 6 additions & 0 deletions packages/agent-bundle/src/dev/project-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import type {
NormalizedMcpServer,
NormalizedPlugin,
} from '../core/types.ts';
import type { AgentRouteGraph } from '../routes/types.ts';
import type { DevRuntimePreparedMcpApp, DevRuntimePreparedMcpServer, DevRuntimePreparedProject } from './runtime-provider.ts';
import { freezeJsonValue, type JsonObject, type JsonValue, type SourceStatus } from './types.ts';

Expand Down Expand Up @@ -59,6 +60,8 @@ export interface PreparedProject {
readonly projectContext?: ProjectContext;
readonly registry: TargetRegistry;
readonly root: string;
/** The immutable filesystem route graph (#93 substrate); absent when no conventional route module matches. */
readonly routeGraph?: AgentRouteGraph;
/**
* Re-snapshots the project with the same output and payload roots the
* prepared identity hashed; a divergent re-snapshot would make
Expand Down Expand Up @@ -534,6 +537,7 @@ const preparedProject = (
devRuntimeDiagnostic?: Diagnostic,
devAgentApiEnabled?: boolean,
tools?: AgentBundleToolsConfig,
routeGraph?: AgentRouteGraph,
): PreparedProject => Object.freeze({
configPath,
...(devAgentApiEnabled === true ? { devAgentApiEnabled } : {}),
Expand All @@ -545,6 +549,7 @@ const preparedProject = (
...(projectContext === undefined ? {} : { projectContext }),
registry,
root,
...(routeGraph === undefined ? {} : { routeGraph }),
snapshotSource,
source,
...(tools === undefined ? {} : { tools }),
Expand Down Expand Up @@ -854,6 +859,7 @@ export class ProjectService {
devRuntimeDiagnostic,
devAgentApiEnabled,
tools,
discovered.routeGraph,
);
}
}
Loading
Loading