@junejs/server 1.0.0-dev.17 → 1.0.0-dev.20

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.
@@ -0,0 +1,111 @@
1
+ import { findMiddlewareFile } from "./router.js";
2
+ import { resolveBoundary } from "./segment.js";
3
+ import { freezeConfig } from "./config-freeze.js";
4
+ import { scanAppRoutes } from "./route-scan.js";
5
+ import { existsSync } from "node:fs";
6
+ import { basename, join } from "node:path";
7
+ import { pathToFileURL } from "node:url";
8
+ import { routeFromModule } from "@junejs/core/route";
9
+ //#region src/manifest.ts
10
+ async function importLayout(file) {
11
+ const mod = await import(pathToFileURL(file).href);
12
+ return typeof mod.default === "function" ? {
13
+ component: mod.default,
14
+ boundary: mod.segmentBoundary === true
15
+ } : null;
16
+ }
17
+ async function buildManifest(appRoot) {
18
+ const appDir = join(appRoot, "app");
19
+ const frozen = await freezeConfig(appRoot);
20
+ const scanned = await scanAppRoutes(appRoot);
21
+ const layoutCache = /* @__PURE__ */ new Map();
22
+ const loadCached = async (f) => {
23
+ if (!layoutCache.has(f)) layoutCache.set(f, await importLayout(f));
24
+ return layoutCache.get(f) ?? null;
25
+ };
26
+ const componentsFor = async (files) => {
27
+ const items = [];
28
+ for (const f of files) {
29
+ const c = await loadCached(f);
30
+ items.push({
31
+ file: f,
32
+ entry: c?.component ?? null,
33
+ boundary: !!c?.boundary
34
+ });
35
+ }
36
+ return resolveBoundary(items);
37
+ };
38
+ const loadingCache = /* @__PURE__ */ new Map();
39
+ const loadingFor = async (file) => {
40
+ if (!file) return void 0;
41
+ if (!loadingCache.has(file)) {
42
+ const loaded = await importLayout(file);
43
+ loadingCache.set(file, loaded ? loaded.component : null);
44
+ }
45
+ return loadingCache.get(file) ?? void 0;
46
+ };
47
+ const routes = {};
48
+ const dynamicRoutes = [];
49
+ const resourceRoutes = [];
50
+ const layoutChains = {};
51
+ const layoutBoundaries = {};
52
+ const loadings = {};
53
+ for (const r of scanned) {
54
+ const mod = await import(pathToFileURL(r.file).href);
55
+ if (r.resource) {
56
+ const handler = mod.default;
57
+ if (typeof handler === "function") resourceRoutes.push({
58
+ pattern: r.path,
59
+ handler
60
+ });
61
+ continue;
62
+ }
63
+ const def = routeFromModule(mod);
64
+ if (!def) continue;
65
+ const { chain, boundaryIndex, key } = await componentsFor(r.layouts);
66
+ const loading = await loadingFor(r.loading);
67
+ if (r.dynamic) {
68
+ dynamicRoutes.push({
69
+ pattern: r.path,
70
+ def
71
+ });
72
+ layoutChains[r.path] = chain;
73
+ } else {
74
+ routes[r.path] = def;
75
+ layoutChains[r.path] = chain;
76
+ }
77
+ if (boundaryIndex !== null && key !== null) layoutBoundaries[r.path] = {
78
+ index: boundaryIndex,
79
+ key
80
+ };
81
+ if (loading) loadings[r.path] = loading;
82
+ }
83
+ let extra;
84
+ const extraFile = findMiddlewareFile(appDir);
85
+ if (extraFile) {
86
+ const mod = await import(pathToFileURL(extraFile).href);
87
+ if (typeof mod.default === "function") extra = mod.default;
88
+ }
89
+ let agentName;
90
+ const agentDir = join(appDir, frozen.agent.runtime.dir);
91
+ if (frozen.agent.runtime.enabled && existsSync(agentDir)) {
92
+ const cfgFile = join(agentDir, "agent.ts");
93
+ agentName = (existsSync(cfgFile) ? (await import(pathToFileURL(cfgFile).href)).default : void 0)?.name ?? basename(agentDir);
94
+ }
95
+ return {
96
+ routes,
97
+ dynamicRoutes,
98
+ resourceRoutes,
99
+ layoutChains,
100
+ layoutBoundaries,
101
+ loadings,
102
+ document: frozen.document,
103
+ agent: frozen.agent,
104
+ agentName,
105
+ i18n: frozen.i18n,
106
+ earlyHints: frozen.earlyHints,
107
+ extra
108
+ };
109
+ }
110
+ //#endregion
111
+ export { buildManifest, importLayout };
@@ -0,0 +1,13 @@
1
+ //#region src/route-scan.d.ts
2
+ type RouteEntry = {
3
+ path: string;
4
+ file: string;
5
+ dynamic: boolean;
6
+ resource?: boolean;
7
+ layouts: string[];
8
+ loading?: string;
9
+ };
10
+ declare function scanRoutes(appDir: string, dir?: string, layouts?: string[], out?: RouteEntry[], loading?: string): Promise<RouteEntry[]>;
11
+ declare function scanAppRoutes(appRoot: string): Promise<RouteEntry[]>;
12
+ //#endregion
13
+ export { scanAppRoutes as n, scanRoutes as r, RouteEntry as t };
@@ -0,0 +1,2 @@
1
+ import { n as scanAppRoutes, r as scanRoutes, t as RouteEntry } from "./route-scan-DMlwoESy.js";
2
+ export { RouteEntry, scanAppRoutes, scanRoutes };
@@ -0,0 +1,55 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readdir } from "node:fs/promises";
3
+ import { basename, join, relative, sep } from "node:path";
4
+ //#region src/route-scan.ts
5
+ const PAGE_BASENAMES = /* @__PURE__ */ new Set(["page", "index"]);
6
+ const ROUTE_EXTS = [
7
+ ".tsx",
8
+ ".jsx",
9
+ ".ts",
10
+ ".js"
11
+ ];
12
+ const isRouteGroup = (name) => /^\(.+\)$/.test(name);
13
+ function segmentFile(dir, base) {
14
+ return ROUTE_EXTS.map((e) => join(dir, `${base}${e}`)).find(existsSync);
15
+ }
16
+ async function scanRoutes(appDir, dir = appDir, layouts = [], out = [], loading) {
17
+ const ownLayout = segmentFile(dir, "layout");
18
+ const chain = ownLayout ? [...layouts, ownLayout] : layouts;
19
+ const nearestLoading = segmentFile(dir, "loading") ?? loading;
20
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
21
+ for (const e of entries) {
22
+ if (e.name.startsWith("_") || e.name.startsWith(".")) continue;
23
+ const full = join(dir, e.name);
24
+ if (e.isDirectory()) {
25
+ await scanRoutes(appDir, full, chain, out, nearestLoading);
26
+ continue;
27
+ }
28
+ const ext = e.name.match(/\.[^.]+$/)?.[0] ?? "";
29
+ if (!ROUTE_EXTS.includes(ext)) continue;
30
+ const base = basename(e.name, ext);
31
+ const resource = base === "route";
32
+ if (!PAGE_BASENAMES.has(base) && !resource) continue;
33
+ const relDir = relative(appDir, dir);
34
+ const path = "/" + (relDir === "" ? [] : relDir.split(sep).filter((s) => !isRouteGroup(s))).join("/");
35
+ out.push({
36
+ path: path === "/" ? "/" : path,
37
+ file: full,
38
+ dynamic: /\[.+\]/.test(path),
39
+ resource,
40
+ layouts: chain,
41
+ loading: nearestLoading
42
+ });
43
+ }
44
+ return out;
45
+ }
46
+ async function scanAppRoutes(appRoot) {
47
+ const appDir = join(appRoot, "app");
48
+ const juneRoutesDir = join(appRoot, ".june", "routes");
49
+ const appRoutes = await scanRoutes(appDir);
50
+ const frameworkRoutes = existsSync(juneRoutesDir) ? await scanRoutes(juneRoutesDir) : [];
51
+ const appPaths = new Set(appRoutes.map((r) => r.path));
52
+ return [...appRoutes, ...frameworkRoutes.filter((r) => !appPaths.has(r.path))].sort((a, b) => a.path.localeCompare(b.path));
53
+ }
54
+ //#endregion
55
+ export { scanAppRoutes, scanRoutes };
@@ -0,0 +1,61 @@
1
+ import { l as ResourceHandler, n as LayoutComponent, r as LoadingComponent, t as ExtraHandler } from "./pipeline-cCR6MGNK.js";
2
+ import { Channel, ChannelFactory } from "@junejs/core/agent-config";
3
+ import { BrandedRoute } from "@junejs/core/route";
4
+ import { AgentConfig } from "@junejs/core/config";
5
+ import React from "react";
6
+ import { DocumentConfig } from "@junejs/core/document";
7
+ import { I18nConfig } from "@junejs/core/i18n";
8
+ import { Resources } from "@junejs/core/resources";
9
+ //#region src/worker.d.ts
10
+ type WorkerExecutionContext = {
11
+ waitUntil?(p: Promise<unknown>): void;
12
+ };
13
+ type WorkerManifest = {
14
+ routes: Record<string, BrandedRoute>;
15
+ dynamicRoutes?: Array<{
16
+ pattern: string;
17
+ def: BrandedRoute;
18
+ }>;
19
+ resourceRoutes?: Array<{
20
+ pattern: string;
21
+ handler: ResourceHandler;
22
+ }>;
23
+ layoutChains?: Record<string, LayoutComponent[]>;
24
+ layoutBoundaries?: Record<string, {
25
+ index: number;
26
+ key: string;
27
+ }>;
28
+ loadings?: Record<string, LoadingComponent>;
29
+ document: DocumentConfig;
30
+ agent: AgentConfig;
31
+ agentName?: string;
32
+ agentChannels?: (Channel | ChannelFactory)[];
33
+ i18n?: I18nConfig;
34
+ earlyHints?: string[];
35
+ htmlCacheControl?: string;
36
+ notFound?: React.ComponentType<{
37
+ pathname: string;
38
+ }>;
39
+ extra?: ExtraHandler;
40
+ resources?: (env?: unknown) => Promise<Resources> | Resources;
41
+ services?: (env?: unknown) => unknown;
42
+ };
43
+ declare function createWorker(manifest: WorkerManifest): {
44
+ fetch(request: Request, env?: unknown, ctx?: WorkerExecutionContext): Promise<Response>;
45
+ };
46
+ type AssetEnv = {
47
+ ASSETS?: {
48
+ fetch(request: Request): Promise<Response>;
49
+ };
50
+ };
51
+ type FetchPipeline = {
52
+ fetch(request: Request, env?: unknown, ctx?: WorkerExecutionContext): Promise<Response>;
53
+ };
54
+ declare function withAssets(pipeline: FetchPipeline, opts?: {
55
+ link?: string | null;
56
+ }): {
57
+ fetch(request: Request, env?: AssetEnv, ctx?: WorkerExecutionContext): Promise<Response>;
58
+ };
59
+ declare function withDenoAssets(pipeline: FetchPipeline): (request: Request) => Promise<Response>;
60
+ //#endregion
61
+ export { withDenoAssets as a, withAssets as i, WorkerManifest as n, createWorker as r, WorkerExecutionContext as t };
package/dist/worker.d.ts CHANGED
@@ -1,56 +1,2 @@
1
- import { l as ResourceHandler, n as LayoutComponent, r as LoadingComponent, t as ExtraHandler } from "./pipeline-cCR6MGNK.js";
2
- import { BrandedRoute } from "@junejs/core/route";
3
- import { AgentConfig } from "@junejs/core/config";
4
- import React from "react";
5
- import { DocumentConfig } from "@junejs/core/document";
6
- import { I18nConfig } from "@junejs/core/i18n";
7
- import { Resources } from "@junejs/core/resources";
8
- //#region src/worker.d.ts
9
- type WorkerManifest = {
10
- routes: Record<string, BrandedRoute>;
11
- dynamicRoutes?: Array<{
12
- pattern: string;
13
- def: BrandedRoute;
14
- }>;
15
- resourceRoutes?: Array<{
16
- pattern: string;
17
- handler: ResourceHandler;
18
- }>;
19
- layoutChains?: Record<string, LayoutComponent[]>;
20
- layoutBoundaries?: Record<string, {
21
- index: number;
22
- key: string;
23
- }>;
24
- loadings?: Record<string, LoadingComponent>;
25
- document: DocumentConfig;
26
- agent: AgentConfig;
27
- agentName?: string;
28
- i18n?: I18nConfig;
29
- earlyHints?: string[];
30
- htmlCacheControl?: string;
31
- notFound?: React.ComponentType<{
32
- pathname: string;
33
- }>;
34
- extra?: ExtraHandler;
35
- resources?: (env?: unknown) => Promise<Resources> | Resources;
36
- services?: (env?: unknown) => unknown;
37
- };
38
- declare function createWorker(manifest: WorkerManifest): {
39
- fetch(request: Request, env?: unknown): Promise<Response>;
40
- };
41
- type AssetEnv = {
42
- ASSETS?: {
43
- fetch(request: Request): Promise<Response>;
44
- };
45
- };
46
- type FetchPipeline = {
47
- fetch(request: Request, env?: unknown): Promise<Response>;
48
- };
49
- declare function withAssets(pipeline: FetchPipeline, opts?: {
50
- link?: string | null;
51
- }): {
52
- fetch(request: Request, env?: AssetEnv): Promise<Response>;
53
- };
54
- declare function withDenoAssets(pipeline: FetchPipeline): (request: Request) => Promise<Response>;
55
- //#endregion
56
- export { WorkerManifest, createWorker, withAssets, withDenoAssets };
1
+ import { a as withDenoAssets, i as withAssets, n as WorkerManifest, r as createWorker, t as WorkerExecutionContext } from "./worker-BkFHGtxp.js";
2
+ export { WorkerExecutionContext, WorkerManifest, createWorker, withAssets, withDenoAssets };
package/dist/worker.js CHANGED
@@ -1,4 +1,4 @@
1
- import { durableAgentSurface } from "./agent-durable.js";
1
+ import { durableAgentSurface, durableChannelSurface } from "./agent-durable.js";
2
2
  import { createPipeline } from "./pipeline.js";
3
3
  import { RESERVED_PREFIX, contentTypeFor, safeRelativePath } from "./static-files.js";
4
4
  //#region src/worker.ts
@@ -61,15 +61,30 @@ function createWorker(manifest) {
61
61
  const loadingFor = (key) => manifest.loadings?.[key];
62
62
  const boundaryFor = (key) => manifest.layoutBoundaries?.[key] ?? null;
63
63
  let currentEnv;
64
+ const requestLocals = /* @__PURE__ */ new WeakMap();
64
65
  const provider = manifest.resources;
65
66
  const servicesProvider = manifest.services;
66
67
  let servicesCache;
67
68
  const agentSurface = manifest.agent.runtime.enabled && manifest.agentName ? (() => {
68
- const route = durableAgentSurface(() => currentEnv?.AGENT, {
69
- agentName: manifest.agentName,
70
- chatPath: manifest.agent.runtime.chat.path
71
- });
72
- return (req) => route(req);
69
+ const agentName = manifest.agentName;
70
+ const wantChannels = manifest.agent.runtime.channels !== false && (manifest.agentChannels?.length ?? 0) > 0;
71
+ return async (req) => {
72
+ const locals = requestLocals.get(req);
73
+ const ns = () => (locals?.env)?.AGENT;
74
+ const chatRes = await durableAgentSurface(ns, {
75
+ agentName,
76
+ chatPath: manifest.agent.runtime.chat.path
77
+ })(req);
78
+ if (chatRes) return chatRes;
79
+ if (!wantChannels) return null;
80
+ return durableChannelSurface(ns, {
81
+ agentName,
82
+ channels: manifest.agentChannels,
83
+ env: locals?.env,
84
+ services: servicesProvider,
85
+ waitUntil: locals?.ctx?.waitUntil?.bind(locals.ctx)
86
+ })(req);
87
+ };
73
88
  })() : void 0;
74
89
  const pipeline = createPipeline({
75
90
  docConfig: manifest.document,
@@ -130,14 +145,18 @@ function createWorker(manifest) {
130
145
  return null;
131
146
  }
132
147
  });
133
- return { fetch(request, env) {
148
+ return { fetch(request, env, ctx) {
134
149
  currentEnv = env;
150
+ requestLocals.set(request, {
151
+ env,
152
+ ctx
153
+ });
135
154
  return pipeline.fetch(request);
136
155
  } };
137
156
  }
138
157
  const estimateTokens = (s) => String(Math.ceil(s.length / 4));
139
158
  function withAssets(pipeline, opts = {}) {
140
- return { async fetch(request, env) {
159
+ return { async fetch(request, env, ctx) {
141
160
  const assets = env?.ASSETS;
142
161
  const url = new URL(request.url);
143
162
  const isPagePath = !/\.[a-z0-9]+$/i.test(url.pathname);
@@ -177,7 +196,7 @@ function withAssets(pipeline, opts = {}) {
177
196
  return a;
178
197
  }
179
198
  }
180
- return pipeline.fetch(request, env);
199
+ return pipeline.fetch(request, env, ctx);
181
200
  } };
182
201
  }
183
202
  function withDenoAssets(pipeline) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junejs/server",
3
- "version": "1.0.0-dev.17",
3
+ "version": "1.0.0-dev.20",
4
4
  "description": "June host adapters + dev server. Host-coupled; depends on the pure @junejs/core contract layer.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -138,7 +138,7 @@
138
138
  "rolldown": "^1.1.0"
139
139
  },
140
140
  "peerDependencies": {
141
- "@junejs/core": ">=0.2.0-dev.33",
141
+ "@junejs/core": ">=0.2.0-dev.38",
142
142
  "react": "^19.2.0",
143
143
  "react-dom": "^19.2.0",
144
144
  "react-server-dom-webpack": "19.2.7",
@@ -31,6 +31,8 @@ export type AgentDirScan = {
31
31
  name: string;
32
32
  hasConfig: boolean;
33
33
  instructions: string | null;
34
+ // instructions.<source>.md at the agent root — per-surface variants (#149).
35
+ surfaceVariants: { source: string; raw: string }[];
34
36
  tools: string[]; // relative specifiers, e.g. "./tools/create_order.ts"
35
37
  skills: { name: string; raw: string }[];
36
38
  channels: string[]; // "./channels/slack.ts"
@@ -59,6 +61,12 @@ export function scanAgentDir(dir: string): AgentDirScan | null {
59
61
  name: basename(dir),
60
62
  hasConfig: existsSync(join(dir, "agent.ts")),
61
63
  instructions: existsSync(instructionsFile) ? readFileSync(instructionsFile, "utf8") : null,
64
+ surfaceVariants: readdirSync(dir, { withFileTypes: true })
65
+ .filter((e) => e.isFile())
66
+ .map((e) => e.name.match(/^instructions\.([a-z0-9_-]+)\.md$/))
67
+ .filter((m): m is RegExpMatchArray => m !== null)
68
+ .sort((a, b) => a[1]!.localeCompare(b[1]!))
69
+ .map((m) => ({ source: m[1]!, raw: readFileSync(join(dir, m[0]!), "utf8") })),
62
70
  tools: list(join(dir, "tools"), ".ts").map((f) => `./tools/${f}`),
63
71
  skills: list(join(dir, "skills"), ".md").map((f) => ({
64
72
  name: basename(f, ".md"),
@@ -77,6 +85,7 @@ export function scanAgentDir(dir: string): AgentDirScan | null {
77
85
  !scan.tools.length &&
78
86
  !scan.skills.length &&
79
87
  !scan.channels.length &&
88
+ !scan.surfaceVariants.length &&
80
89
  !scan.channelOverlays.length &&
81
90
  !scan.connections.length;
82
91
  return empty ? null : scan;
@@ -139,6 +148,9 @@ export function emitAgentModule(scan: AgentDirScan, opts?: EmitOptions): string
139
148
  "export const agentModule: AgentModule = {",
140
149
  " config,",
141
150
  ` instructions: ${JSON.stringify(scan.instructions ?? "")},`,
151
+ scan.surfaceVariants.length
152
+ ? ` surfaceInstructions: {\n${scan.surfaceVariants.map((v) => ` ${JSON.stringify(v.source)}: ${JSON.stringify(v.raw)},`).join("\n")}\n },`
153
+ : " surfaceInstructions: {},",
142
154
  ` tools: [${toolIds.join(", ")}],`,
143
155
  scan.skills.length
144
156
  ? ` skills: [\n${scan.skills.map((s) => ` parseSkill(${JSON.stringify(s.name)}, ${JSON.stringify(s.raw)}),`).join("\n")}\n ],`
@@ -235,6 +247,20 @@ export function generateAgentModule(
235
247
  ): { file: string; code: string; stale: boolean; written: boolean } | null {
236
248
  const scan = scanAgentDir(dir);
237
249
  if (!scan) return null;
250
+ for (const o of scan.channelOverlays) {
251
+ console.warn(
252
+ `[june] deprecated: channels/${o.source}.md — move it to instructions.${o.source}.md at the agent root (per-surface behavior is agent-level; see junebuild/june#149).`,
253
+ );
254
+ }
255
+ // A variant-shaped file the grammar doesn't parse yet must never be silently
256
+ // inert (the same failure class this whole convention exists to kill).
257
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
258
+ if (e.isFile() && /^instructions\..+\.md$/.test(e.name) && !/^instructions\.[a-z0-9_-]+\.md$/.test(e.name)) {
259
+ console.warn(
260
+ `[june] ${e.name}: unrecognized instructions variant — only single-segment sources (instructions.<source>.md) are parsed today; locale variants are a planned follow-up (junebuild/june#149). This file is ignored.`,
261
+ );
262
+ }
263
+ }
238
264
  const code = emitAgentModule(scan, { tsExtensions: opts?.tsExtensions ?? sniffTsExtensions(dir) });
239
265
  const file = join(dir, AGENT_MODULE_FILE);
240
266
  const existing = existsSync(file) ? readFileSync(file, "utf8") : null;
@@ -3,13 +3,16 @@
3
3
  // An agent is a DIRECTORY, not a config object. Location determines function:
4
4
  //
5
5
  // agent/
6
- // agent.ts → default-exports a plain config { name, model?, description? }
7
- // instructions.md → system prompt
8
- // tools/*.ts → each default-exports a defineAction (a tool)
9
- // skills/*.md → each a procedure, loaded on demand (progressive disclosure)
10
- // channels/*.ts → each default-exports a Channel (an inbound edge)
11
- // channels/*.md → source-keyed system overlay for the same-named channel
12
- // connections/*.ts → each default-exports a Connection (an outbound tool source)
6
+ // agent.ts → default-exports a plain config { name, model?, surfaces? }
7
+ // instructions.md → base system prompt
8
+ // instructions.<source>.md → per-surface instruction variant (#149)
9
+ // tools/*.ts → each default-exports a defineAction (a tool),
10
+ // OR an array of defineActions (an integration
11
+ // shipping several tools, e.g. googleDriveTools())
12
+ // skills/*.md → each a procedure, loaded on demand (progressive disclosure)
13
+ // channels/*.ts → each default-exports a Channel (an inbound edge — pure transport)
14
+ // channels/*.md → DEPRECATED overlay location (warned; use instructions.<source>.md)
15
+ // connections/*.ts → each default-exports a Connection (an outbound tool source)
13
16
  //
14
17
  // There is no central registry to keep in sync — the directory IS the manifest.
15
18
  // Because tools are `defineAction`s, the SAME directory is also an MCP server and
@@ -23,8 +26,7 @@ import { readdir, readFile } from "node:fs/promises";
23
26
  import { basename, extname, join } from "node:path";
24
27
  import { pathToFileURL } from "node:url";
25
28
 
26
- import type { AnyAction } from "@junejs/core/agent";
27
- import { assembleAgent, parseSkill, type AgentConfigFile, type AgentDefinition, type AgentModule, type Channel, type ChannelFactory, type Skill } from "@junejs/core/agent-config";
29
+ import { assembleAgent, parseSkill, type AgentConfigFile, type AgentDefinition, type AgentModule, type Channel, type ChannelFactory, type Skill, type ToolEntry } from "@junejs/core/agent-config";
28
30
  import type { Connection } from "@junejs/core/connections";
29
31
 
30
32
  // `_`-prefixed files are private by convention (mirrors the app/ router) — most
@@ -51,10 +53,32 @@ export async function discoverAgentModule(dir: string): Promise<AgentModule> {
51
53
  const instructionsFile = join(dir, "instructions.md");
52
54
  const instructions = existsSync(instructionsFile) ? await readFile(instructionsFile, "utf8") : "";
53
55
 
54
- const tools: AnyAction[] = [];
56
+ // instructions.<source>.md — the agent's per-surface instruction variants
57
+ // (#149): applied to turns arriving through that source, composed per
58
+ // config.surfaces (append by default, "replace" to stand alone).
59
+ const surfaceInstructions: Record<string, string> = {};
60
+ if (existsSync(dir)) {
61
+ for (const e of await readdir(dir, { withFileTypes: true })) {
62
+ if (!e.isFile() || !/^instructions\..+\.md$/.test(e.name)) continue;
63
+ const m = e.name.match(/^instructions\.([a-z0-9_-]+)\.md$/);
64
+ if (m) {
65
+ surfaceInstructions[m[1]!] = await readFile(join(dir, e.name), "utf8");
66
+ } else {
67
+ // A variant-shaped file the grammar doesn't parse yet (e.g. a locale
68
+ // suffix, instructions.slack.zh-TW.md) must never be SILENTLY inert.
69
+ console.warn(
70
+ `[june] ${e.name}: unrecognized instructions variant — only single-segment sources (instructions.<source>.md) are parsed today; locale variants are a planned follow-up (junebuild/june#149). This file is ignored.`,
71
+ );
72
+ }
73
+ }
74
+ }
75
+
76
+ // A tool file default-exports one tool OR an array of tools (an integration
77
+ // like googleDriveTools()); assembly flattens arrays.
78
+ const tools: ToolEntry[] = [];
55
79
  for (const f of await scan(join(dir, "tools"), ".ts")) {
56
80
  const mod = await import(pathToFileURL(f).href);
57
- if (mod.default) tools.push(mod.default as AnyAction);
81
+ if (mod.default) tools.push(mod.default as ToolEntry);
58
82
  }
59
83
 
60
84
  const skills: Skill[] = [];
@@ -71,11 +95,16 @@ export async function discoverAgentModule(dir: string): Promise<AgentModule> {
71
95
  if (mod.default) channels[basename(f, ".ts")] = mod.default as Channel | ChannelFactory;
72
96
  }
73
97
 
74
- // channels/<source>.md — a system overlay applied when a turn's inbound event
75
- // source matches the file's basename (see AgentDefinition.channelInstructions).
98
+ // channels/<source>.md — the DEPRECATED overlay convention (#149): behavior
99
+ // prose belongs at the agent level (instructions.<source>.md), not beside
100
+ // transport code. Still honored for one dev-series, loudly.
76
101
  const channelInstructions: Record<string, string> = {};
77
102
  for (const f of await scan(join(dir, "channels"), ".md")) {
78
- channelInstructions[basename(f, ".md")] = await readFile(f, "utf8");
103
+ const source = basename(f, ".md");
104
+ console.warn(
105
+ `[june] deprecated: channels/${source}.md — move it to instructions.${source}.md at the agent root (per-surface behavior is agent-level; see junebuild/june#149).`,
106
+ );
107
+ channelInstructions[source] = await readFile(f, "utf8");
79
108
  }
80
109
 
81
110
  // connections/*.ts — outbound: definitions of external MCP/OpenAPI servers;
@@ -86,7 +115,7 @@ export async function discoverAgentModule(dir: string): Promise<AgentModule> {
86
115
  if (mod.default) connections.push(mod.default as Connection);
87
116
  }
88
117
 
89
- return { config, instructions, tools, skills, channels, channelInstructions, connections };
118
+ return { config, instructions, surfaceInstructions, tools, skills, channels, channelInstructions, connections };
90
119
  }
91
120
 
92
121
  // Discover an agent from its directory, returning the assembled AgentDefinition