passalong 0.11.0 → 0.12.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/api.js +13 -2
- package/src/guide.js +37 -7
- package/src/mcp.js +26 -2
- package/src/passalong.js +32 -7
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
// Client for the hosted sync API (apps/api). Everything here is optional: with no token the
|
|
2
2
|
// CLI is a purely local tool, and every function throws an ApiError the CLI turns into a hint.
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
3
4
|
import { readConfig } from "./store.js";
|
|
4
5
|
|
|
6
|
+
/**
|
|
7
|
+
* This build's version, sent on every authenticated call as `x-passalong-version`. The server
|
|
8
|
+
* refuses a CLI below its floor (apps/api/src/clients.ts), because the rules it enforces deploy on
|
|
9
|
+
* merge and the text telling an agent how to follow them only arrives with a reinstall.
|
|
10
|
+
*/
|
|
11
|
+
export const VERSION = JSON.parse(
|
|
12
|
+
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
|
13
|
+
).version;
|
|
14
|
+
const CLIENT = { "x-passalong-version": VERSION };
|
|
15
|
+
|
|
5
16
|
export const DEFAULT_API = "https://passalong.dev";
|
|
6
17
|
|
|
7
18
|
export class ApiError extends Error {
|
|
@@ -24,7 +35,7 @@ export function loggedIn() {
|
|
|
24
35
|
}
|
|
25
36
|
|
|
26
37
|
async function call(path, { method = "GET", body, auth = true, raw = false } = {}) {
|
|
27
|
-
const headers = {};
|
|
38
|
+
const headers = { ...CLIENT };
|
|
28
39
|
if (auth) {
|
|
29
40
|
const t = token();
|
|
30
41
|
if (!t) throw new ApiError(401, "not logged in — run `passalong login` to enable sync");
|
|
@@ -220,7 +231,7 @@ export const ack = (id, taken, note = "") =>
|
|
|
220
231
|
export async function uploadShot(bytes, type, name = "") {
|
|
221
232
|
const t = token();
|
|
222
233
|
if (!t) throw new ApiError(401, "not logged in — run `passalong login` to enable sync");
|
|
223
|
-
const headers = { authorization: `Bearer ${t}`, "content-type": type };
|
|
234
|
+
const headers = { ...CLIENT, authorization: `Bearer ${t}`, "content-type": type };
|
|
224
235
|
// A header is latin-1: a filename with an accent in it throws on the way out rather than at the
|
|
225
236
|
// server, and the label is not worth failing an upload over.
|
|
226
237
|
if (name) headers["x-shot-name"] = String(name).replace(/[^\x20-\x7e]/g, "");
|
package/src/guide.js
CHANGED
|
@@ -127,8 +127,22 @@ export function sectionsFor(kind) {
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
/** The sections it cannot be published without. */
|
|
130
|
+
/**
|
|
131
|
+
* The sections a kind cannot do without, and the one that has none.
|
|
132
|
+
*
|
|
133
|
+
* A transfer is context exchange. Context is whatever the next agent needs to not start cold —
|
|
134
|
+
* a decision and why, a constraint, where the bodies are — and none of that arrives in six fixed
|
|
135
|
+
* headings. Requiring `## Problem` and `## Steps` made every short note into a document: of 56
|
|
136
|
+
* follow-ups in real use, the ones that fill this template average 599 words and ten of sixteen
|
|
137
|
+
* were never opened by anyone, while the ones that ignored it average 137 words and every single
|
|
138
|
+
* one was opened. The template was not raising the floor, it was padding to reach it.
|
|
139
|
+
*
|
|
140
|
+
* A task and a bug keep theirs, because theirs are not prose. `## Acceptance` is the list a
|
|
141
|
+
* hand-in answers line by line, and `## Reproduce` is how a reader sees the defect. Those earn
|
|
142
|
+
* their requirement by being read mechanically; `## Steps` on a piece of context does not.
|
|
143
|
+
*/
|
|
130
144
|
const REQUIRED = {
|
|
131
|
-
transfer: [
|
|
145
|
+
transfer: [],
|
|
132
146
|
bug: ["Problem", "Reproduce"],
|
|
133
147
|
task: ["Goal", "Acceptance"],
|
|
134
148
|
};
|
|
@@ -255,10 +269,21 @@ export function parseFrontmatter(text) {
|
|
|
255
269
|
// Only the lists every guide has. `blocked_by` is a task's, and defaulting it would write an
|
|
256
270
|
// empty one into every guide anybody re-shares.
|
|
257
271
|
for (const k of ["stack_assumptions", "tags"]) if (meta[k] === undefined) meta[k] = [];
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
|
|
272
|
+
// Absent is absent. It is NOT a transfer guide.
|
|
273
|
+
//
|
|
274
|
+
// This line, and the sentence publish_guide tells every agent — "kind: task (the default)" —
|
|
275
|
+
// disagreed for thirteen days. An agent that left `kind:` off, believing what it had been told,
|
|
276
|
+
// got a transfer guide. 162 of 200 guides in real use are labelled transfer; their titles are
|
|
277
|
+
// tasks and bug reports. Everything downstream followed from that: a transfer has no states, so
|
|
278
|
+
// nothing could show as in progress; it demands Problem and Steps, so a two-line correction was
|
|
279
|
+
// padded to six sections; and there is nowhere to report into one, so agents published a second
|
|
280
|
+
// guide to carry what they had found.
|
|
281
|
+
//
|
|
282
|
+
// So it stays empty and `validate()` refuses it by name. The back-compat this default protected
|
|
283
|
+
// is a guide written before kinds existed being re-shared; that guide is re-shared once with an
|
|
284
|
+
// explicit `kind: transfer`, which is a sentence its author can write, unlike thirteen days of
|
|
285
|
+
// mislabelled work nobody could see.
|
|
286
|
+
meta.kind = String(meta.kind ?? "").trim();
|
|
262
287
|
// Both ends of this module: what it reads and what it writes are in one style, so a guide
|
|
263
288
|
// written before there was a rule comes back normalised and goes out normalised.
|
|
264
289
|
meta.tags = tagList(meta.tags);
|
|
@@ -305,6 +330,10 @@ export function serializeFrontmatter(meta) {
|
|
|
305
330
|
];
|
|
306
331
|
const out = [];
|
|
307
332
|
for (const key of keys) {
|
|
333
|
+
// A kind nobody stated is left unstated. Writing `kind: ""` would put an answer in the
|
|
334
|
+
// document that its author never gave, and the next parse would read it back as a stated
|
|
335
|
+
// empty rather than an absent one — which is the whole distinction validate() turns on.
|
|
336
|
+
if (key === "kind" && !String(meta[key] ?? "").trim()) continue;
|
|
308
337
|
const v = key === "tags" ? tagList(meta[key]) : meta[key];
|
|
309
338
|
if (Array.isArray(v)) {
|
|
310
339
|
out.push(v.length ? `${key}: [${v.map(quote).join(", ")}]` : `${key}: []`);
|
|
@@ -497,8 +526,9 @@ export function bugGuide({
|
|
|
497
526
|
|
|
498
527
|
/** A draft with the section skeleton. Placeholders are HTML comments so they vanish when rendered. */
|
|
499
528
|
export function template(meta = {}) {
|
|
500
|
-
// A task unless asked otherwise: most guides assign work.
|
|
501
|
-
//
|
|
529
|
+
// A task unless asked otherwise: most guides assign work. Every skeleton writes its kind out —
|
|
530
|
+
// absent is now refused rather than read as transfer, so a template that left it off would
|
|
531
|
+
// produce a document its own author could not publish.
|
|
502
532
|
if (meta.kind === "bug") return bugTemplate(meta);
|
|
503
533
|
if (meta.kind !== "transfer") return taskTemplate(meta);
|
|
504
534
|
const body = [
|
package/src/mcp.js
CHANGED
|
@@ -261,7 +261,9 @@ export function buildServer() {
|
|
|
261
261
|
"Collect it as you work rather than writing it from memory at the end. hand_in without " +
|
|
262
262
|
"it is refused, because the write-up and the verdict are both your word for your own " +
|
|
263
263
|
"work and evidence is the part the person reviewing it can check.\n" +
|
|
264
|
-
|
|
264
|
+
"SAY WHICH KIND IT IS. There is no default: a guide with no `kind:` line is refused, and so " +
|
|
265
|
+
"is a spelling that is not one of the three. " +
|
|
266
|
+
`kind: task is work nobody has done yet. Sections: ${TASK_SECTIONS.join(", ")}. ` +
|
|
265
267
|
"It has no Steps: work out how to reach Goal within Constraints, leave Out of scope " +
|
|
266
268
|
"alone, and treat Acceptance as the definition of done. hand_in with `markdown`: a " +
|
|
267
269
|
"transfer guide about what you did (guide_template kind transfer shows the shape) — its " +
|
|
@@ -381,7 +383,19 @@ export function buildServer() {
|
|
|
381
383
|
}
|
|
382
384
|
}
|
|
383
385
|
|
|
384
|
-
async function doHandIn({
|
|
386
|
+
async function doHandIn({
|
|
387
|
+
id,
|
|
388
|
+
ok,
|
|
389
|
+
note,
|
|
390
|
+
evidence,
|
|
391
|
+
checks,
|
|
392
|
+
writeup,
|
|
393
|
+
risk,
|
|
394
|
+
markdown,
|
|
395
|
+
report,
|
|
396
|
+
pr,
|
|
397
|
+
cwd,
|
|
398
|
+
}) {
|
|
385
399
|
try {
|
|
386
400
|
// A check that names a command is run here, before anything is recorded, and its exit code
|
|
387
401
|
// decides it rather than the agent's account of it. A failure is refused with the command's
|
|
@@ -402,6 +416,7 @@ export function buildServer() {
|
|
|
402
416
|
evidence: evidence || "",
|
|
403
417
|
checks: done.checks,
|
|
404
418
|
writeup: writeup || "",
|
|
419
|
+
risk: risk || "",
|
|
405
420
|
markdown,
|
|
406
421
|
report,
|
|
407
422
|
pr: pr || "",
|
|
@@ -763,6 +778,15 @@ export function buildServer() {
|
|
|
763
778
|
"it out when it worked as written. The next person to open the guide is shown it, " +
|
|
764
779
|
"which is why it does not need to be a guide of its own",
|
|
765
780
|
),
|
|
781
|
+
risk: z
|
|
782
|
+
.string()
|
|
783
|
+
.optional()
|
|
784
|
+
.describe(
|
|
785
|
+
"what this could break, in one line — the part of a PR template's Risk that is " +
|
|
786
|
+
"worth reading: a shared helper changed, a migration that cannot be undone, a " +
|
|
787
|
+
"route other clients call. Optional; leave it out when there is nothing specific, " +
|
|
788
|
+
'rather than writing "low risk". Its author reads it before accepting the work',
|
|
789
|
+
),
|
|
766
790
|
markdown: z
|
|
767
791
|
.string()
|
|
768
792
|
.optional()
|
package/src/passalong.js
CHANGED
|
@@ -358,8 +358,16 @@ export async function finishTask(
|
|
|
358
358
|
requireEvidence(evidence);
|
|
359
359
|
if (!report && markdown) {
|
|
360
360
|
const g = parse(markdown);
|
|
361
|
-
|
|
362
|
-
|
|
361
|
+
// A task's report is a transfer guide, and this code is what is making one — so it says so
|
|
362
|
+
// rather than hoping the agent's frontmatter did. Absent is refused now, and refusing an
|
|
363
|
+
// agent's hand-in because its write-up left a line off would be this function's omission
|
|
364
|
+
// charged to the agent. Stated, not defaulted: `kind` is the one field here that is not the
|
|
365
|
+
// author's to leave open, because the caller already knows the answer.
|
|
366
|
+
report = (
|
|
367
|
+
await share(serialize({ meta: { ...g.meta, kind: "transfer", parent: id }, body: g.body }), {
|
|
368
|
+
cwd,
|
|
369
|
+
})
|
|
370
|
+
).guide.meta.id;
|
|
363
371
|
}
|
|
364
372
|
if (!report)
|
|
365
373
|
throw new PassalongError(
|
|
@@ -414,6 +422,7 @@ export async function handIn(
|
|
|
414
422
|
evidence = "",
|
|
415
423
|
checks = [],
|
|
416
424
|
writeup = "",
|
|
425
|
+
risk = "",
|
|
417
426
|
markdown,
|
|
418
427
|
report,
|
|
419
428
|
pr = "",
|
|
@@ -443,8 +452,16 @@ export async function handIn(
|
|
|
443
452
|
else requireEvidence(evidence);
|
|
444
453
|
if (!report && markdown) {
|
|
445
454
|
const g = parse(markdown);
|
|
446
|
-
|
|
447
|
-
|
|
455
|
+
// A task's report is a transfer guide, and this code is what is making one — so it says so
|
|
456
|
+
// rather than hoping the agent's frontmatter did. Absent is refused now, and refusing an
|
|
457
|
+
// agent's hand-in because its write-up left a line off would be this function's omission
|
|
458
|
+
// charged to the agent. Stated, not defaulted: `kind` is the one field here that is not the
|
|
459
|
+
// author's to leave open, because the caller already knows the answer.
|
|
460
|
+
report = (
|
|
461
|
+
await share(serialize({ meta: { ...g.meta, kind: "transfer", parent: id }, body: g.body }), {
|
|
462
|
+
cwd,
|
|
463
|
+
})
|
|
464
|
+
).guide.meta.id;
|
|
448
465
|
}
|
|
449
466
|
const fence = fenceFor(id, cwd);
|
|
450
467
|
return api.handIn(id, {
|
|
@@ -466,6 +483,8 @@ export async function handIn(
|
|
|
466
483
|
// Left out when there is nothing to say, rather than sent as "": most hand-ins have nothing
|
|
467
484
|
// to adapt, and the field is for the ones that do.
|
|
468
485
|
...(writeup ? { writeup } : {}),
|
|
486
|
+
// What it could break, for the reviewer. Left out when empty, like the write-up.
|
|
487
|
+
...(risk ? { risk } : {}),
|
|
469
488
|
...(typeof ok === "boolean" ? { ok } : {}),
|
|
470
489
|
});
|
|
471
490
|
}
|
|
@@ -713,6 +732,11 @@ export async function attach(file, { name = "" } = {}) {
|
|
|
713
732
|
* found doing it. Whoever opens the original, person or agent, gets its follow-ups with it, which
|
|
714
733
|
* is why context belongs there rather than in a one-line verdict note.
|
|
715
734
|
*
|
|
735
|
+
* What an agent did and found working on the guide is not context for it: that is the hand-in,
|
|
736
|
+
* and the note says so. It used to list "what you found doing it" among the things to publish, on
|
|
737
|
+
* every `take`, while hand_in said a follow-up was not for answering — the agent followed the one
|
|
738
|
+
* it read last, and the author got a second guide to review with the evidence inside it.
|
|
739
|
+
*
|
|
716
740
|
* Trailing, like the handoff nudge, so it never sits in front of the frontmatter. It is here and
|
|
717
741
|
* not only in the server's instructions because an agent reads the payload it is working from and
|
|
718
742
|
* skims everything else — the same reason the bug lead is inside the document. For a bug, the
|
|
@@ -729,9 +753,10 @@ export function followUpNote(meta = {}) {
|
|
|
729
753
|
);
|
|
730
754
|
return (
|
|
731
755
|
"<!-- passalong: a follow-up is more context for this guide, written as its own guide. If " +
|
|
732
|
-
"this guide needs more — a missing detail, a step that needed explaining, what changed since
|
|
733
|
-
|
|
734
|
-
"
|
|
756
|
+
"this guide needs more — a missing detail, a step that needed explaining, what changed since " +
|
|
757
|
+
`— publish that with publish_guide parent=${meta.id}, and whoever opens this guide gets it ` +
|
|
758
|
+
"too. What you did and found working on it is not a follow-up: it goes on hand_in, in " +
|
|
759
|
+
"`checks` and `writeup`. -->"
|
|
735
760
|
);
|
|
736
761
|
}
|
|
737
762
|
|