passalong 0.20.0 → 1.0.0
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/README.md +62 -72
- package/bin/passalong +217 -378
- package/package.json +4 -4
- package/skill/SKILL.md +55 -167
- package/src/api.js +100 -190
- package/src/capture.js +15 -51
- package/src/checks.js +5 -5
- package/src/hooks.js +9 -18
- package/src/mcp.js +691 -1484
- package/src/passalong.js +365 -702
- package/src/store.js +18 -28
- package/src/work.js +416 -0
- package/src/guide.js +0 -811
package/src/mcp.js
CHANGED
|
@@ -1,33 +1,22 @@
|
|
|
1
|
-
// The Passalong MCP server (stdio).
|
|
2
|
-
//
|
|
3
|
-
// team, the same tools see the team's guides and the user's inbox.
|
|
1
|
+
// The Passalong MCP server (stdio). Any MCP-capable agent can take work, hand it in, write work
|
|
2
|
+
// down, and read and write a project's documents without leaving its session.
|
|
4
3
|
//
|
|
5
4
|
// claude mcp add passalong -- passalong mcp
|
|
5
|
+
//
|
|
6
|
+
// Seventeen tools, four nouns: work, project, document, file (docs/WORK.md). The same tools are
|
|
7
|
+
// served over HTTP by apps/api/src/mcp-http.ts; the two say the same things in the same words.
|
|
6
8
|
import { readFileSync } from "node:fs";
|
|
7
9
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
10
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9
11
|
import { z } from "zod";
|
|
10
12
|
import * as api from "./api.js";
|
|
11
13
|
import { refusal, runChecks } from "./checks.js";
|
|
12
|
-
import {
|
|
13
|
-
AREAS,
|
|
14
|
-
BUG_SECTIONS,
|
|
15
|
-
GUIDE_WARN,
|
|
16
|
-
outlineText,
|
|
17
|
-
parse,
|
|
18
|
-
SECTIONS,
|
|
19
|
-
section as sectionOf,
|
|
20
|
-
sizeWarning,
|
|
21
|
-
TASK_SECTIONS,
|
|
22
|
-
template,
|
|
23
|
-
} from "./guide.js";
|
|
24
14
|
import * as passalong from "./passalong.js";
|
|
15
|
+
import { outlineText, parse, section as sectionOf, serialize, WORK_WARN } from "./work.js";
|
|
25
16
|
|
|
26
17
|
/**
|
|
27
18
|
* What the server calls itself in the MCP handshake — read from package.json rather than written
|
|
28
|
-
* here, because a literal drifts.
|
|
29
|
-
* 0.2.2, and the one thing a version string in a handshake is for is telling a client which build
|
|
30
|
-
* it is talking to. `pnpm release` bumps package.json, so there is only one number to bump.
|
|
19
|
+
* here, because a literal drifts. `pnpm release` bumps package.json, so there is one number.
|
|
31
20
|
*/
|
|
32
21
|
const VERSION = JSON.parse(
|
|
33
22
|
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
|
@@ -36,42 +25,36 @@ const VERSION = JSON.parse(
|
|
|
36
25
|
const text = (s) => ({ content: [{ type: "text", text: s }] });
|
|
37
26
|
|
|
38
27
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
28
|
+
* Where the line `add_file` returns goes, which is not always new work. Proof of a hand-in belongs
|
|
29
|
+
* on the hand-in: six screenshots of a delivered change were once published as a document with six
|
|
30
|
+
* sections wrapped round them, because the upload tool named only one destination.
|
|
31
|
+
* Mirrors WHERE_THE_LINE_GOES in apps/api/src/mcp-http.ts.
|
|
42
32
|
*/
|
|
43
33
|
const WHERE_THE_LINE_GOES =
|
|
44
|
-
"WHERE THE LINE GOES DEPENDS ON WHAT THE
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"a
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
" a
|
|
53
|
-
"o make them publishable is how a set of pictures becomes fifteen hundred words nobody asked for" +
|
|
54
|
-
". png, jpg, webp or gif.";
|
|
34
|
+
"WHERE THE LINE GOES DEPENDS ON WHAT THE FILE IS EVIDENCE OF. Proof that work you are handing in " +
|
|
35
|
+
"holds — a screen that renders right, a total that matches — goes in that check's `ran` on " +
|
|
36
|
+
"hand_in, and nothing new is written: a check answered with a picture is shown, which is what " +
|
|
37
|
+
"the hand-in asks for. A picture that is part of the work itself — the screenshot of a bug, a " +
|
|
38
|
+
"diagram it explains — goes in its text (create_work, or update_work), because work travels as " +
|
|
39
|
+
"its text and a file beside it does not travel at all. DO NOT WRITE NEW WORK TO CARRY " +
|
|
40
|
+
"SCREENSHOTS: six images and a caption each is a hand-in. With `project`, the file is kept in " +
|
|
41
|
+
"that project instead. Pictures (png, jpg, webp, gif) are drawn where they are named; anything " +
|
|
42
|
+
"else is a file to download.";
|
|
55
43
|
|
|
56
44
|
/**
|
|
57
|
-
* What a screenshot has to be a screenshot OF.
|
|
58
|
-
*
|
|
59
|
-
* "A check is run or shown" made a picture mandatory, and an agent short of one built a page to
|
|
60
|
-
* photograph: it added `src/app/notes-preview`, shot that, then `rm -rf`'d it and published the
|
|
61
|
-
* shots as evidence that the real screen rendered. The rule was satisfied and almost nothing was
|
|
62
|
-
* proved — the measure had become the target, and the page the picture showed no longer existed.
|
|
63
|
-
*
|
|
45
|
+
* What a screenshot has to be a screenshot OF. An agent short of a picture once built a page to
|
|
46
|
+
* photograph, shot it, deleted it, and offered the shots as evidence the real screen rendered.
|
|
64
47
|
* Mirrors SHOOT_THE_REAL_THING in apps/api/src/mcp-http.ts.
|
|
65
48
|
*/
|
|
66
49
|
const SHOOT_THE_REAL_THING =
|
|
67
50
|
"SHOOT THE RUNNING THING, NOT SOMETHING YOU BUILT TO SHOOT. A screenshot is worth what the sc" +
|
|
68
51
|
"reen behind it is worth: the real route in the real app, reached the way a reader would reac" +
|
|
69
52
|
"h it. A scratch page, a story, a harness or a component rendered on its own is a picture of " +
|
|
70
|
-
"your scaffolding, and it can look right while the thing the
|
|
53
|
+
"your scaffolding, and it can look right while the thing the work is about does not. If the " +
|
|
71
54
|
"only way you could get a picture was to build something, that is not evidence the real scree" +
|
|
72
55
|
"n works — say what you built and why in `writeup`, and leave the check to the commands that " +
|
|
73
56
|
"do hold. NEVER DELETE WHAT YOUR EVIDENCE POINTS AT: a shot of a page you removed afterwards " +
|
|
74
|
-
"is one nobody can take again, including you
|
|
57
|
+
"is one nobody can take again, including you.\n\n";
|
|
75
58
|
|
|
76
59
|
const KEEP_EVIDENCE =
|
|
77
60
|
"KEEP YOUR EVIDENCE AS YOU GO. hand_in needs it: the commands you ran and what came back, the " +
|
|
@@ -79,15 +62,9 @@ const KEEP_EVIDENCE =
|
|
|
79
62
|
"writing from memory, which is the thing evidence is here to replace.\n\n";
|
|
80
63
|
|
|
81
64
|
/**
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* and concludes from that. It then reports that a thing is absent, or that it found the only
|
|
86
|
-
* occurrence, having never looked past the place it happened to start. The conclusion is confident
|
|
87
|
-
* and wrong, and nothing downstream can tell, because the evidence it brings is real — it is just
|
|
88
|
-
* evidence of a search that was too small.
|
|
89
|
-
*
|
|
90
|
-
* Mirrors SEARCH_WIDE in apps/api/src/mcp-http.ts.
|
|
65
|
+
* The failure this stops: an agent lists one directory, or opens the one file it expected, and
|
|
66
|
+
* reports that a thing is absent, or that it found the only occurrence. Confident and wrong, and
|
|
67
|
+
* nothing downstream can tell. Mirrors SEARCH_WIDE in apps/api/src/mcp-http.ts.
|
|
91
68
|
*/
|
|
92
69
|
const SEARCH_WIDE =
|
|
93
70
|
"SEARCH THE WHOLE TREE BEFORE YOU CONCLUDE. Listing a directory, or reading the one file you " +
|
|
@@ -96,77 +73,12 @@ const SEARCH_WIDE =
|
|
|
96
73
|
'already handled, search across the repository and let the output be your reason. "I looked" ' +
|
|
97
74
|
"is a claim; the search and what it printed is evidence.\n\n";
|
|
98
75
|
|
|
99
|
-
/**
|
|
100
|
-
|
|
101
|
-
*
|
|
102
|
-
* The heading text is what an agent keys on, and a bug's or a task's headings are close enough to
|
|
103
|
-
* a transfer guide's to be treated as one by an agent that never opened the frontmatter. So the
|
|
104
|
-
* kind is stated before the document, in the imperative. A transfer guide needs nothing: following
|
|
105
|
-
* it is what an agent does with an unmarked document anyway.
|
|
106
|
-
*/
|
|
107
|
-
function leadFor(meta) {
|
|
108
|
-
if (meta.kind === "bug")
|
|
109
|
-
return (
|
|
110
|
-
"THIS IS A BUG REPORT, NOT WORK TO REPEAT. Do not follow Reproduce as instructions " +
|
|
111
|
-
"— those steps produce the defect. Fix what Problem describes, then check " +
|
|
112
|
-
"Verification and answer with hand_in.\n\n" +
|
|
113
|
-
SEARCH_WIDE +
|
|
114
|
-
SHOOT_THE_REAL_THING +
|
|
115
|
-
KEEP_EVIDENCE
|
|
116
|
-
);
|
|
117
|
-
if (meta.kind === "task")
|
|
118
|
-
return (
|
|
119
|
-
"THIS IS A TASK: WORK NOBODY HAS DONE YET. There are no Steps to follow — work out how to " +
|
|
120
|
-
"reach Goal within Constraints, and leave Out of scope alone. It is done when every check " +
|
|
121
|
-
"under Acceptance holds. Opening it here does not make it yours: to work on it, call take " +
|
|
122
|
-
"with its id in the repo it is for, so no other agent can. Then progress, and hand_in " +
|
|
123
|
-
"with a write-up.\n\n" +
|
|
124
|
-
SEARCH_WIDE +
|
|
125
|
-
SHOOT_THE_REAL_THING +
|
|
126
|
-
KEEP_EVIDENCE
|
|
127
|
-
);
|
|
128
|
-
// A transfer guide is handed over untouched: it is a document to follow, and anything in front
|
|
129
|
-
// of it is one more thing that is not the document. The reminder still reaches the agent on the
|
|
130
|
-
// answer's `next` line, which is where every other instruction from the server rides.
|
|
131
|
-
return "";
|
|
132
|
-
}
|
|
76
|
+
/** Said in front of every piece of work an agent takes. Mirrors LEAD in apps/api/src/mcp-http.ts. */
|
|
77
|
+
const LEAD = SEARCH_WIDE + SHOOT_THE_REAL_THING + KEEP_EVIDENCE;
|
|
133
78
|
|
|
134
79
|
/**
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
* A QA pass files six bugs at once, and they arrive in an inbox as six unrelated guides — which
|
|
138
|
-
* is how the same root cause gets fixed three times, or how someone fixes one of a pair and calls
|
|
139
|
-
* the sweep done. Naming the siblings costs one request and only happens for a guide that has a
|
|
140
|
-
* parent. A failure here is not worth failing the pull over: the issue itself is what was asked
|
|
141
|
-
* for, and it is already in hand.
|
|
142
|
-
*/
|
|
143
|
-
async function related(meta) {
|
|
144
|
-
if (!meta.report) return "";
|
|
145
|
-
try {
|
|
146
|
-
const { report } = await api.report(meta.report);
|
|
147
|
-
const others = (report.areas || [])
|
|
148
|
-
.flatMap((a) => a.issues.map((i) => ({ ...i, area: a.area })))
|
|
149
|
-
.filter((i) => i.id !== meta.id);
|
|
150
|
-
if (!others.length) return "";
|
|
151
|
-
const lines = others.map(
|
|
152
|
-
(i) => ` ${i.id} [${i.severity || "--"}] ${i.area || "no area"} — ${i.title}`,
|
|
153
|
-
);
|
|
154
|
-
return (
|
|
155
|
-
`\n\n<!-- passalong: filed with ${others.length} other issue${others.length === 1 ? "" : "s"}` +
|
|
156
|
-
`${report.title ? ` in "${report.title}"` : ""}. Fix only this one unless asked; the others are\n` +
|
|
157
|
-
`${lines.join("\n")}\n-->`
|
|
158
|
-
);
|
|
159
|
-
} catch {
|
|
160
|
-
return "";
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* An answer a client can read as data as well as prose.
|
|
165
|
-
*
|
|
166
|
-
* `structuredContent` is the same JSON the text block holds; the spec asks for both, because a
|
|
167
|
-
* tool "that returns structured content SHOULD also return the serialized JSON in a TextContent
|
|
168
|
-
* block" for clients that only read text. Only an object goes in the structured field — the spec
|
|
169
|
-
* allows any JSON value there, but a bare array or string tells a client nothing it could key on.
|
|
80
|
+
* An answer a client can read as data as well as prose. The spec asks for both: structured content
|
|
81
|
+
* "SHOULD also return the serialized JSON in a TextContent block" for clients that only read text.
|
|
170
82
|
*/
|
|
171
83
|
const json = (data) => ({
|
|
172
84
|
...text(JSON.stringify(data, null, 2)),
|
|
@@ -174,52 +86,25 @@ const json = (data) => ({
|
|
|
174
86
|
});
|
|
175
87
|
|
|
176
88
|
/**
|
|
177
|
-
* What
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
* reaches outside — and a client acts on them: over HTTP, ChatGPT badged every tool here, `inbox`
|
|
183
|
-
* included, as a destructive public write. The spec is equally clear that these are hints and
|
|
184
|
-
* nothing more: "clients MUST consider tool annotations to be untrusted unless they come from
|
|
185
|
-
* trusted servers". They are a description, not a permission.
|
|
186
|
-
*
|
|
187
|
-
* `openWorldHint` is false for everything that stays inside Passalong. Only attach_screenshot
|
|
188
|
-
* reaches outside it, for a file on this machine that Passalong did not put there.
|
|
189
|
-
*/
|
|
190
|
-
/**
|
|
191
|
-
* What the three verbs that answer with JSON return. Declared only for those: `outputSchema` puts
|
|
192
|
-
* the obligation on the server — "Servers MUST provide structured results that conform to this
|
|
193
|
-
* schema", while a client only SHOULD check — so it is a promise, and promising a shape this
|
|
194
|
-
* server does not control would be worse than staying quiet. `take` is the reason for the rule
|
|
195
|
-
* rather than an exception to it: it answers with the guide's markdown and a lead-in to read, not
|
|
196
|
-
* with a record.
|
|
197
|
-
*
|
|
198
|
-
* Every field is optional and the objects are open. The four calls serve three kinds of guide and
|
|
199
|
-
* the routes add fields freely, so a required key here would be a promise broken by a kind that
|
|
200
|
-
* does not carry it.
|
|
89
|
+
* What the four calls that answer with a record return. Declared only for those: `outputSchema`
|
|
90
|
+
* puts a MUST on the server, so it is a promise, and promising a shape this server does not
|
|
91
|
+
* control would be worse than staying quiet. Every field is optional and the objects are open —
|
|
92
|
+
* the object itself goes to registerTool, never its `.shape`, which is rebuilt closed and
|
|
93
|
+
* advertised as `additionalProperties: false`.
|
|
201
94
|
*/
|
|
202
|
-
// The object itself is handed to registerTool, not its `.shape`: a shape is rebuilt into a closed
|
|
203
|
-
// object and advertised as `additionalProperties: false`, which would be this server promising that
|
|
204
|
-
// an answer carries nothing else — and then breaking that promise the first time a route adds a
|
|
205
|
-
// field. Passing the object through keeps it open.
|
|
206
95
|
const openObject = (shape) => z.object(shape).partial().passthrough();
|
|
207
|
-
const progressOut = openObject({
|
|
208
|
-
|
|
209
|
-
lease_until: z.string(),
|
|
210
|
-
note: z.string(),
|
|
211
|
-
});
|
|
212
|
-
const handInOut = openObject({
|
|
213
|
-
id: z.string(),
|
|
214
|
-
state: z.string(),
|
|
215
|
-
report: z.string(),
|
|
216
|
-
ok: z.boolean(),
|
|
217
|
-
});
|
|
96
|
+
const progressOut = openObject({ id: z.string(), lease_until: z.string(), note: z.string() });
|
|
97
|
+
const handInOut = openObject({ id: z.string(), state: z.string() });
|
|
218
98
|
const passOut = openObject({ id: z.string(), passed: z.boolean() });
|
|
99
|
+
const askOut = openObject({ id: z.string(), lease_until: z.string() });
|
|
219
100
|
|
|
101
|
+
/**
|
|
102
|
+
* What each tool does to the world, said out loud. MCP's defaults for a tool that says nothing are
|
|
103
|
+
* the worst case, and clients act on them. These are hints, not permissions.
|
|
104
|
+
*/
|
|
220
105
|
const READS = { readOnlyHint: true, destructiveHint: false, openWorldHint: false };
|
|
221
106
|
const ADDS = { readOnlyHint: false, destructiveHint: false, openWorldHint: false };
|
|
222
|
-
// Removes something that cannot be brought back. Only a person's request should lead here.
|
|
107
|
+
// Removes or replaces something that cannot be brought back. Only a person's request should lead here.
|
|
223
108
|
const REMOVES = { readOnlyHint: false, destructiveHint: true, openWorldHint: false };
|
|
224
109
|
const fail = (err) => ({ content: [{ type: "text", text: err.message }], isError: true });
|
|
225
110
|
|
|
@@ -231,8 +116,7 @@ export function nextNote({ next = [], say = "" } = {}, id = "") {
|
|
|
231
116
|
const lines = next.map(
|
|
232
117
|
(s) =>
|
|
233
118
|
` ${s.tool}${id && s.tool !== "take" ? ` ${id}` : ""} — when ${s.when} (${s.why})` +
|
|
234
|
-
// What the call has to carry, under the call it belongs to
|
|
235
|
-
// end of a long session should not have to remember hand_in takes evidence.
|
|
119
|
+
// What the call has to carry, under the call it belongs to.
|
|
236
120
|
(s.with ? `\n with ${s.with}` : ""),
|
|
237
121
|
);
|
|
238
122
|
if (say) lines.unshift(` ${say}`);
|
|
@@ -241,8 +125,7 @@ export function nextNote({ next = [], say = "" } = {}, id = "") {
|
|
|
241
125
|
|
|
242
126
|
/**
|
|
243
127
|
* What the person said to this agent since it last heard, in front of everything else: a reply is
|
|
244
|
-
* the thing the agent stopped for
|
|
245
|
-
* missed. See claims.deliver() in apps/api/src/claims.ts.
|
|
128
|
+
* the thing the agent stopped for. See claims.deliver() in apps/api/src/claims.ts.
|
|
246
129
|
*/
|
|
247
130
|
export function repliesNote(replies = []) {
|
|
248
131
|
if (!Array.isArray(replies) || !replies.length) return "";
|
|
@@ -263,6 +146,22 @@ const failWith = (err) => {
|
|
|
263
146
|
};
|
|
264
147
|
};
|
|
265
148
|
|
|
149
|
+
/** One call, its answer as JSON, its refusal as an error. */
|
|
150
|
+
const run = (fn) => async (args) => {
|
|
151
|
+
try {
|
|
152
|
+
return json(await fn(args));
|
|
153
|
+
} catch (err) {
|
|
154
|
+
return fail(err);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const CWD = z
|
|
159
|
+
.string()
|
|
160
|
+
.optional()
|
|
161
|
+
.describe("the worktree you are working in; default is the server's cwd");
|
|
162
|
+
|
|
163
|
+
const STATUS = { ready: "ready", draft: "draft", archived: "done" };
|
|
164
|
+
|
|
266
165
|
/**
|
|
267
166
|
* The server, built but not connected. Split out so the tool surface can be read without a
|
|
268
167
|
* transport — `test/mcp-surface.test.js` lists the tools over an in-memory pair and checks what
|
|
@@ -273,143 +172,75 @@ export function buildServer() {
|
|
|
273
172
|
{ name: "passalong", version: VERSION },
|
|
274
173
|
{
|
|
275
174
|
instructions:
|
|
276
|
-
"Passalong
|
|
277
|
-
"
|
|
278
|
-
"for
|
|
279
|
-
"
|
|
280
|
-
"
|
|
281
|
-
"
|
|
282
|
-
"
|
|
283
|
-
"
|
|
284
|
-
"
|
|
285
|
-
"
|
|
286
|
-
"
|
|
287
|
-
"
|
|
288
|
-
"
|
|
175
|
+
"Passalong keeps a team's work and what it knows, as four things. WORK is something " +
|
|
176
|
+
"someone must still do: one thing, with no kinds, sections or templates — a title, a " +
|
|
177
|
+
"summary for a person, and whatever text, pictures and files the next reader needs, in " +
|
|
178
|
+
"whatever shape fits. Read it and work out what done means; nothing tells you in advance. " +
|
|
179
|
+
"A PROJECT is a place for one product or body of work; its DOCUMENTS are what to know " +
|
|
180
|
+
"about it — a README saying what it is, who uses it and how it is built, a page per area, " +
|
|
181
|
+
"a Log of what changed. FILES are pictures and other files, on work or in a project.\n" +
|
|
182
|
+
"DOING WORK. take (with an id, or none for the next thing waiting here) says you are doing " +
|
|
183
|
+
"it and returns it; nobody else can take it while you hold it, and whoever sent it sees " +
|
|
184
|
+
"you are on it. progress, with a one-line note at each milestone — 30 minutes of silence " +
|
|
185
|
+
"marks it stalled. hand_in when it is done. pass, with the reason, when it is not yours or " +
|
|
186
|
+
"you are stuck. ask, with a question, when you need the person's answer: you keep it, stop " +
|
|
187
|
+
"and wait, and take it again once they have replied. Every answer ends with `next`: what " +
|
|
188
|
+
"to call now. Follow it, and when it says to stop, stop. OPEN WORK YOU MEAN TO DO WITH " +
|
|
189
|
+
"take, NOT find_work: find_work only reads, and nobody learns you are on it. If take says " +
|
|
190
|
+
"somebody else has it, tell the user instead of doing the work twice.\n" +
|
|
289
191
|
"EVERY HAND-IN CARRIES EVIDENCE: what you ran and what came back — the command and the " +
|
|
290
192
|
"lines that decided it, a test summary, a link to the change, or a screenshot url. " +
|
|
291
193
|
"Collect it as you work rather than writing it from memory at the end. hand_in without " +
|
|
292
|
-
"it is refused, because
|
|
293
|
-
"
|
|
294
|
-
"
|
|
295
|
-
"
|
|
296
|
-
"
|
|
297
|
-
"
|
|
298
|
-
"
|
|
299
|
-
"
|
|
300
|
-
"
|
|
301
|
-
"
|
|
302
|
-
"
|
|
303
|
-
"
|
|
304
|
-
|
|
305
|
-
"
|
|
306
|
-
"
|
|
307
|
-
"
|
|
308
|
-
"
|
|
309
|
-
"
|
|
310
|
-
|
|
311
|
-
"
|
|
312
|
-
"
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
"
|
|
316
|
-
"
|
|
317
|
-
"
|
|
318
|
-
"
|
|
319
|
-
"
|
|
320
|
-
"
|
|
321
|
-
"
|
|
322
|
-
"
|
|
323
|
-
"
|
|
324
|
-
"
|
|
325
|
-
"
|
|
326
|
-
"
|
|
327
|
-
"
|
|
328
|
-
"
|
|
329
|
-
"
|
|
330
|
-
"
|
|
331
|
-
"
|
|
332
|
-
"
|
|
333
|
-
"
|
|
334
|
-
"
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
"that — never publish a second guide about it. A transfer guide is for work that has to " +
|
|
340
|
-
"cross a boundary: another repo, another machine, a teammate without your branch. Work you " +
|
|
341
|
-
"committed and pushed where the team can already see it has crossed no boundary, and a " +
|
|
342
|
-
"guide about it is one more thing for somebody to read and review; what is worth " +
|
|
343
|
-
"publishing from a session like that is what is still open, as a bug or a task. " +
|
|
344
|
-
'"Update the passalong", "add this to passalong" and the like are ambiguous — hand in ' +
|
|
345
|
-
"what you hold, add a follow-up, or publish something new? Ask which, in one line, rather " +
|
|
346
|
-
"than publishing and leaving the user to undo it. " +
|
|
347
|
-
"At the start of work, inbox shows guides teammates have handed to this user — take one " +
|
|
348
|
-
"to start it — and activity shows whether the guides they handed off have landed. When " +
|
|
349
|
-
"the user asks what they have been working on, or wants a standup or a summary of a " +
|
|
350
|
-
"period, call log — but say that it holds what they passed along and not everything they " +
|
|
351
|
-
"did. Gotchas are the highest-value section: record what failed and why.\n" +
|
|
352
|
-
"A FOLDER IS A PLACE; ITS DOCUMENTS ARE WHAT TO KNOW; GUIDES ARE WHAT TO DO. Sort every " +
|
|
353
|
-
"item before writing it into a folder. If someone still has to act on it — a defect that " +
|
|
354
|
-
"is still open, work not yet done, a check nobody has run — it is a guide: kind bug for " +
|
|
355
|
-
"something broken, kind task for work to do. Publish it and link_folder_guide it into the " +
|
|
356
|
-
"folder; a guide has an owner, a state and an answer. A document has none of those and " +
|
|
357
|
-
"tells nobody, so open work recorded only as a document is lost. If it is what someone " +
|
|
358
|
-
"needs to know — an overview, a decision, a finding, something already fixed with its " +
|
|
359
|
-
"commit — it is a document. An incident is both: one overview document, a document per " +
|
|
360
|
-
"resolved finding, and a bug or task guide per open item, all in the incident's folder. " +
|
|
361
|
-
"WHERE A FOLDER LIVES is in the `space` field of create_folder and get_folder: private, " +
|
|
362
|
-
"or a named team. Tell the person that, never what you meant to do; a folder made without " +
|
|
363
|
-
"`team` is private, and a subfolder takes its parent's space.\n" +
|
|
364
|
-
"FOLDERS ARE REUSABLE PROJECT CONTEXT, NOT GUIDES. When the user asks for a folder for a " +
|
|
365
|
-
"script, brief, assets or work spanning several guides, call list_folders first to avoid " +
|
|
366
|
-
"making a duplicate. If none fits, call create_folder with a title and short description. " +
|
|
367
|
-
"Use its parent id to nest a subfolder at any depth; keep a related project together. " +
|
|
368
|
-
"A folder's color can mark an area, and update_folder can recolor or move it later. " +
|
|
369
|
-
"A short request is enough: name it from the person's purpose and draft the first Markdown " +
|
|
370
|
-
"document with create_folder_document. Do not leave an empty folder or make them fill a form. " +
|
|
371
|
-
"Pass `team` only when the user wants it shared with that team; use the slug they named " +
|
|
372
|
-
"or one returned by list_folders, and omit it for a private folder. Use the returned " +
|
|
373
|
-
"folder id with create_folder_document to add Markdown, get_folder to see its contents, " +
|
|
374
|
-
"get_folder_document to read text and its version, and save_folder_document with that " +
|
|
375
|
-
"version when editing. On a stale-version refusal, reread before retrying so another " +
|
|
376
|
-
"person's work is not lost. Use add_folder_asset for a screenshot or file available on " +
|
|
377
|
-
"this machine, and get_folder_asset to read one already there. " +
|
|
378
|
-
"To put a guide in a folder — a task, bug or handoff, new or already published — publish it " +
|
|
379
|
-
"as usual (for a team folder, to that folder's team, or its members cannot see it), then call " +
|
|
380
|
-
"link_folder_guide with the folder id and the guide id. Linking leaves the guide unchanged; " +
|
|
381
|
-
"unlink_folder_guide takes it out again. Delete a document or file with " +
|
|
382
|
-
"delete_folder_document or delete_folder_asset only when the person asked for it to go: " +
|
|
383
|
-
"neither can be undone. No tool deletes a whole folder; a person does that in the hub. " +
|
|
384
|
-
"share_folder returns a link anyone can read the folder by, like a guide's; use it only " +
|
|
385
|
-
"when the person asked to share the folder. " +
|
|
386
|
-
"list_folders with query finds folders and documents by their words, and with guide the " +
|
|
387
|
-
"folders a guide is in. rename_folder_document renames a document and move_folder_document " +
|
|
388
|
-
"moves it to another folder in the same space, keeping its history; " +
|
|
389
|
-
"get_folder_document_history reads earlier versions, so a bad edit can be put back.\n" +
|
|
390
|
-
"A FOLLOW-UP IS MORE CONTEXT FOR A GUIDE, WRITTEN AS ITS OWN GUIDE. When a guide needs " +
|
|
391
|
-
"more context — a missing detail, a step that needed explaining, what changed since, what " +
|
|
392
|
-
"you found doing it — publish that context with publish_guide `parent` set to the guide's " +
|
|
393
|
-
"id. It is listed under the original, and anyone who opens the original, person or agent, " +
|
|
394
|
-
"gets it too. get_guide and take return a guide's follow-ups after it; read them " +
|
|
395
|
-
"before acting. They also return the guide it follows, in front of it: a follow-up is a " +
|
|
396
|
-
"note on a bigger piece of work, not the work. If it needs that guide done and it is " +
|
|
397
|
-
"not, say so rather than starting.",
|
|
398
|
-
},
|
|
399
|
-
);
|
|
400
|
-
|
|
401
|
-
// ---- the four verbs (docs/V2.md §11) --------------------------------------------------------
|
|
402
|
-
// One implementation each. The older tool names above call these too, so an agent following an
|
|
403
|
-
// old prompt gets exactly what one following a new prompt gets.
|
|
404
|
-
|
|
405
|
-
/** An answer as JSON, with the server's next move after it. */
|
|
194
|
+
"it is refused, because your note is your word for your own work and evidence is the " +
|
|
195
|
+
"part a person can check.\n" +
|
|
196
|
+
"WRITING WORK DOWN. create_work takes one piece of work or several. Each says what it is " +
|
|
197
|
+
"to a person in `summary` — one or two plain sentences, 400 characters at most, no ids, " +
|
|
198
|
+
"paths or jargon, saying what this is and whether anybody has to act. It is what the " +
|
|
199
|
+
"person sees first; the text behind it is for agents: exact, dense, full of ids and " +
|
|
200
|
+
"paths. New work is Ready: it reaches whoever it is for at once. Pass `draft: true` when " +
|
|
201
|
+
"a person should read it first — a plan of several pieces, or something you are unsure " +
|
|
202
|
+
"of — and tell them. A tag such as bug, feature or question may describe it; nothing " +
|
|
203
|
+
"turns on one. BEFORE WRITING ANYTHING DOWN, CHECK WHAT IS ALREADY OPEN: find_work with " +
|
|
204
|
+
"nothing shows the board and what this worktree holds. If this session's work answers " +
|
|
205
|
+
"something you hold, hand_in that — never write a second piece of work about it. Defects " +
|
|
206
|
+
"you found and are not fixing go in one create_work call, each its own item, so each can " +
|
|
207
|
+
"be taken on its own. AN IMAGE THE USER SHOWED YOU IS EVIDENCE, NOT CONTEXT: add_file it " +
|
|
208
|
+
"and put the line it returns in the text — a screenshot described instead of attached is " +
|
|
209
|
+
"the most useful thing in the report, thrown away. update_work changes work that exists: " +
|
|
210
|
+
"Ready, Draft or archived, who it is for, or its text.\n" +
|
|
211
|
+
"WHAT YOU LEARN GOES INTO THE PROJECT'S DOCUMENTS. When the user asks you to pass this " +
|
|
212
|
+
"along, hand it off, capture it, or share what was just done, write what the next person " +
|
|
213
|
+
"needs to know into the project's documents: find_projects, read_document its README and " +
|
|
214
|
+
"the page for that area, and save_document to update the page that already covers it " +
|
|
215
|
+
"rather than adding a new one each time, with a line on its Log. Never hand knowledge to " +
|
|
216
|
+
'one person as a piece of work. "Update the passalong", "add this to passalong" and the ' +
|
|
217
|
+
"like are ambiguous — hand in what you hold, update the documents, or write new work? Ask " +
|
|
218
|
+
"which, in one line, rather than writing and leaving the user to undo it.\n" +
|
|
219
|
+
"A PROJECT IS A PLACE; ITS DOCUMENTS ARE WHAT TO KNOW; WORK IS WHAT TO DO. Sort " +
|
|
220
|
+
"everything before writing it. If someone still has to act on it — a defect still open, " +
|
|
221
|
+
"work not yet done, a check nobody has run — it is work: it has an owner, a state and an " +
|
|
222
|
+
"answer. A document has none of those and tells nobody, so open work recorded only as a " +
|
|
223
|
+
"document is lost. If it is what someone needs to know — an overview, a decision, a " +
|
|
224
|
+
"finding, something already fixed with its commit — it is a document. WHERE A PROJECT " +
|
|
225
|
+
"LIVES is in the `space` field find_projects and save_project return: private, or a named " +
|
|
226
|
+
"team. Tell the person that, never what you meant to do; a project made without `team` " +
|
|
227
|
+
"is private, and one made inside another takes its space. Call find_projects before " +
|
|
228
|
+
"save_project, so you do not make a duplicate; a short request is enough to name one and " +
|
|
229
|
+
"write its first document. read_document gives a document's text and version; " +
|
|
230
|
+
"save_document with that version, and on a stale-version refusal read it again before " +
|
|
231
|
+
"retrying, so nobody's writing is lost. Every version is kept, and read_document with " +
|
|
232
|
+
"`history` lists them, so a bad edit can be put back. delete_document and delete_file " +
|
|
233
|
+
"only when the person asked for that one to go: neither can be undone, and no tool " +
|
|
234
|
+
"deletes a whole project.\n" +
|
|
235
|
+
"SEVERAL ACCOUNTS. When this machine has more than one signed in and none is chosen, " +
|
|
236
|
+
"every call is refused until accounts with `use` says which. Ask the person; never choose.",
|
|
237
|
+
},
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
/** An answer as JSON, with the server's next move after it, and a reply in front of it. */
|
|
406
241
|
const answer = (r, id) => {
|
|
407
242
|
const { next, say, ...rest } = r || {};
|
|
408
243
|
const note = nextNote({ next, say }, id);
|
|
409
|
-
// The note is what the agent acts on and it is prose, so it stays in the text half only. The
|
|
410
|
-
// structured half is the answer itself, which is what `outputSchema` below describes.
|
|
411
|
-
// A reply goes first, in prose: it is what the agent stopped for, and inside the JSON it reads
|
|
412
|
-
// as one more field.
|
|
413
244
|
const said = repliesNote(rest.replies);
|
|
414
245
|
return {
|
|
415
246
|
...text(
|
|
@@ -418,1177 +249,664 @@ export function buildServer() {
|
|
|
418
249
|
structuredContent: rest,
|
|
419
250
|
};
|
|
420
251
|
};
|
|
421
|
-
|
|
422
|
-
async function doTake({ id, cwd, any }) {
|
|
423
|
-
const dir = cwd || process.cwd();
|
|
424
|
-
try {
|
|
425
|
-
const r = await passalong.take(id, { cwd: dir, any: Boolean(any) });
|
|
426
|
-
if (!r.guide) return text(nextNote(r) || "Nothing is waiting for this agent here.");
|
|
427
|
-
const meta = parse(r.guide.markdown).meta;
|
|
428
|
-
const siblings = await related(meta);
|
|
429
|
-
const context = await passalong.followUps(meta);
|
|
430
|
-
const from = await passalong.parentGuide(meta);
|
|
431
|
-
const held =
|
|
432
|
-
`<!-- passalong: ${r.guide.id} is yours` +
|
|
433
|
-
`${r.guide.resumed ? " (you already held it — carry on from where it was left)" : ""}. -->`;
|
|
434
|
-
const said = repliesNote(r.replies);
|
|
435
|
-
return text(
|
|
436
|
-
`${said ? `${said}\n\n` : ""}${from}${leadFor(meta)}${r.guide.markdown}${siblings}${context ? `\n\n${context}` : ""}` +
|
|
437
|
-
`\n\n${held}\n${nextNote(r, r.guide.id)}\n${passalong.followUpNote(meta)}`,
|
|
438
|
-
);
|
|
439
|
-
} catch (err) {
|
|
440
|
-
const h = err.body?.holder;
|
|
441
|
-
if (h)
|
|
442
|
-
return fail({
|
|
443
|
-
message:
|
|
444
|
-
`${err.message}. Somebody else is on it here${h.agent ? ` (agent ${h.agent}${h.host ? ` on ${h.host}` : ""})` : ""}${h.note ? ` (last said: "${h.note}")` : ""}. ` +
|
|
445
|
-
"Tell the user before doing this work too; get_guide reads it without taking it.",
|
|
446
|
-
});
|
|
447
|
-
return failWith(err);
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
async function doProgress({ id, note, cwd }) {
|
|
452
|
-
try {
|
|
453
|
-
return answer(await passalong.progress(id, note || "", { cwd: cwd || process.cwd() }), id);
|
|
454
|
-
} catch (err) {
|
|
455
|
-
return failWith(err);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
async function doHandIn({
|
|
460
|
-
id,
|
|
461
|
-
ok,
|
|
462
|
-
note,
|
|
463
|
-
evidence,
|
|
464
|
-
checks,
|
|
465
|
-
writeup,
|
|
466
|
-
risk,
|
|
467
|
-
markdown,
|
|
468
|
-
report,
|
|
469
|
-
pr,
|
|
470
|
-
cwd,
|
|
471
|
-
}) {
|
|
472
|
-
try {
|
|
473
|
-
// A check that names a command is run here, before anything is recorded, and its exit code
|
|
474
|
-
// decides it rather than the agent's account of it. A failure is refused with the command's
|
|
475
|
-
// own output: the claim is still held, nothing is written, and the agent has what it needs
|
|
476
|
-
// to fix. Checks with no `cmd` pass straight through to the prose rule.
|
|
477
|
-
const at = cwd || process.cwd();
|
|
478
|
-
const done = runChecks(checks || [], { cwd: at });
|
|
479
|
-
if (done.failed)
|
|
480
|
-
return {
|
|
481
|
-
content: [
|
|
482
|
-
{ type: "text", text: refusal(done.failed, { ran: done.ran, total: checks.length }) },
|
|
483
|
-
],
|
|
484
|
-
isError: true,
|
|
485
|
-
};
|
|
486
|
-
const r = await passalong.handIn(id, {
|
|
487
|
-
ok,
|
|
488
|
-
note: note || "",
|
|
489
|
-
evidence: evidence || "",
|
|
490
|
-
checks: done.checks,
|
|
491
|
-
writeup: writeup || "",
|
|
492
|
-
risk: risk || "",
|
|
493
|
-
markdown,
|
|
494
|
-
report,
|
|
495
|
-
pr: pr || "",
|
|
496
|
-
cwd: at,
|
|
497
|
-
});
|
|
498
|
-
return answer(r, id);
|
|
499
|
-
} catch (err) {
|
|
500
|
-
return failWith(err);
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
async function doAsk({ id, question, cwd }) {
|
|
252
|
+
const verb = (fn) => async (args) => {
|
|
505
253
|
try {
|
|
506
|
-
return answer(await
|
|
254
|
+
return answer(await fn(args), args.id);
|
|
507
255
|
} catch (err) {
|
|
508
256
|
return failWith(err);
|
|
509
257
|
}
|
|
510
|
-
}
|
|
258
|
+
};
|
|
511
259
|
|
|
512
|
-
|
|
513
|
-
try {
|
|
514
|
-
return answer(await passalong.pass(id, why, { cwd: cwd || process.cwd() }), id);
|
|
515
|
-
} catch (err) {
|
|
516
|
-
return failWith(err);
|
|
517
|
-
}
|
|
518
|
-
}
|
|
260
|
+
// ---- work: take, progress, hand_in, pass, ask ---------------------------------------------
|
|
519
261
|
|
|
520
262
|
server.registerTool(
|
|
521
|
-
"
|
|
263
|
+
"take",
|
|
522
264
|
{
|
|
523
|
-
title: "
|
|
265
|
+
title: "Take work",
|
|
524
266
|
annotations: ADDS,
|
|
525
267
|
description:
|
|
526
|
-
"
|
|
527
|
-
|
|
528
|
-
"
|
|
529
|
-
"
|
|
268
|
+
"Say you are doing it, and get it. With `id`, that piece of work. With no id, the next " +
|
|
269
|
+
"thing waiting here — always something new, never what you hold. While you hold it no " +
|
|
270
|
+
"other agent can take it, and whoever sent it sees you are on it. You may hold several; " +
|
|
271
|
+
"the person watches them on the board. If somebody else has it, the answer says who — " +
|
|
272
|
+
"tell the user rather than doing the work twice. Every answer ends with what to call next.",
|
|
530
273
|
inputSchema: {
|
|
531
|
-
id: z
|
|
532
|
-
|
|
274
|
+
id: z
|
|
275
|
+
.string()
|
|
276
|
+
.optional()
|
|
277
|
+
.describe("passalong id or share link; leave out for the next one"),
|
|
278
|
+
cwd: CWD,
|
|
279
|
+
any: z
|
|
280
|
+
.boolean()
|
|
281
|
+
.optional()
|
|
282
|
+
.describe("work for another repo, or none — only when the user asks"),
|
|
533
283
|
},
|
|
534
284
|
},
|
|
535
|
-
async ({ id,
|
|
285
|
+
async ({ id, cwd, any }) => {
|
|
536
286
|
try {
|
|
537
|
-
|
|
287
|
+
const r = await passalong.take(id, { cwd: cwd || process.cwd(), any: Boolean(any) });
|
|
288
|
+
if (!r.work) return text(nextNote(r) || "Nothing is waiting for this agent here.");
|
|
289
|
+
const from = await passalong.parentWork(parse(r.work.markdown).meta);
|
|
290
|
+
const held =
|
|
291
|
+
`<!-- passalong: ${r.work.id} is yours` +
|
|
292
|
+
`${r.work.resumed ? " (you already held it — carry on from where it was left)" : ""}. -->`;
|
|
293
|
+
const said = repliesNote(r.replies);
|
|
294
|
+
return text(
|
|
295
|
+
`${said ? `${said}\n\n` : ""}${from}${LEAD}${r.work.markdown}` +
|
|
296
|
+
`\n\n${held}\n${nextNote(r, r.work.id)}\n${passalong.AFTER}`,
|
|
297
|
+
);
|
|
538
298
|
} catch (err) {
|
|
539
|
-
|
|
299
|
+
const h = err.body?.holder;
|
|
300
|
+
if (h)
|
|
301
|
+
return fail({
|
|
302
|
+
message:
|
|
303
|
+
`${err.message}. Somebody else is on it${h.agent ? ` (agent ${h.agent}${h.host ? ` on ${h.host}` : ""})` : ""}${h.note ? ` (last said: "${h.note}")` : ""}. ` +
|
|
304
|
+
"Tell the user before doing this work too; find_work reads it without taking it.",
|
|
305
|
+
});
|
|
306
|
+
return failWith(err);
|
|
540
307
|
}
|
|
541
308
|
},
|
|
542
309
|
);
|
|
543
310
|
|
|
544
311
|
server.registerTool(
|
|
545
|
-
"
|
|
312
|
+
"progress",
|
|
546
313
|
{
|
|
547
|
-
title: "
|
|
548
|
-
|
|
549
|
-
|
|
314
|
+
title: "Report progress",
|
|
315
|
+
outputSchema: progressOut,
|
|
316
|
+
annotations: ADDS,
|
|
317
|
+
description:
|
|
318
|
+
"Say you are still on what you hold, with a one-line note the hub shows. 30 minutes " +
|
|
319
|
+
"without one marks it stalled. If the answer says you no longer hold it, stop.",
|
|
550
320
|
inputSchema: {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
321
|
+
id: z.string().describe("the id of what you hold"),
|
|
322
|
+
note: z
|
|
323
|
+
.string()
|
|
324
|
+
.optional()
|
|
325
|
+
.describe(
|
|
326
|
+
'one plain sentence in the first person, as you would tell the person: "Reading ' +
|
|
327
|
+
'the settings page to find where the toggle goes." Start it with BLOCKED: when ' +
|
|
328
|
+
"you are stopped and need an answer",
|
|
329
|
+
),
|
|
330
|
+
cwd: CWD,
|
|
554
331
|
},
|
|
555
332
|
},
|
|
556
|
-
|
|
557
|
-
try {
|
|
558
|
-
return json(await api.folders(scope || "all", { query, guide }));
|
|
559
|
-
} catch (err) {
|
|
560
|
-
return fail(err);
|
|
561
|
-
}
|
|
562
|
-
},
|
|
333
|
+
verb(({ id, note, cwd }) => passalong.progress(id, note || "", { cwd: cwd || process.cwd() })),
|
|
563
334
|
);
|
|
335
|
+
|
|
564
336
|
server.registerTool(
|
|
565
|
-
"
|
|
337
|
+
"hand_in",
|
|
566
338
|
{
|
|
567
|
-
title: "
|
|
339
|
+
title: "Hand it in",
|
|
340
|
+
outputSchema: handInOut,
|
|
568
341
|
annotations: ADDS,
|
|
569
342
|
description:
|
|
570
|
-
"
|
|
343
|
+
"Finished here, with proof. IT IS FINAL: the work goes to Done with what you showed, and " +
|
|
344
|
+
"nobody reviews it after you (anyone can reopen it with a reason). So SHOW IT: a " +
|
|
345
|
+
"screenshot for anything with a screen — before and after, for a defect — or, for " +
|
|
346
|
+
"backend work, the one command and the line of its output that shows it works. Send " +
|
|
347
|
+
"`checks`: one entry per thing the work asked for, each with that line and the evidence " +
|
|
348
|
+
"for it. That is what a person reads, line against line, AND IT IS HOW YOU KNOW YOU ARE " +
|
|
349
|
+
"DONE: every line has one. Give a check `cmd` when a command proves it: the command is " +
|
|
350
|
+
"run here before the hand-in lands, a non-zero exit refuses it, and what it printed is " +
|
|
351
|
+
"recorded instead of your account of it. Use `ran` for a check no command can settle — " +
|
|
352
|
+
"add_file the screenshot and put the line it returns there. `evidence` as one block works " +
|
|
353
|
+
"too. DO NOT write new work to carry your evidence: it belongs on this call. What you had " +
|
|
354
|
+
"to adapt goes in `writeup`; what you learned about the project goes into its documents " +
|
|
355
|
+
"(save_document).",
|
|
571
356
|
inputSchema: {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
357
|
+
id: z.string().describe("passalong id"),
|
|
358
|
+
evidence: z
|
|
359
|
+
.string()
|
|
360
|
+
.optional()
|
|
361
|
+
.describe(
|
|
362
|
+
"what you ran and what came back: the command and the lines that decided it, a test " +
|
|
363
|
+
'summary, a link to the change, or a screenshot url. "it works" is a claim, not evidence',
|
|
364
|
+
),
|
|
365
|
+
checks: z
|
|
366
|
+
.array(
|
|
367
|
+
// A bare string is a check line with nothing behind it. Let it through to the server's
|
|
368
|
+
// refusal, which says what to send, rather than a schema error that says only "expected
|
|
369
|
+
// object".
|
|
370
|
+
z.preprocess(
|
|
371
|
+
(v) => (typeof v === "string" ? { check: v } : v),
|
|
372
|
+
z.object({
|
|
373
|
+
check: z
|
|
374
|
+
.string()
|
|
375
|
+
.describe("what the work asked for that this answers, in its own words"),
|
|
376
|
+
says: z
|
|
377
|
+
.string()
|
|
378
|
+
.optional()
|
|
379
|
+
.describe(
|
|
380
|
+
'what happened, in one plain sentence for a PERSON: "Swagger lists all six ' +
|
|
381
|
+
'endpoints, each with request and response schemas." It is the row they read; ' +
|
|
382
|
+
"the evidence stays folded behind it",
|
|
383
|
+
),
|
|
384
|
+
ran: z
|
|
385
|
+
.string()
|
|
386
|
+
.optional()
|
|
387
|
+
.describe(
|
|
388
|
+
"what you ran for it, and what came back; not needed when `cmd` is set",
|
|
389
|
+
),
|
|
390
|
+
cmd: z
|
|
391
|
+
.string()
|
|
392
|
+
.optional()
|
|
393
|
+
.describe(
|
|
394
|
+
"the shell command that proves this line, run here before the hand-in lands — " +
|
|
395
|
+
"its exit code decides the check and its output is recorded as the evidence. " +
|
|
396
|
+
"Non-zero refuses the hand-in. Leave it out for a check nobody can run, like " +
|
|
397
|
+
'"the badge reads 3" — then SHOW it with add_file and put the line it returns in `ran`; a check is run or shown, and describing what you saw is refused',
|
|
398
|
+
),
|
|
399
|
+
}),
|
|
400
|
+
),
|
|
401
|
+
)
|
|
402
|
+
.optional()
|
|
403
|
+
.describe(
|
|
404
|
+
"one entry per thing the work asked for, in the order you worked them. Each is RUN " +
|
|
405
|
+
"(`cmd`) or SHOWN (an add_file line in `ran`). Pasting a command you typed into " +
|
|
406
|
+
"`ran` is not running it and is refused",
|
|
407
|
+
),
|
|
408
|
+
note: z
|
|
409
|
+
.string()
|
|
410
|
+
.describe(
|
|
411
|
+
"one plain sentence, in the first person, saying what you did and how it went: " +
|
|
412
|
+
'"Added the toggle to Settings; it survives a reload." It is the first thing the ' +
|
|
413
|
+
"person reads, and the evidence is behind it",
|
|
414
|
+
),
|
|
415
|
+
writeup: z
|
|
576
416
|
.string()
|
|
577
417
|
.optional()
|
|
578
|
-
.describe(
|
|
579
|
-
|
|
580
|
-
|
|
418
|
+
.describe(
|
|
419
|
+
"what you had to adapt to make it work here — a version, a name, a step the work did " +
|
|
420
|
+
"not mention. 500 characters at most; anything longer belongs in the change. " +
|
|
421
|
+
"Optional: leave it out when it went as written",
|
|
422
|
+
),
|
|
423
|
+
risk: z
|
|
424
|
+
.string()
|
|
581
425
|
.optional()
|
|
582
|
-
.describe(
|
|
426
|
+
.describe(
|
|
427
|
+
"what this could break, in one line: a shared helper changed, a migration that " +
|
|
428
|
+
"cannot be undone, a route other clients call. Optional; leave it out when there is " +
|
|
429
|
+
'nothing specific, rather than writing "low risk"',
|
|
430
|
+
),
|
|
431
|
+
pr: z.string().optional().describe("PR, branch or commit link, when there is one"),
|
|
432
|
+
cwd: CWD,
|
|
583
433
|
},
|
|
584
434
|
},
|
|
585
|
-
async ({
|
|
586
|
-
try {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
435
|
+
async ({ id, note, evidence, checks, writeup, risk, pr, cwd }) => {
|
|
436
|
+
try {
|
|
437
|
+
// A check that names a command is run here, before anything is recorded, and its exit
|
|
438
|
+
// code decides it rather than the agent's account of it. A failure is refused with the
|
|
439
|
+
// command's own output: the claim is still held, nothing is written.
|
|
440
|
+
const at = cwd || process.cwd();
|
|
441
|
+
const done = runChecks(checks || [], { cwd: at });
|
|
442
|
+
if (done.failed)
|
|
443
|
+
return fail({
|
|
444
|
+
message: refusal(done.failed, { ran: done.ran, total: checks.length }),
|
|
445
|
+
});
|
|
446
|
+
const r = await passalong.handIn(id, {
|
|
447
|
+
note,
|
|
448
|
+
evidence: evidence || "",
|
|
449
|
+
checks: done.checks,
|
|
450
|
+
writeup: writeup || "",
|
|
451
|
+
risk: risk || "",
|
|
452
|
+
pr: pr || "",
|
|
453
|
+
cwd: at,
|
|
454
|
+
});
|
|
455
|
+
return answer(r, id);
|
|
590
456
|
} catch (err) {
|
|
591
|
-
return
|
|
457
|
+
return failWith(err);
|
|
592
458
|
}
|
|
593
459
|
},
|
|
594
460
|
);
|
|
461
|
+
|
|
595
462
|
server.registerTool(
|
|
596
|
-
"
|
|
463
|
+
"pass",
|
|
597
464
|
{
|
|
598
|
-
title: "
|
|
465
|
+
title: "Pass it",
|
|
466
|
+
outputSchema: passOut,
|
|
599
467
|
annotations: ADDS,
|
|
600
468
|
description:
|
|
601
|
-
"
|
|
469
|
+
"Not yours to do, or you are stuck: give it back with the reason. It is open again for " +
|
|
470
|
+
"the next agent, and the reason goes to whoever is next — say why, or they start where " +
|
|
471
|
+
"silence left them.",
|
|
602
472
|
inputSchema: {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
parent: z
|
|
607
|
-
.string()
|
|
608
|
-
.optional()
|
|
609
|
-
.describe("New parent folder id; empty string means top level"),
|
|
610
|
-
color: z.enum(["", "coral", "amber", "green", "blue", "violet"]).optional(),
|
|
473
|
+
id: z.string().describe("passalong id"),
|
|
474
|
+
why: z.string().describe("one line: why it is not yours, or where you got stuck"),
|
|
475
|
+
cwd: CWD,
|
|
611
476
|
},
|
|
612
477
|
},
|
|
613
|
-
|
|
614
|
-
try {
|
|
615
|
-
const changes = Object.fromEntries(
|
|
616
|
-
Object.entries({ title, description, parent, color }).filter(
|
|
617
|
-
([, value]) => value !== undefined,
|
|
618
|
-
),
|
|
619
|
-
);
|
|
620
|
-
return json(await api.updateFolder(folder, changes));
|
|
621
|
-
} catch (err) {
|
|
622
|
-
return fail(err);
|
|
623
|
-
}
|
|
624
|
-
},
|
|
478
|
+
verb(({ id, why, cwd }) => passalong.pass(id, why, { cwd: cwd || process.cwd() })),
|
|
625
479
|
);
|
|
480
|
+
|
|
626
481
|
server.registerTool(
|
|
627
|
-
"
|
|
482
|
+
"ask",
|
|
628
483
|
{
|
|
629
|
-
title: "
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
484
|
+
title: "Ask the person",
|
|
485
|
+
outputSchema: askOut,
|
|
486
|
+
annotations: ADDS,
|
|
487
|
+
description:
|
|
488
|
+
"You need the person's answer before you can go on. Ask one clear question, with the " +
|
|
489
|
+
"context it needs to be answered without opening your terminal. You keep what you hold " +
|
|
490
|
+
"and nobody else can take it. Then STOP and tell the user you are waiting: do not carry " +
|
|
491
|
+
"on guessing. When they have replied, call take with this id and their reply comes back " +
|
|
492
|
+
"with it. Use `pass` instead when the work is not yours.",
|
|
493
|
+
inputSchema: {
|
|
494
|
+
id: z.string().describe("passalong id"),
|
|
495
|
+
question: z
|
|
496
|
+
.string()
|
|
497
|
+
.describe(
|
|
498
|
+
"one clear question in the first person, a few sentences at most, plain text: " +
|
|
499
|
+
'"Should the dark-mode toggle live in Settings or the header? The header is ' +
|
|
500
|
+
'crowded on mobile." What you were about to do, if they just say yes, helps',
|
|
501
|
+
),
|
|
502
|
+
cwd: CWD,
|
|
503
|
+
},
|
|
640
504
|
},
|
|
505
|
+
verb(({ id, question, cwd }) => passalong.ask(id, question, { cwd: cwd || process.cwd() })),
|
|
641
506
|
);
|
|
507
|
+
|
|
508
|
+
// ---- work: create_work, find_work, update_work ---------------------------------------------
|
|
509
|
+
|
|
642
510
|
server.registerTool(
|
|
643
|
-
"
|
|
511
|
+
"create_work",
|
|
644
512
|
{
|
|
645
|
-
title: "
|
|
646
|
-
annotations:
|
|
647
|
-
description:
|
|
648
|
-
|
|
513
|
+
title: "Write work down",
|
|
514
|
+
annotations: ADDS,
|
|
515
|
+
description:
|
|
516
|
+
"Write down work someone must still do: one piece, or several at once — a plan broken " +
|
|
517
|
+
"into steps, or every defect a test run turned up, each its own item so each can be " +
|
|
518
|
+
"taken on its own. Check what is open first (find_work): if this session answers " +
|
|
519
|
+
"something you hold, hand_in that instead. Each item is Ready at once and reaches whoever " +
|
|
520
|
+
"it is for; `draft: true` holds them all for a person to read first. A picture belongs " +
|
|
521
|
+
"in the text: add_file it and put the line it returns in `body`. Knowledge is not work: " +
|
|
522
|
+
"what someone needs to know goes into a project's documents (save_document).",
|
|
523
|
+
inputSchema: {
|
|
524
|
+
items: z
|
|
525
|
+
.array(
|
|
526
|
+
z.object({
|
|
527
|
+
title: z.string().describe("what it is, in a few words"),
|
|
528
|
+
summary: z
|
|
529
|
+
.string()
|
|
530
|
+
.describe(
|
|
531
|
+
"one or two plain sentences for a PERSON, no ids or paths: what this is and " +
|
|
532
|
+
"whether anybody has to act. 400 characters at most",
|
|
533
|
+
),
|
|
534
|
+
body: z
|
|
535
|
+
.string()
|
|
536
|
+
.optional()
|
|
537
|
+
.describe(
|
|
538
|
+
"markdown for whoever does it: what is wrong or wanted, what you found, what " +
|
|
539
|
+
"done looks like if you know — in whatever shape fits",
|
|
540
|
+
),
|
|
541
|
+
to: z
|
|
542
|
+
.string()
|
|
543
|
+
.optional()
|
|
544
|
+
.describe("who it is for, when not the same as the top-level `to`"),
|
|
545
|
+
parent: z
|
|
546
|
+
.string()
|
|
547
|
+
.optional()
|
|
548
|
+
.describe("id of the work this comes out of — a bigger piece, a customer request"),
|
|
549
|
+
after: z
|
|
550
|
+
.array(z.number().int())
|
|
551
|
+
.optional()
|
|
552
|
+
.describe("positions (from 0) of earlier items this one needs done first"),
|
|
553
|
+
repo: z
|
|
554
|
+
.string()
|
|
555
|
+
.optional()
|
|
556
|
+
.describe("owner/repo it is for; default is the repo you are in, '' for none"),
|
|
557
|
+
tags: z
|
|
558
|
+
.array(z.string())
|
|
559
|
+
.optional()
|
|
560
|
+
.describe(
|
|
561
|
+
"words that describe it, e.g. bug, feature, question; nothing turns on one",
|
|
562
|
+
),
|
|
563
|
+
}),
|
|
564
|
+
)
|
|
565
|
+
.min(1)
|
|
566
|
+
.max(20),
|
|
567
|
+
to: z
|
|
568
|
+
.string()
|
|
569
|
+
.optional()
|
|
570
|
+
.describe(
|
|
571
|
+
"team slug, team/@handle for one teammate, team/@ada,@bola for several, or " +
|
|
572
|
+
"team/#group for the people who do a thing; default is your current team",
|
|
573
|
+
),
|
|
574
|
+
draft: z
|
|
575
|
+
.boolean()
|
|
576
|
+
.optional()
|
|
577
|
+
.describe("hold it all in Draft for a person to read and make Ready"),
|
|
578
|
+
cwd: z.string().optional().describe("the repo you are writing from"),
|
|
579
|
+
},
|
|
649
580
|
},
|
|
650
|
-
async ({
|
|
581
|
+
async ({ items, to, draft, cwd }) => {
|
|
651
582
|
try {
|
|
652
|
-
|
|
583
|
+
const made = await passalong.createWork(items, {
|
|
584
|
+
draft: Boolean(draft),
|
|
585
|
+
to,
|
|
586
|
+
cwd: cwd || process.cwd(),
|
|
587
|
+
});
|
|
588
|
+
return json({
|
|
589
|
+
work: made,
|
|
590
|
+
// Nothing in Draft reaches anybody, and an agent that reports "sent it" has told the
|
|
591
|
+
// user something untrue. Said in the result so it is said to them.
|
|
592
|
+
...(draft
|
|
593
|
+
? {
|
|
594
|
+
next: `in Draft: ask the user to read it and mark it Ready at ${api.baseUrl()}/hub/work, or run \`passalong ready --all\``,
|
|
595
|
+
}
|
|
596
|
+
: {}),
|
|
597
|
+
});
|
|
653
598
|
} catch (err) {
|
|
654
599
|
return fail(err);
|
|
655
600
|
}
|
|
656
601
|
},
|
|
657
602
|
);
|
|
603
|
+
|
|
658
604
|
server.registerTool(
|
|
659
|
-
"
|
|
605
|
+
"find_work",
|
|
660
606
|
{
|
|
661
|
-
title: "
|
|
607
|
+
title: "Find work",
|
|
662
608
|
annotations: READS,
|
|
663
609
|
description:
|
|
664
|
-
"
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
? [
|
|
674
|
-
{ type: "text", text: file.name },
|
|
675
|
-
{ type: "image", data, mimeType: file.type },
|
|
676
|
-
]
|
|
677
|
-
: [{ type: "text", text: `${file.name}\n\n${body || ""}` }],
|
|
678
|
-
structuredContent: meta,
|
|
679
|
-
};
|
|
680
|
-
} catch (err) {
|
|
681
|
-
return fail(err);
|
|
682
|
-
}
|
|
683
|
-
},
|
|
684
|
-
);
|
|
685
|
-
|
|
686
|
-
server.registerTool(
|
|
687
|
-
"add_folder_asset",
|
|
688
|
-
{
|
|
689
|
-
title: "Add file to project folder",
|
|
690
|
-
annotations: { ...ADDS, openWorldHint: true },
|
|
691
|
-
description:
|
|
692
|
-
"Add a screenshot, reference file or other asset from this machine to a folder. Use a real file path you can read; the bytes are uploaded directly, not copied into a guide. Images, PDF, ZIP and text are accepted up to 10 MB.",
|
|
693
|
-
inputSchema: {
|
|
694
|
-
folder: z.string().describe("folder id returned by create_folder or list_folders"),
|
|
695
|
-
file: z.string().describe("path to the file on this machine"),
|
|
696
|
-
name: z.string().optional().describe("label in the folder; defaults to the filename"),
|
|
697
|
-
},
|
|
698
|
-
},
|
|
699
|
-
async ({ folder, file, name }) => {
|
|
700
|
-
try {
|
|
701
|
-
return json(await api.addFolderAsset(folder, file, name || ""));
|
|
702
|
-
} catch (err) {
|
|
703
|
-
return fail(err);
|
|
704
|
-
}
|
|
705
|
-
},
|
|
706
|
-
);
|
|
707
|
-
|
|
708
|
-
server.registerTool(
|
|
709
|
-
"create_folder_document",
|
|
710
|
-
{
|
|
711
|
-
title: "Write new folder document",
|
|
712
|
-
annotations: ADDS,
|
|
713
|
-
description:
|
|
714
|
-
"Add a Markdown document to a folder: reference to read, such as a brief, script, overview or a finding already resolved. It has no owner and no state and notifies nobody, so anything someone must still act on (an open bug, work to do) belongs in a bug or task guide linked to the folder instead.",
|
|
715
|
-
inputSchema: { folder: z.string(), name: z.string(), body: z.string() },
|
|
716
|
-
},
|
|
717
|
-
async ({ folder, name, body }) => {
|
|
718
|
-
try {
|
|
719
|
-
return json(await api.createFolderDocument(folder, name, body));
|
|
720
|
-
} catch (err) {
|
|
721
|
-
return fail(err);
|
|
722
|
-
}
|
|
723
|
-
},
|
|
724
|
-
);
|
|
725
|
-
server.registerTool(
|
|
726
|
-
"save_folder_document",
|
|
727
|
-
{
|
|
728
|
-
title: "Save folder document",
|
|
729
|
-
annotations: ADDS,
|
|
730
|
-
description:
|
|
731
|
-
"Save a new revision. Send the version returned when you read it; a newer edit is refused.",
|
|
732
|
-
inputSchema: {
|
|
733
|
-
folder: z.string(),
|
|
734
|
-
document: z.string(),
|
|
735
|
-
version: z.number().int().positive(),
|
|
736
|
-
body: z.string(),
|
|
737
|
-
},
|
|
738
|
-
},
|
|
739
|
-
async ({ folder, document, version, body }) => {
|
|
740
|
-
try {
|
|
741
|
-
return json(await api.saveFolderDocument(folder, document, version, body));
|
|
742
|
-
} catch (err) {
|
|
743
|
-
return fail(err);
|
|
744
|
-
}
|
|
745
|
-
},
|
|
746
|
-
);
|
|
747
|
-
server.registerTool(
|
|
748
|
-
"rename_folder_document",
|
|
749
|
-
{
|
|
750
|
-
title: "Rename folder document",
|
|
751
|
-
annotations: ADDS,
|
|
752
|
-
description:
|
|
753
|
-
"Rename a folder document. Its id, text and history stay; no version is needed, because a rename cannot overwrite anyone's text.",
|
|
754
|
-
inputSchema: { folder: z.string(), document: z.string(), name: z.string() },
|
|
755
|
-
},
|
|
756
|
-
async ({ folder, document, name }) => {
|
|
757
|
-
try {
|
|
758
|
-
return json(await api.renameFolderDocument(folder, document, name));
|
|
759
|
-
} catch (err) {
|
|
760
|
-
return fail(err);
|
|
761
|
-
}
|
|
762
|
-
},
|
|
763
|
-
);
|
|
764
|
-
server.registerTool(
|
|
765
|
-
"share_folder",
|
|
766
|
-
{
|
|
767
|
-
title: "Share folder link",
|
|
768
|
-
annotations: ADDS,
|
|
769
|
-
description:
|
|
770
|
-
"Get the folder's share link, making one if it has none: like a guide's link, anyone holding it can read the folder and the folders inside it and download its files, with no account. Linked guides are not included. Call it only when the person asked to share the folder or for its link; stopping sharing is done in the hub.",
|
|
771
|
-
inputSchema: { folder: z.string() },
|
|
772
|
-
},
|
|
773
|
-
async ({ folder }) => {
|
|
774
|
-
try {
|
|
775
|
-
return json(await api.shareFolder(folder));
|
|
776
|
-
} catch (err) {
|
|
777
|
-
return fail(err);
|
|
778
|
-
}
|
|
779
|
-
},
|
|
780
|
-
);
|
|
781
|
-
server.registerTool(
|
|
782
|
-
"move_folder_document",
|
|
783
|
-
{
|
|
784
|
-
title: "Move folder document",
|
|
785
|
-
annotations: ADDS,
|
|
786
|
-
description:
|
|
787
|
-
"Move a folder document into another folder in the same space, private or the same team. It keeps its id, text and history; pass name too to rename it on the way. Refused if the other folder already has a document by that name.",
|
|
788
|
-
inputSchema: {
|
|
789
|
-
folder: z.string().describe("The folder the document is in now"),
|
|
790
|
-
document: z.string(),
|
|
791
|
-
to: z.string().describe("The folder to move it into"),
|
|
792
|
-
name: z.string().optional().describe("A new name, if it should change too"),
|
|
793
|
-
},
|
|
794
|
-
},
|
|
795
|
-
async ({ folder, document, to, name }) => {
|
|
796
|
-
try {
|
|
797
|
-
return json(await api.moveFolderDocument(folder, document, to, name || ""));
|
|
798
|
-
} catch (err) {
|
|
799
|
-
return fail(err);
|
|
800
|
-
}
|
|
801
|
-
},
|
|
802
|
-
);
|
|
803
|
-
server.registerTool(
|
|
804
|
-
"get_folder_document_history",
|
|
805
|
-
{
|
|
806
|
-
title: "Read folder document history",
|
|
807
|
-
annotations: READS,
|
|
808
|
-
description:
|
|
809
|
-
"Read a folder document's earlier versions. Without version, lists them newest first; with version, returns that version's text. To bring one back, save it with save_folder_document over the current version.",
|
|
810
|
-
inputSchema: {
|
|
811
|
-
folder: z.string(),
|
|
812
|
-
document: z.string(),
|
|
813
|
-
version: z.number().int().positive().optional(),
|
|
814
|
-
},
|
|
815
|
-
},
|
|
816
|
-
async ({ folder, document, version }) => {
|
|
817
|
-
try {
|
|
818
|
-
return json(await api.folderDocumentHistory(folder, document, version || 0));
|
|
819
|
-
} catch (err) {
|
|
820
|
-
return fail(err);
|
|
821
|
-
}
|
|
822
|
-
},
|
|
823
|
-
);
|
|
824
|
-
server.registerTool(
|
|
825
|
-
"link_folder_guide",
|
|
826
|
-
{
|
|
827
|
-
title: "Link guide to folder",
|
|
828
|
-
annotations: ADDS,
|
|
829
|
-
description:
|
|
830
|
-
"Put a task, bug or handoff guide in the folder, beside the documents and assets it uses: one just published or one that already existed.",
|
|
831
|
-
inputSchema: { folder: z.string(), guide: z.string() },
|
|
832
|
-
},
|
|
833
|
-
async ({ folder, guide }) => {
|
|
834
|
-
try {
|
|
835
|
-
return json(await api.linkFolderGuide(folder, guide));
|
|
836
|
-
} catch (err) {
|
|
837
|
-
return fail(err);
|
|
838
|
-
}
|
|
839
|
-
},
|
|
840
|
-
);
|
|
841
|
-
|
|
842
|
-
server.registerTool(
|
|
843
|
-
"unlink_folder_guide",
|
|
844
|
-
{
|
|
845
|
-
title: "Unlink guide from folder",
|
|
846
|
-
annotations: ADDS,
|
|
847
|
-
description:
|
|
848
|
-
"Take a guide out of a folder. Only the link goes: the guide itself is unchanged, and link_folder_guide puts it back.",
|
|
849
|
-
inputSchema: { folder: z.string(), guide: z.string() },
|
|
850
|
-
},
|
|
851
|
-
async ({ folder, guide }) => {
|
|
852
|
-
try {
|
|
853
|
-
return json(await api.unlinkFolderGuide(folder, guide));
|
|
854
|
-
} catch (err) {
|
|
855
|
-
return fail(err);
|
|
856
|
-
}
|
|
857
|
-
},
|
|
858
|
-
);
|
|
859
|
-
|
|
860
|
-
server.registerTool(
|
|
861
|
-
"delete_folder_document",
|
|
862
|
-
{
|
|
863
|
-
title: "Delete folder document",
|
|
864
|
-
annotations: REMOVES,
|
|
865
|
-
description:
|
|
866
|
-
"Delete one document from a folder, with its earlier versions. It cannot be brought back, so do it only when the person asked for that document to go; to change what it says, use save_folder_document instead.",
|
|
867
|
-
inputSchema: { folder: z.string(), document: z.string() },
|
|
868
|
-
},
|
|
869
|
-
async ({ folder, document }) => {
|
|
870
|
-
try {
|
|
871
|
-
return json(await api.deleteFolderDocument(folder, document));
|
|
872
|
-
} catch (err) {
|
|
873
|
-
return fail(err);
|
|
874
|
-
}
|
|
875
|
-
},
|
|
876
|
-
);
|
|
877
|
-
|
|
878
|
-
server.registerTool(
|
|
879
|
-
"delete_folder_asset",
|
|
880
|
-
{
|
|
881
|
-
title: "Delete folder file",
|
|
882
|
-
annotations: REMOVES,
|
|
883
|
-
description:
|
|
884
|
-
"Delete one file from a folder. It cannot be brought back, so do it only when the person asked for that file to go.",
|
|
885
|
-
inputSchema: { folder: z.string(), asset: z.string() },
|
|
886
|
-
},
|
|
887
|
-
async ({ folder, asset }) => {
|
|
888
|
-
try {
|
|
889
|
-
return json(await api.deleteFolderAsset(folder, asset));
|
|
890
|
-
} catch (err) {
|
|
891
|
-
return fail(err);
|
|
892
|
-
}
|
|
893
|
-
},
|
|
894
|
-
);
|
|
895
|
-
|
|
896
|
-
server.registerTool(
|
|
897
|
-
"search_guides",
|
|
898
|
-
{
|
|
899
|
-
title: "Search guides",
|
|
900
|
-
annotations: READS,
|
|
901
|
-
description:
|
|
902
|
-
"Search transfer guides by words in the title, tags, stack, or body: the user's own " +
|
|
903
|
-
"plus every team they belong to. Empty query lists everything, newest first.",
|
|
904
|
-
inputSchema: {
|
|
905
|
-
query: z.string().default(""),
|
|
906
|
-
scope: z.string().optional().describe('"mine", "all" (default), or a team slug'),
|
|
907
|
-
},
|
|
908
|
-
},
|
|
909
|
-
async ({ query, scope }) => {
|
|
910
|
-
try {
|
|
911
|
-
const rows = await passalong.list(query, { scope: scope || "" });
|
|
912
|
-
return json({ guides: rows });
|
|
913
|
-
} catch (err) {
|
|
914
|
-
return fail(err);
|
|
915
|
-
}
|
|
916
|
-
},
|
|
917
|
-
);
|
|
918
|
-
|
|
919
|
-
server.registerTool(
|
|
920
|
-
"inbox",
|
|
921
|
-
{
|
|
922
|
-
title: "Inbox",
|
|
923
|
-
annotations: READS,
|
|
924
|
-
description:
|
|
925
|
-
"Guides handed to this user (or to their teams) that they have not pulled yet. Call it " +
|
|
926
|
-
"when starting work so handoffs are not missed. Needs sync (passalong login).",
|
|
927
|
-
inputSchema: {},
|
|
928
|
-
},
|
|
929
|
-
async () => {
|
|
930
|
-
try {
|
|
931
|
-
return json({ guides: await passalong.inbox() });
|
|
932
|
-
} catch (err) {
|
|
933
|
-
return fail(err);
|
|
934
|
-
}
|
|
935
|
-
},
|
|
936
|
-
);
|
|
937
|
-
|
|
938
|
-
server.registerTool(
|
|
939
|
-
"board",
|
|
940
|
-
{
|
|
941
|
-
title: "Board",
|
|
942
|
-
annotations: READS,
|
|
943
|
-
description:
|
|
944
|
-
"The state of this user's transfers as queues: waiting on you (handed to you, not " +
|
|
945
|
-
"pulled), not working (someone gave it a failing verdict — the most urgent), in flight " +
|
|
946
|
-
"(handed over, nobody has taken it — `stale` means it has sat over a week), and landed " +
|
|
947
|
-
"(someone else has it), and `held`: what this worktree's agents hold now. Use it to answer " +
|
|
948
|
-
"'what is outstanding?' and to see what you hold, without taking anything.",
|
|
949
|
-
inputSchema: {},
|
|
950
|
-
},
|
|
951
|
-
async () => {
|
|
952
|
-
try {
|
|
953
|
-
const [board, now] = await Promise.all([
|
|
954
|
-
passalong.board(),
|
|
955
|
-
passalong.now().catch(() => ({ held: [] })),
|
|
956
|
-
]);
|
|
957
|
-
return json({ ...board, held: now.held });
|
|
958
|
-
} catch (err) {
|
|
959
|
-
return fail(err);
|
|
960
|
-
}
|
|
961
|
-
},
|
|
962
|
-
);
|
|
963
|
-
|
|
964
|
-
server.registerTool(
|
|
965
|
-
"log",
|
|
966
|
-
{
|
|
967
|
-
title: "What this user did",
|
|
968
|
-
annotations: READS,
|
|
969
|
-
description:
|
|
970
|
-
"This user's own acts on guides, newest first: what they published, what they took " +
|
|
971
|
-
"delivery of, and every verdict and ack they gave. Each item has a rendered `text` line " +
|
|
972
|
-
"and the guide's repo. Use it to answer 'what have I been working on', to write a " +
|
|
973
|
-
"standup or a weekly summary, or to find work from a repo by when it happened rather " +
|
|
974
|
-
"than by what it was called. The hub calls this page Activities, so 'my activities' means " +
|
|
975
|
-
"this tool. It is the opposite of `activity`, which is what other people did. IMPORTANT: it records what was passed along, not what was worked on — work " +
|
|
976
|
-
"that never became a guide has no entry, so never present it as a complete record of " +
|
|
977
|
-
"this user's work, and never infer that a quiet period was an idle one.",
|
|
978
|
-
inputSchema: {
|
|
979
|
-
repo: z.string().default("").describe("narrow to guides whose source repo matches this"),
|
|
980
|
-
since: z
|
|
981
|
-
.string()
|
|
982
|
-
.default("")
|
|
983
|
-
.describe("only what happened on or after this date: 2026, 2026-09, or 2026-09-11"),
|
|
984
|
-
},
|
|
985
|
-
},
|
|
986
|
-
async ({ repo, since }) => {
|
|
987
|
-
try {
|
|
988
|
-
return json({ log: await passalong.log({ repo: repo || "", since: since || "" }) });
|
|
989
|
-
} catch (err) {
|
|
990
|
-
return fail(err);
|
|
991
|
-
}
|
|
992
|
-
},
|
|
993
|
-
);
|
|
994
|
-
|
|
995
|
-
server.registerTool(
|
|
996
|
-
"activity",
|
|
997
|
-
{
|
|
998
|
-
title: "Activity",
|
|
999
|
-
annotations: READS,
|
|
1000
|
-
description:
|
|
1001
|
-
"What has happened to this user's guides and handoffs: who pulled one, who archived one, " +
|
|
1002
|
-
"who was handed what, who joined a team. Each item has a ready-made `text` line. This is " +
|
|
1003
|
-
"the hub's notifications, not its Activities page — for the user's own acts ('my " +
|
|
1004
|
-
"activities'), use log. This only reads; clear_activity is what marks the feed seen.",
|
|
1005
|
-
inputSchema: {
|
|
1006
|
-
all: z.boolean().default(false).describe("include what the user has already seen"),
|
|
1007
|
-
},
|
|
1008
|
-
},
|
|
1009
|
-
async ({ all }) => {
|
|
1010
|
-
try {
|
|
1011
|
-
return json(await passalong.activity({ all }));
|
|
1012
|
-
} catch (err) {
|
|
1013
|
-
return fail(err);
|
|
1014
|
-
}
|
|
1015
|
-
},
|
|
1016
|
-
);
|
|
1017
|
-
|
|
1018
|
-
/**
|
|
1019
|
-
* Split out of `activity`, which took `mark_read` and so read on one call and wrote on the next.
|
|
1020
|
-
*
|
|
1021
|
-
* An annotation cannot say "sometimes": `readOnlyHint` was false for a tool that almost always
|
|
1022
|
-
* only reads, which is a client being told the cautious lie on every call rather than the truth
|
|
1023
|
-
* on the one that matters. Two tools, each true about itself.
|
|
1024
|
-
*/
|
|
1025
|
-
server.registerTool(
|
|
1026
|
-
"clear_activity",
|
|
1027
|
-
{
|
|
1028
|
-
title: "Mark activity seen",
|
|
1029
|
-
annotations: ADDS,
|
|
1030
|
-
description:
|
|
1031
|
-
"Clear the user's unread feed, after they have been shown what is in it. Read it with " +
|
|
1032
|
-
"activity first — clearing what nobody was told about loses it.",
|
|
1033
|
-
inputSchema: {},
|
|
1034
|
-
},
|
|
1035
|
-
async () => {
|
|
1036
|
-
try {
|
|
1037
|
-
const res = await passalong.activity({ all: false });
|
|
1038
|
-
if (!res.unread) return json({ cleared: 0, unread: 0 });
|
|
1039
|
-
await passalong.seen();
|
|
1040
|
-
return json({ cleared: res.unread, unread: 0 });
|
|
1041
|
-
} catch (err) {
|
|
1042
|
-
return fail(err);
|
|
1043
|
-
}
|
|
1044
|
-
},
|
|
1045
|
-
);
|
|
1046
|
-
|
|
1047
|
-
server.registerTool(
|
|
1048
|
-
"plan_tasks",
|
|
1049
|
-
{
|
|
1050
|
-
title: "Plan a goal as tasks",
|
|
1051
|
-
annotations: ADDS,
|
|
1052
|
-
description:
|
|
1053
|
-
"Write tasks for agents: one step for a single task, or a larger goal broken into steps " +
|
|
1054
|
-
"an agent can each finish and a person can each check, written as drafts in order. The " +
|
|
1055
|
-
"tool for any task, rather than publish_guide. Give each step a Goal and an Acceptance " +
|
|
1056
|
-
"a person can run. " +
|
|
1057
|
-
"`after` names the earlier steps (by position, from 0) a step needs; it waits for them " +
|
|
1058
|
-
"to be approved, and steps that need nothing of each other can run side by side. Keep a " +
|
|
1059
|
-
"step to one sitting of work in one repo. Nothing runs until the user makes them ready.",
|
|
1060
|
-
inputSchema: {
|
|
1061
|
-
steps: z
|
|
1062
|
-
.array(
|
|
1063
|
-
z.object({
|
|
1064
|
-
title: z.string(),
|
|
1065
|
-
summary: z
|
|
1066
|
-
.string()
|
|
1067
|
-
.describe(
|
|
1068
|
-
"one or two plain sentences for a PERSON, no ids or paths: what this task is and " +
|
|
1069
|
-
"why it matters. The goal below is for the agent; this is what a person reads",
|
|
1070
|
-
),
|
|
1071
|
-
goal: z.string().describe("what is true when this step is done"),
|
|
1072
|
-
acceptance: z.string().describe("checks a person can run, one per line"),
|
|
1073
|
-
context: z.string().optional(),
|
|
1074
|
-
constraints: z.string().optional(),
|
|
1075
|
-
out_of_scope: z.string().optional(),
|
|
1076
|
-
target_context: z
|
|
1077
|
-
.string()
|
|
1078
|
-
.optional()
|
|
1079
|
-
.describe("owner/repo it is for; default is the repo you are in, '' for none"),
|
|
1080
|
-
after: z.array(z.number().int()).optional().describe("earlier steps this one needs"),
|
|
1081
|
-
}),
|
|
1082
|
-
)
|
|
1083
|
-
.min(1)
|
|
1084
|
-
.max(20),
|
|
1085
|
-
cwd: z.string().optional().describe("the repo you are planning from"),
|
|
1086
|
-
},
|
|
1087
|
-
},
|
|
1088
|
-
async ({ steps, cwd }) => {
|
|
1089
|
-
try {
|
|
1090
|
-
const ids = await passalong.planTasks(steps, { cwd: cwd || process.cwd() });
|
|
1091
|
-
return json({
|
|
1092
|
-
drafts: ids.map((id, i) => ({ id, title: steps[i].title, after: steps[i].after || [] })),
|
|
1093
|
-
next: `all in Draft: ask the user to read them and mark them Ready on the board at ${api.baseUrl()}/hub/work, or run \`passalong ready --all\``,
|
|
1094
|
-
});
|
|
1095
|
-
} catch (err) {
|
|
1096
|
-
return fail(err);
|
|
1097
|
-
}
|
|
1098
|
-
},
|
|
1099
|
-
);
|
|
1100
|
-
|
|
1101
|
-
server.registerTool(
|
|
1102
|
-
"take",
|
|
1103
|
-
{
|
|
1104
|
-
title: "Take work",
|
|
1105
|
-
annotations: ADDS,
|
|
1106
|
-
description:
|
|
1107
|
-
"Say you are doing it, and get it. With `id`, that guide — any kind: a task, a bug, a " +
|
|
1108
|
-
"handoff. With no id, the next thing waiting — always something new, never what you hold. " +
|
|
1109
|
-
"While you hold it no other agent can take it here, and its sender sees you are on it. You " +
|
|
1110
|
-
"may hold several at once; the person watches them on the board. If somebody else has it, the " +
|
|
1111
|
-
"answer says who — tell the user rather than doing the work twice. Every answer ends with " +
|
|
1112
|
-
"what to call next.",
|
|
1113
|
-
inputSchema: {
|
|
1114
|
-
id: z
|
|
1115
|
-
.string()
|
|
1116
|
-
.optional()
|
|
1117
|
-
.describe("passalong id or share link; leave out for the next one"),
|
|
1118
|
-
cwd: z
|
|
1119
|
-
.string()
|
|
1120
|
-
.optional()
|
|
1121
|
-
.describe("the worktree you are working in; default is the server's cwd"),
|
|
1122
|
-
any: z
|
|
1123
|
-
.boolean()
|
|
1124
|
-
.optional()
|
|
1125
|
-
.describe("a task for another repo, or none — only when the user asks"),
|
|
1126
|
-
},
|
|
1127
|
-
},
|
|
1128
|
-
async (args) => doTake(args),
|
|
1129
|
-
);
|
|
1130
|
-
|
|
1131
|
-
server.registerTool(
|
|
1132
|
-
"progress",
|
|
1133
|
-
{
|
|
1134
|
-
title: "Report progress",
|
|
1135
|
-
outputSchema: progressOut,
|
|
1136
|
-
annotations: ADDS,
|
|
1137
|
-
description:
|
|
1138
|
-
"Say you are still on what you hold, with a one-line note the hub shows. 30 minutes " +
|
|
1139
|
-
"without one marks it stalled. If the answer says you no longer hold it, stop.",
|
|
1140
|
-
inputSchema: {
|
|
1141
|
-
id: z.string().describe("the id of what you hold"),
|
|
1142
|
-
note: z
|
|
1143
|
-
.string()
|
|
1144
|
-
.optional()
|
|
1145
|
-
.describe(
|
|
1146
|
-
'one plain sentence in the first person, as you would tell the person: "Reading ' +
|
|
1147
|
-
'the settings page to find where the toggle goes." Start it with BLOCKED: when ' +
|
|
1148
|
-
"you are stopped and need an answer",
|
|
1149
|
-
),
|
|
1150
|
-
cwd: z
|
|
1151
|
-
.string()
|
|
1152
|
-
.optional()
|
|
1153
|
-
.describe("the worktree you are working in; default is the server's cwd"),
|
|
1154
|
-
},
|
|
1155
|
-
},
|
|
1156
|
-
async (args) => doProgress(args),
|
|
1157
|
-
);
|
|
1158
|
-
|
|
1159
|
-
server.registerTool(
|
|
1160
|
-
"hand_in",
|
|
1161
|
-
{
|
|
1162
|
-
title: "Hand it in",
|
|
1163
|
-
outputSchema: handInOut,
|
|
1164
|
-
annotations: ADDS,
|
|
1165
|
-
description:
|
|
1166
|
-
"Finished here, with proof. IT IS FINAL: the work goes to Done with what you showed, and nobody reviews it after you (anyone can reopen it with a reason). So SHOW IT: a screenshot for anything with a screen — before and after, for a bug — or, for backend work, the one command and the line of its output that shows it works. Send `checks`: one entry per " +
|
|
1167
|
-
"line the guide asks for — `## Acceptance` on a task, `## Verification` on a handoff or a " +
|
|
1168
|
-
"bug — each with that line and the evidence for it. That is what its author reads, line " +
|
|
1169
|
-
"against line, instead of hunting through a wall of output for the part that answers each " +
|
|
1170
|
-
"one, AND IT IS HOW YOU KNOW YOU ARE DONE: every line has one. Give a check `cmd` when a " +
|
|
1171
|
-
"command proves it: the command is run here before the hand-in lands, a non-zero exit " +
|
|
1172
|
-
"refuses it, and what it printed is recorded instead of your account of it. Use `ran` for " +
|
|
1173
|
-
"a check no command can settle. `evidence` as one block is the older shape and still " +
|
|
1174
|
-
"works. A task needs no write-up guide any more. A handoff or a bug takes `ok`, whether its " +
|
|
1175
|
-
"Verification held here, and `note` saying what went wrong when it did not. DO NOT " +
|
|
1176
|
-
"publish a guide to carry your evidence: it belongs on this call, and a second document " +
|
|
1177
|
-
'titled "Hand-in evidence" is the thing `checks` exists to replace. If it worked but you ' +
|
|
1178
|
-
"had to adapt something, that goes in `writeup` and the next reader of the guide is shown " +
|
|
1179
|
-
"it — that is its home. A follow-up guide is for work somebody else should now do, not " +
|
|
1180
|
-
"for answering.",
|
|
1181
|
-
inputSchema: {
|
|
1182
|
-
id: z.string().describe("passalong id"),
|
|
1183
|
-
evidence: z
|
|
1184
|
-
.string()
|
|
1185
|
-
.optional()
|
|
1186
|
-
.describe(
|
|
1187
|
-
"what you ran and what came back: the command and the lines that decided it, a test " +
|
|
1188
|
-
'summary, a link to the change, or a screenshot url. "it works" is a claim, not evidence',
|
|
1189
|
-
),
|
|
1190
|
-
checks: z
|
|
1191
|
-
.array(
|
|
1192
|
-
// A bare string is a check line with nothing behind it. Let it through to the server's
|
|
1193
|
-
// refusal, which says what to send, rather than a schema error that says only "expected
|
|
1194
|
-
// object".
|
|
1195
|
-
z.preprocess(
|
|
1196
|
-
(v) => (typeof v === "string" ? { check: v } : v),
|
|
1197
|
-
z.object({
|
|
1198
|
-
check: z
|
|
1199
|
-
.string()
|
|
1200
|
-
.describe(
|
|
1201
|
-
"the line this answers, in the guide's own words: an Acceptance line on a task, " +
|
|
1202
|
-
"a Verification line on a handoff or a bug",
|
|
1203
|
-
),
|
|
1204
|
-
says: z
|
|
1205
|
-
.string()
|
|
1206
|
-
.optional()
|
|
1207
|
-
.describe(
|
|
1208
|
-
'what happened, in one plain sentence for a PERSON: "Swagger lists all six ' +
|
|
1209
|
-
'endpoints, each with request and response schemas." It is the row they read ' +
|
|
1210
|
-
"in the review table; the evidence stays folded behind it. Strongly " +
|
|
1211
|
-
"recommended: a check without one shows only the line it answers",
|
|
1212
|
-
),
|
|
1213
|
-
ran: z
|
|
1214
|
-
.string()
|
|
1215
|
-
.optional()
|
|
1216
|
-
.describe(
|
|
1217
|
-
"what you ran for it, and what came back; not needed when `cmd` is set",
|
|
1218
|
-
),
|
|
1219
|
-
cmd: z
|
|
1220
|
-
.string()
|
|
1221
|
-
.optional()
|
|
1222
|
-
.describe(
|
|
1223
|
-
"the shell command that proves this line, run here before the hand-in lands — " +
|
|
1224
|
-
"its exit code decides the check and its output is recorded as the evidence. " +
|
|
1225
|
-
"Non-zero refuses the hand-in. Leave it out for a check nobody can run, like " +
|
|
1226
|
-
'"the badge reads 3" — then SHOW it with attach_screenshot and put the line it returns in `ran`; a check is run or shown, and describing what you saw is refused',
|
|
1227
|
-
),
|
|
1228
|
-
}),
|
|
1229
|
-
),
|
|
1230
|
-
)
|
|
1231
|
-
.optional()
|
|
1232
|
-
.describe(
|
|
1233
|
-
"one entry per line the guide asks for — Acceptance on a task, Verification on a " +
|
|
1234
|
-
"handoff or a bug — in the order you worked them. Each is RUN (`cmd`) or SHOWN (an " +
|
|
1235
|
-
"attach_screenshot line in `ran`). Pasting a command you typed into `ran` is not " +
|
|
1236
|
-
"running it and is refused",
|
|
1237
|
-
),
|
|
1238
|
-
ok: z.boolean().optional().describe("handoff or bug: did its Verification hold"),
|
|
1239
|
-
note: z
|
|
1240
|
-
.string()
|
|
1241
|
-
.describe(
|
|
1242
|
-
"one plain sentence, in the first person, saying what you did and how it went: " +
|
|
1243
|
-
'"Added the toggle to Settings; it survives a reload." It is the first thing the ' +
|
|
1244
|
-
"person reads, and the evidence is behind it. Say what went wrong when ok is false",
|
|
1245
|
-
),
|
|
1246
|
-
writeup: z
|
|
1247
|
-
.string()
|
|
1248
|
-
.optional()
|
|
1249
|
-
.describe(
|
|
1250
|
-
"what you had to adapt to make it work here — a version, a name, a step that needed " +
|
|
1251
|
-
"something the guide does not mention. 500 characters at most; anything longer " +
|
|
1252
|
-
"belongs in the change. Optional: leave it out when it worked as written. The next " +
|
|
1253
|
-
"person to open the guide is shown it",
|
|
1254
|
-
),
|
|
1255
|
-
risk: z
|
|
1256
|
-
.string()
|
|
1257
|
-
.optional()
|
|
1258
|
-
.describe(
|
|
1259
|
-
"what this could break, in one line — the part of a PR template's Risk that is " +
|
|
1260
|
-
"worth reading: a shared helper changed, a migration that cannot be undone, a " +
|
|
1261
|
-
"route other clients call. Optional; leave it out when there is nothing specific, " +
|
|
1262
|
-
'rather than writing "low risk". Its author reads it before accepting the work',
|
|
1263
|
-
),
|
|
1264
|
-
markdown: z
|
|
1265
|
-
.string()
|
|
1266
|
-
.optional()
|
|
1267
|
-
.describe("older shape, not needed: say what you adapted in `writeup` instead"),
|
|
1268
|
-
report: z
|
|
610
|
+
"READ work without taking it. With `id`, that piece of work — its markdown, read only; " +
|
|
611
|
+
"work over 20,000 characters comes back as an outline unless you ask for `section` or " +
|
|
612
|
+
"`full`. With `query`, the work whose words match. With neither, the board: what needs " +
|
|
613
|
+
"the person, who is on what, what is ready, and what this worktree holds. To DO the work, " +
|
|
614
|
+
"call take instead: it is the only way anyone learns somebody picked it up.",
|
|
615
|
+
inputSchema: {
|
|
616
|
+
id: z.string().optional().describe("passalong id (e.g. k3mq2xa7) or share link"),
|
|
617
|
+
query: z.string().optional().describe("words to look for in titles, tags and text"),
|
|
618
|
+
scope: z
|
|
1269
619
|
.string()
|
|
1270
620
|
.optional()
|
|
1271
|
-
.describe(
|
|
1272
|
-
|
|
1273
|
-
cwd: z
|
|
621
|
+
.describe('with query: "mine", "all" (default), or a team slug'),
|
|
622
|
+
section: z
|
|
1274
623
|
.string()
|
|
1275
624
|
.optional()
|
|
1276
|
-
.describe("the
|
|
625
|
+
.describe("with id: only the part under this heading; see the outline"),
|
|
626
|
+
full: z.boolean().optional().describe("with id: the whole text, however long"),
|
|
1277
627
|
},
|
|
1278
628
|
},
|
|
1279
|
-
async (
|
|
629
|
+
async ({ id, query, scope, section, full }) => {
|
|
630
|
+
try {
|
|
631
|
+
if (id) {
|
|
632
|
+
const { work } = await passalong.resolve(id, { peek: true });
|
|
633
|
+
const markdown = serialize(work);
|
|
634
|
+
const ref = work.meta.id || id;
|
|
635
|
+
if (section) {
|
|
636
|
+
const part = sectionOf(markdown, section);
|
|
637
|
+
return text(
|
|
638
|
+
part
|
|
639
|
+
? `${part.text}\n\n<!-- passalong: ${ref}, "${part.heading}" only; ask with full: true for all of it -->`
|
|
640
|
+
: `No heading matching "${section}".\n\n${outlineText(markdown, { id: ref })}`,
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
if (!full && markdown.length > WORK_WARN) return text(outlineText(markdown, { id: ref }));
|
|
644
|
+
return text(
|
|
645
|
+
`${await passalong.parentWork(work.meta)}${markdown}\n\n` +
|
|
646
|
+
`<!-- passalong: reading it does not make it yours: to work on it, call take ${ref}. -->`,
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
if (query !== undefined && query !== "")
|
|
650
|
+
return json({ work: await passalong.list(query, { scope: scope || "" }) });
|
|
651
|
+
const [board, now] = await Promise.all([
|
|
652
|
+
passalong.board(),
|
|
653
|
+
passalong.now().catch(() => ({ held: [] })),
|
|
654
|
+
]);
|
|
655
|
+
return json({ ...board, held: now.held });
|
|
656
|
+
} catch (err) {
|
|
657
|
+
return fail(err);
|
|
658
|
+
}
|
|
659
|
+
},
|
|
1280
660
|
);
|
|
1281
661
|
|
|
1282
662
|
server.registerTool(
|
|
1283
|
-
"
|
|
663
|
+
"update_work",
|
|
1284
664
|
{
|
|
1285
|
-
title: "
|
|
1286
|
-
|
|
1287
|
-
annotations:
|
|
665
|
+
title: "Change work",
|
|
666
|
+
// Destructive because `markdown` replaces the text that was there.
|
|
667
|
+
annotations: REMOVES,
|
|
1288
668
|
description:
|
|
1289
|
-
"
|
|
1290
|
-
"
|
|
1291
|
-
"
|
|
1292
|
-
"
|
|
1293
|
-
"
|
|
1294
|
-
"
|
|
669
|
+
"Change work that exists, when the person asks or when you wrote it. `status`: ready " +
|
|
670
|
+
"(whoever it is for may take it), draft (held for a person to read), or archived (off the " +
|
|
671
|
+
"board; it comes back to Draft). `to`: who it is for — @handle, several as @ada,@bola, " +
|
|
672
|
+
'#group, or "" for the whole team; whoever held it and is left out has it taken back and ' +
|
|
673
|
+
"is told. `markdown`: its whole text, frontmatter first, as find_work gave it — say " +
|
|
674
|
+
"what changed, keep what did not.",
|
|
1295
675
|
inputSchema: {
|
|
1296
676
|
id: z.string().describe("passalong id"),
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
"one clear question in the first person, a few sentences at most, plain text: " +
|
|
1301
|
-
'"Should the dark-mode toggle live in Settings or the header? The header is ' +
|
|
1302
|
-
'crowded on mobile." What you were about to do, if they just say yes, helps',
|
|
1303
|
-
),
|
|
1304
|
-
cwd: z
|
|
677
|
+
status: z.enum(["ready", "draft", "archived"]).optional(),
|
|
678
|
+
to: z.string().optional().describe('"@handle", "@ada,@bola", "#group", or "" for the team'),
|
|
679
|
+
markdown: z
|
|
1305
680
|
.string()
|
|
1306
681
|
.optional()
|
|
1307
|
-
.describe("the
|
|
682
|
+
.describe("the whole document, frontmatter first, to replace its text"),
|
|
683
|
+
cwd: z.string().optional().describe("the repo you are working in"),
|
|
1308
684
|
},
|
|
1309
685
|
},
|
|
1310
|
-
async (
|
|
686
|
+
async ({ id, status, to, markdown, cwd }) => {
|
|
687
|
+
try {
|
|
688
|
+
if (!markdown && !status && to === undefined)
|
|
689
|
+
throw new Error("say what to change: status, to, or markdown");
|
|
690
|
+
const done = { id };
|
|
691
|
+
if (markdown)
|
|
692
|
+
done.url = (await passalong.updateText(id, markdown, { cwd: cwd || process.cwd() })).url;
|
|
693
|
+
if (to !== undefined) done.to = (await passalong.assign(id, to)).to ?? to;
|
|
694
|
+
if (status) {
|
|
695
|
+
await passalong.setStatus(id, STATUS[status]);
|
|
696
|
+
done.status = status;
|
|
697
|
+
}
|
|
698
|
+
return json(done);
|
|
699
|
+
} catch (err) {
|
|
700
|
+
return fail(err);
|
|
701
|
+
}
|
|
702
|
+
},
|
|
1311
703
|
);
|
|
1312
704
|
|
|
705
|
+
// ---- projects and documents -----------------------------------------------------------------
|
|
706
|
+
|
|
1313
707
|
server.registerTool(
|
|
1314
|
-
"
|
|
708
|
+
"find_projects",
|
|
1315
709
|
{
|
|
1316
|
-
title: "
|
|
1317
|
-
|
|
1318
|
-
annotations: ADDS,
|
|
710
|
+
title: "Find projects",
|
|
711
|
+
annotations: READS,
|
|
1319
712
|
description:
|
|
1320
|
-
"
|
|
1321
|
-
"the
|
|
1322
|
-
"
|
|
713
|
+
"With `id`, one project: its documents, files and the projects inside it. With `query`, " +
|
|
714
|
+
"the projects whose name, description or any document mentions those words, each naming " +
|
|
715
|
+
"its matching documents. With neither, every project you can see. Each answer's `space` " +
|
|
716
|
+
'says where it lives: private, or a named team; `repo` is the repo it is built from, "" ' +
|
|
717
|
+
"when it has none. Read that repo when you write or check its documents.",
|
|
1323
718
|
inputSchema: {
|
|
1324
|
-
id: z.string().describe("
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
.string()
|
|
1328
|
-
.optional()
|
|
1329
|
-
.describe("the worktree you are working in; default is the server's cwd"),
|
|
719
|
+
id: z.string().optional().describe("project id"),
|
|
720
|
+
query: z.string().optional().describe("words to look for in projects and their documents"),
|
|
721
|
+
scope: z.string().optional().describe('"all" (default), "mine", or a team slug'),
|
|
1330
722
|
},
|
|
1331
723
|
},
|
|
1332
|
-
|
|
724
|
+
run(({ id, query, scope }) =>
|
|
725
|
+
id ? api.project(id) : api.projects(scope || "all", query || ""),
|
|
726
|
+
),
|
|
1333
727
|
);
|
|
1334
728
|
|
|
1335
729
|
server.registerTool(
|
|
1336
|
-
"
|
|
730
|
+
"save_project",
|
|
1337
731
|
{
|
|
1338
|
-
title: "
|
|
1339
|
-
annotations:
|
|
732
|
+
title: "Save a project",
|
|
733
|
+
annotations: ADDS,
|
|
1340
734
|
description:
|
|
1341
|
-
"
|
|
1342
|
-
"
|
|
1343
|
-
"
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
"
|
|
1347
|
-
"somebody picked it up.",
|
|
735
|
+
"With no `id`, make a project, after find_projects shows none fits: a `title` and a short " +
|
|
736
|
+
"`description`, and its `repo` when it has one. Leave out `team` for a private one, and " +
|
|
737
|
+
"pass a team slug only when the person wants it shared; `parent` puts it inside another, " +
|
|
738
|
+
'in that one\'s space. With `id`, change its title, description, repo ("" for none), ' +
|
|
739
|
+
'colour or place (`parent` "" for the top level); ' +
|
|
740
|
+
"its space never changes. The answer's `space` says where it is: tell the person.",
|
|
1348
741
|
inputSchema: {
|
|
1349
|
-
|
|
1350
|
-
|
|
742
|
+
id: z.string().optional().describe("the project to change; leave out to make one"),
|
|
743
|
+
title: z.string().optional().describe("name of the product or body of work"),
|
|
744
|
+
description: z.string().optional().describe("what it is for, in a line"),
|
|
745
|
+
team: z.string().optional().describe("new project only: team slug; omit for private"),
|
|
746
|
+
parent: z.string().optional().describe('project to put it inside; "" for the top level'),
|
|
747
|
+
color: z.enum(["", "coral", "amber", "green", "blue", "violet"]).optional(),
|
|
748
|
+
repo: z
|
|
1351
749
|
.string()
|
|
1352
750
|
.optional()
|
|
1353
|
-
.describe(
|
|
1354
|
-
outline: z
|
|
1355
|
-
.boolean()
|
|
1356
|
-
.optional()
|
|
1357
|
-
.describe("just the headings and their sizes, whatever the guide's length"),
|
|
1358
|
-
full: z
|
|
1359
|
-
.boolean()
|
|
1360
|
-
.optional()
|
|
1361
|
-
.describe("the whole document, even one over 20,000 characters"),
|
|
751
|
+
.describe('the repo it is built from, e.g. github.com/acme/billing; "" for none'),
|
|
1362
752
|
},
|
|
1363
753
|
},
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
return text(
|
|
1375
|
-
`${part.text}\n\n<!-- passalong: ${meta.id || ref}, "${part.heading}" only; ask with full: true for the whole guide -->`,
|
|
1376
|
-
);
|
|
1377
|
-
}
|
|
1378
|
-
if (outline || (!full && markdown.length > GUIDE_WARN))
|
|
1379
|
-
return text(
|
|
1380
|
-
`${outlineText(markdown, { id: meta.id || ref })}\n\n<!-- passalong: ${from} -->`,
|
|
1381
|
-
);
|
|
1382
|
-
const lead = leadFor(meta);
|
|
1383
|
-
const siblings = await related(meta);
|
|
1384
|
-
// Follow-ups are more context for this guide, so they come with it — after the document,
|
|
1385
|
-
// never inside it, so the guide an agent writes back out is still only the guide.
|
|
1386
|
-
const context = await passalong.followUps(meta);
|
|
1387
|
-
// The guide this one came out of, in front of the document: a follow-up read on its own
|
|
1388
|
-
// looks like a small piece of work and is a note on a bigger one.
|
|
1389
|
-
const follows = await passalong.parentGuide(meta);
|
|
1390
|
-
// After the document, with the other trailing comments, not in front of it. An
|
|
1391
|
-
// instruction that arrives with the payload is what gets read — but anything before the
|
|
1392
|
-
// opening `---` stops the frontmatter being frontmatter, and this fires on every guide
|
|
1393
|
-
// anyone was handed rather than only on bugs. The bug lead stays where it is: it is a
|
|
1394
|
-
// warning against executing the document, so being read first is its whole job.
|
|
1395
|
-
return text(
|
|
1396
|
-
`${follows}${lead}${markdown}${siblings}${context ? `\n\n${context}` : ""}` +
|
|
1397
|
-
`\n\n<!-- passalong: ${from} -->` +
|
|
1398
|
-
`\n${passalong.handoffNudge(meta)}` +
|
|
1399
|
-
`\n${passalong.followUpNote(meta)}`,
|
|
754
|
+
run(({ id, title, description, team, parent, color, repo }) => {
|
|
755
|
+
if (!id) {
|
|
756
|
+
if (!title) throw new Error("a new project needs a `title`");
|
|
757
|
+
return api.createProject(
|
|
758
|
+
title,
|
|
759
|
+
description || "",
|
|
760
|
+
team || "",
|
|
761
|
+
parent || "",
|
|
762
|
+
color || "",
|
|
763
|
+
repo || "",
|
|
1400
764
|
);
|
|
1401
|
-
} catch (err) {
|
|
1402
|
-
return fail(err);
|
|
1403
765
|
}
|
|
1404
|
-
|
|
766
|
+
if (team) throw new Error("a project's space does not change; leave out `team`");
|
|
767
|
+
const changes = Object.fromEntries(
|
|
768
|
+
Object.entries({ title, description, parent, color, repo }).filter(
|
|
769
|
+
([, v]) => v !== undefined,
|
|
770
|
+
),
|
|
771
|
+
);
|
|
772
|
+
return api.updateProject(id, changes);
|
|
773
|
+
}),
|
|
1405
774
|
);
|
|
1406
775
|
|
|
1407
776
|
server.registerTool(
|
|
1408
|
-
"
|
|
777
|
+
"read_document",
|
|
1409
778
|
{
|
|
1410
|
-
title: "
|
|
1411
|
-
annotations:
|
|
779
|
+
title: "Read a document",
|
|
780
|
+
annotations: READS,
|
|
1412
781
|
description:
|
|
1413
|
-
"
|
|
1414
|
-
"
|
|
1415
|
-
|
|
782
|
+
"One document's Markdown and its `version`, which save_document needs. `history: true` " +
|
|
783
|
+
"lists its earlier versions, newest first; `version` returns that version's text. To " +
|
|
784
|
+
"bring one back, save it over the current version.",
|
|
1416
785
|
inputSchema: {
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
data: z
|
|
1422
|
-
.string()
|
|
1423
|
-
.default("")
|
|
1424
|
-
.describe(
|
|
1425
|
-
"the image itself, base64 (a data: URL is fine), for when you have the bytes and no " +
|
|
1426
|
-
"file — a browser tool that hands back an image inline and never writes to disk. " +
|
|
1427
|
-
"5MB; larger belongs in create_upload",
|
|
1428
|
-
),
|
|
1429
|
-
type: z
|
|
1430
|
-
.string()
|
|
1431
|
-
.default("image/png")
|
|
1432
|
-
.describe("media type of `data`: image/png, image/jpeg, image/webp or image/gif"),
|
|
1433
|
-
name: z.string().default("").describe("label for the image; defaults to its filename"),
|
|
786
|
+
project: z.string().describe("project id"),
|
|
787
|
+
document: z.string().describe("document id"),
|
|
788
|
+
history: z.boolean().optional().describe("list its earlier versions"),
|
|
789
|
+
version: z.number().int().positive().optional().describe("read this earlier version"),
|
|
1434
790
|
},
|
|
1435
791
|
},
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
"send `file` (a path on this machine) or `data` (the image, base64). If your " +
|
|
1442
|
-
"screenshot is a tool result you cannot write out, capture it to a file instead " +
|
|
1443
|
-
"— a headless browser's page.screenshot({ path }) or your platform's capture " +
|
|
1444
|
-
"command — and pass that path.",
|
|
1445
|
-
),
|
|
1446
|
-
);
|
|
1447
|
-
}
|
|
1448
|
-
const shot = data
|
|
1449
|
-
? await passalong.attachBytes(data, { name: name || "", type: type || "image/png" })
|
|
1450
|
-
: await passalong.attach(file, { name: name || "" });
|
|
1451
|
-
return text(
|
|
1452
|
-
`${JSON.stringify({ id: shot.id, url: shot.url, bytes: shot.bytes }, null, 2)}\n\n` +
|
|
1453
|
-
`Put this in the guide body:\n${shot.markdown}`,
|
|
1454
|
-
);
|
|
1455
|
-
} catch (err) {
|
|
1456
|
-
return fail(err);
|
|
1457
|
-
}
|
|
1458
|
-
},
|
|
792
|
+
run(({ project, document, history, version }) =>
|
|
793
|
+
history || version
|
|
794
|
+
? api.projectDocumentHistory(project, document, version || 0)
|
|
795
|
+
: api.projectDocument(project, document),
|
|
796
|
+
),
|
|
1459
797
|
);
|
|
1460
798
|
|
|
1461
799
|
server.registerTool(
|
|
1462
|
-
"
|
|
800
|
+
"save_document",
|
|
1463
801
|
{
|
|
1464
|
-
title: "
|
|
1465
|
-
annotations:
|
|
802
|
+
title: "Save a document",
|
|
803
|
+
annotations: ADDS,
|
|
1466
804
|
description:
|
|
1467
|
-
"
|
|
1468
|
-
"
|
|
1469
|
-
"
|
|
1470
|
-
"
|
|
1471
|
-
"
|
|
805
|
+
"Write what someone needs to know into a project. With no `document`, a new one: `name` " +
|
|
806
|
+
"and `body`. With `document`, change it: `body` with the `version` read_document gave you " +
|
|
807
|
+
"— a newer edit is refused, so nobody's writing is lost — and/or `name` to rename it, " +
|
|
808
|
+
"and/or `move_to` another project in the same space. Its id and history stay. Update the " +
|
|
809
|
+
"page that already covers something rather than adding another. A document has no owner " +
|
|
810
|
+
"and no state and tells nobody: anything someone must still act on is work (create_work).",
|
|
1472
811
|
inputSchema: {
|
|
1473
|
-
|
|
1474
|
-
|
|
812
|
+
project: z.string().describe("project id"),
|
|
813
|
+
document: z.string().optional().describe("the document to change; leave out for a new one"),
|
|
814
|
+
name: z
|
|
815
|
+
.string()
|
|
816
|
+
.optional()
|
|
817
|
+
.describe("its name: required for a new one, a rename otherwise"),
|
|
818
|
+
body: z.string().optional().describe("the whole Markdown text"),
|
|
819
|
+
version: z
|
|
820
|
+
.number()
|
|
821
|
+
.int()
|
|
822
|
+
.positive()
|
|
823
|
+
.optional()
|
|
824
|
+
.describe("with `body` on an existing document: the version you read"),
|
|
825
|
+
move_to: z.string().optional().describe("a project id in the same space to move it into"),
|
|
1475
826
|
},
|
|
1476
827
|
},
|
|
1477
|
-
async ({
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
return
|
|
1481
|
-
`${JSON.stringify({ id: got.id, url: got.url, bytes: got.bytes }, null, 2)}\n\n` +
|
|
1482
|
-
`Put this in the guide body:\n${got.markdown}`,
|
|
1483
|
-
);
|
|
1484
|
-
} catch (err) {
|
|
1485
|
-
return fail(err);
|
|
828
|
+
run(async ({ project, document, name, body, version, move_to }) => {
|
|
829
|
+
if (!document) {
|
|
830
|
+
if (!name || body === undefined) throw new Error("a new document needs `name` and `body`");
|
|
831
|
+
return api.createProjectDocument(project, name, body);
|
|
1486
832
|
}
|
|
1487
|
-
|
|
833
|
+
let saved;
|
|
834
|
+
if (body !== undefined) {
|
|
835
|
+
if (!version) throw new Error("send the `version` read_document gave you with `body`");
|
|
836
|
+
saved = await api.saveProjectDocument(project, document, version, body);
|
|
837
|
+
}
|
|
838
|
+
if (name || move_to)
|
|
839
|
+
saved = await api.patchProjectDocument(project, document, {
|
|
840
|
+
...(name ? { name } : {}),
|
|
841
|
+
...(move_to ? { project: move_to } : {}),
|
|
842
|
+
});
|
|
843
|
+
if (!saved)
|
|
844
|
+
throw new Error("say what to change: `body` with `version`, `name`, or `move_to`");
|
|
845
|
+
return saved;
|
|
846
|
+
}),
|
|
1488
847
|
);
|
|
1489
848
|
|
|
1490
849
|
server.registerTool(
|
|
1491
|
-
"
|
|
850
|
+
"delete_document",
|
|
1492
851
|
{
|
|
1493
|
-
title: "
|
|
1494
|
-
annotations:
|
|
852
|
+
title: "Delete a document",
|
|
853
|
+
annotations: REMOVES,
|
|
1495
854
|
description:
|
|
1496
|
-
"
|
|
1497
|
-
"
|
|
1498
|
-
|
|
1499
|
-
"text, 1000 characters. `files` are paths on this machine: a picture is drawn in the " +
|
|
1500
|
-
"thread and anything else is a file to download. Only its author, whoever it is " +
|
|
1501
|
-
"assigned to, or whoever holds it can write; the agent reads it the next time it " +
|
|
1502
|
-
"checks in. Agents answering their own questions do not use this: they use ask and take.",
|
|
1503
|
-
inputSchema: {
|
|
1504
|
-
id: z.string().describe("passalong id"),
|
|
1505
|
-
text: z.string().default("").describe("what to say; may be empty when attaching a file"),
|
|
1506
|
-
files: z
|
|
1507
|
-
.array(z.string())
|
|
1508
|
-
.max(3)
|
|
1509
|
-
.default([])
|
|
1510
|
-
.describe("paths to up to three files or pictures to attach"),
|
|
1511
|
-
},
|
|
1512
|
-
},
|
|
1513
|
-
async ({ id, text: said, files }) => {
|
|
1514
|
-
try {
|
|
1515
|
-
const r = await passalong.reply(id, said, { files });
|
|
1516
|
-
return text(
|
|
1517
|
-
r?.noted
|
|
1518
|
-
? `Nobody holds ${id} yet, so this is a note: whoever takes it is handed it first.`
|
|
1519
|
-
: `Sent to whoever holds ${id}. It reads this the next time it checks in.`,
|
|
1520
|
-
);
|
|
1521
|
-
} catch (err) {
|
|
1522
|
-
return failWith(err);
|
|
1523
|
-
}
|
|
855
|
+
"Delete one document, with its earlier versions. It cannot be brought back, so do it only " +
|
|
856
|
+
"when the person asked for that document to go; to change what it says, use save_document.",
|
|
857
|
+
inputSchema: { project: z.string(), document: z.string() },
|
|
1524
858
|
},
|
|
859
|
+
run(({ project, document }) => api.deleteProjectDocument(project, document)),
|
|
1525
860
|
);
|
|
1526
861
|
|
|
862
|
+
// ---- files ----------------------------------------------------------------------------------
|
|
863
|
+
|
|
1527
864
|
server.registerTool(
|
|
1528
|
-
"
|
|
865
|
+
"add_file",
|
|
1529
866
|
{
|
|
1530
|
-
title: "
|
|
1531
|
-
annotations: {
|
|
867
|
+
title: "Add a file",
|
|
868
|
+
annotations: { ...ADDS, openWorldHint: true },
|
|
1532
869
|
description:
|
|
1533
|
-
"
|
|
1534
|
-
"
|
|
1535
|
-
"
|
|
1536
|
-
|
|
1537
|
-
"a single bug with `kind: bug`; use file_bugs for more than one; or a task for work nobody " +
|
|
1538
|
-
"has done yet with `kind: task`. Missing id, created, " +
|
|
1539
|
-
"author, and source_context are filled in. A screenshot belongs in the markdown: attach it " +
|
|
1540
|
-
"with attach_screenshot and put the line it returns in the body, because publishing claims " +
|
|
1541
|
-
"whatever the markdown names. " +
|
|
1542
|
-
'`to` addresses it to a team ("khaime") or a ' +
|
|
1543
|
-
'teammate ("khaime/lukman"), who is notified. Returns the id and share link.',
|
|
870
|
+
"Upload a picture or any other file — a path in `file`, or a picture's bytes in `data` " +
|
|
871
|
+
"when you have the image and no file — and get back the markdown line that points at it. " +
|
|
872
|
+
"Files up to 10MB, pictures up to 5MB. " +
|
|
873
|
+
WHERE_THE_LINE_GOES,
|
|
1544
874
|
inputSchema: {
|
|
1545
|
-
|
|
1546
|
-
|
|
875
|
+
file: z.string().optional().describe("path to the file on this machine"),
|
|
876
|
+
data: z
|
|
1547
877
|
.string()
|
|
1548
878
|
.optional()
|
|
1549
879
|
.describe(
|
|
1550
|
-
"
|
|
880
|
+
"a picture itself, base64 (a data: URL is fine), for when a tool handed you the image " +
|
|
881
|
+
"and never wrote it to disk",
|
|
1551
882
|
),
|
|
1552
|
-
|
|
1553
|
-
.string()
|
|
1554
|
-
.optional()
|
|
1555
|
-
.describe("directory the work happened in, used to infer source_context"),
|
|
1556
|
-
parent: z
|
|
883
|
+
type: z
|
|
1557
884
|
.string()
|
|
1558
885
|
.optional()
|
|
1559
|
-
.describe(
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
),
|
|
886
|
+
.describe("media type of `data`: image/png, image/jpeg, image/webp or image/gif"),
|
|
887
|
+
name: z.string().optional().describe("label for it; defaults to its filename"),
|
|
888
|
+
project: z.string().optional().describe("keep it in this project instead"),
|
|
1563
889
|
},
|
|
1564
890
|
},
|
|
1565
|
-
async ({
|
|
891
|
+
async ({ file, data, type, name, project }) => {
|
|
1566
892
|
try {
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
...(guide.meta.status === "draft"
|
|
1585
|
-
? {
|
|
1586
|
-
status: "draft",
|
|
1587
|
-
// The hub page, never the share link: the share page has no Ready button.
|
|
1588
|
-
next: `in Draft: ask the user to read it and mark it Ready at ${api.baseUrl()}/hub/g/${guide.meta.id}, or run \`passalong ready ${guide.meta.id}\``,
|
|
1589
|
-
}
|
|
1590
|
-
: {}),
|
|
1591
|
-
});
|
|
893
|
+
if (project) {
|
|
894
|
+
if (!file) throw new Error("a file for a project needs `file`, a path on this machine");
|
|
895
|
+
return json(await api.addProjectFile(project, file, name || ""));
|
|
896
|
+
}
|
|
897
|
+
if (!file && !data)
|
|
898
|
+
throw new Error(
|
|
899
|
+
"send `file` (a path on this machine) or `data` (a picture, base64). If your " +
|
|
900
|
+
"screenshot is a tool result you cannot write out, capture it to a file instead — a " +
|
|
901
|
+
"headless browser's page.screenshot({ path }) or your platform's capture command.",
|
|
902
|
+
);
|
|
903
|
+
const got = data
|
|
904
|
+
? await passalong.attachBytes(data, { name: name || "", type: type || "image/png" })
|
|
905
|
+
: await passalong.attachAny(file, { name: name || "" });
|
|
906
|
+
return text(
|
|
907
|
+
`${JSON.stringify({ id: got.id, url: got.url, bytes: got.bytes }, null, 2)}\n\n` +
|
|
908
|
+
`The line that points at it:\n${got.markdown}`,
|
|
909
|
+
);
|
|
1592
910
|
} catch (err) {
|
|
1593
911
|
return fail(err);
|
|
1594
912
|
}
|
|
@@ -1596,94 +914,28 @@ export function buildServer() {
|
|
|
1596
914
|
);
|
|
1597
915
|
|
|
1598
916
|
server.registerTool(
|
|
1599
|
-
"
|
|
917
|
+
"read_file",
|
|
1600
918
|
{
|
|
1601
|
-
title: "
|
|
1602
|
-
annotations:
|
|
919
|
+
title: "Read a file",
|
|
920
|
+
annotations: READS,
|
|
1603
921
|
description:
|
|
1604
|
-
"
|
|
1605
|
-
"
|
|
1606
|
-
|
|
1607
|
-
"pass, or a review that turned up defects; use publish_guide instead for work you " +
|
|
1608
|
-
"finished and want repeated elsewhere. If there is a screenshot of any of this, it is " +
|
|
1609
|
-
"evidence: call attach_screenshot first and pass what it returns as that issue's " +
|
|
1610
|
-
"`evidence`. Describing a screenshot instead of attaching it throws away the most useful " +
|
|
1611
|
-
"thing in the report. Needs sync (`passalong login`).",
|
|
1612
|
-
inputSchema: {
|
|
1613
|
-
title: z
|
|
1614
|
-
.string()
|
|
1615
|
-
.optional()
|
|
1616
|
-
.describe('what the sweep was, e.g. "Checkout regression pass, 8 Sep build"'),
|
|
1617
|
-
environment: z
|
|
1618
|
-
.string()
|
|
1619
|
-
.optional()
|
|
1620
|
-
.describe("production, staging or development — where you saw these"),
|
|
1621
|
-
to: z
|
|
1622
|
-
.string()
|
|
1623
|
-
.optional()
|
|
1624
|
-
.describe(
|
|
1625
|
-
"team slug, team/@handle for one teammate, team/@ada,@bola for several named teammates, or team/#group for a set of them",
|
|
1626
|
-
),
|
|
1627
|
-
issues: z
|
|
1628
|
-
.array(
|
|
1629
|
-
z.object({
|
|
1630
|
-
evidence: z
|
|
1631
|
-
.array(z.string())
|
|
1632
|
-
.default([])
|
|
1633
|
-
.describe(
|
|
1634
|
-
"screenshot URLs from attach_screenshot, or the markdown lines it returned; " +
|
|
1635
|
-
"they go under Problem, where a reader looks first",
|
|
1636
|
-
),
|
|
1637
|
-
title: z.string().describe("what is broken, in one line"),
|
|
1638
|
-
summary: z
|
|
1639
|
-
.string()
|
|
1640
|
-
.optional()
|
|
1641
|
-
.describe(
|
|
1642
|
-
"one or two plain sentences for a PERSON, no ids or paths: what is broken and " +
|
|
1643
|
-
"whether anyone needs to act. Defaults to the title",
|
|
1644
|
-
),
|
|
1645
|
-
problem: z
|
|
1646
|
-
.string()
|
|
1647
|
-
.describe(
|
|
1648
|
-
"what is broken and what it stops someone doing; the error if there is one",
|
|
1649
|
-
),
|
|
1650
|
-
reproduce: z
|
|
1651
|
-
.string()
|
|
1652
|
-
.describe(
|
|
1653
|
-
"numbered steps that show the bug — these produce it, they are not a fix",
|
|
1654
|
-
),
|
|
1655
|
-
verification: z
|
|
1656
|
-
.string()
|
|
1657
|
-
.optional()
|
|
1658
|
-
.describe(
|
|
1659
|
-
"the behaviour that should have happened, as something a fixer can check",
|
|
1660
|
-
),
|
|
1661
|
-
gotchas: z
|
|
1662
|
-
.string()
|
|
1663
|
-
.optional()
|
|
1664
|
-
.describe("anything already ruled out, or that made it hard to pin down"),
|
|
1665
|
-
area: z
|
|
1666
|
-
.string()
|
|
1667
|
-
.optional()
|
|
1668
|
-
.describe(`which surface it is on — known: ${AREAS.map((a) => a.slug).join(", ")}`),
|
|
1669
|
-
severity: z
|
|
1670
|
-
.string()
|
|
1671
|
-
.optional()
|
|
1672
|
-
.describe("s1 blocker, s2 major, s3 minor, s4 cosmetic (default s3)"),
|
|
1673
|
-
}),
|
|
1674
|
-
)
|
|
1675
|
-
.min(1)
|
|
1676
|
-
.describe("one entry per defect; file them together rather than one call each"),
|
|
1677
|
-
cwd: z.string().optional().describe("directory the bugs were found in"),
|
|
1678
|
-
},
|
|
922
|
+
"Read a picture or a small text file kept in a project into this conversation. Other " +
|
|
923
|
+
"files stay downloads in the hub.",
|
|
924
|
+
inputSchema: { project: z.string(), file: z.string().describe("file id") },
|
|
1679
925
|
},
|
|
1680
|
-
async ({
|
|
926
|
+
async ({ project, file }) => {
|
|
1681
927
|
try {
|
|
1682
|
-
const
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
928
|
+
const got = await api.projectFile(project, file);
|
|
929
|
+
const { data, text: body, ...meta } = got;
|
|
930
|
+
return {
|
|
931
|
+
content: data
|
|
932
|
+
? [
|
|
933
|
+
{ type: "text", text: got.name },
|
|
934
|
+
{ type: "image", data, mimeType: got.type },
|
|
935
|
+
]
|
|
936
|
+
: [{ type: "text", text: `${got.name}\n\n${body || ""}` }],
|
|
937
|
+
structuredContent: meta,
|
|
938
|
+
};
|
|
1687
939
|
} catch (err) {
|
|
1688
940
|
return fail(err);
|
|
1689
941
|
}
|
|
@@ -1691,100 +943,55 @@ export function buildServer() {
|
|
|
1691
943
|
);
|
|
1692
944
|
|
|
1693
945
|
server.registerTool(
|
|
1694
|
-
"
|
|
946
|
+
"delete_file",
|
|
1695
947
|
{
|
|
1696
|
-
title: "
|
|
1697
|
-
annotations:
|
|
948
|
+
title: "Delete a file",
|
|
949
|
+
annotations: REMOVES,
|
|
1698
950
|
description:
|
|
1699
|
-
"
|
|
1700
|
-
"
|
|
1701
|
-
|
|
1702
|
-
inputSchema: {
|
|
1703
|
-
kind: z
|
|
1704
|
-
.enum(["transfer", "bug", "task"])
|
|
1705
|
-
.optional()
|
|
1706
|
-
.describe(
|
|
1707
|
-
"task (default) for work nobody has done yet; transfer for finished work to repeat; " +
|
|
1708
|
-
"bug for a defect to fix",
|
|
1709
|
-
),
|
|
1710
|
-
},
|
|
951
|
+
"Delete one file from a project. It cannot be brought back, so do it only when the person " +
|
|
952
|
+
"asked for that file to go.",
|
|
953
|
+
inputSchema: { project: z.string(), file: z.string().describe("file id") },
|
|
1711
954
|
},
|
|
1712
|
-
|
|
955
|
+
run(({ project, file }) => api.deleteProjectFile(project, file)),
|
|
1713
956
|
);
|
|
1714
957
|
|
|
958
|
+
// ---- account ----------------------------------------------------------------------------------
|
|
959
|
+
|
|
1715
960
|
server.registerTool(
|
|
1716
961
|
"accounts",
|
|
1717
962
|
{
|
|
1718
963
|
title: "Accounts on this machine",
|
|
1719
|
-
|
|
964
|
+
// Read-only to the world: `use` is this process remembering a name until it exits, and
|
|
965
|
+
// nothing on disk or on the server changes, so the machine's default stays where it was.
|
|
966
|
+
annotations: { ...READS, idempotentHint: true },
|
|
1720
967
|
description:
|
|
1721
968
|
"The Passalong accounts signed in on this machine, which one this session is using, and " +
|
|
1722
|
-
"whether it still has to be chosen. With two or more and nothing chosen, every call
|
|
1723
|
-
"
|
|
1724
|
-
|
|
1725
|
-
},
|
|
1726
|
-
async () => {
|
|
1727
|
-
const list = api.accountList();
|
|
1728
|
-
return json({
|
|
1729
|
-
accounts: list,
|
|
1730
|
-
in_use: api.accountInUse(),
|
|
1731
|
-
must_choose: api.needsChoice(),
|
|
1732
|
-
...(api.needsChoice() ? { next: api.choiceMessage() } : {}),
|
|
1733
|
-
});
|
|
1734
|
-
},
|
|
1735
|
-
);
|
|
1736
|
-
|
|
1737
|
-
server.registerTool(
|
|
1738
|
-
"use_account",
|
|
1739
|
-
{
|
|
1740
|
-
title: "Use an account for this session",
|
|
1741
|
-
annotations: { ...ADDS, idempotentHint: true },
|
|
1742
|
-
description:
|
|
1743
|
-
"Say which signed-in account this session acts as. Call it only with a name the PERSON " +
|
|
1744
|
-
"gave you: when a machine has more than one account and none is chosen, ask them which, " +
|
|
1745
|
-
"in plain words, then pass their answer. It lasts until this session ends and changes " +
|
|
1746
|
-
"nothing on disk, so it never moves the machine's default.",
|
|
969
|
+
"whether it still has to be chosen. With two or more and nothing chosen, every other call " +
|
|
970
|
+
"is refused until `use` says which: ask the PERSON, in plain words, and pass their " +
|
|
971
|
+
"answer. It lasts until this session ends and never moves the machine's default.",
|
|
1747
972
|
inputSchema: {
|
|
1748
|
-
|
|
973
|
+
use: z.string().optional().describe("an account name from the list, as the person said it"),
|
|
1749
974
|
},
|
|
1750
975
|
},
|
|
1751
|
-
async ({
|
|
1752
|
-
|
|
1753
|
-
api.chooseAccount(
|
|
976
|
+
run(async ({ use }) => {
|
|
977
|
+
if (use) {
|
|
978
|
+
api.chooseAccount(use);
|
|
1754
979
|
const who = await api.me();
|
|
1755
|
-
return
|
|
1756
|
-
using:
|
|
980
|
+
return {
|
|
981
|
+
using: use,
|
|
1757
982
|
account: who.account,
|
|
1758
983
|
handle: who.handle || "",
|
|
1759
984
|
email: who.email || "",
|
|
1760
985
|
teams: (who.teams || []).map((t) => t.slug),
|
|
1761
|
-
}
|
|
1762
|
-
} catch (err) {
|
|
1763
|
-
return fail(err);
|
|
1764
|
-
}
|
|
1765
|
-
},
|
|
1766
|
-
);
|
|
1767
|
-
|
|
1768
|
-
server.registerTool(
|
|
1769
|
-
"set_guide_status",
|
|
1770
|
-
{
|
|
1771
|
-
title: "Set guide status",
|
|
1772
|
-
annotations: ADDS,
|
|
1773
|
-
description:
|
|
1774
|
-
"Archive a guide (`consumed`) or put it back on the board (`published`). Archiving is the " +
|
|
1775
|
-
"author's shelf: off the board, out of the free tier's count, reversible — it is not a " +
|
|
1776
|
-
"judgement that the work landed, which is what hand_in reports. `promoted` was " +
|
|
1777
|
-
"retired and the server refuses it.",
|
|
1778
|
-
inputSchema: { id: z.string(), status: z.enum(["published", "consumed"]) },
|
|
1779
|
-
},
|
|
1780
|
-
async ({ id, status }) => {
|
|
1781
|
-
try {
|
|
1782
|
-
const g = await passalong.setStatus(id, status);
|
|
1783
|
-
return json(passalong.summary(g));
|
|
1784
|
-
} catch (err) {
|
|
1785
|
-
return fail(err);
|
|
986
|
+
};
|
|
1786
987
|
}
|
|
1787
|
-
|
|
988
|
+
return {
|
|
989
|
+
accounts: api.accountList(),
|
|
990
|
+
in_use: api.accountInUse(),
|
|
991
|
+
must_choose: api.needsChoice(),
|
|
992
|
+
...(api.needsChoice() ? { next: api.choiceMessage() } : {}),
|
|
993
|
+
};
|
|
994
|
+
}),
|
|
1788
995
|
);
|
|
1789
996
|
|
|
1790
997
|
return server;
|