@stringsync/splates 0.0.55 → 0.0.56

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 fail9 } from "@stringsync/splates-server/exit";
53
+ import { exits, fail as fail8 } 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";
@@ -65,7 +65,6 @@ import {
65
65
  newest,
66
66
  recordsFirst,
67
67
  rosterOf as rosterOf2,
68
- runHeavy,
69
68
  seeded as seeded2,
70
69
  update
71
70
  } from "@stringsync/splates-server/server";
@@ -86,9 +85,9 @@ import {
86
85
  untilAfter as untilAfter3,
87
86
  wait
88
87
  } from "@stringsync/splates-server/tasks";
89
- import { cli as cli16 } from "webappwiz/cmd";
88
+ import { cli as cli14 } from "webappwiz/cmd";
90
89
  import { Duration as Duration3 } from "webappwiz/time";
91
- import { z as z17 } from "zod";
90
+ import { z as z15 } from "zod";
92
91
 
93
92
  // artifact.ts
94
93
  import { basename, resolve as resolve2 } from "node:path";
@@ -245,96 +244,30 @@ splatesArtifact.command("remove").use(repository({ needs: "own" })).description(
245
244
  splatesArtifact.command("list").use(repository({ needs: "own" })).description("the artifacts this conversation lists, and their current version").option(...conversation).action(async (opts, ctx) => listArtifacts(ctx, await conversationOf(ctx.service, opts.conversation)));
246
245
  splatesArtifact.command("show").use(repository({ needs: "own" })).description("every version of an artifact, and which is current").arg("name", z2.string(), { description: "the artifact's name" }).option(...conversation).action(async (opts, ctx) => listArtifacts(ctx, await conversationOf(ctx.service, opts.conversation), opts.name));
247
246
 
248
- // budget.ts
249
- import {
250
- endBudgetRaise,
251
- raiseBudget,
252
- setBudget,
253
- showBudget
254
- } from "@stringsync/splates-server/workers";
255
- import { cli as cli2 } from "webappwiz/cmd";
256
- import { z as z3 } from "zod";
257
- var DEV = {
258
- default: "",
259
- description: "the `splates server` to ask, a URL such as http://localhost:4310; by default the one an agent runs under, from SPLATES_DEV. With none answering, this machine's own budget file is read and changed"
260
- };
261
- var JSON_OPTION = { default: false, description: "emit JSON" };
262
- var LIMIT = z3.enum(["hour", "day", "month"]);
263
- var SET_LIMIT = z3.enum(["hour", "day", "month", "runners"]);
264
- var LIMIT_ARG = {
265
- description: "hour, the rate cap on what made workers running at once cost an hour; day; or month"
266
- };
267
- var splatesBudget = cli2("budget").description("what the workers a `workerProvider` makes may cost, in dollars: a rate cap per hour on those running at once ($1/h by default), and limits on a day ($15) and a month ($200). Past the rate cap, or once a day's or month's limit is spent, no worker is made and the queue says why; reaching the day's or month's stops made workers, whose agents stop at their checkpoint. Each warns at 80%, 90% and 100% by Web Push and on the page's header. No more than 16 runners are up at once by default, those the server made and `splates run` made together, counted from the cloud's own list; past that the queue waits for one to free. Spend is an estimate at each type's list price. Agents' own API spend is not counted here");
268
- splatesBudget.command("show").description("show each limit: what made workers spent in its window, of how much, and what is left").option("dev", z3.string(), DEV).option("json", z3.boolean(), JSON_OPTION).action((opts, ctx) => showBudget({
269
- dev: opts.dev,
270
- json: opts.json,
271
- log: ctx.log,
272
- fs: ctx.fs,
273
- ps: ctx.ps,
274
- send: ctx.fetch
275
- }));
276
- splatesBudget.command("set").description("set a limit, in dollars (dollars an hour for hour, a count for runners), then show the budget").arg("limit", SET_LIMIT, {
277
- description: `${LIMIT_ARG.description}; or runners, the most runners up at once in the account, whoever made them (16 by default)`
278
- }).arg("dollars", z3.string(), {
279
- description: "like 5 or 0.25 (a whole number for runners, like 16), or `default` to give it back to its default"
280
- }).option("dev", z3.string(), DEV).option("json", z3.boolean(), JSON_OPTION).action((opts, ctx) => setBudget({
281
- limit: opts.limit,
282
- dollars: opts.dollars,
283
- dev: opts.dev,
284
- json: opts.json,
285
- log: ctx.log,
286
- fs: ctx.fs,
287
- ps: ctx.ps,
288
- send: ctx.fetch
289
- }));
290
- splatesBudget.command("raise").description("add dollars to a limit for a while, on top of what it is set to, then show the budget").arg("limit", LIMIT, LIMIT_ARG).arg("dollars", z3.string(), { description: "how much to add, like 2 or 0.1" }).option("for", z3.string(), {
291
- default: "1d",
292
- description: "how long it lasts, in hours, days or weeks, like 24h, 1d or 2w"
293
- }).option("dev", z3.string(), DEV).option("json", z3.boolean(), JSON_OPTION).action((opts, ctx) => raiseBudget({
294
- limit: opts.limit,
295
- dollars: opts.dollars,
296
- for: opts.for,
297
- dev: opts.dev,
298
- json: opts.json,
299
- log: ctx.log,
300
- fs: ctx.fs,
301
- ps: ctx.ps,
302
- send: ctx.fetch
303
- }));
304
- splatesBudget.command("end").description("end a raise now, by the number `show` gives it").arg("id", z3.coerce.number().int(), { description: "the raise's number" }).option("dev", z3.string(), DEV).option("json", z3.boolean(), JSON_OPTION).action((opts, ctx) => endBudgetRaise({
305
- id: opts.id,
306
- dev: opts.dev,
307
- json: opts.json,
308
- log: ctx.log,
309
- fs: ctx.fs,
310
- ps: ctx.ps,
311
- send: ctx.fetch
312
- }));
313
-
314
247
  // creds.ts
315
248
  import { DEFAULT_TIMEOUT, PROVIDERS } from "@stringsync/splates-server/creds";
316
249
  import { fail as fail3 } from "@stringsync/splates-server/exit";
317
250
  import { repository as repository2 } from "@stringsync/splates-server/records";
318
251
  import { untilAfter } from "@stringsync/splates-server/tasks";
319
- import { cli as cli3 } from "webappwiz/cmd";
252
+ import { cli as cli2 } from "webappwiz/cmd";
320
253
  import { color } from "webappwiz/log";
321
254
  import { Duration } from "webappwiz/time";
322
- import { z as z4 } from "zod";
323
- var splatesCreds = cli3("creds").description("creds an agent needs a person for, such as an API token: request one saying what it is for, with instructions for getting it, which the Creds page of `splates server` shows for the person to paste, then wait for it; the value goes into the system's store (the macOS Keychain, Windows Credential Manager, a Linux secret service), never to the agent, and code reads it through webappwiz/creds");
324
- splatesCreds.command("request").use(repository2({ needs: "own" })).description("ask a person for a cred, saying why it is needed and what it will be used for, with markdown instructions for getting it; returns at once, and does nothing when the store already keeps it unless --replace; run from a worktree, it records the task asking").arg("name", z4.string(), {
255
+ import { z as z3 } from "zod";
256
+ var splatesCreds = cli2("creds").description("creds an agent needs a person for, such as an API token: request one saying what it is for, with instructions for getting it, which the Creds page of `splates server` shows for the person to paste, then wait for it; the value goes into the system's store (the macOS Keychain, Windows Credential Manager, a Linux secret service), never to the agent, and code reads it through webappwiz/creds");
257
+ splatesCreds.command("request").use(repository2({ needs: "own" })).description("ask a person for a cred, saying why it is needed and what it will be used for, with markdown instructions for getting it; returns at once, and does nothing when the store already keeps it unless --replace; run from a worktree, it records the task asking").arg("name", z3.string(), {
325
258
  description: "what it is kept as, an environment variable's name"
326
- }).option("purpose", z4.string(), {
259
+ }).option("purpose", z3.string(), {
327
260
  default: "",
328
261
  description: "why it is needed and what it will be used for, in a sentence the person reads before getting it"
329
- }).option("instructions", z4.string(), {
262
+ }).option("instructions", z3.string(), {
330
263
  default: "",
331
264
  description: "how to get it, in markdown: where to go, what to pick, what to paste"
332
- }).option("device", z4.boolean(), {
265
+ }).option("device", z3.boolean(), {
333
266
  default: false,
334
267
  description: "keep it for every project on this device, as for a person's own account, rather than this project alone"
335
- }).option("provider", z4.optional(z4.enum(PROVIDERS)), {
268
+ }).option("provider", z3.optional(z3.enum(PROVIDERS)), {
336
269
  description: "who keeps it: `system`, the machine's own store"
337
- }).option("replace", z4.boolean(), {
270
+ }).option("replace", z3.boolean(), {
338
271
  default: false,
339
272
  description: "ask again although one is kept, since the kept one does not work; --purpose and --instructions left off are those of the last request"
340
273
  }).action(async (opts, ctx) => {
@@ -355,7 +288,7 @@ splatesCreds.command("request").use(repository2({ needs: "own" })).description("
355
288
  ctx.log.info(`${color.yellow("requested")} ${opts.name} ${color.dim(request.purpose)}`);
356
289
  ctx.log.error(`It shows on the Creds page of \`splates server\`. \`splates creds wait ${opts.name}\` until it is saved.`);
357
290
  });
358
- splatesCreds.command("wait").use(repository2({ needs: "own" })).description("block until a person saves the cred requested, or dismisses the request (`dismissed`); gives up with `timeout` rather than waiting forever").arg("name", z4.string(), { description: "the cred requested" }).option("timeout-secs", z4.coerce.number(), {
291
+ splatesCreds.command("wait").use(repository2({ needs: "own" })).description("block until a person saves the cred requested, or dismisses the request (`dismissed`); gives up with `timeout` rather than waiting forever").arg("name", z3.string(), { description: "the cred requested" }).option("timeout-secs", z3.coerce.number(), {
359
292
  default: DEFAULT_TIMEOUT.secs,
360
293
  description: "how long to wait before giving up"
361
294
  }).action(async (opts, ctx) => {
@@ -368,7 +301,7 @@ splatesCreds.command("wait").use(repository2({ needs: "own" })).description("blo
368
301
  });
369
302
  ctx.log.info(`${color.green("saved")} ${opts.name}`);
370
303
  });
371
- splatesCreds.command("list").use(repository2({ needs: "read" })).description("the creds requested and still open, with what each is for, and every cred the project's store and the device's keep, by name; never a value").option("json", z4.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => {
304
+ splatesCreds.command("list").use(repository2({ needs: "read" })).description("the creds requested and still open, with what each is for, and every cred the project's store and the device's keep, by name; never a value").option("json", z3.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => {
372
305
  const open = (await ctx.creds.requests()).filter((each) => each.status === "open");
373
306
  const kept = await ctx.creds.kept();
374
307
  if (opts.json) {
@@ -401,42 +334,43 @@ import {
401
334
  hookList,
402
335
  hookPause,
403
336
  hookRemove,
337
+ hookRun,
404
338
  lanesNamed,
405
339
  stagesNamed,
406
340
  TASK_EVENTS
407
341
  } from "@stringsync/splates-server/hooks";
408
342
  import { repository as repository3 } from "@stringsync/splates-server/records";
409
- import { cli as cli4 } from "webappwiz/cmd";
410
- import { z as z5 } from "zod";
411
- 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`);
412
- 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 }));
413
- 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(), {
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(), {
414
348
  description: "the task event, as task.created; none for a schedule"
415
- }).option("stage", z5.string(), {
349
+ }).option("stage", z4.string(), {
416
350
  default: "",
417
351
  description: "only for tasks in these stages, comma separated: backlog, ready, queued, running"
418
- }).option("lane", z5.string(), {
352
+ }).option("lane", z4.string(), {
419
353
  default: "",
420
354
  description: "only for tasks in these lanes, by number, comma separated"
421
- }).option("every", z5.string(), {
355
+ }).option("every", z4.string(), {
422
356
  default: "",
423
357
  description: "run every so often, as 30m, 2h or 1d"
424
- }).option("cron", z5.string(), {
358
+ }).option("cron", z4.string(), {
425
359
  default: "",
426
360
  description: `run on a five-field cron line, in this machine's time, as "0 9 * * 1-5"`
427
- }).option("agent", z5.string(), {
361
+ }).option("agent", z4.string(), {
428
362
  default: "",
429
363
  description: "the agent to run, as `splates agents` names it"
430
- }).option("model", z5.string(), {
364
+ }).option("model", z4.string(), {
431
365
  default: "",
432
366
  description: "its model (default: the agent's own)"
433
- }).option("effort", z5.string(), {
367
+ }).option("effort", z4.string(), {
434
368
  default: "",
435
369
  description: "its thinking level (default: the agent's own)"
436
- }).option("prompt", z5.string(), {
370
+ }).option("prompt", z4.string(), {
437
371
  default: "",
438
372
  description: "what the agent is told after the event"
439
- }).option("run", z5.string(), {
373
+ }).option("run", z4.string(), {
440
374
  default: "",
441
375
  description: "a shell command to run"
442
376
  }).action(async (opts, ctx) => {
@@ -456,34 +390,34 @@ splatesHook.command("add").use(repository3({ needs: "tasks" })).description("run
456
390
  }
457
391
  await hookAdd({ hooks: storeOf(ctx), log: ctx.log }, triggerOf(opts), hook);
458
392
  });
459
- 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(), {
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(), {
460
394
  default: "",
461
395
  description: "a task event to run on, as task.created, in place of what sets it off now"
462
- }).option("stage", z5.string(), {
396
+ }).option("stage", z4.string(), {
463
397
  default: "",
464
398
  description: "only for tasks in these stages, comma separated, or `any`: backlog, ready, queued, running"
465
- }).option("lane", z5.string(), {
399
+ }).option("lane", z4.string(), {
466
400
  default: "",
467
401
  description: "only for tasks in these lanes, by number, comma separated, or `any`"
468
- }).option("every", z5.string(), {
402
+ }).option("every", z4.string(), {
469
403
  default: "",
470
404
  description: "run every so often, as 30m, 2h or 1d"
471
- }).option("cron", z5.string(), {
405
+ }).option("cron", z4.string(), {
472
406
  default: "",
473
407
  description: `run on a five-field cron line, in this machine's time, as "0 9 * * 1-5"`
474
- }).option("agent", z5.string(), {
408
+ }).option("agent", z4.string(), {
475
409
  default: "",
476
410
  description: "the agent to run, as `splates agents` names it"
477
- }).option("model", z5.string(), {
411
+ }).option("model", z4.string(), {
478
412
  default: "",
479
413
  description: "its model, with --agent (default: the agent's own)"
480
- }).option("effort", z5.string(), {
414
+ }).option("effort", z4.string(), {
481
415
  default: "",
482
416
  description: "its thinking level, with --agent (default: the agent's own)"
483
- }).option("prompt", z5.string(), {
417
+ }).option("prompt", z4.string(), {
484
418
  default: "",
485
419
  description: "what the agent is told after the event"
486
- }).option("run", z5.string(), {
420
+ }).option("run", z4.string(), {
487
421
  default: "",
488
422
  description: "a shell command to run in place of an agent"
489
423
  }).action(async (opts, ctx) => {
@@ -497,21 +431,24 @@ splatesHook.command("edit").use(repository3({ needs: "tasks" })).description("ch
497
431
  ...pick === undefined ? {} : { pick }
498
432
  });
499
433
  });
500
- 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(), {
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(), {
501
435
  description: "its number, or a task event, as task.created"
502
436
  }).action((opts, ctx) => hookRemove({ hooks: storeOf(ctx), log: ctx.log }, opts.id));
503
- 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));
504
- 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));
505
- 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(), {
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`"
441
+ }).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(), {
506
443
  default: "",
507
444
  description: "the agent it runs as, as `splates agents` names it"
508
- }).option("model", z5.string(), {
445
+ }).option("model", z4.string(), {
509
446
  default: "",
510
447
  description: "its model (default: the agent's own)"
511
- }).option("effort", z5.string(), {
448
+ }).option("effort", z4.string(), {
512
449
  default: "",
513
450
  description: "its thinking level (default: the agent's own)"
514
- }).option("off", z5.boolean(), {
451
+ }).option("off", z4.boolean(), {
515
452
  default: false,
516
453
  description: "turn it off"
517
454
  }).action(async (opts, ctx) => {
@@ -521,22 +458,22 @@ splatesHook.command("groomer").use(repository3({ needs: "tasks" })).description(
521
458
  }
522
459
  await hookGroomer({ hooks: storeOf(ctx), log: ctx.log }, pick ?? null);
523
460
  });
524
- 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", z5.string(), {
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(), {
525
462
  default: "",
526
463
  description: "the agent it runs as, as `splates agents` names it"
527
- }).option("model", z5.string(), {
464
+ }).option("model", z4.string(), {
528
465
  default: "",
529
466
  description: "its model (default: the agent's own)"
530
- }).option("effort", z5.string(), {
467
+ }).option("effort", z4.string(), {
531
468
  default: "",
532
469
  description: "its thinking level (default: the agent's own)"
533
- }).option("every", z5.string(), {
470
+ }).option("every", z4.string(), {
534
471
  default: "",
535
472
  description: "run every so often, as 1d (default: as it was, or 7d)"
536
- }).option("cron", z5.string(), {
473
+ }).option("cron", z4.string(), {
537
474
  default: "",
538
475
  description: `run on a five-field cron line, in this machine's time, as "0 9 * * 1"`
539
- }).option("off", z5.boolean(), {
476
+ }).option("off", z4.boolean(), {
540
477
  default: false,
541
478
  description: "turn it off"
542
479
  }).action(async (opts, ctx) => {
@@ -590,25 +527,25 @@ import { refuseGroomer } from "@stringsync/splates-server/conversations";
590
527
  import { UNTRIAGED } from "@stringsync/splates-server/plans";
591
528
  import { refuseAgentQueue } from "@stringsync/splates-server/queue";
592
529
  import { repository as repository4 } from "@stringsync/splates-server/records";
593
- import { cli as cli5 } from "webappwiz/cmd";
594
- import { z as z6 } from "zod";
595
- var laneNumber = z6.union([
596
- z6.literal("backlog").transform(() => BACKLOG_LANE2),
597
- z6.coerce.number().int().nonnegative()
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()
598
535
  ]);
599
- 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");
600
- 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 }));
601
- 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], {
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], {
602
539
  error: "pass a lane's number, backlog, or untriaged"
603
- }), { 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 }));
604
- splatesLane.command("add").use(repository4({ needs: "tasks" })).description("start an empty lane, after the others").arg("name", z6.string(), {
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(), {
605
542
  description: "what its tasks are for, like `Licensing`"
606
543
  }).action(async (opts, ctx) => {
607
544
  await ctx.journal.record("lane add", null, () => laneAdd(ctx, opts.name), (lane) => ({ lane: lane.id, name: lane.name }));
608
545
  });
609
546
  splatesLane.command("update").use(repository4({ needs: "tasks" })).description("rename a lane, or move it among the others").arg("lane", laneNumber, {
610
547
  description: "lane number"
611
- }).option("name", z6.string().optional(), {
548
+ }).option("name", z5.string().optional(), {
612
549
  description: "what it is called from now on"
613
550
  }).option("position", position, {
614
551
  description: "where to move it among the lanes, 1 the first column on the board; the others close up around it"
@@ -638,11 +575,11 @@ splatesLane.command("unqueue").use(repository4({ needs: "tasks" })).description(
638
575
  const change = { queued: false };
639
576
  await ctx.journal.record("lane update", null, () => laneUpdate(ctx, opts.lane, change), (record) => laneUpdated(opts.lane, record, change));
640
577
  });
641
- splatesLane.command("remove").use(repository4({ needs: "tasks" })).description("remove a lane and its tasks, kept to restore; a lane whose last task merges goes on its own; refused from an agent, which asks with a permission card").arg("lane", laneNumber, {
578
+ splatesLane.command("remove").use(repository4({ needs: "tasks" })).description("remove a lane and its tasks, kept to restore; a lane whose last task merges goes on its own; agents may remove an empty lane; for one containing tasks, ask with a permission card").arg("lane", laneNumber, {
642
579
  description: "lane number"
643
580
  }).action((opts, ctx) => {
644
- refuseAgentQueue(ctx.service, "lane remove");
645
- return recordLaneRemove(ctx.journal, ctx.tasks, opts.lane, () => laneRemove(ctx, opts.lane));
581
+ const emptyOnly = ctx.service.ps.env("SPLATES_CONVERSATION") !== undefined || ctx.service.ps.env("SPLATES_STARTED_BY") !== undefined;
582
+ return recordLaneRemove(ctx.journal, ctx.tasks, opts.lane, () => laneRemove(ctx, opts.lane, { emptyOnly }));
646
583
  });
647
584
 
648
585
  // notes.ts
@@ -664,11 +601,11 @@ import {
664
601
  REACHES
665
602
  } from "@stringsync/splates-server/projects";
666
603
  import { repository as repository5 } from "@stringsync/splates-server/records";
667
- import { cli as cli6 } from "webappwiz/cmd";
668
- import { z as z7 } from "zod";
669
- 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");
670
- 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 }));
671
- 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", z7.string(), { description: "the other project's slug" }).arg("reach", z7.enum(REACHES), {
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), {
672
609
  description: "read, queue or tasks"
673
610
  }).action((opts, ctx) => grantReach(ctx, opts.project, opts.reach));
674
611
 
@@ -683,29 +620,46 @@ import {
683
620
  refuseAgentQueue as refuseAgentQueue2
684
621
  } from "@stringsync/splates-server/queue";
685
622
  import { repository as repository6 } from "@stringsync/splates-server/records";
686
- import { cli as cli7 } from "webappwiz/cmd";
687
- import { z as z8 } from "zod";
688
- var splatesQueue = cli7("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");
689
- 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 }));
690
- 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(), {
623
+ 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(), {
691
629
  description: "task numbers"
692
630
  }).action(async (opts, ctx) => {
693
631
  refuseAgentQueue2(ctx.service, "queue add");
694
632
  await refuseGroomer2(ctx.service, "queue add");
695
633
  await ctx.journal.record("queue add", await here(ctx), () => queueAdd(ctx, opts.ids), (queued) => ({ tasks: tasksNamed(queued) }));
696
634
  });
697
- 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(), {
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(), {
698
636
  description: "task numbers"
699
637
  }).action(async (opts, ctx) => {
700
638
  refuseAgentQueue2(ctx.service, "queue remove");
701
639
  await ctx.journal.record("queue remove", await here(ctx), () => queueRemove(ctx, opts.ids), (removed) => ({ tasks: tasksNamed(removed) }));
702
640
  });
703
- 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(), {
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(), {
704
642
  description: "where in the queue to put it, from 1"
705
643
  }).action(async (opts, ctx) => {
706
644
  refuseAgentQueue2(ctx.service, "queue move");
707
645
  return queueMove(ctx, opts.id, opts.position);
708
646
  });
647
+ 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(), {
649
+ default: "",
650
+ description: "the `splates server` to ask, a URL such as http://localhost:4310; by default the one from SPLATES_DEV"
651
+ }).action(async (opts, ctx) => {
652
+ refuseAgentQueue2(ctx.service, `queue ${command}`);
653
+ await setQueuePaused({
654
+ dev: opts.dev,
655
+ paused: command === "pause",
656
+ log: ctx.log,
657
+ fs: ctx.fs,
658
+ ps: ctx.ps,
659
+ send: ctx.fetch
660
+ });
661
+ });
662
+ }
709
663
 
710
664
  // replica.ts
711
665
  import { dirname as dirname2 } from "node:path";
@@ -726,11 +680,11 @@ import {
726
680
  restoreFromReplica,
727
681
  statusLines
728
682
  } from "@stringsync/splates-server/replica";
729
- import { cli as cli8 } from "webappwiz/cmd";
683
+ import { cli as cli7 } from "webappwiz/cmd";
730
684
  import { Credentials } from "webappwiz/creds";
731
- import { z as z9 } from "zod";
732
- 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");
733
- 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) => {
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) => {
734
688
  const status = await Replicator.recorded(ctx.storage);
735
689
  if (opts.json) {
736
690
  ctx.log.info(JSON.stringify(status));
@@ -739,7 +693,7 @@ splatesReplica.command("status").use(repository7({ needs: "read" })).description
739
693
  ctx.log.info(statusLines(status, ctx.config.replica !== null, Date.now()).join(`
740
694
  `));
741
695
  });
742
- 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(), {
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(), {
743
697
  default: false,
744
698
  description: "restore over the records here, leaving them aside"
745
699
  }).action(async (opts, ctx) => {
@@ -783,7 +737,7 @@ splatesReplica.command("restore").description("with `splates server` stopped, re
783
737
  fail5("usage", error instanceof Error ? error.message : String(error));
784
738
  }
785
739
  });
786
- 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(), {
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(), {
787
741
  description: "where the server went, as `droplet.example.com`"
788
742
  }).action(async (opts, ctx) => {
789
743
  const home = new ProjectHome(ctx.git.root, ctx);
@@ -806,143 +760,39 @@ async function mainTree(ctx) {
806
760
  return dirname2(stdout.trim());
807
761
  }
808
762
 
809
- // runners.ts
810
- import { join as join2 } from "node:path";
811
- import { loadConfig as loadConfig3 } from "@stringsync/splates-server/config";
812
- import { fail as fail6 } from "@stringsync/splates-server/exit";
813
- import { splatesHome as splatesHome2 } from "@stringsync/splates-server/models";
814
- import {
815
- REAPER_FILE,
816
- withRunners
817
- } from "@stringsync/splates-server/server";
818
- import {
819
- DIRECT_IDLE,
820
- NoRunner,
821
- parseTimeout,
822
- runnerServe,
823
- sizeLabel
824
- } from "@stringsync/splates-server/workers";
825
- import { cli as cli9 } from "webappwiz/cmd";
826
- import { sleep } from "webappwiz/time";
827
- import { z as z10 } from "zod";
828
- var splatesRunners = cli9("runners").description(`the runners \`splates run\` makes for itself in the project's cloud (\`cloud\` in splates.config.ts) when \`splates server\` has none for it: each takes as many commands at once as fit in its CPUs and memory, another is made when none has room, within \`splates budget\`, and each is deleted once idle ${DIRECT_IDLE.mins} minutes`);
829
- splatesRunners.command("list").description("list them: name, size and price, the address Fly's proxy reaches them at, and busy, idle, or the machine's state").option("json", z10.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => {
830
- const runners = await opened(ctx, (each) => each.list());
831
- if (opts.json) {
832
- ctx.log.info(JSON.stringify(runners));
833
- return;
834
- }
835
- ctx.log.info(runners.length === 0 ? "no runner is up" : runners.map(runnerLine).join(`
836
- `));
837
- });
838
- splatesRunners.command("reap").description(`delete each runner idle ${DIRECT_IDLE.mins} minutes or more, every idle one once the budget's day or month is spent, and any that never booted; with --watch, again every so often for as long as any is up, which \`splates run\` leaves running behind it, one on this machine at a time, so none is left running unused`).option("watch", z10.string(), {
839
- default: "",
840
- description: "how long to wait before each look, as 10m or 90s, looking again for as long as any runner is up"
841
- }).option("idle", z10.string(), {
842
- default: "",
843
- description: `how long a runner must have stood idle, as 10m or 0s; ${DIRECT_IDLE.mins}m by default`
844
- }).action(async (opts, ctx) => {
845
- const idle = opts.idle === "" ? DIRECT_IDLE : lengthOf(opts.idle);
846
- if (opts.watch === "") {
847
- const reaped = await opened(ctx, (each) => each.reap(idle));
848
- ctx.log.info(reaped.length === 0 ? "nothing to reap" : reaped.map((line) => `deleted ${line}`).join(`
849
- `));
850
- return;
851
- }
852
- if (!await watching(ctx)) {
853
- return;
854
- }
855
- const every = lengthOf(opts.watch);
856
- for (;; ) {
857
- await sleep(every);
858
- const left = await opened(ctx, async (each) => {
859
- await each.reap(idle);
860
- return (await each.list()).length;
861
- });
862
- if (left === 0) {
863
- return;
864
- }
865
- }
866
- });
867
- async function watching(ctx) {
868
- const file = reaperFile(ctx);
869
- const held = Number((await ctx.fs.read(file).catch(() => "")).trim());
870
- if (Number.isInteger(held) && held > 0 && held !== process.pid && ctx.ps.alive(held)) {
871
- return false;
872
- }
873
- await ctx.fs.write(file, String(process.pid));
874
- return true;
875
- }
876
- async function opened(ctx, use) {
877
- const { stdout, exitCode } = await ctx.ps.spawnCapture([
878
- "git",
879
- "rev-parse",
880
- "--show-toplevel"
881
- ]);
882
- if (exitCode !== 0) {
883
- fail6("usage", "run it in a splates project", {});
884
- }
885
- const { cloud } = await loadConfig3(stdout.trim(), { fs: ctx.fs });
886
- if (cloud === null) {
887
- fail6("no_runner", "this project names no `cloud` in splates.config.ts, so `splates run` makes no runners", {});
888
- }
889
- try {
890
- return await withRunners(ctx, cloud, use);
891
- } catch (error) {
892
- if (error instanceof NoRunner) {
893
- fail6("no_runner", error.message, {});
894
- }
895
- throw error;
896
- }
897
- }
898
- function lengthOf(text) {
899
- return parseTimeout(text) ?? fail6("usage", `${text} is no length of time: name one as 10m or 90s`, {});
900
- }
901
- function runnerLine(runner) {
902
- const size = runner.size === null ? runner.type : sizeLabel(runner.size, ...runner.dollarsPerHour === null ? [] : [runner.dollarsPerHour]);
903
- return `${runner.id} ${size} ${runner.address ?? "no address yet"} ${runner.state}`;
904
- }
905
- function reaperFile(ctx) {
906
- return join2(splatesHome2(ctx.ps), REAPER_FILE);
907
- }
908
- splatesRunners.command("serve").description("serve only job channels at /api/jobs over authenticated WebSockets; for the runner image, with SPLATES_RUNNER_SECRET from its hosting environment").option("port", z10.coerce.number().int().min(0).max(65535), {
909
- default: 8080,
910
- description: "HTTP port behind the hosting proxy"
911
- }).action(({ port }, { ps }) => runnerServe({ port, ps }));
912
-
913
763
  // server.ts
914
764
  import {
915
765
  DEFAULT_PORT,
916
766
  serveDev
917
767
  } from "@stringsync/splates-server/server";
918
- import { cli as cli10 } from "webappwiz/cmd";
919
- import { z as z11 } from "zod";
920
- var splatesServer = cli10("server").description("the splates server: run it on this machine").fallback("run");
921
- 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", z11.coerce.number(), {
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(), {
922
772
  default: DEFAULT_PORT,
923
773
  description: "port to listen on, or the next open one above it"
924
- }).option("allow-hosts", z11.string(), {
774
+ }).option("allow-hosts", z9.string(), {
925
775
  default: "",
926
776
  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"
927
- }).option("access-team", z11.string(), {
777
+ }).option("access-team", z9.string(), {
928
778
  default: "",
929
779
  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"
930
- }).option("access-aud", z11.string(), {
780
+ }).option("access-aud", z9.string(), {
931
781
  default: "",
932
782
  description: "the AUD tag of that Access application"
933
- }).option("tunnel", z11.string(), {
783
+ }).option("tunnel", z9.string(), {
934
784
  default: "",
935
785
  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"
936
- }).option("tunnel-name", z11.string(), {
786
+ }).option("tunnel-name", z9.string(), {
937
787
  default: "",
938
788
  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"
939
- }).option("ports-host", z11.string(), {
789
+ }).option("ports-host", z9.string(), {
940
790
  default: "",
941
791
  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"
942
- }).option("keepalive", z11.boolean(), {
792
+ }).option("keepalive", z9.boolean(), {
943
793
  default: false,
944
794
  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"
945
- }).option("slot", z11.boolean(), {
795
+ }).option("slot", z9.boolean(), {
946
796
  default: false,
947
797
  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"
948
798
  }).action((opts, ctx) => serveDev(ctx, opts));
@@ -950,8 +800,8 @@ splatesServer.command("run").description("serve every splates project on this ma
950
800
  // setup.ts
951
801
  import { dirname as dirname3 } from "node:path";
952
802
  import { SYSTEM_STORES as SYSTEM_STORES3 } from "@stringsync/splates-server/creds";
953
- import { fail as fail7 } from "@stringsync/splates-server/exit";
954
- import { splatesHome as splatesHome3 } from "@stringsync/splates-server/models";
803
+ import { fail as fail6 } from "@stringsync/splates-server/exit";
804
+ import { splatesHome as splatesHome2 } from "@stringsync/splates-server/models";
955
805
  import { Projects as Projects2 } from "@stringsync/splates-server/projects";
956
806
  import {
957
807
  openRepository
@@ -962,23 +812,19 @@ import {
962
812
  Setup,
963
813
  setupProjectAt
964
814
  } from "@stringsync/splates-server/setup";
965
- import {
966
- fetchWorkerBudget,
967
- fetchWorkers,
968
- putWorkerLimit
969
- } from "@stringsync/splates-server/workers";
815
+ import { fetchWorkers } from "@stringsync/splates-server/workers";
970
816
  import { color as color2 } from "webappwiz/log";
971
817
  async function setup(deps, opts) {
972
818
  if (opts.step === "" && (opts.skip || opts.unskip || opts.undo)) {
973
- fail7("usage", "--skip and --unskip name a step, --undo an area", {});
819
+ fail6("usage", "--skip and --unskip name a step, --undo an area", {});
974
820
  }
975
821
  if (opts.undo && (opts.skip || opts.unskip)) {
976
- fail7("usage", "--undo names an area, not a step to skip", {
822
+ fail6("usage", "--undo names an area, not a step to skip", {
977
823
  area: opts.step
978
824
  });
979
825
  }
980
826
  if (opts.skip && opts.unskip) {
981
- fail7("usage", "--skip or --unskip, not both", { step: opts.step });
827
+ fail6("usage", "--skip or --unskip, not both", { step: opts.step });
982
828
  }
983
829
  const machine = await setupOf(deps);
984
830
  const { log } = deps;
@@ -1021,18 +867,10 @@ async function setupOf(deps) {
1021
867
  return new Setup({
1022
868
  fs,
1023
869
  ps,
1024
- home: splatesHome3(ps),
870
+ home: splatesHome2(ps),
1025
871
  device: secrets.device,
1026
872
  fetch,
1027
873
  workers: deps.ps.env("SPLATES_DEV") ? () => fetchWorkers({ dev: "", log: deps.log, ps: deps.ps }) : undefined,
1028
- workerBudget: deps.ps.env("SPLATES_DEV") ? {
1029
- report: () => fetchWorkerBudget({ dev: "", log: deps.log, ps: deps.ps }),
1030
- set: (limit, dollars) => putWorkerLimit(limit, dollars, {
1031
- dev: "",
1032
- log: deps.log,
1033
- ps: deps.ps
1034
- })
1035
- } : undefined,
1036
874
  project: root === null ? undefined : setupProjectAt(root, { fs, ps }, {
1037
875
  replica: () => withRecords(deps, root, ({ storage }) => Replicator2.recorded(storage)),
1038
876
  kept: (name) => withRecords(deps, root, async ({ creds }) => await creds.credentials.get(name) !== undefined)
@@ -1133,10 +971,10 @@ function printUndo(log, plan) {
1133
971
 
1134
972
  // slots.ts
1135
973
  import { Slots } from "@stringsync/splates-server/ports";
1136
- import { cli as cli11 } from "webappwiz/cmd";
1137
- import { z as z12 } from "zod";
1138
- var splatesSlots = cli11("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");
1139
- 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", z12.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => {
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) => {
1140
978
  const held = await Slots.ofMachine(ctx).status();
1141
979
  if (opts.json) {
1142
980
  ctx.log.info(JSON.stringify(held.map(({ slot, tree, pid, orphaned }) => ({
@@ -1181,17 +1019,17 @@ import {
1181
1019
  } from "@stringsync/splates-server/board";
1182
1020
  import { commaList as commaList4 } from "@stringsync/splates-server/comma-list";
1183
1021
  import { refuseGroomer as refuseGroomer3 } from "@stringsync/splates-server/conversations";
1184
- import { fail as fail8 } from "@stringsync/splates-server/exit";
1022
+ import { fail as fail7 } from "@stringsync/splates-server/exit";
1185
1023
  import { repository as repository8 } from "@stringsync/splates-server/records";
1186
1024
  import {
1187
1025
  DEFAULT_TIMEOUT as DEFAULT_TIMEOUT2,
1188
1026
  taskWait,
1189
1027
  untilAfter as untilAfter2
1190
1028
  } from "@stringsync/splates-server/tasks";
1191
- import { cli as cli12 } from "webappwiz/cmd";
1029
+ import { cli as cli10 } from "webappwiz/cmd";
1192
1030
  import { Duration as Duration2 } from "webappwiz/time";
1193
- import { z as z13 } from "zod";
1194
- var splatesTask = cli12("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");
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");
1195
1033
  async function projectFrom(ctx) {
1196
1034
  if (ctx.elsewhere === null) {
1197
1035
  return;
@@ -1199,34 +1037,34 @@ async function projectFrom(ctx) {
1199
1037
  return (await ctx.projects.list()).find((each) => each.root === ctx.git.root)?.slug;
1200
1038
  }
1201
1039
  function adding(name, description, ready) {
1202
- splatesTask.command(name).use(repository8({ needs: "queue" })).description(description).arg("subject", z13.string(), {
1040
+ splatesTask.command(name).use(repository8({ needs: "queue" })).description(description).arg("subject", z11.string(), {
1203
1041
  description: "what is left to do, in a line"
1204
- }).arg("text", z13.string(), {
1042
+ }).arg("text", z11.string(), {
1205
1043
  default: "",
1206
1044
  description: "whatever more there is to say about it, in markdown"
1207
- }).option("slug", z13.string(), {
1045
+ }).option("slug", z11.string(), {
1208
1046
  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"
1209
1047
  }).option("position", position, {
1210
1048
  description: "where it goes in the list, 1 at the top, pushing those from there down (default: the bottom)"
1211
- }).repeated("file", z13.string(), {
1049
+ }).repeated("file", z11.string(), {
1212
1050
  description: "files to attach, comma separated or the flag repeated: copied under .git/splates/tasks/<n>/"
1213
- }).repeated("blocked-by", z13.string(), {
1051
+ }).repeated("blocked-by", z11.string(), {
1214
1052
  description: "numbers of the tasks blocking it, comma separated or the flag repeated: each has to merge before this one starts"
1215
- }).option("lane", z13.string(), {
1053
+ }).option("lane", z11.string(), {
1216
1054
  default: "",
1217
1055
  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)"
1218
- }).option("agent", z13.string(), {
1056
+ }).option("agent", z11.string(), {
1219
1057
  default: "",
1220
1058
  description: "the agent on the roster (`splates agents`) that should work it, which Start on the `splates server` board offers first"
1221
- }).option("model", z13.string(), {
1059
+ }).option("model", z11.string(), {
1222
1060
  default: "",
1223
1061
  description: "with --agent: the model, by its id or name on the roster"
1224
- }).option("effort", z13.string(), {
1062
+ }).option("effort", z11.string(), {
1225
1063
  default: "",
1226
1064
  description: "with --agent: the thinking level, one the agent offers (default: the agent's own)"
1227
1065
  }).action(async (opts, ctx) => {
1228
1066
  if (ctx.elsewhere !== null && opts.lane !== "") {
1229
- 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 });
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 });
1230
1068
  }
1231
1069
  const from = await here(ctx);
1232
1070
  const assignee = await assigneeIn(ctx, opts.agent, opts.model, opts.effort);
@@ -1251,10 +1089,10 @@ function adding(name, description, ready) {
1251
1089
  }
1252
1090
  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);
1253
1091
  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);
1254
- 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", z13.boolean(), { default: false, description: "emit JSON" }).option("open", z13.boolean(), {
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(), {
1255
1093
  default: false,
1256
1094
  description: "only tasks no started one has taken up: the ones free to pick up"
1257
- }).option("ready", z13.boolean(), {
1095
+ }).option("ready", z11.boolean(), {
1258
1096
  default: false,
1259
1097
  description: "only tasks free to start now: none has taken them, and nothing blocking them is still open"
1260
1098
  }).action(async (opts, ctx) => taskList(ctx, {
@@ -1263,54 +1101,54 @@ splatesTask.command("list").use(repository8({ needs: "read" })).description("eve
1263
1101
  ready: opts.ready,
1264
1102
  project: await projectFrom(ctx)
1265
1103
  }));
1266
- splatesTask.command("show").use(repository8({ needs: "read" })).description("one task in full: where it stands, its files, and its detail").arg("id", z13.coerce.number().int().positive(), { description: "task number" }).option("json", z13.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => taskShow(ctx, opts.id, {
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, {
1267
1105
  json: opts.json,
1268
1106
  project: await projectFrom(ctx),
1269
1107
  others: (await ctx.projects.list()).filter((project) => project.root !== ctx.git.root).map((project) => project.slug)
1270
1108
  }));
1271
- 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", z13.coerce.number().int().positive(), { description: "task number" }).arg("text", z13.string(), {
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(), {
1272
1110
  default: "",
1273
1111
  description: "whatever more there is to say about it, in markdown; leave out to keep the detail"
1274
- }).option("subject", z13.string(), {
1112
+ }).option("subject", z11.string(), {
1275
1113
  default: "",
1276
1114
  description: "what is left to do, in a line; leave out to keep it"
1277
- }).option("slug", z13.string(), {
1115
+ }).option("slug", z11.string(), {
1278
1116
  default: "",
1279
1117
  description: "a new name for the task that takes it (lowercase-with-dashes); leave out to keep it"
1280
1118
  }).option("position", position, {
1281
1119
  description: "where to move it in the list, 1 at the top; the others close up around it"
1282
- }).repeated("file", z13.string(), {
1120
+ }).repeated("file", z11.string(), {
1283
1121
  description: "files to attach, comma separated or the flag repeated"
1284
- }).repeated("remove-file", z13.string(), {
1122
+ }).repeated("remove-file", z11.string(), {
1285
1123
  description: "attached files to drop, comma separated or the flag repeated, by path or stored name"
1286
- }).repeated("blocked-by", z13.string(), {
1124
+ }).repeated("blocked-by", z11.string(), {
1287
1125
  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"
1288
- }).repeated("remove-blocked-by", z13.string(), {
1126
+ }).repeated("remove-blocked-by", z11.string(), {
1289
1127
  description: "numbers of tasks no longer blocking it, comma separated or the flag repeated"
1290
- }).option("lane", z13.string(), {
1128
+ }).option("lane", z11.string(), {
1291
1129
  default: "",
1292
1130
  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"
1293
- }).option("agent", z13.string(), {
1131
+ }).option("agent", z11.string(), {
1294
1132
  default: "",
1295
1133
  description: "the agent on the roster (`splates agents`) that should work it, which Start on the `splates server` board offers first, or `none`"
1296
- }).option("model", z13.string(), {
1134
+ }).option("model", z11.string(), {
1297
1135
  default: "",
1298
1136
  description: "with --agent: the model, by its id or name on the roster"
1299
- }).option("effort", z13.string(), {
1137
+ }).option("effort", z11.string(), {
1300
1138
  default: "",
1301
1139
  description: "with --agent: the thinking level, one the agent offers (default: the agent's own)"
1302
- }).option("ready", z13.boolean(), {
1140
+ }).option("ready", z11.boolean(), {
1303
1141
  default: false,
1304
1142
  description: "mark it groomed, moving it from the backlog to ready: no task already asks it, and what blocks it is linked"
1305
- }).option("manual", z13.boolean(), {
1143
+ }).option("manual", z11.boolean(), {
1306
1144
  default: false,
1307
1145
  description: "queue it by hand only: a queued lane passes over it, and it starts once you `splates queue add` it"
1308
- }).option("auto", z13.boolean(), {
1146
+ }).option("auto", z11.boolean(), {
1309
1147
  default: false,
1310
1148
  description: "let a queued lane queue it again, undoing --manual"
1311
1149
  }).action(async (opts, ctx) => {
1312
1150
  if (opts.manual && opts.auto) {
1313
- fail8("usage", "pass --manual or --auto, not both", { number: opts.id });
1151
+ fail7("usage", "pass --manual or --auto, not both", { number: opts.id });
1314
1152
  }
1315
1153
  await refuseGroomer3(ctx.service, "task update", await ctx.tasks.find(opts.id));
1316
1154
  const assignee = await assigneeIn(ctx, opts.agent, opts.model, opts.effort);
@@ -1329,11 +1167,11 @@ splatesTask.command("update").use(repository8({ needs: "tasks" })).description("
1329
1167
  manual: opts.manual ? true : opts.auto ? false : undefined
1330
1168
  }));
1331
1169
  });
1332
- splatesTask.command("remove").use(repository8({ needs: "tasks" })).description("drop a queued task that was done some other way or no longer applies").arg("id", z13.coerce.number().int().positive(), { description: "task number" }).action(async (opts, ctx) => {
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) => {
1333
1171
  await refuseGroomer3(ctx.service, "task remove", await ctx.tasks.find(opts.id));
1334
1172
  await ctx.journal.record("task remove", null, () => taskRemove(ctx, opts.id), (removed) => ({ tasks: tasksNamed2([removed]) }));
1335
1173
  });
1336
- 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", z13.coerce.number().int().positive(), { description: "task number" }).option("timeout-secs", z13.coerce.number(), {
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(), {
1337
1175
  default: DEFAULT_TIMEOUT2.secs,
1338
1176
  description: "how long to wait before giving up"
1339
1177
  }).action(async (opts, ctx) => {
@@ -1354,18 +1192,18 @@ import {
1354
1192
  tunnelRemove,
1355
1193
  tunnelSetup
1356
1194
  } from "@stringsync/splates-server/tunnel";
1357
- import { cli as cli13 } from "webappwiz/cmd";
1358
- import { z as z14 } from "zod";
1359
- var splatesTunnel = cli13("tunnel").description("reach `splates server` from elsewhere: a Cloudflare tunnel with Cloudflare Access in front, set up once per machine");
1360
- 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", z14.string(), {
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(), {
1361
1199
  description: "the public host name, in a zone of the account, as in splates.example.com"
1362
- }).option("email", z14.string(), {
1200
+ }).option("email", z12.string(), {
1363
1201
  default: "",
1364
1202
  description: "comma-separated email addresses Access lets in"
1365
- }).option("name", z14.string(), {
1203
+ }).option("name", z12.string(), {
1366
1204
  default: "splates",
1367
1205
  description: "what the tunnel, the Access application and its policy are called"
1368
- }).option("team", z14.string(), {
1206
+ }).option("team", z12.string(), {
1369
1207
  default: "",
1370
1208
  description: "the Zero Trust team to make, as in <team>.cloudflareaccess.com, when the account has none yet"
1371
1209
  }).action(async (opts, ctx) => ctx.journal.record("tunnel setup", null, async () => tunnelSetup({
@@ -1376,7 +1214,7 @@ splatesTunnel.command("setup").use(repository9({ needs: "own" })).description(`m
1376
1214
  log: ctx.log,
1377
1215
  fetch: ctx.fetch
1378
1216
  })));
1379
- 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", z14.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => {
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) => {
1380
1218
  const saved = savedTunnelOf(ctx);
1381
1219
  const record = await saved.read();
1382
1220
  const token = record === null ? false : await saved.token() !== undefined;
@@ -1408,33 +1246,29 @@ splatesTunnel.command("remove").use(repository9({ needs: "own" })).description(`
1408
1246
  })));
1409
1247
 
1410
1248
  // worker.ts
1411
- import { splatesHome as splatesHome4 } from "@stringsync/splates-server/models";
1249
+ import { splatesHome as splatesHome3 } from "@stringsync/splates-server/models";
1412
1250
  import {
1413
- addWorker,
1414
1251
  drainWorker,
1415
1252
  FileLogger,
1416
1253
  listWorkers,
1417
1254
  removeWorker,
1418
1255
  setWorkerSlots,
1419
- WORKER_JOIN,
1420
- workerJoin,
1421
- workerRecipe,
1422
1256
  workerRun
1423
1257
  } from "@stringsync/splates-server/workers";
1424
- import { cli as cli15 } from "webappwiz/cmd";
1258
+ import { cli as cli13 } from "webappwiz/cmd";
1425
1259
  import { CompositeLogger } from "webappwiz/log";
1426
- import { z as z16 } from "zod";
1260
+ import { z as z14 } from "zod";
1427
1261
 
1428
1262
  // waiting.ts
1429
1263
  import { listQueue, moveQueued } from "@stringsync/splates-server/workers";
1430
- import { cli as cli14 } from "webappwiz/cmd";
1431
- import { z as z15 } from "zod";
1432
- var DEV2 = {
1264
+ import { cli as cli12 } from "webappwiz/cmd";
1265
+ import { z as z13 } from "zod";
1266
+ var DEV = {
1433
1267
  default: "",
1434
1268
  description: "the `splates server` to ask, a URL such as http://localhost:4310; by default the one an agent runs under, from SPLATES_DEV"
1435
1269
  };
1436
- var splatesWaiting = cli14("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");
1437
- 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", z15.string(), DEV2).option("json", z15.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => listQueue({
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({
1438
1272
  dev: opts.dev,
1439
1273
  json: opts.json,
1440
1274
  log: ctx.log,
@@ -1442,11 +1276,11 @@ splatesWaiting.command("list").description("list the agents waiting for a slot,
1442
1276
  ps: ctx.ps,
1443
1277
  send: ctx.fetch
1444
1278
  }));
1445
- 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", z15.string(), {
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(), {
1446
1280
  description: "the task it works, or its conversation's id"
1447
- }).arg("position", z15.coerce.number().int(), {
1281
+ }).arg("position", z13.coerce.number().int(), {
1448
1282
  description: "where in line to put it, from 1"
1449
- }).option("dev", z15.string(), DEV2).option("json", z15.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => moveQueued({
1283
+ }).option("dev", z13.string(), DEV).option("json", z13.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => moveQueued({
1450
1284
  which: opts.which,
1451
1285
  position: opts.position,
1452
1286
  dev: opts.dev,
@@ -1458,71 +1292,34 @@ splatesWaiting.command("move").description("move an agent waiting for a slot to
1458
1292
  }));
1459
1293
 
1460
1294
  // worker.ts
1461
- var splatesWorker = cli15("worker").description("the machines that run tasks for `splates server`: each dials out to it and runs what it is given, agents, terminals and the git and process reads around them, on its own machine");
1462
- splatesWorker.command("run").description("run a worker on this machine until stopped: it dials `splates server`, by default the one on this machine through a socket in ~/.splates only you may open, and dials again whenever the link drops, while what it runs carries on; it stops reading a process's output while the server is away too long, so nothing is lost. Once its agents have moved off it, a server on another build has it restart on that build, through `bunx @stringsync/splates@<build>`, as it does when it refuses this worker for another protocol; one that says no build has it exit `outdated`").option("server", z16.string(), {
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(), {
1463
1297
  default: "",
1464
- description: "the `splates server` to dial: its URL, whose /api/workers it opens, or unix:<path>; by default this machine's"
1465
- }).option("coordinator", z16.string(), {
1298
+ description: "the `splates server` to dial: unix:<path>; by default this machine's"
1299
+ }).option("coordinator", z14.string(), {
1466
1300
  default: "",
1467
1301
  description: "the old name for --server, still read for workers installed before it, and passed to a build older than it"
1468
- }).option("slots", z16.coerce.number().int().min(0), {
1302
+ }).option("slots", z14.coerce.number().int().min(0), {
1469
1303
  default: 0,
1470
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"
1471
- }).option("id", z16.string(), {
1472
- default: "",
1473
- description: "what the server knows this worker by, kept across restarts so it picks up where it was; by default the id it joined as, kept beside its key, else the machine's host name"
1474
- }).option("join", z16.string(), {
1475
- default: "",
1476
- description: "a join token from `splates worker add` on the server, spent to join it before dialing; the key it answers is kept in this machine's system store, or read from SPLATES_WORKER_KEY on one without"
1477
- }).option("ephemeral", z16.boolean(), {
1478
- default: false,
1479
- description: "keep the key --join answers in memory alone, for a machine destroyed rather than restarted, as one a worker provider makes"
1480
- }).option("key-file", z16.boolean(), {
1481
- default: false,
1482
- description: "keep the key --join answers in ~/.splates/worker.env, which only this user may read, rather than the system store, for a machine whose store is locked or missing at boot, as WSL's is with no one signed in; read it from there on each run after"
1483
- }).option("quiet", z16.boolean(), {
1305
+ }).option("id", z14.literal("local"), {
1306
+ default: "local",
1307
+ 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(), {
1484
1309
  default: false,
1485
1310
  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"
1486
1311
  }).action((opts, ctx) => workerRun({
1487
1312
  coordinator: opts.server || opts.coordinator,
1488
1313
  slots: opts.slots,
1489
1314
  id: opts.id,
1490
- join: opts.join || ctx.ps.env(WORKER_JOIN) || "",
1491
- ephemeral: opts.ephemeral,
1492
- keyFile: opts["key-file"],
1493
1315
  log: opts.quiet ? quietLog(ctx) : ctx.log,
1494
1316
  ps: ctx.ps,
1495
1317
  fs: ctx.fs
1496
1318
  }));
1497
- splatesWorker.command("join").description("spend a join token from `splates worker add` and keep the key it answers, as `splates worker run --join` does, then exit, for an installer that leaves running the worker to a service").option("server", z16.string(), {
1498
- default: "",
1499
- description: "the `splates server` to join: its URL"
1500
- }).option("coordinator", z16.string(), {
1501
- default: "",
1502
- description: "the old name for --server"
1503
- }).option("join", z16.string(), {
1504
- default: "",
1505
- description: "the join token `splates worker add` printed there; by default read from SPLATES_WORKER_JOIN, which keeps it out of the process list other users of the machine can read"
1506
- }).option("id", z16.string(), {
1507
- default: "",
1508
- description: "what the server knows this worker by, kept beside its key so a change of host name leaves it the same; by default the machine's host name"
1509
- }).option("key-file", z16.boolean(), {
1510
- default: false,
1511
- description: "keep the key in ~/.splates/worker.env, which only this user may read, rather than the system store, for `splates worker run --key-file` to read"
1512
- }).action((opts, ctx) => workerJoin({
1513
- coordinator: opts.server || opts.coordinator,
1514
- join: opts.join || ctx.ps.env(WORKER_JOIN) || "",
1515
- id: opts.id,
1516
- keyFile: opts["key-file"],
1517
- log: ctx.log,
1518
- ps: ctx.ps,
1519
- fs: ctx.fs
1520
- }));
1521
- splatesWorker.command("recipe").description("print the shell script, run as root on a fresh Debian or Ubuntu machine, that installs what a Linux worker needs: bun, git, the shared browser and this build of splates, for the user `splates`. It is what a provider's snapshot is built from, and what the Windows installer runs in WSL").action((_opts, ctx) => workerRecipe({ log: ctx.log }));
1522
- splatesWorker.command("list").description("list the workers the running `splates server` has: kind (local over its socket, registered otherwise), platform, build, protocol, slots used and total, health with when it was last heard, tasks and $/hour, then any it refused for speaking another protocol").option("dev", z16.string(), {
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(), {
1523
1320
  default: "",
1524
1321
  description: "the `splates server` to ask, a URL such as http://localhost:4310; by default the one an agent runs under, from SPLATES_DEV"
1525
- }).option("json", z16.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => listWorkers({
1322
+ }).option("json", z14.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => listWorkers({
1526
1323
  dev: opts.dev,
1527
1324
  json: opts.json,
1528
1325
  log: ctx.log,
@@ -1534,31 +1331,12 @@ var dev = {
1534
1331
  default: "",
1535
1332
  description: "the `splates server` to ask, a URL such as http://localhost:4310; by default the one an agent runs under, from SPLATES_DEV"
1536
1333
  };
1537
- splatesWorker.command("add").description("add a worker to the running `splates server`: prints a join token, single use and good for 15 minutes, for `splates worker run --join <token>` on the machine to join, which gets a key of its own that `splates worker remove` revokes; with --local N, makes this machine's own worker run N agents at once and take new ones again, adding it back if it was removed; with --provider, has the `workerProvider` in splates.config.ts make one, booted from the snapshot splates builds for it, which joins on its own and whose machine `splates worker remove` destroys").option("local", z16.coerce.number().int().min(0), {
1538
- default: 0,
1539
- description: "this machine's own worker, with this many slots"
1540
- }).option("provider", z16.boolean(), {
1541
- default: false,
1542
- description: "have the server's worker provider make the worker, of its cheapest type unless --type says"
1543
- }).option("type", z16.string(), {
1544
- default: "",
1545
- description: "the worker provider's type to make, as it names it; implies --provider"
1546
- }).option("dev", z16.string(), dev).action((opts, ctx) => addWorker({
1547
- local: opts.local,
1548
- provider: opts.provider,
1549
- type: opts.type,
1550
- dev: opts.dev,
1551
- log: ctx.log,
1552
- fs: ctx.fs,
1553
- ps: ctx.ps,
1554
- send: ctx.fetch
1555
- }));
1556
- 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", z16.string(), {
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(), {
1557
1335
  description: "the worker, as `splates worker list` names it"
1558
- }).option("undo", z16.boolean(), {
1336
+ }).option("undo", z14.boolean(), {
1559
1337
  default: false,
1560
1338
  description: "take new agents again"
1561
- }).option("dev", z16.string(), dev).action((opts, ctx) => drainWorker({
1339
+ }).option("dev", z14.string(), dev).action((opts, ctx) => drainWorker({
1562
1340
  id: opts.id,
1563
1341
  undo: opts.undo,
1564
1342
  dev: opts.dev,
@@ -1567,11 +1345,11 @@ splatesWorker.command("drain").description("take no new agents on a worker, lett
1567
1345
  ps: ctx.ps,
1568
1346
  send: ctx.fetch
1569
1347
  }));
1570
- splatesWorker.command("slots").description("set how many agents a worker runs at once, from now on, over what it started with").arg("id", z16.string(), {
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(), {
1571
1349
  description: "the worker, as `splates worker list` names it"
1572
- }).arg("slots", z16.coerce.number().int().min(1), {
1350
+ }).arg("slots", z14.coerce.number().int().min(1), {
1573
1351
  description: "how many at once"
1574
- }).option("dev", z16.string(), dev).action((opts, ctx) => setWorkerSlots({
1352
+ }).option("dev", z14.string(), dev).action((opts, ctx) => setWorkerSlots({
1575
1353
  id: opts.id,
1576
1354
  slots: opts.slots,
1577
1355
  dev: opts.dev,
@@ -1580,12 +1358,12 @@ splatesWorker.command("slots").description("set how many agents a worker runs at
1580
1358
  ps: ctx.ps,
1581
1359
  send: ctx.fetch
1582
1360
  }));
1583
- splatesWorker.command("remove").description("remove a worker, revoking its key: one with agents on it drains and is removed once they end; --force ends them now, frees their slots, and places each again on another worker, to carry on its task from the checkpoint it last pushed. This machine's own worker, `local`, stops taking agents, its agents running to their end, and leaves this machine only coordinating until `splates worker add --local <slots>`; it takes no --force").arg("id", z16.string(), {
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(), {
1584
1362
  description: "the worker, as `splates worker list` names it"
1585
- }).option("force", z16.boolean(), {
1363
+ }).option("force", z14.boolean(), {
1586
1364
  default: false,
1587
1365
  description: "end the agents on it now rather than wait for them"
1588
- }).option("dev", z16.string(), dev).action((opts, ctx) => removeWorker({
1366
+ }).option("dev", z14.string(), dev).action((opts, ctx) => removeWorker({
1589
1367
  id: opts.id,
1590
1368
  force: opts.force,
1591
1369
  dev: opts.dev,
@@ -1596,7 +1374,7 @@ splatesWorker.command("remove").description("remove a worker, revoking its key:
1596
1374
  }));
1597
1375
  splatesWorker.mount("queue", splatesWaiting);
1598
1376
  function quietLog(ctx) {
1599
- const file = new FileLogger(ctx.fs, `${splatesHome4(ctx.ps)}/worker.log`);
1377
+ const file = new FileLogger(ctx.fs, `${splatesHome3(ctx.ps)}/worker.log`);
1600
1378
  return new CompositeLogger([
1601
1379
  { info: () => {}, error: (...said) => ctx.log.error(...said) },
1602
1380
  file
@@ -1617,7 +1395,7 @@ function projectArgs(argv) {
1617
1395
  }
1618
1396
  return { argv };
1619
1397
  }
1620
- var splates = cli16("splates").use(exits());
1398
+ var splates = cli14("splates").use(exits());
1621
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));
1622
1400
  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));
1623
1401
  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) => {
@@ -1638,62 +1416,42 @@ splates.command("update").use(recordsFirst()).use(repository10({ needs: "own" })
1638
1416
  ctx.ps.exit(code);
1639
1417
  }
1640
1418
  });
1641
- splates.command("run").description("run a command that needs a machine of its own (a full test run, an install) on a runner, in a checkout of this worktree as it stands, uncommitted edits included, its output streamed back and its exit code yours: one that has joined `splates server` when it has one, else, in a project whose splates.config.ts names a `cloud`, one `splates run` makes there itself, server or no server (`splates runners`), refusing with `no_runner` rather than run here when none can be had, as without the cloud's token or past `splates budget`. A project that names no cloud runs it here, in one of this machine's heavy-job slots, waiting for one to come free first, so agents testing at once queue rather than run out of memory; `SPLATES_HEAVY_JOBS` sets how many run at once, 1 by default. Run on a runner, it changes nothing here: files it writes stay there").option("cpus", z17.coerce.number().positive().optional(), {
1642
- description: "the CPUs it asks of a runner, which takes as many commands at once as fit; the project's `job` in splates.config.ts, else 4, by default"
1643
- }).option("memory", z17.string(), {
1644
- default: "",
1645
- description: "the memory it asks of a runner, as 8GB or 512MB; the project's `job` in splates.config.ts, else 8GB, by default"
1646
- }).option("timeout", z17.string(), {
1647
- default: "",
1648
- description: "how long it may run, as 30m, 90s or 2h, before it is killed and exits 124, so a hung command holds no runner or slot; the project's `job.timeout` in splates.config.ts, else 30m, by default"
1649
- }).rest("command", z17.string(), {
1650
- description: "the command and its arguments, after `--`"
1651
- }).action(async (opts, ctx) => {
1652
- const code = await runHeavy(ctx, opts.command, undefined, {
1653
- ...opts.cpus === undefined ? {} : { cpus: opts.cpus },
1654
- memory: opts.memory,
1655
- timeout: opts.timeout
1656
- });
1657
- if (code !== 0) {
1658
- ctx.ps.exit(code);
1659
- }
1660
- });
1661
1419
  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));
1662
- 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", z17.string(), {
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(), {
1663
1421
  description: "the task's name, for its branch and worktree (lowercase-with-dashes)"
1664
- }).option("base", z17.string(), {
1422
+ }).option("base", z15.string(), {
1665
1423
  default: "",
1666
1424
  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"
1667
- }).option("task", z17.string(), {
1425
+ }).option("task", z15.string(), {
1668
1426
  default: "",
1669
1427
  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"
1670
- }).option("goal", z17.string(), {
1428
+ }).option("goal", z15.string(), {
1671
1429
  default: "",
1672
1430
  description: "one or two lines on what done means, written under `## Goal` in the task's SPLATES.md; required unless --task seeds the Goal"
1673
- }).option("agent", z17.string(), {
1431
+ }).option("agent", z15.string(), {
1674
1432
  default: "",
1675
1433
  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"
1676
- }).option("model", z17.string(), {
1434
+ }).option("model", z15.string(), {
1677
1435
  default: "",
1678
1436
  description: "with --agent: the model it works on, by its id or name on the roster (default: the agent's own)"
1679
- }).option("effort", z17.string(), {
1437
+ }).option("effort", z15.string(), {
1680
1438
  default: "",
1681
1439
  description: "with --agent: the thinking level it works at, one the agent offers (default: the agent's own)"
1682
- }).option("prompt", z17.string(), {
1440
+ }).option("prompt", z15.string(), {
1683
1441
  default: "",
1684
1442
  description: "with --agent: what to tell the agent beyond claiming the task"
1685
- }).option("held", z17.string(), {
1443
+ }).option("held", z15.string(), {
1686
1444
  default: "",
1687
1445
  description: "with --agent: the conversation `splates server`'s queue held for the queued task, which takes the task rather than a new one"
1688
1446
  }).action(async (opts, ctx) => {
1689
1447
  await refuseGroomer4(ctx.service, "add");
1690
1448
  if (opts.agent === "" && (opts.model !== "" || opts.effort !== "" || opts.prompt !== "" || opts.held !== "")) {
1691
- fail9("usage", "--model, --effort, --prompt and --held go with --agent", {
1449
+ fail8("usage", "--model, --effort, --prompt and --held go with --agent", {
1692
1450
  task: opts.slug
1693
1451
  });
1694
1452
  }
1695
1453
  if (opts.goal.trim() === "" && opts.task === "") {
1696
- fail9("usage", `say what done means: \`splates add ${opts.slug} --goal "<one or two lines>"\`, or start queued tasks with --task`, { task: opts.slug });
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 });
1697
1455
  }
1698
1456
  const dispatch = opts.agent === "" ? undefined : {
1699
1457
  agent: opts.agent,
@@ -1727,18 +1485,18 @@ splates.command("add").use(repository10({ needs: "tasks" })).description("start
1727
1485
  await serveProject(ctx);
1728
1486
  }
1729
1487
  });
1730
- 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", z17.string(), { description: "task name" }).action(async (opts, ctx) => {
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) => {
1731
1489
  await ctx.journal.record("claim", opts.task, () => claim(ctx, opts.task), (claimed) => claimed);
1732
1490
  await follow(ctx, opts.task);
1733
1491
  });
1734
- 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", z17.string(), {
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(), {
1735
1493
  default: "",
1736
1494
  description: "squash the task's commits into one with this message before landing, saying what the base gains in at least 5 characters"
1737
1495
  }).action(async (opts, ctx) => {
1738
1496
  await refuseGroomer4(ctx.service, "merge");
1739
1497
  const task = await here(ctx);
1740
1498
  const facts = {};
1741
- await ctx.journal.record("merge", task, () => merge({ ...ctx, send: ctx.fetch }, ctx.ps.cwd(), facts, {
1499
+ await ctx.journal.record("merge", task, () => merge(ctx, ctx.ps.cwd(), facts, {
1742
1500
  message: opts.message
1743
1501
  }), facts);
1744
1502
  if (task !== null) {
@@ -1746,39 +1504,39 @@ splates.command("merge").use(repository10({ needs: "own" })).description("land t
1746
1504
  await follow(ctx, null, { from: task });
1747
1505
  }
1748
1506
  });
1749
- splates.command("remove").use(repository10({ needs: "tasks" })).description("discard a task: worktree, branch and state file; cheap and encouraged, since redoing a task against current trunk often beats a hard rebase").arg("task", z17.string(), { description: "task name" }).option("force", z17.boolean(), {
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(), {
1750
1508
  default: false,
1751
1509
  description: "discard even when another agent holds the lease"
1752
- }).option("reason", z17.optional(z17.enum(REMOVE_REASONS)), {
1510
+ }).option("reason", z15.optional(z15.enum(REMOVE_REASONS)), {
1753
1511
  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)"
1754
1512
  }).action(async (opts, ctx) => {
1755
1513
  await ctx.journal.record("remove", opts.task, () => remove(ctx, opts.task, { force: opts.force, reason: opts.reason }), ({ released }) => ({ why: opts.reason ?? null, released }));
1756
1514
  await follow(ctx, null, { from: opts.task });
1757
1515
  });
1758
- 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", z17.boolean(), { default: false, description: "emit JSON" }).option("files", z17.boolean(), {
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(), {
1759
1517
  default: false,
1760
1518
  description: "add each task's Goal and changed files (committed or not), to check for overlap before starting"
1761
1519
  }).action((opts, ctx) => list(ctx, { json: opts.json, files: opts.files }));
1762
- 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", z17.string(), { description: "task name" }).option("json", z17.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => show(ctx, opts.task, { json: opts.json }));
1763
- 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", z17.string(), { description: "task name" }).option("timeout-secs", z17.coerce.number(), {
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(), {
1764
1522
  default: DEFAULT_TIMEOUT3.secs,
1765
1523
  description: "how long to wait before giving up"
1766
1524
  }).action((opts, ctx) => {
1767
1525
  const timeout = Duration3.secs(opts["timeout-secs"]);
1768
1526
  return ctx.journal.waiting("wait", opts.task, { until: untilAfter3(timeout) }, () => wait(ctx, opts.task, { timeout }));
1769
1527
  });
1770
- 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", z17.coerce.number(), {
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(), {
1771
1529
  default: DEFAULT_COUNT,
1772
1530
  description: "how many entries to show"
1773
- }).option("json", z17.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => showLog(ctx, { count: opts.count, json: opts.json }));
1531
+ }).option("json", z15.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => showLog(ctx, { count: opts.count, json: opts.json }));
1774
1532
  splates.mount("server", splatesServer);
1775
- 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", z17.string(), {
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(), {
1776
1534
  default: "",
1777
1535
  description: "only this task's ports; every task's by default"
1778
- }).option("dev", z17.string(), {
1536
+ }).option("dev", z15.string(), {
1779
1537
  default: "",
1780
1538
  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"
1781
- }).option("json", z17.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => portsLs({
1539
+ }).option("json", z15.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => portsLs({
1782
1540
  task: opts.task,
1783
1541
  dev: opts.dev,
1784
1542
  json: opts.json,
@@ -1786,41 +1544,41 @@ splates.command("ports").description("list the ports tasks serve, found by the r
1786
1544
  ps: ctx.ps,
1787
1545
  send: ctx.fetch
1788
1546
  }));
1789
- 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", z17.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => agents({
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({
1790
1548
  roster: rosterOf2(ctx),
1791
1549
  offered: await offeredOf(ctx).all(),
1792
1550
  log: ctx.log
1793
1551
  }, { json: opts.json }));
1794
- 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", z17.string(), {
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(), {
1795
1553
  default: "",
1796
1554
  description: "a step's id; omit for every area's progress"
1797
- }).option("json", z17.boolean(), { default: false, description: "emit JSON" }).option("skip", z17.boolean(), {
1555
+ }).option("json", z15.boolean(), { default: false, description: "emit JSON" }).option("skip", z15.boolean(), {
1798
1556
  default: false,
1799
1557
  description: "with a step: stop asking for it"
1800
- }).option("unskip", z17.boolean(), {
1558
+ }).option("unskip", z15.boolean(), {
1801
1559
  default: false,
1802
1560
  description: "with a step: ask for it again"
1803
- }).option("undo", z17.boolean(), {
1561
+ }).option("undo", z15.boolean(), {
1804
1562
  default: false,
1805
1563
  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"
1806
1564
  }).action((opts, ctx) => setup(ctx, opts));
1807
- 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", z17.string(), {
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(), {
1808
1566
  default: "",
1809
1567
  description: "task name; omit for the main tree"
1810
1568
  }).action((opts, ctx) => path(ctx, opts.task || undefined));
1811
1569
  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) => {
1812
1570
  await checkpoint(ctx, ctx.ps.cwd());
1813
1571
  });
1814
- 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", z17.string(), {
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(), {
1815
1573
  default: "",
1816
1574
  description: "files to store, comma separated"
1817
- }).option("task", z17.string(), {
1575
+ }).option("task", z15.string(), {
1818
1576
  default: "",
1819
1577
  description: "task name, when run outside its worktree"
1820
- }).option("remove", z17.string(), {
1578
+ }).option("remove", z15.string(), {
1821
1579
  default: "",
1822
1580
  description: "files it holds to drop first, comma separated, by path or stored name"
1823
- }).option("conversation", z17.string(), {
1581
+ }).option("conversation", z15.string(), {
1824
1582
  default: "",
1825
1583
  description: "conversation to keep the files with, when $SPLATES_CONVERSATION is not set"
1826
1584
  }).action(async (opts, ctx) => ctx.journal.record("attach", opts.task || await here(ctx), () => attach(ctx, commaList5(opts.files), ctx.ps.cwd(), {
@@ -1828,8 +1586,8 @@ splates.command("attach").use(repository10({ needs: "own" })).description("store
1828
1586
  remove: commaList5(opts.remove),
1829
1587
  conversation: opts.conversation || undefined
1830
1588
  }), (stored) => ({ stored, removed: commaList5(opts.remove) })));
1831
- 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", z17.string(), { description: "task name" }).action((opts, ctx) => recordRetry(ctx, opts.task));
1832
- 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("runners", splatesRunners).mount("queue", splatesQueue).mount("hook", splatesHook).mount("budget", splatesBudget).mount("replica", splatesReplica);
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);
1833
1591
 
1834
1592
  // index.ts
1835
1593
  if (basename2(process.argv[1] ?? "") === LEGACY) {