eve-hub 0.9.1 → 0.10.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # eve-hub
2
2
 
3
+ ## 0.10.1
4
+
5
+ ### Patch Changes
6
+
7
+ - The kit's hub client can store a Dash plan (`dashPlan`); the hub checks it and keeps it as the newest.
8
+
9
+ ## 0.10.0
10
+
11
+ ### Minor Changes
12
+
13
+ - Every bot's system prompt is rendered from kit templates, each in its own tag: identity, team, execution, engineering, what_reaches_you, reaching_others, writing, role, execution_context. A bot with `agents/<bot>/agent/role.md` (who it is in the first paragraph, then how it works) gets an `instructions.ts` stub that renders the whole prompt in that order; a bot with only `instructions.md` still builds, with the shared sections after it. The forms guide is the `forms` skill, every bot has it. The team text is replaced by `<team_architecture>`, which a bot builds from `hub.workspace.json` (a hosted bot's is embedded at build) and recalls once per session and when it changes; it no longer asks the hub. A bot's own additions to its role go in `agents/<bot>/agent/role/*.md`, inside `<role>`; `hub workspace sync` moves a kit-rendered bot's `instructions/*.md` there (eve would load that folder after the execution context, outside every tag), and reports a bot that has both `role.md` and `instructions.md` without changing it. `create_agent` writes `role.md`.
14
+
3
15
  ## 0.9.1
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -7,8 +7,8 @@ The bot-side code every hub agent shares (milestone M3 in [`docs/architecture`](
7
7
  | `tools/update_thread.ts`, `list_threads.ts`, `read_thread.ts`, `react.ts`, `show.ts`, `attach_image.ts`, `browser.ts` | Registry and workspace tools (`defineTool`) | `agent/tools/<same name>.ts` |
8
8
  | `tools/create_thread.ts`, `post_message.ts`, `archive_thread.ts`, `unarchive_thread.ts`, `list_agents.ts`, `create_agent.ts`, `remove_agent.ts`, `restart_agent.ts`, `tag_threads.ts`, `apply_tags.ts` | The orchestrator's tools | `main`'s `agent/tools/<same name>.ts` |
9
9
  | `hooks/sandbox.ts` | Migrates legacy Docker sessions to the host sandbox (`defineHook`) | `agent/hooks/sandbox.ts` |
10
- | `instructions/main.ts`, `main-instructions.mjs` | main's instructions (`defineInstructions`), rendered at build time from the kit's `templates/main-instructions.md` + `workspace-protocol.md`, so no workspace carries a copy | `main`'s `agent/instructions.ts`; workspace-only additions in `agent/instructions/*.md` |
11
- | `instructions/protocol.ts` | The shared protocol alone, rendered from `templates/workspace-protocol.md` followed by `templates/engineering.md` | every other bot's `agent/instructions/protocol.ts`, appended after its own `instructions.md` (who the bot is) |
10
+ | `instructions/main.ts`, `main-instructions.mjs` | main's instructions (`defineInstructions`), rendered at build time from the kit's `templates/main-instructions.md` (identity, role) between the shared templates, each in its own tag, so no workspace carries a copy | `main`'s `agent/instructions.ts`; workspace-only additions in `agent/role/*.md` (inside `<role>`) |
11
+ | `instructions/protocol.ts` | The shared sections alone (`templates/team.md`, `execution.md`, `engineering.md`, `deliveries.md`, `reaching-others.md`, `writing.md`, each in its tag), for a bot whose `instructions.md` still says who it is and how it works | every other bot's `agent/instructions/protocol.ts`, appended after its own `instructions.md`; a bot with `agent/role.md` has `agent/instructions.ts` (`botInstructions`) instead, which renders identity, the shared sections and role in order |
12
12
  | `instructions/engineering.ts` | The engineering page alone (`templates/engineering.md`), for a subagent that doesn't carry the protocol | the subagent's `instructions/engineering.ts` |
13
13
  | `memory/workspace.ts`, `memory.mjs` | an eve memory slot (default export, constant `workspace` scope) whose provider recalls the workspace's `memory/<id>.md` entries (plus the `memory/<domain>/` folders the bot lists as `domains` in its roster entry), one message each, at every turn start and after compaction, only the ones whose text changed since the session last saw them, as new messages after the history; `validateMemory(root)` for a workspace's tests | every bot's (main too) `agent/memory/workspace.ts`: `export { default } from "eve-hub/memory/workspace"` |
14
14
  | `memory/bot.ts` | the same for the bot's own `agents/<bot>/memory/<id>.md` entries; the bot is found from its directory (`findBotDir`), scope is its name | every bot's `agent/memory/bot.ts`: `export { default } from "eve-hub/memory/bot"` |
@@ -5,11 +5,11 @@
5
5
  // instructions/execution-context.ts stub calls `executionContext`), or, for a
6
6
  // bot the Vercel build deploys, a hosted page the build writes over that stub
7
7
  // (prepare.mjs), because only the build knows the deployment. The shared
8
- // prompts (templates/main-instructions.md, workspace-protocol.md) hold only
8
+ // prompts (templates/*.md) hold only
9
9
  // what is true in both contexts.
10
10
  import { readFileSync } from "node:fs";
11
11
  import { join } from "node:path";
12
- import { templatesDir } from "./main-instructions.mjs";
12
+ import { section, templatesDir } from "./main-instructions.mjs";
13
13
  import { agentRunsOn, parseWorkspace } from "./workspace.mjs";
14
14
 
15
15
  const page = (name, options) => readFileSync(join(templatesDir(options), `execution-context-${name}.md`), "utf8").trimEnd();
@@ -42,7 +42,6 @@ const list = (items) => items.map((item) => `\`${item}\``).join(", ");
42
42
  export function hostedContext({ bot, kit, removed, deployment: d }, options) {
43
43
  const dashboard = d.team && d.project ? `https://vercel.com/${d.team}/${d.project}` : undefined;
44
44
  const lines = [
45
- "## Execution context",
46
45
  `You are \`${bot}\`, an eve agent deployed on Vercel, not on a laptop: \`hub.workspace.json\` defines you as hosted.`,
47
46
  "",
48
47
  "- **Workspace:** one hosted hub holds the threads and routes messages to each bot. Who is on the team, where each bot runs and what each can do come live from the hub: the team model message at the top of your turn (when it changes) and `list_agents`.",
@@ -64,7 +63,7 @@ export function hostedContext({ bot, kit, removed, deployment: d }, options) {
64
63
  `- **Logs:** ${dashboard ? `[runtime logs](${dashboard}/logs)${d.deployment ? ` and [this deployment](${dashboard}/${d.deployment.replace(/^dpl_/, "")})` : ""} in the Vercel dashboard` : "the Vercel project's runtime logs in the dashboard"}.`,
65
64
  ...(bot === "main" ? [page("hosted-main", options)] : []),
66
65
  ];
67
- return `${lines.join("\n")}\n`;
66
+ return `${section("execution_context", lines.join("\n"))}\n`;
68
67
  }
69
68
 
70
69
  // What a bot's instructions/execution-context.ts stub renders. Hosted is the
@@ -75,5 +74,6 @@ export function executionContext(bot, workspace, options) {
75
74
  if (runsOn !== "local") {
76
75
  throw new Error(`${bot} is defined as hosted in hub.workspace.json, and a hosted bot's execution context is written by its Vercel build (eve-hub/vercel). A bot on this machine needs "runsOn": "local", on the agent or at the top of hub.workspace.json.`);
77
76
  }
78
- return ["## Execution context", `You are \`${bot}\`: \`hub.workspace.json\` defines you as local.`, "", page("local", options), ...(bot === "main" ? [page("local-main", options)] : [])].join("\n") + "\n";
77
+ const lines = [`You are \`${bot}\`: \`hub.workspace.json\` defines you as local.`, "", page("local", options), ...(bot === "main" ? [page("local-main", options)] : [])];
78
+ return `${section("execution_context", lines.join("\n"))}\n`;
79
79
  }
@@ -1,10 +1,41 @@
1
+ ---
2
+ name: forms
3
+ description: How to show structure in a message. Load before you explain anything with parts that connect, events in order, numbers to compare, a before and after, or a choice the human must make; and before you post a table, diagram or chart.
4
+ ---
1
5
 
2
- ## Forms
6
+ # Showing structure
3
7
 
4
- A form is an interactive figure the app draws from a fenced block with a JSON body. Any path through three or more parts, order of events across two or more actors, or numbers to compare is a form, never prose steps, an arrow chain or a character drawing; draw it by default (principle 3 says why). Pick the shape: a path through parts (`flow`), an order of events (`sequence`), numbers to compare (`chart`), a choice you need from the human (`ask`). Anchor the picture in something they know, e.g. a before form then an after form, each labeled, in one message. Every part of one diagram is the same kind of thing (all hosts, all processes or all functions); say which in the sentence before it, and for a sequence in `unit`. A group is the level that contains them (processes grouped under their host). Plain prose is for what no shape helps. At most one or two forms per message.
8
+ ## Examples
9
+
10
+ The prompt's show-don't-tell rule says when to draw. These are past misses.
11
+
12
+ <example>
13
+ The human asks "how do we detect an existing workspace? is it stored in the hub db?" Miss: one sentence and numbered steps. Instead: the one-line answer, then a flow of today's sign-in check and, beside it, the flow with a "create new" option. The question never says "diagram"; it asks for a path through parts.
14
+ </example>
15
+ <example>
16
+ Build times by step. Miss: a schedule chart, then a paragraph repeating every step's seconds. Instead: the chart's title is the claim ("Linking is 70% of the build"), and the prose says only what to do about it.
17
+ </example>
18
+ <example>
19
+ Three candidate looks for a theme. Miss: a prose list of concepts. Instead: a rendered image of each, labeled in a few words.
20
+ </example>
21
+
22
+ ## Picking the shape
23
+ - A path through three or more parts: `flow`.
24
+ - An order of events across two or more actors: `sequence`.
25
+ - Numbers to compare: `chart`. Time split into phases or hops (a latency breakdown, a turn's timeline) is a `schedule` chart, one row per request and one segment per phase, never one `bar` per total. A `bar` compares one number across items.
26
+ - A choice you need from the human: `ask`. Choosing an option sends its label back as their reply, so write labels you can act on.
27
+ - Two or more rows of short, comparable values: a table, at most four columns, a few words per cell. A single item, or anything that needs a reason, is a sentence or bullets.
28
+ - Anchor the picture in something the reader knows: a labeled before form, then an after form, in one message.
29
+ - Every part of one diagram is the same kind of thing (all hosts, all processes or all functions); say which in the sentence before it. A group is the level that contains them.
30
+ - At most one or two forms per message.
31
+ - Never draw with characters: no boxes, arrows, timelines or tables in a code fence.
32
+
33
+ ## How the app shows it
34
+ - Each top-level code fence is a card with a copy button; a table is a table card; a link is a chip showing its label; an attached image is its own tile.
35
+ - A fenced form becomes an interactive figure the human can step through.
36
+
37
+ ## Form syntax
5
38
 
6
- - A code fence is for text the reader will paste, never for a picture: no boxes, arrows, timelines or tables drawn with characters.
7
- - Pick the shape by what the numbers are. Time split into phases or hops (a latency breakdown, a turn's timeline) is a `schedule` chart: one row per turn or request, one start/end segment per phase, never one `bar` per total. A `bar` compares one number across items.
8
39
  - Claim first: a sentence that says what the form shows comes before it. A chart's `title` is its claim.
9
40
  - Link a word in your prose to a part with `[word](#id)`. Ids are lowercase slugs (`a-z`, `0-9`, `-`, up to 32 characters), unique in the whole message.
10
41
  - Put the explanation of a part in `note` (up to 240 characters), which the human sees when they point at or step to it. Keep labels short.
@@ -2,5 +2,5 @@ import { defineInstructions } from "eve/instructions";
2
2
  import { mainInstructions } from "../main-instructions.mjs";
3
3
  // main's system prompt, from hub's templates (see main-instructions.mjs). A
4
4
  // workspace's main re-exports this as agent/instructions.ts; workspace-only
5
- // additions go in agent/instructions/*.md, which eve appends after it.
5
+ // additions go in agent/role/*.md, which the kit puts inside <role>.
6
6
  export default defineInstructions({ content: mainInstructions() });
package/kit-modules.json CHANGED
@@ -11,6 +11,7 @@
11
11
  "memory/workspace",
12
12
  "schedules/dash",
13
13
  "schedules/prune",
14
+ "skills/forms",
14
15
  "skills/onboarding-profile",
15
16
  "tools/apply_tags",
16
17
  "tools/archive_thread",
@@ -1,19 +1,28 @@
1
- // main's instructions, rendered from the kit's templates: templates/main-instructions.md,
2
- // then the shared protocol (templates/workspace-protocol.md, the forms guide,
3
- // templates/forms.md, then the engineering page, templates/engineering.md). One source for
4
- // `hub workspace init` (lib/agent-template.mjs) and for every workspace's main,
5
- // which imports it (instructions/main.ts) instead of carrying a copy, so a
6
- // template edit reaches each workspace on main's next build. Every other bot
7
- // imports the protocol alone (instructions/protocol.ts) after its own head.
8
- import { existsSync, readFileSync, realpathSync } from "node:fs";
1
+ // A bot's system prompt, rendered from the kit's templates. Every section is wrapped in its own
2
+ // tag, in this order: identity, team, execution, engineering, what_reaches_you, reaching_others,
3
+ // writing, role, execution_context (the last one is execution-context.mjs). eve joins a bot's
4
+ // instruction sources with a blank line, flat instructions.{md,ts} first, then instructions/*
5
+ // by file name, so the order is made in code: one source renders identity, the shared sections
6
+ // and role together (botInstructions, mainInstructions), and instructions/execution-context.ts
7
+ // follows. One source for `hub workspace init` (lib/agent-template.mjs) and for every
8
+ // workspace's main, which imports it (instructions/main.ts) instead of carrying a copy, so a
9
+ // template edit reaches each workspace on its next build.
10
+ import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
9
11
  import { dirname, join } from "node:path";
10
12
  import { fileURLToPath } from "node:url";
11
- import { findWorkspaceRoot } from "./workspace.mjs";
13
+ import { ROLE_DIR, ROLE_FILE } from "./role-files.mjs";
14
+ import { WorkspaceError, findWorkspaceRoot } from "./workspace.mjs";
12
15
 
13
16
  const HEAD = "main-instructions.md";
14
- const PROTOCOL = "workspace-protocol.md";
15
- const FORMS = "forms.md";
16
17
  const ENGINEERING = "engineering.md";
18
+ const SHARED = [
19
+ ["team", "team.md"],
20
+ ["execution", "execution.md"],
21
+ [ENGINEERING.slice(0, -3), ENGINEERING],
22
+ ["what_reaches_you", "deliveries.md"],
23
+ ["reaching_others", "reaching-others.md"],
24
+ ["writing", "writing.md"],
25
+ ];
17
26
  const templatesOf = (kitDir) => join(kitDir, "templates");
18
27
 
19
28
  // The templates sit next to the kit in the hub checkout. Run by node, this
@@ -26,22 +35,54 @@ export function templatesDir({ env = process.env, cwd = process.cwd() } = {}) {
26
35
  const link = join(findWorkspaceRoot({ env, cwd }), "node_modules", "eve-hub");
27
36
  const linked = templatesOf(realpathSync(link));
28
37
  if (existsSync(join(linked, HEAD))) return linked;
29
- throw new Error(`main's instruction templates aren't at ${here} or ${linked} (through ${link})`);
38
+ throw new Error(`the kit's prompt templates aren't at ${here} or ${linked} (through ${link})`);
30
39
  }
31
40
 
32
- export function mainInstructions(options) {
33
- const head = readFileSync(join(templatesDir(options), HEAD), "utf8");
34
- return `${head.trimEnd()}\n\n${workspaceProtocol(options)}`;
41
+ export const section = (tag, body) => `<${tag}>\n${body.trim()}\n</${tag}>`;
42
+ const template = (name, options) => readFileSync(join(templatesDir(options), name), "utf8");
43
+
44
+ // A role file is who the bot is (its first paragraph), then how it works.
45
+ export function splitRole(text, source = "role file") {
46
+ const [identity, ...rest] = text.trim().split(/\n\s*\n/);
47
+ const role = rest.join("\n\n").trim();
48
+ if (!identity || !role) throw new Error(`${source} needs an identity paragraph, a blank line, then the role`);
49
+ return { identity, role };
35
50
  }
36
51
 
37
- // The engineering page alone: the protocol ends with it, and a subagent that
38
- // never sees the protocol includes it (instructions/engineering.ts).
39
- export function engineeringPage(options) {
40
- return readFileSync(join(templatesDir(options), ENGINEERING), "utf8");
52
+ // What eve joins: each source's content with a blank line between, and trims only the whole. So a
53
+ // source ends at its closing tag, with no newline of its own.
54
+ const assemble = ({ identity, role }, additions, options) =>
55
+ [section("identity", identity), workspaceProtocol(options), section("role", [role, ...additions].join("\n\n"))].join("\n\n");
56
+
57
+ // A bot's own additions to its role: agents/<bot>/agent/role/*.md, by file name, inside <role>.
58
+ // They can't sit in instructions/*.md: eve loads that folder itself, after execution-context.ts,
59
+ // outside every tag. A workspace with no folder (or no workspace at all) has none.
60
+ function additionsOf(bot, options) {
61
+ let root;
62
+ try {
63
+ root = findWorkspaceRoot(options);
64
+ } catch (error) {
65
+ if (error instanceof WorkspaceError) return [];
66
+ throw error;
67
+ }
68
+ const dir = join(root, "agents", bot, "agent", ROLE_DIR);
69
+ if (!existsSync(dir)) return [];
70
+ return readdirSync(dir).filter((f) => f.endsWith(".md")).sort().map((f) => readFileSync(join(dir, f), "utf8").trim());
41
71
  }
42
72
 
43
- // The shared protocol: every bot but main appends it after its own
44
- // instructions.md (agent/instructions/protocol.ts re-exports instructions/protocol.ts).
45
- export function workspaceProtocol(options) {
46
- return `${readFileSync(join(templatesDir(options), PROTOCOL), "utf8")}${readFileSync(join(templatesDir(options), FORMS), "utf8")}\n${engineeringPage(options)}`;
73
+ export const mainInstructions = (options) => assemble(splitRole(template(HEAD, options), HEAD), additionsOf("main", options), options);
74
+
75
+ // Every other bot: agents/<bot>/agent/role.md in the workspace.
76
+ export { ROLE_DIR, ROLE_FILE };
77
+ export function botInstructions(bot, options) {
78
+ const file = join(findWorkspaceRoot(options), "agents", bot, "agent", ROLE_FILE);
79
+ return assemble(splitRole(readFileSync(file, "utf8"), file), additionsOf(bot, options), options);
47
80
  }
81
+
82
+ // The engineering section alone, for a subagent that doesn't carry the shared sections
83
+ // (instructions/engineering.ts).
84
+ export const engineeringPage = (options) => `${section("engineering", template(ENGINEERING, options))}\n`;
85
+
86
+ // The shared sections, with no identity or role: what a bot whose instructions.md still holds
87
+ // both gets appended (instructions/protocol.ts).
88
+ export const workspaceProtocol = (options) => SHARED.map(([tag, file]) => section(tag, template(file, options))).join("\n\n");
package/memory/team.d.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  declare const recall: ({ messages }: {
2
2
  messages: readonly unknown[];
3
- }) => Promise<{
3
+ }) => {
4
4
  messages: {
5
5
  id: string;
6
6
  content: string;
7
7
  }[];
8
- }>;
8
+ };
9
9
  declare const _default: import("eve/memory").DefinedMemory<{
10
- readonly description: "The team: who is on it, what each bot can do, and who hands work to whom.";
10
+ readonly description: "The team: how work flows between roles, what each owns, where bots run, and your place.";
11
11
  readonly namespace: "hub-team-memory";
12
12
  readonly scope: "workspace";
13
13
  readonly provider: {
package/memory/team.js CHANGED
@@ -1,23 +1,20 @@
1
+ import { basename } from "node:path";
1
2
  import { defineMemory, defineMemoryProvider } from "eve/memory";
2
- import { registryFetch } from "../registry-fetch.mjs";
3
- import { teamRecall } from "../team.mjs";
4
- // The team model (team.mjs) as an eve memory slot: roster, each bot's tools
5
- // and the handoff rules, from the hub's GET /api/team. Recall runs at the start
6
- // of every turn and after compaction, and adds one message only when the team
7
- // differs from what this session was last given, so a turn where nothing
8
- // changed carries nothing. A hub that can't answer within 5 s fails the recall, not the turn.
3
+ import { findBotDir } from "../memory.mjs";
4
+ import { architectureText, teamRecall } from "../team.mjs";
5
+ import { findWorkspaceRoot, readWorkspaceFile } from "../workspace.mjs";
6
+ // The team architecture (team.mjs) as an eve memory slot: how work flows between roles, what each
7
+ // role owns, where bots run and this bot's own place, built from the workspace's hub.workspace.json
8
+ // (a hosted bot's is embedded by its Vercel build, prepare.mjs). Recall runs at the start of every
9
+ // turn and after compaction, and adds one message only when the text differs from what this
10
+ // session was last given. The bot is the agents/<bot> directory this process runs from (findBotDir).
9
11
  const warned = new Set();
10
- async function fetchTeam(since) {
11
- const res = await registryFetch(`/api/team${since ? `?since=${since}` : ""}`, { signal: AbortSignal.timeout(5_000) });
12
- if (res.status === 304)
13
- return null;
14
- if (!res.ok)
15
- throw new Error(`GET /api/team: HTTP ${res.status}: ${(await res.text()).slice(0, 200)}`);
16
- return (await res.json());
17
- }
18
- const recall = async ({ messages }) => {
12
+ const recall = ({ messages }) => {
19
13
  try {
20
- return await teamRecall(messages, fetchTeam);
14
+ const dir = findBotDir();
15
+ if (!dir)
16
+ return { messages: [] };
17
+ return teamRecall(messages, architectureText(readWorkspaceFile(findWorkspaceRoot()).data, basename(dir)));
21
18
  }
22
19
  catch (error) {
23
20
  const note = error.message;
@@ -29,7 +26,7 @@ const recall = async ({ messages }) => {
29
26
  }
30
27
  };
31
28
  export default defineMemory({
32
- description: "The team: who is on it, what each bot can do, and who hands work to whom.",
29
+ description: "The team: how work flows between roles, what each owns, where bots run, and your place.",
33
30
  namespace: "hub-team-memory",
34
31
  scope: "workspace",
35
32
  provider: defineMemoryProvider({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eve-hub",
3
- "version": "0.9.1",
3
+ "version": "0.10.1",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Bot-side code shared by every hub agent: registry tools, the roster hook, the eve channel, the host sandbox and browser, and model helpers.",
6
6
  "type": "module",
@@ -18,6 +18,7 @@
18
18
  "./roster-model": "./roster-model.mjs",
19
19
  "./workspace": "./workspace.mjs",
20
20
  "./memory": "./memory.mjs",
21
+ "./team": "./team.mjs",
21
22
  "./workspace-roster": "./workspace-roster.mjs",
22
23
  "./bot-model": "./bot-model.mjs",
23
24
  "./subagent-cap": "./subagent-cap.mjs",
package/prepare.mjs CHANGED
@@ -11,7 +11,7 @@
11
11
  // is the host's fails the build, and the build never writes a sandbox;
12
12
  // - the kit extension, host-only workers and kit stubs are dropped, the hub's
13
13
  // workflow world is wired in (and the Vercel Kit connection), every bot gets eve's git extension, and the
14
- // workspace's memory is embedded in the bots' memory providers (a function
14
+ // workspace's memory and each bot's team architecture are embedded in the bots' memory providers (a function
15
15
  // has no workspace cwd);
16
16
  // - /hub/manifest: the roster and the commit sha the deployment was built
17
17
  // from, for the hub to sync from: a custom channel on main, routed in
@@ -25,6 +25,7 @@ export { DEPLOYED_KIT_FILES };
25
25
  import { deploymentFacts, hostedContext } from "./execution-context.mjs";
26
26
  import { botDomains, domainFolders, readBotMemory, readMemory, validateMemory } from "./memory.mjs";
27
27
  import { agentRunsOn, readWorkspaceFile, removeWorkspaceAgent } from "./workspace.mjs";
28
+ import { architectureText } from "./team.mjs";
28
29
 
29
30
  // eve's git extension, for every bot: it points the bot at `gh` and `git` in
30
31
  // bash, whose GitHub credential is the sandbox firewall's (the Vercel sandbox lib/agent-template.mjs writes).
@@ -97,6 +98,15 @@ const recall = ({ messages }) => ({ messages: memoryRecall(entries, messages, {
97
98
  export default defineMemory({ namespace: ${JSON.stringify(`hub-${slot}-memory`)}, scope: ${JSON.stringify(scope)}, provider: defineMemoryProvider({ recall: { "turn.started": recall, "compaction.completed": recall } }) });
98
99
  `;
99
100
 
101
+ // The team slot of a hosted bot: its architecture text, built from the whole roster (Mac bots
102
+ // included) at build time, since a function has no workspace file to read.
103
+ const teamProvider = (text) => `import { defineMemory, defineMemoryProvider } from "eve/memory";
104
+ import { teamRecall } from "eve-hub/team";
105
+ const text = ${JSON.stringify(text)};
106
+ const recall = ({ messages }) => teamRecall(messages, text);
107
+ export default defineMemory({ namespace: "hub-team-memory", scope: "workspace", provider: defineMemoryProvider({ recall: { "turn.started": recall, "compaction.completed": recall } }) });
108
+ `;
109
+
100
110
  // Replaces the kit's execution-context stub (the local page) with the hosted
101
111
  // page, for a bot the definition says is hosted. Its removed-tools list is read
102
112
  // from the bot's folder as pruned.
@@ -158,6 +168,9 @@ export function prepareDeployment(dir) {
158
168
  mkdirSync(join(bot, "memory"), { recursive: true });
159
169
  const botDir = join(dir, "agents", name);
160
170
  writeFileSync(join(bot, "memory", "workspace.ts"), memoryProvider("workspace", readMemory(dir, { folders: domainFolders(botDomains(dir, botDir)) }).entries, "Standing decision", "workspace"));
171
+ // A folder the roster doesn't list has no place in the team, and a function can't read the file the stub would.
172
+ if (definition.agents.some((a) => a.name === name)) writeFileSync(join(bot, "memory", "team.ts"), teamProvider(architectureText(definition.data, name)));
173
+ else rmSync(join(bot, "memory", "team.ts"), { force: true });
161
174
  writeFileSync(join(bot, "memory", "bot.ts"), memoryProvider("bot", readBotMemory(dir, botDir).entries, "Your standing decision", name));
162
175
  }
163
176
  }
package/role-files.mjs ADDED
@@ -0,0 +1,5 @@
1
+ // Where a bot's role lives in agents/<bot>/agent/: role.md (who it is, then how it works) and
2
+ // role/*.md (its own additions to the role). Shared by the sync (stubs.mjs) and the renderer
3
+ // (main-instructions.mjs), which a deployed bot imports without stubs.mjs.
4
+ export const ROLE_FILE = "role.md";
5
+ export const ROLE_DIR = "role";
@@ -18,6 +18,7 @@ export type Hub = {
18
18
  setSummary(path: string, text: string): Promise<unknown>;
19
19
  dashClassified(): Promise<Ok & { classifiedAt: string }>;
20
20
  fileThread(threadId: string, path: string): Promise<unknown>;
21
+ dashPlan(basis: string, plan: object): Promise<Ok & { id: string }>;
21
22
  dashEntries(entries: object[], remove?: string[], notice?: Notice | null): Promise<Ok & { entries: DashEntries }>;
22
23
  };
23
24
  export function createHub(options?: { base?: string; agent?: string; fetchImpl?: typeof fetch }): Hub;
package/schedules/hub.mjs CHANGED
@@ -72,6 +72,8 @@ export function createHub({ base = registryBase(), agent = "main", fetchImpl = f
72
72
  // File an unfiled thread into a track (the classifier's pick), as a bot:
73
73
  // the hub refuses a thread that's already filed and doesn't lock it.
74
74
  fileThread: (threadId, path) => must(`/api/threads/${encodeURIComponent(threadId)}/track`, { method: "POST", body: JSON.stringify({ by: agent, path }) }),
75
+ // The plan main's pass made (wire DashPlanIn, checked in hosted-hub/src/domain/dash.mjs normalizePlan); the hub keeps it as the newest.
76
+ dashPlan: (basis, plan) => must("/api/dash/plan", { method: "POST", body: JSON.stringify({ by: agent, basis, plan }) }),
75
77
  // `notice` ({ schedule, text, clientId? }): posted in that schedule's
76
78
  // thread in the same write as the entries, or neither is kept.
77
79
  dashEntries: (entries, remove = [], notice = null) => must("/api/dash/entries", { method: "POST", body: JSON.stringify({ by: agent, entries, remove, ...(notice ? { notice } : {}) }) }),
@@ -0,0 +1,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -0,0 +1,2 @@
1
+ // Generated by scripts/build-onboarding.mjs from forms/SKILL.md. Do not edit.
2
+ export default {"description":"How to show structure in a message. Load before you explain anything with parts that connect, events in order, numbers to compare, a before and after, or a choice the human must make; and before you post a table, diagram or chart.","markdown":"# Showing structure\n\n## Examples\n\nThe prompt's show-don't-tell rule says when to draw. These are past misses.\n\n<example>\nThe human asks \"how do we detect an existing workspace? is it stored in the hub db?\" Miss: one sentence and numbered steps. Instead: the one-line answer, then a flow of today's sign-in check and, beside it, the flow with a \"create new\" option. The question never says \"diagram\"; it asks for a path through parts.\n</example>\n<example>\nBuild times by step. Miss: a schedule chart, then a paragraph repeating every step's seconds. Instead: the chart's title is the claim (\"Linking is 70% of the build\"), and the prose says only what to do about it.\n</example>\n<example>\nThree candidate looks for a theme. Miss: a prose list of concepts. Instead: a rendered image of each, labeled in a few words.\n</example>\n\n## Picking the shape\n- A path through three or more parts: `flow`.\n- An order of events across two or more actors: `sequence`.\n- Numbers to compare: `chart`. Time split into phases or hops (a latency breakdown, a turn's timeline) is a `schedule` chart, one row per request and one segment per phase, never one `bar` per total. A `bar` compares one number across items.\n- A choice you need from the human: `ask`. Choosing an option sends its label back as their reply, so write labels you can act on.\n- Two or more rows of short, comparable values: a table, at most four columns, a few words per cell. A single item, or anything that needs a reason, is a sentence or bullets.\n- Anchor the picture in something the reader knows: a labeled before form, then an after form, in one message.\n- Every part of one diagram is the same kind of thing (all hosts, all processes or all functions); say which in the sentence before it. A group is the level that contains them.\n- At most one or two forms per message.\n- Never draw with characters: no boxes, arrows, timelines or tables in a code fence.\n\n## How the app shows it\n- Each top-level code fence is a card with a copy button; a table is a table card; a link is a chip showing its label; an attached image is its own tile.\n- A fenced form becomes an interactive figure the human can step through.\n\n## Form syntax\n\n- Claim first: a sentence that says what the form shows comes before it. A chart's `title` is its claim.\n- Link a word in your prose to a part with `[word](#id)`. Ids are lowercase slugs (`a-z`, `0-9`, `-`, up to 32 characters), unique in the whole message.\n- Put the explanation of a part in `note` (up to 240 characters), which the human sees when they point at or step to it. Keep labels short.\n- `hi: true` marks the one part that matters, at most one per form. Unknown keys are rejected.\n- Send a form with `post_message`, never in your end-of-turn reply. The hub checks a posted form and rejects a bad one (400) with what's wrong: fix it and post again. Nothing checks a final reply, so a bad form there shows as raw JSON.\n\nA request leaves the [app](#app), the [hub](#hub) routes it, and a [bot](#bot) answers. `flow`: nodes 2–12 (`role`, `group`, `ref` as file:line), edges 1–24 (`from`, `to`, `label` up to 16, `fail` for a failure path), optional `groups`. Steps follow node order.\n\n```flow\n{\"nodes\": [\n {\"id\": \"app\", \"label\": \"App\", \"role\": \"on your Mac\"},\n {\"id\": \"hub\", \"label\": \"Hub\", \"role\": \"threads, routing\", \"hi\": true, \"ref\": \"server.mjs:5323\", \"note\": \"Stores the message, then wakes the bot.\"},\n {\"id\": \"bot\", \"label\": \"Bot\", \"role\": \"one turn\"}],\n \"edges\": [{\"from\": \"app\", \"to\": \"hub\", \"label\": \"send\"}, {\"from\": \"hub\", \"to\": \"bot\", \"label\": \"wake\"}, {\"from\": \"bot\", \"to\": \"hub\", \"label\": \"post\"}]}\n```\n\nThe stream [drops](#drop) before the client [retries](#retry). `sequence`: actors 2–6, messages 1–16 (`from`, `to`, `label` up to 32, `fail` where it breaks, `ref` as file:line; `from` = `to` for a wait or retry inside one actor). Optional `unit` (up to 48 characters) is printed above the actors, e.g. \"each line = one process\"; optional `groups` (up to 3, `id` and `label`) with `group` on an actor draws a box around actors that share a host; grouped actors must sit next to each other. Steps follow message order.\n\n```sequence\n{\"unit\": \"each line = one process\", \"groups\": [{\"id\": \"mac\", \"label\": \"Mac\"}, {\"id\": \"hub-proj\", \"label\": \"Hub project\"}],\n \"actors\": [{\"id\": \"client\", \"label\": \"Client\", \"group\": \"mac\"}, {\"id\": \"hub\", \"label\": \"Hub\", \"group\": \"hub-proj\"}],\n \"messages\": [\n {\"id\": \"drop\", \"from\": \"hub\", \"to\": \"client\", \"label\": \"stream drops\", \"fail\": true, \"ref\": \"Client.swift:257\"},\n {\"id\": \"retry\", \"from\": \"client\", \"to\": \"hub\", \"label\": \"reconnects\", \"note\": \"Resumes from the last event id.\"}]}\n```\n\nFirst token is under 2 s everywhere but [Sydney](#syd). `chart`: `title` (the claim) and `points` 1–24, each with an id and `label`. `type` is one of four:\n- `bar`: `value`; optional `unit`, `target`.\n- `scatter`: axis titles `x`, `y`; points have numeric `x`, `y`.\n- `budget`: `value` > 0; optional `total`; `spare: true` for unused room.\n- `schedule`: `row` (up to 8 rows), `start` < `end`; optional `unit`.\n\n```chart\n{\"type\": \"bar\", \"title\": \"First token is under 2 s everywhere but Sydney\", \"unit\": \"s\", \"target\": 2,\n \"points\": [{\"id\": \"sfo\", \"label\": \"San Francisco\", \"value\": 0.9}, {\"id\": \"syd\", \"label\": \"Sydney\", \"value\": 2.6, \"hi\": true, \"note\": \"No region near it.\"}]}\n```\n\nYou need a decision, so I'm asking you to pick one. `ask`: 2–5 `options`, each with `id`, `label` (up to 40) and an optional one-line `line` (up to 160). Choosing one sends its label back as the human's reply, so write labels you can act on.\n\n```ask\n{\"options\": [\n {\"id\": \"heron\", \"label\": \"Heron Lodge\", \"line\": \"Lake house 90 minutes north. Rooms for all 14, $5,400.\"},\n {\"id\": \"pines\", \"label\": \"The Pines\", \"line\": \"In town, walkable. Sleeps 10, so 4 need a hotel.\"}]}\n```\n"};
@@ -0,0 +1,6 @@
1
+ import { defineSkill } from "eve/skills";
2
+ import skill from "./forms.gen.mjs";
3
+ // The forms skill: forms/SKILL.md, embedded at build time (scripts/build-onboarding.mjs) so it
4
+ // loads the same on the hub's machine and in a deployed bot. The prompt's "show, don't tell"
5
+ // rule says when to draw; the skill says how.
6
+ export default defineSkill(skill);
package/stubs.d.mts CHANGED
@@ -5,3 +5,6 @@ export const MEMORY_STUBS: Record<string, string>;
5
5
  export const PROTOCOL_STUB_FILE: string;
6
6
  export const PROTOCOL_STUB: string;
7
7
  export function kitStub(file: string, options?: { files?: Record<string, string[]> }): string;
8
+ export const ROLE_FILE: string;
9
+ export const ROLE_DIR: string;
10
+ export function botInstructionsStub(name: string): string;
package/stubs.mjs CHANGED
@@ -1,3 +1,5 @@
1
+ import { ROLE_DIR, ROLE_FILE } from "./role-files.mjs";
2
+
1
3
  // eve finds tools, hooks, and channels by file (the tool name is the file
2
4
  // name), so each bot keeps a one-line stub per kit module. Bot file -> what it
3
5
  // is, and which kit module it re-exports (default: the same path).
@@ -15,6 +17,7 @@ export const KIT_FILES = {
15
17
  "tools/read_thread.ts": ["Read any workspace thread."],
16
18
  "tools/list_agents.ts": ["The team right now: kind, place, tools, models, who's busy."],
17
19
  "tools/post_message.ts": ["Post into an existing thread or a bot's DM; with `status`, report the thread's phase."],
20
+ "skills/forms.ts": ["The forms skill (packages/agent-kit/forms/SKILL.md): how to draw a flow, sequence, chart or ask the app renders."],
18
21
  };
19
22
 
20
23
  // main, the orchestrator: the shared files (the same channel as every bot),
@@ -56,6 +59,7 @@ export const DEPLOYED_KIT_FILES = new Set([
56
59
  "tools/onboarding_scripts.ts",
57
60
  ...["react", "show", "update_thread", "list_threads", "read_thread", "post_message"].map((t) => `tools/${t}.ts`),
58
61
  "memory/team.ts",
62
+ "skills/forms.ts",
59
63
  ...["create_thread", "set_summary", "move_path", "archive_thread", "unarchive_thread", "list_agents", "create_agent", "remove_agent", "restart_agent", "tag_threads", "dash_update", "apply_tags"].map((t) => `tools/${t}.ts`),
60
64
  ]);
61
65
 
@@ -67,17 +71,30 @@ export function kitStub(file, { files = KIT_FILES, kit = files[file]?.[1] ?? fil
67
71
  return `// ${what} From eve-hub (packages/agent-kit/${kit}).\nexport { default } from "${kitPath(kit)}";\n`;
68
72
  }
69
73
 
70
- // Every bot but main: instructions.md says who the bot is (its head), and
71
- // instructions/protocol.ts re-exports the kit's protocol, which renders
72
- // packages/agent-kit/templates/workspace-protocol.md at build time. eve appends the
73
- // instructions/ entries after instructions.md, so the protocol follows the head
74
- // and is never copied into a bot.
74
+ // A bot whose instructions.md still holds both who it is and how it works: instructions/protocol.ts
75
+ // re-exports the kit's shared sections (packages/agent-kit/templates/, each in its own tag). eve
76
+ // appends the instructions/ entries after instructions.md, so they follow it and are never copied
77
+ // into a bot. A bot with a role.md (below) gets the whole prompt in order instead.
75
78
  export const PROTOCOL_STUB_FILE = "instructions/protocol.ts";
76
- export const PROTOCOL_STUB = `// The workspace protocol every bot shares, from packages/agent-kit/templates/workspace-protocol.md,
77
- // through eve-hub (packages/agent-kit/instructions/protocol.ts). eve appends it after
78
- // instructions.md, which holds only who this bot is. Don't copy the protocol there.
79
+ export const PROTOCOL_STUB = `// The shared sections every bot carries, from packages/agent-kit/templates/, through eve-hub
80
+ // (packages/agent-kit/instructions/protocol.ts). eve appends them after instructions.md.
81
+ // Don't copy them there.
79
82
  export { default } from "eve-hub/instructions/protocol";
80
83
  `;
84
+ // A bot with a role.md (agents/<bot>/agent/role.md: who it is in the first paragraph, then how it
85
+ // works): instructions.ts renders its whole prompt, in order: identity, the shared sections, role
86
+ // (packages/agent-kit/main-instructions.mjs). eve puts instructions.ts before instructions/*, so
87
+ // the execution context stub follows.
88
+ export { ROLE_DIR, ROLE_FILE };
89
+ export const botInstructionsStub = (name) => `import { defineInstructions } from "eve/instructions";
90
+ import { botInstructions } from "eve-hub/main-instructions";
91
+
92
+ // ${name}'s system prompt: its role.md between the kit's templates (packages/agent-kit/templates/).
93
+ // Edit who ${name} is and how it works in role.md, not here.
94
+ export default defineInstructions({ content: botInstructions(${JSON.stringify(name)}) });
95
+ `;
96
+ const BOT_STUB = /^import \{ defineInstructions \} from "eve\/instructions";\s*import \{ botInstructions \} from "eve-hub\/main-instructions";\s*export default defineInstructions\(\{ content: botInstructions\("[a-z][a-z0-9-]*"\) \}\);?$/;
97
+
81
98
  // Every bot (main too): instructions/execution-context.ts renders the bot's
82
99
  // execution context from its definition in hub.workspace.json
83
100
  // (packages/agent-kit/execution-context.mjs). A hosted bot's is replaced by
@@ -107,13 +124,13 @@ export const MEMORY_STUBS = {
107
124
  };
108
125
 
109
126
  // main's instructions aren't copied into a workspace: agent/instructions.ts
110
- // re-exports the kit's, which renders packages/agent-kit/templates/main-instructions.md +
111
- // workspace-protocol.md at build time (packages/agent-kit/main-instructions.mjs).
112
- // Workspace-only additions go in agent/instructions/*.md; eve appends them.
127
+ // re-exports the kit's, which renders packages/agent-kit/templates/main-instructions.md between
128
+ // the shared templates at build time (packages/agent-kit/main-instructions.mjs).
129
+ // Workspace-only additions go in agent/role/*.md, inside <role>.
113
130
  export const MAIN_INSTRUCTIONS = "instructions.ts";
114
- export const MAIN_INSTRUCTIONS_STUB = `// main's instructions, from packages/agent-kit/templates (main-instructions.md + workspace-protocol.md),
131
+ export const MAIN_INSTRUCTIONS_STUB = `// main's instructions, from packages/agent-kit/templates (main-instructions.md + the shared sections),
115
132
  // through eve-hub (packages/agent-kit/instructions/main.ts). Don't copy them here:
116
- // workspace-only additions go in instructions/*.md next to this file, appended after these.
133
+ // workspace-only additions go in role/*.md next to this file, inside <role>.
117
134
  export { default } from "eve-hub/instructions/main";
118
135
  `;
119
136
 
@@ -129,8 +146,8 @@ export function isKitReexport(source, module = "instructions/main") {
129
146
 
130
147
 
131
148
  // Where a bot's instructions come from, given its files ({ "instructions.md": text, ... }):
132
- // main ("kit", "copy", "custom", "conflict", "missing"), or any other bot (the protocol:
133
- // "kit", "copy", "conflict", "custom", "missing").
149
+ // main ("kit", "copy", "custom", "conflict", "missing"), or any other bot (the shared sections:
150
+ // "kit", "copy", "conflict", "custom", "missing"; "kit" also for a role.md with its instructions.ts stub).
134
151
  const PROTOCOL_HEADING = /^## How this workspace works\s*$/m;
135
152
  export function instructionsSource(files, main) {
136
153
  const has = (file) => typeof files[file] === "string";
@@ -139,6 +156,11 @@ export function instructionsSource(files, main) {
139
156
  if (has(MAIN_INSTRUCTIONS)) return isKitReexport(files[MAIN_INSTRUCTIONS]) ? "kit" : "custom";
140
157
  return has("instructions.md") ? "copy" : "missing";
141
158
  }
159
+ if (has(MAIN_INSTRUCTIONS)) {
160
+ if (has("instructions.md")) return "conflict";
161
+ const code = files[MAIN_INSTRUCTIONS].replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "").trim();
162
+ return BOT_STUB.test(code) ? "kit" : "custom";
163
+ }
142
164
  const copy = has("instructions.md") && PROTOCOL_HEADING.test(files["instructions.md"]);
143
165
  if (!has(PROTOCOL_STUB_FILE)) return copy ? "copy" : "missing";
144
166
  if (copy) return "conflict";