passalong 0.9.0 → 0.11.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/package.json +1 -1
- package/src/mcp.js +92 -10
- package/src/passalong.js +56 -0
package/package.json
CHANGED
package/src/mcp.js
CHANGED
|
@@ -29,11 +29,62 @@ const text = (s) => ({ content: [{ type: "text", text: s }] });
|
|
|
29
29
|
* evidence is collected while the work happens, not reconstructed from memory once it is done.
|
|
30
30
|
* Mirrors KEEP_EVIDENCE in apps/api/src/mcp-http.ts.
|
|
31
31
|
*/
|
|
32
|
+
const WHERE_THE_LINE_GOES =
|
|
33
|
+
"WHERE THE LINE GOES DEPENDS ON WHAT THE IMAGE IS EVIDENCE OF. Proof that work you are handing i" +
|
|
34
|
+
"n holds — a screen that renders right, a total that matches — goes in that check's `ran` on han" +
|
|
35
|
+
"d_in, and nothing is published: a check answered with a picture is shown, which is what the han" +
|
|
36
|
+
"d-in asks for. Only an image that is part of a DOCUMENT goes in a guide body — the screenshot i" +
|
|
37
|
+
"n a bug report, a diagram a guide is explaining — and then it must be in the markdown, because " +
|
|
38
|
+
"a guide travels as markdown to whoever holds its link and evidence beside the document does not" +
|
|
39
|
+
" travel at all; publishing claims whatever the markdown names, so attach first and publish afte" +
|
|
40
|
+
"r. DO NOT PUBLISH A GUIDE TO CARRY SCREENSHOTS. Six images and a caption each is a hand-in, not" +
|
|
41
|
+
" a document, and wrapping them in Problem / Solution shape / Decisions / Steps / Verification t" +
|
|
42
|
+
"o make them publishable is how a set of pictures becomes fifteen hundred words nobody asked for" +
|
|
43
|
+
". png, jpg, webp or gif.";
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* What a screenshot has to be a screenshot OF.
|
|
47
|
+
*
|
|
48
|
+
* "A check is run or shown" made a picture mandatory, and an agent short of one built a page to
|
|
49
|
+
* photograph: it added `src/app/notes-preview`, shot that, then `rm -rf`'d it and published the
|
|
50
|
+
* shots as evidence that the real screen rendered. The rule was satisfied and almost nothing was
|
|
51
|
+
* proved — the measure had become the target, and the page the picture showed no longer existed.
|
|
52
|
+
*
|
|
53
|
+
* Mirrors SHOOT_THE_REAL_THING in apps/api/src/mcp-http.ts.
|
|
54
|
+
*/
|
|
55
|
+
const SHOOT_THE_REAL_THING =
|
|
56
|
+
"SHOOT THE RUNNING THING, NOT SOMETHING YOU BUILT TO SHOOT. A screenshot is worth what the sc" +
|
|
57
|
+
"reen behind it is worth: the real route in the real app, reached the way a reader would reac" +
|
|
58
|
+
"h it. A scratch page, a story, a harness or a component rendered on its own is a picture of " +
|
|
59
|
+
"your scaffolding, and it can look right while the thing the guide is about does not. If the " +
|
|
60
|
+
"only way you could get a picture was to build something, that is not evidence the real scree" +
|
|
61
|
+
"n works — say what you built and why in `writeup`, and leave the check to the commands that " +
|
|
62
|
+
"do hold. NEVER DELETE WHAT YOUR EVIDENCE POINTS AT: a shot of a page you removed afterwards " +
|
|
63
|
+
"is one nobody can take again, including you.";
|
|
64
|
+
|
|
32
65
|
const KEEP_EVIDENCE =
|
|
33
66
|
"KEEP YOUR EVIDENCE AS YOU GO. hand_in needs it: the commands you ran and what came back, the " +
|
|
34
67
|
"test summary, the link to the change. Copy each one when it happens — at the end you will be " +
|
|
35
68
|
"writing from memory, which is the thing evidence is here to replace.\n\n";
|
|
36
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Said in front of every guide an agent has to locate things in for itself — a bug and a task.
|
|
72
|
+
*
|
|
73
|
+
* The failure it is here to stop: an agent lists one directory, or opens the one file it expected,
|
|
74
|
+
* and concludes from that. It then reports that a thing is absent, or that it found the only
|
|
75
|
+
* occurrence, having never looked past the place it happened to start. The conclusion is confident
|
|
76
|
+
* and wrong, and nothing downstream can tell, because the evidence it brings is real — it is just
|
|
77
|
+
* evidence of a search that was too small.
|
|
78
|
+
*
|
|
79
|
+
* Mirrors SEARCH_WIDE in apps/api/src/mcp-http.ts.
|
|
80
|
+
*/
|
|
81
|
+
const SEARCH_WIDE =
|
|
82
|
+
"SEARCH THE WHOLE TREE BEFORE YOU CONCLUDE. Listing a directory, or reading the one file you " +
|
|
83
|
+
"expected, tells you what is in that directory — not whether the thing exists, and not whether " +
|
|
84
|
+
"you have found all of it. Before you report that something is absent, is the only one, or is " +
|
|
85
|
+
'already handled, search across the repository and let the output be your reason. "I looked" ' +
|
|
86
|
+
"is a claim; the search and what it printed is evidence.\n\n";
|
|
87
|
+
|
|
37
88
|
/**
|
|
38
89
|
* What to say in front of a guide whose kind changes what the reader should do with it.
|
|
39
90
|
*
|
|
@@ -48,6 +99,8 @@ function leadFor(meta) {
|
|
|
48
99
|
"THIS IS A BUG REPORT, NOT WORK TO REPEAT. Do not follow Reproduce as instructions " +
|
|
49
100
|
"— those steps produce the defect. Fix what Problem describes, then check " +
|
|
50
101
|
"Verification and answer with hand_in.\n\n" +
|
|
102
|
+
SEARCH_WIDE +
|
|
103
|
+
SHOOT_THE_REAL_THING +
|
|
51
104
|
KEEP_EVIDENCE
|
|
52
105
|
);
|
|
53
106
|
if (meta.kind === "task")
|
|
@@ -57,6 +110,8 @@ function leadFor(meta) {
|
|
|
57
110
|
"under Acceptance holds. Opening it here does not make it yours: to work on it, call take " +
|
|
58
111
|
"with its id in the repo it is for, so no other agent can. Then progress, and hand_in " +
|
|
59
112
|
"with a write-up.\n\n" +
|
|
113
|
+
SEARCH_WIDE +
|
|
114
|
+
SHOOT_THE_REAL_THING +
|
|
60
115
|
KEEP_EVIDENCE
|
|
61
116
|
);
|
|
62
117
|
// A transfer guide is handed over untouched: it is a document to follow, and anything in front
|
|
@@ -686,14 +741,16 @@ export function buildServer() {
|
|
|
686
741
|
"the shell command that proves this line, run here before the hand-in lands — " +
|
|
687
742
|
"its exit code decides the check and its output is recorded as the evidence. " +
|
|
688
743
|
"Non-zero refuses the hand-in. Leave it out for a check nobody can run, like " +
|
|
689
|
-
'"the badge reads 3"
|
|
744
|
+
'"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',
|
|
690
745
|
),
|
|
691
746
|
}),
|
|
692
747
|
)
|
|
693
748
|
.optional()
|
|
694
749
|
.describe(
|
|
695
750
|
"one entry per line the guide asks for — Acceptance on a task, Verification on a " +
|
|
696
|
-
"handoff or a bug — in the order you worked them"
|
|
751
|
+
"handoff or a bug — in the order you worked them. Each is RUN (`cmd`) or SHOWN (an " +
|
|
752
|
+
"attach_screenshot line in `ran`). Pasting a command you typed into `ran` is not " +
|
|
753
|
+
"running it and is refused",
|
|
697
754
|
),
|
|
698
755
|
ok: z.boolean().optional().describe("handoff or bug: did its Verification hold"),
|
|
699
756
|
note: z.string().optional().describe("one line; required when ok is false"),
|
|
@@ -801,19 +858,44 @@ export function buildServer() {
|
|
|
801
858
|
title: "Attach a screenshot",
|
|
802
859
|
annotations: { ...ADDS, openWorldHint: true },
|
|
803
860
|
description:
|
|
804
|
-
"Upload an image
|
|
805
|
-
"
|
|
806
|
-
|
|
807
|
-
"claims whatever the markdown names, so attach first and publish after. png, jpg, webp " +
|
|
808
|
-
"or gif.",
|
|
861
|
+
"Upload an image as evidence — a path in `file`, or the bytes in `data` when you have " +
|
|
862
|
+
"the image and no file — and get back the markdown line that points at it. " +
|
|
863
|
+
WHERE_THE_LINE_GOES,
|
|
809
864
|
inputSchema: {
|
|
810
|
-
file: z
|
|
865
|
+
file: z
|
|
866
|
+
.string()
|
|
867
|
+
.default("")
|
|
868
|
+
.describe("path to the image on this machine; leave out when sending `data`"),
|
|
869
|
+
data: z
|
|
870
|
+
.string()
|
|
871
|
+
.default("")
|
|
872
|
+
.describe(
|
|
873
|
+
"the image itself, base64 (a data: URL is fine), for when you have the bytes and no " +
|
|
874
|
+
"file — a browser tool that hands back an image inline and never writes to disk. " +
|
|
875
|
+
"5MB; larger belongs in create_upload",
|
|
876
|
+
),
|
|
877
|
+
type: z
|
|
878
|
+
.string()
|
|
879
|
+
.default("image/png")
|
|
880
|
+
.describe("media type of `data`: image/png, image/jpeg, image/webp or image/gif"),
|
|
811
881
|
name: z.string().default("").describe("label for the image; defaults to its filename"),
|
|
812
882
|
},
|
|
813
883
|
},
|
|
814
|
-
async ({ file, name }) => {
|
|
884
|
+
async ({ file, data, type, name }) => {
|
|
815
885
|
try {
|
|
816
|
-
|
|
886
|
+
if (!file && !data) {
|
|
887
|
+
return fail(
|
|
888
|
+
new Error(
|
|
889
|
+
"send `file` (a path on this machine) or `data` (the image, base64). If your " +
|
|
890
|
+
"screenshot is a tool result you cannot write out, capture it to a file instead " +
|
|
891
|
+
"— a headless browser's page.screenshot({ path }) or your platform's capture " +
|
|
892
|
+
"command — and pass that path.",
|
|
893
|
+
),
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
const shot = data
|
|
897
|
+
? await passalong.attachBytes(data, { name: name || "", type: type || "image/png" })
|
|
898
|
+
: await passalong.attach(file, { name: name || "" });
|
|
817
899
|
return text(
|
|
818
900
|
`${JSON.stringify({ id: shot.id, url: shot.url, bytes: shot.bytes }, null, 2)}\n\n` +
|
|
819
901
|
`Put this in the guide body:\n${shot.markdown}`,
|
package/src/passalong.js
CHANGED
|
@@ -7,6 +7,7 @@ import { hostname } from "node:os";
|
|
|
7
7
|
import { basename, join } from "node:path";
|
|
8
8
|
import * as api from "./api.js";
|
|
9
9
|
import { context } from "./capture.js";
|
|
10
|
+
import { refusal, runChecks } from "./checks.js";
|
|
10
11
|
import {
|
|
11
12
|
bugGuide,
|
|
12
13
|
ID_RE,
|
|
@@ -420,6 +421,21 @@ export async function handIn(
|
|
|
420
421
|
} = {},
|
|
421
422
|
) {
|
|
422
423
|
needsSync("handing work in");
|
|
424
|
+
// A check that names a command is run here, before anything is sent.
|
|
425
|
+
//
|
|
426
|
+
// The MCP server did this and this function did not, so "put the command in `cmd` and it is
|
|
427
|
+
// executed here" was true over one transport and a quiet lie over the other: a `cmd` handed to
|
|
428
|
+
// this function was passed along as a string nothing had executed. Checks that already carry an
|
|
429
|
+
// exit came from the server's own runner and are left alone, so nothing runs twice.
|
|
430
|
+
const pending = checks.filter((c) => c?.cmd && typeof c?.exit !== "number" && c?.exit !== null);
|
|
431
|
+
if (pending.length) {
|
|
432
|
+
const done = runChecks(pending, { cwd });
|
|
433
|
+
const byCheck = new Map(done.checks.map((c) => [c.check, c]));
|
|
434
|
+
checks = checks.map((c) => byCheck.get(c.check) ?? c);
|
|
435
|
+
if (done.failed) {
|
|
436
|
+
throw new Error(refusal(done.failed, { ran: done.ran, total: checks.length }));
|
|
437
|
+
}
|
|
438
|
+
}
|
|
423
439
|
// Checks are evidence, sorted against the lines they answer: bringing them is bringing it. A
|
|
424
440
|
// check whose command was actually run is exempt from the length rule — its exit code is the
|
|
425
441
|
// evidence, and `test -f dist/app.js` exiting 0 says more than any sentence about it would.
|
|
@@ -635,6 +651,46 @@ export const SHOT_TYPES = {
|
|
|
635
651
|
* Local because it reads a path. An agent in a terminal has one; a hosted assistant does not, and
|
|
636
652
|
* reaches the same route through `attach_screenshot` on the HTTP server instead.
|
|
637
653
|
*/
|
|
654
|
+
/**
|
|
655
|
+
* The same upload, for an agent that has the image and not a path to it.
|
|
656
|
+
*
|
|
657
|
+
* `attach()` takes a path because a screenshot usually lands on disk. An agent driving a browser
|
|
658
|
+
* often has the opposite: the pane hands back an image as a tool result and never writes a file, so
|
|
659
|
+
* the only door into evidence was one it could not open. It did what anyone would — swapped to
|
|
660
|
+
* `grep` and `tsc` checks and put what it saw in `writeup` — and said so itself: "a grep proves the
|
|
661
|
+
* code changed, not that the screen renders right."
|
|
662
|
+
*
|
|
663
|
+
* Base64 in a tool call, which the HTTP server's instructions rightly warn against, because there
|
|
664
|
+
* the alternative is `create_upload` and a curl. Here there is no alternative for bytes held in
|
|
665
|
+
* memory, and this server is a local process talking to the same upload the file path uses.
|
|
666
|
+
*/
|
|
667
|
+
export async function attachBytes(data, { name = "", type = "image/png" } = {}) {
|
|
668
|
+
if (!api.loggedIn())
|
|
669
|
+
throw new PassalongError("attaching evidence needs sync — run `passalong login` first");
|
|
670
|
+
const allowed = new Set(Object.values(SHOT_TYPES));
|
|
671
|
+
if (!allowed.has(type))
|
|
672
|
+
throw new PassalongError(`screenshots must be ${[...allowed].join(", ")} — ${type} is not one`);
|
|
673
|
+
let bytes;
|
|
674
|
+
try {
|
|
675
|
+
// A data: URL is what several browser tools hand back, so it is accepted rather than made the
|
|
676
|
+
// caller's problem to strip.
|
|
677
|
+
bytes = Buffer.from(String(data).replace(/^data:[^,]*,/, ""), "base64");
|
|
678
|
+
} catch {
|
|
679
|
+
throw new PassalongError("`data` is not base64");
|
|
680
|
+
}
|
|
681
|
+
if (!bytes.length) throw new PassalongError("`data` decoded to nothing");
|
|
682
|
+
// The server's own ceiling, checked here so a large paste fails before it is sent rather than
|
|
683
|
+
// after. Anything bigger belongs in create_upload, which streams from a file.
|
|
684
|
+
if (bytes.length > 5 * 1024 * 1024)
|
|
685
|
+
throw new PassalongError(
|
|
686
|
+
`that image is ${Math.round(bytes.length / 1024 / 1024)}MB and the limit is 5MB — ` +
|
|
687
|
+
"write it to a file and use create_upload, which streams",
|
|
688
|
+
);
|
|
689
|
+
const label = name || "screenshot";
|
|
690
|
+
const shot = await api.uploadShot(bytes, type, label);
|
|
691
|
+
return { ...shot, markdown: `` };
|
|
692
|
+
}
|
|
693
|
+
|
|
638
694
|
export async function attach(file, { name = "" } = {}) {
|
|
639
695
|
if (!api.loggedIn())
|
|
640
696
|
throw new PassalongError("attaching evidence needs sync — run `passalong login` first");
|