eve-hub 0.16.12 → 0.16.14
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/github-connector.mjs +9 -0
- package/local-onboarding/SKILL.md +2 -2
- package/package.json +1 -1
- package/prepare.mjs +2 -1
- package/schedules/dash-run.mjs +74 -18
- package/skills/local-onboarding.gen.mjs +1 -1
- package/templates/main-instructions.md +3 -1
- package/tools/create_agent.d.ts +8 -6
- package/tools/create_agent.js +5 -4
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// The Vercel Connect connector that holds the workspace human's GitHub token (the eve-hub-github
|
|
2
|
+
// App). The bots' `git__gh` (prepare.mjs) and main's dash pass (schedules/dash-run.mjs) ask it for
|
|
3
|
+
// the same subject, so both act as the human. Its own module so a schedule can import it without
|
|
4
|
+
// prepare.mjs's build-time imports.
|
|
5
|
+
export const GITHUB_CONNECTOR = "github/eve-hub-github";
|
|
6
|
+
|
|
7
|
+
// The subject eve's connections resolve for a hub turn (channels/eve-roster-user.ts: principal
|
|
8
|
+
// `roster:<human>`, authenticator `roster`, no issuer of its own, so eve uses the authenticator).
|
|
9
|
+
export const rosterSubject = (human) => ({ type: "user", id: `roster:${human}`, issuer: "roster" });
|
|
@@ -78,10 +78,10 @@ Ask whether to open the PR.
|
|
|
78
78
|
|
|
79
79
|
The steps live in one file, which is the only source; never hand-write them into `sandbox.ts`. Write `$TMPDIR/prepare.json` as `{"commands": [{"command": "<shell>", "what": "<short name for the error>"}]}`.
|
|
80
80
|
|
|
81
|
-
Main's handoff names the workspace repo. Clone it into a scratch folder, branch, and add the steps to each hosted bot that runs code
|
|
81
|
+
Main's handoff names the workspace repo. Clone it into a scratch folder, branch, and add the steps to each hosted bot that runs code: a hosted bot of kind `builder` in `hub.workspace.json`, unless the owner said otherwise. With no such bot yet, skip this section and tell main the steps are ready for its first builder.
|
|
82
82
|
|
|
83
83
|
```sh
|
|
84
|
-
node {{scripts}}/prepare-edit.mjs "$TMPDIR/prepare.json" agents
|
|
84
|
+
node {{scripts}}/prepare-edit.mjs "$TMPDIR/prepare.json" agents/<bot>/agent/sandbox.ts
|
|
85
85
|
```
|
|
86
86
|
|
|
87
87
|
Commit, push and open the PR with `gh pr create` (a hosted bot runs `git` and `gh` through its `git__gh` tool). The body lists the programs, why each one is there, and the usage counts behind them. Never name the owner's other repos.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eve-hub",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.14",
|
|
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",
|
package/prepare.mjs
CHANGED
|
@@ -28,6 +28,8 @@ import { botDomains, domainFolders, readBotMemory, readMemory, validateMemory }
|
|
|
28
28
|
import { agentRunsOn, readWorkspaceFile, removeWorkspaceAgent } from "./workspace.mjs";
|
|
29
29
|
import { architectureText } from "./team.mjs";
|
|
30
30
|
import { sandboxReady } from "./sandbox-migrate.mjs";
|
|
31
|
+
import { GITHUB_CONNECTOR } from "./github-connector.mjs";
|
|
32
|
+
export { GITHUB_CONNECTOR };
|
|
31
33
|
|
|
32
34
|
// eve's git extension, for each bot of a build from a workspace repo that edits files: its `git__gh` tool runs
|
|
33
35
|
// git, gh and gh-signed-commit against the workspace's org with the human's own GitHub token,
|
|
@@ -36,7 +38,6 @@ import { sandboxReady } from "./sandbox-migrate.mjs";
|
|
|
36
38
|
// hub turn runs as the workspace's human (channels/eve-roster-user.ts), so pushes, PRs and commits
|
|
37
39
|
// are theirs. The shell holds no credential. A build without a repo (an uploaded deployment)
|
|
38
40
|
// mounts none.
|
|
39
|
-
export const GITHUB_CONNECTOR = "github/eve-hub-github";
|
|
40
41
|
export const GIT_EXTENSION_FILE = "extensions/git.ts";
|
|
41
42
|
// The read-only preset's stub (lib/agent-template.mjs): eve's write_file, disabled.
|
|
42
43
|
const READ_ONLY_FILE = "tools/write_file.ts";
|
package/schedules/dash-run.mjs
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
|
-
// One tick of main's dash schedule (schedules/dash.ts): the
|
|
1
|
+
// One tick of main's dash schedule (schedules/dash.ts): the GitHub probe and
|
|
2
2
|
// the run around it. A failed GitHub call never fails the tick: the probe is
|
|
3
3
|
// retried once on a 5xx or a timeout, and if it still fails every PR keeps
|
|
4
4
|
// what was last known (nothing said of any), threads and waits are still
|
|
5
5
|
// scanned and posted, and the post carries a one-line note of what was
|
|
6
6
|
// skipped. Only then is the Dash's checkedAt left alone, so the sidebar shows
|
|
7
|
-
// the Dash stale while GitHub stays unreachable (
|
|
7
|
+
// the Dash stale while GitHub stays unreachable (no token, offline).
|
|
8
|
+
//
|
|
9
|
+
// The probe is one GraphQL query over HTTPS, as the workspace human: the
|
|
10
|
+
// search is `author:@me`, so it needs their own user token, never an App's.
|
|
11
|
+
// A hosted hub's function has no `gh`, so it asks Vercel Connect for the
|
|
12
|
+
// token the bots' `git__gh` gets (github-connector.mjs); a local hub asks
|
|
13
|
+
// `gh auth token`. The token goes in the request header and nowhere else.
|
|
8
14
|
//
|
|
9
15
|
// Plain JS on purpose: the schedule (TypeScript, bundled by eve) and the
|
|
10
16
|
// tests (plain node) import the same module.
|
|
11
17
|
import { execFile } from "node:child_process";
|
|
12
18
|
import { createHash } from "node:crypto";
|
|
19
|
+
import { NoValidTokenError, UserAuthorizationRequiredError, getToken } from "@vercel/connect";
|
|
20
|
+
import { GITHUB_CONNECTOR, rosterSubject } from "../github-connector.mjs";
|
|
13
21
|
import { runOnce } from "./hub.mjs";
|
|
14
22
|
import { REFS_PER_QUERY, dashPrs, dashQuery, dashScan, dashWatchList, failedRefs, prsFromSearch } from "./dash-rules.mjs";
|
|
15
23
|
|
|
@@ -17,12 +25,16 @@ import { REFS_PER_QUERY, dashPrs, dashQuery, dashScan, dashWatchList, failedRefs
|
|
|
17
25
|
// SCHEDULE_DISPATCH_TIMEOUT_MS): two 20 s tries and the registry calls fit.
|
|
18
26
|
export const GH_TIMEOUT_MS = 20_000;
|
|
19
27
|
export const GH_RETRY_DELAY_MS = 1_000;
|
|
28
|
+
// The token comes first, once a tick, and leaves the two tries their 20 s.
|
|
29
|
+
export const TOKEN_TIMEOUT_MS = 5_000;
|
|
30
|
+
export const GITHUB_GRAPHQL_URL = "https://api.github.com/graphql";
|
|
20
31
|
|
|
21
32
|
// A failure worth one more try: GitHub's 5xx, or no answer in time.
|
|
22
33
|
export const retryable = (err) => !!(err?.timedOut || /HTTP 5\d\d|timed? ?out|ETIMEDOUT|ECONNRESET|unexpected EOF/i.test(String(err?.message ?? "")));
|
|
23
34
|
|
|
24
|
-
// What a failure is called in the tick's note: "gh 504", "gh timeout", …
|
|
35
|
+
// What a failure is called in the tick's note: "gh 504", "gh timeout", "GitHub approval needed", …
|
|
25
36
|
export function ghReason(err) {
|
|
37
|
+
if (err instanceof UserAuthorizationRequiredError || err instanceof NoValidTokenError) return "GitHub approval needed";
|
|
26
38
|
const msg = String(err?.message ?? "");
|
|
27
39
|
const http = msg.match(/HTTP (\d{3})/);
|
|
28
40
|
if (http) return `gh ${http[1]}`;
|
|
@@ -32,19 +44,23 @@ export function ghReason(err) {
|
|
|
32
44
|
return "gh failed";
|
|
33
45
|
}
|
|
34
46
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
|
|
47
|
+
const timedOut = (what) => Object.assign(new Error(`gh: ${what} timed out`), { timedOut: true });
|
|
48
|
+
|
|
49
|
+
// The workspace human's GitHub user token. Hosted: Vercel Connect's, for the
|
|
50
|
+
// subject `git__gh` resolves (`roster:<human>`); its errors pass through, so
|
|
51
|
+
// a missing approval reads as one. Local: `gh auth token`.
|
|
52
|
+
export function githubToken(human, { hosted = Boolean(process.env.VERCEL), connect = getToken, bin = "gh", timeoutMs = TOKEN_TIMEOUT_MS } = {}) {
|
|
53
|
+
if (hosted) {
|
|
54
|
+
if (!human) return Promise.reject(new Error("gh: the hub names no human to ask GitHub as"));
|
|
55
|
+
let timer;
|
|
56
|
+
const deadline = new Promise((_, reject) => { timer = setTimeout(() => reject(timedOut("Vercel Connect")), timeoutMs); });
|
|
57
|
+
return Promise.race([connect(GITHUB_CONNECTOR, { subject: rosterSubject(human) }), deadline]).finally(() => clearTimeout(timer));
|
|
58
|
+
}
|
|
40
59
|
return new Promise((resolve, reject) => {
|
|
41
|
-
execFile(bin, ["
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
} catch {}
|
|
46
|
-
if (body?.data?.search) return resolve(body);
|
|
47
|
-
const err = new Error(`gh: ${String(stderr || error?.message || "no answer").trim().replace(/^gh: /, "").slice(0, 300)}`);
|
|
60
|
+
execFile(bin, ["auth", "token"], { timeout: timeoutMs }, (error, stdout, stderr) => {
|
|
61
|
+
const token = String(stdout ?? "").trim();
|
|
62
|
+
if (!error && token) return resolve(token);
|
|
63
|
+
const err = new Error(`gh: ${String(stderr || error?.message || "no token").trim().replace(/^gh: /, "").slice(0, 300)}`);
|
|
48
64
|
if (error?.killed || error?.signal) err.timedOut = true;
|
|
49
65
|
if (error?.code === "ENOENT") err.code = "ENOENT";
|
|
50
66
|
reject(err);
|
|
@@ -52,6 +68,44 @@ export function ghGraphql(query, { timeoutMs = GH_TIMEOUT_MS, bin = "gh" } = {})
|
|
|
52
68
|
});
|
|
53
69
|
}
|
|
54
70
|
|
|
71
|
+
// One GraphQL query: the parsed answer, or an Error ("gh: HTTP 502: …", or
|
|
72
|
+
// `timedOut` set when GitHub didn't answer in time). A PR asked about by name
|
|
73
|
+
// that's gone fails only its own alias and the rest of the answer is good, so
|
|
74
|
+
// any answer with the search in it counts.
|
|
75
|
+
export async function ghGraphql(query, { token, timeoutMs = GH_TIMEOUT_MS, fetchImpl = fetch } = {}) {
|
|
76
|
+
let res;
|
|
77
|
+
let text;
|
|
78
|
+
try {
|
|
79
|
+
res = await fetchImpl(GITHUB_GRAPHQL_URL, {
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: { authorization: `bearer ${token}`, "content-type": "application/json", "user-agent": "eve-hub-dash" },
|
|
82
|
+
body: JSON.stringify({ query }),
|
|
83
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
84
|
+
});
|
|
85
|
+
text = await res.text();
|
|
86
|
+
} catch (cause) {
|
|
87
|
+
if (cause?.name === "TimeoutError" || cause?.name === "AbortError") throw timedOut("GitHub");
|
|
88
|
+
throw new Error(`gh: ${String(cause?.message ?? cause).slice(0, 200)}${cause?.cause?.code ? ` (${cause.cause.code})` : ""}`);
|
|
89
|
+
}
|
|
90
|
+
let body = null;
|
|
91
|
+
try {
|
|
92
|
+
body = JSON.parse(text);
|
|
93
|
+
} catch {}
|
|
94
|
+
if (body?.data?.search) return body;
|
|
95
|
+
const why = String(body?.errors?.[0]?.message ?? body?.message ?? text ?? "").trim() || "no answer";
|
|
96
|
+
throw new Error(`gh: ${res.ok ? "" : `HTTP ${res.status}: `}${why.slice(0, 300)}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// The tick's probe: the token asked for once (again only after it failed), each query retried once.
|
|
100
|
+
export function githubProbe(tokenOf, { delayMs = GH_RETRY_DELAY_MS, fetchImpl = fetch, timeoutMs = GH_TIMEOUT_MS } = {}) {
|
|
101
|
+
let token = null;
|
|
102
|
+
const once = () => (token ??= tokenOf().catch((err) => {
|
|
103
|
+
token = null;
|
|
104
|
+
throw err;
|
|
105
|
+
}));
|
|
106
|
+
return (query) => withRetry(async () => ghGraphql(query, { token: await once(), fetchImpl, timeoutMs }), { delayMs });
|
|
107
|
+
}
|
|
108
|
+
|
|
55
109
|
// `call` retried once (after `delayMs`) when it fails in a way worth retrying.
|
|
56
110
|
export async function withRetry(call, { delayMs = GH_RETRY_DELAY_MS } = {}) {
|
|
57
111
|
try {
|
|
@@ -66,16 +120,18 @@ export async function withRetry(call, { delayMs = GH_RETRY_DELAY_MS } = {}) {
|
|
|
66
120
|
const hashOf = (text) => createHash("sha256").update(text).digest("base64url").slice(0, 22);
|
|
67
121
|
const plural = (n) => `${n} PR${n === 1 ? "" : "s"}`;
|
|
68
122
|
|
|
69
|
-
// One tick. `probe(query)` -> the GraphQL answer (rejects on failure)
|
|
123
|
+
// One tick. `probe(query)` -> the GraphQL answer (rejects on failure); by default GitHub's, as the snapshot's human
|
|
124
|
+
// (`token(human)`: githubToken).
|
|
70
125
|
// Returns { ok, sizes }: ok is false when the PRs couldn't be fetched at all; sizes is each PR's title and
|
|
71
126
|
// diff size ("owner/repo#12" -> { title, add, del, files }) as GitHub said this tick, for the plan's cards.
|
|
72
|
-
export async function runDash(hub, { probe =
|
|
127
|
+
export async function runDash(hub, { probe, token = githubToken, now = Date.now(), log = console } = {}) {
|
|
73
128
|
let ok = true;
|
|
74
129
|
let prFacts = {};
|
|
75
130
|
const sizes = {};
|
|
76
131
|
await runOnce(hub, "dash", async (snap, saved, at) => {
|
|
77
132
|
const { refs, reviewed, keep: waited } = dashWatchList(snap, saved);
|
|
78
133
|
const tracked = new Set(Object.keys(dashPrs(saved)));
|
|
134
|
+
const ask = probe ?? githubProbe(() => token(snap?.humans?.[0]?.name));
|
|
79
135
|
const facts = {};
|
|
80
136
|
let prs = null;
|
|
81
137
|
let skip = new Set();
|
|
@@ -83,7 +139,7 @@ export async function runDash(hub, { probe = (q) => withRetry(() => ghGraphql(q)
|
|
|
83
139
|
try {
|
|
84
140
|
const chunks = [];
|
|
85
141
|
for (let i = 0; i < Math.max(refs.length, 1); i += REFS_PER_QUERY) chunks.push(refs.slice(i, i + REFS_PER_QUERY));
|
|
86
|
-
const answers = await Promise.all(chunks.map((c) =>
|
|
142
|
+
const answers = await Promise.all(chunks.map((c) => ask(dashQuery(c))));
|
|
87
143
|
const found = answers.flatMap(prsFromSearch);
|
|
88
144
|
const searched = new Set(answers.flatMap((a) => prsFromSearch({ data: { search: a?.data?.search } })).map((p) => p.key));
|
|
89
145
|
prs = found.filter((p) => !reviewed.has(p.key) || searched.has(p.key) || tracked.has(p.key) || waited.has(p.key));
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by scripts/build-onboarding.mjs from local-onboarding/SKILL.md. Do not edit.
|
|
2
|
-
export default {"description":"The Mac half of onboarding, when main hands it to you: find the owner's live coding-agent work (Claude Code, Codex, OpenCode, Cursor) to port into threads, and propose the sandbox setup their hosted bots need. Reads only; the threads and the sandbox PR wait for the owner's yes.","content":"# The Mac half of onboarding\n\nMain runs onboarding on Vercel, where it can't see this Mac, so it hands you this half and posts its own without waiting. You post two short messages in that thread, the live work first, then the sandbox setup. Follow your writing rules: each first bubble is at most two sentences, with tables below a `---` line.\n\nTwo scripts gather the facts. They live in `{{scripts}}`, read only, and print metadata: repos, branches, counts, times, the agents' own session titles and program names. Work only from what they print. Don't open the agents' session files yourself: they hold the owner's prompts and code. Delete the JSON files when you are done.\n\n## 1. Live work to port\n\n```sh\nnode {{scripts}}/sessions-facts.mjs > \"$TMPDIR/sessions.json\"\n```\n\nThe script covers the last 14 days of sessions, grouped into work-streams by repo and branch, newest first. Each stream has:\n- `repo`, `branch` and `pr` (the open PR);\n- `checkouts`: one per clone or worktree it was worked in. Each has `worktree` (true for a linked git worktree), `uncommitted` (changed files) and `unpushed` (commits ahead of `aheadOf`);\n- `lastActive`;\n- `sessions`: each with its agent, title, time and `checkout` (its index in `checkouts`).\n\nA stream with no remote is keyed `local-N`. A stream marked `gone` is a folder that no longer exists. `unreadable` lists the sources that exist but couldn't be read. An `uncommitted` or `unpushed` of `null` means the count couldn't be determined (git timed out on a huge untracked tree, or there is no upstream or default branch to compare against): it is unknown, not zero.\n\nPost the streams that have work in flight: any checkout with uncommitted files or unpushed commits, or an open PR. A checkout with an unknown count gets a row too, with \"unknown\" in that cell. A stream with more than one checkout gets one row per checkout that has work. Put the rest in one line with their count.\n\n```md\nYou have 3 work-streams in flight on this Mac. Say which to port, and main will open a thread for each so the team can pick it up.\n\n---\n\n| Stream | Agent and session | Last active | Uncommitted | Unpushed | PR |\n|---|---|---|---|---|---|\n| `vercel/app` on `fix-login` (worktree) | Claude Code, \"Fix login redirect\" | 2h ago | 4 files | 2 | [#812](...) |\n\nAlso 5 streams with nothing in flight.\n```\n\nNever post a local path, a file name, a prompt, or anything else from the sessions.\n\nWhen the owner says which streams to port, you still write nothing on the Mac. Post one message that mentions main and asks it to open one thread per stream. For each stream, give a short title, the repo and branch, its state (worktree, uncommitted and unpushed counts, PR), and which agent had it. You don't open threads yourself.\n\n## 2. Sandbox setup\n\n```sh\nnode {{scripts}}/agent-usage-facts.mjs > \"$TMPDIR/agent-usage.json\"\n```\n\nIt takes about 30 seconds on 16 GB of history. It prints names and counts, never a command's arguments, a prompt or a value:\n- `scanned`: records, commands and unparsable commands per agent;\n- `unreadable`;\n- `programs`, `mcpServers`, `repos`, `languages` (file extensions) and `envNames`.\n\nMapping each fact to a setup is your judgment, not a lookup table. Decide, for every program in the top 30:\n\n- **Hosted or Mac.** These can't run in a Linux sandbox, so they stay on the Mac with you:\n - `brew`, `osascript` and `open`;\n - Xcode, `xcodebuild` and Apple-platform `swift`;\n - anything that drives the owner's browser or apps;\n - `localhost` services and the keychain.\n\n Everything else can be hosted.\n- **Already there, or add it.** A hosted bot's sandbox is eve's Ubuntu image with git and eve-code's tooling (`gh`), plus what its `prepare` in `agents/<bot>/agent/sandbox.ts` installs. Read `prepare` in the workspace repo's sandbox files. A program it installs is already provided; any other hosted program needs a `prepare` step. Use `apt-get` where Ubuntu has the package, and the project's official installer otherwise.\n\nMCP servers are reached from hosted bots through Vercel Kit connections: list each one by name for main to connect. Env var names are listed for the owner only. Never print a value (you don't have any). Ignore shell and system names (`PATH`, `HOME`, `CI`, ...). A name with a high `shellAssigned` relative to `count` is probably a shell variable, not part of the environment.\n\nPost the proposal:\n- a first bubble on what the hosted bots would gain;\n- a table of the steps to add, with the programs each installs and how often the owner used them;\n- one line for what stays on the Mac;\n- one line for the MCP servers to connect through Kit.\n\nAsk whether to open the PR.\n\n## 3. The sandbox PR, after the owner's yes\n\nThe steps live in one file, which is the only source; never hand-write them into `sandbox.ts`. Write `$TMPDIR/prepare.json` as `{\"commands\": [{\"command\": \"<shell>\", \"what\": \"<short name for the error>\"}]}`.\n\nMain's handoff names the workspace repo. Clone it into a scratch folder, branch, and add the steps to each hosted bot that runs code
|
|
2
|
+
export default {"description":"The Mac half of onboarding, when main hands it to you: find the owner's live coding-agent work (Claude Code, Codex, OpenCode, Cursor) to port into threads, and propose the sandbox setup their hosted bots need. Reads only; the threads and the sandbox PR wait for the owner's yes.","content":"# The Mac half of onboarding\n\nMain runs onboarding on Vercel, where it can't see this Mac, so it hands you this half and posts its own without waiting. You post two short messages in that thread, the live work first, then the sandbox setup. Follow your writing rules: each first bubble is at most two sentences, with tables below a `---` line.\n\nTwo scripts gather the facts. They live in `{{scripts}}`, read only, and print metadata: repos, branches, counts, times, the agents' own session titles and program names. Work only from what they print. Don't open the agents' session files yourself: they hold the owner's prompts and code. Delete the JSON files when you are done.\n\n## 1. Live work to port\n\n```sh\nnode {{scripts}}/sessions-facts.mjs > \"$TMPDIR/sessions.json\"\n```\n\nThe script covers the last 14 days of sessions, grouped into work-streams by repo and branch, newest first. Each stream has:\n- `repo`, `branch` and `pr` (the open PR);\n- `checkouts`: one per clone or worktree it was worked in. Each has `worktree` (true for a linked git worktree), `uncommitted` (changed files) and `unpushed` (commits ahead of `aheadOf`);\n- `lastActive`;\n- `sessions`: each with its agent, title, time and `checkout` (its index in `checkouts`).\n\nA stream with no remote is keyed `local-N`. A stream marked `gone` is a folder that no longer exists. `unreadable` lists the sources that exist but couldn't be read. An `uncommitted` or `unpushed` of `null` means the count couldn't be determined (git timed out on a huge untracked tree, or there is no upstream or default branch to compare against): it is unknown, not zero.\n\nPost the streams that have work in flight: any checkout with uncommitted files or unpushed commits, or an open PR. A checkout with an unknown count gets a row too, with \"unknown\" in that cell. A stream with more than one checkout gets one row per checkout that has work. Put the rest in one line with their count.\n\n```md\nYou have 3 work-streams in flight on this Mac. Say which to port, and main will open a thread for each so the team can pick it up.\n\n---\n\n| Stream | Agent and session | Last active | Uncommitted | Unpushed | PR |\n|---|---|---|---|---|---|\n| `vercel/app` on `fix-login` (worktree) | Claude Code, \"Fix login redirect\" | 2h ago | 4 files | 2 | [#812](...) |\n\nAlso 5 streams with nothing in flight.\n```\n\nNever post a local path, a file name, a prompt, or anything else from the sessions.\n\nWhen the owner says which streams to port, you still write nothing on the Mac. Post one message that mentions main and asks it to open one thread per stream. For each stream, give a short title, the repo and branch, its state (worktree, uncommitted and unpushed counts, PR), and which agent had it. You don't open threads yourself.\n\n## 2. Sandbox setup\n\n```sh\nnode {{scripts}}/agent-usage-facts.mjs > \"$TMPDIR/agent-usage.json\"\n```\n\nIt takes about 30 seconds on 16 GB of history. It prints names and counts, never a command's arguments, a prompt or a value:\n- `scanned`: records, commands and unparsable commands per agent;\n- `unreadable`;\n- `programs`, `mcpServers`, `repos`, `languages` (file extensions) and `envNames`.\n\nMapping each fact to a setup is your judgment, not a lookup table. Decide, for every program in the top 30:\n\n- **Hosted or Mac.** These can't run in a Linux sandbox, so they stay on the Mac with you:\n - `brew`, `osascript` and `open`;\n - Xcode, `xcodebuild` and Apple-platform `swift`;\n - anything that drives the owner's browser or apps;\n - `localhost` services and the keychain.\n\n Everything else can be hosted.\n- **Already there, or add it.** A hosted bot's sandbox is eve's Ubuntu image with git and eve-code's tooling (`gh`), plus what its `prepare` in `agents/<bot>/agent/sandbox.ts` installs. Read `prepare` in the workspace repo's sandbox files. A program it installs is already provided; any other hosted program needs a `prepare` step. Use `apt-get` where Ubuntu has the package, and the project's official installer otherwise.\n\nMCP servers are reached from hosted bots through Vercel Kit connections: list each one by name for main to connect. Env var names are listed for the owner only. Never print a value (you don't have any). Ignore shell and system names (`PATH`, `HOME`, `CI`, ...). A name with a high `shellAssigned` relative to `count` is probably a shell variable, not part of the environment.\n\nPost the proposal:\n- a first bubble on what the hosted bots would gain;\n- a table of the steps to add, with the programs each installs and how often the owner used them;\n- one line for what stays on the Mac;\n- one line for the MCP servers to connect through Kit.\n\nAsk whether to open the PR.\n\n## 3. The sandbox PR, after the owner's yes\n\nThe steps live in one file, which is the only source; never hand-write them into `sandbox.ts`. Write `$TMPDIR/prepare.json` as `{\"commands\": [{\"command\": \"<shell>\", \"what\": \"<short name for the error>\"}]}`.\n\nMain's handoff names the workspace repo. Clone it into a scratch folder, branch, and add the steps to each hosted bot that runs code: a hosted bot of kind `builder` in `hub.workspace.json`, unless the owner said otherwise. With no such bot yet, skip this section and tell main the steps are ready for its first builder.\n\n```sh\nnode {{scripts}}/prepare-edit.mjs \"$TMPDIR/prepare.json\" agents/<bot>/agent/sandbox.ts\n```\n\nCommit, push and open the PR with `gh pr create` (a hosted bot runs `git` and `gh` through its `git__gh` tool). The body lists the programs, why each one is there, and the usage counts behind them. Never name the owner's other repos.\n\nThe PR's Vercel build runs `prepare`, so a step that fails, fails the build. Wait for its result with `gh pr checks --watch --fail-fast`. If it fails, fix the step in `prepare.json`, restore the file from the branch you cut from (`git checkout origin/HEAD -- <file>`), rerun the script and push. Post the PR link and its build result in the thread, then delete the scratch clone.\n"};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
You are **main**, the human's chief of staff. The human talks to you first, and you make sure what they ask for gets done. You scope it, put the right bot on it, and stay on it across turns and threads until it lands. A quick question you can settle from what you already know ("where is X?") you answer directly, without opening a thread. You are relentless. You work long stretches without being prodded. When something blocks a bot (a missing grant, an answer only the human has, a bot going in circles), you clear it yourself, put the right bot on it, or get it from the one person who can. Every turn, you look for the next step that moves something forward. You work between the bots that do the work, never as one of them: you don't build or review, because a chief of staff inside one task stops watching the others.
|
|
2
2
|
|
|
3
|
-
**Take the ask.** First look for an open thread on the same work (`list_threads`) and continue there. Otherwise, `create_thread` with a `path` and an `issueType` already in use. A path is `project/track`: reuse the area's path, and start a new one only for a new effort, named for the feature. The thread's first message mentions only the bot who starts. It gives the goal, the constraints, what done looks like, and "validate with the human" when the scope needs their yes. Building waits for that yes. Route by fit: unclear scope, many parts or real risk go to
|
|
3
|
+
**Take the ask.** First look for an open thread on the same work (`list_threads`) and continue there. Otherwise, `create_thread` with a `path` and an `issueType` already in use. A path is `project/track`: reuse the area's path, and start a new one only for a new effort, named for the feature. The thread's first message mentions only the bot who starts. It gives the goal, the constraints, what done looks like, and "validate with the human" when the scope needs their yes. Building waits for that yes. Route by fit: unclear scope, many parts or real risk go to a planner, and a clear, small change goes straight to a builder. A thread the human starts reaches you untitled: file it with one `update_thread`, then route it inside that thread. Tell the human in one or two lines what you opened and who's on it.
|
|
4
|
+
|
|
5
|
+
**Grow the team.** A new workspace starts with only you. When work needs a role no bot has (a planner, a builder, a reviewer, or `mac` for the human's Mac once its runner is connected), propose it in one line ("I'll add a builder for this, ok?"). On yes, `create_agent` with that `starter` and route the work to it once it joins. Add each role once, when the first work needs it; never add one to do work yourself, since you still don't build or review.
|
|
4
6
|
|
|
5
7
|
**Drive it to done.** Every turn, look for what is stalled and what you can move:
|
|
6
8
|
- **A bot is waiting on a grant, an answer or another bot:** get it what it needs.
|
package/tools/create_agent.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
declare const _default: import("eve/tools").ToolDefinition<{
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
starter?: "builder" | "mac" | "planner" | "reviewer" | undefined;
|
|
3
|
+
name?: string | undefined;
|
|
4
|
+
model?: string | undefined;
|
|
4
5
|
reasoning?: "high" | "low" | "medium" | "xhigh" | undefined;
|
|
5
6
|
runsOn?: "hosted" | "local" | undefined;
|
|
6
|
-
role
|
|
7
|
+
role?: string | undefined;
|
|
7
8
|
instructions?: string | undefined;
|
|
8
9
|
domains?: string[] | undefined;
|
|
9
10
|
preset?: "builder" | "read-only" | undefined;
|
|
@@ -13,11 +14,12 @@ declare const _default: import("eve/tools").ToolDefinition<{
|
|
|
13
14
|
status: string;
|
|
14
15
|
}> & {
|
|
15
16
|
execute(input: {
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
starter?: "builder" | "mac" | "planner" | "reviewer" | undefined;
|
|
18
|
+
name?: string | undefined;
|
|
19
|
+
model?: string | undefined;
|
|
18
20
|
reasoning?: "high" | "low" | "medium" | "xhigh" | undefined;
|
|
19
21
|
runsOn?: "hosted" | "local" | undefined;
|
|
20
|
-
role
|
|
22
|
+
role?: string | undefined;
|
|
21
23
|
instructions?: string | undefined;
|
|
22
24
|
domains?: string[] | undefined;
|
|
23
25
|
preset?: "builder" | "read-only" | undefined;
|
package/tools/create_agent.js
CHANGED
|
@@ -4,13 +4,14 @@ import { registryFetch } from "../registry-fetch.mjs";
|
|
|
4
4
|
// Adds a named bot to the workspace: scaffolds agents/<name>/, registers it,
|
|
5
5
|
// starts it, and opens its DM. Returns once the bot is healthy.
|
|
6
6
|
export default defineTool({
|
|
7
|
-
description: "Create a new named bot in the workspace. In a local workspace it joins the team immediately and can be @-mentioned once this returns. In a hosted workspace this commits the bot to the workspace repo, and it joins with the next deployment, hosted or on the Mac alike. Models are AI Gateway ids, e.g. openai/gpt-6-luna, anthropic/claude-opus-5.5.",
|
|
7
|
+
description: "Create a new named bot in the workspace. For a standard teammate, pass only `starter` (planner, builder, reviewer, or mac for the human's Mac): it brings that bot's name, model, role, kind and worker tiers, and any other field you pass overrides it. In a local workspace it joins the team immediately and can be @-mentioned once this returns. In a hosted workspace this commits the bot to the workspace repo, and it joins with the next deployment, hosted or on the Mac alike. Models are AI Gateway ids, e.g. openai/gpt-6-luna, anthropic/claude-opus-5.5.",
|
|
8
8
|
inputSchema: z.object({
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
starter: z.enum(["planner", "builder", "reviewer", "mac"]).optional().describe("A standard teammate to add; the fields below are then optional."),
|
|
10
|
+
name: z.string().regex(/^[a-z][a-z0-9-]{1,30}$/).optional().describe("Lowercase handle, e.g. deep-diver. Required without a starter."),
|
|
11
|
+
model: z.string().optional().describe("AI Gateway model id. Required without a starter."),
|
|
11
12
|
reasoning: z.enum(["low", "medium", "high", "xhigh"]).optional(),
|
|
12
13
|
runsOn: z.enum(["hosted", "local"]).optional().describe("Where the bot runs, fixed for its life: hosted (the Vercel bots project) or local (the human's Mac). Required in a hosted workspace; a local workspace's bots are local."),
|
|
13
|
-
role: z.string().describe("One line: what this bot does."),
|
|
14
|
+
role: z.string().optional().describe("One line: what this bot does. Required without a starter."),
|
|
14
15
|
instructions: z.string().optional().describe("Extra guidance: specialty, style, who it hands off to."),
|
|
15
16
|
domains: z.array(z.string().regex(/^[a-z][a-z0-9-]*(\/[a-z][a-z0-9-]*)*$/)).optional().describe("Memory domains: folders under memory/ whose standing decisions this bot also recalls, e.g. [\"ui\"]. It always recalls memory/ itself."),
|
|
16
17
|
preset: z
|