@stringsync/splates 0.0.56 → 0.0.58

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/index.js CHANGED
@@ -50,7 +50,7 @@ import { agents } from "@stringsync/splates-server/agents";
50
50
  import { attach } from "@stringsync/splates-server/artifacts";
51
51
  import { commaList as commaList5 } from "@stringsync/splates-server/comma-list";
52
52
  import { refuseGroomer as refuseGroomer4 } from "@stringsync/splates-server/conversations";
53
- import { exits, fail as fail8 } from "@stringsync/splates-server/exit";
53
+ import { exits, fail as fail9 } from "@stringsync/splates-server/exit";
54
54
  import { DEFAULT_COUNT, log as showLog } from "@stringsync/splates-server/log";
55
55
  import { merge } from "@stringsync/splates-server/merge";
56
56
  import { portsLs } from "@stringsync/splates-server/ports";
@@ -85,9 +85,9 @@ import {
85
85
  untilAfter as untilAfter3,
86
86
  wait
87
87
  } from "@stringsync/splates-server/tasks";
88
- import { cli as cli14 } from "webappwiz/cmd";
88
+ import { cli as cli15 } from "webappwiz/cmd";
89
89
  import { Duration as Duration3 } from "webappwiz/time";
90
- import { z as z15 } from "zod";
90
+ import { z as z16 } from "zod";
91
91
 
92
92
  // artifact.ts
93
93
  import { basename, resolve as resolve2 } from "node:path";
@@ -322,14 +322,61 @@ splatesCreds.command("list").use(repository2({ needs: "read" })).description("th
322
322
  }
323
323
  });
324
324
 
325
+ // evals.ts
326
+ import { resolve as resolve3 } from "node:path";
327
+ import {
328
+ inspectBatch,
329
+ writeImported
330
+ } from "@stringsync/splates-server/evals";
331
+ import { fail as fail4 } from "@stringsync/splates-server/exit";
332
+ import { cli as cli3 } from "webappwiz/cmd";
333
+ import { color as color2 } from "webappwiz/log";
334
+ import { z as z4 } from "zod";
335
+ var splatesEvals = cli3("evals").description("a project's evals from another framework, which the Evals page of `splates server` shows once they are a splates-evals/1 batch in the directory `evals.results` in splates.config.ts names (docs/evals.md)");
336
+ splatesEvals.command("import").description("turn a framework's results into a splates-evals/1 batch in <batch-dir>, replacing one there: for inspect, an Inspect AI eval log, `.json` read as it is and `.eval` through `inspect log dump`, a cell per sample and epoch that passes when every scorer's value, read as Inspect does (C 1, P 0.5, I 0), reaches --pass-at").arg("framework", z4.enum(["inspect"]), {
337
+ description: "what wrote the results: inspect"
338
+ }).arg("file", z4.string(), { description: "the framework's results file" }).arg("batch-dir", z4.string(), {
339
+ description: "the batch's directory, inside the one evals.results names; its name is the batch's"
340
+ }).option("pass-at", z4.coerce.number(), {
341
+ default: 1,
342
+ description: "the value, from 0 to 1, each scorer must reach for a pass"
343
+ }).action(async (opts, ctx) => {
344
+ const file = resolve3(ctx.ps.cwd(), opts.file);
345
+ const dir = resolve3(ctx.ps.cwd(), opts["batch-dir"]);
346
+ if (!await ctx.fs.exists(file)) {
347
+ fail4("not_found", `no file at ${file}`, { file });
348
+ }
349
+ const imported = inspectBatch(await inspectLog(ctx, file), opts["pass-at"]);
350
+ await writeImported(dir, imported);
351
+ ctx.log.info(`${color2.green("imported")} ${imported.batch.cells.length} cells to ${dir}`);
352
+ });
353
+ async function inspectLog(ctx, file) {
354
+ const text = file.endsWith(".eval") ? await dumped(ctx, file) : await ctx.fs.read(file);
355
+ try {
356
+ const log = JSON.parse(text);
357
+ if (typeof log.eval?.task === "string") {
358
+ return log;
359
+ }
360
+ } catch {}
361
+ return fail4("usage", `${file} is no Inspect eval log`, { file });
362
+ }
363
+ async function dumped(ctx, file) {
364
+ const { exitCode, stdout, stderr } = await ctx.ps.spawnCapture(["inspect", "log", "dump", file]).catch(() => fail4("usage", "`inspect` is not on PATH: activate the environment Inspect is installed in, or pass a log written with --log-format json"));
365
+ if (exitCode !== 0) {
366
+ fail4("usage", `inspect log dump ${file} failed: ${stderr.trim()}`, {
367
+ file
368
+ });
369
+ }
370
+ return stdout;
371
+ }
372
+
325
373
  // hook.ts
326
374
  import { commaList as commaList3 } from "@stringsync/splates-server/comma-list";
327
- import { fail as fail4 } from "@stringsync/splates-server/exit";
375
+ import { fail as fail5 } from "@stringsync/splates-server/exit";
328
376
  import {
329
377
  HookStore,
330
378
  hookAdd,
331
379
  hookEdit,
332
- hookGardener,
333
380
  hookGroomer,
334
381
  hookList,
335
382
  hookPause,
@@ -340,37 +387,37 @@ import {
340
387
  TASK_EVENTS
341
388
  } from "@stringsync/splates-server/hooks";
342
389
  import { repository as repository3 } from "@stringsync/splates-server/records";
343
- import { cli as cli3 } from "webappwiz/cmd";
344
- import { z as z4 } from "zod";
345
- var splatesHook = cli3("hook").description(`automations: what runs on a task event (${TASK_EVENTS.join(", ")}), as \`splates server\` sees the journal take it, or on a schedule it keeps: an agent told of it; or a shell command. Kept for you in this project's .git/splates, never checked in, since an agent hook spends your tokens; splates.config.ts's \`on\` holds the shell commands a project checks in. None set, nothing runs. The page's Automations tab lists them with when each last ran and for what task`);
346
- splatesHook.command("list").use(repository3({ needs: "read" })).description("every automation set here, by number, with its trigger").option("json", z4.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => hookList({ hooks: storeOf(ctx), log: ctx.log }, { json: opts.json }));
347
- splatesHook.command("add").use(repository3({ needs: "tasks" })).description("run something on a task event, or on a schedule with `--every` or `--cron` in place of the event: `--agent <a> --prompt <words>` starts that agent in the main tree, told the event as `[SPLATES EVENT <event> #N]`, or a schedule as `[SPLATES SCHEDULE <number>]`, then your words; `--run <command>` runs it via `sh -c` in the main tree, with $SPLATES_EVENT (`schedule` for one), $SPLATES_TASKS and $SPLATES_TASK set. A schedule that came due while `splates server` was down runs once as it starts").arg("event", z4.string().optional(), {
390
+ import { cli as cli4 } from "webappwiz/cmd";
391
+ import { z as z5 } from "zod";
392
+ var splatesHook = cli4("hook").description(`automations: what runs on a task event (${TASK_EVENTS.join(", ")}), as \`splates server\` sees the journal take it, or on a schedule it keeps: an agent told of it; or a shell command. Kept for you in this project's .git/splates, never checked in, since an agent hook spends your tokens; splates.config.ts's \`on\` holds the shell commands a project checks in. None set, nothing runs. The page's Automations tab lists them with when each last ran and for what task`);
393
+ splatesHook.command("list").use(repository3({ needs: "read" })).description("every automation set here, by number, with its trigger").option("json", z5.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => hookList({ hooks: storeOf(ctx), log: ctx.log }, { json: opts.json }));
394
+ splatesHook.command("add").use(repository3({ needs: "tasks" })).description("run something on a task event, or on a schedule with `--every` or `--cron` in place of the event: `--agent <a> --prompt <words>` starts that agent in the main tree, told the event as `[SPLATES EVENT <event> #N]`, or a schedule as `[SPLATES SCHEDULE <number>]`, then your words; `--run <command>` runs it via `sh -c` in the main tree, with $SPLATES_EVENT (`schedule` for one), $SPLATES_TASKS and $SPLATES_TASK set. A schedule that came due while `splates server` was down runs once as it starts").arg("event", z5.string().optional(), {
348
395
  description: "the task event, as task.created; none for a schedule"
349
- }).option("stage", z4.string(), {
396
+ }).option("stage", z5.string(), {
350
397
  default: "",
351
398
  description: "only for tasks in these stages, comma separated: backlog, ready, queued, running"
352
- }).option("lane", z4.string(), {
399
+ }).option("lane", z5.string(), {
353
400
  default: "",
354
401
  description: "only for tasks in these lanes, by number, comma separated"
355
- }).option("every", z4.string(), {
402
+ }).option("every", z5.string(), {
356
403
  default: "",
357
404
  description: "run every so often, as 30m, 2h or 1d"
358
- }).option("cron", z4.string(), {
405
+ }).option("cron", z5.string(), {
359
406
  default: "",
360
407
  description: `run on a five-field cron line, in this machine's time, as "0 9 * * 1-5"`
361
- }).option("agent", z4.string(), {
408
+ }).option("agent", z5.string(), {
362
409
  default: "",
363
410
  description: "the agent to run, as `splates agents` names it"
364
- }).option("model", z4.string(), {
411
+ }).option("model", z5.string(), {
365
412
  default: "",
366
413
  description: "its model (default: the agent's own)"
367
- }).option("effort", z4.string(), {
414
+ }).option("effort", z5.string(), {
368
415
  default: "",
369
416
  description: "its thinking level (default: the agent's own)"
370
- }).option("prompt", z4.string(), {
417
+ }).option("prompt", z5.string(), {
371
418
  default: "",
372
419
  description: "what the agent is told after the event"
373
- }).option("run", z4.string(), {
420
+ }).option("run", z5.string(), {
374
421
  default: "",
375
422
  description: "a shell command to run"
376
423
  }).action(async (opts, ctx) => {
@@ -378,122 +425,94 @@ splatesHook.command("add").use(repository3({ needs: "tasks" })).description("run
378
425
  let hook;
379
426
  if (opts.run !== "") {
380
427
  if (pick != null || opts.prompt !== "") {
381
- fail4("usage", "--run goes alone: a shell command runs no agent", {});
428
+ fail5("usage", "--run goes alone: a shell command runs no agent", {});
382
429
  }
383
430
  hook = { kind: "run", command: opts.run };
384
431
  } else if (pick == null) {
385
- fail4("usage", "say what runs: --agent with --prompt, or --run", {});
432
+ fail5("usage", "say what runs: --agent with --prompt, or --run", {});
386
433
  } else if (opts.prompt === "") {
387
- fail4("usage", "an agent hook needs --prompt: what to tell it", {});
434
+ fail5("usage", "an agent hook needs --prompt: what to tell it", {});
388
435
  } else {
389
436
  hook = { kind: "agent", prompt: opts.prompt, ...pick };
390
437
  }
391
438
  await hookAdd({ hooks: storeOf(ctx), log: ctx.log }, triggerOf(opts), hook);
392
439
  });
393
- splatesHook.command("edit").use(repository3({ needs: "tasks" })).description("change one of your automations, by its number in `splates hook list`, keeping its number, its pause and its last runs. Each flag changes only what it names, as `splates hook add` takes it: `--event`, `--every` or `--cron` sets what sets it off anew, `--stage` or `--lane` alone narrows its event again (`any` for no narrowing), `--prompt` or `--agent` changes what an agent is told or which agent, and `--run` makes it a shell command. The groomer changes through `splates hook groomer`").arg("id", z4.string(), { description: "its number in `splates hook list`" }).option("event", z4.string(), {
440
+ splatesHook.command("edit").use(repository3({ needs: "tasks" })).description("change one of your automations, by its number in `splates hook list`, keeping its number, its pause and its last runs. Each flag changes only what it names, as `splates hook add` takes it: `--event`, `--every` or `--cron` sets what sets it off anew, `--stage` or `--lane` alone narrows its event again (`any` for no narrowing), `--prompt` or `--agent` changes what an agent is told or which agent, and `--run` makes it a shell command. The groomer changes through `splates hook groomer`").arg("id", z5.string(), { description: "its number in `splates hook list`" }).option("event", z5.string(), {
394
441
  default: "",
395
442
  description: "a task event to run on, as task.created, in place of what sets it off now"
396
- }).option("stage", z4.string(), {
443
+ }).option("stage", z5.string(), {
397
444
  default: "",
398
445
  description: "only for tasks in these stages, comma separated, or `any`: backlog, ready, queued, running"
399
- }).option("lane", z4.string(), {
446
+ }).option("lane", z5.string(), {
400
447
  default: "",
401
448
  description: "only for tasks in these lanes, by number, comma separated, or `any`"
402
- }).option("every", z4.string(), {
449
+ }).option("every", z5.string(), {
403
450
  default: "",
404
451
  description: "run every so often, as 30m, 2h or 1d"
405
- }).option("cron", z4.string(), {
452
+ }).option("cron", z5.string(), {
406
453
  default: "",
407
454
  description: `run on a five-field cron line, in this machine's time, as "0 9 * * 1-5"`
408
- }).option("agent", z4.string(), {
455
+ }).option("agent", z5.string(), {
409
456
  default: "",
410
457
  description: "the agent to run, as `splates agents` names it"
411
- }).option("model", z4.string(), {
458
+ }).option("model", z5.string(), {
412
459
  default: "",
413
460
  description: "its model, with --agent (default: the agent's own)"
414
- }).option("effort", z4.string(), {
461
+ }).option("effort", z5.string(), {
415
462
  default: "",
416
463
  description: "its thinking level, with --agent (default: the agent's own)"
417
- }).option("prompt", z4.string(), {
464
+ }).option("prompt", z5.string(), {
418
465
  default: "",
419
466
  description: "what the agent is told after the event"
420
- }).option("run", z4.string(), {
467
+ }).option("run", z5.string(), {
421
468
  default: "",
422
469
  description: "a shell command to run in place of an agent"
423
470
  }).action(async (opts, ctx) => {
424
471
  const { id, agent, model, effort, ...change } = opts;
425
472
  const pick = await assigneeIn(ctx, agent, model, effort);
426
473
  if (pick === null) {
427
- fail4("usage", "an agent automation needs an agent: name one, or --run", {});
474
+ fail5("usage", "an agent automation needs an agent: name one, or --run", {});
428
475
  }
429
476
  await hookEdit({ hooks: storeOf(ctx), log: ctx.log }, id, {
430
477
  ...change,
431
478
  ...pick === undefined ? {} : { pick }
432
479
  });
433
480
  });
434
- splatesHook.command("remove").use(repository3({ needs: "tasks" })).description("take away an automation, by its number in `splates hook list`, or every one on an event").arg("id", z4.string(), {
481
+ splatesHook.command("remove").use(repository3({ needs: "tasks" })).description("take away an automation, by its number in `splates hook list`, or every one on an event").arg("id", z5.string(), {
435
482
  description: "its number, or a task event, as task.created"
436
483
  }).action((opts, ctx) => hookRemove({ hooks: storeOf(ctx), log: ctx.log }, opts.id));
437
- splatesHook.command("pause").use(repository3({ needs: "tasks" })).description("stop an automation running, keeping it to resume").arg("id", z4.string(), { description: "its number in `splates hook list`" }).action((opts, ctx) => hookPause({ hooks: storeOf(ctx), log: ctx.log }, opts.id, true));
438
- splatesHook.command("resume").use(repository3({ needs: "tasks" })).description("let a paused automation run again").arg("id", z4.string(), { description: "its number in `splates hook list`" }).action((opts, ctx) => hookPause({ hooks: storeOf(ctx), log: ctx.log }, opts.id, false));
439
- splatesHook.command("run").use(repository3({ needs: "tasks" })).description("run an automation now, as the Automations page's Run now does, its schedule left as it was: one on a schedule, the groomer or the notes gardener, never one on another task event, which needs that event's tasks. `splates server` starts it at once, or as it starts when it is not running, and lists the run as `by hand`; it runs paused too").arg("id", z4.string(), {
440
- description: "its number in `splates hook list`, or `groomer` or `notes-gardener`"
484
+ splatesHook.command("pause").use(repository3({ needs: "tasks" })).description("stop an automation running, keeping it to resume").arg("id", z5.string(), { description: "its number in `splates hook list`" }).action((opts, ctx) => hookPause({ hooks: storeOf(ctx), log: ctx.log }, opts.id, true));
485
+ splatesHook.command("resume").use(repository3({ needs: "tasks" })).description("let a paused automation run again").arg("id", z5.string(), { description: "its number in `splates hook list`" }).action((opts, ctx) => hookPause({ hooks: storeOf(ctx), log: ctx.log }, opts.id, false));
486
+ splatesHook.command("run").use(repository3({ needs: "tasks" })).description("run an automation now, as the Automations page's Run now does, its schedule left as it was: one on a schedule or the groomer, never one on another task event, which needs that event's tasks. `splates server` starts it at once, or as it starts when it is not running, and lists the run as `by hand`; it runs paused too").arg("id", z5.string(), {
487
+ description: "its number in `splates hook list`, or `groomer`"
441
488
  }).action((opts, ctx) => hookRun({ hooks: storeOf(ctx), log: ctx.log }, opts.id));
442
- splatesHook.command("groomer").use(repository3({ needs: "tasks" })).description("turn on the project's groomer, built-in automation `groomer`, or set the agent it runs as, as the Automations page does: on each task deferred to the backlog, and the backlog as `splates server` starts, a conversation of its own per run, archived as the run ends, organizes each task in order and marks it ready, never doing it. `--off` turns it off, retiring its conversations").option("agent", z4.string(), {
489
+ splatesHook.command("groomer").use(repository3({ needs: "tasks" })).description("turn on the project's groomer, built-in automation `groomer`, or set the agent it runs as, as the Automations page does: on each task deferred to the backlog, and the backlog as `splates server` starts, a conversation of its own per run, archived as the run ends, organizes each task in order and marks it ready, never doing it. `--off` turns it off, retiring its conversations").option("agent", z5.string(), {
443
490
  default: "",
444
491
  description: "the agent it runs as, as `splates agents` names it"
445
- }).option("model", z4.string(), {
492
+ }).option("model", z5.string(), {
446
493
  default: "",
447
494
  description: "its model (default: the agent's own)"
448
- }).option("effort", z4.string(), {
495
+ }).option("effort", z5.string(), {
449
496
  default: "",
450
497
  description: "its thinking level (default: the agent's own)"
451
- }).option("off", z4.boolean(), {
498
+ }).option("off", z5.boolean(), {
452
499
  default: false,
453
500
  description: "turn it off"
454
501
  }).action(async (opts, ctx) => {
455
502
  const pick = await assigneeIn(ctx, opts.agent, opts.model, opts.effort);
456
503
  if (opts.off === (pick != null)) {
457
- fail4("usage", "say how it runs with --agent, or turn it off with --off", {});
504
+ fail5("usage", "say how it runs with --agent, or turn it off with --off", {});
458
505
  }
459
506
  await hookGroomer({ hooks: storeOf(ctx), log: ctx.log }, pick ?? null);
460
507
  });
461
- splatesHook.command("gardener").use(repository3({ needs: "tasks" })).description("turn on the notes gardener, built-in automation `notes-gardener`, or set the agent it runs as and its schedule, as the Automations page does: on each run, an agent reads each project note no open task names by its path, checks it against the code and the other notes, and drafts the fixes in one task, `notes-garden`: rewriting what went stale, merging notes that say one thing, splitting one that says several, settling a contradiction the code settles and sharpening descriptions, then asks you to review the change rather than merge it, asking about any contradiction the code does not settle. No run starts while `notes-garden` is open. Off by default, since each run reads code for every note on your tokens; it runs every 7d until you set `--every` or `--cron`. `--off` turns it off").option("agent", z4.string(), {
462
- default: "",
463
- description: "the agent it runs as, as `splates agents` names it"
464
- }).option("model", z4.string(), {
465
- default: "",
466
- description: "its model (default: the agent's own)"
467
- }).option("effort", z4.string(), {
468
- default: "",
469
- description: "its thinking level (default: the agent's own)"
470
- }).option("every", z4.string(), {
471
- default: "",
472
- description: "run every so often, as 1d (default: as it was, or 7d)"
473
- }).option("cron", z4.string(), {
474
- default: "",
475
- description: `run on a five-field cron line, in this machine's time, as "0 9 * * 1"`
476
- }).option("off", z4.boolean(), {
477
- default: false,
478
- description: "turn it off"
479
- }).action(async (opts, ctx) => {
480
- const pick = await assigneeIn(ctx, opts.agent, opts.model, opts.effort);
481
- if (opts.off === (pick != null)) {
482
- fail4("usage", "say how it runs with --agent, or turn it off with --off", {});
483
- }
484
- await hookGardener({ hooks: storeOf(ctx), log: ctx.log }, pick ?? null, {
485
- every: opts.every,
486
- cron: opts.cron
487
- });
488
- });
489
508
  function triggerOf(opts) {
490
509
  const given = [opts.event, opts.every, opts.cron].filter((each) => each !== undefined && each !== "");
491
510
  if (given.length !== 1) {
492
- fail4("usage", "say what sets it off: a task event, --every or --cron, one of them", {});
511
+ fail5("usage", "say what sets it off: a task event, --every or --cron, one of them", {});
493
512
  }
494
513
  if (opts.event === undefined || opts.event === "") {
495
514
  if (opts.stage !== "" || opts.lane !== "") {
496
- fail4("usage", "--stage and --lane narrow a task event, not a schedule", {});
515
+ fail5("usage", "--stage and --lane narrow a task event, not a schedule", {});
497
516
  }
498
517
  return opts.every !== "" ? { every: opts.every } : { cron: opts.cron };
499
518
  }
@@ -527,25 +546,25 @@ import { refuseGroomer } from "@stringsync/splates-server/conversations";
527
546
  import { UNTRIAGED } from "@stringsync/splates-server/plans";
528
547
  import { refuseAgentQueue } from "@stringsync/splates-server/queue";
529
548
  import { repository as repository4 } from "@stringsync/splates-server/records";
530
- import { cli as cli4 } from "webappwiz/cmd";
531
- import { z as z5 } from "zod";
532
- var laneNumber = z5.union([
533
- z5.literal("backlog").transform(() => BACKLOG_LANE2),
534
- z5.coerce.number().int().nonnegative()
549
+ import { cli as cli5 } from "webappwiz/cmd";
550
+ import { z as z6 } from "zod";
551
+ var laneNumber = z6.union([
552
+ z6.literal("backlog").transform(() => BACKLOG_LANE2),
553
+ z6.coerce.number().int().nonnegative()
535
554
  ]);
536
- var splatesLane = cli4("lane").description("lanes: named groups of tasks, for you to see related work together on the board; queue one whole with `splates lane queue`. Backlog (lane 0) is permanent and holds unmatched deferred work. Untriaged is groomed into lanes and cannot be queued whole");
537
- splatesLane.command("list").use(repository4({ needs: "read" })).description("every lane: its name, its queued tasks in order, the started tasks on it, its next task, and what blocks that in other lanes").option("json", z5.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => laneList(ctx, { json: opts.json }));
538
- splatesLane.command("show").use(repository4({ needs: "read" })).description("one lane step by step, and what its agent does next: start the next task, or wait for the one blocking it in another lane to land first; `untriaged` lists the tasks in no lane, to place in lanes").arg("lane", z5.union([z5.literal(UNTRIAGED).transform(() => null), laneNumber], {
555
+ var splatesLane = cli5("lane").description("lanes: named groups of tasks, for you to see related work together on the board; queue one whole with `splates lane queue`. Backlog (lane 0) is permanent and holds unmatched deferred work. Untriaged is groomed into lanes and cannot be queued whole");
556
+ splatesLane.command("list").use(repository4({ needs: "read" })).description("every lane: its name, its queued tasks in order, the started tasks on it, its next task, and what blocks that in other lanes").option("json", z6.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => laneList(ctx, { json: opts.json }));
557
+ splatesLane.command("show").use(repository4({ needs: "read" })).description("one lane step by step, and what its agent does next: start the next task, or wait for the one blocking it in another lane to land first; `untriaged` lists the tasks in no lane, to place in lanes").arg("lane", z6.union([z6.literal(UNTRIAGED).transform(() => null), laneNumber], {
539
558
  error: "pass a lane's number, backlog, or untriaged"
540
- }), { description: "lane number, `backlog`, or `untriaged`" }).option("json", z5.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => laneShow(ctx, opts.lane, { json: opts.json }));
541
- splatesLane.command("add").use(repository4({ needs: "tasks" })).description("start an empty lane, after the others").arg("name", z5.string(), {
559
+ }), { description: "lane number, `backlog`, or `untriaged`" }).option("json", z6.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => laneShow(ctx, opts.lane, { json: opts.json }));
560
+ splatesLane.command("add").use(repository4({ needs: "tasks" })).description("start an empty lane, after the others").arg("name", z6.string(), {
542
561
  description: "what its tasks are for, like `Licensing`"
543
562
  }).action(async (opts, ctx) => {
544
563
  await ctx.journal.record("lane add", null, () => laneAdd(ctx, opts.name), (lane) => ({ lane: lane.id, name: lane.name }));
545
564
  });
546
565
  splatesLane.command("update").use(repository4({ needs: "tasks" })).description("rename a lane, or move it among the others").arg("lane", laneNumber, {
547
566
  description: "lane number"
548
- }).option("name", z5.string().optional(), {
567
+ }).option("name", z6.string().optional(), {
549
568
  description: "what it is called from now on"
550
569
  }).option("position", position, {
551
570
  description: "where to move it among the lanes, 1 the first column on the board; the others close up around it"
@@ -582,18 +601,6 @@ splatesLane.command("remove").use(repository4({ needs: "tasks" })).description("
582
601
  return recordLaneRemove(ctx.journal, ctx.tasks, opts.lane, () => laneRemove(ctx, opts.lane, { emptyOnly }));
583
602
  });
584
603
 
585
- // notes.ts
586
- import { loadConfig } from "@stringsync/splates-server/config";
587
- import { Notes } from "@stringsync/splates-server/notes";
588
- async function notes(ctx) {
589
- const root = ctx.project === undefined ? (await ctx.git.run(ctx.ps.cwd(), "rev-parse", "--show-toplevel")).stdout.trim() : ctx.git.root;
590
- const config = await loadConfig(root, { fs: ctx.fs, git: ctx.git });
591
- const found = await new Notes(ctx.fs).list(root, config);
592
- for (const note of found) {
593
- ctx.log.info(note.description);
594
- }
595
- }
596
-
597
604
  // projects.ts
598
605
  import {
599
606
  grantReach,
@@ -601,11 +608,11 @@ import {
601
608
  REACHES
602
609
  } from "@stringsync/splates-server/projects";
603
610
  import { repository as repository5 } from "@stringsync/splates-server/records";
604
- import { cli as cli5 } from "webappwiz/cmd";
605
- import { z as z6 } from "zod";
606
- var splatesProjects = cli5("projects").description("the other splates projects on this machine, and what agents of each may do in the others through splates: `read` their tasks and lanes; add tasks to their untriaged lane with `queue`, as every project may until granted otherwise; or run their queue, lanes and hooks with `tasks`. Any repo command runs on another project as `splates --project <slug> <command>`, refused where it needs more than that project granted").fallback("list");
607
- splatesProjects.command("list").use(repository5({ needs: "read" })).description("print each other project: its slug, its root, what it may do here, and what this one may do there").option("json", z6.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => listReaches(ctx, { json: opts.json }));
608
- splatesProjects.command("grant").use(repository5({ needs: "own" })).description("let agents of another project do as much as `read`, `queue` or `tasks` here, each including those before it; the user's call, so an agent runs it only when the user asks it to").arg("project", z6.string(), { description: "the other project's slug" }).arg("reach", z6.enum(REACHES), {
611
+ import { cli as cli6 } from "webappwiz/cmd";
612
+ import { z as z7 } from "zod";
613
+ var splatesProjects = cli6("projects").description("the other splates projects on this machine, and what agents of each may do in the others through splates: `read` their tasks and lanes; add tasks to their untriaged lane with `queue`, as every project may until granted otherwise; or run their queue, lanes and hooks with `tasks`. Any repo command runs on another project as `splates --project <slug> <command>`, refused where it needs more than that project granted").fallback("list");
614
+ splatesProjects.command("list").use(repository5({ needs: "read" })).description("print each other project: its slug, its root, what it may do here, and what this one may do there").option("json", z7.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => listReaches(ctx, { json: opts.json }));
615
+ splatesProjects.command("grant").use(repository5({ needs: "own" })).description("let agents of another project do as much as `read`, `queue` or `tasks` here, each including those before it; the user's call, refused an agent, which asks with a `## \uD83D\uDD10 Permission: grant <this> <reach> to <other>` card instead").arg("project", z7.string(), { description: "the other project's slug" }).arg("reach", z7.enum(REACHES), {
609
616
  description: "read, queue or tasks"
610
617
  }).action((opts, ctx) => grantReach(ctx, opts.project, opts.reach));
611
618
 
@@ -621,31 +628,31 @@ import {
621
628
  } from "@stringsync/splates-server/queue";
622
629
  import { repository as repository6 } from "@stringsync/splates-server/records";
623
630
  import { setQueuePaused } from "@stringsync/splates-server/workers";
624
- import { cli as cli6 } from "webappwiz/cmd";
625
- import { z as z7 } from "zod";
626
- var splatesQueue = cli6("queue").description("the queue: the tasks a worker of `splates server` starts as soon as it has a slot free, each after every task it is blocked by, otherwise in the order you set. Only you queue a task, from the backlog or groomed, and an agent asks you to with a queue card; one that names no model runs on the roster's first agent");
627
- splatesQueue.command("list").use(repository6({ needs: "read" })).description("the queue in the order workers start it, with each task's model and what it waits on: the open tasks it is blocked by, or those removed without merging").option("json", z7.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => queueList(ctx, { json: opts.json }));
628
- splatesQueue.command("add").use(repository6({ needs: "tasks" })).description("queue tasks for a worker to start once nothing they are blocked by is open; refused for one already under way").rest("ids", z7.coerce.number().int().positive(), {
631
+ import { cli as cli7 } from "webappwiz/cmd";
632
+ import { z as z8 } from "zod";
633
+ var splatesQueue = cli7("queue").description("the queue: the tasks `splates server` starts on its worker as soon as nothing they are blocked by is open, without capping how many run, otherwise in the order you set. Only you queue a task, from the backlog or groomed, and an agent asks you to with a queue card; one that names no model uses Auto pick to choose its agent and model");
634
+ splatesQueue.command("list").use(repository6({ needs: "read" })).description("the queue in the order workers start it, with each task's model and what it waits on: the open tasks it is blocked by, or those removed without merging").option("json", z8.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => queueList(ctx, { json: opts.json }));
635
+ splatesQueue.command("add").use(repository6({ needs: "tasks" })).description("queue tasks for a worker to start once nothing they are blocked by is open; refused for one already under way").rest("ids", z8.coerce.number().int().positive(), {
629
636
  description: "task numbers"
630
637
  }).action(async (opts, ctx) => {
631
638
  refuseAgentQueue2(ctx.service, "queue add");
632
639
  await refuseGroomer2(ctx.service, "queue add");
633
640
  await ctx.journal.record("queue add", await here(ctx), () => queueAdd(ctx, opts.ids), (queued) => ({ tasks: tasksNamed(queued) }));
634
641
  });
635
- splatesQueue.command("remove").use(repository6({ needs: "tasks" })).description("take tasks out of the queue, back to ready when they name a model, else the backlog; the tasks stay").rest("ids", z7.coerce.number().int().positive(), {
642
+ splatesQueue.command("remove").use(repository6({ needs: "tasks" })).description("take tasks out of the queue, back to ready when they name a model, else the backlog; the tasks stay").rest("ids", z8.coerce.number().int().positive(), {
636
643
  description: "task numbers"
637
644
  }).action(async (opts, ctx) => {
638
645
  refuseAgentQueue2(ctx.service, "queue remove");
639
646
  await ctx.journal.record("queue remove", await here(ctx), () => queueRemove(ctx, opts.ids), (removed) => ({ tasks: tasksNamed(removed) }));
640
647
  });
641
- splatesQueue.command("move").use(repository6({ needs: "tasks" })).description("move a queued task to another place in the queue, from 1 for the next to start; what it is blocked by still goes first. Then lists the queue").arg("id", z7.coerce.number().int().positive(), { description: "task number" }).arg("position", z7.coerce.number().int().positive(), {
648
+ splatesQueue.command("move").use(repository6({ needs: "tasks" })).description("move a queued task to another place in the queue, from 1 for the next to start; what it is blocked by still goes first. Then lists the queue").arg("id", z8.coerce.number().int().positive(), { description: "task number" }).arg("position", z8.coerce.number().int().positive(), {
642
649
  description: "where in the queue to put it, from 1"
643
650
  }).action(async (opts, ctx) => {
644
651
  refuseAgentQueue2(ctx.service, "queue move");
645
652
  return queueMove(ctx, opts.id, opts.position);
646
653
  });
647
654
  for (const command of ["pause", "resume"]) {
648
- splatesQueue.command(command).use(repository6({ needs: "tasks" })).description(command === "pause" ? "pause automatic starts across the server, leaving running agents to finish and the person's own agents able to start; only the person may pause" : "resume automatic starts across the server; only the person may resume").option("dev", z7.string(), {
655
+ splatesQueue.command(command).use(repository6({ needs: "tasks" })).description(command === "pause" ? "pause automatic starts across the server, leaving running agents to finish and the person's own agents able to start; only the person may pause" : "resume automatic starts across the server; only the person may resume").option("dev", z8.string(), {
649
656
  default: "",
650
657
  description: "the `splates server` to ask, a URL such as http://localhost:4310; by default the one from SPLATES_DEV"
651
658
  }).action(async (opts, ctx) => {
@@ -663,9 +670,9 @@ for (const command of ["pause", "resume"]) {
663
670
 
664
671
  // replica.ts
665
672
  import { dirname as dirname2 } from "node:path";
666
- import { loadConfig as loadConfig2 } from "@stringsync/splates-server/config";
673
+ import { loadConfig } from "@stringsync/splates-server/config";
667
674
  import { SYSTEM_STORES as SYSTEM_STORES2 } from "@stringsync/splates-server/creds";
668
- import { fail as fail5 } from "@stringsync/splates-server/exit";
675
+ import { fail as fail6 } from "@stringsync/splates-server/exit";
669
676
  import { Projects } from "@stringsync/splates-server/projects";
670
677
  import {
671
678
  FORMAT,
@@ -680,11 +687,11 @@ import {
680
687
  restoreFromReplica,
681
688
  statusLines
682
689
  } from "@stringsync/splates-server/replica";
683
- import { cli as cli7 } from "webappwiz/cmd";
690
+ import { cli as cli8 } from "webappwiz/cmd";
684
691
  import { Credentials } from "webappwiz/creds";
685
- import { z as z8 } from "zod";
686
- var splatesReplica = cli7("replica").description("the copy of this project's records `splates server` keeps a few seconds behind every change, in the bucket or folder `replica` names in splates.config.ts");
687
- splatesReplica.command("status").use(repository7({ needs: "read" })).description("print where the replica is, when it last held every change, how many changes wait to reach it and since when, and why it is failing if it is; as of the server's last batch, so it reads the same with the server stopped").option("json", z8.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => {
692
+ import { z as z9 } from "zod";
693
+ var splatesReplica = cli8("replica").description("the copy of this project's records `splates server` keeps a few seconds behind every change, in the bucket or folder `replica` names in splates.config.ts");
694
+ splatesReplica.command("status").use(repository7({ needs: "read" })).description("print where the replica is, when it last held every change, how many changes wait to reach it and since when, and why it is failing if it is; as of the server's last batch, so it reads the same with the server stopped").option("json", z9.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => {
688
695
  const status = await Replicator.recorded(ctx.storage);
689
696
  if (opts.json) {
690
697
  ctx.log.info(JSON.stringify(status));
@@ -693,7 +700,7 @@ splatesReplica.command("status").use(repository7({ needs: "read" })).description
693
700
  ctx.log.info(statusLines(status, ctx.config.replica !== null, Date.now()).join(`
694
701
  `));
695
702
  });
696
- splatesReplica.command("restore").description("with `splates server` stopped, rebuild this project's records from the replica `replica` names in splates.config.ts, adopting its origin so the server started on them replicates on into it: how a server moves to another machine or comes back after its box is lost. Stops when the project already has records here, unless --force leaves them aside as state.db.moved. The roster and Auto pick rules are not in the replica; set them up again. A bucket's REPLICA_ACCESS_KEY and REPLICA_SECRET_KEY must be filled on this machine first").option("force", z8.boolean(), {
703
+ splatesReplica.command("restore").description("with `splates server` stopped, rebuild this project's records from the replica `replica` names in splates.config.ts, adopting its origin so the server started on them replicates on into it: how a server moves to another machine or comes back after its box is lost. Stops when the project already has records here, unless --force leaves them aside as state.db.moved. The roster and Auto pick rules are not in the replica; set them up again. A bucket's REPLICA_ACCESS_KEY and REPLICA_SECRET_KEY must be filled on this machine first").option("force", z9.boolean(), {
697
704
  default: false,
698
705
  description: "restore over the records here, leaving them aside"
699
706
  }).action(async (opts, ctx) => {
@@ -705,9 +712,9 @@ splatesReplica.command("restore").description("with `splates server` stopped, re
705
712
  await stores.project(root),
706
713
  stores.device
707
714
  ]);
708
- const config = await loadConfig2(root, { fs: ctx.fs });
715
+ const config = await loadConfig(root, { fs: ctx.fs });
709
716
  if (config.replica === null) {
710
- fail5("usage", "no replica: name one as `replica` in splates.config.ts first");
717
+ fail6("usage", "no replica: name one as `replica` in splates.config.ts first");
711
718
  }
712
719
  const replica = openReplica(config.replica, {
713
720
  root,
@@ -734,10 +741,10 @@ splatesReplica.command("restore").description("with `splates server` stopped, re
734
741
  ].join(`
735
742
  `));
736
743
  } catch (error) {
737
- fail5("usage", error instanceof Error ? error.message : String(error));
744
+ fail6("usage", error instanceof Error ? error.message : String(error));
738
745
  }
739
746
  });
740
- splatesReplica.command("retire").use(repository7({ needs: "own" })).description("after the server moved elsewhere (stop it and check `splates replica status` says caught up first), leave this project's records aside as state.db.moved, so a splates run here finds no stale records and says where the server went").arg("to", z8.string(), {
747
+ splatesReplica.command("retire").use(repository7({ needs: "own" })).description("after the server moved elsewhere (stop it and check `splates replica status` says caught up first), leave this project's records aside as state.db.moved, so a splates run here finds no stale records and says where the server went").arg("to", z9.string(), {
741
748
  description: "where the server went, as `droplet.example.com`"
742
749
  }).action(async (opts, ctx) => {
743
750
  const home = new ProjectHome(ctx.git.root, ctx);
@@ -755,7 +762,7 @@ async function mainTree(ctx) {
755
762
  "--git-common-dir"
756
763
  ]);
757
764
  if (exitCode !== 0) {
758
- fail5("usage", `not a git repository: ${ctx.ps.cwd()}`);
765
+ fail6("usage", `not a git repository: ${ctx.ps.cwd()}`);
759
766
  }
760
767
  return dirname2(stdout.trim());
761
768
  }
@@ -765,34 +772,34 @@ import {
765
772
  DEFAULT_PORT,
766
773
  serveDev
767
774
  } from "@stringsync/splates-server/server";
768
- import { cli as cli8 } from "webappwiz/cmd";
769
- import { z as z9 } from "zod";
770
- var splatesServer = cli8("server").description("the splates server: run it on this machine").fallback("run");
771
- splatesServer.command("run").description("serve every splates project on this machine as a web page, from wherever it is run: the page picks the project it shows, adds one by its path, running `splates init` there, and shows its queued and started tasks, where queued tasks can be added, updated, reordered or removed; it brings each project's records forward as it opens it. A server that crashes starts again after 2s, then 4s, 8s and 16s, and is left down after five crashes in a row that each came within a minute of its start").option("port", z9.coerce.number(), {
775
+ import { cli as cli9 } from "webappwiz/cmd";
776
+ import { z as z10 } from "zod";
777
+ var splatesServer = cli9("server").description("the splates server: run it on this machine").fallback("run");
778
+ splatesServer.command("run").description("serve every splates project on this machine as a web page, from wherever it is run: the page picks the project it shows, adds one by its path, running `splates init` there, and shows its queued and started tasks, where queued tasks can be added, updated, reordered or removed; it brings each project's records forward as it opens it. A server that crashes starts again after 2s, then 4s, 8s and 16s, and is left down after five crashes in a row that each came within a minute of its start").option("port", z10.coerce.number(), {
772
779
  default: DEFAULT_PORT,
773
780
  description: "port to listen on, or the next open one above it"
774
- }).option("allow-hosts", z9.string(), {
781
+ }).option("allow-hosts", z10.string(), {
775
782
  default: "",
776
783
  description: "comma-separated host names the page may also be reached by, like a tunnel's; put Cloudflare Access in front of them with --access-team and --access-aud"
777
- }).option("access-team", z9.string(), {
784
+ }).option("access-team", z10.string(), {
778
785
  default: "",
779
786
  description: "the Cloudflare Access team in front of --allow-hosts, as in <team>.cloudflareaccess.com; with --access-aud, those hosts answer only to a verified Access token, and may then use conversations and terminals"
780
- }).option("access-aud", z9.string(), {
787
+ }).option("access-aud", z10.string(), {
781
788
  default: "",
782
789
  description: "the AUD tag of that Access application"
783
- }).option("tunnel", z9.string(), {
790
+ }).option("tunnel", z10.string(), {
784
791
  default: "",
785
792
  description: "the public host name of a tunnel to run, with cloudflared or the tunnel provider named by splates.config.ts in the repository it is run in, if any, which is allowed as a host too; refused without --access-team and --access-aud, so a tunnel never exposes the page unauthenticated. With none of the tunnel and Access flags, the tunnel `splates tunnel setup` saved runs, if any"
786
- }).option("tunnel-name", z9.string(), {
793
+ }).option("tunnel-name", z10.string(), {
787
794
  default: "",
788
795
  description: "the named tunnel cloudflared runs for --tunnel, by default the one its own config names; not with a tunnel provider of splates.config.ts"
789
- }).option("ports-host", z9.string(), {
796
+ }).option("ports-host", z10.string(), {
790
797
  default: "",
791
798
  description: "the wildcard base host to serve task ports under, saved for this machine so a bare `splates server` serves it too: each port a task's processes listen on opens at https://<task>-<port>.<base>, to whoever Cloudflare Access lets in, and is refused without Access; routing *.<base> here, its DNS and TLS, are yours. `none` forgets it, leaving each task port forwarded to a port on this machine's loopback alone"
792
- }).option("keepalive", z9.boolean(), {
799
+ }).option("keepalive", z10.boolean(), {
793
800
  default: false,
794
801
  description: "keep a machine left to serve awake while the server runs: on macOS with `caffeinate`, on power or idle, though closing a laptop's lid still sleeps it; on Linux with a systemd-inhibit lock on sleep and idle, which the server serves on without, saying so, where systemd is missing or refuses it. Refused on other platforms"
795
- }).option("slot", z9.boolean(), {
802
+ }).option("slot", z10.boolean(), {
796
803
  default: false,
797
804
  description: "serve this checkout's splates at https://<word>.<zone>, a slot of its own on the zone `splates tunnel setup` uses, behind the same Access policy, so several run side by side and a phone opens each by name: a task's worktree keeps its word, as fox, until the task merges or is removed, which stops its server; run again, it stops the tree's last server first. Refused in a splates repository's main tree, which every agent shares. A slot keeps its own projects in ~/.splates/slots/<word>, apart from yours, starting with a throwaway project there; not with the tunnel and Access flags"
798
805
  }).action((opts, ctx) => serveDev(ctx, opts));
@@ -800,7 +807,7 @@ splatesServer.command("run").description("serve every splates project on this ma
800
807
  // setup.ts
801
808
  import { dirname as dirname3 } from "node:path";
802
809
  import { SYSTEM_STORES as SYSTEM_STORES3 } from "@stringsync/splates-server/creds";
803
- import { fail as fail6 } from "@stringsync/splates-server/exit";
810
+ import { fail as fail7 } from "@stringsync/splates-server/exit";
804
811
  import { splatesHome as splatesHome2 } from "@stringsync/splates-server/models";
805
812
  import { Projects as Projects2 } from "@stringsync/splates-server/projects";
806
813
  import {
@@ -813,18 +820,18 @@ import {
813
820
  setupProjectAt
814
821
  } from "@stringsync/splates-server/setup";
815
822
  import { fetchWorkers } from "@stringsync/splates-server/workers";
816
- import { color as color2 } from "webappwiz/log";
823
+ import { color as color3 } from "webappwiz/log";
817
824
  async function setup(deps, opts) {
818
825
  if (opts.step === "" && (opts.skip || opts.unskip || opts.undo)) {
819
- fail6("usage", "--skip and --unskip name a step, --undo an area", {});
826
+ fail7("usage", "--skip and --unskip name a step, --undo an area", {});
820
827
  }
821
828
  if (opts.undo && (opts.skip || opts.unskip)) {
822
- fail6("usage", "--undo names an area, not a step to skip", {
829
+ fail7("usage", "--undo names an area, not a step to skip", {
823
830
  area: opts.step
824
831
  });
825
832
  }
826
833
  if (opts.skip && opts.unskip) {
827
- fail6("usage", "--skip or --unskip, not both", { step: opts.step });
834
+ fail7("usage", "--skip or --unskip, not both", { step: opts.step });
828
835
  }
829
836
  const machine = await setupOf(deps);
830
837
  const { log } = deps;
@@ -897,10 +904,10 @@ async function repoRoot(ps) {
897
904
  return exitCode === 0 ? dirname3(stdout.trim()) : null;
898
905
  }
899
906
  var MARKS = {
900
- done: color2.green("✓"),
901
- skipped: color2.dim("-"),
902
- ready: color2.yellow("○"),
903
- waiting: color2.dim("○")
907
+ done: color3.green("✓"),
908
+ skipped: color3.dim("-"),
909
+ ready: color3.yellow("○"),
910
+ waiting: color3.dim("○")
904
911
  };
905
912
  function printProgress(log, progress) {
906
913
  for (const area of progress.areas) {
@@ -908,7 +915,7 @@ function printProgress(log, progress) {
908
915
  `${area.done}/${area.steps.length} done`,
909
916
  area.skipped > 0 ? `${area.skipped} skipped` : ""
910
917
  ].filter(Boolean);
911
- log.info(`${color2.bold(area.title)} ${color2.dim(counts.join(", "))}`);
918
+ log.info(`${color3.bold(area.title)} ${color3.dim(counts.join(", "))}`);
912
919
  for (const step of area.steps) {
913
920
  const notes = [
914
921
  step.optional ? "optional" : "",
@@ -916,24 +923,24 @@ function printProgress(log, progress) {
916
923
  step.problem ?? "",
917
924
  step.warning === null ? "" : "to put right"
918
925
  ].filter(Boolean);
919
- log.info(` ${MARKS[step.status]} ${step.id} ${step.title}${notes.length > 0 ? color2.dim(` ${notes.join("; ")}`) : ""}`);
926
+ log.info(` ${MARKS[step.status]} ${step.id} ${step.title}${notes.length > 0 ? color3.dim(` ${notes.join("; ")}`) : ""}`);
920
927
  }
921
928
  }
922
929
  const steps = progress.areas.flatMap((area) => area.steps);
923
930
  const next = steps.find((step) => step.id === progress.next);
924
931
  for (const step of steps.filter((each) => each.warning !== null)) {
925
- log.info(`${color2.yellow("put right:")} ${step.title} ${color2.dim(`splates setup ${step.id}`)}`);
932
+ log.info(`${color3.yellow("put right:")} ${step.title} ${color3.dim(`splates setup ${step.id}`)}`);
926
933
  }
927
- log.info(next === undefined ? color2.green("nothing left to set up") : `next: ${next.title} ${color2.dim(`splates setup ${next.id}`)}`);
934
+ log.info(next === undefined ? color3.green("nothing left to set up") : `next: ${next.title} ${color3.dim(`splates setup ${next.id}`)}`);
928
935
  }
929
936
  function printStep(log, step) {
930
- log.info(`${color2.bold(step.title)} ${color2.dim(`${step.status}${step.optional ? ", optional" : ""}`)}`);
937
+ log.info(`${color3.bold(step.title)} ${color3.dim(`${step.status}${step.optional ? ", optional" : ""}`)}`);
931
938
  log.info(step.why);
932
939
  if (step.detail !== null) {
933
940
  log.info(step.detail);
934
941
  }
935
942
  if (step.warning !== null) {
936
- log.info(`${color2.yellow("put right:")} ${step.warning}`);
943
+ log.info(`${color3.yellow("put right:")} ${step.warning}`);
937
944
  }
938
945
  log.info(`you: ${step.user}`);
939
946
  log.info(`agent: ${step.agent}`);
@@ -941,18 +948,18 @@ function printStep(log, step) {
941
948
  log.info(`after: ${step.after.join(", ")}`);
942
949
  }
943
950
  if (step.problem !== null) {
944
- log.info(`${color2.red("problem:")} ${step.problem}`);
951
+ log.info(`${color3.red("problem:")} ${step.problem}`);
945
952
  }
946
953
  }
947
954
  function printUndo(log, plan) {
948
- log.info(`${color2.bold(`Undo ${plan.title}`)} ${color2.dim(plan.area)}`);
955
+ log.info(`${color3.bold(`Undo ${plan.title}`)} ${color3.dim(plan.area)}`);
949
956
  if (plan.refused !== null) {
950
- log.info(`${color2.red("refused:")} ${plan.refused}`);
957
+ log.info(`${color3.red("refused:")} ${plan.refused}`);
951
958
  return;
952
959
  }
953
960
  for (const step of plan.steps) {
954
- const started = step.done ? "" : ` ${color2.yellow("started")}`;
955
- log.info(` ${step.title} ${color2.dim(step.id)}${started}`);
961
+ const started = step.done ? "" : ` ${color3.yellow("started")}`;
962
+ log.info(` ${step.title} ${color3.dim(step.id)}${started}`);
956
963
  for (const line of step.removes) {
957
964
  log.info(` removes ${line}`);
958
965
  }
@@ -961,20 +968,20 @@ function printUndo(log, plan) {
961
968
  }
962
969
  }
963
970
  for (const cred of plan.creds) {
964
- log.info(cred.keptFor === null ? ` forgets ${cred.name}` : ` keeps ${cred.name} ${color2.dim(`${cred.keptFor} still uses it`)}`);
971
+ log.info(cred.keptFor === null ? ` forgets ${cred.name}` : ` keeps ${cred.name} ${color3.dim(`${cred.keptFor} still uses it`)}`);
965
972
  }
966
973
  if (plan.confirm !== null) {
967
974
  log.info(` the page asks for ${plan.confirm} to be typed`);
968
975
  }
969
- log.info(color2.dim("the Setup page's Undo does it, after the user confirms"));
976
+ log.info(color3.dim("the Setup page's Undo does it, after the user confirms"));
970
977
  }
971
978
 
972
979
  // slots.ts
973
980
  import { Slots } from "@stringsync/splates-server/ports";
974
- import { cli as cli9 } from "webappwiz/cmd";
975
- import { z as z10 } from "zod";
976
- var splatesSlots = cli9("slots").description("the words `splates server --slot` hands out on this machine, each held by the tree that claimed it: a task's worktree holds its word until the task merges or is removed, which stops its server and gives the word back");
977
- splatesSlots.command("list").description("print each claimed word, the tree holding it, its server's pid while one runs, and whether it is orphaned: its tree gone, or a splates repository's main tree, which every agent shares").option("json", z10.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => {
981
+ import { cli as cli10 } from "webappwiz/cmd";
982
+ import { z as z11 } from "zod";
983
+ var splatesSlots = cli10("slots").description("the words `splates server --slot` hands out on this machine, each held by the tree that claimed it: a task's worktree holds its word until the task merges or is removed, which stops its server and gives the word back");
984
+ splatesSlots.command("list").description("print each claimed word, the tree holding it, its server's pid while one runs, and whether it is orphaned: its tree gone, or a splates repository's main tree, which every agent shares").option("json", z11.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => {
978
985
  const held = await Slots.ofMachine(ctx).status();
979
986
  if (opts.json) {
980
987
  ctx.log.info(JSON.stringify(held.map(({ slot, tree, pid, orphaned }) => ({
@@ -1019,17 +1026,17 @@ import {
1019
1026
  } from "@stringsync/splates-server/board";
1020
1027
  import { commaList as commaList4 } from "@stringsync/splates-server/comma-list";
1021
1028
  import { refuseGroomer as refuseGroomer3 } from "@stringsync/splates-server/conversations";
1022
- import { fail as fail7 } from "@stringsync/splates-server/exit";
1029
+ import { fail as fail8 } from "@stringsync/splates-server/exit";
1023
1030
  import { repository as repository8 } from "@stringsync/splates-server/records";
1024
1031
  import {
1025
1032
  DEFAULT_TIMEOUT as DEFAULT_TIMEOUT2,
1026
1033
  taskWait,
1027
1034
  untilAfter as untilAfter2
1028
1035
  } from "@stringsync/splates-server/tasks";
1029
- import { cli as cli10 } from "webappwiz/cmd";
1036
+ import { cli as cli11 } from "webappwiz/cmd";
1030
1037
  import { Duration as Duration2 } from "webappwiz/time";
1031
- import { z as z11 } from "zod";
1032
- var splatesTask = cli10("task").description("the numbered tasks: defer work for later instead of growing the task you are in, and `splates add <slug> --task <n>` starts one; `merge` recommends the next one when a task lands. The person puts tasks in `splates queue`, for workers to start");
1038
+ import { z as z12 } from "zod";
1039
+ var splatesTask = cli11("task").description("the numbered tasks: defer work for later instead of growing the task you are in, and `splates add <slug> --task <n>` starts one; `merge` recommends the next one when a task lands. The person puts tasks in `splates queue`, for workers to start");
1033
1040
  async function projectFrom(ctx) {
1034
1041
  if (ctx.elsewhere === null) {
1035
1042
  return;
@@ -1037,34 +1044,34 @@ async function projectFrom(ctx) {
1037
1044
  return (await ctx.projects.list()).find((each) => each.root === ctx.git.root)?.slug;
1038
1045
  }
1039
1046
  function adding(name, description, ready) {
1040
- splatesTask.command(name).use(repository8({ needs: "queue" })).description(description).arg("subject", z11.string(), {
1047
+ splatesTask.command(name).use(repository8({ needs: "queue" })).description(description).arg("subject", z12.string(), {
1041
1048
  description: "what is left to do, in a line"
1042
- }).arg("text", z11.string(), {
1049
+ }).arg("text", z12.string(), {
1043
1050
  default: "",
1044
1051
  description: "whatever more there is to say about it, in markdown"
1045
- }).option("slug", z11.string(), {
1052
+ }).option("slug", z12.string(), {
1046
1053
  description: "a short name for the task that takes it (lowercase-with-dashes), naming its branch, worktree and conversation: a few words for what it changes, not the subject cut short"
1047
1054
  }).option("position", position, {
1048
1055
  description: "where it goes in the list, 1 at the top, pushing those from there down (default: the bottom)"
1049
- }).repeated("file", z11.string(), {
1056
+ }).repeated("file", z12.string(), {
1050
1057
  description: "files to attach, comma separated or the flag repeated: copied under .git/splates/tasks/<n>/"
1051
- }).repeated("blocked-by", z11.string(), {
1058
+ }).repeated("blocked-by", z12.string(), {
1052
1059
  description: "numbers of the tasks blocking it, comma separated or the flag repeated: each has to merge before this one starts"
1053
- }).option("lane", z11.string(), {
1060
+ }).option("lane", z12.string(), {
1054
1061
  default: "",
1055
1062
  description: "the lane it goes in, at the bottom: a lane's number, `backlog` for the permanent catch-all, or `new` for one of its own (default: none, untriaged)"
1056
- }).option("agent", z11.string(), {
1063
+ }).option("agent", z12.string(), {
1057
1064
  default: "",
1058
1065
  description: "the agent on the roster (`splates agents`) that should work it, which Start on the `splates server` board offers first"
1059
- }).option("model", z11.string(), {
1066
+ }).option("model", z12.string(), {
1060
1067
  default: "",
1061
1068
  description: "with --agent: the model, by its id or name on the roster"
1062
- }).option("effort", z11.string(), {
1069
+ }).option("effort", z12.string(), {
1063
1070
  default: "",
1064
1071
  description: "with --agent: the thinking level, one the agent offers (default: the agent's own)"
1065
1072
  }).action(async (opts, ctx) => {
1066
1073
  if (ctx.elsewhere !== null && opts.lane !== "") {
1067
- fail7("not_granted", "a task from another project goes in its untriaged lane, for whoever orchestrates it there to place: leave --lane off", { subject: opts.subject });
1074
+ fail8("not_granted", "a task from another project goes in its untriaged lane, for whoever orchestrates it there to place: leave --lane off", { subject: opts.subject });
1068
1075
  }
1069
1076
  const from = await here(ctx);
1070
1077
  const assignee = await assigneeIn(ctx, opts.agent, opts.model, opts.effort);
@@ -1089,10 +1096,10 @@ function adding(name, description, ready) {
1089
1096
  }
1090
1097
  adding("add", "queue a task to do later and move on, groomed, so it goes in ready: you checked no task already asks it, and linked what blocks it; run from a worktree, it records the task it came up in", true);
1091
1098
  adding("defer", "queue a task as it is, rough, in the backlog for whoever grooms it: to check for one that already asks it, link what blocks it, and mark it ready; run from a worktree, it records the task it came up in", false);
1092
- splatesTask.command("list").use(repository8({ needs: "read" })).description("every queued task, top of the list first, with the task it came from and the started one that took it up").option("json", z11.boolean(), { default: false, description: "emit JSON" }).option("open", z11.boolean(), {
1099
+ splatesTask.command("list").use(repository8({ needs: "read" })).description("every queued task, top of the list first, with the task it came from and the started one that took it up").option("json", z12.boolean(), { default: false, description: "emit JSON" }).option("open", z12.boolean(), {
1093
1100
  default: false,
1094
1101
  description: "only tasks no started one has taken up: the ones free to pick up"
1095
- }).option("ready", z11.boolean(), {
1102
+ }).option("ready", z12.boolean(), {
1096
1103
  default: false,
1097
1104
  description: "only tasks free to start now: none has taken them, and nothing blocking them is still open"
1098
1105
  }).action(async (opts, ctx) => taskList(ctx, {
@@ -1101,54 +1108,54 @@ splatesTask.command("list").use(repository8({ needs: "read" })).description("eve
1101
1108
  ready: opts.ready,
1102
1109
  project: await projectFrom(ctx)
1103
1110
  }));
1104
- splatesTask.command("show").use(repository8({ needs: "read" })).description("one task in full: where it stands, its files, and its detail").arg("id", z11.coerce.number().int().positive(), { description: "task number" }).option("json", z11.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => taskShow(ctx, opts.id, {
1111
+ splatesTask.command("show").use(repository8({ needs: "read" })).description("one task in full: where it stands, its files, and its detail").arg("id", z12.coerce.number().int().positive(), { description: "task number" }).option("json", z12.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => taskShow(ctx, opts.id, {
1105
1112
  json: opts.json,
1106
1113
  project: await projectFrom(ctx),
1107
1114
  others: (await ctx.projects.list()).filter((project) => project.root !== ctx.git.root).map((project) => project.slug)
1108
1115
  }));
1109
- splatesTask.command("update").use(repository8({ needs: "tasks" })).description("say what a queued task is in other words, move it up or down the list, or attach and drop files; it keeps its number").arg("id", z11.coerce.number().int().positive(), { description: "task number" }).arg("text", z11.string(), {
1116
+ splatesTask.command("update").use(repository8({ needs: "tasks" })).description("say what a queued task is in other words, move it up or down the list, or attach and drop files; it keeps its number").arg("id", z12.coerce.number().int().positive(), { description: "task number" }).arg("text", z12.string(), {
1110
1117
  default: "",
1111
1118
  description: "whatever more there is to say about it, in markdown; leave out to keep the detail"
1112
- }).option("subject", z11.string(), {
1119
+ }).option("subject", z12.string(), {
1113
1120
  default: "",
1114
1121
  description: "what is left to do, in a line; leave out to keep it"
1115
- }).option("slug", z11.string(), {
1122
+ }).option("slug", z12.string(), {
1116
1123
  default: "",
1117
1124
  description: "a new name for the task that takes it (lowercase-with-dashes); leave out to keep it"
1118
1125
  }).option("position", position, {
1119
1126
  description: "where to move it in the list, 1 at the top; the others close up around it"
1120
- }).repeated("file", z11.string(), {
1127
+ }).repeated("file", z12.string(), {
1121
1128
  description: "files to attach, comma separated or the flag repeated"
1122
- }).repeated("remove-file", z11.string(), {
1129
+ }).repeated("remove-file", z12.string(), {
1123
1130
  description: "attached files to drop, comma separated or the flag repeated, by path or stored name"
1124
- }).repeated("blocked-by", z11.string(), {
1131
+ }).repeated("blocked-by", z12.string(), {
1125
1132
  description: "numbers of more tasks blocking it, comma separated or the flag repeated: each has to merge before this one starts; refused when that makes a loop"
1126
- }).repeated("remove-blocked-by", z11.string(), {
1133
+ }).repeated("remove-blocked-by", z12.string(), {
1127
1134
  description: "numbers of tasks no longer blocking it, comma separated or the flag repeated"
1128
- }).option("lane", z11.string(), {
1135
+ }).option("lane", z12.string(), {
1129
1136
  default: "",
1130
1137
  description: "move it to a lane, at the bottom: a lane's number, `backlog` for the permanent catch-all, `new` for one of its own, or `none` for untriaged"
1131
- }).option("agent", z11.string(), {
1138
+ }).option("agent", z12.string(), {
1132
1139
  default: "",
1133
1140
  description: "the agent on the roster (`splates agents`) that should work it, which Start on the `splates server` board offers first, or `none`"
1134
- }).option("model", z11.string(), {
1141
+ }).option("model", z12.string(), {
1135
1142
  default: "",
1136
1143
  description: "with --agent: the model, by its id or name on the roster"
1137
- }).option("effort", z11.string(), {
1144
+ }).option("effort", z12.string(), {
1138
1145
  default: "",
1139
1146
  description: "with --agent: the thinking level, one the agent offers (default: the agent's own)"
1140
- }).option("ready", z11.boolean(), {
1147
+ }).option("ready", z12.boolean(), {
1141
1148
  default: false,
1142
1149
  description: "mark it groomed, moving it from the backlog to ready: no task already asks it, and what blocks it is linked"
1143
- }).option("manual", z11.boolean(), {
1150
+ }).option("manual", z12.boolean(), {
1144
1151
  default: false,
1145
1152
  description: "queue it by hand only: a queued lane passes over it, and it starts once you `splates queue add` it"
1146
- }).option("auto", z11.boolean(), {
1153
+ }).option("auto", z12.boolean(), {
1147
1154
  default: false,
1148
1155
  description: "let a queued lane queue it again, undoing --manual"
1149
1156
  }).action(async (opts, ctx) => {
1150
1157
  if (opts.manual && opts.auto) {
1151
- fail7("usage", "pass --manual or --auto, not both", { number: opts.id });
1158
+ fail8("usage", "pass --manual or --auto, not both", { number: opts.id });
1152
1159
  }
1153
1160
  await refuseGroomer3(ctx.service, "task update", await ctx.tasks.find(opts.id));
1154
1161
  const assignee = await assigneeIn(ctx, opts.agent, opts.model, opts.effort);
@@ -1167,11 +1174,11 @@ splatesTask.command("update").use(repository8({ needs: "tasks" })).description("
1167
1174
  manual: opts.manual ? true : opts.auto ? false : undefined
1168
1175
  }));
1169
1176
  });
1170
- splatesTask.command("remove").use(repository8({ needs: "tasks" })).description("drop a queued task that was done some other way or no longer applies").arg("id", z11.coerce.number().int().positive(), { description: "task number" }).action(async (opts, ctx) => {
1177
+ splatesTask.command("remove").use(repository8({ needs: "tasks" })).description("drop a queued task that was done some other way or no longer applies").arg("id", z12.coerce.number().int().positive(), { description: "task number" }).action(async (opts, ctx) => {
1171
1178
  await refuseGroomer3(ctx.service, "task remove", await ctx.tasks.find(opts.id));
1172
1179
  await ctx.journal.record("task remove", null, () => taskRemove(ctx, opts.id), (removed) => ({ tasks: tasksNamed2([removed]) }));
1173
1180
  });
1174
- splatesTask.command("wait").use(repository8({ needs: "read" })).description("block until a task leaves the queue, which it does when the task that took it merges: for a task blocked by one another lane is on; ends early when that task waits on a person to answer its questions, and gives up with `timeout`").arg("id", z11.coerce.number().int().positive(), { description: "task number" }).option("timeout-secs", z11.coerce.number(), {
1181
+ splatesTask.command("wait").use(repository8({ needs: "read" })).description("block until a task leaves the queue, which it does when the task that took it merges: for a task blocked by one another lane is on; ends early when that task waits on a person to answer its questions, and gives up with `timeout`").arg("id", z12.coerce.number().int().positive(), { description: "task number" }).option("timeout-secs", z12.coerce.number(), {
1175
1182
  default: DEFAULT_TIMEOUT2.secs,
1176
1183
  description: "how long to wait before giving up"
1177
1184
  }).action(async (opts, ctx) => {
@@ -1192,18 +1199,18 @@ import {
1192
1199
  tunnelRemove,
1193
1200
  tunnelSetup
1194
1201
  } from "@stringsync/splates-server/tunnel";
1195
- import { cli as cli11 } from "webappwiz/cmd";
1196
- import { z as z12 } from "zod";
1197
- var splatesTunnel = cli11("tunnel").description("reach `splates server` from elsewhere: a Cloudflare tunnel with Cloudflare Access in front, set up once per machine");
1198
- splatesTunnel.command("setup").use(repository9({ needs: "own" })).description(`make a Cloudflare tunnel to \`splates server\`, the DNS record routing <host> to it, and an Access application letting in only --email, through the API with the token kept as the cred ${API_TOKEN}, then save them for this machine, so a bare \`splates server\` comes up tunneled; run again, it changes only what is not so already. Without that token it requests it on the Creds page of \`splates server\`, with how to make it, and exits \`requested\`: \`splates creds wait ${API_TOKEN}\`, then run it again`).arg("host", z12.string(), {
1202
+ import { cli as cli12 } from "webappwiz/cmd";
1203
+ import { z as z13 } from "zod";
1204
+ var splatesTunnel = cli12("tunnel").description("reach `splates server` from elsewhere: a Cloudflare tunnel with Cloudflare Access in front, set up once per machine");
1205
+ splatesTunnel.command("setup").use(repository9({ needs: "own" })).description(`make a Cloudflare tunnel to \`splates server\`, the DNS record routing <host> to it, and an Access application letting in only --email, through the API with the token kept as the cred ${API_TOKEN}, then save them for this machine, so a bare \`splates server\` comes up tunneled; run again, it changes only what is not so already. Without that token it requests it on the Creds page of \`splates server\`, with how to make it, and exits \`requested\`: \`splates creds wait ${API_TOKEN}\`, then run it again`).arg("host", z13.string(), {
1199
1206
  description: "the public host name, in a zone of the account, as in splates.example.com"
1200
- }).option("email", z12.string(), {
1207
+ }).option("email", z13.string(), {
1201
1208
  default: "",
1202
1209
  description: "comma-separated email addresses Access lets in"
1203
- }).option("name", z12.string(), {
1210
+ }).option("name", z13.string(), {
1204
1211
  default: "splates",
1205
1212
  description: "what the tunnel, the Access application and its policy are called"
1206
- }).option("team", z12.string(), {
1213
+ }).option("team", z13.string(), {
1207
1214
  default: "",
1208
1215
  description: "the Zero Trust team to make, as in <team>.cloudflareaccess.com, when the account has none yet"
1209
1216
  }).action(async (opts, ctx) => ctx.journal.record("tunnel setup", null, async () => tunnelSetup({
@@ -1214,7 +1221,7 @@ splatesTunnel.command("setup").use(repository9({ needs: "own" })).description(`m
1214
1221
  log: ctx.log,
1215
1222
  fetch: ctx.fetch
1216
1223
  })));
1217
- splatesTunnel.command("status").description("print the tunnel `splates tunnel setup` saved for this machine, which a bare `splates server` runs, and whether its token is in the system's secret store").option("json", z12.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => {
1224
+ splatesTunnel.command("status").description("print the tunnel `splates tunnel setup` saved for this machine, which a bare `splates server` runs, and whether its token is in the system's secret store").option("json", z13.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => {
1218
1225
  const saved = savedTunnelOf(ctx);
1219
1226
  const record = await saved.read();
1220
1227
  const token = record === null ? false : await saved.token() !== undefined;
@@ -1252,23 +1259,22 @@ import {
1252
1259
  FileLogger,
1253
1260
  listWorkers,
1254
1261
  removeWorker,
1255
- setWorkerSlots,
1256
1262
  workerRun
1257
1263
  } from "@stringsync/splates-server/workers";
1258
- import { cli as cli13 } from "webappwiz/cmd";
1264
+ import { cli as cli14 } from "webappwiz/cmd";
1259
1265
  import { CompositeLogger } from "webappwiz/log";
1260
- import { z as z14 } from "zod";
1266
+ import { z as z15 } from "zod";
1261
1267
 
1262
1268
  // waiting.ts
1263
1269
  import { listQueue, moveQueued } from "@stringsync/splates-server/workers";
1264
- import { cli as cli12 } from "webappwiz/cmd";
1265
- import { z as z13 } from "zod";
1270
+ import { cli as cli13 } from "webappwiz/cmd";
1271
+ import { z as z14 } from "zod";
1266
1272
  var DEV = {
1267
1273
  default: "",
1268
1274
  description: "the `splates server` to ask, a URL such as http://localhost:4310; by default the one an agent runs under, from SPLATES_DEV"
1269
1275
  };
1270
- var splatesWaiting = cli12("queue").description("the agents waiting for a slot on a worker of the running `splates server`: each one another agent started (with `splates add --agent`, or a lane's orchestrator handing out tasks), started or resumed while every worker it can run on is full, waits here, first come, first served, and starts once a slot frees, as when an agent left idle 30 minutes stops; none is ever failed for waiting. An agent you start waits only while no worker it can run on is up, as with this machine's own removed or none on the platform its project requires");
1271
- splatesWaiting.command("list").description("list the agents waiting for a slot, first in line first: position, project, task, conversation, how long each has waited and why (no free slot, no worker up, or a platform no worker runs, as `needs macOS`)").option("dev", z13.string(), DEV).option("json", z13.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => listQueue({
1276
+ var splatesWaiting = cli13("queue").description("the agents waiting to start on a worker of the running `splates server`: nothing caps how many agents run, so one waits only while what splates starts on its own (the queue, automations, hooks, the groomer) is paused, or while no worker it can run on is up, first come, first served; none is ever failed for waiting. An agent you start waits only while no worker is up");
1277
+ splatesWaiting.command("list").description("list the agents waiting to start, first in line first: position, project, task, conversation, how long each has waited and why (paused, no worker up, or worker draining)").option("dev", z14.string(), DEV).option("json", z14.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => listQueue({
1272
1278
  dev: opts.dev,
1273
1279
  json: opts.json,
1274
1280
  log: ctx.log,
@@ -1276,11 +1282,11 @@ splatesWaiting.command("list").description("list the agents waiting for a slot,
1276
1282
  ps: ctx.ps,
1277
1283
  send: ctx.fetch
1278
1284
  }));
1279
- splatesWaiting.command("move").description("move an agent waiting for a slot to another place in line, from 1 for the next to start, then list the queue").arg("which", z13.string(), {
1285
+ splatesWaiting.command("move").description("move an agent waiting to start to another place in line, from 1 for the next to start, then list the queue").arg("which", z14.string(), {
1280
1286
  description: "the task it works, or its conversation's id"
1281
- }).arg("position", z13.coerce.number().int(), {
1287
+ }).arg("position", z14.coerce.number().int(), {
1282
1288
  description: "where in line to put it, from 1"
1283
- }).option("dev", z13.string(), DEV).option("json", z13.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => moveQueued({
1289
+ }).option("dev", z14.string(), DEV).option("json", z14.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => moveQueued({
1284
1290
  which: opts.which,
1285
1291
  position: opts.position,
1286
1292
  dev: opts.dev,
@@ -1292,34 +1298,33 @@ splatesWaiting.command("move").description("move an agent waiting for a slot to
1292
1298
  }));
1293
1299
 
1294
1300
  // worker.ts
1295
- var splatesWorker = cli13("worker").description("this machine’s worker, which runs agents, terminals and process reads for `splates server` over its Unix socket");
1296
- splatesWorker.command("run").description("run this machine’s worker until stopped, connecting over its private Unix socket and reconnecting when the server restarts").option("server", z14.string(), {
1301
+ var splatesWorker = cli14("worker").description("this machine’s worker, which runs agents, terminals and process reads for `splates server` over its Unix socket");
1302
+ splatesWorker.command("run").description("run this machine’s worker until stopped, connecting over its private Unix socket and reconnecting when the server restarts").option("server", z15.string(), {
1297
1303
  default: "",
1298
1304
  description: "the `splates server` to dial: unix:<path>; by default this machine's"
1299
- }).option("coordinator", z14.string(), {
1305
+ }).option("coordinator", z15.string(), {
1300
1306
  default: "",
1301
1307
  description: "the old name for --server, still read for workers installed before it, and passed to a build older than it"
1302
- }).option("slots", z14.coerce.number().int().min(0), {
1308
+ }).option("slots", z15.coerce.number().int().min(0), {
1303
1309
  default: 0,
1304
- description: "how many tasks this worker takes at once; by default one for each core and 4 GB of memory it has, whichever is fewer, as a VM such as WSL2 sees them"
1305
- }).option("id", z14.literal("local"), {
1310
+ description: "ignored: nothing caps how many agents a worker runs; still read, since a `splates server` started before it was dropped passes it"
1311
+ }).option("id", z15.literal("local"), {
1306
1312
  default: "local",
1307
1313
  description: "what the server knows this worker by, kept across restarts so it picks up where it was; by default local"
1308
- }).option("quiet", z14.boolean(), {
1314
+ }).option("quiet", z15.boolean(), {
1309
1315
  default: false,
1310
1316
  description: "say only what went wrong, not each time it joins or loses its server, as the worker `splates server` starts beside itself does; all of it still goes to worker.log in ~/.splates, the one before it kept as worker.log.1"
1311
1317
  }).action((opts, ctx) => workerRun({
1312
1318
  coordinator: opts.server || opts.coordinator,
1313
- slots: opts.slots,
1314
1319
  id: opts.id,
1315
1320
  log: opts.quiet ? quietLog(ctx) : ctx.log,
1316
1321
  ps: ctx.ps,
1317
1322
  fs: ctx.fs
1318
1323
  }));
1319
- splatesWorker.command("list").description("list the workers the running `splates server` has: platform, build, protocol, slots used and total, health with when it was last heard, and tasks, then any it refused for speaking another protocol").option("dev", z14.string(), {
1324
+ splatesWorker.command("list").description("list the workers the running `splates server` has: platform, build, protocol, health with when it was last heard, and tasks, then any it refused for speaking another protocol").option("dev", z15.string(), {
1320
1325
  default: "",
1321
1326
  description: "the `splates server` to ask, a URL such as http://localhost:4310; by default the one an agent runs under, from SPLATES_DEV"
1322
- }).option("json", z14.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => listWorkers({
1327
+ }).option("json", z15.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => listWorkers({
1323
1328
  dev: opts.dev,
1324
1329
  json: opts.json,
1325
1330
  log: ctx.log,
@@ -1331,12 +1336,12 @@ var dev = {
1331
1336
  default: "",
1332
1337
  description: "the `splates server` to ask, a URL such as http://localhost:4310; by default the one an agent runs under, from SPLATES_DEV"
1333
1338
  };
1334
- splatesWorker.command("drain").description("take no new agents on a worker, letting those on it run to their end, but for one asking you something in a task, which saves its task and lets its slot go, its question kept for you to answer and carry it on elsewhere; --undo takes new ones again").arg("id", z14.string(), {
1339
+ splatesWorker.command("drain").description("take no new agents on a worker, letting those on it run to their end, but for one asking you something in a task, which saves its task and lets its slot go, its question kept for you to answer and carry it on elsewhere; --undo takes new ones again").arg("id", z15.string(), {
1335
1340
  description: "the worker, as `splates worker list` names it"
1336
- }).option("undo", z14.boolean(), {
1341
+ }).option("undo", z15.boolean(), {
1337
1342
  default: false,
1338
1343
  description: "take new agents again"
1339
- }).option("dev", z14.string(), dev).action((opts, ctx) => drainWorker({
1344
+ }).option("dev", z15.string(), dev).action((opts, ctx) => drainWorker({
1340
1345
  id: opts.id,
1341
1346
  undo: opts.undo,
1342
1347
  dev: opts.dev,
@@ -1345,25 +1350,12 @@ splatesWorker.command("drain").description("take no new agents on a worker, lett
1345
1350
  ps: ctx.ps,
1346
1351
  send: ctx.fetch
1347
1352
  }));
1348
- splatesWorker.command("slots").description("set how many agents a worker runs at once, from now on, over what it started with").arg("id", z14.string(), {
1349
- description: "the worker, as `splates worker list` names it"
1350
- }).arg("slots", z14.coerce.number().int().min(1), {
1351
- description: "how many at once"
1352
- }).option("dev", z14.string(), dev).action((opts, ctx) => setWorkerSlots({
1353
- id: opts.id,
1354
- slots: opts.slots,
1355
- dev: opts.dev,
1356
- log: ctx.log,
1357
- fs: ctx.fs,
1358
- ps: ctx.ps,
1359
- send: ctx.fetch
1360
- }));
1361
- splatesWorker.command("remove").description("stop this machine’s worker taking agents, letting its agents run to their end; resume it with `splates worker drain local --undo`").arg("id", z14.string(), {
1353
+ splatesWorker.command("remove").description("stop this machine’s worker taking agents, letting its agents run to their end; resume it with `splates worker drain local --undo`").arg("id", z15.string(), {
1362
1354
  description: "the worker, as `splates worker list` names it"
1363
- }).option("force", z14.boolean(), {
1355
+ }).option("force", z15.boolean(), {
1364
1356
  default: false,
1365
1357
  description: "end the agents on it now rather than wait for them"
1366
- }).option("dev", z14.string(), dev).action((opts, ctx) => removeWorker({
1358
+ }).option("dev", z15.string(), dev).action((opts, ctx) => removeWorker({
1367
1359
  id: opts.id,
1368
1360
  force: opts.force,
1369
1361
  dev: opts.dev,
@@ -1395,8 +1387,7 @@ function projectArgs(argv) {
1395
1387
  }
1396
1388
  return { argv };
1397
1389
  }
1398
- var splates = cli14("splates").use(exits());
1399
- splates.command("notes").use(repository10({ needs: "read" })).description("print each project note's frontmatter description, one per line; notesDir in splates.config.ts defaults to notes; an absent directory prints nothing").action((_opts, ctx) => notes(ctx));
1390
+ var splates = cli15("splates").use(exits());
1400
1391
  splates.command("init").use(repository10({ creates: true, needs: "own" })).description("make this repository a splates project: set up its records under .git/splates when it has none, install the splates skill of this release with `bunx skills add`, and register it in ~/.splates/projects.json, so `splates server` serves it beside every other; run again, it changes only what is not so already").action((_opts, ctx) => init(ctx));
1401
1392
  splates.command("migrate").description(`move this repository's records under .git/splates to this splates's format, as \`splates server\` does as it starts: backed up first, the steps run with ${MIGRATION_LIMIT.human()} to finish, and the backup put back if one fails or runs over, with why kept in .git/splates-migration.json; it tries again past a failure kept there, which the server and every other command leave alone. Run it only while no older \`splates server\` serves the repository, which would refuse the records once moved; \`splates update\` restarts one first`).action(async (_opts, ctx) => {
1402
1393
  const format = await migrateRecords(ctx);
@@ -1417,41 +1408,41 @@ splates.command("update").use(recordsFirst()).use(repository10({ needs: "own" })
1417
1408
  }
1418
1409
  });
1419
1410
  splates.command("forget").use(repository10({ needs: "own" })).description("take this repository off ~/.splates/projects.json, so `splates server` no longer offers it; its records stay, and `splates init` brings it back").action((_opts, ctx) => forget(ctx));
1420
- splates.command("add").use(repository10({ needs: "tasks" })).description("start a task: a new one, or with --task one from the queue; create branch task/<slug>, a worktree at ../<repo>-splates.noindex/<slug> and a state record").arg("slug", z15.string(), {
1411
+ splates.command("add").use(repository10({ needs: "tasks" })).description("start a task: a new one, or with --task one from the queue; create branch task/<slug>, a worktree at ../<repo>-splates.noindex/<slug> and a state record").arg("slug", z16.string(), {
1421
1412
  description: "the task's name, for its branch and worktree (lowercase-with-dashes)"
1422
- }).option("base", z15.string(), {
1413
+ }).option("base", z16.string(), {
1423
1414
  default: "",
1424
1415
  description: "branch this task starts from and merges onto (default: trunk); `task/<other>` stacks this task on that one and lands the work in its worktree"
1425
- }).option("task", z15.string(), {
1416
+ }).option("task", z16.string(), {
1426
1417
  default: "",
1427
1418
  description: "start this queued task by number: its words become the plan's Goal, merging removes it, and removing this task puts it back where it was"
1428
- }).option("goal", z15.string(), {
1419
+ }).option("goal", z16.string(), {
1429
1420
  default: "",
1430
1421
  description: "one or two lines on what done means, written under `## Goal` in the task's SPLATES.md; required unless --task seeds the Goal"
1431
- }).option("agent", z15.string(), {
1422
+ }).option("agent", z16.string(), {
1432
1423
  default: "",
1433
1424
  description: "how `splates server`'s queue hands the task to an agent on the roster (`splates agents`), which it starts in the worktree; refused from an agent, which starts no agents and queues work with `splates task add` instead"
1434
- }).option("model", z15.string(), {
1425
+ }).option("model", z16.string(), {
1435
1426
  default: "",
1436
1427
  description: "with --agent: the model it works on, by its id or name on the roster (default: the agent's own)"
1437
- }).option("effort", z15.string(), {
1428
+ }).option("effort", z16.string(), {
1438
1429
  default: "",
1439
1430
  description: "with --agent: the thinking level it works at, one the agent offers (default: the agent's own)"
1440
- }).option("prompt", z15.string(), {
1431
+ }).option("prompt", z16.string(), {
1441
1432
  default: "",
1442
1433
  description: "with --agent: what to tell the agent beyond claiming the task"
1443
- }).option("held", z15.string(), {
1434
+ }).option("held", z16.string(), {
1444
1435
  default: "",
1445
1436
  description: "with --agent: the conversation `splates server`'s queue held for the queued task, which takes the task rather than a new one"
1446
1437
  }).action(async (opts, ctx) => {
1447
1438
  await refuseGroomer4(ctx.service, "add");
1448
1439
  if (opts.agent === "" && (opts.model !== "" || opts.effort !== "" || opts.prompt !== "" || opts.held !== "")) {
1449
- fail8("usage", "--model, --effort, --prompt and --held go with --agent", {
1440
+ fail9("usage", "--model, --effort, --prompt and --held go with --agent", {
1450
1441
  task: opts.slug
1451
1442
  });
1452
1443
  }
1453
1444
  if (opts.goal.trim() === "" && opts.task === "") {
1454
- fail8("usage", `say what done means: \`splates add ${opts.slug} --goal "<one or two lines>"\`, or start queued tasks with --task`, { task: opts.slug });
1445
+ fail9("usage", `say what done means: \`splates add ${opts.slug} --goal "<one or two lines>"\`, or start queued tasks with --task`, { task: opts.slug });
1455
1446
  }
1456
1447
  const dispatch = opts.agent === "" ? undefined : {
1457
1448
  agent: opts.agent,
@@ -1485,11 +1476,11 @@ splates.command("add").use(repository10({ needs: "tasks" })).description("start
1485
1476
  await serveProject(ctx);
1486
1477
  }
1487
1478
  });
1488
- splates.command("claim").use(repository10({ needs: "own" })).description("resume an existing task: take ownership of its worktree and print its path, status and any half-finished rebase; refuses while another agent holds the lease, but takes a stale one silently, so `splates show` first if the tree may not be abandoned").arg("task", z15.string(), { description: "task name" }).action(async (opts, ctx) => {
1479
+ splates.command("claim").use(repository10({ needs: "own" })).description("resume an existing task: take ownership of its worktree and print its path, status and any half-finished rebase; refuses while another agent holds the lease, but takes a stale one silently, so `splates show` first if the tree may not be abandoned").arg("task", z16.string(), { description: "task name" }).action(async (opts, ctx) => {
1489
1480
  await ctx.journal.record("claim", opts.task, () => claim(ctx, opts.task), (claimed) => claimed);
1490
1481
  await follow(ctx, opts.task);
1491
1482
  });
1492
- splates.command("merge").use(repository10({ needs: "own" })).description("land this worktree's branch on its base (trunk unless created with --base): rebase onto it, run tests on the rebased code, fast-forward it in whichever worktree has it checked out, then discard the worktree, branch and record (linear history, never a merge commit, no flag to skip tests); requires committed work, refusing a dirty worktree").option("message", z15.string(), {
1483
+ splates.command("merge").use(repository10({ needs: "own" })).description("land this worktree's branch on its base (trunk unless created with --base): rebase onto it, run tests on the rebased code, fast-forward it in whichever worktree has it checked out, then discard the worktree, branch and record (linear history, never a merge commit, no flag to skip tests); requires committed work, refusing a dirty worktree").option("message", z16.string(), {
1493
1484
  default: "",
1494
1485
  description: "squash the task's commits into one with this message before landing, saying what the base gains in at least 5 characters"
1495
1486
  }).action(async (opts, ctx) => {
@@ -1504,39 +1495,39 @@ splates.command("merge").use(repository10({ needs: "own" })).description("land t
1504
1495
  await follow(ctx, null, { from: task });
1505
1496
  }
1506
1497
  });
1507
- splates.command("remove").use(repository10({ needs: "tasks" })).description("discard a task: worktree, branch and board tasks; cheap and encouraged, since redoing a task against current trunk often beats a hard rebase").arg("task", z15.string(), { description: "task name" }).option("force", z15.boolean(), {
1498
+ splates.command("remove").use(repository10({ needs: "tasks" })).description("discard a task: worktree, branch and board tasks; cheap and encouraged, since redoing a task against current trunk often beats a hard rebase").arg("task", z16.string(), { description: "task name" }).option("force", z16.boolean(), {
1508
1499
  default: false,
1509
1500
  description: "discard even when another agent holds the lease"
1510
- }).option("reason", z15.optional(z15.enum(REMOVE_REASONS)), {
1501
+ }).option("reason", z16.optional(z16.enum(REMOVE_REASONS)), {
1511
1502
  description: "why, for the Stats page: nothing (nothing to change), moot (no longer needed), superseded (done some other way), redo (better redone against current trunk), blocked (set aside until what it waits on lands, so back on the list)"
1512
1503
  }).action(async (opts, ctx) => {
1513
1504
  await ctx.journal.record("remove", opts.task, () => remove(ctx, opts.task, { force: opts.force, reason: opts.reason }), ({ released }) => ({ why: opts.reason ?? null, released }));
1514
1505
  await follow(ctx, null, { from: opts.task });
1515
1506
  });
1516
- splates.command("list").use(repository10({ needs: "read" })).description("list every started task: its number, name, status, lease (held: an agent is on it now; stale: gone quiet, normal for a task mid-edit; none), commits ahead of trunk, age; `splates task list` lists the queue").option("json", z15.boolean(), { default: false, description: "emit JSON" }).option("files", z15.boolean(), {
1507
+ splates.command("list").use(repository10({ needs: "read" })).description("list every started task: its number, name, status, lease (held: an agent is on it now; stale: gone quiet, normal for a task mid-edit; none), commits ahead of trunk, age; `splates task list` lists the queue").option("json", z16.boolean(), { default: false, description: "emit JSON" }).option("files", z16.boolean(), {
1517
1508
  default: false,
1518
1509
  description: "add each task's Goal and changed files (committed or not), to check for overlap before starting"
1519
1510
  }).action((opts, ctx) => list(ctx, { json: opts.json, files: opts.files }));
1520
- splates.command("show").use(repository10({ needs: "read" })).description("read one task without touching it: everything `list` shows for it, plus the SPLATES.md its agent left at the worktree root; takes no lease, so it cannot knock that agent off its own tree; for a task no longer live, the newest finished one of that name: how it ended, its base, the commit it landed as, its conversation and the SPLATES.md it ended on").arg("task", z15.string(), { description: "task name" }).option("json", z15.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => show(ctx, opts.task, { json: opts.json }));
1521
- splates.command("wait").use(repository10({ needs: "read" })).description("block until a task stops moving: waiting on a person to answer its questions, or gone (merged or removed), or broken; takes no lease, and gives up with `timeout` rather than waiting forever").arg("task", z15.string(), { description: "task name" }).option("timeout-secs", z15.coerce.number(), {
1511
+ splates.command("show").use(repository10({ needs: "read" })).description("read one task without touching it: everything `list` shows for it, plus the SPLATES.md its agent left at the worktree root; takes no lease, so it cannot knock that agent off its own tree; for a task no longer live, the newest finished one of that name: how it ended, its base, the commit it landed as, its conversation and the SPLATES.md it ended on").arg("task", z16.string(), { description: "task name" }).option("json", z16.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => show(ctx, opts.task, { json: opts.json }));
1512
+ splates.command("wait").use(repository10({ needs: "read" })).description("block until a task stops moving: waiting on a person to answer its questions, or gone (merged or removed), or broken; takes no lease, and gives up with `timeout` rather than waiting forever").arg("task", z16.string(), { description: "task name" }).option("timeout-secs", z16.coerce.number(), {
1522
1513
  default: DEFAULT_TIMEOUT3.secs,
1523
1514
  description: "how long to wait before giving up"
1524
1515
  }).action((opts, ctx) => {
1525
1516
  const timeout = Duration3.secs(opts["timeout-secs"]);
1526
1517
  return ctx.journal.waiting("wait", opts.task, { until: untilAfter3(timeout) }, () => wait(ctx, opts.task, { timeout }));
1527
1518
  });
1528
- splates.command("log").use(repository10({ needs: "read" })).description("show what has been done here recently: one line per add, claim, merge, remove, retry, queued task and more, with how it ended; outlives the tasks themselves").option("count", z15.coerce.number(), {
1519
+ splates.command("log").use(repository10({ needs: "read" })).description("show what has been done here recently: one line per add, claim, merge, remove, retry, queued task and more, with how it ended; outlives the tasks themselves").option("count", z16.coerce.number(), {
1529
1520
  default: DEFAULT_COUNT,
1530
1521
  description: "how many entries to show"
1531
- }).option("json", z15.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => showLog(ctx, { count: opts.count, json: opts.json }));
1522
+ }).option("json", z16.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => showLog(ctx, { count: opts.count, json: opts.json }));
1532
1523
  splates.mount("server", splatesServer);
1533
- splates.command("ports").description("list the ports tasks serve, found by the running `splates server` as an editor finds ports to forward: each port a process in a task's worktree, or under its agent, listens on, with its link through the ports host `splates server --ports-host` saved, behind Cloudflare Access, and its link on this machine; give the user both").arg("task", z15.string(), {
1524
+ splates.command("ports").description("list the ports tasks serve, found by the running `splates server` as an editor finds ports to forward: each port a process in a task's worktree, or under its agent, listens on, with its link through the ports host `splates server --ports-host` saved, behind Cloudflare Access, and its link on this machine; give the user both").arg("task", z16.string(), {
1534
1525
  default: "",
1535
1526
  description: "only this task's ports; every task's by default"
1536
- }).option("dev", z15.string(), {
1527
+ }).option("dev", z16.string(), {
1537
1528
  default: "",
1538
1529
  description: "the `splates server` to ask, a URL such as http://localhost:4310; by default the one an agent runs under, from SPLATES_DEV, else this machine's on its default port"
1539
- }).option("json", z15.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => portsLs({
1530
+ }).option("json", z16.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => portsLs({
1540
1531
  task: opts.task,
1541
1532
  dev: opts.dev,
1542
1533
  json: opts.json,
@@ -1544,41 +1535,41 @@ splates.command("ports").description("list the ports tasks serve, found by the r
1544
1535
  ps: ctx.ps,
1545
1536
  send: ctx.fetch
1546
1537
  }));
1547
- splates.command("agents").description("print the roster of agents splates can start, kept for every project in ~/.splates/agents.json: each agent's launch command and models, with what each model is good at and what it costs, and the thinking levels `--effort` takes, as the agent offered them when last started, for choosing whom to delegate to; writes the defaults (Claude, Codex) there first when there is none, and `splates server` starts conversations with those it lists").option("json", z15.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => agents({
1538
+ splates.command("agents").description("print the roster of agents splates can start, kept for every project in ~/.splates/agents.json: each agent's launch command and models, with what each model is good at and what it costs, and the thinking levels `--effort` takes, as the agent offered them when last started, for choosing whom to delegate to; writes the defaults (Claude, Codex) there first when there is none, and `splates server` starts conversations with those it lists").option("json", z16.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => agents({
1548
1539
  roster: rosterOf2(ctx),
1549
1540
  offered: await offeredOf(ctx).all(),
1550
1541
  log: ctx.log
1551
1542
  }, { json: opts.json }));
1552
- splates.command("setup").description("set splates up on this machine, one step at a time: with no step, each area's steps, checked from real state, and the one to do next; with a step, why it matters, the one thing the user does and what the agent does. Skips are kept in ~/.splates/setup.json, so a skipped step stops asking").arg("step", z15.string(), {
1543
+ splates.command("setup").description("set splates up on this machine, one step at a time: with no step, each area's steps, checked from real state, and the one to do next; with a step, why it matters, the one thing the user does and what the agent does. Skips are kept in ~/.splates/setup.json, so a skipped step stops asking").arg("step", z16.string(), {
1553
1544
  default: "",
1554
1545
  description: "a step's id; omit for every area's progress"
1555
- }).option("json", z15.boolean(), { default: false, description: "emit JSON" }).option("skip", z15.boolean(), {
1546
+ }).option("json", z16.boolean(), { default: false, description: "emit JSON" }).option("skip", z16.boolean(), {
1556
1547
  default: false,
1557
1548
  description: "with a step: stop asking for it"
1558
- }).option("unskip", z15.boolean(), {
1549
+ }).option("unskip", z16.boolean(), {
1559
1550
  default: false,
1560
1551
  description: "with a step: ask for it again"
1561
- }).option("undo", z15.boolean(), {
1552
+ }).option("undo", z16.boolean(), {
1562
1553
  default: false,
1563
1554
  description: "with an area's id in place of a step: print what undoing it would remove, the creds it forgets and keeps, and what only the user can remove; the Setup page's Undo does it, after the user confirms"
1564
1555
  }).action((opts, ctx) => setup(ctx, opts));
1565
- splates.command("path").use(repository10({ needs: "read" })).description("print a task's worktree path, or the main tree with no task; names another agent's tree without taking its lease").arg("task", z15.string(), {
1556
+ splates.command("path").use(repository10({ needs: "read" })).description("print a task's worktree path, or the main tree with no task; names another agent's tree without taking its lease").arg("task", z16.string(), {
1566
1557
  default: "",
1567
1558
  description: "task name; omit for the main tree"
1568
1559
  }).action((opts, ctx) => path(ctx, opts.task || undefined));
1569
1560
  splates.command("checkpoint").use(repository10({ needs: "own" })).description("save this task to the central remote (`remote` in splates.config.ts) before a stop: commit what is uncommitted as a `WIP: checkpoint` commit, to squash before merging, and push the branch as `splates/<task>`, so a worker that dies loses nothing; with no remote configured, leaves the tree as it is").action(async (_opts, ctx) => {
1570
1561
  await checkpoint(ctx, ctx.ps.cwd());
1571
1562
  });
1572
- splates.command("attach").use(repository10({ needs: "own" })).description("store screenshots and recordings for this task's questions to show: prints the path each was stored at, under .git/splates, which the `splates server` page shows inline; from a conversation (its $SPLATES_CONVERSATION, or --conversation) they stay with it past the task, otherwise they go when the task merges or is removed; those the task's final SPLATES.md shows are copied to keep with its finished record. A task holds at most attachmentCapacity bytes; with no files, lists what it holds. A file the user will come back for on its own is an artifact instead (`splates artifact add`)").arg("files", z15.string(), {
1563
+ splates.command("attach").use(repository10({ needs: "own" })).description("store screenshots and recordings for this task's questions to show: prints the path each was stored at, under .git/splates, which the `splates server` page shows inline; from a conversation (its $SPLATES_CONVERSATION, or --conversation) they stay with it past the task, otherwise they go when the task merges or is removed; those the task's final SPLATES.md shows are copied to keep with its finished record. A task holds at most attachmentCapacity bytes; with no files, lists what it holds. A file the user will come back for on its own is an artifact instead (`splates artifact add`)").arg("files", z16.string(), {
1573
1564
  default: "",
1574
1565
  description: "files to store, comma separated"
1575
- }).option("task", z15.string(), {
1566
+ }).option("task", z16.string(), {
1576
1567
  default: "",
1577
1568
  description: "task name, when run outside its worktree"
1578
- }).option("remove", z15.string(), {
1569
+ }).option("remove", z16.string(), {
1579
1570
  default: "",
1580
1571
  description: "files it holds to drop first, comma separated, by path or stored name"
1581
- }).option("conversation", z15.string(), {
1572
+ }).option("conversation", z16.string(), {
1582
1573
  default: "",
1583
1574
  description: "conversation to keep the files with, when $SPLATES_CONVERSATION is not set"
1584
1575
  }).action(async (opts, ctx) => ctx.journal.record("attach", opts.task || await here(ctx), () => attach(ctx, commaList5(opts.files), ctx.ps.cwd(), {
@@ -1586,8 +1577,8 @@ splates.command("attach").use(repository10({ needs: "own" })).description("store
1586
1577
  remove: commaList5(opts.remove),
1587
1578
  conversation: opts.conversation || undefined
1588
1579
  }), (stored) => ({ stored, removed: commaList5(opts.remove) })));
1589
- splates.command("retry").use(repository10({ needs: "own" })).description("give a task that spent its merge attempts another mergeRetryCount of them; the way out of `budget_exhausted` that is not remove and redo, for a person to run once they have seen the tree").arg("task", z15.string(), { description: "task name" }).action((opts, ctx) => recordRetry(ctx, opts.task));
1590
- splates.mount("artifact", splatesArtifact).mount("task", splatesTask).mount("lane", splatesLane).mount("projects", splatesProjects).mount("creds", splatesCreds).mount("tunnel", splatesTunnel).mount("slots", splatesSlots).mount("worker", splatesWorker).mount("queue", splatesQueue).mount("hook", splatesHook).mount("replica", splatesReplica);
1580
+ splates.command("retry").use(repository10({ needs: "own" })).description("give a task that spent its merge attempts another mergeRetryCount of them; the way out of `budget_exhausted` that is not remove and redo, for a person to run once they have seen the tree").arg("task", z16.string(), { description: "task name" }).action((opts, ctx) => recordRetry(ctx, opts.task));
1581
+ splates.mount("artifact", splatesArtifact).mount("task", splatesTask).mount("lane", splatesLane).mount("projects", splatesProjects).mount("creds", splatesCreds).mount("tunnel", splatesTunnel).mount("slots", splatesSlots).mount("worker", splatesWorker).mount("queue", splatesQueue).mount("hook", splatesHook).mount("replica", splatesReplica).mount("evals", splatesEvals);
1591
1582
 
1592
1583
  // index.ts
1593
1584
  if (basename2(process.argv[1] ?? "") === LEGACY) {