eve-hub 0.16.39 → 0.16.41

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.
@@ -1,3 +1,9 @@
1
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) asks it for the human's token, so they act as the human.
2
+ // App). The bots' `git__gh` (prepare.mjs) and main's tracker-prs schedule (schedules/tracker-prs-run.mjs)
3
+ // ask it for the same subject, so both act as the human. Its own module so a schedule can import it
4
+ // without prepare.mjs's build-time imports.
3
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" });
package/kit-modules.json CHANGED
@@ -10,6 +10,7 @@
10
10
  "memory/team",
11
11
  "memory/workspace",
12
12
  "schedules/prune",
13
+ "schedules/tracker-prs",
13
14
  "schedules/wake",
14
15
  "skills/deep-dive",
15
16
  "skills/fanout",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eve-hub",
3
- "version": "0.16.39",
3
+ "version": "0.16.41",
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",
@@ -0,0 +1,98 @@
1
+ // One tick of main's tracker-prs schedule (schedules/tracker-prs.ts): tracker rows linked to a GitHub PR or issue move to
2
+ // Done when it merged (or an issue closed as completed) and to Dropped when it closed otherwise. The hub lists the refs on
3
+ // rows still open (GET /api/tracker/prs, which reads only the summaries that carry rows) and settles what GitHub said
4
+ // (POST /api/tracker/prs). GitHub is asked in one GraphQL query per 50 refs, as the workspace human: a hosted hub's
5
+ // function asks Vercel Connect for the token the bots' `git__gh` gets; a local hub asks `gh auth token`.
6
+ //
7
+ // Plain JS on purpose: the schedule (TypeScript, bundled by eve) and the tests (plain node) import the same module.
8
+ import { execFile } from "node:child_process";
9
+ import { getToken } from "@vercel/connect";
10
+ import { GITHUB_CONNECTOR, rosterSubject } from "../github-connector.mjs";
11
+ import { registryFetch } from "../registry-fetch.mjs";
12
+
13
+ export const GITHUB_GRAPHQL_URL = "https://api.github.com/graphql";
14
+ export const REFS_PER_QUERY = 50;
15
+ // The hub gives a schedule 60 s: the token, the queries and the two hub calls fit.
16
+ export const GH_TIMEOUT_MS = 20_000;
17
+ export const TOKEN_TIMEOUT_MS = 5_000;
18
+
19
+ const REF = /^([A-Za-z0-9][\w.-]*)\/([\w.-]+)#(\d+)$/;
20
+
21
+ /** The workspace human's GitHub user token. */
22
+ export function githubToken(human, { hosted = Boolean(process.env.VERCEL), connect = getToken, timeoutMs = TOKEN_TIMEOUT_MS } = {}) {
23
+ if (hosted) {
24
+ if (!human) return Promise.reject(new Error("the hub names no human to ask GitHub as"));
25
+ let timer;
26
+ const deadline = new Promise((_, reject) => { timer = setTimeout(() => reject(new Error("Vercel Connect timed out")), timeoutMs); });
27
+ return Promise.race([connect(GITHUB_CONNECTOR, { subject: rosterSubject(human) }), deadline]).finally(() => clearTimeout(timer));
28
+ }
29
+ return new Promise((resolve, reject) => {
30
+ execFile("gh", ["auth", "token"], { timeout: timeoutMs }, (error, stdout, stderr) => {
31
+ const token = String(stdout ?? "").trim();
32
+ if (!error && token) resolve(token);
33
+ else reject(new Error(`gh auth token: ${String(stderr || error?.message || "no token").trim().slice(0, 300)}`));
34
+ });
35
+ });
36
+ }
37
+
38
+ /** One query naming each ref by alias (`r0`, `r1`, …); refs that aren't `owner/repo#N` are left out. */
39
+ export function stateQuery(refs) {
40
+ const parts = refs.map((ref, i) => {
41
+ const [, owner, name, number] = ref.match(REF);
42
+ return `r${i}: repository(owner: ${JSON.stringify(owner)}, name: ${JSON.stringify(name)}) { issueOrPullRequest(number: ${number}) { __typename ... on PullRequest { state } ... on Issue { state stateReason } } }`;
43
+ });
44
+ return `query { ${parts.join(" ")} }`;
45
+ }
46
+
47
+ /** What a row becomes from GitHub's answer for its ref: "completed", "canceled", or undefined while it is open or unknown. */
48
+ export function settledState(node) {
49
+ if (!node) return undefined;
50
+ if (node.__typename === "PullRequest") return node.state === "MERGED" ? "completed" : node.state === "CLOSED" ? "canceled" : undefined;
51
+ if (node.state !== "CLOSED") return undefined;
52
+ return node.stateReason === "NOT_PLANNED" || node.stateReason === "DUPLICATE" ? "canceled" : "completed";
53
+ }
54
+
55
+ /** POST one GraphQL query; a ref GitHub can't answer (gone, no access) comes back null in `data` and is skipped. */
56
+ export async function ghGraphql(query, { token, fetchImpl = fetch, timeoutMs = GH_TIMEOUT_MS }) {
57
+ const res = await fetchImpl(GITHUB_GRAPHQL_URL, {
58
+ method: "POST",
59
+ headers: { authorization: `bearer ${token}`, "content-type": "application/json", "user-agent": "eve-hub-tracker" },
60
+ body: JSON.stringify({ query }),
61
+ signal: AbortSignal.timeout(timeoutMs),
62
+ });
63
+ const text = await res.text();
64
+ let body = null;
65
+ try { body = JSON.parse(text); } catch {}
66
+ if (res.ok && body?.data) return body.data;
67
+ throw new Error(`GitHub ${res.status}: ${String(body?.errors?.[0]?.message ?? body?.message ?? text).slice(0, 300)}`);
68
+ }
69
+
70
+ /** One tick. Returns { asked, moved }. A failed GitHub call throws, so the hub's run history shows it, and nothing is settled. */
71
+ export async function runTrackerPrs({ hub = hubCalls(), token = githubToken, fetchImpl = fetch } = {}) {
72
+ const { refs, human } = await hub.open();
73
+ const asked = refs.filter((r) => REF.test(r));
74
+ if (!asked.length) return { asked: 0, moved: 0 };
75
+ const key = await token(human);
76
+ const states = {};
77
+ for (let i = 0; i < asked.length; i += REFS_PER_QUERY) {
78
+ const chunk = asked.slice(i, i + REFS_PER_QUERY);
79
+ const data = await ghGraphql(stateQuery(chunk), { token: key, fetchImpl });
80
+ chunk.forEach((ref, j) => {
81
+ const state = settledState(data[`r${j}`]?.issueOrPullRequest);
82
+ if (state) states[ref] = state;
83
+ });
84
+ }
85
+ if (!Object.keys(states).length) return { asked: asked.length, moved: 0 };
86
+ const { moved } = await hub.settle(states);
87
+ return { asked: asked.length, moved };
88
+ }
89
+
90
+ /** The two hub calls, over the registry's HTTP API as the deployment. */
91
+ export function hubCalls({ fetchImpl } = {}) {
92
+ const call = async (init) => {
93
+ const res = await registryFetch("/api/tracker/prs", { ...init, headers: { "content-type": "application/json" } }, { deadlineMs: 10_000, ...(fetchImpl ? { fetchImpl } : {}) });
94
+ if (!res.ok) throw new Error(`${init.method} /api/tracker/prs: ${res.status} ${(await res.text()).slice(0, 300)}`);
95
+ return res.json();
96
+ };
97
+ return { open: () => call({ method: "GET" }), settle: (states) => call({ method: "POST", body: JSON.stringify({ states }) }) };
98
+ }
@@ -0,0 +1,5 @@
1
+ declare const _default: {
2
+ cron: string;
3
+ run(): Promise<void>;
4
+ };
5
+ export default _default;
@@ -0,0 +1,12 @@
1
+ // main's tracker-prs schedule: every 10 minutes, tracker rows linked to a GitHub PR or issue move to Done when it merged
2
+ // and to Dropped when it closed (schedules/tracker-prs-run.mjs).
3
+ import { defineSchedule } from "eve/schedules";
4
+ import { runTrackerPrs } from "./tracker-prs-run.mjs";
5
+ export default defineSchedule({
6
+ cron: "*/10 * * * *",
7
+ async run() {
8
+ const { asked, moved } = await runTrackerPrs();
9
+ if (moved)
10
+ console.log(`[hub:tracker-prs] ${moved} of ${asked} rows moved`);
11
+ },
12
+ });
package/stubs.mjs CHANGED
@@ -41,6 +41,7 @@ export const MAIN_KIT_FILES = {
41
41
  "tools/dash_update.ts": ["Set Dash entries: importance, lane, reason and deferred per thread."],
42
42
  "tools/apply_tags.ts": ["Retag threads, merge or rename tags: applies a staged proposal or a new batch directly."],
43
43
  // Handler schedules: the hub fires them from its minute clock (registry/schedules.mjs).
44
+ "schedules/tracker-prs.ts": ["Every 10 minutes: tracker rows linked to a GitHub PR or issue move to Done when it merged and to Dropped when it closed."],
44
45
  "schedules/prune.ts": ["Every 10 minutes: one prune pass over what archived or deleted threads leave on disk: worktrees, scratch, their-only uploads, idle bots' traces and workflow runs; unsaved work kept as refs/bundles."],
45
46
  };
46
47
 
@@ -54,6 +55,7 @@ export const MAIN_KIT_FILES = {
54
55
  // restart_hub (the hub's own code), memory (the workspace's files), and main's prune schedule (it cleans the hub machine's disk).
55
56
  export const DEPLOYED_KIT_FILES = new Set([
56
57
  "channels/eve.ts",
58
+ "schedules/tracker-prs.ts",
57
59
  "skills/onboarding-profile.ts",
58
60
  "tools/onboarding_scripts.ts",
59
61
  ...["react", "show", "fetch_upload", "update_thread", "list_threads", "read_thread", "post_message"].map((t) => `tools/${t}.ts`),
@@ -10,8 +10,8 @@
10
10
  // workspace's lead and workers read. The lead chooses a tier from its tool description alone
11
11
  // (tierDescription); the fanout skill's report table is generated from `report`. The folder's
12
12
  // files themselves are stubs.mjs tierFiles: this module holds only text, so a deployed bot imports nothing else.
13
- // When the lead hands work to tiers: to run its pieces at once, never to offload one task. The execution template says the same sentence (test/fanout-skill.test.mjs).
14
- export const ROUTING_RULE = "Fan out work that splits into independent pieces: one tier call per piece, all in one step. A single task stays with you, however long. Judgment always stays with you.";
13
+ // When the lead hands work to tiers: every read, check and specified edit, a single task too, except one or two short tool calls it keeps; independent pieces go out at once. The execution template says the same sentence (test/fanout-skill.test.mjs).
14
+ export const ROUTING_RULE = "Hand every read, check and specified edit to its tier, even one on its own: a test run, a CI log, a read across files, a repo search. Keep a task only when it is one or two tool calls with short output. Fan out work that splits into independent pieces: one tier call per piece, all in one step. Judgment always stays with you.";
15
15
 
16
16
  // Null prototype: a lookup by a roster name sees only the tiers, never `constructor` or `__proto__`.
17
17
  export const SUBAGENT_TIERS = Object.freeze(Object.assign(Object.create(null), {
@@ -15,7 +15,7 @@
15
15
  - Why: every question costs the human a context switch, and most answers are in the code or the history.
16
16
  - **Split the work**
17
17
  - Do the part your role owns; hand the rest to the role that owns it, because skipping a role skips its check.
18
- - Your reader, editor and verifier run on models far cheaper than yours: reading goes to reader, specified edits to editor, pass/fail checks (running tests included) to verifier. When work splits, count the pieces before you route: three files to trace is three readers, two test runs to check is two verifiers. Never merge pieces, and never keep one of them yourself. One question, one edit or one test run, with nothing beside it, is a single task, not a piece. Fan out work that splits into independent pieces: one tier call per piece, all in one step. A single task stays with you, however long. Judgment always stays with you.
18
+ - Your reader, editor and verifier run on models far cheaper than yours: reading goes to reader, specified edits to editor, pass/fail checks (running tests included) to verifier. When work splits, count the pieces before you route: three files to trace is three readers, two test runs to check is two verifiers. Never merge pieces, and never keep one of them yourself. Hand every read, check and specified edit to its tier, even one on its own: a test run, a CI log, a read across files, a repo search. Keep a task only when it is one or two tool calls with short output. Fan out work that splits into independent pieces: one tier call per piece, all in one step. Judgment always stays with you.
19
19
  - Before you hand work to a reader, editor, verifier or `agent` copy, load the `fanout` skill and follow it: the receiver starts with none of your context.
20
20
  - A subagent with no commit, push, report or verdict in 30 minutes is stuck: cancel it and finish the work yourself.
21
21
  - **Waiting**
@@ -3,6 +3,15 @@ declare const _default: import("eve/tools").ToolDefinition<{
3
3
  title?: string | undefined;
4
4
  path?: string | null | undefined;
5
5
  issueType?: string | undefined;
6
+ tracker?: {
7
+ id: string;
8
+ title: string;
9
+ state: "backlog" | "canceled" | "completed" | "started" | "unstarted";
10
+ owner: string;
11
+ waitsOn?: string | undefined;
12
+ since?: string | undefined;
13
+ ref: string;
14
+ }[] | undefined;
6
15
  pathLabel?: string | undefined;
7
16
  projectLabel?: string | undefined;
8
17
  issueTypeLabel?: string | undefined;
@@ -17,6 +26,15 @@ declare const _default: import("eve/tools").ToolDefinition<{
17
26
  title?: string | undefined;
18
27
  path?: string | null | undefined;
19
28
  issueType?: string | undefined;
29
+ tracker?: {
30
+ id: string;
31
+ title: string;
32
+ state: "backlog" | "canceled" | "completed" | "started" | "unstarted";
33
+ owner: string;
34
+ waitsOn?: string | undefined;
35
+ since?: string | undefined;
36
+ ref: string;
37
+ }[] | undefined;
20
38
  pathLabel?: string | undefined;
21
39
  projectLabel?: string | undefined;
22
40
  issueTypeLabel?: string | undefined;
@@ -10,13 +10,28 @@ import { z } from "zod";
10
10
  import { registryFetch } from "../registry-fetch.mjs";
11
11
  import { documentUrl } from "../hub-upload.js";
12
12
  import { artifactInput, artifactUrl } from "./post_message.js";
13
+ // The hub checks each row again (packages/wire/src/state.ts TrackerRow); this is what the model reads.
14
+ const trackerRow = z.object({
15
+ id: z.string().describe("Your stable id for the row, unique in the list, e.g. 'server-step-12'. Keep it across rewrites."),
16
+ title: z.string().describe("What the work is, in a few words, e.g. 'Build server step 12'."),
17
+ state: z.enum(["backlog", "unstarted", "started", "completed", "canceled"]).describe("started: being worked on now; unstarted: next, ready to start; backlog: needed before the goal is done, not next; completed: done; canceled: dropped."),
18
+ owner: z.string().describe("Who acts on it: a bot name or the human's handle."),
19
+ waitsOn: z.string().optional().describe("Who or what it is blocked on, e.g. 'rconti' or 'an eve maintainer'."),
20
+ since: z.string().optional().describe("With waitsOn: the ISO date the wait began, e.g. '2026-11-03'."),
21
+ ref: z.string().describe("What the row opens; every row needs one: a post as thr_ID#msg_ID (a hash, never a slash), a PR or issue as owner/repo#N or its github.com URL, or an https URL."),
22
+ });
13
23
  export default defineTool({
14
- description: "Update a workspace thread you're in: its title, track and issue type, in one call. Retitle when the work's scope has changed and the old title no longer fits: everyone sees a short 'renamed this thread: old → new' line; the history is kept. Keep titles short (at most 80 characters). File it on a path (project/track, e.g. 'web/checkout': the first segment is the project, the second the track) and tag an issue type (the kind of work, e.g. 'feature', 'bugfix', 'infra'). Reuse a project, track and issue type in use whenever one fits. File under the project alone until at least 3 threads share a track. For a new tag, also give its display name (projectLabel, pathLabel or issueTypeLabel, e.g. 'Payments API'). The thread's summary isn't yours to write: the hub derives where it stands from its posts after each one. The reply's `inUse` lists the tags in use with their thread counts and flags a tag no other thread uses; when one in use fits better than yours, call update_thread again with it. Tags a human set by hand stay. DMs and archived threads can't be updated.",
24
+ description: "Update a workspace thread you're in: its title, track, issue type and tracker, in one call. Retitle when the work's scope has changed and the old title no longer fits: everyone sees a short 'renamed this thread: old → new' line; the history is kept. Keep titles short (at most 80 characters). File it on a path (project/track, e.g. 'web/checkout': the first segment is the project, the second the track) and tag an issue type (the kind of work, e.g. 'feature', 'bugfix', 'infra'). Reuse a project, track and issue type in use whenever one fits. File under the project alone until at least 3 threads share a track. For a new tag, also give its display name (projectLabel, pathLabel or issueTypeLabel, e.g. 'Payments API'). The thread's summary isn't yours to write: the hub derives where it stands from its posts after each one. The reply's `inUse` lists the tags in use with their thread counts and flags a tag no other thread uses; when one in use fits better than yours, call update_thread again with it. Tags a human set by hand stay. DMs and archived threads can't be updated.",
15
25
  inputSchema: z.object({
16
26
  threadId: z.string().describe("Thread id, e.g. thr_0002 (shown in the [thread: …] header)."),
17
27
  title: z.string().optional().describe("New short title, e.g. 'Checkout: retry failed payments'. Omit to leave it."),
18
28
  path: z.string().nullable().optional().describe("Path: project, then at most one track, e.g. 'web/checkout' or 'api/billing'. Reuse a path in use (the reply's inUse and list_threads show them); start a track only once at least 3 threads share it. null: back to the project alone. A path a human set by hand stays."),
19
29
  issueType: z.string().optional().describe("Issue type tag: the kind of work, e.g. 'feature', 'bugfix', 'infra', 'docs', 'review', 'research'. Lowercase slug."),
30
+ tracker: z
31
+ .array(trackerRow)
32
+ .max(50)
33
+ .optional()
34
+ .describe("The thread's whole tracker: every piece of work toward its goal, one row each, drawn as the Artifacts pane's Pending now (started, unstarted), Pending until done (backlog), Done (completed) and Dropped (canceled) lists. Replaces the previous list; [] clears it. Omit to leave it. Keep it current: send the whole list again whenever a row's state, owner or wait changes. A row whose ref is a PR moves to Done or Dropped by itself when the PR merges or closes."),
20
35
  pathLabel: z.string().optional().describe("Display name for a new path's last segment, e.g. 'Cart'. Only names a path that has none yet."),
21
36
  projectLabel: z.string().optional().describe("Display name for a new project (a path's first segment), shown as its sidebar group, e.g. 'Payments API' for payments-api. Only names a project that has none yet."),
22
37
  issueTypeLabel: z.string().optional().describe("Display name for a new issue type tag, e.g. 'Bug fixes' for bugfix. Only names a tag that has none yet."),