eve-hub 0.5.6 → 0.6.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +5 -4
  3. package/execution-context.mjs +79 -0
  4. package/fork-env.mjs +3 -2
  5. package/host-browser.js +3 -1
  6. package/host-sandbox.d.ts +1 -2
  7. package/host-sandbox.js +10 -5
  8. package/{schedules/classify.d.ts → instructions/engineering.d.ts} +1 -2
  9. package/instructions/engineering.js +6 -0
  10. package/main-instructions.mjs +13 -6
  11. package/memory/workspace.js +8 -3
  12. package/memory.mjs +78 -24
  13. package/package.json +9 -2
  14. package/prepare.mjs +219 -0
  15. package/registry-fetch.mjs +7 -0
  16. package/schedules/dash-classify.mjs +86 -39
  17. package/schedules/dash-pass.mjs +51 -0
  18. package/schedules/dash-rules.mjs +9 -2
  19. package/schedules/dash-run.mjs +16 -7
  20. package/schedules/dash.js +14 -16
  21. package/schedules/hub.d.mts +2 -1
  22. package/schedules/hub.mjs +4 -1
  23. package/schedules/lane-facts.mjs +134 -0
  24. package/schedules/thread-brief.mjs +4 -2
  25. package/schedules/track-summaries.mjs +108 -0
  26. package/stubs.mjs +46 -0
  27. package/subagent-cap.d.mts +4 -3
  28. package/templates/engineering.md +24 -0
  29. package/templates/execution-context-hosted-main.md +4 -0
  30. package/templates/execution-context-local-main.md +7 -0
  31. package/templates/execution-context-local.md +4 -0
  32. package/templates/main-instructions.md +5 -11
  33. package/templates/workspace-protocol.md +9 -7
  34. package/tools/apply_tags.d.ts +6 -6
  35. package/tools/apply_tags.js +4 -4
  36. package/tools/create_agent.d.ts +2 -0
  37. package/tools/create_agent.js +1 -0
  38. package/tools/create_thread.d.ts +4 -4
  39. package/tools/create_thread.js +5 -5
  40. package/tools/dash_update.d.ts +4 -4
  41. package/tools/dash_update.js +7 -7
  42. package/tools/list_threads.d.ts +2 -2
  43. package/tools/list_threads.js +2 -2
  44. package/tools/post_message.d.ts +18 -0
  45. package/tools/post_message.js +11 -5
  46. package/tools/tag_threads.d.ts +6 -6
  47. package/tools/tag_threads.js +4 -4
  48. package/tools/update_thread.d.ts +4 -4
  49. package/tools/update_thread.js +4 -4
  50. package/vendor/world-turso.mjs +6416 -0
  51. package/vercel.mjs +15 -0
  52. package/workspace.mjs +39 -3
  53. package/world.mjs +27232 -0
  54. package/schedules/classify.js +0 -20
  55. package/schedules/tracks-lead.mjs +0 -107
  56. package/schedules/tracks.d.ts +0 -11
  57. package/schedules/tracks.js +0 -18
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # eve-hub
2
2
 
3
+ ## 0.6.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Every bot gets an execution-context prompt (`agents/<bot>/agent/instructions/execution-context.ts`), chosen by its definition in `hub.workspace.json`: `runsOn` on the agent, else a workspace-wide `"runsOn"` (a local hub's workspace declares `"local"`; a deployable repo declares `"hosted"`), else hosted. A local bot's comes from the kit's templates; the Vercel build writes a hosted bot's from what it produced: the eve-hub version, the roster and which bots run on the Mac, the deployment, team and repo commit (or that the hub uploaded the files), the kit tools it removed, and the dashboard links to its logs. The shared prompts keep only what is true in both contexts: main's restart, team, memory and Dash-schedule paragraphs, and the image and local-file link rules, moved into the per-context pages. `hub workspace sync` declares `"runsOn": "local"` in a workspace file that declares nothing.
8
+
9
+ ## 0.6.0
10
+
11
+ ### Minor Changes
12
+
13
+ - A workspace repo carries its bots, memory and workspace file; the Vercel build prepares it (eve-hub/vercel), including bots that run on the Mac (`runsOn: "local"`). The Mac runner downloads the deployed commit through the hub instead of reading a folder.
14
+
15
+ ## 0.5.7
16
+
17
+ ### Patch Changes
18
+
19
+ - One `dash` schedule (every 8 minutes, and when a bot's turn ends) replaces `dash`, `classify` and `tracks`: one GitHub query, a model call per thread whose facts changed and a model call per track with news. The `classify.ts` and `tracks.ts` stubs are retired; the track-writer bot is no longer woken.
20
+ - `post_message` takes a reviewer's `verdict` (ship, fix-first, rethink) and `decisions`; a ship without a change-explainer for the PR shows "no explainer" on the Dash.
21
+
3
22
  ## 0.5.6
4
23
 
5
24
  ### Patch Changes
package/README.md CHANGED
@@ -8,8 +8,9 @@ The bot-side code every hub agent shares (milestone M3 in [`docs/architecture`](
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`, `agent_send.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
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` | every other bot's `agent/instructions/protocol.ts`, appended after its own `instructions.md` (who the bot is) |
12
- | `memory/workspace.ts`, `memory.mjs` | an eve memory slot (default export, constant `workspace` scope) whose provider recalls the workspace's `memory/<id>.md` entries, 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"` |
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) |
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
+ | `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"` |
13
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"` |
14
15
  | `channels/eve-roster-user.ts` | `eveChannel({ auth: [rosterUser, localDev()] })`: eve's default steer policy, and the registry's `x-roster-user` human as a user principal | every bot's `agent/channels/eve.ts` |
15
16
  | `channels/eve.ts` | Re-exports `eve-roster-user.ts`, for stubs written before every bot shared one channel | older `agent/channels/eve.ts` stubs |
@@ -56,11 +57,11 @@ Add a changeset (`pnpm changeset`, pick `eve-hub`) when a tool's name, schema, o
56
57
 
57
58
  ## Published package (deployed workspaces)
58
59
 
59
- A deployed workspace (`node scripts/workspace-template.mjs <dir>`, lib/workspace-template.mjs) has no hub checkout to link, so it installs the kit from npm, published as [`eve-hub`](https://www.npmjs.com/package/eve-hub): `"eve-hub": "<version>"`. `node packages/agent-kit/scripts/pack.mjs <outDir>` builds the package: every `.ts` module compiled to `.js` + `.d.ts` in place (node won't strip types under `node_modules`), the `exports` map pointed at them, and `templates/` included.
60
+ A deployed workspace (lib/workspace-template.mjs, staged by hosted-hub/scripts/stage-workspace.mjs) has no hub checkout to link, so it installs the kit from npm, published as [`eve-hub`](https://www.npmjs.com/package/eve-hub): `"eve-hub": "<version>"`. `node packages/agent-kit/scripts/pack.mjs <outDir>` builds the package: every `.ts` module compiled to `.js` + `.d.ts` in place (node won't strip types under `node_modules`), the `exports` map pointed at them, and `templates/` included.
60
61
 
61
62
  Releases go through Changesets, as in eve. A PR that changes the kit adds a changeset (`pnpm changeset`) and applies it (`pnpm changeset version`); when it merges, `.github/workflows/release.yml` runs `changeset publish` (no bot commits to main: its ruleset allows only PR merges): the kit's `prepack` stages the package into `dist/npm` (its `publishConfig.directory`), npm gets it, and the `eve-hub@<version>` GitHub release is created. npm auth is the repo's `NPM_TOKEN` secret (an npm automation token). The template pins the version this checkout carries.
62
63
 
63
- The template's main gets a Vercel Sandbox and only the kit stubs that run without the hub's machine (lib/workspace-template.mjs `DEPLOYED_KIT_FILES`; eve's own bash replaces the kit's). The published tarball (`scripts/pack.mjs`) leaves out the kit__worker extension, which mounts only on the hub's machine. The hub reaches each bot's eve channel with `Authorization: Bearer $BOT_TOKEN` (`bot-token.mjs`); the kit's calls to the hub (`registry-fetch.mjs`, at `ROSTER_REGISTRY_URL`) carry `Authorization: Bearer $HUB_TOKEN` and, since the hosted hub keeps Vercel Authentication on, `x-vercel-protection-bypass: $HUB_PROTECTION_BYPASS` (the hub project's automation bypass secret), each when set.
64
+ The template's main gets a Vercel Sandbox and only the kit stubs that run without the hub's machine (lib/workspace-template.mjs `DEPLOYED_KIT_FILES`; eve's own bash replaces the kit's). The template's builder also mounts eve-code (`eve/extensions/code`: `apply_patch`, `grep`, the review skills, a read-only worker) and `eve/extensions/git` (`git({})`), both with no `github` option. The bot uses `gh` and `git` from bash, whose GitHub token is the sandbox firewall's (`VERCEL_SANDBOX`), and no `gh` tool is contributed. A deployed workspace's eve is the one pin in `hosted-hub/bots/package.json`, which staging copies over the template's (an eve snapshot with both `eve/world-hub` and `eve/extensions/git`; `hosted-hub/test/stage-workspace.test.mjs` checks the staged imports resolve against it). The hub and local bots stay on the hub's eve. The published tarball (`scripts/pack.mjs`) leaves out the kit__worker extension, which mounts only on the hub's machine. The hub reaches each bot's eve channel with `Authorization: Bearer $BOT_TOKEN` (`bot-token.mjs`); the kit's calls to the hub (`registry-fetch.mjs`, at `ROSTER_REGISTRY_URL`) carry `Authorization: Bearer $HUB_TOKEN` and, since the hosted hub keeps Vercel Authentication on, `x-vercel-protection-bypass: $HUB_PROTECTION_BYPASS` (the hub project's automation bypass secret), each when set.
64
65
 
65
66
  ## Vercel Kit connection
66
67
 
@@ -0,0 +1,79 @@
1
+ // A bot's execution-context prompt. A bot is defined in hub.workspace.json
2
+ // (`runsOn` on the agent, else the file's own default, else hosted:
3
+ // workspace.mjs agentRunsOn), and the page it gets follows from that field
4
+ // alone: the local page from the kit's templates (every bot's
5
+ // instructions/execution-context.ts stub calls `executionContext`), or, for a
6
+ // bot the Vercel build deploys, a hosted page the build writes over that stub
7
+ // (prepare.mjs), because only the build knows the deployment. The shared
8
+ // prompts (templates/main-instructions.md, workspace-protocol.md) hold only
9
+ // what is true in both contexts.
10
+ import { readFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { templatesDir } from "./main-instructions.mjs";
13
+ import { agentRunsOn, parseWorkspace } from "./workspace.mjs";
14
+
15
+ const page = (name, options) => readFileSync(join(templatesDir(options), `execution-context-${name}.md`), "utf8").trimEnd();
16
+
17
+ // The build's Vercel facts. The team and project name come from the build's
18
+ // OIDC token (its `owner` and `project` claims); anything the build doesn't
19
+ // carry stays undefined and the page leaves it out. A malformed token fails
20
+ // the build.
21
+ const claims = (jwt) => JSON.parse(Buffer.from(jwt.split(".")[1], "base64url").toString());
22
+
23
+ export function deploymentFacts(env = process.env) {
24
+ const oidc = env.VERCEL_OIDC_TOKEN ? claims(env.VERCEL_OIDC_TOKEN) : {};
25
+ const repo = env.VERCEL_GIT_REPO_OWNER && env.VERCEL_GIT_REPO_SLUG ? `${env.VERCEL_GIT_REPO_OWNER}/${env.VERCEL_GIT_REPO_SLUG}` : undefined;
26
+ return {
27
+ team: oidc.owner,
28
+ project: oidc.project,
29
+ projectId: env.VERCEL_PROJECT_ID,
30
+ deployment: env.VERCEL_DEPLOYMENT_ID,
31
+ url: env.VERCEL_URL,
32
+ environment: env.VERCEL_ENV,
33
+ repo,
34
+ branch: env.VERCEL_GIT_COMMIT_REF,
35
+ sha: env.VERCEL_GIT_COMMIT_SHA,
36
+ };
37
+ }
38
+
39
+ const list = (items) => items.map((item) => `\`${item}\``).join(", ");
40
+
41
+ export function hostedContext({ bot, roster, kit, removed, deployment: d }, options) {
42
+ const mac = roster.filter((agent) => agent.runsOn === "local").map((agent) => agent.name);
43
+ const cloud = roster.filter((agent) => agent.runsOn !== "local").map((agent) => agent.name);
44
+ const dashboard = d.team && d.project ? `https://vercel.com/${d.team}/${d.project}` : undefined;
45
+ const lines = [
46
+ "## Execution context",
47
+ `You are \`${bot}\`, an eve agent deployed on Vercel, not on a laptop: \`hub.workspace.json\` defines you as hosted.`,
48
+ "",
49
+ `- **Workspace:** ${cloud.length ? `${list(cloud)} run on Vercel` : "no bot runs on Vercel"}${mac.length ? `; ${list(mac)} run on the owner's Mac through the hub runner` : ""}. One hosted hub holds the threads and routes messages to each.`,
50
+ `- **Package:** your tools, hooks, channel and instructions come from the \`eve-hub\` package, version ${kit}.`,
51
+ `- **Deployment:** ${[
52
+ (d.project ?? d.projectId) && `project \`${d.project ?? d.projectId}\``,
53
+ d.team && `team \`${d.team}\``,
54
+ d.deployment && `deployment \`${d.deployment}\``,
55
+ d.environment && `${d.environment} environment`,
56
+ d.url && `at \`https://${d.url}\``,
57
+ ].filter(Boolean).join(", ") || "no Vercel facts were available to this build"}. Each bot is its own service at \`/eve/<name>\` in that one project.`,
58
+ `- **Source:** ${d.repo ? `built from \`${d.repo}\`${d.branch ? ` on \`${d.branch}\`` : ""}${d.sha ? ` at commit \`${d.sha}\`` : ""}` : "no git repository: the hub uploaded these files"}. You are made of files in it: \`agents/${bot}/\`, \`memory/\` and \`hub.workspace.json\`.`,
59
+ d.repo
60
+ ? "- **Changing it:** commit to that repository; Vercel builds and deploys the commit."
61
+ : "- **Changing it:** there is no repository to commit to. The hub replaces these files on its next deploy, so an edit made only in this deployment does not last.",
62
+ "- **Limits:** there is no `hub` CLI, you cannot restart yourself or any bot, and there are no host paths: your shell is a Vercel Sandbox with its own filesystem, not the owner's machine.",
63
+ `- **Kit tools this build removed:** ${removed.length ? list(removed) : "none"}. They need the hub's machine, or change the roster, which a deployment does by commit.`,
64
+ `- **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
+ ...(bot === "main" ? [page("hosted-main", options)] : []),
66
+ ];
67
+ return `${lines.join("\n")}\n`;
68
+ }
69
+
70
+ // What a bot's instructions/execution-context.ts stub renders. Hosted is the
71
+ // Vercel build's to write (hostedContext), never this process's guess.
72
+ export function executionContext(bot, workspace, options) {
73
+ const parsed = parseWorkspace(workspace);
74
+ const runsOn = agentRunsOn(parsed, bot);
75
+ if (runsOn !== "local") {
76
+ 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
+ }
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";
79
+ }
package/fork-env.mjs CHANGED
@@ -5,13 +5,14 @@
5
5
  // the fork brief only restate this; the sandbox enforces the defaults.
6
6
  // Short-lived either way: a fork can be retired (and an in-place fork's
7
7
  // thread go back to the shared root) while its session lives on.
8
- import { registryBase, registryFetch } from "./registry-fetch.mjs";
8
+ import { registryFetch, sandboxRegistry } from "./registry-fetch.mjs";
9
9
 
10
10
  const TTL_MS = 3000;
11
11
 
12
- export function createForkEnv({ registry = registryBase(), fetchImpl = globalThis.fetch, now = Date.now } = {}) {
12
+ export function createForkEnv({ registry = sandboxRegistry(), fetchImpl = globalThis.fetch, now = Date.now } = {}) {
13
13
  const cache = new Map(); // sessionId -> { config, at }
14
14
  return async function lookup(sessionId) {
15
+ if (registry === null) return null; // no local hub: no forks
15
16
  const hit = cache.get(sessionId);
16
17
  if (hit && now() - hit.at < TTL_MS) return hit.config;
17
18
  let config = null;
package/host-browser.js CHANGED
@@ -11,7 +11,7 @@ import { spawn } from "node:child_process";
11
11
  import { existsSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
12
12
  import { homedir, tmpdir } from "node:os";
13
13
  import { join } from "node:path";
14
- import { registryFetch } from "./registry-fetch.mjs";
14
+ import { registryFetch, sandboxRegistry } from "./registry-fetch.mjs";
15
15
  export const VIEWPORT = { width: 1280, height: 800 };
16
16
  const FRAME_MS = 200; // at most 5 frames/s leave for the registry
17
17
  const IDLE_MS = 10 * 60_000; // an unused browser is closed; the next use reopens its last URL
@@ -43,6 +43,8 @@ export function findChrome() {
43
43
  return existsSync(chrome) ? { path: chrome, shell: false } : null;
44
44
  }
45
45
  function reportScreen(body) {
46
+ if (!sandboxRegistry())
47
+ return;
46
48
  void registryFetch(`/api/sandbox/screen`, {
47
49
  method: "POST",
48
50
  headers: { "content-type": "application/json" },
package/host-sandbox.d.ts CHANGED
@@ -104,11 +104,10 @@ export interface Fence {
104
104
  (what: string): Promise<void>;
105
105
  readonly sessionId: string;
106
106
  readonly root: string;
107
- readonly registry: string;
108
107
  admit(kind: TicketKind, op: TicketOp, what: string, group?: TicketGroup): Promise<Ticket>;
109
108
  }
110
109
  export declare function ownershipGuard(sessionId: string, { registry, rootSessionId, agent }?: {
111
- registry?: string;
110
+ registry?: string | null;
112
111
  rootSessionId?: string;
113
112
  agent?: string;
114
113
  }): Fence;
package/host-sandbox.js CHANGED
@@ -15,7 +15,7 @@ import { promisify } from "node:util";
15
15
  import { defineSandboxProvider } from "eve/sandbox/provider";
16
16
  import { closeBrowser } from "./host-browser.js";
17
17
  import { createForkEnv } from "./fork-env.mjs";
18
- import { registryBase, registryFetch, registryHeaders } from "./registry-fetch.mjs";
18
+ import { registryFetch, registryHeaders, sandboxRegistry } from "./registry-fetch.mjs";
19
19
  const execFileAsync = promisify(execFile);
20
20
  export const HOST_ROOT = resolve(process.env.ROSTER_HOST_ROOT ?? join(homedir(), "wrk"));
21
21
  // The provider's lifecycle, exported so tests drive start/resume directly.
@@ -145,11 +145,13 @@ export function killTree(child) {
145
145
  // Live terminal: every process's output is teed to the roster registry so the
146
146
  // UI can show each bot's sandbox as it works. Best-effort; never blocks.
147
147
  let procSeq = 0;
148
- const forkConfig = createForkEnv({ registry: registryBase() });
148
+ const forkConfig = createForkEnv({ registry: sandboxRegistry() });
149
149
  // `retries`: the exit report is the one that must land (without it the
150
150
  // registry eventually marks the proc interrupted), so it rides out a registry
151
151
  // restart of a few seconds.
152
152
  function reportProc(body, retries = 0) {
153
+ if (!sandboxRegistry())
154
+ return;
153
155
  void registryFetch(`/api/sandbox/proc`, {
154
156
  method: "POST",
155
157
  headers: { "content-type": "application/json" },
@@ -233,7 +235,8 @@ function teeProcess(child, sessionId, command, cwd, rootSessionId) {
233
235
  // with identical parameters until a definite answer or a bounded deadline,
234
236
  // after which the op is not performed (a paused launcher is killed; a "not-run"
235
237
  // release is queued for the id) and SessionFenceError is thrown.
236
- // HUB_OWNERSHIP_GUARD=off disables tickets entirely.
238
+ // HUB_OWNERSHIP_GUARD=off disables tickets entirely, as does having no local
239
+ // registry (sandboxRegistry(): a laptop bot of the hosted hub).
237
240
  //
238
241
  // LIMITS of the guarantee: (1) unfenced work while the registry is unreachable
239
242
  // is outside it, by policy, and only reported after the fact; (2) descendants
@@ -402,8 +405,10 @@ export function stopRevocationPollers() {
402
405
  p.stop();
403
406
  pollers.clear();
404
407
  }
405
- export function ownershipGuard(sessionId, { registry = registryBase(), rootSessionId, agent = process.env.HUB_AGENT } = {}) {
408
+ export function ownershipGuard(sessionId, { registry = sandboxRegistry(), rootSessionId, agent = process.env.HUB_AGENT } = {}) {
406
409
  const root = rootSessionId ?? sessionId;
410
+ if (registry === null)
411
+ return Object.assign(async () => { }, { sessionId, root, admit: async () => UNFENCED }); // no local hub: none to fence with
407
412
  let warned = false;
408
413
  const warnOnce = (message) => {
409
414
  if (!warned)
@@ -507,7 +512,7 @@ export function ownershipGuard(sessionId, { registry = registryBase(), rootSessi
507
512
  if (!ownership.owner)
508
513
  throw refusal(what, ownership);
509
514
  };
510
- return Object.assign(check, { sessionId, root, registry, admit });
515
+ return Object.assign(check, { sessionId, root, admit });
511
516
  }
512
517
  // The paused launcher. /bin/sh runs this with fd 3 a socket to us and the
513
518
  // shell-startup variables moved to HUB_LAUNCH_* (so neither sh nor anything it
@@ -1,5 +1,4 @@
1
1
  declare const _default: {
2
- cron: string;
3
- run(): Promise<void>;
2
+ content: any;
4
3
  };
5
4
  export default _default;
@@ -0,0 +1,6 @@
1
+ import { defineInstructions } from "eve/instructions";
2
+ import { engineeringPage } from "../main-instructions.mjs";
3
+ // The engineering page alone, for a subagent that doesn't carry the workspace
4
+ // protocol (the protocol ends with this same page). A subagent re-exports this as
5
+ // its instructions/engineering.ts.
6
+ export default defineInstructions({ content: engineeringPage() });
@@ -1,5 +1,6 @@
1
1
  // main's instructions, rendered from the kit's templates: templates/main-instructions.md,
2
- // then the shared protocol (templates/workspace-protocol.md). One source for
2
+ // then the shared protocol (templates/workspace-protocol.md, then the engineering
3
+ // page, templates/engineering.md). One source for
3
4
  // `hub workspace init` (lib/agent-template.mjs) and for every workspace's main,
4
5
  // which imports it (instructions/main.ts) instead of carrying a copy, so a
5
6
  // template edit reaches each workspace on main's next build. Every other bot
@@ -11,6 +12,7 @@ import { findWorkspaceRoot } from "./workspace.mjs";
11
12
 
12
13
  const HEAD = "main-instructions.md";
13
14
  const PROTOCOL = "workspace-protocol.md";
15
+ const ENGINEERING = "engineering.md";
14
16
  const templatesOf = (kitDir) => join(kitDir, "templates");
15
17
 
16
18
  // The templates sit next to the kit in the hub checkout. Run by node, this
@@ -27,13 +29,18 @@ export function templatesDir({ env = process.env, cwd = process.cwd() } = {}) {
27
29
  }
28
30
 
29
31
  export function mainInstructions(options) {
30
- const dir = templatesDir(options);
31
- const read = (name) => readFileSync(join(dir, name), "utf8");
32
- return `${read(HEAD).trimEnd()}\n\n${read(PROTOCOL)}`;
32
+ const head = readFileSync(join(templatesDir(options), HEAD), "utf8");
33
+ return `${head.trimEnd()}\n\n${workspaceProtocol(options)}`;
33
34
  }
34
35
 
35
- // The shared protocol alone: every bot but main appends it after its own
36
+ // The engineering page alone: the protocol ends with it, and a subagent that
37
+ // never sees the protocol includes it (instructions/engineering.ts).
38
+ export function engineeringPage(options) {
39
+ return readFileSync(join(templatesDir(options), ENGINEERING), "utf8");
40
+ }
41
+
42
+ // The shared protocol: every bot but main appends it after its own
36
43
  // instructions.md (agent/instructions/protocol.ts re-exports instructions/protocol.ts).
37
44
  export function workspaceProtocol(options) {
38
- return readFileSync(join(templatesDir(options), PROTOCOL), "utf8");
45
+ return `${readFileSync(join(templatesDir(options), PROTOCOL), "utf8")}\n${engineeringPage(options)}`;
39
46
  }
@@ -1,13 +1,18 @@
1
1
  import { defineMemory, defineMemoryProvider } from "eve/memory";
2
- import { loadMemoryRecall } from "../memory.mjs";
2
+ import { botDomains, findBotDir, loadMemoryRecall } from "../memory.mjs";
3
3
  import { findWorkspaceRoot } from "../workspace.mjs";
4
4
  // The workspace's memory/ entries as an eve memory slot (see ../memory.mjs).
5
5
  // Recall re-reads memory/ at the start of every turn and after compaction and
6
6
  // returns only the entries whose text changed since this session last saw
7
7
  // them; eve appends those as user-role messages after the history, so the
8
8
  // system prompt and the cached prefix stay put when an entry changes.
9
- // One constant scope and namespace: every bot, every session, the same entries.
10
- const recall = ({ messages }) => ({ messages: loadMemoryRecall(findWorkspaceRoot(), messages) });
9
+ // One constant scope and namespace. The entries are memory/ (every bot) plus
10
+ // the folders for the domains this bot lists in hub.workspace.json.
11
+ const recall = ({ messages }) => {
12
+ const root = findWorkspaceRoot();
13
+ const dir = findBotDir();
14
+ return { messages: loadMemoryRecall(root, messages, { domains: dir ? botDomains(root, dir) : [] }) };
15
+ };
11
16
  export default defineMemory({
12
17
  description: "The human's standing decisions for this workspace, from its memory/ directory.",
13
18
  namespace: "hub-workspace-memory",
package/memory.mjs CHANGED
@@ -5,6 +5,12 @@
5
5
  // them at the start of every turn (memory/workspace.ts, memory/bot.ts), so an
6
6
  // added, edited or superseded entry reaches the bot's next turn with no
7
7
  // rebuild or restart, appended after the history. Node built-ins only.
8
+ // A workspace may group entries into domains: folders under memory/, nestable
9
+ // (memory/ui/, memory/ui/motion/), same entry format. memory/ itself is the
10
+ // domain every bot shares. A bot lists its domains in its hub.workspace.json
11
+ // entry (`domains: ["ui/motion"]`) and recalls memory/ plus each listed
12
+ // folder and every folder above it. The kit names no domain; a workspace
13
+ // defines its own.
8
14
  // Shared entries name no bot: bots pick who acts next from the live workspace
9
15
  // state, never from a name, and a rule for one role says so in its text. A
10
16
  // rule for one bot goes in that bot's memory/ (or its instructions.md).
@@ -24,7 +30,7 @@
24
30
  import { createHash } from "node:crypto";
25
31
  import { existsSync, readFileSync, readdirSync } from "node:fs";
26
32
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
27
- import { isWorkspace } from "./workspace.mjs";
33
+ import { isWorkspace, readWorkspaceFile } from "./workspace.mjs";
28
34
 
29
35
  export const MEMORY_DIR = "memory";
30
36
  export const MAX_RULE_LINES = 3;
@@ -82,28 +88,64 @@ export function parseEntry(text, file = "entry.md") {
82
88
  return { id: f.id, status: f.status, since: f.since, evidence: f.evidence, ...(supersededBy ? { supersededBy } : {}), rule: lines.join(" ") };
83
89
  }
84
90
 
85
- // Every entry under <dir>/memory, sorted by id, and one error string per file
86
- // that isn't a valid entry. No memory/ directory is an empty memory. `label`
87
- // names the directory in errors; `knownIds` are ids a superseded_by may also
88
- // point at (a bot's entries may hand over to a shared one).
89
- export function readMemory(root, { label = MEMORY_DIR, knownIds = new Set() } = {}) {
90
- const dir = join(root, MEMORY_DIR);
91
- if (!existsSync(dir)) return { entries: [], errors: [] };
91
+ // The folders under <root>/memory a bot with these domains recalls, as paths
92
+ // relative to memory/: "" (the shared root), then each domain and its ancestors.
93
+ export function domainFolders(domains = []) {
94
+ const folders = new Set([""]);
95
+ for (const domain of domains) {
96
+ const parts = domain.split("/");
97
+ for (let i = 1; i <= parts.length; i++) folders.add(parts.slice(0, i).join("/"));
98
+ }
99
+ return [...folders].sort();
100
+ }
101
+
102
+ // Every folder under <root>/memory, "" included: what a workspace's tests validate.
103
+ export function allMemoryFolders(root) {
104
+ const folders = [""];
105
+ const walk = (rel) => {
106
+ for (const d of readdirSync(join(root, MEMORY_DIR, rel), { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : 1))) {
107
+ if (!d.isDirectory() || d.name.startsWith(".")) continue;
108
+ folders.push(rel ? `${rel}/${d.name}` : d.name);
109
+ walk(rel ? `${rel}/${d.name}` : d.name);
110
+ }
111
+ };
112
+ if (existsSync(join(root, MEMORY_DIR))) walk("");
113
+ return folders;
114
+ }
115
+
116
+ // Every entry in the given folders of <dir>/memory (default: the root only),
117
+ // sorted by id, and one error string per file that isn't a valid entry, per
118
+ // listed folder that doesn't exist, and per id used twice. No memory/
119
+ // directory is an empty memory. `label` names the directory in errors;
120
+ // `knownIds` are ids a superseded_by may also point at (a bot's entries may
121
+ // hand over to a shared one).
122
+ export function readMemory(root, { label = MEMORY_DIR, knownIds = new Set(), folders = [""] } = {}) {
92
123
  const entries = [];
93
124
  const errors = [];
94
- const files = readdirSync(dir).filter((name) => name.endsWith(".md") && !name.startsWith(".") && name.toLowerCase() !== "readme.md").sort();
95
- for (const name of files) {
96
- const file = `${label}/${name}`;
97
- try {
98
- const entry = parseEntry(readFileSync(join(dir, name), "utf8"), file);
99
- if (`${entry.id}.md` !== name) throw new MemoryError(`${file}: id ${entry.id} doesn't match the file name; rename it ${entry.id}.md`);
100
- entries.push(entry);
101
- } catch (error) {
102
- errors.push(error instanceof MemoryError ? error.message : `${file}: ${error.message}`);
125
+ const where = new Map();
126
+ for (const folder of folders) {
127
+ const dir = folder ? join(root, MEMORY_DIR, folder) : join(root, MEMORY_DIR);
128
+ const prefix = folder ? `${label}/${folder}` : label;
129
+ if (!existsSync(dir)) {
130
+ if (folder) errors.push(`${prefix}: domain folder doesn't exist`);
131
+ continue;
132
+ }
133
+ const files = readdirSync(dir, { withFileTypes: true }).filter((d) => d.isFile() && d.name.endsWith(".md") && !d.name.startsWith(".") && d.name.toLowerCase() !== "readme.md").map((d) => d.name).sort();
134
+ for (const name of files) {
135
+ const file = `${prefix}/${name}`;
136
+ try {
137
+ const entry = parseEntry(readFileSync(join(dir, name), "utf8"), file);
138
+ if (`${entry.id}.md` !== name) throw new MemoryError(`${file}: id ${entry.id} doesn't match the file name; rename it ${entry.id}.md`);
139
+ if (where.has(entry.id)) throw new MemoryError(`${file}: id ${entry.id} is also ${where.get(entry.id)}; an id is used once`);
140
+ where.set(entry.id, file);
141
+ entries.push(entry);
142
+ } catch (error) {
143
+ errors.push(error instanceof MemoryError ? error.message : `${file}: ${error.message}`);
144
+ }
103
145
  }
104
146
  }
105
147
  const ids = new Set(entries.map((e) => e.id));
106
- for (const e of entries) if (e.supersededBy && !ids.has(e.supersededBy) && !knownIds.has(e.supersededBy)) errors.push(`${label}/${e.id}.md: superseded_by ${e.supersededBy} has no entry`);
148
+ for (const e of entries) if (e.supersededBy && !ids.has(e.supersededBy) && !knownIds.has(e.supersededBy)) errors.push(`${where.get(e.id)}: superseded_by ${e.supersededBy} has no entry`);
107
149
  entries.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
108
150
  return { entries, errors };
109
151
  }
@@ -190,10 +232,11 @@ const warnOnce = (msg) => {
190
232
  console.warn(`memory: ${msg}`);
191
233
  };
192
234
 
193
- const unreadableIds = (errors) => new Set(errors.map((e) => /(?:^|\/)memory\/([^/:]+)\.md:/.exec(e)?.[1]).filter(Boolean));
235
+ const unreadableIds = (errors) => new Set(errors.map((e) => /(?:^|\/)memory\/(?:[^/:]+\/)*([^/:]+)\.md:/.exec(e)?.[1]).filter(Boolean));
194
236
 
195
- export function loadMemoryRecall(root, history = [], { warn = warnOnce } = {}) {
196
- const { entries, errors } = readMemory(root);
237
+ // `domains`: the bot's domains (hub.workspace.json), see the header.
238
+ export function loadMemoryRecall(root, history = [], { warn = warnOnce, domains = [] } = {}) {
239
+ const { entries, errors } = readMemory(root, { folders: domainFolders(domains) });
197
240
  for (const error of errors) warn(error);
198
241
  return memoryRecall(entries, history, { unreadable: unreadableIds(errors) });
199
242
  }
@@ -203,7 +246,7 @@ export function loadMemoryRecall(root, history = [], { warn = warnOnce } = {}) {
203
246
  // never by shadowing it), so a clash is an error and that file is skipped.
204
247
  export function readBotMemory(root, botDir) {
205
248
  const label = `${relative(root, botDir).split(sep).join("/")}/${MEMORY_DIR}`;
206
- const shared = new Set(readMemory(root).entries.map((e) => e.id));
249
+ const shared = new Set(readMemory(root, { folders: allMemoryFolders(root) }).entries.map((e) => e.id));
207
250
  const { entries, errors } = readMemory(botDir, { label, knownIds: shared });
208
251
  const clash = entries.filter((e) => shared.has(e.id));
209
252
  for (const e of clash) errors.push(`${label}/${e.id}.md: id ${e.id} is also ${MEMORY_DIR}/${e.id}.md; give it its own id and supersede the other explicitly`);
@@ -231,10 +274,21 @@ export function findBotDir({ env = process.env, cwd = process.cwd() } = {}) {
231
274
  return null;
232
275
  }
233
276
 
277
+ // The domains the bot in `botDir` lists in its hub.workspace.json entry. The
278
+ // bot is its directory name, as in findBotDir.
279
+ export function botDomains(root, botDir) {
280
+ return readWorkspaceFile(root).agents.find((a) => a.name === basename(botDir))?.domains ?? [];
281
+ }
282
+
234
283
  // For a workspace's tests: throws one MemoryError listing every bad file,
235
- // shared and per bot (agents/*/memory/). Returns the shared entries.
284
+ // shared (every folder) and per bot (agents/*/memory/), and every domain a bot
285
+ // lists that has no folder. Returns the shared entries.
236
286
  export function validateMemory(root) {
237
- const { entries, errors } = readMemory(root);
287
+ const { entries, errors } = readMemory(root, { folders: allMemoryFolders(root) });
288
+ if (isWorkspace(root)) {
289
+ const folders = new Set(allMemoryFolders(root));
290
+ for (const { name, domains = [] } of readWorkspaceFile(root).agents) for (const d of domains) if (!folders.has(d)) errors.push(`hub.workspace.json: ${name} lists domain ${d}, which has no memory/${d}/ folder`);
291
+ }
238
292
  const agents = join(root, "agents");
239
293
  if (existsSync(agents)) for (const name of readdirSync(agents).sort()) if (existsSync(join(agents, name, MEMORY_DIR))) errors.push(...readBotMemory(root, join(agents, name)).errors);
240
294
  if (errors.length) throw new MemoryError(`${errors.length} bad memory entr${errors.length === 1 ? "y" : "ies"}:\n${errors.join("\n")}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eve-hub",
3
- "version": "0.5.6",
3
+ "version": "0.6.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",
@@ -23,13 +23,20 @@
23
23
  "./subagent-cap": "./subagent-cap.mjs",
24
24
  "./session-limits": "./session-limits.mjs",
25
25
  "./main-instructions": "./main-instructions.mjs",
26
+ "./execution-context": "./execution-context.mjs",
26
27
  "./connections/*": "./connections/*.js",
27
- "./bot-token": "./bot-token.mjs"
28
+ "./bot-token": "./bot-token.mjs",
29
+ "./prepare": "./prepare.mjs",
30
+ "./vercel": "./vercel.mjs",
31
+ "./world": "./world.mjs"
28
32
  },
29
33
  "peerDependencies": {
30
34
  "eve": "*",
31
35
  "zod": "4.5.4",
32
36
  "@vercel/connect": "^2.4.1",
33
37
  "@vercel/oidc": "4.0.0"
38
+ },
39
+ "dependencies": {
40
+ "ai": "^7.0.122"
34
41
  }
35
42
  }