eve-hub 0.16.38 → 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.
@@ -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",
@@ -35,7 +36,6 @@
35
36
  "tools/remove_agent",
36
37
  "tools/restart_agent",
37
38
  "tools/restart_hub",
38
- "tools/set_summary",
39
39
  "tools/show",
40
40
  "tools/tag_threads",
41
41
  "tools/unarchive_thread",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eve-hub",
3
- "version": "0.16.38",
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",
package/prepare.mjs CHANGED
@@ -88,15 +88,15 @@ import { isHubRequest } from "eve-hub/hub-oidc";
88
88
  import { parseWorkspace } from "eve-hub/workspace";
89
89
  import workspace from "../../../../hub.manifest.json" with { type: "json" };
90
90
 
91
- // The roster, humans and subagent cap as deployed, Mac bots included, and the eve-hub and eve versions that built it, for the hub to sync from:
91
+ // The roster, humans, subagent cap, hop limit and shims as deployed, Mac bots included, and the eve-hub and eve versions that built it, for the hub to sync from:
92
92
  // hub.manifest.json is bundled at build, so a push whose deploy fails never
93
93
  // changes it. \`sha\` is the commit the deployment was built from, which the hub
94
94
  // syncs by. An uploaded deployment (onboarding's first, built while the repo is
95
95
  // made) has no commit, so it answers its own deployment id, which the hub reads
96
96
  // from the project's production deployment.
97
97
  const sha = process.env.VERCEL_GIT_COMMIT_SHA || process.env.VERCEL_DEPLOYMENT_ID;
98
- const { agents, humans, subagentCap, hopLimit } = parseWorkspace(workspace);
99
- const manifest = { sha, kit: ${JSON.stringify(KIT_VERSION)}, eve: ${JSON.stringify(EVE_VERSION)}, agents, humans, subagentCap, ...(hopLimit === undefined ? {} : { hopLimit }) };
98
+ const { agents, humans, subagentCap, hopLimit, shims } = parseWorkspace(workspace);
99
+ const manifest = { sha, kit: ${JSON.stringify(KIT_VERSION)}, eve: ${JSON.stringify(EVE_VERSION)}, agents, humans, subagentCap, ...(hopLimit === undefined ? {} : { hopLimit }), ...(shims === undefined ? {} : { shims }) };
100
100
 
101
101
  export default defineChannel({
102
102
  routes: [
@@ -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
@@ -13,7 +13,7 @@ export const KIT_FILES = {
13
13
  "tools/browser.ts": ["Its own headless browser, watched live in the Computer view."],
14
14
  "tools/fetch_upload.ts": ["Copies a file another bot shared (hub://upload/<id>) into the sandbox."],
15
15
  "tools/view_image.ts": ["Shows the bot an image (a sandbox file or a browser screenshot) so it can see it."],
16
- "tools/update_thread.ts": ["Update a thread this bot is in: title, track, domain and summary."],
16
+ "tools/update_thread.ts": ["Update a thread this bot is in: title, track and domain."],
17
17
  "tools/list_threads.ts": ["Every workspace thread, from any session."],
18
18
  "tools/read_thread.ts": ["Read any workspace thread."],
19
19
  "tools/list_agents.ts": ["The team right now: kind, place, tools, models, who's busy."],
@@ -30,7 +30,6 @@ export const MAIN_KIT_FILES = {
30
30
  "skills/onboarding-profile.ts": ["The onboarding-profile skill (packages/agent-kit/onboarding/SKILL.md): after sign-in, reads the user's Slack and GitHub and posts a profile with suggested schedules."],
31
31
  "tools/onboarding_scripts.ts": ["Writes the onboarding-profile skill's sandbox scripts into the sandbox."],
32
32
  "tools/create_thread.ts": ["Open a workspace thread for one piece of work."],
33
- "tools/set_summary.ts": ["Write the summary of a path (project/track) or a thread."],
34
33
  "tools/move_path.ts": ["Rename or merge a path (project/track): every thread under it moves."],
35
34
  "tools/archive_thread.ts": ["Archive a thread whose work is done."],
36
35
  "tools/unarchive_thread.ts": ["Bring an archived thread back."],
@@ -42,6 +41,7 @@ export const MAIN_KIT_FILES = {
42
41
  "tools/dash_update.ts": ["Set Dash entries: importance, lane, reason and deferred per thread."],
43
42
  "tools/apply_tags.ts": ["Retag threads, merge or rename tags: applies a staged proposal or a new batch directly."],
44
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."],
45
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."],
46
46
  };
47
47
 
@@ -55,13 +55,14 @@ export const MAIN_KIT_FILES = {
55
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).
56
56
  export const DEPLOYED_KIT_FILES = new Set([
57
57
  "channels/eve.ts",
58
+ "schedules/tracker-prs.ts",
58
59
  "skills/onboarding-profile.ts",
59
60
  "tools/onboarding_scripts.ts",
60
61
  ...["react", "show", "fetch_upload", "update_thread", "list_threads", "read_thread", "post_message"].map((t) => `tools/${t}.ts`),
61
62
  "memory/team.ts",
62
63
  "skills/forms.ts",
63
64
  "skills/fanout.ts",
64
- ...["create_thread", "set_summary", "move_path", "archive_thread", "unarchive_thread", "list_agents", "create_agent", "remove_agent", "restart_agent", "tag_threads", "dash_update", "apply_tags"].map((t) => `tools/${t}.ts`),
65
+ ...["create_thread", "move_path", "archive_thread", "unarchive_thread", "list_agents", "create_agent", "remove_agent", "restart_agent", "tag_threads", "dash_update", "apply_tags"].map((t) => `tools/${t}.ts`),
65
66
  ]);
66
67
 
67
68
  // ---- stub text: what each kit stub file says ----
@@ -1,3 +1,3 @@
1
1
  - **Team.** `create_agent` adds a bot to the workspace; if nobody on the team covers the work well, create a specialist (name, model, role, instructions), then route to it. Give planners and reviewers `preset: "read-only"` so they can't edit files. `remove_agent` removes a bot from the team and shuts it down (name); use it when the human asks. It can't remove you (the coordinator) or humans.
2
2
  - **Memory.** A new memory entry ships in the next workspace sync PR; each bot recalls it on its next turn, as a `Standing decision <id>:` or `Your standing decision <id>:` message.
3
- - **Summaries.** A track summary you write with `set_summary` stands until you write another. `list_threads` returns thread and track summaries.
3
+ - **Summaries.** The hub derives each thread's state (who it waits on, since when, the next step) after every post, and each track's summary from its threads. `list_threads` returns both; nobody writes them.
@@ -21,6 +21,6 @@ You are the **coordinator**, the human's chief of staff. The human talks to you
21
21
  - **A merge can unblock other threads:** on a merge report, `list_threads` for threads that wait on it (a PR stacked on it, a build or release of it, a follow-up gated on it), and in the same turn post in each to its owning bot with its next step (rebase, build, start).
22
22
  - **A post addressed to you (a report, verdict or answer):** never end the turn on a reaction alone. Reply with the next step: who acts, on what, by when. If the step is a wait (CI, a run, the human), name it and arm a wake-up. React alone only when the post changes nothing and the next step is already named, armed and live.
23
23
 
24
- **Keep the workspace legible.** Give a thread a summary (`set_summary`) once its opening ask no longer describes it. Keep tags tidy with `tag_threads` and `apply_tags`, and say in one line what changed. On the Dash, the hub sets ready, and a deferral the human made stays. Tags and paths the human set stay too. In a track chat, answer from that track's threads, PRs and summary, and post new work in the thread that owns it.
24
+ **Keep the workspace legible.** Keep tags tidy with `tag_threads` and `apply_tags`, and say in one line what changed. On the Dash, the hub sets ready, and a deferral the human made stays. Tags and paths the human set stay too. In a track chat, answer from that track's threads, their states and PRs, and post new work in the thread that owns it.
25
25
 
26
26
  **Memory.** A preference that should outlast the task becomes `memory/<id>.md` (every bot) or `agents/<bot>/memory/<id>.md` (one bot). It has front matter `id`, `status: active`, `since` and `evidence` (`[thr_…/msg_…]`), then the rule in at most 3 lines. A replaced entry gets `status: superseded` and `superseded_by`, never a rewrite. Keep `memory/human.md` (who the human is and how they work) current the same way.
@@ -7,6 +7,7 @@ declare const _default: import("eve/tools").ToolDefinition<{
7
7
  project?: string | undefined;
8
8
  path?: string | undefined;
9
9
  issueType?: string | undefined;
10
+ waitingOn?: string | undefined;
10
11
  limit?: number | undefined;
11
12
  }, any> & {
12
13
  execute(input: {
@@ -18,6 +19,7 @@ declare const _default: import("eve/tools").ToolDefinition<{
18
19
  project?: string | undefined;
19
20
  path?: string | undefined;
20
21
  issueType?: string | undefined;
22
+ waitingOn?: string | undefined;
21
23
  limit?: number | undefined;
22
24
  }, ctx: import("eve/tools").ToolContext): Promise<any>;
23
25
  };
@@ -5,7 +5,7 @@ import { defineTool } from "eve/tools";
5
5
  import { z } from "zod";
6
6
  import { registryFetch } from "../registry-fetch.mjs";
7
7
  export default defineTool({
8
- description: "List workspace threads, workspace-wide (not just the one you're in): id, title, participants, status (active/archived), who's busy there (since when, doing what), last activity, tags (path: project/track, e.g. 'web/checkout'; project; issue type), and the thread's stored summary when main wrote one. Newest activity first. `paths` holds the listed threads' paths with each one's current summary (what's up in that feature). Use it to find a thread by title or summary before asking someone, and read_thread to read one.",
8
+ description: "List workspace threads, workspace-wide (not just the one you're in): id, title, participants, status (active/archived), who's busy there (since when, doing what), last activity, tags (path: project/track, e.g. 'web/checkout'; project; issue type), and where the thread stands: `state` (who it waits on: `human`, a bot, `ci`, or null for nothing pending; `since`; the `next` line; the message it rests on), which the hub derives after every post, and `summary`, the same as one line. Newest activity first. `paths` holds the listed threads' paths, each summarised from its threads' states. Pass `waitingOn: \"human\"` for the threads waiting on the human, the longest waiting first. Use it to find a thread by title or summary before asking someone, and read_thread to read one.",
9
9
  inputSchema: z.object({
10
10
  query: z.string().optional().describe("Match threads whose title or summary contains this text (or an exact id). Title matches come first."),
11
11
  archived: z.enum(["exclude", "include", "only"]).optional().describe("Archived threads: exclude (default), include, or only."),
@@ -15,6 +15,7 @@ export default defineTool({
15
15
  project: z.string().optional().describe("Only threads with this project tag ('none' for untagged)."),
16
16
  path: z.string().optional().describe("Only threads on this path or under it, e.g. 'web' or 'web/checkout'."),
17
17
  issueType: z.string().optional().describe("Only threads with this issue type tag ('none' for untagged)."),
18
+ waitingOn: z.string().optional().describe("Only threads waiting on this party: 'human', a bot name, 'ci', or 'nobody'. Sorted longest waiting first."),
18
19
  limit: z.number().int().min(1).max(500).optional().describe("Max threads (default 50)."),
19
20
  }),
20
21
  async execute(input) {
@@ -3,7 +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
- summary?: 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;
7
15
  pathLabel?: string | undefined;
8
16
  projectLabel?: string | undefined;
9
17
  issueTypeLabel?: string | undefined;
@@ -18,7 +26,15 @@ declare const _default: import("eve/tools").ToolDefinition<{
18
26
  title?: string | undefined;
19
27
  path?: string | null | undefined;
20
28
  issueType?: string | undefined;
21
- summary?: 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;
22
38
  pathLabel?: string | undefined;
23
39
  projectLabel?: string | undefined;
24
40
  issueTypeLabel?: string | undefined;
@@ -1,22 +1,37 @@
1
1
  // The `update_thread` tool, shared by every bot: say what a thread is, in one
2
- // call. Title, path (project/track), issue type and the thread's summary. The
3
- // registry keeps the rename history and posts an inline "<bot> renamed this
4
- // thread: old → new" marker; tags go on thread.tags (lib/tags.mjs) and the
5
- // summary is the one `set_summary` writes (lib/paths.mjs setSummary). `artifacts`
6
- // adds Artifacts pane entries with no post: the hub writes one silent event row each.
2
+ // call. Title, path (project/track) and issue type. The registry keeps the
3
+ // rename history and posts an inline "<bot> renamed this thread: old → new"
4
+ // marker; tags go on thread.tags (lib/tags.mjs). The thread's summary is the
5
+ // hub's, derived from its posts (hosted-hub/src/shims/index.ts).
6
+ // `artifacts` adds Artifacts pane entries with no post: the hub writes one
7
+ // silent event row each.
7
8
  import { defineTool } from "eve/tools";
8
9
  import { z } from "zod";
9
10
  import { registryFetch } from "../registry-fetch.mjs";
10
11
  import { documentUrl } from "../hub-upload.js";
11
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
+ });
12
23
  export default defineTool({
13
- description: "Update a workspace thread you're in: its title, track, issue type and summary, 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 summary is a short note on what the thread is about and where it stands; it replaces the previous one, and empty text clears it. 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.",
14
25
  inputSchema: z.object({
15
26
  threadId: z.string().describe("Thread id, e.g. thr_0002 (shown in the [thread: …] header)."),
16
27
  title: z.string().optional().describe("New short title, e.g. 'Checkout: retry failed payments'. Omit to leave it."),
17
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."),
18
29
  issueType: z.string().optional().describe("Issue type tag: the kind of work, e.g. 'feature', 'bugfix', 'infra', 'docs', 'review', 'research'. Lowercase slug."),
19
- summary: z.string().optional().describe("The thread's whole new summary, markdown: what it's about and where it stands. Empty clears it. Omit to leave it."),
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."),
package/workspace.mjs CHANGED
@@ -12,6 +12,7 @@
12
12
  // "subagentCap"?: { "model": "…", "reasoning": "low", "models": ["…"] },
13
13
  // "limits"?: { "maxTokenCostUsdPerSession": 75 },
14
14
  // "hopLimit"?: 20,
15
+ // "shims"?: { "thread-tracker": true, "thread-post-model": true, "thread-sitrep": false },
15
16
  // "code"?: ["~/wrk/hub"],
16
17
  // "cachePaths"?: ["node_modules", "packages/kit/node_modules"]
17
18
  // }
@@ -25,6 +26,9 @@
25
26
  // bot delivery is blocked (a positive integer). Absent or null: no limit. The
26
27
  // hub edits it at runtime (Settings, PUT /api/hop-limit); no limit removes it.
27
28
  //
29
+ // `shims`: which of the hosted hub's ways of working out where a thread stands
30
+ // are on (SHIM_DEFAULTS names them and their defaults); an unknown name is refused.
31
+ //
28
32
  // `code`: where the code this workspace's bots work on lives (absolute, ~/,
29
33
  // or relative to the workspace), when it isn't the workspace repo itself.
30
34
  // Every bot's bash tool names it (tools/bash.ts), so no bot has to hunt for it.
@@ -84,7 +88,7 @@ export function agentRunsOn(parsed, name) {
84
88
  return agent.runsOn ?? parsed.runsOn ?? "hosted";
85
89
  }
86
90
 
87
- // Validates the parsed file; returns { agents, runsOn?, humans?, subagentCap? } with
91
+ // Validates the parsed file; returns { agents, runsOn?, humans?, subagentCap?, hopLimit?, shims? } with
88
92
  // ports not yet assigned. Throws a WorkspaceError naming the bad field.
89
93
  export function parseWorkspace(data, file = WORKSPACE_FILE) {
90
94
  if (!isObject(data)) fail(file, "must be a JSON object with an \"agents\" array");
@@ -157,6 +161,10 @@ export function parseWorkspace(data, file = WORKSPACE_FILE) {
157
161
  if (hopLimitError(data.hopLimit)) fail(file, `"hopLimit" ${hopLimitError(data.hopLimit)}`);
158
162
  out.hopLimit = data.hopLimit;
159
163
  }
164
+ if (data.shims !== undefined) {
165
+ if (shimsError(data.shims)) fail(file, `"shims" ${shimsError(data.shims)}`);
166
+ out.shims = { ...data.shims };
167
+ }
160
168
  if (data.code !== undefined) {
161
169
  const code = typeof data.code === "string" ? [data.code] : data.code;
162
170
  if (!Array.isArray(code) || !code.length || code.some((p) => typeof p !== "string" || !p.trim())) fail(file, "\"code\" must be a path or a non-empty array of paths");
@@ -181,6 +189,25 @@ export const hopLimitError = (value) => (value === null || (Number.isInteger(val
181
189
  // The hop limit a parsed workspace sets: its `hopLimit`, else null (no limit).
182
190
  export const workspaceHopLimit = (parsed) => parsed.hopLimit ?? null;
183
191
 
192
+ // The hub's thread-state shims, each on or off in hub.workspace.json's `shims`, in the order the hub reads
193
+ // their answers when more than one is on; a name the file leaves out takes its default here.
194
+ export const SHIM_DEFAULTS = Object.freeze({ "thread-tracker": true, "thread-post-model": true, "thread-sitrep": false });
195
+
196
+ // Why `value` isn't a valid shims map (known shim names to booleans), or null when it is.
197
+ export const shimsError = (value) => {
198
+ if (!isObject(value)) return `must be an object of shim names to true or false (got ${JSON.stringify(value)})`;
199
+ const unknown = Object.keys(value).filter((name) => !(name in SHIM_DEFAULTS));
200
+ if (unknown.length) return `names unknown shims: ${unknown.join(", ")} (known: ${Object.keys(SHIM_DEFAULTS).join(", ")})`;
201
+ const bad = Object.entries(value).filter(([, on]) => typeof on !== "boolean");
202
+ return bad.length ? `must map each shim to true or false (${bad.map(([name]) => name).join(", ")})` : null;
203
+ };
204
+
205
+ // Every shim with whether it is on: the file's in its order, then the defaults of those it leaves out.
206
+ export const workspaceShims = (parsed) => {
207
+ const given = parsed?.shims ?? {};
208
+ return { ...given, ...Object.fromEntries(Object.entries(SHIM_DEFAULTS).filter(([name]) => !(name in given))) };
209
+ };
210
+
184
211
  // Deterministic ports: explicit `port`s are kept; every other agent gets the
185
212
  // first base port from `base` (in file order) that no agent has taken.
186
213
  // `offset` is added to every port.
@@ -1,10 +0,0 @@
1
- declare const _default: import("eve/tools").ToolDefinition<{
2
- scope: string;
3
- text: string;
4
- }, any> & {
5
- execute(input: {
6
- scope: string;
7
- text: string;
8
- }, ctx: import("eve/tools").ToolContext): Promise<any>;
9
- };
10
- export default _default;
@@ -1,25 +0,0 @@
1
- // The `set_summary` tool (main's): write the free-form summary of a path
2
- // (project/track) or a thread, one markdown note a model keeps current. A track's says what's up
3
- // in that feature (the human sees it above the track's threads); a thread's
4
- // says what it's about and where it stands. The classifier files threads by
5
- // these, and list_threads returns them. lib/paths.mjs setSummary.
6
- import { defineTool } from "eve/tools";
7
- import { z } from "zod";
8
- import { registryFetch } from "../registry-fetch.mjs";
9
- export default defineTool({
10
- description: "Write the summary of a path (project/track) or a thread: a short free-form markdown note, kept current. A path's: what's up in that feature, for the human (what's moving, what's blocked or waiting on them, what just landed). A thread's: what it's about and where it stands. You decide what's worth saying. It replaces the previous summary; empty text clears it. list_threads returns both.",
11
- inputSchema: z.object({
12
- scope: z.string().describe("What it summarizes: 'path:<path>' (e.g. 'path:web/checkout') or 'thread:<id>' (e.g. 'thread:thr_0d00')."),
13
- text: z.string().describe("The whole new summary, markdown. Empty clears it."),
14
- }),
15
- async execute({ scope, text }, ctx) {
16
- const res = await registryFetch(`/api/summaries`, {
17
- method: "POST",
18
- headers: { "content-type": "application/json" },
19
- body: JSON.stringify({ sessionId: ctx.session.id, scope, text }),
20
- });
21
- if (!res.ok)
22
- throw new Error(`set_summary failed: ${res.status} ${await res.text()}`);
23
- return await res.json();
24
- },
25
- });