eve-hub 0.16.55 → 0.16.56

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eve-hub",
3
- "version": "0.16.55",
3
+ "version": "0.16.56",
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/stubs.mjs CHANGED
@@ -13,11 +13,11 @@ 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 and domain."],
16
+ "tools/update_thread.ts": ["Update a thread this bot is in: its phase, notes, tracker, artifacts, title and track."],
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."],
20
- "tools/post_message.ts": ["Post into an existing thread or a bot's DM; with `status`, report the thread's phase."],
20
+ "tools/post_message.ts": ["Post a message into an existing thread or a bot's DM; a thread post @-mentions its receiver."],
21
21
  "skills/forms.ts": ["The forms skill (packages/agent-kit/forms/SKILL.md): how to draw a flow, sequence, chart or ask the app renders."],
22
22
  "skills/brief.ts": ["The brief skill (packages/agent-kit/brief/SKILL.md): how to write a brief for the human's yes and read it cold before posting."],
23
23
  "skills/fanout.ts": ["The fanout skill (packages/agent-kit/fanout/SKILL.md): the contract for handing work to the reader, editor and verifier subagents."],
@@ -19,7 +19,7 @@ You are the **coordinator**, the human's chief of staff. The human talks to you
19
19
  - **Cross-thread chores (merge, rebase, close out, restart):** post in each owning thread to the bot that built it. Never collect them into one bot or your own session.
20
20
  - **A builder reports a merge sha:** archive the thread (`archive_thread`) in that turn.
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
- - **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.
22
+ - **A post addressed to you (a report, review 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
24
  **What the human decides.** Scope: the owner of a thread posts a brief and the human confirms it before anything is built. After that, only merge approval and anything that needs their eyes wait on them.
25
25
 
@@ -5,7 +5,8 @@
5
5
  - **Mentions wake**
6
6
  - `@<name>` wakes one bot or person; `@all` wakes every participant in the thread.
7
7
  - Each wake costs that bot a turn, so mention at most one name per message: whoever acts next, in the message that hands off.
8
- - Name everyone else in plain text. A progress post mentions nobody. Never mention yourself.
8
+ - Name everyone else in plain text. Never mention yourself.
9
+ - Every thread post @-mentions its receiver: a bot, `@all` or the human. The hub refuses a post that names none, and an @-name that is not a bot or the human. Progress that is for no one is a `tracker` row through `update_thread`, not a post.
9
10
  - When a post names you but asks someone else to act (you're named for a later step, or only in passing), `react` to it and end the turn: no post and no closing text. A "nothing for me yet" reply wakes the sender and adds a post the human reads.
10
11
  - **The human**
11
12
  - Their handle and aliases are in `<team_status>` (e.g. `me: rconti, Rui`); any of them reaches the same person.
@@ -14,8 +15,9 @@
14
15
  - **DMs**
15
16
  - In a DM, mentions are links and wake nobody, so tag every bot you refer to.
16
17
  - **Phase**
17
- - When the thread's phase changes (planning, working, reviewing, done), add a status post: at most 8 plain words, no @-mention.
18
- - The brief, question or handoff is its own post, without `status`: the app draws a status post as a grey phase row, and the hub refuses one that carries more.
18
+ - When the thread's phase changes (planning, working, reviewing, done), call `update_thread` with `status {phase, text}`: the text is at most 8 plain words, no @-mention. It wakes no one, and the app draws it as a grey phase row.
19
+ - The brief, question or handoff is its own `post_message` that @-mentions whoever acts next; the hub refuses a phase marker that carries more than a marker.
19
20
  - **Notes**
21
+ - A note goes through `update_thread`'s `note`, together with a `status` (the current phase, to stay in it), and the hub refuses a verdict, a done status or a plan artifact without one.
20
22
  - Coordinator writes `problem` and `goal` when the human opens a thread; `decision`, `cleared` or `question`, with `about`, when the human answers or corrects something; and a `summary` (2 to 3 plain sentences: the goal, where it stands, what is next) after each human decision and each merge report.
21
23
  - Planner writes a `finding` on a brief. Reviewer writes an `adjustment` on fix-first and an `outcome` on ship. Builder writes an `outcome` on a merge and on real-path checks, and an `adjustment` when CI or a check changes the plan.
@@ -2,7 +2,7 @@
2
2
  - Your messages land in a chat feed, like Slack or iMessage. The human runs many threads and gives each a few minutes, so every message is read cold, often only its first line.
3
3
  - The reader has none of your context.
4
4
  - **Who these rules are for**
5
- - A post that @-mentions no bot is one the human reads: a result, a status report, a question. Every rule below applies to it.
5
+ - A post that @-mentions the human is one the human reads: a result, a status report, a question. Every rule below applies to it.
6
6
  - A post that @-mentions a bot is a handoff. The app folds bot-to-bot chatter, so a handoff keeps the dense format `<team_architecture>` asks for, ids and shas included. Only the length cap and the layout rules apply to it.
7
7
  - **Sound like a person** (core principle)
8
8
  - Write in the first person, the way a colleague texts you: what you did, what you found, and what happens next. "I fixed the late-link bug and pushed it. The reviewer has it now."
@@ -24,7 +24,7 @@
24
24
  - Never draw with characters: no arrows, boxes, numbered timelines or tables in a code fence.
25
25
  - **Disclose one step at a time**
26
26
  - A post is at most 2 sentences (the point or the question; the inbox shows only its first line), then at most 3 bullets or one small table: what the reader needs to act, and nothing more.
27
- - Supporting detail for the same message can follow a `---` line, with a blank line on each side; it becomes a second bubble, and each bubble holds up to 700 characters. The first bubble must stand alone: a bot the post doesn't name sees only it. A new update, a separate ask or handoff, or anything that needs its own recipient or status is another `post_message`. Most posts are one bubble.
27
+ - Supporting detail for the same message can follow a `---` line, with a blank line on each side; it becomes a second bubble, and each bubble holds up to 700 characters. The first bubble must stand alone: a bot the post doesn't name sees only it. A new update, a separate ask or handoff, or anything that needs its own recipient is another `post_message`; a phase or status goes through `update_thread`, never another post. Most posts are one bubble.
28
28
  - Depth (evidence, analysis, options, a long table) goes in a doc: write a markdown file in your sandbox and link it in the post, `[the analysis](/workspace/notes/analysis.md)`; `post_message` uploads it and the link opens it beside the thread, already styled. Never paste it. Write HTML only for a layout markdown can't hold, following the `vercel-design` skill when you have it.
29
29
  - Images go in `post_message`'s `images` (sandbox paths), never in the text.
30
30
  - A file reaches the human only as a markdown link in a post or through `show`, which upload it. A raw sandbox path in text is a file they can't open.
@@ -1,4 +1,3 @@
1
- import { z } from "zod";
2
1
  export declare const MAX_CHARS = 700;
3
2
  /** The characters the cap counts: the text without its links' targets. */
4
3
  export declare const postLength: (text: string) => number;
@@ -8,89 +7,12 @@ export declare function textError(text: string): string | null;
8
7
  export declare function voiceError(text: string): string | null;
9
8
  /** The text with each sandbox-path link target replaced by where the human's app opens it; a `#…` fragment stays on it. Each file is opened once. */
10
9
  export declare function rewriteDocLinks(text: string, open: (path: string) => Promise<string>): Promise<string>;
11
- export declare const artifactInput: z.ZodObject<{
12
- kind: z.ZodEnum<{
13
- decision: "decision";
14
- doc: "doc";
15
- plan: "plan";
16
- pr: "pr";
17
- review: "review";
18
- sitrep: "sitrep";
19
- walkthrough: "walkthrough";
20
- }>;
21
- title: z.ZodString;
22
- }, z.core.$strip>;
23
- export declare const trackerRow: z.ZodObject<{
24
- id: z.ZodString;
25
- title: z.ZodString;
26
- state: z.ZodEnum<{
27
- backlog: "backlog";
28
- canceled: "canceled";
29
- completed: "completed";
30
- started: "started";
31
- unstarted: "unstarted";
32
- }>;
33
- owner: z.ZodString;
34
- waitsOn: z.ZodOptional<z.ZodString>;
35
- ref: z.ZodString;
36
- }, z.core.$strip>;
37
- export declare const noteInput: z.ZodObject<{
38
- kind: z.ZodEnum<{
39
- adjustment: "adjustment";
40
- cleared: "cleared";
41
- decision: "decision";
42
- finding: "finding";
43
- goal: "goal";
44
- outcome: "outcome";
45
- problem: "problem";
46
- question: "question";
47
- summary: "summary";
48
- }>;
49
- text: z.ZodString;
50
- key: z.ZodOptional<z.ZodBoolean>;
51
- about: z.ZodOptional<z.ZodString>;
52
- }, z.core.$strip>;
53
- /** An artifact's link: the one given (a sandbox path uploaded), else the post's first link, else its PR. */
54
- export declare function artifactUrl(given: string | undefined, text: string, pr: string | undefined, open: (path: string) => Promise<string>): Promise<string>;
55
10
  declare const _default: import("eve/tools").ToolDefinition<{
56
11
  threadId?: string | undefined;
57
12
  agent?: string | undefined;
58
13
  text: string;
59
14
  images?: string[] | undefined;
60
15
  policy?: "abort" | "queue" | "steer" | undefined;
61
- status?: "done" | "planning" | "reviewing" | "working" | undefined;
62
- metadata?: {
63
- pr?: string | undefined;
64
- sha?: string | undefined;
65
- } | undefined;
66
- verdict?: "fix-first" | "rethink" | "ship" | undefined;
67
- decisions?: {
68
- question: string;
69
- options: {
70
- label: string;
71
- pickedBy?: string | undefined;
72
- }[];
73
- }[] | undefined;
74
- tested?: string | undefined;
75
- artifact?: {
76
- kind: "decision" | "doc" | "plan" | "pr" | "review" | "sitrep" | "walkthrough";
77
- title: string;
78
- url?: string | undefined;
79
- } | undefined;
80
- step?: {
81
- id: string;
82
- title: string;
83
- state: "backlog" | "canceled" | "completed" | "started" | "unstarted";
84
- owner: string;
85
- waitsOn?: string | undefined;
86
- ref?: string | undefined;
87
- } | undefined;
88
- note?: {
89
- kind: "adjustment" | "cleared" | "decision" | "finding" | "goal" | "outcome" | "problem" | "question" | "summary";
90
- text: string;
91
- key?: boolean | undefined;
92
- about?: string | undefined;
93
- } | undefined;
94
16
  }, {
95
17
  messageId: string;
96
18
  threadId: string;
@@ -103,39 +25,6 @@ declare const _default: import("eve/tools").ToolDefinition<{
103
25
  text: string;
104
26
  images?: string[] | undefined;
105
27
  policy?: "abort" | "queue" | "steer" | undefined;
106
- status?: "done" | "planning" | "reviewing" | "working" | undefined;
107
- metadata?: {
108
- pr?: string | undefined;
109
- sha?: string | undefined;
110
- } | undefined;
111
- verdict?: "fix-first" | "rethink" | "ship" | undefined;
112
- decisions?: {
113
- question: string;
114
- options: {
115
- label: string;
116
- pickedBy?: string | undefined;
117
- }[];
118
- }[] | undefined;
119
- tested?: string | undefined;
120
- artifact?: {
121
- kind: "decision" | "doc" | "plan" | "pr" | "review" | "sitrep" | "walkthrough";
122
- title: string;
123
- url?: string | undefined;
124
- } | undefined;
125
- step?: {
126
- id: string;
127
- title: string;
128
- state: "backlog" | "canceled" | "completed" | "started" | "unstarted";
129
- owner: string;
130
- waitsOn?: string | undefined;
131
- ref?: string | undefined;
132
- } | undefined;
133
- note?: {
134
- kind: "adjustment" | "cleared" | "decision" | "finding" | "goal" | "outcome" | "problem" | "question" | "summary";
135
- text: string;
136
- key?: boolean | undefined;
137
- about?: string | undefined;
138
- } | undefined;
139
28
  }, ctx: import("eve/tools").ToolContext): Promise<{
140
29
  messageId: string;
141
30
  threadId: string;
@@ -26,9 +26,10 @@ import { registryFetch } from "../registry-fetch.mjs";
26
26
  // waits for that bot's turn to end, steer folds into its running turn, abort
27
27
  // stops its turn and the text starts its next one.
28
28
  //
29
- // A status post: `status` is the thread's phase after it (the hub checks the
30
- // move), the text is its summary, `metadata` its PR and sha. The hub keeps the
31
- // latest one per bot and thread for the workspace state and the Dash.
29
+ // A thread post names its receiver: an @-mention of a bot, `@all` or the human.
30
+ // The hub checks it against the roster and the human's names. A phase, tracker
31
+ // row, note, artifact or verdict is the thread's state, not a message: it goes
32
+ // through update_thread.
32
33
  export const MAX_CHARS = 700;
33
34
  const SCREENSHOT = "screenshot";
34
35
  // The markdown parser finds the links and where each destination starts, so a path in code is never read as a
@@ -129,35 +130,6 @@ function decode(path) {
129
130
  return path;
130
131
  }
131
132
  }
132
- export const artifactInput = z.object({
133
- kind: z.enum(["walkthrough", "sitrep", "plan", "review", "pr", "decision", "doc"]).describe("What the artifact is."),
134
- title: z.string().min(1).max(80).describe("Names the thing, at most 80 characters, e.g. 'First-sync walk-through'. A newer post under the same kind and title is marked as the latest version; older versions stay listed."),
135
- });
136
- // The hub checks each row again (packages/wire/src/state.ts TrackerRow); this is what the model reads.
137
- export const trackerRow = z.object({
138
- id: z.string().describe("Your stable id for the row, e.g. 'review'. Send the same id again to move the row; the hub merges by id."),
139
- title: z.string().describe("What the work is, in a few words, e.g. 'Review the fix'."),
140
- 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."),
141
- owner: z.string().describe("Who acts on it: a bot name or the human's handle."),
142
- waitsOn: z.string().optional().describe("Who or what it is blocked on, e.g. 'rconti' or 'an eve maintainer'."),
143
- 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."),
144
- });
145
- // The hub checks the note again (packages/wire/src/state.ts); this is what the model reads.
146
- export const noteInput = z.object({
147
- kind: z.enum(["problem", "goal", "finding", "decision", "question", "cleared", "adjustment", "outcome", "summary"]).describe("What the note records: problem (the problem and its prior context), goal, finding (what research found), decision, question, cleared (a misconception corrected), adjustment (the plan changed, and why), outcome (how it worked out), summary (2 to 3 plain sentences: the goal, where it stands, what is next)."),
148
- text: z.string().min(1).max(200).describe("One sentence, 1 to 200 characters, plain words, no glyph separators. It stands alone: it names the thing and never says 'it', 'this' or 'the fix' for something in an earlier entry, because the newest entry is shown above the ones that led to it."),
149
- key: z.boolean().optional().describe("true keeps this entry open in the Artifacts pane; the others fold into 'N more steps'. Problem, goal and decision are always open. Set it when the human would lose the story if this entry were folded."),
150
- about: z.string().optional().describe("A `thr_ID#msg_ID` ref (a hash, never a slash), for a note that records someone else's post, e.g. the human's decision; the entry then opens that post."),
151
- });
152
- /** An artifact's link: the one given (a sandbox path uploaded), else the post's first link, else its PR. */
153
- export async function artifactUrl(given, text, pr, open) {
154
- if (given)
155
- return isSandboxPath(given) ? open(given) : given;
156
- const first = destinations(text).find((d) => d.kind !== "image")?.url ?? pr;
157
- if (!first)
158
- throw new Error("artifact needs a url: give artifact.url, link the document in the text, or set metadata.pr");
159
- return first;
160
- }
161
133
  async function screenshot(sessionId) {
162
134
  if (hostedHub())
163
135
  throw new Error("a browser screenshot needs the hub's machine: on a hosted hub save the image to a sandbox file and pass its path in `images`");
@@ -168,44 +140,18 @@ async function screenshot(sessionId) {
168
140
  return browser.screenshot();
169
141
  }
170
142
  export default defineTool({
171
- description: "Post a message as you. Leave out threadId and agent to post into the thread you're in, while you work: posting doesn't end your turn, and once your turn has posted into this thread, its end-of-turn text isn't posted, so put your result in a post. Or post into another workspace thread or a bot's DM. Each bubble is at most 700 characters (link targets don't count); a `---` line with blank lines around it starts a new bubble. Lead with the point in 2 sentences, then at most 3 bullets or one small table. A post that @-mentions no bot is one the human reads, and it is refused for a bullet that opens with a bold label and a colon, or an em dash. Depth goes in a document: write it to a file and link it in the text by its sandbox path, `[the plan](/workspace/plan.md)`; the file is uploaded and the link opens it beside the thread. Images go in `images` (sandbox paths; `screenshot` for your browser's page on a local hub) and show inline; never write `![]()`. @-mentions wake bots the same way your replies do (at most one, never yourself); `@all` wakes every participant; each bot's gate decides whether it replies. Posting into a bot's DM wakes that bot. Archived threads are refused. `policy` says how a post reaches the one bot it @-mentions: queue (default) runs as its next turn, or starts one now if it is idle; steer lands in its RUNNING turn at the next step boundary and the bot folds it in without a restart, to correct course mid-turn; abort stops its turn in that thread (running commands are killed, the process keeps running) and the text starts its fresh turn, \"stop, do this instead\". Steer and abort need exactly one @-mention, not `@all`, and cut into work already under way: use them only to change what a running turn is doing. A status post sets the thread's phase: planning, working, reviewing or done; allowed moves are start → planning or working, planning → working, working → reviewing, reviewing → working (fix first), planning (rethink) or done. Any other move is refused with the allowed next phases. The text is a phase marker, not a report: at most 8 plain words on what is happening now (\"Building the input-count fix\"), with no bot or role names, no restating the phase, no parentheticals. Test results, branch names and caveats go in a separate message. The PR URL and sha go in metadata, never in the text.",
143
+ description: "Post a message as you. Leave out threadId and agent to post into the thread you're in, while you work: posting doesn't end your turn, and once your turn has posted into this thread, its end-of-turn text isn't posted, so put your result in a post. Or post into another workspace thread or a bot's DM. Each bubble is at most 700 characters (link targets don't count); a `---` line with blank lines around it starts a new bubble. Lead with the point in 2 sentences, then at most 3 bullets or one small table. A thread post must @-mention its receiver: a bot, `@all` or the human; a post that only @-mentions the human is one the human reads, and it is refused for a bullet that opens with a bold label and a colon, or an em dash. Depth goes in a document: write it to a file and link it in the text by its sandbox path, `[the plan](/workspace/plan.md)`; the file is uploaded and the link opens it beside the thread. Images go in `images` (sandbox paths; `screenshot` for your browser's page on a local hub) and show inline; never write `![]()`. @-mentions wake bots the same way your replies do (at most one, never yourself); `@all` wakes every participant; each bot's gate decides whether it replies. Posting into a bot's DM wakes that bot. Archived threads are refused. Phase, tracker rows, notes, artifacts and verdicts are the thread's state: set them with update_thread, not a post. `policy` says how a post reaches the one bot it @-mentions: queue (default) runs as its next turn, or starts one now if it is idle; steer lands in its RUNNING turn at the next step boundary and the bot folds it in without a restart, to correct course mid-turn; abort stops its turn in that thread (running commands are killed, the process keeps running) and the text starts its fresh turn, \"stop, do this instead\". Steer and abort need exactly one @-mention, not `@all`, and cut into work already under way: use them only to change what a running turn is doing.",
172
144
  inputSchema: z.object({
173
145
  threadId: z.string().optional().describe("Another thread's id, e.g. thr_0002, or a DM id, dm-<bot>. Omit it to post into the thread you're in."),
174
146
  agent: z.string().optional().describe("Post into this bot's DM instead of a thread."),
175
- text: z.string().describe("The message, at most 700 characters per bubble (link targets don't count); a `---` line with blank lines around it starts a new bubble. A markdown link to a sandbox path uploads that document. @-mention the one bot who should act. On a status post: at most 8 plain words on what is happening now."),
147
+ text: z.string().describe("The message, at most 700 characters per bubble (link targets don't count); a `---` line with blank lines around it starts a new bubble. A markdown link to a sandbox path uploads that document. @-mention the one bot, `@all` or the human who should act."),
176
148
  images: z.array(z.string()).optional().describe("Images to show inline on this post: sandbox paths (PNG, JPEG, GIF or WebP, up to 3 MB each), or `screenshot` for your browser's current page (local hub only)."),
177
149
  policy: z.enum(["queue", "steer", "abort"]).optional().describe("How the post reaches the one bot it @-mentions: queue (default), steer into its running turn, or abort its turn and start a fresh one on this text."),
178
- status: z.enum(["planning", "working", "reviewing", "done"]).optional().describe("The thread's phase after this post. Makes it a status post."),
179
- metadata: z
180
- .object({
181
- pr: z.string().optional().describe("The PR's full URL, e.g. https://github.com/o/r/pull/188."),
182
- sha: z.string().optional().describe("The head (or merge) commit sha, 7–40 hex characters."),
183
- })
184
- .optional()
185
- .describe("With status: the PR and sha it's about."),
186
- verdict: z.enum(["ship", "fix-first", "rethink"]).optional().describe("A reviewer's verdict on metadata.pr at metadata.sha (needs both, and a status). A ship needs decisions."),
187
- decisions: z
188
- .array(z.object({ question: z.string(), options: z.array(z.object({ label: z.string(), pickedBy: z.string().optional().describe("The bot that picked this option.") })) }))
189
- .optional()
190
- .describe("With a verdict: what the human has to pick. [] when nothing; required with ship."),
191
- tested: z.string().optional().describe("With a verdict: what you ran to check it, in a sentence (shown on the Dash row)."),
192
- artifact: artifactInput
193
- .extend({ url: z.string().optional().describe("What a tap opens: a sandbox path (uploaded), hub://upload link or https URL. Default: the post's first link, else metadata.pr.") })
194
- .optional()
195
- .describe("Tags this post as one of the thread's artifacts, shown in the thread's Artifacts pane: a walk-through, sitrep, plan, review, PR, decision or doc. The pane lists every version and marks the newest per kind and title as latest, so post a new version under the same title to supersede the old one."),
196
- step: trackerRow
197
- .extend({ ref: z.string().optional().describe("What the row opens. Default: this post.") })
198
- .optional()
199
- .describe("The thread's tracker row this post marks, written with the post: e.g. on a status post handing a PR to review, {id: 'review', title: 'Review the fix', state: 'started', owner: 'reviewer'}. The hub merges it by id, so the same id on a later post moves the same row, and stamps the time and your name when its state, owner or wait changes. Optional."),
200
- note: noteInput.optional().describe("One entry in the thread's timeline of notes, the Artifacts pane's list, newest first: {kind, text, key?, about?}. The hub refuses a post that has a `verdict`, `status: \"done\"`, or an artifact of kind `plan` and no `note`."),
201
150
  }),
202
- async execute({ threadId, agent, text, images, policy, status, metadata, verdict, decisions, tested, artifact, step, note }, ctx) {
151
+ async execute({ threadId, agent, text, images, policy }, ctx) {
203
152
  const bad = textError(text);
204
153
  if (bad)
205
154
  throw new Error(bad);
206
- const noteVoice = note && voiceError(note.text);
207
- if (noteVoice)
208
- throw new Error(`note.text: ${noteVoice}`);
209
155
  const voice = voiceError(text);
210
156
  if (voice && !(await mentionsBot(text)))
211
157
  throw new Error(voice);
@@ -213,12 +159,11 @@ export default defineTool({
213
159
  let sandbox;
214
160
  const files = async () => (sandbox ??= await ctx.getSandbox());
215
161
  const posted = await rewriteDocLinks(text, async (path) => documentUrl(await files(), path, "post_message"));
216
- const tagged = artifact && { ...artifact, url: await artifactUrl(artifact.url, posted, metadata?.pr, async (path) => documentUrl(await files(), path, "post_message")) };
217
162
  const attachments = await Promise.all((images ?? []).map(async (image) => image === SCREENSHOT ? uploadBytes(await screenshot(ctx.session.id), "screenshot.png", "post_message") : uploadFile(await files(), image, "post_message")));
218
163
  const res = await registryFetch(`/api/threads/${encodeURIComponent(target)}/messages`, {
219
164
  method: "POST",
220
165
  headers: { "content-type": "application/json" },
221
- body: JSON.stringify({ text: posted, sessionId: ctx.session.id, ...(attachments.length ? { attachments: attachments.map(({ id, name }) => ({ id, name })) } : {}), ...(policy ? { policy } : {}), ...(status ? { status } : {}), ...(metadata ? { metadata } : {}), ...(verdict ? { verdict } : {}), ...(decisions ? { decisions } : {}), ...(tested ? { tested } : {}), ...(tagged ? { artifact: tagged } : {}), ...(step ? { step } : {}), ...(note ? { note } : {}) }),
166
+ body: JSON.stringify({ text: posted, sessionId: ctx.session.id, ...(attachments.length ? { attachments: attachments.map(({ id, name }) => ({ id, name })) } : {}), ...(policy ? { policy } : {}) }),
222
167
  });
223
168
  if (!res.ok)
224
169
  throw new Error(`post_message failed: ${res.status} ${await res.text()}`);
@@ -14,6 +14,29 @@ declare const _default: import("eve/tools").ToolDefinition<{
14
14
  pathLabel?: string | undefined;
15
15
  projectLabel?: string | undefined;
16
16
  issueTypeLabel?: string | undefined;
17
+ status?: {
18
+ phase: "done" | "planning" | "reviewing" | "working";
19
+ text: string;
20
+ } | undefined;
21
+ metadata?: {
22
+ pr?: string | undefined;
23
+ sha?: string | undefined;
24
+ } | undefined;
25
+ verdict?: "fix-first" | "rethink" | "ship" | undefined;
26
+ decisions?: {
27
+ question: string;
28
+ options: {
29
+ label: string;
30
+ pickedBy?: string | undefined;
31
+ }[];
32
+ }[] | undefined;
33
+ tested?: string | undefined;
34
+ note?: {
35
+ kind: "adjustment" | "cleared" | "decision" | "finding" | "goal" | "outcome" | "problem" | "question" | "summary";
36
+ text: string;
37
+ key?: boolean | undefined;
38
+ about?: string | undefined;
39
+ } | undefined;
17
40
  artifacts?: {
18
41
  kind: "decision" | "doc" | "plan" | "pr" | "review" | "sitrep" | "walkthrough";
19
42
  title: string;
@@ -36,6 +59,29 @@ declare const _default: import("eve/tools").ToolDefinition<{
36
59
  pathLabel?: string | undefined;
37
60
  projectLabel?: string | undefined;
38
61
  issueTypeLabel?: string | undefined;
62
+ status?: {
63
+ phase: "done" | "planning" | "reviewing" | "working";
64
+ text: string;
65
+ } | undefined;
66
+ metadata?: {
67
+ pr?: string | undefined;
68
+ sha?: string | undefined;
69
+ } | undefined;
70
+ verdict?: "fix-first" | "rethink" | "ship" | undefined;
71
+ decisions?: {
72
+ question: string;
73
+ options: {
74
+ label: string;
75
+ pickedBy?: string | undefined;
76
+ }[];
77
+ }[] | undefined;
78
+ tested?: string | undefined;
79
+ note?: {
80
+ kind: "adjustment" | "cleared" | "decision" | "finding" | "goal" | "outcome" | "problem" | "question" | "summary";
81
+ text: string;
82
+ key?: boolean | undefined;
83
+ about?: string | undefined;
84
+ } | undefined;
39
85
  artifacts?: {
40
86
  kind: "decision" | "doc" | "plan" | "pr" | "review" | "sitrep" | "walkthrough";
41
87
  title: string;
@@ -4,14 +4,37 @@
4
4
  // marker; tags go on thread.tags (lib/tags.mjs). The thread's summary is the
5
5
  // hub's, derived from its posts (hosted-hub/src/shims/index.ts).
6
6
  // `artifacts` adds Artifacts pane entries with no post: the hub writes one
7
- // silent event row each.
7
+ // silent event row each. `status`, `verdict`, `note` and `metadata` are the
8
+ // thread's state (its phase, a reviewer's verdict on a PR, a timeline note): the
9
+ // hub writes them as one grey phase row authored by the calling bot, with no
10
+ // @-mention, so it wakes and notifies no one. A message to someone is post_message.
8
11
  import { defineTool } from "eve/tools";
9
12
  import { z } from "zod";
10
13
  import { registryFetch } from "../registry-fetch.mjs";
11
- import { documentUrl } from "../hub-upload.js";
12
- import { artifactInput, artifactUrl, trackerRow } from "./post_message.js";
14
+ import { documentUrl, isSandboxPath } from "../hub-upload.js";
15
+ import { voiceError } from "./post_message.js";
16
+ const artifactInput = z.object({
17
+ kind: z.enum(["walkthrough", "sitrep", "plan", "review", "pr", "decision", "doc"]).describe("What the artifact is."),
18
+ title: z.string().min(1).max(80).describe("Names the thing, at most 80 characters, e.g. 'First-sync walk-through'. A newer entry under the same kind and title is marked as the latest version; older versions stay listed."),
19
+ });
20
+ // The hub checks each row again (packages/wire/src/state.ts TrackerRow); this is what the model reads.
21
+ const trackerRow = z.object({
22
+ id: z.string().describe("Your stable id for the row, e.g. 'review'. Send the same id again to move the row; the hub merges by id."),
23
+ title: z.string().describe("What the work is, in a few words, e.g. 'Review the fix'."),
24
+ 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."),
25
+ owner: z.string().describe("Who acts on it: a bot name or the human's handle."),
26
+ waitsOn: z.string().optional().describe("Who or what it is blocked on, e.g. 'rconti' or 'an eve maintainer'."),
27
+ 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."),
28
+ });
29
+ // The hub checks the note again (packages/wire/src/state.ts); this is what the model reads.
30
+ const noteInput = z.object({
31
+ kind: z.enum(["problem", "goal", "finding", "decision", "question", "cleared", "adjustment", "outcome", "summary"]).describe("What the note records: problem (the problem and its prior context), goal, finding (what research found), decision, question, cleared (a misconception corrected), adjustment (the plan changed, and why), outcome (how it worked out), summary (2 to 3 plain sentences: the goal, where it stands, what is next)."),
32
+ text: z.string().min(1).max(200).describe("One sentence, 1 to 200 characters, plain words, no glyph separators. It stands alone: it names the thing and never says 'it', 'this' or 'the fix' for something in an earlier entry, because the newest entry is shown above the ones that led to it."),
33
+ key: z.boolean().optional().describe("true keeps this entry open in the Artifacts pane; the others fold into 'N more steps'. Problem, goal and decision are always open. Set it when the human would lose the story if this entry were folded."),
34
+ about: z.string().optional().describe("A `thr_ID#msg_ID` ref (a hash, never a slash), for a note that records someone else's post, e.g. the human's decision; the entry then opens that post."),
35
+ });
13
36
  export default defineTool({
14
- 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.",
37
+ description: "Update a workspace thread you're in: its phase, tracker, notes, artifacts, title, track and issue type, in one call. This is where a thread's state goes; post_message is only for messages to someone. `status` sets the phase: planning, working, reviewing or done; allowed moves are start → planning or working, planning → working, working → reviewing, reviewing → working (fix first), planning (rethink) or done; any other move is refused with the allowed next phases. Its `text` is a phase marker, not a report: at most 8 plain words on what is happening now (\"Building the input-count fix\"), with no @-mention, no bot or role names, no restating the phase. Test results, branch names and caveats go in a post. The PR URL and sha go in `metadata`. A reviewer's `verdict` (with `decisions` and `tested`) is on `metadata.pr` at `metadata.sha`. A `note` and a verdict ride on the status row, so send them with a `status` (the current phase to stay in it). The hub refuses a `verdict`, a `done` status or a `plan` artifact with no `note`. The state wakes no one: to hand work to someone, post_message and @-mention them. 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
38
  inputSchema: z.object({
16
39
  threadId: z.string().describe("Thread id, e.g. thr_0002 (shown in the [thread: …] header)."),
17
40
  title: z.string().optional().describe("New short title, e.g. 'Checkout: retry failed payments'. Omit to leave it."),
@@ -21,21 +44,39 @@ export default defineTool({
21
44
  .array(trackerRow)
22
45
  .max(50)
23
46
  .optional()
24
- .describe("Tracker rows to add or change: the thread's pieces of work toward its goal, one row each, drawn as the Artifacts pane's Done (completed), Pending now (started, unstarted), Pending until done (backlog) and Dropped (canceled) lists. The hub merges them by id: a row with a known id is replaced, a new id is added, and rows you don't send stay as they are, so send only the rows that changed. To drop a row, send it as canceled. The hub stamps the time and your name on each row whose state, owner or wait changes. For a step that has its own post, prefer post_message's `step`. A row whose ref is a PR moves to Done or Dropped by itself when the PR merges or closes."),
47
+ .describe("Tracker rows to add or change: the thread's pieces of work toward its goal, one row each, drawn as the Artifacts pane's Done (completed), Pending now (started, unstarted), Pending until done (backlog) and Dropped (canceled) lists. The hub merges them by id: a row with a known id is replaced, a new id is added, and rows you don't send stay as they are, so send only the rows that changed. To drop a row, send it as canceled. The hub stamps the time and your name on each row whose 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."),
25
48
  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."),
26
49
  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."),
27
50
  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."),
51
+ status: z.object({ phase: z.enum(["planning", "working", "reviewing", "done"]), text: z.string().describe("The phase marker: at most 8 plain words on what is happening now, no @-mention.") }).optional().describe("The thread's phase after this call, shown as a grey phase row. Wakes no one."),
52
+ metadata: z
53
+ .object({
54
+ pr: z.string().optional().describe("The PR's full URL, e.g. https://github.com/o/r/pull/188."),
55
+ sha: z.string().optional().describe("The head (or merge) commit sha, 7–40 hex characters."),
56
+ })
57
+ .optional()
58
+ .describe("With status: the PR and sha it's about."),
59
+ verdict: z.enum(["ship", "fix-first", "rethink"]).optional().describe("A reviewer's verdict on metadata.pr at metadata.sha (needs both, and a status). A ship needs decisions."),
60
+ decisions: z
61
+ .array(z.object({ question: z.string(), options: z.array(z.object({ label: z.string(), pickedBy: z.string().optional().describe("The bot that picked this option.") })) }))
62
+ .optional()
63
+ .describe("With a verdict: what the human has to pick. [] when nothing; required with ship."),
64
+ tested: z.string().optional().describe("With a verdict: what you ran to check it, in a sentence (shown on the Dash row)."),
65
+ note: noteInput.optional().describe("One entry in the thread's timeline of notes, the Artifacts pane's list, newest first: {kind, text, key?, about?}. It rides on the status row, so send it with a `status`. The hub refuses a `verdict`, a done status, or a plan artifact with no `note`."),
28
66
  artifacts: z
29
67
  .array(artifactInput.extend({ url: z.string().describe("What a tap opens: a sandbox path (uploaded), hub://upload link or https URL.") }))
30
68
  .min(1)
31
69
  .max(10)
32
70
  .optional()
33
- .describe("Adds artifacts to the thread's Artifacts pane without posting a message; each shows as a silent line in the thread that wakes nobody. When the artifact comes with a post, use post_message's `artifact` instead."),
71
+ .describe("Adds artifacts to the thread's Artifacts pane without posting a message; each shows as a silent line in the thread that wakes nobody."),
34
72
  }),
35
73
  async execute({ threadId, artifacts, ...fields }, ctx) {
74
+ const noteVoice = fields.note && voiceError(fields.note.text);
75
+ if (noteVoice)
76
+ throw new Error(`note.text: ${noteVoice}`);
36
77
  let sandbox;
37
78
  const open = async (path) => documentUrl((sandbox ??= await ctx.getSandbox()), path, "update_thread");
38
- const added = artifacts && (await Promise.all(artifacts.map(async (a) => ({ ...a, url: await artifactUrl(a.url, "", undefined, open) }))));
79
+ const added = artifacts && (await Promise.all(artifacts.map(async (a) => ({ ...a, url: isSandboxPath(a.url) ? await open(a.url) : a.url }))));
39
80
  const res = await registryFetch(`/api/threads/${encodeURIComponent(threadId)}/rename`, {
40
81
  method: "POST",
41
82
  headers: { "content-type": "application/json" },