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/src/api.js CHANGED
@@ -1,5 +1,5 @@
1
- // Client for the hosted sync API (apps/api). Everything here is optional: with no token the
2
- // CLI is a purely local tool, and every function throws an ApiError the CLI turns into a hint.
1
+ // Client for the hosted API (apps/api). Work lives there, so nearly every call needs a login, and
2
+ // every function throws an ApiError the CLI turns into a hint.
3
3
  import { readFileSync } from "node:fs";
4
4
  import { readFile, stat } from "node:fs/promises";
5
5
  import { basename } from "node:path";
@@ -27,7 +27,7 @@ export class ApiError extends Error {
27
27
  // ---- which account -----------------------------------------------------------------------------
28
28
  //
29
29
  // A machine may be signed in to several (store.js). Who a call is made as comes from, in order:
30
- // PASSALONG_TOKEN, an account named by PASSALONG_ACCOUNT, `--as` or `use_account` (this process
30
+ // PASSALONG_TOKEN, an account named by PASSALONG_ACCOUNT, `--as` or `accounts` `use` (this process
31
31
  // only), then the machine's default. With two or more logins and nobody having said which, a
32
32
  // person at a terminal gets the default and an agent — a process with no terminal to ask on — is
33
33
  // refused with the names, so it asks the person rather than choosing. That refusal is the prompt:
@@ -102,7 +102,7 @@ export function choiceMessage() {
102
102
  .join(", ");
103
103
  return (
104
104
  `More than one Passalong account is signed in on this machine (${list}) and nothing says which ` +
105
- "this session is for. Ask the person which one to use, then call use_account with its name " +
105
+ "this session is for. Ask the person which one to use, then call accounts with `use` set to its name " +
106
106
  "(or run the command with --as <name>). Do not choose one yourself."
107
107
  );
108
108
  }
@@ -215,58 +215,6 @@ const q = (params) => {
215
215
  return s ? `?${s}` : "";
216
216
  };
217
217
 
218
- /**
219
- * Sign in, or make the account, and come back with a token for this machine.
220
- *
221
- * Three calls, because the API answers a browser and this is not one: signing in sets a session
222
- * cookie, and the CLI carries a bearer token instead — one that shows up in the hub's token list
223
- * with a name on it, and can be revoked there without changing the password. So the cookie is used
224
- * once, to mint the token, and then dropped.
225
- *
226
- * `make` is signup rather than sign-in. It is the caller's decision and not a fallback here: an
227
- * account created because an address was mistyped is worse than being told there is no such
228
- * account.
229
- */
230
- export async function signIn(email, password, { make = false, label = "" } = {}) {
231
- const res = await fetch(`${baseUrl()}${make ? "/v1/auth/signup" : "/v1/auth/login"}`, {
232
- method: "POST",
233
- headers: { "content-type": "application/json" },
234
- body: JSON.stringify({ email, password }),
235
- }).catch((err) => {
236
- throw new ApiError(0, `could not reach ${baseUrl()} (${err.message})`);
237
- });
238
- if (!res.ok) {
239
- const said = await res.json().catch(() => ({}));
240
- throw new ApiError(res.status, said.message || res.statusText);
241
- }
242
- const { account } = await res.json();
243
- // `getSetCookie` keeps the cookies apart; a joined `set-cookie` header cannot be split safely,
244
- // because an Expires date has a comma in it.
245
- const cookie = (res.headers.getSetCookie?.() ?? []).map((c) => c.split(";")[0]).join("; ");
246
- if (!cookie) throw new ApiError(500, "signed in, but no session came back");
247
-
248
- const minted = await fetch(`${baseUrl()}/v1/tokens`, {
249
- method: "POST",
250
- headers: { "content-type": "application/json", cookie },
251
- body: JSON.stringify({ name: label || "cli" }),
252
- });
253
- if (!minted.ok) {
254
- const said = await minted.json().catch(() => ({}));
255
- throw new ApiError(minted.status, said.message || "could not make a token for this machine");
256
- }
257
- const { token: made } = await minted.json();
258
- return { account, token: made };
259
- }
260
-
261
- /**
262
- * Mint an account with no email and no password.
263
- *
264
- * Nothing in this package calls it any more — `passalong login` asks for an email and a password,
265
- * because an account nobody can sign in to is one that dies with the file it is stored in, and the
266
- * CLI used to make one without ever saying so. It stays exported because the route it calls is
267
- * still live and still right for the invite page, which mints before it claims, and because
268
- * removing a published export is a breaking change for anyone who imported it.
269
- */
270
218
  /**
271
219
  * Sign this machine in from the browser. `challenge` is the sha256, in hex, of a secret kept here;
272
220
  * what comes back is a short code to show, the page to open, and how often to ask whether it was
@@ -293,7 +241,6 @@ export async function devicePoll(id, verifier) {
293
241
  return said;
294
242
  }
295
243
 
296
- export const createAccount = () => call("/v1/accounts", { method: "POST", auth: false });
297
244
  export const me = () => call("/v1/me");
298
245
  export const updateMe = (patch) => call("/v1/me", { method: "PATCH", body: patch });
299
246
 
@@ -314,101 +261,83 @@ export const removeMember = (slug, who) =>
314
261
  export const join = (code) =>
315
262
  call(`/v1/invites/${encodeURIComponent(code)}/accept`, { method: "POST" });
316
263
 
317
- export const board = () => call("/v1/board");
318
- export const folders = (scope = "all", { query = "", guide = "" } = {}) =>
319
- call(`/v1/folders${q({ scope, q: query, guide })}`);
320
- export const createFolder = (title, description = "", team = "", parent = "", color = "") =>
321
- call("/v1/folders", { method: "POST", body: { title, description, team, parent, color } });
322
- export const folder = (id) => call(`/v1/folders/${encodeURIComponent(id)}`);
323
- export const updateFolder = (id, changes) =>
324
- call(`/v1/folders/${encodeURIComponent(id)}`, { method: "PATCH", body: changes });
325
- export const folderDocument = (folderId, documentId) =>
326
- call(`/v1/folders/${encodeURIComponent(folderId)}/documents/${encodeURIComponent(documentId)}`);
327
- export const folderAsset = (folderId, assetId) =>
328
- call(`/v1/folders/${encodeURIComponent(folderId)}/assets/${encodeURIComponent(assetId)}/agent`);
329
- export async function addFolderAsset(folderId, path, name = "") {
264
+ export const projects = (scope = "all", query = "") =>
265
+ call(`/v1/projects${q({ scope, q: query })}`);
266
+ export const createProject = (
267
+ title,
268
+ description = "",
269
+ team = "",
270
+ parent = "",
271
+ color = "",
272
+ repo = "",
273
+ ) =>
274
+ call("/v1/projects", { method: "POST", body: { title, description, team, parent, color, repo } });
275
+ export const project = (id) => call(`/v1/projects/${encodeURIComponent(id)}`);
276
+ export const updateProject = (id, changes) =>
277
+ call(`/v1/projects/${encodeURIComponent(id)}`, { method: "PATCH", body: changes });
278
+ export const projectDocument = (projectId, documentId) =>
279
+ call(`/v1/projects/${encodeURIComponent(projectId)}/documents/${encodeURIComponent(documentId)}`);
280
+ export const projectFile = (projectId, fileId) =>
281
+ call(`/v1/projects/${encodeURIComponent(projectId)}/files/${encodeURIComponent(fileId)}/agent`);
282
+ export async function addProjectFile(projectId, path, name = "") {
330
283
  const file = await stat(path);
331
- if (!file.isFile()) throw new ApiError(400, "Choose a file to add to the folder.");
284
+ if (!file.isFile()) throw new ApiError(400, "Choose a file to add to the project.");
332
285
  if (file.size > 10 * 1024 * 1024) throw new ApiError(413, "That file is over 10 MB.");
333
- return call(`/v1/folders/${encodeURIComponent(folderId)}/assets`, {
286
+ return call(`/v1/projects/${encodeURIComponent(projectId)}/files`, {
334
287
  method: "POST",
335
288
  body: new Uint8Array(await readFile(path)),
336
289
  uploadName: name || basename(path),
337
290
  });
338
291
  }
339
- export const createFolderDocument = (folderId, name, body) =>
340
- call(`/v1/folders/${encodeURIComponent(folderId)}/documents`, {
292
+ export const createProjectDocument = (projectId, name, body) =>
293
+ call(`/v1/projects/${encodeURIComponent(projectId)}/documents`, {
341
294
  method: "POST",
342
295
  body: { name, body },
343
296
  });
344
- export const saveFolderDocument = (folderId, documentId, version, body) =>
345
- call(`/v1/folders/${encodeURIComponent(folderId)}/documents/${encodeURIComponent(documentId)}`, {
346
- method: "PUT",
347
- body: { version, body },
348
- });
349
- export const renameFolderDocument = (folderId, documentId, name) =>
350
- call(`/v1/folders/${encodeURIComponent(folderId)}/documents/${encodeURIComponent(documentId)}`, {
351
- method: "PATCH",
352
- body: { name },
353
- });
354
- export const moveFolderDocument = (folderId, documentId, to, name = "") =>
355
- call(`/v1/folders/${encodeURIComponent(folderId)}/documents/${encodeURIComponent(documentId)}`, {
356
- method: "PATCH",
357
- body: name ? { folder: to, name } : { folder: to },
358
- });
359
- export const folderDocumentHistory = (folderId, documentId, version = 0) =>
297
+ export const saveProjectDocument = (projectId, documentId, version, body) =>
360
298
  call(
361
- `/v1/folders/${encodeURIComponent(folderId)}/documents/${encodeURIComponent(documentId)}/revisions${version ? `/${version}` : ""}`,
299
+ `/v1/projects/${encodeURIComponent(projectId)}/documents/${encodeURIComponent(documentId)}`,
300
+ {
301
+ method: "PUT",
302
+ body: { version, body },
303
+ },
362
304
  );
363
- export const shareFolder = (folderId) =>
364
- call(`/v1/folders/${encodeURIComponent(folderId)}/share`, { method: "POST" });
365
- export const linkFolderGuide = (folderId, guide) =>
366
- call(`/v1/folders/${encodeURIComponent(folderId)}/guides`, { method: "POST", body: { guide } });
367
- export const unlinkFolderGuide = (folderId, guide) =>
368
- call(`/v1/folders/${encodeURIComponent(folderId)}/guides/${encodeURIComponent(guide)}`, {
369
- method: "DELETE",
370
- });
371
- export const deleteFolderDocument = (folderId, documentId) =>
372
- call(`/v1/folders/${encodeURIComponent(folderId)}/documents/${encodeURIComponent(documentId)}`, {
373
- method: "DELETE",
374
- });
375
- export const deleteFolderAsset = (folderId, assetId) =>
376
- call(`/v1/folders/${encodeURIComponent(folderId)}/assets/${encodeURIComponent(assetId)}`, {
305
+ /** Rename a document, move it to another project (`project`), or both. Its id and history stay. */
306
+ export const patchProjectDocument = (projectId, documentId, changes) =>
307
+ call(
308
+ `/v1/projects/${encodeURIComponent(projectId)}/documents/${encodeURIComponent(documentId)}`,
309
+ {
310
+ method: "PATCH",
311
+ body: changes,
312
+ },
313
+ );
314
+ export const projectDocumentHistory = (projectId, documentId, version = 0) =>
315
+ call(
316
+ `/v1/projects/${encodeURIComponent(projectId)}/documents/${encodeURIComponent(documentId)}/revisions${version ? `/${version}` : ""}`,
317
+ );
318
+ export const deleteProjectDocument = (projectId, documentId) =>
319
+ call(
320
+ `/v1/projects/${encodeURIComponent(projectId)}/documents/${encodeURIComponent(documentId)}`,
321
+ {
322
+ method: "DELETE",
323
+ },
324
+ );
325
+ export const deleteProjectFile = (projectId, fileId) =>
326
+ call(`/v1/projects/${encodeURIComponent(projectId)}/files/${encodeURIComponent(fileId)}`, {
377
327
  method: "DELETE",
378
328
  });
379
- /** What you did, newest first. `since` is a date prefix: 2026, 2026-09, 2026-09-11. */
380
- export const log = ({ repo = "", since = "", limit = 0 } = {}) =>
381
- call(`/v1/log${q({ repo, since, limit: limit || "" })}`);
382
- export const notifications = ({ unread = true, limit = 0 } = {}) =>
383
- call(`/v1/notifications${q({ unread: unread ? "1" : "", limit: limit || "" })}`);
384
- /** No ids means "everything unread". */
385
- export const markRead = (ids = []) =>
386
- call("/v1/notifications/read", { method: "POST", body: { ids } });
387
-
388
- export const publish = (id, markdown) =>
389
- call(`/v1/guides/${id}`, { method: "PUT", body: markdown });
390
- export const list = (query = "", scope = "") => call(`/v1/guides${q({ q: query, scope })}`);
391
- export const inbox = () => call("/v1/inbox");
392
- /** Open a report for a set of issues to be filed under. */
393
- export const createReport = (body) => call("/v1/reports", { method: "POST", body });
394
- /** One report and the issues filed under it, grouped by product area. */
395
- export const report = (id) => call(`/v1/reports/${encodeURIComponent(id)}`);
329
+ export const publish = (id, markdown) => call(`/v1/work/${id}`, { method: "PUT", body: markdown });
330
+ export const list = (query = "", scope = "") => call(`/v1/work${q({ q: query, scope })}`);
396
331
  export const get = (id, { peek = false, as = null } = {}) =>
397
- call(`/v1/guides/${id}${q({ peek: peek ? "1" : "" })}`, { raw: true, as });
398
- /** One page of every guide this account can read, for `export`. Records no pull. */
332
+ call(`/v1/work/${id}${q({ peek: peek ? "1" : "" })}`, { raw: true, as });
333
+ /** One page of every piece of work this account can read, for `export`. Records no pull. */
399
334
  export const exportPage = (after = "", as = null) => call(`/v1/export${q({ after })}`, { as });
400
335
  /**
401
- * A guide's follow-ups, one level down. `markdown` adds each one's content, oldest first and
402
- * capped server-side; neither form counts as opening them.
403
- */
404
- export const children = (id, { markdown = false } = {}) =>
405
- call(`/v1/guides/${encodeURIComponent(id)}/children${q({ markdown: markdown ? "1" : "" })}`);
406
- /**
407
- * The guide this one came out of, with where it has got to. `markdown` adds its content. Like the
408
- * follow-ups, reading it is not opening it: no pull is recorded on the parent.
336
+ * The work this one came out of, with where it has got to. `markdown` adds its content. Reading it
337
+ * is not opening it: no pull is recorded on the parent.
409
338
  */
410
339
  export const parent = (id, { markdown = false } = {}) =>
411
- call(`/v1/guides/${encodeURIComponent(id)}/parent${q({ markdown: markdown ? "1" : "" })}`);
340
+ call(`/v1/work/${encodeURIComponent(id)}/parent${q({ markdown: markdown ? "1" : "" })}`);
412
341
  /**
413
342
  * Giving a plan away, for whoever the deployment names in `ADMIN_ACCOUNTS`. Every other account
414
343
  * gets a 404 from these, so the CLI does not have to know who the operator is either.
@@ -424,63 +353,29 @@ export const listGifts = () => call("/v1/admin/gifts");
424
353
  export const takeGiftBack = (id) =>
425
354
  call(`/v1/admin/gifts/${encodeURIComponent(id)}`, { method: "DELETE" });
426
355
  export const setStatus = (id, status) =>
427
- call(`/v1/guides/${id}/status`, { method: "PATCH", body: { status } });
428
- export const remove = (id) => call(`/v1/guides/${id}`, { method: "DELETE" });
429
- /** Does it actually work? `note` is required when it does not. */
430
- export const verdict = (id, ok, note = "", detail = "") =>
431
- call(`/v1/guides/${id}/verdict`, {
432
- method: "PUT",
433
- body: { ok, note, ...(detail ? { detail } : {}) },
434
- });
435
-
356
+ call(`/v1/work/${id}/status`, { method: "PATCH", body: { status } });
357
+ export const remove = (id) => call(`/v1/work/${id}`, { method: "DELETE" });
436
358
  export const tasks = () => call("/v1/tasks");
437
359
  export const nextTask = (body) => call("/v1/tasks/next", { method: "POST", body });
438
- export const taskProgress = (id, body) =>
439
- call(`/v1/tasks/${encodeURIComponent(id)}/progress`, { method: "PUT", body });
440
- export const finishTask = (id, body) =>
441
- call(`/v1/tasks/${encodeURIComponent(id)}/finish`, { method: "POST", body });
442
- export const approveTask = (id) =>
443
- call(`/v1/tasks/${encodeURIComponent(id)}/approve`, { method: "POST", body: {} });
444
- export const rejectTask = (id, why) =>
445
- call(`/v1/tasks/${encodeURIComponent(id)}/reject`, { method: "POST", body: { why } });
446
360
  export const reopen = (id, why) =>
447
- call(`/v1/guides/${encodeURIComponent(id)}/reopen`, { method: "POST", body: { why } });
448
- /** A handoff's or bug's approve and send-back. No `place` names the only hand-in there is. */
449
- export const closeGuide = (id) =>
450
- call(`/v1/guides/${encodeURIComponent(id)}/close`, { method: "POST", body: {} });
451
- export const sendBack = (id, why) =>
452
- call(`/v1/guides/${encodeURIComponent(id)}/send_back`, { method: "POST", body: { why } });
453
- /** Take work back from whoever holds it, of any kind. `/v1/tasks/…/release` is the old path. */
361
+ call(`/v1/work/${encodeURIComponent(id)}/reopen`, { method: "POST", body: { why } });
362
+ /** Take work back from whoever holds it. */
454
363
  export const release = (id) =>
455
- call(`/v1/guides/${encodeURIComponent(id)}/release`, { method: "POST", body: {} });
456
- /** The four verbs every guide answers to. See docs/V2.md §11. */
364
+ call(`/v1/work/${encodeURIComponent(id)}/release`, { method: "POST", body: {} });
457
365
  export const working = () => call("/v1/working");
458
- export const handedIn = () => call("/v1/handed_in");
459
- export const recall = (id) =>
460
- call(`/v1/guides/${encodeURIComponent(id)}/recall`, { method: "POST", body: {} });
461
366
  export const assign = (id, to) =>
462
- call(`/v1/guides/${encodeURIComponent(id)}/assign`, { method: "POST", body: { to } });
367
+ call(`/v1/work/${encodeURIComponent(id)}/assign`, { method: "POST", body: { to } });
463
368
  export const take = (body) => call("/v1/take", { method: "POST", body });
464
369
  export const progress = (id, body) =>
465
- call(`/v1/guides/${encodeURIComponent(id)}/progress`, { method: "PUT", body });
370
+ call(`/v1/work/${encodeURIComponent(id)}/progress`, { method: "PUT", body });
466
371
  export const handIn = (id, body) =>
467
- call(`/v1/guides/${encodeURIComponent(id)}/hand_in`, { method: "POST", body });
372
+ call(`/v1/work/${encodeURIComponent(id)}/hand_in`, { method: "POST", body });
468
373
  export const ask = (id, body) =>
469
- call(`/v1/guides/${encodeURIComponent(id)}/ask`, { method: "POST", body });
374
+ call(`/v1/work/${encodeURIComponent(id)}/ask`, { method: "POST", body });
470
375
  export const reply = (id, body) =>
471
- call(`/v1/guides/${encodeURIComponent(id)}/reply`, { method: "POST", body: { body } });
376
+ call(`/v1/work/${encodeURIComponent(id)}/reply`, { method: "POST", body: { body } });
472
377
  export const pass = (id, body) =>
473
- call(`/v1/guides/${encodeURIComponent(id)}/pass`, { method: "POST", body });
474
- export const ack = (id, taken, note = "") =>
475
- call(`/v1/guides/${id}/ack`, { method: "PUT", body: { taken, note } });
476
-
477
- /**
478
- * Upload an image as evidence, and get back the URL a guide points at.
479
- *
480
- * Not `call()`: that serialises a body as JSON or markdown, and this route reads bytes and takes
481
- * the content type as the declaration of what they are. `name` is the label, and it travels in a
482
- * header rather than the body for the same reason.
483
- */
378
+ call(`/v1/work/${encodeURIComponent(id)}/pass`, { method: "POST", body });
484
379
  /**
485
380
  * Upload a file that is not a picture, and get back what the server made of it. The server decides
486
381
  * what the bytes are and refuses what it does not keep; `type` and `name` are hints. The name travels
@@ -491,6 +386,7 @@ export async function uploadFile(bytes, type, name = "") {
491
386
  const t = token();
492
387
  if (!t) throw new ApiError(401, "not logged in — run `passalong login` to enable sync");
493
388
  const headers = {
389
+ ...CLIENT,
494
390
  authorization: `Bearer ${t}`,
495
391
  "content-type": type || "application/octet-stream",
496
392
  };
@@ -499,7 +395,7 @@ export async function uploadFile(bytes, type, name = "") {
499
395
  try {
500
396
  res = await fetch(`${baseUrl()}/v1/attachments`, { method: "POST", headers, body: bytes });
501
397
  } catch (err) {
502
- throw new ApiError(0, `could not reach ${base} (${err.message})`);
398
+ throw new ApiError(0, `could not reach ${baseUrl()} (${err.message})`);
503
399
  }
504
400
  if (!res.ok) {
505
401
  const body = await res.text();
@@ -512,6 +408,10 @@ export async function uploadFile(bytes, type, name = "") {
512
408
  return (await res.json()).attachment;
513
409
  }
514
410
 
411
+ /**
412
+ * Upload a picture, and get back the URL work points at. Not `call()`: that serialises a body as
413
+ * JSON or markdown, and this route reads bytes and takes the content type as what they are.
414
+ */
515
415
  export async function uploadShot(bytes, type, name = "") {
516
416
  const t = token();
517
417
  if (!t) throw new ApiError(401, "not logged in — run `passalong login` to enable sync");
@@ -523,7 +423,7 @@ export async function uploadShot(bytes, type, name = "") {
523
423
  try {
524
424
  res = await fetch(`${baseUrl()}/v1/shots`, { method: "POST", headers, body: bytes });
525
425
  } catch (err) {
526
- throw new ApiError(0, `could not reach ${base} (${err.message})`);
426
+ throw new ApiError(0, `could not reach ${baseUrl()} (${err.message})`);
527
427
  }
528
428
  if (!res.ok) {
529
429
  const body = await res.text();
@@ -536,7 +436,7 @@ export async function uploadShot(bytes, type, name = "") {
536
436
  return (await res.json()).shot;
537
437
  }
538
438
 
539
- /** Fetch a guide by its share link (no account needed). Accepts the web URL or the .md URL. */
439
+ /** Fetch work by its share link (no account needed). Accepts the web URL or the .md URL. */
540
440
  export async function fetchShared(url) {
541
441
  const u = new URL(url);
542
442
  if (!u.pathname.endsWith(".md")) u.pathname = `${u.pathname.replace(/\/$/, "")}.md`;
@@ -545,7 +445,3 @@ export async function fetchShared(url) {
545
445
  throw new ApiError(res.status, `could not fetch ${url}: ${res.status} ${res.statusText}`);
546
446
  return res.text();
547
447
  }
548
-
549
- export function shareUrl(id, key) {
550
- return `${baseUrl()}/g/${id}/${key}`;
551
- }
package/src/capture.js CHANGED
@@ -1,9 +1,8 @@
1
- // Fallback capture when no agent wrote a draft: scaffold one from the working directory so the
2
- // developer starts from "what changed" instead of a blank page. The Claude Code skill in
3
- // skills/passalong-capture does the real distillation from a session transcript.
1
+ // What can be learned about where a command runs, without reading any session: the repo, its
2
+ // branch, who is committing. Work written from inside a repo is for that repo.
4
3
  import { execFileSync } from "node:child_process";
5
4
  import { basename } from "node:path";
6
- import { template } from "./guide.js";
5
+ import { serialize } from "./work.js";
7
6
 
8
7
  function git(cwd, ...args) {
9
8
  try {
@@ -40,59 +39,24 @@ export function context(cwd = process.cwd()) {
40
39
  const name = basename(root || cwd);
41
40
  const branch = root ? git(cwd, "rev-parse", "--abbrev-ref", "HEAD") : "";
42
41
  const author = git(cwd, "config", "user.name");
43
- const commits = root ? git(cwd, "log", "--oneline", "-8").split("\n").filter(Boolean) : [];
44
- const changed = root
45
- ? git(cwd, "diff", "--stat", "HEAD~5..HEAD").split("\n").filter(Boolean).slice(-1)[0] || ""
46
- : "";
47
- const files = root
48
- ? git(cwd, "diff", "--name-only", "HEAD~5..HEAD").split("\n").filter(Boolean)
49
- : [];
50
42
  // The repo as the task queue names it: its remote when it has one, because two worktrees of one
51
- // repo have two folder names and one remote. The folder name when there is no remote.
43
+ // repo have two project names and one remote. The project name when there is no remote.
52
44
  const repo = root ? repoKey(git(cwd, "remote", "get-url", "origin") || name) : "";
53
- return { root, name, repo, branch, author, commits, changed, files };
45
+ return { root, name, repo, branch, author };
54
46
  }
55
47
 
56
48
  /**
57
- * A task draft from one sentence: the sentence as the title and the Goal, the rest left to fill.
49
+ * A piece of work from one sentence: the sentence is its title, what it says to a person, and its
50
+ * text, for whoever wrote it to add to before it goes.
58
51
  *
59
- * `target_context` defaults to the repo this runs in, because that is the likeliest repo a task
60
- * written from inside one is for — and it is in the editor, in the frontmatter, for the author
61
- * to change before it goes anywhere. Outside a repo it stays empty, which is a task for no repo.
52
+ * `target_context` is the repo this runs in, because that is the likeliest repo work written from
53
+ * inside one is for — and it is in the frontmatter, for the author to change before it goes
54
+ * anywhere. Outside a repo it stays empty, which is work for no repo.
62
55
  */
63
- export function taskScaffold(sentence, cwd = process.cwd()) {
64
- const c = context(cwd);
65
- const md = template({
66
- kind: "task",
67
- title: sentence,
68
- // The person typed this sentence, so it is theirs to say to a person as well as to name the task.
69
- summary: String(sentence).slice(0, 400),
70
- author: c.author,
71
- source_context: c.branch && c.branch !== "HEAD" ? `${c.name}@${c.branch}` : c.name,
72
- target_context: c.repo,
56
+ export function workScaffold(sentence, cwd = process.cwd()) {
57
+ const said = String(sentence).trim();
58
+ return serialize({
59
+ meta: { title: said, summary: said.slice(0, 400), target_context: context(cwd).repo },
60
+ body: `${said}\n`,
73
61
  });
74
- return md.replace(/^## Goal\n/m, `## Goal\n${sentence}\n`);
75
- }
76
-
77
- /** A draft pre-filled with repo facts. Recent activity is appended as a comment for reference. */
78
- export function scaffold(cwd = process.cwd()) {
79
- const c = context(cwd);
80
- let md = template({
81
- kind: "transfer",
82
- title: "",
83
- author: c.author,
84
- source_context: c.branch && c.branch !== "HEAD" ? `${c.name}@${c.branch}` : c.name,
85
- });
86
- if (c.commits.length) {
87
- md += [
88
- "",
89
- "<!-- passalong: recent activity in this repo, for reference while you write. Delete when done.",
90
- ...c.commits.map((l) => ` ${l}`),
91
- c.changed ? ` ${c.changed}` : "",
92
- ...c.files.slice(0, 30).map((f) => ` ${f}`),
93
- "-->",
94
- "",
95
- ].join("\n");
96
- }
97
- return md;
98
62
  }
package/src/checks.js CHANGED
@@ -2,15 +2,15 @@
2
2
  * Running an Acceptance check, rather than reading the agent's account of running it.
3
3
  *
4
4
  * A hand-in used to be the agent's word about its own work: `ran` was a string, and the only rule
5
- * was that it was longer than a verdict. "ran the tests, all green" passed. This module is the
5
+ * was that it was longer than a yes. "ran the tests, all green" passed. This module is the
6
6
  * other half — when a check names the command that proves it, the command is run here and the exit
7
7
  * code is recorded from the process, so the agent reports what it did and the machine reports how
8
8
  * it went.
9
9
  *
10
- * **The command comes from the agent's own hand_in call, never from a guide.** A guide is written
10
+ * **The command comes from the agent's own hand_in call, never from work.** Work is written
11
11
  * somewhere else and pulled from a stranger's account; lifting a command out of its Acceptance
12
- * section and running it would turn every pulled guide into remote code execution. Nothing here
13
- * parses guide markdown, and nothing should.
12
+ * section and running it would turn every pulled work into remote code execution. Nothing here
13
+ * parses work markdown, and nothing should.
14
14
  *
15
15
  * This runs only where there is a shell. `src/mcp.js` is a local stdio server on the agent's own
16
16
  * machine, so it can; `apps/api/src/mcp-http.ts` is a Worker, so it cannot, and a hand-in over HTTP
@@ -19,7 +19,7 @@
19
19
  * What this deliberately does NOT do is parse test output. SWE-bench needs 57 per-framework log
20
20
  * parsers to tell a skipped test from a passing one, and a half-parser that guesses is worse than
21
21
  * none: it would refuse honest runs in shapes nobody thought of, and pass the ones it misread. The
22
- * exit code is the whole verdict here, which is also how a CI step decides.
22
+ * exit code is the whole answer here, which is also how a CI step decides.
23
23
  */
24
24
  import { spawnSync } from "node:child_process";
25
25
  import { existsSync } from "node:fs";
package/src/hooks.js CHANGED
@@ -1,4 +1,4 @@
1
- // The Claude Code hooks `passalong setup` installs. See docs/V2.md §11.
1
+ // The Claude Code hooks `passalong setup` installs.
2
2
  //
3
3
  // Two moments an agent forgets what it holds: when a session starts (a restart, a /clear, a
4
4
  // compaction), and when it decides it is done. SessionStart runs `passalong now --hook session`
@@ -26,14 +26,10 @@ const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
26
26
  * agent to the queue. So a session is told what the worktree holds, and to carry on only with what
27
27
  * was its own before a restart — never to resume another session's work.
28
28
  */
29
- export function nowText({ held = [], waiting = {} }) {
29
+ export function nowText({ held = [], ready = 0 }) {
30
30
  if (held.length === 1) {
31
31
  const [h] = held;
32
- const lines = [
33
- // `kind` is required on what /v1/working sends. The `|| "task"` that used to be here was
34
- // both dead and wrong — absent has meant transfer since kinds existed, never task.
35
- `Passalong: This worktree holds ${h.id} (${h.kind}): ${h.title || h.id}.`,
36
- ];
32
+ const lines = [`Passalong: This worktree holds ${h.id}: ${h.title || h.id}.`];
37
33
  if (h.note) lines.push(`Last note: "${h.note}".`);
38
34
  // The one thing here that is new information rather than a reminder: the person has written, and
39
35
  // nothing else in the session will say so until the agent next checks in.
@@ -56,7 +52,7 @@ export function nowText({ held = [], waiting = {} }) {
56
52
  const lines = [`Passalong: This worktree holds ${held.length} things:`];
57
53
  for (const h of held)
58
54
  lines.push(
59
- `- ${h.id} (${h.kind}): ${h.title || h.id}${h.state === "stalled" ? " — stalled" : ""}` +
55
+ `- ${h.id}: ${h.title || h.id}${h.state === "stalled" ? " — stalled" : ""}` +
60
56
  `${h.replies ? ` — ${plural(h.replies, "message", "messages")} waiting` : ""}`,
61
57
  );
62
58
  lines.push(
@@ -65,13 +61,8 @@ export function nowText({ held = [], waiting = {} }) {
65
61
  );
66
62
  return lines.join("\n");
67
63
  }
68
- const parts = [];
69
- if (waiting.ready)
70
- parts.push(plural(waiting.ready, "ready task for this repo", "ready tasks for this repo"));
71
- if (waiting.inbox)
72
- parts.push(plural(waiting.inbox, "guide handed to you", "guides handed to you"));
73
- if (!parts.length) return "";
74
- return `Passalong: ${parts.join(" and ")}. Call take with no id to start the next one, when the user wants that.`;
64
+ if (!ready) return "";
65
+ return `Passalong: ${plural(ready, "piece of work is", "pieces of work are")} ready for this repo. Call take with no id to start the next one, when the user wants that.`;
75
66
  }
76
67
 
77
68
  /**
@@ -81,7 +72,7 @@ export function nowText({ held = [], waiting = {} }) {
81
72
  * would leave it no way to end. The hook cannot tell which session took what, so it names
82
73
  * everything still open here and lets the session settle its own.
83
74
  */
84
- export function stopVerdict({ held = [], input = {} }) {
75
+ export function stopDecision({ held = [], input = {} }) {
85
76
  // An agent that asked a question was told to stop and wait, so it must be let: blocking it would
86
77
  // make the instruction and the hook contradict each other, and the agent would pick one.
87
78
  const open = held.filter((h) => !h.asking);
@@ -131,8 +122,8 @@ const clip = (s, n) => (s.length > n ? `${s.slice(0, n - 1)}…` : s);
131
122
 
132
123
  /**
133
124
  * One line: what this worktree's agent holds, then what needs you and what is ready here.
134
- * `needs` counts everything waiting on the person: work handed in for review, agents stuck on
135
- * them, and guides handed to them.
125
+ * `needs` counts everything waiting on the person: drafts to read, agents asking or stuck on them,
126
+ * and agents gone quiet.
136
127
  */
137
128
  export function statusText({ held = [], needs = 0, ready = 0 }) {
138
129
  const parts = [];