eve-hub 0.16.39 → 0.16.40
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 +7 -1
- package/kit-modules.json +1 -0
- package/package.json +1 -1
- package/schedules/tracker-prs-run.mjs +98 -0
- package/schedules/tracker-prs.d.ts +5 -0
- package/schedules/tracker-prs.js +12 -0
- package/stubs.mjs +2 -0
- package/tools/update_thread.d.ts +18 -0
- package/tools/update_thread.js +16 -1
package/github-connector.mjs
CHANGED
|
@@ -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)
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eve-hub",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.40",
|
|
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,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`),
|
package/tools/update_thread.d.ts
CHANGED
|
@@ -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;
|
package/tools/update_thread.js
CHANGED
|
@@ -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
|
|
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."),
|