eve-hub 0.6.8 → 0.6.10

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.6.10
4
+
5
+ ### Patch Changes
6
+
7
+ - The change-explainer is gone: its skill and the `artifact_publish`, `artifact_status` and `artifact_list` tools are no longer kit files (sync and the hosted deploy remove them from every bot), and the protocol no longer tells reviewers to publish an explainer.
8
+
9
+ ## 0.6.9
10
+
11
+ ### Patch Changes
12
+
13
+ - A hosted deployment reaches GitHub one way for every bot: `gh` and `git` in bash through the sandbox firewall token, with eve's git extension mounted on each bot. The build no longer wires in the Vercel Kit connection (`GITHUB_PATH` in `github-path.mjs` is the one switch; `"kit"` brings it back).
14
+
3
15
  ## 0.6.8
4
16
 
5
17
  ### Patch Changes
@@ -0,0 +1 @@
1
+ export const GITHUB_PATH: "gh" | "kit";
@@ -0,0 +1,8 @@
1
+ // How every deployed bot reaches GitHub, decided here and nowhere else:
2
+ // "gh" `gh` and `git` in bash (the firewall token and eve's git extension, both
3
+ // always on, prepare.mjs); no Vercel Kit connection, so no bot can pick
4
+ // Kit for GitHub, and onboarding sets up no Kit connector.
5
+ // "kit" the same, plus the Kit connection on every bot (prepare.mjs, and the
6
+ // peer dependencies it needs in hosted-hub/scripts/stage-workspace.mjs) and
7
+ // the Kit connector on each new bots project (hosted-hub/src/onboard/provision-steps.ts).
8
+ export const GITHUB_PATH = "gh";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eve-hub",
3
- "version": "0.6.8",
3
+ "version": "0.6.10",
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",
@@ -12,7 +12,6 @@
12
12
  "./channels/*": "./channels/*.js",
13
13
  "./instructions/*": "./instructions/*.js",
14
14
  "./memory/*": "./memory/*.js",
15
- "./skills/*": "./skills/*.js",
16
15
  "./host-sandbox": "./host-sandbox.js",
17
16
  "./host-browser": "./host-browser.js",
18
17
  "./roster-model": "./roster-model.mjs",
package/prepare.mjs CHANGED
@@ -9,7 +9,8 @@
9
9
  // pays for model calls) with only the kit stubs that run off the hub's
10
10
  // machine (DEPLOYED_KIT_FILES);
11
11
  // - the kit extension, host-only workers and kit stubs are dropped, the hub's
12
- // workflow world and the Vercel Kit connection are wired in, and the
12
+ // workflow world is wired in (and the Vercel Kit connection, if GITHUB_PATH
13
+ // says "kit"), every bot gets eve's git extension, and the
13
14
  // workspace's memory is embedded in the bots' memory providers (a function
14
15
  // has no workspace cwd);
15
16
  // - /hub/manifest: the roster and the commit sha the deployment was built
@@ -17,7 +18,8 @@
17
18
  // vercel.mjs. Bearer BOT_TOKEN, as on each bot's eve channel (bot-token.mjs).
18
19
  import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, rmdirSync, unlinkSync, writeFileSync } from "node:fs";
19
20
  import { basename, dirname, join } from "node:path";
20
- import { KIT_FILES, MAIN_KIT_FILES } from "./stubs.mjs";
21
+ import { GITHUB_PATH } from "./github-path.mjs";
22
+ import { KIT_FILES, MAIN_KIT_FILES, REMOVED_KIT_FILES } from "./stubs.mjs";
21
23
  import { deploymentFacts, hostedContext } from "./execution-context.mjs";
22
24
  import { botDomains, domainFolders, readBotMemory, readMemory, validateMemory } from "./memory.mjs";
23
25
  import { agentRunsOn, readWorkspaceFile, removeWorkspaceAgent } from "./workspace.mjs";
@@ -35,9 +37,8 @@ import { agentRunsOn, readWorkspaceFile, removeWorkspaceAgent } from "./workspac
35
37
  // roster by commit), memory (the workspace's files), and main's schedules.
36
38
  export const DEPLOYED_KIT_FILES = new Set([
37
39
  "channels/eve.ts",
38
- "skills/change-explainer.ts",
39
40
  ...["react", "show", "update_thread", "list_threads", "read_thread", "post_message"].map((t) => `tools/${t}.ts`),
40
- ...["create_thread", "set_summary", "move_path", "archive_thread", "unarchive_thread", "list_agents", "agent_send", "tag_threads", "dash_update", "apply_tags", "artifact_publish", "artifact_status", "artifact_list"].map((t) => `tools/${t}.ts`),
41
+ ...["create_thread", "set_summary", "move_path", "archive_thread", "unarchive_thread", "list_agents", "agent_send", "tag_threads", "dash_update", "apply_tags"].map((t) => `tools/${t}.ts`),
41
42
  ]);
42
43
 
43
44
 
@@ -84,11 +85,14 @@ export const VERCEL_SANDBOX = vercelSandbox("");
84
85
  // The builder's sandbox also carries eve-code's tooling (agents/builder/agent/extensions/code/).
85
86
  export const CODE_SANDBOX = vercelSandbox("\n await installCodeTooling(sandbox);");
86
87
 
88
+ // eve's git extension, for every bot: it points the bot at `gh` and `git` in
89
+ // bash, whose GitHub credential is the sandbox firewall's (VERCEL_SANDBOX above).
90
+ // With no `github` option it brokers no credential. It only guides: its `pr`
91
+ // skill (commit, push, draft PR) loads when a bot publishes.
92
+ export const gitExtensionFiles = () => ({ "extensions/git/extension.ts": `import git from "eve/extensions/git";\n\nexport default git({});\n` });
93
+
87
94
  // Bots that edit code: eve-code (apply_patch, grep, the review skills and its
88
- // read-only worker) plus eve's git extension, both with no `github` option. The
89
- // git extension then points the bot at `gh` and `git` in bash, whose GitHub
90
- // credential is the sandbox firewall's (VERCEL_SANDBOX above), and its `pr` skill
91
- // is the plain commit, push and draft-PR one. The worker runs on the roster's
95
+ // read-only worker), with no `github` option. The worker runs on the roster's
92
96
  // subagent cap like every other subagent, not on eve's built-in default.
93
97
  export const CODE_BOTS = ["builder"];
94
98
  export const codeExtensionFiles = (bot) => ({
@@ -102,7 +106,6 @@ const { model, reasoning } = resolveSubagent(agents, subagentCap, ${JSON.stringi
102
106
 
103
107
  export default code({ worker: { model, reasoning } });
104
108
  `,
105
- "extensions/git/extension.ts": `import git from "eve/extensions/git";\n\nexport default git({});\n`,
106
109
  });
107
110
 
108
111
  const KIT_VERSION = JSON.parse(readFileSync(new URL("./package.json", import.meta.url), "utf8")).version;
@@ -183,16 +186,15 @@ export function prepareDeployment(dir) {
183
186
  if (!file.endsWith("/agent.ts")) continue;
184
187
  const bot = dirname(join(dir, "agents", file));
185
188
  for (const stub of Object.keys({ ...MAIN_KIT_FILES, "memory/workspace.ts": 0, "memory/bot.ts": 0 })) if (!DEPLOYED_KIT_FILES.has(stub) && existsSync(join(bot, stub))) removeStub(bot, stub);
189
+ for (const stub of REMOVED_KIT_FILES) if (existsSync(join(bot, stub))) removeStub(bot, stub);
186
190
  // learning-review is not in MAIN_KIT_FILES, but also requires the host.
187
191
  if (existsSync(join(bot, "schedules/learning-review.ts"))) removeStub(bot, "schedules/learning-review.ts");
188
192
  const name = basename(dirname(bot));
189
193
  const code = CODE_BOTS.includes(name);
190
194
  writeFileSync(join(bot, "sandbox.ts"), code ? CODE_SANDBOX : VERCEL_SANDBOX);
191
- if (code) {
192
- for (const [stub, content] of Object.entries(codeExtensionFiles(name))) {
193
- mkdirSync(dirname(join(bot, stub)), { recursive: true });
194
- writeFileSync(join(bot, stub), content);
195
- }
195
+ for (const [stub, content] of Object.entries({ ...gitExtensionFiles(), ...(code ? codeExtensionFiles(name) : {}) })) {
196
+ mkdirSync(dirname(join(bot, stub)), { recursive: true });
197
+ writeFileSync(join(bot, stub), content);
196
198
  }
197
199
  }
198
200
  const roster = resolved.agents;
@@ -207,10 +209,12 @@ export function prepareDeployment(dir) {
207
209
  }
208
210
  const agentFile = join(bot, "agent.ts");
209
211
  writeFileSync(agentFile, injectWorkflowWorld(readFileSync(agentFile, "utf8"), agentFile));
210
- // Hosted bots reach the human's connected services (GitHub) through Vercel Kit,
211
- // with the project's KIT_CONNECTOR; there is no host credential to fall back on.
212
- mkdirSync(join(bot, "connections"), { recursive: true });
213
- writeFileSync(join(bot, "connections", "vercel-kit.ts"), 'export { default } from "eve-hub/connections/vercel-kit";\n');
212
+ // GITHUB_PATH "kit": the human's connected services (GitHub) come through Vercel Kit,
213
+ // with the project's KIT_CONNECTOR. "gh" deploys no Kit connection.
214
+ if (GITHUB_PATH === "kit") {
215
+ mkdirSync(join(bot, "connections"), { recursive: true });
216
+ writeFileSync(join(bot, "connections", "vercel-kit.ts"), 'export { default } from "eve-hub/connections/vercel-kit";\n');
217
+ }
214
218
  mkdirSync(join(bot, "memory"), { recursive: true });
215
219
  const botDir = join(dir, "agents", name);
216
220
  writeFileSync(join(bot, "memory", "workspace.ts"), memoryProvider("workspace", readMemory(dir, { folders: domainFolders(botDomains(dir, botDir)) }).entries, "Standing decision", "workspace"));
package/stubs.mjs CHANGED
@@ -1,6 +1,10 @@
1
1
  // eve finds tools, hooks, and channels by file (the tool name is the file
2
2
  // name), so each bot keeps a one-line stub per kit module. Bot file -> what it
3
3
  // is, and which kit module it re-exports (default: the same path).
4
+ // Kit files whose module is gone. Sync retires them from every bot, and the
5
+ // hosted deploy prunes them (the change-explainer: its skill and artifact tools).
6
+ export const REMOVED_KIT_FILES = Object.freeze(["tools/artifact_publish.ts", "tools/artifact_status.ts", "tools/artifact_list.ts", "skills/change-explainer.ts"]);
7
+
4
8
  export const KIT_FILES = {
5
9
  "channels/eve.ts": ["Queued eve channel that resolves the registry's human as a user.", "channels/eve-roster-user.ts"],
6
10
  "hooks/sandbox.ts": ["Migrates legacy Docker sessions to the host sandbox."],
@@ -14,10 +18,6 @@ export const KIT_FILES = {
14
18
  "tools/update_thread.ts": ["Update a thread this bot is in: title, track, domain and summary."],
15
19
  "tools/list_threads.ts": ["Every workspace thread, from any session."],
16
20
  "tools/read_thread.ts": ["Read any workspace thread."],
17
- "tools/artifact_publish.ts": ["Publish the next version of an artifact (a change explainer, ...) and show its card in the thread."],
18
- "tools/artifact_status.ts": ["Whether an artifact this bot published still describes its source."],
19
- "tools/artifact_list.ts": ["The artifacts this bot owns, or every artifact in the workspace."],
20
- "skills/change-explainer.ts": ["The change-explainer skill (packages/explainer/skills/change-explainer/SKILL.md), which artifact_publish's change explainers follow."],
21
21
  "tools/post_message.ts": ["Post into an existing thread or a bot's DM; with `status`, report the thread's phase."],
22
22
  };
23
23
 
@@ -30,7 +30,7 @@ When several bots work on one piece of work, coordinate through posts; don't rou
30
30
  2. When you push, tell the bots your change affects: the owner of a branch stacked on yours, and the reviewer. Use `@all` if you can't tell who. To reach a bot in another thread, use `post_message`. A branch has one owner; anyone else works on a branch stacked on top, or the owner hands it over.
31
31
  3. When the thread's phase changes (planning → working → reviewing → done; a review can send it back to working or planning), post a status: `post_message` into this thread with `status` set to the new phase, as the text at most 8 plain words on what is happening now ("Building the input-count fix", not "Plan approved by Rui; builder starting PR 1 (count only new input)": no names, no restating the phase, no parentheticals), and `metadata: { pr, sha }` when there are any (the PR URL and sha go there, never in the text). A status is a phase marker, not a report: test results, branch names and caveats go in a separate message. It wakes nobody unless you @-mention someone, so it doesn't replace your message. The workspace state shows the thread's current phase.
32
32
 
33
- A reviewer records its verdict on the same kind of post: `verdict` (`ship`, `fix-first` or `rethink`) with `metadata: { pr, sha }`, the PR as its full GitHub URL (like every status post) and the sha you reviewed; the text is the one-line what-changed. Add `decisions`, a list of `{ question, options: [{ label, pickedBy? }] }` for what the human has to pick (`[]` if nothing; a `ship` without the list is refused). Publish the change explainer with `artifact_publish` at the first ship and reuse it on later passes; the Dash row says "no explainer" without one. CI and the PR's head come from GitHub, not from your post; don't state them as facts there.
33
+ A reviewer records its verdict on the same kind of post: `verdict` (`ship`, `fix-first` or `rethink`) with `metadata: { pr, sha }`, the PR as its full GitHub URL (like every status post) and the sha you reviewed; the text is the one-line what-changed. Add `decisions`, a list of `{ question, options: [{ label, pickedBy? }] }` for what the human has to pick (`[]` if nothing; a `ship` without the list is refused). CI and the PR's head come from GitHub, not from your post; don't state them as facts there.
34
34
  4. When you need another specialty or more help, ask main, or the bot whose role fits.
35
35
 
36
36
  On a conflict, read the push posts and ask the bot that pushed.
@@ -3,7 +3,7 @@ import { z } from "zod";
3
3
  import { registryFetch } from "../registry-fetch.mjs";
4
4
  // Posts as you into a thread or DM, from whichever session you're in. With no
5
5
  // target it posts into the thread you're in, as part of the running turn: it
6
- // carries the turn's id and the images and artifact cards collected so far,
6
+ // carries the turn's id and the images collected so far,
7
7
  // and if it's your turn's last act, your end-of-turn text isn't posted. Into
8
8
  // another thread, mentions wake bots exactly as your replies do; posting into
9
9
  // a bot's DM wakes that bot; archived threads refuse it. Your session in the
package/world.mjs CHANGED
@@ -413,29 +413,10 @@ function reduceChild(member, { type, data = {}, meta: meta4 }, at = Date.parse(m
413
413
  return cardChanged || ["reasoning.appended", "message.appended", "message.completed", "actions.requested", "agent.started", "task.started", "task.settled", "action.result", "turn.completed", "session.waiting", "task.progress", "task.blocked", "task.updated"].includes(type);
414
414
  }
415
415
 
416
- // ../../registry/artifacts.mjs
417
- var ID = /^[a-z0-9][a-z0-9-]{0,39}:[\w.-]{1,40}$/;
418
- var STATUSES = /* @__PURE__ */ new Set(["fresh", "stale", "updating", "failed", "merged", "closed"]);
419
- var isStatus = (status) => STATUSES.has(status);
420
- var PROJECT = process.env.HUB_ARTIFACTS_PROJECT ?? "hub-artifacts";
421
- var DOMAIN = process.env.HUB_ARTIFACTS_DOMAIN ?? "playground-vercel.tools";
422
- var MAX_FILE_BYTES = 4 * 1024 * 1024;
423
- var validId = (id) => typeof id === "string" && ID.test(id);
424
- var SITE_CONFIG = new TextEncoder().encode(JSON.stringify({ headers: [{ source: "/(.*)", headers: [{ key: "x-robots-tag", value: "noindex" }, { key: "referrer-policy", value: "no-referrer" }] }] }));
425
-
426
416
  // ../../hosted-hub/src/store/db.ts
427
417
  import { createClient } from "@libsql/client";
428
418
  var s = (sql, ...args) => ({ sql, args });
429
419
 
430
- // ../../hosted-hub/src/store/artifacts.ts
431
- var ARTIFACT_READ = s("SELECT id, record FROM artifacts WHERE record IS NOT NULL");
432
- var ARTIFACT_LEASE_MS = 15 * 6e4;
433
- function statusStatement(checked) {
434
- return s(`UPDATE artifacts SET record = json_patch(record, ?)
435
- WHERE id = ? AND json_extract(record, '$.version') = ?`, JSON.stringify({ status: checked.status, ...checked.checkedAt ? { checkedAt: checked.checkedAt.slice(0, 40) } : {} }), checked.id, checked.version);
436
- }
437
- var cardsTo = (turnId2, messageId2) => s("UPDATE artifact_links SET message_id = ? WHERE turn_id = ? AND message_id IS NULL AND EXISTS (SELECT 1 FROM messages WHERE id = ?)", messageId2, turnId2, messageId2);
438
-
439
420
  // subagent-cap.mjs
440
421
  var DEFAULT_SUBAGENT_CAP = Object.freeze({
441
422
  model: "anthropic/claude-fable-5.1",
@@ -545,7 +526,7 @@ var TAG_FIELDS = ["project", "issueType"];
545
526
  var LABEL_FIELDS = [...TAG_FIELDS, "path"];
546
527
 
547
528
  // ../../lib/subscriptions.mjs
548
- var STATUSES2 = Object.freeze(["idle", "working", "waiting"]);
529
+ var STATUSES = Object.freeze(["idle", "working", "waiting"]);
549
530
 
550
531
  // ../../hosted-hub/src/store/dash.ts
551
532
  var COLUMNS = { importance: "importance", importanceBy: "importance_by", lane: "lane", delay: "delay", reason: "reason", basis: "basis", deferred: "deferred", updatedAt: "classified_at", by: "classified_by" };
@@ -26609,8 +26590,6 @@ var Message = object2({
26609
26590
  event: maybe(MessageEvent),
26610
26591
  attachments: maybe(array2(Attachment)),
26611
26592
  taskCard: maybe(TaskCard),
26612
- /** Artifact ids its turn published; the cards read Snapshot.artifacts. */
26613
- artifacts: maybe(array2(string5())),
26614
26593
  hop: maybe(int2()),
26615
26594
  /** Handles (lowercase) that resolved to a person when this was posted, so an old @handle still links after a rename. */
26616
26595
  mentionAliases: maybe(map2("MentionAliases", string5())),
@@ -26850,7 +26829,6 @@ var DecisionPackage = object2({
26850
26829
  ci: string5(),
26851
26830
  /** The thread's latest verdict on this PR; `current`: it was given at the head (null: head unknown). `rounds`: non-ship verdicts so far. */
26852
26831
  review: maybe(object2({ verdict: string5(), sha: string5(), at: string5(), by: string5(), messageId: string5(), current: boolean5().nullable(), rounds: int2() })),
26853
- explainer: maybe(object2({ id: string5(), sha: string5().nullable(), at: string5(), url: maybe(string5()), behind: boolean5().nullable() })),
26854
26832
  /** The latest verdict is a ship at the current head: the input of the Ready lane. */
26855
26833
  ready: boolean5(),
26856
26834
  /** The agent that posted last before the verdict, the reviewer aside: who the human's approval is addressed to. */
@@ -26893,23 +26871,6 @@ var HubTrack = object2({
26893
26871
  closed: maybe(boolean5()),
26894
26872
  locked: maybe(boolean5())
26895
26873
  }).meta({ id: "HubTrack" });
26896
- var ArtifactCard = object2({
26897
- title: string5(),
26898
- gist: maybe(string5()),
26899
- /** Change explainer: "repo #PR". */
26900
- subtitle: maybe(string5()),
26901
- /** Change explainer: "base..head". */
26902
- ref: maybe(string5())
26903
- }).meta({ id: "ArtifactCard" });
26904
- var Artifact = object2({
26905
- id: string5(),
26906
- version: int2(),
26907
- /** fresh | stale | updating | failed | merged | closed */
26908
- status: string5(),
26909
- card: ArtifactCard,
26910
- /** The stable link, moved to each new version; what a click opens. */
26911
- alias: maybe(string5())
26912
- }).meta({ id: "Artifact" });
26913
26874
  var Snapshot = object2({
26914
26875
  type: literal2("state"),
26915
26876
  proto: maybe(int2()),
@@ -26947,9 +26908,7 @@ var Snapshot = object2({
26947
26908
  usageHoursSince: maybe(string5()),
26948
26909
  server: maybe(ServerID),
26949
26910
  /** Track summaries by path; the web client reads them (the app doesn't), the hosted hub sends them. */
26950
- pathSummaries: maybe(map2("PathSummaries", object2({ text: string5(), by: string5(), at: string5() }).meta({ id: "PathSummary" }))),
26951
- /** Published artifacts by id, each at its latest version. */
26952
- artifacts: maybe(map2("Artifacts", Artifact))
26911
+ pathSummaries: maybe(map2("PathSummaries", object2({ text: string5(), by: string5(), at: string5() }).meta({ id: "PathSummary" })))
26953
26912
  }).meta({ id: "Snapshot" });
26954
26913
 
26955
26914
  // ../../hosted-hub/src/store/workspace-repo.ts
@@ -27104,28 +27063,10 @@ function fold(state, chunks, now = Date.now()) {
27104
27063
  break;
27105
27064
  }
27106
27065
  case "action.result": {
27107
- const tool = open3.activity.tools.find((t) => t.callId === data.result?.callId);
27108
- const toolName = data.result?.toolName ?? tool?.name;
27109
- const output2 = data.result?.output;
27110
- const artifact = output2 && typeof output2 === "object" && "artifact" in output2 ? output2.artifact : void 0;
27111
- if (data.status === "completed" && artifact && typeof artifact === "object" && "id" in artifact && typeof artifact.id === "string" && validId(artifact.id)) {
27112
- if (toolName === "artifact_publish")
27113
- stmts.push(s("INSERT OR IGNORE INTO artifact_links (turn_id,id) VALUES (?,?)", open3.id, artifact.id));
27114
- if (toolName === "artifact_status" && "version" in artifact && typeof artifact.version === "number" && "status" in artifact && typeof artifact.status === "string" && isStatus(artifact.status)) {
27115
- stmts.push(statusStatement({
27116
- id: artifact.id,
27117
- version: artifact.version,
27118
- status: artifact.status,
27119
- ..."checkedAt" in artifact && typeof artifact.checkedAt === "string" ? { checkedAt: artifact.checkedAt } : {}
27120
- }));
27121
- log("artifact.updated", { id: artifact.id });
27122
- }
27123
- }
27124
27066
  const ls = open3.lastStep, callId = data.result?.callId;
27125
27067
  if (!ls || !callId || ls.calls[callId] !== null) break;
27126
27068
  const postOutput = data.result?.output;
27127
27069
  ls.calls[callId] = data.status === "completed" && postOutput?.own === true;
27128
- if (ls.calls[callId] && typeof postOutput?.messageId === "string") stmts.push(cardsTo(open3.id, postOutput.messageId));
27129
27070
  touched.set(open3.id, open3);
27130
27071
  break;
27131
27072
  }
@@ -27139,10 +27080,9 @@ function fold(state, chunks, now = Date.now()) {
27139
27080
  if (open3.status !== "cancelling" && !lastActWasOwnPost(open3.lastStep)) {
27140
27081
  const messageId2 = derivedMessageId(open3.id);
27141
27082
  stmts.push(s(`INSERT OR IGNORE INTO messages (id, thread_id, author, author_kind, text, turn_id, created_at)
27142
- SELECT ?, ?, ?, 'agent', ?, ?, ? WHERE (? <> '' OR EXISTS (SELECT 1 FROM artifact_links WHERE turn_id = ? AND message_id IS NULL))
27143
- AND EXISTS (SELECT 1 FROM turns WHERE id = ? AND status NOT IN ('cancelling','cancelled') AND NOT (${REVOKED_TURN}))`, messageId2, session.thread, session.bot, text, open3.id, at, text, open3.id, open3.id));
27083
+ SELECT ?, ?, ?, 'agent', ?, ?, ? WHERE ? <> ''
27084
+ AND EXISTS (SELECT 1 FROM turns WHERE id = ? AND status NOT IN ('cancelling','cancelled') AND NOT (${REVOKED_TURN}))`, messageId2, session.thread, session.bot, text, open3.id, at, text, open3.id));
27144
27085
  log("message.created", { messageId: messageId2, author: session.bot, authorKind: "agent" }, IF_INSERTED);
27145
- stmts.push(cardsTo(open3.id, messageId2));
27146
27086
  const to = recipients(state.roster, text, { name: session.bot, kind: "agent" }, session.thread);
27147
27087
  stmts.push(...routingStatements(messageId2, session.thread, to, at));
27148
27088
  deliver.push(messageId2);
@@ -1,73 +0,0 @@
1
- // Artifacts: a versioned static site a bot publishes and keeps current, such
2
- // as a change explainer. The core is type-agnostic. A type (artifacts/types/)
3
- // builds the site's files and its card from a source, in the bot's sandbox
4
- // (where the repo is), and says whether a published version is still current.
5
- //
6
- // The hub owns the record and the deploy (registry/artifacts.mjs). Publishing
7
- // a version:
8
- // 1. the type builds the site: { files: [{ file, data }], card, meta }
9
- // 2. each file goes to the hub: PUT /api/artifacts/:id/files/:sha
10
- // 3. POST /api/artifacts/:id/v/:n creates version n (the hub refuses with
11
- // 409 when another session got there first), deploys it and moves the
12
- // stable link; its answer is the record.
13
- import { createHash } from "node:crypto";
14
- import changeExplainer from "./types/change-explainer.mjs";
15
- import { registryFetch } from "../registry-fetch.mjs";
16
-
17
- export const TYPES = { "change-explainer": changeExplainer };
18
- export const artifactId = (type, key) => `${type}:${key}`;
19
- const UPLOADS_AT_ONCE = 8;
20
-
21
- export function typeOf(name) {
22
- const type = TYPES[name];
23
- if (!type) throw new Error(`unknown artifact type ${JSON.stringify(name)} (known: ${Object.keys(TYPES).join(", ")})`);
24
- return type;
25
- }
26
-
27
- async function hub(path, init, fetchImpl) {
28
- const res = await registryFetch(path, init, { fetchImpl });
29
- const body = await res.json().catch(() => ({}));
30
- if (!res.ok) throw new Error(`${init?.method ?? "GET"} ${path.split("?")[0]}: ${body.error ?? res.status}`);
31
- return body;
32
- }
33
-
34
- // The hub's record of an artifact, or null when it has none.
35
- export async function registryRecord(id, fetchImpl = fetch) {
36
- const res = await registryFetch(`/api/artifacts/${encodeURIComponent(id)}`, {}, { fetchImpl });
37
- if (res.status === 404) return null;
38
- if (!res.ok) throw new Error(`GET /api/artifacts/${id}: ${(await res.json().catch(() => ({}))).error ?? res.status}`);
39
- return res.json();
40
- }
41
-
42
- /**
43
- * Publishes the next version of `type:key` from `source` and returns the
44
- * hub's record. `sandbox` is the session's (ctx.getSandbox()).
45
- */
46
- export async function publish({ type: typeName, key, source }, { sandbox, sessionId, fetch: fetchImpl = fetch }) {
47
- const type = typeOf(typeName);
48
- const id = artifactId(typeName, key);
49
- const at = `/api/artifacts/${encodeURIComponent(id)}`;
50
- const prior = await registryRecord(id, fetchImpl);
51
- const version = (prior?.version ?? 0) + 1;
52
- const built = await type.build(source, { sandbox, id, version });
53
- const files = built.files.map(({ file, data }) => ({ file, data, sha: createHash("sha1").update(data).digest("hex"), size: data.length }));
54
- const unique = [...new Map(files.map((f) => [f.sha, f])).values()];
55
- for (let i = 0; i < unique.length; i += UPLOADS_AT_ONCE) {
56
- await Promise.all(unique.slice(i, i + UPLOADS_AT_ONCE).map((f) =>
57
- // A file body is the bytes alone, so the session rides in the header.
58
- hub(`${at}/files/${f.sha}`, { method: "PUT", headers: { "x-hub-session": sessionId, "content-type": "application/octet-stream" }, body: f.data, signal: AbortSignal.timeout(120_000) }, fetchImpl),
59
- ));
60
- }
61
- return hub(`${at}/v/${version}`, {
62
- method: "POST",
63
- headers: { "content-type": "application/json" },
64
- body: JSON.stringify({ sessionId, type: typeName, key, source, card: built.card, meta: built.meta ?? {}, files: files.map(({ file, sha, size }) => ({ file, sha, size })) }),
65
- signal: AbortSignal.timeout(600_000),
66
- }, fetchImpl);
67
- }
68
-
69
- /** What the type says about a published record: { status, reasons[] }. */
70
- export async function status(record, { sandbox }) {
71
- const type = typeOf(record.type);
72
- return type.status ? type.status(record, { sandbox }) : { status: record.status ?? "fresh", reasons: [] };
73
- }
@@ -1,83 +0,0 @@
1
- // The change-explainer artifact type: a narrated walkthrough of a PR or a
2
- // stack (packages/explainer), built from a fixture (fixture.json,
3
- // overview.json, sequence.json) and a read-only checkout of the repo.
4
- //
5
- // source: { fixture: <fixture dir>, repo: <git checkout>, scip?: <index.scip>, branch?: <ref the change lives on> }
6
- import { randomUUID } from "node:crypto";
7
- import { posix as path } from "node:path";
8
- import { appFiles, scripts } from "./explainer-assets.gen.mjs";
9
-
10
- const quote = (value) => "'" + String(value).replaceAll("'", "'\\''") + "'";
11
- async function run(sandbox, args) {
12
- const result = await sandbox.run({ command: args.map(quote).join(" ") });
13
- if (result.exitCode !== 0) throw new Error(`${args[0]} failed: ${result.stderr || result.stdout}`);
14
- return result.stdout.trim();
15
- }
16
- const git = (sandbox, repo, ...args) => run(sandbox, ["git", "-C", repo, ...args]);
17
- const readJson = async (sandbox, file) => JSON.parse(await sandbox.readTextFile({ path: file }));
18
-
19
- export function firstSentence(text = "") {
20
- const sentence = String(text).trim().split(/(?<=[.!?])\s+(?=[A-Z"'`(])/, 1)[0] ?? "";
21
- return sentence.length > 220 ? `${sentence.slice(0, 219).replace(/\s+\S*$/, "")}…` : sentence;
22
- }
23
-
24
- // Card fields from the fixture: what the change is, and which commits. A
25
- // change with no PRs (a local repo) names its branch instead.
26
- export function cardOf(fixture, overview, branch) {
27
- const short = (sha) => String(sha ?? "").slice(0, 9);
28
- const prs = (fixture.prs ?? []).filter((p) => p.number !== undefined).map((p) => `#${p.number}`);
29
- return {
30
- title: fixture.title ?? fixture.id,
31
- gist: firstSentence(overview?.thesis),
32
- subtitle: [fixture.repo, ...(prs.length ? prs : [branch])].filter(Boolean).join(" "),
33
- ref: `${short(fixture.base)}..${short(fixture.head)}`,
34
- };
35
- }
36
-
37
- async function build(source, { sandbox }) {
38
- const work = `/tmp/hub-explainer/${randomUUID()}`;
39
- try {
40
- await run(sandbox, ["mkdir", "-p", work]);
41
- for (const [name, text] of Object.entries(scripts)) await sandbox.writeTextFile({ path: `${work}/${name}.mjs`, content: text });
42
- const fixture = await readJson(sandbox, path.join(source.fixture, "fixture.json"));
43
- await run(sandbox, ["node", `${work}/lint-fixture.mjs`, source.fixture]);
44
- const data = `${work}/data`;
45
- await run(sandbox, ["node", `${work}/build-data.mjs`, "--fixture", source.fixture, "--repo", source.repo, ...(source.scip ? ["--scip", source.scip] : []), "--out", data]);
46
- const overview = await readJson(sandbox, path.join(source.fixture, "overview.json"));
47
- const [base, head] = await Promise.all([git(sandbox, source.repo, "rev-parse", `${fixture.base}^{commit}`), git(sandbox, source.repo, "rev-parse", `${fixture.head}^{commit}`)]);
48
- const files = appFiles.map(({ file, base64 }) => {
49
- let bytes = Buffer.from(base64, "base64");
50
- if (file === "index.html") bytes = Buffer.from(bytes.toString("utf8").replace("</title>", `</title>\n <meta name="explainer-fixture" content="${fixture.id.replace(/[^\w.-]/g, "")}" />`));
51
- return { file, data: bytes };
52
- });
53
- const listing = await run(sandbox, ["find", data, "-type", "f", "-print0"]);
54
- for (const file of listing.split("\0").filter(Boolean)) files.push({ file: `data/${file.slice(data.length + 1)}`, data: await sandbox.readBinaryFile({ path: file }) });
55
- return { files, card: cardOf(fixture, overview, source.branch), meta: { repo: fixture.repo, base, head, prs: (fixture.prs ?? []).flatMap((p) => p.number ?? []) } };
56
- } finally {
57
- await sandbox.removePath({ path: work, recursive: true, force: true });
58
- }
59
- }
60
-
61
- // Whether the published version still describes the change. This checks the
62
- // pinned range against the fixture and, with source.branch, the branch head;
63
- // which parts went stale (snippet hashes, re-anchoring) is the next step.
64
- async function status(record, { sandbox }) {
65
- const { source = {}, meta = {} } = record;
66
- const reasons = [];
67
- if (record.status === "merged" || record.status === "closed") return { status: record.status, reasons };
68
- try {
69
- const fixture = await readJson(sandbox, path.join(source.fixture, "fixture.json"));
70
- const [base, head] = await Promise.all([git(sandbox, source.repo, "rev-parse", `${fixture.base}^{commit}`), git(sandbox, source.repo, "rev-parse", `${fixture.head}^{commit}`)]);
71
- if (base !== meta.base) reasons.push(`the fixture's base is now ${base.slice(0, 9)} (published at ${String(meta.base).slice(0, 9)})`);
72
- if (head !== meta.head) reasons.push(`the fixture's head is now ${head.slice(0, 9)} (published at ${String(meta.head).slice(0, 9)})`);
73
- if (source.branch) {
74
- const tip = await git(sandbox, source.repo, "rev-parse", `${source.branch}^{commit}`);
75
- if (tip !== meta.head) reasons.push(`${source.branch} is at ${tip.slice(0, 9)}, ahead of the published head ${String(meta.head).slice(0, 9)}`);
76
- }
77
- } catch (error) {
78
- reasons.push(`couldn't read the source: ${error.message}`);
79
- }
80
- return { status: reasons.length ? "stale" : "fresh", reasons };
81
- }
82
-
83
- export default { name: "change-explainer", build, status };