passalong 0.21.0 → 1.0.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 +62 -72
- package/bin/passalong +217 -378
- package/package.json +4 -4
- package/skill/SKILL.md +55 -167
- package/src/api.js +81 -185
- package/src/capture.js +15 -51
- package/src/checks.js +5 -5
- package/src/hooks.js +9 -18
- package/src/mcp.js +691 -1484
- package/src/passalong.js +310 -691
- package/src/store.js +18 -28
- package/src/work.js +416 -0
- package/src/guide.js +0 -811
package/bin/passalong
CHANGED
|
@@ -1,32 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Passalong CLI. Few flags on purpose: the defaults are the product.
|
|
3
3
|
//
|
|
4
|
-
// passalong
|
|
5
|
-
//
|
|
6
|
-
// handed to whoever pulls or starts that guide
|
|
4
|
+
// passalong new "<what needs doing>" [--to TARGET] [--parent ID] [--draft] write work down
|
|
5
|
+
// passalong new <file.md> [--to TARGET] [--parent ID] [--draft] the same, from a file
|
|
7
6
|
// TARGET is a team, team/@handle for one person, or "team/#group" for the people who do a
|
|
8
7
|
// thing — quote that one, since a shell may read the # as a comment
|
|
9
|
-
// passalong
|
|
10
|
-
// passalong
|
|
11
|
-
// passalong
|
|
12
|
-
// passalong
|
|
8
|
+
// passalong board what needs you, who is on what, and what is ready
|
|
9
|
+
// passalong list [query] work you and your teams can see, newest first
|
|
10
|
+
// passalong open <id> [--print] view work in the browser (or the terminal)
|
|
11
|
+
// passalong take [id] [--save] you are doing it: nobody else can take it while you hold it
|
|
12
|
+
// passalong progress <id> <note> still on it, with a one-line note the hub shows
|
|
13
|
+
// passalong hand-in <id> <note> --evidence TEXT [--writeup TEXT] [--pr LINK] it is done
|
|
14
|
+
// passalong pass <id> <why> it is not yours, or you are stuck — hands it back with your reason
|
|
15
|
+
// passalong ask <id> <question> ask the person, keep it, and wait for their answer
|
|
16
|
+
// passalong reply <id> <text> write to whoever holds it (--file PATH to attach); or leave a note
|
|
17
|
+
// passalong assign <id> <to> give it to @handle (or several: @ada @bola), a #group, or "team"
|
|
18
|
+
// passalong release <id> take work back from whoever holds it
|
|
13
19
|
// passalong reopen <id> <why> finished work isn't right: back to To do, and whoever takes it next reads why
|
|
14
|
-
// passalong
|
|
20
|
+
// passalong ready <id...|--all> work you have read is Ready — whoever it is for may take it now
|
|
21
|
+
// passalong archive <id> off the board and out of the free tier's count
|
|
22
|
+
// passalong rm <id> delete it
|
|
15
23
|
// passalong attach <file> upload a picture or a file and print the markdown that points at it
|
|
16
|
-
// passalong
|
|
17
|
-
// passalong
|
|
18
|
-
// passalong
|
|
19
|
-
// passalong
|
|
20
|
-
// passalong board waiting on you, in flight, landed, worth keeping
|
|
21
|
-
// passalong activity [--all] what happened while you were away; clears unless --all
|
|
22
|
-
// passalong log [repo] what you did, newest first, by month
|
|
23
|
-
// passalong list [query] your guides and your teams'
|
|
24
|
-
// passalong open <id> [--print] view a guide in the browser (or the terminal)
|
|
25
|
-
// passalong hub open your synced guides in the browser
|
|
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
|
|
28
|
-
// passalong assign <id> <to> give it to @handle (or several: @ada @bola), a #group, or "team" — yours, or assigned to you
|
|
29
|
-
// passalong recall <id> take back something you sent: private again, gone from their inboxes
|
|
24
|
+
// passalong work [--once] [--agent CMD] run an agent on each piece of ready work here, one after another
|
|
25
|
+
// passalong work --watch [--every SECS] the runner: keep taking work marked safe to run alone
|
|
26
|
+
// passalong hub open the hub in the browser
|
|
27
|
+
// passalong export [dir] dump every piece of work as plain markdown (default ./passalong-export)
|
|
30
28
|
// passalong admin create <email> make an account that runs Passalong (owner only)
|
|
31
29
|
// passalong admin list who runs it
|
|
32
30
|
// passalong admin grant|revoke <@handle> make an existing account an admin, or stop it being one
|
|
@@ -34,9 +32,6 @@
|
|
|
34
32
|
// give Solo to @handle, or Team to team/slug, until a date (operator only)
|
|
35
33
|
// passalong gifts what has been given away, and what is still running
|
|
36
34
|
// passalong ungift <gift id> take one back now
|
|
37
|
-
// passalong archive <id> off your board and out of the free tier's count (was `done`)
|
|
38
|
-
// passalong rm <id> delete a guide locally and from sync
|
|
39
|
-
// passalong export [dir] dump every guide as plain markdown (default ./passalong-export)
|
|
40
35
|
// passalong login sign in from the browser
|
|
41
36
|
// passalong login --with-token read a token from stdin instead (CI, scripts)
|
|
42
37
|
// passalong login --new add another account to this machine
|
|
@@ -62,8 +57,8 @@ import { homedir, hostname } from "node:os";
|
|
|
62
57
|
import { dirname, join, resolve } from "node:path";
|
|
63
58
|
import { fileURLToPath } from "node:url";
|
|
64
59
|
import * as api from "../src/api.js";
|
|
65
|
-
import {
|
|
66
|
-
import { parse, serialize } from "../src/
|
|
60
|
+
import { workScaffold } from "../src/capture.js";
|
|
61
|
+
import { parse, serialize } from "../src/work.js";
|
|
67
62
|
import * as passalong from "../src/passalong.js";
|
|
68
63
|
import * as store from "../src/store.js";
|
|
69
64
|
|
|
@@ -202,41 +197,12 @@ function relDate(iso) {
|
|
|
202
197
|
return iso.slice(0, 10);
|
|
203
198
|
}
|
|
204
199
|
|
|
205
|
-
function
|
|
206
|
-
const who = r.team ? ` ${r.team}${r.to ?
|
|
200
|
+
function workLine(r) {
|
|
201
|
+
const who = r.team ? ` ${r.team}${r.to ? `/${r.to}` : ""}${r.mine ? "" : ` from @${r.from || "?"}`}` : "";
|
|
207
202
|
const tags = r.tags?.length ? ` #${r.tags.join(" #")}` : "";
|
|
208
|
-
|
|
209
|
-
if (r.mine && r.pulled_by?.length) {
|
|
210
|
-
const p = r.pulled_by[0];
|
|
211
|
-
pulled = ` ← ${p.handle ? `@${p.handle}` : "link"} ${relDate(p.at)}${r.pulled_by.length > 1 ? ` +${r.pulled_by.length - 1}` : ""}`;
|
|
212
|
-
}
|
|
213
|
-
return `${r.id} ${r.status.padEnd(9)} ${relDate(r.created).padEnd(10)} ${r.title}${who}${tags}${pulled}`;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function activityLine(n) {
|
|
217
|
-
const dot = n.read ? " " : "•";
|
|
218
|
-
return `${dot} ${(n.guide || "").padEnd(8)} ${relDate(n.at).padEnd(10)} ${n.text}`;
|
|
203
|
+
return `${r.id} ${r.status.padEnd(9)} ${relDate(r.created).padEnd(10)} ${r.title}${who}${tags}`;
|
|
219
204
|
}
|
|
220
205
|
|
|
221
|
-
/**
|
|
222
|
-
* After a pulled guide, the follow-ups written for it: more context, each its own guide. Listed on
|
|
223
|
-
* stderr rather than printed in full, so stdout stays exactly the document; nothing at all when
|
|
224
|
-
* there are none or sync is off.
|
|
225
|
-
*/
|
|
226
|
-
async function listFollowUps(meta) {
|
|
227
|
-
const guides = await passalong.followUpGuides(meta);
|
|
228
|
-
if (!guides.length) return;
|
|
229
|
-
console.error(
|
|
230
|
-
`\n${guides.length} follow-up${guides.length === 1 ? "" : "s"} — more context for this guide:`,
|
|
231
|
-
);
|
|
232
|
-
for (const g of guides) console.error(` ${g.id} ${g.title || "untitled"}`);
|
|
233
|
-
console.error("Read one with: passalong pull <id>");
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const MONTHS = "January February March April May June July August September October November December".split(" ");
|
|
237
|
-
/** "September 2026" from an ISO timestamp, without pulling in a date library for one heading. */
|
|
238
|
-
const monthOf = (iso) => `${MONTHS[Number(iso.slice(5, 7)) - 1]} ${iso.slice(0, 4)}`;
|
|
239
|
-
|
|
240
206
|
// ---- the status line cache --------------------------------------------------------------------------
|
|
241
207
|
|
|
242
208
|
/** One small file per working directory, so two sessions in two repos each see their own. */
|
|
@@ -285,111 +251,149 @@ async function statusLine() {
|
|
|
285
251
|
}
|
|
286
252
|
|
|
287
253
|
const commands = {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
254
|
+
/**
|
|
255
|
+
* Write work down: a sentence becomes its title, summary and text, or a file is sent as it is. At
|
|
256
|
+
* a terminal it opens in $EDITOR first, so whoever wrote it can add to it before it goes.
|
|
257
|
+
*/
|
|
258
|
+
async new(f) {
|
|
259
|
+
const arg =
|
|
260
|
+
f._.join(" ").trim() ||
|
|
261
|
+
die('usage: passalong new "<what needs doing>" | <file.md> [--to TARGET] [--parent ID] [--draft]');
|
|
262
|
+
let path = arg;
|
|
263
|
+
if (!(arg.endsWith(".md") && existsSync(arg))) {
|
|
264
|
+
path = store.draftPath(`work-${new Date().toISOString().replace(/[:.]/g, "-")}`);
|
|
265
|
+
writeFileSync(path, workScaffold(arg));
|
|
301
266
|
}
|
|
302
|
-
if (!existsSync(path)) die(`no such file: ${path}`);
|
|
303
267
|
if (tty && !f["no-edit"]) edit(path);
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
const {
|
|
307
|
-
.
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
268
|
+
const g = parse(readFileSync(path, "utf8"));
|
|
269
|
+
if (f.draft) g.meta.status = "draft";
|
|
270
|
+
const { work, url } = await passalong
|
|
271
|
+
.publish(serialize(g), {
|
|
272
|
+
to: typeof f.to === "string" ? f.to : undefined,
|
|
273
|
+
parent: typeof f.parent === "string" ? f.parent : undefined,
|
|
274
|
+
})
|
|
275
|
+
.catch((e) => die(`${e.message}\nkept at ${path} — fix it and run \`passalong new ${path}\``));
|
|
276
|
+
// A draft that went out is spent. removeDraft() touches nothing outside the drafts directory.
|
|
312
277
|
store.removeDraft(resolve(path));
|
|
313
|
-
console.log(
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
if (guide.meta.team)
|
|
317
|
-
console.error(
|
|
318
|
-
`handed to ${guide.meta.team}${guide.meta.to ? `/@${guide.meta.to}${notified ? " (emailed)" : " (in their inbox)"}` : " (team inbox)"}`,
|
|
319
|
-
);
|
|
278
|
+
console.log(work.meta.id);
|
|
279
|
+
const where = work.meta.team ? ` for ${work.meta.team}${work.meta.to ? `/${work.meta.to}` : ""}` : "";
|
|
280
|
+
console.error(`"${work.meta.title}" is ${work.meta.status === "draft" ? "in Draft" : "Ready"}${where}`);
|
|
320
281
|
console.error(`link: ${url}`);
|
|
321
282
|
},
|
|
322
283
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const id = guide.meta.id;
|
|
337
|
-
console.log(id);
|
|
338
|
-
const target = guide.meta.target_context;
|
|
339
|
-
console.error(`task "${guide.meta.title}" is Ready${target ? ` for ${target}` : " (no repo)"}`);
|
|
340
|
-
if (guide.meta.team) console.error(`handed to ${guide.meta.team} (team inbox)`);
|
|
284
|
+
async board() {
|
|
285
|
+
const b = await passalong.board().catch((e) => die(e.message));
|
|
286
|
+
const section = (title, rows, line) => {
|
|
287
|
+
if (!rows.length) return;
|
|
288
|
+
console.log(`\n${title} · ${rows.length}`);
|
|
289
|
+
for (const r of rows) console.log(` ${r.id} ${line(r)}`);
|
|
290
|
+
};
|
|
291
|
+
section("Needs you", b.needs, (n) => `${n.title} — ${n.why}`);
|
|
292
|
+
section("Being worked on", b.working, (w) =>
|
|
293
|
+
`${w.title} ${w.who}${w.stalled ? " (went quiet)" : ""}${w.note ? ` "${w.note}"` : ""}`);
|
|
294
|
+
section("Ready", b.ready, (t) => `${t.title}${t.target ? ` [${t.target}]` : ""}${t.to ? ` for ${t.to}` : ""}`);
|
|
295
|
+
if (!b.needs.length && !b.working.length && !b.ready.length) console.error("nothing open");
|
|
296
|
+
console.error(`\n${b.counts.done} done. The board: ${b.hub}/work`);
|
|
341
297
|
},
|
|
342
298
|
|
|
343
|
-
async
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
299
|
+
async list(f) {
|
|
300
|
+
const scope = typeof f.team === "string" ? f.team : f.mine ? "mine" : "";
|
|
301
|
+
const rows = await passalong.list(f._.join(" "), { scope }).catch((e) => die(e.message));
|
|
302
|
+
if (!rows.length) return console.error('no work yet — write some with: passalong new "<what needs doing>"');
|
|
303
|
+
for (const r of rows) console.log(workLine(r));
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
async open(f) {
|
|
307
|
+
const id = f._[0] || die("usage: passalong open <id>");
|
|
308
|
+
const { work } = await passalong.resolve(id).catch((e) => die(e.message));
|
|
309
|
+
if (work.meta.url && tty && !f.print) {
|
|
310
|
+
openInBrowser(work.meta.url);
|
|
311
|
+
console.error(work.meta.url);
|
|
312
|
+
} else {
|
|
313
|
+
process.stdout.write(serialize(work));
|
|
314
|
+
if (work.meta.url) console.error(`\nlink: ${work.meta.url}`);
|
|
354
315
|
}
|
|
355
|
-
if (rows.some((t) => t.state === "review"))
|
|
356
|
-
console.error("\nread a write-up with: passalong open <id> — then approve or reject the task");
|
|
357
316
|
},
|
|
358
317
|
|
|
359
318
|
/**
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
* files without asking — which is yours to decide, not this command's.
|
|
319
|
+
* Take it: you are doing it, and nobody else can take it while you hold it. It is printed, and
|
|
320
|
+
* `--save` also writes it to ./.passalong/. With no id, the next piece of work waiting here.
|
|
363
321
|
*/
|
|
364
|
-
async
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
322
|
+
async take(f) {
|
|
323
|
+
const r = await passalong.take(f._[0], { save: Boolean(f.save) }).catch((e) => die(e.message));
|
|
324
|
+
if (!r.work?.markdown) return console.error(r.say || "nothing to take");
|
|
325
|
+
process.stdout.write(r.work.markdown);
|
|
326
|
+
if (r.path) console.error(`\nsaved to ${r.path}`);
|
|
327
|
+
console.error(`\nyou hold ${r.work.id} — hand it in when it is done, or pass it with the reason`);
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
async progress(f) {
|
|
331
|
+
const id = f._[0] || die("usage: passalong progress <id> <note>");
|
|
332
|
+
await passalong.progress(id, f._.slice(1).join(" ")).catch((e) => die(e.message));
|
|
333
|
+
console.error(`${id} — still yours`);
|
|
334
|
+
},
|
|
335
|
+
|
|
336
|
+
async ["hand-in"](f) {
|
|
337
|
+
const id = f._[0] || die("usage: passalong hand-in <id> <note> --evidence TEXT");
|
|
338
|
+
const note = f._.slice(1).join(" ") || die("say in one sentence what you did: passalong hand-in <id> <note>");
|
|
339
|
+
const r = await passalong
|
|
340
|
+
.handIn(id, {
|
|
341
|
+
note,
|
|
342
|
+
evidence: typeof f.evidence === "string" ? f.evidence : "",
|
|
343
|
+
writeup: typeof f.writeup === "string" ? f.writeup : "",
|
|
344
|
+
pr: typeof f.pr === "string" ? f.pr : "",
|
|
371
345
|
})
|
|
372
346
|
.catch((e) => die(e.message));
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
347
|
+
console.error(`${id} is ${r.state === "done" ? "Done" : "handed in here; others still hold it"}`);
|
|
348
|
+
},
|
|
349
|
+
|
|
350
|
+
async pass(f) {
|
|
351
|
+
const id = f._[0] || die("usage: passalong pass <id> <why it is not yours>");
|
|
352
|
+
const why = f._.slice(1).join(" ");
|
|
353
|
+
// Required, and the reason is the whole point: "not me" without "why" leaves the sender where
|
|
354
|
+
// the silence did, still guessing whether to re-address it, wait, or go and ask.
|
|
355
|
+
if (!why) die("say why you are passing: passalong pass <id> <why it is not yours>");
|
|
356
|
+
await passalong.pass(id, why).catch((e) => die(e.message));
|
|
357
|
+
console.error(`${id} handed back with your reason: "${why}"`);
|
|
383
358
|
},
|
|
384
359
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
360
|
+
async ask(f) {
|
|
361
|
+
const id = f._[0] || die("usage: passalong ask <id> <question>");
|
|
362
|
+
await passalong.ask(id, f._.slice(1).join(" ")).catch((e) => die(e.message));
|
|
363
|
+
console.error(`${id} — asked. It is still yours; take it again once they have replied`);
|
|
364
|
+
},
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Write to whoever holds a piece of work, or leave a note on one nobody holds yet. The person's
|
|
368
|
+
* side of `ask`: an agent that stopped to wait hears this when it takes the work again.
|
|
369
|
+
*/
|
|
370
|
+
async reply(f) {
|
|
371
|
+
const id = f._[0] || die("usage: passalong reply <id> <message> [--file PATH ...]");
|
|
372
|
+
const files = [f.file].flat().filter((x) => typeof x === "string");
|
|
373
|
+
const said = f._.slice(1).join(" ");
|
|
374
|
+
if (!said && !files.length) die("say something, or attach a file: passalong reply <id> <message>");
|
|
375
|
+
const r = await passalong.reply(id, said, { files }).catch((e) => die(e.message));
|
|
388
376
|
console.error(
|
|
389
|
-
|
|
377
|
+
r?.noted
|
|
378
|
+
? `${id} — nobody holds it yet, so this is a note: whoever takes it is handed it first`
|
|
379
|
+
: `${id} — sent. Whoever holds it reads it the next time it checks in`,
|
|
390
380
|
);
|
|
391
381
|
},
|
|
392
382
|
|
|
383
|
+
async assign(f) {
|
|
384
|
+
const id = f._[0] || die('usage: passalong assign <id> <@handle | #group | team>');
|
|
385
|
+
const to = f._.slice(1).join(",") || die('say who: @handle (or several), #group, or "team" for everyone');
|
|
386
|
+
const r = await passalong.assign(id, to).catch((e) => die(e.message));
|
|
387
|
+
const back = r.taken_back ? ` — taken back from ${r.taken_back} who had it, and they were told` : "";
|
|
388
|
+
console.error(`${r.id} is now for ${r.to || "everyone in the team"}${back}`);
|
|
389
|
+
},
|
|
390
|
+
|
|
391
|
+
async release(f) {
|
|
392
|
+
const id = f._[0] || die("usage: passalong release <id>");
|
|
393
|
+
await passalong.release(id).catch((e) => die(e.message));
|
|
394
|
+
console.error(`${id} is back in Ready — whoever had it has been told`);
|
|
395
|
+
},
|
|
396
|
+
|
|
393
397
|
/** Finished work back to To do, with why at its top; whoever finished it is told. */
|
|
394
398
|
async reopen(f) {
|
|
395
399
|
const id = f._[0] || die("usage: passalong reopen <id> <why>");
|
|
@@ -399,189 +403,86 @@ const commands = {
|
|
|
399
403
|
console.error(`${id} is back in To do — whoever takes it next reads why`);
|
|
400
404
|
},
|
|
401
405
|
|
|
402
|
-
async reject(f) {
|
|
403
|
-
return commands.reopen(f);
|
|
404
|
-
},
|
|
405
|
-
|
|
406
|
-
async release(f) {
|
|
407
|
-
const id = f._[0] || die("usage: passalong release <id>");
|
|
408
|
-
const r = await passalong.release(id).catch((e) => die(e.message));
|
|
409
|
-
const from = Number(r?.from) || 1;
|
|
410
|
-
console.error(
|
|
411
|
-
`${id} is back in Ready${from > 1 ? `, taken back from ${from} places` : ""}` +
|
|
412
|
-
" — whoever had it has been told",
|
|
413
|
-
);
|
|
414
|
-
},
|
|
415
|
-
|
|
416
406
|
async ready(f) {
|
|
417
407
|
if (f.all) {
|
|
418
408
|
const ids = await passalong.readyDrafts().catch((e) => die(e.message));
|
|
419
409
|
if (!ids.length) return console.error("no drafts of yours to make ready");
|
|
420
410
|
for (const id of ids) console.log(id);
|
|
421
|
-
return console.error(`${ids.length}
|
|
411
|
+
return console.error(`${ids.length} Ready — whoever they are for may take them`);
|
|
422
412
|
}
|
|
423
413
|
if (!f._.length) die("usage: passalong ready <id...> | --all");
|
|
424
|
-
for (const
|
|
425
|
-
const
|
|
426
|
-
console.error(`${
|
|
414
|
+
for (const ref of f._) {
|
|
415
|
+
const id = await passalong.ready(ref).catch((e) => die(e.message));
|
|
416
|
+
console.error(`${id} is Ready — whoever it is for may take it`);
|
|
427
417
|
}
|
|
428
418
|
},
|
|
429
419
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
process.stdout.write(markdown);
|
|
436
|
-
if (path) console.error(`\nsaved to ${path}`);
|
|
437
|
-
await listFollowUps(guide.meta);
|
|
420
|
+
/** Archive: off the board, and out of what the free tier counts. It comes back to Draft. */
|
|
421
|
+
async archive(f) {
|
|
422
|
+
const id = f._[0] || die("usage: passalong archive <id>");
|
|
423
|
+
await passalong.setStatus(id, "done").catch((e) => die(e.message));
|
|
424
|
+
console.error(`${id} archived — off the board, and it no longer counts against your plan`);
|
|
438
425
|
},
|
|
439
426
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
*/
|
|
445
|
-
async start(f) {
|
|
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");
|
|
449
|
-
process.stdout.write(r.markdown);
|
|
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);
|
|
427
|
+
async rm(f) {
|
|
428
|
+
const id = f._[0] || die("usage: passalong rm <id>");
|
|
429
|
+
await passalong.remove(id).catch((e) => die(e.message));
|
|
430
|
+
console.error(`${id} removed`);
|
|
453
431
|
},
|
|
454
432
|
|
|
455
|
-
|
|
456
|
-
/**
|
|
457
|
-
* Upload an image and print the markdown that points at it. Evidence goes *in* the document:
|
|
458
|
-
* a guide travels as markdown to whoever holds its link, so a file beside it does not travel.
|
|
459
|
-
*/
|
|
433
|
+
/** Upload a picture or a file and print the markdown that points at it. */
|
|
460
434
|
async attach(f) {
|
|
461
435
|
const file = f._[0] || die("usage: passalong attach <file> [--name LABEL]");
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
name: f.name || "",
|
|
466
|
-
}).catch((e) => die(e.message));
|
|
467
|
-
process.stdout.write(`${shot.markdown}\n`);
|
|
468
|
-
console.error(`uploaded ${shot.bytes} bytes → ${shot.url}`);
|
|
469
|
-
},
|
|
470
|
-
|
|
471
|
-
/**
|
|
472
|
-
* Write to whoever holds a guide, or leave a note on one nobody holds yet. The person's side of
|
|
473
|
-
* `ask`: an agent that stopped to wait hears this when it takes the guide again.
|
|
474
|
-
*/
|
|
475
|
-
async reply(f) {
|
|
476
|
-
const id = f._[0] || die("usage: passalong reply <id> <message> [--file PATH ...]");
|
|
477
|
-
const files = [f.file].flat().filter((x) => typeof x === "string");
|
|
478
|
-
const said = f._.slice(1).join(" ");
|
|
479
|
-
if (!said && !files.length) die("say something, or attach a file: passalong reply <id> <message>");
|
|
480
|
-
const r = await passalong.reply(id, said, { files }).catch((e) => die(e.message));
|
|
481
|
-
console.error(
|
|
482
|
-
r?.noted
|
|
483
|
-
? `${id} — nobody holds it yet, so this is a note: whoever takes it is handed it first`
|
|
484
|
-
: `${id} — sent. Whoever holds it reads it the next time it checks in`,
|
|
485
|
-
);
|
|
486
|
-
},
|
|
487
|
-
|
|
488
|
-
async inbox() {
|
|
489
|
-
const rows = await passalong.inbox().catch((e) => die(e.message));
|
|
490
|
-
if (!rows.length) return console.error("inbox empty — nothing handed to you that you haven't pulled");
|
|
491
|
-
for (const r of rows) console.log(guideLine(r));
|
|
492
|
-
console.error(`\n${rows.length} waiting. Take one with: passalong start <id>`);
|
|
493
|
-
},
|
|
494
|
-
|
|
495
|
-
async board() {
|
|
496
|
-
const b = await passalong.board().catch((e) => die(e.message));
|
|
497
|
-
const queue = (title, note, rows, line) => {
|
|
498
|
-
if (!rows.length) return;
|
|
499
|
-
console.log(`\n${title} · ${rows.length} ${note}`);
|
|
500
|
-
for (const g of rows) console.log(` ${g.id} ${relDate(g.created).padEnd(10)} ${line(g)}`);
|
|
501
|
-
};
|
|
502
|
-
queue("Waiting on you", "handed to you, not pulled yet", b.waiting, (g) => `${g.title} from @${g.from || "?"}`);
|
|
503
|
-
queue("Not working", "someone tried it and it does not hold up", b.failing, (g) =>
|
|
504
|
-
`${g.title} ${g.verdict?.by ? `@${g.verdict.by}` : "someone"}: ${g.verdict?.note || "no reason given"}`);
|
|
505
|
-
queue("In flight", "handed over, nobody has taken it", b.in_flight, (g) =>
|
|
506
|
-
`${g.title} to ${g.team}${g.to ? `/@${g.to}` : ""}${g.stale ? " ← not picked up" : ""}`);
|
|
507
|
-
queue("Landed", "someone else has it", b.landed, (g) => `${g.title} ${g.pulls} pull${g.pulls === 1 ? "" : "s"}`);
|
|
508
|
-
const total = b.waiting.length + b.failing.length + b.in_flight.length + b.landed.length;
|
|
509
|
-
if (!total) console.error("nothing in flight — every transfer is closed");
|
|
510
|
-
else if (b.unread) console.error(`\n${b.unread} new: passalong activity`);
|
|
436
|
+
const got = await passalong.attachAny(file, { name: f.name || "" }).catch((e) => die(e.message));
|
|
437
|
+
process.stdout.write(`${got.markdown}\n`);
|
|
438
|
+
console.error(`uploaded ${got.bytes} bytes → ${got.url}`);
|
|
511
439
|
},
|
|
512
440
|
|
|
513
441
|
/**
|
|
514
|
-
*
|
|
515
|
-
*
|
|
516
|
-
*
|
|
517
|
-
* you — the board's queues, `list`'s state rank — and none of them can answer "what did I get
|
|
518
|
-
* done in September". Time is the axis nothing else offers.
|
|
519
|
-
*
|
|
520
|
-
* A repo narrows it, positionally and without a flag, for the same reason `list` takes its query
|
|
521
|
-
* that way: it is the one thing you are likely to say, and a flag would make you name it twice.
|
|
442
|
+
* Work the queue from this worktree until it is empty. The agent is `claude -p` unless --agent or
|
|
443
|
+
* PASSALONG_AGENT says otherwise, e.g. "claude -p --permission-mode acceptEdits" so it can edit
|
|
444
|
+
* files without asking — which is yours to decide, not this command's.
|
|
522
445
|
*/
|
|
523
|
-
async
|
|
524
|
-
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
if (!f.all && unread) await passalong.seen().catch(() => {});
|
|
558
|
-
},
|
|
559
|
-
|
|
560
|
-
async list(f) {
|
|
561
|
-
const scope = typeof f.team === "string" ? f.team : f.mine ? "mine" : "";
|
|
562
|
-
const rows = await passalong.list(f._.join(" "), { scope });
|
|
563
|
-
if (!rows.length)
|
|
564
|
-
return console.error(
|
|
565
|
-
"no guides yet — run `passalong share` after your next finished piece of work",
|
|
446
|
+
async work(f) {
|
|
447
|
+
// The runner: only work a person marked safe to run alone, waiting for more when there is none.
|
|
448
|
+
const watch = Boolean(f.watch);
|
|
449
|
+
let idle = false;
|
|
450
|
+
if (watch)
|
|
451
|
+
console.error("Runner: taking only work marked safe to run alone. Stop it with Ctrl-C.");
|
|
452
|
+
const run = await passalong
|
|
453
|
+
.work({
|
|
454
|
+
agent: typeof f.agent === "string" ? f.agent : undefined,
|
|
455
|
+
once: Boolean(f.once),
|
|
456
|
+
any: Boolean(f.any),
|
|
457
|
+
watch,
|
|
458
|
+
every: Number(f.every) || 60,
|
|
459
|
+
onTask: (t) => {
|
|
460
|
+
idle = false;
|
|
461
|
+
console.error(`\n→ ${t.id} ${t.title}${t.resumed ? " (resuming)" : ""}`);
|
|
462
|
+
},
|
|
463
|
+
onIdle: () => {
|
|
464
|
+
if (!idle) console.error("waiting for work marked safe to run alone…");
|
|
465
|
+
idle = true;
|
|
466
|
+
},
|
|
467
|
+
onUnfinished: (t, state) =>
|
|
468
|
+
console.error(
|
|
469
|
+
`✗ ${t.id} is still ${state || "open"}: left held here; the sweep puts it back if it goes quiet`,
|
|
470
|
+
),
|
|
471
|
+
})
|
|
472
|
+
.catch((e) => die(e.message));
|
|
473
|
+
for (const id of run.finished) console.log(id);
|
|
474
|
+
const n = run.finished.length;
|
|
475
|
+
const done = `${n} finished`;
|
|
476
|
+
if (run.stopped === "unfinished")
|
|
477
|
+
die(
|
|
478
|
+
`${done}. Stopped: the agent exited with ${run.task} still ${run.state || "open"} — ` +
|
|
479
|
+
`run again to resume it, or \`passalong release ${run.task}\``,
|
|
566
480
|
);
|
|
567
|
-
for
|
|
568
|
-
},
|
|
569
|
-
|
|
570
|
-
async open(f) {
|
|
571
|
-
const id = f._[0] || die("usage: passalong open <id>");
|
|
572
|
-
const { guide } = await passalong.resolve(id).catch((e) => die(e.message));
|
|
573
|
-
if (guide.meta.url && tty && !f.print) {
|
|
574
|
-
openInBrowser(guide.meta.url);
|
|
575
|
-
console.error(guide.meta.url);
|
|
576
|
-
} else {
|
|
577
|
-
process.stdout.write(serialize(guide));
|
|
578
|
-
if (guide.meta.url) console.error(`\nlink: ${guide.meta.url}`);
|
|
579
|
-
}
|
|
481
|
+
console.error(`${done}${run.stopped === "empty" ? " — nothing left for this repo" : ""}`);
|
|
580
482
|
},
|
|
581
483
|
|
|
582
484
|
async hub() {
|
|
583
|
-
if (!api.loggedIn())
|
|
584
|
-
die("not logged in — run `passalong login` first (the hub shows synced guides)");
|
|
485
|
+
if (!api.loggedIn()) die("not logged in — run `passalong login` first");
|
|
585
486
|
// The token rides in the URL fragment: the browser never sends it to the server, and the
|
|
586
487
|
// page moves it into localStorage and scrubs the address bar on load.
|
|
587
488
|
const url = `${api.baseUrl()}/hub#token=${encodeURIComponent(api.token())}`;
|
|
@@ -591,35 +492,10 @@ const commands = {
|
|
|
591
492
|
} else console.log(url);
|
|
592
493
|
},
|
|
593
494
|
|
|
594
|
-
async
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
async pass(f) {
|
|
599
|
-
const id = f._[0] || die("usage: passalong pass <id> <why it is not yours>");
|
|
600
|
-
const why = f._.slice(1).join(" ");
|
|
601
|
-
// Required, and the reason is the whole point: "not me" without "why" leaves the sender where
|
|
602
|
-
// the silence did, still guessing whether to re-address it, wait, or go and ask.
|
|
603
|
-
if (!why) die("say why you are passing: passalong pass <id> <why it is not yours>");
|
|
604
|
-
await passalong.pass(id, why).catch((e) => die(e.message));
|
|
605
|
-
console.error(`${id} handed back with your reason: "${why}"`);
|
|
606
|
-
},
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
async assign(f) {
|
|
610
|
-
const id = f._[0] || die('usage: passalong assign <id> <@handle | #group | team>');
|
|
611
|
-
const to = f._.slice(1).join(",") || die('say who: @handle (or several), #group, or "team" for everyone');
|
|
612
|
-
const r = await passalong.assign(id, to).catch((e) => die(e.message));
|
|
613
|
-
const back = r.taken_back ? ` — taken back from ${r.taken_back} who had it, and they were told` : "";
|
|
614
|
-
console.error(`${r.id} is now for ${r.to || "everyone in the team"}${back}`);
|
|
615
|
-
},
|
|
616
|
-
|
|
617
|
-
async recall(f) {
|
|
618
|
-
const id = f._[0] || die("usage: passalong recall <id>");
|
|
619
|
-
const r = await passalong.recall(id).catch((e) => die(e.message));
|
|
620
|
-
console.error(`${r.id} is private again, and gone from their inboxes`);
|
|
621
|
-
if (r.already_opened_by)
|
|
622
|
-
console.error(`${r.already_opened_by} ${r.already_opened_by === 1 ? "person" : "people"} had already opened it, and keep what they pulled`);
|
|
495
|
+
async export(f) {
|
|
496
|
+
const dir = resolve(f._[0] || "passalong-export");
|
|
497
|
+
const paths = await passalong.exportAll(dir).catch((e) => die(e.message));
|
|
498
|
+
console.error(`exported ${paths.length} → ${dir}`);
|
|
623
499
|
},
|
|
624
500
|
|
|
625
501
|
// ---- who runs the product ---------------------------------------------------------------
|
|
@@ -694,44 +570,6 @@ const commands = {
|
|
|
694
570
|
console.error(`${id} taken back — that plan is lapsed as of now`);
|
|
695
571
|
},
|
|
696
572
|
|
|
697
|
-
/**
|
|
698
|
-
* Archive a guide: off your board, and out of what the free tier counts.
|
|
699
|
-
*
|
|
700
|
-
* The command was `done`, and the status it sets is still `consumed`, because both once meant "I
|
|
701
|
-
* implemented it". That meaning went to the verdict, where it belongs — it is the reader's
|
|
702
|
-
* judgement, not the author's — and what was left is shelf space, which has a job nothing else
|
|
703
|
-
* does: the only way under the sync limit that does not delete the work, and the 402 says so by
|
|
704
|
-
* name. So the command says what it does now.
|
|
705
|
-
*
|
|
706
|
-
* `consumed` stays on disk. It is a status inside markdown files in other people's
|
|
707
|
-
* repositories, and `validate()` refuses a status it does not know, so renaming the value would
|
|
708
|
-
* make a guide shared a month ago fail to re-share today. The word people read and the token the
|
|
709
|
-
* format stores are allowed to differ; what is not allowed is two words for people.
|
|
710
|
-
*/
|
|
711
|
-
async archive(f) {
|
|
712
|
-
const id = f._[0] || die("usage: passalong archive <id>");
|
|
713
|
-
const g = await passalong.setStatus(id, "consumed").catch((e) => die(e.message));
|
|
714
|
-
console.error(`${g.meta.id} archived — off your board, and it no longer counts against sync`);
|
|
715
|
-
},
|
|
716
|
-
|
|
717
|
-
/** What `archive` was called. Kept so a script written against it still runs, and says so. */
|
|
718
|
-
async done(f) {
|
|
719
|
-
console.error("`passalong done` is now `passalong archive` — it archives, it does not mark work done.");
|
|
720
|
-
return commands.archive(f);
|
|
721
|
-
},
|
|
722
|
-
|
|
723
|
-
async rm(f) {
|
|
724
|
-
const id = f._[0] || die("usage: passalong rm <id>");
|
|
725
|
-
await passalong.remove(id).catch((e) => die(e.message));
|
|
726
|
-
console.error(`${id} removed`);
|
|
727
|
-
},
|
|
728
|
-
|
|
729
|
-
async export(f) {
|
|
730
|
-
const dir = resolve(f._[0] || "passalong-export");
|
|
731
|
-
const paths = await passalong.exportAll(dir).catch((e) => die(e.message));
|
|
732
|
-
console.error(`exported ${paths.length} guide${paths.length === 1 ? "" : "s"} → ${dir}`);
|
|
733
|
-
},
|
|
734
|
-
|
|
735
573
|
async login(f) {
|
|
736
574
|
// A token on the command line lands in shell history, so it is only read from stdin.
|
|
737
575
|
if (f._[0]) die("pass a token on stdin instead: passalong login --with-token < token.txt");
|
|
@@ -812,7 +650,7 @@ const commands = {
|
|
|
812
650
|
: who.sync === "none"
|
|
813
651
|
? " (no plan — nothing is syncing)"
|
|
814
652
|
: " (no limit on your plan)";
|
|
815
|
-
console.log(`
|
|
653
|
+
console.log(`work ${who.work} active${ceiling}`);
|
|
816
654
|
const current = api.currentTeam();
|
|
817
655
|
console.log(
|
|
818
656
|
`teams ${who.teams.length ? who.teams.map((t) => `${t.slug}${t.slug === current ? "*" : ""} (${t.role})`).join(", ") : "(none — passalong team create <name>)"}`,
|
|
@@ -829,7 +667,7 @@ const commands = {
|
|
|
829
667
|
return console.error(`you are in ${teams.map((t) => t.slug).join(", ")}; pick one: passalong team use <slug>`);
|
|
830
668
|
}
|
|
831
669
|
const t = await api.team(cfg.team).catch((e) => die(e.message));
|
|
832
|
-
console.log(`${t.name} (${t.slug}) — ${t.members.length} member${t.members.length === 1 ? "" : "s"}, ${t.
|
|
670
|
+
console.log(`${t.name} (${t.slug}) — ${t.members.length} member${t.members.length === 1 ? "" : "s"}, ${t.work} piece${t.work === 1 ? "" : "s"} of work`);
|
|
833
671
|
for (const m of t.members)
|
|
834
672
|
console.log(` ${m.handle ? `@${m.handle}` : "(no handle)"}${m.name ? ` ${m.name}` : ""} ${m.role === "owner" ? "admin" : m.role} joined ${relDate(m.joined)}`);
|
|
835
673
|
return;
|
|
@@ -929,9 +767,10 @@ const commands = {
|
|
|
929
767
|
} catch (err) {
|
|
930
768
|
console.error(`could not install Claude Code hooks (${err.message}); ${settingsPath} left as it was`);
|
|
931
769
|
}
|
|
932
|
-
if (!api.loggedIn())
|
|
933
|
-
|
|
934
|
-
|
|
770
|
+
if (!api.loggedIn()) console.error("not signed in yet: run `passalong login`");
|
|
771
|
+
console.error(
|
|
772
|
+
'done. In Claude Code, say "pass this along" to write what a session learned into the project\'s documents, or run /passalong-capture.',
|
|
773
|
+
);
|
|
935
774
|
},
|
|
936
775
|
|
|
937
776
|
/**
|
|
@@ -940,7 +779,7 @@ const commands = {
|
|
|
940
779
|
* nothing at all when anything goes wrong — a hook must never get in the way of a session.
|
|
941
780
|
*/
|
|
942
781
|
async now(f) {
|
|
943
|
-
const { nowText, statusText,
|
|
782
|
+
const { nowText, statusText, stopDecision } = await import("../src/hooks.js");
|
|
944
783
|
// The status line: printed from a cache, refreshed behind it. Claude Code cancels a status
|
|
945
784
|
// line still running when the next update comes, so this never waits on the network.
|
|
946
785
|
if (f.statusline) return statusLine(f);
|
|
@@ -948,7 +787,7 @@ const commands = {
|
|
|
948
787
|
const cwd = typeof f["statusline-refresh"] === "string" ? f["statusline-refresh"] : process.cwd();
|
|
949
788
|
try {
|
|
950
789
|
const r = await passalong.now({ cwd });
|
|
951
|
-
writeStatus(cwd, statusText(
|
|
790
|
+
writeStatus(cwd, statusText(r));
|
|
952
791
|
} catch {}
|
|
953
792
|
return;
|
|
954
793
|
}
|
|
@@ -997,7 +836,7 @@ const commands = {
|
|
|
997
836
|
}),
|
|
998
837
|
);
|
|
999
838
|
} else if (f.hook === "stop") {
|
|
1000
|
-
const v =
|
|
839
|
+
const v = stopDecision({ held: r.held, input });
|
|
1001
840
|
if (v) console.log(JSON.stringify(v));
|
|
1002
841
|
}
|
|
1003
842
|
} catch {}
|
|
@@ -1020,7 +859,7 @@ const commands = {
|
|
|
1020
859
|
async help() {
|
|
1021
860
|
const src = readFileSync(fileURLToPath(import.meta.url), "utf8");
|
|
1022
861
|
const lines = src.split("\n").filter((l) => l.startsWith("// passalong "));
|
|
1023
|
-
console.log("passalong —
|
|
862
|
+
console.log("passalong — work for people and agents, and what they know\n");
|
|
1024
863
|
console.log(lines.map((l) => l.slice(3)).join("\n"));
|
|
1025
864
|
},
|
|
1026
865
|
};
|