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 +12 -0
- package/github-path.d.mts +1 -0
- package/github-path.mjs +8 -0
- package/package.json +1 -2
- package/prepare.mjs +22 -18
- package/stubs.mjs +4 -4
- package/templates/workspace-protocol.md +1 -1
- package/tools/post_message.js +1 -1
- package/world.mjs +4 -64
- package/artifacts/core.mjs +0 -73
- package/artifacts/types/change-explainer.mjs +0 -83
- package/artifacts/types/explainer-assets.gen.mjs +0 -3
- package/skills/change-explainer.d.ts +0 -2
- package/skills/change-explainer.gen.mjs +0 -2
- package/skills/change-explainer.js +0 -7
- package/tools/artifact_list.d.ts +0 -28
- package/tools/artifact_list.js +0 -17
- package/tools/artifact_publish.d.ts +0 -18
- package/tools/artifact_publish.js +0 -19
- package/tools/artifact_status.d.ts +0 -26
- package/tools/artifact_status.js +0 -20
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";
|
package/github-path.mjs
ADDED
|
@@ -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.
|
|
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
|
|
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 {
|
|
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"
|
|
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)
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
//
|
|
211
|
-
// with the project's KIT_CONNECTOR
|
|
212
|
-
|
|
213
|
-
|
|
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).
|
|
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.
|
package/tools/post_message.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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);
|
package/artifacts/core.mjs
DELETED
|
@@ -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 };
|