passalong 0.1.0 → 0.3.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
@@ -25,25 +25,72 @@ passalong login # optional: sync guides across machines and get share links
25
25
  ## Commands
26
26
 
27
27
  ```
28
- passalong share [file] publish a guide (newest draft, or a scaffold you edit) → id + link
29
- passalong pull <id|link> fetch a guide into ./.passalong/ and print it
30
- passalong list [query] your guides, local and synced
28
+ passalong share [file] [--to team[/handle]] publish a guide → id + link; --to hands it to a team or teammate
29
+ passalong pull <id|link> fetch a guide into ./.passalong/ (git-ignored) and print it
30
+ passalong inbox guides handed to you that you have not pulled yet
31
+ passalong board waiting on you, in flight, landed, worth keeping
32
+ passalong activity [--all] what happened while you were away; clears unless --all
33
+ passalong list [query] your guides and your teams', local and synced
31
34
  passalong open <id> [--print] view a guide in the browser (or the terminal)
32
35
  passalong hub open your synced guides in the browser
33
- passalong done <id> mark consumed: implemented on the receiving side
34
- passalong promote <id> mark promoted: graduated into a reusable reference
36
+ passalong works <id> you tried it and it holds up
37
+ passalong broken <id> <why> you tried it and it does not — the author is told, with your reason
38
+ passalong done <id> deprecated: mark consumed
39
+ passalong promote <id> deprecated: mark promoted
35
40
  passalong rm <id> delete a guide locally and from sync
36
41
  passalong export [dir] dump every guide as plain markdown
37
- passalong login [token] create an account, or attach this machine to an existing one
42
+ passalong login [token] create an account, or attach this machine with a token from your hub
43
+ passalong me [--handle H] [--name N] [--email E] who you are to teammates
44
+ passalong team current team and its members
45
+ passalong team create <name> start a team (you become its owner)
46
+ passalong team invite [email] make an invite link (mailed when an email is given)
47
+ passalong team join <link> accept an invite
48
+ passalong team use <slug> switch the current team
38
49
  passalong setup install the Claude Code capture skill + register the MCP server
39
50
  passalong mcp run the MCP server over stdio
40
51
  ```
41
52
 
53
+ ## Teams
54
+
55
+ A team is the unit of sharing. Members can find, pull, and mark consumed every guide shared to
56
+ the team; a guide can also be handed to one person, who sees it in their inbox (and by email
57
+ when the server has mail configured).
58
+
59
+ ```sh
60
+ passalong me --handle lukman # how teammates address you
61
+ passalong team create Khaime # you become owner; "khaime" is now current
62
+ passalong team invite ada@example.com # or no email: prints a link to send yourself
63
+ passalong team join <link> # on Ada's machine — or she just opens the link
64
+ passalong share --to khaime/ada # hand this guide to Ada
65
+ passalong inbox # on Ada's side: what was handed to you
66
+ passalong activity # back on your side: it landed, and who pulled it
67
+ ```
68
+
69
+ A receiver who tries the work reports back with `passalong works <id>` or `passalong broken <id> <why>`.
70
+ That is separate from `done`, which means *implemented*: a verdict means it actually runs, a failing
71
+ one has to say why, and the author sees it on their board and in their inbox.
72
+
73
+ Sign in at [the hub](https://passalong.dev/hub) with an email and password, and mint
74
+ an API token there for the CLI and MCP servers — named, revocable, and shown once. Your password
75
+ never goes near the terminal. `passalong login` with no arguments still makes an account without
76
+ any of that; add an email and password later to be able to sign in from a browser and recover it.
77
+
78
+ Not everyone who receives a guide implements it. Every guide page has a **Verify** view
79
+ (`?view=verify`) that leads with Problem, Verification and Gotchas and folds the implementation
80
+ away — for a tester or anyone checking the work rather than doing it. Invites are accepted in the
81
+ browser, so a teammate needs no terminal to be part of a team.
82
+
83
+ `passalong activity` is the other half of the transfer: it tells you when someone pulled a guide
84
+ you handed over, when they marked it consumed, and when an invite was taken up. A handoff
85
+ addressed to a person is also emailed; a team-wide share is not, because mail nobody is
86
+ named in is the first thing people filter out. Guides that keep getting pulled get a nudge to
87
+ promote them into the team's small set of maintained references.
88
+
42
89
  ## MCP
43
90
 
44
91
  `passalong setup` registers the server with Claude Code. For other clients, run `passalong mcp` over stdio.
45
92
 
46
- Tools: `search_guides`, `get_guide`, `publish_guide`, `guide_template`, `set_guide_status`.
93
+ Tools: `search_guides`, `inbox`, `get_guide`, `publish_guide` (with `to`), `guide_template`, `set_guide_status`.
47
94
 
48
95
  ## Guides are files
49
96
 
package/bin/passalong CHANGED
@@ -1,16 +1,34 @@
1
1
  #!/usr/bin/env node
2
2
  // Passalong CLI. Few flags on purpose: the defaults are the product.
3
3
  //
4
- // passalong share [file] publish a guide (newest draft, or a scaffold you edit) → id + link
4
+ // passalong share [file] [--to TARGET] publish a guide → id + link
5
+ // TARGET is a team, team/@handle for one person, or "team/#group" for the people who do a
6
+ // thing — quote that one, since a shell may read the # as a comment
7
+ // passalong attach <image> upload evidence and print the markdown that points at it
5
8
  // passalong pull <id|link> fetch a guide into ./.passalong/ and print it
6
- // passalong list [query] your guides, local and synced
9
+ // passalong start <id|link> pull it and take it — one command for "I am doing this"
10
+ // passalong inbox guides handed to you that you have not pulled yet
11
+ // passalong board waiting on you, in flight, landed, worth keeping
12
+ // passalong activity [--all] what happened while you were away; clears unless --all
13
+ // passalong log [repo] what you did, newest first, by month
14
+ // passalong list [query] your guides and your teams', local and synced
7
15
  // passalong open <id> [--print] view a guide in the browser (or the terminal)
8
16
  // passalong hub open your synced guides in the browser
9
- // passalong done <id> mark consumed: implemented on the receiving side
10
- // passalong promote <id> mark promoted: graduated into a reusable reference
17
+ // passalong take <id> you are doing it — said before the work, so the sender stops guessing
18
+ // passalong pass <id> <why> it is not yours — hands it back to its author with your reason
19
+ // passalong works <id> you tried it and it holds up
20
+ // passalong broken <id> <why> you tried it and it does not — the author is told, with your reason
21
+ // passalong done <id> archive it: off your board and out of the free tier's count
22
+ // passalong promote <id> deprecated: mark promoted
11
23
  // passalong rm <id> delete a guide locally and from sync
12
24
  // passalong export [dir] dump every guide as plain markdown (default ./passalong-export)
13
- // passalong login [token] create an account, or attach this machine to an existing one
25
+ // passalong login [token] create an account, or attach this machine with a token from your hub
26
+ // passalong me [--handle H] [--name N] [--email E] who you are to teammates
27
+ // passalong team current team and its members
28
+ // passalong team create <name> start a team (you become its owner)
29
+ // passalong team invite [email] make an invite link (mailed when an email is given)
30
+ // passalong team join <link|code> accept an invite
31
+ // passalong team use <slug> switch the current team
14
32
  // passalong setup install the Claude Code capture skill + register the MCP server
15
33
  // passalong mcp run the MCP server over stdio
16
34
  import { execFileSync, spawnSync } from "node:child_process";
@@ -53,18 +71,41 @@ function edit(path) {
53
71
  }
54
72
 
55
73
  function openInBrowser(url) {
56
- const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
74
+ const cmd =
75
+ process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
57
76
  spawnSync(cmd, [url], { stdio: "ignore", shell: process.platform === "win32" });
58
77
  }
59
78
 
60
79
  function relDate(iso) {
61
80
  if (!iso) return "";
62
81
  const d = (Date.now() - new Date(iso).getTime()) / 864e5;
63
- if (d < 1) return "today";
82
+ if (d < 1 / 24) return "just now";
83
+ if (d < 1) return `${Math.floor(d * 24)}h ago`;
64
84
  if (d < 30) return `${Math.floor(d)}d ago`;
65
85
  return iso.slice(0, 10);
66
86
  }
67
87
 
88
+ function guideLine(r) {
89
+ const who = r.team ? ` ${r.team}${r.to ? `/@${r.to}` : ""}${r.mine ? "" : ` from @${r.from || "?"}`}` : "";
90
+ const tags = r.tags?.length ? ` #${r.tags.join(" #")}` : "";
91
+ let pulled = "";
92
+ if (r.mine && r.pulled_by?.length) {
93
+ const p = r.pulled_by[0];
94
+ pulled = ` ← ${p.handle ? `@${p.handle}` : "link"} ${relDate(p.at)}${r.pulled_by.length > 1 ? ` +${r.pulled_by.length - 1}` : ""}`;
95
+ }
96
+ const nudge = r.mine && r.status === "published" && r.pulls >= 3 ? " ★ promote?" : "";
97
+ return `${r.id} ${r.status.padEnd(9)} ${relDate(r.created).padEnd(10)} ${r.title}${who}${tags}${pulled}${nudge}`;
98
+ }
99
+
100
+ function activityLine(n) {
101
+ const dot = n.read ? " " : "•";
102
+ return `${dot} ${(n.guide || "").padEnd(8)} ${relDate(n.at).padEnd(10)} ${n.text}`;
103
+ }
104
+
105
+ const MONTHS = "January February March April May June July August September October November December".split(" ");
106
+ /** "September 2026" from an ISO timestamp, without pulling in a date library for one heading. */
107
+ const monthOf = (iso) => `${MONTHS[Number(iso.slice(5, 7)) - 1]} ${iso.slice(0, 4)}`;
108
+
68
109
  const commands = {
69
110
  async share(f) {
70
111
  let path = f._[0];
@@ -79,15 +120,23 @@ const commands = {
79
120
  path = store.draftPath(new Date().toISOString().replace(/[:.]/g, "-"));
80
121
  writeFileSync(path, scaffold());
81
122
  fromDrafts = true;
82
- if (!tty) die(`no draft to share; scaffolded ${path} — fill it in and run \`passalong share ${path}\``);
123
+ if (!tty)
124
+ die(`no draft to share; scaffolded ${path} — fill it in and run \`passalong share ${path}\``);
83
125
  }
84
126
  }
85
127
  if (!existsSync(path)) die(`no such file: ${path}`);
86
128
  if (tty && !f["no-edit"]) edit(path);
87
- const { guide, url, synced, path: saved } = await passalong.share(readFileSync(path, "utf8")).catch((e) => die(e.message));
129
+ const to = typeof f.to === "string" ? f.to : undefined;
130
+ const { guide, url, synced, notified, path: saved } = await passalong
131
+ .share(readFileSync(path, "utf8"), { to })
132
+ .catch((e) => die(e.message));
88
133
  if (fromDrafts) store.removeDraft(path);
89
134
  console.log(guide.meta.id);
90
135
  console.error(`shared "${guide.meta.title}" → ${saved}`);
136
+ if (guide.meta.team)
137
+ console.error(
138
+ `handed to ${guide.meta.team}${guide.meta.to ? `/@${guide.meta.to}${notified ? " (emailed)" : " (in their inbox)"}` : " (team inbox)"}`,
139
+ );
91
140
  if (synced) console.error(`link: ${url}`);
92
141
  else console.error("not synced (run `passalong login` to sync across machines)");
93
142
  },
@@ -99,14 +148,116 @@ const commands = {
99
148
  console.error(`\npulled from ${from} → ${path}`);
100
149
  },
101
150
 
102
- async list(f) {
103
- const rows = await passalong.list(f._.join(" "));
104
- if (rows.warning) console.error(`warning: ${rows.warning}`);
105
- if (!rows.length) return console.error("no guides yet — run `passalong share` after your next finished piece of work");
151
+ /**
152
+ * Pull and take in one command. `pull` says it arrived, `take` says somebody is doing it, and
153
+ * they stay separate because reading a guide to decide it is not yours is what `pass` is for.
154
+ * This is the pair for the case where you already know: you are picking this up.
155
+ */
156
+ async start(f) {
157
+ const ref = f._[0] || die("usage: passalong start <id|link>");
158
+ const r = await passalong.start(ref).catch((e) => die(e.message));
159
+ process.stdout.write(r.markdown);
160
+ console.error(`\npulled from ${r.from} → ${r.path}`);
161
+ if (r.took) console.error("you are on it, and the author has been told");
162
+ else if (r.own) console.error("your own guide — nothing to take");
163
+ else if (r.ack_error) console.error(`not taken (${r.ack_error}) — run \`passalong take ${r.guide.meta.id}\``);
164
+ // Saying nothing here would be the silence the whole hop exists to end: you would have the
165
+ // guide, believe you had answered for it, and the sender would still be waiting.
166
+ else console.error("not taken — sync is off, so nobody was told (run `passalong login`)");
167
+ },
168
+
169
+ /**
170
+ * Upload an image and print the markdown that points at it. Evidence goes *in* the document:
171
+ * a guide travels as markdown to whoever holds its link, so a file beside it does not travel.
172
+ */
173
+ async attach(f) {
174
+ const file = f._[0] || die("usage: passalong attach <image> [--name LABEL]");
175
+ const shot = await passalong.attach(file, { name: f.name || "" }).catch((e) => die(e.message));
176
+ process.stdout.write(`${shot.markdown}\n`);
177
+ console.error(`uploaded ${shot.bytes} bytes → ${shot.url}`);
178
+ },
179
+
180
+ async inbox() {
181
+ const rows = await passalong.inbox().catch((e) => die(e.message));
182
+ if (!rows.length) return console.error("inbox empty — nothing handed to you that you haven't pulled");
183
+ for (const r of rows) console.log(guideLine(r));
184
+ console.error(`\n${rows.length} waiting. Take one with: passalong start <id>`);
185
+ },
186
+
187
+ async board() {
188
+ const b = await passalong.board().catch((e) => die(e.message));
189
+ const queue = (title, note, rows, line) => {
190
+ if (!rows.length) return;
191
+ console.log(`\n${title} · ${rows.length} ${note}`);
192
+ for (const g of rows) console.log(` ${g.id} ${relDate(g.created).padEnd(10)} ${line(g)}`);
193
+ };
194
+ queue("Waiting on you", "handed to you, not pulled yet", b.waiting, (g) => `${g.title} from @${g.from || "?"}`);
195
+ queue("Not working", "someone tried it and it does not hold up", b.failing, (g) =>
196
+ `${g.title} ${g.verdict?.by ? `@${g.verdict.by}` : "someone"}: ${g.verdict?.note || "no reason given"}`);
197
+ queue("In flight", "handed over, nobody has taken it", b.in_flight, (g) =>
198
+ `${g.title} to ${g.team}${g.to ? `/@${g.to}` : ""}${g.stale ? " ← not picked up" : ""}`);
199
+ queue("Landed", "someone else has it", b.landed, (g) => `${g.title} ${g.pulls} pull${g.pulls === 1 ? "" : "s"}`);
200
+ const total = b.waiting.length + b.failing.length + b.in_flight.length + b.landed.length;
201
+ if (!total) console.error("nothing in flight — every transfer is closed");
202
+ else if (b.unread) console.error(`\n${b.unread} new: passalong activity`);
203
+ },
204
+
205
+ /**
206
+ * What you did, newest first, under a heading per month.
207
+ *
208
+ * The month headings are the whole point: every other list in this CLI is ordered by what needs
209
+ * you — the board's queues, `list`'s state rank — and none of them can answer "what did I get
210
+ * done in September". Time is the axis nothing else offers.
211
+ *
212
+ * A repo narrows it, positionally and without a flag, for the same reason `list` takes its query
213
+ * that way: it is the one thing you are likely to say, and a flag would make you name it twice.
214
+ */
215
+ async log(f) {
216
+ const repo = f._.join(" ");
217
+ const rows = await passalong.log({ repo }).catch((e) => die(e.message));
218
+ if (!rows.length)
219
+ return console.error(
220
+ repo
221
+ ? `nothing passed along from anything matching "${repo}"`
222
+ : "nothing yet — your log fills up as you share, pull and answer guides",
223
+ );
224
+ let month = "";
106
225
  for (const r of rows) {
107
- const tags = r.tags?.length ? ` #${r.tags.join(" #")}` : "";
108
- console.log(`${r.id} ${r.status.padEnd(9)} ${relDate(r.created).padEnd(10)} ${r.title}${tags}`);
226
+ const m = monthOf(r.at);
227
+ if (m !== month) {
228
+ console.log(`\n${m}`);
229
+ month = m;
230
+ }
231
+ console.log(` ${r.guide} ${r.at.slice(0, 10)} ${r.text}${r.repo && !repo ? ` ${r.repo}` : ""}`);
109
232
  }
233
+ // Said every time, because the list looks like a work log and is not one. A month you shipped
234
+ // without sharing looks identical here to a month you did nothing.
235
+ console.error(`\n${rows.length} thing${rows.length === 1 ? "" : "s"}. Work that never became a guide is not here.`);
236
+ },
237
+
238
+ async activity(f) {
239
+ const { notifications, unread } = await passalong.activity({ all: f.all }).catch((e) => die(e.message));
240
+ if (!notifications.length)
241
+ return console.error(
242
+ f.all ? "nothing has happened yet" : "nothing new — your guides and handoffs are quiet",
243
+ );
244
+ for (const n of notifications) console.log(activityLine(n));
245
+ const waiting = notifications.filter((n) => n.kind === "handoff" && !n.read);
246
+ if (waiting.length) console.error(`\nPull one with: passalong pull ${waiting[0].guide}`);
247
+ // Reading the feed is what marks it read: an unread count you have to clear by hand is a
248
+ // chore, and `--all` is there for when you want to look without clearing.
249
+ if (!f.all && unread) await passalong.seen().catch(() => {});
250
+ },
251
+
252
+ async list(f) {
253
+ const scope = typeof f.team === "string" ? f.team : f.mine ? "mine" : "";
254
+ const rows = await passalong.list(f._.join(" "), { scope });
255
+ if (rows.warning) console.error(`warning: ${rows.warning}`);
256
+ if (!rows.length)
257
+ return console.error(
258
+ "no guides yet — run `passalong share` after your next finished piece of work",
259
+ );
260
+ for (const r of rows) console.log(guideLine(r));
110
261
  },
111
262
 
112
263
  async open(f) {
@@ -122,7 +273,8 @@ const commands = {
122
273
  },
123
274
 
124
275
  async hub() {
125
- if (!api.loggedIn()) die("not logged in — run `passalong login` first (the hub shows synced guides)");
276
+ if (!api.loggedIn())
277
+ die("not logged in — run `passalong login` first (the hub shows synced guides)");
126
278
  // The token rides in the URL fragment: the browser never sends it to the server, and the
127
279
  // page moves it into localStorage and scrubs the address bar on load.
128
280
  const url = `${api.baseUrl()}/hub#token=${encodeURIComponent(api.token())}`;
@@ -132,16 +284,64 @@ const commands = {
132
284
  } else console.log(url);
133
285
  },
134
286
 
287
+ /**
288
+ * The first word back, before any of the work.
289
+ *
290
+ * Between a handoff and a pull the sender had no signal at all: an unanswered guide looks
291
+ * exactly like one nobody has noticed. These two are the hop the MCP tool and the hub already
292
+ * had and a terminal did not.
293
+ */
294
+ async take(f) {
295
+ const id = f._[0] || die("usage: passalong take <id>");
296
+ const r = await passalong.ack(id, true).catch((e) => die(e.message));
297
+ console.error(`${r.id} — you are on it, and the author has been told`);
298
+ },
299
+
300
+ async pass(f) {
301
+ const id = f._[0] || die("usage: passalong pass <id> <why it is not yours>");
302
+ const why = f._.slice(1).join(" ");
303
+ // Required, and the reason is the whole point: "not me" without "why" leaves the sender where
304
+ // the silence did, still guessing whether to re-address it, wait, or go and ask.
305
+ if (!why) die("say why you are passing: passalong pass <id> <why it is not yours>");
306
+ const r = await passalong.ack(id, false, why).catch((e) => die(e.message));
307
+ console.error(`${r.id} handed back — the author has been told: "${r.note}"`);
308
+ },
309
+
310
+ async works(f) {
311
+ const id = f._[0] || die("usage: passalong works <id>");
312
+ const r = await passalong.verdict(id, true, f._.slice(1).join(" ")).catch((e) => die(e.message));
313
+ console.error(`${r.id} verified — the author has been told`);
314
+ },
315
+
316
+ async broken(f) {
317
+ const id = f._[0] || die("usage: passalong broken <id> <what happened>");
318
+ const why = f._.slice(1).join(" ");
319
+ if (!why) die("say what went wrong: passalong broken <id> <what happened>");
320
+ const r = await passalong.verdict(id, false, why).catch((e) => die(e.message));
321
+ console.error(`${r.id} reported as not working — the author has been told: "${r.note}"`);
322
+ },
323
+
324
+ /**
325
+ * Archive a guide: off your board, and out of what the free tier counts.
326
+ *
327
+ * It was deprecated for meaning "I implemented it", which is the reader's judgement and belongs
328
+ * to the verdict. As shelf space it has a job nothing else does — it is the only way under the
329
+ * sync limit that does not delete the work, and the 402 says so by name.
330
+ */
135
331
  async done(f) {
136
332
  const id = f._[0] || die("usage: passalong done <id>");
137
333
  const g = await passalong.setStatus(id, "consumed").catch((e) => die(e.message));
138
- console.error(`${g.meta.id} marked consumed`);
334
+ console.error(`${g.meta.id} archived — off your board, and it no longer counts against sync`);
139
335
  },
140
336
 
337
+ // Still on the way out: it sets an author lifecycle nothing reads, and the board reports pull
338
+ // counts on its own. It stays, and warns, because removing a command from a published package
339
+ // is a breaking change.
141
340
  async promote(f) {
142
341
  const id = f._[0] || die("usage: passalong promote <id>");
143
342
  const g = await passalong.setStatus(id, "promoted").catch((e) => die(e.message));
144
343
  console.error(`${g.meta.id} marked promoted`);
344
+ console.error("note: `promote` is deprecated — the board reports pull counts on their own");
145
345
  },
146
346
 
147
347
  async rm(f) {
@@ -177,7 +377,81 @@ const commands = {
177
377
  console.error(`account ${account} created on ${api.baseUrl()}. Sync is on.`);
178
378
  console.error("Attach other machines with:");
179
379
  console.error(` passalong login ${token}`);
180
- console.error("(the token is the account — keep it private)");
380
+ console.error("(keep it private — it is a credential, not a password)");
381
+ console.error("Give teammates a name to address you by: passalong me --handle <you>");
382
+ console.error(`Add an email and password so you can sign in and recover it: ${api.baseUrl()}/hub`);
383
+ },
384
+
385
+ async me(f) {
386
+ const patch = {};
387
+ for (const k of ["handle", "name", "email"]) if (typeof f[k] === "string") patch[k] = f[k];
388
+ const who = Object.keys(patch).length
389
+ ? await api.updateMe(patch).then(api.me).catch((e) => die(e.message))
390
+ : await api.me().catch((e) => die(e.message));
391
+ console.log(`account ${who.account}`);
392
+ console.log(`handle ${who.handle ? `@${who.handle}` : "(none — set one: passalong me --handle <you>)"}`);
393
+ if (who.name) console.log(`name ${who.name}`);
394
+ if (who.email) console.log(`email ${who.email}`);
395
+ console.log(`guides ${who.guides} synced (${who.limit} active on the free tier)`);
396
+ const current = store.readConfig().team;
397
+ console.log(
398
+ `teams ${who.teams.length ? who.teams.map((t) => `${t.slug}${t.slug === current ? "*" : ""} (${t.role})`).join(", ") : "(none — passalong team create <name>)"}`,
399
+ );
400
+ },
401
+
402
+ async team(f) {
403
+ const [sub, ...rest] = f._;
404
+ const cfg = store.readConfig();
405
+ if (!sub) {
406
+ if (!cfg.team) {
407
+ const { teams } = await api.teams().catch((e) => die(e.message));
408
+ if (!teams.length) return console.error("no team yet — passalong team create <name>");
409
+ return console.error(`you are in ${teams.map((t) => t.slug).join(", ")}; pick one: passalong team use <slug>`);
410
+ }
411
+ const t = await api.team(cfg.team).catch((e) => die(e.message));
412
+ console.log(`${t.name} (${t.slug}) — ${t.members.length} member${t.members.length === 1 ? "" : "s"}, ${t.guides} guide${t.guides === 1 ? "" : "s"}`);
413
+ for (const m of t.members)
414
+ console.log(` ${m.handle ? `@${m.handle}` : "(no handle)"}${m.name ? ` ${m.name}` : ""} ${m.role} joined ${relDate(m.joined)}`);
415
+ return;
416
+ }
417
+ if (sub === "create") {
418
+ const name = rest.join(" ") || die("usage: passalong team create <name>");
419
+ const t = await api.createTeam(name).catch((e) => die(e.message));
420
+ store.writeConfig({ team: t.slug });
421
+ console.error(`team ${t.name} created as "${t.slug}" — now current. Invite people: passalong team invite [email]`);
422
+ return;
423
+ }
424
+ if (sub === "invite") {
425
+ const slug = cfg.team || die("no current team — passalong team use <slug>");
426
+ const inv = await api.invite(slug, rest[0] || "").catch((e) => die(e.message));
427
+ console.log(inv.url);
428
+ console.error(
429
+ inv.email
430
+ ? inv.mailed
431
+ ? `emailed ${inv.email}`
432
+ : `not emailed (mail is not configured on the server) — send ${inv.email} the link above`
433
+ : "send this link; they run: passalong team join <link>",
434
+ );
435
+ return;
436
+ }
437
+ if (sub === "join") {
438
+ const ref = rest[0] || die("usage: passalong team join <link|code>");
439
+ const code = ref.includes("/") ? ref.split("/").filter(Boolean).pop() : ref;
440
+ const r = await api.join(code).catch((e) => die(e.message));
441
+ store.writeConfig({ team: r.slug });
442
+ console.error(`${r.joined ? "joined" : "already in"} ${r.name} (${r.slug}) — now current`);
443
+ const who = await api.me().catch(() => null);
444
+ if (who && !who.handle) console.error("teammates address you by handle: passalong me --handle <you>");
445
+ return;
446
+ }
447
+ if (sub === "use") {
448
+ const slug = rest[0] || die("usage: passalong team use <slug>");
449
+ await api.team(slug).catch((e) => die(e.message));
450
+ store.writeConfig({ team: slug });
451
+ console.error(`current team: ${slug}`);
452
+ return;
453
+ }
454
+ die("usage: passalong team [create <name> | invite [email] | join <link> | use <slug>]");
181
455
  },
182
456
 
183
457
  async setup() {
@@ -186,15 +460,21 @@ const commands = {
186
460
  cpSync(join(PKG, "skill"), skillDir, { recursive: true });
187
461
  console.error(`installed Claude Code skill → ${skillDir}`);
188
462
  try {
189
- execFileSync("claude", ["mcp", "add", "--scope", "user", "passalong", "--", "passalong", "mcp"], { stdio: "pipe" });
463
+ execFileSync("claude", ["mcp", "add", "--scope", "user", "passalong", "--", "passalong", "mcp"], {
464
+ stdio: "pipe",
465
+ });
190
466
  console.error("registered MCP server: claude mcp add passalong -- passalong mcp");
191
467
  } catch (err) {
192
468
  const msg = String(err.stderr || err.message);
193
469
  if (/already exists/i.test(msg)) console.error("MCP server already registered");
194
- else console.error(`could not register MCP server automatically (${msg.trim()}).\n run: claude mcp add passalong -- passalong mcp`);
470
+ else
471
+ console.error(
472
+ `could not register MCP server automatically (${msg.trim()}).\n run: claude mcp add passalong -- passalong mcp`,
473
+ );
195
474
  }
196
- if (!api.loggedIn()) console.error("sync is off; run `passalong login` when you want guides to follow you across machines");
197
- console.error("done. In Claude Code, say \"pass this along\" at the end of a task, or run /passalong-capture.");
475
+ if (!api.loggedIn())
476
+ console.error("sync is off; run `passalong login` when you want guides to follow you across machines");
477
+ console.error('done. In Claude Code, say "pass this along" at the end of a task, or run /passalong-capture.');
198
478
  },
199
479
 
200
480
  async mcp() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "passalong",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Hand a finished implementation from one context to another, in a form an AI agent can act on.",
5
5
  "keywords": [
6
6
  "mcp",
@@ -11,7 +11,7 @@
11
11
  "cli",
12
12
  "markdown"
13
13
  ],
14
- "homepage": "https://passalong.kreativekorna.com",
14
+ "homepage": "https://passalong.dev",
15
15
  "repository": {
16
16
  "type": "git",
17
17
  "url": "https://github.com/codellyson/passalong.git",
package/skill/SKILL.md CHANGED
@@ -11,27 +11,58 @@ session's history. Write for execution, not for permanence.
11
11
 
12
12
  ## Procedure
13
13
 
14
- 1. **Reconstruct the story from the session.** What was the problem, what was tried, what failed,
14
+ 1. **Pick the one thing to transfer.** If the user named it (`/passalong-capture <what>` or
15
+ "pass along the webhook fix"), that is the scope. Otherwise take the most recent completed
16
+ piece of work. A long session usually holds several; do not merge them, and if it is
17
+ genuinely unclear which one the user means, ask before writing.
18
+ 2. **Reconstruct the story from the session.** What was the problem, what was tried, what failed,
15
19
  what was decided and why, what finally worked, and how it was verified. Read the diff
16
20
  (`git diff`, `git log`) only to confirm details; the reasoning is in the transcript.
17
- 2. **Write the guide** with the structure below. Be concrete: file paths, commands, exact error
21
+ 3. **Write the guide** with the structure below. Be concrete: file paths, commands, exact error
18
22
  strings, version numbers. Prefer a short guide with every section filled over a long one.
19
- 3. **Mark context-specific parts** so the receiver can adapt rather than copy. Put
23
+ 4. **Mark context-specific parts** so the receiver can adapt rather than copy. Put
20
24
  `ASSUMES: <thing>. If <alternative>, <what changes>.` at the start of any step that depends on
21
25
  this repo's stack, layout, or conventions.
22
- 4. **Save the draft** to `~/.passalong/drafts/<slug>.md` (create the directory if needed), then run:
26
+ 5. **Save the draft** to `~/.passalong/drafts/<slug>.md` (create the directory if needed), then run:
23
27
 
24
28
  ```bash
25
29
  passalong share ~/.passalong/drafts/<slug>.md --no-edit
26
30
  ```
27
31
 
28
- It prints the guide id on stdout and the share link on stderr.
29
- 5. **Report** the id (and link if synced) and one line on how to use it on the other side:
30
- `passalong pull <id>` in the target repo, or "pull passalong <id>" to an agent with the Passalong MCP server.
32
+ If the user named a person or team ("share this with Ada", "hand this to the platform
33
+ team"), add `--to <team>` or `--to <team>/<handle>`; `passalong team` lists the current
34
+ team's members and their handles. It prints the guide id on stdout and the share link (and
35
+ who it was handed to) on stderr.
36
+ 6. **Report** the id (and link if synced) and one line on how to use it on the other side:
37
+ `passalong start <id>` in the target repo, or "start passalong <id>" to an agent with the
38
+ Passalong MCP server. `start` rather than `pull` on purpose: it takes the handoff as well as
39
+ fetching it, and until somebody says they are on it the sender cannot tell a guide nobody
40
+ noticed from one somebody is deep in.
31
41
 
32
42
  If the `passalong` command is missing, tell the user to run `npm i -g passalong && passalong setup` and
33
43
  still write the draft file so nothing is lost.
34
44
 
45
+ ## Two kinds of guide
46
+
47
+ `kind:` in the frontmatter says what a guide is for, and the receiver behaves differently for
48
+ each. Absent means `transfer`, which is what every guide written before bug reports existed is.
49
+
50
+ | kind | what it is | what the receiver does |
51
+ | --- | --- | --- |
52
+ | `transfer` (default) | finished work to repeat somewhere else | follows the **Steps** |
53
+ | `bug` | a defect to fix where it is | fixes it — **Reproduce** shows the problem, it is not a procedure to apply |
54
+
55
+ To file bugs you found but are not fixing, call `file_bugs` with all of them in one call — it
56
+ opens a report and publishes each issue as its own guide, so any one of them can be handed to
57
+ whoever fixes it. `publish_guide` is for a single guide: work you finished and want repeated, or
58
+ one bug on its own.
59
+
60
+ A bug has no `## Steps` and publishing one with a Steps section is refused. That is the whole
61
+ reason the kinds are separate: "follow its Steps" is what the MCP server tells every agent that
62
+ pulls a guide, and steps that reproduce a defect are the one list that must never be run as a
63
+ remedy — an agent that follows them reproduces the bug, checks the Verification, finds it false
64
+ because the bug is real, and reports that the guide does not work.
65
+
35
66
  ## Guide structure
36
67
 
37
68
  ```markdown
@@ -64,12 +95,46 @@ Commands to run and what they should print. Test cases. What "done" looks like.
64
95
  - What failed along the way, the exact error, and why. What looked right but wasn't.
65
96
  ```
66
97
 
98
+ ## Bug structure
99
+
100
+ ```markdown
101
+ ---
102
+ title: <what is broken, in one line: "Undo leaves section drag handles dead">
103
+ kind: bug
104
+ severity: <s1 blocker | s2 major | s3 minor | s4 cosmetic>
105
+ area: <which surface it is on>
106
+ report: <the report id, when it was filed with others>
107
+ status: published
108
+ tags: [bug, <2 to 4 more>]
109
+ ---
110
+
111
+ > **Bug report.** The steps under Reproduce show the problem — they are not a fix to apply. Fix what Problem describes, then check Verification.
112
+
113
+ ## Problem
114
+ What is broken and what it stops someone doing. The observable symptom, and the error if there is one.
115
+
116
+ ## Reproduce
117
+ 1. The steps that show the bug. These produce the defect — they are not a fix to apply.
118
+
119
+ ## Verification
120
+ The behaviour that should have happened, as something the fixer can check.
121
+
122
+ ## Gotchas
123
+ - Anything already ruled out, or that made it hard to pin down.
124
+ ```
125
+
67
126
  ## Quality bar
68
127
 
69
128
  - **Gotchas is the highest-value section.** If nothing went wrong, say what would have gone
70
129
  wrong without a specific decision. Never leave it empty.
71
130
  - **Verification must be runnable.** A command and an expected output, not "make sure it works".
72
131
  - **Every ASSUMES has an alternative.** The point is adaptation on the other side.
132
+ - **One problem per guide.** A lesson from the same session that is not part of this fix goes in
133
+ its own guide (write and share it separately), not in this one's Gotchas.
73
134
  - **No session narration.** "First I looked at..." is noise. State the finding.
74
135
  - **No secrets.** Redact tokens, keys, and internal hostnames that would not apply elsewhere.
75
136
  - Title, Problem, and Steps are required; the guide will not publish without them.
137
+ - For a bug: title, Problem and **Reproduce**. Never a Steps section — see the two kinds above.
138
+ - The line above Problem is written by `file_bugs` and the hub's form. It is in the document, not
139
+ added by whatever served it, so an agent that fetched the share link over plain HTTP — with no
140
+ MCP server and no knowledge of Passalong — still reads what the document is before acting on it.