passalong 0.8.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/bin/passalong +7 -3
- package/package.json +1 -1
- package/src/api.js +8 -4
- package/src/mcp.js +128 -24
- package/src/passalong.js +83 -6
package/bin/passalong
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// passalong work [--once] [--agent CMD] run an agent on each ready task here, one after another
|
|
13
13
|
// passalong approve <id> the work in review is done
|
|
14
14
|
// passalong reject <id> <why> not done: back to Ready, and the next agent reads why
|
|
15
|
-
// passalong release <id> take
|
|
15
|
+
// passalong release <id> take work back from whoever holds it, of any kind
|
|
16
16
|
// passalong attach <image> upload evidence and print the markdown that points at it
|
|
17
17
|
// passalong pull <id|link> fetch a guide into ./.passalong/ and print it
|
|
18
18
|
// passalong start <id|link> pull it and take it — one command for "I am doing this"
|
|
@@ -332,8 +332,12 @@ const commands = {
|
|
|
332
332
|
|
|
333
333
|
async release(f) {
|
|
334
334
|
const id = f._[0] || die("usage: passalong release <id>");
|
|
335
|
-
await passalong.
|
|
336
|
-
|
|
335
|
+
const r = await passalong.release(id).catch((e) => die(e.message));
|
|
336
|
+
const from = Number(r?.from) || 1;
|
|
337
|
+
console.error(
|
|
338
|
+
`${id} is back in Ready${from > 1 ? `, taken back from ${from} places` : ""}` +
|
|
339
|
+
" — whoever had it has been told",
|
|
340
|
+
);
|
|
337
341
|
},
|
|
338
342
|
|
|
339
343
|
async ready(f) {
|
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -176,8 +176,11 @@ export const setStatus = (id, status) =>
|
|
|
176
176
|
call(`/v1/guides/${id}/status`, { method: "PATCH", body: { status } });
|
|
177
177
|
export const remove = (id) => call(`/v1/guides/${id}`, { method: "DELETE" });
|
|
178
178
|
/** Does it actually work? `note` is required when it does not. */
|
|
179
|
-
export const verdict = (id, ok, note = "") =>
|
|
180
|
-
call(`/v1/guides/${id}/verdict`, {
|
|
179
|
+
export const verdict = (id, ok, note = "", detail = "") =>
|
|
180
|
+
call(`/v1/guides/${id}/verdict`, {
|
|
181
|
+
method: "PUT",
|
|
182
|
+
body: { ok, note, ...(detail ? { detail } : {}) },
|
|
183
|
+
});
|
|
181
184
|
|
|
182
185
|
export const tasks = () => call("/v1/tasks");
|
|
183
186
|
export const nextTask = (body) => call("/v1/tasks/next", { method: "POST", body });
|
|
@@ -189,8 +192,9 @@ export const approveTask = (id) =>
|
|
|
189
192
|
call(`/v1/tasks/${encodeURIComponent(id)}/approve`, { method: "POST", body: {} });
|
|
190
193
|
export const rejectTask = (id, why) =>
|
|
191
194
|
call(`/v1/tasks/${encodeURIComponent(id)}/reject`, { method: "POST", body: { why } });
|
|
192
|
-
|
|
193
|
-
|
|
195
|
+
/** Take work back from whoever holds it, of any kind. `/v1/tasks/…/release` is the old path. */
|
|
196
|
+
export const release = (id) =>
|
|
197
|
+
call(`/v1/guides/${encodeURIComponent(id)}/release`, { method: "POST", body: {} });
|
|
194
198
|
/** The four verbs every guide answers to. See docs/V2.md §11. */
|
|
195
199
|
export const working = () => call("/v1/working");
|
|
196
200
|
export const handedIn = () => call("/v1/handed_in");
|
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
|
|
@@ -326,7 +381,7 @@ export function buildServer() {
|
|
|
326
381
|
}
|
|
327
382
|
}
|
|
328
383
|
|
|
329
|
-
async function doHandIn({ id, ok, note, evidence, checks, markdown, report, pr, cwd }) {
|
|
384
|
+
async function doHandIn({ id, ok, note, evidence, checks, writeup, markdown, report, pr, cwd }) {
|
|
330
385
|
try {
|
|
331
386
|
// A check that names a command is run here, before anything is recorded, and its exit code
|
|
332
387
|
// decides it rather than the agent's account of it. A failure is refused with the command's
|
|
@@ -346,6 +401,7 @@ export function buildServer() {
|
|
|
346
401
|
note: note || "",
|
|
347
402
|
evidence: evidence || "",
|
|
348
403
|
checks: done.checks,
|
|
404
|
+
writeup: writeup || "",
|
|
349
405
|
markdown,
|
|
350
406
|
report,
|
|
351
407
|
pr: pr || "",
|
|
@@ -640,18 +696,22 @@ export function buildServer() {
|
|
|
640
696
|
outputSchema: handInOut,
|
|
641
697
|
annotations: ADDS,
|
|
642
698
|
description:
|
|
643
|
-
"Done here, with proof
|
|
644
|
-
"
|
|
645
|
-
"
|
|
646
|
-
"of output for the part that answers each
|
|
647
|
-
"
|
|
648
|
-
"
|
|
649
|
-
"
|
|
650
|
-
"
|
|
651
|
-
"transfer guide about what you did and decided —
|
|
652
|
-
"handoff or a bug takes `ok`, whether its
|
|
653
|
-
"went wrong when it did not.
|
|
654
|
-
"
|
|
699
|
+
"Done here, with proof, sorted against the line it answers. Send `checks`: one entry per " +
|
|
700
|
+
"line the guide asks for — `## Acceptance` on a task, `## Verification` on a handoff or a " +
|
|
701
|
+
"bug — each with that line and the evidence for it. That is what its author reads, line " +
|
|
702
|
+
"against line, instead of hunting through a wall of output for the part that answers each " +
|
|
703
|
+
"one, AND IT IS HOW YOU KNOW YOU ARE DONE: every line has one. Give a check `cmd` when a " +
|
|
704
|
+
"command proves it: the command is run here before the hand-in lands, a non-zero exit " +
|
|
705
|
+
"refuses it, and what it printed is recorded instead of your account of it. Use `ran` for " +
|
|
706
|
+
"a check no command can settle. `evidence` as one block is the older shape and still " +
|
|
707
|
+
"works. A task also takes `markdown`, a transfer guide about what you did and decided — " +
|
|
708
|
+
"this publishes it and attaches it. A handoff or a bug takes `ok`, whether its " +
|
|
709
|
+
"Verification held here, and `note` saying what went wrong when it did not. DO NOT " +
|
|
710
|
+
"publish a guide to carry your evidence: it belongs on this call, and a second document " +
|
|
711
|
+
'titled "Hand-in evidence" is the thing `checks` exists to replace. If it worked but you ' +
|
|
712
|
+
"had to adapt something, that goes in `writeup` and the next reader of the guide is shown " +
|
|
713
|
+
"it — that is its home. A follow-up guide is for work somebody else should now do, not " +
|
|
714
|
+
"for answering.",
|
|
655
715
|
inputSchema: {
|
|
656
716
|
id: z.string().describe("passalong id"),
|
|
657
717
|
evidence: z
|
|
@@ -664,7 +724,12 @@ export function buildServer() {
|
|
|
664
724
|
checks: z
|
|
665
725
|
.array(
|
|
666
726
|
z.object({
|
|
667
|
-
check: z
|
|
727
|
+
check: z
|
|
728
|
+
.string()
|
|
729
|
+
.describe(
|
|
730
|
+
"the line this answers, in the guide's own words: an Acceptance line on a task, " +
|
|
731
|
+
"a Verification line on a handoff or a bug",
|
|
732
|
+
),
|
|
668
733
|
ran: z
|
|
669
734
|
.string()
|
|
670
735
|
.optional()
|
|
@@ -676,14 +741,28 @@ export function buildServer() {
|
|
|
676
741
|
"the shell command that proves this line, run here before the hand-in lands — " +
|
|
677
742
|
"its exit code decides the check and its output is recorded as the evidence. " +
|
|
678
743
|
"Non-zero refuses the hand-in. Leave it out for a check nobody can run, like " +
|
|
679
|
-
'"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',
|
|
680
745
|
),
|
|
681
746
|
}),
|
|
682
747
|
)
|
|
683
748
|
.optional()
|
|
684
|
-
.describe(
|
|
749
|
+
.describe(
|
|
750
|
+
"one entry per line the guide asks for — Acceptance on a task, Verification on a " +
|
|
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",
|
|
754
|
+
),
|
|
685
755
|
ok: z.boolean().optional().describe("handoff or bug: did its Verification hold"),
|
|
686
756
|
note: z.string().optional().describe("one line; required when ok is false"),
|
|
757
|
+
writeup: z
|
|
758
|
+
.string()
|
|
759
|
+
.optional()
|
|
760
|
+
.describe(
|
|
761
|
+
"handoff or bug: what you had to adapt to make it work here — a version, a name, a " +
|
|
762
|
+
"step that needed something the guide does not mention. Prose, and optional: leave " +
|
|
763
|
+
"it out when it worked as written. The next person to open the guide is shown it, " +
|
|
764
|
+
"which is why it does not need to be a guide of its own",
|
|
765
|
+
),
|
|
687
766
|
markdown: z
|
|
688
767
|
.string()
|
|
689
768
|
.optional()
|
|
@@ -779,19 +858,44 @@ export function buildServer() {
|
|
|
779
858
|
title: "Attach a screenshot",
|
|
780
859
|
annotations: { ...ADDS, openWorldHint: true },
|
|
781
860
|
description:
|
|
782
|
-
"Upload an image
|
|
783
|
-
"
|
|
784
|
-
|
|
785
|
-
"claims whatever the markdown names, so attach first and publish after. png, jpg, webp " +
|
|
786
|
-
"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,
|
|
787
864
|
inputSchema: {
|
|
788
|
-
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"),
|
|
789
881
|
name: z.string().default("").describe("label for the image; defaults to its filename"),
|
|
790
882
|
},
|
|
791
883
|
},
|
|
792
|
-
async ({ file, name }) => {
|
|
884
|
+
async ({ file, data, type, name }) => {
|
|
793
885
|
try {
|
|
794
|
-
|
|
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 || "" });
|
|
795
899
|
return text(
|
|
796
900
|
`${JSON.stringify({ id: shot.id, url: shot.url, bytes: shot.bytes }, null, 2)}\n\n` +
|
|
797
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,
|
|
@@ -412,6 +413,7 @@ export async function handIn(
|
|
|
412
413
|
note = "",
|
|
413
414
|
evidence = "",
|
|
414
415
|
checks = [],
|
|
416
|
+
writeup = "",
|
|
415
417
|
markdown,
|
|
416
418
|
report,
|
|
417
419
|
pr = "",
|
|
@@ -419,6 +421,21 @@ export async function handIn(
|
|
|
419
421
|
} = {},
|
|
420
422
|
) {
|
|
421
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
|
+
}
|
|
422
439
|
// Checks are evidence, sorted against the lines they answer: bringing them is bringing it. A
|
|
423
440
|
// check whose command was actually run is exempt from the length rule — its exit code is the
|
|
424
441
|
// evidence, and `test -f dist/app.js` exiting 0 says more than any sentence about it would.
|
|
@@ -446,6 +463,9 @@ export async function handIn(
|
|
|
446
463
|
}
|
|
447
464
|
: {}),
|
|
448
465
|
...(report ? { report, pr } : {}),
|
|
466
|
+
// Left out when there is nothing to say, rather than sent as "": most hand-ins have nothing
|
|
467
|
+
// to adapt, and the field is for the ones that do.
|
|
468
|
+
...(writeup ? { writeup } : {}),
|
|
449
469
|
...(typeof ok === "boolean" ? { ok } : {}),
|
|
450
470
|
});
|
|
451
471
|
}
|
|
@@ -539,9 +559,16 @@ export async function rejectTask(id, why) {
|
|
|
539
559
|
return r;
|
|
540
560
|
}
|
|
541
561
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
562
|
+
/**
|
|
563
|
+
* Take it back from whoever is holding it — a task, a handoff or a bug, and from all of them at
|
|
564
|
+
* once when several people hold the same handoff in different repos.
|
|
565
|
+
*
|
|
566
|
+
* It was tasks only, which left an asymmetry nobody chose: the author of a handoff could mark it
|
|
567
|
+
* done or give it to somebody else, and had no way to simply have it back.
|
|
568
|
+
*/
|
|
569
|
+
export async function release(id) {
|
|
570
|
+
needsSync("taking work back");
|
|
571
|
+
const r = await api.release(id);
|
|
545
572
|
store.remove(id);
|
|
546
573
|
return r;
|
|
547
574
|
}
|
|
@@ -624,6 +651,46 @@ export const SHOT_TYPES = {
|
|
|
624
651
|
* Local because it reads a path. An agent in a terminal has one; a hosted assistant does not, and
|
|
625
652
|
* reaches the same route through `attach_screenshot` on the HTTP server instead.
|
|
626
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
|
+
|
|
627
694
|
export async function attach(file, { name = "" } = {}) {
|
|
628
695
|
if (!api.loggedIn())
|
|
629
696
|
throw new PassalongError("attaching evidence needs sync — run `passalong login` first");
|
|
@@ -808,11 +875,21 @@ export async function activity({ all = false, limit = 50 } = {}) {
|
|
|
808
875
|
* `done` says "I implemented it", a verdict says "I tried it and it holds up" — or does not.
|
|
809
876
|
* A failing verdict needs a note; "it doesn't work" without a reason helps nobody.
|
|
810
877
|
*/
|
|
878
|
+
/**
|
|
879
|
+
* Say whether a guide held, and why at the length that takes.
|
|
880
|
+
*
|
|
881
|
+
* The reason used to be one field, capped at 280 characters by the row it was written for — so an
|
|
882
|
+
* agent that had run the Verification and found it did not hold published a guide called
|
|
883
|
+
* "Correction: …" instead, because measurements, commands and commit ids do not fit in a tweet.
|
|
884
|
+
* The first line is the summary a row shows; the whole thing is what the guide shows.
|
|
885
|
+
*/
|
|
811
886
|
export async function verdict(id, ok, note = "") {
|
|
812
887
|
if (!api.loggedIn()) throw new PassalongError("verdicts need sync — run `passalong login` first");
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
888
|
+
const said = note.trim();
|
|
889
|
+
if (!ok && !said)
|
|
890
|
+
throw new PassalongError("say what went wrong: passalong broken <id> <what happened>");
|
|
891
|
+
const line = said.split("\n")[0].slice(0, 280);
|
|
892
|
+
return api.verdict(await resolveId(id), ok, line, said.length > line.length ? said : "");
|
|
816
893
|
}
|
|
817
894
|
|
|
818
895
|
/**
|