passalong 0.17.0 → 0.19.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 CHANGED
@@ -28,25 +28,24 @@ codex mcp add passalong -- passalong mcp # Codex: the same MCP server
28
28
  ## Commands
29
29
 
30
30
  ```
31
- passalong share [file] [--to team[/handle]] publish a guide → id + link; --to hands it to a team or teammate
32
- passalong pull <id|link> fetch a guide into ./.passalong/ (git-ignored) and print it
31
+ passalong share [file] [--to team[/handle]] publish a guide → id + link, Ready; --to addresses it
32
+ passalong pull <id|link> print a guide; --save also writes ./.passalong/<id>.md (git-ignored)
33
33
  passalong inbox guides handed to you that you have not pulled yet
34
34
  passalong board waiting on you, in flight, landed, worth keeping
35
35
  passalong activity [--all] what happened while you were away; clears unless --all
36
- passalong list [query] your guides and your teams', local and synced
36
+ passalong list [query] your guides and your teams'
37
37
  passalong open <id> [--print] view a guide in the browser (or the terminal)
38
38
  passalong hub open your synced guides in the browser
39
- passalong works <id> <shot> you tried it and it holds up, with a screenshot of it working
40
- passalong broken <id> <why> you tried it and it does not — the author is told, with your reason
41
39
  passalong archive <id> off your board and out of the free tier's count (was `done`)
42
40
  passalong promote <id> deprecated: mark promoted
43
41
  passalong rm <id> delete a guide locally and from sync
44
42
  passalong export [dir] dump every guide as plain markdown
45
- passalong login [token] sign in from the browser, or attach this machine with a token
43
+ passalong login sign in from the browser
44
+ passalong login --with-token read a token from stdin instead (CI, scripts)
46
45
  passalong login --new add another account to this machine
47
46
  passalong accounts the accounts signed in here, and which is the default
48
47
  passalong use <name> make one of them the default
49
- passalong logout <name> forget one of them
48
+ passalong logout [name] forget one of them (default: the current one)
50
49
  passalong me [--handle H] [--name N] [--email E] who you are to teammates
51
50
  passalong team current team and its members
52
51
  passalong team create <name> start a team (you become its owner)
@@ -74,11 +73,10 @@ passalong inbox # on Ada's side: what was handed to you
74
73
  passalong activity # back on your side: it landed, and who pulled it
75
74
  ```
76
75
 
77
- A receiver who tries the work reports back with `passalong works <id> <screenshot.png>` or
78
- `passalong broken <id> <why>`. That is separate from `done`, which means *implemented*: a verdict
79
- means it actually runs. A passing one has to show it — at least one screenshot of it working, kept
80
- until 5 days after the guide is closed — a failing one has to say why, and the author sees it on
81
- their board and in their inbox.
76
+ Every guide, whatever its kind, moves the same way: it is Ready when shared (or a draft, with
77
+ `status: draft`, until a person runs `passalong ready <id>`), then whoever takes it (`passalong take <id>`) holds it, hands it in with
78
+ the evidence, and a person approves it or sends it back with the reason (`passalong approve` /
79
+ `passalong reject`).
82
80
 
83
81
  Sign in at [the hub](https://passalong.dev/hub) with an email and password, and mint
84
82
  an API token there for the CLI and MCP servers — named, revocable, and shown once. Your password
@@ -102,10 +100,10 @@ promote them into the team's small set of maintained references.
102
100
 
103
101
  Tools: `search_guides`, `inbox`, `get_guide`, `publish_guide` (with `to`), `guide_template`, `set_guide_status`.
104
102
 
105
- ## Guides are files
103
+ ## Guides are plain markdown
106
104
 
107
- Plain markdown with frontmatter in `~/.passalong/guides`. Yours to edit, grep, and commit.
108
- `passalong export` dumps everything. Works with no account; `passalong login` adds sync and share links.
105
+ Plain markdown with frontmatter, held on the server; nothing piles up on your machine. Ask for a
106
+ file when you want one: `passalong pull <id> --save`, or `passalong export` for every guide.
109
107
 
110
108
  Point at a self-hosted server with `PASSALONG_API=https://your-host` before `passalong login`.
111
109
 
package/bin/passalong CHANGED
@@ -6,26 +6,25 @@
6
6
  // handed to whoever pulls or starts that guide
7
7
  // TARGET is a team, team/@handle for one person, or "team/#group" for the people who do a
8
8
  // thing — quote that one, since a shell may read the # as a comment
9
- // passalong task "<what needs doing>" write a task for an agent; it waits in Draft
10
- // passalong ready <id...|--all> tasks you have read are ready — an agent may take them now
9
+ // passalong task "<what needs doing>" write a task for an agent; Ready at once
10
+ // passalong ready <id...|--all> guides you have read are ready — whoever they are for may take them now
11
11
  // passalong tasks every task and where it is: draft, ready, claimed, stalled, review
12
12
  // passalong work [--once] [--agent CMD] run an agent on each ready task here, one after another
13
- // passalong approve <id> the work in review is done
14
- // passalong reject <id> <why> not done: back to Ready, and the next agent reads why
13
+ // passalong reopen <id> <why> finished work isn't right: back to To do, and whoever takes it next reads why
15
14
  // passalong release <id> take work back from whoever holds it, of any kind
16
15
  // passalong attach <file> upload a picture or a file and print the markdown that points at it
17
16
  // passalong reply <id> <text> write to whoever holds it (--file PATH to attach); or leave a note
18
- // passalong pull <id|link> fetch a guide into ./.passalong/ and print it
19
- // passalong start <id|link> pull it and take it — one command for "I am doing this"
17
+ // passalong pull <id|link> [--save] print a guide; --save also writes ./.passalong/<id>.md
18
+ // passalong start <id|link> [--save] the same as take, by link as well as id
20
19
  // passalong inbox guides handed to you that you have not pulled yet
21
20
  // passalong board waiting on you, in flight, landed, worth keeping
22
21
  // passalong activity [--all] what happened while you were away; clears unless --all
23
22
  // passalong log [repo] what you did, newest first, by month
24
- // passalong list [query] your guides and your teams', local and synced
23
+ // passalong list [query] your guides and your teams'
25
24
  // passalong open <id> [--print] view a guide in the browser (or the terminal)
26
25
  // passalong hub open your synced guides in the browser
27
- // passalong take <id> you are doing it — said before the work, so the sender stops guessing
28
- // passalong pass <id> <why> it is not yours — hands it back to its author with your reason
26
+ // passalong take <id> [--save] you are doing it: nobody else can take it while you hold it
27
+ // passalong pass <id> <why> it is not yours, or you are stuck — hands it back with your reason
29
28
  // passalong assign <id> <to> give it to @handle (or several: @ada @bola), a #group, or "team" — yours, or assigned to you
30
29
  // passalong recall <id> take back something you sent: private again, gone from their inboxes
31
30
  // passalong admin create <email> make an account that runs Passalong (owner only)
@@ -35,16 +34,15 @@
35
34
  // give Solo to @handle, or Team to team/slug, until a date (operator only)
36
35
  // passalong gifts what has been given away, and what is still running
37
36
  // passalong ungift <gift id> take one back now
38
- // passalong works <id> <shot.png...> you tried it and it holds up — with a screenshot of it working
39
- // passalong broken <id> <why> you tried it and it does not — the author is told, with your reason
40
37
  // passalong archive <id> off your board and out of the free tier's count (was `done`)
41
38
  // passalong rm <id> delete a guide locally and from sync
42
39
  // passalong export [dir] dump every guide as plain markdown (default ./passalong-export)
43
- // passalong login [token] sign in from the browser, or attach this machine with a token
40
+ // passalong login sign in from the browser
41
+ // passalong login --with-token read a token from stdin instead (CI, scripts)
44
42
  // passalong login --new add another account to this machine
45
43
  // passalong accounts the accounts signed in here, and which is the default
46
44
  // passalong use <name> make one of them the default
47
- // passalong logout <name> forget one of them
45
+ // passalong logout [name] forget one of them (default: the current one)
48
46
  // passalong me [--handle H] [--name N] [--email E] who you are to teammates
49
47
  // passalong team current team and its members
50
48
  // passalong team create <name> start a team (you become its owner)
@@ -65,7 +63,7 @@ import { dirname, join, resolve } from "node:path";
65
63
  import { fileURLToPath } from "node:url";
66
64
  import * as api from "../src/api.js";
67
65
  import { scaffold, taskScaffold } from "../src/capture.js";
68
- import { serialize } from "../src/guide.js";
66
+ import { parse, serialize } from "../src/guide.js";
69
67
  import * as passalong from "../src/passalong.js";
70
68
  import * as store from "../src/store.js";
71
69
 
@@ -167,75 +165,20 @@ async function browserLogin() {
167
165
  const got = await api.devicePoll(start.id, verifier).catch((e) => die(e.message));
168
166
  if (!got) continue;
169
167
  await keepLogin(got.token);
170
- console.error("Attach other machines with:");
171
- console.error(` passalong login ${got.token}`);
172
- console.error("(keep it private — it is a credential, not a password)");
173
168
  console.error("Give teammates a name to address you by: passalong me --handle <you>");
174
169
  return;
175
170
  }
176
171
  die("nobody approved it in time. Run `passalong login` again.");
177
172
  }
178
173
 
179
- /** The old way, kept for anyone who asks for it with `--email`: an email and a password, typed here. */
180
- async function passwordLogin(f) {
181
- // An account you cannot sign in to is one that dies with this file. This used to mint one
182
- // silently and then suggest adding a password afterwards; it asks up front instead.
183
- if (!tty)
184
- die(
185
- "no terminal to ask on. Sign in at " +
186
- `${api.baseUrl()}/hub, then attach this machine with \`passalong login <token>\``,
187
- );
188
-
189
- const email = f.email || (await ask("Email: "));
190
- if (!email) die("an email is needed to sign in");
191
- const password = await ask("Password: ", { hidden: true });
192
- if (!password) die("a password is needed to sign in");
193
-
194
- const label = `${hostname()} cli`;
195
- let who = await api.signIn(email, password, { label }).catch((e) => e);
196
- if (who instanceof Error) {
197
- // 401 is "no such account, or the wrong password", said the same way on purpose so the API
198
- // does not confirm which addresses exist. The CLI has to ask rather than guess, because
199
- // creating an account for a mistyped address is worse than being told to try again.
200
- if (who.status !== 401) die(who.message);
201
- const yes = (await ask(`No account signed in as ${email}. Create one? [y/N] `)).toLowerCase();
202
- if (yes !== "y" && yes !== "yes")
203
- die("not signed in. If the password was wrong, try again, or reset it at " + `${api.baseUrl()}/hub`);
204
- const again = await ask("Choose a password: ", { hidden: true });
205
- if (again !== password) die("those did not match");
206
- who = await api.signIn(email, password, { make: true, label }).catch((e) => die(e.message));
207
- }
208
-
209
- await keepLogin(who.token);
210
- console.error("Attach other machines with:");
211
- console.error(` passalong login ${who.token}`);
212
- console.error("(keep it private — it is a credential, not a password)");
213
- console.error("Give teammates a name to address you by: passalong me --handle <you>");
214
- }
215
-
216
- async function ask(question, { hidden = false } = {}) {
217
- const { createInterface } = await import("node:readline");
218
- const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
219
- if (hidden) {
220
- // readline still writes the prompt itself; everything after it is swallowed. The newline the
221
- // user's Return would have echoed is put back by hand, or the next line starts mid-prompt.
222
- rl.output.write(question);
223
- rl._writeToOutput = () => {};
224
- }
225
- try {
226
- // `close` as well as an answer: stdin reaching EOF part-way through — a pipe, a closed pty, a
227
- // ^D — never calls the question's callback, and without this the promise is simply never
228
- // settled and the process exits on a warning about it rather than saying what happened.
229
- const answer = await new Promise((done) => {
230
- rl.on("close", () => done(null));
231
- rl.question(hidden ? "" : question, done);
232
- });
233
- if (hidden) process.stdout.write("\n");
234
- if (answer === null) die("nothing to read from — sign in at the terminal, or pass a token");
235
- return answer.trim();
236
- } finally {
237
- rl.close();
238
- }
174
+ /** A token piped in, for machines with no browser: `passalong login --with-token < token.txt`. */
175
+ async function stdinLogin() {
176
+ if (process.stdin.isTTY) die("--with-token reads the token from stdin: passalong login --with-token < token.txt");
177
+ let raw = "";
178
+ for await (const chunk of process.stdin) raw += chunk;
179
+ const token = raw.trim();
180
+ if (!token) die("no token on stdin");
181
+ await keepLogin(token);
239
182
  }
240
183
 
241
184
  function edit(path) {
@@ -360,7 +303,7 @@ const commands = {
360
303
  if (tty && !f["no-edit"]) edit(path);
361
304
  const to = typeof f.to === "string" ? f.to : undefined;
362
305
  const follows = typeof f.follows === "string" ? f.follows : undefined;
363
- const { guide, url, synced, notified, path: saved } = await passalong
306
+ const { guide, url, notified } = await passalong
364
307
  .share(readFileSync(path, "utf8"), { to, follows })
365
308
  .catch((e) => die(e.message));
366
309
  // Any draft that went out is spent, however it was named — a task that failed validation is
@@ -368,14 +311,13 @@ const commands = {
368
311
  // removeDraft() touches nothing outside the drafts directory.
369
312
  store.removeDraft(resolve(path));
370
313
  console.log(guide.meta.id);
371
- console.error(`shared "${guide.meta.title}" → ${saved}`);
314
+ console.error(`shared "${guide.meta.title}"`);
372
315
  if (guide.meta.parent) console.error(`follows ${guide.meta.parent}`);
373
316
  if (guide.meta.team)
374
317
  console.error(
375
318
  `handed to ${guide.meta.team}${guide.meta.to ? `/@${guide.meta.to}${notified ? " (emailed)" : " (in their inbox)"}` : " (team inbox)"}`,
376
319
  );
377
- if (synced) console.error(`link: ${url}`);
378
- else console.error("not synced (run `passalong login` to sync across machines)");
320
+ console.error(`link: ${url}`);
379
321
  },
380
322
 
381
323
  /**
@@ -387,17 +329,15 @@ const commands = {
387
329
  const path = store.draftPath(`task-${new Date().toISOString().replace(/[:.]/g, "-")}`);
388
330
  writeFileSync(path, taskScaffold(sentence));
389
331
  if (tty && !f["no-edit"]) edit(path);
390
- const { guide, synced } = await passalong
332
+ const { guide } = await passalong
391
333
  .share(readFileSync(path, "utf8"))
392
334
  .catch((e) => die(`${e.message}\ndraft kept at ${path} — fix it and run \`passalong share ${path}\``));
393
335
  store.removeDraft(path);
394
336
  const id = guide.meta.id;
395
337
  console.log(id);
396
338
  const target = guide.meta.target_context;
397
- console.error(`task "${guide.meta.title}" is in Draft${target ? ` for ${target}` : " (no repo)"}`);
339
+ console.error(`task "${guide.meta.title}" is Ready${target ? ` for ${target}` : " (no repo)"}`);
398
340
  if (guide.meta.team) console.error(`handed to ${guide.meta.team} (team inbox)`);
399
- console.error(`when an agent may take it: passalong ready ${id}`);
400
- if (!synced) console.error("not synced (run `passalong login` — agents elsewhere cannot see it)");
401
341
  },
402
342
 
403
343
  async tasks() {
@@ -442,16 +382,25 @@ const commands = {
442
382
  if (n) console.error("review them with: passalong tasks");
443
383
  },
444
384
 
445
- async approve(f) {
446
- const id = f._[0] || die("usage: passalong approve <id>");
447
- await passalong.approveTask(id).catch((e) => die(e.message));
448
- console.error(`${id} is done`);
385
+ // Nothing waits for approval now (docs/REVIEW.md): finishing is final. Kept so a script that
386
+ // calls it is told, not refused.
387
+ async approve() {
388
+ console.error(
389
+ "nothing to approve: finished work is already Done. If it isn't right: passalong reopen <id> <why>",
390
+ );
391
+ },
392
+
393
+ /** Finished work back to To do, with why at its top; whoever finished it is told. */
394
+ async reopen(f) {
395
+ const id = f._[0] || die("usage: passalong reopen <id> <why>");
396
+ const why = f._.slice(1).join(" ");
397
+ if (!why) die("say why: passalong reopen <id> <what is wrong>");
398
+ await passalong.reopen(id, why).catch((e) => die(e.message));
399
+ console.error(`${id} is back in To do — whoever takes it next reads why`);
449
400
  },
450
401
 
451
402
  async reject(f) {
452
- const id = f._[0] || die("usage: passalong reject <id> <why>");
453
- await passalong.rejectTask(id, f._.slice(1).join(" ")).catch((e) => die(e.message));
454
- console.error(`${id} is back in Ready — the next agent reads why`);
403
+ return commands.reopen(f);
455
404
  },
456
405
 
457
406
  async release(f) {
@@ -480,31 +429,30 @@ const commands = {
480
429
 
481
430
  async pull(f) {
482
431
  const ref = f._[0] || die("usage: passalong pull <id|link>");
483
- const { guide, markdown, path, from } = await passalong.pull(ref).catch((e) => die(e.message));
432
+ const { guide, markdown, path } = await passalong
433
+ .pull(ref, { save: Boolean(f.save) })
434
+ .catch((e) => die(e.message));
484
435
  process.stdout.write(markdown);
485
- console.error(`\npulled from ${from} → ${path}`);
436
+ if (path) console.error(`\nsaved to ${path}`);
486
437
  await listFollowUps(guide.meta);
487
438
  },
488
439
 
489
440
  /**
490
- * Pull and take in one command. `pull` says it arrived, `take` says somebody is doing it, and
491
- * they stay separate because reading a guide to decide it is not yours is what `pass` is for.
492
- * This is the pair for the case where you already know: you are picking this up.
441
+ * Take it: you are doing it, and nobody else can take it while you hold it. The guide is printed,
442
+ * and `--save` also writes it to ./.passalong/. Every kind is taken the same way, and `start` is
443
+ * the same command by its older name, which also takes a share link.
493
444
  */
494
445
  async start(f) {
495
- const ref = f._[0] || die("usage: passalong start <id|link>");
496
- const r = await passalong.start(ref).catch((e) => die(e.message));
446
+ const ref = f._[0] || die("usage: passalong take <id|link>");
447
+ const r = await passalong.start(ref, { save: Boolean(f.save) }).catch((e) => die(e.message));
448
+ if (!r.guide?.markdown) return console.error(r.say || "nothing to take");
497
449
  process.stdout.write(r.markdown);
498
- console.error(`\npulled from ${r.from} → ${r.path}`);
499
- if (r.took) console.error("you are on it, and the author has been told");
500
- else if (r.own) console.error("your own guide — nothing to take");
501
- else if (r.ack_error) console.error(`not taken (${r.ack_error}) — run \`passalong take ${r.guide.meta.id}\``);
502
- // Saying nothing here would be the silence the whole hop exists to end: you would have the
503
- // guide, believe you had answered for it, and the sender would still be waiting.
504
- else console.error("not taken — sync is off, so nobody was told (run `passalong login`)");
505
- await listFollowUps(r.guide.meta);
450
+ if (r.path) console.error(`\nsaved to ${r.path}`);
451
+ console.error(`\nyou hold ${r.guide.id} — hand it in when it is done, or pass it with the reason`);
452
+ await listFollowUps(parse(r.markdown).meta);
506
453
  },
507
454
 
455
+
508
456
  /**
509
457
  * Upload an image and print the markdown that points at it. Evidence goes *in* the document:
510
458
  * a guide travels as markdown to whoever holds its link, so a file beside it does not travel.
@@ -612,7 +560,6 @@ const commands = {
612
560
  async list(f) {
613
561
  const scope = typeof f.team === "string" ? f.team : f.mine ? "mine" : "";
614
562
  const rows = await passalong.list(f._.join(" "), { scope });
615
- if (rows.warning) console.error(`warning: ${rows.warning}`);
616
563
  if (!rows.length)
617
564
  return console.error(
618
565
  "no guides yet — run `passalong share` after your next finished piece of work",
@@ -644,17 +591,8 @@ const commands = {
644
591
  } else console.log(url);
645
592
  },
646
593
 
647
- /**
648
- * The first word back, before any of the work.
649
- *
650
- * Between a handoff and a pull the sender had no signal at all: an unanswered guide looks
651
- * exactly like one nobody has noticed. These two are the hop the MCP tool and the hub already
652
- * had and a terminal did not.
653
- */
654
594
  async take(f) {
655
- const id = f._[0] || die("usage: passalong take <id>");
656
- const r = await passalong.ack(id, true).catch((e) => die(e.message));
657
- console.error(`${r.id} — you are on it, and the author has been told`);
595
+ return commands.start(f);
658
596
  },
659
597
 
660
598
  async pass(f) {
@@ -663,10 +601,11 @@ const commands = {
663
601
  // Required, and the reason is the whole point: "not me" without "why" leaves the sender where
664
602
  // the silence did, still guessing whether to re-address it, wait, or go and ask.
665
603
  if (!why) die("say why you are passing: passalong pass <id> <why it is not yours>");
666
- const r = await passalong.ack(id, false, why).catch((e) => die(e.message));
667
- console.error(`${r.id} handed back — the author has been told: "${r.note}"`);
604
+ await passalong.pass(id, why).catch((e) => die(e.message));
605
+ console.error(`${id} handed back with your reason: "${why}"`);
668
606
  },
669
607
 
608
+
670
609
  async assign(f) {
671
610
  const id = f._[0] || die('usage: passalong assign <id> <@handle | #group | team>');
672
611
  const to = f._.slice(1).join(",") || die('say who: @handle (or several), #group, or "team" for everyone');
@@ -701,7 +640,7 @@ const commands = {
701
640
  console.log(`token ${r.token}`);
702
641
  console.log(`password link ${r.password_url}`);
703
642
  console.error(
704
- `\n${r.email} is an admin. The token is for \`passalong login\`, the link sets their ` +
643
+ `\n${r.email} is an admin. The token is for \`passalong login --with-token\`, the link sets their ` +
705
644
  `password for the hub, and it expires ${r.expires.slice(0, 10)}. Neither is shown again.`,
706
645
  );
707
646
  return;
@@ -755,23 +694,6 @@ const commands = {
755
694
  console.error(`${id} taken back — that plan is lapsed as of now`);
756
695
  },
757
696
 
758
- async works(f) {
759
- const id = f._[0] || die("usage: passalong works <id> <screenshot.png...> [what you checked]");
760
- const { images, note } = passalong.proofArgs(f._.slice(1));
761
- const r = await passalong.verdict(id, true, note, { images }).catch((e) => die(e.message));
762
- console.error(
763
- `${r.id} verified with ${images.length || "your"} screenshot${images.length === 1 ? "" : "s"} — the author has been told`,
764
- );
765
- },
766
-
767
- async broken(f) {
768
- const id = f._[0] || die("usage: passalong broken <id> <what happened>");
769
- const why = f._.slice(1).join(" ");
770
- if (!why) die("say what went wrong: passalong broken <id> <what happened>");
771
- const r = await passalong.verdict(id, false, why).catch((e) => die(e.message));
772
- console.error(`${r.id} reported as not working — the author has been told: "${r.note}"`);
773
- },
774
-
775
697
  /**
776
698
  * Archive a guide: off your board, and out of what the free tier counts.
777
699
  *
@@ -806,29 +728,26 @@ const commands = {
806
728
 
807
729
  async export(f) {
808
730
  const dir = resolve(f._[0] || "passalong-export");
809
- const paths = passalong.exportAll(dir);
731
+ const paths = await passalong.exportAll(dir).catch((e) => die(e.message));
810
732
  console.error(`exported ${paths.length} guide${paths.length === 1 ? "" : "s"} → ${dir}`);
811
733
  },
812
734
 
813
735
  async login(f) {
814
- const given = f._[0];
815
- if (given) {
816
- await keepLogin(given);
817
- return;
818
- }
736
+ // A token on the command line lands in shell history, so it is only read from stdin.
737
+ if (f._[0]) die("pass a token on stdin instead: passalong login --with-token < token.txt");
738
+ if (f.email || f.password)
739
+ die("password sign-in is gone: run `passalong login` and approve it in the browser");
740
+ if (f["with-token"]) return stdinLogin();
819
741
  if (api.loggedIn() && !f.new) {
820
742
  const who = await api.me().catch(() => null);
821
743
  if (who) {
822
- console.error(`already logged in (account ${who.account}). Attach another machine with:`);
823
- console.error(` passalong login ${api.token()}`);
744
+ console.error(`already logged in as ${who.handle ? `@${who.handle}` : who.email || who.account}.`);
824
745
  console.error("Add another account to this machine with `passalong login --new`.");
825
746
  return;
826
747
  }
827
748
  }
828
- // Passwords are for someone who asks: `--email` keeps the prompts. Everyone else is sent to the
829
- // browser, which works with no terminal at all — an agent can start it, and the person it is
749
+ // The browser works with no terminal at all — an agent can start it, and the person it is
830
750
  // working for clicks once.
831
- if (f.email || f.password) return passwordLogin(f);
832
751
  return browserLogin();
833
752
  },
834
753
 
@@ -865,7 +784,7 @@ const commands = {
865
784
 
866
785
  /** Forget one of them on this machine. The account itself is untouched: only its token here goes. */
867
786
  async logout(f) {
868
- const name = f._[0] || die("usage: passalong logout <name> (see `passalong accounts`)");
787
+ const name = f._[0] || store.readConfig().active || die("not logged in (see `passalong accounts`)");
869
788
  const r = store.removeAccount(name);
870
789
  if (!r.removed) die(`no account called "${name}" here`);
871
790
  console.error(
@@ -1135,6 +1054,8 @@ else if (!commands[cmd])
1135
1054
  "if you expected this command, `npm i -g passalong` for the newest one.",
1136
1055
  );
1137
1056
  else {
1057
+ // Once per machine; a failure leaves it for the next command rather than failing this one.
1058
+ await passalong.tidy().catch(() => {});
1138
1059
  await commands[cmd](argv).catch((e) => die(e.message));
1139
1060
  // After the command, never before it: a version check is not worth a millisecond of somebody's
1140
1061
  // first impression, and it must never be the reason a command felt slow. `mcp` is a server that
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "passalong",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "description": "Hand tasks, bugs and finished work to AI coding agents, and get back a write-up with evidence.",
5
5
  "mcpName": "io.github.codellyson/passalong",
6
6
  "keywords": [
package/skill/SKILL.md CHANGED
@@ -66,13 +66,12 @@ machine, another agent session, or a teammate without your branch.
66
66
  whoever opens the original, person or agent. It travels the other way too — whoever opens the
67
67
  follow-up gets the guide it came out of, and where that guide has got to — so write it as a
68
68
  note on that work rather than as a standalone brief. What you did or found while working on a
69
- guide belongs in its `hand_in`, not a follow-up. Whether a guide worked is its own answer:
70
- `passalong works <id> <screenshot>` or `passalong broken <id> <why>`.
71
- 6. **Report** the id (and link if synced) and one line on how to use it on the other side:
72
- `passalong start <id>` in the target repo, or "start passalong <id>" to an agent with the
73
- Passalong MCP server. `start` rather than `pull` on purpose: it takes the handoff as well as
74
- fetching it, and until somebody says they are on it the sender cannot tell a guide nobody
75
- noticed from one somebody is deep in.
69
+ guide belongs in its `hand_in`, not a follow-up.
70
+ 6. **Report** the id (and link). It is Ready and has reached whoever it is for. Then one line on how to use
71
+ it on the other side: `passalong take <id>` in the target repo, or "take passalong <id>" to an
72
+ agent with the Passalong MCP server. `take` rather than `pull` on purpose: it holds the work as
73
+ well as fetching it, and until somebody takes it the sender cannot tell a guide nobody noticed
74
+ from one somebody is deep in.
76
75
 
77
76
  If the `passalong` command is missing, tell the user to run `npm i -g passalong && passalong setup` and
78
77
  still write the draft file so nothing is lost.
package/src/api.js CHANGED
@@ -337,10 +337,17 @@ export const renameFolderDocument = (folderId, documentId, name) =>
337
337
  method: "PATCH",
338
338
  body: { name },
339
339
  });
340
+ export const moveFolderDocument = (folderId, documentId, to, name = "") =>
341
+ call(`/v1/folders/${encodeURIComponent(folderId)}/documents/${encodeURIComponent(documentId)}`, {
342
+ method: "PATCH",
343
+ body: name ? { folder: to, name } : { folder: to },
344
+ });
340
345
  export const folderDocumentHistory = (folderId, documentId, version = 0) =>
341
346
  call(
342
347
  `/v1/folders/${encodeURIComponent(folderId)}/documents/${encodeURIComponent(documentId)}/revisions${version ? `/${version}` : ""}`,
343
348
  );
349
+ export const shareFolder = (folderId) =>
350
+ call(`/v1/folders/${encodeURIComponent(folderId)}/share`, { method: "POST" });
344
351
  export const linkFolderGuide = (folderId, guide) =>
345
352
  call(`/v1/folders/${encodeURIComponent(folderId)}/guides`, { method: "POST", body: { guide } });
346
353
  export const unlinkFolderGuide = (folderId, guide) =>
@@ -372,7 +379,10 @@ export const inbox = () => call("/v1/inbox");
372
379
  export const createReport = (body) => call("/v1/reports", { method: "POST", body });
373
380
  /** One report and the issues filed under it, grouped by product area. */
374
381
  export const report = (id) => call(`/v1/reports/${encodeURIComponent(id)}`);
375
- export const get = (id) => call(`/v1/guides/${id}`, { raw: true });
382
+ export const get = (id, { peek = false } = {}) =>
383
+ call(`/v1/guides/${id}${q({ peek: peek ? "1" : "" })}`, { raw: true });
384
+ /** One page of every guide this account can read, for `export`. Records no pull. */
385
+ export const exportPage = (after = "") => call(`/v1/export${q({ after })}`);
376
386
  /**
377
387
  * A guide's follow-ups, one level down. `markdown` adds each one's content, oldest first and
378
388
  * capped server-side; neither form counts as opening them.
@@ -419,6 +429,13 @@ export const approveTask = (id) =>
419
429
  call(`/v1/tasks/${encodeURIComponent(id)}/approve`, { method: "POST", body: {} });
420
430
  export const rejectTask = (id, why) =>
421
431
  call(`/v1/tasks/${encodeURIComponent(id)}/reject`, { method: "POST", body: { why } });
432
+ export const reopen = (id, why) =>
433
+ call(`/v1/guides/${encodeURIComponent(id)}/reopen`, { method: "POST", body: { why } });
434
+ /** A handoff's or bug's approve and send-back. No `place` names the only hand-in there is. */
435
+ export const closeGuide = (id) =>
436
+ call(`/v1/guides/${encodeURIComponent(id)}/close`, { method: "POST", body: {} });
437
+ export const sendBack = (id, why) =>
438
+ call(`/v1/guides/${encodeURIComponent(id)}/send_back`, { method: "POST", body: { why } });
422
439
  /** Take work back from whoever holds it, of any kind. `/v1/tasks/…/release` is the old path. */
423
440
  export const release = (id) =>
424
441
  call(`/v1/guides/${encodeURIComponent(id)}/release`, { method: "POST", body: {} });
package/src/guide.js CHANGED
@@ -497,7 +497,9 @@ export function stamp(guide, defaults = {}) {
497
497
  const meta = { ...guide.meta };
498
498
  if (!meta.id) meta.id = newId();
499
499
  if (!meta.created) meta.created = new Date().toISOString();
500
- if (!meta.status) meta.status = "draft";
500
+ // Ready unless whoever wrote it said draft: a guide sent to someone is meant to reach them, and
501
+ // drafts were where addressed work sat forgotten (every draft in production on 2026-10-15).
502
+ if (!meta.status) meta.status = "published";
501
503
  for (const [k, v] of Object.entries(defaults))
502
504
  if (meta[k] === undefined || meta[k] === "") meta[k] = v;
503
505
  return { meta, body: guide.body };
@@ -616,7 +618,7 @@ export function template(meta = {}) {
616
618
  summary: "",
617
619
  author: "",
618
620
  source_context: "",
619
- status: "draft",
621
+ status: "published",
620
622
  stack_assumptions: [],
621
623
  tags: [],
622
624
  ...meta,
@@ -654,7 +656,7 @@ function bugTemplate(meta = {}) {
654
656
  kind: "bug",
655
657
  author: "",
656
658
  source_context: "",
657
- status: "draft",
659
+ status: "published",
658
660
  severity: "s3",
659
661
  stack_assumptions: [],
660
662
  tags: ["bug"],
@@ -696,7 +698,7 @@ function taskTemplate(meta = {}) {
696
698
  author: "",
697
699
  source_context: "",
698
700
  target_context: "",
699
- status: "draft",
701
+ status: "published",
700
702
  stack_assumptions: [],
701
703
  tags: [],
702
704
  ...meta,